mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-23 07:51:44 +00:00
chore(ui): remove unused tags files and minor ui fixes (#12264)
* remove unused tags files and minor ui fixes * no tags shown as per global styling * fix unit test
This commit is contained in:
parent
cf2aafa770
commit
8635fd7a28
@ -19,7 +19,7 @@ import DescriptionV1 from 'components/common/description/DescriptionV1';
|
|||||||
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
|
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
|
||||||
import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader';
|
import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader';
|
||||||
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
||||||
import TagsContainerV1 from 'components/Tag/TagsContainerV1/TagsContainerV1';
|
import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
|
||||||
import { getVersionPathWithTab } from 'constants/constants';
|
import { getVersionPathWithTab } from 'constants/constants';
|
||||||
import { EntityField } from 'constants/Feeds.constants';
|
import { EntityField } from 'constants/Feeds.constants';
|
||||||
import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
|
import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
|
||||||
@ -153,9 +153,7 @@ const ContainerVersion: React.FC<ContainerVersionProp> = ({
|
|||||||
flex="220px">
|
flex="220px">
|
||||||
<Space className="w-full" direction="vertical" size="large">
|
<Space className="w-full" direction="vertical" size="large">
|
||||||
{Object.keys(TagSource).map((tagType) => (
|
{Object.keys(TagSource).map((tagType) => (
|
||||||
<TagsContainerV1
|
<TagsContainerV2
|
||||||
isVersionView
|
|
||||||
showLimited
|
|
||||||
entityFqn={containerFQN}
|
entityFqn={containerFQN}
|
||||||
entityType={EntityType.CONTAINER}
|
entityType={EntityType.CONTAINER}
|
||||||
key={tagType}
|
key={tagType}
|
||||||
|
@ -21,7 +21,7 @@ import DescriptionV1 from 'components/common/description/DescriptionV1';
|
|||||||
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
|
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
|
||||||
import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader';
|
import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader';
|
||||||
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
||||||
import TagsContainerV1 from 'components/Tag/TagsContainerV1/TagsContainerV1';
|
import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
|
||||||
import { getVersionPathWithTab } from 'constants/constants';
|
import { getVersionPathWithTab } from 'constants/constants';
|
||||||
import { EntityField } from 'constants/Feeds.constants';
|
import { EntityField } from 'constants/Feeds.constants';
|
||||||
import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
|
import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
|
||||||
@ -195,9 +195,7 @@ const DashboardVersion: FC<DashboardVersionProp> = ({
|
|||||||
flex="220px">
|
flex="220px">
|
||||||
<Space className="w-full" direction="vertical" size="large">
|
<Space className="w-full" direction="vertical" size="large">
|
||||||
{Object.keys(TagSource).map((tagType) => (
|
{Object.keys(TagSource).map((tagType) => (
|
||||||
<TagsContainerV1
|
<TagsContainerV2
|
||||||
isVersionView
|
|
||||||
showLimited
|
|
||||||
entityFqn={currentVersionData.fullyQualifiedName}
|
entityFqn={currentVersionData.fullyQualifiedName}
|
||||||
entityType={EntityType.DASHBOARD}
|
entityType={EntityType.DASHBOARD}
|
||||||
key={tagType}
|
key={tagType}
|
||||||
|
@ -17,7 +17,7 @@ import DescriptionV1 from 'components/common/description/DescriptionV1';
|
|||||||
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
|
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
|
||||||
import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader';
|
import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader';
|
||||||
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
||||||
import TagsContainerV1 from 'components/Tag/TagsContainerV1/TagsContainerV1';
|
import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
|
||||||
import VersionTable from 'components/VersionTable/VersionTable.component';
|
import VersionTable from 'components/VersionTable/VersionTable.component';
|
||||||
import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
|
import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
|
||||||
import { EntityTabs, EntityType, FqnPart } from 'enums/entity.enum';
|
import { EntityTabs, EntityType, FqnPart } from 'enums/entity.enum';
|
||||||
@ -301,9 +301,7 @@ const DataModelVersion: FC<DataModelVersionProp> = ({
|
|||||||
flex="220px">
|
flex="220px">
|
||||||
<Space className="w-full" direction="vertical" size="large">
|
<Space className="w-full" direction="vertical" size="large">
|
||||||
{Object.keys(TagSource).map((tagType) => (
|
{Object.keys(TagSource).map((tagType) => (
|
||||||
<TagsContainerV1
|
<TagsContainerV2
|
||||||
isVersionView
|
|
||||||
showLimited
|
|
||||||
entityFqn={currentVersionData.fullyQualifiedName}
|
entityFqn={currentVersionData.fullyQualifiedName}
|
||||||
entityType={EntityType.DASHBOARD_DATA_MODEL}
|
entityType={EntityType.DASHBOARD_DATA_MODEL}
|
||||||
key={tagType}
|
key={tagType}
|
||||||
|
@ -19,7 +19,7 @@ import DescriptionV1 from 'components/common/description/DescriptionV1';
|
|||||||
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
|
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
|
||||||
import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader';
|
import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader';
|
||||||
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
||||||
import TagsContainerV1 from 'components/Tag/TagsContainerV1/TagsContainerV1';
|
import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
|
||||||
import { getVersionPathWithTab } from 'constants/constants';
|
import { getVersionPathWithTab } from 'constants/constants';
|
||||||
import { EntityField } from 'constants/Feeds.constants';
|
import { EntityField } from 'constants/Feeds.constants';
|
||||||
import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
|
import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
|
||||||
@ -173,9 +173,7 @@ const DatasetVersion: React.FC<DatasetVersionProp> = ({
|
|||||||
flex="220px">
|
flex="220px">
|
||||||
<Space className="w-full" direction="vertical" size="large">
|
<Space className="w-full" direction="vertical" size="large">
|
||||||
{Object.keys(TagSource).map((tagType) => (
|
{Object.keys(TagSource).map((tagType) => (
|
||||||
<TagsContainerV1
|
<TagsContainerV2
|
||||||
isVersionView
|
|
||||||
showLimited
|
|
||||||
entityFqn={datasetFQN}
|
entityFqn={datasetFQN}
|
||||||
entityType={EntityType.TABLE}
|
entityType={EntityType.TABLE}
|
||||||
key={tagType}
|
key={tagType}
|
||||||
|
@ -30,7 +30,7 @@ import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichText
|
|||||||
import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader';
|
import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader';
|
||||||
import SourceList from 'components/MlModelDetail/SourceList.component';
|
import SourceList from 'components/MlModelDetail/SourceList.component';
|
||||||
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
||||||
import TagsContainerV1 from 'components/Tag/TagsContainerV1/TagsContainerV1';
|
import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
|
||||||
import TagsViewer from 'components/Tag/TagsViewer/tags-viewer';
|
import TagsViewer from 'components/Tag/TagsViewer/tags-viewer';
|
||||||
import { getVersionPathWithTab } from 'constants/constants';
|
import { getVersionPathWithTab } from 'constants/constants';
|
||||||
import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
|
import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
|
||||||
@ -272,7 +272,7 @@ const MlModelVersion: FC<MlModelVersionProp> = ({
|
|||||||
</Col>
|
</Col>
|
||||||
<Col className="m-b-xs" span={24}>
|
<Col className="m-b-xs" span={24}>
|
||||||
<Row gutter={8} wrap={false}>
|
<Row gutter={8} wrap={false}>
|
||||||
<Col flex="120px">
|
<Col flex="130px">
|
||||||
<Typography.Text className="text-grey-muted">
|
<Typography.Text className="text-grey-muted">
|
||||||
{`${t('label.glossary-term-plural')} :`}
|
{`${t('label.glossary-term-plural')} :`}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@ -293,7 +293,7 @@ const MlModelVersion: FC<MlModelVersionProp> = ({
|
|||||||
|
|
||||||
<Col className="m-b-xs" span={24}>
|
<Col className="m-b-xs" span={24}>
|
||||||
<Row gutter={8} wrap={false}>
|
<Row gutter={8} wrap={false}>
|
||||||
<Col flex="120px">
|
<Col flex="130px">
|
||||||
<Typography.Text className="text-grey-muted">
|
<Typography.Text className="text-grey-muted">
|
||||||
{`${t('label.tag-plural')} :`}
|
{`${t('label.tag-plural')} :`}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@ -358,9 +358,7 @@ const MlModelVersion: FC<MlModelVersionProp> = ({
|
|||||||
flex="220px">
|
flex="220px">
|
||||||
<Space className="w-full" direction="vertical" size="large">
|
<Space className="w-full" direction="vertical" size="large">
|
||||||
{Object.keys(TagSource).map((tagType) => (
|
{Object.keys(TagSource).map((tagType) => (
|
||||||
<TagsContainerV1
|
<TagsContainerV2
|
||||||
isVersionView
|
|
||||||
showLimited
|
|
||||||
entityFqn={currentVersionData.fullyQualifiedName}
|
entityFqn={currentVersionData.fullyQualifiedName}
|
||||||
entityType={EntityType.MLMODEL}
|
entityType={EntityType.MLMODEL}
|
||||||
key={tagType}
|
key={tagType}
|
||||||
|
@ -21,7 +21,7 @@ import DescriptionV1 from 'components/common/description/DescriptionV1';
|
|||||||
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
|
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
|
||||||
import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader';
|
import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader';
|
||||||
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
||||||
import TagsContainerV1 from 'components/Tag/TagsContainerV1/TagsContainerV1';
|
import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
|
||||||
import TagsViewer from 'components/Tag/TagsViewer/tags-viewer';
|
import TagsViewer from 'components/Tag/TagsViewer/tags-viewer';
|
||||||
import { getVersionPathWithTab } from 'constants/constants';
|
import { getVersionPathWithTab } from 'constants/constants';
|
||||||
import { TABLE_SCROLL_VALUE } from 'constants/Table.constants';
|
import { TABLE_SCROLL_VALUE } from 'constants/Table.constants';
|
||||||
@ -379,9 +379,7 @@ const PipelineVersion: FC<PipelineVersionProp> = ({
|
|||||||
flex="220px">
|
flex="220px">
|
||||||
<Space className="w-full" direction="vertical" size="large">
|
<Space className="w-full" direction="vertical" size="large">
|
||||||
{Object.keys(TagSource).map((tagType) => (
|
{Object.keys(TagSource).map((tagType) => (
|
||||||
<TagsContainerV1
|
<TagsContainerV2
|
||||||
isVersionView
|
|
||||||
showLimited
|
|
||||||
entityFqn={currentVersionData.fullyQualifiedName}
|
entityFqn={currentVersionData.fullyQualifiedName}
|
||||||
entityType={EntityType.PIPELINE}
|
entityType={EntityType.PIPELINE}
|
||||||
key={tagType}
|
key={tagType}
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface';
|
import { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface';
|
||||||
import { TagsDetailsProps } from 'components/Tag/TagsContainerV1/TagsContainerV1.interface';
|
|
||||||
import { Query } from 'generated/entity/data/query';
|
import { Query } from 'generated/entity/data/query';
|
||||||
|
|
||||||
export interface TableQueryRightPanelProps {
|
export interface TableQueryRightPanelProps {
|
||||||
@ -21,9 +20,3 @@ export interface TableQueryRightPanelProps {
|
|||||||
permission: OperationPermission;
|
permission: OperationPermission;
|
||||||
onQueryUpdate: (updatedQuery: Query, key: keyof Query) => Promise<void>;
|
onQueryUpdate: (updatedQuery: Query, key: keyof Query) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TagDetails = {
|
|
||||||
isLoading: boolean;
|
|
||||||
options: TagsDetailsProps[];
|
|
||||||
isError: boolean;
|
|
||||||
};
|
|
||||||
|
@ -204,7 +204,6 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
|
|||||||
() =>
|
() =>
|
||||||
showLimited ? (
|
showLimited ? (
|
||||||
<TagsViewer
|
<TagsViewer
|
||||||
isTextPlaceholder
|
|
||||||
showNoDataPlaceholder={showNoDataPlaceholder}
|
showNoDataPlaceholder={showNoDataPlaceholder}
|
||||||
tags={selectedTags}
|
tags={selectedTags}
|
||||||
type="border"
|
type="border"
|
||||||
|
@ -1,162 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2022 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 { Button, Space } from 'antd';
|
|
||||||
import { ReactComponent as IconEdit } from 'assets/svg/edit-new.svg';
|
|
||||||
import Loader from 'components/Loader/Loader';
|
|
||||||
import { TableTagsProps } from 'components/TableTags/TableTags.interface';
|
|
||||||
import Tags from 'components/Tag/Tags/tags';
|
|
||||||
import { TAG_CONSTANT, TAG_START_WITH } from 'constants/Tag.constants';
|
|
||||||
import { TagSource } from 'generated/type/tagLabel';
|
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
import { EntityTags } from 'Models';
|
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { getFilterTags } from 'utils/TableTags/TableTags.utils';
|
|
||||||
import { getTagPlaceholder } from 'utils/TagsUtils';
|
|
||||||
import TagTree from '../TagsTree/TagsTreeForm.component';
|
|
||||||
import TagsViewer from '../TagsViewer/tags-viewer';
|
|
||||||
import { TagsContainerEntityTableProps } from './TagsContainerEntityTable.interface';
|
|
||||||
|
|
||||||
const TagsContainerEntityTable = ({
|
|
||||||
isLoading,
|
|
||||||
isEditing,
|
|
||||||
permission,
|
|
||||||
selectedTags,
|
|
||||||
showEditButton,
|
|
||||||
tagType,
|
|
||||||
treeData,
|
|
||||||
onCancel,
|
|
||||||
onSelectionChange,
|
|
||||||
onAddButtonClick,
|
|
||||||
}: TagsContainerEntityTableProps) => {
|
|
||||||
const [tags, setTags] = useState<TableTagsProps>();
|
|
||||||
|
|
||||||
const isGlossaryType = useMemo(
|
|
||||||
() => tagType === TagSource.Glossary,
|
|
||||||
[tagType]
|
|
||||||
);
|
|
||||||
|
|
||||||
const showAddTagButton = useMemo(
|
|
||||||
() => permission && isEmpty(tags?.[tagType]),
|
|
||||||
[permission, tags?.[tagType]]
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedTagsInternal = useMemo(
|
|
||||||
() => tags?.[tagType].map(({ tagFQN }) => tagFQN as string),
|
|
||||||
[tags, tagType]
|
|
||||||
);
|
|
||||||
|
|
||||||
const showNoDataPlaceholder = useMemo(
|
|
||||||
() => !showAddTagButton && isEmpty(tags?.[tagType]),
|
|
||||||
[showAddTagButton, tags?.[tagType]]
|
|
||||||
);
|
|
||||||
|
|
||||||
const getUpdatedTags = (selectedTag: string[]): EntityTags[] => {
|
|
||||||
const updatedTags = selectedTag.map((t) => ({
|
|
||||||
tagFQN: t,
|
|
||||||
source: isGlossaryType ? TagSource.Glossary : TagSource.Classification,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return updatedTags;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = useCallback(
|
|
||||||
(selectedTag: string[]) => {
|
|
||||||
const updatedTags = getUpdatedTags(selectedTag);
|
|
||||||
onSelectionChange([
|
|
||||||
...updatedTags,
|
|
||||||
...((isGlossaryType
|
|
||||||
? tags?.[TagSource.Classification]
|
|
||||||
: tags?.[TagSource.Glossary]) ?? []),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
[isGlossaryType, tags, getUpdatedTags]
|
|
||||||
);
|
|
||||||
|
|
||||||
const editTagButton = useMemo(
|
|
||||||
() =>
|
|
||||||
!isEmpty(tags?.[tagType]) && showEditButton ? (
|
|
||||||
<Button
|
|
||||||
className="p-0 flex-center text-primary"
|
|
||||||
data-testid="edit-button"
|
|
||||||
icon={<IconEdit className="anticon" height={16} width={16} />}
|
|
||||||
size="small"
|
|
||||||
type="text"
|
|
||||||
onClick={onAddButtonClick}
|
|
||||||
/>
|
|
||||||
) : null,
|
|
||||||
[selectedTags, showEditButton, onAddButtonClick]
|
|
||||||
);
|
|
||||||
|
|
||||||
const addTagButton = useMemo(
|
|
||||||
() =>
|
|
||||||
showAddTagButton ? (
|
|
||||||
<span onClick={onAddButtonClick}>
|
|
||||||
<Tags
|
|
||||||
className="tw-font-semibold tw-text-primary"
|
|
||||||
startWith={TAG_START_WITH.PLUS}
|
|
||||||
tag={TAG_CONSTANT}
|
|
||||||
type="border"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : null,
|
|
||||||
[showAddTagButton]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderTags = useMemo(
|
|
||||||
() => (
|
|
||||||
<TagsViewer
|
|
||||||
isTextPlaceholder
|
|
||||||
showNoDataPlaceholder={showNoDataPlaceholder}
|
|
||||||
tags={tags?.[tagType] ?? []}
|
|
||||||
type="border"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[showNoDataPlaceholder, tags?.[tagType]]
|
|
||||||
);
|
|
||||||
|
|
||||||
const tagsSelectContainer = useMemo(() => {
|
|
||||||
return isLoading ? (
|
|
||||||
<Loader size="small" />
|
|
||||||
) : (
|
|
||||||
<TagTree
|
|
||||||
defaultValue={selectedTagsInternal ?? []}
|
|
||||||
placeholder={getTagPlaceholder(isGlossaryType)}
|
|
||||||
treeData={treeData}
|
|
||||||
onCancel={onCancel}
|
|
||||||
onSubmit={handleSave}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}, [isGlossaryType, selectedTagsInternal, treeData, onCancel, handleSave]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTags(getFilterTags(selectedTags));
|
|
||||||
}, [selectedTags]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="w-full"
|
|
||||||
data-testid={isGlossaryType ? 'glossary-container' : 'tags-container'}>
|
|
||||||
{!isEditing && (
|
|
||||||
<Space wrap align="center" data-testid="entity-tags" size={4}>
|
|
||||||
{addTagButton}
|
|
||||||
{renderTags}
|
|
||||||
{editTagButton}
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
{isEditing && tagsSelectContainer}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TagsContainerEntityTable;
|
|
@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { TagSource } from 'generated/type/tagLabel';
|
|
||||||
import { EntityTags } from 'Models';
|
|
||||||
import { HierarchyTagsProps } from '../TagsContainerV1/TagsContainerV1.interface';
|
|
||||||
|
|
||||||
export type TagsContainerEntityTableProps = {
|
|
||||||
isEditing: boolean;
|
|
||||||
isLoading: boolean;
|
|
||||||
permission: boolean;
|
|
||||||
selectedTags: EntityTags;
|
|
||||||
tagType: TagSource;
|
|
||||||
treeData: HierarchyTagsProps[];
|
|
||||||
showEditButton?: boolean;
|
|
||||||
onCancel: () => void;
|
|
||||||
onAddButtonClick: () => void;
|
|
||||||
onSelectionChange: (selectedTags: EntityTags[]) => void;
|
|
||||||
};
|
|
@ -1,80 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { ThreadType } from 'generated/api/feed/createThread';
|
|
||||||
import { Tag } from 'generated/entity/classification/tag';
|
|
||||||
import { GlossaryTerm } from 'generated/entity/data/glossaryTerm';
|
|
||||||
import { TagSource } from 'generated/type/tagLabel';
|
|
||||||
import { EntityTags } from 'Models';
|
|
||||||
|
|
||||||
interface TagsTreeProps {
|
|
||||||
title: string;
|
|
||||||
value: string;
|
|
||||||
key: string;
|
|
||||||
selectable: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HierarchyTagsProps extends TagsTreeProps {
|
|
||||||
children: TagsTreeProps[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TagsDetailsProps {
|
|
||||||
name: string;
|
|
||||||
fqn: string;
|
|
||||||
classification: Tag['classification'];
|
|
||||||
source: TagSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TagDetailsProps = {
|
|
||||||
isLoading: boolean;
|
|
||||||
options: TagsDetailsProps[];
|
|
||||||
isError: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GlossaryDetailsProps = {
|
|
||||||
isLoading: boolean;
|
|
||||||
options: GlossaryTermDetailsProps[];
|
|
||||||
isError: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GlossaryTermDetailsProps = {
|
|
||||||
name: string;
|
|
||||||
fqn: string;
|
|
||||||
children: GlossaryTerm['children'];
|
|
||||||
parent: GlossaryTerm['parent'];
|
|
||||||
glossary: GlossaryTerm['glossary'];
|
|
||||||
source: TagSource;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TagsContainerV1Props = {
|
|
||||||
isVersionView?: boolean;
|
|
||||||
permission: boolean;
|
|
||||||
selectedTags: Array<EntityTags>;
|
|
||||||
onSelectionChange?: (selectedTags: Array<EntityTags>) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
showLimited?: boolean;
|
|
||||||
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
|
|
||||||
entityType?: string;
|
|
||||||
entityThreadLink?: string;
|
|
||||||
entityFqn?: string;
|
|
||||||
tagType: TagSource;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TagsTreeComponentProps = {
|
|
||||||
placeholder: string;
|
|
||||||
treeData: HierarchyTagsProps[];
|
|
||||||
defaultValue: string[];
|
|
||||||
onChange?: (value: string[]) => void;
|
|
||||||
onSubmit: (tags: string[]) => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
};
|
|
@ -1,388 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2022 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 { Button, Col, Form, Row, Space, Tooltip, Typography } from 'antd';
|
|
||||||
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
|
|
||||||
import Loader from 'components/Loader/Loader';
|
|
||||||
import { TableTagsProps } from 'components/TableTags/TableTags.interface';
|
|
||||||
import Tags from 'components/Tag/Tags/tags';
|
|
||||||
import {
|
|
||||||
API_RES_MAX_SIZE,
|
|
||||||
DE_ACTIVE_COLOR,
|
|
||||||
PAGE_SIZE_LARGE,
|
|
||||||
} from 'constants/constants';
|
|
||||||
import { TAG_CONSTANT, TAG_START_WITH } from 'constants/Tag.constants';
|
|
||||||
import { EntityType } from 'enums/entity.enum';
|
|
||||||
import { TagSource } from 'generated/type/tagLabel';
|
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
import { EntityTags } from 'Models';
|
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import { getGlossariesList, getGlossaryTerms } from 'rest/glossaryAPI';
|
|
||||||
import { getEntityFeedLink } from 'utils/EntityUtils';
|
|
||||||
import { getGlossaryTermHierarchy } from 'utils/GlossaryUtils';
|
|
||||||
import { getFilterTags } from 'utils/TableTags/TableTags.utils';
|
|
||||||
import {
|
|
||||||
getAllTagsForOptions,
|
|
||||||
getTagPlaceholder,
|
|
||||||
getTagsHierarchy,
|
|
||||||
} from 'utils/TagsUtils';
|
|
||||||
import {
|
|
||||||
getRequestTagsPath,
|
|
||||||
getUpdateTagsPath,
|
|
||||||
TASK_ENTITIES,
|
|
||||||
} from 'utils/TasksUtils';
|
|
||||||
import { ReactComponent as IconComments } from '../../../assets/svg/comment.svg';
|
|
||||||
import { ReactComponent as IconRequest } from '../../../assets/svg/request-icon.svg';
|
|
||||||
import TagTree from '../TagsTree/TagsTreeForm.component';
|
|
||||||
import TagsViewer from '../TagsViewer/tags-viewer';
|
|
||||||
import {
|
|
||||||
GlossaryDetailsProps,
|
|
||||||
GlossaryTermDetailsProps,
|
|
||||||
TagDetailsProps,
|
|
||||||
TagsContainerV1Props,
|
|
||||||
} from './TagsContainerV1.interface';
|
|
||||||
|
|
||||||
const TagsContainerV1 = ({
|
|
||||||
permission,
|
|
||||||
selectedTags,
|
|
||||||
entityType,
|
|
||||||
entityThreadLink,
|
|
||||||
entityFqn,
|
|
||||||
tagType,
|
|
||||||
onSelectionChange,
|
|
||||||
onThreadLinkSelect,
|
|
||||||
isVersionView,
|
|
||||||
}: TagsContainerV1Props) => {
|
|
||||||
const history = useHistory();
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const [isEditTags, setIsEditTags] = useState(false);
|
|
||||||
const [tagDetails, setTagDetails] = useState<TagDetailsProps>({
|
|
||||||
isLoading: false,
|
|
||||||
isError: false,
|
|
||||||
options: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const [glossaryDetails, setGlossaryDetails] = useState<GlossaryDetailsProps>({
|
|
||||||
isLoading: false,
|
|
||||||
isError: false,
|
|
||||||
options: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const [tags, setTags] = useState<TableTagsProps>();
|
|
||||||
|
|
||||||
const isGlossaryType = useMemo(
|
|
||||||
() => tagType === TagSource.Glossary,
|
|
||||||
[tagType]
|
|
||||||
);
|
|
||||||
|
|
||||||
const showAddTagButton = useMemo(
|
|
||||||
() => permission && isEmpty(tags?.[tagType]),
|
|
||||||
[permission, tags?.[tagType]]
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedTagsInternal = useMemo(
|
|
||||||
() => tags?.[tagType].map(({ tagFQN }) => tagFQN as string),
|
|
||||||
[tags, tagType]
|
|
||||||
);
|
|
||||||
|
|
||||||
const treeData = useMemo(() => {
|
|
||||||
const tags = getTagsHierarchy(tagDetails.options);
|
|
||||||
const glossary = getGlossaryTermHierarchy(glossaryDetails.options);
|
|
||||||
|
|
||||||
return [...tags, ...glossary];
|
|
||||||
}, [tagDetails.options, glossaryDetails.options]);
|
|
||||||
|
|
||||||
const fetchTags = async () => {
|
|
||||||
if (isEmpty(tagDetails.options) || tagDetails.isError) {
|
|
||||||
setTagDetails((pre) => ({ ...pre, isLoading: true }));
|
|
||||||
try {
|
|
||||||
const tags = await getAllTagsForOptions();
|
|
||||||
setTagDetails((pre) => ({
|
|
||||||
...pre,
|
|
||||||
options: tags.map((tag) => {
|
|
||||||
return {
|
|
||||||
name: tag.name,
|
|
||||||
fqn: tag.fullyQualifiedName ?? '',
|
|
||||||
classification: tag.classification,
|
|
||||||
source: TagSource.Classification,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
setIsEditTags(true);
|
|
||||||
} catch (_error) {
|
|
||||||
setTagDetails((pre) => ({ ...pre, isError: true, options: [] }));
|
|
||||||
} finally {
|
|
||||||
setTagDetails((pre) => ({ ...pre, isLoading: false }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchGlossaryList = async () => {
|
|
||||||
if (isEmpty(glossaryDetails.options) || glossaryDetails.isError) {
|
|
||||||
setGlossaryDetails((pre) => ({ ...pre, isLoading: true }));
|
|
||||||
try {
|
|
||||||
const glossaryTermList: GlossaryTermDetailsProps[] = [];
|
|
||||||
const { data } = await getGlossariesList({
|
|
||||||
limit: PAGE_SIZE_LARGE,
|
|
||||||
});
|
|
||||||
|
|
||||||
const promises = data.map((item) =>
|
|
||||||
getGlossaryTerms({
|
|
||||||
glossary: item.id,
|
|
||||||
limit: API_RES_MAX_SIZE,
|
|
||||||
fields: 'children,parent',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const response = await Promise.allSettled(promises);
|
|
||||||
|
|
||||||
response.forEach((res) => {
|
|
||||||
if (res.status === 'fulfilled') {
|
|
||||||
glossaryTermList.push(
|
|
||||||
...res.value.data.map((data) => ({
|
|
||||||
name: data.name,
|
|
||||||
fqn: data.fullyQualifiedName ?? '',
|
|
||||||
children: data.children,
|
|
||||||
parent: data.parent,
|
|
||||||
glossary: data.glossary,
|
|
||||||
source: TagSource.Glossary,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setGlossaryDetails((pre) => ({ ...pre, options: glossaryTermList }));
|
|
||||||
} catch (error) {
|
|
||||||
setGlossaryDetails((pre) => ({ ...pre, isError: true, options: [] }));
|
|
||||||
} finally {
|
|
||||||
setGlossaryDetails((pre) => ({ ...pre, isLoading: false }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showNoDataPlaceholder = useMemo(
|
|
||||||
() => !showAddTagButton && isEmpty(tags?.[tagType]),
|
|
||||||
[showAddTagButton, tags?.[tagType]]
|
|
||||||
);
|
|
||||||
|
|
||||||
const getUpdatedTags = (selectedTag: string[]): EntityTags[] => {
|
|
||||||
const updatedTags = selectedTag.map((t) => ({
|
|
||||||
tagFQN: t,
|
|
||||||
source: [...tagDetails.options, ...glossaryDetails.options].find(
|
|
||||||
(tag) => tag.fqn === t
|
|
||||||
)?.source,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return updatedTags;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = (data: string[]) => {
|
|
||||||
const updatedTags = getUpdatedTags(data);
|
|
||||||
if (onSelectionChange) {
|
|
||||||
onSelectionChange([
|
|
||||||
...updatedTags,
|
|
||||||
...((isGlossaryType
|
|
||||||
? tags?.[TagSource.Classification]
|
|
||||||
: tags?.[TagSource.Glossary]) ?? []),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
form.resetFields();
|
|
||||||
setIsEditTags(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = useCallback(() => {
|
|
||||||
setIsEditTags(false);
|
|
||||||
form.resetFields();
|
|
||||||
}, [form]);
|
|
||||||
|
|
||||||
const handleAddClick = useCallback(() => {
|
|
||||||
if (isGlossaryType) {
|
|
||||||
fetchGlossaryList();
|
|
||||||
} else {
|
|
||||||
fetchTags();
|
|
||||||
}
|
|
||||||
setIsEditTags(true);
|
|
||||||
}, [isGlossaryType, fetchGlossaryList, fetchTags]);
|
|
||||||
|
|
||||||
const addTagButton = useMemo(
|
|
||||||
() =>
|
|
||||||
showAddTagButton ? (
|
|
||||||
<span onClick={handleAddClick}>
|
|
||||||
<Tags
|
|
||||||
className="tw-font-semibold tw-text-primary"
|
|
||||||
startWith={TAG_START_WITH.PLUS}
|
|
||||||
tag={TAG_CONSTANT}
|
|
||||||
type="border"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : null,
|
|
||||||
[showAddTagButton, fetchTags, fetchGlossaryList]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderTags = useMemo(
|
|
||||||
() => (
|
|
||||||
<TagsViewer
|
|
||||||
isTextPlaceholder
|
|
||||||
showNoDataPlaceholder={showNoDataPlaceholder}
|
|
||||||
tags={tags?.[tagType] ?? []}
|
|
||||||
type="border"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[showNoDataPlaceholder, tags?.[tagType]]
|
|
||||||
);
|
|
||||||
|
|
||||||
const tagsSelectContainer = useMemo(() => {
|
|
||||||
return tagDetails.isLoading || glossaryDetails.isLoading ? (
|
|
||||||
<Loader size="small" />
|
|
||||||
) : (
|
|
||||||
<TagTree
|
|
||||||
defaultValue={selectedTagsInternal ?? []}
|
|
||||||
placeholder={getTagPlaceholder(isGlossaryType)}
|
|
||||||
treeData={treeData}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
onSubmit={handleSave}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}, [
|
|
||||||
isGlossaryType,
|
|
||||||
selectedTagsInternal,
|
|
||||||
glossaryDetails,
|
|
||||||
tagDetails,
|
|
||||||
treeData,
|
|
||||||
handleCancel,
|
|
||||||
handleSave,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleRequestTags = () => {
|
|
||||||
history.push(getRequestTagsPath(entityType as string, entityFqn as string));
|
|
||||||
};
|
|
||||||
const handleUpdateTags = () => {
|
|
||||||
history.push(getUpdateTagsPath(entityType as string, entityFqn as string));
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestTagElement = useMemo(() => {
|
|
||||||
const hasTags = !isEmpty(tags?.[tagType]);
|
|
||||||
|
|
||||||
return TASK_ENTITIES.includes(entityType as EntityType) ? (
|
|
||||||
<Col>
|
|
||||||
<Button
|
|
||||||
className="p-0 flex-center"
|
|
||||||
data-testid="request-entity-tags"
|
|
||||||
size="small"
|
|
||||||
type="text"
|
|
||||||
onClick={hasTags ? handleUpdateTags : handleRequestTags}>
|
|
||||||
<Tooltip
|
|
||||||
placement="left"
|
|
||||||
title={
|
|
||||||
hasTags
|
|
||||||
? t('label.update-request-tag-plural')
|
|
||||||
: t('label.request-tag-plural')
|
|
||||||
}>
|
|
||||||
<IconRequest
|
|
||||||
className="anticon"
|
|
||||||
height={14}
|
|
||||||
name="request-tags"
|
|
||||||
style={{ color: DE_ACTIVE_COLOR }}
|
|
||||||
width={14}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
) : null;
|
|
||||||
}, [tags?.[tagType], handleUpdateTags, handleRequestTags]);
|
|
||||||
|
|
||||||
const conversationThreadElement = useMemo(
|
|
||||||
() => (
|
|
||||||
<Col>
|
|
||||||
<Button
|
|
||||||
className="p-0 flex-center"
|
|
||||||
data-testid="tag-thread"
|
|
||||||
size="small"
|
|
||||||
type="text"
|
|
||||||
onClick={() => {
|
|
||||||
if (onThreadLinkSelect) {
|
|
||||||
onThreadLinkSelect(
|
|
||||||
entityThreadLink ??
|
|
||||||
getEntityFeedLink(entityType, entityFqn, 'tags')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<Tooltip
|
|
||||||
placement="left"
|
|
||||||
title={t('label.list-entity', {
|
|
||||||
entity: t('label.conversation'),
|
|
||||||
})}>
|
|
||||||
<IconComments
|
|
||||||
height={14}
|
|
||||||
name="comments"
|
|
||||||
style={{ color: DE_ACTIVE_COLOR }}
|
|
||||||
width={14}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
),
|
|
||||||
[
|
|
||||||
entityType,
|
|
||||||
entityFqn,
|
|
||||||
entityThreadLink,
|
|
||||||
getEntityFeedLink,
|
|
||||||
onThreadLinkSelect,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTags(getFilterTags(selectedTags));
|
|
||||||
}, [selectedTags]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-testid={isGlossaryType ? 'glossary-container' : 'tags-container'}>
|
|
||||||
<div className="d-flex justify-between m-b-xs">
|
|
||||||
<div className="d-flex items-center">
|
|
||||||
<Typography.Text className="right-panel-label">
|
|
||||||
{isGlossaryType ? t('label.glossary-term') : t('label.tag-plural')}
|
|
||||||
</Typography.Text>
|
|
||||||
{permission && !isEmpty(tags?.[tagType]) && (
|
|
||||||
<Button
|
|
||||||
className="cursor-pointer flex-center m-l-xss"
|
|
||||||
data-testid="edit-button"
|
|
||||||
icon={<EditIcon color={DE_ACTIVE_COLOR} width="14px" />}
|
|
||||||
size="small"
|
|
||||||
type="text"
|
|
||||||
onClick={handleAddClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{permission && !isVersionView && (
|
|
||||||
<Row gutter={8}>
|
|
||||||
{tagType === TagSource.Classification && requestTagElement}
|
|
||||||
{conversationThreadElement}
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isEditTags && (
|
|
||||||
<Space wrap align="center" data-testid="entity-tags" size={4}>
|
|
||||||
{addTagButton}
|
|
||||||
{renderTags}
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
{isEditTags && tagsSelectContainer}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TagsContainerV1;
|
|
@ -18,6 +18,7 @@ import { ReactElement } from 'react';
|
|||||||
|
|
||||||
export type TagsContainerV2Props = {
|
export type TagsContainerV2Props = {
|
||||||
permission: boolean;
|
permission: boolean;
|
||||||
|
isVersionView?: boolean;
|
||||||
selectedTags: EntityTags[];
|
selectedTags: EntityTags[];
|
||||||
entityType?: string;
|
entityType?: string;
|
||||||
entityThreadLink?: string;
|
entityThreadLink?: string;
|
||||||
@ -27,6 +28,6 @@ export type TagsContainerV2Props = {
|
|||||||
showBottomEditButton?: boolean;
|
showBottomEditButton?: boolean;
|
||||||
showInlineEditButton?: boolean;
|
showInlineEditButton?: boolean;
|
||||||
children?: ReactElement;
|
children?: ReactElement;
|
||||||
onSelectionChange: (selectedTags: EntityTags[]) => Promise<void>;
|
onSelectionChange?: (selectedTags: EntityTags[]) => Promise<void>;
|
||||||
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
|
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
|
||||||
};
|
};
|
||||||
|
@ -44,6 +44,7 @@ import { TagsContainerV2Props } from './TagsContainerV2.interface';
|
|||||||
|
|
||||||
const TagsContainerV2 = ({
|
const TagsContainerV2 = ({
|
||||||
permission,
|
permission,
|
||||||
|
isVersionView,
|
||||||
selectedTags,
|
selectedTags,
|
||||||
entityType,
|
entityType,
|
||||||
entityThreadLink,
|
entityThreadLink,
|
||||||
@ -134,12 +135,15 @@ const TagsContainerV2 = ({
|
|||||||
source: tagType,
|
source: tagType,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (onSelectionChange) {
|
||||||
await onSelectionChange([
|
await onSelectionChange([
|
||||||
...updatedTags,
|
...updatedTags,
|
||||||
...((isGlossaryType
|
...((isGlossaryType
|
||||||
? tags?.[TagSource.Classification]
|
? tags?.[TagSource.Classification]
|
||||||
: tags?.[TagSource.Glossary]) ?? []),
|
: tags?.[TagSource.Glossary]) ?? []),
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
setIsEditTags(false);
|
setIsEditTags(false);
|
||||||
};
|
};
|
||||||
@ -166,7 +170,6 @@ const TagsContainerV2 = ({
|
|||||||
const renderTags = useMemo(
|
const renderTags = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<TagsViewer
|
<TagsViewer
|
||||||
isTextPlaceholder
|
|
||||||
showNoDataPlaceholder={showNoDataPlaceholder}
|
showNoDataPlaceholder={showNoDataPlaceholder}
|
||||||
tags={tags?.[tagType] ?? []}
|
tags={tags?.[tagType] ?? []}
|
||||||
type="border"
|
type="border"
|
||||||
@ -289,10 +292,14 @@ const TagsContainerV2 = ({
|
|||||||
onClick={handleAddClick}
|
onClick={handleAddClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{permission && !isVersionView && (
|
||||||
|
<Row gutter={8}>
|
||||||
{tagType === TagSource.Classification && requestTagElement}
|
{tagType === TagSource.Classification && requestTagElement}
|
||||||
{onThreadLinkSelect && conversationThreadElement}
|
{onThreadLinkSelect && conversationThreadElement}
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -302,6 +309,7 @@ const TagsContainerV2 = ({
|
|||||||
showHeader,
|
showHeader,
|
||||||
isEditTags,
|
isEditTags,
|
||||||
permission,
|
permission,
|
||||||
|
isVersionView,
|
||||||
isGlossaryType,
|
isGlossaryType,
|
||||||
requestTagElement,
|
requestTagElement,
|
||||||
conversationThreadElement,
|
conversationThreadElement,
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
|
||||||
import { Button, Col, Form, Row, Space, TreeSelect } from 'antd';
|
|
||||||
import { useForm } from 'antd/lib/form/Form';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import React from 'react';
|
|
||||||
import { TagsTreeComponentProps } from '../TagsContainerV1/TagsContainerV1.interface';
|
|
||||||
|
|
||||||
const TagTree = ({
|
|
||||||
defaultValue,
|
|
||||||
placeholder,
|
|
||||||
treeData,
|
|
||||||
onSubmit,
|
|
||||||
onCancel,
|
|
||||||
}: TagsTreeComponentProps) => {
|
|
||||||
const [form] = useForm();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form form={form} name="tagsForm" onFinish={(data) => onSubmit(data.tags)}>
|
|
||||||
<Row gutter={[0, 8]}>
|
|
||||||
<Col className="gutter-row d-flex justify-end" span={24}>
|
|
||||||
<Space align="center">
|
|
||||||
<Button
|
|
||||||
className="p-x-05"
|
|
||||||
data-testid="cancelAssociatedTag"
|
|
||||||
icon={<CloseOutlined size={12} />}
|
|
||||||
size="small"
|
|
||||||
onClick={onCancel}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
className="p-x-05"
|
|
||||||
data-testid="saveAssociatedTag"
|
|
||||||
htmlType="submit"
|
|
||||||
icon={<CheckOutlined size={12} />}
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
<Col className="gutter-row" span={24}>
|
|
||||||
<Form.Item noStyle name="tags">
|
|
||||||
<TreeSelect
|
|
||||||
autoFocus
|
|
||||||
multiple
|
|
||||||
showSearch
|
|
||||||
treeDefaultExpandAll
|
|
||||||
treeLine
|
|
||||||
className={classNames('w-full')}
|
|
||||||
data-testid="tag-selector"
|
|
||||||
defaultValue={defaultValue}
|
|
||||||
placeholder={placeholder}
|
|
||||||
removeIcon={
|
|
||||||
<CloseOutlined data-testid="remove-tags" height={8} width={8} />
|
|
||||||
}
|
|
||||||
showCheckedStrategy={TreeSelect.SHOW_ALL}
|
|
||||||
treeData={treeData}
|
|
||||||
treeNodeFilterProp="title"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TagTree;
|
|
@ -17,6 +17,5 @@ export interface TagsViewerProps {
|
|||||||
tags: Array<EntityTags>;
|
tags: Array<EntityTags>;
|
||||||
sizeCap?: number;
|
sizeCap?: number;
|
||||||
type?: 'label' | 'contained' | 'outlined' | 'border';
|
type?: 'label' | 'contained' | 'outlined' | 'border';
|
||||||
isTextPlaceholder?: boolean;
|
|
||||||
showNoDataPlaceholder?: boolean;
|
showNoDataPlaceholder?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ import { TAG_START_WITH } from 'constants/Tag.constants';
|
|||||||
import { isEmpty, sortBy, uniqBy } from 'lodash';
|
import { isEmpty, sortBy, uniqBy } from 'lodash';
|
||||||
import { EntityTags } from 'Models';
|
import { EntityTags } from 'Models';
|
||||||
import React, { FunctionComponent, useCallback, useMemo } from 'react';
|
import React, { FunctionComponent, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { LIST_SIZE, NO_DATA_PLACEHOLDER } from '../../../constants/constants';
|
import { LIST_SIZE, NO_DATA_PLACEHOLDER } from '../../../constants/constants';
|
||||||
import { TagSource } from '../../../generated/type/tagLabel';
|
import { TagSource } from '../../../generated/type/tagLabel';
|
||||||
import { TagsViewerProps } from './tags-viewer.interface';
|
import { TagsViewerProps } from './tags-viewer.interface';
|
||||||
@ -28,10 +27,8 @@ const TagsViewer: FunctionComponent<TagsViewerProps> = ({
|
|||||||
tags,
|
tags,
|
||||||
sizeCap = LIST_SIZE,
|
sizeCap = LIST_SIZE,
|
||||||
type = 'label',
|
type = 'label',
|
||||||
isTextPlaceholder,
|
|
||||||
showNoDataPlaceholder = true,
|
showNoDataPlaceholder = true,
|
||||||
}: TagsViewerProps) => {
|
}: TagsViewerProps) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const tagChipStye = {
|
const tagChipStye = {
|
||||||
margin: '0 0 8px 0',
|
margin: '0 0 8px 0',
|
||||||
justifyContent: 'start',
|
justifyContent: 'start',
|
||||||
@ -65,11 +62,7 @@ const TagsViewer: FunctionComponent<TagsViewerProps> = ({
|
|||||||
<Space wrap size={4}>
|
<Space wrap size={4}>
|
||||||
{isEmpty(sortedTagsBySource) && showNoDataPlaceholder ? (
|
{isEmpty(sortedTagsBySource) && showNoDataPlaceholder ? (
|
||||||
<Typography.Text className="text-grey-muted m-r-xss">
|
<Typography.Text className="text-grey-muted m-r-xss">
|
||||||
{isTextPlaceholder
|
{NO_DATA_PLACEHOLDER}
|
||||||
? t('label.no-entity', {
|
|
||||||
entity: t('label.tag-plural'),
|
|
||||||
})
|
|
||||||
: NO_DATA_PLACEHOLDER}
|
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
) : sizeCap > -1 ? (
|
) : sizeCap > -1 ? (
|
||||||
<>
|
<>
|
||||||
|
@ -90,8 +90,8 @@ jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => {
|
|||||||
return jest.fn().mockReturnValue(<p>RichTextEditorPreviwer</p>);
|
return jest.fn().mockReturnValue(<p>RichTextEditorPreviwer</p>);
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('components/Tag/TagsContainerV1/TagsContainerV1', () => {
|
jest.mock('components/Tag/TagsContainerV2/TagsContainerV2', () => {
|
||||||
return jest.fn().mockReturnValue(<p>TagsContainerV1</p>);
|
return jest.fn().mockReturnValue(<p>TagsContainerV2</p>);
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('components/Tag/Tags/tags', () => {
|
jest.mock('components/Tag/Tags/tags', () => {
|
||||||
|
@ -19,7 +19,7 @@ import DescriptionV1 from 'components/common/description/DescriptionV1';
|
|||||||
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
|
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
|
||||||
import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader';
|
import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader';
|
||||||
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
|
||||||
import TagsContainerV1 from 'components/Tag/TagsContainerV1/TagsContainerV1';
|
import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
|
||||||
import TopicSchemaFields from 'components/TopicDetails/TopicSchema/TopicSchema';
|
import TopicSchemaFields from 'components/TopicDetails/TopicSchema/TopicSchema';
|
||||||
import { getVersionPathWithTab } from 'constants/constants';
|
import { getVersionPathWithTab } from 'constants/constants';
|
||||||
import { EntityField } from 'constants/Feeds.constants';
|
import { EntityField } from 'constants/Feeds.constants';
|
||||||
@ -142,9 +142,8 @@ const TopicVersion: FC<TopicVersionProp> = ({
|
|||||||
flex="220px">
|
flex="220px">
|
||||||
<Space className="w-full" direction="vertical" size="large">
|
<Space className="w-full" direction="vertical" size="large">
|
||||||
{Object.keys(TagSource).map((tagType) => (
|
{Object.keys(TagSource).map((tagType) => (
|
||||||
<TagsContainerV1
|
<TagsContainerV2
|
||||||
isVersionView
|
isVersionView
|
||||||
showLimited
|
|
||||||
entityFqn={currentVersionData.fullyQualifiedName}
|
entityFqn={currentVersionData.fullyQualifiedName}
|
||||||
entityType={EntityType.TOPIC}
|
entityType={EntityType.TOPIC}
|
||||||
key={tagType}
|
key={tagType}
|
||||||
|
@ -60,10 +60,10 @@ jest.mock('components/common/description/Description', () => {
|
|||||||
.mockReturnValue(<div data-testid="description">Description</div>);
|
.mockReturnValue(<div data-testid="description">Description</div>);
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('components/Tag/TagsContainerV1/TagsContainerV1', () => {
|
jest.mock('components/Tag/TagsContainerV2/TagsContainerV2', () => {
|
||||||
return jest
|
return jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue(<div data-testid="entity-page-info">TagsContainerV1</div>);
|
.mockReturnValue(<div data-testid="entity-page-info">TagsContainerV2</div>);
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('components/FeedEditor/FeedEditor', () => {
|
jest.mock('components/FeedEditor/FeedEditor', () => {
|
||||||
|
@ -13,18 +13,8 @@
|
|||||||
|
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { ModifiedGlossaryTerm } from 'components/Glossary/GlossaryTermTab/GlossaryTermTab.interface';
|
import { ModifiedGlossaryTerm } from 'components/Glossary/GlossaryTermTab/GlossaryTermTab.interface';
|
||||||
import {
|
|
||||||
GlossaryTermDetailsProps,
|
|
||||||
HierarchyTagsProps,
|
|
||||||
} from 'components/Tag/TagsContainerV1/TagsContainerV1.interface';
|
|
||||||
import { API_RES_MAX_SIZE, PAGE_SIZE_LARGE } from 'constants/constants';
|
|
||||||
import { TagSource } from 'generated/type/tagLabel';
|
|
||||||
import { isUndefined, omit } from 'lodash';
|
import { isUndefined, omit } from 'lodash';
|
||||||
import {
|
import { ListGlossaryTermsParams } from 'rest/glossaryAPI';
|
||||||
getGlossariesList,
|
|
||||||
getGlossaryTerms,
|
|
||||||
ListGlossaryTermsParams,
|
|
||||||
} from 'rest/glossaryAPI';
|
|
||||||
import { searchData } from 'rest/miscAPI';
|
import { searchData } from 'rest/miscAPI';
|
||||||
import { WILD_CARD_CHAR } from '../constants/char.constants';
|
import { WILD_CARD_CHAR } from '../constants/char.constants';
|
||||||
import { SearchIndex } from '../enums/search.enum';
|
import { SearchIndex } from '../enums/search.enum';
|
||||||
@ -237,86 +227,3 @@ export const formatRelatedTermOptions = (
|
|||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGlossaryTermHierarchy = (
|
|
||||||
data: GlossaryTermDetailsProps[]
|
|
||||||
): HierarchyTagsProps[] => {
|
|
||||||
const nodes: Record<string, HierarchyTagsProps> = {};
|
|
||||||
const tree: HierarchyTagsProps[] = [];
|
|
||||||
|
|
||||||
data.forEach((obj) => {
|
|
||||||
if (obj.fqn) {
|
|
||||||
nodes[obj.fqn] = {
|
|
||||||
title: obj.name,
|
|
||||||
value: obj.fqn,
|
|
||||||
key: obj.fqn,
|
|
||||||
selectable: true,
|
|
||||||
children: [],
|
|
||||||
};
|
|
||||||
const parentNode =
|
|
||||||
obj.parent &&
|
|
||||||
obj.parent.fullyQualifiedName &&
|
|
||||||
nodes[obj.parent.fullyQualifiedName];
|
|
||||||
parentNode && nodes[obj.fqn] && parentNode.children?.push(nodes[obj.fqn]);
|
|
||||||
|
|
||||||
if (!parentNode) {
|
|
||||||
const glossaryName = obj.glossary.name ?? '';
|
|
||||||
const existInTree = tree.find((item) => item.title === glossaryName);
|
|
||||||
|
|
||||||
if (existInTree) {
|
|
||||||
nodes[glossaryName].children?.push(nodes[obj.fqn]);
|
|
||||||
} else {
|
|
||||||
nodes[glossaryName] = {
|
|
||||||
title: glossaryName,
|
|
||||||
value: obj.glossary.fullyQualifiedName ?? '',
|
|
||||||
key: obj.glossary.fullyQualifiedName ?? '',
|
|
||||||
selectable: false,
|
|
||||||
children: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
nodes[glossaryName].children?.push(nodes[obj.fqn]);
|
|
||||||
tree.push(nodes[glossaryName]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return tree;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGlossaryTermsList = async () => {
|
|
||||||
try {
|
|
||||||
const glossaryTermList: GlossaryTermDetailsProps[] = [];
|
|
||||||
const { data } = await getGlossariesList({
|
|
||||||
limit: PAGE_SIZE_LARGE,
|
|
||||||
});
|
|
||||||
|
|
||||||
const promises = data.map((item) =>
|
|
||||||
getGlossaryTerms({
|
|
||||||
glossary: item.id,
|
|
||||||
limit: API_RES_MAX_SIZE,
|
|
||||||
fields: 'children,parent',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const response = await Promise.allSettled(promises);
|
|
||||||
|
|
||||||
response.forEach((res) => {
|
|
||||||
if (res.status === 'fulfilled') {
|
|
||||||
glossaryTermList.push(
|
|
||||||
...res.value.data.map((data) => ({
|
|
||||||
name: data.name,
|
|
||||||
fqn: data.fullyQualifiedName ?? '',
|
|
||||||
children: data.children,
|
|
||||||
parent: data.parent,
|
|
||||||
glossary: data.glossary,
|
|
||||||
source: TagSource.Glossary,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.resolve(glossaryTermList);
|
|
||||||
} catch (error) {
|
|
||||||
return Promise.reject({ data: (error as AxiosError).response });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
@ -18,10 +18,6 @@ import { ReactComponent as DeleteIcon } from 'assets/svg/ic-delete.svg';
|
|||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
|
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
|
||||||
import Loader from 'components/Loader/Loader';
|
import Loader from 'components/Loader/Loader';
|
||||||
import {
|
|
||||||
HierarchyTagsProps,
|
|
||||||
TagsDetailsProps,
|
|
||||||
} from 'components/Tag/TagsContainerV1/TagsContainerV1.interface';
|
|
||||||
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
|
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
|
||||||
import { getExplorePath, PAGE_SIZE } from 'constants/constants';
|
import { getExplorePath, PAGE_SIZE } from 'constants/constants';
|
||||||
import { delimiterRegex } from 'constants/regex.constants';
|
import { delimiterRegex } from 'constants/regex.constants';
|
||||||
@ -329,77 +325,6 @@ export const getUsageCountLink = (tagFQN: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTagsHierarchy = (
|
|
||||||
tags: TagsDetailsProps[]
|
|
||||||
): HierarchyTagsProps[] => {
|
|
||||||
const filteredTags = tags.filter(
|
|
||||||
(tag) => !tag.fqn?.startsWith(`Tier${FQN_SEPARATOR_CHAR}Tier`)
|
|
||||||
);
|
|
||||||
|
|
||||||
let hierarchyTags: HierarchyTagsProps[] = [];
|
|
||||||
|
|
||||||
filteredTags.forEach((tags) => {
|
|
||||||
const haveParent = hierarchyTags.find(
|
|
||||||
(h) => h.title === tags?.classification?.name
|
|
||||||
);
|
|
||||||
|
|
||||||
if (haveParent) {
|
|
||||||
hierarchyTags = hierarchyTags.map((h) => {
|
|
||||||
if (h.title === tags?.classification?.name) {
|
|
||||||
return {
|
|
||||||
...h,
|
|
||||||
children: [
|
|
||||||
...h.children,
|
|
||||||
{
|
|
||||||
title: tags.name,
|
|
||||||
value: tags.fqn,
|
|
||||||
key: tags.fqn,
|
|
||||||
selectable: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return h;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
hierarchyTags.push({
|
|
||||||
title: tags.classification?.name ?? '',
|
|
||||||
value: tags.classification?.name ?? '',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
title: tags.name,
|
|
||||||
value: tags.fqn,
|
|
||||||
key: tags.fqn,
|
|
||||||
selectable: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
key: tags.classification?.name ?? '',
|
|
||||||
selectable: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return hierarchyTags;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAllTagsList = async () => {
|
|
||||||
try {
|
|
||||||
const tags = await getAllTagsForOptions();
|
|
||||||
|
|
||||||
return Promise.resolve(
|
|
||||||
tags.map((tag) => ({
|
|
||||||
name: tag.name,
|
|
||||||
fqn: tag.fullyQualifiedName ?? '',
|
|
||||||
classification: tag.classification,
|
|
||||||
source: TagSource.Classification,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
return Promise.reject({ data: (error as AxiosError).response });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTagPlaceholder = (isGlossaryType: boolean): string =>
|
export const getTagPlaceholder = (isGlossaryType: boolean): string =>
|
||||||
isGlossaryType
|
isGlossaryType
|
||||||
? i18next.t('label.search-entity', {
|
? i18next.t('label.search-entity', {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user