import React, { useCallback, useState, useEffect, useMemo, type FC, type RefObject, type RefCallback, type MouseEvent, } from 'react'; import { useSWRxPageChildren } from '~/stores/page-listing'; import { usePageTreeDescCountMap } from '~/stores/ui'; import { ItemNode } from './ItemNode'; import { SimpleItemContent } from './SimpleItemContent'; import type { TreeItemProps, TreeItemToolProps } from './interfaces'; import styles from './TreeItemLayout.module.scss'; const moduleClass = styles['tree-item-layout'] ?? ''; type TreeItemLayoutProps = TreeItemProps & { className?: string, itemRef?: RefObject | RefCallback, indentSize?: number, } export const TreeItemLayout: FC = (props) => { const { className, itemClassName, indentSize = 10, itemLevel: baseItemLevel = 1, itemNode, targetPathOrId, isOpen: _isOpen = false, onRenamed, onClick, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions, isReadOnlyUser, isWipPageShown = true, itemRef, itemClass, showAlternativeContent, } = props; const { page, children } = itemNode; 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) { setCurrentChildren(children); } }, [children, currentChildren.length, targetPathOrId]); /* * When swr fetch succeeded */ useEffect(() => { if (isOpen && data != null) { const newChildren = ItemNode.generateNodesFromPages(data.children); setCurrentChildren(newChildren); } }, [data, isOpen, targetPathOrId]); const isSelected = useMemo(() => { return page._id === targetPathOrId || page.path === targetPathOrId; }, [page, targetPathOrId]); const ItemClassFixed = itemClass ?? TreeItemLayout; const baseProps: Omit = { isEnableActions, isReadOnlyUser, isOpen: false, isWipPageShown, targetPathOrId, onRenamed, onClickDuplicateMenuItem, onClickDeleteMenuItem, }; const toolProps: TreeItemToolProps = { ...baseProps, itemLevel: baseItemLevel, itemNode, stateHandlers: { setIsOpen, }, }; const EndComponents = props.customEndComponents; const HoveredEndComponents = props.customHoveredEndComponents; const HeadObChildrenComponents = props.customHeadOfChildrenComponents; const AlternativeComponents = props.customAlternativeComponents; if (!isWipPageShown && page.wip) { return <>; } return (
1 ? indentSize : 0}px` }} >
  • {hasDescendants && ( )}
    { showAlternativeContent && AlternativeComponents != null ? ( AlternativeComponents.map((AlternativeContent, index) => ( // eslint-disable-next-line react/no-array-index-key )) ) : ( <>
    {EndComponents?.map((EndComponent, index) => ( // eslint-disable-next-line react/no-array-index-key ))}
    {HoveredEndComponents?.map((HoveredEndContent, index) => ( // eslint-disable-next-line react/no-array-index-key ))}
    ) }
  • { isOpen && (
    {HeadObChildrenComponents?.map((HeadObChildrenContents, index) => ( // eslint-disable-next-line react/no-array-index-key ))} { hasChildren() && currentChildren.map((node) => { const itemProps = { ...baseProps, className, itemLevel: baseItemLevel + 1, itemNode: node, itemClass, itemClassName, onClick, }; return ( ); }) }
    ) }
    ); };