Update data contract details fields (#22746)

* update data contract details fields

* fix the select option chip not properly displaying the data

* supported new form field for UserTeamSelectInput Selector

* remove component changes that we not needed

* fix the selected owner chip design

* supported the userPopoverList width as per screen size with max and min limitation

* fix the tab inside userTeamSelect list not proper as the content and some oprimiztion on component side

* fix the playwright because of owner changes

* fix the description box overflow and owner selcect box overlapping on select input

* enum label fix

* remove the comment code and fix the localization and sematic status changing issue are contract validation run
This commit is contained in:
Ashish Gupta 2025-08-06 20:52:58 +05:30 committed by GitHub
parent 882d858972
commit accc05a494
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 306 additions and 71 deletions

View File

@ -100,7 +100,7 @@ test.describe('Data Contracts', () => {
DATA_CONTRACT_DETAILS.description DATA_CONTRACT_DETAILS.description
); );
await page.getByTestId('add-owner').click(); await page.getByTestId('select-owners').click();
await page.getByRole('tab', { name: 'Users' }).click(); await page.getByRole('tab', { name: 'Users' }).click();
await page await page
.getByTestId('owner-select-users-search-bar') .getByTestId('owner-select-users-search-bar')
@ -114,9 +114,7 @@ test.describe('Data Contracts', () => {
await page.getByTestId('selectable-list-update-btn').click(); await page.getByTestId('selectable-list-update-btn').click();
await expect( await expect(
page page.getByTestId('user-tag').getByText(user.responseData.name)
.getByTestId('owner-link')
.getByTestId(user.responseData.displayName)
).toBeVisible(); ).toBeVisible();
}); });

View File

