import React, { useState, useCallback, useEffect, } from 'react'; import { type IPageInfoAll, isIPageInfoForOperation, } from '@growi/core'; import { LoadingSpinner } from '@growi/ui/dist/components'; import { useTranslation } from 'next-i18next'; import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem, } from 'reactstrap'; import { NotAvailableForGuest } from '~/components/NotAvailableForGuest'; import type { IPageOperationProcessData } from '~/interfaces/page-operation'; import { useSWRxPageInfo } from '~/stores/page'; import loggerFactory from '~/utils/logger'; import { shouldRecoverPagePaths } from '~/utils/page-operation'; const logger = loggerFactory('growi:cli:PageItemControl'); export const MenuItemType = { BOOKMARK: 'bookmark', RENAME: 'rename', DUPLICATE: 'duplicate', DELETE: 'delete', REVERT: 'revert', PATH_RECOVERY: 'pathRecovery', SWITCH_CONTENT_WIDTH: 'switch_content_width', } as const; export type MenuItemType = typeof MenuItemType[keyof typeof MenuItemType]; export type ForceHideMenuItems = MenuItemType[]; export type AdditionalMenuItemsRendererProps = { pageInfo: IPageInfoAll }; type CommonProps = { pageInfo?: IPageInfoAll, isEnableActions?: boolean, isReadOnlyUser?: boolean, forceHideMenuItems?: ForceHideMenuItems, onClickBookmarkMenuItem?: (pageId: string, newValue?: boolean) => Promise, onClickRenameMenuItem?: (pageId: string, pageInfo: IPageInfoAll | undefined) => Promise | void, onClickDuplicateMenuItem?: (pageId: string) => Promise | void, onClickDeleteMenuItem?: (pageId: string, pageInfo: IPageInfoAll | undefined) => Promise | void, onClickRevertMenuItem?: (pageId: string) => Promise | void, onClickPathRecoveryMenuItem?: (pageId: string) => Promise | void, additionalMenuItemOnTopRenderer?: React.FunctionComponent, additionalMenuItemRenderer?: React.FunctionComponent, isInstantRename?: boolean, alignEnd?: boolean, } type DropdownMenuProps = CommonProps & { pageId: string, isLoading?: boolean, operationProcessData?: IPageOperationProcessData, } const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.Element => { const { t } = useTranslation(''); const { pageId, isLoading, pageInfo, isEnableActions, isReadOnlyUser, forceHideMenuItems, operationProcessData, onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDuplicateMenuItem, onClickDeleteMenuItem, onClickRevertMenuItem, onClickPathRecoveryMenuItem, additionalMenuItemOnTopRenderer: AdditionalMenuItemsOnTop, additionalMenuItemRenderer: AdditionalMenuItems, isInstantRename, alignEnd, } = props; // eslint-disable-next-line react-hooks/rules-of-hooks const bookmarkItemClickedHandler = useCallback(async() => { if (!isIPageInfoForOperation(pageInfo) || onClickBookmarkMenuItem == null) { return; } await onClickBookmarkMenuItem(pageId, !pageInfo.isBookmarked); }, [onClickBookmarkMenuItem, pageId, pageInfo]); // eslint-disable-next-line react-hooks/rules-of-hooks const renameItemClickedHandler = useCallback(async() => { if (onClickRenameMenuItem == null) { return; } if (!pageInfo?.isMovable) { logger.warn('This page could not be renamed.'); return; } await onClickRenameMenuItem(pageId, pageInfo); }, [onClickRenameMenuItem, pageId, pageInfo]); // eslint-disable-next-line react-hooks/rules-of-hooks const duplicateItemClickedHandler = useCallback(async() => { if (onClickDuplicateMenuItem == null) { return; } await onClickDuplicateMenuItem(pageId); }, [onClickDuplicateMenuItem, pageId]); const revertItemClickedHandler = useCallback(async() => { if (onClickRevertMenuItem == null) { return; } await onClickRevertMenuItem(pageId); }, [onClickRevertMenuItem, pageId]); // eslint-disable-next-line react-hooks/rules-of-hooks const deleteItemClickedHandler = useCallback(async() => { if (pageInfo == null || onClickDeleteMenuItem == null) { return; } if (!pageInfo.isDeletable) { logger.warn('This page could not be deleted.'); return; } await onClickDeleteMenuItem(pageId, pageInfo); }, [onClickDeleteMenuItem, pageId, pageInfo]); // eslint-disable-next-line react-hooks/rules-of-hooks const pathRecoveryItemClickedHandler = useCallback(async() => { if (onClickPathRecoveryMenuItem == null) { return; } await onClickPathRecoveryMenuItem(pageId); }, [onClickPathRecoveryMenuItem, pageId]); let contents = <>; if (isLoading) { contents = (
); } else if (pageId != null && pageInfo != null) { const showDeviderBeforeAdditionalMenuItems = (forceHideMenuItems?.length ?? 0) < 3; const showDeviderBeforeDelete = AdditionalMenuItems != null || showDeviderBeforeAdditionalMenuItems; // PathRecovery // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl const shouldShowPathRecoveryButton = operationProcessData != null ? shouldRecoverPagePaths(operationProcessData) : false; contents = ( <> { !isEnableActions && (

{t('search_result.currently_not_implemented')}

) } { AdditionalMenuItemsOnTop && ( <> ) } {/* Bookmark */} { !forceHideMenuItems?.includes(MenuItemType.BOOKMARK) && isEnableActions && isIPageInfoForOperation(pageInfo) && ( bookmark { pageInfo.isBookmarked ? t('remove_bookmark') : t('add_bookmark') } ) } {/* Move/Rename */} { !forceHideMenuItems?.includes(MenuItemType.RENAME) && isEnableActions && !isReadOnlyUser && pageInfo.isMovable && ( redo {t(isInstantRename ? 'Rename' : 'Move/Rename')} ) } {/* Duplicate */} { !forceHideMenuItems?.includes(MenuItemType.DUPLICATE) && isEnableActions && !isReadOnlyUser && ( file_copy {t('Duplicate')} ) } {/* Revert */} { !forceHideMenuItems?.includes(MenuItemType.REVERT) && isEnableActions && !isReadOnlyUser && pageInfo.isRevertible && ( undo {t('modal_putback.label.Put Back Page')} ) } { AdditionalMenuItems && ( <> { showDeviderBeforeAdditionalMenuItems && } ) } {/* PathRecovery */} { !forceHideMenuItems?.includes(MenuItemType.PATH_RECOVERY) && isEnableActions && !isReadOnlyUser && shouldShowPathRecoveryButton && ( build {t('PathRecovery')} ) } {/* divider */} {/* Delete */} { !forceHideMenuItems?.includes(MenuItemType.DELETE) && isEnableActions && !isReadOnlyUser && pageInfo.isDeletable && ( <> { showDeviderBeforeDelete && } delete {t('Delete')} )} ); } return ( {contents} ); }); PageItemControlDropdownMenu.displayName = 'PageItemControl'; type PageItemControlSubstanceProps = CommonProps & { pageId: string, children?: React.ReactNode, operationProcessData?: IPageOperationProcessData, } export const PageItemControlSubstance = (props: PageItemControlSubstanceProps): JSX.Element => { const { pageId, pageInfo: presetPageInfo, children, onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDuplicateMenuItem, onClickDeleteMenuItem, onClickPathRecoveryMenuItem, } = props; const [isOpen, setIsOpen] = useState(false); const [shouldFetch, setShouldFetch] = useState(false); const { data: fetchedPageInfo, mutate: mutatePageInfo } = useSWRxPageInfo(shouldFetch ? pageId : null); // update shouldFetch (and will never be false) useEffect(() => { if (shouldFetch) { return; } if (!isIPageInfoForOperation(presetPageInfo) && isOpen) { setShouldFetch(true); } }, [isOpen, presetPageInfo, shouldFetch]); // mutate after handle event const bookmarkMenuItemClickHandler = useCallback(async(_pageId: string, _newValue: boolean) => { if (onClickBookmarkMenuItem != null) { await onClickBookmarkMenuItem(_pageId, _newValue); } if (shouldFetch) { mutatePageInfo(); } }, [mutatePageInfo, onClickBookmarkMenuItem, shouldFetch]); const isLoading = shouldFetch && fetchedPageInfo == null; const renameMenuItemClickHandler = useCallback(async() => { if (onClickRenameMenuItem == null) { return; } await onClickRenameMenuItem(pageId, fetchedPageInfo ?? presetPageInfo); }, [onClickRenameMenuItem, pageId, fetchedPageInfo, presetPageInfo]); const duplicateMenuItemClickHandler = useCallback(async() => { if (onClickDuplicateMenuItem == null) { return; } await onClickDuplicateMenuItem(pageId); }, [onClickDuplicateMenuItem, pageId]); const deleteMenuItemClickHandler = useCallback(async() => { if (onClickDeleteMenuItem == null) { return; } await onClickDeleteMenuItem(pageId, fetchedPageInfo ?? presetPageInfo); }, [onClickDeleteMenuItem, pageId, fetchedPageInfo, presetPageInfo]); const pathRecoveryMenuItemClickHandler = useCallback(async() => { if (onClickPathRecoveryMenuItem == null) { return; } await onClickPathRecoveryMenuItem(pageId); }, [onClickPathRecoveryMenuItem, pageId]); return ( setIsOpen(!isOpen)} className="grw-page-item-control" data-testid="open-page-item-control-btn"> { children ?? ( more_vert ) } ); }; export type PageItemControlProps = CommonProps & { pageId?: string, children?: React.ReactNode, operationProcessData?: IPageOperationProcessData, } export const PageItemControl = (props: PageItemControlProps): JSX.Element => { const { pageId } = props; if (pageId == null) { return <>; } return ; };