Просмотр исходного кода

Merge branch 'master' into feat/GW-3658-can-use-GCP-from-db-vars

yusuketk 5 лет назад
Родитель
Сommit
4c9010021d
37 измененных файлов с 327 добавлено и 208 удалено
  1. 1 1
      .devcontainer/Dockerfile
  2. 2 0
      .gitignore
  3. 11 1
      CHANGES.md
  4. 1 1
      README.md
  5. 5 0
      resource/locales/en_US/admin/admin.json
  6. 1 1
      resource/locales/en_US/translation.json
  7. 5 0
      resource/locales/ja_JP/admin/admin.json
  8. 6 1
      resource/locales/zh_CN/admin/admin.json
  9. 1 1
      resource/locales/zh_CN/translation.json
  10. 5 1
      src/client/js/app.jsx
  11. 38 0
      src/client/js/components/Admin/Common/LabeledProgressBar.jsx
  12. 0 45
      src/client/js/components/Admin/Common/ProgressBar.jsx
  13. 13 3
      src/client/js/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx
  14. 2 2
      src/client/js/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  15. 3 3
      src/client/js/components/Admin/ExportArchiveDataPage.jsx
  16. 18 4
      src/client/js/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  17. 50 8
      src/client/js/components/Admin/ImportData/GrowiArchiveSection.jsx
  18. 42 0
      src/client/js/components/Navbar/AuthorInfo.jsx
  19. 7 12
      src/client/js/components/Navbar/GrowiSubNavigation.jsx
  20. 0 37
      src/client/js/components/Navbar/PageCreator.jsx
  21. 0 41
      src/client/js/components/Navbar/RevisionAuthor.jsx
  22. 2 2
      src/client/js/components/Page/TrashPageAlert.jsx
  23. 1 1
      src/client/js/components/PageAttachment.jsx
  24. 7 11
      src/client/js/components/PageAttachment/Attachment.jsx
  25. 0 3
      src/client/js/components/PageAttachment/PageAttachmentList.jsx
  26. 6 2
      src/client/js/components/PageComment/Comment.jsx
  27. 2 1
      src/client/js/services/CommentContainer.js
  28. 15 3
      src/client/js/services/PageContainer.js
  29. 1 1
      src/client/styles/scss/theme/_apply-colors-light.scss
  30. 16 1
      src/client/styles/scss/theme/_apply-colors.scss
  31. 6 5
      src/client/styles/scss/theme/default.scss
  32. 13 6
      src/server/routes/apiv3/import.js
  33. 18 3
      src/server/routes/attachment.js
  34. 7 2
      src/server/routes/page.js
  35. 17 1
      src/server/service/import.js
  36. 2 2
      src/server/service/search-delegator/elasticsearch.js
  37. 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.
 #-------------------------------------------------------------------------------------------------------------
 
-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
 # "remoteUser" property in devcontainer.json to use it. On Linux, update

+ 2 - 0
.gitignore

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

+ 11 - 1
CHANGES.md

@@ -1,6 +1,6 @@
 # CHANGES
 
-## v4.2.0
+## v4.2.0-RC
 
 ### BREAKING CHANGES
 
@@ -12,6 +12,16 @@
 * Improvement: Basic layout of page
 * 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
 

+ 1 - 1
README.md

@@ -95,7 +95,7 @@ Development
 - Node.js v12.x or v14.x
 - npm 6.x
 - 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).
 

+ 5 - 0
resource/locales/en_US/admin/admin.json

@@ -155,6 +155,7 @@
       "upload": "Upload",
       "discard": "Discard uploaded data",
       "errors": {
+        "different_versions": "this growi and the uploarded data versions are not met",
         "at_least_one": "Select one or more collections.",
         "page_and_revision": "'Pages' and 'Revisions' must be imported both.",
         "depends": "'{{target}}' must be selected when '{{condition}}' is selected."
@@ -197,6 +198,10 @@
       "test_connection": "Test connection to qiita:team"
     },
     "import": "Import",
+    "skip_username_and_email_when_overlapped": "Skip username and email using same username and email in new environment",
+    "prepare_new_account_for_migration":"Prepare new account for migration",
+    "archive_data_import_detail":"More Details? Ckick here.",
+    "admin_archive_data_import_guide_url":"https://docs.growi.org/en/admin-guide/management-cookbook/import.html",
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
     "Directory_hierarchy_tag": "Directory hierarchy tag"
   },

