fix(customHomePage): fixes for hierarchy view (#14365)

This commit is contained in:
v-tarasevich-blitz-brain 2025-08-08 21:09:42 +03:00 committed by GitHub
parent ef200eff7b
commit a9b7d79220
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 485 additions and 46 deletions

View File

@ -3,10 +3,10 @@ import { isEqual } from 'lodash';
import { useEffect } from 'react';
import { useDomainsContext } from '@app/domainV2/DomainsContext';
import EntityRegistry from '@app/entity/EntityRegistry';
import { GenericEntityProperties } from '@app/entity/shared/types';
import usePrevious from '@app/shared/usePrevious';
import { useEntityRegistry } from '@app/useEntityRegistry';
import { EntityRegistry } from '@src/entityRegistryContext';
import { ListDomainsDocument, ListDomainsQuery } from '@graphql/domain.generated';
import { Entity, EntityType } from '@types';
@ -135,11 +135,13 @@ export function useUpdateDomainEntityDataOnChange(entityData: GenericEntityPrope
export function useSortedDomains<T extends Entity>(domains?: Array<T>, sortBy?: 'displayName') {
const entityRegistry = useEntityRegistry();
if (!domains || !sortBy) return domains;
return [...domains].sort((a, b) => {
const nameA = entityRegistry.getDisplayName(EntityType.Domain, a) || '';
const nameB = entityRegistry.getDisplayName(EntityType.Domain, b) || '';
return nameA.localeCompare(nameB);
});
return [...domains].sort((a, b) => compareDomainsByDisplayName(a, b, entityRegistry));
}
export function compareDomainsByDisplayName<T extends Entity>(domainA: T, domainB: T, entityRegistry: EntityRegistry) {
const nameA = entityRegistry.getDisplayName(EntityType.Domain, domainA) || '';
const nameB = entityRegistry.getDisplayName(EntityType.Domain, domainB) || '';
return nameA.localeCompare(nameB);
}
export function getParentDomains<T extends Entity>(domain: T, entityRegistry: EntityRegistry) {

View File

@ -77,6 +77,7 @@ describe('useLoader hook', () => {
parentValue: mockParentValue,
metadata: undefined,
maxNumberToLoad: mockMaxNumberOfChildrenToLoad,
forceHasAsyncChildren: false,
});
});
});
@ -95,6 +96,7 @@ describe('useLoader hook', () => {
parentValue: mockParentValue,
metadata: mockMetadata,
maxNumberToLoad: mockMaxNumberOfChildrenToLoad,
forceHasAsyncChildren: false,
});
});

View File

