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 { useTranslation } from 'next-i18next'; import { UncontrolledTooltip } from 'reactstrap'; 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: FC = (props) => { const { t } = useTranslation(); const { onClick } = props; const { page } = props.itemNode; const pageName = nodePath.basename(page.path ?? '') || '/'; const shouldShowAttentionIcon = page.processData != null ? shouldRecoverPagePaths(page.processData) : false; const clickHandler = (e: MouseEvent) => { if (onClick == null) { return; } e.preventDefault(); onClick(page); }; return ( <> {shouldShowAttentionIcon && ( <> {t('tooltip.operation.attention.rename')} )} {page != null && page.path != null && page._id != null && (

{pageName}

)} ); }; 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, 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); // 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(async() => { 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, targetPathOrId, onRenamed, onClick, onClickDuplicateMenuItem, onClickDeleteMenuItem, }; const toolProps: TreeItemToolProps = { ...baseProps, itemNode, }; const CustomNextComponents = props.customNextComponents; 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, }; return (
    {isProcessingSubmission && (currentChildren.length - 1 === index) && (
    )}
    ); }) }
    ); };