Browse Source

fix auto expand logic

Yuki Takei 4 months ago
parent
commit
b07be36fe0

+ 81 - 6
apps/app/src/features/page-tree/client/components/SimplifiedItemsTree.tsx

@@ -1,5 +1,6 @@
 import type { FC } from 'react';
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { addTrailingSlash } from '@growi/core/dist/utils/path-utils';
 import {
   asyncDataLoaderFeature,
   hotkeysCoreFeature,
@@ -60,7 +61,8 @@ export const SimplifiedItemsTree: FC<Props> = (props: Props) => {
   const { rename, getPageName } = usePageRename();
 
   // Page create hook
-  const { createFromPlaceholder, isCreatingPlaceholder, cancelCreating } = usePageCreate();
+  const { createFromPlaceholder, isCreatingPlaceholder, cancelCreating } =
+    usePageCreate();
 
   // Get creating parent id to determine if item should be treated as folder
   const creatingParentId = useCreatingParentId();
@@ -74,12 +76,10 @@ export const SimplifiedItemsTree: FC<Props> = (props: Props) => {
         if (newValue.trim() === '') {
           // Empty value means cancel (Esc key or blur)
           cancelCreating();
-        }
-        else {
+        } else {
           await createFromPlaceholder(item, newValue);
         }
-      }
-      else {
+      } else {
         // Normal node: rename page
         await rename(item, newValue);
       }
@@ -161,6 +161,81 @@ export const SimplifiedItemsTree: FC<Props> = (props: Props) => {
 
   const items = tree.getItems();
 
+  // Track items count to detect when async data loading completes
+  const prevItemsCountRef = useRef(items.length);
+  useEffect(() => {
+    if (items.length !== prevItemsCountRef.current) {
+      prevItemsCountRef.current = items.length;
+      // Trigger re-render when items count changes (e.g., after async load completes)
+      setRebuildTrigger((prev) => prev + 1);
+    }
+  }, [items.length]);
+
+  // Auto-expand items that are ancestors of targetPath
+  // This runs at the parent level to handle all items regardless of virtualization
+  const expandedForTargetPathRef = useRef<string | null>(null);
+  useEffect(() => {
+    // Skip if no items loaded yet
+    if (items.length === 0) {
+      return;
+    }
+
+    // Skip if already fully processed for this targetPath
+    if (expandedForTargetPathRef.current === targetPath) {
+      return;
+    }
+
+    let didExpand = false;
+
+    for (const item of items) {
+      const itemData = item.getItemData();
+      const itemPath = itemData.path;
+
+      if (itemPath == null) continue;
+
+      // Check if this item is an ancestor of targetPath
+      const isAncestorOfTarget =
+        itemPath === '/' ||
+        (targetPath.startsWith(addTrailingSlash(itemPath)) &&
+          targetPath !== itemPath);
+
+      if (!isAncestorOfTarget) continue;
+
+      const isFolder = item.isFolder();
+      const isExpanded = item.isExpanded();
+
+      if (isFolder && !isExpanded) {
+        item.expand();
+        didExpand = true;
+      }
+    }
+
+    // If we expanded any items, trigger re-render to load children
+    if (didExpand) {
+      setRebuildTrigger((prev) => prev + 1);
+    }
+    else {
+      // Only mark as fully processed when all ancestors are expanded
+      // Check if we have all the ancestors we need
+      const targetSegments = targetPath.split('/').filter(Boolean);
+      let hasAllAncestors = true;
+
+      // Build ancestor paths and check if they exist in items
+      for (let i = 0; i < targetSegments.length - 1; i++) {
+        const ancestorPath = '/' + targetSegments.slice(0, i + 1).join('/');
+        const ancestorItem = items.find(item => item.getItemData().path === ancestorPath);
+        if (!ancestorItem) {
+          hasAllAncestors = false;
+          break;
+        }
+      }
+
+      if (hasAllAncestors) {
+        expandedForTargetPathRef.current = targetPath;
+      }
+    }
+  }, [items, targetPath]);
+
   const virtualizer = useVirtualizer({
     count: items.length,
     getScrollElement: () => scrollerElem ?? null,

+ 0 - 23
apps/app/src/features/page-tree/client/components/TreeItemLayout.tsx

@@ -2,11 +2,8 @@ import {
   type JSX,
   type MouseEvent,
   useCallback,
-  useEffect,
   useMemo,
-  useState,
 } from 'react';
-import { addTrailingSlash } from '@growi/core/dist/utils/path-utils';
 
 import type { TreeItemProps, TreeItemToolProps } from '../interfaces';
 import { SimpleItemContent } from './SimpleItemContent';
@@ -26,7 +23,6 @@ export const TreeItemLayout = (props: TreeItemLayoutProps): JSX.Element => {
     className,
     itemClassName,
     item,
-    targetPath,
     targetPathOrId,
     isEnableActions,
     isReadOnlyUser,
@@ -43,8 +39,6 @@ export const TreeItemLayout = (props: TreeItemLayoutProps): JSX.Element => {
   const page = item.getItemData();
   const itemLevel = item.getItemMeta().level;
 
-  const [isAutoOpenerInitialized, setAutoOpenerInitialized] = useState(false);
-
   const toggleHandler = useCallback(() => {
     if (item.isExpanded()) {
       item.collapse();
@@ -86,23 +80,6 @@ export const TreeItemLayout = (props: TreeItemLayoutProps): JSX.Element => {
   // This will be re-evaluated after rebuildTree()
   const hasDescendants = item.isFolder();
 
-  // auto open if targetPath is descendant of this page
-  useEffect(() => {
-    if (!isAutoOpenerInitialized) {
-      const isPathToTarget =
-        page.path != null &&
-        targetPath.startsWith(addTrailingSlash(page.path)) &&
-        targetPath !== page.path; // Target Page does not need to be opened
-
-      if (page.path === '/' || isPathToTarget) {
-        item.expand();
-        onToggle?.();
-      }
-    }
-
-    setAutoOpenerInitialized(true);
-  }, [targetPath, page.path, isAutoOpenerInitialized, item, onToggle]);
-
   const isSelected = useMemo(() => {
     return page._id === targetPathOrId || page.path === targetPathOrId;
   }, [page, targetPathOrId]);