import React, { useCallback, useMemo, useRef, useState, useEffect, } from 'react'; import { useGlobalSocket } from '@growi/core/dist/swr'; import { LoadingSpinner } from '@growi/ui/dist/components'; import { useTranslation } from 'next-i18next'; import { UncontrolledButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem, Modal, ModalHeader, ModalBody, ModalFooter, } from 'reactstrap'; import type { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all'; import { apiv3Post } from '~/client/util/apiv3-client'; import { toastSuccess, toastError } 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 '~/stores/context'; import type { ILegacyPrivatePage } from '~/stores/modal'; import { usePrivateLegacyPagesMigrationModal } from '~/stores/modal'; import { mutatePageTree, useSWRxV5MigrationStatus } from '~/stores/page-listing'; import { useSWRxSearch, } from '~/stores/search'; import { MenuItemType } from './Common/Dropdown/PageItemControl'; import PaginationWrapper from './PaginationWrapper'; import { PrivateLegacyPagesMigrationModal } from './PrivateLegacyPagesMigrationModal'; import { OperateAllControl } from './SearchPage/OperateAllControl'; import SearchControl from './SearchPage/SearchControl'; import type { IReturnSelectedPageIds } from './SearchPage/SearchPageBase'; import { 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')}
{/* eslint-disable-next-line react/no-danger */}

); } 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') })}
{/* eslint-disable-next-line react/no-danger */}

); }); 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); }, [props.isOpen]); 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 { data: isAdmin } = useIsAdmin(); const [keyword, setKeyword] = useState(initQ); 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(null); 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(); setKeyword(_keyword); setOffset(0); }, [mutateMigrationStatus]); const { open: openModal, close: closeModal } = usePrivateLegacyPagesMigrationModal(); const { data: 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()}
); // eslint-disable-next-line max-len }, [convertMenuItemClickedHandler, deleteAllButtonClickedHandler, hitsCount, isAdmin, isControlEnabled, renderOpenModalButton, selectAllCheckboxChangedHandler, t]); const searchControl = useMemo(() => { return ( ); }, [searchInvokedHandler, extraControls]); const searchResultListHead = useMemo(() => { if (data == null) { return <>; } return ( ); }, [data, limit, offset, pagingSizeChangedHandler, migrationStatus]); const searchPager = useMemo(() => { // when pager is not needed if (data == null || data.meta.hitsCount === data.meta.total) { 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;