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'; @import (reference) '../../../../styles/variables.less';
.test-case-form-v1 { .test-case-form-v1 {
position: relative;
min-height: 100vh;
padding-bottom: 80px; // Space for fixed buttons
.ant-form-item { .ant-form-item {
margin-bottom: @size-lg; margin-bottom: @size-lg;
@ -28,7 +32,8 @@
} }
.select-table-card, .select-table-card,
.test-type-card { .test-type-card,
.test-details-card {
background-color: @grey-1; background-color: @grey-1;
border: 1px solid @grey-200; border: 1px solid @grey-200;
border-radius: @border-radius-sm; border-radius: @border-radius-sm;
@ -51,9 +56,17 @@
font-weight: @font-medium; font-weight: @font-medium;
font-size: @font-size-base; 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; margin-bottom: 0;
} }
@ -74,4 +87,44 @@
font-size: @font-size-base; 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 * See the License for the specific language governing permissions and
* limitations under the License. * 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 { useForm } from 'antd/lib/form/Form';
import classNames from 'classnames'; import classNames from 'classnames';
import { FC, useCallback, useEffect, useMemo, useState } from 'react'; 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 ColumnIcon } from '../../../../assets/svg/ic-column.svg';
import { ReactComponent as TableIcon } from '../../../../assets/svg/ic-format-table.svg'; import { ReactComponent as TableIcon } from '../../../../assets/svg/ic-format-table.svg';
import { import {
PAGE_SIZE_LARGE, PAGE_SIZE_LARGE,
PAGE_SIZE_MEDIUM, PAGE_SIZE_MEDIUM,
} from '../../../../constants/constants'; } from '../../../../constants/constants';
import { ENTITY_NAME_REGEX } from '../../../../constants/regex.constants';
import { SearchIndex } from '../../../../enums/search.enum'; import { SearchIndex } from '../../../../enums/search.enum';
import { TagSource } from '../../../../generated/api/domains/createDataProduct';
import { Table } from '../../../../generated/entity/data/table'; import { Table } from '../../../../generated/entity/data/table';
import { TagLabel } from '../../../../generated/tests/testCase';
import { import {
EntityType, EntityType,
TestDefinition, TestDefinition,
TestPlatform, TestPlatform,
} from '../../../../generated/tests/testDefinition'; } from '../../../../generated/tests/testDefinition';
import {
FieldProp,
FieldTypes,
FormItemLayout,
} from '../../../../interface/FormUtils.interface';
import { TableSearchSource } from '../../../../interface/search.interface'; import { TableSearchSource } from '../../../../interface/search.interface';
import { searchQuery } from '../../../../rest/searchAPI'; import { searchQuery } from '../../../../rest/searchAPI';
import { getTableDetailsByFQN } from '../../../../rest/tableAPI'; import { getTableDetailsByFQN } from '../../../../rest/tableAPI';
import { getListTestDefinitions } from '../../../../rest/testAPI'; import { getListTestDefinitions } from '../../../../rest/testAPI';
import { filterSelectOptions } from '../../../../utils/CommonUtils'; import { filterSelectOptions } from '../../../../utils/CommonUtils';
import { getEntityName } from '../../../../utils/EntityUtils'; import { getEntityName } from '../../../../utils/EntityUtils';
import { generateFormFields } from '../../../../utils/formUtils';
import { AsyncSelect } from '../../../common/AsyncSelect/AsyncSelect'; import { AsyncSelect } from '../../../common/AsyncSelect/AsyncSelect';
import SelectionCardGroup from '../../../common/SelectionCardGroup/SelectionCardGroup'; import SelectionCardGroup from '../../../common/SelectionCardGroup/SelectionCardGroup';
import { SelectionOption } from '../../../common/SelectionCardGroup/SelectionCardGroup.interface'; import { SelectionOption } from '../../../common/SelectionCardGroup/SelectionCardGroup.interface';
@ -45,7 +64,9 @@ export interface TestCaseFormV1Props {
className?: string; className?: string;
table?: Table; table?: Table;
onFormSubmit?: (values: FormValues) => void; onFormSubmit?: (values: FormValues) => void;
onCancel?: () => void;
initialValues?: Partial<FormValues>; initialValues?: Partial<FormValues>;
loading?: boolean;
} }
interface FormValues { interface FormValues {
@ -53,6 +74,11 @@ interface FormValues {
selectedTable?: string; selectedTable?: string;
selectedColumn?: string; selectedColumn?: string;
testTypeId?: string; testTypeId?: string;
testName?: string;
description?: string;
tags?: TagLabel[];
glossaryTerms?: TagLabel[];
computePassedFailedRowCount?: boolean;
} }
type TablesCache = Map<string, TableSearchSource>; type TablesCache = Map<string, TableSearchSource>;
@ -92,16 +118,6 @@ const convertSearchSourceToTable = (searchSource: TableSearchSource): Table =>
columns: searchSource.columns || [], columns: searchSource.columns || [],
} as Table); } 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> = ({ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
className, className,
drawerProps, drawerProps,
@ -109,8 +125,12 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
isDrawer = false, isDrawer = false,
table, table,
onFormSubmit, onFormSubmit,
onCancel,
loading: externalLoading = false,
}) => { }) => {
const { t } = useTranslation();
const [form] = useForm<FormValues>(); const [form] = useForm<FormValues>();
const [loading, setLoading] = useState(false);
const [testDefinitions, setTestDefinitions] = useState<TestDefinition[]>([]); const [testDefinitions, setTestDefinitions] = useState<TestDefinition[]>([]);
const [selectedTestDefinition, setSelectedTestDefinition] = const [selectedTestDefinition, setSelectedTestDefinition] =
useState<TestDefinition>(); useState<TestDefinition>();
@ -123,12 +143,47 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
const selectedTestLevel = Form.useWatch('testLevel', form); const selectedTestLevel = Form.useWatch('testLevel', form);
const selectedTable = Form.useWatch('selectedTable', form); const selectedTable = Form.useWatch('selectedTable', form);
const selectedColumn = Form.useWatch('selectedColumn', form); const selectedColumn = Form.useWatch('selectedColumn', form);
const selectedTestType = Form.useWatch('testTypeId', form);
const handleSubmit = (values: FormValues) => { const handleSubmit = async (values: FormValues) => {
onFormSubmit?.(values); 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) => { const fetchTables = useCallback(async (searchValue = '', page = 1) => {
try { try {
const response = await searchQuery({ const response = await searchQuery({
@ -326,6 +381,103 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
return null; return null;
}, [selectedTestDefinition, selectedTableData]); }, [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 = ( const formContent = (
<div <div
className={classNames( className={classNames(
@ -359,7 +511,6 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
showSearch showSearch
api={fetchTables} api={fetchTables}
placeholder="Select one or more table at a time" placeholder="Select one or more table at a time"
size="large"
/> />
</Form.Item> </Form.Item>
@ -379,7 +530,6 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
loading={!selectedTableData} loading={!selectedTableData}
options={columnOptions} options={columnOptions}
placeholder="Select a column" placeholder="Select a column"
size="large"
/> />
</Form.Item> </Form.Item>
)} )}
@ -397,26 +547,41 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
options={testTypeOptions} options={testTypeOptions}
placeholder="Select a test type" placeholder="Select a test type"
popupClassName="no-wrap-option" popupClassName="no-wrap-option"
size="large"
onChange={handleTestDefinitionChange} onChange={handleTestDefinitionChange}
/> />
</Form.Item> </Form.Item>
{selectedTestDefinition && generateParamsField} {selectedTestDefinition && generateParamsField}
</Card> </Card>
<Card className="test-details-card">
{generateFormFields(testDetailsFormFields)}
{isComputeRowCountFieldVisible &&
generateFormFields(computeRowCountField)}
</Card>
</Form> </Form>
{!isDrawer && (
<div className="test-case-form-actions">{renderActionButtons}</div>
)}
</div> </div>
); );
const drawerFooter = (
<div className="drawer-footer-actions">{renderActionButtons}</div>
);
if (isDrawer) { if (isDrawer) {
return ( return (
<Drawer <Drawer
destroyOnClose destroyOnClose
open open
footer={drawerFooter}
placement="right" placement="right"
size="large" size="large"
{...drawerProps}> {...drawerProps}>
{formContent} <div className="drawer-form-content">{formContent}</div>
</Drawer> </Drawer>
); );
} }