|
|
@@ -1,6 +1,11 @@
|
|
|
import type { FC } from 'react';
|
|
|
-import { useMemo } from 'react';
|
|
|
+import { useCallback, useRef } from 'react';
|
|
|
|
|
|
+import { asyncDataLoaderFeature } from '@headless-tree/core';
|
|
|
+import { useTree } from '@headless-tree/react';
|
|
|
+import { useVirtualizer } from '@tanstack/react-virtual';
|
|
|
+
|
|
|
+import { apiv3Get } from '~/client/util/apiv3-client';
|
|
|
import type { IPageForTreeItem } from '~/interfaces/page';
|
|
|
|
|
|
import { SimplifiedTreeItem } from './SimplifiedTreeItem';
|
|
|
@@ -12,55 +17,124 @@ type Props = {
|
|
|
targetPathOrId?: string | null;
|
|
|
};
|
|
|
|
|
|
-// Mock data for M1 - will be replaced with real API in M2
|
|
|
-const MOCK_DATA: IPageForTreeItem[] = [
|
|
|
- {
|
|
|
- _id: '1',
|
|
|
- path: '/page1',
|
|
|
- parent: '/',
|
|
|
- descendantCount: 0,
|
|
|
- revision: 'rev1',
|
|
|
- grant: 1,
|
|
|
- isEmpty: false,
|
|
|
- wip: false,
|
|
|
- },
|
|
|
- {
|
|
|
- _id: '2',
|
|
|
- path: '/page2',
|
|
|
- parent: '/',
|
|
|
- descendantCount: 0,
|
|
|
- revision: 'rev2',
|
|
|
- grant: 1,
|
|
|
- isEmpty: false,
|
|
|
- wip: false,
|
|
|
- },
|
|
|
- {
|
|
|
- _id: '3',
|
|
|
- path: '/page3',
|
|
|
- parent: '/',
|
|
|
- descendantCount: 0,
|
|
|
- revision: 'rev3',
|
|
|
- grant: 1,
|
|
|
- isEmpty: false,
|
|
|
- wip: false,
|
|
|
- },
|
|
|
-];
|
|
|
-
|
|
|
export const SimplifiedItemsTree: FC<Props> = ({ targetPathOrId }) => {
|
|
|
- const items = useMemo(() => MOCK_DATA, []);
|
|
|
+ const scrollElementRef = useRef<HTMLDivElement>(null);
|
|
|
+
|
|
|
+ const getItem = useCallback(async (itemId: string): Promise<IPageForTreeItem> => {
|
|
|
+ if (itemId === '/') {
|
|
|
+ const response = await apiv3Get<{ rootPage: IPageForTreeItem }>('/page-listing/root');
|
|
|
+ return response.data.rootPage;
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await apiv3Get<{ item: IPageForTreeItem }>('/page-listing/item', { id: itemId });
|
|
|
+ return response.data.item;
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const getChildrenWithData = useCallback(async (itemId: string) => {
|
|
|
+ if (itemId === '/') {
|
|
|
+ const rootResponse = await apiv3Get<{ rootPage: IPageForTreeItem }>('/page-listing/root');
|
|
|
+ const rootPageId = rootResponse.data.rootPage._id;
|
|
|
+ const childrenResponse = await apiv3Get<{ children: IPageForTreeItem[] }>('/page-listing/children', { id: rootPageId });
|
|
|
+ return childrenResponse.data.children.map(child => ({
|
|
|
+ id: child._id,
|
|
|
+ data: child,
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await apiv3Get<{ children: IPageForTreeItem[] }>('/page-listing/children', { id: itemId });
|
|
|
+ return response.data.children.map(child => ({
|
|
|
+ id: child._id,
|
|
|
+ data: child,
|
|
|
+ }));
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const tree = useTree<IPageForTreeItem>({
|
|
|
+ rootItemId: '/',
|
|
|
+ getItemName: item => item.getItemData().path,
|
|
|
+ isItemFolder: item => item.getItemData().descendantCount > 0,
|
|
|
+ createLoadingItemData: () => ({
|
|
|
+ _id: '',
|
|
|
+ path: 'Loading...',
|
|
|
+ parent: '',
|
|
|
+ descendantCount: 0,
|
|
|
+ revision: '',
|
|
|
+ grant: 1,
|
|
|
+ isEmpty: false,
|
|
|
+ wip: false,
|
|
|
+ }),
|
|
|
+ dataLoader: {
|
|
|
+ getItem,
|
|
|
+ getChildrenWithData,
|
|
|
+ },
|
|
|
+ features: [asyncDataLoaderFeature],
|
|
|
+ });
|
|
|
+
|
|
|
+ const items = tree.getItems();
|
|
|
+
|
|
|
+ const virtualizer = useVirtualizer({
|
|
|
+ count: items.length,
|
|
|
+ getScrollElement: () => scrollElementRef.current,
|
|
|
+ estimateSize: () => 36,
|
|
|
+ overscan: 5,
|
|
|
+ });
|
|
|
|
|
|
return (
|
|
|
- <div className={styles['simplified-items-tree']}>
|
|
|
- {items.map((item) => {
|
|
|
- const isSelected = targetPathOrId === item._id || targetPathOrId === item.path;
|
|
|
- return (
|
|
|
- <SimplifiedTreeItem
|
|
|
- key={item._id}
|
|
|
- item={item}
|
|
|
- isSelected={isSelected}
|
|
|
- />
|
|
|
- );
|
|
|
- })}
|
|
|
+ <div
|
|
|
+ {...tree.getContainerProps()}
|
|
|
+ ref={scrollElementRef}
|
|
|
+ className={styles['simplified-items-tree']}
|
|
|
+ style={{ height: '100%', overflow: 'auto' }}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ height: `${virtualizer.getTotalSize()}px`,
|
|
|
+ width: '100%',
|
|
|
+ position: 'relative',
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {virtualizer.getVirtualItems().map((virtualItem) => {
|
|
|
+ const item = items[virtualItem.index];
|
|
|
+ const itemData = item.getItemData();
|
|
|
+ const isSelected = targetPathOrId === itemData._id || targetPathOrId === itemData.path;
|
|
|
+ const props = item.getProps();
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ key={virtualItem.key}
|
|
|
+ data-index={virtualItem.index}
|
|
|
+ ref={(node) => {
|
|
|
+ virtualizer.measureElement(node);
|
|
|
+ if (node && props.ref) {
|
|
|
+ (props.ref as (node: HTMLElement) => void)(node);
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ style={{
|
|
|
+ position: 'absolute',
|
|
|
+ top: 0,
|
|
|
+ left: 0,
|
|
|
+ width: '100%',
|
|
|
+ transform: `translateY(${virtualItem.start}px)`,
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <SimplifiedTreeItem
|
|
|
+ item={itemData}
|
|
|
+ isSelected={isSelected}
|
|
|
+ level={item.getItemMeta().level}
|
|
|
+ isExpanded={item.isExpanded()}
|
|
|
+ isFolder={item.isFolder()}
|
|
|
+ onToggle={() => {
|
|
|
+ if (item.isExpanded()) {
|
|
|
+ item.collapse();
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ item.expand();
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
);
|
|
|
};
|