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

refactor: streamline item handling and improve type definitions across components

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

+ 1 - 9
apps/app/src/client/components/ItemsTree/SimplifiedItemsTree.tsx

@@ -114,21 +114,13 @@ export const SimplifiedItemsTree: FC<Props> = (props: Props) => {
             }}
           >
             <CustomTreeItem
-              item={itemData}
-              itemLevel={item.getItemMeta().level}
-              isExpanded={item.isExpanded()}
+              item={item}
               targetPath={targetPath}
               targetPathOrId={targetPathOrId}
               isWipPageShown={isWipPageShown}
               isEnableActions={isEnableActions}
               isReadOnlyUser={isReadOnlyUser}
               onToggle={() => {
-                if (item.isExpanded()) {
-                  item.collapse();
-                }
-                else {
-                  item.expand();
-                }
                 // Trigger re-render to show/hide children
                 setRebuildTrigger(prev => prev + 1);
               }}

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

@@ -8,7 +8,8 @@ import { usePageTreeDescCountMap } from '~/states/ui/page-tree-desc-count-map';
 export const CountBadgeForPageTreeItem = (props: TreeItemToolProps): JSX.Element => {
   const { getDescCount } = usePageTreeDescCountMap();
 
-  const { item: page } = props;
+  const { item } = props;
+  const page = item.getItemData();
 
   const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
 

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

@@ -29,8 +29,6 @@ export const simplifiedPageTreeItemSize = 40; // in px
 
 export const SimplifiedPageTreeItem: FC<TreeItemProps> = ({
   item,
-  itemLevel,
-  isExpanded,
   targetPath,
   targetPathOrId,
   isWipPageShown,
@@ -40,6 +38,8 @@ export const SimplifiedPageTreeItem: FC<TreeItemProps> = ({
 }) => {
   const router = useRouter();
 
+  const itemData = item.getItemData();
+
   const { open: openDuplicateModal } = usePageDuplicateModalActions();
   const { open: openDeleteModal } = usePageDeleteModalActions();
   const { notifyUpdateItems } = usePageTreeInformationUpdate();
@@ -48,21 +48,21 @@ export const SimplifiedPageTreeItem: FC<TreeItemProps> = ({
     openDuplicateModal(page, {
       onDuplicated: () => {
         // Notify headless-tree update
-        const parentId = item.parent != null ? getIdStringForRef(item.parent) : ROOT_PAGE_VIRTUAL_ID;
+        const parentId = itemData.parent != null ? getIdStringForRef(itemData.parent) : ROOT_PAGE_VIRTUAL_ID;
         notifyUpdateItems([parentId]);
       },
     });
-  }, [openDuplicateModal, notifyUpdateItems, item.parent]);
+  }, [openDuplicateModal, notifyUpdateItems, itemData.parent]);
 
   const onClickDeleteMenuItem = useCallback((page: IPageToDeleteWithMeta) => {
     openDeleteModal([page], {
       onDeleted: () => {
         // Notify headless-tree update
-        const parentId = item.parent != null ? getIdStringForRef(item.parent) : ROOT_PAGE_VIRTUAL_ID;
+        const parentId = itemData.parent != null ? getIdStringForRef(itemData.parent) : ROOT_PAGE_VIRTUAL_ID;
         notifyUpdateItems([parentId]);
       },
     });
-  }, [openDeleteModal, item.parent, notifyUpdateItems]);
+  }, [openDeleteModal, itemData.parent, notifyUpdateItems]);
 
   const { Control } = usePageItemControl();
 
