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

Merge branch 'feat/article-area-renovation' into imprv/article-area-gw3981

白石誠 5 лет назад
Родитель
Сommit
d1c835b5f1

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

+ 6 - 1
CHANGES.md

@@ -1,6 +1,6 @@
 # CHANGES
 
-## v4.2.0
+## v4.2.0-RC
 
 ### BREAKING CHANGES
 
@@ -12,6 +12,11 @@
 * Improvement: Basic layout of page
 * Support: Support MongoDB 4.0, 4.2 and 4.4
 
+## 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
 

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

@@ -148,6 +148,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."
@@ -190,6 +191,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

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

@@ -166,6 +166,7 @@
       "upload": "アップロード",
       "discard": "アップロードしたデータを破棄する",
       "errors": {
+        "different_versions": "現在のGROWIとアップロードしたデータのバージョンが違います",
         "at_least_one": "コレクションが選択されていません",
         "page_and_revision": "'Pages' と 'Revisions' はセットでインポートする必要があります",
         "depends": "'{{condition}}' をインポートする場合は、'{{target}}' を一緒に選択する必要があります"
@@ -208,6 +209,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

@@ -160,6 +160,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."
@@ -201,7 +202,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

@@ -647,7 +647,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",

+ 10 - 1
src/client/js/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx

@@ -58,7 +58,16 @@ 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

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

+ 1 - 0
src/client/js/components/Admin/ManageExternalAccount.jsx

@@ -43,6 +43,7 @@ class ManageExternalAccount extends React.Component {
         totalItemsCount={adminExternalAccountsContainer.state.totalAccounts}
         pagingLimit={adminExternalAccountsContainer.state.pagingLimit}
         align="right"
+        size="sm"
       />
 
     );

+ 1 - 0
src/client/js/components/Admin/Security/ShareLinkSetting.jsx

@@ -98,6 +98,7 @@ class ShareLinkSetting extends React.Component {
           totalItemsCount={totalshareLinks}
           pagingLimit={shareLinksPagingLimit}
           align="right"
+          size="sm"
         />
       );
     }

+ 1 - 0
src/client/js/components/Admin/UserGroup/UserGroupPage.jsx

@@ -157,6 +157,7 @@ class UserGroupPage extends React.Component {
           changePage={this.handlePage}
           totalItemsCount={this.state.totalUserGroups}
           pagingLimit={this.state.pagingLimit}
+          size="sm"
         />
         <UserGroupDeleteModal
           userGroups={this.state.userGroups}

+ 1 - 0
src/client/js/components/Admin/UserGroupDetail/UserGroupPageList.jsx

@@ -64,6 +64,7 @@ class UserGroupPageList extends React.Component {
           changePage={this.handlePageChange}
           totalItemsCount={this.state.total}
           pagingLimit={this.state.pagingLimit}
+          size="sm"
         />
       </Fragment>
     );

+ 1 - 0
src/client/js/components/Admin/UserManagement.jsx

@@ -121,6 +121,7 @@ class UserManagement extends React.Component {
           totalItemsCount={adminUsersContainer.state.totalUsers}
           pagingLimit={adminUsersContainer.state.pagingLimit}
           align="right"
+          size="sm"
         />
       </div>
     );

+ 1 - 0
src/client/js/components/MyDraftList/MyDraftList.jsx

@@ -160,6 +160,7 @@ class MyDraftList extends React.Component {
               changePage={this.handlePage}
               totalItemsCount={this.state.totalDrafts}
               pagingLimit={this.state.pagingLimit}
+              size="sm"
             />
           </React.Fragment>
         ) }

+ 3 - 1
src/client/js/components/PaginationWrapper.jsx

@@ -173,7 +173,7 @@ class PaginationWrapper extends React.Component {
 
     return (
       <React.Fragment>
-        <Pagination size="sm" listClassName={this.getListClassName()}>{paginationItems}</Pagination>
+        <Pagination size={this.props.size} listClassName={this.getListClassName()}>{paginationItems}</Pagination>
       </React.Fragment>
     );
   }
@@ -191,9 +191,11 @@ PaginationWrapper.propTypes = {
   totalItemsCount: PropTypes.number.isRequired,
   pagingLimit: PropTypes.number.isRequired,
   align: PropTypes.string,
+  size: PropTypes.string,
 };
 PaginationWrapper.defaultProps = {
   align: 'left',
+  size: 'md',
 };
 
 export default withTranslation()(PaginationWrappered);

+ 1 - 0
src/client/js/components/RecentCreated/RecentCreated.jsx

@@ -78,6 +78,7 @@ class RecentCreated extends React.Component {
           changePage={this.handlePage}
           totalItemsCount={this.state.totalPages}
           pagingLimit={this.state.pagingLimit}
+          size="sm"
         />
       </div>
     );

+ 1 - 0
src/client/js/components/TagsList.jsx

@@ -80,6 +80,7 @@ class TagsList extends React.Component {
             changePage={this.handlePage}
             totalItemsCount={this.state.totalTags}
             pagingLimit={this.state.pagingLimit}
+            size="sm"
           />
         </div>
       </div>

+ 11 - 0
src/client/styles/scss/style-app.scss

@@ -69,11 +69,22 @@
   cursor: not-allowed;
 }
 
+.view-button {
+  min-width: 74px;
+}
+
+.edit-button {
+  min-width: 71px;
+}
 // TODO: reactify and replace with `grw-not-available-for-guest`
 .edit-button.edit-button-disabled {
   cursor: not-allowed;
 }
 
+.hackmd-button {
+  min-width: 97px;
+}
+
 .grw-not-available-for-guest {
   cursor: not-allowed !important;
 }

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

+ 13 - 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
@@ -463,7 +463,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);
+      }
+    }
   };
 
   /**

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