From dda61b7e44779c7e898cee2c41505da7d3fab372 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Tue, 5 Dec 2023 12:20:38 +0200 Subject: [PATCH 001/278] [useTree]: init. --- .../src/data/processing/views/tree/hooks/index.ts | 0 .../processing/views/tree/hooks/useDataStrategy.ts | 3 +++ .../src/data/processing/views/tree/hooks/useTree.ts | 11 +++++++++++ 3 files changed, 14 insertions(+) create mode 100644 uui-core/src/data/processing/views/tree/hooks/index.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/useDataStrategy.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/useTree.ts diff --git a/uui-core/src/data/processing/views/tree/hooks/index.ts b/uui-core/src/data/processing/views/tree/hooks/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/uui-core/src/data/processing/views/tree/hooks/useDataStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/useDataStrategy.ts new file mode 100644 index 0000000000..8df517f653 --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/useDataStrategy.ts @@ -0,0 +1,3 @@ +export function useDataStrategy() { + +} \ No newline at end of file diff --git a/uui-core/src/data/processing/views/tree/hooks/useTree.ts b/uui-core/src/data/processing/views/tree/hooks/useTree.ts new file mode 100644 index 0000000000..44f8c3b865 --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/useTree.ts @@ -0,0 +1,11 @@ +import { useMemo } from 'react'; +import { Tree } from '../Tree'; + +export function useTree({ items, ...params }: any, deps: any[]) { + const tree = useMemo( + () => Tree.create(items, params), + deps, + ); + + return tree; +} From 1a1f245ea345aa5412d521a244442e527620efed Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Wed, 6 Dec 2023 13:09:53 +0200 Subject: [PATCH 002/278] [useTree]: more boilerplate code. --- .../views/tree/hooks/strategies/constants.ts | 5 +++++ .../views/tree/hooks/strategies/index.ts | 5 +++++ .../views/tree/hooks/strategies/types.ts | 16 ++++++++++++++++ .../hooks/strategies/usePlainTreeStrategy.ts | 8 ++++++++ .../data/processing/views/tree/hooks/types.ts | 4 ++++ .../views/tree/hooks/useDataStrategy.ts | 3 --- .../data/processing/views/tree/hooks/useTree.ts | 11 ++++------- .../views/tree/hooks/useTreeStrategy.ts | 17 +++++++++++++++++ 8 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/constants.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/index.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/types.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/usePlainTreeStrategy.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/types.ts delete mode 100644 uui-core/src/data/processing/views/tree/hooks/useDataStrategy.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/useTreeStrategy.ts diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/constants.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/constants.ts new file mode 100644 index 0000000000..1c7827c82a --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/constants.ts @@ -0,0 +1,5 @@ +export const STRATEGIES = { + plain: 'plain', + async: 'async', + lazy: 'lazy', +} as const; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/index.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/index.ts new file mode 100644 index 0000000000..5be2614596 --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/index.ts @@ -0,0 +1,5 @@ +import { usePlainTreeStrategy } from './usePlainTreeStrategy'; + +export const strategies = { + plain: usePlainTreeStrategy, +}; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts new file mode 100644 index 0000000000..bc21ff9d03 --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts @@ -0,0 +1,16 @@ +import { ITree } from '../../ITree'; +import { STRATEGIES } from './constants'; + +export type Strategies = typeof STRATEGIES[keyof typeof STRATEGIES]; + +export type TreeStrategyProps = { + getId?(item: TItem): TId; + getParentId?(item: TItem): TId | undefined; + complexIds?: boolean; +}; + +export type PlainTreeStrategyProps = TreeStrategyProps & { + type?: typeof STRATEGIES.plain, + items: TItem[], + tree?: ITree +}; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/usePlainTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/usePlainTreeStrategy.ts new file mode 100644 index 0000000000..ae55594eb1 --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/usePlainTreeStrategy.ts @@ -0,0 +1,8 @@ +import { useMemo } from 'react'; +import { Tree } from '../../Tree'; +import { PlainTreeStrategyProps } from './types'; + +export function usePlainTreeStrategy({ items, ...props }: PlainTreeStrategyProps, deps: any[]) { + const tree = useMemo(() => Tree.create(props, items), deps); + return tree; +} diff --git a/uui-core/src/data/processing/views/tree/hooks/types.ts b/uui-core/src/data/processing/views/tree/hooks/types.ts new file mode 100644 index 0000000000..6622fb7565 --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/types.ts @@ -0,0 +1,4 @@ +import { PlainTreeStrategyProps } from './strategies/types'; + +export type UseTreeStrategyProps = (PlainTreeStrategyProps); +export type UseTreeProps = {} & UseTreeStrategyProps; diff --git a/uui-core/src/data/processing/views/tree/hooks/useDataStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/useDataStrategy.ts deleted file mode 100644 index 8df517f653..0000000000 --- a/uui-core/src/data/processing/views/tree/hooks/useDataStrategy.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function useDataStrategy() { - -} \ No newline at end of file diff --git a/uui-core/src/data/processing/views/tree/hooks/useTree.ts b/uui-core/src/data/processing/views/tree/hooks/useTree.ts index 44f8c3b865..dc79d5bcd5 100644 --- a/uui-core/src/data/processing/views/tree/hooks/useTree.ts +++ b/uui-core/src/data/processing/views/tree/hooks/useTree.ts @@ -1,11 +1,8 @@ -import { useMemo } from 'react'; -import { Tree } from '../Tree'; +import { useTreeStrategy } from './useTreeStrategy'; +import { UseTreeProps } from './types'; -export function useTree({ items, ...params }: any, deps: any[]) { - const tree = useMemo( - () => Tree.create(items, params), - deps, - ); +export function useTree(params: UseTreeProps, deps: any[]) { + const tree = useTreeStrategy(params, deps); return tree; } diff --git a/uui-core/src/data/processing/views/tree/hooks/useTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/useTreeStrategy.ts new file mode 100644 index 0000000000..651858bd9e --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/useTreeStrategy.ts @@ -0,0 +1,17 @@ +import { useMemo } from 'react'; +import { strategies } from './strategies'; +import { UseTreeStrategyProps } from './types'; + +export function useTreeStrategy({ type = 'plain', ...props }: UseTreeStrategyProps, deps: any[]) { + const useStrategy = useMemo( + () => strategies[type], + [type], + ); + + const tree = useStrategy( + { ...props, type }, + [type, ...deps], + ); + + return tree; +} From 2aa6d611bbd084b8df9e168f962ae6ae0e442f5c Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Wed, 6 Dec 2023 19:49:01 +0200 Subject: [PATCH 003/278] [useTable]: implemented PlainTreeStrategy (partially). --- .../_examples/tables/ArrayTable.example.tsx | 9 ++++- .../data/processing/views/tree/hooks/index.ts | 2 + .../views/tree/hooks/strategies/index.ts | 4 +- .../tree/hooks/strategies/plainTree/index.ts | 1 + .../tree/hooks/strategies/plainTree/types.ts | 14 +++++++ .../strategies/plainTree/useCreateTree.ts | 15 ++++++++ .../strategies/plainTree/useFilterTree.ts | 29 +++++++++++++++ .../plainTree/usePlainTreeStrategy.ts | 32 ++++++++++++++++ .../strategies/plainTree/useSearchTree.ts | 37 +++++++++++++++++++ .../hooks/strategies/plainTree/useSortTree.ts | 35 ++++++++++++++++++ .../views/tree/hooks/strategies/types.ts | 11 ++---- .../hooks/strategies/usePlainTreeStrategy.ts | 8 ---- .../data/processing/views/tree/hooks/types.ts | 6 +-- .../src/data/processing/views/tree/index.ts | 1 + 14 files changed, 183 insertions(+), 21 deletions(-) create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/index.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCreateTree.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useFilterTree.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useSearchTree.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useSortTree.ts delete mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/usePlainTreeStrategy.ts diff --git a/app/src/docs/_examples/tables/ArrayTable.example.tsx b/app/src/docs/_examples/tables/ArrayTable.example.tsx index 6982c43132..41def5a8e0 100644 --- a/app/src/docs/_examples/tables/ArrayTable.example.tsx +++ b/app/src/docs/_examples/tables/ArrayTable.example.tsx @@ -1,11 +1,18 @@ import React, { useMemo, useState } from 'react'; -import { DataColumnProps, useArrayDataSource } from '@epam/uui-core'; +import { DataColumnProps, useArrayDataSource, useTree } from '@epam/uui-core'; import { DataTable, Panel, Text } from '@epam/uui'; import { demoData, FeatureClass } from '@epam/uui-docs'; import css from './TablesExamples.module.scss'; export default function ArrayDataTableExample() { const [value, onValueChange] = useState({}); + + const tree = useTree({ + type: 'plain', + items: demoData.featureClasses, + getId: (item) => item.id, + dataSourceState: value, + }, []); const dataSource = useArrayDataSource( { diff --git a/uui-core/src/data/processing/views/tree/hooks/index.ts b/uui-core/src/data/processing/views/tree/hooks/index.ts index e69de29bb2..f56da96d6d 100644 --- a/uui-core/src/data/processing/views/tree/hooks/index.ts +++ b/uui-core/src/data/processing/views/tree/hooks/index.ts @@ -0,0 +1,2 @@ +export { useTree } from './useTree'; +export * from './types'; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/index.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/index.ts index 5be2614596..8ec6585444 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/index.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/index.ts @@ -1,4 +1,6 @@ -import { usePlainTreeStrategy } from './usePlainTreeStrategy'; +import { usePlainTreeStrategy } from './plainTree'; + +export type { PlainTreeStrategyProps } from './plainTree/types'; export const strategies = { plain: usePlainTreeStrategy, diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/index.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/index.ts new file mode 100644 index 0000000000..95c20b0261 --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/index.ts @@ -0,0 +1 @@ +export { usePlainTreeStrategy } from './usePlainTreeStrategy'; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts new file mode 100644 index 0000000000..f58a68e87d --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts @@ -0,0 +1,14 @@ +import { SortingOption } from '../../../../../../../types'; +import { ITree } from '../../../ITree'; +import { STRATEGIES } from '../constants'; +import { TreeStrategyProps } from '../types'; + +export type PlainTreeStrategyProps = TreeStrategyProps & { + type?: typeof STRATEGIES.plain, + items: TItem[], + tree?: ITree, + getSearchFields?(item: TItem): string[]; + sortBy?(item: TItem, sorting: SortingOption): any; + getFilter?(filter: TFilter): (item: TItem) => boolean; + sortSearchByRelevance?: boolean; +}; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCreateTree.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCreateTree.ts new file mode 100644 index 0000000000..2a66887ff2 --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCreateTree.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from 'react'; +import { PlainTreeStrategyProps } from './types'; +import { ITree, Tree } from '../../..'; + +export function useCreateTree(props: PlainTreeStrategyProps, deps: any[]) { + const [tree, setTree] = useState>(Tree.blank(props)); + + useEffect(() => { + if (props.items) { + setTree(Tree.create(props, props.items)); + } + }, [props.items, ...deps]); + + return tree; +} diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useFilterTree.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useFilterTree.ts new file mode 100644 index 0000000000..6b40b8991f --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useFilterTree.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from 'react'; +import { usePrevious } from '../../../../../../../hooks'; +import { DataSourceState } from '../../../../../../../types'; +import { ITree } from '../../..'; +import isEqual from 'lodash.isequal'; + +export type UseFilterTreeProps = { + getFilter?: (filter: TFilter) => (item: TItem) => boolean; + tree: ITree; + dataSourceState: DataSourceState; +}; + +export function useFilterTree( + { tree, dataSourceState: { filter }, getFilter }: UseFilterTreeProps, + deps: any[], +) { + const prevTree = usePrevious(tree); + const prevFilter = usePrevious(filter); + + const [filteredTree, setFilteredTree] = useState>(tree.filter({ filter, getFilter })); + + useEffect(() => { + if (prevTree !== tree || !isEqual(filter, prevFilter)) { + setFilteredTree(tree.filter({ filter, getFilter })); + } + }, [tree, filter, ...deps]); + + return filteredTree; +} diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts new file mode 100644 index 0000000000..11fd745a4b --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts @@ -0,0 +1,32 @@ +import { PlainTreeStrategyProps } from './types'; +import { useCreateTree } from './useCreateTree'; +import { useFilterTree } from './useFilterTree'; +import { useSearchTree } from './useSearchTree'; +import { useSortTree } from './useSortTree'; + +export function usePlainTreeStrategy( + { sortSearchByRelevance = true, ...restProps }: PlainTreeStrategyProps, + deps: any[], +) { + const props = { ...restProps, sortSearchByRelevance }; + const tree = useCreateTree(props, deps); + + const { dataSourceState, getFilter, getSearchFields, sortBy } = props; + + const filteredTree = useFilterTree( + { tree, getFilter, dataSourceState }, + deps, + ); + + const searchTree = useSearchTree( + { tree: filteredTree, getSearchFields, sortSearchByRelevance, dataSourceState }, + deps, + ); + + const sortedTree = useSortTree( + { tree: searchTree, sortBy, dataSourceState }, + deps, + ); + + return sortedTree; +} diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useSearchTree.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useSearchTree.ts new file mode 100644 index 0000000000..a27887c586 --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useSearchTree.ts @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; +import { usePrevious } from '../../../../../../../hooks'; +import { DataSourceState } from '../../../../../../../types'; +import { ITree } from '../../..'; +import isEqual from 'lodash.isequal'; + +export type UseSearchTreeProps = { + getSearchFields?: (item: TItem) => string[]; + sortSearchByRelevance?: boolean; + tree: ITree; + dataSourceState: DataSourceState; +}; + +export function useSearchTree( + { + tree, + dataSourceState: { search }, + getSearchFields, + sortSearchByRelevance, + }: UseSearchTreeProps, + deps: any[], +) { + const prevTree = usePrevious(tree); + const prevSearch = usePrevious(search); + + const [searchTree, setSearchTree] = useState>( + tree.search({ search, getSearchFields, sortSearchByRelevance }), + ); + + useEffect(() => { + if (!isEqual(search, prevSearch) || prevTree !== tree) { + setSearchTree(tree.search({ search, getSearchFields, sortSearchByRelevance })); + } + }, [tree, search, ...deps]); + + return searchTree; +} diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useSortTree.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useSortTree.ts new file mode 100644 index 0000000000..30db4c8c6a --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useSortTree.ts @@ -0,0 +1,35 @@ +import { useEffect, useState } from 'react'; +import { usePrevious } from '../../../../../../../hooks'; +import { DataSourceState, SortingOption } from '../../../../../../../types'; +import { ITree } from '../../..'; +import isEqual from 'lodash.isequal'; + +export type UseSortTreeProps = { + sortBy?(item: TItem, sorting: SortingOption): any; + tree: ITree; + dataSourceState: DataSourceState; +}; + +export function useSortTree( + { + tree, + dataSourceState: { sorting }, + sortBy, + }: UseSortTreeProps, + deps: any[], +) { + const prevTree = usePrevious(tree); + const prevSorting = usePrevious(sorting); + + const [sortedTree, setSortedTree] = useState>( + tree.sort({ sorting, sortBy }), + ); + + useEffect(() => { + if (!isEqual(sorting, prevSorting) || prevTree !== tree) { + setSortedTree(tree.sort({ sorting, sortBy })); + } + }, [tree, sorting, ...deps]); + + return sortedTree; +} diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts index bc21ff9d03..55d02acc9e 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts @@ -1,16 +1,11 @@ -import { ITree } from '../../ITree'; +import { DataSourceState } from '../../../../../../types'; import { STRATEGIES } from './constants'; export type Strategies = typeof STRATEGIES[keyof typeof STRATEGIES]; -export type TreeStrategyProps = { +export type TreeStrategyProps = { + dataSourceState: DataSourceState; getId?(item: TItem): TId; getParentId?(item: TItem): TId | undefined; complexIds?: boolean; }; - -export type PlainTreeStrategyProps = TreeStrategyProps & { - type?: typeof STRATEGIES.plain, - items: TItem[], - tree?: ITree -}; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/usePlainTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/usePlainTreeStrategy.ts deleted file mode 100644 index ae55594eb1..0000000000 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/usePlainTreeStrategy.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { useMemo } from 'react'; -import { Tree } from '../../Tree'; -import { PlainTreeStrategyProps } from './types'; - -export function usePlainTreeStrategy({ items, ...props }: PlainTreeStrategyProps, deps: any[]) { - const tree = useMemo(() => Tree.create(props, items), deps); - return tree; -} diff --git a/uui-core/src/data/processing/views/tree/hooks/types.ts b/uui-core/src/data/processing/views/tree/hooks/types.ts index 6622fb7565..528e016ad7 100644 --- a/uui-core/src/data/processing/views/tree/hooks/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/types.ts @@ -1,4 +1,4 @@ -import { PlainTreeStrategyProps } from './strategies/types'; +import { PlainTreeStrategyProps } from './strategies'; -export type UseTreeStrategyProps = (PlainTreeStrategyProps); -export type UseTreeProps = {} & UseTreeStrategyProps; +export type UseTreeStrategyProps = (PlainTreeStrategyProps); +export type UseTreeProps = {} & UseTreeStrategyProps; diff --git a/uui-core/src/data/processing/views/tree/index.ts b/uui-core/src/data/processing/views/tree/index.ts index 5e945595db..7e421f3376 100644 --- a/uui-core/src/data/processing/views/tree/index.ts +++ b/uui-core/src/data/processing/views/tree/index.ts @@ -1,2 +1,3 @@ export * from './Tree'; export * from './ITree'; +export * from './hooks'; From 7ffac3f77069d3be11008de4e6ce23977ca207ed Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Fri, 8 Dec 2023 15:07:03 +0200 Subject: [PATCH 004/278] Formed usePlainTree. --- .../tree/hooks/strategies/plainTree/types.ts | 3 +- .../plainTree/useCheckingService.ts | 60 +++++++++++++++++++ .../plainTree/usePlainTreeStrategy.ts | 19 ++++-- .../views/tree/hooks/strategies/types.ts | 14 ++++- 4 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCheckingService.ts diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts index f58a68e87d..3790360f7b 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts @@ -1,4 +1,4 @@ -import { SortingOption } from '../../../../../../../types'; +import { CascadeSelection, SortingOption } from '../../../../../../../types'; import { ITree } from '../../../ITree'; import { STRATEGIES } from '../constants'; import { TreeStrategyProps } from '../types'; @@ -11,4 +11,5 @@ export type PlainTreeStrategyProps = TreeStrategyProps boolean; sortSearchByRelevance?: boolean; + cascadeSelection?: CascadeSelection; }; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCheckingService.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCheckingService.ts new file mode 100644 index 0000000000..a472779f8a --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCheckingService.ts @@ -0,0 +1,60 @@ +import { useMemo } from 'react'; +import { CascadeSelectionTypes, DataRowProps } from '../../../../../../../types'; +import { ITree, NOT_FOUND_RECORD } from '../../..'; +import { CheckingService, UseCheckingServiceProps } from '../types'; + +const idToKey = (id: TId) => typeof id === 'object' ? JSON.stringify(id) : `${id}`; + +const getCheckingInfo = (checked: TId[] = [], tree: ITree, getParentId?: (item: TItem) => TId) => { + const checkedByKey: Record = {}; + const someChildCheckedByKey: Record = {}; + const checkedItems = checked ?? []; + + for (let i = checkedItems.length - 1; i >= 0; i--) { + const id = checkedItems[i]; + checkedByKey[idToKey(id)] = true; + if (!tree || !getParentId) { + continue; + } + + const item = tree.getById(id); + if (item === NOT_FOUND_RECORD) { + continue; + } + + const parentId = getParentId(item); + if (!someChildCheckedByKey[idToKey(parentId)]) { + const parents = tree.getParentIdsRecursive(id).reverse(); + for (const parent of parents) { + if (someChildCheckedByKey[idToKey(parent)]) { + break; + } + someChildCheckedByKey[idToKey(parent)] = true; + } + } + } + return { checkedByKey, someChildCheckedByKey }; +}; + +export function useCheckingService( + { tree, getParentId, checked = [], cascadeSelection}: UseCheckingServiceProps, +): CheckingService { + const checkingInfoById = useMemo( + () => getCheckingInfo(checked, tree, getParentId), + [tree, checked], + ); + + const { checkedByKey, someChildCheckedByKey } = checkingInfoById; + + const isRowChecked = (row: DataRowProps) => { + const exactCheck = !!checkedByKey[row.rowKey]; + if (exactCheck || cascadeSelection !== CascadeSelectionTypes.IMPLICIT) { + return exactCheck; + } + + const { path } = row; + return path.some(({ id }) => !!checkedByKey[idToKey(id)]); + }; + + return useMemo(() => ({ isRowChecked }), [checkingInfoById, isRowChecked]); +} diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts index 11fd745a4b..e706ea82d8 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts @@ -1,4 +1,6 @@ +import { useMemo } from 'react'; import { PlainTreeStrategyProps } from './types'; +import { useCheckingService } from './useCheckingService'; import { useCreateTree } from './useCreateTree'; import { useFilterTree } from './useFilterTree'; import { useSearchTree } from './useSearchTree'; @@ -9,12 +11,12 @@ export function usePlainTreeStrategy( deps: any[], ) { const props = { ...restProps, sortSearchByRelevance }; - const tree = useCreateTree(props, deps); + const fullTree = useCreateTree(props, deps); - const { dataSourceState, getFilter, getSearchFields, sortBy } = props; + const { dataSourceState, getFilter, getSearchFields, sortBy, cascadeSelection, getParentId } = props; const filteredTree = useFilterTree( - { tree, getFilter, dataSourceState }, + { tree: fullTree, getFilter, dataSourceState }, deps, ); @@ -23,10 +25,17 @@ export function usePlainTreeStrategy( deps, ); - const sortedTree = useSortTree( + const tree = useSortTree( { tree: searchTree, sortBy, dataSourceState }, deps, ); - return sortedTree; + const checkingService = useCheckingService({ + tree, checked: dataSourceState.checked, cascadeSelection, getParentId, + }); + + return useMemo( + () => ({ tree, ...checkingService }), + [tree, checkingService], + ); } diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts index 55d02acc9e..0aff297d32 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts @@ -1,4 +1,5 @@ -import { DataSourceState } from '../../../../../../types'; +import { CascadeSelection, DataSourceState } from '../../../../../../types'; +import { ITree } from '../../ITree'; import { STRATEGIES } from './constants'; export type Strategies = typeof STRATEGIES[keyof typeof STRATEGIES]; @@ -9,3 +10,14 @@ export type TreeStrategyProps = { getParentId?(item: TItem): TId | undefined; complexIds?: boolean; }; + +export interface UseCheckingServiceProps { + tree: ITree; + checked?: TId[]; + getParentId?: (item: TItem) => TId; + cascadeSelection?: CascadeSelection; +} + +export interface CheckingService { + +} From edfd8b229830d69e2cc0af9f4919289c09c8753c Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Mon, 11 Dec 2023 13:36:21 +0200 Subject: [PATCH 005/278] draft changes (not working). --- .../processing/views/dataRows/useDataRows.ts | 158 ++++++++++++++++++ .../plainTree/useCheckingService.ts | 64 ++++++- .../plainTree/usePlainTreeStrategy.ts | 28 +++- .../views/tree/hooks/strategies/types.ts | 8 +- 4 files changed, 248 insertions(+), 10 deletions(-) create mode 100644 uui-core/src/data/processing/views/dataRows/useDataRows.ts diff --git a/uui-core/src/data/processing/views/dataRows/useDataRows.ts b/uui-core/src/data/processing/views/dataRows/useDataRows.ts new file mode 100644 index 0000000000..3f548d4e3f --- /dev/null +++ b/uui-core/src/data/processing/views/dataRows/useDataRows.ts @@ -0,0 +1,158 @@ +import { useMemo } from 'react'; +import { ITree, NOT_FOUND_RECORD } from '../tree'; +import { DataRowProps } from '../../../../types'; + +interface NodeStats { + isSomeCheckable: boolean; + isSomeChecked: boolean; + isAllChecked: boolean; + isSomeSelected: boolean; + hasMoreRows: boolean; + isSomeCheckboxEnabled: boolean; +} + +export interface UseDataRowsProps { + tree: ITree; +} + +export function useDataRows( + { tree }: UseDataRowsProps, + deps: any[], +) { + const rebuildRows = () => { + const rows: DataRowProps[] = []; + const pinned: Record = {}; + const pinnedByParentId: Record = {}; + + const lastIndex = this.getLastRecordIndex(); + + const isFlattenSearch = this.isFlattenSearch?.() ?? false; + const iterateNode = ( + parentId: TId, + appendRows: boolean, // Will be false, if we are iterating folded nodes. + ): NodeStats => { + let currentLevelRows = 0; + let stats = this.getDefaultNodeStats(); + + const layerRows: DataRowProps[] = []; + const nodeInfo = tree.getNodeInfo(parentId); + + const ids = tree.getChildrenIdsByParentId(parentId); + + for (let n = 0; n < ids.length; n++) { + const id = ids[n]; + const item = tree.getById(id); + if (item === NOT_FOUND_RECORD) { + continue; + } + + const row = this.getRowProps(item, rows.length); + if (appendRows && (!this.isPartialLoad() || (this.isPartialLoad() && rows.length < lastIndex))) { + rows.push(row); + layerRows.push(row); + currentLevelRows++; + } + + stats = this.getRowStats(row, stats); + row.isLastChild = n === ids.length - 1 && nodeInfo.count === ids.length; + row.indent = isFlattenSearch ? 0 : row.path.length + 1; + const estimatedChildrenCount = this.getEstimatedChildrenCount(id); + if (!isFlattenSearch && estimatedChildrenCount !== undefined) { + const childrenIds = tree.getChildrenIdsByParentId(id); + + if (estimatedChildrenCount > 0) { + row.isFolded = this.isFolded(item); + row.onFold = row.isFoldable && this.handleOnFold; + + if (childrenIds.length > 0) { + // some children are loaded + const childStats = iterateNode(id, appendRows && !row.isFolded); + row.isChildrenChecked = row.isChildrenChecked || childStats.isSomeChecked; + row.isChildrenSelected = childStats.isSomeSelected; + stats = this.mergeStats(stats, childStats); + // while searching and no children in visible tree, no need to append placeholders. + } else if (!this.value.search && !row.isFolded && appendRows) { + // children are not loaded + const parentsWithRow = [...row.path, tree.getPathItem(item)]; + for (let m = 0; m < estimatedChildrenCount && rows.length < lastIndex; m++) { + const loadingRow = this.getLoadingRow('_loading_' + rows.length, rows.length, parentsWithRow); + loadingRow.indent = parentsWithRow.length + 1; + loadingRow.isLastChild = m === estimatedChildrenCount - 1; + rows.push(loadingRow); + currentLevelRows++; + } + } + } + } + + row.isPinned = row.pin?.(row) ?? false; + if (row.isPinned) { + pinned[this.idToKey(row.id)] = row.index; + if (!pinnedByParentId[this.idToKey(row.parentId)]) { + pinnedByParentId[this.idToKey(row.parentId)] = []; + } + pinnedByParentId[this.idToKey(row.parentId)]?.push(row.index); + } + } + + const pathToParent = tree.getPathById(parentId); + const parent = tree.getById(parentId); + const parentPathItem = parent !== NOT_FOUND_RECORD ? [tree.getPathItem(parent)] : []; + const path = parentId ? [...pathToParent, ...parentPathItem] : pathToParent; + if (appendRows) { + let missingCount: number = this.getMissingRecordsCount(parentId, rows.length, currentLevelRows); + if (missingCount > 0) { + stats.hasMoreRows = true; + } + + // Append loading rows, stop at lastIndex (last row visible) + while (rows.length < lastIndex && missingCount > 0) { + const row = this.getLoadingRow('_loading_' + rows.length, rows.length, path); + rows.push(row); + layerRows.push(row); + currentLevelRows++; + missingCount--; + } + } + + const isListFlat = path.length === 0 && !layerRows.some((r) => r.isFoldable); + if (isListFlat || isFlattenSearch) { + layerRows.forEach((r) => { + r.indent = 0; + }); + } + + return stats; + }; + + const rootStats = iterateNode(undefined, true); + + return { + rows, + pinned, + pinnedByParentId, + stats: rootStats, + }; + }; + + const { rows, pinned, pinnedByParentId, stats } = useMemo(() => rebuildRows(), []); + + const selectAll = useMemo(() => { + if (stats.isSomeCheckable && this.isSelectAllEnabled()) { + return { + value: stats.isSomeCheckboxEnabled ? stats.isAllChecked : false, + onValueChange: this.handleSelectAll, + indeterminate: this.value.checked && this.value.checked.length > 0 && !stats.isAllChecked, + }; + } else if (tree.getRootIds().length === 0 && this.props.rowOptions?.checkbox?.isVisible && this.isSelectAllEnabled()) { + // Nothing loaded yet, but we guess that something is checkable. Add disabled checkbox for less flicker. + return { + value: false, + onValueChange: () => {}, + isDisabled: true, + indeterminate: this.value.checked?.length > 0, + }; + } + return null; + }, [stats]); +} diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCheckingService.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCheckingService.ts index a472779f8a..33fbd46ab3 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCheckingService.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCheckingService.ts @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { CascadeSelectionTypes, DataRowProps } from '../../../../../../../types'; import { ITree, NOT_FOUND_RECORD } from '../../..'; import { CheckingService, UseCheckingServiceProps } from '../types'; @@ -37,7 +37,15 @@ const getCheckingInfo = (checked: TId[] = [], tree: ITree( - { tree, getParentId, checked = [], cascadeSelection}: UseCheckingServiceProps, + { + tree, + getParentId, + checked = [], + setChecked, + cascadeSelection, + getRowOptions, + rowOptions, + }: UseCheckingServiceProps, ): CheckingService { const checkingInfoById = useMemo( () => getCheckingInfo(checked, tree, getParentId), @@ -46,7 +54,7 @@ export function useCheckingService( const { checkedByKey, someChildCheckedByKey } = checkingInfoById; - const isRowChecked = (row: DataRowProps) => { + const isRowChecked = useCallback((row: DataRowProps) => { const exactCheck = !!checkedByKey[row.rowKey]; if (exactCheck || cascadeSelection !== CascadeSelectionTypes.IMPLICIT) { return exactCheck; @@ -54,7 +62,53 @@ export function useCheckingService( const { path } = row; return path.some(({ id }) => !!checkedByKey[idToKey(id)]); - }; + }, [checkedByKey]); - return useMemo(() => ({ isRowChecked }), [checkingInfoById, isRowChecked]); + const isRowChildrenChecked = useCallback((row: DataRowProps) => { + return someChildCheckedByKey[row.rowKey] ?? false; + }, [someChildCheckedByKey]); + + const getRowProps = useCallback((item: TItem) => { + const externalRowOptions = getRowOptions ? getRowOptions(item) : {}; + return { ...rowOptions, ...externalRowOptions }; + }, [rowOptions, getRowOptions]); + + const isItemCheckable = useCallback((item: TItem) => { + const rowProps = getRowProps(item); + return rowProps?.checkbox?.isVisible && !rowProps?.checkbox?.isDisabled; + }, [getRowProps]); + + const handleCheck = useCallback((isChecked: boolean, checkedId?: TId) => { + const updatedChecked = tree.cascadeSelection(checked, checkedId, isChecked, { + cascade: cascadeSelection, + isSelectable: (item: TItem) => isItemCheckable(item), + }); + + setChecked(updatedChecked); + }, [tree, checked, setChecked, isItemCheckable, cascadeSelection]); + + const handleSelectAll = useCallback((isChecked: boolean) => { + handleCheck(isChecked); + }, [handleCheck]); + + const clearAllChecked = useCallback(() => { + handleCheck(false); + }, [handleCheck]); + + return useMemo( + () => ({ + isRowChecked, + isRowChildrenChecked, + handleCheck, + handleSelectAll, + clearAllChecked, + }), + [ + isRowChecked, + isRowChildrenChecked, + handleCheck, + handleSelectAll, + clearAllChecked, + ], + ); } diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts index e706ea82d8..7eaf8dc20c 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { PlainTreeStrategyProps } from './types'; import { useCheckingService } from './useCheckingService'; import { useCreateTree } from './useCreateTree'; @@ -13,7 +13,17 @@ export function usePlainTreeStrategy( const props = { ...restProps, sortSearchByRelevance }; const fullTree = useCreateTree(props, deps); - const { dataSourceState, getFilter, getSearchFields, sortBy, cascadeSelection, getParentId } = props; + const { + dataSourceState, + setDataSourceState, + getFilter, + getSearchFields, + sortBy, + cascadeSelection, + getParentId, + rowOptions, + getRowOptions, + } = props; const filteredTree = useFilterTree( { tree: fullTree, getFilter, dataSourceState }, @@ -30,12 +40,22 @@ export function usePlainTreeStrategy( deps, ); + const { checked } = dataSourceState; + const setChecked = useCallback( + (newChecked: TId[]) => setDataSourceState({ ...dataSourceState, checked: newChecked }), + [setDataSourceState, dataSourceState], + ); + const checkingService = useCheckingService({ - tree, checked: dataSourceState.checked, cascadeSelection, getParentId, + tree, + checked, + setChecked, + cascadeSelection, + getParentId, }); return useMemo( - () => ({ tree, ...checkingService }), + () => ({ tree, ...checkingService, rowOptions, getRowOptions }), [tree, checkingService], ); } diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts index 0aff297d32..ce9bf180d1 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts @@ -1,4 +1,4 @@ -import { CascadeSelection, DataSourceState } from '../../../../../../types'; +import { CascadeSelection, DataRowOptions, DataSourceState } from '../../../../../../types'; import { ITree } from '../../ITree'; import { STRATEGIES } from './constants'; @@ -6,16 +6,22 @@ export type Strategies = typeof STRATEGIES[keyof typeof STRATEGIES]; export type TreeStrategyProps = { dataSourceState: DataSourceState; + setDataSourceState: (dataSourceState: DataSourceState) => void; getId?(item: TItem): TId; getParentId?(item: TItem): TId | undefined; complexIds?: boolean; + rowOptions?: DataRowOptions; + getRowOptions?(item: TItem, index?: number): DataRowOptions; }; export interface UseCheckingServiceProps { tree: ITree; checked?: TId[]; + setChecked: (checked: TId[]) => void; getParentId?: (item: TItem) => TId; cascadeSelection?: CascadeSelection; + rowOptions?: DataRowOptions; + getRowOptions?(item: TItem, index?: number): DataRowOptions; } export interface CheckingService { From bd1de45f547a49bed298f5ae6d11023ddc2dc384 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Mon, 11 Dec 2023 16:21:30 +0200 Subject: [PATCH 006/278] One more draft. --- .../processing/views/dataRows/useDataRows.ts | 479 ++++++++++++++++-- .../plainTree/useCheckingService.ts | 11 +- uui-core/src/types/dataSources.ts | 1 + 3 files changed, 460 insertions(+), 31 deletions(-) diff --git a/uui-core/src/data/processing/views/dataRows/useDataRows.ts b/uui-core/src/data/processing/views/dataRows/useDataRows.ts index 3f548d4e3f..34d22897ed 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRows.ts @@ -1,6 +1,6 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { ITree, NOT_FOUND_RECORD } from '../tree'; -import { DataRowProps } from '../../../../types'; +import { CascadeSelection, CascadeSelectionTypes, DataRowOptions, DataRowPathItem, DataRowProps, DataSourceListProps, DataSourceState, ScrollToConfig, VirtualListRange } from '../../../../types'; interface NodeStats { isSomeCheckable: boolean; @@ -13,26 +13,293 @@ interface NodeStats { export interface UseDataRowsProps { tree: ITree; + dataSourceState: DataSourceState; + setDataSourceState?: (dataSourceState: DataSourceState) => void; + flattenSearchResults?: boolean; + isPartialLoad?: boolean; + rowOptions?: DataRowOptions; + getRowOptions?(item: TItem, index?: number): DataRowOptions; + isRowChildrenChecked: (row: DataRowProps) => boolean; + isRowChecked: (row: DataRowProps) => boolean; + getChildCount?(item: TItem): number; + getId: (item: TItem) => TId; + cascadeSelection?: CascadeSelection; + handleOnCheck: (rowProps: DataRowProps) => void; + isFoldedByDefault?(item: TItem): boolean; + selectAll?: boolean; + handleSelectAll: (isChecked: boolean) => void; } +const getDefaultNodeStats = () => ({ + isSomeCheckable: false, + isSomeChecked: false, + isAllChecked: true, + isSomeSelected: false, + hasMoreRows: false, + isSomeCheckboxEnabled: false, +}); + +const mergeStats = (parentStats: NodeStats, childStats: NodeStats) => ({ + ...parentStats, + isSomeCheckable: parentStats.isSomeCheckable || childStats.isSomeCheckable, + isSomeChecked: parentStats.isSomeChecked || childStats.isSomeChecked, + isAllChecked: parentStats.isAllChecked && childStats.isAllChecked, + isSomeCheckboxEnabled: parentStats.isSomeCheckboxEnabled || childStats.isSomeCheckboxEnabled, + hasMoreRows: parentStats.hasMoreRows || childStats.hasMoreRows, +}); + +const idToKey = (id: TId) => typeof id === 'object' ? JSON.stringify(id) : `${id}`; +const setObjectFlag = (object: any, key: string, value: boolean) => { + return { ...object, [key]: value }; +}; + export function useDataRows( - { tree }: UseDataRowsProps, - deps: any[], + props: UseDataRowsProps, ) { + const { + tree, + getId, + dataSourceState, + setDataSourceState, + flattenSearchResults, + isPartialLoad, + getRowOptions, + rowOptions, + isRowChecked, + isRowChildrenChecked, + handleOnCheck, + getChildCount, + cascadeSelection, + isFoldedByDefault, + handleSelectAll, + } = props; + + const lastIndex = useMemo( + () => { + const currentLastIndex = dataSourceState.topIndex + dataSourceState.visibleCount; + const actualCount = tree.getTotalRecursiveCount() ?? 0; + + if (actualCount < currentLastIndex) return actualCount; + return currentLastIndex; + }, + [tree, dataSourceState.topIndex, dataSourceState.visibleCount], + ); + + const isFlattenSearch = useMemo(() => dataSourceState.search && flattenSearchResults, []); + + const isFolded = (item: TItem) => { + const searchIsApplied = !!dataSourceState?.search; + if (searchIsApplied) { + return false; + } + + const folded = dataSourceState.folded || {}; + const key = idToKey(getId(item)); + if (folded[key] != null) { + return folded[key]; + } + + if (isFoldedByDefault) { + return isFoldedByDefault(item); + } + + return true; + }; + + const getEstimatedChildrenCount = (id: TId) => { + if (id === undefined) return undefined; + + const item = tree.getById(id); + if (item === NOT_FOUND_RECORD) return undefined; + + const childCount = getChildCount?.(item) ?? undefined; + if (childCount === undefined) return undefined; + + const nodeInfo = tree.getNodeInfo(id); + if (nodeInfo?.count !== undefined) { + // nodes are already loaded, and we know the actual count + return nodeInfo.count; + } + + return childCount; + }; + + const getMissingRecordsCount = (id: TId, totalRowsCount: number, loadedChildrenCount: number) => { + const nodeInfo = tree.getNodeInfo(id); + + const estimatedChildCount = getEstimatedChildrenCount(id); + + // Estimate how many more nodes there are at current level, to put 'loading' placeholders. + if (nodeInfo.count !== undefined) { + // Exact count known + return nodeInfo.count - loadedChildrenCount; + } + + // estimatedChildCount = undefined for top-level rows only. + if (id === undefined && totalRowsCount < lastIndex) { + return lastIndex - totalRowsCount; // let's put placeholders down to the bottom of visible list + } + + if (estimatedChildCount > loadedChildrenCount) { + // According to getChildCount (put into estimatedChildCount), there are more rows on this level + return estimatedChildCount - loadedChildrenCount; + } + + // We have a bad estimate - it even less that actual items we have + // This would happen is getChildCount provides a guess count, and we scroll thru children past this count + // let's guess we have at least 1 item more than loaded + return 1; + }; + + const handleOnSelect = (rowProps: DataRowProps) => { + setDataSourceState?.({ + ...dataSourceState, + selectedId: rowProps.id, + }); + }; + + const handleOnFocus = (focusIndex: number) => { + setDataSourceState({ + ...dataSourceState, + focusedIndex: focusIndex, + }); + }; + + const handleOnFold = (rowProps: DataRowProps) => { + if (setDataSourceState) { + const fold = !rowProps.isFolded; + const indexToScroll = rowProps.index - (rowProps.path?.length ?? 0); + const scrollTo: ScrollToConfig = fold && rowProps.isPinned + ? { index: indexToScroll, align: 'nearest' } + : dataSourceState.scrollTo; + + setDataSourceState({ + ...dataSourceState, + scrollTo, + folded: setObjectFlag(dataSourceState && dataSourceState.folded, rowProps.rowKey, fold), + }); + } + }; + + const applyRowOptions = (row: DataRowProps) => { + const externalRowOptions = (getRowOptions && !row.isLoading) + ? getRowOptions(row.value, row.index) + : {}; + + const fullRowOptions = { ...rowOptions, ...externalRowOptions }; + + const estimatedChildrenCount = getEstimatedChildrenCount(row.id); + + row.isFoldable = false; + if (!isFlattenSearch && estimatedChildrenCount !== undefined && estimatedChildrenCount > 0) { + row.isFoldable = true; + } + + const isCheckable = fullRowOptions && fullRowOptions.checkbox && fullRowOptions.checkbox.isVisible && !fullRowOptions.checkbox.isDisabled; + const isSelectable = fullRowOptions && fullRowOptions.isSelectable; + if (fullRowOptions != null) { + const rowValue = row.value; + Object.assign(row, fullRowOptions); + row.value = fullRowOptions.value ?? rowValue; + } + row.isFocused = dataSourceState.focusedIndex === row.index; + row.isChecked = isRowChecked(row); + row.isSelected = dataSourceState.selectedId === row.id; + row.isCheckable = isCheckable; + row.onCheck = isCheckable && handleOnCheck; + row.onSelect = fullRowOptions && fullRowOptions.isSelectable && handleOnSelect; + row.onFocus = (isSelectable || isCheckable || row.isFoldable) && handleOnFocus; + row.isChildrenChecked = isRowChildrenChecked(row); + }; + + const getRowProps = (item: TItem, index: number): DataRowProps => { + const id = getId(item); + const key = idToKey(id); + const path = tree.getPathById(id); + const parentId = path.length > 0 ? path[path.length - 1].id : undefined; + const rowProps = { + id, + parentId, + key, + rowKey: key, + index, + value: item, + depth: path.length, + path, + } as DataRowProps; + + applyRowOptions(rowProps); + + return rowProps; + }; + + const getRowStats = (row: DataRowProps, actualStats: NodeStats): NodeStats => { + let { + isSomeCheckable, isSomeChecked, isAllChecked, isSomeSelected, isSomeCheckboxEnabled, + } = actualStats; + + if (row.checkbox) { + isSomeCheckable = true; + if (row.isChecked || row.isChildrenChecked) { + isSomeChecked = true; + } + if (!row.checkbox.isDisabled || isSomeCheckboxEnabled) { + isSomeCheckboxEnabled = true; + } + + const isImplicitCascadeSelection = cascadeSelection === CascadeSelectionTypes.IMPLICIT; + if ( + (!row.isChecked && !row.checkbox.isDisabled && !isImplicitCascadeSelection) + || (row.parentId === undefined && !row.isChecked && isImplicitCascadeSelection) + ) { + isAllChecked = false; + } + } + + if (row.isSelected) { + isSomeSelected = true; + } + + return { + ...actualStats, isSomeCheckable, isSomeChecked, isAllChecked, isSomeSelected, isSomeCheckboxEnabled, + }; + }; + + const getEmptyRowProps = (id: any, index: number = 0, path: DataRowPathItem[] = null): DataRowProps => { + const checked = dataSourceState?.checked ?? []; + const isChecked = checked.includes(id); + return { + id, + rowKey: idToKey(id), + value: undefined, + index, + depth: path ? path.length : 0, + path: path ?? [], + checkbox: rowOptions?.checkbox?.isVisible && { isVisible: true, isDisabled: true }, + isChecked, + }; + }; + + const getLoadingRow = (id: any, index: number = 0, path: DataRowPathItem[] = null): DataRowProps => { + const rowProps = getEmptyRowProps(id, index, path); + return { + ...rowProps, + checkbox: { ...rowProps.checkbox, isDisabled: true }, + isLoading: true, + }; + }; + const rebuildRows = () => { const rows: DataRowProps[] = []; const pinned: Record = {}; const pinnedByParentId: Record = {}; - const lastIndex = this.getLastRecordIndex(); - - const isFlattenSearch = this.isFlattenSearch?.() ?? false; const iterateNode = ( parentId: TId, appendRows: boolean, // Will be false, if we are iterating folded nodes. ): NodeStats => { let currentLevelRows = 0; - let stats = this.getDefaultNodeStats(); + let stats = getDefaultNodeStats(); const layerRows: DataRowProps[] = []; const nodeInfo = tree.getNodeInfo(parentId); @@ -46,36 +313,36 @@ export function useDataRows( continue; } - const row = this.getRowProps(item, rows.length); - if (appendRows && (!this.isPartialLoad() || (this.isPartialLoad() && rows.length < lastIndex))) { + const row = getRowProps(item, rows.length); + if (appendRows && (!isPartialLoad || (isPartialLoad && rows.length < lastIndex))) { rows.push(row); layerRows.push(row); currentLevelRows++; } - stats = this.getRowStats(row, stats); + stats = getRowStats(row, stats); row.isLastChild = n === ids.length - 1 && nodeInfo.count === ids.length; row.indent = isFlattenSearch ? 0 : row.path.length + 1; - const estimatedChildrenCount = this.getEstimatedChildrenCount(id); + const estimatedChildrenCount = getEstimatedChildrenCount(id); if (!isFlattenSearch && estimatedChildrenCount !== undefined) { const childrenIds = tree.getChildrenIdsByParentId(id); if (estimatedChildrenCount > 0) { - row.isFolded = this.isFolded(item); - row.onFold = row.isFoldable && this.handleOnFold; + row.isFolded = isFolded(item); + row.onFold = row.isFoldable && handleOnFold; if (childrenIds.length > 0) { // some children are loaded const childStats = iterateNode(id, appendRows && !row.isFolded); row.isChildrenChecked = row.isChildrenChecked || childStats.isSomeChecked; row.isChildrenSelected = childStats.isSomeSelected; - stats = this.mergeStats(stats, childStats); + stats = mergeStats(stats, childStats); // while searching and no children in visible tree, no need to append placeholders. - } else if (!this.value.search && !row.isFolded && appendRows) { + } else if (!dataSourceState.search && !row.isFolded && appendRows) { // children are not loaded const parentsWithRow = [...row.path, tree.getPathItem(item)]; for (let m = 0; m < estimatedChildrenCount && rows.length < lastIndex; m++) { - const loadingRow = this.getLoadingRow('_loading_' + rows.length, rows.length, parentsWithRow); + const loadingRow = getLoadingRow('_loading_' + rows.length, rows.length, parentsWithRow); loadingRow.indent = parentsWithRow.length + 1; loadingRow.isLastChild = m === estimatedChildrenCount - 1; rows.push(loadingRow); @@ -87,11 +354,11 @@ export function useDataRows( row.isPinned = row.pin?.(row) ?? false; if (row.isPinned) { - pinned[this.idToKey(row.id)] = row.index; - if (!pinnedByParentId[this.idToKey(row.parentId)]) { - pinnedByParentId[this.idToKey(row.parentId)] = []; + pinned[idToKey(row.id)] = row.index; + if (!pinnedByParentId[idToKey(row.parentId)]) { + pinnedByParentId[idToKey(row.parentId)] = []; } - pinnedByParentId[this.idToKey(row.parentId)]?.push(row.index); + pinnedByParentId[idToKey(row.parentId)]?.push(row.index); } } @@ -100,14 +367,14 @@ export function useDataRows( const parentPathItem = parent !== NOT_FOUND_RECORD ? [tree.getPathItem(parent)] : []; const path = parentId ? [...pathToParent, ...parentPathItem] : pathToParent; if (appendRows) { - let missingCount: number = this.getMissingRecordsCount(parentId, rows.length, currentLevelRows); + let missingCount = getMissingRecordsCount(parentId, rows.length, currentLevelRows); if (missingCount > 0) { stats.hasMoreRows = true; } // Append loading rows, stop at lastIndex (last row visible) while (rows.length < lastIndex && missingCount > 0) { - const row = this.getLoadingRow('_loading_' + rows.length, rows.length, path); + const row = getLoadingRow('_loading_' + rows.length, rows.length, path); rows.push(row); layerRows.push(row); currentLevelRows++; @@ -137,22 +404,176 @@ export function useDataRows( const { rows, pinned, pinnedByParentId, stats } = useMemo(() => rebuildRows(), []); + const isSelectAllEnabled = useMemo(() => props.selectAll === undefined ? true : props.selectAll, [props.selectAll]); + const selectAll = useMemo(() => { - if (stats.isSomeCheckable && this.isSelectAllEnabled()) { + if (stats.isSomeCheckable && isSelectAllEnabled) { return { value: stats.isSomeCheckboxEnabled ? stats.isAllChecked : false, - onValueChange: this.handleSelectAll, - indeterminate: this.value.checked && this.value.checked.length > 0 && !stats.isAllChecked, + onValueChange: handleSelectAll, + indeterminate: dataSourceState.checked && dataSourceState.checked.length > 0 && !stats.isAllChecked, }; - } else if (tree.getRootIds().length === 0 && this.props.rowOptions?.checkbox?.isVisible && this.isSelectAllEnabled()) { + } else if (tree.getRootIds().length === 0 && rowOptions?.checkbox?.isVisible && isSelectAllEnabled) { // Nothing loaded yet, but we guess that something is checkable. Add disabled checkbox for less flicker. return { value: false, onValueChange: () => {}, isDisabled: true, - indeterminate: this.value.checked?.length > 0, + indeterminate: dataSourceState.checked?.length > 0, }; } return null; - }, [stats]); + }, [tree, rowOptions, dataSourceState.checked, stats, isSelectAllEnabled, handleSelectAll]); + + const getListProps = useCallback((): DataSourceListProps => { + return { + rowsCount: rows.length, + knownRowsCount: rows.length, + exactRowsCount: rows.length, + totalCount: tree?.getTotalRecursiveCount() ?? 0, // TODO: totalCount should be taken from fullTree (?). + selectAll, + }; + }, [rows.length, tree, selectAll]); + + const getLastPinnedBeforeRow = (row: DataRowProps, pinnedIndexes: number[]) => { + const isBeforeOrEqualToRow = (pinnedRowIndex: number) => { + const pinnedRow = rows[pinnedRowIndex]; + if (!pinnedRow) { + return false; + } + return row.index >= pinnedRow.index; + }; + + let foundRowIndex = -1; + for (const pinnedRowIndex of pinnedIndexes) { + if (isBeforeOrEqualToRow(pinnedRowIndex)) { + foundRowIndex = pinnedRowIndex; + } else if (foundRowIndex !== -1) { + break; + } + } + + if (foundRowIndex === -1) { + return undefined; + } + return foundRowIndex; + }; + + const getLastHiddenPinnedByParent = (row: DataRowProps, alreadyAdded: TId[]) => { + const pinnedIndexes = pinnedByParentId[idToKey(row.parentId)]; + if (!pinnedIndexes || !pinnedIndexes.length) { + return undefined; + } + + const lastPinnedBeforeRow = getLastPinnedBeforeRow(row, pinnedIndexes); + if (lastPinnedBeforeRow === undefined) { + return undefined; + } + + const lastHiddenPinned = rows[lastPinnedBeforeRow]; + if (!lastHiddenPinned || alreadyAdded.includes(lastHiddenPinned.id)) { + return undefined; + } + + return lastHiddenPinned; + }; + + const getRowsWithPinned = (allRows: DataRowProps[]) => { + if (!allRows.length) return []; + + const rowsWithPinned: DataRowProps[] = []; + const alreadyAdded = allRows.map(({ id }) => id); + const [firstRow] = allRows; + firstRow.path.forEach((item) => { + const pinnedIndex = pinned[idToKey(item.id)]; + if (pinnedIndex === undefined) return; + + const parent = rows[pinnedIndex]; + if (!parent || alreadyAdded.includes(parent.id)) return; + + rowsWithPinned.push(parent); + alreadyAdded.push(parent.id); + }); + + const lastHiddenPinned = getLastHiddenPinnedByParent(firstRow, alreadyAdded); + if (lastHiddenPinned) { + rowsWithPinned.push(lastHiddenPinned); + } + + return rowsWithPinned.concat(allRows); + }; + + const getVisibleRows = () => { + const visibleRows = rows.slice(dataSourceState.topIndex, lastIndex); + return getRowsWithPinned(visibleRows); + }; + + const getUnknownRow = (id: any, index: number = 0, path: DataRowPathItem[] = null): DataRowProps => { + const emptyRowProps = getEmptyRowProps(id, index, path); + const checkbox = (rowOptions?.checkbox?.isVisible || emptyRowProps.isChecked) + ? { isVisible: true, isDisabled: false } + : undefined; + + return { + ...emptyRowProps, + checkbox, + isUnknown: true, + }; + }; + + const getById = (id: TId, index: number) => { + // if originalTree is not created, but blank tree is defined, get item from it + const item = tree.getById(id); + if (item === NOT_FOUND_RECORD) { + return getUnknownRow(id, index, []); + } + + return getRowProps(item, index); + }; + + const getSelectedRows = ({ topIndex = 0, visibleCount }: VirtualListRange = {}) => { + let checked: TId[] = []; + if (dataSourceState.selectedId !== null && dataSourceState.selectedId !== undefined) { + checked = [dataSourceState.selectedId]; + } else if (dataSourceState.checked) { + checked = dataSourceState.checked; + } + + if (visibleCount !== undefined) { + checked = checked.slice(topIndex, topIndex + visibleCount); + } + const selectedRows: Array> = []; + const missingIds: TId[] = []; + checked.forEach((id, n) => { + const row = getById(id, topIndex + n); + if (row.isUnknown) { + missingIds.push(id); + } + selectedRows.push(row); + }); + if (missingIds.length) { + console.error(`DataSource can't find selected/checked items with following IDs: ${missingIds.join(', ')}. + Read more here: https://github.com/epam/UUI/issues/89`); + } + + return selectedRows; + }; + + const getSelectedRowsCount = () => { + const count = dataSourceState.checked?.length ?? 0; + if (!count) { + return (dataSourceState.selectedId !== undefined && dataSourceState.selectedId !== null) ? 1 : 0; + } + + return count; + }; + + return { + getListProps, + getVisibleRows, + getSelectedRows, + getSelectedRowsCount, + + selectAll, + }; } diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCheckingService.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCheckingService.ts index 33fbd46ab3..f787e69be8 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCheckingService.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCheckingService.ts @@ -95,18 +95,25 @@ export function useCheckingService( handleCheck(false); }, [handleCheck]); + const handleOnCheck = useCallback((rowProps: DataRowProps) => { + const id = rowProps.id; + const isChecked = !rowProps.isChecked; + + handleCheck(isChecked, id); + }, []); + return useMemo( () => ({ isRowChecked, isRowChildrenChecked, - handleCheck, + handleOnCheck, handleSelectAll, clearAllChecked, }), [ isRowChecked, isRowChildrenChecked, - handleCheck, + handleOnCheck, handleSelectAll, clearAllChecked, ], diff --git a/uui-core/src/types/dataSources.ts b/uui-core/src/types/dataSources.ts index 5863be664e..563e7804f7 100644 --- a/uui-core/src/types/dataSources.ts +++ b/uui-core/src/types/dataSources.ts @@ -218,6 +218,7 @@ export type IDataSourceView = { loadData(): void; clearAllChecked(): void; _forceUpdate(): void; + selectAll?: ICheckable; }; From 207069620226b1a0d2e929925ee710047fc0652d Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Mon, 11 Dec 2023 17:58:55 +0200 Subject: [PATCH 007/278] Separated rows actions in services. --- .../data/processing/views/dataRows/stats.ts | 60 +++++ .../processing/views/dataRows/useDataRows.ts | 220 +++--------------- .../data/processing/views/helpers/convert.ts | 1 + .../data/processing/views/helpers/index.ts | 2 + .../data/processing/views/helpers/setters.ts | 3 + .../processing/views/tree/hooks/helpers.ts | 0 .../views/tree/hooks/services/index.ts | 9 + .../useCheckingService.ts | 29 ++- .../tree/hooks/services/useFocusService.ts | 28 +++ .../tree/hooks/services/useFoldingService.ts | 62 +++++ .../hooks/services/useSelectingService.ts | 28 +++ .../tree/hooks/strategies/plainTree/types.ts | 1 + .../plainTree/usePlainTreeStrategy.ts | 94 +++++++- .../views/tree/hooks/strategies/types.ts | 18 +- 14 files changed, 341 insertions(+), 214 deletions(-) create mode 100644 uui-core/src/data/processing/views/dataRows/stats.ts create mode 100644 uui-core/src/data/processing/views/helpers/convert.ts create mode 100644 uui-core/src/data/processing/views/helpers/index.ts create mode 100644 uui-core/src/data/processing/views/helpers/setters.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/helpers.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/services/index.ts rename uui-core/src/data/processing/views/tree/hooks/{strategies/plainTree => services}/useCheckingService.ts (79%) create mode 100644 uui-core/src/data/processing/views/tree/hooks/services/useFocusService.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/services/useFoldingService.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/services/useSelectingService.ts diff --git a/uui-core/src/data/processing/views/dataRows/stats.ts b/uui-core/src/data/processing/views/dataRows/stats.ts new file mode 100644 index 0000000000..b6eddbe609 --- /dev/null +++ b/uui-core/src/data/processing/views/dataRows/stats.ts @@ -0,0 +1,60 @@ +import { CascadeSelection, CascadeSelectionTypes, DataRowProps } from '../../../../types'; + +export interface NodeStats { + isSomeCheckable: boolean; + isSomeChecked: boolean; + isAllChecked: boolean; + isSomeSelected: boolean; + hasMoreRows: boolean; + isSomeCheckboxEnabled: boolean; +} + +export const getDefaultNodeStats = () => ({ + isSomeCheckable: false, + isSomeChecked: false, + isAllChecked: true, + isSomeSelected: false, + hasMoreRows: false, + isSomeCheckboxEnabled: false, +}); + +export const mergeStats = (parentStats: NodeStats, childStats: NodeStats) => ({ + ...parentStats, + isSomeCheckable: parentStats.isSomeCheckable || childStats.isSomeCheckable, + isSomeChecked: parentStats.isSomeChecked || childStats.isSomeChecked, + isAllChecked: parentStats.isAllChecked && childStats.isAllChecked, + isSomeCheckboxEnabled: parentStats.isSomeCheckboxEnabled || childStats.isSomeCheckboxEnabled, + hasMoreRows: parentStats.hasMoreRows || childStats.hasMoreRows, +}); + +export const getRowStats = (row: DataRowProps, actualStats: NodeStats, cascadeSelection: CascadeSelection): NodeStats => { + let { + isSomeCheckable, isSomeChecked, isAllChecked, isSomeSelected, isSomeCheckboxEnabled, + } = actualStats; + + if (row.checkbox) { + isSomeCheckable = true; + if (row.isChecked || row.isChildrenChecked) { + isSomeChecked = true; + } + if (!row.checkbox.isDisabled || isSomeCheckboxEnabled) { + isSomeCheckboxEnabled = true; + } + + const isImplicitCascadeSelection = cascadeSelection === CascadeSelectionTypes.IMPLICIT; + if ( + (!row.isChecked && !row.checkbox.isDisabled && !isImplicitCascadeSelection) + || (row.parentId === undefined && !row.isChecked && isImplicitCascadeSelection) + ) { + isAllChecked = false; + } + } + + if (row.isSelected) { + isSomeSelected = true; + } + + return { + ...actualStats, isSomeCheckable, isSomeChecked, isAllChecked, isSomeSelected, isSomeCheckboxEnabled, + }; +}; diff --git a/uui-core/src/data/processing/views/dataRows/useDataRows.ts b/uui-core/src/data/processing/views/dataRows/useDataRows.ts index 34d22897ed..caa0743244 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRows.ts @@ -1,20 +1,13 @@ import { useCallback, useMemo } from 'react'; import { ITree, NOT_FOUND_RECORD } from '../tree'; -import { CascadeSelection, CascadeSelectionTypes, DataRowOptions, DataRowPathItem, DataRowProps, DataSourceListProps, DataSourceState, ScrollToConfig, VirtualListRange } from '../../../../types'; - -interface NodeStats { - isSomeCheckable: boolean; - isSomeChecked: boolean; - isAllChecked: boolean; - isSomeSelected: boolean; - hasMoreRows: boolean; - isSomeCheckboxEnabled: boolean; -} +import { CascadeSelection, DataRowOptions, DataRowPathItem, DataRowProps, DataSourceListProps, DataSourceState, VirtualListRange } from '../../../../types'; +import { idToKey } from '../helpers'; +import { FoldingService, CheckingService, FocusService, SelectingService } from '../tree/hooks/services'; +import { NodeStats, getDefaultNodeStats, getRowStats, mergeStats } from './stats'; -export interface UseDataRowsProps { +export interface UseDataRowsProps extends FoldingService, CheckingService, FocusService, SelectingService { tree: ITree; dataSourceState: DataSourceState; - setDataSourceState?: (dataSourceState: DataSourceState) => void; flattenSearchResults?: boolean; isPartialLoad?: boolean; rowOptions?: DataRowOptions; @@ -25,34 +18,13 @@ export interface UseDataRowsProps { getId: (item: TItem) => TId; cascadeSelection?: CascadeSelection; handleOnCheck: (rowProps: DataRowProps) => void; - isFoldedByDefault?(item: TItem): boolean; selectAll?: boolean; handleSelectAll: (isChecked: boolean) => void; + getEstimatedChildrenCount: (id: TId) => number; + getMissingRecordsCount: (id: TId, totalRowsCount: number, loadedChildrenCount: number) => number; + lastRowIndex: number; } -const getDefaultNodeStats = () => ({ - isSomeCheckable: false, - isSomeChecked: false, - isAllChecked: true, - isSomeSelected: false, - hasMoreRows: false, - isSomeCheckboxEnabled: false, -}); - -const mergeStats = (parentStats: NodeStats, childStats: NodeStats) => ({ - ...parentStats, - isSomeCheckable: parentStats.isSomeCheckable || childStats.isSomeCheckable, - isSomeChecked: parentStats.isSomeChecked || childStats.isSomeChecked, - isAllChecked: parentStats.isAllChecked && childStats.isAllChecked, - isSomeCheckboxEnabled: parentStats.isSomeCheckboxEnabled || childStats.isSomeCheckboxEnabled, - hasMoreRows: parentStats.hasMoreRows || childStats.hasMoreRows, -}); - -const idToKey = (id: TId) => typeof id === 'object' ? JSON.stringify(id) : `${id}`; -const setObjectFlag = (object: any, key: string, value: boolean) => { - return { ...object, [key]: value }; -}; - export function useDataRows( props: UseDataRowsProps, ) { @@ -60,127 +32,29 @@ export function useDataRows( tree, getId, dataSourceState, - setDataSourceState, flattenSearchResults, isPartialLoad, getRowOptions, rowOptions, + + getEstimatedChildrenCount, + getMissingRecordsCount, + cascadeSelection, + lastRowIndex, + + isFolded, isRowChecked, isRowChildrenChecked, - handleOnCheck, - getChildCount, - cascadeSelection, - isFoldedByDefault, + + handleOnFold, handleSelectAll, + handleOnCheck, + handleOnFocus, + handleOnSelect, } = props; - const lastIndex = useMemo( - () => { - const currentLastIndex = dataSourceState.topIndex + dataSourceState.visibleCount; - const actualCount = tree.getTotalRecursiveCount() ?? 0; - - if (actualCount < currentLastIndex) return actualCount; - return currentLastIndex; - }, - [tree, dataSourceState.topIndex, dataSourceState.visibleCount], - ); - const isFlattenSearch = useMemo(() => dataSourceState.search && flattenSearchResults, []); - const isFolded = (item: TItem) => { - const searchIsApplied = !!dataSourceState?.search; - if (searchIsApplied) { - return false; - } - - const folded = dataSourceState.folded || {}; - const key = idToKey(getId(item)); - if (folded[key] != null) { - return folded[key]; - } - - if (isFoldedByDefault) { - return isFoldedByDefault(item); - } - - return true; - }; - - const getEstimatedChildrenCount = (id: TId) => { - if (id === undefined) return undefined; - - const item = tree.getById(id); - if (item === NOT_FOUND_RECORD) return undefined; - - const childCount = getChildCount?.(item) ?? undefined; - if (childCount === undefined) return undefined; - - const nodeInfo = tree.getNodeInfo(id); - if (nodeInfo?.count !== undefined) { - // nodes are already loaded, and we know the actual count - return nodeInfo.count; - } - - return childCount; - }; - - const getMissingRecordsCount = (id: TId, totalRowsCount: number, loadedChildrenCount: number) => { - const nodeInfo = tree.getNodeInfo(id); - - const estimatedChildCount = getEstimatedChildrenCount(id); - - // Estimate how many more nodes there are at current level, to put 'loading' placeholders. - if (nodeInfo.count !== undefined) { - // Exact count known - return nodeInfo.count - loadedChildrenCount; - } - - // estimatedChildCount = undefined for top-level rows only. - if (id === undefined && totalRowsCount < lastIndex) { - return lastIndex - totalRowsCount; // let's put placeholders down to the bottom of visible list - } - - if (estimatedChildCount > loadedChildrenCount) { - // According to getChildCount (put into estimatedChildCount), there are more rows on this level - return estimatedChildCount - loadedChildrenCount; - } - - // We have a bad estimate - it even less that actual items we have - // This would happen is getChildCount provides a guess count, and we scroll thru children past this count - // let's guess we have at least 1 item more than loaded - return 1; - }; - - const handleOnSelect = (rowProps: DataRowProps) => { - setDataSourceState?.({ - ...dataSourceState, - selectedId: rowProps.id, - }); - }; - - const handleOnFocus = (focusIndex: number) => { - setDataSourceState({ - ...dataSourceState, - focusedIndex: focusIndex, - }); - }; - - const handleOnFold = (rowProps: DataRowProps) => { - if (setDataSourceState) { - const fold = !rowProps.isFolded; - const indexToScroll = rowProps.index - (rowProps.path?.length ?? 0); - const scrollTo: ScrollToConfig = fold && rowProps.isPinned - ? { index: indexToScroll, align: 'nearest' } - : dataSourceState.scrollTo; - - setDataSourceState({ - ...dataSourceState, - scrollTo, - folded: setObjectFlag(dataSourceState && dataSourceState.folded, rowProps.rowKey, fold), - }); - } - }; - const applyRowOptions = (row: DataRowProps) => { const externalRowOptions = (getRowOptions && !row.isLoading) ? getRowOptions(row.value, row.index) @@ -191,7 +65,7 @@ export function useDataRows( const estimatedChildrenCount = getEstimatedChildrenCount(row.id); row.isFoldable = false; - if (!isFlattenSearch && estimatedChildrenCount !== undefined && estimatedChildrenCount > 0) { + if (!isFlattenSearch && estimatedChildrenCount > 0) { row.isFoldable = true; } @@ -207,7 +81,7 @@ export function useDataRows( row.isSelected = dataSourceState.selectedId === row.id; row.isCheckable = isCheckable; row.onCheck = isCheckable && handleOnCheck; - row.onSelect = fullRowOptions && fullRowOptions.isSelectable && handleOnSelect; + row.onSelect = fullRowOptions?.isSelectable && handleOnSelect; row.onFocus = (isSelectable || isCheckable || row.isFoldable) && handleOnFocus; row.isChildrenChecked = isRowChildrenChecked(row); }; @@ -233,38 +107,6 @@ export function useDataRows( return rowProps; }; - const getRowStats = (row: DataRowProps, actualStats: NodeStats): NodeStats => { - let { - isSomeCheckable, isSomeChecked, isAllChecked, isSomeSelected, isSomeCheckboxEnabled, - } = actualStats; - - if (row.checkbox) { - isSomeCheckable = true; - if (row.isChecked || row.isChildrenChecked) { - isSomeChecked = true; - } - if (!row.checkbox.isDisabled || isSomeCheckboxEnabled) { - isSomeCheckboxEnabled = true; - } - - const isImplicitCascadeSelection = cascadeSelection === CascadeSelectionTypes.IMPLICIT; - if ( - (!row.isChecked && !row.checkbox.isDisabled && !isImplicitCascadeSelection) - || (row.parentId === undefined && !row.isChecked && isImplicitCascadeSelection) - ) { - isAllChecked = false; - } - } - - if (row.isSelected) { - isSomeSelected = true; - } - - return { - ...actualStats, isSomeCheckable, isSomeChecked, isAllChecked, isSomeSelected, isSomeCheckboxEnabled, - }; - }; - const getEmptyRowProps = (id: any, index: number = 0, path: DataRowPathItem[] = null): DataRowProps => { const checked = dataSourceState?.checked ?? []; const isChecked = checked.includes(id); @@ -280,7 +122,7 @@ export function useDataRows( }; }; - const getLoadingRow = (id: any, index: number = 0, path: DataRowPathItem[] = null): DataRowProps => { + const getLoadingRowProps = (id: any, index: number = 0, path: DataRowPathItem[] = null): DataRowProps => { const rowProps = getEmptyRowProps(id, index, path); return { ...rowProps, @@ -314,13 +156,13 @@ export function useDataRows( } const row = getRowProps(item, rows.length); - if (appendRows && (!isPartialLoad || (isPartialLoad && rows.length < lastIndex))) { + if (appendRows && (!isPartialLoad || (isPartialLoad && rows.length < lastRowIndex))) { rows.push(row); layerRows.push(row); currentLevelRows++; } - stats = getRowStats(row, stats); + stats = getRowStats(row, stats, cascadeSelection); row.isLastChild = n === ids.length - 1 && nodeInfo.count === ids.length; row.indent = isFlattenSearch ? 0 : row.path.length + 1; const estimatedChildrenCount = getEstimatedChildrenCount(id); @@ -341,8 +183,8 @@ export function useDataRows( } else if (!dataSourceState.search && !row.isFolded && appendRows) { // children are not loaded const parentsWithRow = [...row.path, tree.getPathItem(item)]; - for (let m = 0; m < estimatedChildrenCount && rows.length < lastIndex; m++) { - const loadingRow = getLoadingRow('_loading_' + rows.length, rows.length, parentsWithRow); + for (let m = 0; m < estimatedChildrenCount && rows.length < lastRowIndex; m++) { + const loadingRow = getLoadingRowProps('_loading_' + rows.length, rows.length, parentsWithRow); loadingRow.indent = parentsWithRow.length + 1; loadingRow.isLastChild = m === estimatedChildrenCount - 1; rows.push(loadingRow); @@ -372,9 +214,9 @@ export function useDataRows( stats.hasMoreRows = true; } - // Append loading rows, stop at lastIndex (last row visible) - while (rows.length < lastIndex && missingCount > 0) { - const row = getLoadingRow('_loading_' + rows.length, rows.length, path); + // Append loading rows, stop at lastRowIndex (last row visible) + while (rows.length < lastRowIndex && missingCount > 0) { + const row = getLoadingRowProps('_loading_' + rows.length, rows.length, path); rows.push(row); layerRows.push(row); currentLevelRows++; @@ -504,7 +346,7 @@ export function useDataRows( }; const getVisibleRows = () => { - const visibleRows = rows.slice(dataSourceState.topIndex, lastIndex); + const visibleRows = rows.slice(dataSourceState.topIndex, lastRowIndex); return getRowsWithPinned(visibleRows); }; diff --git a/uui-core/src/data/processing/views/helpers/convert.ts b/uui-core/src/data/processing/views/helpers/convert.ts new file mode 100644 index 0000000000..4296723918 --- /dev/null +++ b/uui-core/src/data/processing/views/helpers/convert.ts @@ -0,0 +1 @@ +export const idToKey = (id: TId) => typeof id === 'object' ? JSON.stringify(id) : `${id}`; diff --git a/uui-core/src/data/processing/views/helpers/index.ts b/uui-core/src/data/processing/views/helpers/index.ts new file mode 100644 index 0000000000..1df02f3a6b --- /dev/null +++ b/uui-core/src/data/processing/views/helpers/index.ts @@ -0,0 +1,2 @@ +export * from './convert'; +export * from './setters'; diff --git a/uui-core/src/data/processing/views/helpers/setters.ts b/uui-core/src/data/processing/views/helpers/setters.ts new file mode 100644 index 0000000000..a2e1a0c413 --- /dev/null +++ b/uui-core/src/data/processing/views/helpers/setters.ts @@ -0,0 +1,3 @@ +export const setObjectFlag = (object: any, key: string, value: boolean) => { + return { ...object, [key]: value }; +}; diff --git a/uui-core/src/data/processing/views/tree/hooks/helpers.ts b/uui-core/src/data/processing/views/tree/hooks/helpers.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/uui-core/src/data/processing/views/tree/hooks/services/index.ts b/uui-core/src/data/processing/views/tree/hooks/services/index.ts new file mode 100644 index 0000000000..7681a8f4bb --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/services/index.ts @@ -0,0 +1,9 @@ +export { useCheckingService } from './useCheckingService'; +export { useFoldingService } from './useFoldingService'; +export { useFocusService } from './useFocusService'; +export { useSelectingService } from './useSelectingService'; + +export type { UseCheckingServiceProps, CheckingService } from './useCheckingService'; +export type { UseFoldingServiceProps, FoldingService } from './useFoldingService'; +export type { UseFocusServiceProps, FocusService } from './useFocusService'; +export type { UseSelectingServiceProps, SelectingService } from './useSelectingService'; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCheckingService.ts b/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts similarity index 79% rename from uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCheckingService.ts rename to uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts index f787e69be8..f6f7cbfc20 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/useCheckingService.ts +++ b/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts @@ -1,7 +1,24 @@ import { useCallback, useMemo } from 'react'; -import { CascadeSelectionTypes, DataRowProps } from '../../../../../../../types'; -import { ITree, NOT_FOUND_RECORD } from '../../..'; -import { CheckingService, UseCheckingServiceProps } from '../types'; +import { CascadeSelection, CascadeSelectionTypes, DataRowOptions, DataRowProps } from '../../../../../../types'; +import { ITree, NOT_FOUND_RECORD } from '../..'; + +export interface UseCheckingServiceProps { + tree: ITree; + checked?: TId[]; + setChecked: (checked: TId[]) => void; + getParentId?: (item: TItem) => TId; + cascadeSelection?: CascadeSelection; + rowOptions?: DataRowOptions; + getRowOptions?(item: TItem, index?: number): DataRowOptions; +} + +export interface CheckingService { + isRowChecked: (row: DataRowProps) => boolean; + isRowChildrenChecked: (row: DataRowProps) => boolean; + handleOnCheck: (rowProps: DataRowProps) => void; + handleSelectAll: (isChecked: boolean) => void; + clearAllChecked: () => void; +} const idToKey = (id: TId) => typeof id === 'object' ? JSON.stringify(id) : `${id}`; @@ -36,7 +53,7 @@ const getCheckingInfo = (checked: TId[] = [], tree: ITree( +export function useCheckingService( { tree, getParentId, @@ -45,8 +62,8 @@ export function useCheckingService( cascadeSelection, getRowOptions, rowOptions, - }: UseCheckingServiceProps, -): CheckingService { + }: UseCheckingServiceProps, +): CheckingService { const checkingInfoById = useMemo( () => getCheckingInfo(checked, tree, getParentId), [tree, checked], diff --git a/uui-core/src/data/processing/views/tree/hooks/services/useFocusService.ts b/uui-core/src/data/processing/views/tree/hooks/services/useFocusService.ts new file mode 100644 index 0000000000..2c9de8b2bf --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/services/useFocusService.ts @@ -0,0 +1,28 @@ +import { useCallback, useMemo } from 'react'; +import { DataSourceState } from '../../../../../../types'; + +export interface UseFocusServiceProps { + dataSourceState: DataSourceState, + setDataSourceState?: (dataSourceState: DataSourceState) => void; +} + +export interface FocusService { + handleOnFocus: (focusedIndex: number) => void; +} + +export function useFocusService({ + dataSourceState, + setDataSourceState, +}: UseFocusServiceProps): FocusService { + const handleOnFocus = useCallback((focusIndex: number) => { + setDataSourceState({ + ...dataSourceState, + focusedIndex: focusIndex, + }); + }, [dataSourceState, setDataSourceState]); + + return useMemo( + () => ({ handleOnFocus }), + [handleOnFocus], + ); +} diff --git a/uui-core/src/data/processing/views/tree/hooks/services/useFoldingService.ts b/uui-core/src/data/processing/views/tree/hooks/services/useFoldingService.ts new file mode 100644 index 0000000000..c5f12a35c9 --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/services/useFoldingService.ts @@ -0,0 +1,62 @@ +import { useCallback, useMemo } from 'react'; +import { DataRowProps, DataSourceState, ScrollToConfig } from '../../../../../../types'; +import { idToKey, setObjectFlag } from '../../../helpers'; + +export interface UseFoldingServiceProps { + getId: (item: TItem) => TId; + dataSourceState: DataSourceState, + setDataSourceState?: (dataSourceState: DataSourceState) => void; + isFoldedByDefault?(item: TItem): boolean; +} + +export interface FoldingService { + handleOnFold: (rowProps: DataRowProps) => void; + isFolded: (item: TItem) => boolean +} + +export function useFoldingService({ + dataSourceState, + setDataSourceState, + isFoldedByDefault, + getId, +}: UseFoldingServiceProps): FoldingService { + const isFolded = useCallback((item: TItem) => { + const searchIsApplied = !!dataSourceState?.search; + if (searchIsApplied) { + return false; + } + + const folded = dataSourceState.folded || {}; + const key = idToKey(getId(item)); + if (folded[key] != null) { + return folded[key]; + } + + if (isFoldedByDefault) { + return isFoldedByDefault(item); + } + + return true; + }, [isFoldedByDefault, dataSourceState?.search, dataSourceState.folded]); + + const handleOnFold = useCallback((rowProps: DataRowProps) => { + if (setDataSourceState) { + const fold = !rowProps.isFolded; + const indexToScroll = rowProps.index - (rowProps.path?.length ?? 0); + const scrollTo: ScrollToConfig = fold && rowProps.isPinned + ? { index: indexToScroll, align: 'nearest' } + : dataSourceState.scrollTo; + + setDataSourceState({ + ...dataSourceState, + scrollTo, + folded: setObjectFlag(dataSourceState && dataSourceState.folded, rowProps.rowKey, fold), + }); + } + }, [setDataSourceState, dataSourceState]); + + return useMemo( + () => ({ handleOnFold, isFolded }), + [handleOnFold, isFolded], + ); +} diff --git a/uui-core/src/data/processing/views/tree/hooks/services/useSelectingService.ts b/uui-core/src/data/processing/views/tree/hooks/services/useSelectingService.ts new file mode 100644 index 0000000000..113eee5b41 --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/services/useSelectingService.ts @@ -0,0 +1,28 @@ +import { useCallback, useMemo } from 'react'; +import { DataRowProps, DataSourceState } from '../../../../../../types'; + +export interface UseSelectingServiceProps { + dataSourceState: DataSourceState, + setDataSourceState?: (dataSourceState: DataSourceState) => void; +} + +export interface SelectingService { + handleOnSelect: (rowProps: DataRowProps) => void; +} + +export function useSelectingService({ + dataSourceState, + setDataSourceState, +}: UseSelectingServiceProps): SelectingService { + const handleOnSelect = useCallback((rowProps: DataRowProps) => { + setDataSourceState?.({ + ...dataSourceState, + selectedId: rowProps.id, + }); + }, [dataSourceState, setDataSourceState]); + + return useMemo( + () => ({ handleOnSelect }), + [handleOnSelect], + ); +} diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts index 3790360f7b..95b07bd371 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts @@ -12,4 +12,5 @@ export type PlainTreeStrategyProps = TreeStrategyProps boolean; sortSearchByRelevance?: boolean; cascadeSelection?: CascadeSelection; + isFoldedByDefault?(item: TItem): boolean; }; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts index 7eaf8dc20c..2934aab395 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts @@ -1,10 +1,12 @@ import { useCallback, useMemo } from 'react'; import { PlainTreeStrategyProps } from './types'; -import { useCheckingService } from './useCheckingService'; +import { useCheckingService, useFocusService } from '../../services'; import { useCreateTree } from './useCreateTree'; import { useFilterTree } from './useFilterTree'; import { useSearchTree } from './useSearchTree'; import { useSortTree } from './useSortTree'; +import { useFoldingService } from '../../services/useFoldingService'; +import { NOT_FOUND_RECORD } from '../../../ITree'; export function usePlainTreeStrategy( { sortSearchByRelevance = true, ...restProps }: PlainTreeStrategyProps, @@ -14,6 +16,7 @@ export function usePlainTreeStrategy( const fullTree = useCreateTree(props, deps); const { + getId, dataSourceState, setDataSourceState, getFilter, @@ -23,6 +26,8 @@ export function usePlainTreeStrategy( getParentId, rowOptions, getRowOptions, + isFoldedByDefault, + getChildCount, } = props; const filteredTree = useFilterTree( @@ -54,8 +59,91 @@ export function usePlainTreeStrategy( getParentId, }); + const foldingService = useFoldingService({ + dataSourceState, setDataSourceState, isFoldedByDefault, getId, + }); + + const focusService = useFocusService({ + dataSourceState, setDataSourceState, + }); + + const getEstimatedChildrenCount = useCallback((id: TId) => { + if (id === undefined) return undefined; + + const item = tree.getById(id); + if (item === NOT_FOUND_RECORD) return undefined; + + const childCount = getChildCount?.(item) ?? undefined; + if (childCount === undefined) return undefined; + + const nodeInfo = tree.getNodeInfo(id); + if (nodeInfo?.count !== undefined) { + // nodes are already loaded, and we know the actual count + return nodeInfo.count; + } + + return childCount; + }, [getChildCount, tree]); + + const lastRowIndex = useMemo( + () => { + const currentLastIndex = dataSourceState.topIndex + dataSourceState.visibleCount; + const actualCount = tree.getTotalRecursiveCount() ?? 0; + + if (actualCount < currentLastIndex) return actualCount; + return currentLastIndex; + }, + [tree, dataSourceState.topIndex, dataSourceState.visibleCount], + ); + + const getMissingRecordsCount = useCallback((id: TId, totalRowsCount: number, loadedChildrenCount: number) => { + const nodeInfo = tree.getNodeInfo(id); + + const estimatedChildCount = getEstimatedChildrenCount(id); + + // Estimate how many more nodes there are at current level, to put 'loading' placeholders. + if (nodeInfo.count !== undefined) { + // Exact count known + return nodeInfo.count - loadedChildrenCount; + } + + // estimatedChildCount = undefined for top-level rows only. + if (id === undefined && totalRowsCount < lastRowIndex) { + return lastRowIndex - totalRowsCount; // let's put placeholders down to the bottom of visible list + } + + if (estimatedChildCount > loadedChildrenCount) { + // According to getChildCount (put into estimatedChildCount), there are more rows on this level + return estimatedChildCount - loadedChildrenCount; + } + + // We have a bad estimate - it even less that actual items we have + // This would happen is getChildCount provides a guess count, and we scroll thru children past this count + // let's guess we have at least 1 item more than loaded + return 1; + }, [lastRowIndex, tree, getEstimatedChildrenCount]); + return useMemo( - () => ({ tree, ...checkingService, rowOptions, getRowOptions }), - [tree, checkingService], + () => ({ + tree, + ...checkingService, + ...foldingService, + ...focusService, + rowOptions, + getRowOptions, + getEstimatedChildrenCount, + getMissingRecordsCount, + lastRowIndex, + }), + [ + tree, + checkingService, + foldingService, + rowOptions, + getRowOptions, + getEstimatedChildrenCount, + getMissingRecordsCount, + lastRowIndex, + ], ); } diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts index ce9bf180d1..bc5cc2c554 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts @@ -1,5 +1,4 @@ -import { CascadeSelection, DataRowOptions, DataSourceState } from '../../../../../../types'; -import { ITree } from '../../ITree'; +import { DataRowOptions, DataSourceState } from '../../../../../../types'; import { STRATEGIES } from './constants'; export type Strategies = typeof STRATEGIES[keyof typeof STRATEGIES]; @@ -12,18 +11,5 @@ export type TreeStrategyProps = { complexIds?: boolean; rowOptions?: DataRowOptions; getRowOptions?(item: TItem, index?: number): DataRowOptions; + getChildCount?(item: TItem): number; }; - -export interface UseCheckingServiceProps { - tree: ITree; - checked?: TId[]; - setChecked: (checked: TId[]) => void; - getParentId?: (item: TItem) => TId; - cascadeSelection?: CascadeSelection; - rowOptions?: DataRowOptions; - getRowOptions?(item: TItem, index?: number): DataRowOptions; -} - -export interface CheckingService { - -} From 1692b669d21385a10c94f396fcb214ed7c676da1 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Mon, 11 Dec 2023 18:13:03 +0200 Subject: [PATCH 008/278] [useTree]: moved getRowProps and etc to useDataRowProps. --- .../views/dataRows/useDataRowProps.ts | 128 ++++++++++++++++++ .../processing/views/dataRows/useDataRows.ts | 100 +++----------- 2 files changed, 149 insertions(+), 79 deletions(-) create mode 100644 uui-core/src/data/processing/views/dataRows/useDataRowProps.ts diff --git a/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts b/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts new file mode 100644 index 0000000000..38fa1e663e --- /dev/null +++ b/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts @@ -0,0 +1,128 @@ +import { DataRowOptions, DataRowPathItem, DataRowProps, DataSourceState } from '../../../../types'; +import { CheckingService, FocusService, FoldingService, SelectingService } from '../tree/hooks/services'; +import { ITree } from '../tree'; +import { idToKey } from '../helpers'; +import { useCallback, useMemo } from 'react'; + +export interface UseDataRowPropsProps extends FoldingService, + Omit, 'clearAllChecked'>, + FocusService, + SelectingService { + + getId: (item: TItem) => TId; + tree: ITree; + dataSourceState: DataSourceState; + rowOptions?: DataRowOptions; + getRowOptions?(item: TItem, index?: number): DataRowOptions; + isFlattenSearch: boolean; + getEstimatedChildrenCount: (id: TId) => number; +} + +export function useDataRowProps( + { + tree, + getId, + dataSourceState, + getRowOptions, + rowOptions, + handleOnCheck, + handleOnSelect, + handleOnFocus, + isRowChecked, + isRowChildrenChecked, + isFlattenSearch, + getEstimatedChildrenCount, + }: UseDataRowPropsProps, +) { + const applyRowOptions = useCallback((row: DataRowProps) => { + const externalRowOptions = (getRowOptions && !row.isLoading) + ? getRowOptions(row.value, row.index) + : {}; + + const fullRowOptions = { ...rowOptions, ...externalRowOptions }; + + const estimatedChildrenCount = getEstimatedChildrenCount(row.id); + + row.isFoldable = false; + if (!isFlattenSearch && estimatedChildrenCount > 0) { + row.isFoldable = true; + } + + const isCheckable = fullRowOptions && fullRowOptions.checkbox && fullRowOptions.checkbox.isVisible && !fullRowOptions.checkbox.isDisabled; + const isSelectable = fullRowOptions && fullRowOptions.isSelectable; + if (fullRowOptions != null) { + const rowValue = row.value; + Object.assign(row, fullRowOptions); + row.value = fullRowOptions.value ?? rowValue; + } + row.isFocused = dataSourceState.focusedIndex === row.index; + row.isChecked = isRowChecked(row); + row.isSelected = dataSourceState.selectedId === row.id; + row.isCheckable = isCheckable; + row.onCheck = isCheckable && handleOnCheck; + row.onSelect = fullRowOptions?.isSelectable && handleOnSelect; + row.onFocus = (isSelectable || isCheckable || row.isFoldable) && handleOnFocus; + row.isChildrenChecked = isRowChildrenChecked(row); + }, [ + getRowOptions, + rowOptions, + getEstimatedChildrenCount, + dataSourceState.focusedIndex, + dataSourceState.selectedId, + isRowChecked, + isRowChildrenChecked, + handleOnCheck, + handleOnSelect, + handleOnFocus, + ]); + + const getRowProps = useCallback((item: TItem, index: number): DataRowProps => { + const id = getId(item); + const key = idToKey(id); + const path = tree.getPathById(id); + const parentId = path.length > 0 ? path[path.length - 1].id : undefined; + const rowProps = { + id, + parentId, + key, + rowKey: key, + index, + value: item, + depth: path.length, + path, + } as DataRowProps; + + applyRowOptions(rowProps); + + return rowProps; + }, [getId, tree, applyRowOptions]); + + const getEmptyRowProps = useCallback((id: any, index: number = 0, path: DataRowPathItem[] = null): DataRowProps => { + const checked = dataSourceState?.checked ?? []; + const isChecked = checked.includes(id); + return { + id, + rowKey: idToKey(id), + value: undefined, + index, + depth: path ? path.length : 0, + path: path ?? [], + checkbox: rowOptions?.checkbox?.isVisible && { isVisible: true, isDisabled: true }, + isChecked, + }; + }, [dataSourceState?.checked, rowOptions]); + + const getLoadingRowProps = useCallback((id: any, index: number = 0, path: DataRowPathItem[] = null): DataRowProps => { + const rowProps = getEmptyRowProps(id, index, path); + return { + ...rowProps, + checkbox: { ...rowProps.checkbox, isDisabled: true }, + isLoading: true, + }; + }, [getEmptyRowProps]); + + return useMemo( + () => ({ getRowProps, getEmptyRowProps, getLoadingRowProps }), + [getRowProps, getEmptyRowProps, getLoadingRowProps], + ); +} diff --git a/uui-core/src/data/processing/views/dataRows/useDataRows.ts b/uui-core/src/data/processing/views/dataRows/useDataRows.ts index caa0743244..79971dd510 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRows.ts @@ -4,6 +4,7 @@ import { CascadeSelection, DataRowOptions, DataRowPathItem, DataRowProps, DataSo import { idToKey } from '../helpers'; import { FoldingService, CheckingService, FocusService, SelectingService } from '../tree/hooks/services'; import { NodeStats, getDefaultNodeStats, getRowStats, mergeStats } from './stats'; +import { useDataRowProps } from './useDataRowProps'; export interface UseDataRowsProps extends FoldingService, CheckingService, FocusService, SelectingService { tree: ITree; @@ -12,14 +13,13 @@ export interface UseDataRowsProps extends FoldingServ isPartialLoad?: boolean; rowOptions?: DataRowOptions; getRowOptions?(item: TItem, index?: number): DataRowOptions; - isRowChildrenChecked: (row: DataRowProps) => boolean; - isRowChecked: (row: DataRowProps) => boolean; + getChildCount?(item: TItem): number; getId: (item: TItem) => TId; cascadeSelection?: CascadeSelection; - handleOnCheck: (rowProps: DataRowProps) => void; + selectAll?: boolean; - handleSelectAll: (isChecked: boolean) => void; + getEstimatedChildrenCount: (id: TId) => number; getMissingRecordsCount: (id: TId, totalRowsCount: number, loadedChildrenCount: number) => number; lastRowIndex: number; @@ -55,81 +55,23 @@ export function useDataRows( const isFlattenSearch = useMemo(() => dataSourceState.search && flattenSearchResults, []); - const applyRowOptions = (row: DataRowProps) => { - const externalRowOptions = (getRowOptions && !row.isLoading) - ? getRowOptions(row.value, row.index) - : {}; - - const fullRowOptions = { ...rowOptions, ...externalRowOptions }; - - const estimatedChildrenCount = getEstimatedChildrenCount(row.id); - - row.isFoldable = false; - if (!isFlattenSearch && estimatedChildrenCount > 0) { - row.isFoldable = true; - } - - const isCheckable = fullRowOptions && fullRowOptions.checkbox && fullRowOptions.checkbox.isVisible && !fullRowOptions.checkbox.isDisabled; - const isSelectable = fullRowOptions && fullRowOptions.isSelectable; - if (fullRowOptions != null) { - const rowValue = row.value; - Object.assign(row, fullRowOptions); - row.value = fullRowOptions.value ?? rowValue; - } - row.isFocused = dataSourceState.focusedIndex === row.index; - row.isChecked = isRowChecked(row); - row.isSelected = dataSourceState.selectedId === row.id; - row.isCheckable = isCheckable; - row.onCheck = isCheckable && handleOnCheck; - row.onSelect = fullRowOptions?.isSelectable && handleOnSelect; - row.onFocus = (isSelectable || isCheckable || row.isFoldable) && handleOnFocus; - row.isChildrenChecked = isRowChildrenChecked(row); - }; - - const getRowProps = (item: TItem, index: number): DataRowProps => { - const id = getId(item); - const key = idToKey(id); - const path = tree.getPathById(id); - const parentId = path.length > 0 ? path[path.length - 1].id : undefined; - const rowProps = { - id, - parentId, - key, - rowKey: key, - index, - value: item, - depth: path.length, - path, - } as DataRowProps; - - applyRowOptions(rowProps); - - return rowProps; - }; - - const getEmptyRowProps = (id: any, index: number = 0, path: DataRowPathItem[] = null): DataRowProps => { - const checked = dataSourceState?.checked ?? []; - const isChecked = checked.includes(id); - return { - id, - rowKey: idToKey(id), - value: undefined, - index, - depth: path ? path.length : 0, - path: path ?? [], - checkbox: rowOptions?.checkbox?.isVisible && { isVisible: true, isDisabled: true }, - isChecked, - }; - }; - - const getLoadingRowProps = (id: any, index: number = 0, path: DataRowPathItem[] = null): DataRowProps => { - const rowProps = getEmptyRowProps(id, index, path); - return { - ...rowProps, - checkbox: { ...rowProps.checkbox, isDisabled: true }, - isLoading: true, - }; - }; + const { getRowProps, getLoadingRowProps, getEmptyRowProps } = useDataRowProps({ + tree, + getId, + dataSourceState, + getRowOptions, + rowOptions, + handleOnCheck, + handleOnSelect, + handleOnFocus, + handleSelectAll, + handleOnFold, + isRowChecked, + isRowChildrenChecked, + isFlattenSearch, + getEstimatedChildrenCount, + isFolded, + }); const rebuildRows = () => { const rows: DataRowProps[] = []; From 7dee0982198fb9f82ba900d779d35fe6ec1b8184 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Mon, 11 Dec 2023 18:42:52 +0200 Subject: [PATCH 009/278] [useTree]: rebuildRows and getRowProps to separate files. --- .../processing/views/dataRows/useBuildRows.ts | 154 ++++++++++++++++++ .../views/dataRows/useDataRowProps.ts | 24 ++- .../processing/views/dataRows/useDataRows.ts | 152 ++--------------- 3 files changed, 190 insertions(+), 140 deletions(-) create mode 100644 uui-core/src/data/processing/views/dataRows/useBuildRows.ts diff --git a/uui-core/src/data/processing/views/dataRows/useBuildRows.ts b/uui-core/src/data/processing/views/dataRows/useBuildRows.ts new file mode 100644 index 0000000000..abf11ec620 --- /dev/null +++ b/uui-core/src/data/processing/views/dataRows/useBuildRows.ts @@ -0,0 +1,154 @@ +import { useMemo } from 'react'; +import { ITree, NOT_FOUND_RECORD } from '../tree'; +import { CascadeSelection, DataRowOptions, DataRowPathItem, DataRowProps, DataSourceState } from '../../../../types'; +import { idToKey } from '../helpers'; +import { FoldingService } from '../tree/hooks/services'; +import { NodeStats, getDefaultNodeStats, getRowStats, mergeStats } from './stats'; + +export interface UseBuildRowsProps extends FoldingService { + + tree: ITree; + dataSourceState: DataSourceState; + isPartialLoad?: boolean; + getEstimatedChildrenCount: (id: TId) => number; + getMissingRecordsCount: (id: TId, totalRowsCount: number, loadedChildrenCount: number) => number; + cascadeSelection?: CascadeSelection; + lastRowIndex: number; + + rowOptions?: DataRowOptions; + getRowOptions?(item: TItem, index?: number): DataRowOptions; + + isFlattenSearch?: boolean; + getRowProps: (item: TItem, index: number) => DataRowProps; + getLoadingRowProps: (id: any, index?: number, path?: DataRowPathItem[]) => DataRowProps; +} + +export function useBuildRows({ + tree, + dataSourceState, + isPartialLoad, + getEstimatedChildrenCount, + getMissingRecordsCount, + cascadeSelection, + lastRowIndex, + isFolded, + handleOnFold, + isFlattenSearch, + getRowProps, + getLoadingRowProps, +}: UseBuildRowsProps) { + const rebuildRows = () => { + const rows: DataRowProps[] = []; + const pinned: Record = {}; + const pinnedByParentId: Record = {}; + + const iterateNode = ( + parentId: TId, + appendRows: boolean, // Will be false, if we are iterating folded nodes. + ): NodeStats => { + let currentLevelRows = 0; + let stats = getDefaultNodeStats(); + + const layerRows: DataRowProps[] = []; + const nodeInfo = tree.getNodeInfo(parentId); + + const ids = tree.getChildrenIdsByParentId(parentId); + + for (let n = 0; n < ids.length; n++) { + const id = ids[n]; + const item = tree.getById(id); + if (item === NOT_FOUND_RECORD) { + continue; + } + + const row = getRowProps(item, rows.length); + if (appendRows && (!isPartialLoad || (isPartialLoad && rows.length < lastRowIndex))) { + rows.push(row); + layerRows.push(row); + currentLevelRows++; + } + + stats = getRowStats(row, stats, cascadeSelection); + row.isLastChild = n === ids.length - 1 && nodeInfo.count === ids.length; + row.indent = isFlattenSearch ? 0 : row.path.length + 1; + const estimatedChildrenCount = getEstimatedChildrenCount(id); + if (!isFlattenSearch && estimatedChildrenCount !== undefined) { + const childrenIds = tree.getChildrenIdsByParentId(id); + + if (estimatedChildrenCount > 0) { + row.isFolded = isFolded(item); + row.onFold = row.isFoldable && handleOnFold; + + if (childrenIds.length > 0) { + // some children are loaded + const childStats = iterateNode(id, appendRows && !row.isFolded); + row.isChildrenChecked = row.isChildrenChecked || childStats.isSomeChecked; + row.isChildrenSelected = childStats.isSomeSelected; + stats = mergeStats(stats, childStats); + // while searching and no children in visible tree, no need to append placeholders. + } else if (!dataSourceState.search && !row.isFolded && appendRows) { + // children are not loaded + const parentsWithRow = [...row.path, tree.getPathItem(item)]; + for (let m = 0; m < estimatedChildrenCount && rows.length < lastRowIndex; m++) { + const loadingRow = getLoadingRowProps('_loading_' + rows.length, rows.length, parentsWithRow); + loadingRow.indent = parentsWithRow.length + 1; + loadingRow.isLastChild = m === estimatedChildrenCount - 1; + rows.push(loadingRow); + currentLevelRows++; + } + } + } + } + + row.isPinned = row.pin?.(row) ?? false; + if (row.isPinned) { + pinned[idToKey(row.id)] = row.index; + if (!pinnedByParentId[idToKey(row.parentId)]) { + pinnedByParentId[idToKey(row.parentId)] = []; + } + pinnedByParentId[idToKey(row.parentId)]?.push(row.index); + } + } + + const pathToParent = tree.getPathById(parentId); + const parent = tree.getById(parentId); + const parentPathItem = parent !== NOT_FOUND_RECORD ? [tree.getPathItem(parent)] : []; + const path = parentId ? [...pathToParent, ...parentPathItem] : pathToParent; + if (appendRows) { + let missingCount = getMissingRecordsCount(parentId, rows.length, currentLevelRows); + if (missingCount > 0) { + stats.hasMoreRows = true; + } + + // Append loading rows, stop at lastRowIndex (last row visible) + while (rows.length < lastRowIndex && missingCount > 0) { + const row = getLoadingRowProps('_loading_' + rows.length, rows.length, path); + rows.push(row); + layerRows.push(row); + currentLevelRows++; + missingCount--; + } + } + + const isListFlat = path.length === 0 && !layerRows.some((r) => r.isFoldable); + if (isListFlat || isFlattenSearch) { + layerRows.forEach((r) => { + r.indent = 0; + }); + } + + return stats; + }; + + const rootStats = iterateNode(undefined, true); + + return { + rows, + pinned, + pinnedByParentId, + stats: rootStats, + }; + }; + + return useMemo(() => rebuildRows(), [tree]); +} diff --git a/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts b/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts index 38fa1e663e..c7a4d6ff5d 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts @@ -1,11 +1,10 @@ +import { useCallback, useMemo } from 'react'; import { DataRowOptions, DataRowPathItem, DataRowProps, DataSourceState } from '../../../../types'; -import { CheckingService, FocusService, FoldingService, SelectingService } from '../tree/hooks/services'; +import { CheckingService, FocusService, SelectingService } from '../tree/hooks/services'; import { ITree } from '../tree'; import { idToKey } from '../helpers'; -import { useCallback, useMemo } from 'react'; -export interface UseDataRowPropsProps extends FoldingService, - Omit, 'clearAllChecked'>, +export interface UseDataRowPropsProps extends Omit, 'clearAllChecked' | 'handleSelectAll'>, FocusService, SelectingService { @@ -121,8 +120,21 @@ export function useDataRowProps( }; }, [getEmptyRowProps]); + const getUnknownRowProps = useCallback((id: any, index: number = 0, path: DataRowPathItem[] = null): DataRowProps => { + const emptyRowProps = getEmptyRowProps(id, index, path); + const checkbox = (rowOptions?.checkbox?.isVisible || emptyRowProps.isChecked) + ? { isVisible: true, isDisabled: false } + : undefined; + + return { + ...emptyRowProps, + checkbox, + isUnknown: true, + }; + }, [getEmptyRowProps, rowOptions]); + return useMemo( - () => ({ getRowProps, getEmptyRowProps, getLoadingRowProps }), - [getRowProps, getEmptyRowProps, getLoadingRowProps], + () => ({ getRowProps, getEmptyRowProps, getLoadingRowProps, getUnknownRowProps }), + [getRowProps, getEmptyRowProps, getLoadingRowProps, getUnknownRowProps], ); } diff --git a/uui-core/src/data/processing/views/dataRows/useDataRows.ts b/uui-core/src/data/processing/views/dataRows/useDataRows.ts index 79971dd510..47c7257886 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRows.ts @@ -1,10 +1,10 @@ import { useCallback, useMemo } from 'react'; import { ITree, NOT_FOUND_RECORD } from '../tree'; -import { CascadeSelection, DataRowOptions, DataRowPathItem, DataRowProps, DataSourceListProps, DataSourceState, VirtualListRange } from '../../../../types'; +import { CascadeSelection, DataRowOptions, DataRowProps, DataSourceListProps, DataSourceState, VirtualListRange } from '../../../../types'; import { idToKey } from '../helpers'; import { FoldingService, CheckingService, FocusService, SelectingService } from '../tree/hooks/services'; -import { NodeStats, getDefaultNodeStats, getRowStats, mergeStats } from './stats'; import { useDataRowProps } from './useDataRowProps'; +import { useBuildRows } from './useBuildRows'; export interface UseDataRowsProps extends FoldingService, CheckingService, FocusService, SelectingService { tree: ITree; @@ -55,7 +55,7 @@ export function useDataRows( const isFlattenSearch = useMemo(() => dataSourceState.search && flattenSearchResults, []); - const { getRowProps, getLoadingRowProps, getEmptyRowProps } = useDataRowProps({ + const { getRowProps, getUnknownRowProps, getLoadingRowProps } = useDataRowProps({ tree, getId, dataSourceState, @@ -64,129 +64,26 @@ export function useDataRows( handleOnCheck, handleOnSelect, handleOnFocus, - handleSelectAll, - handleOnFold, isRowChecked, isRowChildrenChecked, isFlattenSearch, getEstimatedChildrenCount, - isFolded, }); - const rebuildRows = () => { - const rows: DataRowProps[] = []; - const pinned: Record = {}; - const pinnedByParentId: Record = {}; - - const iterateNode = ( - parentId: TId, - appendRows: boolean, // Will be false, if we are iterating folded nodes. - ): NodeStats => { - let currentLevelRows = 0; - let stats = getDefaultNodeStats(); - - const layerRows: DataRowProps[] = []; - const nodeInfo = tree.getNodeInfo(parentId); - - const ids = tree.getChildrenIdsByParentId(parentId); - - for (let n = 0; n < ids.length; n++) { - const id = ids[n]; - const item = tree.getById(id); - if (item === NOT_FOUND_RECORD) { - continue; - } - - const row = getRowProps(item, rows.length); - if (appendRows && (!isPartialLoad || (isPartialLoad && rows.length < lastRowIndex))) { - rows.push(row); - layerRows.push(row); - currentLevelRows++; - } - - stats = getRowStats(row, stats, cascadeSelection); - row.isLastChild = n === ids.length - 1 && nodeInfo.count === ids.length; - row.indent = isFlattenSearch ? 0 : row.path.length + 1; - const estimatedChildrenCount = getEstimatedChildrenCount(id); - if (!isFlattenSearch && estimatedChildrenCount !== undefined) { - const childrenIds = tree.getChildrenIdsByParentId(id); - - if (estimatedChildrenCount > 0) { - row.isFolded = isFolded(item); - row.onFold = row.isFoldable && handleOnFold; - - if (childrenIds.length > 0) { - // some children are loaded - const childStats = iterateNode(id, appendRows && !row.isFolded); - row.isChildrenChecked = row.isChildrenChecked || childStats.isSomeChecked; - row.isChildrenSelected = childStats.isSomeSelected; - stats = mergeStats(stats, childStats); - // while searching and no children in visible tree, no need to append placeholders. - } else if (!dataSourceState.search && !row.isFolded && appendRows) { - // children are not loaded - const parentsWithRow = [...row.path, tree.getPathItem(item)]; - for (let m = 0; m < estimatedChildrenCount && rows.length < lastRowIndex; m++) { - const loadingRow = getLoadingRowProps('_loading_' + rows.length, rows.length, parentsWithRow); - loadingRow.indent = parentsWithRow.length + 1; - loadingRow.isLastChild = m === estimatedChildrenCount - 1; - rows.push(loadingRow); - currentLevelRows++; - } - } - } - } - - row.isPinned = row.pin?.(row) ?? false; - if (row.isPinned) { - pinned[idToKey(row.id)] = row.index; - if (!pinnedByParentId[idToKey(row.parentId)]) { - pinnedByParentId[idToKey(row.parentId)] = []; - } - pinnedByParentId[idToKey(row.parentId)]?.push(row.index); - } - } - - const pathToParent = tree.getPathById(parentId); - const parent = tree.getById(parentId); - const parentPathItem = parent !== NOT_FOUND_RECORD ? [tree.getPathItem(parent)] : []; - const path = parentId ? [...pathToParent, ...parentPathItem] : pathToParent; - if (appendRows) { - let missingCount = getMissingRecordsCount(parentId, rows.length, currentLevelRows); - if (missingCount > 0) { - stats.hasMoreRows = true; - } - - // Append loading rows, stop at lastRowIndex (last row visible) - while (rows.length < lastRowIndex && missingCount > 0) { - const row = getLoadingRowProps('_loading_' + rows.length, rows.length, path); - rows.push(row); - layerRows.push(row); - currentLevelRows++; - missingCount--; - } - } - - const isListFlat = path.length === 0 && !layerRows.some((r) => r.isFoldable); - if (isListFlat || isFlattenSearch) { - layerRows.forEach((r) => { - r.indent = 0; - }); - } - - return stats; - }; - - const rootStats = iterateNode(undefined, true); - - return { - rows, - pinned, - pinnedByParentId, - stats: rootStats, - }; - }; - - const { rows, pinned, pinnedByParentId, stats } = useMemo(() => rebuildRows(), []); + const { rows, pinned, pinnedByParentId, stats } = useBuildRows({ + tree, + dataSourceState, + cascadeSelection, + isPartialLoad, + isFlattenSearch, + lastRowIndex, + getEstimatedChildrenCount, + getMissingRecordsCount, + isFolded, + handleOnFold, + getRowProps, + getLoadingRowProps, + }); const isSelectAllEnabled = useMemo(() => props.selectAll === undefined ? true : props.selectAll, [props.selectAll]); @@ -292,24 +189,11 @@ export function useDataRows( return getRowsWithPinned(visibleRows); }; - const getUnknownRow = (id: any, index: number = 0, path: DataRowPathItem[] = null): DataRowProps => { - const emptyRowProps = getEmptyRowProps(id, index, path); - const checkbox = (rowOptions?.checkbox?.isVisible || emptyRowProps.isChecked) - ? { isVisible: true, isDisabled: false } - : undefined; - - return { - ...emptyRowProps, - checkbox, - isUnknown: true, - }; - }; - const getById = (id: TId, index: number) => { // if originalTree is not created, but blank tree is defined, get item from it const item = tree.getById(id); if (item === NOT_FOUND_RECORD) { - return getUnknownRow(id, index, []); + return getUnknownRowProps(id, index, []); } return getRowProps(item, index); From 9adfad3fe08e26d77443cb701242ee1e57b43598 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Tue, 12 Dec 2023 10:13:52 +0200 Subject: [PATCH 010/278] [useTree]: added useSelectAll hook. --- .../processing/views/dataRows/useBuildRows.ts | 5 +-- .../processing/views/dataRows/useDataRows.ts | 28 ++++-------- .../processing/views/dataRows/useSelectAll.ts | 44 +++++++++++++++++++ 3 files changed, 54 insertions(+), 23 deletions(-) create mode 100644 uui-core/src/data/processing/views/dataRows/useSelectAll.ts diff --git a/uui-core/src/data/processing/views/dataRows/useBuildRows.ts b/uui-core/src/data/processing/views/dataRows/useBuildRows.ts index abf11ec620..d1788b8166 100644 --- a/uui-core/src/data/processing/views/dataRows/useBuildRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useBuildRows.ts @@ -6,7 +6,6 @@ import { FoldingService } from '../tree/hooks/services'; import { NodeStats, getDefaultNodeStats, getRowStats, mergeStats } from './stats'; export interface UseBuildRowsProps extends FoldingService { - tree: ITree; dataSourceState: DataSourceState; isPartialLoad?: boolean; @@ -37,7 +36,7 @@ export function useBuildRows({ getRowProps, getLoadingRowProps, }: UseBuildRowsProps) { - const rebuildRows = () => { + const buildRows = () => { const rows: DataRowProps[] = []; const pinned: Record = {}; const pinnedByParentId: Record = {}; @@ -150,5 +149,5 @@ export function useBuildRows({ }; }; - return useMemo(() => rebuildRows(), [tree]); + return useMemo(() => buildRows(), [tree]); } diff --git a/uui-core/src/data/processing/views/dataRows/useDataRows.ts b/uui-core/src/data/processing/views/dataRows/useDataRows.ts index 47c7257886..19dba65cfc 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRows.ts @@ -5,6 +5,7 @@ import { idToKey } from '../helpers'; import { FoldingService, CheckingService, FocusService, SelectingService } from '../tree/hooks/services'; import { useDataRowProps } from './useDataRowProps'; import { useBuildRows } from './useBuildRows'; +import { useSelectAll } from './useSelectAll'; export interface UseDataRowsProps extends FoldingService, CheckingService, FocusService, SelectingService { tree: ITree; @@ -85,26 +86,13 @@ export function useDataRows( getLoadingRowProps, }); - const isSelectAllEnabled = useMemo(() => props.selectAll === undefined ? true : props.selectAll, [props.selectAll]); - - const selectAll = useMemo(() => { - if (stats.isSomeCheckable && isSelectAllEnabled) { - return { - value: stats.isSomeCheckboxEnabled ? stats.isAllChecked : false, - onValueChange: handleSelectAll, - indeterminate: dataSourceState.checked && dataSourceState.checked.length > 0 && !stats.isAllChecked, - }; - } else if (tree.getRootIds().length === 0 && rowOptions?.checkbox?.isVisible && isSelectAllEnabled) { - // Nothing loaded yet, but we guess that something is checkable. Add disabled checkbox for less flicker. - return { - value: false, - onValueChange: () => {}, - isDisabled: true, - indeterminate: dataSourceState.checked?.length > 0, - }; - } - return null; - }, [tree, rowOptions, dataSourceState.checked, stats, isSelectAllEnabled, handleSelectAll]); + const selectAll = useSelectAll({ + tree, + checked: dataSourceState.checked, + stats, + areCheckboxesVisible: rowOptions?.checkbox?.isVisible, + handleSelectAll, + }); const getListProps = useCallback((): DataSourceListProps => { return { diff --git a/uui-core/src/data/processing/views/dataRows/useSelectAll.ts b/uui-core/src/data/processing/views/dataRows/useSelectAll.ts new file mode 100644 index 0000000000..4d8c83977d --- /dev/null +++ b/uui-core/src/data/processing/views/dataRows/useSelectAll.ts @@ -0,0 +1,44 @@ +import { useMemo } from 'react'; +import { ITree } from '../tree'; +import { NodeStats } from './stats'; + +export interface UseSelectAllProps { + tree: ITree; + selectAll?: boolean; + stats: NodeStats; + checked?: TId[]; + areCheckboxesVisible: boolean; + handleSelectAll: (isChecked: boolean) => void; +} + +export function useSelectAll(props: UseSelectAllProps) { + const isSelectAllEnabled = useMemo(() => props.selectAll === undefined ? true : props.selectAll, [props.selectAll]); + + const selectAll = useMemo(() => { + if (props.stats.isSomeCheckable && isSelectAllEnabled) { + return { + value: props.stats.isSomeCheckboxEnabled ? props.stats.isAllChecked : false, + onValueChange: props.handleSelectAll, + indeterminate: props.checked && props.checked.length > 0 && !props.stats.isAllChecked, + }; + } else if (props.tree.getRootIds().length === 0 && props.areCheckboxesVisible && isSelectAllEnabled) { + // Nothing loaded yet, but we guess that something is checkable. Add disabled checkbox for less flicker. + return { + value: false, + onValueChange: () => {}, + isDisabled: true, + indeterminate: props.checked?.length > 0, + }; + } + return null; + }, [ + props.tree, + props.areCheckboxesVisible, + props.checked, + props.stats, + isSelectAllEnabled, + props.handleSelectAll, + ]); + + return selectAll; +} From ac4568bbacd563ed64bd15574d0348f86b63d47e Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Tue, 12 Dec 2023 10:49:15 +0200 Subject: [PATCH 011/278] [useTree]: added usePinnedRows and used useTree + useDataRows. --- .../_examples/tables/ArrayTable.example.tsx | 16 +-- .../data/processing/views/dataRows/index.ts | 2 + .../processing/views/dataRows/useDataRows.ts | 98 ++++--------------- .../views/dataRows/usePinnedRows.ts | 85 ++++++++++++++++ uui-core/src/data/processing/views/index.ts | 1 + .../plainTree/usePlainTreeStrategy.ts | 10 +- 6 files changed, 120 insertions(+), 92 deletions(-) create mode 100644 uui-core/src/data/processing/views/dataRows/index.ts create mode 100644 uui-core/src/data/processing/views/dataRows/usePinnedRows.ts diff --git a/app/src/docs/_examples/tables/ArrayTable.example.tsx b/app/src/docs/_examples/tables/ArrayTable.example.tsx index 41def5a8e0..427fb6ad28 100644 --- a/app/src/docs/_examples/tables/ArrayTable.example.tsx +++ b/app/src/docs/_examples/tables/ArrayTable.example.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useState } from 'react'; -import { DataColumnProps, useArrayDataSource, useTree } from '@epam/uui-core'; +import { DataColumnProps, useDataRows, useTree } from '@epam/uui-core'; import { DataTable, Panel, Text } from '@epam/uui'; import { demoData, FeatureClass } from '@epam/uui-docs'; import css from './TablesExamples.module.scss'; @@ -12,16 +12,10 @@ export default function ArrayDataTableExample() { items: demoData.featureClasses, getId: (item) => item.id, dataSourceState: value, + setDataSourceState: onValueChange, }, []); - const dataSource = useArrayDataSource( - { - items: demoData.featureClasses, - }, - [], - ); - - const view = dataSource.useView(value, onValueChange, {}); + const { getListProps, getVisibleRows } = useDataRows(tree); const productColumns: DataColumnProps[] = useMemo( () => [ @@ -52,8 +46,8 @@ export default function ArrayDataTableExample() { return ( extends FoldingService, CheckingService, FocusService, SelectingService { tree: ITree; @@ -86,6 +86,12 @@ export function useDataRows( getLoadingRowProps, }); + const withPinnedRows = usePinnedRows({ + rows, + pinned, + pinnedByParentId, + }); + const selectAll = useSelectAll({ tree, checked: dataSourceState.checked, @@ -94,6 +100,16 @@ export function useDataRows( handleSelectAll, }); + const getById = (id: TId, index: number) => { + // if originalTree is not created, but blank tree is defined, get item from it + const item = tree.getById(id); + if (item === NOT_FOUND_RECORD) { + return getUnknownRowProps(id, index, []); + } + + return getRowProps(item, index); + }; + const getListProps = useCallback((): DataSourceListProps => { return { rowsCount: rows.length, @@ -104,87 +120,9 @@ export function useDataRows( }; }, [rows.length, tree, selectAll]); - const getLastPinnedBeforeRow = (row: DataRowProps, pinnedIndexes: number[]) => { - const isBeforeOrEqualToRow = (pinnedRowIndex: number) => { - const pinnedRow = rows[pinnedRowIndex]; - if (!pinnedRow) { - return false; - } - return row.index >= pinnedRow.index; - }; - - let foundRowIndex = -1; - for (const pinnedRowIndex of pinnedIndexes) { - if (isBeforeOrEqualToRow(pinnedRowIndex)) { - foundRowIndex = pinnedRowIndex; - } else if (foundRowIndex !== -1) { - break; - } - } - - if (foundRowIndex === -1) { - return undefined; - } - return foundRowIndex; - }; - - const getLastHiddenPinnedByParent = (row: DataRowProps, alreadyAdded: TId[]) => { - const pinnedIndexes = pinnedByParentId[idToKey(row.parentId)]; - if (!pinnedIndexes || !pinnedIndexes.length) { - return undefined; - } - - const lastPinnedBeforeRow = getLastPinnedBeforeRow(row, pinnedIndexes); - if (lastPinnedBeforeRow === undefined) { - return undefined; - } - - const lastHiddenPinned = rows[lastPinnedBeforeRow]; - if (!lastHiddenPinned || alreadyAdded.includes(lastHiddenPinned.id)) { - return undefined; - } - - return lastHiddenPinned; - }; - - const getRowsWithPinned = (allRows: DataRowProps[]) => { - if (!allRows.length) return []; - - const rowsWithPinned: DataRowProps[] = []; - const alreadyAdded = allRows.map(({ id }) => id); - const [firstRow] = allRows; - firstRow.path.forEach((item) => { - const pinnedIndex = pinned[idToKey(item.id)]; - if (pinnedIndex === undefined) return; - - const parent = rows[pinnedIndex]; - if (!parent || alreadyAdded.includes(parent.id)) return; - - rowsWithPinned.push(parent); - alreadyAdded.push(parent.id); - }); - - const lastHiddenPinned = getLastHiddenPinnedByParent(firstRow, alreadyAdded); - if (lastHiddenPinned) { - rowsWithPinned.push(lastHiddenPinned); - } - - return rowsWithPinned.concat(allRows); - }; - const getVisibleRows = () => { const visibleRows = rows.slice(dataSourceState.topIndex, lastRowIndex); - return getRowsWithPinned(visibleRows); - }; - - const getById = (id: TId, index: number) => { - // if originalTree is not created, but blank tree is defined, get item from it - const item = tree.getById(id); - if (item === NOT_FOUND_RECORD) { - return getUnknownRowProps(id, index, []); - } - - return getRowProps(item, index); + return withPinnedRows(visibleRows); }; const getSelectedRows = ({ topIndex = 0, visibleCount }: VirtualListRange = {}) => { diff --git a/uui-core/src/data/processing/views/dataRows/usePinnedRows.ts b/uui-core/src/data/processing/views/dataRows/usePinnedRows.ts new file mode 100644 index 0000000000..834aea9abf --- /dev/null +++ b/uui-core/src/data/processing/views/dataRows/usePinnedRows.ts @@ -0,0 +1,85 @@ +import { useCallback } from 'react'; +import { DataRowProps } from '../../../../types'; +import { idToKey } from '../helpers'; + +export interface UsePinnedRowsProps { + rows: DataRowProps[]; + pinned: Record; + pinnedByParentId: Record; +} + +export function usePinnedRows({ + rows, + pinned, + pinnedByParentId, +}: UsePinnedRowsProps) { + const getLastPinnedBeforeRow = useCallback((row: DataRowProps, pinnedIndexes: number[]) => { + const isBeforeOrEqualToRow = (pinnedRowIndex: number) => { + const pinnedRow = rows[pinnedRowIndex]; + if (!pinnedRow) { + return false; + } + return row.index >= pinnedRow.index; + }; + + let foundRowIndex = -1; + for (const pinnedRowIndex of pinnedIndexes) { + if (isBeforeOrEqualToRow(pinnedRowIndex)) { + foundRowIndex = pinnedRowIndex; + } else if (foundRowIndex !== -1) { + break; + } + } + + if (foundRowIndex === -1) { + return undefined; + } + return foundRowIndex; + }, [rows]); + + const getLastHiddenPinnedByParent = useCallback((row: DataRowProps, alreadyAdded: TId[]) => { + const pinnedIndexes = pinnedByParentId[idToKey(row.parentId)]; + if (!pinnedIndexes || !pinnedIndexes.length) { + return undefined; + } + + const lastPinnedBeforeRow = getLastPinnedBeforeRow(row, pinnedIndexes); + if (lastPinnedBeforeRow === undefined) { + return undefined; + } + + const lastHiddenPinned = rows[lastPinnedBeforeRow]; + if (!lastHiddenPinned || alreadyAdded.includes(lastHiddenPinned.id)) { + return undefined; + } + + return lastHiddenPinned; + }, [pinnedByParentId, rows, getLastPinnedBeforeRow]); + + const withPinnedRows = useCallback((allRows: DataRowProps[]) => { + if (!allRows.length) return []; + + const rowsWithPinned: DataRowProps[] = []; + const alreadyAdded = allRows.map(({ id }) => id); + const [firstRow] = allRows; + firstRow.path.forEach((item) => { + const pinnedIndex = pinned[idToKey(item.id)]; + if (pinnedIndex === undefined) return; + + const parent = rows[pinnedIndex]; + if (!parent || alreadyAdded.includes(parent.id)) return; + + rowsWithPinned.push(parent); + alreadyAdded.push(parent.id); + }); + + const lastHiddenPinned = getLastHiddenPinnedByParent(firstRow, alreadyAdded); + if (lastHiddenPinned) { + rowsWithPinned.push(lastHiddenPinned); + } + + return rowsWithPinned.concat(allRows); + }, [pinned, rows, getLastHiddenPinnedByParent]); + + return withPinnedRows; +} diff --git a/uui-core/src/data/processing/views/index.ts b/uui-core/src/data/processing/views/index.ts index d98c4366ba..c602a42b2e 100644 --- a/uui-core/src/data/processing/views/index.ts +++ b/uui-core/src/data/processing/views/index.ts @@ -3,3 +3,4 @@ export * from './ArrayListView'; export * from './LazyListView'; export * from './AsyncListView'; export * from './tree'; +export * from './dataRows'; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts index 2934aab395..eba0295bbc 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo } from 'react'; import { PlainTreeStrategyProps } from './types'; -import { useCheckingService, useFocusService } from '../../services'; +import { useCheckingService, useFocusService, useSelectingService } from '../../services'; import { useCreateTree } from './useCreateTree'; import { useFilterTree } from './useFilterTree'; import { useSearchTree } from './useSearchTree'; @@ -67,6 +67,10 @@ export function usePlainTreeStrategy( dataSourceState, setDataSourceState, }); + const selectingService = useSelectingService({ + dataSourceState, setDataSourceState, + }); + const getEstimatedChildrenCount = useCallback((id: TId) => { if (id === undefined) return undefined; @@ -129,11 +133,14 @@ export function usePlainTreeStrategy( ...checkingService, ...foldingService, ...focusService, + ...selectingService, rowOptions, getRowOptions, getEstimatedChildrenCount, getMissingRecordsCount, lastRowIndex, + getId, + dataSourceState, }), [ tree, @@ -144,6 +151,7 @@ export function usePlainTreeStrategy( getEstimatedChildrenCount, getMissingRecordsCount, lastRowIndex, + dataSourceState, ], ); } From 9aba93ecccd46c50fac827fe6aa57b0941d05b65 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Tue, 12 Dec 2023 13:11:25 +0200 Subject: [PATCH 012/278] [useTree]: Refactored plainTreeStrategy. --- .../_examples/tables/ArrayTable.example.tsx | 19 ++--- .../hooks => dataRows}/services/index.ts | 0 .../services/useCheckingService.ts | 21 +++--- .../services/useFocusService.ts | 2 +- .../services/useFoldingService.ts | 4 +- .../services/useSelectingService.ts | 2 +- .../processing/views/dataRows/useBuildRows.ts | 2 +- .../views/dataRows/useDataRowProps.ts | 2 +- .../processing/views/dataRows/useDataRows.ts | 71 +++++++++++++------ .../tree/hooks/strategies/plainTree/types.ts | 6 +- .../plainTree/usePlainTreeStrategy.ts | 38 ---------- .../views/tree/hooks/strategies/types.ts | 7 +- 12 files changed, 85 insertions(+), 89 deletions(-) rename uui-core/src/data/processing/views/{tree/hooks => dataRows}/services/index.ts (100%) rename uui-core/src/data/processing/views/{tree/hooks => dataRows}/services/useCheckingService.ts (87%) rename uui-core/src/data/processing/views/{tree/hooks => dataRows}/services/useFocusService.ts (93%) rename uui-core/src/data/processing/views/{tree/hooks => dataRows}/services/useFoldingService.ts (96%) rename uui-core/src/data/processing/views/{tree/hooks => dataRows}/services/useSelectingService.ts (92%) diff --git a/app/src/docs/_examples/tables/ArrayTable.example.tsx b/app/src/docs/_examples/tables/ArrayTable.example.tsx index 427fb6ad28..8ea75657f2 100644 --- a/app/src/docs/_examples/tables/ArrayTable.example.tsx +++ b/app/src/docs/_examples/tables/ArrayTable.example.tsx @@ -5,17 +5,20 @@ import { demoData, FeatureClass } from '@epam/uui-docs'; import css from './TablesExamples.module.scss'; export default function ArrayDataTableExample() { - const [value, onValueChange] = useState({}); - - const tree = useTree({ + const [dataSourceState, setDataSourceState] = useState({}); + + const { tree, ...restProps } = useTree({ type: 'plain', items: demoData.featureClasses, getId: (item) => item.id, - dataSourceState: value, - setDataSourceState: onValueChange, + dataSourceState, }, []); - const { getListProps, getVisibleRows } = useDataRows(tree); + const { getListProps, getVisibleRows } = useDataRows({ + tree, + setDataSourceState, + ...restProps, + }); const productColumns: DataColumnProps[] = useMemo( () => [ @@ -48,8 +51,8 @@ export default function ArrayDataTableExample() { diff --git a/uui-core/src/data/processing/views/tree/hooks/services/index.ts b/uui-core/src/data/processing/views/dataRows/services/index.ts similarity index 100% rename from uui-core/src/data/processing/views/tree/hooks/services/index.ts rename to uui-core/src/data/processing/views/dataRows/services/index.ts diff --git a/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts b/uui-core/src/data/processing/views/dataRows/services/useCheckingService.ts similarity index 87% rename from uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts rename to uui-core/src/data/processing/views/dataRows/services/useCheckingService.ts index f6f7cbfc20..a44df056aa 100644 --- a/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts +++ b/uui-core/src/data/processing/views/dataRows/services/useCheckingService.ts @@ -1,15 +1,16 @@ import { useCallback, useMemo } from 'react'; -import { CascadeSelection, CascadeSelectionTypes, DataRowOptions, DataRowProps } from '../../../../../../types'; -import { ITree, NOT_FOUND_RECORD } from '../..'; +import { CascadeSelection, CascadeSelectionTypes, DataRowOptions, DataRowProps, DataSourceState } from '../../../../../types'; +import { ITree, NOT_FOUND_RECORD } from '../../tree'; -export interface UseCheckingServiceProps { +export interface UseCheckingServiceProps { tree: ITree; - checked?: TId[]; - setChecked: (checked: TId[]) => void; getParentId?: (item: TItem) => TId; cascadeSelection?: CascadeSelection; rowOptions?: DataRowOptions; getRowOptions?(item: TItem, index?: number): DataRowOptions; + + dataSourceState: DataSourceState, + setDataSourceState?: (dataSourceState: DataSourceState) => void; } export interface CheckingService { @@ -57,13 +58,15 @@ export function useCheckingService( { tree, getParentId, - checked = [], - setChecked, + dataSourceState, + setDataSourceState, cascadeSelection, getRowOptions, rowOptions, }: UseCheckingServiceProps, ): CheckingService { + const checked = dataSourceState.checked ?? []; + const checkingInfoById = useMemo( () => getCheckingInfo(checked, tree, getParentId), [tree, checked], @@ -101,8 +104,8 @@ export function useCheckingService( isSelectable: (item: TItem) => isItemCheckable(item), }); - setChecked(updatedChecked); - }, [tree, checked, setChecked, isItemCheckable, cascadeSelection]); + setDataSourceState({ ...dataSourceState, checked: updatedChecked }); + }, [tree, checked, dataSourceState, setDataSourceState, isItemCheckable, cascadeSelection]); const handleSelectAll = useCallback((isChecked: boolean) => { handleCheck(isChecked); diff --git a/uui-core/src/data/processing/views/tree/hooks/services/useFocusService.ts b/uui-core/src/data/processing/views/dataRows/services/useFocusService.ts similarity index 93% rename from uui-core/src/data/processing/views/tree/hooks/services/useFocusService.ts rename to uui-core/src/data/processing/views/dataRows/services/useFocusService.ts index 2c9de8b2bf..2767135d35 100644 --- a/uui-core/src/data/processing/views/tree/hooks/services/useFocusService.ts +++ b/uui-core/src/data/processing/views/dataRows/services/useFocusService.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react'; -import { DataSourceState } from '../../../../../../types'; +import { DataSourceState } from '../../../../../types'; export interface UseFocusServiceProps { dataSourceState: DataSourceState, diff --git a/uui-core/src/data/processing/views/tree/hooks/services/useFoldingService.ts b/uui-core/src/data/processing/views/dataRows/services/useFoldingService.ts similarity index 96% rename from uui-core/src/data/processing/views/tree/hooks/services/useFoldingService.ts rename to uui-core/src/data/processing/views/dataRows/services/useFoldingService.ts index c5f12a35c9..ea744066da 100644 --- a/uui-core/src/data/processing/views/tree/hooks/services/useFoldingService.ts +++ b/uui-core/src/data/processing/views/dataRows/services/useFoldingService.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo } from 'react'; -import { DataRowProps, DataSourceState, ScrollToConfig } from '../../../../../../types'; -import { idToKey, setObjectFlag } from '../../../helpers'; +import { DataRowProps, DataSourceState, ScrollToConfig } from '../../../../../types'; +import { idToKey, setObjectFlag } from '../../helpers'; export interface UseFoldingServiceProps { getId: (item: TItem) => TId; diff --git a/uui-core/src/data/processing/views/tree/hooks/services/useSelectingService.ts b/uui-core/src/data/processing/views/dataRows/services/useSelectingService.ts similarity index 92% rename from uui-core/src/data/processing/views/tree/hooks/services/useSelectingService.ts rename to uui-core/src/data/processing/views/dataRows/services/useSelectingService.ts index 113eee5b41..ae94903aaf 100644 --- a/uui-core/src/data/processing/views/tree/hooks/services/useSelectingService.ts +++ b/uui-core/src/data/processing/views/dataRows/services/useSelectingService.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react'; -import { DataRowProps, DataSourceState } from '../../../../../../types'; +import { DataRowProps, DataSourceState } from '../../../../../types'; export interface UseSelectingServiceProps { dataSourceState: DataSourceState, diff --git a/uui-core/src/data/processing/views/dataRows/useBuildRows.ts b/uui-core/src/data/processing/views/dataRows/useBuildRows.ts index d1788b8166..e04000c3ca 100644 --- a/uui-core/src/data/processing/views/dataRows/useBuildRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useBuildRows.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { ITree, NOT_FOUND_RECORD } from '../tree'; import { CascadeSelection, DataRowOptions, DataRowPathItem, DataRowProps, DataSourceState } from '../../../../types'; import { idToKey } from '../helpers'; -import { FoldingService } from '../tree/hooks/services'; +import { FoldingService } from './services'; import { NodeStats, getDefaultNodeStats, getRowStats, mergeStats } from './stats'; export interface UseBuildRowsProps extends FoldingService { diff --git a/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts b/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts index c7a4d6ff5d..592fd1bde4 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo } from 'react'; import { DataRowOptions, DataRowPathItem, DataRowProps, DataSourceState } from '../../../../types'; -import { CheckingService, FocusService, SelectingService } from '../tree/hooks/services'; +import { CheckingService, FocusService, SelectingService } from './services'; import { ITree } from '../tree'; import { idToKey } from '../helpers'; diff --git a/uui-core/src/data/processing/views/dataRows/useDataRows.ts b/uui-core/src/data/processing/views/dataRows/useDataRows.ts index c4dc571225..57de597fc7 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRows.ts @@ -1,29 +1,37 @@ import { useCallback, useMemo } from 'react'; import { ITree, NOT_FOUND_RECORD } from '../tree'; import { CascadeSelection, DataRowOptions, DataRowProps, DataSourceListProps, DataSourceState, VirtualListRange } from '../../../../types'; -import { FoldingService, CheckingService, FocusService, SelectingService } from '../tree/hooks/services'; +import { useCheckingService, useFoldingService, useFocusService, useSelectingService } from './services'; import { useDataRowProps } from './useDataRowProps'; import { useBuildRows } from './useBuildRows'; import { useSelectAll } from './useSelectAll'; import { usePinnedRows } from './usePinnedRows'; -export interface UseDataRowsProps extends FoldingService, CheckingService, FocusService, SelectingService { +export interface UseDataRowsProps { tree: ITree; dataSourceState: DataSourceState; + setDataSourceState: (dataSourceState: DataSourceState) => void; + flattenSearchResults?: boolean; isPartialLoad?: boolean; + rowOptions?: DataRowOptions; getRowOptions?(item: TItem, index?: number): DataRowOptions; + isFoldedByDefault?(item: TItem): boolean; + getChildCount?(item: TItem): number; + getId: (item: TItem) => TId; - cascadeSelection?: CascadeSelection; + getParentId?(item: TItem): TId | undefined; - selectAll?: boolean; + cascadeSelection?: CascadeSelection; getEstimatedChildrenCount: (id: TId) => number; getMissingRecordsCount: (id: TId, totalRowsCount: number, loadedChildrenCount: number) => number; lastRowIndex: number; + + selectAll?: boolean; } export function useDataRows( @@ -32,7 +40,10 @@ export function useDataRows( const { tree, getId, + getParentId, dataSourceState, + setDataSourceState, + flattenSearchResults, isPartialLoad, getRowOptions, @@ -41,34 +52,49 @@ export function useDataRows( getEstimatedChildrenCount, getMissingRecordsCount, cascadeSelection, + isFoldedByDefault, lastRowIndex, + } = props; - isFolded, - isRowChecked, - isRowChildrenChecked, + const checkingService = useCheckingService({ + tree, + dataSourceState, + setDataSourceState, + cascadeSelection, + getParentId, + }); - handleOnFold, - handleSelectAll, - handleOnCheck, - handleOnFocus, - handleOnSelect, - } = props; + const foldingService = useFoldingService({ + dataSourceState, setDataSourceState, isFoldedByDefault, getId, + }); + + const focusService = useFocusService({ + dataSourceState, setDataSourceState, + }); + + const selectingService = useSelectingService({ + dataSourceState, setDataSourceState, + }); const isFlattenSearch = useMemo(() => dataSourceState.search && flattenSearchResults, []); const { getRowProps, getUnknownRowProps, getLoadingRowProps } = useDataRowProps({ tree, getId, + + isFlattenSearch, dataSourceState, - getRowOptions, + rowOptions, - handleOnCheck, - handleOnSelect, - handleOnFocus, - isRowChecked, - isRowChildrenChecked, - isFlattenSearch, + getRowOptions, + getEstimatedChildrenCount, + + handleOnCheck: checkingService.handleOnCheck, + handleOnSelect: selectingService.handleOnSelect, + handleOnFocus: focusService.handleOnFocus, + isRowChecked: checkingService.isRowChecked, + isRowChildrenChecked: checkingService.isRowChildrenChecked, }); const { rows, pinned, pinnedByParentId, stats } = useBuildRows({ @@ -80,10 +106,9 @@ export function useDataRows( lastRowIndex, getEstimatedChildrenCount, getMissingRecordsCount, - isFolded, - handleOnFold, getRowProps, getLoadingRowProps, + ...foldingService, }); const withPinnedRows = usePinnedRows({ @@ -97,7 +122,7 @@ export function useDataRows( checked: dataSourceState.checked, stats, areCheckboxesVisible: rowOptions?.checkbox?.isVisible, - handleSelectAll, + handleSelectAll: checkingService.handleSelectAll, }); const getById = (id: TId, index: number) => { diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts index 95b07bd371..995f2090d6 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts @@ -1,4 +1,4 @@ -import { CascadeSelection, SortingOption } from '../../../../../../../types'; +import { SortingOption } from '../../../../../../../types'; import { ITree } from '../../../ITree'; import { STRATEGIES } from '../constants'; import { TreeStrategyProps } from '../types'; @@ -7,10 +7,10 @@ export type PlainTreeStrategyProps = TreeStrategyProps, + getSearchFields?(item: TItem): string[]; sortBy?(item: TItem, sorting: SortingOption): any; getFilter?(filter: TFilter): (item: TItem) => boolean; + sortSearchByRelevance?: boolean; - cascadeSelection?: CascadeSelection; - isFoldedByDefault?(item: TItem): boolean; }; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts index eba0295bbc..7ce462fdf7 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts @@ -1,11 +1,9 @@ import { useCallback, useMemo } from 'react'; import { PlainTreeStrategyProps } from './types'; -import { useCheckingService, useFocusService, useSelectingService } from '../../services'; import { useCreateTree } from './useCreateTree'; import { useFilterTree } from './useFilterTree'; import { useSearchTree } from './useSearchTree'; import { useSortTree } from './useSortTree'; -import { useFoldingService } from '../../services/useFoldingService'; import { NOT_FOUND_RECORD } from '../../../ITree'; export function usePlainTreeStrategy( @@ -18,15 +16,11 @@ export function usePlainTreeStrategy( const { getId, dataSourceState, - setDataSourceState, getFilter, getSearchFields, sortBy, - cascadeSelection, - getParentId, rowOptions, getRowOptions, - isFoldedByDefault, getChildCount, } = props; @@ -45,32 +39,6 @@ export function usePlainTreeStrategy( deps, ); - const { checked } = dataSourceState; - const setChecked = useCallback( - (newChecked: TId[]) => setDataSourceState({ ...dataSourceState, checked: newChecked }), - [setDataSourceState, dataSourceState], - ); - - const checkingService = useCheckingService({ - tree, - checked, - setChecked, - cascadeSelection, - getParentId, - }); - - const foldingService = useFoldingService({ - dataSourceState, setDataSourceState, isFoldedByDefault, getId, - }); - - const focusService = useFocusService({ - dataSourceState, setDataSourceState, - }); - - const selectingService = useSelectingService({ - dataSourceState, setDataSourceState, - }); - const getEstimatedChildrenCount = useCallback((id: TId) => { if (id === undefined) return undefined; @@ -130,10 +98,6 @@ export function usePlainTreeStrategy( return useMemo( () => ({ tree, - ...checkingService, - ...foldingService, - ...focusService, - ...selectingService, rowOptions, getRowOptions, getEstimatedChildrenCount, @@ -144,8 +108,6 @@ export function usePlainTreeStrategy( }), [ tree, - checkingService, - foldingService, rowOptions, getRowOptions, getEstimatedChildrenCount, diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts index bc5cc2c554..0a473debfe 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts @@ -1,15 +1,18 @@ -import { DataRowOptions, DataSourceState } from '../../../../../../types'; +import { CascadeSelection, DataRowOptions, DataSourceState } from '../../../../../../types'; import { STRATEGIES } from './constants'; export type Strategies = typeof STRATEGIES[keyof typeof STRATEGIES]; export type TreeStrategyProps = { dataSourceState: DataSourceState; - setDataSourceState: (dataSourceState: DataSourceState) => void; getId?(item: TItem): TId; getParentId?(item: TItem): TId | undefined; complexIds?: boolean; + rowOptions?: DataRowOptions; getRowOptions?(item: TItem, index?: number): DataRowOptions; + getChildCount?(item: TItem): number; + + cascadeSelection?: CascadeSelection; }; From 94abcfda9e364796ce812abb7b8cab1bb22b6d0c Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Tue, 12 Dec 2023 14:56:03 +0200 Subject: [PATCH 013/278] Removed dependency on dataSourceState. --- .../dataRows/services/useCheckingService.ts | 6 ++--- .../dataRows/services/useFocusService.ts | 12 ++++----- .../dataRows/services/useFoldingService.ts | 26 ++++++++++--------- .../dataRows/services/useSelectingService.ts | 12 ++++----- .../processing/views/dataRows/useDataRows.ts | 10 +++---- 5 files changed, 30 insertions(+), 36 deletions(-) diff --git a/uui-core/src/data/processing/views/dataRows/services/useCheckingService.ts b/uui-core/src/data/processing/views/dataRows/services/useCheckingService.ts index a44df056aa..f6aacbfd6a 100644 --- a/uui-core/src/data/processing/views/dataRows/services/useCheckingService.ts +++ b/uui-core/src/data/processing/views/dataRows/services/useCheckingService.ts @@ -10,7 +10,7 @@ export interface UseCheckingServiceProps { getRowOptions?(item: TItem, index?: number): DataRowOptions; dataSourceState: DataSourceState, - setDataSourceState?: (dataSourceState: DataSourceState) => void; + setDataSourceState?: React.Dispatch>>; } export interface CheckingService { @@ -104,8 +104,8 @@ export function useCheckingService( isSelectable: (item: TItem) => isItemCheckable(item), }); - setDataSourceState({ ...dataSourceState, checked: updatedChecked }); - }, [tree, checked, dataSourceState, setDataSourceState, isItemCheckable, cascadeSelection]); + setDataSourceState((dsState) => ({ ...dsState, checked: updatedChecked })); + }, [tree, checked, setDataSourceState, isItemCheckable, cascadeSelection]); const handleSelectAll = useCallback((isChecked: boolean) => { handleCheck(isChecked); diff --git a/uui-core/src/data/processing/views/dataRows/services/useFocusService.ts b/uui-core/src/data/processing/views/dataRows/services/useFocusService.ts index 2767135d35..d90f14faba 100644 --- a/uui-core/src/data/processing/views/dataRows/services/useFocusService.ts +++ b/uui-core/src/data/processing/views/dataRows/services/useFocusService.ts @@ -2,8 +2,7 @@ import { useCallback, useMemo } from 'react'; import { DataSourceState } from '../../../../../types'; export interface UseFocusServiceProps { - dataSourceState: DataSourceState, - setDataSourceState?: (dataSourceState: DataSourceState) => void; + setDataSourceState?: React.Dispatch>>; } export interface FocusService { @@ -11,15 +10,14 @@ export interface FocusService { } export function useFocusService({ - dataSourceState, setDataSourceState, }: UseFocusServiceProps): FocusService { const handleOnFocus = useCallback((focusIndex: number) => { - setDataSourceState({ - ...dataSourceState, + setDataSourceState((dsState) => ({ + ...dsState, focusedIndex: focusIndex, - }); - }, [dataSourceState, setDataSourceState]); + })); + }, [setDataSourceState]); return useMemo( () => ({ handleOnFocus }), diff --git a/uui-core/src/data/processing/views/dataRows/services/useFoldingService.ts b/uui-core/src/data/processing/views/dataRows/services/useFoldingService.ts index ea744066da..5e73596e82 100644 --- a/uui-core/src/data/processing/views/dataRows/services/useFoldingService.ts +++ b/uui-core/src/data/processing/views/dataRows/services/useFoldingService.ts @@ -5,7 +5,7 @@ import { idToKey, setObjectFlag } from '../../helpers'; export interface UseFoldingServiceProps { getId: (item: TItem) => TId; dataSourceState: DataSourceState, - setDataSourceState?: (dataSourceState: DataSourceState) => void; + setDataSourceState?: React.Dispatch>>; isFoldedByDefault?(item: TItem): boolean; } @@ -41,19 +41,21 @@ export function useFoldingService({ const handleOnFold = useCallback((rowProps: DataRowProps) => { if (setDataSourceState) { - const fold = !rowProps.isFolded; - const indexToScroll = rowProps.index - (rowProps.path?.length ?? 0); - const scrollTo: ScrollToConfig = fold && rowProps.isPinned - ? { index: indexToScroll, align: 'nearest' } - : dataSourceState.scrollTo; - - setDataSourceState({ - ...dataSourceState, - scrollTo, - folded: setObjectFlag(dataSourceState && dataSourceState.folded, rowProps.rowKey, fold), + setDataSourceState((dsState) => { + const fold = !rowProps.isFolded; + const indexToScroll = rowProps.index - (rowProps.path?.length ?? 0); + const scrollTo: ScrollToConfig = fold && rowProps.isPinned + ? { index: indexToScroll, align: 'nearest' } + : dsState.scrollTo; + + return { + ...dsState, + scrollTo, + folded: setObjectFlag(dsState && dsState.folded, rowProps.rowKey, fold), + }; }); } - }, [setDataSourceState, dataSourceState]); + }, [setDataSourceState]); return useMemo( () => ({ handleOnFold, isFolded }), diff --git a/uui-core/src/data/processing/views/dataRows/services/useSelectingService.ts b/uui-core/src/data/processing/views/dataRows/services/useSelectingService.ts index ae94903aaf..bc6f9db771 100644 --- a/uui-core/src/data/processing/views/dataRows/services/useSelectingService.ts +++ b/uui-core/src/data/processing/views/dataRows/services/useSelectingService.ts @@ -2,8 +2,7 @@ import { useCallback, useMemo } from 'react'; import { DataRowProps, DataSourceState } from '../../../../../types'; export interface UseSelectingServiceProps { - dataSourceState: DataSourceState, - setDataSourceState?: (dataSourceState: DataSourceState) => void; + setDataSourceState?: React.Dispatch>>; } export interface SelectingService { @@ -11,15 +10,14 @@ export interface SelectingService { } export function useSelectingService({ - dataSourceState, setDataSourceState, }: UseSelectingServiceProps): SelectingService { const handleOnSelect = useCallback((rowProps: DataRowProps) => { - setDataSourceState?.({ - ...dataSourceState, + setDataSourceState((dsState) => ({ + ...dsState, selectedId: rowProps.id, - }); - }, [dataSourceState, setDataSourceState]); + })); + }, [setDataSourceState]); return useMemo( () => ({ handleOnSelect }), diff --git a/uui-core/src/data/processing/views/dataRows/useDataRows.ts b/uui-core/src/data/processing/views/dataRows/useDataRows.ts index 57de597fc7..03d44266ba 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRows.ts @@ -10,7 +10,7 @@ import { usePinnedRows } from './usePinnedRows'; export interface UseDataRowsProps { tree: ITree; dataSourceState: DataSourceState; - setDataSourceState: (dataSourceState: DataSourceState) => void; + setDataSourceState?: React.Dispatch>>; flattenSearchResults?: boolean; isPartialLoad?: boolean; @@ -68,13 +68,9 @@ export function useDataRows( dataSourceState, setDataSourceState, isFoldedByDefault, getId, }); - const focusService = useFocusService({ - dataSourceState, setDataSourceState, - }); + const focusService = useFocusService({ setDataSourceState }); - const selectingService = useSelectingService({ - dataSourceState, setDataSourceState, - }); + const selectingService = useSelectingService({ setDataSourceState }); const isFlattenSearch = useMemo(() => dataSourceState.search && flattenSearchResults, []); From bc930f24b247bd51599b2a8639f5133ba1492860 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Wed, 13 Dec 2023 14:54:20 +0200 Subject: [PATCH 014/278] [useTree]: made getListProps and getVisibleRows unified. --- .../processing/views/dataRows/useDataRows.ts | 73 +++++++++++++++++-- .../plainTree/usePlainTreeStrategy.ts | 18 +++++ 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/uui-core/src/data/processing/views/dataRows/useDataRows.ts b/uui-core/src/data/processing/views/dataRows/useDataRows.ts index 03d44266ba..7c8ce412b6 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRows.ts @@ -32,6 +32,11 @@ export interface UseDataRowsProps { lastRowIndex: number; selectAll?: boolean; + // TODO: add solid type + getTreeRowsStats: () => { + completeFlatListRowsCount: any; + totalCount: number; + } } export function useDataRows( @@ -54,6 +59,7 @@ export function useDataRows( cascadeSelection, isFoldedByDefault, lastRowIndex, + getTreeRowsStats, } = props; const checkingService = useCheckingService({ @@ -132,19 +138,72 @@ export function useDataRows( }; const getListProps = useCallback((): DataSourceListProps => { + const { completeFlatListRowsCount, totalCount } = getTreeRowsStats(); + + let rowsCount; + if (completeFlatListRowsCount !== undefined) { + // We have a flat list, and know exact count of items on top level. So, we can have an exact number of rows w/o iterating the whole tree. + rowsCount = completeFlatListRowsCount; + } else if (!stats.hasMoreRows) { + // We are at the bottom of the list. Some children might still be loading, but that's ok - we'll re-count everything after we load them. + rowsCount = rows.length; + } else { + // We definitely have more rows to show below the last visible row. + // We need to add at least 1 row below, so VirtualList or other component would not detect the end of the list, and query loading more rows later. + // We have to balance this number. + // To big - would make scrollbar size to shrink when we hit bottom + // To small - and VirtualList will re-request rows until it will fill it's last block. + // So, it should be at least greater than VirtualList block size (default is 20) + // Probably, we'll move this const to props later if needed; + const rowsToAddBelowLastKnown = 20; + + rowsCount = Math.max(rows.length, lastRowIndex + rowsToAddBelowLastKnown); + } + return { - rowsCount: rows.length, + rowsCount, knownRowsCount: rows.length, exactRowsCount: rows.length, - totalCount: tree?.getTotalRecursiveCount() ?? 0, // TODO: totalCount should be taken from fullTree (?). + totalCount, selectAll, + isReloading: false, }; - }, [rows.length, tree, selectAll]); + }, [rows.length, selectAll, getTreeRowsStats, stats.hasMoreRows, lastRowIndex]); + + const getVisibleRows = useCallback( + () => { + const from = dataSourceState.topIndex; + const count = dataSourceState.visibleCount; + const visibleRows = withPinnedRows(rows.slice(from, from + count)); + + if (stats.hasMoreRows) { + const listProps = getListProps(); + // We don't run rebuild rows on scrolling. We rather wait for the next load to happen. + // So there can be a case when we haven't updated rows (to add more loading rows), and view is scrolled down + // We need to add more loading rows in such case. + const lastRow = rows[rows.length - 1]; + + while (visibleRows.length < count && from + visibleRows.length < listProps.rowsCount) { + const index = from + visibleRows.length; + const row = getLoadingRowProps('_loading_' + index, index); + row.indent = lastRow.indent; + row.path = lastRow.path; + row.depth = lastRow.depth; + visibleRows.push(row); + } + } - const getVisibleRows = () => { - const visibleRows = rows.slice(dataSourceState.topIndex, lastRowIndex); - return withPinnedRows(visibleRows); - }; + return visibleRows; + }, + [ + rows, + dataSourceState.topIndex, + dataSourceState.visibleCount, + withPinnedRows, + getListProps, + getLoadingRowProps, + ], + ); const getSelectedRows = ({ topIndex = 0, visibleCount }: VirtualListRange = {}) => { let checked: TId[] = []; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts index 7ce462fdf7..e532246293 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts @@ -95,6 +95,22 @@ export function usePlainTreeStrategy( return 1; }, [lastRowIndex, tree, getEstimatedChildrenCount]); + const getTreeRowsStats = useCallback(() => { + const rootInfo = tree.getNodeInfo(undefined); + /* TODO: For lazy list... + + const rootCount = rootInfo.count; + if (!getChildCount && rootCount != null) { + completeFlatListRowsCount = rootCount; + } + + */ + return { + completeFlatListRowsCount: undefined, + totalCount: rootInfo.totalCount ?? tree.getTotalRecursiveCount() ?? 0, + }; + }, [getChildCount, tree]); + return useMemo( () => ({ tree, @@ -105,6 +121,7 @@ export function usePlainTreeStrategy( lastRowIndex, getId, dataSourceState, + getTreeRowsStats, }), [ tree, @@ -114,6 +131,7 @@ export function usePlainTreeStrategy( getMissingRecordsCount, lastRowIndex, dataSourceState, + getTreeRowsStats, ], ); } From b1e3d92ed7dd389b5ca040dd4210829717442027 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Wed, 13 Dec 2023 17:37:57 +0200 Subject: [PATCH 015/278] [useTree]: moved services to useTree. --- .../_examples/tables/ArrayTable.example.tsx | 9 +- .../processing/views/dataRows/useBuildRows.ts | 2 +- .../views/dataRows/useDataRowProps.ts | 2 +- .../processing/views/dataRows/useDataRows.ts | 84 ++++++++++++------- .../hooks}/services/index.ts | 0 .../hooks}/services/useCheckingService.ts | 4 +- .../hooks}/services/useFocusService.ts | 2 +- .../hooks}/services/useFoldingService.ts | 4 +- .../hooks}/services/useSelectingService.ts | 2 +- .../plainTree/usePlainTreeStrategy.ts | 76 ++++++----------- .../views/tree/hooks/strategies/types.ts | 3 + .../processing/views/tree/hooks/useTree.ts | 6 +- 12 files changed, 97 insertions(+), 97 deletions(-) rename uui-core/src/data/processing/views/{dataRows => tree/hooks}/services/index.ts (100%) rename uui-core/src/data/processing/views/{dataRows => tree/hooks}/services/useCheckingService.ts (97%) rename uui-core/src/data/processing/views/{dataRows => tree/hooks}/services/useFocusService.ts (92%) rename uui-core/src/data/processing/views/{dataRows => tree/hooks}/services/useFoldingService.ts (96%) rename uui-core/src/data/processing/views/{dataRows => tree/hooks}/services/useSelectingService.ts (91%) diff --git a/app/src/docs/_examples/tables/ArrayTable.example.tsx b/app/src/docs/_examples/tables/ArrayTable.example.tsx index 8ea75657f2..8ff49ec37e 100644 --- a/app/src/docs/_examples/tables/ArrayTable.example.tsx +++ b/app/src/docs/_examples/tables/ArrayTable.example.tsx @@ -7,17 +7,16 @@ import css from './TablesExamples.module.scss'; export default function ArrayDataTableExample() { const [dataSourceState, setDataSourceState] = useState({}); - const { tree, ...restProps } = useTree({ - type: 'plain', - items: demoData.featureClasses, + const { tree, ...dataRowProps } = useTree({ getId: (item) => item.id, dataSourceState, + setDataSourceState, + items: demoData.featureClasses, }, []); const { getListProps, getVisibleRows } = useDataRows({ tree, - setDataSourceState, - ...restProps, + ...dataRowProps, }); const productColumns: DataColumnProps[] = useMemo( diff --git a/uui-core/src/data/processing/views/dataRows/useBuildRows.ts b/uui-core/src/data/processing/views/dataRows/useBuildRows.ts index e04000c3ca..d1788b8166 100644 --- a/uui-core/src/data/processing/views/dataRows/useBuildRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useBuildRows.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { ITree, NOT_FOUND_RECORD } from '../tree'; import { CascadeSelection, DataRowOptions, DataRowPathItem, DataRowProps, DataSourceState } from '../../../../types'; import { idToKey } from '../helpers'; -import { FoldingService } from './services'; +import { FoldingService } from '../tree/hooks/services'; import { NodeStats, getDefaultNodeStats, getRowStats, mergeStats } from './stats'; export interface UseBuildRowsProps extends FoldingService { diff --git a/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts b/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts index 592fd1bde4..c7a4d6ff5d 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo } from 'react'; import { DataRowOptions, DataRowPathItem, DataRowProps, DataSourceState } from '../../../../types'; -import { CheckingService, FocusService, SelectingService } from './services'; +import { CheckingService, FocusService, SelectingService } from '../tree/hooks/services'; import { ITree } from '../tree'; import { idToKey } from '../helpers'; diff --git a/uui-core/src/data/processing/views/dataRows/useDataRows.ts b/uui-core/src/data/processing/views/dataRows/useDataRows.ts index 7c8ce412b6..6a0576499e 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRows.ts @@ -1,13 +1,13 @@ import { useCallback, useMemo } from 'react'; import { ITree, NOT_FOUND_RECORD } from '../tree'; import { CascadeSelection, DataRowOptions, DataRowProps, DataSourceListProps, DataSourceState, VirtualListRange } from '../../../../types'; -import { useCheckingService, useFoldingService, useFocusService, useSelectingService } from './services'; +import { SelectingService, FoldingService, FocusService, CheckingService } from '../tree/hooks/services'; import { useDataRowProps } from './useDataRowProps'; import { useBuildRows } from './useBuildRows'; import { useSelectAll } from './useSelectAll'; import { usePinnedRows } from './usePinnedRows'; -export interface UseDataRowsProps { +export interface UseDataRowsProps extends CheckingService, SelectingService, FoldingService, FocusService { tree: ITree; dataSourceState: DataSourceState; setDataSourceState?: React.Dispatch>>; @@ -18,17 +18,12 @@ export interface UseDataRowsProps { rowOptions?: DataRowOptions; getRowOptions?(item: TItem, index?: number): DataRowOptions; - isFoldedByDefault?(item: TItem): boolean; - getChildCount?(item: TItem): number; getId: (item: TItem) => TId; getParentId?(item: TItem): TId | undefined; cascadeSelection?: CascadeSelection; - - getEstimatedChildrenCount: (id: TId) => number; - getMissingRecordsCount: (id: TId, totalRowsCount: number, loadedChildrenCount: number) => number; lastRowIndex: number; selectAll?: boolean; @@ -45,40 +40,64 @@ export function useDataRows( const { tree, getId, - getParentId, dataSourceState, - setDataSourceState, flattenSearchResults, isPartialLoad, getRowOptions, rowOptions, - getEstimatedChildrenCount, - getMissingRecordsCount, cascadeSelection, - isFoldedByDefault, lastRowIndex, getTreeRowsStats, } = props; - const checkingService = useCheckingService({ - tree, - dataSourceState, - setDataSourceState, - cascadeSelection, - getParentId, - }); + const isFlattenSearch = useMemo(() => dataSourceState.search && flattenSearchResults, []); - const foldingService = useFoldingService({ - dataSourceState, setDataSourceState, isFoldedByDefault, getId, - }); + const getEstimatedChildrenCount = useCallback((id: TId) => { + if (id === undefined) return undefined; - const focusService = useFocusService({ setDataSourceState }); + const item = tree.getById(id); + if (item === NOT_FOUND_RECORD) return undefined; - const selectingService = useSelectingService({ setDataSourceState }); + const childCount = props.getChildCount?.(item) ?? undefined; + if (childCount === undefined) return undefined; - const isFlattenSearch = useMemo(() => dataSourceState.search && flattenSearchResults, []); + const nodeInfo = tree.getNodeInfo(id); + if (nodeInfo?.count !== undefined) { + // nodes are already loaded, and we know the actual count + return nodeInfo.count; + } + + return childCount; + }, [props.getChildCount, tree]); + + const getMissingRecordsCount = useCallback((id: TId, totalRowsCount: number, loadedChildrenCount: number) => { + const nodeInfo = tree.getNodeInfo(id); + + const estimatedChildCount = getEstimatedChildrenCount(id); + + // Estimate how many more nodes there are at current level, to put 'loading' placeholders. + if (nodeInfo.count !== undefined) { + // Exact count known + return nodeInfo.count - loadedChildrenCount; + } + + // estimatedChildCount = undefined for top-level rows only. + if (id === undefined && totalRowsCount < lastRowIndex) { + return lastRowIndex - totalRowsCount; // let's put placeholders down to the bottom of visible list + } + + if (estimatedChildCount > loadedChildrenCount) { + // According to getChildCount (put into estimatedChildCount), there are more rows on this level + return estimatedChildCount - loadedChildrenCount; + } + + // We have a bad estimate - it even less that actual items we have + // This would happen is getChildCount provides a guess count, and we scroll thru children past this count + // let's guess we have at least 1 item more than loaded + return 1; + }, [lastRowIndex, tree, getEstimatedChildrenCount]); const { getRowProps, getUnknownRowProps, getLoadingRowProps } = useDataRowProps({ tree, @@ -92,11 +111,11 @@ export function useDataRows( getEstimatedChildrenCount, - handleOnCheck: checkingService.handleOnCheck, - handleOnSelect: selectingService.handleOnSelect, - handleOnFocus: focusService.handleOnFocus, - isRowChecked: checkingService.isRowChecked, - isRowChildrenChecked: checkingService.isRowChildrenChecked, + handleOnCheck: props.handleOnCheck, + handleOnSelect: props.handleOnSelect, + handleOnFocus: props.handleOnFocus, + isRowChecked: props.isRowChecked, + isRowChildrenChecked: props.isRowChildrenChecked, }); const { rows, pinned, pinnedByParentId, stats } = useBuildRows({ @@ -110,7 +129,8 @@ export function useDataRows( getMissingRecordsCount, getRowProps, getLoadingRowProps, - ...foldingService, + isFolded: props.isFolded, + handleOnFold: props.handleOnFold, }); const withPinnedRows = usePinnedRows({ @@ -124,7 +144,7 @@ export function useDataRows( checked: dataSourceState.checked, stats, areCheckboxesVisible: rowOptions?.checkbox?.isVisible, - handleSelectAll: checkingService.handleSelectAll, + handleSelectAll: props.handleSelectAll, }); const getById = (id: TId, index: number) => { diff --git a/uui-core/src/data/processing/views/dataRows/services/index.ts b/uui-core/src/data/processing/views/tree/hooks/services/index.ts similarity index 100% rename from uui-core/src/data/processing/views/dataRows/services/index.ts rename to uui-core/src/data/processing/views/tree/hooks/services/index.ts diff --git a/uui-core/src/data/processing/views/dataRows/services/useCheckingService.ts b/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts similarity index 97% rename from uui-core/src/data/processing/views/dataRows/services/useCheckingService.ts rename to uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts index f6aacbfd6a..7b566d2aa4 100644 --- a/uui-core/src/data/processing/views/dataRows/services/useCheckingService.ts +++ b/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo } from 'react'; -import { CascadeSelection, CascadeSelectionTypes, DataRowOptions, DataRowProps, DataSourceState } from '../../../../../types'; -import { ITree, NOT_FOUND_RECORD } from '../../tree'; +import { CascadeSelection, CascadeSelectionTypes, DataRowOptions, DataRowProps, DataSourceState } from '../../../../../../types'; +import { ITree, NOT_FOUND_RECORD } from '../..'; export interface UseCheckingServiceProps { tree: ITree; diff --git a/uui-core/src/data/processing/views/dataRows/services/useFocusService.ts b/uui-core/src/data/processing/views/tree/hooks/services/useFocusService.ts similarity index 92% rename from uui-core/src/data/processing/views/dataRows/services/useFocusService.ts rename to uui-core/src/data/processing/views/tree/hooks/services/useFocusService.ts index d90f14faba..becd03b755 100644 --- a/uui-core/src/data/processing/views/dataRows/services/useFocusService.ts +++ b/uui-core/src/data/processing/views/tree/hooks/services/useFocusService.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react'; -import { DataSourceState } from '../../../../../types'; +import { DataSourceState } from '../../../../../../types'; export interface UseFocusServiceProps { setDataSourceState?: React.Dispatch>>; diff --git a/uui-core/src/data/processing/views/dataRows/services/useFoldingService.ts b/uui-core/src/data/processing/views/tree/hooks/services/useFoldingService.ts similarity index 96% rename from uui-core/src/data/processing/views/dataRows/services/useFoldingService.ts rename to uui-core/src/data/processing/views/tree/hooks/services/useFoldingService.ts index 5e73596e82..21460af085 100644 --- a/uui-core/src/data/processing/views/dataRows/services/useFoldingService.ts +++ b/uui-core/src/data/processing/views/tree/hooks/services/useFoldingService.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo } from 'react'; -import { DataRowProps, DataSourceState, ScrollToConfig } from '../../../../../types'; -import { idToKey, setObjectFlag } from '../../helpers'; +import { DataRowProps, DataSourceState, ScrollToConfig } from '../../../../../../types'; +import { idToKey, setObjectFlag } from '../../../helpers'; export interface UseFoldingServiceProps { getId: (item: TItem) => TId; diff --git a/uui-core/src/data/processing/views/dataRows/services/useSelectingService.ts b/uui-core/src/data/processing/views/tree/hooks/services/useSelectingService.ts similarity index 91% rename from uui-core/src/data/processing/views/dataRows/services/useSelectingService.ts rename to uui-core/src/data/processing/views/tree/hooks/services/useSelectingService.ts index bc6f9db771..7e25635af0 100644 --- a/uui-core/src/data/processing/views/dataRows/services/useSelectingService.ts +++ b/uui-core/src/data/processing/views/tree/hooks/services/useSelectingService.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react'; -import { DataRowProps, DataSourceState } from '../../../../../types'; +import { DataRowProps, DataSourceState } from '../../../../../../types'; export interface UseSelectingServiceProps { setDataSourceState?: React.Dispatch>>; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts index e532246293..70f1e89e49 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts @@ -4,7 +4,7 @@ import { useCreateTree } from './useCreateTree'; import { useFilterTree } from './useFilterTree'; import { useSearchTree } from './useSearchTree'; import { useSortTree } from './useSortTree'; -import { NOT_FOUND_RECORD } from '../../../ITree'; +import { useCheckingService, useFocusService, useFoldingService, useSelectingService } from '../../services'; export function usePlainTreeStrategy( { sortSearchByRelevance = true, ...restProps }: PlainTreeStrategyProps, @@ -15,47 +15,48 @@ export function usePlainTreeStrategy( const { getId, + getParentId, dataSourceState, + setDataSourceState, getFilter, getSearchFields, sortBy, rowOptions, getRowOptions, getChildCount, + cascadeSelection, } = props; const filteredTree = useFilterTree( { tree: fullTree, getFilter, dataSourceState }, - deps, + [fullTree], ); const searchTree = useSearchTree( { tree: filteredTree, getSearchFields, sortSearchByRelevance, dataSourceState }, - deps, + [filteredTree], ); const tree = useSortTree( { tree: searchTree, sortBy, dataSourceState }, - deps, + [searchTree], ); - const getEstimatedChildrenCount = useCallback((id: TId) => { - if (id === undefined) return undefined; - - const item = tree.getById(id); - if (item === NOT_FOUND_RECORD) return undefined; + const checkingService = useCheckingService({ + tree, + dataSourceState, + setDataSourceState, + cascadeSelection, + getParentId, + }); - const childCount = getChildCount?.(item) ?? undefined; - if (childCount === undefined) return undefined; + const foldingService = useFoldingService({ + dataSourceState, setDataSourceState, isFoldedByDefault: restProps.isFoldedByDefault, getId, + }); - const nodeInfo = tree.getNodeInfo(id); - if (nodeInfo?.count !== undefined) { - // nodes are already loaded, and we know the actual count - return nodeInfo.count; - } + const focusService = useFocusService({ setDataSourceState }); - return childCount; - }, [getChildCount, tree]); + const selectingService = useSelectingService({ setDataSourceState }); const lastRowIndex = useMemo( () => { @@ -68,33 +69,6 @@ export function usePlainTreeStrategy( [tree, dataSourceState.topIndex, dataSourceState.visibleCount], ); - const getMissingRecordsCount = useCallback((id: TId, totalRowsCount: number, loadedChildrenCount: number) => { - const nodeInfo = tree.getNodeInfo(id); - - const estimatedChildCount = getEstimatedChildrenCount(id); - - // Estimate how many more nodes there are at current level, to put 'loading' placeholders. - if (nodeInfo.count !== undefined) { - // Exact count known - return nodeInfo.count - loadedChildrenCount; - } - - // estimatedChildCount = undefined for top-level rows only. - if (id === undefined && totalRowsCount < lastRowIndex) { - return lastRowIndex - totalRowsCount; // let's put placeholders down to the bottom of visible list - } - - if (estimatedChildCount > loadedChildrenCount) { - // According to getChildCount (put into estimatedChildCount), there are more rows on this level - return estimatedChildCount - loadedChildrenCount; - } - - // We have a bad estimate - it even less that actual items we have - // This would happen is getChildCount provides a guess count, and we scroll thru children past this count - // let's guess we have at least 1 item more than loaded - return 1; - }, [lastRowIndex, tree, getEstimatedChildrenCount]); - const getTreeRowsStats = useCallback(() => { const rootInfo = tree.getNodeInfo(undefined); /* TODO: For lazy list... @@ -116,22 +90,26 @@ export function usePlainTreeStrategy( tree, rowOptions, getRowOptions, - getEstimatedChildrenCount, - getMissingRecordsCount, lastRowIndex, getId, dataSourceState, getTreeRowsStats, + ...checkingService, + ...selectingService, + ...focusService, + ...foldingService, }), [ tree, rowOptions, getRowOptions, - getEstimatedChildrenCount, - getMissingRecordsCount, lastRowIndex, dataSourceState, getTreeRowsStats, + checkingService, + selectingService, + focusService, + foldingService, ], ); } diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts index 0a473debfe..3462948f1d 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts @@ -5,6 +5,8 @@ export type Strategies = typeof STRATEGIES[keyof typeof STRATEGIES]; export type TreeStrategyProps = { dataSourceState: DataSourceState; + setDataSourceState: React.Dispatch>>; + getId?(item: TItem): TId; getParentId?(item: TItem): TId | undefined; complexIds?: boolean; @@ -12,6 +14,7 @@ export type TreeStrategyProps = { rowOptions?: DataRowOptions; getRowOptions?(item: TItem, index?: number): DataRowOptions; + isFoldedByDefault?(item: TItem): boolean; getChildCount?(item: TItem): number; cascadeSelection?: CascadeSelection; diff --git a/uui-core/src/data/processing/views/tree/hooks/useTree.ts b/uui-core/src/data/processing/views/tree/hooks/useTree.ts index dc79d5bcd5..d3cd597f2e 100644 --- a/uui-core/src/data/processing/views/tree/hooks/useTree.ts +++ b/uui-core/src/data/processing/views/tree/hooks/useTree.ts @@ -1,8 +1,8 @@ import { useTreeStrategy } from './useTreeStrategy'; import { UseTreeProps } from './types'; -export function useTree(params: UseTreeProps, deps: any[]) { - const tree = useTreeStrategy(params, deps); +export function useTree(props: UseTreeProps, deps: any[]) { + const { tree, ...restProps } = useTreeStrategy(props, deps); - return tree; + return { tree, ...restProps }; } From 8b603406fadef8839b95a9d4b6a6f16ea013b4a4 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Thu, 14 Dec 2023 14:36:16 +0200 Subject: [PATCH 016/278] Added loadData hook. --- .../src/data/processing/views/LazyListView.ts | 4 +- .../tree/hooks/strategies/lazyTree/index.ts | 0 .../tree/hooks/strategies/lazyTree/types.ts | 12 ++ .../lazyTree/useLazyTreeStrategy.ts | 12 ++ .../hooks/strategies/lazyTree/useLoadData.ts | 157 ++++++++++++++++++ 5 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/index.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/types.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts diff --git a/uui-core/src/data/processing/views/LazyListView.ts b/uui-core/src/data/processing/views/LazyListView.ts index 059f19f8aa..9236ce4caa 100644 --- a/uui-core/src/data/processing/views/LazyListView.ts +++ b/uui-core/src/data/processing/views/LazyListView.ts @@ -177,8 +177,6 @@ export class LazyListView extends BaseListView extends BaseListView = TreeStrategyProps & { + type: typeof STRATEGIES.lazy, + api: LazyDataSourceApi; + filter?: TFilter; + fetchStrategy?: 'sequential' | 'parallel'; + flattenSearchResults?: boolean; + legacyLoadDataBehavior?: boolean; +}; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts new file mode 100644 index 0000000000..3066b98d6b --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts @@ -0,0 +1,12 @@ +import { useMemo } from 'react'; +import { Tree } from '../../../Tree'; +import { LazyTreeStrategyProps } from './types'; + +export function useLazyTreeStrategy( + { flattenSearchResults = true, ...props }: LazyTreeStrategyProps, + deps: any[], +) { + const tree = useMemo(() => Tree.blank(props), [deps]); + + +} diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts new file mode 100644 index 0000000000..bf3b981c99 --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts @@ -0,0 +1,157 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { usePrevious } from '../../../../../../../hooks'; +import { DataSourceState, LazyDataSourceApi } from '../../../../../../../types'; +import { ITree } from '../../../../tree'; +import isEqual from 'lodash.isequal'; + +export interface UseLoadDataProps { + api: LazyDataSourceApi; + tree: ITree; + filter?: TFilter; + dataSourceState: DataSourceState; + backgroundReload?: boolean; + isFolded: (item: TItem) => boolean; +} + +interface LoadResult { + isUpdated: boolean; + isOutdated: boolean; + tree: ITree; +} + +const searchWasChanged = ( + prevValue?: DataSourceState, newValue?: DataSourceState, +) => newValue?.search !== prevValue?.search; + +const sortingWasChanged = ( + prevValue?: DataSourceState, newValue?: DataSourceState, +) => !isEqual(newValue?.sorting, prevValue?.sorting); + +const filterWasChanged = ( + prevValue: DataSourceState, newValue?: DataSourceState, +) => !isEqual(newValue?.filter, prevValue?.filter); + +const shouldRebuildTree = (prevValue: DataSourceState, newValue: DataSourceState) => + searchWasChanged(prevValue, newValue) + || sortingWasChanged(prevValue, newValue) + || filterWasChanged(prevValue, newValue) + || newValue?.page !== prevValue?.page + || newValue?.pageSize !== prevValue?.pageSize; + +const onlySearchWasUnset = (prevValue: DataSourceState, newValue: DataSourceState) => + searchWasChanged(prevValue, newValue) && !newValue.search + && !( + sortingWasChanged(prevValue, newValue) + || filterWasChanged(prevValue, newValue) + || newValue?.page !== prevValue?.page + || newValue?.pageSize !== prevValue?.pageSize); + +export function useLoadData( + props: UseLoadDataProps, +) { + const { api, tree, filter, dataSourceState, backgroundReload, isFolded } = props; + const [treeWithData, setTreeWithData] = useState(tree); + const prevFilter = usePrevious(filter); + const prevDataSourceState = usePrevious(dataSourceState); + + const isReloadingRef = useRef(false); + const promiseInProgressRef = useRef>>(); + const fingerprintRef = useRef(); + + const actualRowsCount = useMemo(() => treeWithData.getTotalRecursiveCount() ?? 0, [treeWithData]); + + const lastRecordIndex = useMemo( + () => dataSourceState.topIndex + dataSourceState.visibleCount, + [dataSourceState.topIndex, dataSourceState.visibleCount], + ); + + const areMoreRowsNeeded = useCallback((prevValue?: DataSourceState, newValue?: DataSourceState) => { + const isFetchPositionAndAmountChanged = prevValue?.topIndex !== newValue?.topIndex + || prevValue?.visibleCount !== newValue?.visibleCount; + + return isFetchPositionAndAmountChanged && lastRecordIndex > actualRowsCount; + }, [lastRecordIndex, actualRowsCount]); + + const loadMissing = (abortInProgress: boolean, sourceTree: ITree): Promise> => { + // Make tree updates sequential, by executing all consequent calls after previous promise completed + if (promiseInProgressRef.current === null || abortInProgress) { + promiseInProgressRef.current = Promise.resolve({ isUpdated: false, isOutdated: false, tree: sourceTree }); + } + + promiseInProgressRef.current = promiseInProgressRef.current.then(() => + loadMissingImpl(sourceTree)); + + return promiseInProgressRef.current; + }; + + const loadMissingImpl = async (sourceTree: ITree): Promise> => { + const loadingTree = sourceTree; + + try { + const newTreePromise = sourceTree.load( + { + ...props, + isFolded, + api, + filter: { ...{}, ...filter }, + }, + dataSourceState, + ); + + const newTree = await newTreePromise; + + const linkToTree = treeWithData; + + // If tree is changed during this load, than there was reset occurred (new value arrived) + // We need to tell caller to reject this result + const isOutdated = linkToTree !== loadingTree; + const isUpdated = linkToTree !== newTree; + return { isUpdated, isOutdated, tree: newTree }; + } catch (e) { + // TBD - correct error handling + console.error('LazyListView: Error while loading items.', e); + return { isUpdated: false, isOutdated: false, tree: loadingTree }; + } + }; + + useEffect(() => { + let completeReset = false; + const shouldReloadData = !isEqual(prevFilter, filter) + || shouldRebuildTree(prevDataSourceState, dataSourceState); + + let currentTree = treeWithData; + if (prevDataSourceState == null || shouldReloadData) { + isReloadingRef.current = true; + if (!onlySearchWasUnset(prevDataSourceState, dataSourceState)) { + currentTree = treeWithData.clearStructure(); + } + completeReset = true; + } + const isFoldingChanged = !prevDataSourceState || dataSourceState.folded !== prevDataSourceState.folded; + const shouldShowPlacehodlers = !shouldReloadData + || (shouldReloadData && !backgroundReload); + + if ((completeReset && shouldShowPlacehodlers) || isFoldingChanged) { + fingerprintRef.current = new Date().toISOString(); + } + + const moreRowsNeeded = areMoreRowsNeeded(prevDataSourceState, dataSourceState); + if (completeReset || isFoldingChanged || moreRowsNeeded) { + loadMissing(completeReset, currentTree) + .then(({ isUpdated, isOutdated, tree: newTree }) => { + if (isUpdated && !isOutdated) { + setTreeWithData(newTree); + isReloadingRef.current = false; + fingerprintRef.current = new Date().toISOString(); + } + }).finally(() => { + isReloadingRef.current = false; + }); + } + }, [prevDataSourceState, dataSourceState, prevFilter, filter, treeWithData, setTreeWithData, areMoreRowsNeeded]); + + return { + tree: treeWithData, + fingerprint: fingerprintRef.current, + }; +} From cb5e2463399d80776a90c34135d8b996d38bd6f9 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Thu, 14 Dec 2023 14:41:55 +0200 Subject: [PATCH 017/278] [useTree]: refactoring. --- .../views/tree/hooks/strategies/lazyTree/useLoadData.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts index bf3b981c99..c705a15f38 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts @@ -46,6 +46,8 @@ const onlySearchWasUnset = (prevValue: DataSourceState new Date().toISOString(); + export function useLoadData( props: UseLoadDataProps, ) { @@ -132,7 +134,7 @@ export function useLoadData( || (shouldReloadData && !backgroundReload); if ((completeReset && shouldShowPlacehodlers) || isFoldingChanged) { - fingerprintRef.current = new Date().toISOString(); + fingerprintRef.current = generateFingerprint(); } const moreRowsNeeded = areMoreRowsNeeded(prevDataSourceState, dataSourceState); @@ -142,7 +144,7 @@ export function useLoadData( if (isUpdated && !isOutdated) { setTreeWithData(newTree); isReloadingRef.current = false; - fingerprintRef.current = new Date().toISOString(); + fingerprintRef.current = generateFingerprint(); } }).finally(() => { isReloadingRef.current = false; From 6dca86bd0cada77dce1a1141ecccca46d28df7d3 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Thu, 14 Dec 2023 16:08:35 +0200 Subject: [PATCH 018/278] Added loadMissing useLazyCheckingService. --- .../tree/hooks/services/useCheckingService.ts | 4 + .../tree/hooks/strategies/lazyTree/helpers.ts | 31 +++++ .../tree/hooks/strategies/lazyTree/types.ts | 2 + .../lazyTree/useLazyCheckingService.ts | 72 ++++++++++ .../lazyTree/useLazyTreeStrategy.ts | 76 +++++++++- .../hooks/strategies/lazyTree/useLoadData.ts | 131 ++++-------------- 6 files changed, 212 insertions(+), 104 deletions(-) create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/helpers.ts create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyCheckingService.ts diff --git a/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts b/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts index 7b566d2aa4..2eb65847c8 100644 --- a/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts +++ b/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts @@ -18,7 +18,9 @@ export interface CheckingService { isRowChildrenChecked: (row: DataRowProps) => boolean; handleOnCheck: (rowProps: DataRowProps) => void; handleSelectAll: (isChecked: boolean) => void; + clearAllChecked: () => void; + isItemCheckable: (item: TItem) => boolean; } const idToKey = (id: TId) => typeof id === 'object' ? JSON.stringify(id) : `${id}`; @@ -129,6 +131,7 @@ export function useCheckingService( handleOnCheck, handleSelectAll, clearAllChecked, + isItemCheckable, }), [ isRowChecked, @@ -136,6 +139,7 @@ export function useCheckingService( handleOnCheck, handleSelectAll, clearAllChecked, + isItemCheckable, ], ); } diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/helpers.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/helpers.ts new file mode 100644 index 0000000000..d9b1253750 --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/helpers.ts @@ -0,0 +1,31 @@ +import isEqual from 'lodash.isequal'; +import { DataSourceState } from '../../../../../../../types'; + +export const searchWasChanged = ( + prevValue?: DataSourceState, newValue?: DataSourceState, +) => newValue?.search !== prevValue?.search; + +export const sortingWasChanged = ( + prevValue?: DataSourceState, newValue?: DataSourceState, +) => !isEqual(newValue?.sorting, prevValue?.sorting); + +export const filterWasChanged = ( + prevValue: DataSourceState, newValue?: DataSourceState, +) => !isEqual(newValue?.filter, prevValue?.filter); + +export const shouldRebuildTree = (prevValue: DataSourceState, newValue: DataSourceState) => + searchWasChanged(prevValue, newValue) + || sortingWasChanged(prevValue, newValue) + || filterWasChanged(prevValue, newValue) + || newValue?.page !== prevValue?.page + || newValue?.pageSize !== prevValue?.pageSize; + +export const onlySearchWasUnset = (prevValue: DataSourceState, newValue: DataSourceState) => + searchWasChanged(prevValue, newValue) && !newValue.search + && !( + sortingWasChanged(prevValue, newValue) + || filterWasChanged(prevValue, newValue) + || newValue?.page !== prevValue?.page + || newValue?.pageSize !== prevValue?.pageSize); + +export const generateFingerprint = () => new Date().toISOString(); diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/types.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/types.ts index c6f78acb20..c58a8d7870 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/types.ts @@ -9,4 +9,6 @@ export type LazyTreeStrategyProps = TreeStrategyProps extends UseCheckingServiceProps { + loadMissing: (sourceTree: ITree, abortInProgress: boolean, options?: Partial<{ + loadAllChildren?(id: TId): boolean; + isLoadStrict?: boolean; + }>, dataSourceState?: DataSourceState, withNestedChildren?: boolean) => Promise> +} + +export function useLazyCheckingService({ loadMissing, ...props }: UseLazyCheckingService) { + const checkingService = useCheckingService({ ...props }); + + const { tree, dataSourceState, cascadeSelection } = props; + const checkItems = async (isChecked: boolean, isRoot: boolean, checkedId?: TId) => { + let checked = dataSourceState?.checked ?? []; + + const isImplicitMode = cascadeSelection === CascadeSelectionTypes.IMPLICIT; + let resultTree = tree; + if (cascadeSelection || isRoot) { + const loadNestedLayersChildren = !isImplicitMode; + const parents = tree.getParentIdsRecursive(checkedId); + + const result = await loadMissing( + tree, + false, + { + // If cascadeSelection is implicit and the element is unchecked, it is necessary to load all children + // of all parents of the unchecked element to be checked explicitly. Only one layer of each parent should be loaded. + // Otherwise, should be loaded only checked element and all its nested children. + loadAllChildren: (id) => { + if (!cascadeSelection) { + return isChecked && isRoot; + } + + if (isImplicitMode) { + return id === ROOT_ID || parents.some((parent) => isEqual(parent, id)); + } + + // `isEqual` is used, because complex ids can be recreated after fetching of parents. + // So, they should be compared not by reference, but by value. + return isRoot || isEqual(id, checkedId) || (dataSourceState.search && parents.some((parent) => isEqual(parent, id))); + }, + isLoadStrict: true, + }, + { search: null }, + loadNestedLayersChildren, + ); + + resultTree = result.tree; + } + + checked = resultTree.cascadeSelection(checked, checkedId, isChecked, { + cascade: isImplicitMode ? cascadeSelection : (isRoot && isChecked) || cascadeSelection, + isSelectable: (item: TItem) => checkingService.isItemCheckable(item), + }); + + // handleCheckedChange(checked); + } + + const handleSelectAll = () => { + + } + + return { + ...checkingService, + handleSelectAll, + } +} diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts index 3066b98d6b..f484128941 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts @@ -1,12 +1,84 @@ -import { useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Tree } from '../../../Tree'; import { LazyTreeStrategyProps } from './types'; +import { usePrevious } from '../../../../../../../hooks'; +import { DataSourceState } from '../../../../../../../types'; + +import isEqual from 'lodash.isequal'; +import { generateFingerprint, onlySearchWasUnset, shouldRebuildTree } from './helpers'; +import { useFoldingService } from '../../services'; +import { useLoadData } from './useLoadData'; export function useLazyTreeStrategy( - { flattenSearchResults = true, ...props }: LazyTreeStrategyProps, + { flattenSearchResults = true, ...restProps }: LazyTreeStrategyProps, deps: any[], ) { + const props = { flattenSearchResults, ...restProps }; + const { api, filter, dataSourceState, backgroundReload, isFoldedByDefault, getId, setDataSourceState } = props; + const tree = useMemo(() => Tree.blank(props), [deps]); + const [treeWithData, setTreeWithData] = useState(tree); + const prevFilter = usePrevious(filter); + const prevDataSourceState = usePrevious(dataSourceState); + const isReloadingRef = useRef(false); + const fingerprintRef = useRef(); + + const actualRowsCount = useMemo(() => treeWithData.getTotalRecursiveCount() ?? 0, [treeWithData]); + + const lastRecordIndex = useMemo( + () => dataSourceState.topIndex + dataSourceState.visibleCount, + [dataSourceState.topIndex, dataSourceState.visibleCount], + ); + + const areMoreRowsNeeded = useCallback((prevValue?: DataSourceState, newValue?: DataSourceState) => { + const isFetchPositionAndAmountChanged = prevValue?.topIndex !== newValue?.topIndex + || prevValue?.visibleCount !== newValue?.visibleCount; + + return isFetchPositionAndAmountChanged && lastRecordIndex > actualRowsCount; + }, [lastRecordIndex, actualRowsCount]); + + const foldingService = useFoldingService({ dataSourceState, isFoldedByDefault, getId, setDataSourceState }); + + const { loadMissing } = useLoadData({ api, filter, dataSourceState, isFolded: foldingService.isFolded }); + + useEffect(() => { + let completeReset = false; + const shouldReloadData = !isEqual(prevFilter, filter) + || shouldRebuildTree(prevDataSourceState, dataSourceState); + + let currentTree = treeWithData; + if (prevDataSourceState == null || shouldReloadData) { + isReloadingRef.current = true; + if (!onlySearchWasUnset(prevDataSourceState, dataSourceState)) { + currentTree = treeWithData.clearStructure(); + } + completeReset = true; + } + const isFoldingChanged = !prevDataSourceState || dataSourceState.folded !== prevDataSourceState.folded; + const shouldShowPlacehodlers = !shouldReloadData + || (shouldReloadData && !backgroundReload); + + if ((completeReset && shouldShowPlacehodlers) || isFoldingChanged) { + fingerprintRef.current = generateFingerprint(); + } + const moreRowsNeeded = areMoreRowsNeeded(prevDataSourceState, dataSourceState); + if (completeReset || isFoldingChanged || moreRowsNeeded) { + loadMissing(currentTree, completeReset) + .then(({ isUpdated, isOutdated, tree: newTree }) => { + if (isUpdated && !isOutdated) { + setTreeWithData(newTree); + isReloadingRef.current = false; + fingerprintRef.current = generateFingerprint(); + } + }).finally(() => { + isReloadingRef.current = false; + }); + } + }, [prevDataSourceState, dataSourceState, prevFilter, filter, treeWithData, setTreeWithData, areMoreRowsNeeded]); + return { + tree: treeWithData, + fingerprint: fingerprintRef.current, + }; } diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts index c705a15f38..c3f688283c 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts @@ -1,108 +1,74 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { usePrevious } from '../../../../../../../hooks'; +import { useRef } from 'react'; import { DataSourceState, LazyDataSourceApi } from '../../../../../../../types'; import { ITree } from '../../../../tree'; -import isEqual from 'lodash.isequal'; export interface UseLoadDataProps { api: LazyDataSourceApi; - tree: ITree; filter?: TFilter; dataSourceState: DataSourceState; - backgroundReload?: boolean; isFolded: (item: TItem) => boolean; } -interface LoadResult { +export interface LoadResult { isUpdated: boolean; isOutdated: boolean; tree: ITree; } -const searchWasChanged = ( - prevValue?: DataSourceState, newValue?: DataSourceState, -) => newValue?.search !== prevValue?.search; - -const sortingWasChanged = ( - prevValue?: DataSourceState, newValue?: DataSourceState, -) => !isEqual(newValue?.sorting, prevValue?.sorting); - -const filterWasChanged = ( - prevValue: DataSourceState, newValue?: DataSourceState, -) => !isEqual(newValue?.filter, prevValue?.filter); - -const shouldRebuildTree = (prevValue: DataSourceState, newValue: DataSourceState) => - searchWasChanged(prevValue, newValue) - || sortingWasChanged(prevValue, newValue) - || filterWasChanged(prevValue, newValue) - || newValue?.page !== prevValue?.page - || newValue?.pageSize !== prevValue?.pageSize; - -const onlySearchWasUnset = (prevValue: DataSourceState, newValue: DataSourceState) => - searchWasChanged(prevValue, newValue) && !newValue.search - && !( - sortingWasChanged(prevValue, newValue) - || filterWasChanged(prevValue, newValue) - || newValue?.page !== prevValue?.page - || newValue?.pageSize !== prevValue?.pageSize); - -const generateFingerprint = () => new Date().toISOString(); - export function useLoadData( props: UseLoadDataProps, ) { - const { api, tree, filter, dataSourceState, backgroundReload, isFolded } = props; - const [treeWithData, setTreeWithData] = useState(tree); - const prevFilter = usePrevious(filter); - const prevDataSourceState = usePrevious(dataSourceState); + const { api, filter, isFolded } = props; - const isReloadingRef = useRef(false); const promiseInProgressRef = useRef>>(); - const fingerprintRef = useRef(); - - const actualRowsCount = useMemo(() => treeWithData.getTotalRecursiveCount() ?? 0, [treeWithData]); - - const lastRecordIndex = useMemo( - () => dataSourceState.topIndex + dataSourceState.visibleCount, - [dataSourceState.topIndex, dataSourceState.visibleCount], - ); - - const areMoreRowsNeeded = useCallback((prevValue?: DataSourceState, newValue?: DataSourceState) => { - const isFetchPositionAndAmountChanged = prevValue?.topIndex !== newValue?.topIndex - || prevValue?.visibleCount !== newValue?.visibleCount; - return isFetchPositionAndAmountChanged && lastRecordIndex > actualRowsCount; - }, [lastRecordIndex, actualRowsCount]); - - const loadMissing = (abortInProgress: boolean, sourceTree: ITree): Promise> => { + const loadMissing = ( + sourceTree: ITree, + abortInProgress: boolean, + options?: Partial<{ + loadAllChildren?(id: TId): boolean; + isLoadStrict?: boolean; + }>, + dataSourceState?: DataSourceState, + withNestedChildren?: boolean, + ): Promise> => { // Make tree updates sequential, by executing all consequent calls after previous promise completed if (promiseInProgressRef.current === null || abortInProgress) { promiseInProgressRef.current = Promise.resolve({ isUpdated: false, isOutdated: false, tree: sourceTree }); } promiseInProgressRef.current = promiseInProgressRef.current.then(() => - loadMissingImpl(sourceTree)); + loadMissingImpl(sourceTree, options, dataSourceState, withNestedChildren)); return promiseInProgressRef.current; }; - const loadMissingImpl = async (sourceTree: ITree): Promise> => { + const loadMissingImpl = async ( + sourceTree: ITree, + options?: Partial<{ + loadAllChildren?(id: TId): boolean; + isLoadStrict?: boolean; + }>, + dataSourceState?: DataSourceState, + withNestedChildren?: boolean, + ): Promise> => { const loadingTree = sourceTree; try { const newTreePromise = sourceTree.load( { ...props, + ...options, isFolded, api, - filter: { ...{}, ...filter }, + filter: { ...filter }, }, - dataSourceState, + { ...props.dataSourceState, ...dataSourceState }, + withNestedChildren, ); const newTree = await newTreePromise; - - const linkToTree = treeWithData; + const linkToTree = sourceTree; // If tree is changed during this load, than there was reset occurred (new value arrived) // We need to tell caller to reject this result @@ -116,44 +82,5 @@ export function useLoadData( } }; - useEffect(() => { - let completeReset = false; - const shouldReloadData = !isEqual(prevFilter, filter) - || shouldRebuildTree(prevDataSourceState, dataSourceState); - - let currentTree = treeWithData; - if (prevDataSourceState == null || shouldReloadData) { - isReloadingRef.current = true; - if (!onlySearchWasUnset(prevDataSourceState, dataSourceState)) { - currentTree = treeWithData.clearStructure(); - } - completeReset = true; - } - const isFoldingChanged = !prevDataSourceState || dataSourceState.folded !== prevDataSourceState.folded; - const shouldShowPlacehodlers = !shouldReloadData - || (shouldReloadData && !backgroundReload); - - if ((completeReset && shouldShowPlacehodlers) || isFoldingChanged) { - fingerprintRef.current = generateFingerprint(); - } - - const moreRowsNeeded = areMoreRowsNeeded(prevDataSourceState, dataSourceState); - if (completeReset || isFoldingChanged || moreRowsNeeded) { - loadMissing(completeReset, currentTree) - .then(({ isUpdated, isOutdated, tree: newTree }) => { - if (isUpdated && !isOutdated) { - setTreeWithData(newTree); - isReloadingRef.current = false; - fingerprintRef.current = generateFingerprint(); - } - }).finally(() => { - isReloadingRef.current = false; - }); - } - }, [prevDataSourceState, dataSourceState, prevFilter, filter, treeWithData, setTreeWithData, areMoreRowsNeeded]); - - return { - tree: treeWithData, - fingerprint: fingerprintRef.current, - }; + return { loadMissing }; } From 7f8ff7d5c50783d60aea68333eac783b4e503e89 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Thu, 14 Dec 2023 20:11:13 +0200 Subject: [PATCH 019/278] [useTree]: Potentially finished useLazyTreeStrategy. --- .../processing/views/dataRows/useDataRows.ts | 1 + .../tree/hooks/services/useCheckingService.ts | 15 ++- .../lazyTree/useLazyCheckingService.ts | 72 ------------ .../lazyTree/useLazyTreeStrategy.ts | 105 +++++++++++++++++- .../plainTree/usePlainTreeStrategy.ts | 7 -- 5 files changed, 111 insertions(+), 89 deletions(-) delete mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyCheckingService.ts diff --git a/uui-core/src/data/processing/views/dataRows/useDataRows.ts b/uui-core/src/data/processing/views/dataRows/useDataRows.ts index 6a0576499e..b7fed61d40 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRows.ts @@ -116,6 +116,7 @@ export function useDataRows( handleOnFocus: props.handleOnFocus, isRowChecked: props.isRowChecked, isRowChildrenChecked: props.isRowChildrenChecked, + isItemCheckable: props.isItemCheckable, }); const { rows, pinned, pinnedByParentId, stats } = useBuildRows({ diff --git a/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts b/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts index 2eb65847c8..dc39a87859 100644 --- a/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts +++ b/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts @@ -11,6 +11,10 @@ export interface UseCheckingServiceProps { dataSourceState: DataSourceState, setDataSourceState?: React.Dispatch>>; + + loadMissingRecords?: ( + tree: ITree, id: TId | undefined, isChecked: boolean, isRoot: boolean, + ) => Promise>; } export interface CheckingService { @@ -65,6 +69,7 @@ export function useCheckingService( cascadeSelection, getRowOptions, rowOptions, + loadMissingRecords = async () => tree, }: UseCheckingServiceProps, ): CheckingService { const checked = dataSourceState.checked ?? []; @@ -100,8 +105,10 @@ export function useCheckingService( return rowProps?.checkbox?.isVisible && !rowProps?.checkbox?.isDisabled; }, [getRowProps]); - const handleCheck = useCallback((isChecked: boolean, checkedId?: TId) => { - const updatedChecked = tree.cascadeSelection(checked, checkedId, isChecked, { + const handleCheck = useCallback(async (isChecked: boolean, checkedId?: TId, isRoot?: boolean) => { + const fullTree = await loadMissingRecords(tree, checkedId, isChecked, isRoot); + + const updatedChecked = fullTree.cascadeSelection(checked, checkedId, isChecked, { cascade: cascadeSelection, isSelectable: (item: TItem) => isItemCheckable(item), }); @@ -110,11 +117,11 @@ export function useCheckingService( }, [tree, checked, setDataSourceState, isItemCheckable, cascadeSelection]); const handleSelectAll = useCallback((isChecked: boolean) => { - handleCheck(isChecked); + handleCheck(isChecked, undefined, true); }, [handleCheck]); const clearAllChecked = useCallback(() => { - handleCheck(false); + handleCheck(false, undefined, true); }, [handleCheck]); const handleOnCheck = useCallback((rowProps: DataRowProps) => { diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyCheckingService.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyCheckingService.ts deleted file mode 100644 index aaacdbc1b2..0000000000 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyCheckingService.ts +++ /dev/null @@ -1,72 +0,0 @@ -import isEqual from 'lodash.isequal'; -import { CascadeSelectionTypes, DataSourceState } from '../../../../../../../types'; -import { ITree, ROOT_ID } from '../../../ITree'; -import { UseCheckingServiceProps, useCheckingService } from '../../services'; -import { LoadResult } from './useLoadData'; - -interface UseLazyCheckingService extends UseCheckingServiceProps { - loadMissing: (sourceTree: ITree, abortInProgress: boolean, options?: Partial<{ - loadAllChildren?(id: TId): boolean; - isLoadStrict?: boolean; - }>, dataSourceState?: DataSourceState, withNestedChildren?: boolean) => Promise> -} - -export function useLazyCheckingService({ loadMissing, ...props }: UseLazyCheckingService) { - const checkingService = useCheckingService({ ...props }); - - const { tree, dataSourceState, cascadeSelection } = props; - const checkItems = async (isChecked: boolean, isRoot: boolean, checkedId?: TId) => { - let checked = dataSourceState?.checked ?? []; - - const isImplicitMode = cascadeSelection === CascadeSelectionTypes.IMPLICIT; - let resultTree = tree; - if (cascadeSelection || isRoot) { - const loadNestedLayersChildren = !isImplicitMode; - const parents = tree.getParentIdsRecursive(checkedId); - - const result = await loadMissing( - tree, - false, - { - // If cascadeSelection is implicit and the element is unchecked, it is necessary to load all children - // of all parents of the unchecked element to be checked explicitly. Only one layer of each parent should be loaded. - // Otherwise, should be loaded only checked element and all its nested children. - loadAllChildren: (id) => { - if (!cascadeSelection) { - return isChecked && isRoot; - } - - if (isImplicitMode) { - return id === ROOT_ID || parents.some((parent) => isEqual(parent, id)); - } - - // `isEqual` is used, because complex ids can be recreated after fetching of parents. - // So, they should be compared not by reference, but by value. - return isRoot || isEqual(id, checkedId) || (dataSourceState.search && parents.some((parent) => isEqual(parent, id))); - }, - isLoadStrict: true, - }, - { search: null }, - loadNestedLayersChildren, - ); - - resultTree = result.tree; - } - - checked = resultTree.cascadeSelection(checked, checkedId, isChecked, { - cascade: isImplicitMode ? cascadeSelection : (isRoot && isChecked) || cascadeSelection, - isSelectable: (item: TItem) => checkingService.isItemCheckable(item), - }); - - // handleCheckedChange(checked); - } - - const handleSelectAll = () => { - - } - - return { - ...checkingService, - handleSelectAll, - } -} diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts index f484128941..fbe991f2c8 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts @@ -2,22 +2,30 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Tree } from '../../../Tree'; import { LazyTreeStrategyProps } from './types'; import { usePrevious } from '../../../../../../../hooks'; -import { DataSourceState } from '../../../../../../../types'; +import { CascadeSelectionTypes, DataSourceState } from '../../../../../../../types'; import isEqual from 'lodash.isequal'; import { generateFingerprint, onlySearchWasUnset, shouldRebuildTree } from './helpers'; -import { useFoldingService } from '../../services'; +import { useCheckingService, useFocusService, useFoldingService, useSelectingService } from '../../services'; import { useLoadData } from './useLoadData'; +import { ITree, ROOT_ID } from '../../..'; export function useLazyTreeStrategy( { flattenSearchResults = true, ...restProps }: LazyTreeStrategyProps, deps: any[], ) { const props = { flattenSearchResults, ...restProps }; - const { api, filter, dataSourceState, backgroundReload, isFoldedByDefault, getId, setDataSourceState } = props; + const { + api, filter, dataSourceState, backgroundReload, + isFoldedByDefault, getId, setDataSourceState, + cascadeSelection, getRowOptions, rowOptions, + getChildCount, + } = props; const tree = useMemo(() => Tree.blank(props), [deps]); const [treeWithData, setTreeWithData] = useState(tree); + const [fullTree, setFullTree] = useState(treeWithData); + const prevFilter = usePrevious(filter); const prevDataSourceState = usePrevious(dataSourceState); const isReloadingRef = useRef(false); @@ -49,8 +57,9 @@ export function useLazyTreeStrategy( let currentTree = treeWithData; if (prevDataSourceState == null || shouldReloadData) { isReloadingRef.current = true; - if (!onlySearchWasUnset(prevDataSourceState, dataSourceState)) { - currentTree = treeWithData.clearStructure(); + currentTree = treeWithData.clearStructure(); + if (onlySearchWasUnset(prevDataSourceState, dataSourceState)) { + currentTree = fullTree; } completeReset = true; } @@ -68,6 +77,9 @@ export function useLazyTreeStrategy( .then(({ isUpdated, isOutdated, tree: newTree }) => { if (isUpdated && !isOutdated) { setTreeWithData(newTree); + const newFullTree = dataSourceState.search ? fullTree.mergeItems(newTree) : newTree; + setFullTree(newFullTree); + isReloadingRef.current = false; fingerprintRef.current = generateFingerprint(); } @@ -75,10 +87,91 @@ export function useLazyTreeStrategy( isReloadingRef.current = false; }); } - }, [prevDataSourceState, dataSourceState, prevFilter, filter, treeWithData, setTreeWithData, areMoreRowsNeeded]); + }, [ + prevDataSourceState, + dataSourceState, + prevFilter, + filter, + treeWithData, + setTreeWithData, + areMoreRowsNeeded, + ]); + + const loadMissingRecords = async (currentTree: ITree, id: TId, isChecked: boolean, isRoot: boolean) => { + const isImplicitMode = cascadeSelection === CascadeSelectionTypes.IMPLICIT; + + if (!cascadeSelection && !isRoot) { + return currentTree; + } + + const loadNestedLayersChildren = !isImplicitMode; + const parents = currentTree.getParentIdsRecursive(id); + const { tree: treeWithMissingRecords } = await loadMissing( + currentTree, + false, + { + // If cascadeSelection is implicit and the element is unchecked, it is necessary to load all children + // of all parents of the unchecked element to be checked explicitly. Only one layer of each parent should be loaded. + // Otherwise, should be loaded only checked element and all its nested children. + loadAllChildren: (itemId) => { + if (!cascadeSelection) { + return isChecked && isRoot; + } + + if (isImplicitMode) { + return itemId === ROOT_ID || parents.some((parent) => isEqual(parent, itemId)); + } + + // `isEqual` is used, because complex ids can be recreated after fetching of parents. + // So, they should be compared not by reference, but by value. + return isRoot || isEqual(itemId, id) || (dataSourceState.search && parents.some((parent) => isEqual(parent, itemId))); + }, + isLoadStrict: true, + }, + { search: null }, + loadNestedLayersChildren, + ); + + setFullTree(treeWithMissingRecords); + setTreeWithData(treeWithData.mergeItems(treeWithMissingRecords)); + + return treeWithMissingRecords; + }; + + const checkingService = useCheckingService({ + tree: fullTree, + dataSourceState, + setDataSourceState, + cascadeSelection, + getRowOptions, + rowOptions, + loadMissingRecords, + }); + + const focusService = useFocusService({ setDataSourceState }); + const selectingService = useSelectingService({ setDataSourceState }); + + const getTreeRowsStats = useCallback(() => { + const rootInfo = tree.getNodeInfo(undefined); + const rootCount = rootInfo.count; + let completeFlatListRowsCount = undefined; + if (!getChildCount && rootCount != null) { + completeFlatListRowsCount = rootCount; + } + + return { + completeFlatListRowsCount, + totalCount: rootInfo.totalCount ?? tree.getTotalRecursiveCount() ?? 0, + }; + }, [getChildCount, tree]); return { tree: treeWithData, fingerprint: fingerprintRef.current, + ...checkingService, + ...foldingService, + ...focusService, + ...selectingService, + getTreeRowsStats, }; } diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts index 70f1e89e49..fbab8a9417 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts @@ -71,14 +71,7 @@ export function usePlainTreeStrategy( const getTreeRowsStats = useCallback(() => { const rootInfo = tree.getNodeInfo(undefined); - /* TODO: For lazy list... - const rootCount = rootInfo.count; - if (!getChildCount && rootCount != null) { - completeFlatListRowsCount = rootCount; - } - - */ return { completeFlatListRowsCount: undefined, totalCount: rootInfo.totalCount ?? tree.getTotalRecursiveCount() ?? 0, From c5682569fd63487582f74719160aa43dffaf921a Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Fri, 15 Dec 2023 12:41:40 +0200 Subject: [PATCH 020/278] [useTree]: Moved logic of loadMissingRecords on check into useLazyCheckingService. --- .../lazyTree/useLazyCheckingService.ts | 84 +++++++++++++++++++ .../lazyTree/useLazyTreeStrategy.ts | 55 ++---------- 2 files changed, 93 insertions(+), 46 deletions(-) create mode 100644 uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyCheckingService.ts diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyCheckingService.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyCheckingService.ts new file mode 100644 index 0000000000..bcc5a0f348 --- /dev/null +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyCheckingService.ts @@ -0,0 +1,84 @@ +import { useMemo } from 'react'; +import { CascadeSelection, CascadeSelectionTypes, DataRowOptions, DataSourceState } from '../../../../../../../types'; +import { ITree, ROOT_ID } from '../../../'; +import { CheckingService, useCheckingService } from '../../services'; +import { LoadResult } from './useLoadData'; +import isEqual from 'lodash.isequal'; + +export interface UseLazyCheckingServiceProps { + tree: ITree; + onTreeUpdate: (tree: ITree) => void; + getParentId?: (item: TItem) => TId; + cascadeSelection?: CascadeSelection; + rowOptions?: DataRowOptions; + getRowOptions?(item: TItem, index?: number): DataRowOptions; + + dataSourceState: DataSourceState, + setDataSourceState?: React.Dispatch>>; + + loadMissingRecords?: ( + sourceTree: ITree, + abortInProgress: boolean, + options?: Partial<{ + loadAllChildren?(id: TId): boolean; + isLoadStrict?: boolean; + }>, + dataSourceState?: DataSourceState, + withNestedChildren?: boolean, + ) => Promise>; +} + +export function useLazyCheckingService(props: UseLazyCheckingServiceProps): CheckingService { + const { cascadeSelection, dataSourceState } = props; + const loadMissingRecords = async (currentTree: ITree, id: TId, isChecked: boolean, isRoot: boolean) => { + const isImplicitMode = cascadeSelection === CascadeSelectionTypes.IMPLICIT; + + if (!cascadeSelection && !isRoot) { + return currentTree; + } + + const loadNestedLayersChildren = !isImplicitMode; + const parents = currentTree.getParentIdsRecursive(id); + const { tree: treeWithMissingRecords } = await props.loadMissingRecords( + currentTree, + false, + { + // If cascadeSelection is implicit and the element is unchecked, it is necessary to load all children + // of all parents of the unchecked element to be checked explicitly. Only one layer of each parent should be loaded. + // Otherwise, should be loaded only checked element and all its nested children. + loadAllChildren: (itemId) => { + if (!cascadeSelection) { + return isChecked && isRoot; + } + + if (isImplicitMode) { + return itemId === ROOT_ID || parents.some((parent) => isEqual(parent, itemId)); + } + + // `isEqual` is used, because complex ids can be recreated after fetching of parents. + // So, they should be compared not by reference, but by value. + return isRoot || isEqual(itemId, id) || (dataSourceState.search && parents.some((parent) => isEqual(parent, itemId))); + }, + isLoadStrict: true, + }, + { search: null }, + loadNestedLayersChildren, + ); + + if (currentTree !== treeWithMissingRecords) { + props.onTreeUpdate(treeWithMissingRecords); + } + + return treeWithMissingRecords; + }; + + const checkingService = useCheckingService({ + ...props, + loadMissingRecords, + }); + + return useMemo( + () => ({ ...checkingService }), + [checkingService], + ); +} diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts index fbe991f2c8..1b0d408877 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts @@ -2,13 +2,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Tree } from '../../../Tree'; import { LazyTreeStrategyProps } from './types'; import { usePrevious } from '../../../../../../../hooks'; -import { CascadeSelectionTypes, DataSourceState } from '../../../../../../../types'; +import { DataSourceState } from '../../../../../../../types'; import isEqual from 'lodash.isequal'; import { generateFingerprint, onlySearchWasUnset, shouldRebuildTree } from './helpers'; -import { useCheckingService, useFocusService, useFoldingService, useSelectingService } from '../../services'; +import { useFocusService, useFoldingService, useSelectingService } from '../../services'; import { useLoadData } from './useLoadData'; -import { ITree, ROOT_ID } from '../../..'; +import { useLazyCheckingService } from './useLazyCheckingService'; export function useLazyTreeStrategy( { flattenSearchResults = true, ...restProps }: LazyTreeStrategyProps, @@ -97,55 +97,18 @@ export function useLazyTreeStrategy( areMoreRowsNeeded, ]); - const loadMissingRecords = async (currentTree: ITree, id: TId, isChecked: boolean, isRoot: boolean) => { - const isImplicitMode = cascadeSelection === CascadeSelectionTypes.IMPLICIT; - - if (!cascadeSelection && !isRoot) { - return currentTree; - } - - const loadNestedLayersChildren = !isImplicitMode; - const parents = currentTree.getParentIdsRecursive(id); - const { tree: treeWithMissingRecords } = await loadMissing( - currentTree, - false, - { - // If cascadeSelection is implicit and the element is unchecked, it is necessary to load all children - // of all parents of the unchecked element to be checked explicitly. Only one layer of each parent should be loaded. - // Otherwise, should be loaded only checked element and all its nested children. - loadAllChildren: (itemId) => { - if (!cascadeSelection) { - return isChecked && isRoot; - } - - if (isImplicitMode) { - return itemId === ROOT_ID || parents.some((parent) => isEqual(parent, itemId)); - } - - // `isEqual` is used, because complex ids can be recreated after fetching of parents. - // So, they should be compared not by reference, but by value. - return isRoot || isEqual(itemId, id) || (dataSourceState.search && parents.some((parent) => isEqual(parent, itemId))); - }, - isLoadStrict: true, - }, - { search: null }, - loadNestedLayersChildren, - ); - - setFullTree(treeWithMissingRecords); - setTreeWithData(treeWithData.mergeItems(treeWithMissingRecords)); - - return treeWithMissingRecords; - }; - - const checkingService = useCheckingService({ + const checkingService = useLazyCheckingService({ tree: fullTree, + onTreeUpdate: (newTree) => { + setFullTree(newTree); + setTreeWithData(treeWithData.mergeItems(newTree)); + }, dataSourceState, setDataSourceState, cascadeSelection, getRowOptions, rowOptions, - loadMissingRecords, + loadMissingRecords: loadMissing, }); const focusService = useFocusService({ setDataSourceState }); From 4df40e2c9df808aa4436eefd2f6a9e7dd6730acb Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Fri, 15 Dec 2023 17:05:02 +0200 Subject: [PATCH 021/278] [useTree]: Made useLazyTreeStrategy working as expected. --- .../tables/TableWithPinnedRows.example.tsx | 56 +++++++++++----- .../processing/views/dataRows/useBuildRows.ts | 5 +- .../processing/views/dataRows/useDataRows.ts | 22 +++++-- .../data/processing/views/tree/BaseTree.ts | 5 +- .../processing/views/tree/hooks/helpers.ts | 0 .../tree/hooks/services/useFoldingService.ts | 30 ++++----- .../views/tree/hooks/strategies/constants.ts | 2 +- .../views/tree/hooks/strategies/index.ts | 14 +++- .../tree/hooks/strategies/lazyTree/helpers.ts | 2 - .../tree/hooks/strategies/lazyTree/types.ts | 4 +- .../lazyTree/useLazyTreeStrategy.ts | 66 +++++++++++++------ .../hooks/strategies/lazyTree/useLoadData.ts | 8 ++- .../tree/hooks/strategies/plainTree/types.ts | 6 +- .../plainTree/usePlainTreeStrategy.ts | 24 +++---- .../views/tree/hooks/strategies/types.ts | 11 +++- .../data/processing/views/tree/hooks/types.ts | 39 ++++++++++- .../processing/views/tree/hooks/useTree.ts | 4 +- .../views/tree/hooks/useTreeStrategy.ts | 14 ++-- uui-core/src/hooks/usePrevious.ts | 2 +- 19 files changed, 216 insertions(+), 98 deletions(-) delete mode 100644 uui-core/src/data/processing/views/tree/hooks/helpers.ts diff --git a/app/src/docs/_examples/tables/TableWithPinnedRows.example.tsx b/app/src/docs/_examples/tables/TableWithPinnedRows.example.tsx index 87280280f8..19ebe3feae 100644 --- a/app/src/docs/_examples/tables/TableWithPinnedRows.example.tsx +++ b/app/src/docs/_examples/tables/TableWithPinnedRows.example.tsx @@ -1,12 +1,15 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { DataSourceState, DataColumnProps, useUuiContext, useLazyDataSource } from '@epam/uui-core'; +import React, { useMemo, useState } from 'react'; +import { DataSourceState, DataColumnProps, useUuiContext, useTree, useDataRows } from '@epam/uui-core'; import { Text, DataTable, Panel } from '@epam/uui'; import { Location } from '@epam/uui-docs'; import css from './TablesExamples.module.scss'; export default function TableWithPinnedRows() { const svc = useUuiContext(); - const [tableState, setTableState] = useState({}); + const [tableState, setTableState] = useState({ + topIndex: 0, + visibleCount: 20, + }); const locationsColumns: DataColumnProps[] = useMemo( () => [ { @@ -51,35 +54,56 @@ export default function TableWithPinnedRows() { [], ); - const locationsDS = useLazyDataSource({ + // const locationsDS = useLazyDataSource({ + // api: (request, ctx) => { + // const filter = { parentId: ctx?.parentId }; + // return svc.api.demo.locations({ ...request, filter }); + // }, + // getParentId: ({ parentId }) => parentId, + // getChildCount: (l) => l.childCount, + // backgroundReload: true, + // }, []); + + // useEffect(() => { + // return () => locationsDS.unsubscribeView(setTableState); + // }, [locationsDS]); + + // const view = locationsDS.useView(tableState, setTableState, { + // rowOptions: { + // // To make some row `pinned`, it is required to define `pin` function. + // // Parents and elements of the same level can be pinned. + // pin: (location) => location.value.type !== 'city', + // }, + // }); + + const { tree, ...restProps } = useTree({ + type: 'lazy', api: (request, ctx) => { const filter = { parentId: ctx?.parentId }; return svc.api.demo.locations({ ...request, filter }); }, + getId: ({ id }) => id, getParentId: ({ parentId }) => parentId, getChildCount: (l) => l.childCount, - backgroundReload: true, - }, []); - - useEffect(() => { - return () => locationsDS.unsubscribeView(setTableState); - }, [locationsDS]); - - const view = locationsDS.useView(tableState, setTableState, { + backgroundReload: false, + dataSourceState: tableState, + setDataSourceState: setTableState, rowOptions: { // To make some row `pinned`, it is required to define `pin` function. // Parents and elements of the same level can be pinned. pin: (location) => location.value.type !== 'city', - }, - }); + }, + }, []); + + const { getVisibleRows, getListProps } = useDataRows({ tree, ...restProps }); return ( diff --git a/uui-core/src/data/processing/views/dataRows/useBuildRows.ts b/uui-core/src/data/processing/views/dataRows/useBuildRows.ts index d1788b8166..ae75ad3838 100644 --- a/uui-core/src/data/processing/views/dataRows/useBuildRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useBuildRows.ts @@ -20,6 +20,7 @@ export interface UseBuildRowsProps extends FoldingSer isFlattenSearch?: boolean; getRowProps: (item: TItem, index: number) => DataRowProps; getLoadingRowProps: (id: any, index?: number, path?: DataRowPathItem[]) => DataRowProps; + isLoading?: boolean; } export function useBuildRows({ @@ -35,6 +36,7 @@ export function useBuildRows({ isFlattenSearch, getRowProps, getLoadingRowProps, + isLoading = false, }: UseBuildRowsProps) { const buildRows = () => { const rows: DataRowProps[] = []; @@ -118,7 +120,6 @@ export function useBuildRows({ if (missingCount > 0) { stats.hasMoreRows = true; } - // Append loading rows, stop at lastRowIndex (last row visible) while (rows.length < lastRowIndex && missingCount > 0) { const row = getLoadingRowProps('_loading_' + rows.length, rows.length, path); @@ -149,5 +150,5 @@ export function useBuildRows({ }; }; - return useMemo(() => buildRows(), [tree]); + return useMemo(() => buildRows(), [tree, dataSourceState.folded, isLoading]); } diff --git a/uui-core/src/data/processing/views/dataRows/useDataRows.ts b/uui-core/src/data/processing/views/dataRows/useDataRows.ts index b7fed61d40..2a14b79ea1 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRows.ts @@ -24,7 +24,6 @@ export interface UseDataRowsProps extends CheckingSer getParentId?(item: TItem): TId | undefined; cascadeSelection?: CascadeSelection; - lastRowIndex: number; selectAll?: boolean; // TODO: add solid type @@ -32,6 +31,9 @@ export interface UseDataRowsProps extends CheckingSer completeFlatListRowsCount: any; totalCount: number; } + + isFetching?: boolean; + isLoading?: boolean; } export function useDataRows( @@ -48,10 +50,21 @@ export function useDataRows( rowOptions, cascadeSelection, - lastRowIndex, getTreeRowsStats, + isLoading, + isFetching, } = props; + const lastRowIndex = useMemo( + () => { + const currentLastIndex = dataSourceState.topIndex + dataSourceState.visibleCount; + const actualCount = tree.getTotalRecursiveCount(); + if (actualCount != null && actualCount < currentLastIndex) return actualCount; + return currentLastIndex; + }, + [tree, dataSourceState.topIndex, dataSourceState.visibleCount], + ); + const isFlattenSearch = useMemo(() => dataSourceState.search && flattenSearchResults, []); const getEstimatedChildrenCount = useCallback((id: TId) => { @@ -132,6 +145,7 @@ export function useDataRows( getLoadingRowProps, isFolded: props.isFolded, handleOnFold: props.handleOnFold, + isLoading, }); const withPinnedRows = usePinnedRows({ @@ -187,9 +201,9 @@ export function useDataRows( exactRowsCount: rows.length, totalCount, selectAll, - isReloading: false, + isReloading: isFetching, }; - }, [rows.length, selectAll, getTreeRowsStats, stats.hasMoreRows, lastRowIndex]); + }, [rows.length, selectAll, getTreeRowsStats, stats.hasMoreRows, lastRowIndex, isFetching]); const getVisibleRows = useCallback( () => { diff --git a/uui-core/src/data/processing/views/tree/BaseTree.ts b/uui-core/src/data/processing/views/tree/BaseTree.ts index dca548e03d..c4aa04f265 100644 --- a/uui-core/src/data/processing/views/tree/BaseTree.ts +++ b/uui-core/src/data/processing/views/tree/BaseTree.ts @@ -148,12 +148,15 @@ export abstract class BaseTree implements ITree { } public getTotalRecursiveCount() { - let count = 0; + let count = undefined; for (const [, info] of this.nodeInfoById) { if (info.count == null) { // TBD: getTotalRecursiveCount() is used for totalCount, but we can't have correct count until all branches are loaded return null; } else { + if (count === undefined) { + count = 0; + } count += info.count; } } diff --git a/uui-core/src/data/processing/views/tree/hooks/helpers.ts b/uui-core/src/data/processing/views/tree/hooks/helpers.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/uui-core/src/data/processing/views/tree/hooks/services/useFoldingService.ts b/uui-core/src/data/processing/views/tree/hooks/services/useFoldingService.ts index 21460af085..eca6440599 100644 --- a/uui-core/src/data/processing/views/tree/hooks/services/useFoldingService.ts +++ b/uui-core/src/data/processing/views/tree/hooks/services/useFoldingService.ts @@ -5,7 +5,7 @@ import { idToKey, setObjectFlag } from '../../../helpers'; export interface UseFoldingServiceProps { getId: (item: TItem) => TId; dataSourceState: DataSourceState, - setDataSourceState?: React.Dispatch>>; + setDataSourceState: React.Dispatch>>; isFoldedByDefault?(item: TItem): boolean; } @@ -40,21 +40,19 @@ export function useFoldingService({ }, [isFoldedByDefault, dataSourceState?.search, dataSourceState.folded]); const handleOnFold = useCallback((rowProps: DataRowProps) => { - if (setDataSourceState) { - setDataSourceState((dsState) => { - const fold = !rowProps.isFolded; - const indexToScroll = rowProps.index - (rowProps.path?.length ?? 0); - const scrollTo: ScrollToConfig = fold && rowProps.isPinned - ? { index: indexToScroll, align: 'nearest' } - : dsState.scrollTo; - - return { - ...dsState, - scrollTo, - folded: setObjectFlag(dsState && dsState.folded, rowProps.rowKey, fold), - }; - }); - } + setDataSourceState((dsState) => { + const fold = !rowProps.isFolded; + const indexToScroll = rowProps.index - (rowProps.path?.length ?? 0); + const scrollTo: ScrollToConfig = fold && rowProps.isPinned + ? { index: indexToScroll, align: 'nearest' } + : dsState.scrollTo; + + return { + ...dsState, + scrollTo, + folded: setObjectFlag(dsState && dsState.folded, rowProps.rowKey, fold), + }; + }); }, [setDataSourceState]); return useMemo( diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/constants.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/constants.ts index 1c7827c82a..eeac8ab0bd 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/constants.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/constants.ts @@ -1,5 +1,5 @@ export const STRATEGIES = { plain: 'plain', - async: 'async', + // async: 'async', lazy: 'lazy', } as const; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/index.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/index.ts index 8ec6585444..35d92ac5d0 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/index.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/index.ts @@ -1,7 +1,19 @@ +import { UseTreeResult } from '../types'; +import { useLazyTreeStrategy } from './lazyTree/useLazyTreeStrategy'; import { usePlainTreeStrategy } from './plainTree'; +import { Strategies, TreeStrategyProps } from './types'; export type { PlainTreeStrategyProps } from './plainTree/types'; -export const strategies = { +export type ExtractTreeStrategyProps = Extract, { type: T }>; + +export type TreeStrategyHook = + ( + props: ExtractTreeStrategyProps, + deps: any[], + ) => UseTreeResult; + +export const strategies: { plain: TreeStrategyHook<'plain'>, lazy: TreeStrategyHook<'lazy'> } = { plain: usePlainTreeStrategy, + lazy: useLazyTreeStrategy, }; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/helpers.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/helpers.ts index d9b1253750..acaf9937dc 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/helpers.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/helpers.ts @@ -27,5 +27,3 @@ export const onlySearchWasUnset = (prevValue: DataSourceState new Date().toISOString(); diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/types.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/types.ts index c58a8d7870..34c15c092f 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/types.ts @@ -1,8 +1,8 @@ import { LazyDataSourceApi } from '../../../../../../../types'; import { STRATEGIES } from '../constants'; -import { TreeStrategyProps } from '../types'; +import { CommonTreeStrategyProps } from '../types'; -export type LazyTreeStrategyProps = TreeStrategyProps & { +export type LazyTreeStrategyProps = CommonTreeStrategyProps & { type: typeof STRATEGIES.lazy, api: LazyDataSourceApi; filter?: TFilter; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts index 1b0d408877..33601f039d 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts @@ -1,19 +1,20 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Tree } from '../../../Tree'; import { LazyTreeStrategyProps } from './types'; import { usePrevious } from '../../../../../../../hooks'; import { DataSourceState } from '../../../../../../../types'; import isEqual from 'lodash.isequal'; -import { generateFingerprint, onlySearchWasUnset, shouldRebuildTree } from './helpers'; +import { onlySearchWasUnset, shouldRebuildTree } from './helpers'; import { useFocusService, useFoldingService, useSelectingService } from '../../services'; import { useLoadData } from './useLoadData'; import { useLazyCheckingService } from './useLazyCheckingService'; +import { UseTreeResult } from '../../types'; export function useLazyTreeStrategy( { flattenSearchResults = true, ...restProps }: LazyTreeStrategyProps, deps: any[], -) { +): UseTreeResult { const props = { flattenSearchResults, ...restProps }; const { api, filter, dataSourceState, backgroundReload, @@ -28,12 +29,19 @@ export function useLazyTreeStrategy( const prevFilter = usePrevious(filter); const prevDataSourceState = usePrevious(dataSourceState); - const isReloadingRef = useRef(false); - const fingerprintRef = useRef(); + + const [isFetching, setIsFetching] = useState(false); + const [isLoading, setIsLoading] = useState(false); const actualRowsCount = useMemo(() => treeWithData.getTotalRecursiveCount() ?? 0, [treeWithData]); - const lastRecordIndex = useMemo( + useEffect(() => { + if (dataSourceState.topIndex === undefined || dataSourceState.visibleCount === undefined) { + setDataSourceState({ topIndex: dataSourceState.topIndex ?? 0, visibleCount: dataSourceState?.visibleCount ?? 20 }); + } + }, [dataSourceState]); + + const lastRowIndex = useMemo( () => dataSourceState.topIndex + dataSourceState.visibleCount, [dataSourceState.topIndex, dataSourceState.visibleCount], ); @@ -42,12 +50,20 @@ export function useLazyTreeStrategy( const isFetchPositionAndAmountChanged = prevValue?.topIndex !== newValue?.topIndex || prevValue?.visibleCount !== newValue?.visibleCount; - return isFetchPositionAndAmountChanged && lastRecordIndex > actualRowsCount; - }, [lastRecordIndex, actualRowsCount]); + return isFetchPositionAndAmountChanged && lastRowIndex > actualRowsCount; + }, [lastRowIndex, actualRowsCount]); const foldingService = useFoldingService({ dataSourceState, isFoldedByDefault, getId, setDataSourceState }); - const { loadMissing } = useLoadData({ api, filter, dataSourceState, isFolded: foldingService.isFolded }); + const { loadMissing } = useLoadData({ + api, + filter, + dataSourceState, + isFolded: foldingService.isFolded, + fetchStrategy: props.fetchStrategy, + flattenSearchResults: props.flattenSearchResults, + getChildCount: props.getChildCount, + }); useEffect(() => { let completeReset = false; @@ -56,7 +72,7 @@ export function useLazyTreeStrategy( let currentTree = treeWithData; if (prevDataSourceState == null || shouldReloadData) { - isReloadingRef.current = true; + setIsFetching(true); currentTree = treeWithData.clearStructure(); if (onlySearchWasUnset(prevDataSourceState, dataSourceState)) { currentTree = fullTree; @@ -67,11 +83,14 @@ export function useLazyTreeStrategy( const shouldShowPlacehodlers = !shouldReloadData || (shouldReloadData && !backgroundReload); - if ((completeReset && shouldShowPlacehodlers) || isFoldingChanged) { - fingerprintRef.current = generateFingerprint(); + const moreRowsNeeded = areMoreRowsNeeded(prevDataSourceState, dataSourceState); + if ((completeReset && shouldShowPlacehodlers) || isFoldingChanged || moreRowsNeeded) { + if (currentTree !== treeWithData) { + setTreeWithData(currentTree); + } + setIsLoading(true); } - const moreRowsNeeded = areMoreRowsNeeded(prevDataSourceState, dataSourceState); if (completeReset || isFoldingChanged || moreRowsNeeded) { loadMissing(currentTree, completeReset) .then(({ isUpdated, isOutdated, tree: newTree }) => { @@ -79,18 +98,14 @@ export function useLazyTreeStrategy( setTreeWithData(newTree); const newFullTree = dataSourceState.search ? fullTree.mergeItems(newTree) : newTree; setFullTree(newFullTree); - - isReloadingRef.current = false; - fingerprintRef.current = generateFingerprint(); } }).finally(() => { - isReloadingRef.current = false; + setIsFetching(false); + setIsLoading(false); }); } }, [ - prevDataSourceState, dataSourceState, - prevFilter, filter, treeWithData, setTreeWithData, @@ -112,7 +127,7 @@ export function useLazyTreeStrategy( }); const focusService = useFocusService({ setDataSourceState }); - const selectingService = useSelectingService({ setDataSourceState }); + const selectingService = useSelectingService({ setDataSourceState }); const getTreeRowsStats = useCallback(() => { const rootInfo = tree.getNodeInfo(undefined); @@ -130,11 +145,20 @@ export function useLazyTreeStrategy( return { tree: treeWithData, - fingerprint: fingerprintRef.current, + dataSourceState, + isFoldedByDefault, + getId, + cascadeSelection, + getRowOptions, + rowOptions, + getChildCount, ...checkingService, ...foldingService, ...focusService, ...selectingService, getTreeRowsStats, + + isFetching, + isLoading, }; } diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts index c3f688283c..ca83bbc757 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts @@ -7,6 +7,9 @@ export interface UseLoadDataProps { filter?: TFilter; dataSourceState: DataSourceState; isFolded: (item: TItem) => boolean; + fetchStrategy?: 'sequential' | 'parallel'; + flattenSearchResults?: boolean; + getChildCount?(item: TItem): number; } export interface LoadResult { @@ -59,7 +62,10 @@ export function useLoadData( { ...props, ...options, - isFolded, + isFolded: (item) => { + console.log('item', item, isFolded(item)); + return isFolded(item); + }, api, filter: { ...filter }, }, diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts index 995f2090d6..458b421ec2 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/types.ts @@ -1,10 +1,10 @@ import { SortingOption } from '../../../../../../../types'; import { ITree } from '../../../ITree'; import { STRATEGIES } from '../constants'; -import { TreeStrategyProps } from '../types'; +import { CommonTreeStrategyProps } from '../types'; -export type PlainTreeStrategyProps = TreeStrategyProps & { - type?: typeof STRATEGIES.plain, +export type PlainTreeStrategyProps = CommonTreeStrategyProps & { + type: typeof STRATEGIES.plain, items: TItem[], tree?: ITree, diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts index fbab8a9417..f4f6dc00a0 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts @@ -1,15 +1,16 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { PlainTreeStrategyProps } from './types'; import { useCreateTree } from './useCreateTree'; import { useFilterTree } from './useFilterTree'; import { useSearchTree } from './useSearchTree'; import { useSortTree } from './useSortTree'; import { useCheckingService, useFocusService, useFoldingService, useSelectingService } from '../../services'; +import { UseTreeResult } from '../../types'; export function usePlainTreeStrategy( { sortSearchByRelevance = true, ...restProps }: PlainTreeStrategyProps, deps: any[], -) { +): UseTreeResult { const props = { ...restProps, sortSearchByRelevance }; const fullTree = useCreateTree(props, deps); @@ -27,6 +28,12 @@ export function usePlainTreeStrategy( cascadeSelection, } = props; + useEffect(() => { + if (dataSourceState.topIndex === undefined || dataSourceState.visibleCount === undefined) { + setDataSourceState({ topIndex: dataSourceState.topIndex ?? 0, visibleCount: dataSourceState?.visibleCount ?? 20 }); + } + }, [dataSourceState]); + const filteredTree = useFilterTree( { tree: fullTree, getFilter, dataSourceState }, [fullTree], @@ -58,17 +65,6 @@ export function usePlainTreeStrategy( const selectingService = useSelectingService({ setDataSourceState }); - const lastRowIndex = useMemo( - () => { - const currentLastIndex = dataSourceState.topIndex + dataSourceState.visibleCount; - const actualCount = tree.getTotalRecursiveCount() ?? 0; - - if (actualCount < currentLastIndex) return actualCount; - return currentLastIndex; - }, - [tree, dataSourceState.topIndex, dataSourceState.visibleCount], - ); - const getTreeRowsStats = useCallback(() => { const rootInfo = tree.getNodeInfo(undefined); @@ -83,7 +79,6 @@ export function usePlainTreeStrategy( tree, rowOptions, getRowOptions, - lastRowIndex, getId, dataSourceState, getTreeRowsStats, @@ -96,7 +91,6 @@ export function usePlainTreeStrategy( tree, rowOptions, getRowOptions, - lastRowIndex, dataSourceState, getTreeRowsStats, checkingService, diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts index 3462948f1d..a367cf81be 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/types.ts @@ -1,13 +1,15 @@ import { CascadeSelection, DataRowOptions, DataSourceState } from '../../../../../../types'; import { STRATEGIES } from './constants'; +import { LazyTreeStrategyProps } from './lazyTree/types'; +import { PlainTreeStrategyProps } from './plainTree/types'; export type Strategies = typeof STRATEGIES[keyof typeof STRATEGIES]; -export type TreeStrategyProps = { +export type CommonTreeStrategyProps = { dataSourceState: DataSourceState; setDataSourceState: React.Dispatch>>; - getId?(item: TItem): TId; + getId(item: TItem): TId; getParentId?(item: TItem): TId | undefined; complexIds?: boolean; @@ -19,3 +21,8 @@ export type TreeStrategyProps = { cascadeSelection?: CascadeSelection; }; + +export type TreeStrategyProps = ( + PlainTreeStrategyProps + | LazyTreeStrategyProps +); diff --git a/uui-core/src/data/processing/views/tree/hooks/types.ts b/uui-core/src/data/processing/views/tree/hooks/types.ts index 528e016ad7..ae74b7da05 100644 --- a/uui-core/src/data/processing/views/tree/hooks/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/types.ts @@ -1,4 +1,39 @@ +import { ITree } from '../ITree'; +import { CheckingService, FocusService, FoldingService, SelectingService } from './services'; +import { TreeStrategyProps } from './strategies/types'; +import { CascadeSelection, DataRowOptions, DataSourceState } from '../../../../../types'; import { PlainTreeStrategyProps } from './strategies'; -export type UseTreeStrategyProps = (PlainTreeStrategyProps); -export type UseTreeProps = {} & UseTreeStrategyProps; +type PlainTreeStrategyPropsWithOptionalType = Omit, 'type'> & { type?: 'plain' }; + +export type UseTreeStrategyProps = Exclude, { type: 'plain' }> +| PlainTreeStrategyPropsWithOptionalType; + +export type UseTreeProps = UseTreeStrategyProps; + +export interface UseTreeResult extends + CheckingService, FoldingService, FocusService, SelectingService { + + tree: ITree; + getTreeRowsStats: () => { + completeFlatListRowsCount: number; + totalCount: number; + }; + + dataSourceState: DataSourceState; + + getId(item: TItem): TId; + getParentId?(item: TItem): TId | undefined; + complexIds?: boolean; + + rowOptions?: DataRowOptions; + getRowOptions?(item: TItem, index?: number): DataRowOptions; + + isFoldedByDefault?(item: TItem): boolean; + getChildCount?(item: TItem): number; + + cascadeSelection?: CascadeSelection; + + isFetching?: boolean; + isLoading?: boolean; +} diff --git a/uui-core/src/data/processing/views/tree/hooks/useTree.ts b/uui-core/src/data/processing/views/tree/hooks/useTree.ts index d3cd597f2e..999cad8952 100644 --- a/uui-core/src/data/processing/views/tree/hooks/useTree.ts +++ b/uui-core/src/data/processing/views/tree/hooks/useTree.ts @@ -1,7 +1,7 @@ import { useTreeStrategy } from './useTreeStrategy'; -import { UseTreeProps } from './types'; +import { UseTreeProps, UseTreeResult } from './types'; -export function useTree(props: UseTreeProps, deps: any[]) { +export function useTree(props: UseTreeProps, deps: any[]): UseTreeResult { const { tree, ...restProps } = useTreeStrategy(props, deps); return { tree, ...restProps }; diff --git a/uui-core/src/data/processing/views/tree/hooks/useTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/useTreeStrategy.ts index 651858bd9e..e1b0b64522 100644 --- a/uui-core/src/data/processing/views/tree/hooks/useTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/useTreeStrategy.ts @@ -1,15 +1,17 @@ import { useMemo } from 'react'; -import { strategies } from './strategies'; +import { ExtractTreeStrategyProps, TreeStrategyHook, strategies } from './strategies'; import { UseTreeStrategyProps } from './types'; -export function useTreeStrategy({ type = 'plain', ...props }: UseTreeStrategyProps, deps: any[]) { - const useStrategy = useMemo( - () => strategies[type], +export function useTreeStrategy(props: UseTreeStrategyProps, deps: any[]) { + const { type = 'plain' } = props; + + const useStrategy: TreeStrategyHook = useMemo( + () => strategies[type] as TreeStrategyHook, [type], ); - const tree = useStrategy( - { ...props, type }, + const tree = useStrategy( + { ...props, type } as ExtractTreeStrategyProps, [type, ...deps], ); diff --git a/uui-core/src/hooks/usePrevious.ts b/uui-core/src/hooks/usePrevious.ts index 8d41c09ed5..b5760e8662 100644 --- a/uui-core/src/hooks/usePrevious.ts +++ b/uui-core/src/hooks/usePrevious.ts @@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react'; import isEqual from 'lodash.isequal'; export function usePrevious(value: T) { - const previousValueRef = useRef(value); + const previousValueRef = useRef(null); useEffect(() => { if (!isEqual(previousValueRef.current, value)) { From 47cd274de45670a2081817ee4fa000a43a228b0b Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Fri, 15 Dec 2023 17:11:34 +0200 Subject: [PATCH 022/278] [useTree]: Removed console.log. --- .../views/tree/hooks/strategies/lazyTree/useLoadData.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts index ca83bbc757..0ccdfd3134 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLoadData.ts @@ -62,10 +62,7 @@ export function useLoadData( { ...props, ...options, - isFolded: (item) => { - console.log('item', item, isFolded(item)); - return isFolded(item); - }, + isFolded, api, filter: { ...filter }, }, From e020e686fbecf5caa87b8c18f75c133d747200a6 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Fri, 15 Dec 2023 18:45:06 +0200 Subject: [PATCH 023/278] [useTree]: Fixed check functionality in useTree. --- .../src/data/processing/views/dataRows/useBuildRows.ts | 2 +- uui-core/src/data/processing/views/dataRows/useDataRows.ts | 1 - .../views/tree/hooks/services/useCheckingService.ts | 3 +-- .../hooks/strategies/plainTree/usePlainTreeStrategy.ts | 7 +++++++ 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/uui-core/src/data/processing/views/dataRows/useBuildRows.ts b/uui-core/src/data/processing/views/dataRows/useBuildRows.ts index ae75ad3838..c0ebd85465 100644 --- a/uui-core/src/data/processing/views/dataRows/useBuildRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useBuildRows.ts @@ -150,5 +150,5 @@ export function useBuildRows({ }; }; - return useMemo(() => buildRows(), [tree, dataSourceState.folded, isLoading]); + return useMemo(() => buildRows(), [tree, dataSourceState.folded, isLoading, dataSourceState.checked]); } diff --git a/uui-core/src/data/processing/views/dataRows/useDataRows.ts b/uui-core/src/data/processing/views/dataRows/useDataRows.ts index 2a14b79ea1..256fbd5ada 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRows.ts @@ -210,7 +210,6 @@ export function useDataRows( const from = dataSourceState.topIndex; const count = dataSourceState.visibleCount; const visibleRows = withPinnedRows(rows.slice(from, from + count)); - if (stats.hasMoreRows) { const listProps = getListProps(); // We don't run rebuild rows on scrolling. We rather wait for the next load to happen. diff --git a/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts b/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts index dc39a87859..97b641f0f0 100644 --- a/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts +++ b/uui-core/src/data/processing/views/tree/hooks/services/useCheckingService.ts @@ -73,7 +73,6 @@ export function useCheckingService( }: UseCheckingServiceProps, ): CheckingService { const checked = dataSourceState.checked ?? []; - const checkingInfoById = useMemo( () => getCheckingInfo(checked, tree, getParentId), [tree, checked], @@ -129,7 +128,7 @@ export function useCheckingService( const isChecked = !rowProps.isChecked; handleCheck(isChecked, id); - }, []); + }, [handleCheck]); return useMemo( () => ({ diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts index f4f6dc00a0..a2c11e4832 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/plainTree/usePlainTreeStrategy.ts @@ -55,6 +55,8 @@ export function usePlainTreeStrategy( setDataSourceState, cascadeSelection, getParentId, + rowOptions, + getRowOptions, }); const foldingService = useFoldingService({ @@ -79,6 +81,8 @@ export function usePlainTreeStrategy( tree, rowOptions, getRowOptions, + getChildCount, + getParentId, getId, dataSourceState, getTreeRowsStats, @@ -93,6 +97,9 @@ export function usePlainTreeStrategy( getRowOptions, dataSourceState, getTreeRowsStats, + getChildCount, + getParentId, + getId, checkingService, selectingService, focusService, From ffd7657d272c5d0dcd3a22f5360aab44aab21101 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Fri, 15 Dec 2023 19:38:33 +0200 Subject: [PATCH 024/278] [useTree]: fixed cascade selection with search. --- uui-core/src/data/processing/views/dataRows/useBuildRows.ts | 2 +- .../src/data/processing/views/dataRows/useDataRowProps.ts | 1 + uui-core/src/data/processing/views/dataRows/useDataRows.ts | 6 ++++-- .../tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts | 1 + uui-core/src/data/processing/views/tree/hooks/types.ts | 1 + 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/uui-core/src/data/processing/views/dataRows/useBuildRows.ts b/uui-core/src/data/processing/views/dataRows/useBuildRows.ts index c0ebd85465..61034722c5 100644 --- a/uui-core/src/data/processing/views/dataRows/useBuildRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useBuildRows.ts @@ -150,5 +150,5 @@ export function useBuildRows({ }; }; - return useMemo(() => buildRows(), [tree, dataSourceState.folded, isLoading, dataSourceState.checked]); + return useMemo(() => buildRows(), [tree, dataSourceState.folded, isLoading, dataSourceState.checked, isFlattenSearch]); } diff --git a/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts b/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts index c7a4d6ff5d..f6043515e6 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRowProps.ts @@ -65,6 +65,7 @@ export function useDataRowProps( }, [ getRowOptions, rowOptions, + isFlattenSearch, getEstimatedChildrenCount, dataSourceState.focusedIndex, dataSourceState.selectedId, diff --git a/uui-core/src/data/processing/views/dataRows/useDataRows.ts b/uui-core/src/data/processing/views/dataRows/useDataRows.ts index 256fbd5ada..c0fb1b0122 100644 --- a/uui-core/src/data/processing/views/dataRows/useDataRows.ts +++ b/uui-core/src/data/processing/views/dataRows/useDataRows.ts @@ -64,8 +64,10 @@ export function useDataRows( }, [tree, dataSourceState.topIndex, dataSourceState.visibleCount], ); - - const isFlattenSearch = useMemo(() => dataSourceState.search && flattenSearchResults, []); + const isFlattenSearch = useMemo( + () => dataSourceState.search && flattenSearchResults, + [dataSourceState.search, flattenSearchResults], + ); const getEstimatedChildrenCount = useCallback((id: TId) => { if (id === undefined) return undefined; diff --git a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts index 33601f039d..585c2b0010 100644 --- a/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts +++ b/uui-core/src/data/processing/views/tree/hooks/strategies/lazyTree/useLazyTreeStrategy.ts @@ -152,6 +152,7 @@ export function useLazyTreeStrategy( getRowOptions, rowOptions, getChildCount, + flattenSearchResults, ...checkingService, ...foldingService, ...focusService, diff --git a/uui-core/src/data/processing/views/tree/hooks/types.ts b/uui-core/src/data/processing/views/tree/hooks/types.ts index ae74b7da05..5f80bd8dfa 100644 --- a/uui-core/src/data/processing/views/tree/hooks/types.ts +++ b/uui-core/src/data/processing/views/tree/hooks/types.ts @@ -33,6 +33,7 @@ export interface UseTreeResult extends getChildCount?(item: TItem): number; cascadeSelection?: CascadeSelection; + flattenSearchResults?: boolean; isFetching?: boolean; isLoading?: boolean; From ed28189406b04d45f2946c7d82aa6b62b8b9474c Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Mon, 18 Dec 2023 10:33:19 +0200 Subject: [PATCH 025/278] [useTree]: Fixed problem with usePrevious. --- uui-core/src/data/processing/hooks/useView.ts | 2 +- uui-core/src/hooks/usePrevious.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uui-core/src/data/processing/hooks/useView.ts b/uui-core/src/data/processing/hooks/useView.ts index aee9a38ffb..b3fa03555c 100644 --- a/uui-core/src/data/processing/hooks/useView.ts +++ b/uui-core/src/data/processing/hooks/useView.ts @@ -10,7 +10,7 @@ export function useView>(null); const prevDeps = usePrevious(deps); - const isDepsChanged = prevDeps.length !== deps.length || prevDeps.some((devVal, index) => devVal !== deps[index]); + const isDepsChanged = prevDeps?.length !== deps.length || prevDeps?.some((devVal, index) => devVal !== deps[index]); if (viewRef.current === null || isDepsChanged) { viewRef.current = create(); diff --git a/uui-core/src/hooks/usePrevious.ts b/uui-core/src/hooks/usePrevious.ts index b5760e8662..83df08b8d1 100644 --- a/uui-core/src/hooks/usePrevious.ts +++ b/uui-core/src/hooks/usePrevious.ts @@ -1,7 +1,7 @@ import { useEffect, useRef } from 'react'; import isEqual from 'lodash.isequal'; -export function usePrevious(value: T) { +export function usePrevious(value: T): T | null { const previousValueRef = useRef(null); useEffect(() => { From 8e3150e753f90aa914001c700bd3dbe9e6216dc2 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Mon, 18 Dec 2023 13:04:48 +0200 Subject: [PATCH 026/278] [useTree]: Added usePatchTree to ProductsTableDemo. --- .../productsTable/ProductsTableDemo.tsx | 60 +++++++++++++------ app/src/sandbox/productsTable/usePatchTree.ts | 23 +++++++ .../processing/views/dataRows/useBuildRows.ts | 2 +- .../views/dataRows/useDataRowProps.ts | 13 ++-- .../processing/views/dataRows/useDataRows.ts | 7 ++- .../views/dataRows/useUpdateRowProps.ts | 17 ++++++ 6 files changed, 94 insertions(+), 28 deletions(-) create mode 100644 app/src/sandbox/productsTable/usePatchTree.ts create mode 100644 uui-core/src/data/processing/views/dataRows/useUpdateRowProps.ts diff --git a/app/src/sandbox/productsTable/ProductsTableDemo.tsx b/app/src/sandbox/productsTable/ProductsTableDemo.tsx index 9afd1766ff..09726f6eca 100644 --- a/app/src/sandbox/productsTable/ProductsTableDemo.tsx +++ b/app/src/sandbox/productsTable/ProductsTableDemo.tsx @@ -1,12 +1,14 @@ import { DataTable, useForm, Panel, Button, FlexCell, FlexRow, FlexSpacer } from '@epam/loveship'; -import React from 'react'; -import { Metadata, useList, useUuiContext, UuiContexts } from '@epam/uui-core'; +import React, { useCallback } from 'react'; +import { DataSourceState, Metadata, useDataRows, useTree, useUuiContext, UuiContexts } from '@epam/uui-core'; import { Product } from '@epam/uui-docs'; import type { TApi } from '../../data'; import { productColumns } from './columns'; import { ReactComponent as undoIcon } from '@epam/assets/icons/common/content-edit_undo-18.svg'; import { ReactComponent as redoIcon } from '@epam/assets/icons/common/content-edit_redo-18.svg'; +import { ReactComponent as add } from '@epam/assets/icons/common/action-add-12.svg'; import css from './ProductsTableDemo.module.scss'; +import { usePatchTree } from './usePatchTree'; interface FormState { items: Record; @@ -28,12 +30,13 @@ const metadata: Metadata = { }; let savedValue: FormState = { items: {} }; +let lastId = -1; export function ProductsTableDemo() { const svc = useUuiContext(); const { - lens, save, isChanged, revert, undo, canUndo, redo, canRedo, + lens, save, isChanged, revert, undo, canUndo, redo, canRedo, value: updatedRows, setValue, } = useForm({ value: savedValue, onSave: async (value) => { @@ -43,33 +46,54 @@ export function ProductsTableDemo() { getMetadata: () => metadata, }); - const [tableState, setTableState] = React.useState({}); + const [tableState, setTableState] = React.useState({ + topIndex: 0, + visibleCount: 60, + }); - const { rows, listProps } = useList( - { - type: 'lazy', - api: svc.api.demo.products, - getId: (i) => i.ProductID, - getRowOptions: (product) => ({ ...lens.prop('items').prop(product.ProductID).default(product).toProps() }), - listState: tableState, - setListState: setTableState, - backgroundReload: true, - }, - [], - ); + const insertTask = useCallback(() => { + const product: Product = { ProductID: lastId-- } as Product; + + setValue((currentValue) => { + return { ...currentValue, items: { [product.ProductID]: product, ...currentValue.items } }; + }); + }, [setValue]); + + const { tree, ...restProps } = useTree({ + type: 'lazy', + api: svc.api.demo.products, + getId: (i) => i.ProductID, + getRowOptions: (product) => ({ ...lens.prop('items').prop(product.ProductID).default(product).toProps() }), + dataSourceState: tableState, + setDataSourceState: setTableState, + backgroundReload: true, + }, []); + + const patchedTree = usePatchTree({ + tree, + additionalRows: Object.values(updatedRows.items), + }); + + const { getVisibleRows, getListProps } = useDataRows({ tree: patchedTree, ...restProps }); return ( + + + + ); - } + }; - renderDemo() { - const icon = this.state.selectedIcon.icon; + const renderDemo = () => { + const icon = state.selectedIcon.icon; return ( @@ -160,39 +165,43 @@ export class IconsDoc extends React.Component { {} } icon={ icon } /> -