change contract update to patch call from put (#23229)

* change contract update to patch call from put

* collapse the card when data is not availbale instead of hiding

* minor fix

* fix the sonar issue and addressed comments

* disabled save button while edit, if not changes detected and added playwright for it

* fix the playwright test due to recent pagination merge
This commit is contained in:
Ashish Gupta 2025-09-06 00:41:28 +05:30 committed by GitHub
parent cb5bcdbb9c
commit 1f65e92c58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 407 additions and 49 deletions

View File

@ -15,6 +15,8 @@ import { uuid } from '../utils/common';
export const DATA_CONTRACT_DETAILS = { export const DATA_CONTRACT_DETAILS = {
name: `data_contract_${uuid()}`, name: `data_contract_${uuid()}`,
description: 'new data contract description', description: 'new data contract description',
displayName: `Data Contract_${uuid()}`,
description2: 'Modified Data Contract Description',
}; };
export const DATA_CONTRACT_SEMANTICS1 = { export const DATA_CONTRACT_SEMANTICS1 = {

View File

@ -39,7 +39,7 @@ import {
validateDataContractInsideBundleTestSuites, validateDataContractInsideBundleTestSuites,
waitForDataContractExecution, waitForDataContractExecution,
} from '../../utils/dataContracts'; } from '../../utils/dataContracts';
import { addOwner } from '../../utils/entity'; import { addOwner, addOwnerWithoutValidation } from '../../utils/entity';
import { settingClick } from '../../utils/sidebar'; import { settingClick } from '../../utils/sidebar';
const adminUser = new UserClass(); const adminUser = new UserClass();
@ -314,6 +314,11 @@ test.describe('Data Contracts', () => {
await page.reload(); await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
await expect( await expect(
page.getByTestId('contract-status-card-item-Semantics-status') page.getByTestId('contract-status-card-item-Semantics-status')
).toContainText('Passed'); ).toContainText('Passed');
@ -517,6 +522,85 @@ test.describe('Data Contracts', () => {
await download.saveAs('downloads/' + download.suggestedFilename()); await download.saveAs('downloads/' + download.suggestedFilename());
}); });
await test.step('Edit and Validate Contract data', async () => {
await page.getByTestId('contract-edit-button').click();
await expect(page.getByTestId('save-contract-btn')).toBeDisabled();
// Change the Contract Details
await page
.getByTestId('contract-name')
.fill(DATA_CONTRACT_DETAILS.displayName);
await page.click('.om-block-editor[contenteditable="true"]');
await page.keyboard.press('Control+A');
await page.keyboard.type(DATA_CONTRACT_DETAILS.description2);
await addOwnerWithoutValidation({
page,
owner: 'admin',
type: 'Users',
initiatorId: 'select-owners',
});
await expect(
page.getByTestId('user-tag').getByText('admin')
).toBeVisible();
// Move to Schema Tab
await page.getByRole('button', { name: 'Schema' }).click();
// TODO: will enable this once nested column is fixed
// await page.waitForSelector('[data-testid="loader"]', {
// state: 'detached',
// });
// await page.getByRole('checkbox', { name: 'Select all' }).click();
// await expect(
// page.getByRole('checkbox', { name: 'Select all' })
// ).not.toBeChecked();
// Move to Semantic Tab
await page.getByRole('button', { name: 'Semantics' }).click();
await page.getByTestId('delete-condition-button').last().click();
await expect(
page.getByTestId('query-builder-form-field').getByText('Description')
).not.toBeVisible();
await expect(page.getByTestId('save-contract-btn')).not.toBeDisabled();
const saveContractResponse = page.waitForResponse(
'/api/v1/dataContracts/*'
);
await page.getByTestId('save-contract-btn').click();
await saveContractResponse;
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
// Validate the Updated Values
await expect(page.getByTestId('contract-title')).toContainText(
DATA_CONTRACT_DETAILS.displayName
);
await expect(
page.getByTestId('contract-owner-card').getByTestId('admin')
).toBeVisible();
await expect(
page.locator(
'[data-testid="viewer-container"] [data-testid="markdown-parser"]'
)
).toContainText(DATA_CONTRACT_DETAILS.description2);
// TODO: will enable this once nested column is fixed
// await expect(page.getByTestId('schema-table-card')).not.toBeVisible();
});
await test.step('Delete contract', async () => { await test.step('Delete contract', async () => {
const deleteContractResponse = page.waitForResponse( const deleteContractResponse = page.waitForResponse(
'api/v1/dataContracts/*?hardDelete=true&recursive=true' 'api/v1/dataContracts/*?hardDelete=true&recursive=true'
@ -893,7 +977,7 @@ test.describe('Data Contracts', () => {
await test.step('Save contract and validate for schema', async () => { await test.step('Save contract and validate for schema', async () => {
const saveContractResponse = page.waitForResponse( const saveContractResponse = page.waitForResponse(
'/api/v1/dataContracts' '/api/v1/dataContracts/*'
); );
await page.getByTestId('save-contract-btn').click(); await page.getByTestId('save-contract-btn').click();
@ -937,7 +1021,7 @@ test.describe('Data Contracts', () => {
).not.toBeChecked(); ).not.toBeChecked();
const saveContractResponse = page.waitForResponse( const saveContractResponse = page.waitForResponse(
'/api/v1/dataContracts' '/api/v1/dataContracts/*'
); );
await page.getByTestId('save-contract-btn').click(); await page.getByTestId('save-contract-btn').click();
@ -979,7 +1063,7 @@ test.describe('Data Contracts', () => {
} }
const saveContractResponse = page.waitForResponse( const saveContractResponse = page.waitForResponse(
'/api/v1/dataContracts' '/api/v1/dataContracts/*'
); );
await page.getByTestId('save-contract-btn').click(); await page.getByTestId('save-contract-btn').click();

View File

@ -19,7 +19,7 @@ export const saveAndTriggerDataContractValidation = async (
page: Page, page: Page,
isContractStatusNotVisible?: boolean isContractStatusNotVisible?: boolean
): Promise<string | undefined> => { ): Promise<string | undefined> => {
const saveContractResponse = page.waitForResponse('/api/v1/dataContracts'); const saveContractResponse = page.waitForResponse('/api/v1/dataContracts/*');
await page.getByTestId('save-contract-btn').click(); await page.getByTestId('save-contract-btn').click();
const response = await saveContractResponse; const response = await saveContractResponse;
const responseData = await response.json(); const responseData = await response.json();
@ -40,6 +40,11 @@ export const saveAndTriggerDataContractValidation = async (
await page.reload(); await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
return responseData; return responseData;
}; };

View File

@ -164,6 +164,51 @@ export const addOwner = async ({
).toBeVisible(); ).toBeVisible();
}; };
export const addOwnerWithoutValidation = async ({
page,
owner,
type = 'Users',
initiatorId = 'edit-owner',
}: {
page: Page;
owner: string;
type?: 'Teams' | 'Users';
initiatorId?: string;
}) => {
await page.getByTestId(initiatorId).click();
if (type === 'Users') {
const userListResponse = page.waitForResponse(
'/api/v1/search/query?q=*isBot:false*index=user_search_index*'
);
await page.getByRole('tab', { name: type }).click();
await userListResponse;
}
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
const ownerSearchBar = await page
.getByTestId(`owner-select-${lowerCase(type)}-search-bar`)
.isVisible();
if (!ownerSearchBar) {
await page.getByRole('tab', { name: type }).click();
}
const searchUser = page.waitForResponse(
`/api/v1/search/query?q=*${encodeURIComponent(owner)}*`
);
await page
.getByTestId(`owner-select-${lowerCase(type)}-search-bar`)
.fill(owner);
await searchUser;
if (type === 'Teams') {
await page.getByRole('listitem', { name: owner, exact: true }).click();
} else {
await page.getByRole('listitem', { name: owner, exact: true }).click();
await page.getByTestId('selectable-list-update-btn').click();
}
};
export const updateOwner = async ({ export const updateOwner = async ({
page, page,
owner, owner,

View File

@ -12,6 +12,7 @@
*/ */
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { act, fireEvent, render, screen } from '@testing-library/react'; import { act, fireEvent, render, screen } from '@testing-library/react';
import { AxiosError } from 'axios';
import { EntityType } from '../../../enums/entity.enum'; import { EntityType } from '../../../enums/entity.enum';
import { import {
DataContract, DataContract,
@ -73,7 +74,9 @@ jest.mock('../ContractDetailFormTab/ContractDetailFormTab', () => ({
.mockImplementation(({ onChange, onNext }) => ( .mockImplementation(({ onChange, onNext }) => (
<div> <div>
<h2>Contract Details</h2> <h2>Contract Details</h2>
<button onClick={onChange}>Change</button> <button onClick={() => onChange({ name: 'Test Contract Change' })}>
Change
</button>
<button onClick={onNext}>Next</button> <button onClick={onNext}>Next</button>
</div> </div>
)), )),
@ -84,7 +87,9 @@ jest.mock('../ContractQualityFormTab/ContractQualityFormTab', () => ({
.mockImplementation(({ onChange, onNext }) => ( .mockImplementation(({ onChange, onNext }) => (
<div> <div>
<h2>Contract Quality</h2> <h2>Contract Quality</h2>
<button onClick={onChange}>Change</button> <button onClick={() => onChange({ qualityExpectations: [] })}>
Change
</button>
<button onClick={onNext}>Next</button> <button onClick={onNext}>Next</button>
</div> </div>
)), )),
@ -96,7 +101,7 @@ jest.mock('../ContractSchemaFormTab/ContractScehmaFormTab', () => ({
<div> <div>
<h2>Contract Schema</h2> <h2>Contract Schema</h2>
<button onClick={onPrev}>Previous</button> <button onClick={onPrev}>Previous</button>
<button onClick={onChange}>Change</button> <button onClick={() => onChange({ schema: [] })}>Change</button>
<button onClick={onNext}>Next</button> <button onClick={onNext}>Next</button>
</div> </div>
)), )),
@ -109,7 +114,7 @@ jest.mock('../ContractSemanticFormTab/ContractSemanticFormTab', () => ({
<div> <div>
<h2>Contract Semantics</h2> <h2>Contract Semantics</h2>
<button onClick={onPrev}>Previous</button> <button onClick={onPrev}>Previous</button>
<button onClick={onChange}>Change</button> <button onClick={() => onChange({ semantics: [] })}>Change</button>
<button onClick={onNext}>Next</button> <button onClick={onNext}>Next</button>
</div> </div>
)), )),
@ -202,9 +207,19 @@ describe('AddDataContract', () => {
}); });
describe('Save Functionality', () => { describe('Save Functionality', () => {
it('should call createContract for new contract', async () => { beforeEach(() => {
jest.clearAllMocks();
});
it('should call createContract for new contract with correct parameters', async () => {
render(<AddDataContract onCancel={mockOnCancel} onSave={mockOnSave} />); render(<AddDataContract onCancel={mockOnCancel} onSave={mockOnSave} />);
// First trigger a form change to enable the save button
const changeButton = screen.getByText('Change');
await act(async () => {
fireEvent.click(changeButton);
});
const saveButton = screen.getByTestId('save-contract-btn'); const saveButton = screen.getByTestId('save-contract-btn');
await act(async () => { await act(async () => {
@ -213,12 +228,13 @@ describe('AddDataContract', () => {
expect(createContract).toHaveBeenCalledWith( expect(createContract).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
displayName: 'Test Contract Change', // Now formValues.name is set from mock
entity: { entity: {
id: 'table-id', id: 'table-id',
type: EntityType.TABLE, type: EntityType.TABLE,
}, },
semantics: undefined, // validSemantics - undefined when no semantics provided
entityStatus: EntityStatus.Approved, entityStatus: EntityStatus.Approved,
semantics: undefined,
}) })
); );
expect(showSuccessToast).toHaveBeenCalledWith( expect(showSuccessToast).toHaveBeenCalledWith(
@ -227,7 +243,39 @@ describe('AddDataContract', () => {
expect(mockOnSave).toHaveBeenCalled(); expect(mockOnSave).toHaveBeenCalled();
}); });
it('should call updateContract for existing contract', async () => { it('should call createContract with form changes applied', async () => {
render(<AddDataContract onCancel={mockOnCancel} onSave={mockOnSave} />);
// Trigger a form change to enable the save button and set form values
const changeButton = screen.getByText('Change');
await act(async () => {
fireEvent.click(changeButton);
});
const saveButton = screen.getByTestId('save-contract-btn');
await act(async () => {
fireEvent.click(saveButton);
});
expect(createContract).toHaveBeenCalledWith(
expect.objectContaining({
displayName: 'Test Contract Change', // formValues.name from mock onChange
entity: {
id: 'table-id',
type: EntityType.TABLE,
},
semantics: undefined, // validSemantics - undefined when no semantics provided
entityStatus: EntityStatus.Approved,
})
);
expect(showSuccessToast).toHaveBeenCalledWith(
'message.data-contract-saved-successfully'
);
expect(mockOnSave).toHaveBeenCalled();
});
it('should call updateContract for existing contract with JSON patch', async () => {
render( render(
<AddDataContract <AddDataContract
contract={mockContract} contract={mockContract}
@ -236,13 +284,22 @@ describe('AddDataContract', () => {
/> />
); );
// Trigger a form change to enable the save button for existing contracts
const changeButton = screen.getByText('Change');
await act(async () => {
fireEvent.click(changeButton);
});
const saveButton = screen.getByTestId('save-contract-btn'); const saveButton = screen.getByTestId('save-contract-btn');
await act(async () => { await act(async () => {
fireEvent.click(saveButton); fireEvent.click(saveButton);
}); });
expect(updateContract).toHaveBeenCalled(); expect(updateContract).toHaveBeenCalledWith(
'contract-1',
expect.any(Array) // JSON patch array from fast-json-patch compare
);
expect(showSuccessToast).toHaveBeenCalledWith( expect(showSuccessToast).toHaveBeenCalledWith(
'message.data-contract-saved-successfully' 'message.data-contract-saved-successfully'
); );
@ -255,6 +312,12 @@ describe('AddDataContract', () => {
render(<AddDataContract onCancel={mockOnCancel} onSave={mockOnSave} />); render(<AddDataContract onCancel={mockOnCancel} onSave={mockOnSave} />);
// Trigger form change to enable save button
const changeButton = screen.getByText('Change');
await act(async () => {
fireEvent.click(changeButton);
});
const saveButton = screen.getByTestId('save-contract-btn'); const saveButton = screen.getByTestId('save-contract-btn');
await act(async () => { await act(async () => {
@ -262,6 +325,35 @@ describe('AddDataContract', () => {
}); });
expect(showErrorToast).toHaveBeenCalledWith(mockError); expect(showErrorToast).toHaveBeenCalledWith(mockError);
expect(mockOnSave).not.toHaveBeenCalled(); // Should not call onSave on error
});
it('should handle update contract errors gracefully', async () => {
const mockError = new AxiosError('Update failed');
(updateContract as jest.Mock).mockRejectedValue(mockError);
render(
<AddDataContract
contract={mockContract}
onCancel={mockOnCancel}
onSave={mockOnSave}
/>
);
// Trigger form change to enable save button
const changeButton = screen.getByText('Change');
await act(async () => {
fireEvent.click(changeButton);
});
const saveButton = screen.getByTestId('save-contract-btn');
await act(async () => {
fireEvent.click(saveButton);
});
expect(showErrorToast).toHaveBeenCalledWith(mockError);
expect(mockOnSave).not.toHaveBeenCalled(); // Should not call onSave on error
}); });
it('should filter out empty semantics before saving', async () => { it('should filter out empty semantics before saving', async () => {
@ -269,7 +361,9 @@ describe('AddDataContract', () => {
...mockContract, ...mockContract,
semantics: [ semantics: [
{ name: 'Valid Semantic', rule: 'valid rule' }, { name: 'Valid Semantic', rule: 'valid rule' },
{ name: '', rule: '' }, { name: '', rule: '' }, // Should be filtered out
{ name: 'Valid Name', rule: '' }, // Should be filtered out (empty rule)
{ name: '', rule: 'valid rule' }, // Should be filtered out (empty name)
{ name: 'Another Valid', rule: 'another rule' }, { name: 'Another Valid', rule: 'another rule' },
] as SemanticsRule[], ] as SemanticsRule[],
}; };
@ -288,15 +382,102 @@ describe('AddDataContract', () => {
fireEvent.click(saveButton); fireEvent.click(saveButton);
}); });
// Should call updateContract with only valid semantics
expect(updateContract).toHaveBeenCalledWith( expect(updateContract).toHaveBeenCalledWith(
'contract-1',
expect.any(Array) // JSON patch comparing with filtered semantics
);
});
it('should set displayName from formValues.name in create mode', async () => {
render(<AddDataContract onCancel={mockOnCancel} onSave={mockOnSave} />);
// Trigger form change first
const changeButton = screen.getByText('Change');
await act(async () => {
fireEvent.click(changeButton);
});
const saveButton = screen.getByTestId('save-contract-btn');
await act(async () => {
fireEvent.click(saveButton);
});
expect(createContract).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
semantics: [ displayName: 'Test Contract Change', // formValues.name from mock
{ name: 'Valid Semantic', rule: 'valid rule' },
{ name: 'Another Valid', rule: 'another rule' },
],
}) })
); );
}); });
it('should include displayName in update patch for edit mode', async () => {
render(
<AddDataContract
contract={mockContract}
onCancel={mockOnCancel}
onSave={mockOnSave}
/>
);
// Trigger form change to enable save button
const changeButton = screen.getByText('Change');
await act(async () => {
fireEvent.click(changeButton);
});
const saveButton = screen.getByTestId('save-contract-btn');
await act(async () => {
fireEvent.click(saveButton);
});
// For update, displayName should be set to formValues.name in the patch comparison
expect(updateContract).toHaveBeenCalledWith(
'contract-1',
expect.any(Array)
);
});
it('should disable save button when no changes are detected', async () => {
render(
<AddDataContract
contract={mockContract}
onCancel={mockOnCancel}
onSave={mockOnSave}
/>
);
const saveButton = screen.getByTestId('save-contract-btn');
// Button should be disabled when no changes are made (isSaveDisabled logic)
// This happens when the JSON patch comparison results in an empty array
expect(saveButton).toBeInTheDocument();
});
it('should show loading state during save operation', async () => {
// Mock a delayed response to test loading state
(createContract as jest.Mock).mockImplementation(
() => new Promise((resolve) => setTimeout(resolve, 100))
);
render(<AddDataContract onCancel={mockOnCancel} onSave={mockOnSave} />);
// Trigger form change to enable save button first
const changeButton = screen.getByText('Change');
await act(async () => {
fireEvent.click(changeButton);
});
const saveButton = screen.getByTestId('save-contract-btn');
act(() => {
fireEvent.click(saveButton);
});
// Should show loading state (Ant Design Button shows loading via classes)
expect(saveButton).toHaveClass('ant-btn-loading');
});
}); });
describe('Cancel Functionality', () => { describe('Cancel Functionality', () => {

View File

@ -13,6 +13,7 @@
import { Button, Card, RadioChangeEvent, Tabs, Typography } from 'antd'; import { Button, Card, RadioChangeEvent, Tabs, Typography } from 'antd';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -32,7 +33,6 @@ import {
} from '../../../generated/entity/data/dataContract'; } from '../../../generated/entity/data/dataContract';
import { Table } from '../../../generated/entity/data/table'; import { Table } from '../../../generated/entity/data/table';
import { createContract, updateContract } from '../../../rest/contractAPI'; import { createContract, updateContract } from '../../../rest/contractAPI';
import { getUpdatedContractDetails } from '../../../utils/DataContract/DataContractUtils';
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider';
import SchemaEditor from '../../Database/SchemaEditor/SchemaEditor'; import SchemaEditor from '../../Database/SchemaEditor/SchemaEditor';
@ -73,28 +73,49 @@ const AddDataContract: React.FC<{
setActiveTab(key); setActiveTab(key);
}, []); }, []);
const handleSave = useCallback(async () => { const { validSemantics, isSaveDisabled } = useMemo(() => {
setIsSubmitting(true);
const validSemantics = formValues.semantics?.filter( const validSemantics = formValues.semantics?.filter(
(semantic) => !isEmpty(semantic.name) && !isEmpty(semantic.rule) (semantic) => !isEmpty(semantic.name) && !isEmpty(semantic.rule)
); );
return {
validSemantics,
isSaveDisabled: isEmpty(
compare(contract ?? {}, {
...contract,
...formValues,
semantics: validSemantics,
})
),
};
}, [contract, formValues]);
const handleSave = useCallback(async () => {
setIsSubmitting(true);
try { try {
await (contract if (contract) {
? updateContract({ await updateContract(
...getUpdatedContractDetails(contract, formValues), contract?.id,
semantics: validSemantics, compare(contract, {
}) ...contract,
: createContract({
...formValues, ...formValues,
entity: {
id: table.id,
type: EntityType.TABLE,
},
semantics: validSemantics, semantics: validSemantics,
entityStatus: EntityStatus.Approved, displayName: formValues.name,
})); })
);
} else {
await createContract({
...formValues,
displayName: formValues.name,
entity: {
id: table.id,
type: EntityType.TABLE,
},
semantics: validSemantics,
entityStatus: EntityStatus.Approved,
});
}
showSuccessToast(t('message.data-contract-saved-successfully')); showSuccessToast(t('message.data-contract-saved-successfully'));
onSave(); onSave();
@ -103,7 +124,7 @@ const AddDataContract: React.FC<{
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
}, [contract, formValues, table?.id]); }, [contract, formValues, table?.id, validSemantics]);
const onFormChange = useCallback( const onFormChange = useCallback(
(data: Partial<DataContract>) => { (data: Partial<DataContract>) => {
@ -227,6 +248,7 @@ const AddDataContract: React.FC<{
<Button <Button
className="add-contract-save-button" className="add-contract-save-button"
data-testid="save-contract-btn" data-testid="save-contract-btn"
disabled={isSaveDisabled}
loading={isSubmitting} loading={isSubmitting}
type="primary" type="primary"
onClick={handleSave}> onClick={handleSave}>
@ -235,7 +257,14 @@ const AddDataContract: React.FC<{
</div> </div>
</div> </div>
); );
}, [mode, handleModeChange, onCancel, handleSave, isSubmitting]); }, [
mode,
isSubmitting,
isSaveDisabled,
handleModeChange,
onCancel,
handleSave,
]);
const cardContent = useMemo(() => { const cardContent = useMemo(() => {
if (mode === DataContractMode.YAML) { if (mode === DataContractMode.YAML) {

View File

@ -17,6 +17,7 @@ import { useTranslation } from 'react-i18next';
import { ReactComponent as RightIcon } from '../../../assets/svg/right-arrow.svg'; import { ReactComponent as RightIcon } from '../../../assets/svg/right-arrow.svg';
import { DataContract } from '../../../generated/entity/data/dataContract'; import { DataContract } from '../../../generated/entity/data/dataContract';
import { FieldProp, FieldTypes } from '../../../interface/FormUtils.interface'; import { FieldProp, FieldTypes } from '../../../interface/FormUtils.interface';
import { getEntityName } from '../../../utils/EntityUtils';
import { generateFormFields } from '../../../utils/formUtils'; import { generateFormFields } from '../../../utils/formUtils';
import './contract-detail-form-tab.less'; import './contract-detail-form-tab.less';
@ -72,7 +73,7 @@ export const ContractDetailFormTab: React.FC<{
useEffect(() => { useEffect(() => {
if (initialValues) { if (initialValues) {
form.setFieldsValue({ form.setFieldsValue({
name: initialValues.name, name: getEntityName(initialValues),
description: initialValues.description, description: initialValues.description,
owners: initialValues.owners, owners: initialValues.owners,
}); });

View File

@ -286,11 +286,15 @@ const ContractDetail: React.FC<{
return ( return (
<Row align="middle" justify="space-between"> <Row align="middle" justify="space-between">
<Col flex="auto"> <Col flex="auto">
<Typography.Text className="contract-title"> <Typography.Text
className="contract-title"
data-testid="contract-title">
{getEntityName(contract)} {getEntityName(contract)}
</Typography.Text> </Typography.Text>
<Typography.Text className="contract-time"> <Typography.Text
className="contract-time"
data-testid="contract-last-updated-time">
{t('message.modified-time-ago-by', { {t('message.modified-time-ago-by', {
time: getRelativeTime(contract.updatedAt), time: getRelativeTime(contract.updatedAt),
by: contract.updatedBy, by: contract.updatedBy,
@ -316,7 +320,9 @@ const ContractDetail: React.FC<{
<Col> <Col>
<div className="contract-action-container"> <div className="contract-action-container">
{!isEmpty(contract.owners) && ( {!isEmpty(contract.owners) && (
<div className="contract-owner-label-container"> <div
className="contract-owner-label-container"
data-testid="contract-owner-card">
<Typography.Text>{t('label.owner-plural')}</Typography.Text> <Typography.Text>{t('label.owner-plural')}</Typography.Text>
<OwnerLabel <OwnerLabel
avatarSize={24} avatarSize={24}
@ -475,7 +481,10 @@ const ContractDetail: React.FC<{
</Typography.Text> </Typography.Text>
</div> </div>
), ),
}}> }}
dataTestId="schema-table-card"
defaultExpanded={!isEmpty(schemaDetail)}
isExpandDisabled={isEmpty(schemaDetail)}>
<Table <Table
columns={schemaColumns} columns={schemaColumns}
dataSource={schemaDetail} dataSource={schemaDetail}

View File

@ -17,7 +17,7 @@ import { Button, Col, Form, Input, Row, Switch, Typography } from 'antd';
import Card from 'antd/lib/card/Card'; import Card from 'antd/lib/card/Card';
import TextArea from 'antd/lib/input/TextArea'; import TextArea from 'antd/lib/input/TextArea';
import classNames from 'classnames'; import classNames from 'classnames';
import { isNull, isNumber } from 'lodash'; import { isEmpty, isNull, isNumber } from 'lodash';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ReactComponent as DeleteIcon } from '../../../assets/svg/ic-trash.svg'; import { ReactComponent as DeleteIcon } from '../../../assets/svg/ic-trash.svg';
@ -84,9 +84,9 @@ export const ContractSemanticFormTab: React.FC<{
}, [queryBuilderAddRule]); }, [queryBuilderAddRule]);
useEffect(() => { useEffect(() => {
if (initialValues?.semantics) { if (!isEmpty(initialValues?.semantics)) {
form.setFieldsValue({ form.setFieldsValue({
semantics: initialValues.semantics, semantics: initialValues?.semantics,
}); });
} else { } else {
form.setFieldsValue({ form.setFieldsValue({

View File

@ -10,6 +10,8 @@
* 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 { AxiosResponse } from 'axios';
import { Operation } from 'fast-json-patch';
import { import {
ContractAllResult, ContractAllResult,
ContractResultFilter, ContractResultFilter,
@ -62,11 +64,11 @@ export const createContract = async (contract: CreateDataContract) => {
return response.data; return response.data;
}; };
export const updateContract = async (contract: CreateDataContract) => { export const updateContract = async (id: string, data: Operation[]) => {
const response = await APIClient.put<CreateDataContract>( const response = await APIClient.patch<
`/dataContracts`, Operation[],
contract AxiosResponse<CreateDataContract>
); >(`/dataContracts/${id}`, data);
return response.data; return response.data;
}; };