import React, { useCallback, useState, FC, useEffect, memo, } from 'react'; import nodePath from 'path'; import { useTranslation } from 'react-i18next'; import { pagePathUtils } from '@growi/core'; import { useDrag, useDrop } from 'react-dnd'; import { toastWarning } from '~/client/util/apiNotification'; import { ItemNode } from './ItemNode'; import { IPageHasId } from '~/interfaces/page'; import { useSWRxPageChildren } from '../../../stores/page-listing'; import ClosableTextInput, { AlertInfo, AlertType } from '../../Common/ClosableTextInput'; import PageItemControl from '../../Common/Dropdown/PageItemControl'; import { IPageForPageDeleteModal } from '~/components/PageDeleteModal'; import TriangleIcon from '~/components/Icons/TriangleIcon'; const { isTopPage } = pagePathUtils; interface ItemProps { isEnableActions: boolean itemNode: ItemNode targetPathOrId?: string isOpen?: boolean onClickDeleteByPage?(page: IPageForPageDeleteModal): void } // Utility to mark target const markTarget = (children: ItemNode[], targetPathOrId?: string): void => { if (targetPathOrId == null) { return; } children.forEach((node) => { if (node.page._id === targetPathOrId || node.page.path === targetPathOrId) { node.page.isTarget = true; } return node; }); }; type ItemControlProps = { page: Partial isEnableActions: boolean isDeletable: boolean onClickPlusButton?(): void onClickDeleteButton?(): void onClickRenameButton?(): void } const ItemControl: FC = memo((props: ItemControlProps) => { const onClickPlusButton = () => { if (props.onClickPlusButton == null) { return; } props.onClickPlusButton(); }; const onClickDeleteButtonHandler = () => { if (props.onClickDeleteButton == null) { return; } props.onClickDeleteButton(); }; const onClickRenameButtonHandler = () => { if (props.onClickRenameButton == null) { return; } props.onClickRenameButton(); }; if (props.page == null) { return <>; } return ( <> ); }); const ItemCount: FC = () => { return ( <> {/* TODO: consider to show the number of children pages */} 00 ); }; const Item: FC = (props: ItemProps) => { const { t } = useTranslation(); const { itemNode, targetPathOrId, isOpen: _isOpen = false, onClickDeleteByPage, isEnableActions, } = props; const { page, children } = itemNode; const [currentChildren, setCurrentChildren] = useState(children); const [isOpen, setIsOpen] = useState(_isOpen); const [isNewPageInputShown, setNewPageInputShown] = useState(false); const [isRenameInputShown, setRenameInputShown] = useState(false); const { data, error } = useSWRxPageChildren(isOpen ? page._id : null); const hasDescendants = (page.descendantCount != null && page?.descendantCount > 0); const [{ isDragging }, drag] = useDrag(() => ({ type: 'PAGE_TREE', item: { page }, collect: monitor => ({ isDragging: monitor.isDragging(), }), })); const pageItemDropHandler = () => { // TODO: hit an api to rename the page by 85175 // eslint-disable-next-line no-console console.log('pageItem was droped!!'); }; const [{ isOver }, drop] = useDrop(() => ({ accept: 'PAGE_TREE', drop: pageItemDropHandler, hover: (item, monitor) => { // when a drag item is overlapped more than 1 sec, the drop target item will be opened. if (monitor.isOver()) { setTimeout(() => { if (monitor.isOver()) { setIsOpen(true); } }, 1000); } }, collect: monitor => ({ isOver: monitor.isOver(), }), })); const hasChildren = useCallback((): boolean => { return currentChildren != null && currentChildren.length > 0; }, [currentChildren]); const onClickLoadChildren = useCallback(async() => { setIsOpen(!isOpen); }, [isOpen]); const onClickPlusButton = useCallback(() => { setNewPageInputShown(true); }, []); const onClickDeleteButton = useCallback(() => { if (onClickDeleteByPage == null) { return; } const { _id: pageId, revision: revisionId, path } = page; if (pageId == null || revisionId == null || path == null) { throw Error('Any of _id, revision, and path must not be null.'); } const pageToDelete: IPageForPageDeleteModal = { pageId, revisionId: revisionId as string, path, }; onClickDeleteByPage(pageToDelete); }, [page, onClickDeleteByPage]); const onClickRenameButton = useCallback(() => { setRenameInputShown(true); }, []); // TODO: make a put request to pages/title const onPressEnterForRenameHandler = () => { toastWarning(t('search_result.currently_not_implemented')); setRenameInputShown(false); }; // TODO: go to create page page const onPressEnterForCreateHandler = () => { toastWarning(t('search_result.currently_not_implemented')); setNewPageInputShown(false); }; const inputValidator = (title: string | null): AlertInfo | null => { if (title == null || title === '') { return { type: AlertType.WARNING, message: t('form_validation.title_required'), }; } return null; }; // didMount useEffect(() => { if (hasChildren()) setIsOpen(true); }, []); /* * Make sure itemNode.children and currentChildren are synced */ useEffect(() => { if (children.length > currentChildren.length) { markTarget(children, targetPathOrId); setCurrentChildren(children); } }, []); /* * When swr fetch succeeded */ useEffect(() => { if (isOpen && error == null && data != null) { const newChildren = ItemNode.generateNodesFromPages(data.children); markTarget(newChildren, targetPathOrId); setCurrentChildren(newChildren); } }, [data, isOpen]); return (
  • { drag(c); drop(c) }} className={`list-group-item list-group-item-action border-0 py-1 d-flex align-items-center ${page.isTarget ? 'grw-pagetree-is-target' : ''}`} >
    {hasDescendants && ( )}
    { isRenameInputShown && ( { setRenameInputShown(false) }} onPressEnter={onPressEnterForRenameHandler} inputValidator={inputValidator} /> )} { !isRenameInputShown && (

    {nodePath.basename(page.path as string) || '/'}

    )}
  • {isEnableActions && ( { setNewPageInputShown(false) }} onPressEnter={onPressEnterForCreateHandler} inputValidator={inputValidator} /> )} { isOpen && hasChildren() && currentChildren.map(node => (
    )) }
    ); }; export default Item;