Browse Source

Merge pull request #2859 from weseek/imprv/support-null-author

Imprv/support null author
Yuki Takei 5 years ago
parent
commit
b0aa1c305c

+ 5 - 0
CHANGES.md

@@ -12,6 +12,11 @@
 * Improvement: Basic layout of page
 * Improvement: Basic layout of page
 * Support: Support MongoDB 4.0, 4.2 and 4.4
 * Support: Support MongoDB 4.0, 4.2 and 4.4
 
 
+## v4.1.8
+
+* Improvement: Rebuilding progress bar colors for Full Text Search Management
+* Improvement: Support operations on page data with a null value for author
+
 ## v4.1.7
 ## v4.1.7
 
 
 * Improvement: Fire global notification when a new page is created by uploading file
 * Improvement: Fire global notification when a new page is created by uploading file

+ 5 - 1
src/client/js/app.jsx

@@ -93,10 +93,14 @@ if (pageContainer.state.pageId != null) {
     'seen-user-list': <SeenUserList />,
     'seen-user-list': <SeenUserList />,
     'liker-list': <LikerList />,
     'liker-list': <LikerList />,
 
 
-    'user-created-list': <RecentCreated userId={pageContainer.state.creator._id} />,
     'user-draft-list': <MyDraftList />,
     'user-draft-list': <MyDraftList />,
   });
   });
 }
 }
