mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-14 03:26:47 +00:00
feat(customHomePage/hierarchy): add support of pagination for root level nodes (#14485)
Co-authored-by: Chris Collins <chriscollins3456@gmail.com>
This commit is contained in:
parent
85fa97623b
commit
8234cbec8f
@ -1,13 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { ChildrenLoaderContextType } from '@app/homeV3/modules/hierarchyViewModule/childrenLoader/types';
|
import { ChildrenLoaderContextType } from '@app/homeV3/modules/hierarchyViewModule/childrenLoader/types';
|
||||||
import { DEFAULT_NUMBER_OF_CHILDREN_TO_LOAD } from '@app/homeV3/modules/hierarchyViewModule/treeView/constants';
|
import { DEFAULT_LOAD_BATCH_SIZE } from '@app/homeV3/modules/hierarchyViewModule/treeView/constants';
|
||||||
|
|
||||||
const DEFAULT_CONTEXT_STATE: ChildrenLoaderContextType = {
|
const DEFAULT_CONTEXT_STATE: ChildrenLoaderContextType = {
|
||||||
get: () => undefined,
|
get: () => undefined,
|
||||||
upsert: () => {},
|
upsert: () => {},
|
||||||
onLoad: () => {},
|
onLoad: () => {},
|
||||||
maxNumberOfChildrenToLoad: DEFAULT_NUMBER_OF_CHILDREN_TO_LOAD,
|
maxNumberOfChildrenToLoad: DEFAULT_LOAD_BATCH_SIZE,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChildrenLoaderContext = React.createContext<ChildrenLoaderContextType>(DEFAULT_CONTEXT_STATE);
|
const ChildrenLoaderContext = React.createContext<ChildrenLoaderContextType>(DEFAULT_CONTEXT_STATE);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React, { useCallback, useState } from 'react';
|
|||||||
|
|
||||||
import ChildrenLoaderContext from '@app/homeV3/modules/hierarchyViewModule/childrenLoader/context/ChildrenLoaderContext';
|
import ChildrenLoaderContext from '@app/homeV3/modules/hierarchyViewModule/childrenLoader/context/ChildrenLoaderContext';
|
||||||
import { ChildrenLoaderMetadata, MetadataMap } from '@app/homeV3/modules/hierarchyViewModule/childrenLoader/types';
|
import { ChildrenLoaderMetadata, MetadataMap } from '@app/homeV3/modules/hierarchyViewModule/childrenLoader/types';
|
||||||
import { DEFAULT_NUMBER_OF_CHILDREN_TO_LOAD } from '@app/homeV3/modules/hierarchyViewModule/treeView/constants';
|
import { DEFAULT_LOAD_BATCH_SIZE } from '@app/homeV3/modules/hierarchyViewModule/treeView/constants';
|
||||||
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -13,7 +13,7 @@ interface Props {
|
|||||||
export function ChildrenLoaderProvider({
|
export function ChildrenLoaderProvider({
|
||||||
children,
|
children,
|
||||||
onLoadFinished,
|
onLoadFinished,
|
||||||
maxNumberOfChildrenToLoad = DEFAULT_NUMBER_OF_CHILDREN_TO_LOAD,
|
maxNumberOfChildrenToLoad = DEFAULT_LOAD_BATCH_SIZE,
|
||||||
}: React.PropsWithChildren<Props>) {
|
}: React.PropsWithChildren<Props>) {
|
||||||
const [metadataMap, setMetadataMap] = useState<MetadataMap>({});
|
const [metadataMap, setMetadataMap] = useState<MetadataMap>({});
|
||||||
|
|
||||||
|
|||||||
@ -12,10 +12,15 @@ import useSelectableDomainTree from '@app/homeV3/modules/hierarchyViewModule/com
|
|||||||
import { useHierarchyFormContext } from '@app/homeV3/modules/hierarchyViewModule/components/form/HierarchyFormContext';
|
import { useHierarchyFormContext } from '@app/homeV3/modules/hierarchyViewModule/components/form/HierarchyFormContext';
|
||||||
import TreeView from '@app/homeV3/modules/hierarchyViewModule/treeView/TreeView';
|
import TreeView from '@app/homeV3/modules/hierarchyViewModule/treeView/TreeView';
|
||||||
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
||||||
import { getTopLevelSelectedValuesFromTree } from '@app/homeV3/modules/hierarchyViewModule/treeView/utils';
|
import {
|
||||||
|
getOutOfTreeSelectedValues,
|
||||||
|
getTopLevelSelectedValuesFromTree,
|
||||||
|
} from '@app/homeV3/modules/hierarchyViewModule/treeView/utils';
|
||||||
|
|
||||||
const Wrapper = styled.div``;
|
const Wrapper = styled.div``;
|
||||||
|
|
||||||
|
const LOAD_BATCH_SIZE = 25;
|
||||||
|
|
||||||
export default function DomainsSelectableTreeView() {
|
export default function DomainsSelectableTreeView() {
|
||||||
const form = Form.useFormInstance();
|
const form = Form.useFormInstance();
|
||||||
const {
|
const {
|
||||||
@ -24,12 +29,23 @@ export default function DomainsSelectableTreeView() {
|
|||||||
|
|
||||||
const { parentValues, addParentValue, removeParentValue } = useParentValuesToLoadChildren();
|
const { parentValues, addParentValue, removeParentValue } = useParentValuesToLoadChildren();
|
||||||
|
|
||||||
const { tree, selectedValues, setSelectedValues, loading } = useSelectableDomainTree(initialSelectedValues);
|
const {
|
||||||
|
tree,
|
||||||
|
selectedValues,
|
||||||
|
setSelectedValues,
|
||||||
|
loading,
|
||||||
|
loadMoreRootNodes,
|
||||||
|
rootDomainsMoreLoading,
|
||||||
|
rootNodesTotal,
|
||||||
|
} = useSelectableDomainTree(initialSelectedValues, LOAD_BATCH_SIZE);
|
||||||
|
|
||||||
const updateSelectedValues = useCallback(
|
const updateSelectedValues = useCallback(
|
||||||
(newSelectedValues: string[]) => {
|
(newSelectedValues: string[]) => {
|
||||||
const topLevelSelectedValues = getTopLevelSelectedValuesFromTree(newSelectedValues, tree.nodes);
|
const topLevelSelectedValues = getTopLevelSelectedValuesFromTree(newSelectedValues, tree.nodes);
|
||||||
form.setFieldValue('domainAssets', topLevelSelectedValues);
|
// add out of tree selected values as some root nodes could not be loaded yet
|
||||||
|
const outOfTreeSelectedValues = getOutOfTreeSelectedValues(newSelectedValues, tree.nodes);
|
||||||
|
const newProcessedSelectedValues = new Set([...topLevelSelectedValues, ...outOfTreeSelectedValues]);
|
||||||
|
form.setFieldValue('domainAssets', Array.from(newProcessedSelectedValues));
|
||||||
setSelectedValues(newSelectedValues);
|
setSelectedValues(newSelectedValues);
|
||||||
},
|
},
|
||||||
[form, setSelectedValues, tree],
|
[form, setSelectedValues, tree],
|
||||||
@ -57,7 +73,7 @@ export default function DomainsSelectableTreeView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<ChildrenLoaderProvider onLoadFinished={onLoadFinished}>
|
<ChildrenLoaderProvider onLoadFinished={onLoadFinished} maxNumberOfChildrenToLoad={LOAD_BATCH_SIZE}>
|
||||||
<ChildrenLoader parentValues={parentValues} loadChildren={useChildrenDomainsLoader} />
|
<ChildrenLoader parentValues={parentValues} loadChildren={useChildrenDomainsLoader} />
|
||||||
|
|
||||||
<TreeView
|
<TreeView
|
||||||
@ -70,6 +86,11 @@ export default function DomainsSelectableTreeView() {
|
|||||||
updateSelectedValues={updateSelectedValues}
|
updateSelectedValues={updateSelectedValues}
|
||||||
loadChildren={startLoadingOfChildren}
|
loadChildren={startLoadingOfChildren}
|
||||||
renderNodeLabel={(nodeProps) => <DomainSelectableTreeNodeRenderer {...nodeProps} />}
|
renderNodeLabel={(nodeProps) => <DomainSelectableTreeNodeRenderer {...nodeProps} />}
|
||||||
|
loadRootNodes={loadMoreRootNodes}
|
||||||
|
rootNodesLoading={rootDomainsMoreLoading}
|
||||||
|
loadingTriggerType="infiniteScroll"
|
||||||
|
rootNodesTotal={rootNodesTotal}
|
||||||
|
loadBatchSize={LOAD_BATCH_SIZE}
|
||||||
/>
|
/>
|
||||||
</ChildrenLoaderProvider>
|
</ChildrenLoaderProvider>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
|
import { ApolloQueryResult, NetworkStatus } from '@apollo/client';
|
||||||
import { act } from '@testing-library/react';
|
import { act } from '@testing-library/react';
|
||||||
import { renderHook } from '@testing-library/react-hooks';
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import useInitialDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useInitialDomains';
|
import useInitialDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useInitialDomains';
|
||||||
|
import useLoadMoreRootDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useLoadMoreRootDomains';
|
||||||
import useRootDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useRootDomains';
|
import useRootDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useRootDomains';
|
||||||
import useSelectableDomainTree from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useSelectableDomainTree';
|
import useSelectableDomainTree from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useSelectableDomainTree';
|
||||||
import useTreeNodesFromFlatDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useTreeNodesFromFlatDomains';
|
import useTreeNodesFromFlatDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useTreeNodesFromFlatDomains';
|
||||||
@ -10,11 +12,13 @@ import useTreeNodesFromDomains from '@app/homeV3/modules/hierarchyViewModule/com
|
|||||||
import useTree from '@app/homeV3/modules/hierarchyViewModule/treeView/useTree';
|
import useTree from '@app/homeV3/modules/hierarchyViewModule/treeView/useTree';
|
||||||
import { mergeTrees } from '@app/homeV3/modules/hierarchyViewModule/treeView/utils';
|
import { mergeTrees } from '@app/homeV3/modules/hierarchyViewModule/treeView/utils';
|
||||||
|
|
||||||
import { EntityType } from '@types';
|
import { GetSearchResultsForMultipleQuery } from '@graphql/search.generated';
|
||||||
|
import { Domain, EntityType } from '@types';
|
||||||
|
|
||||||
// Mock all the dependent hooks
|
// Mock all the dependent hooks
|
||||||
vi.mock('@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useInitialDomains');
|
vi.mock('@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useInitialDomains');
|
||||||
vi.mock('@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useRootDomains');
|
vi.mock('@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useRootDomains');
|
||||||
|
vi.mock('@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useLoadMoreRootDomains');
|
||||||
vi.mock('@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useTreeNodesFromFlatDomains');
|
vi.mock('@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useTreeNodesFromFlatDomains');
|
||||||
vi.mock('@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useTreeNodesFromListDomains');
|
vi.mock('@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useTreeNodesFromListDomains');
|
||||||
vi.mock('@app/homeV3/modules/hierarchyViewModule/treeView/useTree');
|
vi.mock('@app/homeV3/modules/hierarchyViewModule/treeView/useTree');
|
||||||
@ -22,11 +26,18 @@ vi.mock('@app/homeV3/modules/hierarchyViewModule/treeView/utils');
|
|||||||
|
|
||||||
const mockUseInitialDomains = vi.mocked(useInitialDomains);
|
const mockUseInitialDomains = vi.mocked(useInitialDomains);
|
||||||
const mockUseRootDomains = vi.mocked(useRootDomains);
|
const mockUseRootDomains = vi.mocked(useRootDomains);
|
||||||
|
const mockUseLoadMoreRootDomains = vi.mocked(useLoadMoreRootDomains);
|
||||||
const mockUseTreeNodesFromFlatDomains = vi.mocked(useTreeNodesFromFlatDomains);
|
const mockUseTreeNodesFromFlatDomains = vi.mocked(useTreeNodesFromFlatDomains);
|
||||||
const mockUseTreeNodesFromDomains = vi.mocked(useTreeNodesFromDomains);
|
const mockUseTreeNodesFromDomains = vi.mocked(useTreeNodesFromDomains);
|
||||||
const mockUseTree = vi.mocked(useTree);
|
const mockUseTree = vi.mocked(useTree);
|
||||||
const mockMergeTrees = vi.mocked(mergeTrees);
|
const mockMergeTrees = vi.mocked(mergeTrees);
|
||||||
|
|
||||||
|
const mockRefetch = (): Promise<ApolloQueryResult<GetSearchResultsForMultipleQuery>> => {
|
||||||
|
return new Promise<ApolloQueryResult<GetSearchResultsForMultipleQuery>>((resolve) => {
|
||||||
|
resolve({ data: { searchAcrossEntities: undefined }, networkStatus: NetworkStatus.refetch, loading: false });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Mock tree object with dynamic nodes
|
// Mock tree object with dynamic nodes
|
||||||
const mockTreeMethods = {
|
const mockTreeMethods = {
|
||||||
nodes: [] as any[],
|
nodes: [] as any[],
|
||||||
@ -75,7 +86,20 @@ describe('useSelectableDomainTree hook', () => {
|
|||||||
|
|
||||||
// Setup default mock implementations
|
// Setup default mock implementations
|
||||||
mockUseInitialDomains.mockReturnValue({ data: undefined, domains: [], loading: false });
|
mockUseInitialDomains.mockReturnValue({ data: undefined, domains: [], loading: false });
|
||||||
mockUseRootDomains.mockReturnValue({ data: undefined, domains: [], total: 0, loading: false });
|
mockUseRootDomains.mockReturnValue({
|
||||||
|
data: undefined,
|
||||||
|
domains: [],
|
||||||
|
total: 0,
|
||||||
|
loading: false,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
});
|
||||||
|
mockUseLoadMoreRootDomains.mockReturnValue({
|
||||||
|
loading: false,
|
||||||
|
loadMoreRootDomains: () =>
|
||||||
|
new Promise<Domain[]>((resolve) => {
|
||||||
|
resolve([]);
|
||||||
|
}),
|
||||||
|
});
|
||||||
mockUseTreeNodesFromFlatDomains.mockReturnValue(undefined);
|
mockUseTreeNodesFromFlatDomains.mockReturnValue(undefined);
|
||||||
mockUseTreeNodesFromDomains.mockReturnValue(undefined);
|
mockUseTreeNodesFromDomains.mockReturnValue(undefined);
|
||||||
mockUseTree.mockReturnValue(mockTreeMethods);
|
mockUseTree.mockReturnValue(mockTreeMethods);
|
||||||
@ -115,7 +139,13 @@ describe('useSelectableDomainTree hook', () => {
|
|||||||
|
|
||||||
describe('tree initialization', () => {
|
describe('tree initialization', () => {
|
||||||
it('should not initialize tree when root domains are loading', () => {
|
it('should not initialize tree when root domains are loading', () => {
|
||||||
mockUseRootDomains.mockReturnValue({ data: undefined, domains: [], total: 0, loading: true });
|
mockUseRootDomains.mockReturnValue({
|
||||||
|
data: undefined,
|
||||||
|
domains: [],
|
||||||
|
total: 0,
|
||||||
|
loading: true,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
});
|
||||||
|
|
||||||
renderHook(() => useSelectableDomainTree([]));
|
renderHook(() => useSelectableDomainTree([]));
|
||||||
|
|
||||||
@ -123,7 +153,13 @@ describe('useSelectableDomainTree hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not initialize tree when root tree nodes are undefined', () => {
|
it('should not initialize tree when root tree nodes are undefined', () => {
|
||||||
mockUseRootDomains.mockReturnValue({ data: undefined, domains: [mockDomain1], total: 1, loading: false });
|
mockUseRootDomains.mockReturnValue({
|
||||||
|
data: undefined,
|
||||||
|
domains: [mockDomain1],
|
||||||
|
total: 1,
|
||||||
|
loading: false,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
});
|
||||||
mockUseTreeNodesFromDomains.mockReturnValue(undefined);
|
mockUseTreeNodesFromDomains.mockReturnValue(undefined);
|
||||||
|
|
||||||
renderHook(() => useSelectableDomainTree([]));
|
renderHook(() => useSelectableDomainTree([]));
|
||||||
@ -132,7 +168,13 @@ describe('useSelectableDomainTree hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not initialize tree when initial selected tree nodes are undefined', () => {
|
it('should not initialize tree when initial selected tree nodes are undefined', () => {
|
||||||
mockUseRootDomains.mockReturnValue({ data: undefined, domains: [mockDomain1], total: 1, loading: false });
|
mockUseRootDomains.mockReturnValue({
|
||||||
|
data: undefined,
|
||||||
|
domains: [mockDomain1],
|
||||||
|
total: 1,
|
||||||
|
loading: false,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
});
|
||||||
mockUseTreeNodesFromDomains.mockReturnValue([mockTreeNode1]);
|
mockUseTreeNodesFromDomains.mockReturnValue([mockTreeNode1]);
|
||||||
mockUseTreeNodesFromFlatDomains.mockReturnValue(undefined);
|
mockUseTreeNodesFromFlatDomains.mockReturnValue(undefined);
|
||||||
|
|
||||||
@ -146,21 +188,33 @@ describe('useSelectableDomainTree hook', () => {
|
|||||||
const initialSelectedTreeNodes = [mockTreeNode2];
|
const initialSelectedTreeNodes = [mockTreeNode2];
|
||||||
const mergedTreeNodes = [mockTreeNode1, mockTreeNode2];
|
const mergedTreeNodes = [mockTreeNode1, mockTreeNode2];
|
||||||
|
|
||||||
mockUseRootDomains.mockReturnValue({ data: undefined, domains: [mockDomain1], total: 1, loading: false });
|
mockUseRootDomains.mockReturnValue({
|
||||||
|
data: undefined,
|
||||||
|
domains: [mockDomain1],
|
||||||
|
total: 1,
|
||||||
|
loading: false,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
});
|
||||||
mockUseTreeNodesFromDomains.mockReturnValue(rootTreeNodes);
|
mockUseTreeNodesFromDomains.mockReturnValue(rootTreeNodes);
|
||||||
mockUseTreeNodesFromFlatDomains.mockReturnValue(initialSelectedTreeNodes);
|
mockUseTreeNodesFromFlatDomains.mockReturnValue(initialSelectedTreeNodes);
|
||||||
mockMergeTrees.mockReturnValue(mergedTreeNodes);
|
mockMergeTrees.mockReturnValue(mergedTreeNodes);
|
||||||
|
|
||||||
renderHook(() => useSelectableDomainTree([]));
|
renderHook(() => useSelectableDomainTree([]));
|
||||||
|
|
||||||
expect(mockMergeTrees).toHaveBeenCalledWith(rootTreeNodes, initialSelectedTreeNodes);
|
expect(mockMergeTrees).toHaveBeenCalledWith(rootTreeNodes, []);
|
||||||
expect(mockTreeMethods.replace).toHaveBeenCalledWith(mergedTreeNodes);
|
expect(mockTreeMethods.replace).toHaveBeenCalledWith(mergedTreeNodes);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('selectedValues management', () => {
|
describe('selectedValues management', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockUseRootDomains.mockReturnValue({ data: undefined, domains: [mockDomain1], total: 1, loading: false });
|
mockUseRootDomains.mockReturnValue({
|
||||||
|
data: undefined,
|
||||||
|
domains: [mockDomain1],
|
||||||
|
total: 1,
|
||||||
|
loading: false,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
});
|
||||||
mockUseTreeNodesFromDomains.mockReturnValue([mockTreeNode1]);
|
mockUseTreeNodesFromDomains.mockReturnValue([mockTreeNode1]);
|
||||||
mockUseTreeNodesFromFlatDomains.mockReturnValue([mockTreeNode2]);
|
mockUseTreeNodesFromFlatDomains.mockReturnValue([mockTreeNode2]);
|
||||||
});
|
});
|
||||||
@ -185,7 +239,13 @@ describe('useSelectableDomainTree hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle loading state correctly when root domains are loading', () => {
|
it('should handle loading state correctly when root domains are loading', () => {
|
||||||
mockUseRootDomains.mockReturnValue({ data: undefined, domains: [], total: 0, loading: true });
|
mockUseRootDomains.mockReturnValue({
|
||||||
|
data: undefined,
|
||||||
|
domains: [],
|
||||||
|
total: 0,
|
||||||
|
loading: true,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
});
|
||||||
|
|
||||||
const { result } = renderHook(() => useSelectableDomainTree([]));
|
const { result } = renderHook(() => useSelectableDomainTree([]));
|
||||||
|
|
||||||
@ -193,7 +253,13 @@ describe('useSelectableDomainTree hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle ready state when all data is loaded', () => {
|
it('should handle ready state when all data is loaded', () => {
|
||||||
mockUseRootDomains.mockReturnValue({ data: undefined, domains: [mockDomain1], total: 1, loading: false });
|
mockUseRootDomains.mockReturnValue({
|
||||||
|
data: undefined,
|
||||||
|
domains: [mockDomain1],
|
||||||
|
total: 1,
|
||||||
|
loading: false,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
});
|
||||||
mockUseTreeNodesFromDomains.mockReturnValue([mockTreeNode1]);
|
mockUseTreeNodesFromDomains.mockReturnValue([mockTreeNode1]);
|
||||||
|
|
||||||
const { result } = renderHook(() => useSelectableDomainTree([]));
|
const { result } = renderHook(() => useSelectableDomainTree([]));
|
||||||
@ -204,7 +270,13 @@ describe('useSelectableDomainTree hook', () => {
|
|||||||
|
|
||||||
describe('tree method delegation', () => {
|
describe('tree method delegation', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockUseRootDomains.mockReturnValue({ data: undefined, domains: [mockDomain1], total: 1, loading: false });
|
mockUseRootDomains.mockReturnValue({
|
||||||
|
data: undefined,
|
||||||
|
domains: [mockDomain1],
|
||||||
|
total: 1,
|
||||||
|
loading: false,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
});
|
||||||
mockUseTreeNodesFromDomains.mockReturnValue([mockTreeNode1]);
|
mockUseTreeNodesFromDomains.mockReturnValue([mockTreeNode1]);
|
||||||
mockUseTreeNodesFromFlatDomains.mockReturnValue([mockTreeNode2]);
|
mockUseTreeNodesFromFlatDomains.mockReturnValue([mockTreeNode2]);
|
||||||
});
|
});
|
||||||
@ -254,7 +326,13 @@ describe('useSelectableDomainTree hook', () => {
|
|||||||
it('should use domains from root domains hook', () => {
|
it('should use domains from root domains hook', () => {
|
||||||
const rootDomains = [mockDomain1];
|
const rootDomains = [mockDomain1];
|
||||||
|
|
||||||
mockUseRootDomains.mockReturnValue({ data: undefined, domains: rootDomains, total: 1, loading: false });
|
mockUseRootDomains.mockReturnValue({
|
||||||
|
data: undefined,
|
||||||
|
domains: rootDomains,
|
||||||
|
total: 1,
|
||||||
|
loading: false,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
});
|
||||||
|
|
||||||
renderHook(() => useSelectableDomainTree([]));
|
renderHook(() => useSelectableDomainTree([]));
|
||||||
|
|
||||||
@ -262,14 +340,20 @@ describe('useSelectableDomainTree hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle mixed states correctly', () => {
|
it('should handle mixed states correctly', () => {
|
||||||
mockUseRootDomains.mockReturnValue({ data: undefined, domains: [mockDomain1], total: 1, loading: false });
|
mockUseRootDomains.mockReturnValue({
|
||||||
|
data: undefined,
|
||||||
|
domains: [mockDomain1],
|
||||||
|
total: 1,
|
||||||
|
loading: false,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
});
|
||||||
mockUseTreeNodesFromDomains.mockReturnValue([mockTreeNode1]);
|
mockUseTreeNodesFromDomains.mockReturnValue([mockTreeNode1]);
|
||||||
mockUseTreeNodesFromFlatDomains.mockReturnValue([mockTreeNode2]);
|
mockUseTreeNodesFromFlatDomains.mockReturnValue([mockTreeNode2]);
|
||||||
|
|
||||||
const { result } = renderHook(() => useSelectableDomainTree([]));
|
const { result } = renderHook(() => useSelectableDomainTree([]));
|
||||||
|
|
||||||
expect(result.current.loading).toBe(false);
|
expect(result.current.loading).toBe(false);
|
||||||
expect(mockMergeTrees).toHaveBeenCalledWith([mockTreeNode1], [mockTreeNode2]);
|
expect(mockMergeTrees).toHaveBeenCalledWith([mockTreeNode1], []);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty initial domains', () => {
|
it('should handle empty initial domains', () => {
|
||||||
@ -281,7 +365,13 @@ describe('useSelectableDomainTree hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty root domains', () => {
|
it('should handle empty root domains', () => {
|
||||||
mockUseRootDomains.mockReturnValue({ data: undefined, domains: [mockDomain1], total: 1, loading: false });
|
mockUseRootDomains.mockReturnValue({
|
||||||
|
data: undefined,
|
||||||
|
domains: [mockDomain1],
|
||||||
|
total: 1,
|
||||||
|
loading: false,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
});
|
||||||
mockUseTreeNodesFromDomains.mockReturnValue([mockTreeNode1]);
|
mockUseTreeNodesFromDomains.mockReturnValue([mockTreeNode1]);
|
||||||
mockUseTreeNodesFromFlatDomains.mockReturnValue([mockTreeNode2]);
|
mockUseTreeNodesFromFlatDomains.mockReturnValue([mockTreeNode2]);
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,28 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { useListDomainsQuery } from '@graphql/domain.generated';
|
import { isDomain } from '@app/entityV2/domain/utils';
|
||||||
|
import { ENTITY_NAME_FIELD } from '@app/searchV2/context/constants';
|
||||||
|
|
||||||
export default function useDomains(parentDomainUrns: string | undefined, start: number, count: number, skip: boolean) {
|
import { useGetSearchResultsForMultipleQuery } from '@graphql/search.generated';
|
||||||
const { data, loading } = useListDomainsQuery({
|
import { EntityType, FilterOperator, SortOrder } from '@types';
|
||||||
|
|
||||||
|
export default function useDomains(parentDomainUrn: string | undefined, start: number, count: number, skip: boolean) {
|
||||||
|
const { data, loading, refetch } = useGetSearchResultsForMultipleQuery({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
start,
|
start,
|
||||||
count,
|
count,
|
||||||
parentDomain: parentDomainUrns,
|
query: '*',
|
||||||
|
types: [EntityType.Domain],
|
||||||
|
orFilters: parentDomainUrn
|
||||||
|
? [{ and: [{ field: 'parentDomain', values: [parentDomainUrn] }] }]
|
||||||
|
: [{ and: [{ field: 'parentDomain', condition: FilterOperator.Exists, negated: true }] }],
|
||||||
|
sortInput: {
|
||||||
|
sortCriteria: [{ field: ENTITY_NAME_FIELD, sortOrder: SortOrder.Ascending }],
|
||||||
|
},
|
||||||
|
searchFlags: {
|
||||||
|
skipCache: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
skip,
|
skip,
|
||||||
@ -16,10 +30,10 @@ export default function useDomains(parentDomainUrns: string | undefined, start:
|
|||||||
|
|
||||||
const domains = useMemo(() => {
|
const domains = useMemo(() => {
|
||||||
if (skip) return [];
|
if (skip) return [];
|
||||||
return data?.listDomains?.domains;
|
return data?.searchAcrossEntities?.searchResults.map((result) => result.entity).filter(isDomain);
|
||||||
}, [data, skip]);
|
}, [data, skip]);
|
||||||
|
|
||||||
const total = useMemo(() => data?.listDomains?.total, [data]);
|
const total = useMemo(() => data?.searchAcrossEntities?.total, [data]);
|
||||||
|
|
||||||
return { data, domains, total, loading };
|
return { data, domains, total, loading, refetch };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,66 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { isDomain } from '@app/entityV2/domain/utils';
|
||||||
|
import { ENTITY_NAME_FIELD } from '@app/searchV2/context/constants';
|
||||||
|
|
||||||
|
import { useGetSearchResultsForMultipleQuery } from '@graphql/search.generated';
|
||||||
|
import { EntityType, FilterOperator, SortOrder } from '@types';
|
||||||
|
|
||||||
|
export default function useLoadMoreRootDomains() {
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { refetch } = useGetSearchResultsForMultipleQuery({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
query: '*',
|
||||||
|
types: [EntityType.Domain],
|
||||||
|
start: 0,
|
||||||
|
count: 0,
|
||||||
|
sortInput: {
|
||||||
|
sortCriteria: [{ field: ENTITY_NAME_FIELD, sortOrder: SortOrder.Ascending }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
skip: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadMoreRootDomains = useCallback(
|
||||||
|
async (start: number, pageSize: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await refetch({
|
||||||
|
input: {
|
||||||
|
start,
|
||||||
|
types: [EntityType.Domain],
|
||||||
|
query: '*',
|
||||||
|
count: pageSize,
|
||||||
|
orFilters: [
|
||||||
|
{ and: [{ field: 'parentDomain', condition: FilterOperator.Exists, negated: true }] },
|
||||||
|
],
|
||||||
|
sortInput: {
|
||||||
|
sortCriteria: [{ field: ENTITY_NAME_FIELD, sortOrder: SortOrder.Ascending }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
response.data.searchAcrossEntities?.searchResults
|
||||||
|
?.map((searchResult) => searchResult.entity)
|
||||||
|
.filter(isDomain) ?? []
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Something went wrong during fetching root domains', e);
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[refetch],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadMoreRootDomains,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,7 +1,5 @@
|
|||||||
import useDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useDomains';
|
import useDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useDomains';
|
||||||
|
|
||||||
const MAX_ROOT_DOMAINS = 1000;
|
export default function useRootDomains(count: number) {
|
||||||
|
return useDomains(undefined, 0, count, false);
|
||||||
export default function useRootDomains() {
|
|
||||||
return useDomains(undefined, 0, MAX_ROOT_DOMAINS, false);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,64 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import useDomainTreeNodesSorter from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useDomainTreeNodesSorter';
|
import useDomainTreeNodesSorter from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useDomainTreeNodesSorter';
|
||||||
import useInitialDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useInitialDomains';
|
import useInitialDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useInitialDomains';
|
||||||
|
import useLoadMoreRootDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useLoadMoreRootDomains';
|
||||||
import useRootDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useRootDomains';
|
import useRootDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useRootDomains';
|
||||||
import useTreeNodesFromFlatDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useTreeNodesFromFlatDomains';
|
import useTreeNodesFromFlatDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useTreeNodesFromFlatDomains';
|
||||||
import useTreeNodesFromDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useTreeNodesFromListDomains';
|
import useTreeNodesFromDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useTreeNodesFromListDomains';
|
||||||
|
import { convertDomainToTreeNode } from '@app/homeV3/modules/hierarchyViewModule/components/domains/utils';
|
||||||
|
import { DEFAULT_LOAD_BATCH_SIZE } from '@app/homeV3/modules/hierarchyViewModule/treeView/constants';
|
||||||
|
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
||||||
import useTree from '@app/homeV3/modules/hierarchyViewModule/treeView/useTree';
|
import useTree from '@app/homeV3/modules/hierarchyViewModule/treeView/useTree';
|
||||||
import { mergeTrees } from '@app/homeV3/modules/hierarchyViewModule/treeView/utils';
|
import { mergeTrees } from '@app/homeV3/modules/hierarchyViewModule/treeView/utils';
|
||||||
|
|
||||||
export default function useSelectableDomainTree(initialSelectedDomainUrns: string[] | undefined) {
|
export default function useSelectableDomainTree(
|
||||||
|
initialSelectedDomainUrns: string[] | undefined,
|
||||||
|
loadBatchSize = DEFAULT_LOAD_BATCH_SIZE,
|
||||||
|
) {
|
||||||
const [isInitialized, setIsInitialized] = useState<boolean>(false);
|
const [isInitialized, setIsInitialized] = useState<boolean>(false);
|
||||||
const [selectedValues, setSelectedValues] = useState<string[]>(initialSelectedDomainUrns ?? []);
|
const [selectedValues, setSelectedValues] = useState<string[]>(initialSelectedDomainUrns ?? []);
|
||||||
const nodesSorter = useDomainTreeNodesSorter();
|
const nodesSorter = useDomainTreeNodesSorter();
|
||||||
const tree = useTree(undefined, nodesSorter);
|
const tree = useTree(undefined, nodesSorter);
|
||||||
|
|
||||||
|
// FYI: We get and extract parents from initially selected domains
|
||||||
|
// to have possibility to detect if some node has any selected nested nodes
|
||||||
const { domains: initialDomains } = useInitialDomains(initialSelectedDomainUrns ?? []);
|
const { domains: initialDomains } = useInitialDomains(initialSelectedDomainUrns ?? []);
|
||||||
const initialSelectedTreeNodes = useTreeNodesFromFlatDomains(initialDomains);
|
const initialSelectedTreeNodes = useTreeNodesFromFlatDomains(initialDomains);
|
||||||
|
|
||||||
const { domains: rootDomains, loading: rootDomainsLoading } = useRootDomains();
|
const {
|
||||||
|
domains: rootDomains,
|
||||||
|
loading: rootDomainsLoading,
|
||||||
|
total: rootDomainsTotal,
|
||||||
|
} = useRootDomains(loadBatchSize);
|
||||||
const rootTreeNodes = useTreeNodesFromDomains(rootDomains, false);
|
const rootTreeNodes = useTreeNodesFromDomains(rootDomains, false);
|
||||||
|
|
||||||
|
const { loadMoreRootDomains, loading: rootDomainsMoreLoading } = useLoadMoreRootDomains();
|
||||||
|
|
||||||
|
const preprocessRootNodes = useCallback(
|
||||||
|
(rootNodes: TreeNode[]) => {
|
||||||
|
if (!initialSelectedTreeNodes) return rootNodes;
|
||||||
|
|
||||||
|
// Merge initial selected nodes with the same root nodes
|
||||||
|
const initialSelectedTreeNodesToMerge = initialSelectedTreeNodes.filter((initialSelectedTreeNode) =>
|
||||||
|
rootNodes.some((rootNode) => rootNode.value === initialSelectedTreeNode.value),
|
||||||
|
);
|
||||||
|
return mergeTrees(rootNodes, initialSelectedTreeNodesToMerge);
|
||||||
|
},
|
||||||
|
[initialSelectedTreeNodes],
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadMoreRootNodes = useCallback(async () => {
|
||||||
|
const hasMoreRootNodes = (rootDomainsTotal ?? 0) > tree.nodes.length;
|
||||||
|
if (!rootDomainsMoreLoading && hasMoreRootNodes) {
|
||||||
|
const domains = await loadMoreRootDomains(tree.nodes.length, loadBatchSize);
|
||||||
|
if (domains) {
|
||||||
|
const treeNodes = domains.map((domain) => convertDomainToTreeNode(domain));
|
||||||
|
tree.merge(preprocessRootNodes(treeNodes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [tree, loadMoreRootDomains, rootDomainsTotal, rootDomainsMoreLoading, preprocessRootNodes, loadBatchSize]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!rootDomainsLoading &&
|
!rootDomainsLoading &&
|
||||||
@ -27,15 +66,18 @@ export default function useSelectableDomainTree(initialSelectedDomainUrns: strin
|
|||||||
rootTreeNodes !== undefined &&
|
rootTreeNodes !== undefined &&
|
||||||
initialSelectedTreeNodes !== undefined
|
initialSelectedTreeNodes !== undefined
|
||||||
) {
|
) {
|
||||||
tree.replace(mergeTrees(rootTreeNodes, initialSelectedTreeNodes));
|
tree.replace(preprocessRootNodes(rootTreeNodes));
|
||||||
setIsInitialized(true);
|
setIsInitialized(true);
|
||||||
}
|
}
|
||||||
}, [tree, isInitialized, rootTreeNodes, initialSelectedTreeNodes, rootDomainsLoading]);
|
}, [tree, isInitialized, rootTreeNodes, initialSelectedTreeNodes, rootDomainsLoading, preprocessRootNodes]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tree,
|
tree,
|
||||||
loading: !isInitialized,
|
loading: !isInitialized,
|
||||||
selectedValues,
|
selectedValues,
|
||||||
setSelectedValues,
|
setSelectedValues,
|
||||||
|
loadMoreRootNodes,
|
||||||
|
rootDomainsMoreLoading,
|
||||||
|
rootNodesTotal: rootDomainsTotal,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,20 @@
|
|||||||
import { Button, Checkbox, Loader } from '@components';
|
import { Checkbox } from '@components';
|
||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import ChildrenLoader from '@app/homeV3/modules/hierarchyViewModule/treeView/ChildrenLoader';
|
import ChildrenLoader from '@app/homeV3/modules/hierarchyViewModule/treeView/ChildrenLoader';
|
||||||
import DepthMargin from '@app/homeV3/modules/hierarchyViewModule/treeView/DepthMargin';
|
import DepthMargin from '@app/homeV3/modules/hierarchyViewModule/treeView/DepthMargin';
|
||||||
import ExpandToggler from '@app/homeV3/modules/hierarchyViewModule/treeView/ExpandToggler';
|
import ExpandToggler from '@app/homeV3/modules/hierarchyViewModule/treeView/ExpandToggler';
|
||||||
|
import Row from '@app/homeV3/modules/hierarchyViewModule/treeView/components/Row';
|
||||||
|
import TreeNodesViewLoader from '@app/homeV3/modules/hierarchyViewModule/treeView/components/TreeNodesViewLoader';
|
||||||
|
import NodesLoaderWrapper from '@app/homeV3/modules/hierarchyViewModule/treeView/components/itemsLoaderWrapper/NodesLoaderWrapper';
|
||||||
import useTreeViewContext from '@app/homeV3/modules/hierarchyViewModule/treeView/context/useTreeViewContext';
|
import useTreeViewContext from '@app/homeV3/modules/hierarchyViewModule/treeView/context/useTreeViewContext';
|
||||||
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
||||||
|
|
||||||
const Row = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SpaceFiller = styled.div`
|
const SpaceFiller = styled.div`
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const LoaderWrapper = styled.div``;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: TreeNode;
|
node: TreeNode;
|
||||||
depth: number;
|
depth: number;
|
||||||
@ -29,8 +22,11 @@ interface Props {
|
|||||||
|
|
||||||
export default function TreeNodeRenderer({ node, depth }: Props) {
|
export default function TreeNodeRenderer({ node, depth }: Props) {
|
||||||
const {
|
const {
|
||||||
|
loadingTriggerType,
|
||||||
getHasParentNode,
|
getHasParentNode,
|
||||||
getIsRootNode,
|
getIsRootNode,
|
||||||
|
getChildrenTotal,
|
||||||
|
getChildrenLength,
|
||||||
renderNodeLabel,
|
renderNodeLabel,
|
||||||
getIsExpandable,
|
getIsExpandable,
|
||||||
getIsExpanded,
|
getIsExpanded,
|
||||||
@ -45,9 +41,8 @@ export default function TreeNodeRenderer({ node, depth }: Props) {
|
|||||||
enableIntermediateSelectState,
|
enableIntermediateSelectState,
|
||||||
toggleSelected,
|
toggleSelected,
|
||||||
getIsChildrenLoading,
|
getIsChildrenLoading,
|
||||||
getNumberOfNotLoadedChildren,
|
|
||||||
loadChildren,
|
loadChildren,
|
||||||
numberOfChildrenToLoad,
|
loadBatchSize: numberOfChildrenToLoad,
|
||||||
} = useTreeViewContext();
|
} = useTreeViewContext();
|
||||||
|
|
||||||
const isExpandable = useMemo(() => getIsExpandable(node), [node, getIsExpandable]);
|
const isExpandable = useMemo(() => getIsExpandable(node), [node, getIsExpandable]);
|
||||||
@ -73,13 +68,8 @@ export default function TreeNodeRenderer({ node, depth }: Props) {
|
|||||||
|
|
||||||
const isRootNode = useMemo(() => getIsRootNode(node), [node, getIsRootNode]);
|
const isRootNode = useMemo(() => getIsRootNode(node), [node, getIsRootNode]);
|
||||||
|
|
||||||
const numberOfNotLoadedChildren = useMemo(
|
const childrenTotal = useMemo(() => getChildrenTotal(node), [node, getChildrenTotal]);
|
||||||
() => getNumberOfNotLoadedChildren(node),
|
const childrenLength = useMemo(() => getChildrenLength(node), [node, getChildrenLength]);
|
||||||
[node, getNumberOfNotLoadedChildren],
|
|
||||||
);
|
|
||||||
|
|
||||||
const maxNumberOfChildrenToLoad = Math.min(numberOfNotLoadedChildren, numberOfChildrenToLoad);
|
|
||||||
const shouldShowLoadMoreButton = isExpanded && !isChildrenLoading && numberOfNotLoadedChildren > 0;
|
|
||||||
|
|
||||||
// Automatically select the current node if parent is selected if explicitlySelectChildren is not enabled
|
// Automatically select the current node if parent is selected if explicitlySelectChildren is not enabled
|
||||||
// FYI: required to get loaded children selected if parent is selected
|
// FYI: required to get loaded children selected if parent is selected
|
||||||
@ -90,7 +80,16 @@ export default function TreeNodeRenderer({ node, depth }: Props) {
|
|||||||
}, [explicitlySelectChildren, hasParentNode, isParentSelected, select, node, isSelected, isSelectable]);
|
}, [explicitlySelectChildren, hasParentNode, isParentSelected, select, node, isSelected, isSelectable]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<NodesLoaderWrapper
|
||||||
|
trigger={loadingTriggerType}
|
||||||
|
onLoad={() => loadChildren(node)}
|
||||||
|
total={childrenTotal}
|
||||||
|
current={childrenLength}
|
||||||
|
pageSize={numberOfChildrenToLoad}
|
||||||
|
depth={depth + 1}
|
||||||
|
enabled={isExpanded}
|
||||||
|
loading={isChildrenLoading}
|
||||||
|
>
|
||||||
<Row>
|
<Row>
|
||||||
<DepthMargin depth={depth} />
|
<DepthMargin depth={depth} />
|
||||||
|
|
||||||
@ -127,27 +126,8 @@ export default function TreeNodeRenderer({ node, depth }: Props) {
|
|||||||
{/* Run loading on expand */}
|
{/* Run loading on expand */}
|
||||||
{isExpanded && <ChildrenLoader node={node} />}
|
{isExpanded && <ChildrenLoader node={node} />}
|
||||||
|
|
||||||
{/* Show more button */}
|
|
||||||
{shouldShowLoadMoreButton && (
|
|
||||||
<Row>
|
|
||||||
<DepthMargin depth={depth + 1} />
|
|
||||||
<ExpandToggler expandable={false} />
|
|
||||||
<Button onClick={() => loadChildren(node)} variant="link" color="gray">
|
|
||||||
Show {maxNumberOfChildrenToLoad} more
|
|
||||||
</Button>
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Loading indicator */}
|
{/* Loading indicator */}
|
||||||
{isExpanded && isChildrenLoading && (
|
{isExpanded && isChildrenLoading && <TreeNodesViewLoader depth={depth + 1} />}
|
||||||
<Row>
|
</NodesLoaderWrapper>
|
||||||
<DepthMargin depth={depth + 1} />
|
|
||||||
<ExpandToggler expandable={false} />
|
|
||||||
<LoaderWrapper>
|
|
||||||
<Loader size="xs" />
|
|
||||||
</LoaderWrapper>
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import TreeNodeRenderer from '@app/homeV3/modules/hierarchyViewModule/treeView/TreeNodeRenderer';
|
import TreeNodeRenderer from '@app/homeV3/modules/hierarchyViewModule/treeView/TreeNodeRenderer';
|
||||||
|
import TreeNodesViewLoader from '@app/homeV3/modules/hierarchyViewModule/treeView/components/TreeNodesViewLoader';
|
||||||
|
import NodesLoaderWrapper from '@app/homeV3/modules/hierarchyViewModule/treeView/components/itemsLoaderWrapper/NodesLoaderWrapper';
|
||||||
import useTreeViewContext from '@app/homeV3/modules/hierarchyViewModule/treeView/context/useTreeViewContext';
|
import useTreeViewContext from '@app/homeV3/modules/hierarchyViewModule/treeView/context/useTreeViewContext';
|
||||||
|
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -17,14 +20,38 @@ const InlineBlockWrapper = styled.div<{ $hasExpanded: boolean }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export default function TreeNodesRenderer() {
|
export default function TreeNodesRenderer() {
|
||||||
const { nodes, hasAnyExpanded } = useTreeViewContext();
|
const {
|
||||||
|
nodes,
|
||||||
|
hasAnyExpanded,
|
||||||
|
loadBatchSize: numberOfChildrenToLoad,
|
||||||
|
loadRootNodes,
|
||||||
|
rootNodesLength,
|
||||||
|
rootNodesTotal,
|
||||||
|
rootNodesLoading,
|
||||||
|
loadingTriggerType,
|
||||||
|
} = useTreeViewContext();
|
||||||
|
|
||||||
|
const renderNode = useCallback((node: TreeNode) => <TreeNodeRenderer node={node} depth={0} key={node.value} />, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InlineBlockWrapper $hasExpanded={hasAnyExpanded}>
|
<InlineBlockWrapper $hasExpanded={hasAnyExpanded}>
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
{nodes.map((node) => (
|
{loadRootNodes ? (
|
||||||
<TreeNodeRenderer node={node} depth={0} key={node.value} />
|
<NodesLoaderWrapper
|
||||||
))}
|
trigger={loadingTriggerType}
|
||||||
|
total={rootNodesTotal}
|
||||||
|
current={rootNodesLength}
|
||||||
|
enabled={!rootNodesLoading}
|
||||||
|
depth={0}
|
||||||
|
onLoad={loadRootNodes}
|
||||||
|
pageSize={numberOfChildrenToLoad}
|
||||||
|
>
|
||||||
|
{nodes.map(renderNode)}
|
||||||
|
</NodesLoaderWrapper>
|
||||||
|
) : (
|
||||||
|
nodes.map(renderNode)
|
||||||
|
)}
|
||||||
|
{rootNodesLoading && <TreeNodesViewLoader depth={0} />}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</InlineBlockWrapper>
|
</InlineBlockWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
flattenTreeNodes,
|
flattenTreeNodes,
|
||||||
getAllParentValues,
|
getAllParentValues,
|
||||||
getAllValues,
|
getAllValues,
|
||||||
|
getOutOfTreeSelectedValues,
|
||||||
getTopLevelSelectedValuesFromTree,
|
getTopLevelSelectedValuesFromTree,
|
||||||
getValueToTreeNodeMapping,
|
getValueToTreeNodeMapping,
|
||||||
mergeTrees,
|
mergeTrees,
|
||||||
@ -418,4 +419,56 @@ describe('treeView utils', () => {
|
|||||||
expect(result).toEqual(['parent1', 'child2']);
|
expect(result).toEqual(['parent1', 'child2']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getOutOfTreeSelectedValues', () => {
|
||||||
|
const child1 = createTreeNode('child1');
|
||||||
|
const child2 = createTreeNode('child2');
|
||||||
|
const parent1 = createTreeNode('parent1', 'parent1', [child1]);
|
||||||
|
const parent2 = createTreeNode('parent2', 'parent2', [child2]);
|
||||||
|
const tree = [parent1, parent2];
|
||||||
|
|
||||||
|
it('should return only values out of tree', () => {
|
||||||
|
const selectedValues = ['parent1', 'child2', 'parent3'];
|
||||||
|
|
||||||
|
const result = getOutOfTreeSelectedValues(selectedValues, tree);
|
||||||
|
|
||||||
|
expect(result).toEqual(['parent3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty tree', () => {
|
||||||
|
const selectedValues = ['parent1', 'child2', 'parent3'];
|
||||||
|
|
||||||
|
const result = getOutOfTreeSelectedValues(selectedValues, []);
|
||||||
|
|
||||||
|
expect(result).toEqual(selectedValues);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty selected values', () => {
|
||||||
|
const result = getOutOfTreeSelectedValues([], tree);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty selected values and tree', () => {
|
||||||
|
const result = getOutOfTreeSelectedValues([], []);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array when all values included in tree', () => {
|
||||||
|
const selectedValues = ['parent1', 'child2'];
|
||||||
|
|
||||||
|
const result = getOutOfTreeSelectedValues(selectedValues, tree);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array when all values not included in tree', () => {
|
||||||
|
const selectedValues = ['parent3'];
|
||||||
|
|
||||||
|
const result = getOutOfTreeSelectedValues(selectedValues, tree);
|
||||||
|
|
||||||
|
expect(result).toEqual(['parent3']);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Row = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Row;
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { Loader } from '@components';
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import DepthMargin from '@app/homeV3/modules/hierarchyViewModule/treeView/DepthMargin';
|
||||||
|
import ExpandToggler from '@app/homeV3/modules/hierarchyViewModule/treeView/ExpandToggler';
|
||||||
|
import Row from '@app/homeV3/modules/hierarchyViewModule/treeView/components/Row';
|
||||||
|
|
||||||
|
const LoaderWrapper = styled.div``;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TreeNodesViewLoader({ depth }: Props) {
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<DepthMargin depth={depth + 1} />
|
||||||
|
<ExpandToggler expandable={false} />
|
||||||
|
<LoaderWrapper>
|
||||||
|
<Loader size="xs" />
|
||||||
|
</LoaderWrapper>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { Button } from '@components';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import DepthMargin from '@app/homeV3/modules/hierarchyViewModule/treeView/DepthMargin';
|
||||||
|
import ExpandToggler from '@app/homeV3/modules/hierarchyViewModule/treeView/ExpandToggler';
|
||||||
|
import Row from '@app/homeV3/modules/hierarchyViewModule/treeView/components/Row';
|
||||||
|
import { NodesLoaderWrapperProps } from '@app/homeV3/modules/hierarchyViewModule/treeView/components/itemsLoaderWrapper/types';
|
||||||
|
|
||||||
|
export default function NodesLoaderByButtonWrapper({
|
||||||
|
children,
|
||||||
|
total,
|
||||||
|
current,
|
||||||
|
pageSize,
|
||||||
|
depth,
|
||||||
|
enabled,
|
||||||
|
loading,
|
||||||
|
onLoad,
|
||||||
|
}: React.PropsWithChildren<NodesLoaderWrapperProps>) {
|
||||||
|
const loadMoreNumber = useMemo(() => Math.min(total - current, pageSize), [total, current, pageSize]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{enabled && !loading && loadMoreNumber > 0 && (
|
||||||
|
<Row>
|
||||||
|
<DepthMargin depth={depth + 1} />
|
||||||
|
<ExpandToggler expandable={false} />
|
||||||
|
<Button onClick={onLoad} variant="link" color="gray">
|
||||||
|
Show {loadMoreNumber} more
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { NodesLoaderWrapperProps } from '@app/homeV3/modules/hierarchyViewModule/treeView/components/itemsLoaderWrapper/types';
|
||||||
|
|
||||||
|
export const ObserverContainer = styled.div`
|
||||||
|
height: 1px;
|
||||||
|
margin-top: 1px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function NodesLoaderInfiniteScrollWrapper({
|
||||||
|
children,
|
||||||
|
total,
|
||||||
|
current,
|
||||||
|
enabled,
|
||||||
|
loading,
|
||||||
|
onLoad,
|
||||||
|
}: React.PropsWithChildren<NodesLoaderWrapperProps>) {
|
||||||
|
const [scrollRef, inView] = useInView();
|
||||||
|
// FYI: additional flag to prevent `onLoad` calling
|
||||||
|
// when infinite scroll triggered but real loading hasn't started yet
|
||||||
|
const [shouldPreventLoading, setShouldPreventLoading] = useState<boolean>(false);
|
||||||
|
// reset flag when observer container is out of view (it's hidden while real loading because `loading` is true)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!inView) setShouldPreventLoading(false);
|
||||||
|
}, [inView]);
|
||||||
|
|
||||||
|
const hasMoreNodes = useMemo(() => total - current > 0, [total, current]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (enabled && inView && hasMoreNodes && !shouldPreventLoading) {
|
||||||
|
setShouldPreventLoading(true);
|
||||||
|
onLoad();
|
||||||
|
}
|
||||||
|
}, [shouldPreventLoading, inView, enabled, hasMoreNodes, onLoad]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{enabled && !loading && hasMoreNodes && <ObserverContainer ref={scrollRef} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import NodesLoaderByButtonWrapper from '@app/homeV3/modules/hierarchyViewModule/treeView/components/itemsLoaderWrapper/NodesLoaderByButtonWrapper';
|
||||||
|
import NodesLoaderInfiniteScrollWrapper from '@app/homeV3/modules/hierarchyViewModule/treeView/components/itemsLoaderWrapper/NodesLoaderInfiniteScrollWrapper';
|
||||||
|
import { NodesLoaderWrapperProps } from '@app/homeV3/modules/hierarchyViewModule/treeView/components/itemsLoaderWrapper/types';
|
||||||
|
import { LoadingTriggerType } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
||||||
|
|
||||||
|
interface Props extends NodesLoaderWrapperProps {
|
||||||
|
trigger?: LoadingTriggerType;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NodesLoaderWrapper({ children, trigger, ...props }: React.PropsWithChildren<Props>) {
|
||||||
|
const NodesLoaderWrapperComponent = useMemo(() => {
|
||||||
|
switch (trigger) {
|
||||||
|
case 'button':
|
||||||
|
return NodesLoaderByButtonWrapper;
|
||||||
|
case 'infiniteScroll':
|
||||||
|
return NodesLoaderInfiniteScrollWrapper;
|
||||||
|
default:
|
||||||
|
return NodesLoaderByButtonWrapper;
|
||||||
|
}
|
||||||
|
}, [trigger]);
|
||||||
|
|
||||||
|
return <NodesLoaderWrapperComponent {...props}>{children}</NodesLoaderWrapperComponent>;
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
export interface NodesLoaderWrapperProps {
|
||||||
|
total: number;
|
||||||
|
// how many items already loaded
|
||||||
|
current: number;
|
||||||
|
pageSize: number;
|
||||||
|
depth: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
onLoad: () => void;
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export const DEFAULT_NUMBER_OF_CHILDREN_TO_LOAD = 5;
|
export const DEFAULT_LOAD_BATCH_SIZE = 5;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { DEFAULT_NUMBER_OF_CHILDREN_TO_LOAD } from '@app/homeV3/modules/hierarchyViewModule/treeView/constants';
|
import { DEFAULT_LOAD_BATCH_SIZE } from '@app/homeV3/modules/hierarchyViewModule/treeView/constants';
|
||||||
import { TreeViewContextType } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
import { TreeViewContextType } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
||||||
|
|
||||||
const DEFAULT_TREE_VIEW_CONTEXT: TreeViewContextType = {
|
const DEFAULT_TREE_VIEW_CONTEXT: TreeViewContextType = {
|
||||||
@ -8,6 +8,10 @@ const DEFAULT_TREE_VIEW_CONTEXT: TreeViewContextType = {
|
|||||||
|
|
||||||
getHasParentNode: () => false,
|
getHasParentNode: () => false,
|
||||||
getIsRootNode: () => false,
|
getIsRootNode: () => false,
|
||||||
|
rootNodesLength: 0,
|
||||||
|
rootNodesTotal: 0,
|
||||||
|
getChildrenLength: () => 0,
|
||||||
|
getChildrenTotal: () => 0,
|
||||||
|
|
||||||
getIsExpandable: () => false,
|
getIsExpandable: () => false,
|
||||||
getIsExpanded: () => false,
|
getIsExpanded: () => false,
|
||||||
@ -26,9 +30,8 @@ const DEFAULT_TREE_VIEW_CONTEXT: TreeViewContextType = {
|
|||||||
toggleSelected: () => {},
|
toggleSelected: () => {},
|
||||||
|
|
||||||
getIsChildrenLoading: () => false,
|
getIsChildrenLoading: () => false,
|
||||||
getNumberOfNotLoadedChildren: () => 0,
|
|
||||||
loadChildren: () => {},
|
loadChildren: () => {},
|
||||||
numberOfChildrenToLoad: DEFAULT_NUMBER_OF_CHILDREN_TO_LOAD,
|
loadBatchSize: DEFAULT_LOAD_BATCH_SIZE,
|
||||||
|
|
||||||
explicitlySelectChildren: false,
|
explicitlySelectChildren: false,
|
||||||
explicitlyUnselectChildren: false,
|
explicitlyUnselectChildren: false,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { DEFAULT_NUMBER_OF_CHILDREN_TO_LOAD } from '@app/homeV3/modules/hierarchyViewModule/treeView/constants';
|
import { DEFAULT_LOAD_BATCH_SIZE } from '@app/homeV3/modules/hierarchyViewModule/treeView/constants';
|
||||||
import TreeViewContext from '@app/homeV3/modules/hierarchyViewModule/treeView/context/TreeViewContext';
|
import TreeViewContext from '@app/homeV3/modules/hierarchyViewModule/treeView/context/TreeViewContext';
|
||||||
import { TreeNode, TreeViewContextProviderProps } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
import { TreeNode, TreeViewContextProviderProps } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
||||||
import {
|
import {
|
||||||
@ -22,6 +22,10 @@ export default function TreeViewContextProvider({
|
|||||||
selectable,
|
selectable,
|
||||||
updateSelectedValues,
|
updateSelectedValues,
|
||||||
expandParentNodesOfInitialSelectedValues,
|
expandParentNodesOfInitialSelectedValues,
|
||||||
|
loadingTriggerType = 'button',
|
||||||
|
rootNodesTotal: rootNodesTotalProperty,
|
||||||
|
loadRootNodes,
|
||||||
|
rootNodesLoading,
|
||||||
loadChildren: loadAsyncChildren,
|
loadChildren: loadAsyncChildren,
|
||||||
renderNodeLabel,
|
renderNodeLabel,
|
||||||
explicitlySelectChildren,
|
explicitlySelectChildren,
|
||||||
@ -29,7 +33,7 @@ export default function TreeViewContextProvider({
|
|||||||
explicitlySelectParent,
|
explicitlySelectParent,
|
||||||
explicitlyUnselectParent,
|
explicitlyUnselectParent,
|
||||||
enableIntermediateSelectState,
|
enableIntermediateSelectState,
|
||||||
numberOfChildrenToLoad = DEFAULT_NUMBER_OF_CHILDREN_TO_LOAD,
|
loadBatchSize = DEFAULT_LOAD_BATCH_SIZE,
|
||||||
}: React.PropsWithChildren<TreeViewContextProviderProps>) {
|
}: React.PropsWithChildren<TreeViewContextProviderProps>) {
|
||||||
const [internalExpandedValues, setInternalExpandedValues] = useState<string[]>(expandedValues ?? []);
|
const [internalExpandedValues, setInternalExpandedValues] = useState<string[]>(expandedValues ?? []);
|
||||||
const [isExpandedValuesInitialized, setIsExpandedValuesInitialized] = useState<boolean>(false);
|
const [isExpandedValuesInitialized, setIsExpandedValuesInitialized] = useState<boolean>(false);
|
||||||
@ -92,6 +96,18 @@ export default function TreeViewContextProvider({
|
|||||||
[getHasParentNode, getRootNodes, valueToTreeNodeMap],
|
[getHasParentNode, getRootNodes, valueToTreeNodeMap],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const rootNodesLength = useMemo(() => nodes.length, [nodes]);
|
||||||
|
const rootNodesTotal = useMemo(
|
||||||
|
() => rootNodesTotalProperty ?? rootNodesLength,
|
||||||
|
[rootNodesTotalProperty, rootNodesLength],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getChildrenLength = useCallback((node: TreeNode) => node.children?.length ?? 0, []);
|
||||||
|
const getChildrenTotal = useCallback(
|
||||||
|
(node: TreeNode) => node.totalChildren ?? getChildrenLength(node),
|
||||||
|
[getChildrenLength],
|
||||||
|
);
|
||||||
|
|
||||||
// Expanding
|
// Expanding
|
||||||
|
|
||||||
const getIsExpandable = useCallback((node: TreeNode) => !!node.children?.length || !!node.hasAsyncChildren, []);
|
const getIsExpandable = useCallback((node: TreeNode) => !!node.children?.length || !!node.hasAsyncChildren, []);
|
||||||
@ -258,17 +274,18 @@ export default function TreeViewContextProvider({
|
|||||||
// Loading of children
|
// Loading of children
|
||||||
const getIsChildrenLoading = useCallback((node: TreeNode) => !!node.isChildrenLoading, []);
|
const getIsChildrenLoading = useCallback((node: TreeNode) => !!node.isChildrenLoading, []);
|
||||||
|
|
||||||
const getNumberOfNotLoadedChildren = useCallback(
|
|
||||||
(node: TreeNode) => (node.totalChildren ? node.totalChildren - (node.children?.length ?? 0) : 0),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TreeViewContext.Provider
|
<TreeViewContext.Provider
|
||||||
value={{
|
value={{
|
||||||
nodes: preprocessedNodes,
|
nodes: preprocessedNodes,
|
||||||
|
|
||||||
|
// Node utils
|
||||||
getHasParentNode,
|
getHasParentNode,
|
||||||
getIsRootNode,
|
getIsRootNode,
|
||||||
|
rootNodesLength,
|
||||||
|
rootNodesTotal,
|
||||||
|
getChildrenLength,
|
||||||
|
getChildrenTotal,
|
||||||
|
|
||||||
// Expanding
|
// Expanding
|
||||||
getIsExpandable,
|
getIsExpandable,
|
||||||
@ -293,11 +310,13 @@ export default function TreeViewContextProvider({
|
|||||||
explicitlyUnselectParent,
|
explicitlyUnselectParent,
|
||||||
enableIntermediateSelectState,
|
enableIntermediateSelectState,
|
||||||
|
|
||||||
|
loadingTriggerType,
|
||||||
|
loadRootNodes,
|
||||||
|
rootNodesLoading,
|
||||||
// Async loading of children
|
// Async loading of children
|
||||||
getIsChildrenLoading,
|
getIsChildrenLoading,
|
||||||
getNumberOfNotLoadedChildren,
|
|
||||||
loadChildren,
|
loadChildren,
|
||||||
numberOfChildrenToLoad,
|
loadBatchSize,
|
||||||
|
|
||||||
renderNodeLabel,
|
renderNodeLabel,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -24,11 +24,17 @@ export interface TreeNodeProps {
|
|||||||
depth: number;
|
depth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TreeViewContextType {
|
export type LoadingTriggerType = 'button' | 'infiniteScroll';
|
||||||
nodes: TreeNode[];
|
|
||||||
|
|
||||||
|
export interface TreeViewContextType {
|
||||||
|
// Tree/node utils and params
|
||||||
|
nodes: TreeNode[];
|
||||||
getHasParentNode: (node: TreeNode) => boolean;
|
getHasParentNode: (node: TreeNode) => boolean;
|
||||||
getIsRootNode: (node: TreeNode) => boolean;
|
getIsRootNode: (node: TreeNode) => boolean;
|
||||||
|
rootNodesLength: number;
|
||||||
|
rootNodesTotal: number;
|
||||||
|
getChildrenLength: (node: TreeNode) => number;
|
||||||
|
getChildrenTotal: (node: TreeNode) => number;
|
||||||
|
|
||||||
// Expand
|
// Expand
|
||||||
getIsExpandable: (node: TreeNode) => boolean;
|
getIsExpandable: (node: TreeNode) => boolean;
|
||||||
@ -54,13 +60,18 @@ export interface TreeViewContextType {
|
|||||||
explicitlyUnselectParent?: boolean;
|
explicitlyUnselectParent?: boolean;
|
||||||
enableIntermediateSelectState?: boolean;
|
enableIntermediateSelectState?: boolean;
|
||||||
|
|
||||||
// Async loading of children
|
// Async loading
|
||||||
// -------------------------------------------------
|
// -------------------------------------------------
|
||||||
|
loadingTriggerType?: LoadingTriggerType;
|
||||||
|
// Optional loading of root nodes
|
||||||
|
loadRootNodes?: () => void;
|
||||||
|
hasMoreRootNodes?: boolean;
|
||||||
|
rootNodesLoading?: boolean;
|
||||||
|
|
||||||
getIsChildrenLoading: (node: TreeNode) => boolean;
|
getIsChildrenLoading: (node: TreeNode) => boolean;
|
||||||
getNumberOfNotLoadedChildren: (node: TreeNode) => number;
|
|
||||||
loadChildren: (node: TreeNode) => void;
|
loadChildren: (node: TreeNode) => void;
|
||||||
// Max number of children to load per each loadChildren call
|
// Max number of children to load per each loadChildren call
|
||||||
numberOfChildrenToLoad: number;
|
loadBatchSize: number;
|
||||||
|
|
||||||
// Optional custom node label renderer
|
// Optional custom node label renderer
|
||||||
renderNodeLabel?: (props: TreeNodeProps) => React.ReactNode;
|
renderNodeLabel?: (props: TreeNodeProps) => React.ReactNode;
|
||||||
@ -104,7 +115,13 @@ export interface TreeViewContextProviderProps {
|
|||||||
renderNodeLabel?: (props: TreeNodeProps) => React.ReactNode;
|
renderNodeLabel?: (props: TreeNodeProps) => React.ReactNode;
|
||||||
|
|
||||||
// Async
|
// Async
|
||||||
|
// Optional pagination/loading of root nodes
|
||||||
|
loadingTriggerType?: LoadingTriggerType;
|
||||||
|
rootNodesTotal?: number;
|
||||||
|
loadRootNodes?: () => void;
|
||||||
|
hasMoreRootNodes?: boolean;
|
||||||
|
rootNodesLoading?: boolean;
|
||||||
// Callback to load children of a specific node
|
// Callback to load children of a specific node
|
||||||
loadChildren?: (node: TreeNode) => void;
|
loadChildren?: (node: TreeNode) => void;
|
||||||
numberOfChildrenToLoad?: number;
|
loadBatchSize?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -192,3 +192,8 @@ export function sortTree(tree: TreeNode[], nodesSorter: (nodes: TreeNode[]) => T
|
|||||||
|
|
||||||
return nodesSorter(tree).map(traverse);
|
return nodesSorter(tree).map(traverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getOutOfTreeSelectedValues(selectedValues: string[], tree: TreeNode[]): string[] {
|
||||||
|
const allTreeValues = getAllValues(tree);
|
||||||
|
return selectedValues.filter((value) => !allTreeValues.includes(value));
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user