Taichi Masuyama 4 anni fa
parent
commit
f5f328f9e5

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

@@ -22,7 +22,6 @@ import PageComments from '../components/PageComments';
 import PageContentFooter from '../components/PageContentFooter';
 import PageContentFooter from '../components/PageContentFooter';
 import PageTimeline from '../components/PageTimeline';
 import PageTimeline from '../components/PageTimeline';
 import CommentEditorLazyRenderer from '../components/PageComment/CommentEditorLazyRenderer';
 import CommentEditorLazyRenderer from '../components/PageComment/CommentEditorLazyRenderer';
-import PageManagement from '../components/Page/PageManagement';
 import ShareLinkAlert from '../components/Page/ShareLinkAlert';
 import ShareLinkAlert from '../components/Page/ShareLinkAlert';
 import DuplicatedAlert from '../components/Page/DuplicatedAlert';
 import DuplicatedAlert from '../components/Page/DuplicatedAlert';
 import RedirectedAlert from '../components/Page/RedirectedAlert';
 import RedirectedAlert from '../components/Page/RedirectedAlert';
@@ -127,7 +126,6 @@ if (pageContainer.state.pageId != null) {
   Object.assign(componentMappings, {
   Object.assign(componentMappings, {
     'page-comments-list': <PageComments />,
     'page-comments-list': <PageComments />,
     'page-comment-write': <CommentEditorLazyRenderer />,
     'page-comment-write': <CommentEditorLazyRenderer />,
-    'page-management': <PageManagement />,
     'page-content-footer': <PageContentFooter />,
     'page-content-footer': <PageContentFooter />,
 
 
     'recent-created-icon': <RecentlyCreatedIcon />,
     'recent-created-icon': <RecentlyCreatedIcon />,

+ 86 - 25
packages/app/src/components/DescendantsPageList.tsx

@@ -1,39 +1,49 @@
-import React, { useState } from 'react';
+import React, { useCallback, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { toastSuccess } from '~/client/util/apiNotification';
 import {
 import {
   IPageHasId, IPageWithMeta,
   IPageHasId, IPageWithMeta,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
 import { IPagingResult } from '~/interfaces/paging-result';
 import { IPagingResult } from '~/interfaces/paging-result';
-import { useCurrentPagePath, useIsGuestUser, useIsSharedUser } from '~/stores/context';
+import { OnDeletedFunction } from '~/interfaces/ui';
+import { useIsGuestUser, useIsSharedUser } from '~/stores/context';
 
 
-import { useSWRxPageInfoForList, useSWRxPageList } from '~/stores/page';
+import { useSWRxDescendantsPageListForCurrrentPath, useSWRxPageInfoForList, useSWRxPageList } from '~/stores/page';
+import { usePageTreeTermManager } from '~/stores/page-listing';
 
 
 import PageList from './PageList/PageList';
 import PageList from './PageList/PageList';
 import PaginationWrapper from './PaginationWrapper';
 import PaginationWrapper from './PaginationWrapper';
 
 
-type Props = {
-  path: string,
-}
 
 
+type SubstanceProps = {
+  pagingResult: IPagingResult<IPageHasId> | undefined,
+  activePage: number,
+  setActivePage: (activePage: number) => void,
+  onPagesDeleted?: OnDeletedFunction,
+}
 
 
 const convertToIPageWithEmptyMeta = (page: IPageHasId): IPageWithMeta => {
 const convertToIPageWithEmptyMeta = (page: IPageHasId): IPageWithMeta => {
   return { pageData: page };
   return { pageData: page };
 };
 };
 
 
-export const DescendantsPageList = (props: Props): JSX.Element => {
-  const { path } = props;
+export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
 
 
-  const [activePage, setActivePage] = useState(1);
+  const { t } = useTranslation();
 
 
-  const { data: isGuestUser } = useIsGuestUser();
-  const { data: isSharedUser } = useIsSharedUser();
+  const {
+    pagingResult, activePage, setActivePage, onPagesDeleted,
+  } = props;
 
 
-  const { data: pagingResult, error } = useSWRxPageList(isSharedUser ? null : path, activePage);
+  const { data: isGuestUser } = useIsGuestUser();
 
 
   const pageIds = pagingResult?.items?.map(page => page._id);
   const pageIds = pagingResult?.items?.map(page => page._id);
   const { data: idToPageInfo } = useSWRxPageInfoForList(pageIds);
   const { data: idToPageInfo } = useSWRxPageInfoForList(pageIds);
 
 
   let pagingResultWithMeta: IPagingResult<IPageWithMeta> | undefined;
   let pagingResultWithMeta: IPagingResult<IPageWithMeta> | undefined;
 
 
+  // for mutation
+  const { advance: advancePt } = usePageTreeTermManager();
+
   // initial data
   // initial data
   if (pagingResult != null) {
   if (pagingResult != null) {
     const pages = pagingResult.items;
     const pages = pagingResult.items;
@@ -64,18 +74,20 @@ export const DescendantsPageList = (props: Props): JSX.Element => {
     };
     };
   }
   }
 
 
+  const pageDeletedHandler: OnDeletedFunction = useCallback((...args) => {
+    toastSuccess(args[2] ? t('deleted_pages_completely') : t('deleted_pages'));
+
+    advancePt();
+
+    if (onPagesDeleted != null) {
+      onPagesDeleted(...args);
+    }
+  }, [advancePt, onPagesDeleted, t]);
+
   function setPageNumber(selectedPageNumber) {
   function setPageNumber(selectedPageNumber) {
     setActivePage(selectedPageNumber);
     setActivePage(selectedPageNumber);
   }
   }
 
 
-  if (error != null) {
-    return (
-      <div className="my-5">
-        <div className="text-danger">{error.message}</div>
-      </div>
-    );
-  }
-
   if (pagingResult == null || pagingResultWithMeta == null) {
   if (pagingResult == null || pagingResultWithMeta == null) {
     return (
     return (
       <div className="wiki">
       <div className="wiki">
@@ -90,7 +102,11 @@ export const DescendantsPageList = (props: Props): JSX.Element => {
 
 
   return (
   return (
     <>
     <>
-      <PageList pages={pagingResultWithMeta} isEnableActions={!isGuestUser} />
+      <PageList
+        pages={pagingResultWithMeta}
+        isEnableActions={!isGuestUser}
+        onPagesDeleted={pageDeletedHandler}
+      />
 
 
       { showPager && (
       { showPager && (
         <div className="my-4">
         <div className="my-4">
@@ -107,12 +123,57 @@ export const DescendantsPageList = (props: Props): JSX.Element => {
   );
   );
 };
 };
 
 
+type Props = {
+  path: string,
+}
+
+export const DescendantsPageList = (props: Props): JSX.Element => {
+  const { path } = props;
+
+  const [activePage, setActivePage] = useState(1);
+
+  const { data: isSharedUser } = useIsSharedUser();
+
+  const { data: pagingResult, error, mutate } = useSWRxPageList(isSharedUser ? null : path, activePage);
+
+  if (error != null) {
+    return (
+      <div className="my-5">
+        <div className="text-danger">{error.message}</div>
+      </div>
+    );
+  }
+
+  return (
+    <DescendantsPageListSubstance
+      pagingResult={pagingResult}
+      activePage={activePage}
+      setActivePage={setActivePage}
+      onPagesDeleted={() => mutate()}
+    />
+  );
+};
+
 export const DescendantsPageListForCurrentPath = (): JSX.Element => {
 export const DescendantsPageListForCurrentPath = (): JSX.Element => {
 
 
-  const { data: path } = useCurrentPagePath();
+  const [activePage, setActivePage] = useState(1);
+  const { data: pagingResult, error, mutate } = useSWRxDescendantsPageListForCurrrentPath(activePage);
+
+  if (error != null) {
+    return (
+      <div className="my-5">
+        <div className="text-danger">{error.message}</div>
+      </div>
+    );
+  }
 
 
-  return path != null
-    ? <DescendantsPageList path={path} />
-    : <></>;
+  return (
+    <DescendantsPageListSubstance
+      pagingResult={pagingResult}
+      activePage={activePage}
+      setActivePage={setActivePage}
+      onPagesDeleted={() => mutate()}
+    />
+  );
 
 
 };
 };

+ 4 - 2
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -5,6 +5,9 @@ import PropTypes from 'prop-types';
 
 
 import { DropdownItem } from 'reactstrap';
 import { DropdownItem } from 'reactstrap';
 
 
+import { OnDeletedFunction } from '~/interfaces/ui';
+import { IPageHasId } from '~/interfaces/page';
+
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import EditorContainer from '~/client/services/EditorContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import {
 import {
@@ -13,7 +16,7 @@ import {
 } from '~/stores/ui';
 } from '~/stores/ui';
 import {
 import {
   usePageAccessoriesModal, PageAccessoriesModalContents,
   usePageAccessoriesModal, PageAccessoriesModalContents,
-  usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, OnDeletedFunction, usePagePresentationModal, IPageForPageDeleteModal,
+  usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal, IPageForPageDeleteModal,
 } from '~/stores/modal';
 } from '~/stores/modal';
 
 
 
 
@@ -26,7 +29,6 @@ import { useSWRTagsInfo } from '~/stores/page';
 
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiPost } from '~/client/util/apiv1-client';
 import { apiPost } from '~/client/util/apiv1-client';
-import { IPageHasId } from '~/interfaces/page';
 
 
 import HistoryIcon from '../Icons/HistoryIcon';
 import HistoryIcon from '../Icons/HistoryIcon';
 import AttachmentIcon from '../Icons/AttachmentIcon';
 import AttachmentIcon from '../Icons/AttachmentIcon';

+ 0 - 264
packages/app/src/components/Page/PageManagement.jsx

@@ -1,264 +0,0 @@
-import React, { useState } from 'react';
-import PropTypes from 'prop-types';
-import { UncontrolledTooltip } from 'reactstrap';
-import { withTranslation } from 'react-i18next';
-import urljoin from 'url-join';
-
-import { pagePathUtils } from '@growi/core';
-import { usePageDeleteModal } from '~/stores/modal';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import PageRenameModal from '../PageRenameModal';
-import PageDuplicateModal from '../PageDuplicateModal';
-import CreateTemplateModal from '../CreateTemplateModal';
-import PagePresentationModal from '../PagePresentationModal';
-import PresentationIcon from '../Icons/PresentationIcon';
-
-const { isTopPage } = pagePathUtils;
-
-
-const LegacyPageManagemenet = (props) => {
-  const {
-    t, appContainer, isCompactMode, pageId, revisionId, path, isDeletable, isAbleToDeleteCompletely,
-  } = props;
-
-  const { open: openDeleteModal } = usePageDeleteModal();
-
-  const { currentUser } = appContainer;
-  const isTopPagePath = isTopPage(path);
-  const [isPageRenameModalShown, setIsPageRenameModalShown] = useState(false);
-  const [isPageDuplicateModalShown, setIsPageDuplicateModalShown] = useState(false);
-  const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false);
-  const [isPagePresentationModalShown, setIsPagePresentationModalShown] = useState(false);
-  const presentationHref = urljoin(window.location.origin, path, '?presentation=1');
-
-  function openPageRenameModalHandler() {
-    setIsPageRenameModalShown(true);
-  }
-
-  function closePageRenameModalHandler() {
-    setIsPageRenameModalShown(false);
-  }
-
-  function openPageDuplicateModalHandler() {
-    setIsPageDuplicateModalShown(true);
-  }
-  function closePageDuplicateModalHandler() {
-    setIsPageDuplicateModalShown(false);
-  }
-
-  function openPageTemplateModalHandler() {
-    setIsPageTempleteModalShown(true);
-  }
-
-  function closePageTemplateModalHandler() {
-    setIsPageTempleteModalShown(false);
-  }
-
-  function openPagePresentationModalHandler() {
-    setIsPagePresentationModalShown(true);
-  }
-
-  function closePagePresentationModalHandler() {
-    setIsPagePresentationModalShown(false);
-  }
-
-
-  // TODO GW-2746 bulk export pages
-  // async function getArchivePageData() {
-  //   try {
-  //     const res = await appContainer.apiv3Get('page/count-children-pages', { pageId });
-  //     setTotalPages(res.data.dummy);
-  //   }
-  //   catch (err) {
-  //     setErrorMessage(t('export_bulk.failed_to_count_pages'));
-  //   }
-  // }
-
-  async function exportPageHandler(format) {
-    const url = new URL(urljoin(window.location.origin, '_api/v3/page/export', pageId));
-    url.searchParams.append('format', format);
-    url.searchParams.append('revisionId', revisionId);
-    window.location.href = url.href;
-  }
-
-  // TODO GW-2746 create api to bulk export pages
-  // function openArchiveModalHandler() {
-  //   setIsArchiveCreateModalShown(true);
-  //   getArchivePageData();
-  // }
-
-  // TODO GW-2746 create api to bulk export pages
-  // function closeArchiveCreateModalHandler() {
-  //   setIsArchiveCreateModalShown(false);
-  // }
-
-  function renderDropdownItemForTopPage() {
-    return (
-      <>
-        <button className="dropdown-item" type="button" onClick={openPageDuplicateModalHandler}>
-          <i className="icon-fw icon-docs"></i> { t('Duplicate') }
-        </button>
-        {/* TODO Presentation Mode is not function. So if it is really necessary, survey this cause and implement Presentation Mode in top page */}
-        {/* <button className="dropdown-item" type="button" onClick={openPagePresentationModalHandler}>
-          <i className="icon-fw"><PresentationIcon /></i><span className="d-none d-sm-inline"> { t('Presentation Mode') }</span>
-        </button> */}
-        <button type="button" className="dropdown-item" onClick={() => { exportPageHandler('md') }}>
-          <i className="icon-fw icon-cloud-download"></i>{t('export_bulk.export_page_markdown')}
-        </button>
-        <div className="dropdown-divider"></div>
-      </>
-    );
-  }
-
-  function renderDropdownItemForNotTopPage() {
-    return (
-      <>
-        <button className="dropdown-item" type="button" onClick={openPageRenameModalHandler}>
-          <i className="icon-fw icon-action-redo"></i> { t('Move/Rename') }
-        </button>
-        <button className="dropdown-item" type="button" onClick={openPageDuplicateModalHandler}>
-          <i className="icon-fw icon-docs"></i> { t('Duplicate') }
-        </button>
-        <button className="dropdown-item" type="button" onClick={openPagePresentationModalHandler}>
-          <i className="icon-fw"><PresentationIcon /></i> { t('Presentation Mode') }
-        </button>
-        <button className="dropdown-item" type="button" onClick={() => { exportPageHandler('md') }}>
-          <i className="icon-fw icon-cloud-download"></i>{t('export_bulk.export_page_markdown')}
-        </button>
-        {/* TODO GW-2746 create api to bulk export pages */}
-        {/* <button className="dropdown-item" type="button" onClick={openArchiveModalHandler}>
-          <i className="icon-fw"></i>{t('Create Archive Page')}
-        </button> */}
-        <div className="dropdown-divider"></div>
-      </>
-    );
-  }
-
-  function generatePageObjectToDelete() {
-    return { pageId, revisionId, path };
-  }
-  const pageToDelete = generatePageObjectToDelete();
-
-  function renderDropdownItemForDeletablePage() {
-    return (
-      <>
-        <div className="dropdown-divider"></div>
-        <button className="dropdown-item text-danger" type="button" onClick={() => openDeleteModal([pageToDelete])}>
-          <i className="icon-fw icon-fire"></i> { t('Delete') }
-        </button>
-      </>
-    );
-  }
-
-
-  function renderModals() {
-    if (currentUser == null) {
-      return null;
-    }
-
-    return (
-      <>
-        <PageRenameModal
-          isOpen={isPageRenameModalShown}
-          onClose={closePageRenameModalHandler}
-          pageId={pageId}
-          revisionId={revisionId}
-          path={path}
-        />
-        <PageDuplicateModal
-          isOpen={isPageDuplicateModalShown}
-          onClose={closePageDuplicateModalHandler}
-          pageId={pageId}
-          path={path}
-        />
-        <CreateTemplateModal
-          path={path}
-          isOpen={isPageTemplateModalShown}
-          onClose={closePageTemplateModalHandler}
-        />
-        <PagePresentationModal
-          isOpen={isPagePresentationModalShown}
-          onClose={closePagePresentationModalHandler}
-          href={presentationHref}
-        />
-      </>
-    );
-  }
-
-  function renderDotsIconForCurrentUser() {
-    return (
-      <>
-        <button
-          type="button"
-          className="btn-link nav-link dropdown-toggle dropdown-toggle-no-caret border-0 rounded btn-page-item-control"
-          data-toggle="dropdown"
-        >
-          <i className="text-muted icon-options"></i>
-        </button>
-      </>
-    );
-  }
-
-  function renderDotsIconForGuestUser() {
-    return (
-      <>
-        <button
-          type="button"
-          className="btn nav-link bg-transparent dropdown-toggle dropdown-toggle-no-caret disabled"
-          id="icon-options-guest-tltips"
-        >
-          <i className="text-muted icon-options"></i>
-        </button>
-        <UncontrolledTooltip placement="top" target="icon-options-guest-tltips" fade={false}>
-          {t('Not available for guest')}
-        </UncontrolledTooltip>
-      </>
-    );
-  }
-
-
-  return (
-    <>
-      {currentUser == null ? renderDotsIconForGuestUser() : renderDotsIconForCurrentUser()}
-      <div className="dropdown-menu dropdown-menu-right">
-        {isTopPagePath ? renderDropdownItemForTopPage() : renderDropdownItemForNotTopPage()}
-        <button className="dropdown-item" type="button" onClick={openPageTemplateModalHandler}>
-          <i className="icon-fw icon-magic-wand"></i> { t('template.option_label.create/edit') }
-        </button>
-        {(!isTopPagePath && isDeletable) && renderDropdownItemForDeletablePage()}
-      </div>
-      {renderModals()}
-    </>
-  );
-};
-
-/**
- * Wrapper component for using unstated
- */
-const LegacyPageManagemenetWrapper = withUnstatedContainers(LegacyPageManagemenet, [AppContainer]);
-
-
-LegacyPageManagemenet.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-
-  pageId: PropTypes.string.isRequired,
-  revisionId: PropTypes.string.isRequired,
-  path: PropTypes.string.isRequired,
-  isDeletable: PropTypes.bool.isRequired,
-  isAbleToDeleteCompletely: PropTypes.bool,
-
-  isCompactMode: PropTypes.bool,
-};
-
-LegacyPageManagemenet.defaultProps = {
-  isCompactMode: false,
-};
-
-const PageManagement = (props) => {
-  return <LegacyPageManagemenetWrapper {...props}></LegacyPageManagemenetWrapper>;
-};
-export default withTranslation()(PageManagement);

+ 9 - 2
packages/app/src/components/PageList/PageList.tsx

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
 
 
 import { IPageWithMeta } from '~/interfaces/page';
 import { IPageWithMeta } from '~/interfaces/page';
 import { IPagingResult } from '~/interfaces/paging-result';
 import { IPagingResult } from '~/interfaces/paging-result';
+import { OnDeletedFunction } from '~/interfaces/ui';
 
 
 import { PageListItemL } from './PageListItemL';
 import { PageListItemL } from './PageListItemL';
 
 
@@ -10,11 +11,12 @@ import { PageListItemL } from './PageListItemL';
 type Props = {
 type Props = {
   pages: IPagingResult<IPageWithMeta>,
   pages: IPagingResult<IPageWithMeta>,
   isEnableActions?: boolean,
   isEnableActions?: boolean,
+  onPagesDeleted?: OnDeletedFunction,
 }
 }
 
 
 const PageList = (props: Props): JSX.Element => {
 const PageList = (props: Props): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { pages, isEnableActions } = props;
+  const { pages, isEnableActions, onPagesDeleted } = props;
 
 
   if (pages == null) {
   if (pages == null) {
     return (
     return (
@@ -27,7 +29,12 @@ const PageList = (props: Props): JSX.Element => {
   }
   }
 
 
   const pageList = pages.items.map(page => (
   const pageList = pages.items.map(page => (
-    <PageListItemL key={page.pageData._id} page={page} isEnableActions={isEnableActions} />
+    <PageListItemL
+      key={page.pageData._id}
+      page={page}
+      isEnableActions={isEnableActions}
+      onPageDeleted={onPagesDeleted}
+    />
   ));
   ));
 
 
   if (pageList.length === 0) {
   if (pageList.length === 0) {

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

@@ -19,6 +19,7 @@ import {
   IPageInfoAll, IPageWithMeta, isIPageInfoForEntity, isIPageInfoForListing,
   IPageInfoAll, IPageWithMeta, isIPageInfoForEntity, isIPageInfoForListing,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
 import { IPageSearchMeta, isIPageSearchMeta } from '~/interfaces/search';
 import { IPageSearchMeta, isIPageSearchMeta } from '~/interfaces/search';
+import { OnDeletedFunction } from '~/interfaces/ui';
 
 
 import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItemControl';
 import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItemControl';
 import LinkedPagePath from '~/models/linked-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
@@ -33,6 +34,7 @@ type Props = {
   showPageUpdatedTime?: boolean, // whether to show page's updated time at the top-right corner of item
   showPageUpdatedTime?: boolean, // whether to show page's updated time at the top-right corner of item
   onCheckboxChanged?: (isChecked: boolean, pageId: string) => void,
   onCheckboxChanged?: (isChecked: boolean, pageId: string) => void,
   onClickItem?: (pageId: string) => void,
   onClickItem?: (pageId: string) => void,
+  onPageDeleted?: OnDeletedFunction,
 }
 }
 
 
 const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (props: Props, ref): JSX.Element => {
 const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (props: Props, ref): JSX.Element => {
@@ -41,7 +43,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
     page: { pageData, pageMeta }, isSelected, isEnableActions,
     page: { pageData, pageMeta }, isSelected, isEnableActions,
     forceHideMenuItems,
     forceHideMenuItems,
     showPageUpdatedTime,
     showPageUpdatedTime,
-    onClickItem, onCheckboxChanged,
+    onClickItem, onCheckboxChanged, onPageDeleted,
   } = props;
   } = props;
 
 
   const inputRef = useRef<HTMLInputElement>(null);
   const inputRef = useRef<HTMLInputElement>(null);
@@ -107,8 +109,10 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
     const pageToDelete = {
     const pageToDelete = {
       pageId, revisionId: revisionId as string, path, isAbleToDeleteCompletely,
       pageId, revisionId: revisionId as string, path, isAbleToDeleteCompletely,
     };
     };
-    openDeleteModal([pageToDelete]);
-  }, [pageData, openDeleteModal]);
+
+    // open modal
+    openDeleteModal([pageToDelete], { onDeleted: onPageDeleted });
+  }, [pageData, openDeleteModal, onPageDeleted]);
 
 
   const revertMenuItemClickHandler = useCallback(() => {
   const revertMenuItemClickHandler = useCallback(() => {
     const { _id: pageId, path } = pageData;
     const { _id: pageId, path } = pageData;
@@ -170,6 +174,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
                         <a
                         <a
                           className="page-segment"
                           className="page-segment"
                           href={encodeURI(urljoin('/', pageData._id))}
                           href={encodeURI(urljoin('/', pageData._id))}
+                          // eslint-disable-next-line react/no-danger
                           dangerouslySetInnerHTML={{ __html: linkedPagePathLatter.pathName }}
                           dangerouslySetInnerHTML={{ __html: linkedPagePathLatter.pathName }}
                         >
                         >
                         </a>
                         </a>

+ 10 - 5
packages/app/src/components/PrivateLegacyPages.tsx

@@ -1,5 +1,5 @@
 import React, {
 import React, {
-  useCallback, useEffect, useMemo, useRef, useState,
+  useCallback, useMemo, useRef, useState,
 } from 'react';
 } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
@@ -14,12 +14,14 @@ import { toastSuccess } from '~/client/util/apiNotification';
 import {
 import {
   ISearchConfigurations, useSWRxNamedQuerySearch,
   ISearchConfigurations, useSWRxNamedQuerySearch,
 } from '~/stores/search';
 } from '~/stores/search';
-import { ILegacyPrivatePage, useLegacyPrivatePagesMigrationModal } from '~/stores/modal';
+import {
+  ILegacyPrivatePage, useLegacyPrivatePagesMigrationModal,
+} from '~/stores/modal';
 
 
 import PaginationWrapper from './PaginationWrapper';
 import PaginationWrapper from './PaginationWrapper';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 
 
-import { IReturnSelectedPageIds, SearchPageBase } from './SearchPage2/SearchPageBase';
+import { IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage2/SearchPageBase';
 import { MenuItemType } from './Common/Dropdown/PageItemControl';
 import { MenuItemType } from './Common/Dropdown/PageItemControl';
 import { LegacyPrivatePagesMigrationModal } from './LegacyPrivatePagesMigrationModal';
 import { LegacyPrivatePagesMigrationModal } from './LegacyPrivatePagesMigrationModal';
 
 
@@ -176,6 +178,9 @@ export const PrivateLegacyPages = (props: Props): JSX.Element => {
     }
     }
   }, []);
   }, []);
 
 
+  // for bulk deletion
+  const deleteAllButtonClickedHandler = usePageDeleteModalForBulkDeletion(data, searchPageBaseRef, () => mutate);
+
   const convertMenuItemClickedHandler = useCallback(() => {
   const convertMenuItemClickedHandler = useCallback(() => {
     if (data == null) {
     if (data == null) {
       return;
       return;
@@ -238,7 +243,7 @@ export const PrivateLegacyPages = (props: Props): JSX.Element => {
                     <i className="icon-fw icon-refresh"></i>
                     <i className="icon-fw icon-refresh"></i>
                     {t('private_legacy_pages.convert_all_selected_pages')}
                     {t('private_legacy_pages.convert_all_selected_pages')}
                   </DropdownItem>
                   </DropdownItem>
-                  <DropdownItem onClick={() => { /* TODO: implement */ }}>
+                  <DropdownItem onClick={deleteAllButtonClickedHandler}>
                     <span className="text-danger">
                     <span className="text-danger">
                       <i className="icon-fw icon-trash"></i>
                       <i className="icon-fw icon-trash"></i>
                       {t('search_result.delete_all_selected_page')}
                       {t('search_result.delete_all_selected_page')}
@@ -251,7 +256,7 @@ export const PrivateLegacyPages = (props: Props): JSX.Element => {
         </div>
         </div>
       </div>
       </div>
     );
     );
-  }, [convertMenuItemClickedHandler, hitsCount, isControlEnabled, selectAllCheckboxChangedHandler, t]);
+  }, [convertMenuItemClickedHandler, deleteAllButtonClickedHandler, hitsCount, isControlEnabled, selectAllCheckboxChangedHandler, t]);
 
 
   const searchResultListHead = useMemo(() => {
   const searchResultListHead = useMemo(() => {
     if (data == null) {
     if (data == null) {

+ 20 - 8
packages/app/src/components/SearchPage.tsx

@@ -15,7 +15,7 @@ import PaginationWrapper from './PaginationWrapper';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 import SearchControl from './SearchPage/SearchControl';
 import SearchControl from './SearchPage/SearchControl';
 
 
-import { SearchPageBase } from './SearchPage2/SearchPageBase';
+import { IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage2/SearchPageBase';
 
 
 
 
 // TODO: replace with "customize:showPageLimitationS"
 // TODO: replace with "customize:showPageLimitationS"
@@ -115,11 +115,11 @@ export const SearchPage = (props: Props): JSX.Element => {
   });
   });
 
 
   const selectAllControlRef = useRef<ISelectableAndIndeterminatable|null>(null);
   const selectAllControlRef = useRef<ISelectableAndIndeterminatable|null>(null);
-  const searchPageBaseRef = useRef<ISelectableAll|null>(null);
+  const searchPageBaseRef = useRef<ISelectableAll & IReturnSelectedPageIds|null>(null);
 
 
   const { data: isSearchServiceReachable } = useIsSearchServiceReachable();
   const { data: isSearchServiceReachable } = useIsSearchServiceReachable();
 
 
-  const { data, conditions } = useSWRxFullTextSearch(keyword, {
+  const { data, conditions, mutate } = useSWRxFullTextSearch(keyword, {
     limit: INITIAL_PAGIONG_SIZE,
     limit: INITIAL_PAGIONG_SIZE,
     ...configurationsByControl,
     ...configurationsByControl,
     ...configurationsByPagination,
     ...configurationsByPagination,
@@ -163,13 +163,22 @@ export const SearchPage = (props: Props): JSX.Element => {
     }
     }
   }, []);
   }, []);
 
 
+  const pagingSizeChangedHandler = useCallback((pagingSize: number) => {
+    setConfigurationsByPagination({
+      ...configurationsByPagination,
+      limit: pagingSize,
+    });
+    mutate();
+  }, [configurationsByPagination, mutate]);
+
   const pagingNumberChangedHandler = useCallback((activePage: number) => {
   const pagingNumberChangedHandler = useCallback((activePage: number) => {
     const currentLimit = configurationsByPagination.limit ?? INITIAL_PAGIONG_SIZE;
     const currentLimit = configurationsByPagination.limit ?? INITIAL_PAGIONG_SIZE;
     setConfigurationsByPagination({
     setConfigurationsByPagination({
       ...configurationsByPagination,
       ...configurationsByPagination,
       offset: (activePage - 1) * currentLimit,
       offset: (activePage - 1) * currentLimit,
     });
     });
-  }, [configurationsByPagination]);
+    mutate();
+  }, [configurationsByPagination, mutate]);
 
 
   const initialSearchConditions: Partial<ISearchConditions> = useMemo(() => {
   const initialSearchConditions: Partial<ISearchConditions> = useMemo(() => {
     return {
     return {
@@ -178,6 +187,9 @@ export const SearchPage = (props: Props): JSX.Element => {
     };
     };
   }, [initQ]);
   }, [initQ]);
 
 
+  // for bulk deletion
+  const deleteAllButtonClickedHandler = usePageDeleteModalForBulkDeletion(data, searchPageBaseRef, () => mutate);
+
   // push state
   // push state
   useEffect(() => {
   useEffect(() => {
     const newUrl = new URL('/_search', 'http://example.com');
     const newUrl = new URL('/_search', 'http://example.com');
@@ -201,14 +213,14 @@ export const SearchPage = (props: Props): JSX.Element => {
           type="button"
           type="button"
           className="btn btn-outline-danger border-0 px-2"
           className="btn btn-outline-danger border-0 px-2"
           disabled={isDisabled}
           disabled={isDisabled}
-          onClick={() => null /* TODO implement */}
+          onClick={deleteAllButtonClickedHandler}
         >
         >
           <i className="icon-fw icon-trash"></i>
           <i className="icon-fw icon-trash"></i>
           {t('search_result.delete_all_selected_page')}
           {t('search_result.delete_all_selected_page')}
         </button>
         </button>
       </OperateAllControl>
       </OperateAllControl>
     );
     );
-  }, [hitsCount, selectAllCheckboxChangedHandler, t]);
+  }, [deleteAllButtonClickedHandler, hitsCount, selectAllCheckboxChangedHandler, t]);
 
 
   const searchControl = useMemo(() => {
   const searchControl = useMemo(() => {
     if (!isSearchServiceReachable) {
     if (!isSearchServiceReachable) {
@@ -235,10 +247,10 @@ export const SearchPage = (props: Props): JSX.Element => {
         searchingKeyword={keyword}
         searchingKeyword={keyword}
         offset={offset}
         offset={offset}
         pagingSize={limit}
         pagingSize={limit}
-        onPagingSizeChanged={() => {}}
+        onPagingSizeChanged={pagingSizeChangedHandler}
       />
       />
     );
     );
-  }, [data, keyword, limit, offset]);
+  }, [data, keyword, limit, offset, pagingSizeChangedHandler]);
 
 
   const searchPager = useMemo(() => {
   const searchPager = useMemo(() => {
     // when pager is not needed
     // when pager is not needed

+ 7 - 1
packages/app/src/components/SearchPage/SearchResultList.tsx

@@ -7,6 +7,8 @@ import { IPageWithMeta, isIPageInfoForListing } from '~/interfaces/page';
 import { IPageSearchMeta } from '~/interfaces/search';
 import { IPageSearchMeta } from '~/interfaces/search';
 import { useIsGuestUser } from '~/stores/context';
 import { useIsGuestUser } from '~/stores/context';
 import { useSWRxPageInfoForList } from '~/stores/page';
 import { useSWRxPageInfoForList } from '~/stores/page';
+import { usePageTreeTermManager } from '~/stores/page-listing';
+import { useFullTextSearchTermManager } from '~/stores/search';
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 
 
 import { PageListItemL } from '../PageList/PageListItemL';
 import { PageListItemL } from '../PageList/PageListItemL';
@@ -34,6 +36,10 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: idToPageInfo } = useSWRxPageInfoForList(pageIdsWithNoSnippet);
   const { data: idToPageInfo } = useSWRxPageInfoForList(pageIdsWithNoSnippet);
 
 
+  // for mutation
+  const { advance: advancePt } = usePageTreeTermManager();
+  const { advance: advanceFts } = useFullTextSearchTermManager();
+
   const itemsRef = useRef<(ISelectable|null)[]>([]);
   const itemsRef = useRef<(ISelectable|null)[]>([]);
 
 
   // publish selectAll()
   // publish selectAll()
@@ -59,7 +65,6 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
     }
     }
   }, [onPageSelected, pages]);
   }, [onPageSelected, pages]);
 
 
-
   let injectedPage;
   let injectedPage;
   // inject data to list
   // inject data to list
   if (idToPageInfo != null) {
   if (idToPageInfo != null) {
@@ -95,6 +100,7 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
             forceHideMenuItems={forceHideMenuItems}
             forceHideMenuItems={forceHideMenuItems}
             onClickItem={clickItemHandler}
             onClickItem={clickItemHandler}
             onCheckboxChanged={props.onCheckboxChanged}
             onCheckboxChanged={props.onCheckboxChanged}
+            onPageDeleted={() => { advancePt(); advanceFts() }}
           />
           />
         );
         );
       })}
       })}

+ 68 - 3
packages/app/src/components/SearchPage2/SearchPageBase.tsx

@@ -1,16 +1,22 @@
 import React, {
 import React, {
-  forwardRef, ForwardRefRenderFunction, useEffect, useImperativeHandle, useRef, useState,
+  forwardRef, ForwardRefRenderFunction, useCallback, useEffect, useImperativeHandle, useRef, useState,
 } from 'react';
 } from 'react';
+import { useTranslation } from 'react-i18next';
 import { ISelectableAll } from '~/client/interfaces/selectable-all';
 import { ISelectableAll } from '~/client/interfaces/selectable-all';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
+import { toastSuccess } from '~/client/util/apiNotification';
 import { IPageWithMeta } from '~/interfaces/page';
 import { IPageWithMeta } from '~/interfaces/page';
-import { IPageSearchMeta } from '~/interfaces/search';
+import { IFormattedSearchResult, IPageSearchMeta } from '~/interfaces/search';
+import { OnDeletedFunction } from '~/interfaces/ui';
 import { useIsGuestUser, useIsSearchServiceConfigured, useIsSearchServiceReachable } from '~/stores/context';
 import { useIsGuestUser, useIsSearchServiceConfigured, useIsSearchServiceReachable } from '~/stores/context';
+import { IPageForPageDeleteModal, usePageDeleteModal } from '~/stores/modal';
+import { usePageTreeTermManager } from '~/stores/page-listing';
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 
 
 import { SearchResultContent } from '../SearchPage/SearchResultContent';
 import { SearchResultContent } from '../SearchPage/SearchResultContent';
 import { SearchResultList } from '../SearchPage/SearchResultList';
 import { SearchResultList } from '../SearchPage/SearchResultList';
 
 
+
 export interface IReturnSelectedPageIds {
 export interface IReturnSelectedPageIds {
   getSelectedPageIds?: () => Set<string>,
   getSelectedPageIds?: () => Set<string>,
 }
 }
@@ -26,7 +32,7 @@ type Props = {
   onSelectedPagesByCheckboxesChanged?: (selectedCount: number, totalCount: number) => void,
   onSelectedPagesByCheckboxesChanged?: (selectedCount: number, totalCount: number) => void,
 
 
   searchControl: React.ReactNode,
   searchControl: React.ReactNode,
-  searchResultListHead: React.ReactNode,
+  searchResultListHead: React.ReactElement,
   searchPager: React.ReactNode,
   searchPager: React.ReactNode,
 }
 }
 
 
@@ -117,6 +123,11 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
     }
     }
   }, [onSelectedPagesByCheckboxesChanged, pages, selectedPageIdsByCheckboxes]);
   }, [onSelectedPagesByCheckboxesChanged, pages, selectedPageIdsByCheckboxes]);
 
 
+  useEffect(() => {
+    if (searchResultListHead != null && searchResultListHead.props != null) {
+      setHightlightKeywords(searchResultListHead.props.searchingKeyword);
+    }
+  }, [searchResultListHead]);
   if (!isSearchServiceConfigured) {
   if (!isSearchServiceConfigured) {
     return (
     return (
       <div className="grw-container-convertible">
       <div className="grw-container-convertible">
@@ -206,4 +217,58 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
 };
 };
 
 
 
 
+type VoidFunction = () => void;
+
+export const usePageDeleteModalForBulkDeletion = (
+    data: IFormattedSearchResult | undefined,
+    ref: React.MutableRefObject<(ISelectableAll & IReturnSelectedPageIds) | null>,
+    onDeleted?: OnDeletedFunction,
+): VoidFunction => {
+
+  const { t } = useTranslation();
+
+  const { open: openDeleteModal } = usePageDeleteModal();
+
+  // for PageTree mutation
+  const { advance: advancePt } = usePageTreeTermManager();
+
+  return () => {
+    if (data == null) {
+      return;
+    }
+
+    const instance = ref.current;
+    if (instance == null || instance.getSelectedPageIds == null) {
+      return;
+    }
+
+    const selectedPageIds = instance.getSelectedPageIds();
+
+    if (selectedPageIds.size === 0) {
+      return;
+    }
+
+    const selectedPages = data.data
+      .filter(pageWithMeta => selectedPageIds.has(pageWithMeta.pageData._id))
+      .map(pageWithMeta => ({
+        pageId: pageWithMeta.pageData._id,
+        path: pageWithMeta.pageData.path,
+        revisionId: pageWithMeta.pageData.revision as string,
+      } as IPageForPageDeleteModal));
+
+    openDeleteModal(selectedPages, {
+      onDeleted: (...args) => {
+        toastSuccess(args[2] ? t('deleted_pages_completely') : t('deleted_pages'));
+        advancePt();
+
+        if (onDeleted != null) {
+          onDeleted(...args);
+        }
+      },
+    });
+  };
+
+};
+
+
 export const SearchPageBase = forwardRef(SearchPageBaseSubstance);
 export const SearchPageBase = forwardRef(SearchPageBaseSubstance);

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

@@ -30,8 +30,7 @@ interface ItemProps {
   isEnabledAttachTitleHeader?: boolean
   isEnabledAttachTitleHeader?: boolean
   onClickDuplicateMenuItem?(pageId: string, path: string): void
   onClickDuplicateMenuItem?(pageId: string, path: string): void
   onClickRenameMenuItem?(pageId: string, revisionId: string, path: string): void
   onClickRenameMenuItem?(pageId: string, revisionId: string, path: string): void
-  onClickDeleteMenuItem?(pageToDelete: IPageForPageDeleteModal, callback?: VoidFunction): void
-  onSelfDeleted?: VoidFunction
+  onClickDeleteMenuItem?(pageToDelete: IPageForPageDeleteModal): void
 }
 }
 
 
 // Utility to mark target
 // Utility to mark target
@@ -73,7 +72,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const {
   const {
     itemNode, targetPathOrId, isOpen: _isOpen = false, isEnabledAttachTitleHeader,
     itemNode, targetPathOrId, isOpen: _isOpen = false, isEnabledAttachTitleHeader,
-    onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, isEnableActions, onSelfDeleted,
+    onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, isEnableActions,
   } = props;
   } = props;
 
 
   const { page, children } = itemNode;
   const { page, children } = itemNode;
@@ -242,10 +241,6 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   }, [onClickRenameMenuItem, page]);
   }, [onClickRenameMenuItem, page]);
 
 
   const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo): Promise<void> => {
   const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo): Promise<void> => {
-    if (onClickDeleteMenuItem == null) {
-      return;
-    }
-
     const { _id: pageId, revision: revisionId, path } = page;
     const { _id: pageId, revision: revisionId, path } = page;
 
 
     if (pageId == null || revisionId == null || path == null) {
     if (pageId == null || revisionId == null || path == null) {
@@ -259,10 +254,10 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
       isAbleToDeleteCompletely: pageInfo?.isAbleToDeleteCompletely,
       isAbleToDeleteCompletely: pageInfo?.isAbleToDeleteCompletely,
     };
     };
 
 
-    onClickDeleteMenuItem(pageToDelete, async() => {
-      if (onSelfDeleted != null) await onSelfDeleted();
-    });
-  }, [onClickDeleteMenuItem, page, onSelfDeleted]);
+    if (onClickDeleteMenuItem != null) {
+      onClickDeleteMenuItem(pageToDelete);
+    }
+  }, [onClickDeleteMenuItem, page]);
 
 
   const onPressEnterForCreateHandler = async(inputText: string) => {
   const onPressEnterForCreateHandler = async(inputText: string) => {
     setNewPageInputShown(false);
     setNewPageInputShown(false);
@@ -424,7 +419,6 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
               onClickDuplicateMenuItem={onClickDuplicateMenuItem}
               onClickDuplicateMenuItem={onClickDuplicateMenuItem}
               onClickRenameMenuItem={onClickRenameMenuItem}
               onClickRenameMenuItem={onClickRenameMenuItem}
               onClickDeleteMenuItem={onClickDeleteMenuItem}
               onClickDeleteMenuItem={onClickDeleteMenuItem}
-              onSelfDeleted={async() => { await mutateChildren() }}
             />
             />
           </div>
           </div>
         ))
         ))

