Glossary expand fix (#16066)

* Fix #16046 : modify glossaryTerms api endpoint to support querying immediate children with childrenCount

* fix(ui): support lazy loading for n level of glossary

* fix modal update for glossaryTerm

* fixed expand/collapse button bug and update glossary page issue

---------

Co-authored-by: sonikashah <sonikashah94@gmail.com>
Co-authored-by: Shailesh Parmar <shailesh.parmar.webdev@gmail.com>
This commit is contained in:
Chirag Madlani 2024-04-30 12:42:43 +05:30 committed by GitHub
parent 97d3625302
commit 0701fed67e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 426 additions and 155 deletions

View File

@ -32,6 +32,7 @@ import TabsLabel from '../../common/TabsLabel/TabsLabel.component';
import GlossaryDetailsRightPanel from '../GlossaryDetailsRightPanel/GlossaryDetailsRightPanel.component'; import GlossaryDetailsRightPanel from '../GlossaryDetailsRightPanel/GlossaryDetailsRightPanel.component';
import GlossaryHeader from '../GlossaryHeader/GlossaryHeader.component'; import GlossaryHeader from '../GlossaryHeader/GlossaryHeader.component';
import GlossaryTermTab from '../GlossaryTermTab/GlossaryTermTab.component'; import GlossaryTermTab from '../GlossaryTermTab/GlossaryTermTab.component';
import { useGlossaryStore } from '../useGlossary.store';
import './glossary-details.less'; import './glossary-details.less';
import { import {
GlossaryDetailsProps, GlossaryDetailsProps,
@ -40,11 +41,9 @@ import {
const GlossaryDetails = ({ const GlossaryDetails = ({
permissions, permissions,
glossary,
updateGlossary, updateGlossary,
updateVote, updateVote,
handleGlossaryDelete, handleGlossaryDelete,
glossaryTerms,
termsLoading, termsLoading,
refreshGlossaryTerms, refreshGlossaryTerms,
onAddGlossaryTerm, onAddGlossaryTerm,
@ -54,7 +53,7 @@ const GlossaryDetails = ({
}: GlossaryDetailsProps) => { }: GlossaryDetailsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory(); const history = useHistory();
const { activeGlossary: glossary } = useGlossaryStore();
const { tab: activeTab } = useParams<{ tab: string }>(); const { tab: activeTab } = useParams<{ tab: string }>();
const [feedCount, setFeedCount] = useState<FeedCounts>( const [feedCount, setFeedCount] = useState<FeedCounts>(
FEED_COUNT_INITIAL_DATA FEED_COUNT_INITIAL_DATA
@ -152,7 +151,7 @@ const GlossaryDetails = ({
entityName={getEntityName(glossary)} entityName={getEntityName(glossary)}
entityType={EntityType.GLOSSARY} entityType={EntityType.GLOSSARY}
hasEditAccess={permissions.EditDescription || permissions.EditAll} hasEditAccess={permissions.EditDescription || permissions.EditAll}
isDescriptionExpanded={isEmpty(glossaryTerms)} isDescriptionExpanded={isEmpty(glossary.children)}
isEdit={isDescriptionEditable} isEdit={isDescriptionEditable}
owner={glossary?.owner} owner={glossary?.owner}
showActions={!glossary.deleted} showActions={!glossary.deleted}
@ -164,10 +163,8 @@ const GlossaryDetails = ({
<GlossaryTermTab <GlossaryTermTab
isGlossary isGlossary
childGlossaryTerms={glossaryTerms}
permissions={permissions} permissions={permissions}
refreshGlossaryTerms={refreshGlossaryTerms} refreshGlossaryTerms={refreshGlossaryTerms}
selectedData={glossary}
termsLoading={termsLoading} termsLoading={termsLoading}
onAddGlossaryTerm={onAddGlossaryTerm} onAddGlossaryTerm={onAddGlossaryTerm}
onEditGlossaryTerm={onEditGlossaryTerm} onEditGlossaryTerm={onEditGlossaryTerm}
@ -192,7 +189,6 @@ const GlossaryDetails = ({
isVersionView, isVersionView,
permissions, permissions,
glossary, glossary,
glossaryTerms,
termsLoading, termsLoading,
description, description,
isDescriptionEditable, isDescriptionEditable,

View File

@ -24,8 +24,7 @@ export enum GlossaryTabs {
export type GlossaryDetailsProps = { export type GlossaryDetailsProps = {
isVersionView?: boolean; isVersionView?: boolean;
permissions: OperationPermission; permissions: OperationPermission;
glossary: Glossary;
glossaryTerms: GlossaryTerm[];
termsLoading: boolean; termsLoading: boolean;
updateGlossary: (value: Glossary) => Promise<void>; updateGlossary: (value: Glossary) => Promise<void>;
updateVote?: (data: VotingDataProps) => Promise<void>; updateVote?: (data: VotingDataProps) => Promise<void>;

View File

@ -12,27 +12,22 @@
*/ */
import { FilterOutlined } from '@ant-design/icons'; import { FilterOutlined } from '@ant-design/icons';
import { import Icon from '@ant-design/icons/lib/components/Icon';
Button, import { Button, Col, Modal, Row, Space, TableProps, Tooltip } from 'antd';
Col,
Modal,
Row,
Space,
Table,
TableProps,
Tooltip,
} from 'antd';
import { ColumnsType, ExpandableConfig } from 'antd/lib/table/interface'; import { ColumnsType, ExpandableConfig } from 'antd/lib/table/interface';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import classNames from 'classnames'; import classNames from 'classnames';
import { compare } from 'fast-json-patch'; import { compare } from 'fast-json-patch';
import { isEmpty, isUndefined } from 'lodash'; import { cloneDeep, isEmpty, isUndefined } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ReactComponent as IconDrag } from '../../../assets/svg/drag.svg';
import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg'; import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg';
import { ReactComponent as IconDown } from '../../../assets/svg/ic-arrow-down.svg';
import { ReactComponent as IconRight } from '../../../assets/svg/ic-arrow-right.svg';
import { ReactComponent as DownUpArrowIcon } from '../../../assets/svg/ic-down-up-arrow.svg'; import { ReactComponent as DownUpArrowIcon } from '../../../assets/svg/ic-down-up-arrow.svg';
import { ReactComponent as UpDownArrowIcon } from '../../../assets/svg/ic-up-down-arrow.svg'; import { ReactComponent as UpDownArrowIcon } from '../../../assets/svg/ic-up-down-arrow.svg';
import { ReactComponent as PlusOutlinedIcon } from '../../../assets/svg/plus-outlined.svg'; import { ReactComponent as PlusOutlinedIcon } from '../../../assets/svg/plus-outlined.svg';
@ -40,7 +35,11 @@ import ErrorPlaceHolder from '../../../components/common/ErrorWithPlaceholder/Er
import { OwnerLabel } from '../../../components/common/OwnerLabel/OwnerLabel.component'; import { OwnerLabel } from '../../../components/common/OwnerLabel/OwnerLabel.component';
import RichTextEditorPreviewer from '../../../components/common/RichTextEditor/RichTextEditorPreviewer'; import RichTextEditorPreviewer from '../../../components/common/RichTextEditor/RichTextEditorPreviewer';
import StatusBadge from '../../../components/common/StatusBadge/StatusBadge.component'; import StatusBadge from '../../../components/common/StatusBadge/StatusBadge.component';
import { DE_ACTIVE_COLOR } from '../../../constants/constants'; import {
API_RES_MAX_SIZE,
DE_ACTIVE_COLOR,
TEXT_BODY_COLOR,
} from '../../../constants/constants';
import { GLOSSARIES_DOCS } from '../../../constants/docs.constants'; import { GLOSSARIES_DOCS } from '../../../constants/docs.constants';
import { TABLE_CONSTANTS } from '../../../constants/Teams.constants'; import { TABLE_CONSTANTS } from '../../../constants/Teams.constants';
import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum';
@ -50,20 +49,28 @@ import {
Status, Status,
} from '../../../generated/entity/data/glossaryTerm'; } from '../../../generated/entity/data/glossaryTerm';
import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useApplicationStore } from '../../../hooks/useApplicationStore';
import { patchGlossaryTerm } from '../../../rest/glossaryAPI'; import {
getFirstLevelGlossaryTerms,
getGlossaryTerms,
GlossaryTermWithChildren,
patchGlossaryTerm,
} from '../../../rest/glossaryAPI';
import { Transi18next } from '../../../utils/CommonUtils'; import { Transi18next } from '../../../utils/CommonUtils';
import { getEntityName } from '../../../utils/EntityUtils'; import { getEntityName } from '../../../utils/EntityUtils';
import Fqn from '../../../utils/Fqn'; import Fqn from '../../../utils/Fqn';
import { import {
buildTree, buildTree,
findExpandableKeysForArray,
findGlossaryTermByFqn,
StatusClass, StatusClass,
StatusFilters, StatusFilters,
} from '../../../utils/GlossaryUtils'; } from '../../../utils/GlossaryUtils';
import { getGlossaryPath } from '../../../utils/RouterUtils'; import { getGlossaryPath } from '../../../utils/RouterUtils';
import { getTableExpandableConfig } from '../../../utils/TableUtils';
import { showErrorToast } from '../../../utils/ToastUtils'; import { showErrorToast } from '../../../utils/ToastUtils';
import { DraggableBodyRowProps } from '../../common/Draggable/DraggableBodyRowProps.interface'; import { DraggableBodyRowProps } from '../../common/Draggable/DraggableBodyRowProps.interface';
import Loader from '../../common/Loader/Loader'; import Loader from '../../common/Loader/Loader';
import Table from '../../common/Table/Table';
import { ModifiedGlossary, useGlossaryStore } from '../useGlossary.store';
import { import {
GlossaryTermTabProps, GlossaryTermTabProps,
ModifiedGlossaryTerm, ModifiedGlossaryTerm,
@ -71,37 +78,38 @@ import {
} from './GlossaryTermTab.interface'; } from './GlossaryTermTab.interface';
const GlossaryTermTab = ({ const GlossaryTermTab = ({
childGlossaryTerms = [],
refreshGlossaryTerms, refreshGlossaryTerms,
permissions, permissions,
isGlossary, isGlossary,
selectedData,
termsLoading, termsLoading,
onAddGlossaryTerm, onAddGlossaryTerm,
onEditGlossaryTerm, onEditGlossaryTerm,
className, className,
}: GlossaryTermTabProps) => { }: GlossaryTermTabProps) => {
const { activeGlossary, updateActiveGlossary } = useGlossaryStore();
const { theme } = useApplicationStore(); const { theme } = useApplicationStore();
const { t } = useTranslation(); const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(true);
const [glossaryTerms, setGlossaryTerms] = useState<ModifiedGlossaryTerm[]>(
[]
);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]); const glossaryTerms = activeGlossary?.children as ModifiedGlossaryTerm[];
const [movedGlossaryTerm, setMovedGlossaryTerm] = const [movedGlossaryTerm, setMovedGlossaryTerm] =
useState<MoveGlossaryTermType>(); useState<MoveGlossaryTermType>();
const [isModalOpen, setIsModalOpen] = useState<boolean>(false); const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [isTableLoading, setIsTableLoading] = useState(false); const [isTableLoading, setIsTableLoading] = useState(false);
const [isTableHovered, setIsTableHovered] = useState(false); const [isTableHovered, setIsTableHovered] = useState(false);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const glossaryTermStatus: Status | null = useMemo(() => { const glossaryTermStatus: Status | null = useMemo(() => {
if (!isGlossary) { if (!isGlossary) {
return (selectedData as GlossaryTerm).status ?? Status.Approved; return (activeGlossary as GlossaryTerm).status ?? Status.Approved;
} }
return null; return null;
}, [isGlossary, selectedData]); }, [isGlossary, activeGlossary]);
const expandableKeys = useMemo(() => {
return findExpandableKeysForArray(glossaryTerms);
}, [glossaryTerms]);
const columns = useMemo(() => { const columns = useMemo(() => {
const data: ColumnsType<ModifiedGlossaryTerm> = [ const data: ColumnsType<ModifiedGlossaryTerm> = [
@ -240,22 +248,71 @@ const GlossaryTermTab = ({
}, [glossaryTerms, permissions]); }, [glossaryTerms, permissions]);
const handleAddGlossaryTermClick = () => { const handleAddGlossaryTermClick = () => {
onAddGlossaryTerm(!isGlossary ? (selectedData as GlossaryTerm) : undefined); onAddGlossaryTerm(
!isGlossary ? (activeGlossary as GlossaryTerm) : undefined
);
}; };
const expandableConfig: ExpandableConfig<ModifiedGlossaryTerm> = useMemo( const expandableConfig: ExpandableConfig<ModifiedGlossaryTerm> = useMemo(
() => ({ () => ({
...getTableExpandableConfig<ModifiedGlossaryTerm>(true), expandIcon: ({ expanded, onExpand, record }) => {
expandedRowKeys, const { children, childrenCount } = record;
onExpand: (expanded, record) => {
setExpandedRowKeys( return childrenCount ?? children?.length ?? 0 > 0 ? (
expanded <>
? [...expandedRowKeys, record.fullyQualifiedName || ''] <IconDrag className="m-r-xs drag-icon" height={12} width={8} />
: expandedRowKeys.filter((key) => key !== record.fullyQualifiedName) <Icon
className="m-r-xs vertical-baseline"
component={expanded ? IconDown : IconRight}
data-testid="expand-icon"
style={{ fontSize: '10px', color: TEXT_BODY_COLOR }}
onClick={(e) => onExpand(record, e)}
/>
</>
) : (
<>
<IconDrag className="m-r-xs drag-icon" height={12} width={8} />
<span className="expand-cell-empty-icon-container" />
</>
); );
}, },
expandedRowKeys: expandedRowKeys,
onExpand: async (expanded, record) => {
if (expanded) {
let children = record.children as GlossaryTermWithChildren[];
if (!children?.length) {
const { data } = await getFirstLevelGlossaryTerms(
record.fullyQualifiedName || ''
);
const terms = cloneDeep(glossaryTerms) ?? [];
const item = findGlossaryTermByFqn(
terms,
record.fullyQualifiedName ?? ''
);
(item as ModifiedGlossary).children = data;
updateActiveGlossary({ children: terms });
children = data;
}
setExpandedRowKeys([
...expandedRowKeys,
record.fullyQualifiedName || '',
]);
return children;
} else {
setExpandedRowKeys(
expandedRowKeys.filter((key) => key !== record.fullyQualifiedName)
);
}
return <Loader />;
},
}), }),
[expandedRowKeys] [glossaryTerms, updateActiveGlossary, expandedRowKeys]
); );
const handleMoveRow = useCallback( const handleMoveRow = useCallback(
@ -324,33 +381,47 @@ const GlossaryTermTab = ({
handleTableHover, handleTableHover,
} as DraggableBodyRowProps<GlossaryTerm>); } as DraggableBodyRowProps<GlossaryTerm>);
const toggleExpandAll = () => {
if (expandedRowKeys.length === childGlossaryTerms.length) {
setExpandedRowKeys([]);
} else {
setExpandedRowKeys(
childGlossaryTerms.map((item) => item.fullyQualifiedName || '')
);
}
};
const onDragConfirmationModalClose = useCallback(() => { const onDragConfirmationModalClose = useCallback(() => {
setIsModalOpen(false); setIsModalOpen(false);
setIsTableHovered(false); setIsTableHovered(false);
}, []); }, []);
useEffect(() => { const fetchAllTerms = async () => {
if (childGlossaryTerms) { setIsTableLoading(true);
const data = buildTree(childGlossaryTerms); const { data } = await getGlossaryTerms({
setGlossaryTerms(data as ModifiedGlossaryTerm[]); glossary: activeGlossary?.id || '',
setExpandedRowKeys( limit: API_RES_MAX_SIZE,
childGlossaryTerms.map((item) => item.fullyQualifiedName || '') fields: 'children,owner,parent',
); });
updateActiveGlossary({
children: buildTree(data) as ModifiedGlossary['children'],
});
const keys = data.reduce((prev, curr) => {
if (curr.children?.length) {
prev.push(curr.fullyQualifiedName ?? '');
} }
setIsLoading(false);
}, [childGlossaryTerms]);
if (termsLoading || isLoading) { return prev;
}, [] as string[]);
setExpandedRowKeys(keys);
setIsTableLoading(false);
};
const toggleExpandAll = () => {
if (expandedRowKeys.length === expandableKeys.length) {
setExpandedRowKeys([]);
} else {
fetchAllTerms();
}
};
const isAllExpanded = useMemo(() => {
return expandedRowKeys.length === expandableKeys.length;
}, [expandedRowKeys, expandableKeys]);
if (termsLoading) {
return <Loader />; return <Loader />;
} }
@ -382,15 +453,13 @@ const GlossaryTermTab = ({
type="text" type="text"
onClick={toggleExpandAll}> onClick={toggleExpandAll}>
<Space align="center" size={4}> <Space align="center" size={4}>
{expandedRowKeys.length === childGlossaryTerms.length ? ( {isAllExpanded ? (
<DownUpArrowIcon color={DE_ACTIVE_COLOR} height="14px" /> <DownUpArrowIcon color={DE_ACTIVE_COLOR} height="14px" />
) : ( ) : (
<UpDownArrowIcon color={DE_ACTIVE_COLOR} height="14px" /> <UpDownArrowIcon color={DE_ACTIVE_COLOR} height="14px" />
)} )}
{expandedRowKeys.length === childGlossaryTerms.length {isAllExpanded ? t('label.collapse-all') : t('label.expand-all')}
? t('label.collapse-all')
: t('label.expand-all')}
</Space> </Space>
</Button> </Button>
</div> </div>
@ -437,7 +506,9 @@ const GlossaryTermTab = ({
renderElement={<strong />} renderElement={<strong />}
values={{ values={{
from: movedGlossaryTerm?.from.name, from: movedGlossaryTerm?.from.name,
to: movedGlossaryTerm?.to?.name ?? getEntityName(selectedData), to:
movedGlossaryTerm?.to?.name ??
(activeGlossary && getEntityName(activeGlossary)),
entity: isUndefined(movedGlossaryTerm?.to) entity: isUndefined(movedGlossaryTerm?.to)
? '' ? ''
: t('label.term-lowercase'), : t('label.term-lowercase'),

View File

@ -12,12 +12,9 @@
*/ */
import { OperationPermission } from '../../../context/PermissionProvider/PermissionProvider.interface'; import { OperationPermission } from '../../../context/PermissionProvider/PermissionProvider.interface';
import { Glossary } from '../../../generated/entity/data/glossary';
import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm'; import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm';
export interface GlossaryTermTabProps { export interface GlossaryTermTabProps {
selectedData: Glossary | GlossaryTerm;
childGlossaryTerms: GlossaryTerm[];
isGlossary: boolean; isGlossary: boolean;
termsLoading: boolean; termsLoading: boolean;
refreshGlossaryTerms: () => void; refreshGlossaryTerms: () => void;

View File

@ -25,13 +25,14 @@ import {
mockedGlossaryTerms, mockedGlossaryTerms,
MOCK_PERMISSIONS, MOCK_PERMISSIONS,
} from '../../../mocks/Glossary.mock'; } from '../../../mocks/Glossary.mock';
import { useGlossaryStore } from '../useGlossary.store';
import GlossaryTermTab from './GlossaryTermTab.component'; import GlossaryTermTab from './GlossaryTermTab.component';
const mockOnAddGlossaryTerm = jest.fn(); const mockOnAddGlossaryTerm = jest.fn();
const mockRefreshGlossaryTerms = jest.fn(); const mockRefreshGlossaryTerms = jest.fn();
const mockOnEditGlossaryTerm = jest.fn(); const mockOnEditGlossaryTerm = jest.fn();
const mockProps1 = { const mockProps = {
childGlossaryTerms: [], childGlossaryTerms: [],
isGlossary: false, isGlossary: false,
permissions: MOCK_PERMISSIONS, permissions: MOCK_PERMISSIONS,
@ -42,11 +43,6 @@ const mockProps1 = {
onEditGlossaryTerm: mockOnEditGlossaryTerm, onEditGlossaryTerm: mockOnEditGlossaryTerm,
}; };
const mockProps2 = {
...mockProps1,
childGlossaryTerms: mockedGlossaryTerms,
};
jest.mock('../../../rest/glossaryAPI', () => ({ jest.mock('../../../rest/glossaryAPI', () => ({
getGlossaryTerms: jest getGlossaryTerms: jest
.fn() .fn()
@ -75,10 +71,16 @@ jest.mock('../../common/Loader/Loader', () =>
jest.mock('../../common/OwnerLabel/OwnerLabel.component', () => ({ jest.mock('../../common/OwnerLabel/OwnerLabel.component', () => ({
OwnerLabel: jest.fn().mockImplementation(() => <div>OwnerLabel</div>), OwnerLabel: jest.fn().mockImplementation(() => <div>OwnerLabel</div>),
})); }));
jest.mock('../useGlossary.store', () => ({
useGlossaryStore: jest.fn().mockImplementation(() => ({
activeGlossary: mockedGlossaryTerms[0],
updateActiveGlossary: jest.fn(),
})),
}));
describe('Test GlossaryTermTab component', () => { describe('Test GlossaryTermTab component', () => {
it('should show the ErrorPlaceHolder component, if no glossary is present', () => { it('should show the ErrorPlaceHolder component, if no glossary is present', () => {
const { container } = render(<GlossaryTermTab {...mockProps1} />, { const { container } = render(<GlossaryTermTab {...mockProps} />, {
wrapper: MemoryRouter, wrapper: MemoryRouter,
}); });
@ -86,7 +88,7 @@ describe('Test GlossaryTermTab component', () => {
}); });
it('should call the onAddGlossaryTerm fn onClick of add button in ErrorPlaceHolder', () => { it('should call the onAddGlossaryTerm fn onClick of add button in ErrorPlaceHolder', () => {
const { container } = render(<GlossaryTermTab {...mockProps1} />, { const { container } = render(<GlossaryTermTab {...mockProps} />, {
wrapper: MemoryRouter, wrapper: MemoryRouter,
}); });
@ -96,7 +98,14 @@ describe('Test GlossaryTermTab component', () => {
}); });
it('should contain all necessary fields value in table when glossary data is not empty', async () => { it('should contain all necessary fields value in table when glossary data is not empty', async () => {
const { container } = render(<GlossaryTermTab {...mockProps2} />, { (useGlossaryStore as unknown as jest.Mock).mockImplementation(() => ({
activeGlossary: {
...mockedGlossaryTerms[0],
children: mockedGlossaryTerms,
},
updateActiveGlossary: jest.fn(),
}));
const { container } = render(<GlossaryTermTab {...mockProps} />, {
wrapper: MemoryRouter, wrapper: MemoryRouter,
}); });

View File

@ -51,6 +51,7 @@ import { AssetSelectionModal } from '../../DataAssets/AssetsSelectionModal/Asset
import { GlossaryTabs } from '../GlossaryDetails/GlossaryDetails.interface'; import { GlossaryTabs } from '../GlossaryDetails/GlossaryDetails.interface';
import GlossaryHeader from '../GlossaryHeader/GlossaryHeader.component'; import GlossaryHeader from '../GlossaryHeader/GlossaryHeader.component';
import GlossaryTermTab from '../GlossaryTermTab/GlossaryTermTab.component'; import GlossaryTermTab from '../GlossaryTermTab/GlossaryTermTab.component';
import { useGlossaryStore } from '../useGlossary.store';
import { GlossaryTermsV1Props } from './GlossaryTermsV1.interface'; import { GlossaryTermsV1Props } from './GlossaryTermsV1.interface';
import AssetsTabs, { AssetsTabRef } from './tabs/AssetsTabs.component'; import AssetsTabs, { AssetsTabRef } from './tabs/AssetsTabs.component';
import { AssetsOfEntity } from './tabs/AssetsTabs.interface'; import { AssetsOfEntity } from './tabs/AssetsTabs.interface';
@ -58,7 +59,6 @@ import GlossaryOverviewTab from './tabs/GlossaryOverviewTab.component';
const GlossaryTermsV1 = ({ const GlossaryTermsV1 = ({
glossaryTerm, glossaryTerm,
childGlossaryTerms,
handleGlossaryTermUpdate, handleGlossaryTermUpdate,
handleGlossaryTermDelete, handleGlossaryTermDelete,
permissions, permissions,
@ -82,6 +82,8 @@ const GlossaryTermsV1 = ({
FEED_COUNT_INITIAL_DATA FEED_COUNT_INITIAL_DATA
); );
const [assetCount, setAssetCount] = useState<number>(0); const [assetCount, setAssetCount] = useState<number>(0);
const { activeGlossary } = useGlossaryStore();
const childGlossaryTerms = activeGlossary?.children ?? [];
const assetPermissions = useMemo(() => { const assetPermissions = useMemo(() => {
const glossaryTermStatus = glossaryTerm.status ?? Status.Approved; const glossaryTermStatus = glossaryTerm.status ?? Status.Approved;
@ -198,12 +200,10 @@ const GlossaryTermsV1 = ({
key: 'terms', key: 'terms',
children: ( children: (
<GlossaryTermTab <GlossaryTermTab
childGlossaryTerms={childGlossaryTerms}
className="p-md glossary-term-table-container" className="p-md glossary-term-table-container"
isGlossary={false} isGlossary={false}
permissions={permissions} permissions={permissions}
refreshGlossaryTerms={refreshGlossaryTerms} refreshGlossaryTerms={refreshGlossaryTerms}
selectedData={glossaryTerm}
termsLoading={termsLoading} termsLoading={termsLoading}
onAddGlossaryTerm={onAddGlossaryTerm} onAddGlossaryTerm={onAddGlossaryTerm}
onEditGlossaryTerm={onEditGlossaryTerm} onEditGlossaryTerm={onEditGlossaryTerm}

View File

@ -19,7 +19,6 @@ export interface GlossaryTermsV1Props {
isVersionView?: boolean; isVersionView?: boolean;
permissions: OperationPermission; permissions: OperationPermission;
glossaryTerm: GlossaryTerm; glossaryTerm: GlossaryTerm;
childGlossaryTerms: GlossaryTerm[];
handleGlossaryTermUpdate: (data: GlossaryTerm) => Promise<void>; handleGlossaryTermUpdate: (data: GlossaryTerm) => Promise<void>;
handleGlossaryTermDelete: (id: string) => Promise<void>; handleGlossaryTermDelete: (id: string) => Promise<void>;
refreshGlossaryTerms: () => void; refreshGlossaryTerms: () => void;

View File

@ -88,7 +88,7 @@ const mockProps = {
describe('Test Glossary-term component', () => { describe('Test Glossary-term component', () => {
it('Should render Glossary-term component', async () => { it('Should render Glossary-term component', async () => {
render(<GlossaryTerms {...mockProps} childGlossaryTerms={[]} />); render(<GlossaryTerms {...mockProps} />);
const glossaryTerm = screen.getByTestId('glossary-term'); const glossaryTerm = screen.getByTestId('glossary-term');
const tabs = await screen.findAllByRole('tab'); const tabs = await screen.findAllByRole('tab');

View File

@ -19,10 +19,7 @@ import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; import { withActivityFeed } from '../../components/AppRouter/withActivityFeed';
import { HTTP_STATUS_CODE } from '../../constants/Auth.constants'; import { HTTP_STATUS_CODE } from '../../constants/Auth.constants';
import { import { getGlossaryTermDetailsPath } from '../../constants/constants';
API_RES_MAX_SIZE,
getGlossaryTermDetailsPath,
} from '../../constants/constants';
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
import { import {
OperationPermission, OperationPermission,
@ -33,13 +30,12 @@ import {
CreateThread, CreateThread,
ThreadType, ThreadType,
} from '../../generated/api/feed/createThread'; } from '../../generated/api/feed/createThread';
import { Glossary } from '../../generated/entity/data/glossary';
import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm'; import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm';
import { VERSION_VIEW_GLOSSARY_PERMISSION } from '../../mocks/Glossary.mock'; import { VERSION_VIEW_GLOSSARY_PERMISSION } from '../../mocks/Glossary.mock';
import { postThread } from '../../rest/feedsAPI'; import { postThread } from '../../rest/feedsAPI';
import { import {
addGlossaryTerm, addGlossaryTerm,
getGlossaryTerms, getFirstLevelGlossaryTerms,
ListGlossaryTermsParams, ListGlossaryTermsParams,
patchGlossaryTerm, patchGlossaryTerm,
} from '../../rest/glossaryAPI'; } from '../../rest/glossaryAPI';
@ -57,6 +53,7 @@ import GlossaryTermsV1 from './GlossaryTerms/GlossaryTermsV1.component';
import { GlossaryV1Props } from './GlossaryV1.interfaces'; import { GlossaryV1Props } from './GlossaryV1.interfaces';
import './glossaryV1.less'; import './glossaryV1.less';
import ImportGlossary from './ImportGlossary/ImportGlossary'; import ImportGlossary from './ImportGlossary/ImportGlossary';
import { useGlossaryStore } from './useGlossary.store';
const GlossaryV1 = ({ const GlossaryV1 = ({
isGlossaryActive, isGlossaryActive,
@ -74,13 +71,15 @@ const GlossaryV1 = ({
const { t } = useTranslation(); const { t } = useTranslation();
const { action, tab } = const { action, tab } =
useParams<{ action: EntityAction; glossaryName: string; tab: string }>(); useParams<{ action: EntityAction; glossaryName: string; tab: string }>();
const history = useHistory(); const history = useHistory();
const [threadLink, setThreadLink] = useState<string>(''); const [threadLink, setThreadLink] = useState<string>('');
const [threadType, setThreadType] = useState<ThreadType>( const [threadType, setThreadType] = useState<ThreadType>(
ThreadType.Conversation ThreadType.Conversation
); );
const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider(); const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider();
const [activeGlossaryTerm, setActiveGlossaryTerm] =
useState<GlossaryTerm | null>(null);
const { getEntityPermission } = usePermissionProvider(); const { getEntityPermission } = usePermissionProvider();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isTermsLoading, setIsTermsLoading] = useState(false); const [isTermsLoading, setIsTermsLoading] = useState(false);
@ -94,13 +93,12 @@ const GlossaryV1 = ({
useState<OperationPermission>(DEFAULT_ENTITY_PERMISSION); useState<OperationPermission>(DEFAULT_ENTITY_PERMISSION);
const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [activeGlossaryTerm, setActiveGlossaryTerm] = useState<
GlossaryTerm | undefined
>();
const [editMode, setEditMode] = useState(false); const [editMode, setEditMode] = useState(false);
const [glossaryTerms, setGlossaryTerms] = useState<GlossaryTerm[]>([]); const { activeGlossary, updateActiveGlossary } = useGlossaryStore();
const { id } = selectedData ?? {};
const { id, fullyQualifiedName } = activeGlossary ?? {};
const isImportAction = useMemo( const isImportAction = useMemo(
() => action === EntityAction.IMPORT, () => action === EntityAction.IMPORT,
@ -124,12 +122,14 @@ const GlossaryV1 = ({
) => { ) => {
refresh ? setIsTermsLoading(true) : setIsLoading(true); refresh ? setIsTermsLoading(true) : setIsLoading(true);
try { try {
const { data } = await getGlossaryTerms({ const { data } = await getFirstLevelGlossaryTerms(
...params, params?.glossary ?? params?.parent ?? ''
limit: API_RES_MAX_SIZE, );
fields: 'children,owner,parent', updateActiveGlossary({
children: data.map((data) =>
data.childrenCount ?? 0 > 0 ? { ...data, children: [] } : data
),
}); });
setGlossaryTerms(data);
} catch (error) { } catch (error) {
showErrorToast(error as AxiosError); showErrorToast(error as AxiosError);
} finally { } finally {
@ -187,16 +187,18 @@ const GlossaryV1 = ({
const loadGlossaryTerms = useCallback( const loadGlossaryTerms = useCallback(
(refresh = false) => { (refresh = false) => {
fetchGlossaryTerm( fetchGlossaryTerm(
isGlossaryActive ? { glossary: id } : { parent: id }, isGlossaryActive
? { glossary: fullyQualifiedName }
: { parent: fullyQualifiedName },
refresh refresh
); );
}, },
[id, isGlossaryActive] [fullyQualifiedName, isGlossaryActive]
); );
const handleGlossaryTermModalAction = ( const handleGlossaryTermModalAction = (
editMode: boolean, editMode: boolean,
glossaryTerm: GlossaryTerm | undefined glossaryTerm: GlossaryTerm | null
) => { ) => {
setEditMode(editMode); setEditMode(editMode);
setActiveGlossaryTerm(glossaryTerm); setActiveGlossaryTerm(glossaryTerm);
@ -349,8 +351,6 @@ const GlossaryV1 = ({
!isEmpty(selectedData) && !isEmpty(selectedData) &&
(isGlossaryActive ? ( (isGlossaryActive ? (
<GlossaryDetails <GlossaryDetails
glossary={selectedData as Glossary}
glossaryTerms={glossaryTerms}
handleGlossaryDelete={onGlossaryDelete} handleGlossaryDelete={onGlossaryDelete}
isVersionView={isVersionsView} isVersionView={isVersionsView}
permissions={glossaryPermission} permissions={glossaryPermission}
@ -359,16 +359,15 @@ const GlossaryV1 = ({
updateGlossary={updateGlossary} updateGlossary={updateGlossary}
updateVote={updateVote} updateVote={updateVote}
onAddGlossaryTerm={(term) => onAddGlossaryTerm={(term) =>
handleGlossaryTermModalAction(false, term) handleGlossaryTermModalAction(false, term ?? null)
} }
onEditGlossaryTerm={(term) => onEditGlossaryTerm={(term) =>
handleGlossaryTermModalAction(true, term) handleGlossaryTermModalAction(true, term ?? null)
} }
onThreadLinkSelect={onThreadLinkSelect} onThreadLinkSelect={onThreadLinkSelect}
/> />
) : ( ) : (
<GlossaryTermsV1 <GlossaryTermsV1
childGlossaryTerms={glossaryTerms}
glossaryTerm={selectedData as GlossaryTerm} glossaryTerm={selectedData as GlossaryTerm}
handleGlossaryTermDelete={onGlossaryTermDelete} handleGlossaryTermDelete={onGlossaryTermDelete}
handleGlossaryTermUpdate={onGlossaryTermUpdate} handleGlossaryTermUpdate={onGlossaryTermUpdate}
@ -380,7 +379,7 @@ const GlossaryV1 = ({
termsLoading={isTermsLoading} termsLoading={isTermsLoading}
updateVote={updateVote} updateVote={updateVote}
onAddGlossaryTerm={(term) => onAddGlossaryTerm={(term) =>
handleGlossaryTermModalAction(false, term) handleGlossaryTermModalAction(false, term ?? null)
} }
onAssetClick={onAssetClick} onAssetClick={onAssetClick}
onEditGlossaryTerm={(term) => onEditGlossaryTerm={(term) =>

View File

@ -124,6 +124,12 @@ jest.mock('./ImportGlossary/ImportGlossary', () =>
jest.mock('../../components/AppRouter/withActivityFeed', () => ({ jest.mock('../../components/AppRouter/withActivityFeed', () => ({
withActivityFeed: jest.fn().mockImplementation((component) => component), withActivityFeed: jest.fn().mockImplementation((component) => component),
})); }));
jest.mock('./useGlossary.store', () => ({
useGlossaryStore: jest.fn().mockImplementation(() => ({
activeGlossary: mockedGlossaryTerms[0],
updateActiveGlossary: jest.fn(),
})),
}));
const mockProps: GlossaryV1Props = { const mockProps: GlossaryV1Props = {
selectedData: mockedGlossaries[0], selectedData: mockedGlossaries[0],

View File

@ -0,0 +1,52 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { create } from 'zustand';
import { Glossary } from '../../generated/entity/data/glossary';
import { GlossaryTermWithChildren } from '../../rest/glossaryAPI';
export type ModifiedGlossary = Glossary & {
children?: GlossaryTermWithChildren[];
};
export const useGlossaryStore = create<{
glossaries: Glossary[];
activeGlossary: ModifiedGlossary;
setGlossaries: (glossaries: Glossary[]) => void;
setActiveGlossary: (glossary: ModifiedGlossary) => void;
updateGlossary: (glossary: Glossary) => void;
updateActiveGlossary: (glossary: Partial<ModifiedGlossary>) => void;
}>()((set, get) => ({
glossaries: [],
activeGlossary: {} as ModifiedGlossary,
setGlossaries: (glossaries: Glossary[]) => {
set({ glossaries });
},
updateGlossary: (glossary: Glossary) => {
const { glossaries } = get();
const newGlossaries = glossaries.map((g) =>
g.fullyQualifiedName === glossary.fullyQualifiedName ? glossary : g
);
set({ glossaries: newGlossaries });
},
setActiveGlossary: (glossary: ModifiedGlossary) => {
set({ activeGlossary: glossary });
},
updateActiveGlossary: (glossary: Partial<ModifiedGlossary>) => {
const { activeGlossary } = get();
set({ activeGlossary: { ...activeGlossary, ...glossary } as Glossary });
},
}));

View File

@ -189,7 +189,7 @@ const TreeAsyncSelectList: FC<Omit<AsyncSelectListProps, 'fetchOptions'>> = ({
label: value, label: value,
}; };
}); });
selectedTagsRef.current = selectedValues; selectedTagsRef.current = selectedValues as SelectOption[];
onChange?.(selectedValues); onChange?.(selectedValues);
}; };

View File

@ -23,6 +23,10 @@ import { VotingDataProps } from '../../../components/Entity/Voting/voting.interf
import EntitySummaryPanel from '../../../components/Explore/EntitySummaryPanel/EntitySummaryPanel.component'; import EntitySummaryPanel from '../../../components/Explore/EntitySummaryPanel/EntitySummaryPanel.component';
import { EntityDetailsObjectInterface } from '../../../components/Explore/ExplorePage.interface'; import { EntityDetailsObjectInterface } from '../../../components/Explore/ExplorePage.interface';
import GlossaryV1 from '../../../components/Glossary/GlossaryV1.component'; import GlossaryV1 from '../../../components/Glossary/GlossaryV1.component';
import {
ModifiedGlossary,
useGlossaryStore,
} from '../../../components/Glossary/useGlossary.store';
import PageLayoutV1 from '../../../components/PageLayoutV1/PageLayoutV1'; import PageLayoutV1 from '../../../components/PageLayoutV1/PageLayoutV1';
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import { PAGE_SIZE_LARGE, ROUTES } from '../../../constants/constants'; import { PAGE_SIZE_LARGE, ROUTES } from '../../../constants/constants';
@ -55,16 +59,24 @@ const GlossaryPage = () => {
const { permissions } = usePermissionProvider(); const { permissions } = usePermissionProvider();
const { fqn: glossaryFqn } = useFqn(); const { fqn: glossaryFqn } = useFqn();
const history = useHistory(); const history = useHistory();
const [glossaries, setGlossaries] = useState<Glossary[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [selectedData, setSelectedData] = useState<Glossary | GlossaryTerm>();
const [isRightPanelLoading, setIsRightPanelLoading] = useState(true); const [isRightPanelLoading, setIsRightPanelLoading] = useState(true);
const [previewAsset, setPreviewAsset] = const [previewAsset, setPreviewAsset] =
useState<EntityDetailsObjectInterface>(); useState<EntityDetailsObjectInterface>();
const {
glossaries,
setGlossaries,
activeGlossary,
setActiveGlossary,
updateActiveGlossary,
} = useGlossaryStore();
const isGlossaryActive = useMemo(() => { const isGlossaryActive = useMemo(() => {
setIsRightPanelLoading(true); setIsRightPanelLoading(true);
setSelectedData(undefined); setActiveGlossary({} as ModifiedGlossary);
if (glossaryFqn) { if (glossaryFqn) {
return Fqn.split(glossaryFqn).length === 1; return Fqn.split(glossaryFqn).length === 1;
@ -140,7 +152,7 @@ const GlossaryPage = () => {
fields: fields:
'relatedTerms,reviewers,tags,owner,children,votes,domain,extension', 'relatedTerms,reviewers,tags,owner,children,votes,domain,extension',
}); });
setSelectedData(response); setActiveGlossary(response as ModifiedGlossary);
} catch (error) { } catch (error) {
showErrorToast(error as AxiosError); showErrorToast(error as AxiosError);
} finally { } finally {
@ -153,7 +165,7 @@ const GlossaryPage = () => {
if (!isGlossaryActive) { if (!isGlossaryActive) {
fetchGlossaryTermDetails(); fetchGlossaryTermDetails();
} else { } else {
setSelectedData( setActiveGlossary(
glossaries.find( glossaries.find(
(glossary) => glossary.fullyQualifiedName === glossaryFqn (glossary) => glossary.fullyQualifiedName === glossaryFqn
) || glossaries[0] ) || glossaries[0]
@ -167,25 +179,17 @@ const GlossaryPage = () => {
}, [isGlossaryActive, glossaryFqn, glossaries]); }, [isGlossaryActive, glossaryFqn, glossaries]);
const updateGlossary = async (updatedData: Glossary) => { const updateGlossary = async (updatedData: Glossary) => {
const jsonPatch = compare(selectedData as Glossary, updatedData); const jsonPatch = compare(activeGlossary as Glossary, updatedData);
try { try {
const response = await patchGlossaries( const response = await patchGlossaries(
selectedData?.id as string, activeGlossary?.id as string,
jsonPatch jsonPatch
); );
setGlossaries((pre) => { updateActiveGlossary(response);
return pre.map((item) => {
if (item.name === response.name) {
return response;
} else {
return item;
}
});
});
if (selectedData?.name !== updatedData.name) { if (activeGlossary?.name !== updatedData.name) {
history.push(getGlossaryPath(response.fullyQualifiedName)); history.push(getGlossaryPath(response.fullyQualifiedName));
fetchGlossaryList(); fetchGlossaryList();
} }
@ -198,36 +202,24 @@ const GlossaryPage = () => {
async (data: VotingDataProps) => { async (data: VotingDataProps) => {
try { try {
const isGlossaryEntity = const isGlossaryEntity =
Fqn.split(selectedData?.fullyQualifiedName ?? '').length <= 1; Fqn.split(activeGlossary?.fullyQualifiedName ?? '').length <= 1;
if (isGlossaryEntity) { if (isGlossaryEntity) {
const { const {
entity: { votes }, entity: { votes },
} = await updateGlossaryVotes(selectedData?.id ?? '', data); } = await updateGlossaryVotes(activeGlossary?.id ?? '', data);
setSelectedData( updateActiveGlossary({ votes });
(pre) =>
pre && {
...pre,
votes,
}
);
} else { } else {
const { const {
entity: { votes }, entity: { votes },
} = await updateGlossaryTermVotes(selectedData?.id ?? '', data); } = await updateGlossaryTermVotes(activeGlossary?.id ?? '', data);
setSelectedData( updateActiveGlossary({ votes });
(pre) =>
pre && {
...pre,
votes,
}
);
} }
} catch (error) { } catch (error) {
showErrorToast(error as AxiosError); showErrorToast(error as AxiosError);
} }
}, },
[setSelectedData, selectedData] [updateActiveGlossary, activeGlossary]
); );
const handleGlossaryDelete = async (id: string) => { const handleGlossaryDelete = async (id: string) => {
@ -260,18 +252,18 @@ const GlossaryPage = () => {
const handleGlossaryTermUpdate = useCallback( const handleGlossaryTermUpdate = useCallback(
async (updatedData: GlossaryTerm) => { async (updatedData: GlossaryTerm) => {
const jsonPatch = compare(selectedData as GlossaryTerm, updatedData); const jsonPatch = compare(activeGlossary as GlossaryTerm, updatedData);
if (isEmpty(jsonPatch)) { if (isEmpty(jsonPatch)) {
return; return;
} }
try { try {
const response = await patchGlossaryTerm( const response = await patchGlossaryTerm(
selectedData?.id as string, activeGlossary?.id as string,
jsonPatch jsonPatch
); );
if (response) { if (response) {
setSelectedData(response); setActiveGlossary(response as ModifiedGlossary);
if (selectedData?.name !== updatedData.name) { if (activeGlossary?.name !== updatedData.name) {
history.push(getGlossaryPath(response.fullyQualifiedName)); history.push(getGlossaryPath(response.fullyQualifiedName));
fetchGlossaryList(); fetchGlossaryList();
} }
@ -284,7 +276,7 @@ const GlossaryPage = () => {
showErrorToast(error as AxiosError); showErrorToast(error as AxiosError);
} }
}, },
[selectedData] [activeGlossary]
); );
const handleGlossaryTermDelete = async (id: string) => { const handleGlossaryTermDelete = async (id: string) => {
@ -373,7 +365,7 @@ const GlossaryPage = () => {
isSummaryPanelOpen={Boolean(previewAsset)} isSummaryPanelOpen={Boolean(previewAsset)}
isVersionsView={false} isVersionsView={false}
refreshActiveGlossaryTerm={fetchGlossaryTermDetails} refreshActiveGlossaryTerm={fetchGlossaryTermDetails}
selectedData={selectedData as Glossary} selectedData={activeGlossary as Glossary}
updateGlossary={updateGlossary} updateGlossary={updateGlossary}
updateVote={updateVote} updateVote={updateVote}
onAssetClick={handleAssetClick} onAssetClick={handleAssetClick}

View File

@ -289,3 +289,23 @@ export const searchGlossaryTerms = async (search: string, page = 1) => {
return data; return data;
}; };
export type GlossaryTermWithChildren = Omit<GlossaryTerm, 'children'> & {
children?: GlossaryTerm[];
};
export const getFirstLevelGlossaryTerms = async (parentFQN: string) => {
const apiUrl = `/glossaryTerms`;
const { data } = await APIClient.get<
PagingResponse<GlossaryTermWithChildren[]>
>(apiUrl, {
params: {
directChildrenOf: parentFQN,
fields: 'childrenCount',
limit: 100000,
},
});
return data;
};

View File

@ -10,6 +10,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { ModifiedGlossaryTerm } from '../components/Glossary/GlossaryTermTab/GlossaryTermTab.interface';
import { EntityType } from '../enums/entity.enum'; import { EntityType } from '../enums/entity.enum';
import { import {
MOCKED_GLOSSARY_TERMS, MOCKED_GLOSSARY_TERMS,
@ -17,7 +18,12 @@ import {
MOCKED_GLOSSARY_TERMS_TREE, MOCKED_GLOSSARY_TERMS_TREE,
MOCKED_GLOSSARY_TERMS_TREE_1, MOCKED_GLOSSARY_TERMS_TREE_1,
} from '../mocks/Glossary.mock'; } from '../mocks/Glossary.mock';
import { buildTree, getQueryFilterToExcludeTerm } from './GlossaryUtils'; import {
buildTree,
findExpandableKeys,
findExpandableKeysForArray,
getQueryFilterToExcludeTerm,
} from './GlossaryUtils';
describe('Glossary Utils', () => { describe('Glossary Utils', () => {
it('getQueryFilterToExcludeTerm returns the correct query filter', () => { it('getQueryFilterToExcludeTerm returns the correct query filter', () => {
@ -79,4 +85,78 @@ describe('Glossary Utils', () => {
MOCKED_GLOSSARY_TERMS_TREE_1 MOCKED_GLOSSARY_TERMS_TREE_1
); );
}); });
it('should return an empty array if no glossary term is provided', () => {
const expandableKeys = findExpandableKeys();
expect(expandableKeys).toEqual([]);
});
it('should return an array of expandable keys when glossary term has children', () => {
const glossaryTerm = {
fullyQualifiedName: 'example',
children: [
{
fullyQualifiedName: 'child1',
children: [
{
fullyQualifiedName: 'grandchild1',
},
{
childrenCount: 2,
fullyQualifiedName: 'grandchild2',
},
],
},
{
fullyQualifiedName: 'child2',
},
],
};
const expandableKeys = findExpandableKeys(
glossaryTerm as ModifiedGlossaryTerm
);
expect(expandableKeys).toEqual(['grandchild2', 'child1', 'example']);
});
it('should return an array of expandable keys when glossary term has childrenCount', () => {
const glossaryTerm = {
fullyQualifiedName: 'example',
childrenCount: 2,
};
const expandableKeys = findExpandableKeys(
glossaryTerm as ModifiedGlossaryTerm
);
expect(expandableKeys).toEqual(['example']);
});
it('should find expandable keys for an array of glossary terms', () => {
const glossaryTerms = [
{
fullyQualifiedName: 'example1',
children: [
{
fullyQualifiedName: 'child1',
},
],
},
{
fullyQualifiedName: 'example2',
childrenCount: 2,
},
{
fullyQualifiedName: 'example3',
},
];
const expandableKeys = findExpandableKeysForArray(
glossaryTerms as ModifiedGlossaryTerm[]
);
expect(expandableKeys).toEqual(['example1', 'example2']);
});
}); });

View File

@ -15,6 +15,7 @@ import { DefaultOptionType } from 'antd/lib/select';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { StatusType } from '../components/common/StatusBadge/StatusBadge.interface'; import { StatusType } from '../components/common/StatusBadge/StatusBadge.interface';
import { ModifiedGlossaryTerm } from '../components/Glossary/GlossaryTermTab/GlossaryTermTab.interface'; import { ModifiedGlossaryTerm } from '../components/Glossary/GlossaryTermTab/GlossaryTermTab.interface';
import { ModifiedGlossary } from '../components/Glossary/useGlossary.store';
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants'; import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
import { EntityType } from '../enums/entity.enum'; import { EntityType } from '../enums/entity.enum';
import { Glossary } from '../generated/entity/data/glossary'; import { Glossary } from '../generated/entity/data/glossary';
@ -148,7 +149,7 @@ export const getGlossaryBreadcrumbs = (fqn: string) => {
export const findGlossaryTermByFqn = ( export const findGlossaryTermByFqn = (
list: ModifiedGlossaryTerm[], list: ModifiedGlossaryTerm[],
fullyQualifiedName: string fullyQualifiedName: string
): GlossaryTerm | Glossary | null => { ): GlossaryTerm | Glossary | ModifiedGlossary | null => {
for (const item of list) { for (const item of list) {
if (item.fullyQualifiedName === fullyQualifiedName) { if (item.fullyQualifiedName === fullyQualifiedName) {
return item; return item;
@ -197,3 +198,53 @@ export const convertGlossaryTermsToTreeOptions = (
return treeData; return treeData;
}; };
/**
* Finds the expandable keys in a glossary term.
* @param glossaryTerm - The glossary term to search for expandable keys.
* @returns An array of expandable keys found in the glossary term.
*/
export const findExpandableKeys = (
glossaryTerm?: ModifiedGlossaryTerm
): string[] => {
let expandableKeys: string[] = [];
if (!glossaryTerm) {
return expandableKeys;
}
if (glossaryTerm.children) {
glossaryTerm.children.forEach((child) => {
expandableKeys = expandableKeys.concat(
findExpandableKeys(child as ModifiedGlossaryTerm)
);
});
if (glossaryTerm.fullyQualifiedName) {
expandableKeys.push(glossaryTerm.fullyQualifiedName);
}
} else if (glossaryTerm.childrenCount) {
if (glossaryTerm.fullyQualifiedName) {
expandableKeys.push(glossaryTerm.fullyQualifiedName);
}
}
return expandableKeys;
};
/**
* Finds the expandable keys for an array of glossary terms.
*
* @param glossaryTerms - An array of ModifiedGlossaryTerm objects.
* @returns An array of expandable keys.
*/
export const findExpandableKeysForArray = (
glossaryTerms: ModifiedGlossaryTerm[]
): string[] => {
let expandableKeys: string[] = [];
glossaryTerms.forEach((glossaryTerm) => {
expandableKeys = expandableKeys.concat(findExpandableKeys(glossaryTerm));
});
return expandableKeys;
};