فهرست منبع

Merge branch 'feat/display-BookMarkList-including-pagination-as-component' into feat/add-paging-for-contents-in-pageAccessoriesModal

zahmis 5 سال پیش
والد
کامیت
9e411c82bd
28فایلهای تغییر یافته به همراه189 افزوده شده و 181 حذف شده
  1. 1 1
      .devcontainer/Dockerfile
  2. 2 0
      .gitignore
  3. 11 1
      CHANGES.md
  4. 1 1
      README.md
  5. 6 0
      src/client/js/app.jsx
  6. 38 0
      src/client/js/components/Admin/Common/LabeledProgressBar.jsx
  7. 0 45
      src/client/js/components/Admin/Common/ProgressBar.jsx
  8. 3 2
      src/client/js/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx
  9. 2 2
      src/client/js/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  10. 3 3
      src/client/js/components/Admin/ExportArchiveDataPage.jsx
  11. 42 0
      src/client/js/components/Navbar/AuthorInfo.jsx
  12. 7 12
      src/client/js/components/Navbar/GrowiSubNavigation.jsx
  13. 0 37
      src/client/js/components/Navbar/PageCreator.jsx
  14. 0 41
      src/client/js/components/Navbar/RevisionAuthor.jsx
  15. 2 2
      src/client/js/components/Page/TrashPageAlert.jsx
  16. 1 1
      src/client/js/components/PageAttachment.jsx
  17. 7 11
      src/client/js/components/PageAttachment/Attachment.jsx
  18. 0 3
      src/client/js/components/PageAttachment/PageAttachmentList.jsx
  19. 6 2
      src/client/js/components/PageComment/Comment.jsx
  20. 2 1
      src/client/js/services/CommentContainer.js
  21. 15 3
      src/client/js/services/PageContainer.js
  22. 1 1
      src/client/styles/scss/theme/_apply-colors-light.scss
  23. 16 1
      src/client/styles/scss/theme/_apply-colors.scss
  24. 6 5
      src/client/styles/scss/theme/default.scss
  25. 5 0
      src/server/routes/attachment.js
  26. 7 2
      src/server/routes/page.js
  27. 2 2
      src/server/service/search-delegator/elasticsearch.js
  28. 3 2
      src/server/views/widget/page_content.html

+ 1 - 1
.devcontainer/Dockerfile

@@ -3,7 +3,7 @@
 # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
 # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
 #-------------------------------------------------------------------------------------------------------------
 #-------------------------------------------------------------------------------------------------------------
 
 
-FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:14
+FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-14
 
 
 # The node image includes a non-root user with sudo access. Use the
 # The node image includes a non-root user with sudo access. Use the
 # "remoteUser" property in devcontainer.json to use it. On Linux, update
 # "remoteUser" property in devcontainer.json to use it. On Linux, update

+ 2 - 0
.gitignore

@@ -18,6 +18,8 @@
 # dist
 # dist
 /dist/
 /dist/
 /report/
 /report/
+/public/static/js
+/public/static/styles
 /public/uploads
 /public/uploads
 /tmp/
 /tmp/
 
 

+ 11 - 1
CHANGES.md

@@ -1,6 +1,6 @@
 # CHANGES
 # CHANGES
 
 
-## v4.2.0
+## v4.2.0-RC
 
 
 ### BREAKING CHANGES
 ### BREAKING CHANGES
 
 
@@ -12,6 +12,16 @@
 * 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
+
+* Improvement: Fire global notification when a new page is created by uploading file
+* Fix: Change default `DRAWIO_URI` to embed.diagrams.net
+* Fix: An unhandled rejection occures when a user who does not send referer accesses
 
 
 ## v4.1.6
 ## v4.1.6
 
 

+ 1 - 1
README.md

@@ -95,7 +95,7 @@ Development
 - Node.js v12.x or v14.x
 - Node.js v12.x or v14.x
 - npm 6.x
 - npm 6.x
 - yarn
 - yarn
