SimplifiedItemsTree.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import type { FC } from 'react';
  2. import { useState } from 'react';
  3. import { asyncDataLoaderFeature } from '@headless-tree/core';
  4. import { useTree } from '@headless-tree/react';
  5. import { useVirtualizer } from '@tanstack/react-virtual';
  6. import { ROOT_PAGE_VIRTUAL_ID } from '~/constants/page-tree';
  7. import type { IPageForTreeItem } from '~/interfaces/page';
  8. import { usePageTreeInformationGeneration, usePageTreeRevalidationEffect } from '~/states/page-tree-update';
  9. import { useSWRxRootPage } from '~/stores/page-listing';
  10. import type { TreeItemProps } from '../TreeItem';
  11. import { usePageTreeDataLoader } from './hooks/usePageTreeDataLoader';
  12. import { useScrollToSelectedItem } from './hooks/useScrollToSelectedItem';
  13. type Props = {
  14. targetPath: string;
  15. targetPathOrId?: string;
  16. isWipPageShown?: boolean;
  17. isEnableActions?: boolean;
  18. isReadOnlyUser?: boolean;
  19. CustomTreeItem: React.FunctionComponent<TreeItemProps>
  20. estimateTreeItemSize: () => number;
  21. scrollerElem?: HTMLElement | null;
  22. };
  23. export const SimplifiedItemsTree: FC<Props> = (props: Props) => {
  24. const {
  25. targetPath, targetPathOrId,
  26. isWipPageShown = true, isEnableActions = false, isReadOnlyUser = false,
  27. CustomTreeItem, estimateTreeItemSize,
  28. scrollerElem,
  29. } = props;
  30. const [, setRebuildTrigger] = useState(0);
  31. const { data: rootPageResult } = useSWRxRootPage({ suspense: true });
  32. const rootPage = rootPageResult?.rootPage;
  33. const rootPageId = rootPage?._id ?? ROOT_PAGE_VIRTUAL_ID;
  34. const allPagesCount = rootPage?.descendantCount ?? 0;
  35. const dataLoader = usePageTreeDataLoader(rootPageId, allPagesCount);
  36. const tree = useTree<IPageForTreeItem>({
  37. rootItemId: ROOT_PAGE_VIRTUAL_ID,
  38. getItemName: item => item.getItemData().path || '/',
  39. initialState: { expandedItems: [ROOT_PAGE_VIRTUAL_ID] },
  40. isItemFolder: item => item.getItemData().descendantCount > 0,
  41. createLoadingItemData: () => ({
  42. _id: '',
  43. path: 'Loading...',
  44. parent: '',
  45. descendantCount: 0,
  46. revision: '',
  47. grant: 1,
  48. isEmpty: false,
  49. wip: false,
  50. }),
  51. dataLoader,
  52. features: [asyncDataLoaderFeature],
  53. });
  54. // Track local generation number
  55. const [localGeneration, setLocalGeneration] = useState(1);
  56. const globalGeneration = usePageTreeInformationGeneration();
  57. // Refetch data when global generation is updated
  58. usePageTreeRevalidationEffect(tree, localGeneration, {
  59. // Update local generation number after revalidation
  60. onRevalidated: () => setLocalGeneration(globalGeneration),
  61. });
  62. const items = tree.getItems();
  63. const virtualizer = useVirtualizer({
  64. count: items.length,
  65. getScrollElement: () => scrollerElem ?? null,
  66. estimateSize: estimateTreeItemSize,
  67. overscan: 5,
  68. });
  69. // Scroll to selected item on mount or when targetPathOrId changes
  70. useScrollToSelectedItem({ targetPathOrId, items, virtualizer });
  71. return (
  72. <div className="list-group">
  73. {virtualizer.getVirtualItems().map((virtualItem) => {
  74. const item = items[virtualItem.index];
  75. const itemData = item.getItemData();
  76. // Skip rendering virtual root
  77. if (itemData._id === ROOT_PAGE_VIRTUAL_ID) {
  78. return null;
  79. }
  80. // Skip rendering WIP pages if not shown
  81. if (!isWipPageShown && itemData.wip) {
  82. return null;
  83. }
  84. const props = item.getProps();
  85. return (
  86. <div
  87. key={virtualItem.key}
  88. data-index={virtualItem.index}
  89. ref={(node) => {
  90. virtualizer.measureElement(node);
  91. if (node && props.ref) {
  92. (props.ref as (node: HTMLElement) => void)(node);
  93. }
  94. }}
  95. >
  96. <CustomTreeItem
  97. item={itemData}
  98. itemLevel={item.getItemMeta().level}
  99. isExpanded={item.isExpanded()}
  100. targetPath={targetPath}
  101. targetPathOrId={targetPathOrId}
  102. isWipPageShown={isWipPageShown}
  103. isEnableActions={isEnableActions}
  104. isReadOnlyUser={isReadOnlyUser}
  105. onToggle={() => {
  106. if (item.isExpanded()) {
  107. item.collapse();
  108. }
  109. else {
  110. item.expand();
  111. }
  112. // Trigger re-render to show/hide children
  113. setRebuildTrigger(prev => prev + 1);
  114. }}
  115. />
  116. </div>
  117. );
  118. })}
  119. </div>
  120. );
  121. };