@ -130,10 +130,10 @@
} }
.ant-btn-group.spaced { .ant-btn-group.spaced {
.ant-btn.data-contract-latest-result-button { .ant-btn.ant-btn-default.data-contract-latest-result-button {
font-size: 14px; font-size: 14px;
padding: 6px 12px; padding: 6px 12px;
border-radius: 12px !important; border-radius: 12px;
font-weight: 600; font-weight: 600;
box-shadow: 0px 2px 2px -1px @grey-35, 0px 4px 6px -2px @grey-35, box-shadow: 0px 2px 2px -1px @grey-35, 0px 4px 6px -2px @grey-35,
0px 12px 16px -4px @grey-35; 0px 12px 16px -4px @grey-35;
@ -153,11 +153,6 @@
color: @orange-7; color: @orange-7;
border: 1px solid @orange-200; border: 1px solid @orange-200;
background-color: @orange-50; background-color: @orange-50;
&:hover {
border: 1px solid @red-19;
background-color: @red-2;
}
} }
&.running { &.running {

View File

@ -10,20 +10,15 @@
* 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 Icon, { PlusOutlined } from '@ant-design/icons'; import Icon from '@ant-design/icons';
import { Button, Card, Form, Typography } from 'antd'; import { Button, Card, Form, Typography } from 'antd';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useTranslation } from 'react-i18next'; 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 { EntityReference } from '../../../generated/type/entityReference'; import { FieldProp, FieldTypes } from '../../../interface/FormUtils.interface';
import {
FieldProp,
FieldTypes,
FormItemLayout,
} from '../../../interface/FormUtils.interface';
import { generateFormFields } from '../../../utils/formUtils'; import { generateFormFields } from '../../../utils/formUtils';
import { OwnerLabel } from '../../common/OwnerLabel/OwnerLabel.component'; import './contract-detail-form-tab.less';
export const ContractDetailFormTab: React.FC<{ export const ContractDetailFormTab: React.FC<{
initialValues?: Partial<DataContract>; initialValues?: Partial<DataContract>;
@ -34,8 +29,6 @@ export const ContractDetailFormTab: React.FC<{
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const owners = Form.useWatch<EntityReference[]>('owners', form);
const fields: FieldProp[] = [ const fields: FieldProp[] = [
{ {
label: t('label.contract-title'), label: t('label.contract-title'),
@ -47,6 +40,22 @@ export const ContractDetailFormTab: React.FC<{
'data-testid': 'contract-name', 'data-testid': 'contract-name',
}, },
}, },
{
label: t('label.owner-plural'),
name: 'owners',
id: 'root/owner',
type: FieldTypes.USER_TEAM_SELECT_INPUT,
required: false,
props: {
owner: initialValues?.owners,
hasPermission: true,
multiple: { user: true, team: false },
},
formItemProps: {
valuePropName: 'owners',
trigger: 'onUpdate',
},
},
{ {
label: t('label.description'), label: t('label.description'),
id: 'description', id: 'description',
@ -58,31 +67,6 @@ export const ContractDetailFormTab: React.FC<{
initialValue: initialValues?.description ?? '', initialValue: initialValues?.description ?? '',
}, },
}, },
{
label: t('label.owner-plural'),
id: 'owners',
name: 'owners',
type: FieldTypes.USER_TEAM_SELECT,
required: false,
props: {
owner: initialValues?.owners,
hasPermission: true,
children: (
<Button
data-testid="add-owner"
icon={<PlusOutlined style={{ color: 'white', fontSize: '12px' }} />}
size="small"
type="primary"
/>
),
multiple: { user: true, team: false },
},
formItemLayout: FormItemLayout.HORIZONTAL,
formItemProps: {
valuePropName: 'owners',
trigger: 'onUpdate',
},
},
]; ];
useEffect(() => { useEffect(() => {
@ -109,13 +93,11 @@ export const ContractDetailFormTab: React.FC<{
<div className="contract-form-content-container"> <div className="contract-form-content-container">
<Form <Form
className="contract-detail-form" className="new-form-style contract-detail-form"
form={form} form={form}
layout="vertical" layout="vertical"
onValuesChange={onChange}> onValuesChange={onChange}>
{generateFormFields(fields)} {generateFormFields(fields)}
{owners?.length > 0 && <OwnerLabel owners={owners} />}
</Form> </Form>
</div> </div>
</Card> </Card>

View File

@ -0,0 +1,19 @@
/*
* Copyright 2025 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.
*/
.contract-detail-form {
.block-editor-wrapper.block-editor-wrapper--bar-menu {
.om-block-editor {
height: 160px;
}
}
}

View File

@ -266,7 +266,6 @@ const ContractDetail: React.FC<{
setValidateLoading(true); setValidateLoading(true);
await validateContractById(contract.id); await validateContractById(contract.id);
showSuccessToast(t('message.contract-validation-trigger-successfully')); showSuccessToast(t('message.contract-validation-trigger-successfully'));
fetchLatestContractResults();
} catch (err) { } catch (err) {
showErrorToast(err as AxiosError); showErrorToast(err as AxiosError);
} finally { } finally {

View File

@ -246,7 +246,9 @@ export const ContractQualityFormTab: React.FC<{
}, },
}} }}
searchProps={{ searchProps={{
placeholder: t('label.search-by-name'), placeholder: t('label.search-by-type', {
type: t('label.name'),
}),
onSearch: (value) => { onSearch: (value) => {
fetchAllTests({ fetchAllTests({
offset: 0, offset: 0,

View File

@ -11,10 +11,11 @@
* limitations under the License. * limitations under the License.
*/ */
import { CloseOutlined } from '@ant-design/icons'; import Icon from '@ant-design/icons';
import { Space, Typography } from 'antd'; import { Space, Typography } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { isUndefined, toString } from 'lodash'; import { isUndefined, toString } from 'lodash';
import { ReactComponent as CloseOutlined } from '../../../assets/svg/close.svg';
import ProfilePicture from '../ProfilePicture/ProfilePicture'; import ProfilePicture from '../ProfilePicture/ProfilePicture';
import './user-tag.less'; import './user-tag.less';
import { UserTags, UserTagSize } from './UserTag.interface'; import { UserTags, UserTagSize } from './UserTag.interface';
@ -66,7 +67,14 @@ export const UserTag = ({
width={toString(width[size])} width={toString(width[size])}
/> />
<Typography.Text className={fontSizes[size]}>{name}</Typography.Text> <Typography.Text className={fontSizes[size]}>{name}</Typography.Text>
{closable && <CloseOutlined size={width[size]} onClick={onRemove} />} {closable && (
<Icon
component={CloseOutlined}
data-testid="close-icon"
size={width[size]}
onClick={onRemove}
/>
)}
</Space> </Space>
); );
}; };

View File

@ -63,9 +63,7 @@ describe('UserTag Component', () => {
it('calls onRemove when close icon is clicked', () => { it('calls onRemove when close icon is clicked', () => {
render(<UserTag {...userTagProps} />); render(<UserTag {...userTagProps} />);
const closeIcon = screen const closeIcon = screen.getByTestId('close-icon');
.getByTestId('user-tag')
.querySelector('.anticon-close');
// Simulate click on the close icon // Simulate click on the close icon
closeIcon && fireEvent.click(closeIcon); closeIcon && fireEvent.click(closeIcon);

View File

@ -51,12 +51,7 @@
.ant-select-item { .ant-select-item {
padding: 8px 12px; padding: 8px 12px;
// &:hover {
// background-color: @item-hover-bg;
// }
&.selected-option { &.selected-option {
// background-color: @primary-color-1;
color: @primary-color; color: @primary-color;
} }
@ -116,14 +111,6 @@
&.ant-select-open { &.ant-select-open {
.ant-select-selector { .ant-select-selector {
border-color: @primary-color; border-color: @primary-color;
// box-shadow: 0 0 0 2px fade(@primary-color, 20%);
}
}
&.ant-select-disabled {
.ant-select-selector {
// background-color: @disabled-bg;
// color: @disabled-color;
} }
} }
} }

View File

@ -12,6 +12,7 @@
*/ */
import Icon from '@ant-design/icons/lib/components/Icon'; import Icon from '@ant-design/icons/lib/components/Icon';
import { Popover, Space, Tabs, Typography } from 'antd'; import { Popover, Space, Tabs, Typography } from 'antd';
import classNames from 'classnames';
import { isArray, isEmpty, noop, toString } from 'lodash'; import { isArray, isEmpty, noop, toString } from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -64,6 +65,7 @@ export const UserTeamSelectableList = ({
previewSelected = false, previewSelected = false,
listHeight = ADD_USER_CONTAINER_HEIGHT, listHeight = ADD_USER_CONTAINER_HEIGHT,
tooltipText, tooltipText,
overlayClassName,
}: UserSelectDropdownProps) => { }: UserSelectDropdownProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [popupVisible, setPopupVisible] = useState(false); const [popupVisible, setPopupVisible] = useState(false);
@ -352,7 +354,10 @@ export const UserTeamSelectableList = ({
</FocusTrapWithContainer> </FocusTrapWithContainer>
} }
open={popupVisible} open={popupVisible}
overlayClassName="user-team-select-popover card-shadow" overlayClassName={classNames(
'user-team-select-popover card-shadow',
overlayClassName
)}
placement="bottomRight" placement="bottomRight"
showArrow={false} showArrow={false}
trigger="click" trigger="click"

View File

@ -31,4 +31,5 @@ export interface UserSelectDropdownProps {
previewSelected?: boolean; previewSelected?: boolean;
listHeight?: number; listHeight?: number;
tooltipText?: string; tooltipText?: string;
overlayClassName?: string;
} }

View File

@ -0,0 +1,172 @@
/*
* Copyright 2025 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 { Select } from 'antd';
import { noop } from 'lodash';
import type { CustomTagProps } from 'rc-select/lib/BaseSelect';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { EntityReference } from '../../../generated/entity/teams/user';
import { UserTag } from '../UserTag/UserTag.component';
import { UserTagSize } from '../UserTag/UserTag.interface';
import { UserTeamSelectableList } from '../UserTeamSelectableList/UserTeamSelectableList.component';
import { UserSelectDropdownProps } from '../UserTeamSelectableList/UserTeamSelectableList.interface';
import './user-team-selectable-list-search-input.less';
interface UserTeamSelectableListSearchProps extends UserSelectDropdownProps {
disabled?: boolean;
}
const UserTeamSelectableListSearchInput: React.FC<UserTeamSelectableListSearchProps> =
({
disabled,
hasPermission,
owner,
onUpdate = noop,
onClose,
multiple,
label,
previewSelected = false,
listHeight,
tooltipText,
}) => {
const [popoverVisible, setPopoverVisible] = useState(false);
const [selectedUsers, setSelectedUsers] = useState<EntityReference[]>([]);
const handleFocus = useCallback(() => {
setPopoverVisible(true);
}, []);
const handleClose = () => {
setPopoverVisible(false);
if (onClose) {
onClose();
}
};
const handlePopoverVisibleChange = (visible: boolean) => {
setPopoverVisible(visible);
if (!visible && onClose) {
onClose();
}
};
const handleUpdate = async (updatedUser?: EntityReference[]) => {
if (onUpdate) {
setSelectedUsers(updatedUser ?? []);
handleClose();
onUpdate(updatedUser);
}
};
const handleOnChangeSelect = (value: string[]) => {
if (onUpdate) {
const updatedUser = selectedUsers.filter((item) =>
value.includes(item.name ?? '')
);
setSelectedUsers(updatedUser ?? []);
handleClose();
onUpdate(updatedUser);
}
};
const selectedValues = useMemo(
() =>
selectedUsers
.map((user) => user.name)
.filter((name): name is string => Boolean(name)),
[selectedUsers]
);
const customTagRender = (props: CustomTagProps) => {
const { value, closable, onClose } = props;
const selectedAssignee = selectedUsers?.find(
(option) => option.name === value
);
const tagProps = {
id: selectedAssignee?.name ?? value,
name: selectedAssignee?.name ?? value,
closable: closable,
onRemove: onClose,
size: UserTagSize.small,
isTeam: selectedAssignee?.type === 'team',
className: 'assignee-tag',
};
return <UserTag {...tagProps} />;
};
const selectInput = useMemo(() => {
return (
<Select
showSearch
className="select-owners"
data-testid="select-owners"
defaultActiveFirstOption={false}
disabled={disabled}
filterOption={false}
mode="multiple"
notFoundContent={null}
suffixIcon={null}
tagRender={customTagRender}
value={selectedValues}
onChange={handleOnChangeSelect}
onFocus={handleFocus}
/>
);
}, [
disabled,
selectedValues,
customTagRender,
handleOnChangeSelect,
handleFocus,
]);
useEffect(() => {
setSelectedUsers(owner ?? []);
}, [owner]);
return (
<>
{popoverVisible && (
<UserTeamSelectableList
hasPermission={hasPermission}
label={label}
listHeight={listHeight}
multiple={multiple}
overlayClassName="user-team-selectable-list-search-input-popover"
owner={selectedUsers}
popoverProps={{
open: popoverVisible,
onOpenChange: handlePopoverVisibleChange,
trigger: 'click',
placement: 'bottomLeft',
}}
previewSelected={previewSelected}
tooltipText={tooltipText}
onClose={handleClose}
onUpdate={handleUpdate}>
{/* Have to pass the selectInput as children, so popover can become targetComponent
and popover don't overflow on it */}
{selectInput}
</UserTeamSelectableList>
)}
{/* Conditionally render the select input, to avoid the UserTeamSelectableList component
render unnecessarily */}
{!popoverVisible && selectInput}
</>
);
};
export default UserTeamSelectableListSearchInput;

View File

@ -0,0 +1,56 @@
/*
* Copyright 2025 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 (reference) url('../../../styles/variables.less');
.ant-popover.user-team-selectable-list-search-input-popover {
width: calc(50vw - 50px);
min-width: 260px;
max-width: 600px;
}
.select-owners {
.assignee-tag {
display: flex;
align-items: center;
margin-right: 8px;
background: @white;
padding: 2px 12px;
height: 28px;
border-radius: 6px;
gap: 4px;
border: 1px solid @grey-300;
.ant-typography {
font-size: 14px;
font-weight: 500;
color: @grey-700;
}
.anticon {
color: @grey-400;
font-weight: 500;
font-size: 10px;
}
.ant-select-selection-item-content {
display: flex;
align-items: center;
}
.ant-select-selection-item-remove {
margin-top: 2px;
}
}
}

View File

@ -39,6 +39,7 @@ export enum FieldTypes {
DESCRIPTION = 'description', DESCRIPTION = 'description',
TAG_SUGGESTION = 'tag_suggestion', TAG_SUGGESTION = 'tag_suggestion',
USER_TEAM_SELECT = 'user_team_select', USER_TEAM_SELECT = 'user_team_select',
USER_TEAM_SELECT_INPUT = 'user_team_select_input',
USER_MULTI_SELECT = 'user_multi_select', USER_MULTI_SELECT = 'user_multi_select',
COLOR_PICKER = 'color_picker', COLOR_PICKER = 'color_picker',
DOMAIN_SELECT = 'domain_select', DOMAIN_SELECT = 'domain_select',

View File

@ -916,7 +916,7 @@ export const migrateJsonLogic = (
}; };
return migrateNode(jsonLogic) as Record<string, unknown>; return migrateNode(jsonLogic) as Record<string, unknown>;
} };
export const getFieldsByKeys = ( export const getFieldsByKeys = (
keys: EntityReferenceFields[], keys: EntityReferenceFields[],

View File

@ -49,6 +49,7 @@ import { UserSelectableList } from '../components/common/UserSelectableList/User
import { UserSelectableListProps } from '../components/common/UserSelectableList/UserSelectableList.interface'; import { UserSelectableListProps } from '../components/common/UserSelectableList/UserSelectableList.interface';
import { UserTeamSelectableList } from '../components/common/UserTeamSelectableList/UserTeamSelectableList.component'; import { UserTeamSelectableList } from '../components/common/UserTeamSelectableList/UserTeamSelectableList.component';
import { UserSelectDropdownProps } from '../components/common/UserTeamSelectableList/UserTeamSelectableList.interface'; import { UserSelectDropdownProps } from '../components/common/UserTeamSelectableList/UserTeamSelectableList.interface';
import UserTeamSelectableListSearchInput from '../components/common/UserTeamSelectableListSearchInput/UserTeamSelectableListSearchInput.component';
import { HTTP_STATUS_CODE } from '../constants/Auth.constants'; import { HTTP_STATUS_CODE } from '../constants/Auth.constants';
import { import {
FieldProp, FieldProp,
@ -225,6 +226,17 @@ export const getField = (field: FieldProp) => {
break; break;
case FieldTypes.USER_TEAM_SELECT_INPUT:
{
fieldElement = (
<UserTeamSelectableListSearchInput
{...(props as unknown as UserSelectDropdownProps)}
/>
);
}
break;
case FieldTypes.USER_MULTI_SELECT: case FieldTypes.USER_MULTI_SELECT:
{ {
const { children, ...rest } = props; const { children, ...rest } = props;