@ -11,6 +11,7 @@ export default function useChildrenDomainsLoader({
parentValue,
metadata,
maxNumberToLoad,
forceHasAsyncChildren,
}: ChildrenLoaderInputType): ChildrenLoaderResultType {
const totalNumberOfChildren = metadata?.totalNumberOfChildren ?? maxNumberToLoad;
const numberOfAlreadyLoadedChildren = metadata?.numberOfLoadedChildren ?? 0;
@ -27,7 +28,7 @@ export default function useChildrenDomainsLoader({
numberOfChildrenToLoad,
numberOfChildrenToLoad === 0,
);
const nodes = useTreeNodesFromDomains(domains);
const nodes = useTreeNodesFromDomains(domains, forceHasAsyncChildren);
const isLoading = useMemo(() => {
if (!shouldLoad) return false;

View File

@ -11,6 +11,7 @@ export default function useChildrenGlossaryLoader({
parentValue,
metadata,
maxNumberToLoad,
forceHasAsyncChildren,
}: ChildrenLoaderInputType): ChildrenLoaderResultType {
const totalNumberOfChildren = metadata?.totalNumberOfChildren ?? maxNumberToLoad;
const numberOfAlreadyLoadedChildren = metadata?.numberOfLoadedChildren ?? 0;
@ -27,7 +28,11 @@ export default function useChildrenGlossaryLoader({
count: numberOfChildrenToLoad,
skip: numberOfChildrenToLoad === 0,
});
const { treeNodes: nodes } = useTreeNodesFromGlossaryNodesAndTerms(glossaryNodes, glossaryTerms, true);
const { treeNodes: nodes } = useTreeNodesFromGlossaryNodesAndTerms(
glossaryNodes,
glossaryTerms,
forceHasAsyncChildren,
);
const isLoading = useMemo(() => {
if (!shouldLoad) return false;

View File

@ -26,6 +26,7 @@ export default function useLoader(
parentValue,
metadata,
maxNumberToLoad,
forceHasAsyncChildren: relatedEntitiesOrFilters !== undefined,
});
const relatedEntitiesResponse = loadRelatedEntities?.({

View File

@ -16,6 +16,7 @@ export interface ChildrenLoaderInputType {
maxNumberToLoad: number;
dependenciesIsLoading?: boolean;
orFilters?: AndFilterInput[];
forceHasAsyncChildren?: boolean;
}
export interface ChildrenLoaderResultType {

View File

@ -0,0 +1,90 @@
import { describe, expect } from 'vitest';
import { sortDomainTreeNodes } from '@app/homeV3/modules/hierarchyViewModule/components/domains/utils';
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
import { getTestEntityRegistry } from '@utils/test-utils/TestPageContainer';
import { Entity, EntityType } from '@types';
// Helper to create tree nodes
const createTreeNode = (entity: Entity): TreeNode => ({
value: entity.urn,
label: entity.urn,
entity,
});
describe('utils', () => {
describe('sortDomainTreeNodes', () => {
it('sorts domain nodes by display name and keeps non-domains in original order', () => {
const entityRegistry = getTestEntityRegistry();
// Create test nodes (intentionally out of order)
const nodes: TreeNode[] = [
createTreeNode({ urn: 'dataset:1', type: EntityType.Dataset, properties: { name: 'Users' } } as Entity), // Non-domain
createTreeNode({
urn: 'domain:1',
type: EntityType.Domain,
properties: { name: 'Marketing' },
} as Entity), // Domain (Marketing)
createTreeNode({
urn: 'dashboard:1',
type: EntityType.Dashboard,
properties: { name: 'Metrics' },
} as Entity), // Non-domain
createTreeNode({
urn: 'domain:2',
type: EntityType.Domain,
properties: { name: 'Engineering' },
} as Entity), // Domain (Engineering)
];
const sorted = sortDomainTreeNodes(nodes, entityRegistry);
// Verify domains are sorted first by display name (Engineering, Marketing)
expect(sorted[0].entity.urn).toBe('domain:2'); // Engineering
expect(sorted[1].entity.urn).toBe('domain:1'); // Marketing
// Verify non-domains maintain original relative order
expect(sorted[2].entity.urn).toBe('dataset:1'); // Users (first non-domain)
expect(sorted[3].entity.urn).toBe('dashboard:1'); // Metrics (second non-domain)
});
it('handles empty input', () => {
const entityRegistry = getTestEntityRegistry();
const sorted = sortDomainTreeNodes([], entityRegistry);
expect(sorted).toEqual([]);
});
it('handles all domain nodes', () => {
const entityRegistry = getTestEntityRegistry();
const nodes: TreeNode[] = [
createTreeNode({ urn: 'domain:3', type: EntityType.Domain, properties: { name: 'Z' } } as Entity),
createTreeNode({ urn: 'domain:4', type: EntityType.Domain, properties: { name: 'A' } } as Entity),
createTreeNode({ urn: 'domain:5', type: EntityType.Domain, properties: { name: 'M' } } as Entity),
];
const sorted = sortDomainTreeNodes(nodes, entityRegistry);
// Should be sorted alphabetically: A, M, Z
expect(sorted.map((n) => n.entity.urn)).toEqual([
'domain:4', // A
'domain:5', // M
'domain:3', // Z
]);
});
it('handles all non-domain nodes', () => {
const entityRegistry = getTestEntityRegistry();
const nodes: TreeNode[] = [
createTreeNode({ urn: 'dataset:2', type: EntityType.Dataset, properties: { name: 'Beta' } } as Entity),
createTreeNode({ urn: 'chart:1', type: EntityType.Chart, properties: { name: 'Alpha' } } as Entity),
];
const sorted = sortDomainTreeNodes(nodes, entityRegistry);
// Should maintain original order
expect(sorted.map((n) => n.entity.urn)).toEqual(['dataset:2', 'chart:1']);
});
});
});

View File

@ -258,7 +258,7 @@ describe('useSelectableDomainTree hook', () => {
renderHook(() => useSelectableDomainTree([]));
expect(mockUseTreeNodesFromDomains).toHaveBeenCalledWith(rootDomains);
expect(mockUseTreeNodesFromDomains).toHaveBeenCalledWith(rootDomains, false);
});
it('should handle mixed states correctly', () => {
@ -287,7 +287,7 @@ describe('useSelectableDomainTree hook', () => {
renderHook(() => useSelectableDomainTree([]));
expect(mockUseTreeNodesFromDomains).toHaveBeenCalledWith([mockDomain1]);
expect(mockUseTreeNodesFromDomains).toHaveBeenCalledWith([mockDomain1], false);
});
});
});

View File

@ -0,0 +1,11 @@
import { useCallback } from 'react';
import { sortDomainTreeNodes } from '@app/homeV3/modules/hierarchyViewModule/components/domains/utils';
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
import { useEntityRegistryV2 } from '@app/useEntityRegistry';
export default function useDomainTreeNodesSorter() {
const entityRegistry = useEntityRegistryV2();
return useCallback((nodes: TreeNode[]) => sortDomainTreeNodes(nodes, entityRegistry), [entityRegistry]);
}

View File

@ -1,3 +1,4 @@
import useDomainTreeNodesSorter from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useDomainTreeNodesSorter';
import useDomainsByUrns from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useDomainsByUrns';
import useTreeNodesFromDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useTreeNodesFromListDomains';
import useTree from '@app/homeV3/modules/hierarchyViewModule/treeView/useTree';
@ -6,7 +7,8 @@ export default function useDomainsTree(domainsUrns: string[], shouldShowRelatedE
const { domains, loading } = useDomainsByUrns(domainsUrns);
const treeNodes = useTreeNodesFromDomains(domains, shouldShowRelatedEntities);
const tree = useTree(treeNodes);
const nodesSorter = useDomainTreeNodesSorter();
const tree = useTree(treeNodes, nodesSorter);
return {
tree,

View File

@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import useDomainTreeNodesSorter from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useDomainTreeNodesSorter';
import useInitialDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useInitialDomains';
import useRootDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useRootDomains';
import useTreeNodesFromFlatDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useTreeNodesFromFlatDomains';
@ -10,13 +11,14 @@ import { mergeTrees } from '@app/homeV3/modules/hierarchyViewModule/treeView/uti
export default function useSelectableDomainTree(initialSelectedDomainUrns: string[] | undefined) {
const [isInitialized, setIsInitialized] = useState<boolean>(false);
const [selectedValues, setSelectedValues] = useState<string[]>(initialSelectedDomainUrns ?? []);
const tree = useTree();
const nodesSorter = useDomainTreeNodesSorter();
const tree = useTree(undefined, nodesSorter);
const { domains: initialDomains } = useInitialDomains(initialSelectedDomainUrns ?? []);
const initialSelectedTreeNodes = useTreeNodesFromFlatDomains(initialDomains);
const { domains: rootDomains, loading: rootDomainsLoading } = useRootDomains();
const rootTreeNodes = useTreeNodesFromDomains(rootDomains);
const rootTreeNodes = useTreeNodesFromDomains(rootDomains, false);
useEffect(() => {
if (

View File

@ -1,8 +1,10 @@
import { compareDomainsByDisplayName } from '@app/domainV2/utils';
import { DomainItem } from '@app/homeV3/modules/hierarchyViewModule/components/domains/types';
import { unwrapParentEntitiesToTreeNodes } from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/utils';
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
import { EntityRegistry } from '@src/entityRegistryContext';
import { Domain } from '@types';
import { Domain, EntityType } from '@types';
export function convertDomainToTreeNode(domain: DomainItem, forceHasAsyncChildren = false): TreeNode {
return {
@ -17,3 +19,15 @@ export function convertDomainToTreeNode(domain: DomainItem, forceHasAsyncChildre
export function unwrapFlatDomainsToTreeNodes(domains: Domain[] | undefined): TreeNode[] | undefined {
return unwrapParentEntitiesToTreeNodes(domains, (item) => [...(item.parentDomains?.domains ?? [])].reverse());
}
export function sortDomainTreeNodes(nodes: TreeNode[], entityRegistry: EntityRegistry) {
const domainTreeNodes = nodes.filter((node) => node.entity.type === EntityType.Domain);
const anotherTreeNodes = nodes.filter((node) => node.entity.type !== EntityType.Domain);
return [
...[...domainTreeNodes].sort((nodeA, nodeB) =>
compareDomainsByDisplayName(nodeA.entity, nodeB.entity, entityRegistry),
),
...anotherTreeNodes,
];
}

View File

@ -1,14 +1,21 @@
import { Input } from '@components';
import { Form } from 'antd';
import React from 'react';
import styled from 'styled-components';
import FormItem from '@app/homeV3/modules/hierarchyViewModule/components/form/components/FormItem';
import RelatedEntitiesSection from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/relatedEntities/RelatedEntitiesSection';
import SelectAssetsSection from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/SelectAssetsSection';
const FormWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
`;
export default function HierarchyViewModuleForm() {
return (
<>
<Form.Item
<FormWrapper>
<FormItem
name="name"
rules={[
{
@ -18,10 +25,10 @@ export default function HierarchyViewModuleForm() {
]}
>
<Input label="Name" placeholder="Choose a name for your module" isRequired />
</Form.Item>
</FormItem>
<SelectAssetsSection />
<RelatedEntitiesSection />
</>
</FormWrapper>
);
}

View File

@ -0,0 +1,16 @@
import { Form } from 'antd';
import styled from 'styled-components';
const FormItem = styled(Form.Item)`
margin-bottom: 0;
.ant-form-item {
margin-bottom: 0;
}
.ant-form-item-control-input {
min-height: 0;
}
`;
export default FormItem;

View File

@ -2,6 +2,7 @@ import { Form } from 'antd';
import React, { useCallback } from 'react';
import { useHierarchyFormContext } from '@app/homeV3/modules/hierarchyViewModule/components/form/HierarchyFormContext';
import FormItem from '@app/homeV3/modules/hierarchyViewModule/components/form/components/FormItem';
import {
FORM_FIELD_RELATED_ENTITIES_FILTER,
FORM_FIELD_SHOW_RELATED_ENTITIES,
@ -46,11 +47,11 @@ export default function RelatedEntitiesSection() {
return (
<>
<Form.Item name={FORM_FIELD_SHOW_RELATED_ENTITIES}>
<FormItem name={FORM_FIELD_SHOW_RELATED_ENTITIES}>
<ShowRelatedEntitiesSwitch isChecked={isChecked} onChange={toggleShowRelatedEntitiesSwitch} />
</Form.Item>
</FormItem>
<Form.Item name={FORM_FIELD_RELATED_ENTITIES_FILTER}>
<FormItem name={FORM_FIELD_RELATED_ENTITIES_FILTER}>
{isChecked && (
<LogicalFiltersBuilder
filters={relatedEntitiesFilter ?? defaultRelatedEntitiesFilter ?? EMPTY_FILTER}
@ -58,7 +59,7 @@ export default function RelatedEntitiesSection() {
properties={properties}
/>
)}
</Form.Item>
</FormItem>
</>
);
}

View File

@ -21,7 +21,7 @@ export default function ShowRelatedEntitiesSwitch({ isChecked, onChange }: Props
return (
<Wrapper>
<LabelContainer>
<Text weight="bold" lineHeight="sm">
<Text weight="bold" color="gray" lineHeight="sm">
Show Related Entities
</Text>
<Text color="gray" lineHeight="sm">

View File

@ -1,14 +1,18 @@
import { Text } from '@components';
import { Form } from 'antd';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import DomainsSelectableTreeView from '@app/homeV3/modules/hierarchyViewModule/components/domains/DomainsSelectableTreeView';
import { useHierarchyFormContext } from '@app/homeV3/modules/hierarchyViewModule/components/form/HierarchyFormContext';
import FormItem from '@app/homeV3/modules/hierarchyViewModule/components/form/components/FormItem';
import { FORM_FIELD_ASSET_TYPE } from '@app/homeV3/modules/hierarchyViewModule/components/form/constants';
import EntityTypeTabs from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs/AssetTypeTabs';
import GlossarySelectableTreeView from '@app/homeV3/modules/hierarchyViewModule/components/glossary/GlossarySelectableTreeView';
import { ASSET_TYPE_DOMAINS, ASSET_TYPE_GLOSSARY } from '@app/homeV3/modules/hierarchyViewModule/constants';
const Wrapper = styled.div``;
export default function SelectAssetsSection() {
const form = Form.useFormInstance();
@ -30,28 +34,30 @@ export default function SelectAssetsSection() {
key: ASSET_TYPE_DOMAINS,
label: 'Domains',
content: (
<Form.Item name="domainAssets">
<FormItem name="domainAssets">
<DomainsSelectableTreeView />
</Form.Item>
</FormItem>
),
},
{
key: ASSET_TYPE_GLOSSARY,
label: 'Glossary',
content: (
<Form.Item name="glossaryAssets">
<FormItem name="glossaryAssets">
<GlossarySelectableTreeView />
</Form.Item>
</FormItem>
),
},
];
return (
<>
<Text weight="bold">Search and Select Assets</Text>
<Form.Item name={FORM_FIELD_ASSET_TYPE}>
<Wrapper>
<Text color="gray" weight="bold">
Search and Select Assets
</Text>
<FormItem name={FORM_FIELD_ASSET_TYPE}>
<EntityTypeTabs tabs={tabs} onTabClick={onTabClick} defaultKey={assetType ?? defaultAssetsType} />
</Form.Item>
</>
</FormItem>
</Wrapper>
);
}

View File

@ -0,0 +1,197 @@
import { sortGlossaryTreeNodes } from '@app/homeV3/modules/hierarchyViewModule/components/glossary/utils';
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
import { getTestEntityRegistry } from '@utils/test-utils/TestPageContainer';
import { Entity, EntityType } from '@types';
// Helper to create tree nodes
const createTreeNode = (entity: Entity): TreeNode => ({
value: entity.urn,
label: entity.urn,
entity,
});
describe('utils', () => {
describe('sortGlossaryTreeNodes', () => {
it('categorizes and sorts glossary nodes, terms, and others', () => {
const entityRegistry = getTestEntityRegistry();
// Create test nodes in shuffled order
const nodes: TreeNode[] = [
createTreeNode({
urn: 'urn:dataset:1',
type: EntityType.Dataset,
properties: { name: 'Users' },
} as Entity), // Other
createTreeNode({
urn: 'urn:glossary:term:1',
type: EntityType.GlossaryTerm,
properties: { name: 'Terminal' },
} as Entity), // Term
createTreeNode({
urn: 'urn:glossary:node:1',
type: EntityType.GlossaryNode,
properties: { name: 'Zebra' },
} as Entity), // Node
createTreeNode({
urn: 'urn:chart:1',
type: EntityType.Chart,
properties: { name: 'Revenue' },
} as Entity), // Other
createTreeNode({
urn: 'urn:glossary:term:2',
type: EntityType.GlossaryTerm,
properties: { name: 'Apple' },
} as Entity), // Term
createTreeNode({
urn: 'urn:glossary:node:2',
type: EntityType.GlossaryNode,
properties: { name: 'Alpha' },
} as Entity), // Node
];
const sorted = sortGlossaryTreeNodes(nodes, entityRegistry);
// Verify grouping order: Nodes -> Terms -> Others
expect(sorted[0].entity.type).toBe(EntityType.GlossaryNode);
expect(sorted[1].entity.type).toBe(EntityType.GlossaryNode);
expect(sorted[2].entity.type).toBe(EntityType.GlossaryTerm);
expect(sorted[3].entity.type).toBe(EntityType.GlossaryTerm);
expect(sorted[4].entity.type).toBe(EntityType.Dataset);
expect(sorted[5].entity.type).toBe(EntityType.Chart);
// Verify sorted order within groups
expect(sorted[0].entity.urn).toBe('urn:glossary:node:2'); // Alpha
expect(sorted[1].entity.urn).toBe('urn:glossary:node:1'); // Zebra
expect(sorted[2].entity.urn).toBe('urn:glossary:term:2'); // Apple
expect(sorted[3].entity.urn).toBe('urn:glossary:term:1'); // Terminal
// Verify others maintain original relative order
expect(sorted[4].entity.urn).toBe('urn:dataset:1');
expect(sorted[5].entity.urn).toBe('urn:chart:1');
});
it('handles empty input', () => {
const entityRegistry = getTestEntityRegistry();
const sorted = sortGlossaryTreeNodes([], entityRegistry);
expect(sorted).toEqual([]);
});
it('handles only glossary nodes', () => {
const entityRegistry = getTestEntityRegistry();
const nodes: TreeNode[] = [
createTreeNode({
urn: 'urn:gn:1',
type: EntityType.GlossaryNode,
properties: { name: 'Beta' },
} as Entity),
createTreeNode({
urn: 'urn:gn:2',
type: EntityType.GlossaryNode,
properties: { name: 'Alpha' },
} as Entity),
createTreeNode({
urn: 'urn:gn:3',
type: EntityType.GlossaryNode,
properties: { name: 'Gamma' },
} as Entity),
];
const sorted = sortGlossaryTreeNodes(nodes, entityRegistry);
expect(sorted.map((n) => n.entity.urn)).toEqual([
'urn:gn:2', // Alpha
'urn:gn:1', // Beta
'urn:gn:3', // Gamma
]);
});
it('handles only glossary terms', () => {
const entityRegistry = getTestEntityRegistry();
const nodes: TreeNode[] = [
createTreeNode({
urn: 'urn:gt:1',
type: EntityType.GlossaryTerm,
properties: { name: 'Zulu' },
} as Entity),
createTreeNode({
urn: 'urn:gt:2',
type: EntityType.GlossaryTerm,
properties: { name: 'Alpha' },
} as Entity),
createTreeNode({
urn: 'urn:gt:3',
type: EntityType.GlossaryTerm,
properties: { name: 'Mike' },
} as Entity),
];
const sorted = sortGlossaryTreeNodes(nodes, entityRegistry);
expect(sorted.map((n) => n.entity.urn)).toEqual([
'urn:gt:2', // Alpha
'urn:gt:3', // Mike
'urn:gt:1', // Zulu
]);
});
it('handles only other entities', () => {
const entityRegistry = getTestEntityRegistry();
const nodes: TreeNode[] = [
createTreeNode({
urn: 'urn:ds:1',
type: EntityType.Dataset,
properties: { name: 'Dataset 1' },
} as Entity),
createTreeNode({
urn: 'urn:dash:1',
type: EntityType.Dashboard,
properties: { name: 'Dashboard 1' },
} as Entity),
];
const sorted = sortGlossaryTreeNodes(nodes, entityRegistry);
// Should maintain original order
expect(sorted.map((n) => n.entity.urn)).toEqual(['urn:ds:1', 'urn:dash:1']);
});
it('handles identical display names', () => {
const entityRegistry = getTestEntityRegistry();
const nodes: TreeNode[] = [
createTreeNode({
urn: 'urn:gn:2',
type: EntityType.GlossaryNode,
properties: { name: 'Same' },
} as Entity),
createTreeNode({
urn: 'urn:gn:1',
type: EntityType.GlossaryNode,
properties: { name: 'Same' },
} as Entity),
createTreeNode({
urn: 'urn:gt:2',
type: EntityType.GlossaryTerm,
properties: { name: 'Same' },
} as Entity),
createTreeNode({
urn: 'urn:gt:1',
type: EntityType.GlossaryTerm,
properties: { name: 'Same' },
} as Entity),
];
const sorted = sortGlossaryTreeNodes(nodes, entityRegistry);
// Should maintain original order within groups for identical names
expect(sorted[0].entity.urn).toBe('urn:gn:2');
expect(sorted[1].entity.urn).toBe('urn:gn:1');
expect(sorted[2].entity.urn).toBe('urn:gt:2');
expect(sorted[3].entity.urn).toBe('urn:gt:1');
});
});
});

View File

@ -1,4 +1,5 @@
import useGlossaryNodesAndTermsByUrns from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useGlossaryNodesAndTermsByUrns';
import useGlossaryTreeNodesSorter from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useGlossaryTreeNodesSorter';
import useTreeNodesFromGlossaryNodesAndTerms from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useTreeNodesFromGlossaryNodesAndTerms';
import useTree from '@app/homeV3/modules/hierarchyViewModule/treeView/useTree';
@ -11,7 +12,8 @@ export default function useGlossaryTree(glossaryNodesAndTermsUrns: string[], sho
shouldShowRelatedEntities,
);
const tree = useTree(treeNodes);
const nodesSorter = useGlossaryTreeNodesSorter();
const tree = useTree(treeNodes, nodesSorter);
return {
tree,

View File

@ -0,0 +1,11 @@
import { useCallback } from 'react';
import { sortGlossaryTreeNodes } from '@app/homeV3/modules/hierarchyViewModule/components/glossary/utils';
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
import { useEntityRegistryV2 } from '@app/useEntityRegistry';
export default function useGlossaryTreeNodesSorter() {
const entityRegistry = useEntityRegistryV2();
return useCallback((nodes: TreeNode[]) => sortGlossaryTreeNodes(nodes, entityRegistry), [entityRegistry]);
}

View File

@ -1,13 +1,15 @@
import { useEffect, useState } from 'react';
import useGlossaryNodesAndTermsByUrns from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useGlossaryNodesAndTermsByUrns';
import useGlossaryTreeNodesSorter from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useGlossaryTreeNodesSorter';
import useRootGlossaryNodesAndTerms from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useRootGlossaryNodesAndTerms';
import useTreeNodesFromFlatGlossaryNodesAndTerms from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useTreeNodesFromFlatGlossaryNodesAndTerms';
import useTreeNodesFromGlossaryNodesAndTerms from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useTreeNodesFromGlossaryNodesAndTerms';
import useTree from '@app/homeV3/modules/hierarchyViewModule/treeView/useTree';
export default function useSelectableGlossaryTree(initialSelectedGlossaryNodesAndTermsUrns: string[]) {
const tree = useTree();
const nodesSorter = useGlossaryTreeNodesSorter();
const tree = useTree(undefined, nodesSorter);
const [isInitialized, setIsInitialized] = useState<boolean>(false);
const [selectedValues, setSelectedValues] = useState<string[]>(initialSelectedGlossaryNodesAndTermsUrns ?? []);

View File

@ -12,11 +12,8 @@ export default function useTreeNodesFromGlossaryNodesAndTerms(
forceHasAsyncChildren?: boolean,
) {
const glossaryNodesTreeNodes = useMemo(
() =>
glossaryNodes?.map((glossaryNode) =>
convertGlossaryNodeToTreeNode(glossaryNode, !!forceHasAsyncChildren),
) ?? [],
[glossaryNodes, forceHasAsyncChildren],
() => glossaryNodes?.map((glossaryNode) => convertGlossaryNodeToTreeNode(glossaryNode)) ?? [],
[glossaryNodes],
);
const glossaryTermsTreeNodes = useMemo(
() =>

View File

@ -1,8 +1,11 @@
import { sortGlossaryNodes } from '@app/entityV2/glossaryNode/utils';
import { sortGlossaryTerms } from '@app/entityV2/glossaryTerm/utils';
import { unwrapParentEntitiesToTreeNodes } from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/utils';
import { GlossaryNodeType, GlossaryTermType } from '@app/homeV3/modules/hierarchyViewModule/components/glossary/types';
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
import { EntityRegistry } from '@src/entityRegistryContext';
import { GlossaryNode, GlossaryTerm } from '@types';
import { EntityType, GlossaryNode, GlossaryTerm } from '@types';
export function convertGlossaryNodeToTreeNode(glossaryNode: GlossaryNodeType, forceHasAsyncChildren = false): TreeNode {
const childrenNodesCount = glossaryNode.childrenCount?.nodesCount ?? 0;
@ -34,3 +37,25 @@ export function unwrapFlatGlossaryNodesToTreeNodes(glossaryItems: GlossaryNode[]
export function unwrapFlatGlossaryTermsToTreeNodes(glossaryItems: GlossaryTerm[] | undefined): TreeNode[] | undefined {
return unwrapParentEntitiesToTreeNodes(glossaryItems, (item) => [...(item.parentNodes?.nodes ?? [])].reverse());
}
function sortGlossaryNodeTreeNodes(nodes: TreeNode[], entityRegistry: EntityRegistry): TreeNode[] {
return [...nodes].sort((nodeA, nodeB) => sortGlossaryNodes(entityRegistry, nodeA.entity, nodeB.entity));
}
function sortGlossaryTermTreeNodes(nodes: TreeNode[], entityRegistry: EntityRegistry): TreeNode[] {
return [...nodes].sort((nodeA, nodeB) => sortGlossaryTerms(entityRegistry, nodeA.entity, nodeB.entity));
}
export function sortGlossaryTreeNodes(nodes: TreeNode[], entityRegistry: EntityRegistry): TreeNode[] {
const glossaryNodeNodes = nodes.filter((node) => node.entity.type === EntityType.GlossaryNode);
const glossaryTermNodes = nodes.filter((node) => node.entity.type === EntityType.GlossaryTerm);
const anotherNodes = nodes.filter(
(node) => ![EntityType.GlossaryNode, EntityType.GlossaryTerm].includes(node.entity.type),
);
return [
...sortGlossaryNodeTreeNodes(glossaryNodeNodes, entityRegistry),
...sortGlossaryTermTreeNodes(glossaryTermNodes, entityRegistry),
...anotherNodes,
];
}

View File

@ -19,7 +19,7 @@ function createTreeNode(value: string, label?: string, children?: TreeNode[], en
describe('useTree hook', () => {
describe('initialization', () => {
it('should initialize with empty array when no initial tree provided', () => {
const { result } = renderHook(() => useTree());
const { result } = renderHook(() => useTree(undefined));
expect(result.current.nodes).toEqual([]);
});
@ -59,6 +59,25 @@ describe('useTree hook', () => {
});
});
describe('sorting of tree', () => {
it('should sort tree if sorter is passed', () => {
const tree = [
createTreeNode('root2', '', [createTreeNode('root2-child2'), createTreeNode('root2-child1')]),
createTreeNode('root1', '', [createTreeNode('root1-child2'), createTreeNode('root1-child1')]),
];
const nodesSorter = (nodes: TreeNode[]) =>
nodes.sort((nodeA, nodeB) => nodeA.value.localeCompare(nodeB.value));
const { result } = renderHook(() => useTree(tree, nodesSorter));
expect(result.current.nodes).toStrictEqual([
createTreeNode('root1', '', [createTreeNode('root1-child1'), createTreeNode('root1-child2')]),
createTreeNode('root2', '', [createTreeNode('root2-child1'), createTreeNode('root2-child2')]),
]);
});
});
describe('replace function', () => {
it('should replace entire tree with new nodes', () => {
const initialNodes = [createTreeNode('old')];

View File

@ -1,15 +1,25 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
import { mergeTrees, updateNodeInTree, updateTree } from '@app/homeV3/modules/hierarchyViewModule/treeView/utils';
import {
mergeTrees,
sortTree,
updateNodeInTree,
updateTree,
} from '@app/homeV3/modules/hierarchyViewModule/treeView/utils';
export default function useTree(tree?: TreeNode[]) {
export default function useTree(tree: TreeNode[] | undefined, nodesSorter?: (nodes: TreeNode[]) => TreeNode[]) {
const [nodes, setNodes] = useState<TreeNode[]>(tree ?? []);
useEffect(() => {
if (tree !== undefined) setNodes(tree);
}, [tree]);
const sortedNodes = useMemo(() => {
if (!nodesSorter) return nodes;
return sortTree(nodes, nodesSorter);
}, [nodes, nodesSorter]);
const replace = useCallback((newNodes: TreeNode[]) => setNodes(newNodes), []);
const merge = useCallback((treeToMerge: TreeNode[]) => {
@ -25,7 +35,7 @@ export default function useTree(tree?: TreeNode[]) {
}, []);
return {
nodes,
nodes: sortedNodes,
replace,
merge,
update,

View File

@ -179,3 +179,16 @@ export function getTopLevelSelectedValuesFromTree(selectedValues: string[], tree
return [...result];
}
export function sortTree(tree: TreeNode[], nodesSorter: (nodes: TreeNode[]) => TreeNode[]): TreeNode[] {
function traverse(node: TreeNode) {
if (!node.children?.length) return node;
return {
...node,
children: nodesSorter(node.children).map(traverse),
};
}
return nodesSorter(tree).map(traverse);
}

View File

@ -17,6 +17,7 @@ import { DataPlatformInstanceEntity } from '@app/entity/dataPlatformInstance/Dat
import { DataProductEntity } from '@app/entity/dataProduct/DataProductEntity';
import { DatasetEntity } from '@app/entity/dataset/DatasetEntity';
import { DomainEntity } from '@app/entity/domain/DomainEntity';
import GlossaryNodeEntity from '@app/entity/glossaryNode/GlossaryNodeEntity';
import { GlossaryTermEntity } from '@app/entity/glossaryTerm/GlossaryTermEntity';
import { GroupEntity } from '@app/entity/group/Group';
import { MLFeatureTableEntity } from '@app/entity/mlFeatureTable/MLFeatureTableEntity';
@ -48,6 +49,7 @@ export function getTestEntityRegistry() {
entityRegistry.register(new TagEntity());
entityRegistry.register(new DataFlowEntity());
entityRegistry.register(new DataJobEntity());
entityRegistry.register(new GlossaryNodeEntity());
entityRegistry.register(new GlossaryTermEntity());
entityRegistry.register(new MLFeatureTableEntity());
entityRegistry.register(new MLModelEntity());