mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-02 12:26:42 +00:00
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:
parent
cb5bcdbb9c
commit
1f65e92c58
@ -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 = {
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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}
|
||||||
|
@ -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({
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user