Просмотр исходного кода

Merge pull request #8335 from weseek/feat/134154-136955-apply-if-user-is-guest

feat: Do not displayed `Create Today's ...` for guest users
Yuki Takei 2 лет назад
Родитель
Сommit
716ac68e93

+ 7 - 15
apps/app/src/components/PageHistory/PageRevisionTable.tsx

@@ -2,7 +2,7 @@ import React, {
   useEffect, useRef, useState,
 } from 'react';
 
-import type { IRevisionHasId, IRevisionHasPageId } from '@growi/core';
+import type { IRevisionHasPageId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 
 import { useSWRxInfinitePageRevisions } from '~/stores/page';
@@ -96,27 +96,19 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
   }, [isLoadingMore, isReachingEnd, setSize, size]);
 
 
-  const onChangeSourceInvoked: React.Dispatch<React.SetStateAction<IRevisionHasId | undefined>> = (revision: IRevisionHasPageId) => {
-    setSourceRevision(revision);
-  };
-  const onChangeTargetInvoked: React.Dispatch<React.SetStateAction<IRevisionHasId | undefined>> = (revision: IRevisionHasPageId) => {
-    setTargetRevision(revision);
-  };
-
-
   const renderRow = (revision: IRevisionHasPageId, previousRevision: IRevisionHasPageId, latestRevision: IRevisionHasPageId,
       isOldestRevision: boolean, hasDiff: boolean) => {
 
     const revisionId = revision._id;
 
     const handleCompareLatestRevisionButton = () => {
-      onChangeSourceInvoked(revision);
-      onChangeTargetInvoked(latestRevision);
+      setSourceRevision(revision);
+      setTargetRevision(latestRevision);
     };
 
     const handleComparePreviousRevisionButton = () => {
-      onChangeSourceInvoked(previousRevision);
-      onChangeTargetInvoked(revision);
+      setSourceRevision(previousRevision);
+      setTargetRevision(revision);
     };
 
     return (
@@ -165,7 +157,7 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
                 name="compareSource"
                 value={revisionId}
                 checked={revisionId === sourceRevision?._id}
-                onChange={() => onChangeSourceInvoked(revision)}
+                onChange={() => setSourceRevision(revision)}
               />
               <label className="form-label form-check-label" htmlFor={`compareSource-${revisionId}`} />
             </div>
@@ -181,7 +173,7 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
                 name="compareTarget"
                 value={revisionId}
                 checked={revisionId === targetRevision?._id}
-                onChange={() => onChangeTargetInvoked(revision)}
+                onChange={() => setTargetRevision(revision)}
               />
               <label className="form-label form-check-label" htmlFor={`compareTarget-${revisionId}`} />
             </div>

+ 3 - 3
apps/app/src/components/PageSelectModal/TreeItemForModal.tsx

@@ -9,7 +9,7 @@ type PageTreeItemProps = Omit<SimpleItemProps, Optional> & {key};
 
 export const TreeItemForModal: FC<PageTreeItemProps> = (props) => {
 
-  const { NewPageInputWrapper, NewPageCreateButtonWrapper } = useNewPageInput();
+  const { Input: NewPageInput, CreateButton: NewPageCreateButton } = useNewPageInput();
 
   return (
     <SimpleItem
@@ -22,9 +22,9 @@ export const TreeItemForModal: FC<PageTreeItemProps> = (props) => {
       onRenamed={props.onRenamed}
       onClickDuplicateMenuItem={props.onClickDuplicateMenuItem}
       onClickDeleteMenuItem={props.onClickDeleteMenuItem}
-      customNextComponents={[NewPageInputWrapper]}
+      customNextComponents={[NewPageInput]}
       itemClass={TreeItemForModal}
-      customEndComponents={[SimpleItemTool, NewPageCreateButtonWrapper]}
+      customEndComponents={[SimpleItemTool, NewPageCreateButton]}
     />
   );
 };

+ 17 - 13
apps/app/src/components/Sidebar/PageCreateButton/DropendMenu.tsx

@@ -5,18 +5,18 @@ import { useTranslation } from 'react-i18next';
 import { LabelType } from '~/interfaces/template';
 
 type DropendMenuProps = {
-  todaysPath: string,
   onClickCreateNewPageButtonHandler: () => Promise<void>
   onClickCreateTodaysButtonHandler: () => Promise<void>
   onClickTemplateButtonHandler: (label: LabelType) => Promise<void>
+  todaysPath: string | null,
 }
 
 export const DropendMenu = React.memo((props: DropendMenuProps): JSX.Element => {
   const {
-    todaysPath,
     onClickCreateNewPageButtonHandler,
     onClickCreateTodaysButtonHandler,
     onClickTemplateButtonHandler,
+    todaysPath,
   } = props;
 
   const { t } = useTranslation('commons');
@@ -32,17 +32,21 @@ export const DropendMenu = React.memo((props: DropendMenuProps): JSX.Element =>
           {t('create_page_dropdown.new_page')}
         </button>
       </li>
-      <li><hr className="dropdown-divider" /></li>
-      <li><span className="text-muted px-3">{t('create_page_dropdown.todays.desc')}</span></li>
-      <li>
-        <button
-          className="dropdown-item"
-          onClick={onClickCreateTodaysButtonHandler}
-          type="button"
-        >
-          {todaysPath}
-        </button>
-      </li>
+      {todaysPath != null && (
+        <>
+          <li><hr className="dropdown-divider" /></li>
+          <li><span className="text-muted px-3">{t('create_page_dropdown.todays.desc')}</span></li>
+          <li>
+            <button
+              className="dropdown-item"
+              onClick={onClickCreateTodaysButtonHandler}
+              type="button"
+            >
+              {todaysPath}
+            </button>
+          </li>
+        </>
+      )}
       <li><hr className="dropdown-divider" /></li>
       <li><span className="text-muted text-nowrap px-3">{t('create_page_dropdown.template.desc')}</span></li>
       <li>

+ 12 - 5
apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx

@@ -1,5 +1,6 @@
 import React, { useState, useCallback } from 'react';
 
+import type { IUserHasId } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { format } from 'date-fns';
 import { useTranslation } from 'react-i18next';
@@ -15,6 +16,12 @@ import { DropendMenu } from './DropendMenu';
 import { DropendToggle } from './DropendToggle';
 import { useOnNewButtonClicked, useOnTodaysButtonClicked } from './hooks';
 
+const generateTodaysPath = (currentUser: IUserHasId, parentDirName: string) => {
+  const now = format(new Date(), 'yyyy/MM/dd');
+  const userHomepagePath = pagePathUtils.userHomepagePath(currentUser);
+  return `${userHomepagePath}/${parentDirName}/${now}`;
+};
+
 export const PageCreateButton = React.memo((): JSX.Element => {
   const { t } = useTranslation('commons');
 
@@ -23,12 +30,12 @@ export const PageCreateButton = React.memo((): JSX.Element => {
 
   const [isHovered, setIsHovered] = useState(false);
 
-  const now = format(new Date(), 'yyyy/MM/dd');
-  const userHomepagePath = pagePathUtils.userHomepagePath(currentUser);
-  const todaysPath = `${userHomepagePath}/${t('create_page_dropdown.todays.memo')}/${now}`;
+  const todaysPath = currentUser == null
+    ? null
+    : generateTodaysPath(currentUser, t('create_page_dropdown.todays.memo'));
 
   const { onClickHandler: onClickNewButton, isPageCreating: isNewPageCreating } = useOnNewButtonClicked(currentPagePath, isLoading);
-  const { onClickHandler: onClickTodaysButton, isPageCreating: isTodaysPageCreating } = useOnTodaysButtonClicked(todaysPath, currentUser);
+  const { onClickHandler: onClickTodaysButton, isPageCreating: isTodaysPageCreating } = useOnTodaysButtonClicked(todaysPath);
   const { onClickHandler: onClickTemplateButton, isPageCreating: isTemplatePageCreating } = useOnTemplateButtonClicked(currentPagePath, isLoading);
 
   const onClickTemplateButtonHandler = useCallback(async(label: LabelType) => {
@@ -69,10 +76,10 @@ export const PageCreateButton = React.memo((): JSX.Element => {
             aria-expanded="false"
           />
           <DropendMenu
-            todaysPath={todaysPath}
             onClickCreateNewPageButtonHandler={onClickNewButton}
             onClickCreateTodaysButtonHandler={onClickTodaysButton}
             onClickTemplateButtonHandler={onClickTemplateButtonHandler}
+            todaysPath={todaysPath}
           />
         </div>
       )}

+ 3 - 5
apps/app/src/components/Sidebar/PageCreateButton/hooks.tsx

@@ -1,6 +1,5 @@
 import { useCallback, useState } from 'react';
 
-import type { Nullable, IUserHasId } from '@growi/core';
 import { useRouter } from 'next/router';
 
 import { createPage, exist } from '~/client/services/page-operation';
@@ -51,8 +50,7 @@ export const useOnNewButtonClicked = (
 };
 
 export const useOnTodaysButtonClicked = (
-    todaysPath: string,
-    currentUser?: Nullable<IUserHasId> | undefined,
+    todaysPath: string | null,
 ): {
   onClickHandler: () => Promise<void>,
   isPageCreating: boolean
@@ -61,7 +59,7 @@ export const useOnTodaysButtonClicked = (
   const [isPageCreating, setIsPageCreating] = useState(false);
 
   const onClickHandler = useCallback(async() => {
-    if (currentUser == null) {
+    if (todaysPath == null) {
       return;
     }
 
@@ -89,7 +87,7 @@ export const useOnTodaysButtonClicked = (
     finally {
       setIsPageCreating(false);
     }
-  }, [currentUser, router, todaysPath]);
+  }, [router, todaysPath]);
 
   return { onClickHandler, isPageCreating };
 };

+ 3 - 3
apps/app/src/components/Sidebar/PageTreeItem/PageTreeItem.tsx

@@ -154,7 +154,7 @@ export const PageTreeItem: FC<PageTreeItemProps> = (props) => {
 
   const mainClassName = `${isOver ? 'grw-pagetree-is-over' : ''} ${shouldHide ? 'd-none' : ''}`;
 
-  const { NewPageInputWrapper, NewPageCreateButtonWrapper } = useNewPageInput();
+  const { Input: NewPageInput, CreateButton: NewPageCreateButton } = useNewPageInput();
 
   return (
     <SimpleItem
@@ -169,8 +169,8 @@ export const PageTreeItem: FC<PageTreeItemProps> = (props) => {
       itemRef={itemRef}
       itemClass={PageTreeItem}
       mainClassName={mainClassName}
-      customEndComponents={[Ellipsis, NewPageCreateButtonWrapper]}
-      customNextComponents={[NewPageInputWrapper]}
+      customEndComponents={[Ellipsis, NewPageCreateButton]}
+      customNextComponents={[NewPageInput]}
     />
   );
 };

+ 0 - 69
apps/app/src/components/TreeItem/NewPageCreateButton.tsx

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

+ 0 - 104
apps/app/src/components/TreeItem/NewPageInput.tsx

@@ -1,104 +0,0 @@
-import React, { FC, useCallback, useEffect } from 'react';
-
-import nodePath from 'path';
-
-
-import { pathUtils, pagePathUtils } from '@growi/core/dist/utils';
-import { useTranslation } from 'next-i18next';
-
-import { apiv3Post } from '~/client/util/apiv3-client';
-import { ValidationTarget } from '~/client/util/input-validator';
-import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
-import ClosableTextInput from '~/components/Common/ClosableTextInput';
-import { useSWRxPageChildren } from '~/stores/page-listing';
-import { usePageTreeDescCountMap } from '~/stores/ui';
-
-import { NewPageCreateButtonProps } from './NewPageCreateButton';
-import { NotDraggableForClosableTextInput } from './SimpleItem';
-
-type NewPageInputProps = NewPageCreateButtonProps & {isEnableActions: boolean};
-
-export const NewPageInput: FC<NewPageInputProps> = (props) => {
-  const { t } = useTranslation();
-
-  const {
-    page, isEnableActions, currentChildren, stateHandlers, isNewPageInputShown, setNewPageInputShown,
-  } = 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) => {
-    setNewPageInputShown(false);
-    // closeNewPageInput();
-    const parentPath = pathUtils.addTrailingSlash(page.path as string);
-    const newPagePath = nodePath.resolve(parentPath, inputText);
-    const isCreatable = pagePathUtils.isCreatablePage(newPagePath);
-
-    if (!isCreatable) {
-      toastWarning(t('you_can_not_create_page_with_this_name'));
-      return;
-    }
-
-    try {
-      setCreating(true);
-
-      await apiv3Post('/pages/', {
-        path: newPagePath,
-        body: undefined,
-        grant: page.grant,
-        // grantUserGroupId: page.grantedGroup,
-        grantUserGroupIds: page.grantedGroups,
-      });
-
-      mutateChildren();
-
-      if (!hasDescendants) {
-        setIsOpen(true);
-      }
-
-      toastSuccess(t('successfully_saved_the_page'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-    finally {
-      setCreating(false);
-    }
-  };
-
-  const onPressEscHandler = useCallback((event) => {
-    if (event.keyCode === 27) {
-      setNewPageInputShown(false);
-    }
-  }, []);
-
-  useEffect(() => {
-    document.addEventListener('keydown', onPressEscHandler, false);
-    return () => {
-      document.removeEventListener('keydown', onPressEscHandler, false);
-    };
-  }, [onPressEscHandler]);
-
-  return (
-    <>
-      {isEnableActions && isNewPageInputShown && (
-        <NotDraggableForClosableTextInput>
-          <ClosableTextInput
-            placeholder={t('Input page name')}
-            onClickOutside={() => { setNewPageInputShown(false) }}
-            onPressEnter={onPressEnterForCreateHandler}
-            validationTarget={ValidationTarget.PAGE}
-          />
-        </NotDraggableForClosableTextInput>
-      )}
-    </>
-  );
-};

+ 37 - 0
apps/app/src/components/TreeItem/NewPageInput/NewPageCreateButton.tsx

@@ -0,0 +1,37 @@
+import React, { type FC } from 'react';
+
+import { pagePathUtils } from '@growi/core/dist/utils';
+
+import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
+import { NotAvailableForReadOnlyUser } from '~/components/NotAvailableForReadOnlyUser';
+import type { IPageForItem } from '~/interfaces/page';
+
+type NewPageCreateButtonProps = {
+  page: IPageForItem,
+  onClick?: () => void,
+};
+
+export const NewPageCreateButton: FC<NewPageCreateButtonProps> = (props) => {
+  const {
+    page, onClick,
+  } = props;
+
+  return (
+    <>
+      {!pagePathUtils.isUsersTopPage(page.path ?? '') && (
+        <NotAvailableForGuest>
+          <NotAvailableForReadOnlyUser>
+            <button
+              id="page-create-button-in-page-tree"
+              type="button"
+              className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
+              onClick={onClick}
+            >
+              <i className="icon-plus d-block p-0" />
+            </button>
+          </NotAvailableForReadOnlyUser>
+        </NotAvailableForGuest>
+      )}
+    </>
+  );
+};

+ 81 - 0
apps/app/src/components/TreeItem/NewPageInput/NewPageInput.tsx

@@ -0,0 +1,81 @@
+import React, { type FC, useCallback, useEffect } from 'react';
+
+import nodePath from 'path';
+
+import { pathUtils, pagePathUtils } from '@growi/core/dist/utils';
+import { useTranslation } from 'next-i18next';
+
+import { ValidationTarget } from '~/client/util/input-validator';
+import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
+import ClosableTextInput from '~/components/Common/ClosableTextInput';
+import type { IPageForItem } from '~/interfaces/page';
+
+import { NotDraggableForClosableTextInput } from '../NotDraggableForClosableTextInput';
+
+type Props = {
+  page: IPageForItem,
+  isEnableActions: boolean,
+  onSubmit?: (newPagePath: string) => Promise<void>,
+  onSubmittionFailed?: () => void,
+  onCanceled?: () => void,
+};
+
+export const NewPageInput: FC<Props> = (props) => {
+  const { t } = useTranslation();
+
+  const {
+    page, isEnableActions,
+    onSubmit, onSubmittionFailed,
+    onCanceled,
+  } = props;
+
+  const onPressEnterForCreateHandler = async(inputText: string) => {
+    const parentPath = pathUtils.addTrailingSlash(page.path as string);
+    const newPagePath = nodePath.resolve(parentPath, inputText);
+    const isCreatable = pagePathUtils.isCreatablePage(newPagePath);
+
+    if (!isCreatable) {
+      toastWarning(t('you_can_not_create_page_with_this_name'));
+      return;
+    }
+
+    try {
+      onSubmit?.(newPagePath);
+      toastSuccess(t('successfully_saved_the_page'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+    finally {
+      onSubmittionFailed?.();
+    }
+  };
+
+  const onPressEscHandler = useCallback((event) => {
+    if (event.keyCode === 27) {
+      onCanceled?.();
+    }
+  }, [onCanceled]);
+
+  useEffect(() => {
+    document.addEventListener('keydown', onPressEscHandler, false);
+    return () => {
+      document.removeEventListener('keydown', onPressEscHandler, false);
+    };
+  }, [onPressEscHandler]);
+
+  return (
+    <>
+      {isEnableActions && (
+        <NotDraggableForClosableTextInput>
+          <ClosableTextInput
+            placeholder={t('Input page name')}
+            onClickOutside={onCanceled}
+            onPressEnter={onPressEnterForCreateHandler}
+            validationTarget={ValidationTarget.PAGE}
+          />
+        </NotDraggableForClosableTextInput>
+      )}
+    </>
+  );
+};

+ 1 - 0
apps/app/src/components/TreeItem/NewPageInput/index.ts

@@ -0,0 +1 @@
+export * from './use-new-page-input';

+ 110 - 0
apps/app/src/components/TreeItem/NewPageInput/use-new-page-input.tsx

@@ -0,0 +1,110 @@
+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 type { SimpleItemContentProps } from '../interfaces';
+
+import { NewPageCreateButton } from './NewPageCreateButton';
+import { NewPageInput } from './NewPageInput';
+
+type UseNewPageInput = {
+  Input: FC<SimpleItemContentProps>,
+  CreateButton: FC<SimpleItemContentProps>,
+  isProcessingSubmission: boolean,
+}
+
+export const useNewPageInput = (): UseNewPageInput => {
+
+  const [showInput, setShowInput] = useState(false);
+  const [isProcessingSubmission, setProcessingSubmission] = useState(false);
+
+  const { getDescCount } = usePageTreeDescCountMap();
+
+  const CreateButton: FC<SimpleItemContentProps> = (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 (
+      <NewPageCreateButton
+        page={props.page}
+        onClick={onClick}
+      />
+    );
+  };
+
+  const Input: FC<SimpleItemContentProps> = (props) => {
+
+    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
+          page={props.page}
+          isEnableActions={props.isEnableActions}
+          onSubmit={submitHandler}
+          onSubmittionFailed={submittionFailedHandler}
+          onCanceled={() => setShowInput(false)}
+        />
+      )
+      : <></>;
+  };
+
+  return {
+    Input,
+    CreateButton,
+    isProcessingSubmission,
+  };
+};

+ 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>;
+};

+ 20 - 58
apps/app/src/components/TreeItem/SimpleItem.tsx

@@ -1,17 +1,15 @@
 import React, {
-  useCallback, useState, FC, useEffect, ReactNode,
+  useCallback, useState, FC, useEffect,
 } from 'react';
 
 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 { 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';
@@ -19,24 +17,10 @@ import { shouldRecoverPagePaths } from '~/utils/page-operation';
 import CountBadge from '../Common/CountBadge';
 
 import { ItemNode } from './ItemNode';
+import { useNewPageInput } from './NewPageInput';
+import type { SimpleItemContentProps, SimpleItemProps, SimpleItemToolProps } from './interfaces';
 
 
-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>>
-};
-
 // Utility to mark target
 const markTarget = (children: ItemNode[], targetPathOrId?: Nullable<string>): void => {
   if (targetPathOrId == null) {
@@ -69,21 +53,10 @@ const markTarget = (children: ItemNode[], targetPathOrId?: Nullable<string>): vo
  * @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) => {
   const { t } = useTranslation();
   const router = useRouter();
+
   const { getDescCount } = usePageTreeDescCountMap();
 
   const page = props.page;
@@ -139,19 +112,13 @@ export const SimpleItem: FC<SimpleItemProps> = (props) => {
 
   const { page, children } = itemNode;
 
+  const { isProcessingSubmission } = useNewPageInput();
+
   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;
@@ -197,7 +164,12 @@ export const SimpleItem: FC<SimpleItemProps> = (props) => {
 
   const ItemClassFixed = itemClass ?? SimpleItem;
 
-  const commonProps = {
+  const CustomEndComponents = props.customEndComponents;
+
+  const SimpleItemContent = CustomEndComponents ?? [SimpleItemTool];
+
+  const simpleItemProps: SimpleItemProps = {
+    itemNode,
     isEnableActions,
     isReadOnlyUser,
     isOpen: false,
@@ -205,23 +177,13 @@ export const SimpleItem: FC<SimpleItemProps> = (props) => {
     onRenamed,
     onClickDuplicateMenuItem,
     onClickDeleteMenuItem,
-    stateHandlers,
   };
 
-  const CustomEndComponents = props.customEndComponents;
-
-  const SimpleItemContent = CustomEndComponents ?? [SimpleItemTool];
-
-  const SimpleItemContentProps = {
-    itemNode,
+  const simpleItemContentProps: SimpleItemContentProps = {
+    ...simpleItemProps,
     page,
-    onRenamed,
-    onClickDuplicateMenuItem,
-    onClickDeleteMenuItem,
-    isEnableActions,
-    isReadOnlyUser,
     children,
-    stateHandlers,
+    stateHandlers: { isOpen, setIsOpen },
   };
 
   const CustomNextComponents = props.customNextComponents;
@@ -254,20 +216,20 @@ export const SimpleItem: FC<SimpleItemProps> = (props) => {
         </div>
         {SimpleItemContent.map((ItemContent, index) => (
           // eslint-disable-next-line react/no-array-index-key
-          <ItemContent key={index} {...SimpleItemContentProps} />
+          <ItemContent key={index} {...simpleItemContentProps} />
         ))}
       </li>
 
       {CustomNextComponents?.map((UnderItemContent, index) => (
         // eslint-disable-next-line react/no-array-index-key
-        <UnderItemContent key={index} {...SimpleItemContentProps} />
+        <UnderItemContent key={index} {...simpleItemContentProps} />
       ))}
 
       {
         isOpen && hasChildren() && currentChildren.map((node, index) => (
           <div key={node.page._id} className="grw-pagetree-item-children">
-            <ItemClassFixed itemNode={node} {...commonProps} />
-            {isCreating && (currentChildren.length - 1 === index) && (
+            <ItemClassFixed {...simpleItemProps} />
+            {isProcessingSubmission && (currentChildren.length - 1 === index) && (
               <div className="text-muted text-center">
                 <i className="fa fa-spinner fa-pulse mr-1"></i>
               </div>

+ 0 - 43
apps/app/src/components/TreeItem/UseNewPageInput.tsx

@@ -1,43 +0,0 @@
-import React, { useState, FC } from 'react';
-
-import { ItemNode } from './ItemNode';
-import { NewPageCreateButton } from './NewPageCreateButton';
-import { NewPageInput } from './NewPageInput';
-import { SimpleItemToolProps } from './SimpleItem';
-
-type UseNewPageInputProps = SimpleItemToolProps & {children: ItemNode[], stateHandlers};
-
-export const useNewPageInput = () => {
-
-  const [isNewPageInputShown, setNewPageInputShown] = useState(false);
-
-  const NewPageCreateButtonWrapper: FC<UseNewPageInputProps> = (props) => {
-    return (
-      <NewPageCreateButton
-        page={props.page}
-        currentChildren={props.children}
-        stateHandlers={props.stateHandlers}
-        setNewPageInputShown={setNewPageInputShown}
-      />
-    );
-  };
-
-  const NewPageInputWrapper = (props) => {
-    return (
-      <NewPageInput
-        page={props.page}
-        isEnableActions={props.isEnableActions}
-        currentChildren={props.chilren}
-        stateHandlers={props.stateHandlers}
-        isNewPageInputShown={isNewPageInputShown}
-        setNewPageInputShown={setNewPageInputShown}
-      />
-    );
-  };
-
-
-  return {
-    NewPageInputWrapper,
-    NewPageCreateButtonWrapper,
-  };
-};

+ 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 './NewPageInput';
 export * from './ItemNode';
+export * from './SimpleItem';
+export * from './NotDraggableForClosableTextInput';

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

@@ -0,0 +1,38 @@
+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';
+
+type SimpleItemToolPropsOptional = 'itemNode' | 'targetPathOrId' | 'isOpen' | 'itemRef' | 'itemClass' | 'mainClassName';
+
+export type SimpleItemToolProps = Omit<SimpleItemProps, SimpleItemToolPropsOptional> & {
+  page: IPageForItem,
+};
+
+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>>
+};
+
+export type SimpleItemContentProps = SimpleItemToolProps & {
+  page: IPageForItem,
+  children: ItemNode[],
+  stateHandlers: {
+    isOpen: boolean,
+    setIsOpen: React.Dispatch<React.SetStateAction<boolean>>,
+  },
+};