Enhance Test Case Form with New Features and Improved UI

- Added new fields for test name, description, tags, and glossary terms to the Test Case Form.
- Implemented loading state management for form submission and cancellation.
- Introduced reusable action buttons for better user interaction.
- Enhanced styling for fixed action buttons and drawer mode adjustments.
- Updated form layout to accommodate new fields and improve overall user experience.
This commit is contained in:
Shailesh Parmar 2025-07-01 20:46:18 +05:30
parent 2aa5431488
commit 13a62cb431
2 changed files with 238 additions and 20 deletions

View File

@ -13,6 +13,10 @@
@import (reference) '../../../../styles/variables.less';
.test-case-form-v1 {
position: relative;
min-height: 100vh;
padding-bottom: 80px; // Space for fixed buttons
.ant-form-item {
margin-bottom: @size-lg;
@ -28,7 +32,8 @@
}
.select-table-card,
.test-type-card {
.test-type-card,
.test-details-card {
background-color: @grey-1;
border: 1px solid @grey-200;
border-radius: @border-radius-sm;
@ -51,9 +56,17 @@
font-weight: @font-medium;
font-size: @font-size-base;
}
.card-title {
color: @grey-800;
font-size: @size-md;
font-weight: @font-medium;
margin-bottom: @size-mlg;
line-height: 1.4;
}
}
.test-type-card {
.test-details-card {
margin-bottom: 0;
}
@ -74,4 +87,44 @@
font-size: @font-size-base;
}
}
.test-case-form-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: @white;
border-top: 1px solid @grey-200;
padding: @size-lg @size-xl;
display: flex;
justify-content: flex-end;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
}
// Drawer mode adjustments
&.drawer-mode {
min-height: auto;
padding-bottom: 0;
.test-case-form-actions {
display: none; // Hide actions in drawer mode, use drawer footer instead
}
}
}
// Drawer specific styles
.drawer-form-content {
.test-case-form-v1 {
padding-bottom: 0;
}
}
.drawer-footer-actions {
display: flex;
justify-content: flex-end;
width: 100%;
.ant-space {
width: auto;
}
}

View File

