import React, { useCallback, useState, FC, useEffect, ReactNode, } from 'react'; import nodePath from 'path'; import type { Nullable, IPageToDeleteWithMeta } from '@growi/core'; import { pathUtils } from '@growi/core/dist/utils'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import { UncontrolledTooltip } from 'reactstrap'; import { IPageForItem } from '~/interfaces/page'; import { IPageForPageDuplicateModal } from '~/stores/modal'; 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'; export type SimpleItemProps = { isEnableActions: boolean isReadOnlyUser: boolean itemNode: ItemNode targetPathOrId?: Nullable isOpen?: boolean onRenamed?(fromPath: string | undefined, toPath: string): void onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void onClickDeleteMenuItem?(pageToDelete: IPageToDeleteWithMeta): void itemRef? itemClass?: React.FunctionComponent mainClassName?: string customEndComponents?: Array> customNextComponents?: Array> }; // 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; }); }; /** * Return new page path after the droppedPagePath is moved under the newParentPagePath * @param droppedPagePath * @param newParentPagePath * @returns */ /** * Return whether the fromPage could be moved under the newParentPage * @param fromPage * @param newParentPage * @param printLog * @returns */ // Component wrapper to make a child element not draggable // https://github.com/react-dnd/react-dnd/issues/335 type NotDraggableProps = { children: ReactNode, }; export const NotDraggableForClosableTextInput = (props: NotDraggableProps): JSX.Element => { return
e.preventDefault()}>{props.children}
; }; type SimpleItemToolPropsOptional = 'itemNode' | 'targetPathOrId' | 'isOpen' | 'itemRef' | 'itemClass' | 'mainClassName'; export type SimpleItemToolProps = Omit & {page: IPageForItem}; export const SimpleItemTool: FC = (props) => { const { t } = useTranslation(); const router = useRouter(); const { getDescCount } = usePageTreeDescCountMap(); const page = props.page; const pageName = nodePath.basename(page.path ?? '') || '/'; const shouldShowAttentionIcon = page.processData != null ? shouldRecoverPagePaths(page.processData) : false; const descendantCount = getDescCount(page._id) || page.descendantCount || 0; const pageTreeItemClickHandler = (e) => { e.preventDefault(); if (page.path == null || page._id == null) { return; } const link = pathUtils.returnPathForURL(page.path, page._id); router.push(link); }; return ( <> {shouldShowAttentionIcon && ( <> {t('tooltip.operation.attention.rename')} )} {page != null && page.path != null && page._id != null && (

{pageName}

)} {descendantCount > 0 && (
)} ); }; export const SimpleItem: FC = (props) => { const { itemNode, targetPathOrId, isOpen: _isOpen = false, onRenamed, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions, isReadOnlyUser, itemRef, itemClass, mainClassName, } = props; const { page, children } = itemNode; const [currentChildren, setCurrentChildren] = useState(children); const [isOpen, setIsOpen] = useState(_isOpen); const [isCreating, setCreating] = useState(false); const { data } = useSWRxPageChildren(isOpen ? page._id : null); const stateHandlers = { isOpen, setIsOpen, isCreating, setCreating, }; // 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 commonProps = { isEnableActions, isReadOnlyUser, isOpen: false, targetPathOrId, onRenamed, onClickDuplicateMenuItem, onClickDeleteMenuItem, stateHandlers, }; const CustomEndComponents = props.customEndComponents; const SimpleItemContent = CustomEndComponents ?? [SimpleItemTool]; const SimpleItemContentProps = { itemNode, page, onRenamed, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions, isReadOnlyUser, children, stateHandlers, }; const CustomNextComponents = props.customNextComponents; return (
  • {hasDescendants && ( )}
    {SimpleItemContent.map(ItemContent => ( ))}
  • {CustomNextComponents?.map(UnderItemContent => ( ))} { isOpen && hasChildren() && currentChildren.map((node, index) => (
    {isCreating && (currentChildren.length - 1 === index) && (
    )}
    )) }
    ); };