feat(ui): Glossary name modal (#10802)

* feat: initial commit glossary redesign

* chore: add localization

* fix: update glossary ui

* fix: missing localization

* feat: update glossary ui

* fix: jest tests

* fix: jest tests

* fix: update breadcrumbs

* fix: update cypress tests

* chore: remove logs

* fix: update glossary right panel

* fix: jest tests

* fix: add reviewer functionality

* feat: add entity name and entity display name rename modal

* fix: add missing localization

* fix: update cypress tests

* fix: jest tests

* fix: redesign reviewer panel

* fix: remove breadcrumb sizing
This commit is contained in:
karanh37 2023-03-29 15:32:20 +05:30 committed by GitHub
parent 0a92a897a1
commit 63edc5d5ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 329 additions and 178 deletions

View File

@ -10,20 +10,20 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { CheckOutlined, CloseOutlined } from '@ant-design/icons'; import { Button, Col, Row, Space, Tooltip, Typography } from 'antd';
import { Button, Col, Input, Row, Space, Tooltip, Typography } from 'antd';
import DescriptionV1 from 'components/common/description/DescriptionV1'; import DescriptionV1 from 'components/common/description/DescriptionV1';
import ProfilePicture from 'components/common/ProfilePicture/ProfilePicture'; import ProfilePicture from 'components/common/ProfilePicture/ProfilePicture';
import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component'; import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component';
import { TitleBreadcrumbProps } from 'components/common/title-breadcrumb/title-breadcrumb.interface'; import { TitleBreadcrumbProps } from 'components/common/title-breadcrumb/title-breadcrumb.interface';
import { UserTeamSelectableList } from 'components/common/UserTeamSelectableList/UserTeamSelectableList.component'; import { UserTeamSelectableList } from 'components/common/UserTeamSelectableList/UserTeamSelectableList.component';
import EntityDisplayNameModal from 'components/Modals/EntityDisplayNameModal/EntityDisplayNameModal.component';
import EntityNameModal from 'components/Modals/EntityNameModal/EntityNameModal.component';
import { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface'; import { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface';
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants'; import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
import { getUserPath } from 'constants/constants'; import { getUserPath } from 'constants/constants';
import { NO_PERMISSION_FOR_ACTION } from 'constants/HelperTextUtil'; import { NO_PERMISSION_FOR_ACTION } from 'constants/HelperTextUtil';
import { Glossary } from 'generated/entity/data/glossary'; import { Glossary } from 'generated/entity/data/glossary';
import { GlossaryTerm } from 'generated/entity/data/glossaryTerm'; import { GlossaryTerm } from 'generated/entity/data/glossaryTerm';
import { useAfterMount } from 'hooks/useAfterMount';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -51,40 +51,38 @@ const GlossaryHeader = ({
}: GlossaryHeaderProps) => { }: GlossaryHeaderProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [displayName, setDisplayName] = useState<string>();
const [isNameEditing, setIsNameEditing] = useState<boolean>(false); const [isNameEditing, setIsNameEditing] = useState<boolean>(false);
const [isDisplayNameEditing, setIsDisplayNameEditing] =
useState<boolean>(false);
const [isDescriptionEditable, setIsDescriptionEditable] = const [isDescriptionEditable, setIsDescriptionEditable] =
useState<boolean>(false); useState<boolean>(false);
const [breadcrumb, setBreadcrumb] = useState< const [breadcrumb, setBreadcrumb] = useState<
TitleBreadcrumbProps['titleLinks'] TitleBreadcrumbProps['titleLinks']
>([]); >([]);
const [addTermButtonWidth, setAddTermButtonWidth] = useState(
document.getElementById('add-term-button')?.offsetWidth || 0
);
const [manageButtonWidth, setManageButtonWidth] = useState(
document.getElementById('manage-button')?.offsetWidth || 0
);
const [leftPanelWidth, setLeftPanelWidth] = useState(
document.getElementById('glossary-left-panel')?.offsetWidth || 0
);
const editDisplayNamePermission = useMemo(() => { const editDisplayNamePermission = useMemo(() => {
return permissions.EditAll || permissions.EditDisplayName; return permissions.EditAll || permissions.EditDisplayName;
}, [permissions]); }, [permissions]);
const onDisplayNameChange = (value: string) => { const onDisplayNameSave = (displayName: string) => {
if (selectedData.displayName !== value) {
setDisplayName(value);
}
};
const onDisplayNameSave = () => {
let updatedDetails = cloneDeep(selectedData); let updatedDetails = cloneDeep(selectedData);
updatedDetails = { updatedDetails = {
...selectedData, ...selectedData,
displayName: displayName?.trim(), displayName: displayName?.trim(),
name: displayName?.trim() || selectedData.name, };
onUpdate(updatedDetails);
setIsDisplayNameEditing(false);
};
const onNameSave = (name: string) => {
let updatedDetails = cloneDeep(selectedData);
updatedDetails = {
...selectedData,
name: name?.trim() || selectedData.name,
}; };
onUpdate(updatedDetails); onUpdate(updatedDetails);
@ -137,27 +135,16 @@ const GlossaryHeader = ({
} }
}; };
useAfterMount(() => {
setLeftPanelWidth(
document.getElementById('glossary-left-panel')?.offsetWidth || 0
);
setAddTermButtonWidth(
document.getElementById('add-term-button')?.offsetWidth || 0
);
setManageButtonWidth(
document.getElementById('manage-button')?.offsetWidth || 0
);
});
useEffect(() => { useEffect(() => {
const { displayName, fullyQualifiedName, name } = selectedData; const { fullyQualifiedName, name } = selectedData;
setDisplayName(displayName);
if (!isGlossary) { if (!isGlossary) {
handleBreadcrumb(fullyQualifiedName ? fullyQualifiedName : name); handleBreadcrumb(fullyQualifiedName ? fullyQualifiedName : name);
} }
}, [selectedData]); }, [selectedData]);
return ( return (
<>
<Row gutter={[0, 16]}> <Row gutter={[0, 16]}>
<Col span={24}> <Col span={24}>
<Row justify="space-between"> <Row justify="space-between">
@ -166,41 +153,10 @@ const GlossaryHeader = ({
<div <div
className="tw-text-link tw-text-base glossary-breadcrumb" className="tw-text-link tw-text-base glossary-breadcrumb"
data-testid="category-name"> data-testid="category-name">
<TitleBreadcrumb <TitleBreadcrumb titleLinks={breadcrumb} />
titleLinks={breadcrumb}
widthDeductions={
leftPanelWidth + addTermButtonWidth + manageButtonWidth + 20 // Additional deduction for margin on the right of leftPanel
}
/>
</div> </div>
)} )}
{isNameEditing ? (
<Space direction="horizontal">
<Input
className="input-width"
data-testid="displayName"
name="displayName"
value={displayName}
onChange={(e) => onDisplayNameChange(e.target.value)}
/>
<Button
data-testid="cancelAssociatedTag"
icon={<CloseOutlined />}
size="small"
type="primary"
onMouseDown={() => setIsNameEditing(false)}
/>
<Button
data-testid="saveAssociatedTag"
icon={<CheckOutlined />}
size="small"
type="primary"
onMouseDown={onDisplayNameSave}
/>
</Space>
) : (
<Space direction="vertical" size={0}> <Space direction="vertical" size={0}>
<Space> <Space>
<Typography.Text <Typography.Text
@ -237,7 +193,9 @@ const GlossaryHeader = ({
<Tooltip <Tooltip
title={ title={
editDisplayNamePermission editDisplayNamePermission
? t('label.edit-entity', { entity: t('label.name') }) ? t('label.edit-entity', {
entity: t('label.display-name'),
})
: NO_PERMISSION_FOR_ACTION : NO_PERMISSION_FOR_ACTION
}> }>
<Button <Button
@ -248,12 +206,11 @@ const GlossaryHeader = ({
} }
size="small" size="small"
type="text" type="text"
onClick={() => setIsNameEditing(true)} onClick={() => setIsDisplayNameEditing(true)}
/> />
</Tooltip> </Tooltip>
</Space> </Space>
</Space> </Space>
)}
</Col> </Col>
<Col span={12}> <Col span={12}>
<div style={{ textAlign: 'right' }}> <div style={{ textAlign: 'right' }}>
@ -317,6 +274,19 @@ const GlossaryHeader = ({
/> />
</Col> </Col>
</Row> </Row>
<EntityNameModal
name={selectedData.name}
visible={isNameEditing}
onCancel={() => setIsNameEditing(false)}
onSave={onNameSave}
/>
<EntityDisplayNameModal
displayName={selectedData.displayName || ''}
visible={isDisplayNameEditing}
onCancel={() => setIsDisplayNameEditing(false)}
onSave={onDisplayNameSave}
/>
</>
); );
}; };

View File

@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { act, fireEvent, render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { Glossary } from 'generated/entity/data/glossary'; import { Glossary } from 'generated/entity/data/glossary';
import React from 'react'; import React from 'react';
import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils'; import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils';
@ -111,41 +111,6 @@ describe('GlossaryHeader component', () => {
expect(screen.getByTestId('edit-name')).toBeDisabled(); expect(screen.getByTestId('edit-name')).toBeDisabled();
}); });
it('should show editing of name after clicking on edit icon', () => {
render(
<GlossaryHeader
isGlossary
permissions={{
...DEFAULT_ENTITY_PERMISSION,
EditAll: true,
EditDisplayName: true,
}}
selectedData={
{
displayName: 'glossaryTest',
reviewers: [
{ displayName: 'reviewer1' },
{ displayName: 'reviewer2' },
],
} as Glossary
}
onDelete={mockOnDelete}
onUpdate={mockOnUpdate}
/>
);
expect(screen.getByTestId('edit-name')).toBeInTheDocument();
act(() => {
fireEvent.click(screen.getByTestId('edit-name'));
});
expect(screen.getByTestId('displayName')).toBeInTheDocument();
expect(screen.getByTestId('displayName')).toHaveValue('glossaryTest');
expect(screen.getByTestId('cancelAssociatedTag')).toBeInTheDocument();
expect(screen.getByTestId('saveAssociatedTag')).toBeInTheDocument();
});
it('should render no owner if owner is not present', () => { it('should render no owner if owner is not present', () => {
render( render(
<GlossaryHeader <GlossaryHeader

View File

@ -83,6 +83,10 @@ const GlossaryHeaderButtons = ({
} else { } else {
history.push(getAddGlossaryTermsPath(glossary)); history.push(getAddGlossaryTermsPath(glossary));
} }
} else {
history.push(
getAddGlossaryTermsPath(selectedData.fullyQualifiedName ?? '')
);
} }
}; };

View File

@ -0,0 +1,93 @@
/*
* 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 { Button, Form, Input, Modal, Typography } from 'antd';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
interface Props {
visible: boolean;
displayName: string;
onCancel: () => void;
onSave: (displayName: string) => void;
}
const EntityDisplayNameModal: React.FC<Props> = ({
visible,
displayName,
onCancel,
onSave,
}) => {
const { t } = useTranslation();
const [form] = Form.useForm<{ displayName: string }>();
const handleSave = async (obj: { displayName: string }) => {
try {
await form.validateFields();
onSave(obj.displayName);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
form.setFieldValue('displayName', displayName);
}, [visible]);
return (
<Modal
destroyOnClose
footer={[
<Button key="cancel-btn" type="link" onClick={onCancel}>
{t('label.cancel')}
</Button>,
<Button
data-testid="save-button"
key="save-btn"
type="primary"
onClick={() => form.submit()}>
{t('label.save')}
</Button>,
]}
okText={t('label.save')}
title={
<Typography.Text strong data-testid="header">
{t('label.edit-glossary-display-name')}
</Typography.Text>
}
visible={visible}>
<Form form={form} layout="vertical" onFinish={handleSave}>
<Form.Item
extra={
<Typography.Text className="help-text p-x-xs tw-text-xs tw-text-grey-muted">
{t('message.edit-glossary-display-name-help')}
</Typography.Text>
}
initialValue={displayName}
label={`${t('label.display-name')}:`}
name="displayName"
rules={[
{
required: true,
message: `${t('label.field-required', {
field: t('label.name'),
})}`,
},
]}>
<Input placeholder={t('message.enter-display-name')} />
</Form.Item>
</Form>
</Modal>
);
};
export default EntityDisplayNameModal;

View File

@ -0,0 +1,97 @@
/*
* 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 { Button, Form, Input, Modal, Typography } from 'antd';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
interface Props {
visible: boolean;
name: string;
onCancel: () => void;
onSave: (name: string) => void;
}
const EntityNameModal: React.FC<Props> = ({
visible,
name,
onCancel,
onSave,
}) => {
const { t } = useTranslation();
const [form] = Form.useForm<{ name: string }>();
const handleSave = async (obj: { name: string }) => {
try {
await form.validateFields();
onSave(obj.name);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
form.setFieldValue('name', name);
}, [visible]);
return (
<Modal
destroyOnClose
footer={[
<Button key="cancel-btn" type="link" onClick={onCancel}>
{t('label.cancel')}
</Button>,
<Button
data-testid="save-button"
key="save-btn"
type="primary"
onClick={() => form.submit()}>
{t('label.save')}
</Button>,
]}
okText={t('label.save')}
open={visible}
title={
<Typography.Text strong data-testid="header">
{t('label.edit-glossary-name')}
</Typography.Text>
}>
<Form form={form} layout="vertical" onFinish={handleSave}>
<Form.Item
extra={
<Typography.Text className="help-text p-x-xs m-t-xs tw-text-xs tw-text-grey-muted">
{t('message.edit-glossary-name-help')}
</Typography.Text>
}
initialValue={name}
label={`${t('label.name')}:`}
name="name"
rules={[
{
required: true,
message: `${t('label.field-required', {
field: t('label.name'),
})}`,
},
]}>
<Input
placeholder={t('label.enter-entity-name', {
entity: t('label.glossary'),
})}
/>
</Form.Item>
</Form>
</Modal>
);
};
export default EntityNameModal;

View File

@ -245,6 +245,8 @@
"edit-description-for": "Edit Description for {{entityName}}", "edit-description-for": "Edit Description for {{entityName}}",
"edit-entity": "Edit {{entity}}", "edit-entity": "Edit {{entity}}",
"edit-entity-name": "Edit {{entityType}}: \"{{entityName}}\"", "edit-entity-name": "Edit {{entityType}}: \"{{entityName}}\"",
"edit-glossary-display-name": "Edit Glossary Display Name",
"edit-glossary-name": "Edit Glossary Name",
"edit-workflow-ingestion": "Edit {{workflow}} Ingestion", "edit-workflow-ingestion": "Edit {{workflow}} Ingestion",
"edited": "Edited", "edited": "Edited",
"effect": "Effect", "effect": "Effect",
@ -959,6 +961,8 @@
"downstream-depth-message": "Please select a value for downstream depth", "downstream-depth-message": "Please select a value for downstream depth",
"downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).", "downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).",
"drag-and-drop-files-here": "Drag & drop files here", "drag-and-drop-files-here": "Drag & drop files here",
"edit-glossary-display-name-help": "Update Display Name",
"edit-glossary-name-help": "Changing Name will remove the existing tag and create new one with mentioned name",
"edit-service-entity-connection": "Edit {{entity}} Service Connection", "edit-service-entity-connection": "Edit {{entity}} Service Connection",
"elastic-search-message": "Ensure that your Elasticsearch indexes are up-to-date by syncing, or recreating all indexes.", "elastic-search-message": "Ensure that your Elasticsearch indexes are up-to-date by syncing, or recreating all indexes.",
"elasticsearch-setup": "Please follow the instructions here to set up Metadata ingestion and index them into Elasticsearch.", "elasticsearch-setup": "Please follow the instructions here to set up Metadata ingestion and index them into Elasticsearch.",

View File

@ -245,6 +245,8 @@
"edit-description-for": "Mettre à Jour la Description pour {{entityName}}", "edit-description-for": "Mettre à Jour la Description pour {{entityName}}",
"edit-entity": "Mettre à Jour {{entity}}", "edit-entity": "Mettre à Jour {{entity}}",
"edit-entity-name": "Edit {{entityType}}: \"{{entityName}}\"", "edit-entity-name": "Edit {{entityType}}: \"{{entityName}}\"",
"edit-glossary-display-name": "Edit Glossary Display Name",
"edit-glossary-name": "Edit Glossary Name",
"edit-workflow-ingestion": "Edit {{workflow}} Ingestion", "edit-workflow-ingestion": "Edit {{workflow}} Ingestion",
"edited": "Edited", "edited": "Edited",
"effect": "Effet", "effect": "Effet",
@ -959,6 +961,8 @@
"downstream-depth-message": "Please select a value for downstream depth", "downstream-depth-message": "Please select a value for downstream depth",
"downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).", "downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).",
"drag-and-drop-files-here": "Drag & drop files here", "drag-and-drop-files-here": "Drag & drop files here",
"edit-glossary-display-name-help": "Update Display Name",
"edit-glossary-name-help": "Changing Name will remove the existing tag and create new one with mentioned name",
"edit-service-entity-connection": "Edit {{entity}} Service Connection", "edit-service-entity-connection": "Edit {{entity}} Service Connection",
"elastic-search-message": "Ensure that your Elasticsearch indexes are up-to-date by syncing, or recreating all indexes.", "elastic-search-message": "Ensure that your Elasticsearch indexes are up-to-date by syncing, or recreating all indexes.",
"elasticsearch-setup": "Please follow the instructions here to set up Metadata ingestion and index them into Elasticsearch.", "elasticsearch-setup": "Please follow the instructions here to set up Metadata ingestion and index them into Elasticsearch.",

View File

@ -245,6 +245,8 @@
"edit-description-for": "{{entityName}}の説明を編集", "edit-description-for": "{{entityName}}の説明を編集",
"edit-entity": "{{entity}}を編集", "edit-entity": "{{entity}}を編集",
"edit-entity-name": "{{entityType}}: \"{{entityName}}\" を編集", "edit-entity-name": "{{entityType}}: \"{{entityName}}\" を編集",
"edit-glossary-display-name": "Edit Glossary Display Name",
"edit-glossary-name": "Edit Glossary Name",
"edit-workflow-ingestion": "{{workflow}}インジェスチョンを編集", "edit-workflow-ingestion": "{{workflow}}インジェスチョンを編集",
"edited": "編集済", "edited": "編集済",
"effect": "エフェクト", "effect": "エフェクト",
@ -959,6 +961,8 @@
"downstream-depth-message": "Please select a value for downstream depth", "downstream-depth-message": "Please select a value for downstream depth",
"downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).", "downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).",
"drag-and-drop-files-here": "ここにファイルをドラッグ&ドロップ", "drag-and-drop-files-here": "ここにファイルをドラッグ&ドロップ",
"edit-glossary-display-name-help": "Update Display Name",
"edit-glossary-name-help": "Changing Name will remove the existing tag and create new one with mentioned name",
"edit-service-entity-connection": "{{entity}}サービスとの接続を編集する", "edit-service-entity-connection": "{{entity}}サービスとの接続を編集する",
"elastic-search-message": "Ensure that your Elasticsearch indexes are up-to-date by syncing, or recreating all indexes.", "elastic-search-message": "Ensure that your Elasticsearch indexes are up-to-date by syncing, or recreating all indexes.",
"elasticsearch-setup": "Please follow the instructions here to set up Metadata ingestion and index them into Elasticsearch.", "elasticsearch-setup": "Please follow the instructions here to set up Metadata ingestion and index them into Elasticsearch.",

View File

@ -245,6 +245,8 @@
"edit-description-for": "编辑描述 {{entityName}}", "edit-description-for": "编辑描述 {{entityName}}",
"edit-entity": "编辑 {{entity}}", "edit-entity": "编辑 {{entity}}",
"edit-entity-name": "编辑 {{entityType}}: \"{{entityName}}\"", "edit-entity-name": "编辑 {{entityType}}: \"{{entityName}}\"",
"edit-glossary-display-name": "Edit Glossary Display Name",
"edit-glossary-name": "Edit Glossary Name",
"edit-workflow-ingestion": "编辑 {{workflow}} 获取", "edit-workflow-ingestion": "编辑 {{workflow}} 获取",
"edited": "编辑", "edited": "编辑",
"effect": "Effect", "effect": "Effect",
@ -959,6 +961,8 @@
"downstream-depth-message": "Please select a value for downstream depth", "downstream-depth-message": "Please select a value for downstream depth",
"downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).", "downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).",
"drag-and-drop-files-here": "Drag & drop files here", "drag-and-drop-files-here": "Drag & drop files here",
"edit-glossary-display-name-help": "Update Display Name",
"edit-glossary-name-help": "Changing Name will remove the existing tag and create new one with mentioned name",
"edit-service-entity-connection": "Edit {{entity}} Service Connection", "edit-service-entity-connection": "Edit {{entity}} Service Connection",
"elastic-search-message": "Ensure that your Elasticsearch indexes are up-to-date by syncing, or recreating all indexes.", "elastic-search-message": "Ensure that your Elasticsearch indexes are up-to-date by syncing, or recreating all indexes.",
"elasticsearch-setup": "Please follow the instructions here to set up Metadata ingestion and index them into Elasticsearch.", "elasticsearch-setup": "Please follow the instructions here to set up Metadata ingestion and index them into Elasticsearch.",

View File

@ -180,7 +180,6 @@ const GlossaryRightPanel = ({
data-testid={`reviewer-${reviewer.displayName}`} data-testid={`reviewer-${reviewer.displayName}`}
key={reviewer.name}> key={reviewer.name}>
<UserTag <UserTag
bordered
id={reviewer.id || ''} id={reviewer.id || ''}
key={reviewer.name} key={reviewer.name}
name={reviewer?.name || ''} name={reviewer?.name || ''}

View File

@ -1298,3 +1298,10 @@ div.ant-typography-ellipsis-custom {
svg { svg {
vertical-align: baseline; vertical-align: baseline;
} }
/**
* Style for Antd Form Item Extra
*/
.help-text {
box-decoration-break: clone;
}