mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-01 19:18:05 +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 { PagingResponse } from 'Models';
|
||||
import { Tag } from '../../generated/entity/classification/tag';
|
||||
import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm';
|
||||
import { Paging } from '../../generated/type/paging';
|
||||
|
||||
export type SelectOption = {
|
||||
label: string;
|
||||
@ -30,12 +30,10 @@ export interface AsyncSelectListProps {
|
||||
defaultValue?: string[];
|
||||
value?: string[];
|
||||
initialOptions?: SelectOption[];
|
||||
filterOptions?: string[]; // array of fqn
|
||||
onChange?: (option: DefaultOptionType | DefaultOptionType[]) => void;
|
||||
fetchOptions: (
|
||||
search: string,
|
||||
page: number
|
||||
) => Promise<{
|
||||
data: SelectOption[];
|
||||
paging: Paging;
|
||||
}>;
|
||||
) => Promise<PagingResponse<SelectOption[]>>;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
debounceTimeout = 800,
|
||||
initialOptions,
|
||||
filterOptions = [],
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
@ -54,13 +55,38 @@ const AsyncSelectList: FC<AsyncSelectListProps> = ({
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
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(
|
||||
async (value: string) => {
|
||||
setOptions([]);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await fetchOptions(value, 1);
|
||||
setOptions(res.data);
|
||||
setOptions(getFilteredOptions(res.data));
|
||||
setPaging(res.paging);
|
||||
setSearchValue(value);
|
||||
setCurrentPage(1);
|
||||
@ -119,11 +145,12 @@ const AsyncSelectList: FC<AsyncSelectListProps> = ({
|
||||
currentTarget.scrollTop + currentTarget.offsetHeight ===
|
||||
currentTarget.scrollHeight
|
||||
) {
|
||||
if (options.length < paging.total) {
|
||||
// optionFilteredCount added to equalize the options received from the server
|
||||
if (options.length + optionFilteredCount < paging.total) {
|
||||
try {
|
||||
setHasContentLoading(true);
|
||||
const res = await fetchOptions(searchValue, currentPage + 1);
|
||||
setOptions((prev) => [...prev, ...res.data]);
|
||||
setOptions((prev) => [...prev, ...getFilteredOptions(res.data)]);
|
||||
setPaging(res.paging);
|
||||
setCurrentPage((prev) => prev + 1);
|
||||
} catch (error) {
|
||||
|
||||
@ -62,7 +62,7 @@ const AddGlossaryTermForm = ({
|
||||
tags = [],
|
||||
mutuallyExclusive = false,
|
||||
references = [],
|
||||
relatedTerms,
|
||||
relatedTerms = [],
|
||||
color,
|
||||
iconURL,
|
||||
} = formObj;
|
||||
@ -252,6 +252,7 @@ const AddGlossaryTermForm = ({
|
||||
value: data.fullyQualifiedName,
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
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