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:
Ashish Gupta 2023-06-28 03:39:06 +05:30 committed by GitHub
parent 206b155a8b
commit d9cd484657
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 113 additions and 133 deletions

View File

@ -288,7 +288,7 @@ const updateTerms = (newTerm) => {
.contains(newTerm) .contains(newTerm)
.should('be.visible') .should('be.visible')
.click(); .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); verifyResponseStatusCode('@saveGlossaryTermData', 200);
cy.get('[data-testid="related-term-container"]') cy.get('[data-testid="related-term-container"]')

View File

@ -18,7 +18,7 @@ export type SelectOption = {
value: string; value: string;
}; };
export interface InfiniteSelectScrollProps { export interface AsyncSelectListProps {
mode?: 'multiple'; mode?: 'multiple';
placeholder?: string; placeholder?: string;
debounceTimeout?: number; debounceTimeout?: number;

View File

@ -21,11 +21,11 @@ import { tagRender } from 'utils/TagsUtils';
import { showErrorToast } from 'utils/ToastUtils'; import { showErrorToast } from 'utils/ToastUtils';
import Fqn from '../../utils/Fqn'; import Fqn from '../../utils/Fqn';
import { import {
InfiniteSelectScrollProps, AsyncSelectListProps,
SelectOption, SelectOption,
} from './InfiniteSelectScroll.interface'; } from './AsyncSelectList.interface';
const InfiniteSelectScroll: FC<InfiniteSelectScrollProps> = ({ const AsyncSelectList: FC<AsyncSelectListProps> = ({
mode, mode,
onChange, onChange,
fetchOptions, fetchOptions,
@ -44,10 +44,11 @@ const InfiniteSelectScroll: FC<InfiniteSelectScrollProps> = ({
setOptions([]); setOptions([]);
setIsLoading(true); setIsLoading(true);
try { try {
const res = await fetchOptions(value, currentPage); const res = await fetchOptions(value, 1);
setOptions(res.data); setOptions(res.data);
setPaging(res.paging); setPaging(res.paging);
setSearchValue(value); setSearchValue(value);
setCurrentPage(1);
} catch (error) { } catch (error) {
showErrorToast(error as AxiosError); showErrorToast(error as AxiosError);
} finally { } finally {
@ -156,4 +157,4 @@ const InfiniteSelectScroll: FC<InfiniteSelectScrollProps> = ({
); );
}; };
export default InfiniteSelectScroll; export default AsyncSelectList;

View File

@ -32,7 +32,7 @@ import RelatedTerms from './RelatedTerms';
type Props = { type Props = {
selectedData: Glossary | GlossaryTerm; selectedData: Glossary | GlossaryTerm;
permissions: OperationPermission; permissions: OperationPermission;
onUpdate: (data: GlossaryTerm | Glossary) => void; onUpdate: (data: GlossaryTerm | Glossary) => Promise<void>;
isGlossary: boolean; isGlossary: boolean;
isVersionView?: boolean; isVersionView?: boolean;
}; };

View File

@ -11,19 +11,21 @@
* limitations under the License. * limitations under the License.
*/ */
import { CheckOutlined, CloseOutlined } from '@ant-design/icons'; import { Button, Tooltip, Typography } from 'antd';
import { Button, Select, Space, Spin, Tooltip, Typography } from 'antd';
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg'; import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import { ReactComponent as IconFlatDoc } from 'assets/svg/ic-flat-doc.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 TagButton from 'components/TagButton/TagButton.component';
import { EntityField } from 'constants/Feeds.constants'; import { EntityField } from 'constants/Feeds.constants';
import { NO_PERMISSION_FOR_ACTION } from 'constants/HelperTextUtil'; import { NO_PERMISSION_FOR_ACTION } from 'constants/HelperTextUtil';
import { ChangeDescription } from 'generated/entity/type'; import { ChangeDescription } from 'generated/entity/type';
import { Paging } from 'generated/type/paging';
import { t } from 'i18next'; 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 React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { searchData } from 'rest/miscAPI'; import { searchData } from 'rest/miscAPI';
import { formatSearchGlossaryTermResponse } from 'utils/APIUtils';
import { getEntityName } from 'utils/EntityUtils'; import { getEntityName } from 'utils/EntityUtils';
import { import {
getChangedEntityNewValue, getChangedEntityNewValue,
@ -41,7 +43,6 @@ import {
import { SearchIndex } from '../../../enums/search.enum'; import { SearchIndex } from '../../../enums/search.enum';
import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm'; import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm';
import { EntityReference } from '../../../generated/type/entityReference'; import { EntityReference } from '../../../generated/type/entityReference';
import { formatSearchGlossaryTermResponse } from '../../../utils/APIUtils';
import { getEntityReferenceFromGlossary } from '../../../utils/GlossaryUtils'; import { getEntityReferenceFromGlossary } from '../../../utils/GlossaryUtils';
import { OperationPermission } from '../../PermissionProvider/PermissionProvider.interface'; import { OperationPermission } from '../../PermissionProvider/PermissionProvider.interface';
@ -49,7 +50,7 @@ interface RelatedTermsProps {
isVersionView?: boolean; isVersionView?: boolean;
permissions: OperationPermission; permissions: OperationPermission;
glossaryTerm: GlossaryTerm; glossaryTerm: GlossaryTerm;
onGlossaryTermUpdate: (data: GlossaryTerm) => void; onGlossaryTermUpdate: (data: GlossaryTerm) => Promise<void>;
} }
const RelatedTerms = ({ const RelatedTerms = ({
@ -60,26 +61,21 @@ const RelatedTerms = ({
}: RelatedTermsProps) => { }: RelatedTermsProps) => {
const history = useHistory(); const history = useHistory();
const [isIconVisible, setIsIconVisible] = useState<boolean>(true); const [isIconVisible, setIsIconVisible] = useState<boolean>(true);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [options, setOptions] = useState<EntityReference[]>([]); const [options, setOptions] = useState<EntityReference[]>([]);
const [selectedOption, setSelectedOption] = 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) => { const handleRelatedTermClick = (fqn: string) => {
history.push(getGlossaryPath(fqn)); 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); let updatedGlossaryTerm = cloneDeep(glossaryTerm);
const oldTerms = newOptions.filter((d) => const oldTerms = newOptions.filter((d) =>
includes(glossaryTerm.relatedTerms, d) includes(glossaryTerm.relatedTerms, d)
@ -97,33 +93,50 @@ const RelatedTerms = ({
relatedTerms: [...oldTerms, ...newTerms], relatedTerms: [...oldTerms, ...newTerms],
}; };
onGlossaryTermUpdate(updatedGlossaryTerm); await onGlossaryTermUpdate(updatedGlossaryTerm);
setIsIconVisible(true); setIsIconVisible(true);
}; };
const suggestionSearch = (searchText = '') => { const fetchGlossaryTerms = async (
setIsLoading(true); searchText = '',
searchData(searchText, 1, PAGE_SIZE, '', '', '', SearchIndex.GLOSSARY) page: number
.then((res) => { ): Promise<{
const termResult = formatSearchGlossaryTermResponse( data: {
res.data.hits.hits label: string;
).filter((item) => { value: string;
return item.fullyQualifiedName !== glossaryTerm.fullyQualifiedName; }[];
}); 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; const results = termResult.map(getEntityReferenceFromGlossary);
setOptions(data); setOptions((prev) => [...prev, ...results]);
})
.catch(() => { return {
setOptions(selectedOption); data: results.map((item) => ({
}) label: item.fullyQualifiedName ?? '',
.finally(() => setIsLoading(false)); value: item.fullyQualifiedName ?? '',
})),
paging: {
total: res.data.hits.total.value,
},
};
}; };
const debounceOnSearch = useCallback(debounce(suggestionSearch, 250), []);
const formatOptions = (data: EntityReference[]) => { const formatOptions = (data: EntityReference[]) => {
return data.map((value) => ({ return data.map((value) => ({
...value, ...value,
@ -134,14 +147,13 @@ const RelatedTerms = ({
}; };
const handleCancel = () => { const handleCancel = () => {
setSelectedOption(formatOptions(glossaryTerm.relatedTerms || []));
setIsIconVisible(true); setIsIconVisible(true);
}; };
useEffect(() => { useEffect(() => {
if (glossaryTerm.relatedTerms?.length) { if (glossaryTerm) {
setOptions(glossaryTerm.relatedTerms); setOptions(glossaryTerm.relatedTerms ?? []);
setSelectedOption(formatOptions(glossaryTerm.relatedTerms)); setSelectedOption(formatOptions(glossaryTerm.relatedTerms ?? []));
} }
}, [glossaryTerm]); }, [glossaryTerm]);
@ -279,41 +291,17 @@ const RelatedTerms = ({
{isIconVisible ? ( {isIconVisible ? (
relatedTermsContainer relatedTermsContainer
) : ( ) : (
<> <TagSelectForm
<Space className="justify-end w-full m-b-xs" size={8}> defaultValue={selectedOption.map(
<Button (item) => item.fullyQualifiedName ?? ''
className="w-6 p-x-05" )}
data-testid="cancel-related-term-btn" fetchApi={fetchGlossaryTerms}
icon={<CloseOutlined size={12} />} placeholder={t('label.add-entity', {
size="small" entity: t('label.related-term-plural'),
onClick={() => handleCancel()} })}
/> onCancel={handleCancel}
<Button onSubmit={handleRelatedTermsSave}
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}
/>
</>
)} )}
</div> </div>
); );

View File

@ -13,7 +13,6 @@
import { Button, Col, Form, Row, Space, Tooltip, Typography } from 'antd'; import { Button, Col, Form, Row, Space, Tooltip, Typography } from 'antd';
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg'; import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import { AxiosError } from 'axios';
import { TableTagsProps } from 'components/TableTags/TableTags.interface'; import { TableTagsProps } from 'components/TableTags/TableTags.interface';
import { DE_ACTIVE_COLOR, PAGE_SIZE } from 'constants/constants'; import { DE_ACTIVE_COLOR, PAGE_SIZE } from 'constants/constants';
import { TAG_CONSTANT, TAG_START_WITH } from 'constants/Tag.constants'; import { TAG_CONSTANT, TAG_START_WITH } from 'constants/Tag.constants';
@ -94,29 +93,25 @@ const TagsContainerV2 = ({
}[]; }[];
paging: Paging; paging: Paging;
}> => { }> => {
try { const tagResponse = await searchQuery({
const tagResponse = await searchQuery({ query: searchQueryParam ? searchQueryParam : '*',
query: searchQueryParam ? searchQueryParam : '*', pageNumber: page,
pageNumber: page, pageSize: PAGE_SIZE,
pageSize: PAGE_SIZE, queryFilter: {},
queryFilter: {}, searchIndex: SearchIndex.TAG,
searchIndex: SearchIndex.TAG, });
});
return Promise.resolve({ return {
data: formatSearchTagsResponse(tagResponse.hits.hits ?? []).map( data: formatSearchTagsResponse(tagResponse.hits.hits ?? []).map(
(item) => ({ (item) => ({
label: item.fullyQualifiedName ?? '', label: item.fullyQualifiedName ?? '',
value: item.fullyQualifiedName ?? '', value: item.fullyQualifiedName ?? '',
}) })
), ),
paging: { paging: {
total: tagResponse.hits.total.value, total: tagResponse.hits.total.value,
}, },
}); };
} catch (error) {
return Promise.reject({ data: (error as AxiosError).response });
}
}, },
[getTags] [getTags]
); );
@ -132,29 +127,25 @@ const TagsContainerV2 = ({
}[]; }[];
paging: Paging; paging: Paging;
}> => { }> => {
try { const glossaryResponse = await searchQuery({
const glossaryResponse = await searchQuery({ query: searchQueryParam ? searchQueryParam : '*',
query: searchQueryParam ? searchQueryParam : '*', pageNumber: page,
pageNumber: page, pageSize: 10,
pageSize: 10, queryFilter: {},
queryFilter: {}, searchIndex: SearchIndex.GLOSSARY,
searchIndex: SearchIndex.GLOSSARY, });
});
return Promise.resolve({ return {
data: formatSearchGlossaryTermResponse( data: formatSearchGlossaryTermResponse(
glossaryResponse.hits.hits ?? [] glossaryResponse.hits.hits ?? []
).map((item) => ({ ).map((item) => ({
label: item.fullyQualifiedName ?? '', label: item.fullyQualifiedName ?? '',
value: item.fullyQualifiedName ?? '', value: item.fullyQualifiedName ?? '',
})), })),
paging: { paging: {
total: glossaryResponse.hits.total.value, total: glossaryResponse.hits.total.value,
}, },
}); };
} catch (error) {
return Promise.reject({ data: (error as AxiosError).response });
}
}, },
[searchQuery, getGlossaryTerms, formatSearchGlossaryTermResponse] [searchQuery, getGlossaryTerms, formatSearchGlossaryTermResponse]
); );

View File

@ -13,7 +13,7 @@
import { CheckOutlined, CloseOutlined } from '@ant-design/icons'; import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { Button, Col, Form, Row, Space } from 'antd'; import { Button, Col, Form, Row, Space } from 'antd';
import { useForm } from 'antd/lib/form/Form'; 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 React, { useState } from 'react';
import { TagsSelectFormProps } from './TagsSelectForm.interface'; import { TagsSelectFormProps } from './TagsSelectForm.interface';
@ -61,7 +61,7 @@ const TagSelectForm = ({
<Col className="gutter-row" span={24}> <Col className="gutter-row" span={24}>
<Form.Item noStyle name="tags"> <Form.Item noStyle name="tags">
<InfiniteSelectScroll <AsyncSelectList
fetchOptions={fetchApi} fetchOptions={fetchApi}
mode="multiple" mode="multiple"
placeholder={placeholder} placeholder={placeholder}

View File

@ -11,7 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { SelectOption } from 'components/InfiniteSelectScroll/InfiniteSelectScroll.interface'; import { SelectOption } from 'components/AsyncSelectList/AsyncSelectList.interface';
import { Paging } from 'generated/type/paging'; import { Paging } from 'generated/type/paging';
export type TagsSelectFormProps = { export type TagsSelectFormProps = {