mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-12 08:52:38 +00:00
Minor: fix glossary add term not working (#14148)
* fix glossary add term not working * minor fixes * added unit test
This commit is contained in:
parent
29296ba939
commit
4e90650aa2
@ -12,9 +12,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { DefaultOptionType } from 'antd/lib/select';
|
import { DefaultOptionType } from 'antd/lib/select';
|
||||||
|
import { PagingResponse } from 'Models';
|
||||||
import { Tag } from '../../generated/entity/classification/tag';
|
import { Tag } from '../../generated/entity/classification/tag';
|
||||||
import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm';
|
import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm';
|
||||||
import { Paging } from '../../generated/type/paging';
|
|
||||||
|
|
||||||
export type SelectOption = {
|
export type SelectOption = {
|
||||||
label: string;
|
label: string;
|
||||||
@ -30,12 +30,10 @@ export interface AsyncSelectListProps {
|
|||||||
defaultValue?: string[];
|
defaultValue?: string[];
|
||||||
value?: string[];
|
value?: string[];
|
||||||
initialOptions?: SelectOption[];
|
initialOptions?: SelectOption[];
|
||||||
|
filterOptions?: string[]; // array of fqn
|
||||||
onChange?: (option: DefaultOptionType | DefaultOptionType[]) => void;
|
onChange?: (option: DefaultOptionType | DefaultOptionType[]) => void;
|
||||||
fetchOptions: (
|
fetchOptions: (
|
||||||
search: string,
|
search: string,
|
||||||
page: number
|
page: number
|
||||||
) => Promise<{
|
) => Promise<PagingResponse<SelectOption[]>>;
|
||||||
data: SelectOption[];
|
|
||||||
paging: Paging;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,230 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 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 {
|
||||||
|
act,
|
||||||
|
findByRole,
|
||||||
|
fireEvent,
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
waitForElement,
|
||||||
|
} from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import React from 'react';
|
||||||
|
import { ASYNC_SELECT_MOCK } from '../../mocks/AsyncSelect.mock';
|
||||||
|
import AsyncSelectList from './AsyncSelectList';
|
||||||
|
|
||||||
|
jest.mock('lodash', () => {
|
||||||
|
const module = jest.requireActual('lodash');
|
||||||
|
module.debounce = jest.fn((fn) => fn);
|
||||||
|
|
||||||
|
return module;
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../../components/Loader/Loader', () =>
|
||||||
|
jest.fn().mockImplementation(() => <div>Loader</div>)
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.mock('../Tag/TagsV1/TagsV1.component', () =>
|
||||||
|
jest.fn().mockImplementation(() => <div>TagsV1</div>)
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.mock('../../utils/ToastUtils', () => ({
|
||||||
|
showErrorToast: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../utils/TagsUtils', () => ({
|
||||||
|
getTagDisplay: jest.fn().mockReturnValue('tags'),
|
||||||
|
tagRender: jest.fn().mockReturnValue(<p>Tags Render</p>),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockOnChange = jest.fn();
|
||||||
|
const mockFetchOptions = jest.fn().mockReturnValue({
|
||||||
|
data: [],
|
||||||
|
paging: { total: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockProps = {
|
||||||
|
fetchOptions: mockFetchOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Test AsyncSelect List Component', () => {
|
||||||
|
it('Should render component', async () => {
|
||||||
|
render(<AsyncSelectList {...mockProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('tag-selector')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should render value if passed', async () => {
|
||||||
|
render(<AsyncSelectList {...mockProps} value={['select-1']} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('tag-selector')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByTitle('select-1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should trigger fetchOptions when focus on select input', async () => {
|
||||||
|
render(<AsyncSelectList {...mockProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('tag-selector')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.focus(screen.getByTestId('tag-selector'));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockFetchOptions).toHaveBeenCalledWith('', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should call fetchOptions with multiple focus operation', async () => {
|
||||||
|
render(<AsyncSelectList {...mockProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('tag-selector')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
// first focus
|
||||||
|
fireEvent.focus(screen.getByTestId('tag-selector'));
|
||||||
|
|
||||||
|
expect(mockFetchOptions).toHaveBeenCalledWith('', 1);
|
||||||
|
|
||||||
|
fireEvent.blur(screen.getByTestId('tag-selector'));
|
||||||
|
|
||||||
|
// second focus
|
||||||
|
fireEvent.focus(screen.getByTestId('tag-selector'));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockFetchOptions).toHaveBeenCalledWith('', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should call fetchOptions with search data when user type text', async () => {
|
||||||
|
render(<AsyncSelectList {...mockProps} />);
|
||||||
|
|
||||||
|
const searchInput = (await screen.findByRole(
|
||||||
|
'combobox'
|
||||||
|
)) as HTMLInputElement;
|
||||||
|
|
||||||
|
expect(searchInput.value).toBe('');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(searchInput, { target: { value: 'entity-tags' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(searchInput.value).toBe('entity-tags');
|
||||||
|
|
||||||
|
expect(mockFetchOptions).toHaveBeenCalledWith('entity-tags', 1);
|
||||||
|
|
||||||
|
expect(mockFetchOptions).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should render options if provided', async () => {
|
||||||
|
mockFetchOptions.mockResolvedValueOnce(ASYNC_SELECT_MOCK);
|
||||||
|
|
||||||
|
render(<AsyncSelectList {...mockProps} />);
|
||||||
|
|
||||||
|
const selectInput = await findByRole(
|
||||||
|
screen.getByTestId('tag-selector'),
|
||||||
|
'combobox'
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
userEvent.click(selectInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait for list to render, checked with item having in the list
|
||||||
|
await waitForElement(() => screen.findByTestId('tag-tags-6'));
|
||||||
|
|
||||||
|
const item = screen.queryByText('tags-6');
|
||||||
|
|
||||||
|
expect(item).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(mockFetchOptions).toHaveBeenCalledWith('', 1);
|
||||||
|
|
||||||
|
expect(mockFetchOptions).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should filter options based on provided filterOptions', async () => {
|
||||||
|
mockFetchOptions.mockResolvedValueOnce(ASYNC_SELECT_MOCK);
|
||||||
|
|
||||||
|
render(<AsyncSelectList {...mockProps} filterOptions={['tags-1']} />);
|
||||||
|
|
||||||
|
const selectInput = await findByRole(
|
||||||
|
screen.getByTestId('tag-selector'),
|
||||||
|
'combobox'
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
userEvent.click(selectInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait for list to render, checked with item having in the list
|
||||||
|
await waitForElement(() => screen.findByTestId('tag-tags-0'));
|
||||||
|
|
||||||
|
const filteredItem = screen.queryByText('tags-1');
|
||||||
|
|
||||||
|
expect(filteredItem).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(mockFetchOptions).toHaveBeenCalledWith('', 1);
|
||||||
|
|
||||||
|
expect(mockFetchOptions).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not trigger onChange on item selection', async () => {
|
||||||
|
mockFetchOptions.mockResolvedValueOnce(ASYNC_SELECT_MOCK);
|
||||||
|
|
||||||
|
render(<AsyncSelectList {...mockProps} mode="multiple" />);
|
||||||
|
|
||||||
|
const selectInput = await findByRole(
|
||||||
|
screen.getByTestId('tag-selector'),
|
||||||
|
'combobox'
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
userEvent.click(selectInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForElement(() => screen.getByTestId('tag-tags-0'));
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByTestId('tag-tags-0'));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockOnChange).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(mockFetchOptions).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should trigger onChange on item selection', async () => {
|
||||||
|
mockFetchOptions.mockResolvedValueOnce(ASYNC_SELECT_MOCK);
|
||||||
|
render(
|
||||||
|
<AsyncSelectList {...mockProps} mode="multiple" onChange={mockOnChange} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectInput = await findByRole(
|
||||||
|
screen.getByTestId('tag-selector'),
|
||||||
|
'combobox'
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
userEvent.click(selectInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForElement(() => screen.getByTestId('tag-tags-0'));
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByTestId('tag-tags-0'));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(mockFetchOptions).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -43,6 +43,7 @@ const AsyncSelectList: FC<AsyncSelectListProps> = ({
|
|||||||
fetchOptions,
|
fetchOptions,
|
||||||
debounceTimeout = 800,
|
debounceTimeout = 800,
|
||||||
initialOptions,
|
initialOptions,
|
||||||
|
filterOptions = [],
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
@ -54,13 +55,38 @@ const AsyncSelectList: FC<AsyncSelectListProps> = ({
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const selectedTagsRef = useRef<SelectOption[]>(initialOptions ?? []);
|
const selectedTagsRef = useRef<SelectOption[]>(initialOptions ?? []);
|
||||||
|
|
||||||
|
const [optionFilteredCount, setOptionFilteredCount] = useState(0);
|
||||||
|
|
||||||
|
const getFilteredOptions = (data: SelectOption[]) => {
|
||||||
|
if (isEmpty(filterOptions)) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = optionFilteredCount;
|
||||||
|
|
||||||
|
const filteredData = data.filter((item) => {
|
||||||
|
const isFiltered = filterOptions.includes(
|
||||||
|
item.data?.fullyQualifiedName ?? ''
|
||||||
|
);
|
||||||
|
if (isFiltered) {
|
||||||
|
count = optionFilteredCount + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !isFiltered;
|
||||||
|
});
|
||||||
|
|
||||||
|
setOptionFilteredCount(count);
|
||||||
|
|
||||||
|
return filteredData;
|
||||||
|
};
|
||||||
|
|
||||||
const loadOptions = useCallback(
|
const loadOptions = useCallback(
|
||||||
async (value: string) => {
|
async (value: string) => {
|
||||||
setOptions([]);
|
setOptions([]);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetchOptions(value, 1);
|
const res = await fetchOptions(value, 1);
|
||||||
setOptions(res.data);
|
setOptions(getFilteredOptions(res.data));
|
||||||
setPaging(res.paging);
|
setPaging(res.paging);
|
||||||
setSearchValue(value);
|
setSearchValue(value);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
@ -119,11 +145,12 @@ const AsyncSelectList: FC<AsyncSelectListProps> = ({
|
|||||||
currentTarget.scrollTop + currentTarget.offsetHeight ===
|
currentTarget.scrollTop + currentTarget.offsetHeight ===
|
||||||
currentTarget.scrollHeight
|
currentTarget.scrollHeight
|
||||||
) {
|
) {
|
||||||
if (options.length < paging.total) {
|
// optionFilteredCount added to equalize the options received from the server
|
||||||
|
if (options.length + optionFilteredCount < paging.total) {
|
||||||
try {
|
try {
|
||||||
setHasContentLoading(true);
|
setHasContentLoading(true);
|
||||||
const res = await fetchOptions(searchValue, currentPage + 1);
|
const res = await fetchOptions(searchValue, currentPage + 1);
|
||||||
setOptions((prev) => [...prev, ...res.data]);
|
setOptions((prev) => [...prev, ...getFilteredOptions(res.data)]);
|
||||||
setPaging(res.paging);
|
setPaging(res.paging);
|
||||||
setCurrentPage((prev) => prev + 1);
|
setCurrentPage((prev) => prev + 1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -62,7 +62,7 @@ const AddGlossaryTermForm = ({
|
|||||||
tags = [],
|
tags = [],
|
||||||
mutuallyExclusive = false,
|
mutuallyExclusive = false,
|
||||||
references = [],
|
references = [],
|
||||||
relatedTerms,
|
relatedTerms = [],
|
||||||
color,
|
color,
|
||||||
iconURL,
|
iconURL,
|
||||||
} = formObj;
|
} = formObj;
|
||||||
@ -252,6 +252,7 @@ const AddGlossaryTermForm = ({
|
|||||||
value: data.fullyQualifiedName,
|
value: data.fullyQualifiedName,
|
||||||
data,
|
data,
|
||||||
})),
|
})),
|
||||||
|
filterOptions: [glossaryTerm?.fullyQualifiedName ?? ''],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 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.
|
||||||
|
*/
|
||||||
|
const getOptions = (initialValue: number, endValue: number) => {
|
||||||
|
const data = [];
|
||||||
|
|
||||||
|
for (let i = initialValue; i < endValue; i++) {
|
||||||
|
data.push({
|
||||||
|
label: `tags-${i}`,
|
||||||
|
value: `tags-${i}`,
|
||||||
|
data: {
|
||||||
|
fullyQualifiedName: `tags-${i}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ASYNC_SELECT_MOCK = {
|
||||||
|
data: getOptions(0, 10),
|
||||||
|
paging: {
|
||||||
|
total: 20,
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -26,59 +26,3 @@ export const mockTagList = [
|
|||||||
mutuallyExclusive: true,
|
mutuallyExclusive: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const mockTagsApiResponse = {
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
id: '0897311f-1321-4e1c-a857-aab7dedc632d',
|
|
||||||
name: 'Personal',
|
|
||||||
fullyQualifiedName: 'PersonalData.Personal',
|
|
||||||
description:
|
|
||||||
'Data that can be used to directly or indirectly identify a person.',
|
|
||||||
classification: {
|
|
||||||
id: '5ce3825b-3227-4326-8beb-37ed2784149e',
|
|
||||||
type: 'classification',
|
|
||||||
name: 'PersonalData',
|
|
||||||
fullyQualifiedName: 'PersonalData',
|
|
||||||
description:
|
|
||||||
'Tags related classifying **Personal data** as defined by **GDPR.**<br/><br/>',
|
|
||||||
deleted: false,
|
|
||||||
href: 'http://localhost:8585/api/v1/classifications/5ce3825b-3227-4326-8beb-37ed2784149e',
|
|
||||||
},
|
|
||||||
version: 0.1,
|
|
||||||
updatedAt: 1675078969456,
|
|
||||||
updatedBy: 'admin',
|
|
||||||
href: 'http://localhost:8585/api/v1/tags/0897311f-1321-4e1c-a857-aab7dedc632d',
|
|
||||||
deprecated: false,
|
|
||||||
deleted: false,
|
|
||||||
provider: 'system',
|
|
||||||
mutuallyExclusive: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '68a9fa7f-9342-404a-b31a-112dea0e0f81',
|
|
||||||
name: 'SpecialCategory',
|
|
||||||
fullyQualifiedName: 'PersonalData.SpecialCategory',
|
|
||||||
description:
|
|
||||||
'GDPR special category data is personal information of data subjects that is especially sensitive',
|
|
||||||
classification: {
|
|
||||||
id: '5ce3825b-3227-4326-8beb-37ed2784149e',
|
|
||||||
type: 'classification',
|
|
||||||
name: 'PersonalData',
|
|
||||||
fullyQualifiedName: 'PersonalData',
|
|
||||||
description:
|
|
||||||
'Tags related classifying **Personal data** as defined by **GDPR.**<br/><br/>',
|
|
||||||
deleted: false,
|
|
||||||
href: 'http://localhost:8585/api/v1/classifications/5ce3825b-3227-4326-8beb-37ed2784149e',
|
|
||||||
},
|
|
||||||
version: 0.1,
|
|
||||||
updatedAt: 1675078969475,
|
|
||||||
updatedBy: 'admin',
|
|
||||||
href: 'http://localhost:8585/api/v1/tags/68a9fa7f-9342-404a-b31a-112dea0e0f81',
|
|
||||||
deprecated: false,
|
|
||||||
deleted: false,
|
|
||||||
provider: 'system',
|
|
||||||
mutuallyExclusive: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
paging: { total: 2 },
|
|
||||||
};
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user