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

Merge pull request #2918 from weseek/feat/gw-3524

feat/gw-3524
itizawa 5 лет назад
Родитель
Сommit
8729ab82af

+ 2 - 0
resource/locales/en_US/translation.json

@@ -145,6 +145,7 @@
   "Recent Changes": "Recent Changes",
   "original_path":"Original path",
   "new_path":"New path",
+  "duplicated_path":"duplicated_path",
   "personal_dropdown": {
     "home": "Home",
     "settings": "Settings",
@@ -307,6 +308,7 @@
       "Move/Rename page": "Move/Rename page",
       "New page name": "New page name",
       "Fail to get subordinated pages": "Fail to get subordinated pages",
+      "Fail to get exist path": "Fail to get exist path",
       "Current page name": "Current page name",
       "Recursively": "Recursively",
       "Do not update metadata": "Do not update metadata",

+ 2 - 0
resource/locales/ja_JP/translation.json

@@ -148,6 +148,7 @@
   "Recent Changes": "最新の変更",
   "original_path":"元のパス",
   "new_path":"新しいパス",
+  "duplicated_path":"重複したパス",
   "personal_dropdown": {
     "home": "ホーム",
     "settings": "設定",
@@ -309,6 +310,7 @@
       "Move/Rename page": "ページを移動/名前変更する",
       "New page name": "移動先のページ名",
       "Fail to get subordinated pages": "配下ページの取得に失敗しました",
+      "Fail to get exist path": "存在するパスの取得に失敗しました",
       "Current page name": "現在のページ名",
       "Recursively": "再帰的に移動/名前変更",
       "Do not update metadata": "メタデータを更新しない",

+ 2 - 0
resource/locales/zh_CN/translation.json

@@ -153,6 +153,7 @@
   "Recent Changes": "最新修改",
   "original_path":"Original path",
   "new_path":"New path",
+  "duplicated_path":"duplicated_path",
 	"form_validation": {
 		"error_message": "有些值不正确",
 		"required": "%s 是必需的",
@@ -287,6 +288,7 @@
 			"Move/Rename page": "页面 移动/重命名",
       "New page name": "新建页面名称",
       "Fail to get subordinated pages": "Fail to get subordinated pages",
+      "Fail to get exist path": "Fail to get exist path",
 			"Current page name": "当前页面名称",
 			"Recursively": "递归地",
 			"Do not update metadata": "不更新元数据",

+ 59 - 0
src/client/js/components/DuplicatedPathsTable.jsx

@@ -0,0 +1,59 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+import { withUnstatedContainers } from './UnstatedUtils';
+
+import PageContainer from '../services/PageContainer';
+import { convertToNewAffiliationPath } from '../../../lib/util/path-utils';
+
+function DuplicatedPathsTable(props) {
+  const {
+    pageContainer, oldPagePath, existingPaths, t,
+  } = props;
+  const { path } = pageContainer.state;
+
+  return (
+    <table className="table table-bordered grw-duplicated-page-table">
+      <thead>
+        <tr className="d-flex">
+          <th className="w-50">{t('original_path')}</th>
+          <th className="w-50 text-danger">{t('duplicated_path')}</th>
+        </tr>
+      </thead>
+      <tbody className="overflow-auto d-block">
+        {existingPaths.map((existPath) => {
+          const convertedPath = convertToNewAffiliationPath(oldPagePath, path, existPath);
+          return (
+            <tr key={existPath} className="d-flex">
+              <td className="text-break w-50">
+                <a href={convertedPath}>
+                  {convertedPath}
+                </a>
+              </td>
+              <td className="text-break text-danger w-50">
+                {existPath}
+              </td>
+            </tr>
+          );
+        })}
+      </tbody>
+    </table>
+  );
+}
+
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageDuplicateModallWrapper = withUnstatedContainers(DuplicatedPathsTable, [PageContainer]);
+
+DuplicatedPathsTable.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+  existingPaths: PropTypes.array.isRequired,
+  oldPagePath: PropTypes.string.isRequired,
+};
+
+
+export default withTranslation()(PageDuplicateModallWrapper);

+ 32 - 2
src/client/js/components/PageRenameModal.jsx

@@ -1,4 +1,6 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React, {
+  useState, useEffect, useCallback,
+} from 'react';
 import PropTypes from 'prop-types';
 
 import {
@@ -7,6 +9,7 @@ import {
 
 import { withTranslation } from 'react-i18next';
 
+import { debounce } from 'throttle-debounce';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { toastError } from '../util/apiNotification';
 
@@ -14,6 +17,8 @@ import AppContainer from '../services/AppContainer';
 import PageContainer from '../services/PageContainer';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ComparePathsTable from './ComparePathsTable';
+import DuplicatedPathsTable from './DuplicatedPathsTable';
+
 
 const PageRenameModal = (props) => {
   const {
@@ -29,13 +34,13 @@ const PageRenameModal = (props) => {
   const [errs, setErrs] = useState(null);
 
   const [subordinatedPages, setSubordinatedPages] = useState([]);
+  const [existingPaths, setExistingPaths] = useState([]);
   const [isRenameRecursively, SetIsRenameRecursively] = useState(true);
   const [isRenameRedirect, SetIsRenameRedirect] = useState(false);
   const [isRenameMetadata, SetIsRenameMetadata] = useState(false);
   const [subordinatedError] = useState(null);
   const [isDuplicateRecursivelyWithoutExistPath, setIsDuplicateRecursivelyWithoutExistPath] = useState(true);
 
-
   function changeIsRenameRecursivelyHandler() {
     SetIsRenameRecursively(!isRenameRecursively);
   }
@@ -70,6 +75,30 @@ const PageRenameModal = (props) => {
     }
   }, [props.isOpen, updateSubordinatedList]);
 
+
+  const checkExistPaths = async(newParentPath) => {
+    try {
+      const res = await appContainer.apiv3Get('/page/exist-paths', { fromPath: path, toPath: newParentPath });
+      const { existPaths } = res.data;
+      setExistingPaths(existPaths);
+    }
+    catch (err) {
+      setErrs(err);
+      toastError(t('modal_rename.label.Fail to get exist path'));
+    }
+  };
+
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+  const checkExistPathsDebounce = useCallback(
+    debounce(1000, checkExistPaths), [],
+  );
+
+  useEffect(() => {
+    if (pageNameInput !== path) {
+      checkExistPathsDebounce(pageNameInput, subordinatedPages);
+    }
+  }, [pageNameInput, subordinatedPages, path, checkExistPathsDebounce]);
+
   /**
    * change pageNameInput
    * @param {string} value
@@ -161,6 +190,7 @@ const PageRenameModal = (props) => {
             </label>
           </div>
           {isRenameRecursively && <ComparePathsTable subordinatedPages={subordinatedPages} newPagePath={pageNameInput} />}
+          {isRenameRecursively && existingPaths.length !== 0 && <DuplicatedPathsTable existingPaths={existingPaths} oldPagePath={pageNameInput} />}
         </div>
 
         <div className="custom-control custom-checkbox custom-checkbox-success">

+ 18 - 0
src/server/models/page.js

@@ -258,6 +258,17 @@ class PageQueryBuilder {
     return this;
   }
 
+  addConditionToListByPathsArray(paths) {
+    this.query = this.query
+      .and({
+        path: {
+          $in: paths,
+        },
+      });
+
+    return this;
+  }
+
   populateDataToList(userPublicFields) {
     this.query = this.query
       .populate({
@@ -1291,6 +1302,13 @@ module.exports = function(crowi) {
     return targetPage;
   };
 
+  pageSchema.statics.findListByPathsArray = async function(paths) {
+    const queryBuilder = new PageQueryBuilder(this.find());
+    queryBuilder.addConditionToListByPathsArray(paths);
+
+    return await queryBuilder.query.exec();
+  };
+
   // TODO: transplant to service/page.js because page deletion affects various models data
   pageSchema.statics.handlePrivatePagesForDeletedGroup = async function(deletedGroup, action, transferToUserGroupId) {
     const Page = mongoose.model('Page');

+ 15 - 13
src/server/routes/apiv3/page.js

@@ -7,6 +7,7 @@ const { body, query } = require('express-validator');
 
 const router = express.Router();
 
+const { convertToNewAffiliationPath } = require('../../../lib/util/path-utils');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 /**
@@ -139,7 +140,8 @@ module.exports = (crowi) => {
       body('hierarchyValue').isNumeric(),
     ],
     exist: [
-      query('newParentPath').isString(),
+      query('fromPath').isString(),
+      query('toPath').isString(),
     ],
   };
 
@@ -266,14 +268,14 @@ module.exports = (crowi) => {
    *        description: Get already exist paths
    *        operationId: getAlreadyExistPaths
    *        parameters:
-   *          - name: newParentPath
+   *          - name: fromPath
    *            in: query
-   *            description: New parent path of search
+   *            description: old parent path
    *            schema:
    *              type: string
-   *          - name: toPaths
+   *          - name: toPath
    *            in: query
-   *            description: Paths to compare with DB
+   *            description: new parent path
    *            schema:
    *              type: string
    *        responses:
@@ -290,18 +292,18 @@ module.exports = (crowi) => {
    *            description: Internal server error.
    */
   router.get('/exist-paths', loginRequired, validator.exist, apiV3FormValidator, async(req, res) => {
-    const { newParentPath, toPaths } = req.query;
+    const { fromPath, toPath } = req.query;
 
     try {
-      const { pages } = await Page.findListByStartWith(newParentPath, req.user);
+      const fromPage = await Page.findByPath(fromPath);
+      const fromPageDescendants = await Page.findManageableListWithDescendants(fromPage, req.user);
 
-      const duplicationPaths = pages.map((page) => {
-        if (toPaths.includes(page.path)) {
-          return page.path;
-        }
-        return null;
+      const toPathDescendantsArray = fromPageDescendants.map((subordinatedPage) => {
+        return convertToNewAffiliationPath(fromPath, toPath, subordinatedPage.path);
       });
-      const existPaths = duplicationPaths.filter(path => path != null);
+
+      const existPages = await Page.findListByPathsArray(toPathDescendantsArray);
+      const existPaths = existPages.map(page => page.path);
 
       return res.apiv3({ existPaths });