Yuki Takei 2 лет назад
Родитель
Сommit
4c4e946287

+ 5 - 32
apps/app/src/components/TreeItem/NewPageCreateButton.tsx

@@ -1,48 +1,21 @@
-import React, {
-  useCallback, FC,
-} from 'react';
+import React, { FC } from 'react';
 
 
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
 
 
 import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
 import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
 import { NotAvailableForReadOnlyUser } from '~/components/NotAvailableForReadOnlyUser';
 import { NotAvailableForReadOnlyUser } from '~/components/NotAvailableForReadOnlyUser';
 import { IPageForItem } from '~/interfaces/page';
 import { IPageForItem } from '~/interfaces/page';
-import { usePageTreeDescCountMap } from '~/stores/ui';
 
 
-import { ItemNode } from './ItemNode';
-import type { StateHandlersType } from './state-handlers-type';
-
-
-export type NewPageCreateButtonProps = {
+type NewPageCreateButtonProps = {
   page: IPageForItem,
   page: IPageForItem,
-  currentChildren: ItemNode[],
-  stateHandlers: StateHandlersType,
-  isNewPageInputShown?: boolean,
-  setNewPageInputShown: React.Dispatch<React.SetStateAction<boolean>>,
+  onClick?: () => void,
 };
 };
 
 
 export const NewPageCreateButton: FC<NewPageCreateButtonProps> = (props) => {
 export const NewPageCreateButton: FC<NewPageCreateButtonProps> = (props) => {
   const {
   const {
-    page, currentChildren, stateHandlers, setNewPageInputShown,
+    page, onClick,
   } = props;
   } = props;
 
 
-  const { setIsOpen } = stateHandlers;
-
-  // descendantCount
-  const { getDescCount } = usePageTreeDescCountMap();
-  const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
-
-  const isChildrenLoaded = currentChildren?.length > 0;
-  const hasDescendants = descendantCount > 0 || isChildrenLoaded;
-
-  const onClickPlusButton = useCallback(() => {
-    setNewPageInputShown(true);
-
-    if (hasDescendants) {
-      setIsOpen(true);
-    }
-  }, [hasDescendants, setIsOpen, setNewPageInputShown]);
-
   return (
   return (
     <>
     <>
       {!pagePathUtils.isUsersTopPage(page.path ?? '') && (
       {!pagePathUtils.isUsersTopPage(page.path ?? '') && (
@@ -52,7 +25,7 @@ export const NewPageCreateButton: FC<NewPageCreateButtonProps> = (props) => {
               id="page-create-button-in-page-tree"
               id="page-create-button-in-page-tree"
               type="button"
               type="button"
               className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
               className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
-              onClick={onClickPlusButton}
+              onClick={onClick}
             >
             >
               <i className="icon-plus d-block p-0" />
               <i className="icon-plus d-block p-0" />
             </button>
             </button>

+ 20 - 43
apps/app/src/components/TreeItem/NewPageInput.tsx

@@ -1,43 +1,35 @@
-import React, { FC, useCallback, useEffect } from 'react';
+import React, { type FC, useCallback, useEffect } from 'react';
 
 
 import nodePath from 'path';
 import nodePath from 'path';
 
 
-
 import { pathUtils, pagePathUtils } from '@growi/core/dist/utils';
 import { pathUtils, pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import { apiv3Post } from '~/client/util/apiv3-client';
 import { ValidationTarget } from '~/client/util/input-validator';
 import { ValidationTarget } from '~/client/util/input-validator';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
 import ClosableTextInput from '~/components/Common/ClosableTextInput';
 import ClosableTextInput from '~/components/Common/ClosableTextInput';
-import { useSWRxPageChildren } from '~/stores/page-listing';
-import { usePageTreeDescCountMap } from '~/stores/ui';
+import type { IPageForItem } from '~/interfaces/page';
 
 
-import { NewPageCreateButtonProps } from './NewPageCreateButton';
-import { NotDraggableForClosableTextInput } from './SimpleItem';
+import { NotDraggableForClosableTextInput } from './NotDraggableForClosableTextInput';
 
 
-type NewPageInputProps = NewPageCreateButtonProps & {isEnableActions: boolean};
+type Props = {
+  page: IPageForItem,
+  isEnableActions: boolean,
+  onSubmit?: (newPagePath: string) => Promise<void>,
+  onSubmittionFailed?: () => void,
+  onCanceled?: () => void,
+};
 
 
-export const NewPageInput: FC<NewPageInputProps> = (props) => {
+export const NewPageInput: FC<Props> = (props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const {
   const {
-    page, isEnableActions, currentChildren, stateHandlers, isNewPageInputShown, setNewPageInputShown,
+    page, isEnableActions,
+    onSubmit, onSubmittionFailed,
+    onCanceled,
   } = props;
   } = props;
 
 
-  const { isOpen, setIsOpen, setCreating } = stateHandlers;
-
-  const { mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
-
-  const { getDescCount } = usePageTreeDescCountMap();
-  const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
-
-  const isChildrenLoaded = currentChildren?.length > 0;
-  const hasDescendants = descendantCount > 0 || isChildrenLoaded;
-
   const onPressEnterForCreateHandler = async(inputText: string) => {
   const onPressEnterForCreateHandler = async(inputText: string) => {
-    setNewPageInputShown(false);
-    // closeNewPageInput();
     const parentPath = pathUtils.addTrailingSlash(page.path as string);
     const parentPath = pathUtils.addTrailingSlash(page.path as string);
     const newPagePath = nodePath.resolve(parentPath, inputText);
     const newPagePath = nodePath.resolve(parentPath, inputText);
     const isCreatable = pagePathUtils.isCreatablePage(newPagePath);
     const isCreatable = pagePathUtils.isCreatablePage(newPagePath);
@@ -48,37 +40,22 @@ export const NewPageInput: FC<NewPageInputProps> = (props) => {
     }
     }
 
 
     try {
     try {
-      setCreating(true);
-
-      await apiv3Post('/pages/', {
-        path: newPagePath,
-        body: undefined,
-        grant: page.grant,
-        // grantUserGroupId: page.grantedGroup,
-        grantUserGroupIds: page.grantedGroups,
-      });
-
-      mutateChildren();
-
-      if (!hasDescendants) {
-        setIsOpen(true);
-      }
-
+      onSubmit?.(newPagePath);
       toastSuccess(t('successfully_saved_the_page'));
       toastSuccess(t('successfully_saved_the_page'));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
     finally {
     finally {
-      setCreating(false);
+      onSubmittionFailed?.();
     }
     }
   };
   };
 
 
   const onPressEscHandler = useCallback((event) => {
   const onPressEscHandler = useCallback((event) => {
     if (event.keyCode === 27) {
     if (event.keyCode === 27) {
-      setNewPageInputShown(false);
+      onCanceled?.();
     }
     }
-  }, []);
+  }, [onCanceled]);
 
 
   useEffect(() => {
   useEffect(() => {
     document.addEventListener('keydown', onPressEscHandler, false);
     document.addEventListener('keydown', onPressEscHandler, false);
@@ -89,11 +66,11 @@ export const NewPageInput: FC<NewPageInputProps> = (props) => {
 
 
   return (
   return (
     <>
     <>
-      {isEnableActions && isNewPageInputShown && (
+      {isEnableActions && (
         <NotDraggableForClosableTextInput>
         <NotDraggableForClosableTextInput>
           <ClosableTextInput
           <ClosableTextInput
             placeholder={t('Input page name')}
             placeholder={t('Input page name')}
-            onClickOutside={() => { setNewPageInputShown(false) }}
+            onClickOutside={onCanceled}
             onPressEnter={onPressEnterForCreateHandler}
             onPressEnter={onPressEnterForCreateHandler}
             validationTarget={ValidationTarget.PAGE}
             validationTarget={ValidationTarget.PAGE}
           />
           />

+ 13 - 0
apps/app/src/components/TreeItem/NotDraggableForClosableTextInput.tsx

@@ -0,0 +1,13 @@
+import type { ReactNode } from 'react';
+
+type NotDraggableProps = {
+  children: ReactNode,
+};
+
+/**
+ * Component wrapper to make a child element not draggable
+ * @see https://github.com/react-dnd/react-dnd/issues/335
+ */
+export const NotDraggableForClosableTextInput = (props: NotDraggableProps): JSX.Element => {
+  return <div draggable onDragStart={e => e.preventDefault()}>{props.children}</div>;
+};

+ 11 - 39
apps/app/src/components/TreeItem/SimpleItem.tsx

@@ -1,17 +1,16 @@
 import React, {
 import React, {
-  useCallback, useState, FC, useEffect, ReactNode,
+  useCallback, useState, FC, useEffect,
 } from 'react';
 } from 'react';
 
 
 import nodePath from 'path';
 import nodePath from 'path';
 
 
-import type { Nullable, IPageToDeleteWithMeta } from '@growi/core';
+import type { Nullable } from '@growi/core';
 import { pathUtils } from '@growi/core/dist/utils';
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
-import { IPageForItem } from '~/interfaces/page';
-import { IPageForPageDuplicateModal } from '~/stores/modal';
+import type { IPageForItem } from '~/interfaces/page';
 import { useSWRxPageChildren } from '~/stores/page-listing';
 import { useSWRxPageChildren } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import { shouldRecoverPagePaths } from '~/utils/page-operation';
 import { shouldRecoverPagePaths } from '~/utils/page-operation';
@@ -19,24 +18,9 @@ import { shouldRecoverPagePaths } from '~/utils/page-operation';
 import CountBadge from '../Common/CountBadge';
 import CountBadge from '../Common/CountBadge';
 
 
 import { ItemNode } from './ItemNode';
 import { ItemNode } from './ItemNode';
-import { StateHandlersType } from './state-handlers-type';
-
-
-export type SimpleItemProps = {
-  isEnableActions: boolean
-  isReadOnlyUser: boolean
-  itemNode: ItemNode
-  targetPathOrId?: Nullable<string>
-  isOpen?: boolean
-  onRenamed?(fromPath: string | undefined, toPath: string): void
-  onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void
-  onClickDeleteMenuItem?(pageToDelete: IPageToDeleteWithMeta): void
-  itemRef?
-  itemClass?: React.FunctionComponent<SimpleItemProps>
-  mainClassName?: string
-  customEndComponents?: Array<React.FunctionComponent<SimpleItemToolProps>>
-  customNextComponents?: Array<React.FunctionComponent<SimpleItemToolProps>>
-};
+import { useNewPageInput } from './UseNewPageInput';
+import { SimpleItemProps, SimpleItemStateHandlers, SimpleItemToolProps } from './interfaces';
+
 
 
 // Utility to mark target
 // Utility to mark target
 const markTarget = (children: ItemNode[], targetPathOrId?: Nullable<string>): void => {
 const markTarget = (children: ItemNode[], targetPathOrId?: Nullable<string>): void => {
@@ -70,21 +54,10 @@ const markTarget = (children: ItemNode[], targetPathOrId?: Nullable<string>): vo
  * @returns
  * @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 <div draggable onDragStart={e => e.preventDefault()}>{props.children}</div>;
-};
-
-type SimpleItemToolPropsOptional = 'itemNode' | 'targetPathOrId' | 'isOpen' | 'itemRef' | 'itemClass' | 'mainClassName';
-export type SimpleItemToolProps = Omit<SimpleItemProps, SimpleItemToolPropsOptional> & {page: IPageForItem};
-
 export const SimpleItemTool: FC<SimpleItemToolProps> = (props) => {
 export const SimpleItemTool: FC<SimpleItemToolProps> = (props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const router = useRouter();
   const router = useRouter();
+
   const { getDescCount } = usePageTreeDescCountMap();
   const { getDescCount } = usePageTreeDescCountMap();
 
 
   const page = props.page;
   const page = props.page;
@@ -134,7 +107,7 @@ export const SimpleItemTool: FC<SimpleItemToolProps> = (props) => {
 type SimpleItemContentProps = SimpleItemProps & {
 type SimpleItemContentProps = SimpleItemProps & {
   page: IPageForItem,
   page: IPageForItem,
   children: ItemNode[],
   children: ItemNode[],
-  stateHandlers: StateHandlersType,
+  stateHandlers: SimpleItemStateHandlers,
 };
 };
 
 
 export const SimpleItem: FC<SimpleItemProps> = (props) => {
 export const SimpleItem: FC<SimpleItemProps> = (props) => {
@@ -146,17 +119,16 @@ export const SimpleItem: FC<SimpleItemProps> = (props) => {
 
 
   const { page, children } = itemNode;
   const { page, children } = itemNode;
 
 
+  const { isProcessingSubmission } = useNewPageInput();
+
   const [currentChildren, setCurrentChildren] = useState(children);
   const [currentChildren, setCurrentChildren] = useState(children);
   const [isOpen, setIsOpen] = useState(_isOpen);
   const [isOpen, setIsOpen] = useState(_isOpen);
-  const [isCreating, setCreating] = useState(false);
 
 
   const { data } = useSWRxPageChildren(isOpen ? page._id : null);
   const { data } = useSWRxPageChildren(isOpen ? page._id : null);
 
 
   const stateHandlers = {
   const stateHandlers = {
     isOpen,
     isOpen,
     setIsOpen,
     setIsOpen,
-    isCreating,
-    setCreating,
   };
   };
 
 
   // descendantCount
   // descendantCount
@@ -274,7 +246,7 @@ export const SimpleItem: FC<SimpleItemProps> = (props) => {
         isOpen && hasChildren() && currentChildren.map((node, index) => (
         isOpen && hasChildren() && currentChildren.map((node, index) => (
           <div key={node.page._id} className="grw-pagetree-item-children">
           <div key={node.page._id} className="grw-pagetree-item-children">
             <ItemClassFixed itemNode={node} {...commonProps} />
             <ItemClassFixed itemNode={node} {...commonProps} />
-            {isCreating && (currentChildren.length - 1 === index) && (
+            {isProcessingSubmission && (currentChildren.length - 1 === index) && (
               <div className="text-muted text-center">
               <div className="text-muted text-center">
                 <i className="fa fa-spinner fa-pulse mr-1"></i>
                 <i className="fa fa-spinner fa-pulse mr-1"></i>
               </div>
               </div>

+ 76 - 14
apps/app/src/components/TreeItem/UseNewPageInput.tsx

@@ -1,44 +1,106 @@
-import React, { useState, FC } from 'react';
+import React, { useState, type FC, useCallback } from 'react';
+
+import { apiv3Post } from '~/client/util/apiv3-client';
+import { useSWRxPageChildren } from '~/stores/page-listing';
+import { usePageTreeDescCountMap } from '~/stores/ui';
 
 
 import { ItemNode } from './ItemNode';
 import { ItemNode } from './ItemNode';
 import { NewPageCreateButton } from './NewPageCreateButton';
 import { NewPageCreateButton } from './NewPageCreateButton';
 import { NewPageInput } from './NewPageInput';
 import { NewPageInput } from './NewPageInput';
-import { SimpleItemToolProps } from './SimpleItem';
-import { StateHandlersType } from './state-handlers-type';
+import { SimpleItemToolProps } from './interfaces';
 
 
-type UseNewPageInputProps = SimpleItemToolProps & {children: ItemNode[], stateHandlers: StateHandlersType };
+type UseNewPageInputProps = SimpleItemToolProps & {
+  children: ItemNode[],
+};
 
 
 export const useNewPageInput = () => {
 export const useNewPageInput = () => {
 
 
-  const [isNewPageInputShown, setNewPageInputShown] = useState(false);
+  const [showInput, setShowInput] = useState(false);
+  const [isProcessingSubmission, setProcessingSubmission] = useState(false);
+
+  const { getDescCount } = usePageTreeDescCountMap();
 
 
   const NewPageCreateButtonWrapper: FC<UseNewPageInputProps> = (props) => {
   const NewPageCreateButtonWrapper: FC<UseNewPageInputProps> = (props) => {
+
+    const { page, children, stateHandlers } = props;
+    const { setIsOpen } = stateHandlers;
+
+    // descendantCount
+    const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
+
+    const isChildrenLoaded = children?.length > 0;
+    const hasDescendants = descendantCount > 0 || isChildrenLoaded;
+
+    const onClick = useCallback(() => {
+      setShowInput(true);
+
+      if (hasDescendants) {
+        setIsOpen(true);
+      }
+    }, [hasDescendants, setIsOpen]);
+
     return (
     return (
       <NewPageCreateButton
       <NewPageCreateButton
         page={props.page}
         page={props.page}
-        currentChildren={props.children}
-        stateHandlers={props.stateHandlers}
-        setNewPageInputShown={setNewPageInputShown}
+        onClick={onClick}
       />
       />
     );
     );
   };
   };
 
 
   const NewPageInputWrapper: FC<UseNewPageInputProps> = (props) => {
   const NewPageInputWrapper: FC<UseNewPageInputProps> = (props) => {
-    return (
+
+    const {
+      page, children, stateHandlers,
+    } = props;
+
+    const { isOpen, setIsOpen } = stateHandlers;
+
+    const { mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
+
+    const { getDescCount } = usePageTreeDescCountMap();
+    const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
+
+    const isChildrenLoaded = children?.length > 0;
+    const hasDescendants = descendantCount > 0 || isChildrenLoaded;
+
+    const submitHandler = useCallback(async(newPagePath: string) => {
+      setProcessingSubmission(true);
+
+      setShowInput(false);
+
+      await apiv3Post('/pages/', {
+        path: newPagePath,
+        body: undefined,
+        grant: page.grant,
+        // grantUserGroupId: page.grantedGroup,
+        grantUserGroupIds: page.grantedGroups,
+      });
+
+      mutateChildren();
+
+      if (!hasDescendants) {
+        setIsOpen(true);
+      }
+    }, [hasDescendants, mutateChildren, page.grant, page.grantedGroups, setIsOpen]);
+
+    const submittionFailedHandler = useCallback(() => {
+      setProcessingSubmission(false);
+    }, []);
+
+    return showInput && (
       <NewPageInput
       <NewPageInput
         page={props.page}
         page={props.page}
         isEnableActions={props.isEnableActions}
         isEnableActions={props.isEnableActions}
-        currentChildren={props.children}
-        stateHandlers={props.stateHandlers}
-        isNewPageInputShown={isNewPageInputShown}
-        setNewPageInputShown={setNewPageInputShown}
+        onSubmit={submitHandler}
+        onSubmittionFailed={submittionFailedHandler}
+        onCanceled={() => setShowInput(false)}
       />
       />
     );
     );
   };
   };
 
 
-
   return {
   return {
     NewPageInputWrapper,
     NewPageInputWrapper,
     NewPageCreateButtonWrapper,
     NewPageCreateButtonWrapper,
+    isProcessingSubmission,
   };
   };
 };
 };

+ 5 - 2
apps/app/src/components/TreeItem/index.ts

@@ -1,3 +1,6 @@
-export { useNewPageInput } from './UseNewPageInput';
-export * from './SimpleItem';
+export * from './interfaces';
+
+export * from './UseNewPageInput';
 export * from './ItemNode';
 export * from './ItemNode';
+export * from './SimpleItem';
+export * from './NotDraggableForClosableTextInput';

+ 35 - 0
apps/app/src/components/TreeItem/interfaces/index.ts

@@ -0,0 +1,35 @@
+import type { IPageToDeleteWithMeta } from '@growi/core';
+import type { Nullable } from 'vitest';
+
+import type { IPageForItem } from '~/interfaces/page';
+import type { IPageForPageDuplicateModal } from '~/stores/modal';
+
+import type { ItemNode } from '../ItemNode';
+
+export type SimpleItemStateHandlers = {
+  isOpen: boolean,
+  setIsOpen: React.Dispatch<React.SetStateAction<boolean>>,
+};
+
+type SimpleItemToolPropsOptional = 'itemNode' | 'targetPathOrId' | 'isOpen' | 'itemRef' | 'itemClass' | 'mainClassName';
+
+export type SimpleItemToolProps = Omit<SimpleItemProps, SimpleItemToolPropsOptional> & {
+  page: IPageForItem,
+  stateHandlers: SimpleItemStateHandlers,
+};
+
+export type SimpleItemProps = {
+  isEnableActions: boolean
+  isReadOnlyUser: boolean
+  itemNode: ItemNode
+  targetPathOrId?: Nullable<string>
+  isOpen?: boolean
+  onRenamed?(fromPath: string | undefined, toPath: string): void
+  onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void
+  onClickDeleteMenuItem?(pageToDelete: IPageToDeleteWithMeta): void
+  itemRef?
+  itemClass?: React.FunctionComponent<SimpleItemProps>
+  mainClassName?: string
+  customEndComponents?: Array<React.FunctionComponent<SimpleItemToolProps>>
+  customNextComponents?: Array<React.FunctionComponent<SimpleItemToolProps>>
+};

+ 0 - 6
apps/app/src/components/TreeItem/state-handlers-type.ts

@@ -1,6 +0,0 @@
-export type StateHandlersType = {
-  isOpen: boolean,
-  setIsOpen: React.Dispatch<React.SetStateAction<boolean>>,
-  isCreating: boolean,
-  setCreating: React.Dispatch<React.SetStateAction<boolean>>,
-};