Kaynağa Gözat

Merge pull request #5498 from weseek/imprv/modal-for-renaming-and-duplicating

imprv: Modal for renaming and duplicating 2
Yuki Takei 4 yıl önce
ebeveyn
işleme
cc134dc0f9

+ 0 - 54
packages/app/src/components/ComparePathsTable.jsx

@@ -1,54 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { withTranslation } from 'react-i18next';
-import { pagePathUtils } from '@growi/core';
-
-
-const { convertToNewAffiliationPath } = pagePathUtils;
-
-function ComparePathsTable(props) {
-  const {
-    path, subordinatedPages, newPagePath, t,
-  } = props;
-
-  return (
-    <table className="table table-bordered grw-compare-paths-table">
-      <thead>
-        <tr className="d-flex">
-          <th className="w-50">{t('original_path')}</th>
-          <th className="w-50">{t('new_path')}</th>
-        </tr>
-      </thead>
-      <tbody className="overflow-auto d-block">
-        {subordinatedPages.map((subordinatedPage) => {
-          const convertedPath = convertToNewAffiliationPath(path, newPagePath, subordinatedPage.path);
-          return (
-            <tr key={subordinatedPage._id} className="d-flex">
-              <td className="text-break w-50">
-                <a href={subordinatedPage.path}>
-                  {subordinatedPage.path}
-                </a>
-              </td>
-              <td className="text-break w-50">
-                {convertedPath}
-              </td>
-            </tr>
-          );
-        })}
-      </tbody>
-    </table>
-  );
-}
-
-
-ComparePathsTable.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-
-  path: PropTypes.string.isRequired,
-  subordinatedPages: PropTypes.array.isRequired,
-  newPagePath: PropTypes.string.isRequired,
-};
-
-
-export default withTranslation()(ComparePathsTable);

+ 86 - 74
packages/app/src/components/PageDuplicateModal.jsx → packages/app/src/components/PageDuplicateModal.tsx

@@ -1,57 +1,68 @@
 import React, {
 import React, {
   useState, useEffect, useCallback, useMemo,
   useState, useEffect, useCallback, useMemo,
 } from 'react';
 } from 'react';
