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

+ 37 - 3
apps/app/src/client/components/ItemsTree/SimplifiedItemsTree.tsx

@@ -8,18 +8,18 @@ import { useTree } from '@headless-tree/react';
 import { useVirtualizer } from '@tanstack/react-virtual';
 
 import { apiv3Get } from '~/client/util/apiv3-client';
+import { ROOT_PAGE_VIRTUAL_ID } from '~/constants/page-tree';
 import type { IPageForTreeItem } from '~/interfaces/page';
+import { useTreeUpdateGeneration, useLastUpdatedItemIds } from '~/states/page-tree-update';
 import { useSWRxRootPage } from '~/stores/page-listing';
 
 import type { TreeItemProps } from '../TreeItem';
 
-
-const ROOT_PAGE_VIRTUAL_ID = '__virtual_root__';
-
 function constructRootPageForVirtualRoot(rootPageId: string, allPagesCount: number): IPageForTreeItem {
   return {
     _id: rootPageId,
     path: '/',
+    parent: null,
     descendantCount: allPagesCount,
     grant: 1,
     isEmpty: false,
@@ -105,6 +105,40 @@ export const SimplifiedItemsTree: FC<Props> = (props: Props) => {
     features: [asyncDataLoaderFeature],
   });
 
+  // Extract getItemInstance for dependency array
+  const getItemInstance = tree.getItemInstance;
+
+  // Track local generation number
+  const [localGeneration, setLocalGeneration] = useState(1);
+  const globalGeneration = useTreeUpdateGeneration();
+  const lastUpdatedItemIds = useLastUpdatedItemIds();
+
+  // Refetch data when global generation is updated
+  useEffect(() => {
+    if (globalGeneration <= localGeneration) {
+      return; // Already up to date
+    }
+
+    // Determine update scope
+    const shouldUpdateAll = lastUpdatedItemIds.includes('*');
+
+    if (shouldUpdateAll) {
+      // Full tree update: refetch from root
+      const root = getItemInstance(ROOT_PAGE_VIRTUAL_ID);
+      root?.invalidateChildrenIds(true);
+    }
+    else {
+      // Partial update: refetch children of specified items
+      lastUpdatedItemIds.forEach((itemId) => {
+        const item = getItemInstance(itemId);
+        item?.invalidateChildrenIds(true);
+      });
+    }
+
+    // Update local generation
+    setLocalGeneration(globalGeneration);
+  }, [globalGeneration, localGeneration, lastUpdatedItemIds, getItemInstance]);
+
   const items = tree.getItems();
 
   const virtualizer = useVirtualizer({

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

@@ -1,15 +1,17 @@
 import type { FC } from 'react';
 import { useCallback } from 'react';
 
-import type { IPageToDeleteWithMeta } from '@growi/core';
+import type { IPageToDeleteWithMeta } from '@growi/core/dist/interfaces';
+import { getIdStringForRef } from '@growi/core/dist/interfaces';
 import { pathUtils } from '@growi/core/dist/utils';
 import { useRouter } from 'next/router';
 
+import { ROOT_PAGE_VIRTUAL_ID } from '~/constants/page-tree';
 import type { IPageForItem } from '~/interfaces/page';
+import { useNotifyTreeUpdate } from '~/states/page-tree-update';
 import { usePageDeleteModalActions } from '~/states/ui/modal/page-delete';
 import type { IPageForPageDuplicateModal } from '~/states/ui/modal/page-duplicate';
 import { usePageDuplicateModalActions } from '~/states/ui/modal/page-duplicate';
-import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 
 import type { TreeItemProps } from '../../TreeItem';
 import { TreeItemLayout } from '../../TreeItem';
@@ -40,24 +42,27 @@ export const SimplifiedPageTreeItem: FC<TreeItemProps> = ({
 
   const { open: openDuplicateModal } = usePageDuplicateModalActions();
   const { open: openDeleteModal } = usePageDeleteModalActions();
+  const { notifyItemsUpdated } = useNotifyTreeUpdate();
 
   const onClickDuplicateMenuItem = useCallback((page: IPageForPageDuplicateModal) => {
     openDuplicateModal(page, {
       onDuplicated: () => {
-        mutatePageTree();
-        mutateRecentlyUpdated();
+        // Notify headless-tree update
+        const parentId = item.parent != null ? getIdStringForRef(item.parent) : ROOT_PAGE_VIRTUAL_ID;
+        notifyItemsUpdated([parentId]);
       },
     });
-  }, [openDuplicateModal]);
+  }, [openDuplicateModal, notifyItemsUpdated, item.parent]);
 
   const onClickDeleteMenuItem = useCallback((page: IPageToDeleteWithMeta) => {
     openDeleteModal([page], {
       onDeleted: () => {
-        mutatePageTree();
-        mutateRecentlyUpdated();
+        // Notify headless-tree update
+        const parentId = item.parent != null ? getIdStringForRef(item.parent) : ROOT_PAGE_VIRTUAL_ID;
+        notifyItemsUpdated([parentId]);
       },
     });
-  }, [openDeleteModal]);
+  }, [openDeleteModal, item.parent, notifyItemsUpdated]);
 
   const { Control } = usePageItemControl();
 

+ 1 - 0
apps/app/src/constants/page-tree.ts

@@ -0,0 +1 @@
+export const ROOT_PAGE_VIRTUAL_ID = '__virtual_root__';

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

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

+ 44 - 0
apps/app/src/states/page-tree-update.ts

@@ -0,0 +1,44 @@
+import { useCallback } from 'react';
+import { atom, useAtomValue, useSetAtom } from 'jotai';
+
+// Update generation number
+const treeUpdateGenerationAtom = atom<number>(1);
+
+// Array of IDs for last updated items
+// ['*'] is a special value meaning full tree update
+const lastUpdatedItemIdsAtom = atom<string[]>([]);
+
+// Read-only hooks
+export const useTreeUpdateGeneration = () => {
+  return useAtomValue(treeUpdateGenerationAtom);
+};
+
+export const useLastUpdatedItemIds = () => {
+  return useAtomValue(lastUpdatedItemIdsAtom);
+};
+
+// Hook for notifying tree updates
+export const useNotifyTreeUpdate = () => {
+  const setGeneration = useSetAtom(treeUpdateGenerationAtom);
+  const setLastUpdatedIds = useSetAtom(lastUpdatedItemIdsAtom);
+
+  // Notify update for specific items
+  const notifyItemsUpdated = useCallback(
+    (itemIds: string[]) => {
+      setLastUpdatedIds(itemIds);
+      setGeneration((prev) => prev + 1);
+    },
+    [setGeneration, setLastUpdatedIds],
+  );
+
+  // Notify update for all trees
+  const notifyAllTreesUpdated = useCallback(() => {
+    setLastUpdatedIds(['*']);
+    setGeneration((prev) => prev + 1);
+  }, [setGeneration, setLastUpdatedIds]);
+
+  return {
+    notifyItemsUpdated,
+    notifyAllTreesUpdated,
+  };
+};