mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-31 04:14:34 +00:00
feat(ui): supported pagination of glossary terms in glossary overview section (#12177)
* supported pagination of glossary terms in glossay overview section * remove unwanted try catch * change as per comments
This commit is contained in:
parent
206b155a8b
commit
d9cd484657
@ -288,7 +288,7 @@ const updateTerms = (newTerm) => {
|
||||
.contains(newTerm)
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.get('[data-testid="save-related-term-btn"]').should('be.visible').click();
|
||||
cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click();
|
||||
verifyResponseStatusCode('@saveGlossaryTermData', 200);
|
||||
|
||||
cy.get('[data-testid="related-term-container"]')
|
||||
|
@ -18,7 +18,7 @@ export type SelectOption = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export interface InfiniteSelectScrollProps {
|
||||
export interface AsyncSelectListProps {
|
||||
mode?: 'multiple';
|
||||
placeholder?: string;
|
||||
debounceTimeout?: number;
|
@ -21,11 +21,11 @@ import { tagRender } from 'utils/TagsUtils';
|
||||
import { showErrorToast } from 'utils/ToastUtils';
|
||||
import Fqn from '../../utils/Fqn';
|
||||
import {
|
||||
InfiniteSelectScrollProps,
|
||||
AsyncSelectListProps,
|
||||
SelectOption,
|
||||
} from './InfiniteSelectScroll.interface';
|
||||
} from './AsyncSelectList.interface';
|
||||
|
||||
const InfiniteSelectScroll: FC<InfiniteSelectScrollProps> = ({
|
||||
const AsyncSelectList: FC<AsyncSelectListProps> = ({
|
||||
mode,
|
||||
onChange,
|
||||
fetchOptions,
|
||||
@ -44,10 +44,11 @@ const InfiniteSelectScroll: FC<InfiniteSelectScrollProps> = ({
|
||||
setOptions([]);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await fetchOptions(value, currentPage);
|
||||
const res = await fetchOptions(value, 1);
|
||||
setOptions(res.data);
|
||||
setPaging(res.paging);
|
||||
setSearchValue(value);
|
||||
setCurrentPage(1);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
} finally {
|
||||
@ -156,4 +157,4 @@ const InfiniteSelectScroll: FC<InfiniteSelectScrollProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default InfiniteSelectScroll;
|
||||
export default AsyncSelectList;
|
@ -32,7 +32,7 @@ import RelatedTerms from './RelatedTerms';
|
||||
type Props = {
|
||||
selectedData: Glossary | GlossaryTerm;
|
||||
permissions: OperationPermission;
|
||||
onUpdate: (data: GlossaryTerm | Glossary) => void;
|
||||
onUpdate: (data: GlossaryTerm | Glossary) => Promise<void>;
|
||||
isGlossary: boolean;
|
||||
isVersionView?: boolean;
|
||||
};
|
||||
|
@ -11,19 +11,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { Button, Select, Space, Spin, Tooltip, Typography } from 'antd';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
|
||||
import { ReactComponent as IconFlatDoc } from 'assets/svg/ic-flat-doc.svg';
|
||||
import TagSelectForm from 'components/Tag/TagsSelectForm/TagsSelectForm.component';
|
||||
import TagButton from 'components/TagButton/TagButton.component';
|
||||
import { EntityField } from 'constants/Feeds.constants';
|
||||
import { NO_PERMISSION_FOR_ACTION } from 'constants/HelperTextUtil';
|
||||
import { ChangeDescription } from 'generated/entity/type';
|
||||
import { Paging } from 'generated/type/paging';
|
||||
import { t } from 'i18next';
|
||||
import { cloneDeep, debounce, includes, isEmpty } from 'lodash';
|
||||
import { cloneDeep, includes, isEmpty, uniqWith } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { searchData } from 'rest/miscAPI';
|
||||
import { formatSearchGlossaryTermResponse } from 'utils/APIUtils';
|
||||
import { getEntityName } from 'utils/EntityUtils';
|
||||
import {
|
||||
getChangedEntityNewValue,
|
||||
@ -41,7 +43,6 @@ import {
|
||||
import { SearchIndex } from '../../../enums/search.enum';
|
||||
import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm';
|
||||
import { EntityReference } from '../../../generated/type/entityReference';
|
||||
import { formatSearchGlossaryTermResponse } from '../../../utils/APIUtils';
|
||||
import { getEntityReferenceFromGlossary } from '../../../utils/GlossaryUtils';
|
||||
import { OperationPermission } from '../../PermissionProvider/PermissionProvider.interface';
|
||||
|
||||
@ -49,7 +50,7 @@ interface RelatedTermsProps {
|
||||
isVersionView?: boolean;
|
||||
permissions: OperationPermission;
|
||||
glossaryTerm: GlossaryTerm;
|
||||
onGlossaryTermUpdate: (data: GlossaryTerm) => void;
|
||||
onGlossaryTermUpdate: (data: GlossaryTerm) => Promise<void>;
|
||||
}
|
||||
|
||||
const RelatedTerms = ({
|
||||
@ -60,26 +61,21 @@ const RelatedTerms = ({
|
||||
}: RelatedTermsProps) => {
|
||||
const history = useHistory();
|
||||
const [isIconVisible, setIsIconVisible] = useState<boolean>(true);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [options, setOptions] = useState<EntityReference[]>([]);
|
||||
const [selectedOption, setSelectedOption] = useState<EntityReference[]>([]);
|
||||
|
||||
const getSearchedTerms = (searchedData: EntityReference[]) => {
|
||||
const currOptions = selectedOption.map(
|
||||
(item) => item.fullyQualifiedName || item.name
|
||||
);
|
||||
const data = searchedData.filter((item: EntityReference) => {
|
||||
return !currOptions.includes(item.fullyQualifiedName);
|
||||
});
|
||||
|
||||
return [...selectedOption, ...data];
|
||||
};
|
||||
|
||||
const handleRelatedTermClick = (fqn: string) => {
|
||||
history.push(getGlossaryPath(fqn));
|
||||
};
|
||||
|
||||
const handleRelatedTermsSave = (newOptions: EntityReference[]) => {
|
||||
const handleRelatedTermsSave = async (
|
||||
selectedData: string[]
|
||||
): Promise<void> => {
|
||||
const newOptions = uniqWith(
|
||||
options,
|
||||
(arrVal, othVal) => arrVal.id === othVal.id
|
||||
).filter((item) => includes(selectedData, item.fullyQualifiedName));
|
||||
|
||||
let updatedGlossaryTerm = cloneDeep(glossaryTerm);
|
||||
const oldTerms = newOptions.filter((d) =>
|
||||
includes(glossaryTerm.relatedTerms, d)
|
||||
@ -97,33 +93,50 @@ const RelatedTerms = ({
|
||||
relatedTerms: [...oldTerms, ...newTerms],
|
||||
};
|
||||
|
||||
onGlossaryTermUpdate(updatedGlossaryTerm);
|
||||
await onGlossaryTermUpdate(updatedGlossaryTerm);
|
||||
setIsIconVisible(true);
|
||||
};
|
||||
|
||||
const suggestionSearch = (searchText = '') => {
|
||||
setIsLoading(true);
|
||||
searchData(searchText, 1, PAGE_SIZE, '', '', '', SearchIndex.GLOSSARY)
|
||||
.then((res) => {
|
||||
const termResult = formatSearchGlossaryTermResponse(
|
||||
res.data.hits.hits
|
||||
).filter((item) => {
|
||||
return item.fullyQualifiedName !== glossaryTerm.fullyQualifiedName;
|
||||
});
|
||||
const fetchGlossaryTerms = async (
|
||||
searchText = '',
|
||||
page: number
|
||||
): Promise<{
|
||||
data: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
paging: Paging;
|
||||
}> => {
|
||||
const res = await searchData(
|
||||
searchText,
|
||||
page,
|
||||
PAGE_SIZE,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
SearchIndex.GLOSSARY
|
||||
);
|
||||
|
||||
const results = termResult.map(getEntityReferenceFromGlossary);
|
||||
const termResult = formatSearchGlossaryTermResponse(
|
||||
res.data.hits.hits
|
||||
).filter(
|
||||
(item) => item.fullyQualifiedName !== glossaryTerm.fullyQualifiedName
|
||||
);
|
||||
|
||||
const data = searchText ? getSearchedTerms(results) : results;
|
||||
setOptions(data);
|
||||
})
|
||||
.catch(() => {
|
||||
setOptions(selectedOption);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
const results = termResult.map(getEntityReferenceFromGlossary);
|
||||
setOptions((prev) => [...prev, ...results]);
|
||||
|
||||
return {
|
||||
data: results.map((item) => ({
|
||||
label: item.fullyQualifiedName ?? '',
|
||||
value: item.fullyQualifiedName ?? '',
|
||||
})),
|
||||
paging: {
|
||||
total: res.data.hits.total.value,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const debounceOnSearch = useCallback(debounce(suggestionSearch, 250), []);
|
||||
|
||||
const formatOptions = (data: EntityReference[]) => {
|
||||
return data.map((value) => ({
|
||||
...value,
|
||||
@ -134,14 +147,13 @@ const RelatedTerms = ({
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setSelectedOption(formatOptions(glossaryTerm.relatedTerms || []));
|
||||
setIsIconVisible(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (glossaryTerm.relatedTerms?.length) {
|
||||
setOptions(glossaryTerm.relatedTerms);
|
||||
setSelectedOption(formatOptions(glossaryTerm.relatedTerms));
|
||||
if (glossaryTerm) {
|
||||
setOptions(glossaryTerm.relatedTerms ?? []);
|
||||
setSelectedOption(formatOptions(glossaryTerm.relatedTerms ?? []));
|
||||
}
|
||||
}, [glossaryTerm]);
|
||||
|
||||
@ -279,41 +291,17 @@ const RelatedTerms = ({
|
||||
{isIconVisible ? (
|
||||
relatedTermsContainer
|
||||
) : (
|
||||
<>
|
||||
<Space className="justify-end w-full m-b-xs" size={8}>
|
||||
<Button
|
||||
className="w-6 p-x-05"
|
||||
data-testid="cancel-related-term-btn"
|
||||
icon={<CloseOutlined size={12} />}
|
||||
size="small"
|
||||
onClick={() => handleCancel()}
|
||||
/>
|
||||
<Button
|
||||
className="w-6 p-x-05"
|
||||
data-testid="save-related-term-btn"
|
||||
icon={<CheckOutlined size={12} />}
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => handleRelatedTermsSave(selectedOption)}
|
||||
/>
|
||||
</Space>
|
||||
<Select
|
||||
className="glossary-select w-full"
|
||||
filterOption={false}
|
||||
mode="multiple"
|
||||
notFoundContent={isLoading ? <Spin size="small" /> : null}
|
||||
options={formatOptions(options)}
|
||||
placeholder={t('label.add-entity', {
|
||||
entity: t('label.related-term-plural'),
|
||||
})}
|
||||
value={selectedOption}
|
||||
onChange={(_, data) => {
|
||||
setSelectedOption(data as EntityReference[]);
|
||||
}}
|
||||
onFocus={() => suggestionSearch()}
|
||||
onSearch={debounceOnSearch}
|
||||
/>
|
||||
</>
|
||||
<TagSelectForm
|
||||
defaultValue={selectedOption.map(
|
||||
(item) => item.fullyQualifiedName ?? ''
|
||||
)}
|
||||
fetchApi={fetchGlossaryTerms}
|
||||
placeholder={t('label.add-entity', {
|
||||
entity: t('label.related-term-plural'),
|
||||
})}
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleRelatedTermsSave}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -13,7 +13,6 @@
|
||||
|
||||
import { Button, Col, Form, Row, Space, Tooltip, Typography } from 'antd';
|
||||
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
|
||||
import { AxiosError } from 'axios';
|
||||
import { TableTagsProps } from 'components/TableTags/TableTags.interface';
|
||||
import { DE_ACTIVE_COLOR, PAGE_SIZE } from 'constants/constants';
|
||||
import { TAG_CONSTANT, TAG_START_WITH } from 'constants/Tag.constants';
|
||||
@ -94,29 +93,25 @@ const TagsContainerV2 = ({
|
||||
}[];
|
||||
paging: Paging;
|
||||
}> => {
|
||||
try {
|
||||
const tagResponse = await searchQuery({
|
||||
query: searchQueryParam ? searchQueryParam : '*',
|
||||
pageNumber: page,
|
||||
pageSize: PAGE_SIZE,
|
||||
queryFilter: {},
|
||||
searchIndex: SearchIndex.TAG,
|
||||
});
|
||||
const tagResponse = await searchQuery({
|
||||
query: searchQueryParam ? searchQueryParam : '*',
|
||||
pageNumber: page,
|
||||
pageSize: PAGE_SIZE,
|
||||
queryFilter: {},
|
||||
searchIndex: SearchIndex.TAG,
|
||||
});
|
||||
|
||||
return Promise.resolve({
|
||||
data: formatSearchTagsResponse(tagResponse.hits.hits ?? []).map(
|
||||
(item) => ({
|
||||
label: item.fullyQualifiedName ?? '',
|
||||
value: item.fullyQualifiedName ?? '',
|
||||
})
|
||||
),
|
||||
paging: {
|
||||
total: tagResponse.hits.total.value,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return Promise.reject({ data: (error as AxiosError).response });
|
||||
}
|
||||
return {
|
||||
data: formatSearchTagsResponse(tagResponse.hits.hits ?? []).map(
|
||||
(item) => ({
|
||||
label: item.fullyQualifiedName ?? '',
|
||||
value: item.fullyQualifiedName ?? '',
|
||||
})
|
||||
),
|
||||
paging: {
|
||||
total: tagResponse.hits.total.value,
|
||||
},
|
||||
};
|
||||
},
|
||||
[getTags]
|
||||
);
|
||||
@ -132,29 +127,25 @@ const TagsContainerV2 = ({
|
||||
}[];
|
||||
paging: Paging;
|
||||
}> => {
|
||||
try {
|
||||
const glossaryResponse = await searchQuery({
|
||||
query: searchQueryParam ? searchQueryParam : '*',
|
||||
pageNumber: page,
|
||||
pageSize: 10,
|
||||
queryFilter: {},
|
||||
searchIndex: SearchIndex.GLOSSARY,
|
||||
});
|
||||
const glossaryResponse = await searchQuery({
|
||||
query: searchQueryParam ? searchQueryParam : '*',
|
||||
pageNumber: page,
|
||||
pageSize: 10,
|
||||
queryFilter: {},
|
||||
searchIndex: SearchIndex.GLOSSARY,
|
||||
});
|
||||
|
||||
return Promise.resolve({
|
||||
data: formatSearchGlossaryTermResponse(
|
||||
glossaryResponse.hits.hits ?? []
|
||||
).map((item) => ({
|
||||
label: item.fullyQualifiedName ?? '',
|
||||
value: item.fullyQualifiedName ?? '',
|
||||
})),
|
||||
paging: {
|
||||
total: glossaryResponse.hits.total.value,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return Promise.reject({ data: (error as AxiosError).response });
|
||||
}
|
||||
return {
|
||||
data: formatSearchGlossaryTermResponse(
|
||||
glossaryResponse.hits.hits ?? []
|
||||
).map((item) => ({
|
||||
label: item.fullyQualifiedName ?? '',
|
||||
value: item.fullyQualifiedName ?? '',
|
||||
})),
|
||||
paging: {
|
||||
total: glossaryResponse.hits.total.value,
|
||||
},
|
||||
};
|
||||
},
|
||||
[searchQuery, getGlossaryTerms, formatSearchGlossaryTermResponse]
|
||||
);
|
||||
|
@ -13,7 +13,7 @@
|
||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { Button, Col, Form, Row, Space } from 'antd';
|
||||
import { useForm } from 'antd/lib/form/Form';
|
||||
import InfiniteSelectScroll from 'components/InfiniteSelectScroll/InfiniteSelectScroll';
|
||||
import AsyncSelectList from 'components/AsyncSelectList/AsyncSelectList';
|
||||
import React, { useState } from 'react';
|
||||
import { TagsSelectFormProps } from './TagsSelectForm.interface';
|
||||
|
||||
@ -61,7 +61,7 @@ const TagSelectForm = ({
|
||||
|
||||
<Col className="gutter-row" span={24}>
|
||||
<Form.Item noStyle name="tags">
|
||||
<InfiniteSelectScroll
|
||||
<AsyncSelectList
|
||||
fetchOptions={fetchApi}
|
||||
mode="multiple"
|
||||
placeholder={placeholder}
|
||||
|
@ -11,7 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SelectOption } from 'components/InfiniteSelectScroll/InfiniteSelectScroll.interface';
|
||||
import { SelectOption } from 'components/AsyncSelectList/AsyncSelectList.interface';
|
||||
import { Paging } from 'generated/type/paging';
|
||||
|
||||
export type TagsSelectFormProps = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user