+if (pageContainer.state.creator != null) {
+  Object.assign(componentMappings, {
+    'user-created-list': <RecentCreated userId={pageContainer.state.creator._id} />,
+  });
+}
 if (pageContainer.state.path != null) {
 if (pageContainer.state.path != null) {
   Object.assign(componentMappings, {
   Object.assign(componentMappings, {
     // eslint-disable-next-line quote-props
     // eslint-disable-next-line quote-props

+ 38 - 0
src/client/js/components/Admin/Common/LabeledProgressBar.jsx

@@ -0,0 +1,38 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { Progress } from 'reactstrap';
+
+const LabeledProgressBar = (props) => {
+
+  const {
+    header, currentCount, totalCount, errorsCount, isInProgress,
+  } = props;
+
+  const progressingColor = isInProgress ? 'info' : 'success';
+
+  return (
+    <>
+      <h6 className="my-1">
+        {header}
+        <div className="float-right">{currentCount} / {totalCount}</div>
+      </h6>
+      <Progress multi>
+        <Progress bar max={totalCount} color={progressingColor} striped={isInProgress} animated={isInProgress} value={currentCount} />
+        <Progress bar max={totalCount} color="danger" striped={isInProgress} animated={isInProgress} value={errorsCount} />
+      </Progress>
+    </>
+  );
+
+};
+
+LabeledProgressBar.propTypes = {
+  header: PropTypes.string.isRequired,
+  currentCount: PropTypes.number.isRequired,
+  totalCount: PropTypes.number.isRequired,
+  errorsCount: PropTypes.number,
+  isInProgress: PropTypes.bool,
+};
+
+export default withTranslation()(LabeledProgressBar);

+ 0 - 45
src/client/js/components/Admin/Common/ProgressBar.jsx

@@ -1,45 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-class ProgressBar extends React.Component {
-
-
-  render() {
-    const {
-      header, currentCount, totalCount, isInProgress,
-    } = this.props;
-
-    const percentage = currentCount / totalCount * 100;
-    const isActive = (isInProgress != null)
-      ? isInProgress //                         apply props.isInProgress if set
-      : (currentCount !== totalCount); //       otherwise, set true when currentCount does not equal totalCount
-
-    return (
-      <>
-        <h6 className="my-1">
-          {header}
-          <div className="float-right">{currentCount} / {totalCount}</div>
-        </h6>
-        <div className="progress">
-          <div
-            className={`progress-bar ${isActive ? 'bg-info progress-bar-striped active' : 'bg-success'}`}
-            style={{ width: `${percentage}%` }}
-          >
-            <span className="sr-only">{percentage.toFixed(0)}% Complete</span>
-          </div>
-        </div>
-      </>
-    );
-  }
-
-}
-
-ProgressBar.propTypes = {
-  header: PropTypes.string.isRequired,
-  currentCount: PropTypes.number.isRequired,
-  totalCount: PropTypes.number.isRequired,
-  isInProgress: PropTypes.bool,
-};
-
-export default withTranslation()(ProgressBar);

+ 3 - 2
src/client/js/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx

@@ -6,7 +6,7 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 import AdminSocketIoContainer from '../../../services/AdminSocketIoContainer';
 import AdminSocketIoContainer from '../../../services/AdminSocketIoContainer';
 
 
-import ProgressBar from '../Common/ProgressBar';
+import LabeledProgressBar from '../Common/LabeledProgressBar';
 
 
 class RebuildIndexControls extends React.Component {
 class RebuildIndexControls extends React.Component {
 
 
@@ -70,9 +70,10 @@ class RebuildIndexControls extends React.Component {
     const header = isRebuildingCompleted ? getCompletedLabel() : getSkipLabel();
     const header = isRebuildingCompleted ? getCompletedLabel() : getSkipLabel();
 
 
     return (
     return (
-      <ProgressBar
+      <LabeledProgressBar
         header={header}
         header={header}
         currentCount={current}
         currentCount={current}
+        errorsCount={skip}
         totalCount={total}
         totalCount={total}
       />
       />
     );
     );

+ 3 - 3
src/client/js/components/Admin/ExportArchiveDataPage.jsx

@@ -10,7 +10,7 @@ import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import AppContainer from '../../services/AppContainer';
 import AdminSocketIoContainer from '../../services/AdminSocketIoContainer';
 import AdminSocketIoContainer from '../../services/AdminSocketIoContainer';
 
 
-import ProgressBar from './Common/ProgressBar';
+import LabeledProgressBar from './Common/LabeledProgressBar';
 
 
 import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
 import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
 import ArchiveFilesTable from './ExportArchiveData/ArchiveFilesTable';
 import ArchiveFilesTable from './ExportArchiveData/ArchiveFilesTable';
@@ -169,7 +169,7 @@ class ExportArchiveDataPage extends React.Component {
       const { collectionName, currentCount, totalCount } = progressData;
       const { collectionName, currentCount, totalCount } = progressData;
       return (
       return (
         <div className="col-md-6" key={collectionName}>
         <div className="col-md-6" key={collectionName}>
-          <ProgressBar
+          <LabeledProgressBar
             header={collectionName}
             header={collectionName}
             currentCount={currentCount}
             currentCount={currentCount}
             totalCount={totalCount}
             totalCount={totalCount}
@@ -192,7 +192,7 @@ class ExportArchiveDataPage extends React.Component {
     return (
     return (
       <div className="row px-3">
       <div className="row px-3">
         <div className="col-md-12" key="progressBarForZipping">
         <div className="col-md-12" key="progressBarForZipping">
-          <ProgressBar
+          <LabeledProgressBar
             header="Zip Files"
             header="Zip Files"
             currentCount={1}
             currentCount={1}
             totalCount={1}
             totalCount={1}

+ 42 - 0
src/client/js/components/Navbar/AuthorInfo.jsx

@@ -0,0 +1,42 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { userPageRoot } from '@commons/util/path-utils';
+
+import UserPicture from '../User/UserPicture';
+
+const AuthorInfo = (props) => {
+  const { mode, user, date } = props;
+
+  const infoLabel = mode === 'create'
+    ? 'Created by'
+    : 'Updated by';
+  const userLabel = user != null
+    ? <a href={userPageRoot(user)}>{user.name}</a>
+    : <i>Unknown</i>;
+
+  return (
+    <div className="d-flex align-items-center">
+      <div className="mr-2">
+        <UserPicture user={user} size="sm" />
+      </div>
+      <div>
+        <div>{infoLabel} {userLabel}</div>
+        <div className="text-muted text-date">{date}</div>
+      </div>
+    </div>
+  );
+};
+
+AuthorInfo.propTypes = {
+  date: PropTypes.string.isRequired,
+  user: PropTypes.object,
+  mode: PropTypes.oneOf(['create', 'update']),
+};
+
+AuthorInfo.defaultProps = {
+  mode: 'create',
+};
+
+
+export default AuthorInfo;

+ 7 - 12
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -20,8 +20,7 @@ import LikeButton from '../LikeButton';
 import BookmarkButton from '../BookmarkButton';
 import BookmarkButton from '../BookmarkButton';
 import ThreeStrandedButton from './ThreeStrandedButton';
 import ThreeStrandedButton from './ThreeStrandedButton';
 
 
-import PageCreator from './PageCreator';
-import RevisionAuthor from './RevisionAuthor';
+import AuthorInfo from './AuthorInfo';
 import DrawerToggler from './DrawerToggler';
 import DrawerToggler from './DrawerToggler';
 import UserPicture from '../User/UserPicture';
 import UserPicture from '../User/UserPicture';
 
 
@@ -196,16 +195,12 @@ const GrowiSubNavigation = (props) => {
         {/* Page Authors */}
         {/* Page Authors */}
         { (!isCompactMode && !isUserPage) && (
         { (!isCompactMode && !isUserPage) && (
           <ul className="authors text-nowrap border-left d-none d-lg-block d-edit-none">
           <ul className="authors text-nowrap border-left d-none d-lg-block d-edit-none">
-            { creator != null && (
-              <li className="pb-1">
-                <PageCreator creator={creator} createdAt={createdAt} />
-              </li>
-            ) }
-            { revisionAuthor != null && (
-              <li className="mt-1 pt-1 border-top">
-                <RevisionAuthor revisionAuthor={revisionAuthor} updatedAt={updatedAt} />
-              </li>
-            ) }
+            <li className="pb-1">
+              <AuthorInfo user={creator} date={createdAt} />
+            </li>
+            <li className="mt-1 pt-1 border-top">
+              <AuthorInfo user={revisionAuthor} date={updatedAt} mode="update" />
+            </li>
           </ul>
           </ul>
         ) }
         ) }
       </div>
       </div>

+ 0 - 37
src/client/js/components/Navbar/PageCreator.jsx

@@ -1,37 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { userPageRoot } from '@commons/util/path-utils';
-
-import UserPicture from '../User/UserPicture';
-
-const PageCreator = (props) => {
-  const { creator, createdAt, isCompactMode } = props;
-  const creatInfo = isCompactMode
-    ? (<div>Created at <span className="text-muted">{createdAt}</span></div>)
-    : (<div><div>Created by <a href={userPageRoot(creator)}>{creator.name}</a></div><div className="text-muted text-date">{createdAt}</div></div>);
-  const pictureSize = isCompactMode ? 'xs' : 'sm';
-
-  return (
-    <div className="d-flex align-items-center">
-      <div className="mr-2">
-        <UserPicture user={creator} size={pictureSize} />
-      </div>
-      {creatInfo}
-    </div>
-  );
-};
-
-PageCreator.propTypes = {
-
-  creator: PropTypes.object.isRequired,
-  createdAt: PropTypes.string.isRequired,
-  isCompactMode: PropTypes.bool,
-};
-
-PageCreator.defaultProps = {
-  isCompactMode: false,
-};
-
-
-export default PageCreator;

+ 0 - 41
src/client/js/components/Navbar/RevisionAuthor.jsx

@@ -1,41 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { userPageRoot } from '@commons/util/path-utils';
-
-import UserPicture from '../User/UserPicture';
-
-const RevisionAuthor = (props) => {
-  const { revisionAuthor, updatedAt, isCompactMode } = props;
-  const updateInfo = isCompactMode
-    ? (<div>Updated at <span className="text-muted">{updatedAt}</span></div>)
-    : (
-      <div>
-        <div>Updated by <a href={userPageRoot(revisionAuthor)}>{revisionAuthor.name}</a></div>
-        <div className="text-muted text-date">{updatedAt}</div>
-      </div>
-    );
-  const pictureSize = isCompactMode ? 'xs' : 'sm';
-
-  return (
-    <div className="d-flex align-items-center">
-      <div className="mr-2">
-        <UserPicture user={revisionAuthor} size={pictureSize} />
-      </div>
-      {updateInfo}
-    </div>
-  );
-};
-
-RevisionAuthor.propTypes = {
-
-  revisionAuthor: PropTypes.object.isRequired,
-  updatedAt: PropTypes.string.isRequired,
-  isCompactMode: PropTypes.bool,
-};
-
-RevisionAuthor.defaultProps = {
-  isCompactMode: false,
-};
-
-export default RevisionAuthor;

+ 1 - 1
src/client/js/components/PageAttachment.jsx

@@ -137,7 +137,7 @@ class PageAttachment extends React.Component {
       deleteAttachmentModal = (
       deleteAttachmentModal = (
         <DeleteAttachmentModal
         <DeleteAttachmentModal
           isOpen={showModal}
           isOpen={showModal}
-          animation={false}
+          animation="false"
           toggle={deleteModalClose}
           toggle={deleteModalClose}
 
 
           attachmentToDelete={attachmentToDelete}
           attachmentToDelete={attachmentToDelete}

+ 6 - 2
src/client/js/components/PageComment/Comment.jsx

@@ -77,7 +77,11 @@ class Comment extends React.PureComponent {
   }
   }
 
 
   isCurrentUserEqualsToAuthor() {
   isCurrentUserEqualsToAuthor() {
-    return this.props.comment.creator.username === this.props.appContainer.currentUsername;
+    const { creator } = this.props.comment;
+    if (creator == null) {
+      return false;
+    }
+    return creator.username === this.props.appContainer.currentUsername;
   }
   }
 
 
   isCurrentRevision() {
   isCurrentRevision() {
@@ -179,7 +183,7 @@ class Comment extends React.PureComponent {
             currentCommentId={commentId}
             currentCommentId={commentId}
             commentBody={comment.comment}
             commentBody={comment.comment}
             replyTo={undefined}
             replyTo={undefined}
-            commentCreator={creator.username}
+            commentCreator={creator?.username}
             onCancelButtonClicked={() => this.setState({ isReEdit: false })}
             onCancelButtonClicked={() => this.setState({ isReEdit: false })}
             onCommentButtonClicked={() => this.setState({ isReEdit: false })}
             onCommentButtonClicked={() => this.setState({ isReEdit: false })}
           />
           />

+ 2 - 1
src/client/js/services/CommentContainer.js

@@ -78,7 +78,8 @@ export default class CommentContainer extends Container {
 
 
   async checkAndUpdateImageOfCommentAuthers(comments) {
   async checkAndUpdateImageOfCommentAuthers(comments) {
     const noImageCacheUserIds = comments.filter((comment) => {
     const noImageCacheUserIds = comments.filter((comment) => {
-      return comment.creator.imageUrlCached == null;
+      const { creator } = comment;
+      return creator != null && creator.imageUrlCached == null;
     }).map((comment) => {
     }).map((comment) => {
       return comment.creator._id;
       return comment.creator._id;
     });
     });

+ 14 - 2
src/client/js/services/PageContainer.js

@@ -45,7 +45,6 @@ export default class PageContainer extends Container {
       pageId: mainContent.getAttribute('data-page-id'),
       pageId: mainContent.getAttribute('data-page-id'),
       revisionId,
       revisionId,
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
-      revisionAuthor: JSON.parse(mainContent.getAttribute('data-page-revision-author')),
       path,
       path,
       tocHtml: '',
       tocHtml: '',
       isLiked: JSON.parse(mainContent.getAttribute('data-page-is-liked')),
       isLiked: JSON.parse(mainContent.getAttribute('data-page-is-liked')),
@@ -58,7 +57,6 @@ export default class PageContainer extends Container {
       sumOfLikers: 0,
       sumOfLikers: 0,
 
 
       createdAt: mainContent.getAttribute('data-page-created-at'),
       createdAt: mainContent.getAttribute('data-page-created-at'),
-      creator: JSON.parse(mainContent.getAttribute('data-page-creator')),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
       isForbidden:  JSON.parse(mainContent.getAttribute('data-page-is-forbidden')),
       isForbidden:  JSON.parse(mainContent.getAttribute('data-page-is-forbidden')),
       isDeleted:  JSON.parse(mainContent.getAttribute('data-page-is-deleted')),
       isDeleted:  JSON.parse(mainContent.getAttribute('data-page-is-deleted')),
@@ -80,6 +78,20 @@ export default class PageContainer extends Container {
       isHackmdDraftUpdatingInRealtime: false,
       isHackmdDraftUpdatingInRealtime: false,
     };
     };
 
 
+    // parse creator and revisionAuthor
+    try {
+      this.state.revisionAuthor = JSON.parse(mainContent.getAttribute('data-page-revision-author'));
+    }
+    catch (e) {
+      logger.warn('The data of \'data-page-revision-author\' is invalid', e);
+    }
+    try {
+      this.state.creator = JSON.parse(mainContent.getAttribute('data-page-creator'));
+    }
+    catch (e) {
+      logger.warn('The data of \'data-page-creator\' is invalid', e);
+    }
+
     const { interceptorManager } = this.appContainer;
     const { interceptorManager } = this.appContainer;
     interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(appContainer), 10); // process as soon as possible
     interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(appContainer), 10); // process as soon as possible
     interceptorManager.addInterceptor(new DrawioInterceptor(appContainer), 20);
     interceptorManager.addInterceptor(new DrawioInterceptor(appContainer), 20);

+ 5 - 0
src/server/routes/attachment.js

@@ -152,6 +152,11 @@ module.exports = function(crowi, app) {
    * @param {Attachment} attachment
    * @param {Attachment} attachment
    */
    */
   async function isDeletableByUser(user, attachment) {
   async function isDeletableByUser(user, attachment) {
+    // deletable if creator is null
+    if (attachment.creator == null) {
+      return true;
+    }
+
     const ownerId = attachment.creator._id || attachment.creator;
     const ownerId = attachment.creator._id || attachment.creator;
     if (attachment.page == null) { // when profile image
     if (attachment.page == null) { // when profile image
       return user.id === ownerId.toString();
       return user.id === ownerId.toString();

+ 7 - 2
src/server/routes/page.js

@@ -221,12 +221,17 @@ module.exports = function(crowi, app) {
 
 
   function addRenderVarsForPage(renderVars, page) {
   function addRenderVarsForPage(renderVars, page) {
     renderVars.page = page;
     renderVars.page = page;
-    renderVars.page.creator = renderVars.page.creator.toObject();
     renderVars.revision = page.revision;
     renderVars.revision = page.revision;
-    renderVars.revision.author = renderVars.revision.author.toObject();
     renderVars.pageIdOnHackmd = page.pageIdOnHackmd;
     renderVars.pageIdOnHackmd = page.pageIdOnHackmd;
     renderVars.revisionHackmdSynced = page.revisionHackmdSynced;
     renderVars.revisionHackmdSynced = page.revisionHackmdSynced;
     renderVars.hasDraftOnHackmd = page.hasDraftOnHackmd;
     renderVars.hasDraftOnHackmd = page.hasDraftOnHackmd;
+
+    if (page.creator != null) {
+      renderVars.page.creator = renderVars.page.creator.toObject();
+    }
+    if (page.revision.author != null) {
+      renderVars.revision.author = renderVars.revision.author.toObject();
+    }
   }
   }
 
 
   function addRenderVarsForPresentation(renderVars, page) {
   function addRenderVarsForPresentation(renderVars, page) {

+ 2 - 2
src/server/service/search-delegator/elasticsearch.js

@@ -51,7 +51,7 @@ class ElasticsearchDelegator {
   }
   }
 
 
   shouldIndexed(page) {
   shouldIndexed(page) {
-    return page.creator != null && page.revision != null && page.redirectTo == null;
+    return page.revision != null && page.redirectTo == null;
   }
   }
 
 
   initClient() {
   initClient() {
@@ -310,7 +310,7 @@ class ElasticsearchDelegator {
     let document = {
     let document = {
       path: page.path,
       path: page.path,
       body: page.revision.body,
       body: page.revision.body,
-      username: page.creator.username,
+      username: page.creator?.username,
       comment_count: page.commentCount,
       comment_count: page.commentCount,
       bookmark_count: bookmarkCount,
       bookmark_count: bookmarkCount,
       like_count: page.liker.length || 0,
       like_count: page.liker.length || 0,

+ 2 - 2
src/server/views/widget/page_content.html

@@ -5,7 +5,7 @@
   data-page-id="{% if page %}{{ page._id.toString() }}{% endif %}"
   data-page-id="{% if page %}{{ page._id.toString() }}{% endif %}"
   data-page-revision-id="{% if revision %}{{ revision._id.toString() }}{% endif %}"
   data-page-revision-id="{% if revision %}{{ revision._id.toString() }}{% endif %}"
   data-page-revision-created="{% if revision %}{{ revision.createdAt|datetz('U') }}{% endif %}"
   data-page-revision-created="{% if revision %}{{ revision.createdAt|datetz('U') }}{% endif %}"
-  data-page-revision-author="{% if revision %}{{ revision.author|json }}{% endif %}"
+  data-page-revision-author="{% if revision && revision.author %}{{ revision.author|json }}{% endif %}"
   data-page-revision-id-hackmd-synced="{% if revisionHackmdSynced %}{{ revisionHackmdSynced.toString() }}{% endif %}"
   data-page-revision-id-hackmd-synced="{% if revisionHackmdSynced %}{{ revisionHackmdSynced.toString() }}{% endif %}"
   data-page-id-on-hackmd="{% if pageIdOnHackmd %}{{ pageIdOnHackmd.toString() }}{% endif %}"
   data-page-id-on-hackmd="{% if pageIdOnHackmd %}{{ pageIdOnHackmd.toString() }}{% endif %}"
   data-page-has-draft-on-hackmd="{% if hasDraftOnHackmd %}{{ hasDraftOnHackmd.toString() }}{% endif %}"
   data-page-has-draft-on-hackmd="{% if hasDraftOnHackmd %}{{ hasDraftOnHackmd.toString() }}{% endif %}"
@@ -20,7 +20,7 @@
   data-page-is-able-to-delete-completely="{% if user.canDeleteCompletely(page.creator._id) %}true{% else %}false{% endif %}"
   data-page-is-able-to-delete-completely="{% if user.canDeleteCompletely(page.creator._id) %}true{% else %}false{% endif %}"
   data-slack-channels="{{ slack|default('') }}"
   data-slack-channels="{{ slack|default('') }}"
   data-page-created-at="{% if page %}{{ page.createdAt|datetz('Y/m/d H:i:s') }}{% endif %}"
   data-page-created-at="{% if page %}{{ page.createdAt|datetz('Y/m/d H:i:s') }}{% endif %}"
-  data-page-creator="{% if page %}{{ page.creator|json }}{% endif %}"
+  data-page-creator="{% if page && page.creator %}{{ page.creator|json }}{% endif %}"
   data-page-updated-at="{% if page %}{{ page.updatedAt|datetz('Y/m/d H:i:s') }}{% endif %}"
   data-page-updated-at="{% if page %}{{ page.updatedAt|datetz('Y/m/d H:i:s') }}{% endif %}"
   data-page-has-children="{% if pages.length > 0 %}true{% else %}false{% endif %}"
   data-page-has-children="{% if pages.length > 0 %}true{% else %}false{% endif %}"
   data-page-user="{% if pageUser %}{{ pageUser|json }}{% else %}null{% endif %}"
   data-page-user="{% if pageUser %}{{ pageUser|json }}{% else %}null{% endif %}"