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

Merge branch 'master' into imprv/88175-show-delete-modal-with-swr

* master: (60 commits)
  improve null check
  fix lint errors
  move swr related to modal to stores/modal file
  refactoring PageDeleteModal
  refactoring pageCreateeModal
  add title comments for each modal
  refactoring for pageDuplicateModal
  refactoring for PageRenameModal
  add white space
  refactor methods
  move react code back into RFC
  add type
  rename usePagePresentationModalStatus to usePagePresentationModal and use it instead of usePagePresentationModalOpened
  moce codes otu of FC
  refs #88095: fix pagetitle links to parmanent url - If the page path URL is not available due to duplicate page names, use persistent links to process pages with the same name.
  Added limitation
  rename
  imprv
  fix fb
  fix cond
  ...
Mao 4 лет назад
Родитель
Сommit
bab4f9d797
32 измененных файлов с 400 добавлено и 338 удалено
  1. 2 0
      packages/app/src/client/base.jsx
  2. 5 0
      packages/app/src/client/services/PageContainer.js
  3. 1 1
      packages/app/src/components/DescendantsPageListModal.tsx
  4. 2 2
      packages/app/src/components/Fab.jsx
  5. 2 2
      packages/app/src/components/Hotkeys/Subscribers/CreatePage.jsx
  6. 1 1
      packages/app/src/components/IdenticalPathPage.tsx
  7. 17 9
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  8. 6 4
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  9. 3 2
      packages/app/src/components/Navbar/GrowiNavbarBottom.jsx
  10. 1 1
      packages/app/src/components/Navbar/SubNavButtons.tsx
  11. 2 1
      packages/app/src/components/Page/DisplaySwitcher.tsx
  12. 1 1
      packages/app/src/components/Page/PageManagement.jsx
  13. 1 0
      packages/app/src/components/Page/RevisionBody.jsx
  14. 1 1
      packages/app/src/components/Page/TrashPageAlert.jsx
  15. 1 1
      packages/app/src/components/PageAccessoriesModal.tsx
  16. 4 5
      packages/app/src/components/PageCreateModal.jsx
  17. 13 14
      packages/app/src/components/PageDeleteModal.tsx
  18. 3 4
      packages/app/src/components/PageDuplicateModal.jsx
  19. 8 0
      packages/app/src/components/PageEditor/AbstractEditor.tsx
  20. 2 10
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  21. 15 3
      packages/app/src/components/PageList/PageListItemL.tsx
  22. 6 16
      packages/app/src/components/PagePresentationModal.jsx
  23. 5 4
      packages/app/src/components/PageRenameModal.jsx
  24. 48 5
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  25. 1 1
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  26. 4 5
      packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  27. 1 3
      packages/app/src/components/UncontrolledCodeMirror.tsx
  28. 6 0
      packages/app/src/server/service/page-grant.ts
  29. 8 1
      packages/app/src/server/service/page.ts
  30. 1 0
      packages/app/src/server/views/layout/layout.html
  31. 229 0
      packages/app/src/stores/modal.tsx
  32. 0 241
      packages/app/src/stores/ui.tsx

+ 2 - 0
packages/app/src/client/base.jsx

@@ -10,6 +10,7 @@ import PageCreateModal from '../components/PageCreateModal';
 import PageDeleteModal from '../components/PageDeleteModal';
 import PageDuplicateModal from '../components/PageDuplicateModal';
 import PageRenameModal from '../components/PageRenameModal';
+import PagePresentationModal from '../components/PagePresentationModal';
 import PageAccessoriesModal from '../components/PageAccessoriesModal';
 
 import AppContainer from '~/client/services/AppContainer';
