From 09180858fce62abec57abb4136443f968aa435cd Mon Sep 17 00:00:00 2001 From: noraleonte Date: Mon, 28 Oct 2024 17:42:32 +0200 Subject: [PATCH 01/45] add getChildrenCount --- docs/next-env.d.ts | 2 +- .../src/RichTreeViewPro/RichTreeViewPro.tsx | 10 ++++++++++ .../x-tree-view/src/RichTreeView/RichTreeView.tsx | 10 ++++++++++ .../src/SimpleTreeView/SimpleTreeView.tsx | 10 ++++++++++ packages/x-tree-view/src/TreeView/TreeView.tsx | 10 ++++++++++ .../hooks/useTreeItemUtils/useTreeItemUtils.tsx | 2 +- .../plugins/useTreeViewItems/useTreeViewItems.tsx | 15 +++++++++++++-- .../useTreeViewItems/useTreeViewItems.types.ts | 12 +++++++++++- 8 files changed, 66 insertions(+), 5 deletions(-) diff --git a/docs/next-env.d.ts b/docs/next-env.d.ts index a4a7b3f5cfa2f..4f11a03dc6cc3 100644 --- a/docs/next-env.d.ts +++ b/docs/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx index d1cf77859bda1..6db37f43c5dbb 100644 --- a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx +++ b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx @@ -191,6 +191,16 @@ RichTreeViewPro.propTypes = { itemsReordering: PropTypes.bool, labelEditing: PropTypes.bool, }), + /** + * Used to determine the number of children the item has. + * Only relevant for lazy-loaded trees. + * + * @template R + * @param {R} item The item to check. + * @returns {number} The number of children. + * @default (item) => number + */ + getChildrenCount: PropTypes.func, /** * Used to determine the id of a given item. * diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx index ccaccc5bcc78b..aa9f547eb868e 100644 --- a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx +++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx @@ -173,6 +173,16 @@ RichTreeView.propTypes = { indentationAtItemLevel: PropTypes.bool, labelEditing: PropTypes.bool, }), + /** + * Used to determine the number of children the item has. + * Only relevant for lazy-loaded trees. + * + * @template R + * @param {R} item The item to check. + * @returns {number} The number of children. + * @default (item) => number + */ + getChildrenCount: PropTypes.func, /** * Used to determine the id of a given item. * diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx index 5eb1c66c9cbbd..5f541733e8e7c 100644 --- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx +++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx @@ -170,6 +170,16 @@ SimpleTreeView.propTypes = { experimentalFeatures: PropTypes.shape({ indentationAtItemLevel: PropTypes.bool, }), + /** + * Used to determine the number of children the item has. + * Only relevant for lazy-loaded trees. + * + * @template R + * @param {R} item The item to check. + * @returns {number} The number of children. + * @default (item) => number + */ + getChildrenCount: PropTypes.func, /** * This prop is used to help implement the accessibility logic. * If you don't provide this prop. It falls back to a randomly generated id. diff --git a/packages/x-tree-view/src/TreeView/TreeView.tsx b/packages/x-tree-view/src/TreeView/TreeView.tsx index 06be3ecba63ab..38fd4bb503fe2 100644 --- a/packages/x-tree-view/src/TreeView/TreeView.tsx +++ b/packages/x-tree-view/src/TreeView/TreeView.tsx @@ -157,6 +157,16 @@ TreeView.propTypes = { experimentalFeatures: PropTypes.shape({ indentationAtItemLevel: PropTypes.bool, }), + /** + * Used to determine the number of children the item has. + * Only relevant for lazy-loaded trees. + * + * @template R + * @param {R} item The item to check. + * @returns {number} The number of children. + * @default (item) => number + */ + getChildrenCount: PropTypes.func, /** * This prop is used to help implement the accessibility logic. * If you don't provide this prop. It falls back to a randomly generated id. diff --git a/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx b/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx index ef03b9510dc36..c227cc37a4e90 100644 --- a/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx +++ b/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx @@ -75,7 +75,7 @@ export const useTreeItemUtils = < } = useTreeViewContext(); const status: UseTreeItemStatus = { - expandable: isItemExpandable(children), + expandable: instance?.isItemExpandable ? instance?.isItemExpandable(itemId) : false, expanded: instance.isItemExpanded(itemId), focused: instance.isItemFocused(itemId), selected: instance.isItemSelected(itemId), diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx index cd1112747d1f3..bd63b39779255 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx @@ -14,7 +14,7 @@ import { generateTreeItemIdAttribute } from '../../corePlugins/useTreeViewId/use interface UpdateNodesStateParameters extends Pick< UseTreeViewItemsDefaultizedParameters, - 'items' | 'isItemDisabled' | 'getItemLabel' | 'getItemId' + 'items' | 'isItemDisabled' | 'getItemLabel' | 'getItemId' | 'getChildrenCount' > {} type State = UseTreeViewItemsState['items']; @@ -23,6 +23,7 @@ const updateItemsState = ({ isItemDisabled, getItemLabel, getItemId, + getChildrenCount, }: UpdateNodesStateParameters): State => { const itemMetaMap: State['itemMetaMap'] = {}; const itemMap: State['itemMap'] = {}; @@ -71,7 +72,7 @@ const updateItemsState = ({ label, parentId, idAttribute: undefined, - expandable: !!item.children?.length, + expandable: !!item.children?.length || getChildrenCount(item) > 0, disabled: isItemDisabled ? isItemDisabled(item) : false, depth, }; @@ -83,6 +84,7 @@ const updateItemsState = ({ } itemOrderedChildrenIds[parentIdWithDefault].push(id); + // lazyLoadMark item.children?.forEach((child) => processItem(child, depth + 1, id)); }; @@ -211,6 +213,7 @@ export const useTreeViewItems: TreeViewPlugin = ({ isItemDisabled: params.isItemDisabled, getItemId: params.getItemId, getItemLabel: params.getItemLabel, + getChildrenCount: params.getChildrenCount, }); Object.values(prevState.items.itemMetaMap).forEach((item) => { @@ -228,6 +231,7 @@ export const useTreeViewItems: TreeViewPlugin = ({ params.isItemDisabled, params.getItemId, params.getItemLabel, + params.getChildrenCount, ]); const getItemsToRender = () => { @@ -290,6 +294,7 @@ useTreeViewItems.getInitialState = (params) => ({ isItemDisabled: params.isItemDisabled, getItemId: params.getItemId, getItemLabel: params.getItemLabel, + getChildrenCount: params.getChildrenCount, }), }); @@ -297,6 +302,11 @@ useTreeViewItems.getDefaultizedParams = ({ params }) => ({ ...params, disabledItemsFocusable: params.disabledItemsFocusable ?? false, itemChildrenIndentation: params.itemChildrenIndentation ?? '12px', + getChildrenCount: + params.getChildrenCount ?? + function getChildrenCount() { + return 0; + }, }); useTreeViewItems.wrapRoot = ({ children, instance }) => { @@ -315,4 +325,5 @@ useTreeViewItems.params = { getItemId: true, onItemClick: true, itemChildrenIndentation: true, + getChildrenCount: true, }; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts index 864fc7c3c5ba2..94b6acd08770d 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts @@ -130,11 +130,21 @@ export interface UseTreeViewItemsParameters { * @default 12px */ itemChildrenIndentation?: string | number; + /** + * Used to determine the number of children the item has. + * Only relevant for lazy-loaded trees. + * + * @template R + * @param {R} item The item to check. + * @returns {number} The number of children. + * @default (item) => number + */ + getChildrenCount?: (item: R) => number; } export type UseTreeViewItemsDefaultizedParameters = DefaultizedProps< UseTreeViewItemsParameters, - 'disabledItemsFocusable' | 'itemChildrenIndentation' + 'disabledItemsFocusable' | 'itemChildrenIndentation' | 'getChildrenCount' >; interface UseTreeViewItemsEventLookup { From 45779bad2c5c27beb7ac8a232ce6ae8ac7aa89cd Mon Sep 17 00:00:00 2001 From: noraleonte Date: Thu, 31 Oct 2024 17:39:49 +0200 Subject: [PATCH 02/45] wip --- .../src/hooks/features/dataSource/utils.ts | 20 ++- .../src/RichTreeView/RichTreeView.plugins.ts | 2 + .../useTreeItemUtils/useTreeItemUtils.tsx | 30 +++- .../useTreeViewItems/useTreeViewItems.tsx | 120 ++++++++++---- .../useTreeViewItems.types.ts | 19 +-- .../plugins/useTreeViewLazyLoading/cache.ts | 61 +++++++ .../useTreeViewLazyLoading.ts | 154 ++++++++++++++++++ .../useTreeViewLazyLoading.types.ts | 50 ++++++ .../plugins/useTreeViewLazyLoading/utils.ts | 127 +++++++++++++++ 9 files changed, 521 insertions(+), 62 deletions(-) create mode 100644 packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/cache.ts create mode 100644 packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.ts create mode 100644 packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.types.ts create mode 100644 packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/utils.ts diff --git a/packages/x-data-grid-pro/src/hooks/features/dataSource/utils.ts b/packages/x-data-grid-pro/src/hooks/features/dataSource/utils.ts index dafc6d9783f21..aa62da36deb83 100644 --- a/packages/x-data-grid-pro/src/hooks/features/dataSource/utils.ts +++ b/packages/x-data-grid-pro/src/hooks/features/dataSource/utils.ts @@ -67,16 +67,18 @@ export class NestedDataManager { this.queuedRequests.add(id); loadingIds[id] = true; }); - this.api.setState((state) => ({ - ...state, - dataSource: { - ...state.dataSource, - loading: { - ...state.dataSource.loading, - ...loadingIds, + this.api.setState((state) => { + return { + ...state, + dataSource: { + ...state.dataSource, + loading: { + ...state.dataSource.loading, + ...loadingIds, + }, }, - }, - })); + }; + }); this.processQueue(); }; diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.plugins.ts b/packages/x-tree-view/src/RichTreeView/RichTreeView.plugins.ts index 6b2d736da3947..1dbb88fff1c62 100644 --- a/packages/x-tree-view/src/RichTreeView/RichTreeView.plugins.ts +++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.plugins.ts @@ -25,6 +25,7 @@ import { useTreeViewLabel, UseTreeViewLabelParameters, } from '../internals/plugins/useTreeViewLabel'; +import { useTreeViewLazyLoading } from '../internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading'; export const RICH_TREE_VIEW_PLUGINS = [ useTreeViewItems, @@ -34,6 +35,7 @@ export const RICH_TREE_VIEW_PLUGINS = [ useTreeViewKeyboardNavigation, useTreeViewIcons, useTreeViewLabel, + useTreeViewLazyLoading, ] as const; export type RichTreeViewPluginSignatures = ConvertPluginsIntoSignatures< diff --git a/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx b/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx index c227cc37a4e90..d22b6eb1c6ad7 100644 --- a/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx +++ b/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx @@ -13,6 +13,7 @@ import { import type { UseTreeItemStatus } from '../../useTreeItem'; import { hasPlugin } from '../../internals/utils/plugins'; import { TreeViewPublicAPI } from '../../internals/models'; +import { UseTreeViewLazyLoadingSignature } from '@mui/x-tree-view/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.types'; export interface UseTreeItemInteractions { handleExpansion: (event: React.MouseEvent) => void; @@ -31,6 +32,7 @@ type UseTreeItemUtilsMinimalPlugins = readonly [ UseTreeViewExpansionSignature, UseTreeViewItemsSignature, UseTreeViewFocusSignature, + UseTreeViewLazyLoadingSignature, ]; /** @@ -51,12 +53,12 @@ interface UseTreeItemUtilsReturnValue< publicAPI: TreeViewPublicAPI; } -const isItemExpandable = (reactChildren: React.ReactNode) => { - if (Array.isArray(reactChildren)) { - return reactChildren.length > 0 && reactChildren.some(isItemExpandable); - } - return Boolean(reactChildren); -}; +// const isItemExpandable = (reactChildren: React.ReactNode) => { +// if (Array.isArray(reactChildren)) { +// return reactChildren.length > 0 && reactChildren.some(isItemExpandable); +// } +// return Boolean(reactChildren); +// }; export const useTreeItemUtils = < TSignatures extends UseTreeItemUtilsMinimalPlugins = UseTreeItemUtilsMinimalPlugins, @@ -71,11 +73,21 @@ export const useTreeItemUtils = < const { instance, selection: { multiSelect }, + lazyLoading = false, publicAPI, } = useTreeViewContext(); + const isItemExpandable = () => { + let expandable = false; + if (Array.isArray(children)) { + expandable = children.length > 0 && children.some(isItemExpandable); + } + expandable = expandable || instance?.isItemExpandable(itemId); + return Boolean(children) || expandable; + }; + const status: UseTreeItemStatus = { - expandable: instance?.isItemExpandable ? instance?.isItemExpandable(itemId) : false, + expandable: isItemExpandable(), expanded: instance.isItemExpanded(itemId), focused: instance.isItemFocused(itemId), selected: instance.isItemSelected(itemId), @@ -93,6 +105,10 @@ export const useTreeItemUtils = < instance.focusItem(event, itemId); } + if (lazyLoading && !status.expanded) { + instance.fetchItems(itemId); + } + const multiple = multiSelect && (event.shiftKey || event.ctrlKey || event.metaKey); // If already expanded and trying to toggle selection don't close diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx index bd63b39779255..ee958640485df 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx @@ -10,19 +10,51 @@ import { TreeViewBaseItem, TreeViewItemId } from '../../../models'; import { buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID } from './useTreeViewItems.utils'; import { TreeViewItemDepthContext } from '../../TreeViewItemDepthContext'; import { generateTreeItemIdAttribute } from '../../corePlugins/useTreeViewId/useTreeViewId.utils'; +import { get } from 'http'; interface UpdateNodesStateParameters extends Pick< UseTreeViewItemsDefaultizedParameters, - 'items' | 'isItemDisabled' | 'getItemLabel' | 'getItemId' | 'getChildrenCount' - > {} + 'items' | 'isItemDisabled' | 'getItemLabel' | 'getItemId' + > { + // not sure where to put these + initialDepth?: number; + initialParentId?: string | null; + getChildrenCount: (item: TreeViewBaseItem) => number; +} type State = UseTreeViewItemsState['items']; + +const checkId = (id: string | null, item: TreeViewBaseItem, itemMetaMap: State['itemMetaMap']) => { + if (id == null) { + throw new Error( + [ + 'MUI X: The Tree View component requires all items to have a unique `id` property.', + 'Alternatively, you can use the `getItemId` prop to specify a custom id for each item.', + 'An item was provided without id in the `items` prop:', + JSON.stringify(item), + ].join('\n'), + ); + } + + if (itemMetaMap[id] != null) { + throw new Error( + [ + 'MUI X: The Tree View component requires all items to have a unique `id` property.', + 'Alternatively, you can use the `getItemId` prop to specify a custom id for each item.', + `Two items were provided with the same id in the \`items\` prop: "${id}"`, + ].join('\n'), + ); + } +}; + const updateItemsState = ({ items, isItemDisabled, getItemLabel, getItemId, + initialDepth = 0, + initialParentId = null, getChildrenCount, }: UpdateNodesStateParameters): State => { const itemMetaMap: State['itemMetaMap'] = {}; @@ -33,28 +65,7 @@ const updateItemsState = ({ const processItem = (item: TreeViewBaseItem, depth: number, parentId: string | null) => { const id: string = getItemId ? getItemId(item) : (item as any).id; - - if (id == null) { - throw new Error( - [ - 'MUI X: The Tree View component requires all items to have a unique `id` property.', - 'Alternatively, you can use the `getItemId` prop to specify a custom id for each item.', - 'An item was provided without id in the `items` prop:', - JSON.stringify(item), - ].join('\n'), - ); - } - - if (itemMetaMap[id] != null) { - throw new Error( - [ - 'MUI X: The Tree View component requires all items to have a unique `id` property.', - 'Alternatively, you can use the `getItemId` prop to specify a custom id for each item.', - `Two items were provided with the same id in the \`items\` prop: "${id}"`, - ].join('\n'), - ); - } - + checkId(id, item, itemMetaMap); const label = getItemLabel ? getItemLabel(item) : (item as { label: string }).label; if (label == null) { throw new Error( @@ -67,18 +78,21 @@ const updateItemsState = ({ ); } + const isItemExpandable = getChildrenCount ? getChildrenCount(item) > 0 : false; + itemMetaMap[id] = { id, label, parentId, idAttribute: undefined, - expandable: !!item.children?.length || getChildrenCount(item) > 0, + expandable: !!item.children?.length || isItemExpandable, disabled: isItemDisabled ? isItemDisabled(item) : false, depth, }; itemMap[id] = item; const parentIdWithDefault = parentId ?? TREE_VIEW_ROOT_PARENT_ID; + console.log('parentIdWithDefault', parentIdWithDefault); if (!itemOrderedChildrenIds[parentIdWithDefault]) { itemOrderedChildrenIds[parentIdWithDefault] = []; } @@ -88,7 +102,7 @@ const updateItemsState = ({ item.children?.forEach((child) => processItem(child, depth + 1, id)); }; - items.forEach((item) => processItem(item, 0, null)); + items.forEach((item) => processItem(item, initialDepth, initialParentId)); const itemChildrenIndexes: State['itemChildrenIndexes'] = {}; Object.keys(itemOrderedChildrenIds).forEach((parentId) => { @@ -202,6 +216,50 @@ export const useTreeViewItems: TreeViewPlugin = ({ const areItemUpdatesPrevented = React.useCallback(() => areItemUpdatesPreventedRef.current, []); + const addItems = async ({ items, parentId, depth, getChildrenCount }) => { + console.log('addItems', items, parentId, depth); + if (items) { + const newState = updateItemsState({ + items, + isItemDisabled: params.isItemDisabled, + getItemId: params.getItemId, + getItemLabel: params.getItemLabel, + getChildrenCount, + initialDepth: depth, + initialParentId: parentId, + }); + + setState((prevState) => { + let newItems = {}; + if (parentId) { + newItems = { + itemMap: prevState.items.itemMap, + itemMetaMap: { ...prevState.items.itemMetaMap, ...newState.itemMetaMap }, + itemOrderedChildrenIds: { + ...newState.itemOrderedChildrenIds, + ...prevState.items.itemOrderedChildrenIds, + }, + itemChildrenIndexes: { + ...newState.itemChildrenIndexes, + ...prevState.items.itemChildrenIndexes, + }, + }; + } else { + newItems = { + itemMap: items, + itemMetaMap: newState.itemMetaMap, + itemOrderedChildrenIds: newState.itemOrderedChildrenIds, + itemChildrenIndexes: newState.itemChildrenIndexes, + }; + } + + console.log('newState', newItems); + + return { ...prevState, items: newItems }; + }); + } + }; + React.useEffect(() => { if (instance.areItemUpdatesPrevented()) { return; @@ -213,7 +271,6 @@ export const useTreeViewItems: TreeViewPlugin = ({ isItemDisabled: params.isItemDisabled, getItemId: params.getItemId, getItemLabel: params.getItemLabel, - getChildrenCount: params.getChildrenCount, }); Object.values(prevState.items.itemMetaMap).forEach((item) => { @@ -231,7 +288,6 @@ export const useTreeViewItems: TreeViewPlugin = ({ params.isItemDisabled, params.getItemId, params.getItemLabel, - params.getChildrenCount, ]); const getItemsToRender = () => { @@ -277,6 +333,7 @@ export const useTreeViewItems: TreeViewPlugin = ({ isItemNavigable, preventItemUpdates, areItemUpdatesPrevented, + addItems, }, contextValue: { items: { @@ -294,7 +351,6 @@ useTreeViewItems.getInitialState = (params) => ({ isItemDisabled: params.isItemDisabled, getItemId: params.getItemId, getItemLabel: params.getItemLabel, - getChildrenCount: params.getChildrenCount, }), }); @@ -302,11 +358,6 @@ useTreeViewItems.getDefaultizedParams = ({ params }) => ({ ...params, disabledItemsFocusable: params.disabledItemsFocusable ?? false, itemChildrenIndentation: params.itemChildrenIndentation ?? '12px', - getChildrenCount: - params.getChildrenCount ?? - function getChildrenCount() { - return 0; - }, }); useTreeViewItems.wrapRoot = ({ children, instance }) => { @@ -325,5 +376,4 @@ useTreeViewItems.params = { getItemId: true, onItemClick: true, itemChildrenIndentation: true, - getChildrenCount: true, }; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts index 94b6acd08770d..9cf99bc1846f0 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts @@ -9,6 +9,12 @@ export interface TreeViewItemToRenderProps { children?: TreeViewItemToRenderProps[]; } +type AddItemsParams = { + items: R[]; + parentId?: TreeViewItemId; + depth: number; + getChildrenCount?: (item: R) => number; +}; export interface UseTreeViewItemsPublicAPI { /** * Get the item with the given id. @@ -84,6 +90,7 @@ export interface UseTreeViewItemsInstance extends UseTreeViewItems * @returns {boolean} `true` if the updates to the state based on the `items` prop are prevented. */ areItemUpdatesPrevented: () => boolean; + addItems: ({ items, parentId, depth, getChildrenCount }: AddItemsParams) => void; } export interface UseTreeViewItemsParameters { @@ -130,21 +137,11 @@ export interface UseTreeViewItemsParameters { * @default 12px */ itemChildrenIndentation?: string | number; - /** - * Used to determine the number of children the item has. - * Only relevant for lazy-loaded trees. - * - * @template R - * @param {R} item The item to check. - * @returns {number} The number of children. - * @default (item) => number - */ - getChildrenCount?: (item: R) => number; } export type UseTreeViewItemsDefaultizedParameters = DefaultizedProps< UseTreeViewItemsParameters, - 'disabledItemsFocusable' | 'itemChildrenIndentation' | 'getChildrenCount' + 'disabledItemsFocusable' | 'itemChildrenIndentation' >; interface UseTreeViewItemsEventLookup { diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/cache.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/cache.ts new file mode 100644 index 0000000000000..41fedd2137e39 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/cache.ts @@ -0,0 +1,61 @@ +import { TreeViewItemMeta } from '../../models'; + +type TreeViewDataSourceCacheDefaultConfig = { + /** + * Time To Live for each cache entry in milliseconds. + * After this time the cache entry will become stale and the next query will result in cache miss. + * @default 300000 (5 minutes) + */ + ttl?: number; +}; + +export interface TreeViewDataSourceCache { + /** + * Set the cache entry for the given key. + * @param {string} key The key of type `string` + * @param {TreeViewItemMeta[]} value The value to be stored in the cache + */ + set: (key: string, value: TreeViewItemMeta[]) => void; + /** + * Get the cache entry for the given key. + * @param {string} key The key of type `string` + * @returns {TreeViewItemMeta[]} The value stored in the cache + */ + get: (key: string) => TreeViewItemMeta[] | undefined; + /** + * Clear the cache. + */ + clear: () => void; +} + +export class TreeViewDataSourceCacheDefault { + private cache: Record; + + private ttl: number; + + constructor({ ttl = 300000 }: TreeViewDataSourceCacheDefaultConfig) { + this.cache = {}; + this.ttl = ttl; + } + + set(key: string, value: TreeViewItemMeta[]) { + const expiry = Date.now() + this.ttl; + this.cache[key] = { value, expiry }; + } + + get(key: string): TreeViewItemMeta[] | undefined { + const entry = this.cache[key]; + if (!entry) { + return undefined; + } + if (Date.now() > entry.expiry) { + delete this.cache[key]; + return undefined; + } + return entry.value; + } + + clear() { + this.cache = {}; + } +} diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.ts new file mode 100644 index 0000000000000..32e0548e490e5 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.ts @@ -0,0 +1,154 @@ +import * as React from 'react'; +import useLazyRef from '@mui/utils/useLazyRef'; +import { warnOnce } from '@mui/x-internals/warning'; +import { TreeViewPlugin } from '../../models'; +import { UseTreeViewLazyLoadingSignature } from './useTreeViewLazyLoading.types'; +import { TreeViewDataSourceCache, TreeViewDataSourceCacheDefault } from './cache'; +import { NestedDataManager, RequestStatus } from './utils'; +import { TreeViewItemId } from '../../../models'; + +const noopCache: TreeViewDataSourceCache = { + clear: () => {}, + get: () => undefined, + set: () => {}, +}; + +function getCache(cacheProp?: TreeViewDataSourceCache | null) { + if (cacheProp === null) { + return noopCache; + } + return cacheProp ?? new TreeViewDataSourceCacheDefault({}); +} + +export const useTreeViewLazyLoading: TreeViewPlugin = ({ + instance, + state, + setState, + params, +}) => { + const nestedDataManager = useLazyRef( + () => new NestedDataManager(instance), + ).current; + const [cache, setCache] = React.useState(() => + getCache(params.treeViewDataSourceCache), + ); + + const fetchItems = React.useCallback( + async (parentIds?: TreeViewItemId[]) => { + const getChildrenCount = params.treeViewDataSource?.getChildrenCount || (() => 0); + + const getTreeItems = params.treeViewDataSource?.getTreeItems; + if (!getTreeItems) { + return; + } + + if (parentIds) { + nestedDataManager.queue([parentIds]); + return; + } + // this should be the first + nestedDataManager.clear(); + + // handle caching here + // handle loading here + + try { + const getTreeItemsResponse = await getTreeItems(); + + // set loading state + + // set caching + + // update the items in the state -> need to write a method for this in useTreeViewItems + instance.addItems({ items: getTreeItemsResponse, depth: 0, getChildrenCount }); + } catch (error) { + const childrenFetchError = error as Error; + // handle errors here + } + }, + [nestedDataManager, params.treeViewDataSource, instance], + ); + + const fetchItemChildren = React.useCallback( + async (id: TreeViewItemId) => { + const getChildrenCount = params.treeViewDataSource?.getChildrenCount || (() => 0); + + const getTreeItems = params.treeViewDataSource?.getTreeItems; + if (!getTreeItems) { + nestedDataManager.clearPendingRequest(id); + return; + } + + const parent = instance.getItemMeta(id); + if (!parent) { + nestedDataManager.clearPendingRequest(id); + return; + } + + const depth = parent.depth ? parent.depth + 1 : 1; + + // handle caching here + // error handling here + + try { + const getTreeItemsResponse = await getTreeItems(id); + + // set loading state + nestedDataManager.setRequestSettled(id); + + // set caching + + // update the items in the state -> need to write a method for this in useTreeViewItems + instance.addItems({ items: getTreeItemsResponse, depth, parentId: id, getChildrenCount }); + } catch (error) { + const childrenFetchError = error as Error; + // handle errors here + } finally { + // unset loading + nestedDataManager.setRequestSettled(id); + } + }, + [nestedDataManager, params.treeViewDataSource, instance], + ); + + React.useEffect(() => { + fetchItems(); + }, []); + + return { + instance: { fetchItemChildren, fetchItems }, + publicAPI: {}, + contextValue: { lazyLoading: params.treeViewDataSource !== undefined }, + }; +}; + +// useTreeViewLazyLoading.itemPlugin = useTreeViewLabelItemPlugin; + +useTreeViewLazyLoading.getDefaultizedParams = ({ params, experimentalFeatures }) => { + const canUseFeature = experimentalFeatures?.lazyLoading; + if (process.env.NODE_ENV !== 'production') { + if (params.treeViewDataSource && !canUseFeature) { + warnOnce([ + 'MUI X: The label editing feature requires the `labelEditing` experimental feature to be enabled.', + 'You can do it by passing `experimentalFeatures={{ labelEditing: true}}` to the Rich Tree View Pro component.', + 'Check the documentation for more details: https://mui.com/x/react-tree-view/rich-tree-view/editing/', + ]); + } + } + const defaultDataSource = params?.treeViewDataSource || { + getChildrenCount: () => 0, + getTreeItems: () => Promise.resolve([]), + }; + + return { + ...params, + treeViewDataSource: canUseFeature ? defaultDataSource : {}, + }; +}; + +useTreeViewLazyLoading.getInitialState = () => ({}); + +useTreeViewLazyLoading.params = { + treeViewDataSource: true, + treeViewDataSourceCache: true, +}; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.types.ts new file mode 100644 index 0000000000000..4a94191683a84 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/useTreeViewLazyLoading.types.ts @@ -0,0 +1,50 @@ +import { DefaultizedProps, TreeViewPluginSignature } from '../../models'; +import { UseTreeViewItemsSignature } from '../useTreeViewItems'; +import { TreeViewDataSourceCache } from './cache'; +import { TreeViewItemId } from '../../../models'; + +type TreeViewDataSource = { + /** + * Used to determine the number of children the item has. + * Only relevant for lazy-loaded trees. + * + * @template R + * @param {R} item The item to check. + * @returns {number} The number of children. + * @default (item) => number + */ + getChildrenCount?: (item: R) => number; + getTreeItems?: (parentId?: TreeViewItemId) => Promise; +}; + +export interface UseTreeViewLazyLoadingPublicAPI {} + +export interface UseTreeViewLazyLoadingInstance extends UseTreeViewLazyLoadingPublicAPI { + fetchItems: (parentIds?: TreeViewItemId[]) => void; + fetchItemChildren: (id: TreeViewItemId) => void; +} + +export interface UseTreeViewLazyLoadingParameters { + treeViewDataSource: TreeViewDataSource; + treeViewDataSourceCache?: TreeViewDataSourceCache; +} +export type UseTreeViewLazyLoadingDefaultizedParameters = DefaultizedProps< + UseTreeViewLazyLoadingParameters, + 'treeViewDataSource' +>; + +interface UseTreeViewLazyLoadingContextValue { + lazyLoading: boolean; +} + +export interface UseTreeViewLazyLoadingState {} +export type UseTreeViewLazyLoadingSignature = TreeViewPluginSignature<{ + params: UseTreeViewLazyLoadingParameters; + defaultizedParams: UseTreeViewLazyLoadingDefaultizedParameters; + publicAPI: UseTreeViewLazyLoadingPublicAPI; + instance: UseTreeViewLazyLoadingInstance; + state: UseTreeViewLazyLoadingState; + experimentalFeatures: 'lazyLoading'; + dependencies: [UseTreeViewItemsSignature]; + contextValue: UseTreeViewLazyLoadingContextValue; +}>; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/utils.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/utils.ts new file mode 100644 index 0000000000000..e2e2a93dbebcb --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewLazyLoading/utils.ts @@ -0,0 +1,127 @@ +import { TreeViewItemId } from '../../../models'; +import { TreeViewInstance, TreeViewUsedInstance } from '../../models'; +import { UseTreeViewLazyLoadingSignature } from './useTreeViewLazyLoading.types'; + +const MAX_CONCURRENT_REQUESTS = Infinity; + +export enum RequestStatus { + QUEUED, + PENDING, + SETTLED, + UNKNOWN, +} + +/** + * Plugins that need to be present in the Tree View in order for `useTreeItemUtils` to work correctly. + */ +type NestedDataManagerMinimalPlugins = readonly [UseTreeViewLazyLoadingSignature]; + +/** + * Plugins that `useTreeItemUtils` can use if they are present, but are not required. + */ + +export type NestedDataManagerOptionalPlugins = readonly []; + +/** + * Fetches row children from the server with option to limit the number of concurrent requests + * Determines the status of a request based on the enum `RequestStatus` + * Uses `ParentId` to uniquely identify a request + */ +export class NestedDataManager { + private pendingRequests: Set = new Set(); + + private queuedRequests: Set = new Set(); + + private settledRequests: Set = new Set(); + + private instance: TreeViewInstance< + NestedDataManagerMinimalPlugins, + NestedDataManagerOptionalPlugins + >; + + private maxConcurrentRequests: number; + + constructor( + instance: TreeViewUsedInstance, + maxConcurrentRequests = MAX_CONCURRENT_REQUESTS, + ) { + this.instance = instance; + this.maxConcurrentRequests = maxConcurrentRequests; + } + + private processQueue = async () => { + if (this.queuedRequests.size === 0 || this.pendingRequests.size >= this.maxConcurrentRequests) { + return; + } + const loopLength = Math.min( + this.maxConcurrentRequests - this.pendingRequests.size, + this.queuedRequests.size, + ); + if (loopLength === 0) { + return; + } + const fetchQueue = Array.from(this.queuedRequests); + + for (let i = 0; i < loopLength; i += 1) { + const id = fetchQueue[i]; + this.queuedRequests.delete(id); + this.pendingRequests.add(id); + this.instance.fetchItemChildren(id); + } + }; + + public queue = async (ids: TreeViewItemId[]) => { + const loadingIds: Record = {}; + console.log('ids', ids); + ids.forEach((id) => { + this.queuedRequests.add(id); + loadingIds[id] = true; + }); + // this.instance.setState((state) => { + // console.log('state', state); + // return { + // ...state, + // dataSource: { + // ...state.dataSource, + // loading: { + // ...state.dataSource.loading, + // ...loadingIds, + // }, + // }, + // }; + // }); + this.processQueue(); + }; + + public setRequestSettled = (id: TreeViewItemId) => { + this.pendingRequests.delete(id); + this.settledRequests.add(id); + this.processQueue(); + }; + + public clear = () => { + this.queuedRequests.clear(); + Array.from(this.pendingRequests).forEach((id) => this.clearPendingRequest(id)); + }; + + public clearPendingRequest = (id: TreeViewItemId) => { + // this.instance.unstable_dataSource.setChildrenLoading(id, false); + this.pendingRequests.delete(id); + this.processQueue(); + }; + + public getRequestStatus = (id: TreeViewItemId) => { + if (this.pendingRequests.has(id)) { + return RequestStatus.PENDING; + } + if (this.queuedRequests.has(id)) { + return RequestStatus.QUEUED; + } + if (this.settledRequests.has(id)) { + return RequestStatus.SETTLED; + } + return RequestStatus.UNKNOWN; + }; + + public getActiveRequestsCount = () => this.pendingRequests.size + this.queuedRequests.size; +} From a29e2d0f9fc726ec02852031b800032c15eb120c Mon Sep 17 00:00:00 2001 From: noraleonte Date: Wed, 6 Nov 2024 12:52:35 +0200 Subject: [PATCH 03/45] wip --- .../src/RichTreeViewPro/RichTreeViewPro.tsx | 10 -- .../src/RichTreeView/RichTreeView.tsx | 11 +- .../src/SimpleTreeView/SimpleTreeView.tsx | 10 -- .../x-tree-view/src/TreeItem/TreeItem.tsx | 7 +- .../x-tree-view/src/TreeView/TreeView.tsx | 10 -- .../useTreeItemUtils/useTreeItemUtils.tsx | 5 +- .../useTreeViewItems/useTreeViewItems.tsx | 44 ++++-- .../useTreeViewItems.types.ts | 5 +- .../useTreeViewLazyLoading.ts | 139 +++++++++++++++--- .../useTreeViewLazyLoading.types.ts | 12 +- .../src/useTreeItem/useTreeItem.types.ts | 1 + 11 files changed, 178 insertions(+), 76 deletions(-) diff --git a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx index 6db37f43c5dbb..d1cf77859bda1 100644 --- a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx +++ b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx @@ -191,16 +191,6 @@ RichTreeViewPro.propTypes = { itemsReordering: PropTypes.bool, labelEditing: PropTypes.bool, }), - /** - * Used to determine the number of children the item has. - * Only relevant for lazy-loaded trees. - * - * @template R - * @param {R} item The item to check. - * @returns {number} The number of children. - * @default (item) => number - */ - getChildrenCount: PropTypes.func, /** * Used to determine the id of a given item. * diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx index aa9f547eb868e..bf985f9b86d4e 100644 --- a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx +++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx @@ -172,17 +172,8 @@ RichTreeView.propTypes = { experimentalFeatures: PropTypes.shape({ indentationAtItemLevel: PropTypes.bool, labelEditing: PropTypes.bool, + lazyLoading: PropTypes.bool, }), - /** - * Used to determine the number of children the item has. - * Only relevant for lazy-loaded trees. - * - * @template R - * @param {R} item The item to check. - * @returns {number} The number of children. - * @default (item) => number - */ - getChildrenCount: PropTypes.func, /** * Used to determine the id of a given item. * diff --git a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx index 5f541733e8e7c..5eb1c66c9cbbd 100644 --- a/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx +++ b/packages/x-tree-view/src/SimpleTreeView/SimpleTreeView.tsx @@ -170,16 +170,6 @@ SimpleTreeView.propTypes = { experimentalFeatures: PropTypes.shape({ indentationAtItemLevel: PropTypes.bool, }), - /** - * Used to determine the number of children the item has. - * Only relevant for lazy-loaded trees. - * - * @template R - * @param {R} item The item to check. - * @returns {number} The number of children. - * @default (item) => number - */ - getChildrenCount: PropTypes.func, /** * This prop is used to help implement the accessibility logic. * If you don't provide this prop. It falls back to a randomly generated id. diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.tsx index ce6e5c252b186..d14a6641be770 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItem.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItem.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; +import CircularProgress from '@mui/material/CircularProgress'; import unsupportedProp from '@mui/utils/unsupportedProp'; import { alpha } from '@mui/material/styles'; import Collapse from '@mui/material/Collapse'; @@ -347,7 +348,11 @@ export const TreeItem = React.forwardRef(function TreeItem( - + {status.loading ? ( + + ) : ( + + )} {status.editing ? :