import React, { type JSX, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { LoadingSpinner } from '@growi/ui/dist/components'; import { useAtomValue } from 'jotai'; import { useTranslation } from 'next-i18next'; import { DropdownItem, DropdownMenu, DropdownToggle, Modal, ModalBody, ModalFooter, ModalHeader, UncontrolledButtonDropdown, } from 'reactstrap'; 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 { apiv3Post } from '~/client/util/apiv3-client'; import { toastError, toastSuccess } from '~/client/util/toastr'; import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error'; import type { V5MigrationStatus } from '~/interfaces/page-listing-results'; import type { IFormattedSearchResult } from '~/interfaces/search'; import type { PageMigrationErrorData } from '~/interfaces/websocket'; import { SocketEventName } from '~/interfaces/websocket'; import { useIsAdmin } from '~/states/context'; import { useSearchKeyword, useSetSearchKeyword } from '~/states/search'; import { disableUserPagesAtom } from '~/states/server-configurations'; import { useGlobalSocket } from '~/states/socket-io'; import type { ILegacyPrivatePage } from '~/states/ui/modal/private-legacy-pages-migration'; import { usePrivateLegacyPagesMigrationModalActions } from '~/states/ui/modal/private-legacy-pages-migration'; import { mutatePageTree, useSWRxV5MigrationStatus, } from '~/stores/page-listing'; import { useSWRxSearch } from '~/stores/search'; import { OperateAllControl } from './SearchPage/OperateAllControl'; import SearchControl from './SearchPage/SearchControl'; import { type IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion, } from './SearchPage/SearchPageBase'; // TODO: replace with "customize:showPageLimitationS" const INITIAL_PAGING_SIZE = 20; const initQ = '/'; /** * SearchResultListHead */ 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; if (migrationStatus == null) { return (
); } const { took, total, hitsCount } = searchResult.meta; const leftNum = offset + 1; const rightNum = offset + hitsCount; const isSuccess = migrationStatus.migratablePagesCount === 0; if (isSuccess) { return (

{t('private_legacy_pages.nopages_title')}

{t('private_legacy_pages.nopages_desc1')}

); } return ( <>
{t('search_result.result_meta')} {`${leftNum}-${rightNum}`} / {total} {took != null && ( ({took}ms) )}

{t('private_legacy_pages.alert_title')}

{t('private_legacy_pages.alert_desc1', { delete_all_selected_page: t( 'search_result.delete_all_selected_page', ), })}

); }, ); SearchResultListHead.displayName = 'SearchResultListHead'; /* * ConvertByPathModal */ type ConvertByPathModalProps = { isOpen: boolean; close?: () => void; onSubmit?: (convertPath: string) => Promise | void; }; const ConvertByPathModal = React.memo( (props: ConvertByPathModalProps): JSX.Element => { const { t } = useTranslation(); const [currentInput, setInput] = useState(''); const [checked, setChecked] = useState(false); useEffect(() => { setChecked(false); }, []); return ( {t('private_legacy_pages.by_path_modal.title')}

{t('private_legacy_pages.by_path_modal.description')}

setInput(e.target.value)} />
{t('private_legacy_pages.by_path_modal.alert')}
setChecked(e.target.checked)} />
); }, ); ConvertByPathModal.displayName = 'ConvertByPathModal'; /** * LegacyPage */ const PrivateLegacyPages = (): JSX.Element => { const { t } = useTranslation(); const isAdmin = useIsAdmin(); const keyword = useSearchKeyword(); const setSearchKeyword = useSetSearchKeyword('/_private-legacy-pages'); const disableUserPages = useAtomValue(disableUserPagesAtom); const [offset, setOffset] = useState(0); const [limit, setLimit] = useState(INITIAL_PAGING_SIZE); const [isOpenConvertModal, setOpenConvertModal] = useState(false); const [isControlEnabled, setControlEnabled] = useState(false); const selectAllControlRef = useRef( null, ); const searchPageBaseRef = useRef< (ISelectableAll & IReturnSelectedPageIds) | null >(null); // biome-ignore lint/correctness/useExhaustiveDependencies: only run on mount useEffect(() => { setSearchKeyword(initQ); }, []); const { data, conditions, mutate } = useSWRxSearch( keyword, 'PrivateLegacyPages', { offset, limit, includeUserPages: true, includeTrashPages: false, }, ); const { data: migrationStatus, mutate: mutateMigrationStatus } = useSWRxV5MigrationStatus(); const searchInvokedHandler = useCallback( (_keyword: string) => { mutateMigrationStatus(); setSearchKeyword(_keyword); setOffset(0); }, [mutateMigrationStatus, setSearchKeyword], ); const { open: openModal, close: closeModal } = usePrivateLegacyPagesMigrationModalActions(); const socket = useGlobalSocket(); useEffect(() => { socket?.on(SocketEventName.PageMigrationSuccess, () => { 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, }), ); } }, ); return () => { socket?.off(SocketEventName.PageMigrationSuccess); socket?.off(SocketEventName.PageMigrationError); }; }, [socket, t]); const selectAllCheckboxChangedHandler = useCallback((isChecked: boolean) => { const instance = searchPageBaseRef.current; if (instance == null) { return; } if (isChecked) { instance.selectAll(); setControlEnabled(true); } else { instance.deselectAll(); setControlEnabled(false); } }, []); const selectedPagesByCheckboxesChangedHandler = useCallback( (selectedCount: number, totalCount: number) => { const instance = selectAllControlRef.current; if (instance == null) { return; } 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 convertMenuItemClickedHandler = useCallback(() => { if (data == null) { return; } const instance = searchPageBaseRef.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.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 pagingNumberChangedHandler = useCallback( (activePage: number) => { setOffset((activePage - 1) * limit); mutate(); }, [limit, mutate], ); const openConvertModalHandler = useCallback(() => { if (!isAdmin) { return; } setOpenConvertModal(true); }, [isAdmin]); const hitsCount = data?.meta.hitsCount; const renderOpenModalButton = useCallback(() => { return (
); }, [t, openConvertModalHandler]); const extraControls = useMemo(() => { const isCheckboxDisabled = hitsCount === 0; return (
{t('private_legacy_pages.bulk_operation')} refresh {t('private_legacy_pages.convert_all_selected_pages')} delete {t('search_result.delete_all_selected_page')}
{isAdmin && renderOpenModalButton()}
); }, [ convertMenuItemClickedHandler, deleteAllButtonClickedHandler, hitsCount, isAdmin, isControlEnabled, renderOpenModalButton, selectAllCheckboxChangedHandler, t, ]); const searchControl = useMemo(() => { return ( ); }, [searchInvokedHandler, extraControls, disableUserPages]); const searchResultListHead = useMemo(() => { if (data == null) { // biome-ignore lint/complexity/noUselessFragments: ignore return <>; } return ( ); }, [data, limit, offset, pagingSizeChangedHandler, migrationStatus]); const searchPager = useMemo(() => { // when pager is not needed if (data == null || data.meta.hitsCount === data.meta.total) { // biome-ignore lint/complexity/noUselessFragments: ignore return <>; } const { total } = data.meta; const { offset, limit } = conditions; return ( ); }, [conditions, data, pagingNumberChangedHandler]); return ( <> setOpenConvertModal(false)} onSubmit={async (convertPath: string) => { try { await apiv3Post('/pages/convert-pages-by-path', { convertPath, }); toastSuccess(t('private_legacy_pages.by_path_modal.success')); setOpenConvertModal(false); mutate(); mutatePageTree(); } 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'), ); break; case V5ConversionErrCode.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', ), ); break; default: toastError(t('private_legacy_pages.by_path_modal.error')); } } else { toastError(t('private_legacy_pages.by_path_modal.error')); } } }} /> ); }; export default PrivateLegacyPages;