@ -10,29 +10,48 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Card, Drawer, DrawerProps, Form, Select, Typography } from 'antd';
import {
Button,
Card,
Drawer,
DrawerProps,
Form,
Select,
Space,
Typography,
} from 'antd';
import { useForm } from 'antd/lib/form/Form';
import classNames from 'classnames';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as ColumnIcon } from '../../../../assets/svg/ic-column.svg';
import { ReactComponent as TableIcon } from '../../../../assets/svg/ic-format-table.svg';
import {
PAGE_SIZE_LARGE,
PAGE_SIZE_MEDIUM,
} from '../../../../constants/constants';
import { ENTITY_NAME_REGEX } from '../../../../constants/regex.constants';
import { SearchIndex } from '../../../../enums/search.enum';
import { TagSource } from '../../../../generated/api/domains/createDataProduct';
import { Table } from '../../../../generated/entity/data/table';
import { TagLabel } from '../../../../generated/tests/testCase';
import {
EntityType,
TestDefinition,
TestPlatform,
} from '../../../../generated/tests/testDefinition';
import {
FieldProp,
FieldTypes,
FormItemLayout,
} from '../../../../interface/FormUtils.interface';
import { TableSearchSource } from '../../../../interface/search.interface';
import { searchQuery } from '../../../../rest/searchAPI';
import { getTableDetailsByFQN } from '../../../../rest/tableAPI';
import { getListTestDefinitions } from '../../../../rest/testAPI';
import { filterSelectOptions } from '../../../../utils/CommonUtils';
import { getEntityName } from '../../../../utils/EntityUtils';
import { generateFormFields } from '../../../../utils/formUtils';
import { AsyncSelect } from '../../../common/AsyncSelect/AsyncSelect';
import SelectionCardGroup from '../../../common/SelectionCardGroup/SelectionCardGroup';
import { SelectionOption } from '../../../common/SelectionCardGroup/SelectionCardGroup.interface';
@ -45,7 +64,9 @@ export interface TestCaseFormV1Props {
className?: string;
table?: Table;
onFormSubmit?: (values: FormValues) => void;
onCancel?: () => void;
initialValues?: Partial<FormValues>;
loading?: boolean;
}
interface FormValues {
@ -53,6 +74,11 @@ interface FormValues {
selectedTable?: string;
selectedColumn?: string;
testTypeId?: string;
testName?: string;
description?: string;
tags?: TagLabel[];
glossaryTerms?: TagLabel[];
computePassedFailedRowCount?: boolean;
}
type TablesCache = Map<string, TableSearchSource>;
@ -92,16 +118,6 @@ const convertSearchSourceToTable = (searchSource: TableSearchSource): Table =>
columns: searchSource.columns || [],
} as Table);
/**
* TestCaseFormV1 - An improved form component for creating test cases
*
* Features:
* - Progressive test level selection (Table/Column)
* - Smart table caching with column data
* - Dynamic test type filtering based on column data types
* - Efficient column selection without additional API calls
* - Dynamic parameter form rendering based on selected test definition
*/
const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
className,
drawerProps,
@ -109,8 +125,12 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
isDrawer = false,
table,
onFormSubmit,
onCancel,
loading: externalLoading = false,
}) => {
const { t } = useTranslation();
const [form] = useForm<FormValues>();
const [loading, setLoading] = useState(false);
const [testDefinitions, setTestDefinitions] = useState<TestDefinition[]>([]);
const [selectedTestDefinition, setSelectedTestDefinition] =
useState<TestDefinition>();
@ -123,12 +143,47 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
const selectedTestLevel = Form.useWatch('testLevel', form);
const selectedTable = Form.useWatch('selectedTable', form);
const selectedColumn = Form.useWatch('selectedColumn', form);
const selectedTestType = Form.useWatch('testTypeId', form);
const handleSubmit = (values: FormValues) => {
onFormSubmit?.(values);
const handleSubmit = async (values: FormValues) => {
setLoading(true);
try {
await onFormSubmit?.(values);
} finally {
setLoading(false);
}
};
const handleCancel = () => {
onCancel?.();
};
const isFormLoading = loading || externalLoading;
// Reusable action buttons component
const renderActionButtons = useMemo(
() => (
<Space size={16}>
<Button
data-testid="cancel-btn"
disabled={isFormLoading}
size="large"
onClick={handleCancel}>
{t('label.cancel')}
</Button>
<Button
data-testid="create-btn"
htmlType="submit"
loading={isFormLoading}
size="large"
type="primary"
onClick={() => form.submit()}>
{t('label.create')}
</Button>
</Space>
),
[isFormLoading, handleCancel, form, t]
);
const fetchTables = useCallback(async (searchValue = '', page = 1) => {
try {
const response = await searchQuery({
@ -326,6 +381,103 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
return null;
}, [selectedTestDefinition, selectedTableData]);
const isComputeRowCountFieldVisible = useMemo(() => {
return selectedTestDefinition?.supportsRowLevelPassedFailed ?? false;
}, [selectedTestDefinition]);
const testDetailsFormFields: FieldProp[] = useMemo(
() => [
{
name: 'testName',
required: false,
label: t('label.name'),
id: 'root/testName',
type: FieldTypes.TEXT,
placeholder: t('message.enter-test-case-name'),
rules: [
{
pattern: ENTITY_NAME_REGEX,
message: t('message.entity-name-validation'),
},
{
max: 256,
message: t('message.entity-maximum-size', {
entity: t('label.name'),
max: 256,
}),
},
],
props: {
'data-testid': 'test-case-name',
},
},
{
name: 'description',
required: false,
label: t('label.description'),
id: 'root/description',
type: FieldTypes.DESCRIPTION,
props: {
'data-testid': 'description',
initialValue: initialValues?.description ?? '',
style: {
margin: 0,
},
},
},
{
name: 'tags',
required: false,
label: t('label.tag-plural'),
id: 'root/tags',
type: FieldTypes.TAG_SUGGESTION,
props: {
selectProps: {
'data-testid': 'tags-selector',
},
},
},
{
name: 'glossaryTerms',
required: false,
label: t('label.glossary-term-plural'),
id: 'root/glossaryTerms',
type: FieldTypes.TAG_SUGGESTION,
props: {
selectProps: {
'data-testid': 'glossary-terms-selector',
},
open: false,
hasNoActionButtons: true,
isTreeSelect: true,
tagType: TagSource.Glossary,
placeholder: t('label.select-field', {
field: t('label.glossary-term-plural'),
}),
},
},
],
[initialValues?.description, initialValues?.tags, t]
);
const computeRowCountField: FieldProp[] = useMemo(
() => [
{
name: 'computePassedFailedRowCount',
label: t('label.compute-row-count'),
type: FieldTypes.SWITCH,
helperText: t('message.compute-row-count-helper-text'),
required: false,
props: {
'data-testid': 'compute-passed-failed-row-count',
},
id: 'root/computePassedFailedRowCount',
formItemLayout: FormItemLayout.HORIZONTAL,
},
],
[]
);
const formContent = (
<div
className={classNames(
@ -359,7 +511,6 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
showSearch
api={fetchTables}
placeholder="Select one or more table at a time"
size="large"
/>
</Form.Item>
@ -379,7 +530,6 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
loading={!selectedTableData}
options={columnOptions}
placeholder="Select a column"
size="large"
/>
</Form.Item>
)}
@ -397,26 +547,41 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
options={testTypeOptions}
placeholder="Select a test type"
popupClassName="no-wrap-option"
size="large"
onChange={handleTestDefinitionChange}
/>
</Form.Item>
{selectedTestDefinition && generateParamsField}
</Card>
<Card className="test-details-card">
{generateFormFields(testDetailsFormFields)}
{isComputeRowCountFieldVisible &&
generateFormFields(computeRowCountField)}
</Card>
</Form>
{!isDrawer && (
<div className="test-case-form-actions">{renderActionButtons}</div>
)}
</div>
);
const drawerFooter = (
<div className="drawer-footer-actions">{renderActionButtons}</div>
);
if (isDrawer) {
return (
<Drawer
destroyOnClose
open
footer={drawerFooter}
placement="right"
size="large"
{...drawerProps}>
{formContent}
<div className="drawer-form-content">{formContent}</div>
</Drawer>
);
}