-import PropTypes from 'prop-types';
 
 
 import {
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
-import { withUnstatedContainers } from './UnstatedUtils';
+
+import { apiv3Get, apiv3Post } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
-import { usePageDuplicateModal } from '~/stores/modal';
 
 
-import AppContainer from '~/client/services/AppContainer';
-import { apiv3Get } from '~/client/util/apiv3-client';
+import { usePageDuplicateModal } from '~/stores/modal';
+import { useIsSearchServiceReachable, useSiteUrl } from '~/stores/context';
 
 
 import PagePathAutoComplete from './PagePathAutoComplete';
 import PagePathAutoComplete from './PagePathAutoComplete';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
-import ComparePathsTable from './ComparePathsTable';
 import DuplicatePathsTable from './DuplicatedPathsTable';
 import DuplicatePathsTable from './DuplicatedPathsTable';
 
 
-const LIMIT_FOR_LIST = 10;
 
 
-const PageDuplicateModal = (props) => {
-  const {
-    t, appContainer,
-  } = props;
+const PageDuplicateModal = (): JSX.Element => {
+  const { t } = useTranslation();
+
+  const { data: siteUrl } = useSiteUrl();
+  const { data: isReachable } = useIsSearchServiceReachable();
 
 
-  const config = appContainer.getConfig();
-  const isReachable = config.isSearchServiceReachable;
-  const { crowi } = appContainer.config;
   const { data: duplicateModalData, close: closeDuplicateModal } = usePageDuplicateModal();
   const { data: duplicateModalData, close: closeDuplicateModal } = usePageDuplicateModal();
 
 
-  const { isOpened, page } = duplicateModalData;
-  const { pageId, path } = page;
+  const isOpened = duplicateModalData?.isOpened ?? false;
+  const page = duplicateModalData?.page;
 
 
-  const [pageNameInput, setPageNameInput] = useState(path);
+  const [pageNameInput, setPageNameInput] = useState('');
 
 
   const [errs, setErrs] = useState(null);
   const [errs, setErrs] = useState(null);
 
 
   const [subordinatedPages, setSubordinatedPages] = useState([]);
   const [subordinatedPages, setSubordinatedPages] = useState([]);
+  const [existingPaths, setExistingPaths] = useState<string[]>([]);
   const [isDuplicateRecursively, setIsDuplicateRecursively] = useState(true);
   const [isDuplicateRecursively, setIsDuplicateRecursively] = useState(true);
   const [isDuplicateRecursivelyWithoutExistPath, setIsDuplicateRecursivelyWithoutExistPath] = useState(true);
   const [isDuplicateRecursivelyWithoutExistPath, setIsDuplicateRecursivelyWithoutExistPath] = useState(true);
-  const [existingPaths, setExistingPaths] = useState([]);
 
 
-  const checkExistPaths = useCallback(async(newParentPath) => {
+  const updateSubordinatedList = useCallback(async() => {
     if (page == null) {
     if (page == null) {
       return;
       return;
     }
     }
 
 
+    const { path } = page;
     try {
     try {
-      const res = await apiv3Get('/page/exist-paths', { fromPath: path, toPath: newParentPath });
+      const res = await apiv3Get('/pages/subordinated-list', { path });
+      setSubordinatedPages(res.data.subordinatedPages);
+    }
+    catch (err) {
+      setErrs(err);
+      toastError(t('modal_duplicate.label.Failed to get subordinated pages'));
+    }
+  }, [page, t]);
+
+  const checkExistPaths = useCallback(async(fromPath, toPath) => {
+    if (page == null) {
+      return;
+    }
+
+    try {
+      const res = await apiv3Get<{ existPaths: string[] }>('/page/exist-paths', { fromPath, toPath });
       const { existPaths } = res.data;
       const { existPaths } = res.data;
       setExistingPaths(existPaths);
       setExistingPaths(existPaths);
     }
     }
@@ -59,24 +70,17 @@ const PageDuplicateModal = (props) => {
       setErrs(err);
       setErrs(err);
       toastError(t('modal_rename.label.Failed to get exist path'));
       toastError(t('modal_rename.label.Failed to get exist path'));
     }
     }
-  }, [page, path, t]);
+  }, [page, t]);
 
 
   const checkExistPathsDebounce = useMemo(() => {
   const checkExistPathsDebounce = useMemo(() => {
     return debounce(1000, checkExistPaths);
     return debounce(1000, checkExistPaths);
   }, [checkExistPaths]);
   }, [checkExistPaths]);
 
 
   useEffect(() => {
   useEffect(() => {
-    if (pageId != null && path != null && pageNameInput !== path) {
-      checkExistPathsDebounce(pageNameInput);
-    }
-  }, [pageNameInput, subordinatedPages, checkExistPathsDebounce, pageId, path]);
-
-
-  useEffect(() => {
-    if (pageId != null && path != null && pageNameInput !== path) {
-      checkExistPathsDebounce(pageNameInput);
+    if (page != null && pageNameInput !== page.path) {
+      checkExistPathsDebounce(page.path, pageNameInput);
     }
     }
-  }, [pageNameInput, subordinatedPages, path, pageId, checkExistPathsDebounce]);
+  }, [pageNameInput, subordinatedPages, checkExistPathsDebounce, page]);
 
 
   /**
   /**
    * change pageNameInput for PagePathAutoComplete
    * change pageNameInput for PagePathAutoComplete
@@ -100,34 +104,24 @@ const PageDuplicateModal = (props) => {
     setIsDuplicateRecursively(!isDuplicateRecursively);
     setIsDuplicateRecursively(!isDuplicateRecursively);
   }
   }
 
 
-  const getSubordinatedList = useCallback(async() => {
-    try {
-      const res = await appContainer.apiv3Get('/pages/subordinated-list', { path, limit: LIMIT_FOR_LIST });
-      setSubordinatedPages(res.data.subordinatedPages);
-    }
-    catch (err) {
-      setErrs(err);
-      toastError(t('modal_duplicate.label.Failed to get subordinated pages'));
-    }
-  }, [appContainer, path, t]);
-
   useEffect(() => {
   useEffect(() => {
-    if (isOpened) {
-      getSubordinatedList();
-      setPageNameInput(path);
+    if (page != null && isOpened) {
+      updateSubordinatedList();
+      setPageNameInput(page.path);
     }
     }
-  }, [isOpened, getSubordinatedList, path]);
+  }, [isOpened, page, updateSubordinatedList]);
 
 
-  function changeIsDuplicateRecursivelyWithoutExistPathHandler() {
-    setIsDuplicateRecursivelyWithoutExistPath(!isDuplicateRecursivelyWithoutExistPath);
-  }
+  const duplicate = useCallback(async() => {
+    if (page == null) {
+      return;
+    }
 
 
-  async function duplicate() {
     setErrs(null);
     setErrs(null);
 
 
+    const { pageId, path } = page;
     try {
     try {
-      const { data } = await appContainer.apiv3Post('/pages/duplicate', { pageId, pageNameInput, isRecursively: isDuplicateRecursively });
-      const onDuplicated = duplicateModalData.opts?.onDuplicated;
+      const { data } = await apiv3Post('/pages/duplicate', { pageId, pageNameInput, isRecursively: isDuplicateRecursively });
+      const onDuplicated = duplicateModalData?.opts?.onDuplicated;
       const fromPath = path;
       const fromPath = path;
       const toPath = data.page.path;
       const toPath = data.page.path;
 
 
@@ -139,12 +133,35 @@ const PageDuplicateModal = (props) => {
     catch (err) {
     catch (err) {
       setErrs(err);
       setErrs(err);
     }
     }
-  }
+  }, [closeDuplicateModal, duplicateModalData?.opts?.onDuplicated, isDuplicateRecursively, page, pageNameInput]);
 
 
-  function ppacSubmitHandler() {
-    duplicate();
+  useEffect(() => {
+    if (isOpened) {
+      return;
+    }
+
+    // reset states after the modal closed
+    setTimeout(() => {
+      setPageNameInput('');
+      setErrs(null);
+      setSubordinatedPages([]);
+      setExistingPaths([]);
+      setIsDuplicateRecursively(true);
+      setIsDuplicateRecursivelyWithoutExistPath(false);
+    }, 1000);
+
+  }, [isOpened]);
+
+  if (page == null) {
+    return <></>;
   }
   }
 
 
+  const { path } = page;
+  const isTargetPageDuplicate = existingPaths.includes(pageNameInput);
+
+  const submitButtonEnabled = existingPaths.length === 0
+    || (isDuplicateRecursively && isDuplicateRecursivelyWithoutExistPath);
+
   return (
   return (
     <Modal size="lg" isOpen={isOpened} toggle={closeDuplicateModal} className="grw-duplicate-page" autoFocus={false}>
     <Modal size="lg" isOpen={isOpened} toggle={closeDuplicateModal} className="grw-duplicate-page" autoFocus={false}>
       <ModalHeader tag="h4" toggle={closeDuplicateModal} className="bg-primary text-light">
       <ModalHeader tag="h4" toggle={closeDuplicateModal} className="bg-primary text-light">
@@ -158,14 +175,14 @@ const PageDuplicateModal = (props) => {
           <label htmlFor="duplicatePageName">{ t('modal_duplicate.label.New page name') }</label><br />
           <label htmlFor="duplicatePageName">{ t('modal_duplicate.label.New page name') }</label><br />
           <div className="input-group">
           <div className="input-group">
             <div className="input-group-prepend">
             <div className="input-group-prepend">
-              <span className="input-group-text">{crowi.url}</span>
+              <span className="input-group-text">{siteUrl}</span>
             </div>
             </div>
             <div className="flex-fill">
             <div className="flex-fill">
               {isReachable
               {isReachable
                 ? (
                 ? (
                   <PagePathAutoComplete
                   <PagePathAutoComplete
                     initializedPath={path}
                     initializedPath={path}
-                    onSubmit={ppacSubmitHandler}
+                    onSubmit={duplicate}
                     onInputChange={ppacInputChangeHandler}
                     onInputChange={ppacInputChangeHandler}
                     autoFocus
                     autoFocus
                   />
                   />
@@ -182,6 +199,11 @@ const PageDuplicateModal = (props) => {
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
+
+        { isTargetPageDuplicate && (
+          <p className="text-danger">Error: Target path is duplicated.</p>
+        ) }
+
         <div className="custom-control custom-checkbox custom-checkbox-warning mb-3">
         <div className="custom-control custom-checkbox custom-checkbox-warning mb-3">
           <input
           <input
             className="custom-control-input"
             className="custom-control-input"
@@ -205,7 +227,7 @@ const PageDuplicateModal = (props) => {
                   id="cbDuplicatewithoutExistRecursively"
                   id="cbDuplicatewithoutExistRecursively"
                   type="checkbox"
                   type="checkbox"
                   checked={isDuplicateRecursivelyWithoutExistPath}
                   checked={isDuplicateRecursivelyWithoutExistPath}
-                  onChange={changeIsDuplicateRecursivelyWithoutExistPathHandler}
+                  onChange={() => setIsDuplicateRecursivelyWithoutExistPath(!isDuplicateRecursivelyWithoutExistPath)}
                 />
                 />
                 <label className="custom-control-label" htmlFor="cbDuplicatewithoutExistRecursively">
                 <label className="custom-control-label" htmlFor="cbDuplicatewithoutExistRecursively">
                   { t('modal_duplicate.label.Duplicate without exist path') }
                   { t('modal_duplicate.label.Duplicate without exist path') }
@@ -214,8 +236,9 @@ const PageDuplicateModal = (props) => {
             )}
             )}
           </div>
           </div>
           <div>
           <div>
-            {isDuplicateRecursively && path != null && <ComparePathsTable path={path} subordinatedPages={subordinatedPages} newPagePath={pageNameInput} />}
-            {isDuplicateRecursively && existingPaths.length !== 0 && <DuplicatePathsTable existingPaths={existingPaths} oldPagePath={pageNameInput} />}
+            {isDuplicateRecursively && existingPaths.length !== 0 && (
+              <DuplicatePathsTable existingPaths={existingPaths} fromPath={path} toPath={pageNameInput} />
+            ) }
           </div>
           </div>
         </div>
         </div>
 
 
@@ -226,7 +249,7 @@ const PageDuplicateModal = (props) => {
           type="button"
           type="button"
           className="btn btn-primary"
           className="btn btn-primary"
           onClick={duplicate}
           onClick={duplicate}
-          disabled={(isDuplicateRecursively && !isDuplicateRecursivelyWithoutExistPath && existingPaths.length !== 0)}
+          disabled={!submitButtonEnabled}
         >
         >
           { t('modal_duplicate.label.Duplicate page') }
           { t('modal_duplicate.label.Duplicate page') }
         </button>
         </button>
@@ -236,15 +259,4 @@ const PageDuplicateModal = (props) => {
 };
 };
 
 
 
 
-/**
- * Wrapper component for using unstated
- */
-const PageDuplicateModallWrapper = withUnstatedContainers(PageDuplicateModal, [AppContainer]);
-
-
-PageDuplicateModal.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
-
-export default withTranslation()(PageDuplicateModallWrapper);
+export default PageDuplicateModal;

+ 5 - 1
packages/app/src/components/PageRenameModal.tsx

@@ -176,6 +176,10 @@ const PageRenameModal = (): JSX.Element => {
   const { path } = page.data;
   const { path } = page.data;
   const isTargetPageDuplicate = existingPaths.includes(pageNameInput);
   const isTargetPageDuplicate = existingPaths.includes(pageNameInput);
 
 
+  const submitButtonDisabled = isV5Compatible(page.meta)
+    ? existingPaths.length !== 0 // v5 data
+    : !isRenameRecursively; // v4 data
+
   return (
   return (
     <Modal size="lg" isOpen={isOpened} toggle={closeRenameModal} autoFocus={false}>
     <Modal size="lg" isOpen={isOpened} toggle={closeRenameModal} autoFocus={false}>
       <ModalHeader tag="h4" toggle={closeRenameModal} className="bg-primary text-light">
       <ModalHeader tag="h4" toggle={closeRenameModal} className="bg-primary text-light">
@@ -290,7 +294,7 @@ const PageRenameModal = (): JSX.Element => {
           type="button"
           type="button"
           className="btn btn-primary"
           className="btn btn-primary"
           onClick={rename}
           onClick={rename}
-          disabled={(!isRenameRecursively && existingPaths.length !== 0)}
+          disabled={submitButtonDisabled}
         >Rename
         >Rename
         </button>
         </button>
       </ModalFooter>
       </ModalFooter>

+ 2 - 2
packages/app/src/stores/modal.tsx

@@ -94,7 +94,7 @@ type DuplicateModalStatusUtils = {
 }
 }
 
 
 export const usePageDuplicateModal = (status?: DuplicateModalStatus): SWRResponse<DuplicateModalStatus, Error> & DuplicateModalStatusUtils => {
 export const usePageDuplicateModal = (status?: DuplicateModalStatus): SWRResponse<DuplicateModalStatus, Error> & DuplicateModalStatusUtils => {
-  const initialData: DuplicateModalStatus = { isOpened: false, page: { pageId: '', path: '/' } };
+  const initialData: DuplicateModalStatus = { isOpened: false };
   const swrResponse = useStaticSWR<DuplicateModalStatus, Error>('duplicateModalStatus', status, { fallbackData: initialData });
   const swrResponse = useStaticSWR<DuplicateModalStatus, Error>('duplicateModalStatus', status, { fallbackData: initialData });
 
 
   return {
   return {
@@ -103,7 +103,7 @@ export const usePageDuplicateModal = (status?: DuplicateModalStatus): SWRRespons
         page?: IPageForPageDuplicateModal,
         page?: IPageForPageDuplicateModal,
         opts?: IDuplicateModalOption,
         opts?: IDuplicateModalOption,
     ) => swrResponse.mutate({ isOpened: true, page, opts }),
     ) => swrResponse.mutate({ isOpened: true, page, opts }),
-    close: () => swrResponse.mutate({ isOpened: false, page: { pageId: '', path: '/' } }),
+    close: () => swrResponse.mutate({ isOpened: false }),
   };
   };
 };
 };
 
 

+ 0 - 5
packages/app/src/styles/molecules/compare-paths-table.scss

@@ -1,5 +0,0 @@
-.grw-compare-paths-table {
-  tbody {
-    max-height: 200px;
-  }
-}

+ 0 - 1
packages/app/src/styles/style-app.scss

@@ -29,7 +29,6 @@
 @import 'molecules/page-editor-mode-manager';
 @import 'molecules/page-editor-mode-manager';
 @import 'molecules/slack-notification';
 @import 'molecules/slack-notification';
 @import 'molecules/duplicated-paths-table.scss';
 @import 'molecules/duplicated-paths-table.scss';
-@import 'molecules/compare-paths-table.scss';
 
 
 // growi component
 // growi component
 @import 'admin';
 @import 'admin';