+ 16 - 6
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -4,15 +4,18 @@ import { useTranslation } from 'react-i18next';
 import { IPageHasId } from '../../../interfaces/page';
 import { IPageHasId } from '../../../interfaces/page';
 import { ItemNode } from './ItemNode';
 import { ItemNode } from './ItemNode';
 import Item from './Item';
 import Item from './Item';
-import { useSWRxPageAncestorsChildren, useSWRxRootPage } from '~/stores/page-listing';
+import { usePageTreeTermManager, useSWRxPageAncestorsChildren, useSWRxRootPage } from '~/stores/page-listing';
 import { TargetAndAncestors } from '~/interfaces/page-listing-results';
 import { TargetAndAncestors } from '~/interfaces/page-listing-results';
+import { OnDeletedFunction } from '~/interfaces/ui';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import {
 import {
-  OnDeletedFunction, IPageForPageDeleteModal, usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
+  IPageForPageDeleteModal, usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
 } from '~/stores/modal';
 } from '~/stores/modal';
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 
 
 import { useIsEnabledAttachTitleHeader } from '~/stores/context';
 import { useIsEnabledAttachTitleHeader } from '~/stores/context';
+import { useFullTextSearchTermManager } from '~/stores/search';
+import { useDescendantsPageListForCurrentPathTermManager } from '~/stores/page';
 
 
 /*
 /*
  * Utility to generate initial node
  * Utility to generate initial node
@@ -67,7 +70,7 @@ const renderByInitialNode = (
     isEnabledAttachTitleHeader?: boolean,
     isEnabledAttachTitleHeader?: boolean,
     onClickDuplicateMenuItem?: (pageId: string, path: string) => void,
     onClickDuplicateMenuItem?: (pageId: string, path: string) => void,
     onClickRenameMenuItem?: (pageId: string, revisionId: string, path: string) => void,
     onClickRenameMenuItem?: (pageId: string, revisionId: string, path: string) => void,
-    onClickDeleteMenuItem?: (pageToDelete: IPageForPageDeleteModal, onItemDeleted: VoidFunction) => void,
+    onClickDeleteMenuItem?: (pageToDelete: IPageForPageDeleteModal) => void,
 ): JSX.Element => {
 ): JSX.Element => {
 
 
   return (
   return (
@@ -105,6 +108,11 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
   const { open: openRenameModal } = usePageRenameModal();
   const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
 
 
+  // for mutation
+  const { advance: advancePt } = usePageTreeTermManager();
+  const { advance: advanceFts } = useFullTextSearchTermManager();
+  const { advance: advanceDpl } = useDescendantsPageListForCurrentPathTermManager();
+
   useEffect(() => {
   useEffect(() => {
     const startFrom = document.getElementById('grw-sidebar-contents-scroll-target');
     const startFrom = document.getElementById('grw-sidebar-contents-scroll-target');
     const targetElem = document.getElementsByClassName('grw-pagetree-is-target');
     const targetElem = document.getElementsByClassName('grw-pagetree-is-target');
@@ -122,14 +130,12 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
     openRenameModal(pageId, revisionId, path);
     openRenameModal(pageId, revisionId, path);
   };
   };
 
 
-  const onClickDeleteMenuItem = (pageToDelete: IPageForPageDeleteModal, onItemDeleted: VoidFunction) => {
+  const onClickDeleteMenuItem = (pageToDelete: IPageForPageDeleteModal) => {
     const onDeletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => {
     const onDeletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => {
       if (typeof pathOrPathsToDelete !== 'string') {
       if (typeof pathOrPathsToDelete !== 'string') {
         return;
         return;
       }
       }
 
 
-      onItemDeleted();
-
       const path = pathOrPathsToDelete;
       const path = pathOrPathsToDelete;
 
 
       if (isCompletely) {
       if (isCompletely) {
@@ -138,6 +144,10 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
       else {
       else {
         toastSuccess(t('deleted_pages', { path }));
         toastSuccess(t('deleted_pages', { path }));
       }
       }
+
+      advancePt();
+      advanceFts();
+      advanceDpl();
     };
     };
 
 
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });

+ 5 - 0
packages/app/src/interfaces/ui.ts

@@ -1,3 +1,5 @@
+import { Nullable } from './common';
+
 export const SidebarContentsType = {
 export const SidebarContentsType = {
   CUSTOM: 'custom',
   CUSTOM: 'custom',
   RECENT: 'recent',
   RECENT: 'recent',
@@ -17,3 +19,6 @@ export type ICustomTabContent = {
 };
 };
 
 
 export type ICustomNavTabMappings = { [key: string]: ICustomTabContent };
 export type ICustomNavTabMappings = { [key: string]: ICustomTabContent };
+
+
+export type OnDeletedFunction = (idOrPaths: string | string[], isRecursively: Nullable<true>, isCompletely: Nullable<true>) => void;

+ 1 - 1
packages/app/src/server/models/page.ts

@@ -544,7 +544,7 @@ schema.statics.findByPageIdsToEdit = async function(ids, user, shouldIncludeEmpt
 
 
   await this.addConditionToFilteringByViewerToEdit(builder, user);
   await this.addConditionToFilteringByViewerToEdit(builder, user);
 
 
-  const pages = await builder.query.lean().exec();
+  const pages = await builder.query.exec();
 
 
   return pages;
   return pages;
 };
 };

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

@@ -450,6 +450,11 @@ class PageService {
     // remove empty pages at leaf position
     // remove empty pages at leaf position
     await Page.removeLeafEmptyPagesRecursively(page.parent);
     await Page.removeLeafEmptyPagesRecursively(page.parent);
 
 
+    // create page redirect
+    if (options.createRedirectPage) {
+      const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;
+      await PageRedirect.create({ fromPath: page.path, toPath: newPagePath });
+    }
     this.pageEvent.emit('rename', page, user);
     this.pageEvent.emit('rename', page, user);
 
 
     // Set to Sub
     // Set to Sub

+ 1 - 3
packages/app/src/stores/modal.tsx

@@ -1,6 +1,6 @@
 import { SWRResponse } from 'swr';
 import { SWRResponse } from 'swr';
 import { useStaticSWR } from './use-static-swr';
 import { useStaticSWR } from './use-static-swr';
-import { Nullable } from '~/interfaces/common';
+import { OnDeletedFunction } from '~/interfaces/ui';
 
 
 
 
 /*
 /*
@@ -41,8 +41,6 @@ export type IDeleteModalOption = {
   onDeleted?: OnDeletedFunction,
   onDeleted?: OnDeletedFunction,
 }
 }
 
 
-export type OnDeletedFunction = (pathOrPaths: string | string[], isRecursively: Nullable<true>, isCompletely: Nullable<true>) => void;
-
 type DeleteModalStatus = {
 type DeleteModalStatus = {
   isOpened: boolean,
   isOpened: boolean,
   pages?: IPageForPageDeleteModal[],
   pages?: IPageForPageDeleteModal[],

+ 8 - 1
packages/app/src/stores/page-listing.tsx

@@ -5,8 +5,13 @@ import { apiv3Get } from '../client/util/apiv3-client';
 import {
 import {
   AncestorsChildrenResult, ChildrenResult, V5MigrationStatus, RootPageResult,
   AncestorsChildrenResult, ChildrenResult, V5MigrationStatus, RootPageResult,
 } from '../interfaces/page-listing-results';
 } from '../interfaces/page-listing-results';
+import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
 
 
 
 
+export const usePageTreeTermManager = (isDisabled?: boolean) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
+  return useTermNumberManager(isDisabled === true ? null : 'fullTextSearchTermNumber');
+};
+
 export const useSWRxRootPage = (): SWRResponse<RootPageResult, Error> => {
 export const useSWRxRootPage = (): SWRResponse<RootPageResult, Error> => {
   return useSWR(
   return useSWR(
     '/page-listing/root',
     '/page-listing/root',
@@ -36,8 +41,10 @@ export const useSWRxPageAncestorsChildren = (
 export const useSWRxPageChildren = (
 export const useSWRxPageChildren = (
     id?: string | null,
     id?: string | null,
 ): SWRResponse<ChildrenResult, Error> => {
 ): SWRResponse<ChildrenResult, Error> => {
+  const { data: termNumber } = usePageTreeTermManager();
+
   return useSWR(
   return useSWR(
-    id ? `/page-listing/children?id=${id}` : null,
+    id ? [`/page-listing/children?id=${id}`, termNumber] : null,
     endpoint => apiv3Get(endpoint).then((response) => {
     endpoint => apiv3Get(endpoint).then((response) => {
       return {
       return {
         children: response.data.children,
         children: response.data.children,

+ 21 - 4
packages/app/src/stores/page.tsx

@@ -8,9 +8,11 @@ import {
 } from '~/interfaces/page';
 } from '~/interfaces/page';
 import { IPagingResult } from '~/interfaces/paging-result';
 import { IPagingResult } from '~/interfaces/paging-result';
 import { apiGet } from '../client/util/apiv1-client';
 import { apiGet } from '../client/util/apiv1-client';
-
 import { IPageTagsInfo } from '../interfaces/pageTagsInfo';
 import { IPageTagsInfo } from '../interfaces/pageTagsInfo';
 
 
+import { useCurrentPagePath } from './context';
+import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
+
 
 
 export const useSWRxPageByPath = (path: string, initialData?: IPageHasId): SWRResponse<IPageHasId, Error> => {
 export const useSWRxPageByPath = (path: string, initialData?: IPageHasId): SWRResponse<IPageHasId, Error> => {
   return useSWR(
   return useSWR(
@@ -32,15 +34,15 @@ export const useSWRxRecentlyUpdated = (): SWRResponse<(IPageHasId)[], Error> =>
 };
 };
 
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const useSWRxPageList = (path: string | null, pageNumber?: number): SWRResponse<IPagingResult<IPageHasId>, Error> => {
+export const useSWRxPageList = (path: string | null, pageNumber?: number, termNumber?: number): SWRResponse<IPagingResult<IPageHasId>, Error> => {
 
 
   const key = path != null
   const key = path != null
-    ? `/pages/list?path=${path}&page=${pageNumber ?? 1}`
+    ? [`/pages/list?path=${path}&page=${pageNumber ?? 1}`, termNumber]
     : null;
     : null;
 
 
   return useSWR(
   return useSWR(
     key,
     key,
-    endpoint => apiv3Get<{pages: IPageHasId[], totalCount: number, limit: number}>(endpoint).then((response) => {
+    (endpoint: string) => apiv3Get<{pages: IPageHasId[], totalCount: number, limit: number}>(endpoint).then((response) => {
       return {
       return {
         items: response.data.pages,
         items: response.data.pages,
         totalCount: response.data.totalCount,
         totalCount: response.data.totalCount,
@@ -50,6 +52,21 @@ export const useSWRxPageList = (path: string | null, pageNumber?: number): SWRRe
   );
   );
 };
 };
 
 
+export const useDescendantsPageListForCurrentPathTermManager = (isDisabled?: boolean) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
+  return useTermNumberManager(isDisabled === true ? null : 'descendantsPageListForCurrentPathTermNumber');
+};
+
+export const useSWRxDescendantsPageListForCurrrentPath = (pageNumber?: number): SWRResponse<IPagingResult<IPageHasId>, Error> => {
+  const { data: currentPagePath } = useCurrentPagePath();
+  const { data: termNumber } = useDescendantsPageListForCurrentPathTermManager();
+
+  const path = currentPagePath == null || termNumber == null
+    ? null
+    : currentPagePath;
+
+  return useSWRxPageList(path, pageNumber, termNumber);
+};
+
 export const useSWRTagsInfo = (pageId: string | null | undefined): SWRResponse<IPageTagsInfo, Error> => {
 export const useSWRTagsInfo = (pageId: string | null | undefined): SWRResponse<IPageTagsInfo, Error> => {
   const key = pageId == null ? null : `/pages.getPageTag?pageId=${pageId}`;
   const key = pageId == null ? null : `/pages.getPageTag?pageId=${pageId}`;
 
 

+ 10 - 2
packages/app/src/stores/search.tsx

@@ -5,6 +5,13 @@ import { apiGet } from '~/client/util/apiv1-client';
 
 
 import { IFormattedSearchResult, SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
 import { IFormattedSearchResult, SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
 
 
+import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
+
+
+export const useFullTextSearchTermManager = (isDisabled?: boolean) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
+  return useTermNumberManager(isDisabled === true ? null : 'fullTextSearchTermNumber');
+};
+
 
 
 export type ISearchConfigurations = {
 export type ISearchConfigurations = {
   limit: number,
   limit: number,
@@ -44,8 +51,9 @@ const createSearchQuery = (keyword: string, includeTrashPages: boolean, includeU
 };
 };
 
 
 export const useSWRxFullTextSearch = (
 export const useSWRxFullTextSearch = (
-    keyword: string, configurations: ISearchConfigurations,
+    keyword: string, configurations: ISearchConfigurations, disableTermManager = false,
 ): SWRResponse<IFormattedSearchResult, Error> & { conditions: ISearchConditions } => {
 ): SWRResponse<IFormattedSearchResult, Error> & { conditions: ISearchConditions } => {
+  const { data: termNumber } = useFullTextSearchTermManager(disableTermManager);
 
 
   const {
   const {
     limit, offset, sort, order, includeTrashPages, includeUserPages,
     limit, offset, sort, order, includeTrashPages, includeUserPages,
@@ -62,7 +70,7 @@ export const useSWRxFullTextSearch = (
   const rawQuery = createSearchQuery(keyword, fixedConfigurations.includeTrashPages, fixedConfigurations.includeUserPages);
   const rawQuery = createSearchQuery(keyword, fixedConfigurations.includeTrashPages, fixedConfigurations.includeUserPages);
 
 
   const swrResult = useSWRImmutable(
   const swrResult = useSWRImmutable(
-    ['/search', keyword, fixedConfigurations],
+    ['/search', keyword, fixedConfigurations, termNumber],
     (endpoint, keyword, fixedConfigurations) => {
     (endpoint, keyword, fixedConfigurations) => {
       const {
       const {
         limit, offset, sort, order,
         limit, offset, sort, order,

+ 25 - 0
packages/app/src/stores/use-static-swr.tsx

@@ -29,3 +29,28 @@ export function useStaticSWR<Data, Error>(
 
 
   return swrResponse;
   return swrResponse;
 }
 }
+
+
+const ADVANCE_DELAY_MS = 800;
+
+export type ITermNumberManagerUtil = {
+  advance(): void,
+}
+
+export const useTermNumberManager = (key: Key) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
+  const swrResult = useStaticSWR<number, Error>(key, undefined, { fallbackData: 0 });
+
+  return {
+    ...swrResult,
+    advance: () => {
+      const { data: currentNum } = swrResult;
+      if (currentNum == null) {
+        return;
+      }
+
+      setTimeout(() => {
+        swrResult.mutate(currentNum + 1);
+      }, ADVANCE_DELAY_MS);
+    },
+  };
+};

+ 17 - 20
packages/app/test/integration/service/v5.page.test.ts

@@ -912,34 +912,31 @@ describe('PageService page operations with only public pages', () => {
       expect(childPage.lastUpdateUser).toStrictEqual(dummyUser1._id);
       expect(childPage.lastUpdateUser).toStrictEqual(dummyUser1._id);
 
 
       const newPath = '/v5_ParentForRename3/renamedChildForRename3';
       const newPath = '/v5_ParentForRename3/renamedChildForRename3';
-      const oldUdpateAt = childPage.updatedAt;
+      const oldUpdateAt = childPage.updatedAt;
       const renamedPage = await renamePage(childPage, newPath, dummyUser2, { updateMetadata: true });
       const renamedPage = await renamePage(childPage, newPath, dummyUser2, { updateMetadata: true });
 
 
       expect(xssSpy).toHaveBeenCalled();
       expect(xssSpy).toHaveBeenCalled();
       expect(renamedPage.path).toBe(newPath);
       expect(renamedPage.path).toBe(newPath);
       expect(renamedPage.parent).toStrictEqual(parentPage._id);
       expect(renamedPage.parent).toStrictEqual(parentPage._id);
       expect(renamedPage.lastUpdateUser).toStrictEqual(dummyUser2._id);
       expect(renamedPage.lastUpdateUser).toStrictEqual(dummyUser2._id);
-      expect(renamedPage.updatedAt.getFullYear()).toBeGreaterThan(oldUdpateAt.getFullYear());
+      expect(renamedPage.updatedAt.getFullYear()).toBeGreaterThan(oldUpdateAt.getFullYear());
     });
     });
 
 
-    // ****************** TODO ******************
-    // uncomment the next test when working on 88097
-    // ******************************************
-    // test('Should move with option createRedirectPage: true', async() => {
-    // const parentPage = await Page.findOne({ path: '/v5_ParentForRename4' });
-    // const childPage = await Page.findOne({ path: '/v5_ChildForRename4' });
-    // expectAllToBeTruthy([parentPage, childPage]);
-
-    //   // rename target page
-    //   const newPath = '/v5_ParentForRename4/renamedChildForRename4';
-    //   const renamedPage = await renamePage(childPage, newPath, dummyUser2, { createRedirectPage: true });
-    //   const pageRedirect = await PageRedirect.find({ fromPath: childPage.path, toPath: renamedPage.path });
-
-    // expect(xssSpy).toHaveBeenCalled();
-    //   expect(renamedPage.path).toBe(newPath);
-    //   expect(renamedPage.parent).toStrictEqual(parentPage._id);
-    //   expect(pageRedirect.length).toBeGreaterThan(0);
-    // });
+    test('Should move with option createRedirectPage: true', async() => {
+      const parentPage = await Page.findOne({ path: '/v5_ParentForRename4' });
+      const childPage = await Page.findOne({ path: '/v5_ChildForRename4' });
+      expectAllToBeTruthy([parentPage, childPage]);
+
+      const oldPath = childPage.path;
+      const newPath = '/v5_ParentForRename4/renamedChildForRename4';
+      const renamedPage = await renamePage(childPage, newPath, dummyUser2, { createRedirectPage: true });
+      const pageRedirect = await PageRedirect.findOne({ fromPath: oldPath, toPath: renamedPage.path });
+
+      expect(xssSpy).toHaveBeenCalled();
+      expect(renamedPage.path).toBe(newPath);
+      expect(renamedPage.parent).toStrictEqual(parentPage._id);
+      expect(pageRedirect).toBeTruthy();
+    });
 
 
     test('Should rename/move with descendants', async() => {
     test('Should rename/move with descendants', async() => {
       const parentPage = await Page.findOne({ path: '/v5_ParentForRename5' });
       const parentPage = await Page.findOne({ path: '/v5_ParentForRename5' });