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

feat(SimplifiedPageTreeItem): add CreateButton to enhance page creation functionality

Yuki Takei 4 месяцев назад
Родитель
Сommit
43107395c5

+ 2 - 2
apps/app/src/client/components/Sidebar/PageTreeItem/SimplifiedPageTreeItem.tsx

@@ -114,7 +114,7 @@ export const SimplifiedPageTreeItem: FC<TreeItemProps> = ({
   const { isRenaming } = usePageRename();
 
   // Page create feature
-  const { cancelCreating } = usePageCreate();
+  const { cancelCreating, CreateButton } = usePageCreate();
 
   // Check if this is the creating placeholder node
   const isCreatingPlaceholder = itemData._id === CREATING_PAGE_VIRTUAL_ID;
@@ -155,7 +155,7 @@ export const SimplifiedPageTreeItem: FC<TreeItemProps> = ({
       onClickDuplicateMenuItem={onClickDuplicateMenuItem}
       onClickDeleteMenuItem={onClickDeleteMenuItem}
       customEndComponents={[CountBadgeForPageTreeItem]}
-      customHoveredEndComponents={[Control]}
+      customHoveredEndComponents={[Control, CreateButton]}
       showAlternativeContent={isRenaming(item) || isCreatingPlaceholder}
       customAlternativeComponents={[NameInputAlternativeComponent]}
     />

+ 1 - 28
apps/app/src/client/components/Sidebar/PageTreeItem/use-page-item-control.tsx

@@ -4,16 +4,14 @@ import React, { useCallback } from 'react';
 import type { IPageInfoExt, IPageToDeleteWithMeta } from '@growi/core';
 import { getIdStringForRef } from '@growi/core';
 import { useTranslation } from 'next-i18next';
-import { DropdownItem, DropdownToggle } from 'reactstrap';
+import { DropdownToggle } from 'reactstrap';
 
 import { NotAvailableForGuest } from '~/client/components/NotAvailableForGuest';
 import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
 import { toastError, toastSuccess } from '~/client/util/toastr';
-import { usePageCreate } from '~/features/page-tree';
 import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
 import { useSWRMUTxPageInfo } from '~/stores/page';
 
-import type { AdditionalMenuItemsRendererProps } from '../../Common/Dropdown/PageItemControl';
 import { PageItemControl } from '../../Common/Dropdown/PageItemControl';
 import type { TreeItemToolProps } from '../../TreeItem';
 
@@ -24,7 +22,6 @@ type UsePageItemControl = {
 
 export const usePageItemControl = (): UsePageItemControl => {
   const { t } = useTranslation();
-  const { startCreating } = usePageCreate();
 
 
   const Control: FC<TreeItemToolProps> = (props) => {
@@ -67,11 +64,6 @@ export const usePageItemControl = (): UsePageItemControl => {
       item.startRenaming();
     }, [item]);
 
-    const createMenuItemClickHandler = useCallback(() => {
-      // Start creating a new page under this item
-      startCreating(item);
-    }, [item]);
-
     const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoExt | undefined): Promise<void> => {
       if (onClickDeleteMenuItem == null) {
         return;
@@ -103,24 +95,6 @@ export const usePageItemControl = (): UsePageItemControl => {
       }
     };
 
-    // Renderer for "Create new page" menu item at the top
-    const CreateMenuItemRenderer: FC<AdditionalMenuItemsRendererProps> = useCallback(() => {
-      if (!isEnableActions || isReadOnlyUser) {
-        return null;
-      }
-
-      return (
-        <DropdownItem
-          onClick={createMenuItemClickHandler}
-          className="grw-page-control-dropdown-item"
-          data-testid="create-page-btn"
-        >
-          <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">add</span>
-          {t('Create')}
-        </DropdownItem>
-      );
-    }, [isEnableActions, isReadOnlyUser, createMenuItemClickHandler]);
-
     return (
       <NotAvailableForGuest>
         <div className="grw-pagetree-control d-flex">
@@ -133,7 +107,6 @@ export const usePageItemControl = (): UsePageItemControl => {
             onClickRenameMenuItem={renameMenuItemClickHandler}
             onClickDeleteMenuItem={deleteMenuItemClickHandler}
             onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
-            additionalMenuItemOnTopRenderer={CreateMenuItemRenderer}
             isInstantRename
             // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl
             operationProcessData={page.processData}

+ 61 - 1
apps/app/src/features/page-tree/client/hooks/use-page-create.tsx

@@ -1,11 +1,13 @@
 import type { FC } from 'react';
-import { useCallback } from 'react';
+import { useCallback, useId } from 'react';
 import { Origin } from '@growi/core';
 import { pagePathUtils, pathUtils } from '@growi/core/dist/utils';
 import type { ItemInstance } from '@headless-tree/core';
 import { useTranslation } from 'next-i18next';
 import { join } from 'pathe';
 
+import { NotAvailableForGuest } from '~/client/components/NotAvailableForGuest';
+import { NotAvailableForReadOnlyUser } from '~/client/components/NotAvailableForReadOnlyUser';
 import { useCreatePage } from '~/client/services/create-page';
 import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
 import type { IPageForItem } from '~/interfaces/page';
@@ -20,6 +22,50 @@ import {
 } from '../states/page-tree-create';
 import { usePageTreeInformationUpdate } from '../states/page-tree-update';
 
+// Inner component for CreateButton to properly use hooks
+type CreateButtonInnerProps = {
+  item: ItemInstance<IPageForItem>;
+  onStartCreating: (item: ItemInstance<IPageForItem>) => void;
+};
+
+const CreateButtonInner: FC<CreateButtonInnerProps> = ({ item, onStartCreating }) => {
+  const buttonId = useId();
+  const creatingParentId = useCreatingParentId();
+  const isCreating = creatingParentId != null;
+
+  const page = item.getItemData();
+  const isUsersTopPage = pagePathUtils.isUsersTopPage(page.path ?? '');
+
+  if (isUsersTopPage) {
+    return null;
+  }
+
+  const handleClick = (e: React.MouseEvent) => {
+    // Always stop propagation to prevent parent item click handlers
+    e.stopPropagation();
+
+    if (isCreating) {
+      return;
+    }
+    onStartCreating(item);
+  };
+
+  return (
+    <NotAvailableForGuest>
+      <NotAvailableForReadOnlyUser>
+        <button
+          id={`page-create-button-in-page-tree-${buttonId}`}
+          type="button"
+          className="border-0 rounded btn btn-page-item-control p-0"
+          onClick={handleClick}
+        >
+          <span className="material-symbols-outlined p-0">add_circle</span>
+        </button>
+      </NotAvailableForReadOnlyUser>
+    </NotAvailableForGuest>
+  );
+};
+
 type CreateResult = {
   success: boolean;
   path?: string;
@@ -63,6 +109,11 @@ type UsePageCreateReturn = {
    * Check if a child is being created under this item
    */
   isCreatingChild: (item: ItemInstance<IPageForItem>) => boolean;
+
+  /**
+   * Button component to trigger page creation
+   */
+  CreateButton: FC<TreeItemToolProps>;
 };
 
 /**
@@ -198,6 +249,14 @@ export const usePageCreate = (): UsePageCreateReturn => {
     [create, cancelCreating],
   );
 
+  // CreateButton component for tree item
+  const CreateButton: FC<TreeItemToolProps> = useCallback(
+    ({ item }) => {
+      return <CreateButtonInner item={item} onStartCreating={startCreating} />;
+    },
+    [startCreating],
+  ) as FC<TreeItemToolProps>;
+
   return {
     create,
     createFromPlaceholder,
@@ -205,5 +264,6 @@ export const usePageCreate = (): UsePageCreateReturn => {
     startCreating,
     cancelCreating,
     isCreatingChild,
+    CreateButton,
   };
 };