+ 1 - 1
resource/locales/en_US/translation.json

@@ -658,7 +658,7 @@
     "how_to": {
       "header": "How to configure Incoming Webhooks?",
       "workspace": "(At Workspace) Add a hook",
-      "workspace_desc1": "Go to <a href='https: //slack.com/services/new/incoming-webhook'>Incoming Webhooks configuration page</a>.",
+      "workspace_desc1": "Go to <a href='https://slack.com/services/new/incoming-webhook'>Incoming Webhooks configuration page</a>.",
       "workspace_desc2": "Choose the default channel to post.",
       "workspace_desc3": "Add.",
       "at_growi": "(At GROWI admin page) Set Webhook URL",

+ 5 - 0
resource/locales/ja_JP/admin/admin.json

@@ -173,6 +173,7 @@
       "upload": "アップロード",
       "discard": "アップロードしたデータを破棄する",
       "errors": {
+        "different_versions": "現在のGROWIとアップロードしたデータのバージョンが違います",
         "at_least_one": "コレクションが選択されていません",
         "page_and_revision": "'Pages' と 'Revisions' はセットでインポートする必要があります",
         "depends": "'{{condition}}' をインポートする場合は、'{{target}}' を一緒に選択する必要があります"
@@ -215,6 +216,10 @@
       "test_connection": "接続テスト"
     },
     "import": "インポート",
+    "skip_username_and_email_when_overlapped": "ユーザー名またはメールアドレスが同じ場合、その部分がスキップされます。",
+    "prepare_new_account_for_migration":"移行用のアカウントを新環境で用意してください。",
+    "archive_data_import_detail":"参考: GROWI Docs - データのインポート",
+    "admin_archive_data_import_guide_url":"https://docs.growi.org/ja/admin-guide/management-cookbook/import.html#growi-%E3%82%A2%E3%83%BC%E3%82%AB%E3%82%A4%E3%83%96%E3%83%87%E3%83%BC%E3%82%BF%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88",
     "page_skip": "既に GROWI 側に同名のページが存在する場合、そのページはスキップされます",
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
   },

+ 6 - 1
resource/locales/zh_CN/admin/admin.json

@@ -167,6 +167,7 @@
 			"upload": "Upload",
 			"discard": "Discard uploaded data",
 			"errors": {
+        "versions_not_met": "this growi and the uploarded data versions are not met",
 				"at_least_one": "Select one or more collections.",
 				"page_and_revision": "'Pages' and 'Revisions' must be imported both.",
 				"depends": "'{{target}}' must be selected when '{{condition}}' is selected."
@@ -208,7 +209,11 @@
 			"access_token": "Access token",
 			"test_connection": "Test connection to qiita:team"
 		},
-		"import": "Import",
+    "import": "Import",
+    "skip_username_and_email_when_overlapped": "Skip username and email using same username and email in new environment",
+    "prepare_new_account_for_migration":"Prepare new account for migration",
+    "archive_data_import_detail":"More details? Click here.",
+    "admin_archive_data_import_guide_url":"https://docs.growi.org/en/admin-guide/management-cookbook/import.html",
 		"page_skip": "Pages with a name that already exists on GROWI are not imported",
 		"Directory_hierarchy_tag": "Directory hierarchy tag"
 	},

+ 1 - 1
resource/locales/zh_CN/translation.json

