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;