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

View File

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

View File

@ -10,20 +10,15 @@
* See the License for the specific language governing permissions and
* 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 { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as RightIcon } from '../../../assets/svg/right-arrow.svg';
import { DataContract } from '../../../generated/entity/data/dataContract';
import { EntityReference } from '../../../generated/type/entityReference';
import {
FieldProp,
FieldTypes,
FormItemLayout,
} from '../../../interface/FormUtils.interface';
import { FieldProp, FieldTypes } from '../../../interface/FormUtils.interface';
import { generateFormFields } from '../../../utils/formUtils';
import { OwnerLabel } from '../../common/OwnerLabel/OwnerLabel.component';
import './contract-detail-form-tab.less';
export const ContractDetailFormTab: React.FC<{
initialValues?: Partial<DataContract>;
@ -34,8 +29,6 @@ export const ContractDetailFormTab: React.FC<{
const { t } = useTranslation();
const [form] = Form.useForm();
const owners = Form.useWatch<EntityReference[]>('owners', form);
const fields: FieldProp[] = [
{
label: t('label.contract-title'),
@ -47,6 +40,22 @@ export const ContractDetailFormTab: React.FC<{
'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'),
id: 'description',
@ -58,31 +67,6 @@ export const ContractDetailFormTab: React.FC<{
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(() => {
@ -109,13 +93,11 @@ export const ContractDetailFormTab: React.FC<{
<div className="contract-form-content-container">
<Form
className="contract-detail-form"
className="new-form-style contract-detail-form"
form={form}
layout="vertical"
onValuesChange={onChange}>
{generateFormFields(fields)}
{owners?.length > 0 && <OwnerLabel owners={owners} />}
</Form>
</div>
</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);
await validateContractById(contract.id);
showSuccessToast(t('message.contract-validation-trigger-successfully'));
fetchLatestContractResults();
} catch (err) {
showErrorToast(err as AxiosError);
} finally {

View File

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

View File

@ -11,10 +11,11 @@
* limitations under the License.
*/
import { CloseOutlined } from '@ant-design/icons';
import Icon from '@ant-design/icons';
import { Space, Typography } from 'antd';
import classNames from 'classnames';
import { isUndefined, toString } from 'lodash';
import { ReactComponent as CloseOutlined } from '../../../assets/svg/close.svg';
import ProfilePicture from '../ProfilePicture/ProfilePicture';
import './user-tag.less';
import { UserTags, UserTagSize } from './UserTag.interface';
@ -66,7 +67,14 @@ export const UserTag = ({
width={toString(width[size])}
/>
<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>
);
};

View File

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

View File

@ -51,12 +51,7 @@
.ant-select-item {
padding: 8px 12px;
// &:hover {
// background-color: @item-hover-bg;
// }
&.selected-option {
// background-color: @primary-color-1;
color: @primary-color;
}
@ -116,14 +111,6 @@
&.ant-select-open {
.ant-select-selector {
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 { Popover, Space, Tabs, Typography } from 'antd';
import classNames from 'classnames';
import { isArray, isEmpty, noop, toString } from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -64,6 +65,7 @@ export const UserTeamSelectableList = ({
previewSelected = false,
listHeight = ADD_USER_CONTAINER_HEIGHT,
tooltipText,
overlayClassName,
}: UserSelectDropdownProps) => {
const { t } = useTranslation();
const [popupVisible, setPopupVisible] = useState(false);
@ -352,7 +354,10 @@ export const UserTeamSelectableList = ({
</FocusTrapWithContainer>
}
open={popupVisible}
overlayClassName="user-team-select-popover card-shadow"
overlayClassName={classNames(
'user-team-select-popover card-shadow',
overlayClassName
)}
placement="bottomRight"
showArrow={false}
trigger="click"

View File

@ -31,4 +31,5 @@ export interface UserSelectDropdownProps {
previewSelected?: boolean;
listHeight?: number;
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',
TAG_SUGGESTION = 'tag_suggestion',
USER_TEAM_SELECT = 'user_team_select',
USER_TEAM_SELECT_INPUT = 'user_team_select_input',
USER_MULTI_SELECT = 'user_multi_select',
COLOR_PICKER = 'color_picker',
DOMAIN_SELECT = 'domain_select',

View File

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

View File

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