-- MongoDB 3.x
+- MongoDB 4.x
 
 
 See [confirmed versions](https://docs.growi.org/en/dev/startup/dev-env.html#set-up-node-js-environment).
 See [confirmed versions](https://docs.growi.org/en/dev/startup/dev-env.html#set-up-node-js-environment).
 
 

+ 6 - 0
src/client/js/app.jsx

@@ -95,6 +95,12 @@ if (pageContainer.state.pageId != null) {
 
 
     'user-bookmark-list': <MyBookmarkList />,
     'user-bookmark-list': <MyBookmarkList />,
     'user-created-list': <RecentCreated />,
     'user-created-list': <RecentCreated />,
+    // '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) {

+ 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}
       />
       />
     );
     );

+ 2 - 2
src/client/js/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx

@@ -219,8 +219,8 @@ class SelectCollectionsModal extends React.Component {
           </ModalBody>
           </ModalBody>
 
 
           <ModalFooter>
           <ModalFooter>
-            <button type="button" className="btn btn-sm btn-outline-secondary" onClick={this.props.onClose}>{t('export_management.cancel')}</button>
-            <button type="submit" className="btn btn-sm btn-primary" disabled={!this.validateForm()}>{t('export_management.export')}</button>
+            <button type="button" className="btn btn-sm btn-outline-secondary" onClick={this.props.onClose}>{t('admin:export_management.cancel')}</button>
+            <button type="submit" className="btn btn-sm btn-primary" disabled={!this.validateForm()}>{t('admin:export_management.export')}</button>
           </ModalFooter>
           </ModalFooter>
         </form>
         </form>
       </Modal>
       </Modal>

+ 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;

+ 2 - 2
src/client/js/components/Page/TrashPageAlert.jsx

@@ -15,7 +15,7 @@ import PageDeleteModal from '../PageDeleteModal';
 const TrashPageAlert = (props) => {
 const TrashPageAlert = (props) => {
   const { t, appContainer, pageContainer } = props;
   const { t, appContainer, pageContainer } = props;
   const {
   const {
-    path, isDeleted, revisionAuthor, updatedAt, hasChildren, isAbleToDeleteCompletely,
+    path, isDeleted, lastUpdateUsername, updatedAt, hasChildren, isAbleToDeleteCompletely,
   } = pageContainer.state;
   } = pageContainer.state;
   const { currentUser } = appContainer;
   const { currentUser } = appContainer;
   const [isEmptyTrashModalShown, setIsEmptyTrashModalShown] = useState(false);
   const [isEmptyTrashModalShown, setIsEmptyTrashModalShown] = useState(false);
@@ -111,7 +111,7 @@ const TrashPageAlert = (props) => {
       <div className="alert alert-warning py-3 px-4 d-flex align-items-center">
       <div className="alert alert-warning py-3 px-4 d-flex align-items-center">
         <div>
         <div>
           This page is in the trash <i className="icon-trash" aria-hidden="true"></i>.
           This page is in the trash <i className="icon-trash" aria-hidden="true"></i>.
-          {isDeleted && <span><br /><UserPicture user={revisionAuthor} /> Deleted by {revisionAuthor.name} at {updatedAt}</span>}
+          {isDeleted && <span><br /><UserPicture user={{ username: lastUpdateUsername }} /> Deleted by {lastUpdateUsername} at {updatedAt}</span>}
         </div>
         </div>
         {(currentUser.admin && path === '/trash' && hasChildren) && renderEmptyButton()}
         {(currentUser.admin && path === '/trash' && hasChildren) && renderEmptyButton()}
         {(isDeleted && currentUser != null) && renderTrashPageManagementButtons()}
         {(isDeleted && currentUser != null) && renderTrashPageManagementButtons()}

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

@@ -138,7 +138,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}

+ 7 - 11
src/client/js/components/PageAttachment/Attachment.jsx

@@ -54,20 +54,16 @@ export default class Attachment extends React.Component {
       : '';
       : '';
 
 
     return (
     return (
-      <li className="attachment">
+      <div className="attachment mb-2">
         <span className="mr-1 attachment-userpicture">
         <span className="mr-1 attachment-userpicture">
           <UserPicture user={attachment.creator} size="sm"></UserPicture>
           <UserPicture user={attachment.creator} size="sm"></UserPicture>
         </span>
         </span>
-
-        <a href={attachment.filePathProxied}><i className={formatIcon}></i> {attachment.originalName}</a>
-
-        {fileType}
-
-        {fileInUse}
-
-        {btnDownload}
-        {btnTrash}
-      </li>
+        <a className="mr-2" href={attachment.filePathProxied}><i className={formatIcon}></i> {attachment.originalName}</a>
+        <span className="mr-2">{fileType}</span>
+        <span className="mr-2">{fileInUse}</span>
+        <span className="mr-2">{btnDownload}</span>
+        <span className="mr-2">{btnTrash}</span>
+      </div>
     );
     );
   }
   }
 
 

+ 0 - 3
src/client/js/components/PageAttachment/PageAttachmentList.jsx

@@ -24,9 +24,6 @@ export default class PageAttachmentList extends React.Component {
 
 
     return (
     return (
       <div>
       <div>
-        {(attachmentList.length !== 0)
-          && <h5><strong>Attachments</strong></h5>
-        }
         <ul className="pl-2">
         <ul className="pl-2">
           {attachmentList}
           {attachmentList}
         </ul>
         </ul>

+ 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;
     });
     });

+ 15 - 3
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')),
@@ -74,12 +72,26 @@ export default class PageContainer extends Container {
       // latest(on remote) information
       // latest(on remote) information
       remoteRevisionId: revisionId,
       remoteRevisionId: revisionId,
       revisionIdHackmdSynced: mainContent.getAttribute('data-page-revision-id-hackmd-synced') || null,
       revisionIdHackmdSynced: mainContent.getAttribute('data-page-revision-id-hackmd-synced') || null,
-      lastUpdateUsername: undefined,
+      lastUpdateUsername: mainContent.getAttribute('data-page-last-update-username') || null,
       pageIdOnHackmd: mainContent.getAttribute('data-page-id-on-hackmd') || null,
       pageIdOnHackmd: mainContent.getAttribute('data-page-id-on-hackmd') || null,
       hasDraftOnHackmd: !!mainContent.getAttribute('data-page-has-draft-on-hackmd'),
       hasDraftOnHackmd: !!mainContent.getAttribute('data-page-has-draft-on-hackmd'),
       isHackmdDraftUpdatingInRealtime: false,
       isHackmdDraftUpdatingInRealtime: false,
     };
     };
 
 
