import React, { useCallback, useState, useEffect, type FC, type RefObject, type RefCallback, type MouseEvent, } from 'react'; import nodePath from 'path'; import type { Nullable } from '@growi/core'; import { LoadingSpinner } from '@growi/ui/dist/components'; import { useTranslation } from 'next-i18next'; import { UncontrolledTooltip } from 'reactstrap'; import type { IPageForItem } from '~/interfaces/page'; import { useSWRxPageChildren } from '~/stores/page-listing'; import { usePageTreeDescCountMap } from '~/stores/ui'; import { shouldRecoverPagePaths } from '~/utils/page-operation'; import CountBadge from '../Common/CountBadge'; import { ItemNode } from './ItemNode'; import { useNewPageInput } from './NewPageInput'; import type { TreeItemProps, TreeItemToolProps } from './interfaces'; // Utility to mark target const markTarget = (children: ItemNode[], targetPathOrId?: Nullable): void => { if (targetPathOrId == null) { return; } children.forEach((node) => { if (node.page._id === targetPathOrId || node.page.path === targetPathOrId) { node.page.isTarget = true; } else { node.page.isTarget = false; } return node; }); }; const SimpleItemContent = ({ page }: { page: IPageForItem }) => { const { t } = useTranslation(); const pageName = nodePath.basename(page.path ?? '') || '/'; const shouldShowAttentionIcon = page.processData != null ? shouldRecoverPagePaths(page.processData) : false; return (
{shouldShowAttentionIcon && ( <> warning {t('tooltip.operation.attention.rename')} )} {page != null && page.path != null && page._id != null && (
{pageName} { page.wip && ( WIP )}
)}
); }; export const SimpleItemTool: FC = (props) => { const { getDescCount } = usePageTreeDescCountMap(); const { page } = props.itemNode; const descendantCount = getDescCount(page._id) || page.descendantCount || 0; return ( <> {descendantCount > 0 && (
)} ); }; type SimpleItemProps = TreeItemProps & { itemRef?: RefObject | RefCallback, } export const SimpleItem: FC = (props) => { const { itemNode, targetPathOrId, isOpen: _isOpen = false, onRenamed, onClick, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions, isReadOnlyUser, isWipPageShown = true, itemRef, itemClass, mainClassName, } = props; const { page, children } = itemNode; const { isProcessingSubmission } = useNewPageInput(); const [currentChildren, setCurrentChildren] = useState(children); const [isOpen, setIsOpen] = useState(_isOpen); const { data } = useSWRxPageChildren(isOpen ? page._id : null); const itemClickHandler = useCallback((e: MouseEvent) => { // DO NOT handle the event when e.currentTarget and e.target is different if (e.target !== e.currentTarget) { return; } onClick?.(page); }, [onClick, page]); // descendantCount const { getDescCount } = usePageTreeDescCountMap(); const descendantCount = getDescCount(page._id) || page.descendantCount || 0; // hasDescendants flag const isChildrenLoaded = currentChildren?.length > 0; const hasDescendants = descendantCount > 0 || isChildrenLoaded; const hasChildren = useCallback((): boolean => { return currentChildren != null && currentChildren.length > 0; }, [currentChildren]); const onClickLoadChildren = useCallback(() => { setIsOpen(!isOpen); }, [isOpen]); // didMount useEffect(() => { if (hasChildren()) setIsOpen(true); }, [hasChildren]); /* * Make sure itemNode.children and currentChildren are synced */ useEffect(() => { if (children.length > currentChildren.length) { markTarget(children, targetPathOrId); setCurrentChildren(children); } }, [children, currentChildren.length, targetPathOrId]); /* * When swr fetch succeeded */ useEffect(() => { if (isOpen && data != null) { const newChildren = ItemNode.generateNodesFromPages(data.children); markTarget(newChildren, targetPathOrId); setCurrentChildren(newChildren); } }, [data, isOpen, targetPathOrId]); const ItemClassFixed = itemClass ?? SimpleItem; const EndComponents = props.customEndComponents ?? [SimpleItemTool]; const baseProps: Omit = { isEnableActions, isReadOnlyUser, isOpen: false, isWipPageShown, targetPathOrId, onRenamed, onClickDuplicateMenuItem, onClickDeleteMenuItem, }; const toolProps: TreeItemToolProps = { ...baseProps, itemNode, }; const CustomNextComponents = props.customNextComponents; if (!isWipPageShown && page.wip) { return <>; } return (
  • {hasDescendants && ( )}
    {EndComponents.map((EndComponent, index) => ( // eslint-disable-next-line react/no-array-index-key ))}
  • {CustomNextComponents?.map((UnderItemContent, index) => ( // eslint-disable-next-line react/no-array-index-key ))} { isOpen && hasChildren() && currentChildren.map((node, index) => { const itemProps = { ...baseProps, itemNode: node, itemClass, mainClassName, onClick, }; return (
    {isProcessingSubmission && (currentChildren.length - 1 === index) && (
    )}
    ); }) }
    ); };