Yuki Takei 6 месяцев назад
Родитель
Сommit
59966875c5
1 измененных файлов с 325 добавлено и 205 удалено
  1. 325 205
      apps/app/src/features/search/client/components/PrivateLegacyPages.tsx

+ 325 - 205
apps/app/src/features/search/client/components/PrivateLegacyPages.tsx

@@ -23,7 +23,10 @@ import {
 import { MenuItemType } from '~/client/components/Common/Dropdown/PageItemControl';
 import PaginationWrapper from '~/client/components/PaginationWrapper';
 import { PrivateLegacyPagesMigrationModal } from '~/client/components/PrivateLegacyPagesMigrationModal';
-import type { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
+import type {
+  ISelectableAll,
+  ISelectableAndIndeterminatable,
+} from '~/client/interfaces/selectable-all';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
@@ -42,8 +45,11 @@ import { useSWRxSearch } from '~/stores/search';
 
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 import SearchControl from './SearchPage/SearchControl';
-import { IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage/SearchPageBase';
-
+import {
+  type IReturnSelectedPageIds,
+  SearchPageBase,
+  usePageDeleteModalForBulkDeletion,
+} from './SearchPage/SearchPageBase';
 
 // TODO: replace with "customize:showPageLimitationS"
 const INITIAL_PAGING_SIZE = 20;
@@ -55,89 +61,126 @@ const initQ = '/';
  */
 
 type SearchResultListHeadProps = {
-  searchResult: IFormattedSearchResult,
-  offset: number,
-  pagingSize: number,
-  onPagingSizeChanged: (size: number) => void,
-  migrationStatus?: V5MigrationStatus,
-}
-
-const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.Element => {
-  const { t } = useTranslation();
-
-  const {
-    searchResult, offset, pagingSize,
-    onPagingSizeChanged, migrationStatus,
-  } = props;
+  searchResult: IFormattedSearchResult;
+  offset: number;
+  pagingSize: number;
+  onPagingSizeChanged: (size: number) => void;
+  migrationStatus?: V5MigrationStatus;
+};
 
-  if (migrationStatus == null) {
-    return (
-      <div className="mw-0 flex-grow-1 flex-basis-0 m-5 text-muted text-center">
-        <LoadingSpinner className="me-1 fs-3" />
-      </div>
-    );
-  }
+const SearchResultListHead = React.memo(
+  (props: SearchResultListHeadProps): JSX.Element => {
+    const { t } = useTranslation();
+
+    const {
+      searchResult,
+      offset,
+      pagingSize,
+      onPagingSizeChanged,
+      migrationStatus,
+    } = props;
+
+    if (migrationStatus == null) {
+      return (
+        <div className="mw-0 flex-grow-1 flex-basis-0 m-5 text-muted text-center">
+          <LoadingSpinner className="me-1 fs-3" />
+        </div>
+      );
+    }
 
-  const { took, total, hitsCount } = searchResult.meta;
-  const leftNum = offset + 1;
-  const rightNum = offset + hitsCount;
+    const { took, total, hitsCount } = searchResult.meta;
+    const leftNum = offset + 1;
+    const rightNum = offset + hitsCount;
 
-  const isSuccess = migrationStatus.migratablePagesCount === 0;
+    const isSuccess = migrationStatus.migratablePagesCount === 0;
 
-  if (isSuccess) {
-    return (
-      <div className="card border-success mt-3" data-testid="search-result-private-legacy-pages">
-        <div className="card-body">
-          <h2 className="card-title text-success">{t('private_legacy_pages.nopages_title')}</h2>
-          <p className="card-text">
-            {t('private_legacy_pages.nopages_desc1')}<br />
-            {/** biome-ignore lint/security/noDangerouslySetInnerHtml: ignore */}
-            <span dangerouslySetInnerHTML={{ __html: t('private_legacy_pages.detail_info') }}></span>
-          </p>
+    if (isSuccess) {
+      return (
+        <div
+          className="card border-success mt-3"
+          data-testid="search-result-private-legacy-pages"
+        >
+          <div className="card-body">
+            <h2 className="card-title text-success">
+              {t('private_legacy_pages.nopages_title')}
+            </h2>
+            <p className="card-text">
+              {t('private_legacy_pages.nopages_desc1')}
+              <br />
+              <span
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
+                dangerouslySetInnerHTML={{
+                  __html: t('private_legacy_pages.detail_info'),
+                }}
+              ></span>
+            </p>
+          </div>
         </div>
-      </div>
-    );
-  }
+      );
+    }
 
-  return (
-    <>
-      <div className="d-flex align-items-center justify-content-between">
-        <div className="text-nowrap">
-          {t('search_result.result_meta')}
-          <span className="ms-3">{`${leftNum}-${rightNum}`} / {total}</span>
-          { took != null && (
-            <span className="ms-3 text-muted">({took}ms)</span>
-          ) }
-        </div>
-        <div className="input-group flex-nowrap search-result-select-group ms-auto d-md-flex d-none">
-          <div>
-            <label className="form-label input-group-text text-muted" htmlFor="inputGroupSelect01">{t('search_result.number_of_list_to_display')}</label>
+    return (
+      <>
+        <div className="d-flex align-items-center justify-content-between">
+          <div className="text-nowrap">
+            {t('search_result.result_meta')}
+            <span className="ms-3">
+              {`${leftNum}-${rightNum}`} / {total}
+            </span>
+            {took != null && (
+              <span className="ms-3 text-muted">({took}ms)</span>
+            )}
+          </div>
+          <div className="input-group flex-nowrap search-result-select-group ms-auto d-md-flex d-none">
+            <div>
+              <label
+                className="form-label input-group-text text-muted"
+                htmlFor="inputGroupSelect01"
+              >
+                {t('search_result.number_of_list_to_display')}
+              </label>
+            </div>
+            <select
+              defaultValue={pagingSize}
+              className="form-select"
+              id="inputGroupSelect01"
+              onChange={(e) => onPagingSizeChanged(Number(e.target.value))}
+            >
+              {[20, 50, 100, 200].map((limit) => {
+                return (
+                  <option key={limit} value={limit}>
+                    {limit} {t('search_result.page_number_unit')}
+                  </option>
+                );
+              })}
+            </select>
           </div>
-          <select
-            defaultValue={pagingSize}
-            className="form-select"
-            id="inputGroupSelect01"
-            onChange={e => onPagingSizeChanged(Number(e.target.value))}
-          >
-            {[20, 50, 100, 200].map((limit) => {
-              return <option key={limit} value={limit}>{limit} {t('search_result.page_number_unit')}</option>;
-            })}
-          </select>
         </div>
-      </div>
-      <div className="card border-warning mt-3">
-        <div className="card-body">
-          <h2 className="card-title text-warning">{t('private_legacy_pages.alert_title')}</h2>
-          <p className="card-text">
-            {t('private_legacy_pages.alert_desc1', { delete_all_selected_page: t('search_result.delete_all_selected_page') })}<br />
-            {/** biome-ignore lint/security/noDangerouslySetInnerHtml: ignore */}
-            <span dangerouslySetInnerHTML={{ __html: t('private_legacy_pages.detail_info') }}></span>
-          </p>
+        <div className="card border-warning mt-3">
+          <div className="card-body">
+            <h2 className="card-title text-warning">
+              {t('private_legacy_pages.alert_title')}
+            </h2>
+            <p className="card-text">
+              {t('private_legacy_pages.alert_desc1', {
+                delete_all_selected_page: t(
+                  'search_result.delete_all_selected_page',
+                ),
+              })}
+              <br />
+              <span
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
+                dangerouslySetInnerHTML={{
+                  __html: t('private_legacy_pages.detail_info'),
+                }}
+              ></span>
+            </p>
+          </div>
         </div>
-      </div>
-    </>
-  );
-});
+      </>
+    );
+  },
+);
 
 SearchResultListHead.displayName = 'SearchResultListHead';
 
@@ -145,55 +188,70 @@ SearchResultListHead.displayName = 'SearchResultListHead';
  * ConvertByPathModal
  */
 type ConvertByPathModalProps = {
-  isOpen: boolean,
-  close?: () => void,
-  onSubmit?: (convertPath: string) => Promise<void> | void,
-}
-const ConvertByPathModal = React.memo((props: ConvertByPathModalProps): JSX.Element => {
-  const { t } = useTranslation();
+  isOpen: boolean;
+  close?: () => void;
+  onSubmit?: (convertPath: string) => Promise<void> | void;
+};
+const ConvertByPathModal = React.memo(
+  (props: ConvertByPathModalProps): JSX.Element => {
+    const { t } = useTranslation();
 
-  const [currentInput, setInput] = useState<string>('');
-  const [checked, setChecked] = useState<boolean>(false);
+    const [currentInput, setInput] = useState<string>('');
+    const [checked, setChecked] = useState<boolean>(false);
 
-  useEffect(() => {
-    setChecked(false);
-  }, []);
+    useEffect(() => {
+      setChecked(false);
+    }, []);
 
-  return (
-    <Modal size="lg" isOpen={props.isOpen} toggle={props.close}>
-      <ModalHeader tag="h4" toggle={props.close}>
-        { t('private_legacy_pages.by_path_modal.title') }
-      </ModalHeader>
-      <ModalBody>
-        <p>{t('private_legacy_pages.by_path_modal.description')}</p>
-        <input type="text" className="form-control" placeholder="/" value={currentInput} onChange={e => setInput(e.target.value)} />
-        <div className="alert alert-danger mt-3" role="alert">
-          { t('private_legacy_pages.by_path_modal.alert') }
-        </div>
-      </ModalBody>
-      <ModalFooter>
-        <div className="form-check">
+    return (
+      <Modal size="lg" isOpen={props.isOpen} toggle={props.close}>
+        <ModalHeader tag="h4" toggle={props.close}>
+          {t('private_legacy_pages.by_path_modal.title')}
+        </ModalHeader>
+        <ModalBody>
+          <p>{t('private_legacy_pages.by_path_modal.description')}</p>
           <input
-            className="form-check-input"
-            type="checkbox"
-            id="understoodCheckbox"
-            onChange={e => setChecked(e.target.checked)}
+            type="text"
+            className="form-control"
+            placeholder="/"
+            value={currentInput}
+            onChange={(e) => setInput(e.target.value)}
           />
-          <label className="form-label form-check-label" htmlFor="understoodCheckbox">{ t('private_legacy_pages.by_path_modal.checkbox_label') }</label>
-        </div>
-        <button
-          type="button"
-          className="btn btn-primary"
-          disabled={!checked}
-          onClick={() => props.onSubmit?.(currentInput)}
-        >
-          <span className="material-symbols-outlined" aria-hidden="true">refresh</span>
-          { t('private_legacy_pages.by_path_modal.button_label') }
-        </button>
-      </ModalFooter>
-    </Modal>
-  );
-});
+          <div className="alert alert-danger mt-3" role="alert">
+            {t('private_legacy_pages.by_path_modal.alert')}
+          </div>
+        </ModalBody>
+        <ModalFooter>
+          <div className="form-check">
+            <input
+              className="form-check-input"
+              type="checkbox"
+              id="understoodCheckbox"
+              onChange={(e) => setChecked(e.target.checked)}
+            />
+            <label
+              className="form-label form-check-label"
+              htmlFor="understoodCheckbox"
+            >
+              {t('private_legacy_pages.by_path_modal.checkbox_label')}
+            </label>
+          </div>
+          <button
+            type="button"
+            className="btn btn-primary"
+            disabled={!checked}
+            onClick={() => props.onSubmit?.(currentInput)}
+          >
+            <span className="material-symbols-outlined" aria-hidden="true">
+              refresh
+            </span>
+            {t('private_legacy_pages.by_path_modal.button_label')}
+          </button>
+        </ModalFooter>
+      </Modal>
+    );
+  },
+);
 
 ConvertByPathModal.displayName = 'ConvertByPathModal';
 
