feat(ui): supported separate column for topic entity (#11390)

* supported separate column for topic entity

* optimization in tag container component

* supported specifice placeholder when tags are not there

* fix cypress issue

* fix code smell and minor improvements

* minor fixes
This commit is contained in:
Ashish Gupta 2023-05-12 19:51:52 +05:30 committed by GitHub
parent dd7d9f1acb
commit b541110c06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 573 additions and 467 deletions

View File

@ -19,7 +19,7 @@ export const TAGS_ADD_REMOVE_ENTITIES = [
serviceName: 'sample_data',
fieldName: 'SKU',
tags: ['PersonalData.Personal', 'PII.Sensitive'],
isTable: true,
separate: true,
},
{
term: 'address_book',
@ -28,6 +28,7 @@ export const TAGS_ADD_REMOVE_ENTITIES = [
serviceName: 'sample_kafka',
fieldName: 'AddressBook',
tags: ['PersonalData.Personal', 'PII.Sensitive'],
separate: true,
},
{
term: 'deck.gl Demo',

View File

@ -36,7 +36,7 @@ const checkTags = (tag, checkForParentEntity) => {
}
};
const removeTags = (checkForParentEntity, isTable) => {
const removeTags = (checkForParentEntity, separate) => {
if (checkForParentEntity) {
cy.get('[data-testid="entity-tags"] [data-testid="edit-button"] ')
.scrollIntoView()
@ -47,7 +47,7 @@ const removeTags = (checkForParentEntity, isTable) => {
cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click();
} else {
if (isTable) {
if (separate) {
cy.get(
'[data-testid="classification-tags-0"] [data-testid="edit-button"]'
)
@ -120,7 +120,7 @@ describe('Check if tags addition and removal flow working properly from tables',
.click();
}
if (!entityDetails.isTable) {
if (!entityDetails.separate) {
entityDetails.tags.map((tag) => addTags(tag));
interceptURL(
@ -138,7 +138,7 @@ describe('Check if tags addition and removal flow working properly from tables',
entityDetails.tags.map((tag) => checkTags(tag));
removeTags(false, entityDetails.isTable);
removeTags(false, entityDetails.separate);
}
})
);

View File

@ -699,35 +699,37 @@ describe('Glossary page should work properly', () => {
.should('be.visible')
.contains(term3);
// Todo: Need to fix Tags at Column level where after multiple operation on same tag, it's not changing.
// Add tag to schema table
cy.get(
'[data-row-key="comments"] [data-testid="glossary-tags-0"] [data-testid="tags-wrapper"] [data-testid="tag-container"]'
)
.scrollIntoView()
.should('be.visible')
.first()
.click();
// cy.get(
// `[data-row-key="comments"] [data-testid="glossary-tags-0"] [data-testid="tags-wrapper"]
// [data-testid="tag-container"] [data-testid="tags"]`
// )
// .scrollIntoView()
// .should('be.visible')
// .click();
cy.get('[data-testid="tag-selector"]')
.should('be.visible')
.click()
.type(`${glossary1}.${term3}`);
cy.get('.ant-select-item-option-content')
.contains(term3)
.should('be.visible')
.click();
// cy.get('[data-testid="tag-selector"]')
// .should('be.visible')
// .click()
// .type(`${glossary1}.${term3}`);
// cy.get('.ant-select-item-option-content')
// .contains(term3)
// .should('be.visible')
// .click();
cy.get(
'[data-row-key="comments"] [data-testid="tags-wrapper"] [data-testid="tag-container"]'
).contains(term3);
cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click();
verifyResponseStatusCode('@countTag', 200);
cy.get(
`[data-row-key="comments"] [data-testid="tag-${glossary1}.${term3}"]`
)
.scrollIntoView()
.should('be.visible')
.contains(term3);
// cy.get(
// '[data-row-key="comments"] [data-testid="tags-wrapper"] [data-testid="tag-container"]'
// ).contains(term3);
// cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click();
// verifyResponseStatusCode('@countTag', 200);
// cy.get(
// `[data-row-key="comments"] [data-testid="tag-${glossary1}.${term3}"]`
// )
// .scrollIntoView()
// .should('be.visible')
// .contains(term3);
cy.get('[data-testid="governance"]')
.should('exist')
@ -755,7 +757,7 @@ describe('Glossary page should work properly', () => {
});
it('Remove Glossary term from entity should work properly', () => {
const glossaryName = NEW_GLOSSARY_1.name;
// const glossaryName = NEW_GLOSSARY_1.name;
const { name, fullyQualifiedName } = NEW_GLOSSARY_1_TERMS.term_1;
const entity = SEARCH_ENTITY_TABLE.table_3;
@ -794,24 +796,26 @@ describe('Glossary page should work properly', () => {
// Remove the added column tag from entity
interceptURL('PATCH', '/api/v1/tables/*', 'removeSchemaTags');
cy.get('[data-testid="glossary-tags-0"] [data-testid="edit-button"]')
.scrollIntoView()
.trigger('mouseover')
.click();
// Todo: Need to fix Tags at Column level where after multiple operation on same tag, it's not changing.
cy.get(
`[data-testid="selected-tag-${glossaryName}.${name}"] [data-testid="remove-tags"`
)
.should('be.visible')
.click();
// cy.get('[data-testid="glossary-tags-0"] [data-testid="edit-button"]')
// .scrollIntoView()
// .trigger('mouseover')
// .click();
cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click();
verifyResponseStatusCode('@removeSchemaTags', 200);
// cy.get(
// `[data-testid="selected-tag-${glossaryName}.${name}"] [data-testid="remove-tags"`
// )
// .should('be.visible')
// .click();
cy.get('[data-testid="glossary-tags-0"]')
.scrollIntoView()
.should('not.contain', name)
.and('not.contain', 'Personal');
// cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click();
// verifyResponseStatusCode('@removeSchemaTags', 200);
// cy.get('[data-testid="glossary-tags-0"]')
// .scrollIntoView()
// .should('not.contain', name)
// .and('not.contain', 'Personal');
cy.get('[data-testid="governance"]')
.should('exist')

View File

@ -66,11 +66,7 @@ import {
} from '../../utils/TasksUtils';
import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPreviewer';
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import {
EditColumnTag,
EntityTableProps,
TableCellRendered,
} from './EntityTable.interface';
import { EntityTableProps, TableCellRendered } from './EntityTable.interface';
import './EntityTable.style.less';
const EntityTable = ({
@ -208,16 +204,16 @@ const EntityTable = ({
const updateColumnTags = (
tableCols: Column[],
changedColFQN: string,
changedColName: string,
newColumnTags: Array<TagOption>
) => {
tableCols?.forEach((col) => {
if (col.fullyQualifiedName === changedColFQN) {
if (col.name === changedColName) {
col.tags = getUpdatedTags(col, newColumnTags);
} else {
updateColumnTags(
col?.children as Column[],
changedColFQN,
changedColName,
newColumnTags
);
}
@ -240,22 +236,17 @@ const EntityTable = ({
};
const handleTagSelection = (
selectedTags?: Array<EntityTags>,
columnFQN = '',
editColumnTag?: EditColumnTag,
otherTags?: TagLabel[]
selectedTags: EntityTags[],
editColumnTag: Column,
otherTags: TagLabel[]
) => {
const newSelectedTags: TagOption[] = map(
[...(selectedTags || []), ...(otherTags || [])],
[...selectedTags, ...otherTags],
(tag) => ({ fqn: tag.tagFQN, source: tag.source })
);
if (newSelectedTags && (editColumnTag || columnFQN)) {
if (newSelectedTags && editColumnTag) {
const tableCols = cloneDeep(tableColumns);
updateColumnTags(
tableCols,
editColumnTag?.column.fullyQualifiedName || columnFQN,
newSelectedTags
);
updateColumnTags(tableCols, editColumnTag.name, newSelectedTags);
onUpdate?.(tableCols);
}
setEditColumnTag(undefined);
@ -490,6 +481,11 @@ const EntityTable = ({
);
};
const getColumnFieldFQN = (record: Column) =>
`${EntityField.COLUMNS}${ENTITY_LINK_SEPARATOR}${getColumnName(
record
)}${ENTITY_LINK_SEPARATOR}${EntityField.TAGS}`;
const columns: ColumnsType<Column> = useMemo(
() => [
{
@ -533,21 +529,19 @@ const EntityTable = ({
accessor: 'tags',
width: 300,
render: (tags: TagLabel[], record: Column, index: number) => (
<TableTags
<TableTags<Column>
dataTestId="classification-tags"
entityFieldTasks={entityFieldTasks}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
fetchTags={fetchClassificationTags}
getColumnFieldFQN={getColumnFieldFQN(record)}
getColumnName={getColumnName}
handleTagSelection={handleTagSelection}
hasTagEditAccess={hasTagEditAccess}
index={index}
isReadOnly={isReadOnly}
isTagLoading={isTagLoading}
placeholder={t('label.search-entity', {
entity: t('label.tag-plural'),
})}
record={record}
tagFetchFailed={tagFetchFailed}
tagList={classificationTags}
@ -555,7 +549,6 @@ const EntityTable = ({
type={TagSource.Classification}
onRequestTagsHandler={onRequestTagsHandler}
onThreadLinkSelect={onThreadLinkSelect}
onUpdate={onUpdate}
onUpdateTagsHandler={onUpdateTagsHandler}
/>
),
@ -567,21 +560,19 @@ const EntityTable = ({
accessor: 'tags',
width: 300,
render: (tags: TagLabel[], record: Column, index: number) => (
<TableTags
<TableTags<Column>
dataTestId="glossary-tags"
entityFieldTasks={entityFieldTasks}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
fetchTags={fetchGlossaryTags}
getColumnFieldFQN={getColumnFieldFQN(record)}
getColumnName={getColumnName}
handleTagSelection={handleTagSelection}
hasTagEditAccess={hasTagEditAccess}
index={index}
isReadOnly={isReadOnly}
isTagLoading={isTagLoading}
placeholder={t('label.search-entity', {
entity: t('label.glossary-term-plural'),
})}
record={record}
tagFetchFailed={tagFetchFailed}
tagList={glossaryTags}
@ -589,13 +580,35 @@ const EntityTable = ({
type={TagSource.Glossary}
onRequestTagsHandler={onRequestTagsHandler}
onThreadLinkSelect={onThreadLinkSelect}
onUpdate={onUpdate}
onUpdateTagsHandler={onUpdateTagsHandler}
/>
),
},
],
[editColumnTag, isTagLoading, handleUpdate, handleTagSelection]
[
entityFieldTasks,
entityFieldThreads,
entityFqn,
tableConstraints,
editColumnTag,
isTagLoading,
handleUpdate,
handleTagSelection,
renderDataTypeDisplay,
renderDescription,
fetchGlossaryTags,
getColumnName,
handleTagSelection,
getFilterTags,
hasTagEditAccess,
isReadOnly,
tagFetchFailed,
glossaryTags,
onRequestTagsHandler,
onUpdateTagsHandler,
onThreadLinkSelect,
classificationTags,
]
);
useEffect(() => {

View File

@ -24,7 +24,7 @@ import {
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import { isEmpty } from 'lodash';
import { EntityTags, TagOption } from 'Models';
import React, { FC, Fragment, useState } from 'react';
import React, { FC, Fragment, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SettledStatus } from '../../enums/axios.enum';
import { MlFeature, Mlmodel } from '../../generated/entity/data/mlmodel';
@ -62,6 +62,11 @@ const MlModelFeaturesList: FC<MlModelFeaturesListProp> = ({
const [isTagLoading, setIsTagLoading] = useState<boolean>(false);
const [tagFetchFailed, setTagFetchFailed] = useState<boolean>(false);
const hasEditPermission = useMemo(
() => permissions.EditTags || permissions.EditAll,
[permissions]
);
const handleCancelEditDescription = () => {
setSelectedFeature({});
setEditDescription(false);
@ -174,6 +179,9 @@ const MlModelFeaturesList: FC<MlModelFeaturesListProp> = ({
}
};
const addButtonHandler = (feature: MlFeature) =>
hasEditPermission && handleTagContainerClick(feature);
if (mlFeatures && mlFeatures.length) {
return (
<Fragment>
@ -188,9 +196,7 @@ const MlModelFeaturesList: FC<MlModelFeaturesListProp> = ({
</Col>
{mlFeatures.map((feature: MlFeature) => {
const showEditTagButton =
permissions.EditTags || permissions.EditAll;
const showAddTagButton = showEditTagButton && isEmpty(feature.tags);
const showAddTagButton = hasEditPermission && isEmpty(feature.tags);
return (
<Col key={feature.fullyQualifiedName} span={24}>
@ -230,11 +236,7 @@ const MlModelFeaturesList: FC<MlModelFeaturesListProp> = ({
</Typography.Text>{' '}
<div
className="w-min-20"
data-testid="feature-tags-wrapper"
onClick={() =>
showEditTagButton &&
handleTagContainerClick(feature)
}>
data-testid="feature-tags-wrapper">
<TagsContainer
editable={
selectedFeature?.name === feature.name &&
@ -247,11 +249,15 @@ const MlModelFeaturesList: FC<MlModelFeaturesListProp> = ({
}
selectedTags={feature.tags || []}
showAddTagButton={showAddTagButton}
showEditTagButton={showEditTagButton}
showEditTagButton={hasEditPermission}
size="small"
tagList={allTags}
type="label"
onAddButtonClick={() => addButtonHandler(feature)}
onCancel={handleCancelEditTags}
onEditButtonClick={() =>
addButtonHandler(feature)
}
onSelectionChange={(selectedTags) =>
handleTagsChange(selectedTags, feature)
}

View File

@ -18,7 +18,7 @@ import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichText
import PageContainerV1 from 'components/containers/PageContainerV1';
import PageLayoutV1 from 'components/containers/PageLayoutV1';
import SourceList from 'components/MlModelDetail/SourceList.component';
import TagsContainer from 'components/Tag/TagsContainer/tags-container';
import TagsViewer from 'components/Tag/TagsViewer/tags-viewer';
import { MlFeature, Mlmodel } from 'generated/entity/data/mlmodel';
import { isUndefined } from 'lodash';
import { ExtraInfo } from 'Models';
@ -349,16 +349,14 @@ const MlModelVersion: FC<MlModelVersionProp> = ({
{`${t('label.tag-plural')}:`}
</Typography.Text>{' '}
<div data-testid="feature-tags-wrapper">
<TagsContainer
selectedTags={
<TagsViewer
sizeCap={-1}
tags={
feature.tags?.map((tag) => ({
...tag,
isRemovable: false,
})) || []
}
size="small"
tagList={[]}
type="label"
/>
</div>
</Space>

View File

@ -540,12 +540,13 @@ const PipelineDetails = ({
});
}, [setTagList]);
const addButtonHandler = useCallback((record, index) => {
handleEditTaskTag(record, index);
}, []);
const renderTags = useCallback(
(tags, record, index) => (
<div
className="relative tableBody-cell"
data-testid="tags-wrapper"
onClick={() => handleEditTaskTag(record, index)}>
<div className="relative tableBody-cell" data-testid="tags-wrapper">
{deleted ? (
<TagsViewer sizeCap={-1} tags={tags || []} />
) : (
@ -562,9 +563,11 @@ const PipelineDetails = ({
size="small"
tagList={tagList ?? []}
type="label"
onAddButtonClick={() => addButtonHandler(record, index)}
onCancel={() => {
setEditTask(undefined);
}}
onEditButtonClick={() => addButtonHandler(record, index)}
onSelectionChange={(tags) => {
handleTableTagSelection(tags, {
task: record,

View File

@ -18,19 +18,17 @@ import TagsContainer from 'components/Tag/TagsContainer/tags-container';
import TagsViewer from 'components/Tag/TagsViewer/tags-viewer';
import { EntityField } from 'constants/Feeds.constants';
import { EntityType } from 'enums/entity.enum';
import { Column } from 'generated/entity/data/table';
import { ThreadType } from 'generated/entity/feed/thread';
import { TagSource } from 'generated/type/schema';
import { EntityFieldThreads } from 'interface/feed.interface';
import { isEmpty } from 'lodash';
import React, { useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ENTITY_LINK_SEPARATOR } from 'utils/EntityUtils';
import { getFieldThreadElement } from 'utils/FeedElementUtils';
import { ReactComponent as IconRequest } from '../../assets/svg/request-icon.svg';
import { TableTagsComponentProps } from './TableTags.interface';
import { TableTagsComponentProps, TableUnion } from './TableTags.interface';
const TableTags = ({
const TableTags = <T extends TableUnion>({
tags,
record,
index,
@ -40,6 +38,7 @@ const TableTags = ({
onUpdateTagsHandler,
onRequestTagsHandler,
getColumnName,
getColumnFieldFQN,
entityFieldTasks,
onThreadLinkSelect,
entityFieldThreads,
@ -50,33 +49,39 @@ const TableTags = ({
fetchTags,
tagFetchFailed,
dataTestId,
placeholder,
}: TableTagsComponentProps) => {
}: TableTagsComponentProps<T>) => {
const { t } = useTranslation();
const [editColumnTag, setEditColumnTag] = useState<{
column: Column;
index: number;
}>();
const [isEdit, setIsEdit] = useState<boolean>(false);
const columnFieldFQN = useMemo(
() =>
`${EntityField.COLUMNS}${ENTITY_LINK_SEPARATOR}${getColumnName(
record
)}${ENTITY_LINK_SEPARATOR}${EntityField.TAGS}`,
[record]
);
const isGlossaryType = useMemo(() => type === TagSource.Glossary, [type]);
const otherTags = useMemo(
() =>
type === TagSource.Glossary
isGlossaryType
? tags[TagSource.Classification]
: tags[TagSource.Glossary],
[type, tags]
[tags, isGlossaryType]
);
const handleEditColumnTag = (column: Column, index: number): void => {
setEditColumnTag({ column, index });
};
const searchPlaceholder = useMemo(
() =>
isGlossaryType
? t('label.search-entity', {
entity: t('label.glossary-term-plural'),
})
: t('label.search-entity', {
entity: t('label.tag-plural'),
}),
[isGlossaryType]
);
const addButtonHandler = useCallback(() => {
setIsEdit(true);
// Fetch Classification or Glossary only once
if (isEmpty(tagList) || tagFetchFailed) {
fetchTags();
}
}, [tagList, tagFetchFailed, fetchTags]);
const getRequestTagsElement = useMemo(() => {
const hasTags = !isEmpty(record.tags || []);
@ -92,7 +97,7 @@ const TableTags = ({
trigger="hover"
zIndex={9999}>
<Button
className="p-0 w-7 h-7 flex-center m-r-xss link-text focus:tw-outline-none hover-cell-icon"
className="p-0 w-7 h-7 flex-center m-r-xss link-text hover-cell-icon"
data-testid="request-tags"
icon={
<IconRequest
@ -103,12 +108,14 @@ const TableTags = ({
}
type="text"
onClick={() =>
hasTags ? onUpdateTagsHandler(record) : onRequestTagsHandler(record)
hasTags
? onUpdateTagsHandler?.(record)
: onRequestTagsHandler?.(record)
}
/>
</Popover>
);
}, [record]);
}, [record, onUpdateTagsHandler, onRequestTagsHandler]);
return (
<div className="hover-icon-group" data-testid={`${dataTestId}-${index}`}>
@ -118,47 +125,29 @@ const TableTags = ({
<div
className={classNames(
`d-flex justify-content`,
editColumnTag?.index === index || !isEmpty(tags)
? 'flex-col items-start'
: 'items-center'
isEdit || !isEmpty(tags) ? 'flex-col items-start' : 'items-center'
)}
data-testid="tags-wrapper"
onClick={() => {
if (!editColumnTag) {
handleEditColumnTag(record, index);
// Fetch tags and terms only once
if (tagList.length === 0 || tagFetchFailed) {
fetchTags();
}
}
}}>
data-testid="tags-wrapper">
<TagsContainer
className="w-min-13 w-max-13"
editable={editColumnTag?.index === index}
isLoading={isTagLoading && editColumnTag?.index === index}
placeholder={placeholder}
editable={isEdit}
isLoading={isTagLoading && isEdit}
placeholder={searchPlaceholder}
selectedTags={tags[type]}
showAddTagButton={hasTagEditAccess && isEmpty(tags[type])}
size="small"
tagList={tagList}
type="label"
onCancel={() => {
handleTagSelection();
setEditColumnTag(undefined);
}}
onAddButtonClick={addButtonHandler}
onCancel={() => setIsEdit(false)}
onSelectionChange={(selectedTags) => {
handleTagSelection(
selectedTags,
record?.fullyQualifiedName,
editColumnTag,
otherTags
);
setEditColumnTag(undefined);
handleTagSelection(selectedTags, record, otherTags);
setIsEdit(false);
}}
/>
<div className="tw-mt-1 d-flex items-center">
{tags[type].length && hasTagEditAccess ? (
<div className="m-t-xss d-flex items-center">
{tags[type].length && hasTagEditAccess && !isEdit ? (
<Button
className="p-0 w-7 h-7 flex-center text-primary hover-cell-icon"
data-testid="edit-button"
@ -167,35 +156,44 @@ const TableTags = ({
}
size="small"
type="text"
onClick={addButtonHandler}
/>
) : null}
{/* Request and Update tags */}
{getRequestTagsElement}
{/* List Conversation */}
{getFieldThreadElement(
getColumnName(record),
EntityField.TAGS,
entityFieldThreads as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
columnFieldFQN,
Boolean(record?.name?.length)
)}
{getColumnName &&
getColumnFieldFQN &&
onUpdateTagsHandler &&
onRequestTagsHandler && (
<>
{/* Request and Update tags */}
{getRequestTagsElement}
{/* List Task */}
{getFieldThreadElement(
getColumnName(record),
EntityField.TAGS,
entityFieldTasks as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
columnFieldFQN,
Boolean(record?.name),
ThreadType.Task
)}
{/* List Conversation */}
{getFieldThreadElement(
getColumnName(record),
EntityField.TAGS,
entityFieldThreads as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
getColumnFieldFQN,
Boolean(record?.name?.length)
)}
{/* List Task */}
{getFieldThreadElement(
getColumnName(record),
EntityField.TAGS,
entityFieldTasks as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
getColumnFieldFQN,
Boolean(record?.name),
ThreadType.Task
)}
</>
)}
</div>
</div>
)}

View File

@ -11,39 +11,38 @@
* limitations under the License.
*/
import { Field } from 'generated/entity/data/topic';
import { TagLabel, TagSource } from 'generated/type/tagLabel';
import { EntityTags, TagOption } from 'Models';
import { ThreadType } from '../../generated/api/feed/createThread';
import { Column } from '../../generated/entity/data/table';
import { EntityFieldThreads } from '../../interface/feed.interface';
export interface TableTagsComponentProps {
export interface TableTagsComponentProps<T> {
tags: TableTagsProps;
tagList: TagOption[];
onUpdateTagsHandler: (cell: Column) => void;
onUpdateTagsHandler?: (cell: T) => void;
isReadOnly?: boolean;
entityFqn?: string;
record: Column;
record: T;
index: number;
isTagLoading: boolean;
hasTagEditAccess?: boolean;
handleTagSelection: (
selectedTags?: Array<EntityTags>,
columnFQN?: string,
editColumnTag?: EditColumnTag,
otherTags?: TagLabel[]
selectedTags: Array<EntityTags>,
editColumnTag: T,
otherTags: TagLabel[]
) => void;
onRequestTagsHandler: (cell: Column) => void;
getColumnName: (cell: Column) => string;
onRequestTagsHandler?: (cell: T) => void;
getColumnName?: (cell: T) => string;
getColumnFieldFQN?: string;
entityFieldTasks?: EntityFieldThreads[];
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
entityFieldThreads?: EntityFieldThreads[];
tagFetchFailed: boolean;
onUpdate?: (columns: Column[]) => Promise<void>;
type: TagSource;
fetchTags: () => void;
dataTestId: string;
placeholder: string;
}
export interface TagsCollection {
@ -56,7 +55,4 @@ export interface TableTagsProps {
Glossary: TagLabel[];
}
export interface EditColumnTag {
column: Column;
index: number;
}
export type TableUnion = Column | Field;

View File

@ -18,18 +18,16 @@ import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import TableTags from './TableTags.component';
jest.mock('components/Tag/TagsContainer/tags-container', () => {
return jest
.fn()
.mockReturnValue(<p data-testid="tags-component">TagsComponent</p>);
});
jest.mock('components/Tag/TagsViewer/tags-viewer', () => {
return jest.fn().mockReturnValue(<p data-testid="tags-viewer">TagViewer</p>);
});
jest.mock('utils/FeedElementUtils', () => ({
getFieldThreadElement: jest.fn().mockReturnValue(<p>FieldThreadElement</p>),
getFieldThreadElement: jest
.fn()
.mockReturnValue(
<p data-testid="field-thread-element">FieldThreadElement</p>
),
}));
const glossaryTags = [
@ -60,6 +58,13 @@ const classificationTags = [
},
];
const requestUpdateTags = {
onUpdateTagsHandler: jest.fn(),
onRequestTagsHandler: jest.fn(),
getColumnName: jest.fn(),
getColumnFieldFQN: 'columns::product_id::tags',
};
const mockProp = {
placeholder: 'Search Tags',
dataTestId: 'tag-container',
@ -85,9 +90,6 @@ const mockProp = {
isReadOnly: false,
isTagLoading: false,
hasTagEditAccess: true,
onUpdateTagsHandler: jest.fn(),
onRequestTagsHandler: jest.fn(),
getColumnName: jest.fn(),
entityFieldTasks: [],
onThreadLinkSelect: jest.fn(),
entityFieldThreads: [
@ -162,9 +164,60 @@ describe('Test EntityTableTags Component', () => {
);
const tagContainer = await screen.findByTestId('tag-container-0');
const tagComponent = await screen.findByTestId('tags-component');
const tagPersonal = await screen.findByTestId('tag-PersonalData.Personal');
expect(tagContainer).toBeInTheDocument();
expect(tagComponent).toBeInTheDocument();
expect(tagPersonal).toBeInTheDocument();
});
it('Should not render update and request tags buttons', async () => {
render(
<TableTags
{...mockProp}
record={{
...mockProp.record,
tags: [...classificationTags, ...glossaryTags],
}}
tags={{
Classification: classificationTags,
Glossary: glossaryTags,
}}
/>,
{
wrapper: MemoryRouter,
}
);
const tagContainer = await screen.findByTestId('tag-container-0');
const requestTags = screen.queryByTestId('field-thread-element');
expect(tagContainer).toBeInTheDocument();
expect(requestTags).not.toBeInTheDocument();
});
it('Should render update and request tags buttons', async () => {
render(
<TableTags
{...mockProp}
{...requestUpdateTags}
record={{
...mockProp.record,
tags: [...classificationTags, ...glossaryTags],
}}
tags={{
Classification: classificationTags,
Glossary: glossaryTags,
}}
/>,
{
wrapper: MemoryRouter,
}
);
const tagContainer = await screen.findByTestId('tag-container-0');
const requestTags = await screen.findAllByTestId('field-thread-element');
expect(tagContainer).toBeInTheDocument();
expect(requestTags).toHaveLength(2);
});
});

View File

@ -12,11 +12,9 @@
*/
import { EntityTags, TagOption } from 'Models';
import { ReactNode } from 'react';
import { TagProps } from '../Tags/tags.interface';
export type TagsContainerProps = {
children?: ReactNode;
editable?: boolean;
dropDownHorzPosRight?: boolean;
selectedTags: Array<EntityTags>;
@ -25,11 +23,10 @@ export type TagsContainerProps = {
showTags?: boolean;
showAddTagButton?: boolean;
showEditTagButton?: boolean;
showNoTagPlaceholder?: boolean;
className?: string;
containerClass?: string;
onSelectionChange?: (selectedTags: Array<EntityTags>) => void;
onCancel?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
onSelectionChange: (selectedTags: Array<EntityTags>) => void;
onCancel?: () => void;
onAddButtonClick?: () => void;
onEditButtonClick?: () => void;
placeholder?: string;

View File

@ -12,6 +12,7 @@
*/
import { getByTestId, render } from '@testing-library/react';
import { NO_DATA_PLACEHOLDER } from 'constants/constants';
import React from 'react';
import TagsContainer from './tags-container';
@ -72,4 +73,22 @@ describe('Test TagsContainer Component', () => {
expect(cancelButton).toBeInTheDocument();
expect(saveButton).toBeInTheDocument();
});
it('Should show no data placeholder when tags is empty and only have view access', () => {
const { container } = render(
<TagsContainer
editable={false}
selectedTags={[]}
showAddTagButton={false}
tagList={[]}
onSelectionChange={onSelectionChange}
/>
);
const tagContainer = getByTestId(container, 'tag-container');
const noTagContainer = getByTestId(container, 'no-tags');
expect(tagContainer).toBeInTheDocument();
expect(noTagContainer).toBeInTheDocument();
expect(noTagContainer).toContainHTML(NO_DATA_PLACEHOLDER);
});
});

View File

@ -16,6 +16,7 @@ import { Button, Select, Space, Tag, Tooltip, Typography } from 'antd';
import { ReactComponent as IconEdit } from 'assets/svg/edit-new.svg';
import classNames from 'classnames';
import Tags from 'components/Tag/Tags/tags';
import { NO_DATA_PLACEHOLDER } from 'constants/constants';
import { TAG_CONSTANT, TAG_START_WITH } from 'constants/Tag.constants';
import { isEmpty } from 'lodash';
import { EntityTags, TagOption } from 'Models';
@ -36,7 +37,6 @@ import TagsViewer from '../TagsViewer/tags-viewer';
import { TagsContainerProps } from './tags-container.interface';
const TagsContainer: FunctionComponent<TagsContainerProps> = ({
children,
editable,
selectedTags,
tagList,
@ -50,7 +50,6 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
showAddTagButton = false,
showEditTagButton = false,
placeholder,
showNoTagPlaceholder = true,
showLimited,
}: TagsContainerProps) => {
const { t } = useTranslation();
@ -58,8 +57,8 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
const [tags, setTags] = useState<Array<EntityTags>>(selectedTags);
const showNoDataPlaceholder = useMemo(
() => !showAddTagButton && tags.length === 0 && showNoTagPlaceholder,
[showAddTagButton, tags, showNoTagPlaceholder]
() => !showAddTagButton && tags.length === 0,
[showAddTagButton, tags]
);
const tagOptions = useMemo(() => {
@ -89,40 +88,32 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
return newTags;
}, [tagList]);
const getUpdatedTags = (selectedTag: string[]): EntityTags[] => {
const updatedTags = selectedTag.map((t) => ({
tagFQN: t,
source: (tagList as TagOption[]).find((tag) => tag.fqn === t)?.source,
}));
return updatedTags;
};
const handleTagSelection = (selectedTag: string[]) => {
if (!isEmpty(selectedTag)) {
setTags(() => {
const updatedTags = selectedTag.map((t) => {
return {
tagFQN: t,
source: (tagList as TagOption[]).find((tag) => tag.fqn === t)
?.source,
} as EntityTags;
});
return updatedTags;
});
setTags(getUpdatedTags(selectedTag));
} else {
setTags([]);
}
};
const handleSave = useCallback(
(event: React.MouseEvent<HTMLElement, MouseEvent>) => {
event.preventDefault();
event.stopPropagation();
onSelectionChange && onSelectionChange(tags);
setTags(selectedTags);
},
[tags, selectedTags, onSelectionChange]
);
const handleCancel = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
event.preventDefault();
event.stopPropagation();
const handleSave = useCallback(() => {
onSelectionChange(tags);
setTags(selectedTags);
onCancel && onCancel(event);
};
}, [onSelectionChange, tags, selectedTags]);
const handleCancel = useCallback(() => {
setTags(selectedTags);
onCancel?.();
}, [selectedTags, onCancel]);
const getTagsElement = (tag: EntityTags, index: number) => {
return (
@ -175,126 +166,156 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
);
};
useEffect(() => {
setTags(selectedTags);
}, [selectedTags]);
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, onAddButtonClick]
);
const editTagButton = useMemo(
() =>
!isEmpty(tags) && showEditTagButton ? (
<Button
className="p-0 flex-center text-primary"
data-testid="edit-button"
icon={
<IconEdit
className="anticon"
height={16}
name={t('label.edit')}
width={16}
/>
}
size="small"
type="text"
onClick={onEditButtonClick}
/>
) : null,
[tags, showEditTagButton, onEditButtonClick]
);
const renderTags = useMemo(
() =>
showLimited ? (
<TagsViewer
isTextPlaceholder
showNoDataPlaceholder={showNoDataPlaceholder}
tags={tags}
type="border"
/>
) : (
<>
{!showAddTagButton && isEmpty(selectedTags) ? (
<Typography.Text data-testid="no-tags">
{NO_DATA_PLACEHOLDER}
</Typography.Text>
) : null}
{tags.map(getTagsElement)}
</>
),
[
showLimited,
showNoDataPlaceholder,
tags,
getTagsElement,
showAddTagButton,
selectedTags,
]
);
const selectedTagsInternal = useMemo(
() => selectedTags.map(({ tagFQN }) => tagFQN as string),
[tags]
);
const tagsSelectContainer = useMemo(() => {
return (
<>
<Select
autoFocus
className={classNames('flex-grow w-max-95', className)}
data-testid="tag-selector"
defaultValue={selectedTagsInternal}
mode="multiple"
optionLabelProp="label"
placeholder={
placeholder
? placeholder
: t('label.select-field', {
field: t('label.tag-plural'),
})
}
removeIcon={
<CloseOutlined data-testid="remove-tags" height={8} width={8} />
}
tagRender={tagRenderer}
onChange={handleTagSelection}>
{tagOptions.map(({ label, value, displayName }) => (
<Select.Option key={label} value={value}>
<Tooltip
destroyTooltipOnHide
mouseEnterDelay={1.5}
placement="leftTop"
title={label}
trigger="hover">
{displayName}
</Tooltip>
</Select.Option>
))}
</Select>
<Button
className="p-x-05"
data-testid="cancelAssociatedTag"
icon={<CloseOutlined size={12} />}
size="small"
onClick={handleCancel}
/>
<Button
className="p-x-05"
data-testid="saveAssociatedTag"
icon={<CheckOutlined size={12} />}
size="small"
type="primary"
onClick={handleSave}
/>
</>
);
}, [
className,
selectedTagsInternal,
tagRenderer,
handleTagSelection,
tagOptions,
handleCancel,
handleSave,
placeholder,
]);
useEffect(() => {
setTags(selectedTags);
}, [selectedTags]);
return (
<div
className={classNames('w-full d-flex items-center gap-2', containerClass)}
data-testid="tag-container">
{showTags && !editable && (
<Space wrap align="center" size={4}>
{showAddTagButton && (
<span onClick={onAddButtonClick}>
<Tags
className="tw-font-semibold tw-text-primary"
startWith={TAG_START_WITH.PLUS}
tag={TAG_CONSTANT}
type="border"
/>
</span>
)}
{showLimited ? (
<TagsViewer
isTextPlaceholder
showNoDataPlaceholder={showNoDataPlaceholder}
tags={tags}
type="border"
/>
) : (
<>
{showNoDataPlaceholder && (
<Typography.Text className="text-grey-muted">
{t('label.no-entity', {
entity: t('label.tag-plural'),
})}
</Typography.Text>
)}
{tags.map(getTagsElement)}
</>
)}
{tags.length && showEditTagButton ? (
<Button
className="p-0 flex-center text-primary"
data-testid="edit-button"
icon={
<IconEdit
className="anticon"
height={16}
name={t('label.edit')}
width={16}
/>
}
size="small"
type="text"
onClick={onEditButtonClick}
/>
) : null}
{addTagButton}
{renderTags}
{editTagButton}
</Space>
)}
{editable ? (
<>
<Select
autoFocus
className={classNames('flex-grow w-max-95', className)}
data-testid="tag-selector"
defaultValue={selectedTagsInternal}
mode="multiple"
optionLabelProp="label"
placeholder={
placeholder
? placeholder
: t('label.select-field', {
field: t('label.tag-plural'),
})
}
removeIcon={
<CloseOutlined data-testid="remove-tags" height={8} width={8} />
}
tagRender={tagRenderer}
onChange={handleTagSelection}>
{tagOptions.map(({ label, value, displayName }) => (
<Select.Option key={label} value={value}>
<Tooltip
destroyTooltipOnHide
mouseEnterDelay={1.5}
placement="leftTop"
title={label}
trigger="hover">
{displayName}
</Tooltip>
</Select.Option>
))}
</Select>
<>
<Button
className="p-x-05"
data-testid="cancelAssociatedTag"
icon={<CloseOutlined size={12} />}
size="small"
onClick={handleCancel}
/>
<Button
className="p-x-05"
data-testid="saveAssociatedTag"
icon={<CheckOutlined size={12} />}
size="small"
type="primary"
onClick={handleSave}
/>
</>
</>
) : (
children
)}
{editable && tagsSelectContainer}
</div>
);
};

View File

@ -14,7 +14,7 @@ import { Button, Typography } from 'antd';
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import { TagDetails } from 'components/TableQueries/TableQueryRightPanel/TableQueryRightPanel.interface';
import TagsContainer from 'components/Tag/TagsContainer/tags-container';
import { DE_ACTIVE_COLOR, NO_DATA_PLACEHOLDER } from 'constants/constants';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import { LabelType, State, TagLabel, TagSource } from 'generated/type/tagLabel';
import { t } from 'i18next';
import { isEmpty } from 'lodash';
@ -120,7 +120,6 @@ const TagsInput: React.FC<Props> = ({ tags = [], editable, onTagsUpdate }) => {
isLoading={tagDetails.isLoading}
selectedTags={getSelectedTags()}
showAddTagButton={editable && isEmpty(tags)}
showNoTagPlaceholder={false}
size="small"
tagList={tagDetails.options}
type="label"
@ -128,7 +127,6 @@ const TagsInput: React.FC<Props> = ({ tags = [], editable, onTagsUpdate }) => {
onCancel={() => setIsEditTags(false)}
onSelectionChange={handleTagSelection}
/>
{!editable && tags.length === 0 && <div>{NO_DATA_PLACEHOLDER}</div>}
</div>
);
};

View File

@ -13,6 +13,7 @@
import {
act,
findAllByTestId,
findByTestId,
findByText,
queryByTestId,
@ -36,19 +37,21 @@ const mockProps: TopicSchemaFieldsProps = {
hasTagEditAccess: true,
};
const mockTags = [
{
tagFQN: 'PII.Sensitive',
source: 'Tag',
},
{
tagFQN: 'PersonalData.Personal',
source: 'Tag',
},
];
jest.mock('../../../utils/TagsUtils', () => ({
fetchTagsAndGlossaryTerms: jest.fn().mockReturnValue([]),
getClassifications: jest.fn().mockReturnValue([]),
getTaglist: jest.fn().mockReturnValue([]),
}));
jest.mock('utils/GlossaryUtils', () => ({
fetchGlossaryTerms: jest.fn().mockReturnValue([]),
getGlossaryTermlist: jest.fn().mockReturnValue([]),
}));
jest.mock('utils/TableTags/TableTags.utils', () => ({
getFilterTags: jest.fn().mockReturnValue({
Classification: [],
Glossary: [],
}),
}));
jest.mock('../../../utils/TopicSchema.utils', () => ({
@ -73,21 +76,12 @@ jest.mock(
})
);
jest.mock('components/Tag/TagsContainer/tags-container', () =>
jest.fn().mockImplementation(({ onSelectionChange }) => (
<div data-testid="tag-container">
Tag Container
<div
data-testid="onSelectionChange"
onClick={() => onSelectionChange(mockTags)}>
onSelectionChange
</div>
</div>
))
);
jest.mock('components/Tag/TagsViewer/tags-viewer', () =>
jest.fn().mockReturnValue(<div data-testid="tag-viewer">Tag Viewer</div>)
jest.mock('components/TableTags/TableTags.component', () =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="table-tag-container">Table Tag Container</div>
))
);
describe('Topic Schema', () => {
@ -107,12 +101,12 @@ describe('Topic Schema', () => {
const name = await findByText(row1, 'Order');
const dataType = await findByText(row1, 'RECORD');
const description = await findByText(row1, 'Description Preview');
const tags = await findByTestId(row1, 'tag-container');
const tagsContainer = await findAllByTestId(row1, 'table-tag-container');
expect(name).toBeInTheDocument();
expect(dataType).toBeInTheDocument();
expect(description).toBeInTheDocument();
expect(tags).toBeInTheDocument();
expect(tagsContainer).toHaveLength(2);
});
it('Should render the children on click of expand icon', async () => {
@ -173,20 +167,4 @@ describe('Topic Schema', () => {
expect(editDescriptionButton).toBeNull();
});
it('onUpdate should be called after the tags are added or removed to a task', async () => {
render(<TopicSchema {...mockProps} />);
const tagsContainer = await screen.findAllByTestId('tag-container');
expect(tagsContainer).toHaveLength(9);
const onSelectionChange = await screen.findAllByTestId('onSelectionChange');
expect(onSelectionChange).toHaveLength(9);
await act(async () => userEvent.click(onSelectionChange[0]));
expect(mockOnUpdate).toHaveBeenCalledTimes(1);
});
});

View File

@ -27,23 +27,25 @@ import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import classNames from 'classnames';
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
import SchemaEditor from 'components/schema-editor/SchemaEditor';
import TableTags from 'components/TableTags/TableTags.component';
import { CSMode } from 'enums/codemirror.enum';
import { cloneDeep, isEmpty, isUndefined } from 'lodash';
import { TagLabel, TagSource } from 'generated/type/tagLabel';
import { cloneDeep, isEmpty, isUndefined, map } from 'lodash';
import { EntityTags, TagOption } from 'Models';
import React, { FC, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getEntityName } from 'utils/EntityUtils';
import { fetchGlossaryTerms, getGlossaryTermlist } from 'utils/GlossaryUtils';
import { getFilterTags } from 'utils/TableTags/TableTags.utils';
import { DataTypeTopic, Field } from '../../../generated/entity/data/topic';
import { getTableExpandableConfig } from '../../../utils/TableUtils';
import { fetchTagsAndGlossaryTerms } from '../../../utils/TagsUtils';
import { getClassifications, getTaglist } from '../../../utils/TagsUtils';
import {
updateFieldDescription,
updateFieldTags,
} from '../../../utils/TopicSchema.utils';
import RichTextEditorPreviewer from '../../common/rich-text-editor/RichTextEditorPreviewer';
import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import TagsContainer from '../../Tag/TagsContainer/tags-container';
import TagsViewer from '../../Tag/TagsViewer/tags-viewer';
import {
CellRendered,
SchemaViewType,
@ -59,24 +61,45 @@ const TopicSchemaFields: FC<TopicSchemaFieldsProps> = ({
hasTagEditAccess,
}) => {
const { t } = useTranslation();
const [editFieldDescription, setEditFieldDescription] = useState<Field>();
const [editFieldTags, setEditFieldTags] = useState<Field>();
const [tagList, setTagList] = useState<TagOption[]>([]);
const [isTagLoading, setIsTagLoading] = useState<boolean>(false);
const [tagFetchFailed, setTagFetchFailed] = useState<boolean>(false);
const [viewType, setViewType] = useState<SchemaViewType>(
SchemaViewType.FIELDS
);
const fetchTags = async () => {
const [glossaryTags, setGlossaryTags] = useState<TagOption[]>([]);
const [classificationTags, setClassificationTags] = useState<TagOption[]>([]);
const fetchGlossaryTags = async () => {
setIsTagLoading(true);
try {
const tagsAndTerms = await fetchTagsAndGlossaryTerms();
setTagList(tagsAndTerms);
} catch (error) {
setTagList([]);
const res = await fetchGlossaryTerms();
const glossaryTerms: TagOption[] = getGlossaryTermlist(res).map(
(tag) => ({ fqn: tag, source: TagSource.Glossary })
);
setGlossaryTags(glossaryTerms);
} catch {
setTagFetchFailed(true);
} finally {
setIsTagLoading(false);
}
};
const fetchClassificationTags = async () => {
setIsTagLoading(true);
try {
const res = await getClassifications();
const tagList = await getTaglist(res.data);
const classificationTag: TagOption[] = map(tagList, (tag) => ({
fqn: tag,
source: TagSource.Classification,
}));
setClassificationTags(classificationTag);
} catch {
setTagFetchFailed(true);
} finally {
setIsTagLoading(false);
@ -85,29 +108,22 @@ const TopicSchemaFields: FC<TopicSchemaFieldsProps> = ({
const handleFieldTagsChange = async (
selectedTags: EntityTags[] = [],
field: Field
editColumnTag: Field,
otherTags: TagLabel[]
) => {
const selectedField = isUndefined(editFieldTags) ? field : editFieldTags;
const newSelectedTags: TagOption[] = selectedTags.map((tag) => ({
fqn: tag.tagFQN,
source: tag.source,
}));
const newSelectedTags: TagOption[] = map(
[...selectedTags, ...otherTags],
(tag) => ({ fqn: tag.tagFQN, source: tag.source })
);
const schema = cloneDeep(messageSchema);
updateFieldTags(schema?.schemaFields, selectedField?.name, newSelectedTags);
await onUpdate(schema);
setEditFieldTags(undefined);
};
const handleAddTagClick = (record: Field) => {
if (isUndefined(editFieldTags)) {
setEditFieldTags(record);
// Fetch tags and terms only once
if (tagList.length === 0 || tagFetchFailed) {
fetchTags();
}
if (newSelectedTags && editColumnTag) {
const schema = cloneDeep(messageSchema);
updateFieldTags(
schema?.schemaFields,
editColumnTag.name,
newSelectedTags
);
await onUpdate(schema);
}
};
@ -161,43 +177,6 @@ const TopicSchemaFields: FC<TopicSchemaFieldsProps> = ({
);
};
const renderFieldTags: CellRendered<Field, 'tags'> = (
tags,
record: Field
) => {
const isSelectedField = editFieldTags?.name === record.name;
const styleFlag = isSelectedField || !isEmpty(tags);
return (
<>
{isReadOnly ? (
<TagsViewer sizeCap={-1} tags={tags || []} />
) : (
<Space
align={styleFlag ? 'start' : 'center'}
className="justify-between"
data-testid="tags-wrapper"
direction={styleFlag ? 'vertical' : 'horizontal'}
onClick={() => handleAddTagClick(record)}>
<TagsContainer
className="w-min-10"
editable={isSelectedField}
isLoading={isTagLoading && isSelectedField}
selectedTags={tags || []}
showAddTagButton={hasTagEditAccess && isEmpty(tags)}
showEditTagButton={hasTagEditAccess}
size="small"
tagList={tagList}
type="label"
onCancel={() => setEditFieldTags(undefined)}
onSelectionChange={(tags) => handleFieldTagsChange(tags, record)}
/>
</Space>
)}
</>
);
};
const columns: ColumnsType<Field> = useMemo(
() => [
{
@ -237,18 +216,60 @@ const TopicSchemaFields: FC<TopicSchemaFieldsProps> = ({
title: t('label.tag-plural'),
dataIndex: 'tags',
key: 'tags',
width: 350,
render: renderFieldTags,
accessor: 'tags',
width: 300,
render: (tags: TagLabel[], record: Field, index: number) => (
<TableTags<Field>
dataTestId="classification-tags"
fetchTags={fetchClassificationTags}
handleTagSelection={handleFieldTagsChange}
hasTagEditAccess={hasTagEditAccess}
index={index}
isReadOnly={isReadOnly}
isTagLoading={isTagLoading}
record={record}
tagFetchFailed={tagFetchFailed}
tagList={classificationTags}
tags={getFilterTags(tags)}
type={TagSource.Classification}
/>
),
},
{
title: t('label.glossary-term-plural'),
dataIndex: 'tags',
key: 'tags',
accessor: 'tags',
width: 300,
render: (tags: TagLabel[], record: Field, index: number) => (
<TableTags<Field>
dataTestId="glossary-tags"
fetchTags={fetchGlossaryTags}
handleTagSelection={handleFieldTagsChange}
hasTagEditAccess={hasTagEditAccess}
index={index}
isReadOnly={isReadOnly}
isTagLoading={isTagLoading}
record={record}
tagFetchFailed={tagFetchFailed}
tagList={glossaryTags}
tags={getFilterTags(tags)}
type={TagSource.Glossary}
/>
),
},
],
[
handleFieldTagsChange,
fetchGlossaryTags,
messageSchema,
hasDescriptionEditAccess,
hasTagEditAccess,
editFieldDescription,
editFieldTags,
isReadOnly,
isTagLoading,
glossaryTags,
tagFetchFailed,
]
);