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

Merge pull request #2604 from weseek/imprv/create-apiv3-rename-page

Imprv/create apiv3 rename page
itizawa 5 лет назад
Родитель
Сommit
abfd2bae3a

+ 7 - 8
src/client/js/components/EmptyTrashModal.jsx

@@ -9,25 +9,24 @@ import { withTranslation } from 'react-i18next';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 import AppContainer from '../services/AppContainer';
-import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 const EmptyTrashModal = (props) => {
   const {
     t, isOpen, onClose, appContainer,
   } = props;
-  const [errorCode, setErrorCode] = useState(null);
-  const [errorMessage, setErrorMessage] = useState(null);
+
+  const [errs, setErrs] = useState(null);
 
   async function emptyTrash() {
-    setErrorCode(null);
-    setErrorMessage(null);
+    setErrs(null);
+
     try {
       await appContainer.apiv3Delete('/pages/empty-trash');
       window.location.reload();
     }
     catch (err) {
-      setErrorCode(err.code);
-      setErrorMessage(err.message);
+      setErrs(err);
     }
   }
 
@@ -44,7 +43,7 @@ const EmptyTrashModal = (props) => {
         { t('modal_empty.notice')}
       </ModalBody>
       <ModalFooter>
-        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} />
+        <ApiErrorMessageList errs={errs} />
         <button type="button" className="btn btn-danger" onClick={emptyButtonHandler}>
           <i className="icon-trash mr-2" aria-hidden="true"></i> Empty
         </button>

+ 6 - 8
src/client/js/components/PageDeleteModal.jsx

@@ -10,7 +10,7 @@ import { withTranslation } from 'react-i18next';
 import { withUnstatedContainers } from './UnstatedUtils';
 import PageContainer from '../services/PageContainer';
 
-import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 const deleteIconAndKey = {
   completely: {
@@ -32,8 +32,8 @@ const PageDeleteModal = (props) => {
   const [isDeleteRecursively, setIsDeleteRecursively] = useState(true);
   const [isDeleteCompletely, setIsDeleteCompletely] = useState(isDeleteCompletelyModal && isAbleToDeleteCompletely);
   const deleteMode = isDeleteCompletely ? 'completely' : 'temporary';
-  const [errorCode, setErrorCode] = useState(null);
-  const [errorMessage, setErrorMessage] = useState(null);
+
+  const [errs, setErrs] = useState(null);
 
   function changeIsDeleteRecursivelyHandler() {
     setIsDeleteRecursively(!isDeleteRecursively);
@@ -47,8 +47,7 @@ const PageDeleteModal = (props) => {
   }
 
   async function deletePage() {
-    setErrorCode(null);
-    setErrorMessage(null);
+    setErrs(null);
 
     try {
       const response = await pageContainer.deletePage(isDeleteRecursively, isDeleteCompletely);
@@ -56,8 +55,7 @@ const PageDeleteModal = (props) => {
       window.location.href = encodeURI(trashPagePath);
     }
     catch (err) {
-      setErrorCode(err.code);
-      setErrorMessage(err.message);
+      setErrs(err);
     }
   }
 
@@ -124,7 +122,7 @@ const PageDeleteModal = (props) => {
         {!isDeleteCompletelyModal && renderDeleteCompletelyForm()}
       </ModalBody>
       <ModalFooter>
-        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} />
+        <ApiErrorMessageList errs={errs} />
         <button type="button" className={`btn btn-${deleteIconAndKey[deleteMode].color}`} onClick={deleteButtonHandler}>
           <i className={`icon-${deleteIconAndKey[deleteMode].icon}`} aria-hidden="true"></i>
           { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }

+ 8 - 8
src/client/js/components/PageDuplicateModal.jsx

@@ -12,7 +12,7 @@ import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '../services/AppContainer';
 import PageContainer from '../services/PageContainer';
 import PagePathAutoComplete from './PagePathAutoComplete';
-import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 const PageDuplicateModal = (props) => {
   const { t, appContainer, pageContainer } = props;
@@ -23,8 +23,9 @@ const PageDuplicateModal = (props) => {
   const { crowi } = appContainer.config;
 
   const [pageNameInput, setPageNameInput] = useState(path);
-  const [errorCode, setErrorCode] = useState(null);
-  const [errorMessage, setErrorMessage] = useState(null);
+
+  const [errs, setErrs] = useState(null);
+
   const [subordinatedPaths, setSubordinatedPaths] = useState([]);
   const [getSubordinatedError, setGetSuborinatedError] = useState(null);
 
@@ -67,16 +68,15 @@ const PageDuplicateModal = (props) => {
   }, [props.isOpen, getSubordinatedList]);
 
   async function duplicate() {
+    setErrs(null);
+
     try {
-      setErrorCode(null);
-      setErrorMessage(null);
       const res = await appContainer.apiPost('/pages.duplicate', { page_id: pageId, new_path: pageNameInput });
       const { page } = res;
       window.location.href = encodeURI(`${page.path}?duplicated=${path}`);
     }
     catch (err) {
-      setErrorCode(err.code);
-      setErrorMessage(err.message);
+      setErrs(err);
     }
   }
 
@@ -141,7 +141,7 @@ const PageDuplicateModal = (props) => {
         </div>
       </ModalBody>
       <ModalFooter>
-        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} targetPath={pageNameInput} />
+        <ApiErrorMessageList errs={errs} targetPath={pageNameInput} />
         <button type="button" className="btn btn-primary" onClick={duplicate}>Duplicate page</button>
       </ModalFooter>
     </Modal>

+ 23 - 0
src/client/js/components/PageManagement/ApiErrorMessageList.jsx

@@ -0,0 +1,23 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import ApiErrorMessage from './ApiErrorMessage';
+import toArrayIfNot from '../../../../lib/util/toArrayIfNot';
+
+function ApiErrorMessageList(props) {
+  const errs = toArrayIfNot(props.errs);
+
+  return (
+    <>
+      {errs.map(err => <ApiErrorMessage key={err.code} errorCode={err.code} errorMessage={err.message} targetPath={props.targetPath} />)}
+    </>
+  );
+
+}
+
+ApiErrorMessageList.propTypes = {
+  errs:         PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
+  targetPath:   PropTypes.string,
+};
+
+export default ApiErrorMessageList;

+ 8 - 10
src/client/js/components/PageRenameModal.jsx

@@ -11,7 +11,7 @@ import { withUnstatedContainers } from './UnstatedUtils';
 
 import AppContainer from '../services/AppContainer';
 import PageContainer from '../services/PageContainer';
-import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 const PageRenameModal = (props) => {
   const {
@@ -23,8 +23,8 @@ const PageRenameModal = (props) => {
   const { crowi } = appContainer.config;
 
   const [pageNameInput, setPageNameInput] = useState(path);
-  const [errorCode, setErrorCode] = useState(null);
-  const [errorMessage, setErrorMessage] = useState(null);
+
+  const [errs, setErrs] = useState(null);
 
   const [isRenameRecursively, SetIsRenameRecursively] = useState(true);
   const [isRenameRedirect, SetIsRenameRedirect] = useState(false);
@@ -51,10 +51,9 @@ const PageRenameModal = (props) => {
   }
 
   async function rename() {
-    try {
-      setErrorCode(null);
-      setErrorMessage(null);
+    setErrs(null);
 
+    try {
       const response = await pageContainer.rename(
         pageNameInput,
         isRenameRecursively,
@@ -62,7 +61,7 @@ const PageRenameModal = (props) => {
         isRenameMetadata,
       );
 
-      const { page } = response;
+      const { page } = response.data;
       const url = new URL(page.path, 'https://dummy');
       url.searchParams.append('renamedFrom', path);
       if (isRenameRedirect) {
@@ -72,8 +71,7 @@ const PageRenameModal = (props) => {
       window.location.href = `${url.pathname}${url.search}`;
     }
     catch (err) {
-      setErrorCode(err.code);
-      setErrorMessage(err.message);
+      setErrs(err);
     }
   }
 
@@ -150,7 +148,7 @@ const PageRenameModal = (props) => {
         </div>
       </ModalBody>
       <ModalFooter>
-        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} targetPath={pageNameInput} />
+        <ApiErrorMessageList errs={errs} targetPath={pageNameInput} />
         <button type="button" className="btn btn-primary" onClick={rename}>Rename</button>
       </ModalFooter>
     </Modal>

+ 5 - 8
src/client/js/components/PutbackPageModal.jsx

@@ -11,15 +11,14 @@ import { withUnstatedContainers } from './UnstatedUtils';
 
 import PageContainer from '../services/PageContainer';
 
-import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 const PutBackPageModal = (props) => {
   const {
     t, isOpen, onClose, pageContainer, path,
   } = props;
 
-  const [errorCode, setErrorCode] = useState(null);
-  const [errorMessage, setErrorMessage] = useState(null);
+  const [errs, setErrs] = useState(null);
 
   const [isPutbackRecursively, setIsPutbackRecursively] = useState(true);
 
@@ -28,8 +27,7 @@ const PutBackPageModal = (props) => {
   }
 
   async function putbackPage() {
-    setErrorCode(null);
-    setErrorMessage(null);
+    setErrs(null);
 
     try {
       const response = await pageContainer.revertRemove(isPutbackRecursively);
@@ -37,8 +35,7 @@ const PutBackPageModal = (props) => {
       window.location.href = encodeURI(putbackPagePath);
     }
     catch (err) {
-      setErrorCode(err.code);
-      setErrorMessage(err.message);
+      setErrs(err);
     }
   }
 
@@ -73,7 +70,7 @@ const PutBackPageModal = (props) => {
         </div>
       </ModalBody>
       <ModalFooter>
-        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} />
+        <ApiErrorMessageList errs={errs} />
         <button type="button" className="btn btn-info" onClick={putbackPageButtonHandler}>
           <i className="icon-action-undo mr-2" aria-hidden="true"></i> { t('Put Back') }
         </button>

+ 9 - 11
src/client/js/services/PageContainer.js

@@ -379,19 +379,17 @@ export default class PageContainer extends Container {
     });
   }
 
-  rename(pageNameInput, isRenameRecursively, isRenameRedirect, isRenameMetadata) {
+  rename(newPagePath, isRecursively, isRenameRedirect, isRemainMetadata) {
     const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
-    const isRecursively = isRenameRecursively ? true : null;
-    const isRedirect = isRenameRedirect ? true : null;
-    const isRemain = isRenameMetadata ? true : null;
+    const { pageId, revisionId } = this.state;
 
-    return this.appContainer.apiPost('/pages.rename', {
-      recursively: isRecursively,
-      page_id: this.state.pageId,
-      revision_id: this.state.revisionId,
-      new_path: pageNameInput,
-      create_redirect: isRedirect,
-      remain_metadata: isRemain,
+    return this.appContainer.apiv3Put('/pages/rename', {
+      revisionId,
+      pageId,
+      isRecursively,
+      isRenameRedirect,
+      isRemainMetadata,
+      newPagePath,
       socketClientId: socketIoContainer.getSocketClientId(),
     });
   }

+ 71 - 10
src/server/routes/apiv3/pages.js

@@ -6,6 +6,7 @@ const express = require('express');
 const pathUtils = require('growi-commons').pathUtils;
 
 const { body } = require('express-validator/check');
+const ErrorV3 = require('../../models/vo/error-apiv3');
 
 const router = express.Router();
 
@@ -179,8 +180,7 @@ module.exports = (crowi) => {
     // check page existence
     const isExist = await Page.count({ path }) > 0;
     if (isExist) {
-      res.code = 'page_exists';
-      return res.apiv3Err('Page exists', 409);
+      return res.apiv3Err(new ErrorV3('Failed to post page', 'page_exists'), 500);
     }
 
     const options = { socketClientId };
@@ -266,12 +266,77 @@ module.exports = (crowi) => {
       return res.apiv3(result);
     }
     catch (err) {
-      res.code = 'unknown';
       logger.error('Failed to get recent pages', err);
-      return res.apiv3Err(err, 500);
+      return res.apiv3Err(new ErrorV3('Failed to get recent pages', 'unknown'), 500);
     }
   });
 
+  // TODO write swagger(GW-3430) and add validation (GW-3429)
+  router.put('/rename', accessTokenParser, loginRequiredStrictly, csrf, async(req, res) => {
+    const { pageId, isRecursively, revisionId } = req.body;
+
+    let newPagePath = pathUtils.normalizePath(req.body.newPagePath);
+
+    const options = {
+      createRedirectPage: req.body.isRenameRedirect,
+      updateMetadata: req.body.isRemainMetadata,
+      socketClientId: +req.body.socketClientId || undefined,
+    };
+
+    if (!Page.isCreatableName(newPagePath)) {
+      return res.apiv3Err(new ErrorV3(`Could not use the path '${newPagePath})'`, 'invalid_path'), 409);
+    }
+
+    // check whether path starts slash
+    newPagePath = pathUtils.addHeadingSlash(newPagePath);
+
+    const isExist = await Page.count({ path: newPagePath }) > 0;
+    if (isExist) {
+      // if page found, cannot cannot rename to that path
+      return res.apiv3Err(new ErrorV3(`${newPagePath} already exists`, 'already_exists'), 409);
+    }
+
+    let page;
+
+    try {
+      page = await Page.findByIdAndViewer(pageId, req.user);
+
+      if (page == null) {
+        return res.apiv3Err(new ErrorV3(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'), 401);
+      }
+
+      if (!page.isUpdatable(revisionId)) {
+        return res.apiv3Err(new ErrorV3('Someone could update this page, so couldn\'t delete.', 'notfound_or_forbidden'), 409);
+      }
+
+      if (isRecursively) {
+        page = await Page.renameRecursively(page, newPagePath, req.user, options);
+      }
+      else {
+        page = await Page.rename(page, newPagePath, req.user, options);
+      }
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
+    }
+
+    const result = { page: pageService.serializeToObj(page) };
+
+    try {
+      // global notification
+      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_MOVE, page, req.user, {
+        oldPath: req.body.path,
+      });
+    }
+    catch (err) {
+      logger.error('Move notification failed', err);
+    }
+
+    return res.apiv3(result);
+  });
+
+
   /**
   * @swagger
   *
@@ -289,9 +354,7 @@ module.exports = (crowi) => {
       return res.apiv3({ pages });
     }
     catch (err) {
-      res.code = 'unknown';
-      logger.error('Failed to delete trash pages', err);
-      return res.apiv3Err(err, 500);
+      return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
     }
   });
 
@@ -308,9 +371,7 @@ module.exports = (crowi) => {
       return res.apiv3({ resultPaths });
     }
     catch (err) {
-      res.code = 'unknown';
-      logger.error('Failed to find the path', err);
-      return res.apiv3Err(err, 500);
+      return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
     }
 
   });

+ 1 - 0
src/server/routes/page.js

@@ -1352,6 +1352,7 @@ module.exports = function(crowi, app) {
    * @apiParam {String} new_path New path name.
    * @apiParam {Bool} create_redirect
    */
+  // TODO remove after GW-3429 and GW-3430
   api.rename = async function(req, res) {
     const pageId = req.body.page_id;
     const previousRevision = req.body.revision_id || null;