@@ -84,10 +84,8 @@ export const SimplifiedPageTreeItem: FC<TreeItemProps> = ({
     <TreeItemLayout
       className={moduleClass}
       item={item}
-      itemLevel={itemLevel}
       targetPath={targetPath}
       targetPathOrId={targetPathOrId ?? undefined}
-      isExpanded={isExpanded}
       isWipPageShown={isWipPageShown}
       isEnableActions={isEnableActions}
       isReadOnlyUser={isReadOnlyUser}

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

@@ -6,6 +6,7 @@ import React, {
 import nodePath from 'path';
 
 import type { IPageInfoExt, IPageToDeleteWithMeta } from '@growi/core';
+import { getIdStringForRef } from '@growi/core';
 import { pathUtils } from '@growi/core/dist/utils';
 import { useRect } from '@growi/ui/dist/utils';
 import { useTranslation } from 'next-i18next';
@@ -39,11 +40,12 @@ export const usePageItemControl = (): UsePageItemControl => {
 
   const Control: FC<TreeItemToolProps> = (props) => {
     const {
-      item: page,
+      item,
       isEnableActions,
       isReadOnlyUser,
       onClickDuplicateMenuItem, onClickDeleteMenuItem,
     } = props;
+    const page = item.getItemData();
 
     const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
     const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(page._id ?? null);
@@ -87,7 +89,7 @@ export const usePageItemControl = (): UsePageItemControl => {
       const pageToDelete: IPageToDeleteWithMeta = {
         data: {
           _id: page._id,
-          revision: page.revision as string,
+          revision: page.revision != null ? getIdStringForRef(page.revision) : null,
           path: page.path,
         },
         meta: pageInfo,
@@ -134,7 +136,8 @@ export const usePageItemControl = (): UsePageItemControl => {
 
 
   const RenameInput: FC<TreeItemToolProps> = (props) => {
-    const { item: page, onRenamed } = props;
+    const { item, onRenamed } = props;
+    const page = item.getItemData();
 
     const parentRef = useRef<HTMLDivElement>(null);
     const [parentRect] = useRect(parentRef);

+ 37 - 16
apps/app/src/client/components/TreeItem/TreeItemLayout.tsx

@@ -1,7 +1,8 @@
-import React, {
+import {
   useCallback,
   useEffect,
   useMemo,
+  useState,
   type MouseEvent,
   type JSX,
 } from 'react';
@@ -28,15 +29,30 @@ export const TreeItemLayout = (props: TreeItemLayoutProps): JSX.Element => {
   const {
     className, itemClassName,
     indentSize = 10,
-    item: page,
-    itemLevel: baseItemLevel = 1,
-    targetPath, targetPathOrId, isExpanded = false,
+    item,
+    targetPath, targetPathOrId,
     isEnableActions, isReadOnlyUser, isWipPageShown = true,
     showAlternativeContent,
     onRenamed, onClick, onClickDuplicateMenuItem, onClickDeleteMenuItem, onWheelClick,
     onToggle,
   } = props;
 
+  const page = item.getItemData();
+  const itemLevel = item.getItemMeta().level;
+
+  const [isAutoOpenerInitialized, setAutoOpenerInitialized] = useState(false);
+
+  const toggleHandler = useCallback(() => {
+    if (item.isExpanded()) {
+      item.collapse();
+    }
+    else {
+      item.expand();
+    }
+
+    onToggle?.();
+  }, [item, onToggle]);
+
   const itemClickHandler = useCallback((e: MouseEvent) => {
     // DO NOT handle the event when e.currentTarget and e.target is different
     if (e.target !== e.currentTarget) {
@@ -70,22 +86,27 @@ export const TreeItemLayout = (props: TreeItemLayoutProps): JSX.Element => {
 
   // auto open if targetPath is descendant of this page
   useEffect(() => {
-    if (isExpanded) return;
+    if (!isAutoOpenerInitialized) {
+      const isPathToTarget = page.path != null
+        && targetPath.startsWith(addTrailingSlash(page.path))
+        && targetPath !== page.path; // Target Page does not need to be opened
+
+      if (isPathToTarget) {
+        item.expand();
+        onToggle?.();
+      }
+    }
 
-    const isPathToTarget = page.path != null
-      && targetPath.startsWith(addTrailingSlash(page.path))
-      && targetPath !== page.path; // Target Page does not need to be opened
+    setAutoOpenerInitialized(true);
 
-    if (isPathToTarget) onToggle?.();
-  }, [targetPath, page.path, isExpanded, onToggle]);
+  }, [targetPath, page.path, isAutoOpenerInitialized, item, onToggle]);
 
   const isSelected = useMemo(() => {
     return page._id === targetPathOrId || page.path === targetPathOrId;
   }, [page, targetPathOrId]);
 
   const toolProps: TreeItemToolProps = {
-    item: page,
-    itemLevel: baseItemLevel,
+    item,
     isEnableActions,
     isReadOnlyUser,
     onRenamed,
@@ -105,8 +126,8 @@ export const TreeItemLayout = (props: TreeItemLayoutProps): JSX.Element => {
     <div
       id={`tree-item-layout-${page._id}`}
       data-testid="grw-pagetree-item-container"
-      className={`${moduleClass} ${className} level-${baseItemLevel}`}
-      style={{ paddingLeft: `${baseItemLevel > 1 ? indentSize : 0}px` }}
+      className={`${moduleClass} ${className} level-${itemLevel}`}
+      style={{ paddingLeft: `${itemLevel > 1 ? indentSize : 0}px` }}
     >
       <li
         role="button"
@@ -123,8 +144,8 @@ export const TreeItemLayout = (props: TreeItemLayoutProps): JSX.Element => {
           {hasDescendants && (
             <button
               type="button"
-              className={`btn btn-triangle p-0 ${isExpanded ? 'open' : ''}`}
-              onClick={onToggle}
+              className={`btn btn-triangle p-0 ${item.isExpanded() ? 'open' : ''}`}
+              onClick={toggleHandler}
             >
               <div className="d-flex justify-content-center">
                 <span className="material-symbols-outlined fs-5">arrow_right</span>

+ 3 - 4
apps/app/src/client/components/TreeItem/interfaces/index.ts

@@ -1,11 +1,11 @@
 import type { IPageToDeleteWithMeta } from '@growi/core';
+import type { ItemInstance } from '@headless-tree/core';
 
 import type { IPageForTreeItem } from '~/interfaces/page';
 import type { IPageForPageDuplicateModal } from '~/states/ui/modal/page-duplicate';
 
 type TreeItemBaseProps = {
-  item: IPageForTreeItem,
-  itemLevel: number,
+  item: ItemInstance<IPageForTreeItem>,
   isEnableActions: boolean,
   isReadOnlyUser: boolean,
   onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void,
@@ -20,7 +20,6 @@ export type TreeItemToolProps = TreeItemBaseProps & {
 };
 
 export type TreeItemProps = TreeItemBaseProps & {
-  isExpanded: boolean;
   targetPath: string;
   targetPathOrId?: string | null;
   isWipPageShown?: boolean;
@@ -30,7 +29,7 @@ export type TreeItemProps = TreeItemBaseProps & {
   customHeadOfChildrenComponents?: Array<React.FunctionComponent<TreeItemToolProps>>,
   showAlternativeContent?: boolean,
   customAlternativeComponents?: Array<React.FunctionComponent<TreeItemToolProps>>,
-  onToggle: () => void;
+  onToggle?: () => void;
   onClick?(page: IPageForTreeItem): void,
   onWheelClick?(page: IPageForTreeItem): void,
 };

+ 8 - 1
apps/app/src/interfaces/page.ts

@@ -23,7 +23,14 @@ export type IPageForItem = Partial<
 
 export type IPageForTreeItem = Pick<
   IPageHasId,
-  '_id' | 'path' | 'parent' | 'descendantCount' | 'grant' | 'isEmpty' | 'wip'
+  | '_id'
+  | 'path'
+  | 'parent'
+  | 'revision'
+  | 'descendantCount'
+  | 'grant'
+  | 'isEmpty'
+  | 'wip'
 > & {
   processData?: IPageOperationProcessData;
 };

+ 1 - 0
apps/app/src/server/routes/apiv3/page-listing.ts

@@ -207,6 +207,7 @@ const routerFactory = (crowi: Crowi): Router => {
           _id: page._id.toString(),
           path: page.path,
           parent: page.parent,
+          revision: page.revision, // required to create an IPageToDeleteWithMeta instance
           descendantCount: page.descendantCount,
           grant: page.grant,
           isEmpty: page.isEmpty,