+    // parse creator, lastUpdateUser and revisionAuthor
+    try {
+      this.state.creator = JSON.parse(mainContent.getAttribute('data-page-creator'));
+    }
+    catch (e) {
+      logger.warn('The data of \'data-page-creator\' is invalid', e);
+    }
+    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);
+    }
+
     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);

+ 1 - 1
src/client/styles/scss/theme/_apply-colors-light.scss

@@ -3,8 +3,8 @@ $color-list: $color-global !default;
 $bgcolor-list: $bgcolor-global !default;
 $bgcolor-list: $bgcolor-global !default;
 $color-list-hover: $color-global !default;
 $color-list-hover: $color-global !default;
 $bgcolor-list-hover: darken($bgcolor-global, 3%) !default;
 $bgcolor-list-hover: darken($bgcolor-global, 3%) !default;
-$color-list-active: $color-reversal !default;
 $bgcolor-list-active: $primary !default;
 $bgcolor-list-active: $primary !default;
+$color-list-active: color-yiq($bgcolor-list-active) !default;
 $bgcolor-subnav: darken($bgcolor-global, 3%) !default;
 $bgcolor-subnav: darken($bgcolor-global, 3%) !default;
 $color-table: $color-global !default;
 $color-table: $color-global !default;
 $bgcolor-table: null !default;
 $bgcolor-table: null !default;

+ 16 - 1
src/client/styles/scss/theme/_apply-colors.scss