@@ -645,7 +645,7 @@
 		"how_to": {
 			"header": "How to configure Incoming Webhooks?",
 			"workspace": "(At Workspace) Add a hook",
-			"workspace_desc1": "Go to <a href='https: //slack.com/services/new/incoming-webhook'>Incoming Webhooks configuration page</a>.",
+			"workspace_desc1": "Go to <a href='https://slack.com/services/new/incoming-webhook'>Incoming Webhooks configuration page</a>.",
 			"workspace_desc2": "Choose the default channel to post.",
 			"workspace_desc3": "Add.",
 			"at_growi": "(At GROWI admin page) Set Webhook URL",

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

@@ -93,10 +93,14 @@ if (pageContainer.state.pageId != null) {
     'seen-user-list': <SeenUserList />,
     'liker-list': <LikerList />,
 
-    'user-created-list': <RecentCreated userId={pageContainer.state.creator._id} />,
     '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) {
   Object.assign(componentMappings, {
     // 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);

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

@@ -6,7 +6,7 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AdminSocketIoContainer from '../../../services/AdminSocketIoContainer';
 
-import ProgressBar from '../Common/ProgressBar';
+import LabeledProgressBar from '../Common/LabeledProgressBar';
 
 class RebuildIndexControls extends React.Component {
 
@@ -58,12 +58,22 @@ class RebuildIndexControls extends React.Component {
       return null;
     }
 
-    const header = isRebuildingCompleted ? 'Completed' : `Processing.. (${skip} skips)`;
+    function getCompletedLabel() {
+      const completedLabel = skip === 0 ? 'Completed' : `Done (${skip} skips)`;
+      return completedLabel;
+    }
+
+    function getSkipLabel() {
+      return `Processing.. (${skip} skips)`;
+    }
+
+    const header = isRebuildingCompleted ? getCompletedLabel() : getSkipLabel();
 
     return (
-      <ProgressBar
+      <LabeledProgressBar
         header={header}
         currentCount={current}
+        errorsCount={skip}
         totalCount={total}
       />
     );

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

@@ -219,8 +219,8 @@ class SelectCollectionsModal extends React.Component {
           </ModalBody>
 
           <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>
         </form>
       </Modal>

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

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

+ 18 - 4
src/client/js/components/Admin/ImportData/GrowiArchive/UploadForm.jsx

@@ -4,7 +4,7 @@ import { withTranslation } from 'react-i18next';
 
 import { withUnstatedContainers } from '../../../UnstatedUtils';
 import AppContainer from '../../../../services/AppContainer';
-// import { toastSuccess, toastError } from '../../../util/apiNotification';
+import { toastError } from '../../../../util/apiNotification';
 
 class UploadForm extends React.Component {
 
@@ -31,9 +31,21 @@ class UploadForm extends React.Component {
     formData.append('_csrf', this.props.appContainer.csrfToken);
     formData.append('file', this.inputRef.current.files[0]);
 
-    const { data } = await this.props.appContainer.apiv3Post('/import/upload', formData);
-    this.props.onUpload(data);
-    // TODO: toastSuccess, toastError
+    try {
+      const { data } = await this.props.appContainer.apiv3Post('/import/upload', formData);
+      // TODO: toastSuccess, toastError
+      this.props.onUpload(data);
+    }
+    catch (err) {
+      if (err[0].code === 'versions-are-not-met') {
+        if (this.props.onVersionMismatch !== null) {
+          this.props.onVersionMismatch(err[0].code);
+        }
+      }
+      else {
+        toastError(err);
+      }
+    }
   }
 
   validateForm() {
@@ -83,6 +95,8 @@ UploadForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   onUpload: PropTypes.func.isRequired,
+  isTheSameVersion: PropTypes.bool,
+  onVersionMismatch: PropTypes.func,
 };
 
 /**

+ 50 - 8
src/client/js/components/Admin/ImportData/GrowiArchiveSection.jsx

@@ -18,6 +18,7 @@ class GrowiArchiveSection extends React.Component {
     this.initialState = {
       fileName: null,
       innerFileStats: null,
+      isTheSameVersion: null,
     };
 
     this.state = this.initialState;
@@ -25,6 +26,8 @@ class GrowiArchiveSection extends React.Component {
     this.handleUpload = this.handleUpload.bind(this);
     this.discardData = this.discardData.bind(this);
     this.resetState = this.resetState.bind(this);
+    this.handleMismatchedVersions = this.handleMismatchedVersions.bind(this);
+    this.renderDefferentVersionAlert = this.renderDefferentVersionAlert.bind(this);
   }
 
   async componentWillMount() {
@@ -33,14 +36,19 @@ class GrowiArchiveSection extends React.Component {
 
     if (res.data.zipFileStat != null) {
       const { fileName, innerFileStats } = res.data.zipFileStat;
-      this.setState({ fileName, innerFileStats });
+      const { isTheSameVersion } = res.data;
+
+      this.setState({ fileName, innerFileStats, isTheSameVersion });
     }
   }
 
-  handleUpload({ meta, fileName, innerFileStats }) {
+  handleUpload({
+    meta, fileName, innerFileStats,
+  }) {
     this.setState({
       fileName,
       innerFileStats,
+      isTheSameVersion: true,
     });
   }
 
@@ -74,18 +82,51 @@ class GrowiArchiveSection extends React.Component {
     }
   }
 
+
+  handleMismatchedVersions(err) {
+    this.setState({
+      isTheSameVersion: false,
+    });
+
+  }
+
+  renderDefferentVersionAlert() {
+    const { t } = this.props;
+    return (
+      <div className="alert alert-warning mt-3">
+        {t('admin:importer_management.growi_settings.errors.different_versions')}
+      </div>
+    );
+  }
+
   resetState() {
     this.setState(this.initialState);
   }
 
   render() {
     const { t } = this.props;
+    const { isTheSameVersion } = this.state;
 
     return (
       <Fragment>
         <h2>{t('admin:importer_management.import_growi_archive')}</h2>
-
-        {this.state.fileName != null ? (
+        <div className="card well mb-4 small">
+          <ul>
+            <li>{t('admin:importer_management.skip_username_and_email_when_overlapped')}</li>
+            <li>{t('admin:importer_management.prepare_new_account_for_migration')}</li>
+            <li>
+              <a
+                href={`${t('admin:importer_management.admin_archive_data_import_guide_url')}`}
+                target="_blank"
+                rel="noopener noreferrer"
+              >{t('admin:importer_management.archive_data_import_detail')}
+              </a>
+            </li>
+          </ul>
+        </div>
+
+        {isTheSameVersion === false && this.renderDefferentVersionAlert()}
+        {this.state.fileName != null && isTheSameVersion === true ? (
           <div className="px-4">
             <ImportForm
               fileName={this.state.fileName}
@@ -94,10 +135,11 @@ class GrowiArchiveSection extends React.Component {
             />
           </div>
         )
-          : (
-            <UploadForm
-              onUpload={this.handleUpload}
-            />
+        : (
+          <UploadForm
+            onUpload={this.handleUpload}
+            onVersionMismatch={this.handleMismatchedVersions}
+          />
           )}
       </Fragment>
     );

+ 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

@@ -19,8 +19,7 @@ import TagLabels from '../Page/TagLabels';
 import LikeButton from '../LikeButton';
 import BookmarkButton from '../BookmarkButton';
 
-import PageCreator from './PageCreator';
-import RevisionAuthor from './RevisionAuthor';
+import AuthorInfo from './AuthorInfo';
 import DrawerToggler from './DrawerToggler';
 import UserPicture from '../User/UserPicture';
 
@@ -200,16 +199,12 @@ const GrowiSubNavigation = (props) => {
         {/* Page Authors */}
         { (!isCompactMode && !isUserPage) && (
           <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>
         ) }
       </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 { t, appContainer, pageContainer } = props;
   const {
-    path, isDeleted, revisionAuthor, updatedAt, hasChildren, isAbleToDeleteCompletely,
+    path, isDeleted, lastUpdateUsername, updatedAt, hasChildren, isAbleToDeleteCompletely,
   } = pageContainer.state;
   const { currentUser } = appContainer;
   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>
           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>
         {(currentUser.admin && path === '/trash' && hasChildren) && renderEmptyButton()}
         {(isDeleted && currentUser != null) && renderTrashPageManagementButtons()}

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

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

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

@@ -54,20 +54,16 @@ export default class Attachment extends React.Component {
       : '';
 
     return (
-      <li className="attachment">
+      <div className="attachment mb-2">
         <span className="mr-1 attachment-userpicture">
           <UserPicture user={attachment.creator} size="sm"></UserPicture>
         </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 (
       <div>
-        {(attachmentList.length !== 0)
-          && <h5><strong>Attachments</strong></h5>
-        }
         <ul className="pl-2">
           {attachmentList}
         </ul>

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

@@ -77,7 +77,11 @@ class Comment extends React.PureComponent {
   }
 
   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() {
@@ -179,7 +183,7 @@ class Comment extends React.PureComponent {
             currentCommentId={commentId}
             commentBody={comment.comment}
             replyTo={undefined}
-            commentCreator={creator.username}
+            commentCreator={creator?.username}
             onCancelButtonClicked={() => 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) {
     const noImageCacheUserIds = comments.filter((comment) => {
-      return comment.creator.imageUrlCached == null;
+      const { creator } = comment;
+      return creator != null && creator.imageUrlCached == null;
     }).map((comment) => {
       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'),
       revisionId,
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
-      revisionAuthor: JSON.parse(mainContent.getAttribute('data-page-revision-author')),
       path,
       tocHtml: '',
       isLiked: JSON.parse(mainContent.getAttribute('data-page-is-liked')),
@@ -58,7 +57,6 @@ export default class PageContainer extends Container {
       sumOfLikers: 0,
 
       createdAt: mainContent.getAttribute('data-page-created-at'),
-      creator: JSON.parse(mainContent.getAttribute('data-page-creator')),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
       isForbidden:  JSON.parse(mainContent.getAttribute('data-page-is-forbidden')),
       isDeleted:  JSON.parse(mainContent.getAttribute('data-page-is-deleted')),
@@ -74,12 +72,26 @@ export default class PageContainer extends Container {
       // latest(on remote) information
       remoteRevisionId: revisionId,
       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,
       hasDraftOnHackmd: !!mainContent.getAttribute('data-page-has-draft-on-hackmd'),
       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;
     interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(appContainer), 10); // process as soon as possible
     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;
 $color-list-hover: $color-global !default;
 $bgcolor-list-hover: darken($bgcolor-global, 3%) !default;
-$color-list-active: $color-reversal !default;
 $bgcolor-list-active: $primary !default;
+$color-list-active: color-yiq($bgcolor-list-active) !default;
 $bgcolor-subnav: darken($bgcolor-global, 3%) !default;
 $color-table: $color-global !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-header {
     border-bottom-color: $border-color-theme;
+    .modal-title {
+      color: color-yiq($primary);
+    }
     .close {
-      color: $light;
+      color: color-yiq($primary);
       opacity: 0.5;
       &:hover {
         opacity: 0.9;
@@ -294,6 +297,16 @@ pre:not(.hljs):not(.CodeMirror-line) {
       color: $secondary;
     }
   }
+
+  .modal-title {
+    position: relative;
+  }
+
+  .nav-link {
+    &:hover {
+      background-color: rgba($link-color, 0.08);
+    }
+  }
   .nav-link svg {
     fill: $color-link;
   }
@@ -302,6 +315,8 @@ pre:not(.hljs):not(.CodeMirror-line) {
   }
 
   .grw-nav-slide-hr {
+    position: absolute;
+    bottom: 0px;
     border-color: $color-link;
   }
 }

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

@@ -108,7 +108,8 @@ html[light] {
 //== Dark Mode
 //
 html[dark] {
-  $primary: #db00c2;
+  $primary: #115cd3;
+  $accent: #db00c2;
 
   // Background colors
   $bgcolor-global: #131418;
@@ -116,7 +117,7 @@ html[dark] {
   $bgcolor-card: darken($bgcolor-global, 5%);
 
   // Font colors
-  $color-global: #a8a8a8;
+  $color-global: $gray-400;
   $color-reversal: $gray-900;
   // $color-header: desaturate($primary, 20%);
   $color-link: #7b9ad5;
@@ -131,7 +132,7 @@ html[dark] {
   // $bgcolor-list: $bgcolor-global; // optional
   // $color-list-hover: $color-global; // optional
   // $bgcolor-list-hover: lighten($bgcolor-global, 3%); // optional
-  $color-list-active: white; // optional
+  // $color-list-active:white ; // optional
   // $bgcolor-list-active: $primary; // optional
 
   // Table colors
@@ -143,7 +144,7 @@ html[dark] {
 
   // Navbar
   $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%);
 
   // Logo colors
@@ -156,7 +157,7 @@ html[dark] {
   $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
   // Sidebar resize button
   $color-resize-button: white;
-  $bgcolor-resize-button: $primary;
+  $bgcolor-resize-button: $accent;
   $color-resize-button-hover: white;
   $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
   // Sidebar contents

+ 13 - 6
src/server/routes/apiv3/import.js

@@ -8,6 +8,7 @@ const multer = require('multer');
 const express = require('express');
 
 const GrowiArchiveImportOption = require('@commons/models/admin/growi-archive-import-option');
+const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
 const router = express.Router();
@@ -305,20 +306,26 @@ module.exports = (crowi) => {
   router.post('/upload', uploads.single('file'), accessTokenParser, loginRequired, adminRequired, csrf, async(req, res) => {
     const { file } = req;
     const zipFile = importService.getFile(file.filename);
+    let data = null;
 
     try {
-      const data = await growiBridgeService.parseZipFile(zipFile);
-
-      // validate with meta.json
-      importService.validate(data.meta);
-
-      return res.apiv3(data);
+      data = await growiBridgeService.parseZipFile(zipFile);
     }
     catch (err) {
       // TODO: use ApiV3Error
       logger.error(err);
       return res.status(500).send({ status: 'ERROR' });
     }
+    try {
+      // validate with meta.json
+      importService.validate(data.meta);
+      return res.apiv3(data);
+    }
+    catch {
+      const msg = 'the version of this growi and the growi that exported the data are not met';
+      const varidationErr = 'versions-are-not-met';
+      return res.apiv3Err(new ErrorV3(msg, varidationErr), 500);
+    }
   });
 
   /**

+ 18 - 3
src/server/routes/attachment.js

@@ -128,8 +128,8 @@ const ApiResponse = require('../util/apiResponse');
 module.exports = function(crowi, app) {
   const Attachment = crowi.model('Attachment');
   const Page = crowi.model('Page');
-  const { fileUploadService, attachmentService } = crowi;
-
+  const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
+  const { fileUploadService, attachmentService, globalNotificationService } = crowi;
 
   /**
    * Check the user is accessible to the related page
@@ -152,6 +152,11 @@ module.exports = function(crowi, app) {
    * @param {Attachment} attachment
    */
   async function isDeletableByUser(user, attachment) {
+    // deletable if creator is null
+    if (attachment.creator == null) {
+      return true;
+    }
+
     const ownerId = attachment.creator._id || attachment.creator;
     if (attachment.page == null) { // when profile image
       return user.id === ownerId.toString();
@@ -463,7 +468,17 @@ module.exports = function(crowi, app) {
       pageCreated,
     };
 
-    return res.json(ApiResponse.success(result));
+    res.json(ApiResponse.success(result));
+
+    if (pageCreated) {
+      // global notification
+      try {
+        await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, page, req.user);
+      }
+      catch (err) {
+        logger.error('Create notification failed', err);
+      }
+    }
   };
 
   /**

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

@@ -221,12 +221,17 @@ module.exports = function(crowi, app) {
 
   function addRenderVarsForPage(renderVars, page) {
     renderVars.page = page;
-    renderVars.page.creator = renderVars.page.creator.toObject();
     renderVars.revision = page.revision;
-    renderVars.revision.author = renderVars.revision.author.toObject();
     renderVars.pageIdOnHackmd = page.pageIdOnHackmd;
     renderVars.revisionHackmdSynced = page.revisionHackmdSynced;
     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) {

+ 17 - 1
src/server/service/import.js

@@ -147,8 +147,24 @@ class ImportService {
 
     const isImporting = this.currentProgressingStatus != null;
 
+    const zipFileStat = filtered.pop();
+    let isTheSameVersion = false;
+
+    if (zipFileStat != null) {
+      try {
+        this.validate(zipFileStat.meta);
+        isTheSameVersion = true;
+      }
+      catch (err) {
+        isTheSameVersion = false;
+        logger.error('the versions are not met', err);
+      }
+    }
+
+
     return {
-      zipFileStat: filtered.pop(),
+      isTheSameVersion,
+      zipFileStat,
       isImporting,
       progressList: isImporting ? this.currentProgressingStatus.progressList : null,
     };

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

@@ -51,7 +51,7 @@ class ElasticsearchDelegator {
   }
 
   shouldIndexed(page) {
-    return page.creator != null && page.revision != null && page.redirectTo == null;
+    return page.revision != null && page.redirectTo == null;
   }
 
   initClient() {
@@ -310,7 +310,7 @@ class ElasticsearchDelegator {
     let document = {
       path: page.path,
       body: page.revision.body,
-      username: page.creator.username,
+      username: page.creator?.username,
       comment_count: page.commentCount,
       bookmark_count: bookmarkCount,
       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-revision-id="{% if revision %}{{ revision._id.toString() }}{% 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-id-on-hackmd="{% if pageIdOnHackmd %}{{ pageIdOnHackmd.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-slack-channels="{{ slack|default('') }}"
   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-has-children="{% if pages.length > 0 %}true{% else %}false{% endif %}"
   data-page-user="{% if pageUser %}{{ pageUser|json }}{% else %}null{% endif %}"