@@ -48,6 +49,7 @@ const componentMappings = {
   'page-delete-modal': <PageDeleteModal />,
   'page-duplicate-modal': <PageDuplicateModal />,
   'page-rename-modal': <PageRenameModal />,
+  'page-presentation-modal': <PagePresentationModal />,
   'page-accessories-modal': <PageAccessoriesModal />,
   'descendants-page-list-modal': <DescendantsPageListModal />,
 

+ 5 - 0
packages/app/src/client/services/PageContainer.js

@@ -459,6 +459,7 @@ export default class PageContainer extends Container {
 
     const { pageId, remoteRevisionId, path } = this.state;
     const editorContainer = this.appContainer.getContainer('EditorContainer');
+    const pageEditor = this.appContainer.getComponentInstance('PageEditor');
     const options = editorContainer.getCurrentOptionsToSave();
     const optionsToSave = Object.assign({}, options);
 
@@ -467,6 +468,10 @@ export default class PageContainer extends Container {
     editorContainer.clearDraft(path);
     this.updateStateAfterSave(res.page, res.tags, res.revision, editorMode);
 
+    if (pageEditor != null) {
+      pageEditor.updateEditorValue(markdown);
+    }
+
     editorContainer.setState({ tags: res.tags });
 
     return res;

+ 1 - 1
packages/app/src/components/DescendantsPageListModal.tsx

@@ -6,7 +6,7 @@ import {
   Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
 
-import { useDescendantsPageListModal } from '~/stores/ui';
+import { useDescendantsPageListModal } from '~/stores/modal';
 import { useIsSharedUser } from '~/stores/context';
 
 import DescendantsPageList from './DescendantsPageList';

+ 2 - 2
packages/app/src/components/Fab.jsx

@@ -6,7 +6,7 @@ import loggerFactory from '~/utils/logger';
 
 import AppContainer from '~/client/services/AppContainer';
 
-import { useCreateModalStatus } from '~/stores/ui';
+import { usePageCreateModal } from '~/stores/modal';
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 
 import { withUnstatedContainers } from './UnstatedUtils';
@@ -19,7 +19,7 @@ const Fab = (props) => {
   const { appContainer } = props;
   const { currentUser } = appContainer;
 
-  const { open: openCreateModal } = useCreateModalStatus();
+  const { open: openCreateModal } = usePageCreateModal();
 
   const [animateClasses, setAnimateClasses] = useState('invisible');
   const [buttonClasses, setButtonClasses] = useState('');

+ 2 - 2
packages/app/src/components/Hotkeys/Subscribers/CreatePage.jsx

@@ -1,11 +1,11 @@
 import React, { useEffect } from 'react';
 import PropTypes from 'prop-types';
 
-import { useCreateModalStatus } from '~/stores/ui';
+import { usePageCreateModal } from '~/stores/modal';
 
 const CreatePage = React.memo((props) => {
 
-  const { open: openCreateModal } = useCreateModalStatus();
+  const { open: openCreateModal } = usePageCreateModal();
 
   // setup effect
   useEffect(() => {

+ 1 - 1
packages/app/src/components/IdenticalPathPage.tsx

@@ -5,7 +5,7 @@ import { DevidedPagePath } from '@growi/core';
 
 import { IPageHasId, IPageWithMeta } from '~/interfaces/page';
 import { useCurrentPagePath, useIsSharedUser } from '~/stores/context';
-import { useDescendantsPageListModal } from '~/stores/ui';
+import { useDescendantsPageListModal } from '~/stores/modal';
 import { useSWRxPageInfoForList } from '~/stores/page';
 
 import PageListIcon from './Icons/PageListIcon';

+ 17 - 9
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -9,9 +9,14 @@ import { withUnstatedContainers } from '../UnstatedUtils';
 import EditorContainer from '~/client/services/EditorContainer';
 import {
   EditorMode, useDrawerMode, useEditorMode, useIsDeviceSmallerThanMd, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
-  useIsAbleToShowPageEditorModeManager, useIsAbleToShowPageAuthors, usePageAccessoriesModal, PageAccessoriesModalContents,
-  usePageDuplicateModalStatus, usePageRenameModalStatus, usePageDeleteModal,
+  useIsAbleToShowPageEditorModeManager, useIsAbleToShowPageAuthors,
 } from '~/stores/ui';
+import {
+  usePageAccessoriesModal, PageAccessoriesModalContents,
+  usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
+} from '~/stores/modal';
+
+
 import {
   useCurrentCreatedAt, useCurrentUpdatedAt, useCurrentPageId, useRevisionId, useCurrentPagePath,
   useCreator, useRevisionAuthor, useCurrentUser, useIsGuestUser, useIsSharedUser, useShareLinkId,
@@ -57,12 +62,15 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isSharedUser } = useIsSharedUser();
 
-  const { open } = usePageAccessoriesModal();
+  const { open: openPresentationModal } = usePagePresentationModal();
+  const { open: openAccessoriesModal } = usePageAccessoriesModal();
+
+  const hrefForPresentationModal = '?presentation=1';
 
   return (
     <>
       {/* Presentation */}
-      <DropdownItem onClick={() => { /* TODO: implement in https://redmine.weseek.co.jp/issues/87672 */ }}>
+      <DropdownItem onClick={() => openPresentationModal(hrefForPresentationModal)}>
         <i className="icon-fw"><PresentationIcon /></i>
         { t('Presentation Mode') }
       </DropdownItem>
@@ -80,7 +88,7 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
         refs: PageAccessoriesModalControl
       */}
       <DropdownItem
-        onClick={() => open(PageAccessoriesModalContents.PageHistory)}
+        onClick={() => openAccessoriesModal(PageAccessoriesModalContents.PageHistory)}
         disabled={isGuestUser || isSharedUser}
       >
         <span className="mr-1"><HistoryIcon /></span>
@@ -88,14 +96,14 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
       </DropdownItem>
 
       <DropdownItem
-        onClick={() => open(PageAccessoriesModalContents.Attachment)}
+        onClick={() => openAccessoriesModal(PageAccessoriesModalContents.Attachment)}
       >
         <span className="mr-1"><AttachmentIcon /></span>
         {t('attachment_data')}
       </DropdownItem>
 
       <DropdownItem
-        onClick={() => open(PageAccessoriesModalContents.ShareLink)}
+        onClick={() => openAccessoriesModal(PageAccessoriesModalContents.ShareLink)}
         disabled={isGuestUser || isSharedUser || isLinkSharingDisabled}
       >
         <span className="mr-1"><ShareLinkIcon /></span>
@@ -136,8 +144,8 @@ const GrowiContextualSubNavigation = (props) => {
 
   const { mutate: mutateSWRTagsInfo, data: tagsInfoData } = useSWRTagsInfo(pageId);
 
-  const { open: openDuplicateModal } = usePageDuplicateModalStatus();
-  const { open: openRenameModal } = usePageRenameModalStatus();
+  const { open: openDuplicateModal } = usePageDuplicateModal();
+  const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
 
   const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false);

+ 6 - 4
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -7,8 +7,9 @@ import { UncontrolledTooltip } from 'reactstrap';
 
 import AppContainer from '~/client/services/AppContainer';
 import { IUser } from '~/interfaces/user';
-import { useIsDeviceSmallerThanMd, useCreateModalStatus } from '~/stores/ui';
-import { useIsSearchPage } from '~/stores/context';
+import { useIsDeviceSmallerThanMd } from '~/stores/ui';
+import { usePageCreateModal } from '~/stores/modal';
+import { useIsSearchPage, useCurrentPagePath } from '~/stores/context';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import GrowiLogo from '../Icons/GrowiLogo';
@@ -23,7 +24,8 @@ type NavbarRightProps = {
 }
 const NavbarRight: FC<NavbarRightProps> = memo((props: NavbarRightProps) => {
   const { t } = useTranslation();
-  const { open: openCreateModal } = useCreateModalStatus();
+  const { data: currentPagePath } = useCurrentPagePath();
+  const { open: openCreateModal } = usePageCreateModal();
 
   const { currentUser } = props;
 
@@ -42,7 +44,7 @@ const NavbarRight: FC<NavbarRightProps> = memo((props: NavbarRightProps) => {
         <button
           className="px-md-3 nav-link btn-create-page border-0 bg-transparent"
           type="button"
-          onClick={() => openCreateModal()}
+          onClick={() => openCreateModal(currentPagePath || '')}
         >
           <i className="icon-pencil mr-2"></i>
           <span className="d-none d-lg-block">{ t('New') }</span>

+ 3 - 2
packages/app/src/components/Navbar/GrowiNavbarBottom.jsx

@@ -2,7 +2,8 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 
-import { useCreateModalStatus, useIsDeviceSmallerThanMd, useDrawerOpened } from '~/stores/ui';
+import { useIsDeviceSmallerThanMd, useDrawerOpened } from '~/stores/ui';
+import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath, useIsSearchPage } from '~/stores/context';
 
 import GlobalSearch from './GlobalSearch';
@@ -11,7 +12,7 @@ const GrowiNavbarBottom = (props) => {
 
   const { data: isDrawerOpened, mutate: mutateDrawerOpened } = useDrawerOpened();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
-  const { open: openCreateModal } = useCreateModalStatus();
+  const { open: openCreateModal } = usePageCreateModal();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: isSearchPage } = useIsSearchPage();
 

+ 1 - 1
packages/app/src/components/Navbar/SubNavButtons.tsx

@@ -6,7 +6,7 @@ import { useSWRxPageInfo } from '../../stores/page';
 import { useSWRBookmarkInfo } from '../../stores/bookmark';
 import { useSWRxUsersList } from '../../stores/user';
 import { useIsGuestUser } from '~/stores/context';
-import { IPageForPageDeleteModal } from '~/stores/ui';
+import { IPageForPageDeleteModal } from '~/stores/modal';
 
 import SubscribeButton from '../SubscribeButton';
 import LikeButtons from '../LikeButtons';

+ 2 - 1
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -4,7 +4,8 @@ import { TabContent, TabPane } from 'reactstrap';
 
 import { pagePathUtils } from '@growi/core';
 
-import { EditorMode, useEditorMode, useDescendantsPageListModal } from '~/stores/ui';
+import { EditorMode, useEditorMode } from '~/stores/ui';
+import { useDescendantsPageListModal } from '~/stores/modal';
 import {
   useCurrentPagePath, useIsSharedUser, useIsEditable, useCurrentPageId, useIsUserPage, usePageUser,
 } from '~/stores/context';

+ 1 - 1
packages/app/src/components/Page/PageManagement.jsx

@@ -5,7 +5,7 @@ import { withTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 
 import { pagePathUtils } from '@growi/core';
-import { usePageDeleteModal } from '~/stores/ui';
+import { usePageDeleteModal } from '~/stores/modal';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';

+ 1 - 0
packages/app/src/components/Page/RevisionBody.jsx

@@ -64,6 +64,7 @@ export default class RevisionBody extends React.PureComponent {
             this.props.inputRef(elm);
           }
         }}
+        id="wiki"
         className={`wiki ${additionalClassName}`}
         // eslint-disable-next-line react/no-danger
         dangerouslySetInnerHTML={this.generateInnerHtml(this.props.html)}

+ 1 - 1
packages/app/src/components/Page/TrashPageAlert.jsx

@@ -11,7 +11,7 @@ import PutbackPageModal from '../PutbackPageModal';
 import EmptyTrashModal from '../EmptyTrashModal';
 
 import { useCurrentUpdatedAt } from '~/stores/context';
-import { usePageDeleteModal } from '~/stores/ui';
+import { usePageDeleteModal } from '~/stores/modal';
 
 const TrashPageAlert = (props) => {
   const { t, pageContainer } = props;

+ 1 - 1
packages/app/src/components/PageAccessoriesModal.tsx

@@ -7,7 +7,7 @@ import {
 import { useTranslation } from 'react-i18next';
 
 import { useIsGuestUser, useIsSharedUser } from '~/stores/context';
-import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/ui';
+import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/modal';
 import AppContainer from '~/client/services/AppContainer';
 
 import HistoryIcon from './Icons/HistoryIcon';

+ 4 - 5
packages/app/src/components/PageCreateModal.jsx

@@ -13,7 +13,8 @@ import { pagePathUtils, pathUtils } from '@growi/core';
 import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { toastError } from '~/client/util/apiNotification';
-import { useCreateModalStatus, useCreateModalOpened, useCreateModalPath } from '~/stores/ui';
+
+import { usePageCreateModal } from '~/stores/modal';
 
 import PagePathAutoComplete from './PagePathAutoComplete';
 
@@ -24,10 +25,8 @@ const {
 const PageCreateModal = (props) => {
   const { t, appContainer } = props;
 
-  const { close: closeCreateModal } = useCreateModalStatus();
-  const { data: isOpened } = useCreateModalOpened();
-  const { data: path } = useCreateModalPath();
-
+  const { data: pageCreateModalData, close: closeCreateModal } = usePageCreateModal();
+  const { isOpened, path } = pageCreateModalData;
 
   const config = appContainer.getConfig();
   const isReachable = config.isSearchServiceReachable;

+ 13 - 14
packages/app/src/components/PageDeleteModal.tsx

@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
 
 import { apiPost } from '~/client/util/apiv1-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
-import { usePageDeleteModal, usePageDeleteModalOpened } from '~/stores/ui';
+import { usePageDeleteModal } from '~/stores/modal';
 
 import { IDeleteSinglePageApiv1Result, IDeleteManyPageApiv3Result } from '~/interfaces/page';
 
@@ -38,10 +38,9 @@ const PageDeleteModal: FC<Props> = (props: Props) => {
     isDeleteCompletelyModal, isAbleToDeleteCompletely,
   } = props;
 
-  const { data: deleteModalStatus, close: closeDeleteModal } = usePageDeleteModal();
-  const { data: pageDeleteModalOpened } = usePageDeleteModalOpened();
+  const { data: deleteModalData, close: closeDeleteModal } = usePageDeleteModal();
 
-  const isOpened = pageDeleteModalOpened?.isOpend != null ? pageDeleteModalOpened.isOpend : false;
+  const isOpened = deleteModalData?.isOpened ?? false;
 
   const [isDeleteRecursively, setIsDeleteRecursively] = useState(true);
   const [isDeleteCompletely, setIsDeleteCompletely] = useState(isDeleteCompletelyModal && isAbleToDeleteCompletely);
@@ -62,20 +61,20 @@ const PageDeleteModal: FC<Props> = (props: Props) => {
   }
 
   async function deletePage() {
-    if (deleteModalStatus == null || deleteModalStatus.pages == null) {
+    if (deleteModalData == null || deleteModalData.pages == null) {
       return;
     }
 
     /*
      * When multiple pages
      */
-    if (deleteModalStatus.pages.length > 1) {
+    if (deleteModalData.pages.length > 1) {
       try {
         const isRecursively = isDeleteRecursively === true ? true : undefined;
         const isCompletely = isDeleteCompletely === true ? true : undefined;
 
         const pageIdToRevisionIdMap = {};
-        deleteModalStatus.pages.forEach((p) => { pageIdToRevisionIdMap[p.pageId] = p.revisionId });
+        deleteModalData.pages.forEach((p) => { pageIdToRevisionIdMap[p.pageId] = p.revisionId });
 
         const { data } = await apiv3Post<IDeleteManyPageApiv3Result>('/pages/delete', {
           pageIdToRevisionIdMap,
@@ -83,8 +82,8 @@ const PageDeleteModal: FC<Props> = (props: Props) => {
           isCompletely,
         });
 
-        if (pageDeleteModalOpened != null && pageDeleteModalOpened.onDeleted != null) {
-          pageDeleteModalOpened.onDeleted(data.paths, data.isRecursively, data.isCompletely);
+        if (deleteModalData.onDeleted != null) {
+          deleteModalData.onDeleted(data.paths, data.isRecursively, data.isCompletely);
         }
       }
       catch (err) {
@@ -99,7 +98,7 @@ const PageDeleteModal: FC<Props> = (props: Props) => {
         const recursively = isDeleteRecursively === true ? true : undefined;
         const completely = isDeleteCompletely === true ? true : undefined;
 
-        const page = deleteModalStatus.pages[0];
+        const page = deleteModalData.pages[0];
 
         const { path, isRecursively, isCompletely } = await apiPost('/pages.remove', {
           page_id: page.pageId,
@@ -108,8 +107,8 @@ const PageDeleteModal: FC<Props> = (props: Props) => {
           completely,
         }) as IDeleteSinglePageApiv1Result;
 
-        if (pageDeleteModalOpened != null && pageDeleteModalOpened.onDeleted != null) {
-          pageDeleteModalOpened.onDeleted(path, isRecursively, isCompletely);
+        if (deleteModalData.onDeleted != null) {
+          deleteModalData.onDeleted(path, isRecursively, isCompletely);
         }
       }
       catch (err) {
@@ -177,8 +176,8 @@ const PageDeleteModal: FC<Props> = (props: Props) => {
   }
 
   const renderPagePathsToDelete = () => {
-    if (deleteModalStatus != null && deleteModalStatus.pages != null) {
-      return deleteModalStatus.pages.map(page => <div key={page.pageId}><code>{ page.path }</code></div>);
+    if (deleteModalData != null && deleteModalData.pages != null) {
+      return deleteModalData.pages.map(page => <div key={page.pageId}><code>{ page.path }</code></div>);
     }
     return <></>;
   };

+ 3 - 4
packages/app/src/components/PageDuplicateModal.jsx

@@ -9,7 +9,7 @@ import { withTranslation } from 'react-i18next';
 import { debounce } from 'throttle-debounce';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { toastError } from '~/client/util/apiNotification';
-import { usePageDuplicateModalStatus, usePageDuplicateModalOpened } from '~/stores/ui';
+import { usePageDuplicateModal } from '~/stores/modal';
 
 import AppContainer from '~/client/services/AppContainer';
 import PagePathAutoComplete from './PagePathAutoComplete';
@@ -27,10 +27,9 @@ const PageDuplicateModal = (props) => {
   const config = appContainer.getConfig();
   const isReachable = config.isSearchServiceReachable;
   const { crowi } = appContainer.config;
-  const { data: pagesDataToDuplicate, close: closeDuplicateModal } = usePageDuplicateModalStatus();
-  const { data: isOpened } = usePageDuplicateModalOpened();
+  const { data: pagesDataToDuplicate, close: closeDuplicateModal } = usePageDuplicateModal();
 
-  const { path, pageId } = pagesDataToDuplicate;
+  const { isOpened, path, pageId } = pagesDataToDuplicate;
 
   const [pageNameInput, setPageNameInput] = useState(path);
 

+ 8 - 0
packages/app/src/components/PageEditor/AbstractEditor.tsx

@@ -12,6 +12,10 @@ export interface AbstractEditorProps extends ICodeMirror {
   onCtrlEnter?: (event: Event) => void;
 }
 
+interface defaultProps {
+  isGfmMode: true,
+}
+
 export default class AbstractEditor<T extends AbstractEditorProps> extends React.Component<T, Record<string, unknown>> {
 
   constructor(props: Readonly<T>) {
@@ -29,6 +33,10 @@ export default class AbstractEditor<T extends AbstractEditorProps> extends React
     this.dispatchSave = this.dispatchSave.bind(this);
   }
 
+  public static defaultProps: defaultProps = {
+    isGfmMode: true,
+  };
+
   forceToFocus(): void {}
 
   /**

+ 2 - 10
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -3,8 +3,6 @@ import PropTypes from 'prop-types';
 
 import urljoin from 'url-join';
 import * as codemirror from 'codemirror';
-import { UnControlled as UncontrolledCodeMirror } from 'react-codemirror2';
-
 import { Button } from 'reactstrap';
 
 import { JSHINT } from 'jshint';
@@ -13,6 +11,7 @@ import * as loadScript from 'simple-load-script';
 import * as loadCssSync from 'load-css-file';
 
 import { createValidator } from '@growi/codemirror-textlint';
+import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
 import InterceptorManager from '~/services/interceptor-manager';
 import loggerFactory from '~/utils/logger';
 
@@ -32,7 +31,6 @@ import LinkEditModal from './LinkEditModal';
 import HandsontableModal from './HandsontableModal';
 import EditorIcon from './EditorIcon';
 import DrawioModal from './DrawioModal';
-// import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
 
 // Textlint
 window.JSHINT = JSHINT;
@@ -110,7 +108,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
     this.state = {
       value: this.props.value,
-      isGfmMode: this.props.isGfmMode ?? true,
+      isGfmMode: this.props.isGfmMode,
       isEnabledEmojiAutoComplete: false,
       isLoadingKeymap: false,
       isSimpleCheatsheetShown: this.props.isGfmMode && this.props.value.length === 0,
@@ -909,7 +907,6 @@ export default class CodeMirrorEditor extends AbstractEditor {
   }
 
   render() {
-    const mode = this.state.isGfmMode ? 'gfm-growi' : undefined;
     const lint = this.props.isTextlintEnabled ? this.codemirrorLintConfig : false;
     const additionalClasses = Array.from(this.state.additionalClassSet).join(' ');
     const placeholder = this.state.isGfmMode ? 'Input with Markdown..' : 'Input with Plain Text..';
@@ -936,11 +933,6 @@ export default class CodeMirrorEditor extends AbstractEditor {
           }}
           value={this.state.value}
           options={{
-            mode,
-            theme: this.props.editorOptions.theme,
-            styleActiveLine: this.props.editorOptions.styleActiveLine,
-            lineNumbers: this.props.lineNumbers,
-            tabSize: 4,
             indentUnit: this.props.indentSize,
             lineWrapping: true,
             scrollPastEnd: true,

+ 15 - 3
packages/app/src/components/PageList/PageListItemL.tsx

@@ -2,10 +2,12 @@ import React, { memo, useCallback } from 'react';
 
 import Clamp from 'react-multiline-clamp';
 import { format } from 'date-fns';
+import urljoin from 'url-join';
 
 import { UserPicture, PageListMeta } from '@growi/ui';
 import { DevidedPagePath } from '@growi/core';
-import { useIsDeviceSmallerThanLg, usePageRenameModalStatus, usePageDeleteModal } from '~/stores/ui';
+import { useIsDeviceSmallerThanLg } from '~/stores/ui';
+import { usePageRenameModal, usePageDuplicateModal, usePageDeleteModal } from '~/stores/modal';
 import {
   IPageInfoAll, IPageWithMeta, isIPageInfoForEntity, isIPageInfoForListing,
 } from '~/interfaces/page';
@@ -34,7 +36,8 @@ export const PageListItemL = memo((props: Props): JSX.Element => {
   } = props;
 
   const { data: isDeviceSmallerThanLg } = useIsDeviceSmallerThanLg();
-  const { open: openRenameModal } = usePageRenameModalStatus();
+  const { open: openDuplicateModal } = usePageDuplicateModal();
+  const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
 
   const elasticSearchResult = isIPageSearchMeta(pageMeta) ? pageMeta.elasticSearchResult : null;
@@ -58,6 +61,11 @@ export const PageListItemL = memo((props: Props): JSX.Element => {
     }
   }, [isDeviceSmallerThanLg, onClickItem, pageData._id]);
 
+  const duplicateMenuItemClickHandler = useCallback(() => {
+    const { _id: pageId, path } = pageData;
+    openDuplicateModal(pageId, path);
+  }, [openDuplicateModal, pageData]);
+
   const renameMenuItemClickHandler = useCallback(() => {
     const { _id: pageId, revision: revisionId, path } = pageData;
     openRenameModal(pageId, revisionId as string, path);
@@ -111,7 +119,10 @@ export const PageListItemL = memo((props: Props): JSX.Element => {
               {/* page title */}
               <Clamp lines={1}>
                 <span className="h5 mb-0">
-                  <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} />
+                  {/* Use permanent links to care for pages with the same name (Cannot use page path url) */}
+                  <span className="grw-page-path-hierarchical-link text-break">
+                    <a className="page-segment" href={encodeURI(urljoin('/', pageData._id))}>{linkedPagePathLatter.pathName}</a>
+                  </span>
                 </span>
               </Clamp>
 
@@ -130,6 +141,7 @@ export const PageListItemL = memo((props: Props): JSX.Element => {
                   onClickDeleteMenuItem={deleteMennuItemClickHandler}
                   onClickRenameMenuItem={renameMenuItemClickHandler}
                   isEnableActions={isEnableActions}
+                  onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
                 />
               </div>
             </div>

+ 6 - 16
packages/app/src/components/PagePresentationModal.jsx

@@ -1,31 +1,21 @@
 import React from 'react';
-import PropTypes from 'prop-types';
 import {
   Modal, ModalBody,
 } from 'reactstrap';
 
-const PagePresentationModal = (props) => {
+import { usePagePresentationModal } from '~/stores/modal';
 
-  function closeModalHandler() {
-    if (props.onClose === null) {
-      return;
-    }
-    props.onClose();
-  }
+const PagePresentationModal = () => {
+
+  const { data: presentationData, close: closePresentationModal } = usePagePresentationModal();
 
   return (
-    <Modal isOpen={props.isOpen} toggle={closeModalHandler} className="grw-presentation-modal" unmountOnClose={false}>
+    <Modal isOpen={presentationData.isOpened} toggle={closePresentationModal} className="grw-presentation-modal" unmountOnClose={false}>
       <ModalBody className="modal-body">
-        <iframe src={props.href} />
+        <iframe src={presentationData.href} />
       </ModalBody>
     </Modal>
   );
 };
-PagePresentationModal.propTypes = {
-  isOpen: PropTypes.bool.isRequired,
-  onClose: PropTypes.func,
-  href: PropTypes.string.isRequired,
-};
-
 
 export default PagePresentationModal;

+ 5 - 4
packages/app/src/components/PageRenameModal.jsx

@@ -10,7 +10,7 @@ import {
 import { withTranslation } from 'react-i18next';
 
 import { debounce } from 'throttle-debounce';
-import { usePageRenameModalStatus, usePageRenameModalOpened } from '~/stores/ui';
+import { usePageRenameModal } from '~/stores/modal';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { toastError } from '~/client/util/apiNotification';
 
@@ -29,10 +29,11 @@ const PageRenameModal = (props) => {
   } = props;
 
   const { crowi } = appContainer.config;
-  const { data: isOpened } = usePageRenameModalOpened();
-  const { data: pagesDataToRename, close: closeRenameModal } = usePageRenameModalStatus();
+  const { data: pagesDataToRename, close: closeRenameModal } = usePageRenameModal();
 
-  const { path, revisionId, pageId } = pagesDataToRename;
+  const {
+    isOpened, path, revisionId, pageId,
+  } = pagesDataToRename;
 
   const [pageNameInput, setPageNameInput] = useState('');
 

+ 48 - 5
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -1,4 +1,6 @@
-import React, { FC, useCallback } from 'react';
+import React, {
+  FC, useCallback, useEffect, useRef,
+} from 'react';
 
 import { DropdownItem } from 'reactstrap';
 import { useTranslation } from 'react-i18next';
@@ -10,11 +12,12 @@ import { exportAsMarkdown } from '~/client/services/page-operation';
 
 import RevisionLoader from '../Page/RevisionLoader';
 import AppContainer from '../../client/services/AppContainer';
+import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import { GrowiSubNavigation } from '../Navbar/GrowiSubNavigation';
 import { SubNavButtons } from '../Navbar/SubNavButtons';
 import { AdditionalMenuItemsRendererProps } from '../Common/Dropdown/PageItemControl';
 
-import { usePageDuplicateModalStatus, usePageRenameModalStatus, usePageDeleteModal } from '~/stores/ui';
+import { usePageDuplicateModal, usePageRenameModal, usePageDeleteModal } from '~/stores/modal';
 
 
 type AdditionalMenuItemsProps = AdditionalMenuItemsRendererProps & {
@@ -40,6 +43,8 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
   );
 };
 
+const SCROLL_OFFSET_TOP = 175; // approximate height of (navigation + subnavigation)
+const MUTATION_OBSERVER_CONFIG = { childList: true, subtree: true };
 
 type Props ={
   appContainer: AppContainer,
@@ -48,15 +53,53 @@ type Props ={
   showPageControlDropdown?: boolean,
 }
 
+const scrollTo = (scrollElement:HTMLElement) => {
+  // use querySelector to intentionally get the first element found
+  const highlightedKeyword = scrollElement.querySelector('.highlighted-keyword') as HTMLElement | null;
+  if (highlightedKeyword != null) {
+    smoothScrollIntoView(highlightedKeyword, SCROLL_OFFSET_TOP, scrollElement);
+  }
+};
+
+const generateObserverCallback = (doScroll: ()=>void) => {
+  return (mutationRecords:MutationRecord[]) => {
+    mutationRecords.forEach((record:MutationRecord) => {
+      const target = record.target as HTMLElement;
+      const targetId = target.id as string;
+      if (targetId !== 'wiki') return;
+      doScroll();
+    });
+  };
+};
+
 const SearchResultContent: FC<Props> = (props: Props) => {
+  const scrollElementRef = useRef(null);
+
+  // ***************************  Auto Scroll  ***************************
+  useEffect(() => {
+    const scrollElement = scrollElementRef.current as HTMLElement | null;
+    if (scrollElement == null) return;
+
+    const observerCallback = generateObserverCallback(() => {
+      scrollTo(scrollElement);
+    });
+
+    const observer = new MutationObserver(observerCallback);
+    observer.observe(scrollElement, MUTATION_OBSERVER_CONFIG);
+    return () => {
+      observer.disconnect();
+    };
+  });
+  // *******************************  end  *******************************
+
   const {
     appContainer,
     focusedSearchResultData,
     showPageControlDropdown,
   } = props;
 
-  const { open: openDuplicateModal } = usePageDuplicateModalStatus();
-  const { open: openRenameModal } = usePageRenameModalStatus();
+  const { open: openDuplicateModal } = usePageDuplicateModal();
+  const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
 
   const page = focusedSearchResultData?.pageData;
@@ -114,7 +157,7 @@ const SearchResultContent: FC<Props> = (props: Props) => {
         page={page}
         controls={ControlComponents}
       />
-      <div className="search-result-page-content">
+      <div className="search-result-page-content" ref={scrollElementRef}>
         <RevisionLoader
           growiRenderer={growiRenderer}
           pageId={page._id}

+ 1 - 1
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -13,7 +13,7 @@ import { pathUtils } from '@growi/core';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/apiNotification';
 
 import { useSWRxPageChildren } from '~/stores/page-listing';
-import { IPageForPageDeleteModal } from '~/stores/ui';
+import { IPageForPageDeleteModal } from '~/stores/modal';
 import { apiv3Put } from '~/client/util/apiv3-client';
 
 import TriangleIcon from '~/components/Icons/TriangleIcon';

+ 4 - 5
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -8,9 +8,8 @@ import { useSWRxPageAncestorsChildren, useSWRxRootPage } from '../../../stores/p
 import { TargetAndAncestors } from '~/interfaces/page-listing-results';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import {
-  IPageForPageDeleteModal, usePageDuplicateModalStatus, usePageRenameModalStatus, usePageDeleteModal,
-  OnDeletedFunction,
-} from '~/stores/ui';
+  OnDeletedFunction, IPageForPageDeleteModal, usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
+} from '~/stores/modal';
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 
 /*
@@ -97,8 +96,8 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
 
   const { data: ancestorsChildrenData, error: error1 } = useSWRxPageAncestorsChildren(targetPath);
   const { data: rootPageData, error: error2 } = useSWRxRootPage();
-  const { open: openDuplicateModal } = usePageDuplicateModalStatus();
-  const { open: openRenameModal } = usePageRenameModalStatus();
+  const { open: openDuplicateModal } = usePageDuplicateModal();
+  const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
 
   useEffect(() => {

+ 1 - 3
packages/app/src/components/UncontrolledCodeMirror.tsx

@@ -12,7 +12,6 @@ export interface UncontrolledCodeMirrorProps extends AbstractEditorProps {
   value: string;
   options?: ICodeMirror['options'];
   isGfmMode?: boolean;
-  indentSize?: number;
   lineNumbers?: boolean;
 }
 
@@ -26,7 +25,7 @@ class UncontrolledCodeMirrorCore extends AbstractEditor<UncontrolledCodeMirrorCo
   render(): ReactNode {
 
     const {
-      value, isGfmMode, indentSize, lineNumbers, editorContainer, options, forwardedRef, ...rest
+      value, isGfmMode, lineNumbers, editorContainer, options, forwardedRef, ...rest
     } = this.props;
 
     const { editorOptions } = editorContainer.state;
@@ -41,7 +40,6 @@ class UncontrolledCodeMirrorCore extends AbstractEditor<UncontrolledCodeMirrorCo
           theme: editorOptions.theme,
           styleActiveLine: editorOptions.styleActiveLine,
           tabSize: 4,
-          indentUnit: indentSize,
           ...options,
         }}
         {...rest}

+ 6 - 0
packages/app/src/server/service/page-grant.ts

@@ -10,6 +10,8 @@ import { isIncludesObjectId, excludeTestIdsFromTargetIds } from '~/server/util/c
 const { addTrailingSlash } = pathUtils;
 const { isTopPage } = pagePathUtils;
 
+const LIMIT_FOR_MULTIPLE_PAGE_OP = 20;
+
 type ObjectIdLike = mongoose.Types.ObjectId | string;
 
 type ComparableTarget = {
@@ -343,6 +345,10 @@ class PageGrantService {
   }
 
   async separateNormalizedAndNonNormalizedPages(pageIds: ObjectIdLike[]): Promise<[(PageDocument & { _id: any })[], (PageDocument & { _id: any })[]]> {
+    if (pageIds.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
+      throw Error(`The maximum number of pageIds allowed is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`);
+    }
+
     const Page = mongoose.model('Page') as unknown as PageModel;
     const { PageQueryBuilder } = Page;
     const shouldCheckDescendants = true;

+ 8 - 1
packages/app/src/server/service/page.ts

@@ -1864,7 +1864,14 @@ class PageService {
       return;
     }
 
-    const [normalizedIds, notNormalizedPaths] = await this.crowi.pageGrantService.separateNormalizedAndNonNormalizedPages(pageIds);
+    let normalizedIds;
+    let notNormalizedPaths;
+    try {
+      [normalizedIds, notNormalizedPaths] = await this.crowi.pageGrantService.separateNormalizedAndNonNormalizedPages(pageIds);
+    }
+    catch (err) {
+      throw err;
+    }
 
     if (normalizedIds.length === 0) {
       // socket.emit('normalizeParentRecursivelyByPageIds', { error: err.message }); TODO: use socket to tell user

+ 1 - 0
packages/app/src/server/views/layout/layout.html

@@ -107,6 +107,7 @@
 <div id="page-delete-modal"></div>
 <div id="page-duplicate-modal"></div>
 <div id="page-rename-modal"></div>
+<div id="page-presentation-modal"></div>
 <div id="page-accessories-modal"></div>
 <div id="descendants-page-list-modal"></div>
 

+ 229 - 0
packages/app/src/stores/modal.tsx

@@ -0,0 +1,229 @@
+import { SWRResponse } from 'swr';
+import { useStaticSWR } from './use-static-swr';
+import { Nullable } from '~/interfaces/common';
+
+
+/*
+* PageCreateModal
+*/
+type CreateModalStatus = {
+  isOpened: boolean,
+  path?: string,
+}
+
+type CreateModalStatusUtils = {
+  open(path?: string): Promise<CreateModalStatus | undefined>
+  close(): Promise<CreateModalStatus | undefined>
+}
+
+export const usePageCreateModal = (status?: CreateModalStatus): SWRResponse<CreateModalStatus, Error> & CreateModalStatusUtils => {
+  const initialData: CreateModalStatus = { isOpened: false };
+  const swrResponse = useStaticSWR<CreateModalStatus, Error>('pageCreateModalStatus', status, { fallbackData: initialData });
+
+  return {
+    ...swrResponse,
+    open: (path?: string) => swrResponse.mutate({ isOpened: true, path }),
+    close: () => swrResponse.mutate({ isOpened: false }),
+  };
+};
+
+/*
+* PageDeleteModal
+*/
+export type IPageForPageDeleteModal = {
+  pageId: string,
+  revisionId?: string,
+  path: string
+}
+
+export type OnDeletedFunction = (pathOrPaths: string | string[], isRecursively: Nullable<true>, isCompletely: Nullable<true>) => void;
+
+type DeleteModalStatus = {
+  isOpened: boolean,
+  pages?: IPageForPageDeleteModal[],
+  onDeleted?: OnDeletedFunction,
+}
+
+type DeleteModalStatusUtils = {
+  open(pages?: IPageForPageDeleteModal[], onDeleted?: OnDeletedFunction): Promise<DeleteModalStatus | undefined>,
+  close(): Promise<DeleteModalStatus | undefined>,
+}
+
+export const usePageDeleteModal = (status?: DeleteModalStatus): SWRResponse<DeleteModalStatus, Error> & DeleteModalStatusUtils => {
+  const initialData: DeleteModalStatus = { isOpened: false, pages: [], onDeleted: () => {} };
+  const swrResponse = useStaticSWR<DeleteModalStatus, Error>('deleteModalStatus', status, { fallbackData: initialData });
+
+  return {
+    ...swrResponse,
+    open: (pages?: IPageForPageDeleteModal[], onDeleted?: OnDeletedFunction) => swrResponse.mutate({ isOpened: true, pages, onDeleted }),
+    close: () => swrResponse.mutate({ isOpened: false }),
+  };
+};
+
+/*
+* PageDuplicateModal
+*/
+export type IPageForPageDuplicateModal = {
+  pageId: string,
+  path: string
+}
+
+type DuplicateModalStatus = {
+  isOpened: boolean,
+  pageId?: string,
+  path?: string,
+}
+
+type DuplicateModalStatusUtils = {
+  open(pageId: string, path: string): Promise<DuplicateModalStatus | undefined>
+  close(): Promise<DuplicateModalStatus | undefined>
+}
+
+export const usePageDuplicateModal = (status?: DuplicateModalStatus): SWRResponse<DuplicateModalStatus, Error> & DuplicateModalStatusUtils => {
+  const initialData: DuplicateModalStatus = { isOpened: false, pageId: '', path: '' };
+  const swrResponse = useStaticSWR<DuplicateModalStatus, Error>('duplicateModalStatus', status, { fallbackData: initialData });
+
+  return {
+    ...swrResponse,
+    open: (pageId: string, path: string) => swrResponse.mutate({ isOpened: true, pageId, path }),
+    close: () => swrResponse.mutate({ isOpened: false }),
+  };
+};
+
+
+/*
+* PageRenameModal
+*/
+export type IPageForPageRenameModal = {
+  pageId: string,
+  revisionId: string,
+  path: string
+}
+
+type RenameModalStatus = {
+  isOpened: boolean,
+  pageId?: string,
+  revisionId?: string
+  path?: string,
+}
+
+type RenameModalStatusUtils = {
+  open(pageId: string, revisionId: string, path: string): Promise<RenameModalStatus | undefined>
+  close(): Promise<RenameModalStatus | undefined>
+}
+
+export const usePageRenameModal = (status?: RenameModalStatus): SWRResponse<RenameModalStatus, Error> & RenameModalStatusUtils => {
+  const initialData: RenameModalStatus = {
+    isOpened: false, pageId: '', revisionId: '', path: '',
+  };
+  const swrResponse = useStaticSWR<RenameModalStatus, Error>('renameModalStatus', status, { fallbackData: initialData });
+
+  return {
+    ...swrResponse,
+    open: (pageId: string, revisionId: string, path: string) => swrResponse.mutate({
+      isOpened: true, pageId, revisionId, path,
+    }),
+    close: () => swrResponse.mutate({ isOpened: false }),
+  };
+};
+
+/*
+* PagePresentationModal
+*/
+type PresentationModalStatus = {
+  isOpened: boolean,
+  href?: string
+}
+
+type PresentationModalStatusUtils = {
+  open(href: string): Promise<PresentationModalStatus | undefined>
+  close(): Promise<PresentationModalStatus | undefined>
+}
+
+export const usePagePresentationModal = (
+    status?: PresentationModalStatus,
+): SWRResponse<PresentationModalStatus, Error> & PresentationModalStatusUtils => {
+  const initialData: PresentationModalStatus = {
+    isOpened: false, href: '?presentation=1',
+  };
+  const swrResponse = useStaticSWR<PresentationModalStatus, Error>('presentationModalStatus', status, { fallbackData: initialData });
+
+  return {
+    ...swrResponse,
+    open: (href: string) => swrResponse.mutate({ isOpened: true, href }),
+    close: () => swrResponse.mutate({ isOpened: false }),
+  };
+};
+
+/*
+* DescendantsPageListModal
+*/
+type DescendantsPageListModalStatus = {
+  isOpened: boolean,
+  path?: string,
+}
+
+type DescendantsPageListUtils = {
+  open(path: string): Promise<DescendantsPageListModalStatus | undefined>
+  close(): Promise<DuplicateModalStatus | undefined>
+}
+
+export const useDescendantsPageListModal = (
+    status?: DescendantsPageListModalStatus,
+): SWRResponse<DescendantsPageListModalStatus, Error> & DescendantsPageListUtils => {
+
+  const initialData: DescendantsPageListModalStatus = { isOpened: false };
+  const swrResponse = useStaticSWR<DescendantsPageListModalStatus, Error>('descendantsPageListModalStatus', status, { fallbackData: initialData });
+
+  return {
+    ...swrResponse,
+    open: (path: string) => swrResponse.mutate({ isOpened: true, path }),
+    close: () => swrResponse.mutate({ isOpened: false }),
+  };
+};
+
+/*
+* PageAccessoriesModal
+*/
+export const PageAccessoriesModalContents = {
+  PageHistory: 'PageHistory',
+  Attachment: 'Attachment',
+  ShareLink: 'ShareLink',
+} as const;
+export type PageAccessoriesModalContents = typeof PageAccessoriesModalContents[keyof typeof PageAccessoriesModalContents];
+
+type PageAccessoriesModalStatus = {
+  isOpened: boolean,
+  onOpened?: (initialActivatedContents: PageAccessoriesModalContents) => void,
+}
+
+type PageAccessoriesModalUtils = {
+  open(activatedContents: PageAccessoriesModalContents): void
+  close(): void
+}
+
+export const usePageAccessoriesModal = (): SWRResponse<PageAccessoriesModalStatus, Error> & PageAccessoriesModalUtils => {
+
+  const initialStatus = { isOpened: false };
+  const swrResponse = useStaticSWR<PageAccessoriesModalStatus, Error>('pageAccessoriesModalStatus', undefined, { fallbackData: initialStatus });
+
+  return {
+    ...swrResponse,
+    open: (activatedContents: PageAccessoriesModalContents) => {
+      if (swrResponse.data == null) {
+        return;
+      }
+      swrResponse.mutate({ isOpened: true });
+
+      if (swrResponse.data.onOpened != null) {
+        swrResponse.data.onOpened(activatedContents);
+      }
+    },
+    close: () => {
+      if (swrResponse.data == null) {
+        return;
+      }
+      swrResponse.mutate({ isOpened: false });
+    },
+  };
+};

+ 0 - 241
packages/app/src/stores/ui.tsx

@@ -253,247 +253,6 @@ export const useSidebarResizeDisabled = (isDisabled?: boolean): SWRResponse<bool
   return useStaticSWR('isSidebarResizeDisabled', isDisabled, { fallbackData: false });
 };
 
-// PageCreateModal
-type CreateModalStatus = {
-  isOpened: boolean,
-  path?: string,
-}
-
-type CreateModalStatusUtils = {
-  open(path?: string): Promise<CreateModalStatus | undefined>
-  close(): Promise<CreateModalStatus | undefined>
-}
-
-export const useCreateModalStatus = (status?: CreateModalStatus): SWRResponse<CreateModalStatus, Error> & CreateModalStatusUtils => {
-  const initialData: CreateModalStatus = { isOpened: false };
-  const swrResponse = useStaticSWR<CreateModalStatus, Error>('pageCreateModalStatus', status, { fallbackData: initialData });
-
-  return {
-    ...swrResponse,
-    open: (path?: string) => swrResponse.mutate({ isOpened: true, path }),
-    close: () => swrResponse.mutate({ isOpened: false }),
-  };
-};
-
-export const useCreateModalOpened = (): SWRResponse<boolean, Error> => {
-  const { data } = useCreateModalStatus();
-  return useSWR(
-    data != null ? ['isCreaateModalOpened', data] : null,
-    () => {
-      return data != null ? data.isOpened : false;
-    },
-  );
-};
-
-export const useCreateModalPath = (): SWRResponse<string | null | undefined, Error> => {
-  const { data: currentPagePath } = useCurrentPagePath();
-  const { data: status } = useCreateModalStatus();
-
-  return useSWR(
-    currentPagePath != null && status != null ? [currentPagePath, status] : null,
-    (currentPagePath, status) => {
-      return status?.path || currentPagePath;
-    },
-  );
-};
-
-// PageDeleteModal
-export type IPageForPageDeleteModal = {
-  pageId: string,
-  revisionId?: string,
-  path: string
-}
-
-export type OnDeletedFunction = (pathOrPaths: string | string[], isRecursively: Nullable<true>, isCompletely: Nullable<true>) => void;
-
-type DeleteModalStatus = {
-  isOpened: boolean,
-  pages?: IPageForPageDeleteModal[],
-  onDeleted?: OnDeletedFunction,
-}
-
-type DeleteModalOpened = {
-  isOpend: boolean,
-  onDeleted?: OnDeletedFunction,
-}
-
-type DeleteModalStatusUtils = {
-  open(pages?: IPageForPageDeleteModal[], onDeleted?: OnDeletedFunction): Promise<DeleteModalStatus | undefined>,
-  close(): Promise<DeleteModalStatus | undefined>,
-}
-
-export const usePageDeleteModal = (status?: DeleteModalStatus): SWRResponse<DeleteModalStatus, Error> & DeleteModalStatusUtils => {
-  const initialData: DeleteModalStatus = { isOpened: false };
-  const swrResponse = useStaticSWR<DeleteModalStatus, Error>('deleteModalStatus', status, { fallbackData: initialData });
-
-  return {
-    ...swrResponse,
-    open: (pages?: IPageForPageDeleteModal[], onDeleted?: OnDeletedFunction) => swrResponse.mutate({ isOpened: true, pages, onDeleted }),
-    close: () => swrResponse.mutate({ isOpened: false }),
-  };
-};
-
-export const usePageDeleteModalOpened = (): SWRResponse<(DeleteModalOpened | null), Error> => {
-  const { data } = usePageDeleteModal();
-  return useSWRImmutable(
-    data != null ? ['isDeleteModalOpened', data] : null,
-    () => {
-      return data != null ? { isOpend: data.isOpened, onDeleted: data?.onDeleted } : null;
-    },
-  );
-};
-
-// PageDuplicateModal
-export type IPageForPageDuplicateModal = {
-  pageId: string,
-  path: string
-}
-
-type DuplicateModalStatus = {
-  isOpened: boolean,
-  pageId?: string,
-  path?: string,
-}
-
-type DuplicateModalStatusUtils = {
-  open(pageId: string, path: string): Promise<DuplicateModalStatus | undefined>
-  close(): Promise<DuplicateModalStatus | undefined>
-}
-
-export const usePageDuplicateModalStatus = (status?: DuplicateModalStatus): SWRResponse<DuplicateModalStatus, Error> & DuplicateModalStatusUtils => {
-  const initialData: DuplicateModalStatus = { isOpened: false, pageId: '', path: '' };
-  const swrResponse = useStaticSWR<DuplicateModalStatus, Error>('duplicateModalStatus', status, { fallbackData: initialData });
-
-  return {
-    ...swrResponse,
-    open: (pageId: string, path: string) => swrResponse.mutate({ isOpened: true, pageId, path }),
-    close: () => swrResponse.mutate({ isOpened: false }),
-  };
-};
-
-export const usePageDuplicateModalOpened = (): SWRResponse<boolean, Error> => {
-  const { data } = usePageDuplicateModalStatus();
-  return useSWRImmutable(
-    data != null ? ['isDuplicateModalOpened', data] : null,
-    () => {
-      return data != null ? data.isOpened : false;
-    },
-  );
-};
-
-// PageRenameModal
-export type IPageForPageRenameModal = {
-  pageId: string,
-  revisionId: string,
-  path: string
-}
-
-type RenameModalStatus = {
-  isOpened: boolean,
-  pageId?: string,
-  revisionId?: string
-  path?: string,
-}
-
-type RenameModalStatusUtils = {
-  open(pageId: string, revisionId: string, path: string): Promise<RenameModalStatus | undefined>
-  close(): Promise<RenameModalStatus | undefined>
-}
-
-export const usePageRenameModalStatus = (status?: RenameModalStatus): SWRResponse<RenameModalStatus, Error> & RenameModalStatusUtils => {
-  const initialData: RenameModalStatus = {
-    isOpened: false, pageId: '', revisionId: '', path: '',
-  };
-  const swrResponse = useStaticSWR<RenameModalStatus, Error>('renameModalStatus', status, { fallbackData: initialData });
-
-  return {
-    ...swrResponse,
-    open: (pageId: string, revisionId: string, path: string) => swrResponse.mutate({
-      isOpened: true, pageId, revisionId, path,
-    }),
-    close: () => swrResponse.mutate({ isOpened: false }),
-  };
-};
-
-export const usePageRenameModalOpened = (): SWRResponse<boolean, Error> => {
-  const { data } = usePageRenameModalStatus();
-  return useSWRImmutable(
-    data != null ? ['isRenameModalOpened', data] : null,
-    () => {
-      return data != null ? data.isOpened : false;
-    },
-  );
-};
-
-
-type DescendantsPageListModalStatus = {
-  isOpened: boolean,
-  path?: string,
-}
-
-type DescendantsPageListUtils = {
-  open(path: string): Promise<DescendantsPageListModalStatus | undefined>
-  close(): Promise<DuplicateModalStatus | undefined>
-}
-
-export const useDescendantsPageListModal = (
-    status?: DescendantsPageListModalStatus,
-): SWRResponse<DescendantsPageListModalStatus, Error> & DescendantsPageListUtils => {
-
-  const initialData: DescendantsPageListModalStatus = { isOpened: false };
-  const swrResponse = useStaticSWR<DescendantsPageListModalStatus, Error>('descendantsPageListModalStatus', status, { fallbackData: initialData });
-
-  return {
-    ...swrResponse,
-    open: (path: string) => swrResponse.mutate({ isOpened: true, path }),
-    close: () => swrResponse.mutate({ isOpened: false }),
-  };
-};
-
-
-export const PageAccessoriesModalContents = {
-  PageHistory: 'PageHistory',
-  Attachment: 'Attachment',
-  ShareLink: 'ShareLink',
-} as const;
-export type PageAccessoriesModalContents = typeof PageAccessoriesModalContents[keyof typeof PageAccessoriesModalContents];
-
-type PageAccessoriesModalStatus = {
-  isOpened: boolean,
-  onOpened?: (initialActivatedContents: PageAccessoriesModalContents) => void,
-}
-
-type PageAccessoriesModalUtils = {
-  open(activatedContents: PageAccessoriesModalContents): void
-  close(): void
-}
-
-export const usePageAccessoriesModal = (): SWRResponse<PageAccessoriesModalStatus, Error> & PageAccessoriesModalUtils => {
-
-  const initialStatus = { isOpened: false };
-  const swrResponse = useStaticSWR<PageAccessoriesModalStatus, Error>('pageAccessoriesModalStatus', undefined, { fallbackData: initialStatus });
-
-  return {
-    ...swrResponse,
-    open: (activatedContents: PageAccessoriesModalContents) => {
-      if (swrResponse.data == null) {
-        return;
-      }
-      swrResponse.mutate({ isOpened: true });
-
-      if (swrResponse.data.onOpened != null) {
-        swrResponse.data.onOpened(activatedContents);
-      }
-    },
-    close: () => {
-      if (swrResponse.data == null) {
-        return;
-      }
-      swrResponse.mutate({ isOpened: false });
-    },
-  };
-};
-
 
 export const useSelectedGrant = (initialData?: Nullable<number>): SWRResponse<Nullable<number>, Error> => {
   return useStaticSWR<Nullable<number>, Error>('grant', initialData);