@@ -246,8 +246,11 @@ pre:not(.hljs):not(.CodeMirror-line) {
 .modal {
 .modal {
   .modal-header {
   .modal-header {
     border-bottom-color: $border-color-theme;
     border-bottom-color: $border-color-theme;
+    .modal-title {
+      color: color-yiq($primary);
+    }
     .close {
     .close {
-      color: $light;
+      color: color-yiq($primary);
       opacity: 0.5;
       opacity: 0.5;
       &:hover {
       &:hover {
         opacity: 0.9;
         opacity: 0.9;
@@ -294,6 +297,16 @@ pre:not(.hljs):not(.CodeMirror-line) {
       color: $secondary;
       color: $secondary;
     }
     }
   }
   }
+
+  .modal-title {
+    position: relative;
+  }
+
+  .nav-link {
+    &:hover {
+      background-color: rgba($link-color, 0.08);
+    }
+  }
   .nav-link svg {
   .nav-link svg {
     fill: $color-link;
     fill: $color-link;
   }
   }
@@ -302,6 +315,8 @@ pre:not(.hljs):not(.CodeMirror-line) {
   }
   }
 
 
   .grw-nav-slide-hr {
   .grw-nav-slide-hr {
+    position: absolute;
+    bottom: 0px;
     border-color: $color-link;
     border-color: $color-link;
   }
   }
 }
 }

+ 6 - 5
src/client/styles/scss/theme/default.scss

@@ -108,7 +108,8 @@ html[light] {
 //== Dark Mode
 //== Dark Mode
 //
 //
 html[dark] {
 html[dark] {
-  $primary: #db00c2;
+  $primary: #115cd3;
+  $accent: #db00c2;
 
 
   // Background colors
   // Background colors
   $bgcolor-global: #131418;
   $bgcolor-global: #131418;
@@ -116,7 +117,7 @@ html[dark] {
   $bgcolor-card: darken($bgcolor-global, 5%);
   $bgcolor-card: darken($bgcolor-global, 5%);
 
 
   // Font colors
   // Font colors
-  $color-global: #a8a8a8;
+  $color-global: $gray-400;
   $color-reversal: $gray-900;
   $color-reversal: $gray-900;
   // $color-header: desaturate($primary, 20%);
   // $color-header: desaturate($primary, 20%);
   $color-link: #7b9ad5;
   $color-link: #7b9ad5;
@@ -131,7 +132,7 @@ html[dark] {
   // $bgcolor-list: $bgcolor-global; // optional
   // $bgcolor-list: $bgcolor-global; // optional
   // $color-list-hover: $color-global; // optional
   // $color-list-hover: $color-global; // optional
   // $bgcolor-list-hover: lighten($bgcolor-global, 3%); // optional
   // $bgcolor-list-hover: lighten($bgcolor-global, 3%); // optional
-  $color-list-active: white; // optional
+  // $color-list-active:white ; // optional
   // $bgcolor-list-active: $primary; // optional
   // $bgcolor-list-active: $primary; // optional
 
 
   // Table colors
   // Table colors
@@ -143,7 +144,7 @@ html[dark] {
 
 
   // Navbar
   // Navbar
   $bgcolor-navbar: #2a2929;
   $bgcolor-navbar: #2a2929;
-  $bgcolor-search-top-dropdown: $primary;
+  $bgcolor-search-top-dropdown: $accent;
   $border-image-navbar: linear-gradient(to right, #44bfe3 0%, #b04aff 50%, #ff1794 100%);
   $border-image-navbar: linear-gradient(to right, #44bfe3 0%, #b04aff 50%, #ff1794 100%);
 
 
   // Logo colors
   // Logo colors
@@ -156,7 +157,7 @@ html[dark] {
   $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
   $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
   // Sidebar resize button
   // Sidebar resize button
   $color-resize-button: white;
   $color-resize-button: white;
-  $bgcolor-resize-button: $primary;
+  $bgcolor-resize-button: $accent;
   $color-resize-button-hover: white;
   $color-resize-button-hover: white;
   $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
   $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
   // Sidebar contents
   // Sidebar contents

+ 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,

+ 3 - 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,8 @@
   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-last-update-username="{% if page && page.lastUpdateUser %}{{ page.lastUpdateUser.name }}{% 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 %}"