@@ -213,25 +271,38 @@ const PrivateLegacyPages = (): JSX.Element => {
 
   const [isControlEnabled, setControlEnabled] = useState(false);
 
-  const selectAllControlRef = useRef<ISelectableAndIndeterminatable|null>(null);
-  const searchPageBaseRef = useRef<ISelectableAll & IReturnSelectedPageIds|null>(null);
-
-  const { data, conditions, mutate } = useSWRxSearch(keyword, 'PrivateLegacyPages', {
-    offset,
-    limit,
-    includeUserPages: true,
-    includeTrashPages: false,
-  });
+  const selectAllControlRef = useRef<ISelectableAndIndeterminatable | null>(
+    null,
+  );
+  const searchPageBaseRef = useRef<
+    (ISelectableAll & IReturnSelectedPageIds) | null
+  >(null);
+
+  const { data, conditions, mutate } = useSWRxSearch(
+    keyword,
+    'PrivateLegacyPages',
+    {
+      offset,
+      limit,
+      includeUserPages: true,
+      includeTrashPages: false,
+    },
+  );
 
-  const { data: migrationStatus, mutate: mutateMigrationStatus } = useSWRxV5MigrationStatus();
+  const { data: migrationStatus, mutate: mutateMigrationStatus } =
+    useSWRxV5MigrationStatus();
 
-  const searchInvokedHandler = useCallback((_keyword: string) => {
-    mutateMigrationStatus();
-    setKeyword(_keyword);
-    setOffset(0);
-  }, [mutateMigrationStatus]);
+  const searchInvokedHandler = useCallback(
+    (_keyword: string) => {
+      mutateMigrationStatus();
+      setKeyword(_keyword);
+      setOffset(0);
+    },
+    [mutateMigrationStatus],
+  );
 
-  const { open: openModal, close: closeModal } = usePrivateLegacyPagesMigrationModalActions();
+  const { open: openModal, close: closeModal } =
+    usePrivateLegacyPagesMigrationModalActions();
   const { data: socket } = useGlobalSocket();
 
   useEffect(() => {
@@ -239,17 +310,24 @@ const PrivateLegacyPages = (): JSX.Element => {
       toastSuccess(t('private_legacy_pages.toaster.page_migration_succeeded'));
     });
 
-    socket?.on(SocketEventName.PageMigrationError, (data?: PageMigrationErrorData) => {
-      if (data == null || data.paths.length === 0) {
-        toastError(t('private_legacy_pages.toaster.page_migration_failed'));
-      }
-      else {
-        const errorPaths = data.paths.length > 3
-          ? `${data.paths.slice(0, 3).join(', ')}...`
-          : data.paths.join(', ');
-        toastError(t('private_legacy_pages.toaster.page_migration_failed_with_paths', { paths: errorPaths }));
-      }
-    });
+    socket?.on(
+      SocketEventName.PageMigrationError,
+      (data?: PageMigrationErrorData) => {
+        if (data == null || data.paths.length === 0) {
+          toastError(t('private_legacy_pages.toaster.page_migration_failed'));
+        } else {
+          const errorPaths =
+            data.paths.length > 3
+              ? `${data.paths.slice(0, 3).join(', ')}...`
+              : data.paths.join(', ');
+          toastError(
+            t('private_legacy_pages.toaster.page_migration_failed_with_paths', {
+              paths: errorPaths,
+            }),
+          );
+        }
+      },
+    );
 
     return () => {
       socket?.off(SocketEventName.PageMigrationSuccess);
@@ -267,36 +345,40 @@ const PrivateLegacyPages = (): JSX.Element => {
     if (isChecked) {
       instance.selectAll();
       setControlEnabled(true);
-    }
-    else {
+    } else {
       instance.deselectAll();
       setControlEnabled(false);
     }
   }, []);
 
-  const selectedPagesByCheckboxesChangedHandler = useCallback((selectedCount: number, totalCount: number) => {
-    const instance = selectAllControlRef.current;
+  const selectedPagesByCheckboxesChangedHandler = useCallback(
+    (selectedCount: number, totalCount: number) => {
+      const instance = selectAllControlRef.current;
 
-    if (instance == null) {
-      return;
-    }
+      if (instance == null) {
+        return;
+      }
 
-    if (selectedCount === 0) {
-      instance.deselect();
-      setControlEnabled(false);
-    }
-    else if (selectedCount === totalCount) {
-      instance.select();
-      setControlEnabled(true);
-    }
-    else {
-      instance.setIndeterminate();
-      setControlEnabled(true);
-    }
-  }, []);
+      if (selectedCount === 0) {
+        instance.deselect();
+        setControlEnabled(false);
+      } else if (selectedCount === totalCount) {
+        instance.select();
+        setControlEnabled(true);
+      } else {
+        instance.setIndeterminate();
+        setControlEnabled(true);
+      }
+    },
+    [],
+  );
 
   // for bulk deletion
-  const deleteAllButtonClickedHandler = usePageDeleteModalForBulkDeletion(data, searchPageBaseRef, () => mutate());
+  const deleteAllButtonClickedHandler = usePageDeleteModalForBulkDeletion(
+    data,
+    searchPageBaseRef,
+    () => mutate(),
+  );
 
   const convertMenuItemClickedHandler = useCallback(() => {
     if (data == null) {
@@ -315,34 +397,45 @@ const PrivateLegacyPages = (): JSX.Element => {
     }
 
     const selectedPages = data.data
-      .filter(pageWithMeta => selectedPageIds.has(pageWithMeta.data._id))
-      .map(pageWithMeta => ({ pageId: pageWithMeta.data._id, path: pageWithMeta.data.path } as ILegacyPrivatePage));
-
-    openModal(
-      selectedPages,
-      () => {
-        toastSuccess(t('Successfully requested'));
-        closeModal();
-        mutateMigrationStatus();
-        mutate();
-        mutatePageTree();
-      },
-    );
+      .filter((pageWithMeta) => selectedPageIds.has(pageWithMeta.data._id))
+      .map(
+        (pageWithMeta) =>
+          ({
+            pageId: pageWithMeta.data._id,
+            path: pageWithMeta.data.path,
+          }) as ILegacyPrivatePage,
+      );
+
+    openModal(selectedPages, () => {
+      toastSuccess(t('Successfully requested'));
+      closeModal();
+      mutateMigrationStatus();
+      mutate();
+      mutatePageTree();
+    });
   }, [data, openModal, t, closeModal, mutateMigrationStatus, mutate]);
 
-  const pagingSizeChangedHandler = useCallback((pagingSize: number) => {
-    setOffset(0);
-    setLimit(pagingSize);
-    mutate();
-  }, [mutate]);
+  const pagingSizeChangedHandler = useCallback(
+    (pagingSize: number) => {
+      setOffset(0);
+      setLimit(pagingSize);
+      mutate();
+    },
+    [mutate],
+  );
 
-  const pagingNumberChangedHandler = useCallback((activePage: number) => {
-    setOffset((activePage - 1) * limit);
-    mutate();
-  }, [limit, mutate]);
+  const pagingNumberChangedHandler = useCallback(
+    (activePage: number) => {
+      setOffset((activePage - 1) * limit);
+      mutate();
+    },
+    [limit, mutate],
+  );
 
   const openConvertModalHandler = useCallback(() => {
-    if (!isAdmin) { return }
+    if (!isAdmin) {
+      return;
+    }
     setOpenConvertModal(true);
   }, [isAdmin]);
 
@@ -351,7 +444,11 @@ const PrivateLegacyPages = (): JSX.Element => {
   const renderOpenModalButton = useCallback(() => {
     return (
       <div className="d-flex ps-md-2">
-        <button type="button" className="btn btn-light" onClick={() => openConvertModalHandler()}>
+        <button
+          type="button"
+          className="btn btn-light"
+          onClick={() => openConvertModalHandler()}
+        >
           {t('private_legacy_pages.input_path_to_convert')}
         </button>
       </div>
@@ -371,7 +468,11 @@ const PrivateLegacyPages = (): JSX.Element => {
             onCheckboxChanged={selectAllCheckboxChangedHandler}
           >
             <UncontrolledButtonDropdown>
-              <DropdownToggle caret color="outline-primary" disabled={!isControlEnabled}>
+              <DropdownToggle
+                caret
+                color="outline-primary"
+                disabled={!isControlEnabled}
+              >
                 {t('private_legacy_pages.bulk_operation')}
               </DropdownToggle>
               <DropdownMenu>
@@ -392,8 +493,17 @@ const PrivateLegacyPages = (): JSX.Element => {
         {isAdmin && renderOpenModalButton()}
       </div>
     );
-  // eslint-disable-next-line max-len
-  }, [convertMenuItemClickedHandler, deleteAllButtonClickedHandler, hitsCount, isAdmin, isControlEnabled, renderOpenModalButton, selectAllCheckboxChangedHandler, t]);
+    // eslint-disable-next-line max-len
+  }, [
+    convertMenuItemClickedHandler,
+    deleteAllButtonClickedHandler,
+    hitsCount,
+    isAdmin,
+    isControlEnabled,
+    renderOpenModalButton,
+    selectAllCheckboxChangedHandler,
+    t,
+  ]);
 
   const searchControl = useMemo(() => {
     return (
@@ -448,7 +558,9 @@ const PrivateLegacyPages = (): JSX.Element => {
       <SearchPageBase
         ref={searchPageBaseRef}
         pages={data?.data}
-        onSelectedPagesByCheckboxesChanged={selectedPagesByCheckboxesChangedHandler}
+        onSelectedPagesByCheckboxesChanged={
+          selectedPagesByCheckboxesChangedHandler
+        }
         forceHideMenuItems={[
           MenuItemType.BOOKMARK,
           MenuItemType.RENAME,
@@ -466,7 +578,7 @@ const PrivateLegacyPages = (): JSX.Element => {
       <ConvertByPathModal
         isOpen={isOpenConvertModal}
         close={() => setOpenConvertModal(false)}
-        onSubmit={async(convertPath: string) => {
+        onSubmit={async (convertPath: string) => {
           try {
             await apiv3Post<void>('/pages/convert-pages-by-path', {
               convertPath,
@@ -475,24 +587,32 @@ const PrivateLegacyPages = (): JSX.Element => {
             setOpenConvertModal(false);
             mutate();
             mutatePageTree();
-          }
-          catch (errs) {
+          } catch (errs) {
             if (errs.length === 1) {
               switch (errs[0].code) {
                 case V5ConversionErrCode.GRANT_INVALID:
-                  toastError(t('private_legacy_pages.by_path_modal.error_grant_invalid'));
+                  toastError(
+                    t('private_legacy_pages.by_path_modal.error_grant_invalid'),
+                  );
                   break;
                 case V5ConversionErrCode.PAGE_NOT_FOUND:
-                  toastError(t('private_legacy_pages.by_path_modal.error_page_not_found'));
+                  toastError(
+                    t(
+                      'private_legacy_pages.by_path_modal.error_page_not_found',
+                    ),
+                  );
                   break;
                 case V5ConversionErrCode.DUPLICATE_PAGES_FOUND:
-                  toastError(t('private_legacy_pages.by_path_modal.error_duplicate_pages_found'));
+                  toastError(
+                    t(
+                      'private_legacy_pages.by_path_modal.error_duplicate_pages_found',
+                    ),
+                  );
                   break;
                 default:
                   toastError(t('private_legacy_pages.by_path_modal.error'));
               }
-            }
-            else {
+            } else {
               toastError(t('private_legacy_pages.by_path_modal.error'));
             }
           }