Fix : User Profile edit widgets and Entity Popover Card (#20535)

* fix user profile edit widget chips

* minor fix for inherited domains tag

* fix entity popover card style issue
This commit is contained in:
Shrushti Polekar 2025-04-02 11:09:16 +05:30 committed by GitHub
parent 3ea4dbd10e
commit 9d19bd3eca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 203 additions and 43 deletions

View File

@ -28,6 +28,7 @@ import {
getEntityName, getEntityName,
getEntityReferenceListFromEntities, getEntityReferenceListFromEntities,
} from '../../../../utils/EntityUtils'; } from '../../../../utils/EntityUtils';
import { TagRenderer } from '../../../common/TagRenderer/TagRenderer';
import { PersonaSelectableListProps } from './PersonaSelectableList.interface'; import { PersonaSelectableListProps } from './PersonaSelectableList.interface';
export const PersonaListItemRenderer = (props: EntityReference) => { export const PersonaListItemRenderer = (props: EntityReference) => {
@ -210,6 +211,7 @@ export const PersonaSelectableList = ({
popupClassName="persona-custom-dropdown-class" popupClassName="persona-custom-dropdown-class"
ref={dropdownRef as any} ref={dropdownRef as any}
style={{ width: '100%' }} style={{ width: '100%' }}
tagRender={TagRenderer}
onChange={(selectedIds) => { onChange={(selectedIds) => {
const selectedPersonasList = selectOptions.filter((persona) => const selectedPersonasList = selectOptions.filter((persona) =>
selectedIds.includes(persona.id) selectedIds.includes(persona.id)

View File

@ -21,6 +21,7 @@ import { TeamHierarchy } from '../../../../generated/entity/teams/teamHierarchy'
import { getTeamsHierarchy } from '../../../../rest/teamsAPI'; import { getTeamsHierarchy } from '../../../../rest/teamsAPI';
import { getEntityName } from '../../../../utils/EntityUtils'; import { getEntityName } from '../../../../utils/EntityUtils';
import { showErrorToast } from '../../../../utils/ToastUtils'; import { showErrorToast } from '../../../../utils/ToastUtils';
import { TagRenderer } from '../../../common/TagRenderer/TagRenderer';
import { TeamsSelectableProps } from './TeamsSelectable.interface'; import { TeamsSelectableProps } from './TeamsSelectable.interface';
const TeamsSelectableNew = forwardRef<any, TeamsSelectableProps>( const TeamsSelectableNew = forwardRef<any, TeamsSelectableProps>(
@ -124,6 +125,7 @@ const TeamsSelectableNew = forwardRef<any, TeamsSelectableProps>(
ref={ref as any} ref={ref as any}
showCheckedStrategy={TreeSelect.SHOW_CHILD} showCheckedStrategy={TreeSelect.SHOW_CHILD}
style={{ width: '100%' }} style={{ width: '100%' }}
tagRender={TagRenderer}
treeData={teamsTree} treeData={teamsTree}
treeLine={{ showLeafIcon }} treeLine={{ showLeafIcon }}
treeNodeFilterProp="title" treeNodeFilterProp="title"

View File

@ -231,7 +231,7 @@ const UserProfileRoles = ({
dropdownMatchSelectWidth={false} dropdownMatchSelectWidth={false}
filterOption={handleSearchFilterOption} filterOption={handleSearchFilterOption}
loading={isLoading} loading={isLoading}
maxTagCount={4} maxTagCount={3}
maxTagPlaceholder={(omittedValues) => ( maxTagPlaceholder={(omittedValues) => (
<span className="max-tag-text"> <span className="max-tag-text">
{t('label.plus-count-more', { {t('label.plus-count-more', {

View File

@ -151,7 +151,7 @@ const UserProfileTeams = ({
<TeamsSelectableNew <TeamsSelectableNew
filterJoinable filterJoinable
handleDropdownChange={handleDropdownChange} handleDropdownChange={handleDropdownChange}
maxValueCount={4} maxValueCount={3}
ref={teamsSelectableRef} ref={teamsSelectableRef}
selectedTeams={selectedTeams} selectedTeams={selectedTeams}
onSelectionChange={setSelectedTeams} onSelectionChange={setSelectedTeams}

View File

@ -332,7 +332,7 @@
.ant-select-item-option-selected, .ant-select-item-option-selected,
.ant-select-selection-item { .ant-select-selection-item {
font-size: 14px; font-size: 12px;
} }
.ant-select:not(.ant-select-customize-input) { .ant-select:not(.ant-select-customize-input) {
@ -468,3 +468,36 @@
vertical-align: text-top; vertical-align: text-top;
} }
} }
.selected-chip-tag-remove {
margin-left: 4px;
font-size: 12px;
cursor: pointer;
padding: 0 4px;
border: none;
background: none;
svg {
width: 10px;
height: 10px;
color: @grey-4;
}
}
.domain-link {
max-width: 180px;
display: inline-block;
.domain-link-name {
color: @text-color;
font-weight: 400;
&:hover {
color: @blue-9;
}
}
}
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 140px;
}

View File

@ -51,7 +51,9 @@ const Chip = ({
item.fullyQualifiedName ?? '' item.fullyQualifiedName ?? ''
)}> )}>
{icon} {icon}
<Typography.Text className="text-left chip-tag-link"> <Typography.Text
className="text-left chip-tag-link chip-name"
ellipsis={{ tooltip: getEntityName(item) }}>
{getEntityName(item)} {getEntityName(item)}
</Typography.Text> </Typography.Text>
</Link> </Link>
@ -73,7 +75,7 @@ const Chip = ({
return ( return (
<Row <Row
wrap wrap
className="align-middle d-flex flex-col flex-start justify-center" className="align-middle d-flex flex-col flex-start justify-center chip-container"
data-testid="chip-container" data-testid="chip-container"
gutter={[20, 0]}> gutter={[20, 0]}>
{(isExpanded ? data : data.slice(0, USER_DATA_SIZE)).map(getChipElement)} {(isExpanded ? data : data.slice(0, USER_DATA_SIZE)).map(getChipElement)}

View File

@ -11,26 +11,31 @@
* limitations under the License. * limitations under the License.
*/ */
@import (reference) url('../../../styles/variables.less'); @import (reference) url('../../../styles/variables.less');
.chip-container {
width: 180px;
.chip-tag-link { .chip-tag-link {
font-weight: 400; font-weight: 400;
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
color: @text-color; color: @text-color;
&:hover { &:hover {
color: @blue-9; color: @blue-9;
text-decoration: none; text-decoration: none;
}
overflow: hidden;
text-overflow: ellipsis;
} }
}
.chip-text { .chip-text {
color: @blue-9;
&:hover {
color: @blue-9; color: @blue-9;
} &:hover {
&::after { color: @blue-9;
animation: none !important; }
display: none !important; &::after {
animation: none !important;
display: none !important;
}
} }
} }
.no-data-chip-placeholder { .no-data-chip-placeholder {

View File

@ -134,7 +134,8 @@ export const DomainLabelNew = ({
domain, domain,
domainDisplayName, domainDisplayName,
showDomainHeading, showDomainHeading,
'chip-tag-link' 'chip-tag-link',
true
)} )}
{inheritedIcon && ( {inheritedIcon && (
<div className="d-flex">{inheritedIcon}</div> <div className="d-flex">{inheritedIcon}</div>

View File

@ -91,7 +91,7 @@ const DomainSelectableListNew = ({
} }
}; };
const [popoverHeight, setPopoverHeight] = useState<number>(136); const [popoverHeight, setPopoverHeight] = useState<number>(156);
const dropdownRef = useRef<HTMLDivElement | null>(null); const dropdownRef = useRef<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {

View File

@ -37,6 +37,7 @@ import { getEntityReferenceFromEntity } from '../../../utils/EntityUtils';
import { findItemByFqn } from '../../../utils/GlossaryUtils'; import { findItemByFqn } from '../../../utils/GlossaryUtils';
import { showErrorToast } from '../../../utils/ToastUtils'; import { showErrorToast } from '../../../utils/ToastUtils';
import Loader from '../Loader/Loader'; import Loader from '../Loader/Loader';
import { TagRenderer } from '../TagRenderer/TagRenderer';
import './domain-selectable.less'; import './domain-selectable.less';
import { import {
DomainSelectableTreeProps, DomainSelectableTreeProps,
@ -227,7 +228,7 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
className="custom-domain-edit-select" className="custom-domain-edit-select"
dropdownRender={() => treeContent} dropdownRender={() => treeContent}
dropdownStyle={{ maxHeight: '200px' }} dropdownStyle={{ maxHeight: '200px' }}
maxTagCount={2} maxTagCount={3}
maxTagPlaceholder={(omittedValues) => ( maxTagPlaceholder={(omittedValues) => (
<span className="max-tag-text"> <span className="max-tag-text">
{t('label.plus-count-more', { count: omittedValues.length })} {t('label.plus-count-more', { count: omittedValues.length })}
@ -241,7 +242,7 @@ const DomainSelectablTreeNew: FC<DomainSelectableTreeProps> = ({
placeholder="Select a domain" placeholder="Select a domain"
popupClassName="domain-custom-dropdown-class" popupClassName="domain-custom-dropdown-class"
ref={dropdownRef as any} ref={dropdownRef as any}
style={{}} tagRender={TagRenderer}
value={ value={
selectedDomains selectedDomains
?.map((domain) => domain.fullyQualifiedName) ?.map((domain) => domain.fullyQualifiedName)

View File

@ -262,7 +262,7 @@ const EntityPopOverCard: FC<Props> = ({
}) => { }) => {
return ( return (
<Popover <Popover
align={{ targetOffset: [0, -10] }} align={{ targetOffset: [0, 10] }}
content={ content={
<PopoverContent <PopoverContent
entityFQN={entityFQN} entityFQN={entityFQN}

View File

@ -25,4 +25,8 @@
top: 0; top: 0;
right: 0; right: 0;
} }
&.ant-popover-placement-top {
padding-top: 0px;
padding-bottom: 0px;
}
} }

View File

@ -0,0 +1,60 @@
/*
* 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 { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { TagRenderer } from './TagRenderer';
describe('TagRenderer', () => {
const mockOnClose = jest.fn();
const defaultProps = {
label: 'Test Label',
value: 'test-value',
closable: true,
onClose: mockOnClose,
disabled: false,
onMouseDown: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should render the label', () => {
render(<TagRenderer {...defaultProps} />);
expect(screen.getByText('Test Label')).toBeInTheDocument();
});
it('should truncate long labels and add ellipsis', () => {
const longLabel = 'test persona test test persona 5';
render(<TagRenderer {...defaultProps} label={longLabel} />);
expect(screen.getByText('test persona...')).toBeInTheDocument();
expect(screen.getByTitle(longLabel)).toBeInTheDocument();
});
it('should render close button when closable is true', () => {
render(<TagRenderer {...defaultProps} />);
expect(screen.getByRole('button')).toBeInTheDocument();
});
it('should call onClose when close button is clicked', () => {
render(<TagRenderer {...defaultProps} />);
const closeButton = screen.getByRole('button');
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,34 @@
/*
* 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 { CloseOutlined } from '@ant-design/icons';
import type { CustomTagProps } from 'rc-select/lib/BaseSelect';
import React from 'react';
export const TagRenderer = (props: CustomTagProps) => {
const { label, closable, onClose } = props;
const displayLabel =
typeof label === 'string' && label.length > 12
? `${label.substring(0, 12)}...`
: label;
return (
<span className="ant-select-selection-item" title={(label as string) ?? ''}>
{displayLabel}
{closable && (
<button className="selected-chip-tag-remove" onClick={onClose}>
<CloseOutlined />
</button>
)}
</span>
);
};

View File

@ -229,23 +229,39 @@ export const renderDomainLink = (
domain: EntityReference, domain: EntityReference,
domainDisplayName: ReactNode, domainDisplayName: ReactNode,
showDomainHeading: boolean, showDomainHeading: boolean,
textClassName?: string textClassName?: string,
) => ( trimLink?: boolean
<Tooltip title={domainDisplayName ?? getEntityName(domain)}> ) => {
<Link const displayName = isUndefined(domainDisplayName)
className={classNames( ? getEntityName(domain)
'no-underline domain-link domain-link-text font-medium', : domainDisplayName;
{ 'text-sm': !showDomainHeading },
textClassName return (
)} <Tooltip title={domainDisplayName ?? getEntityName(domain)}>
data-testid="domain-link" <Link
to={getDomainPath(domain?.fullyQualifiedName)}> className={classNames(
{isUndefined(domainDisplayName) 'no-underline domain-link domain-link-text font-medium',
? getEntityName(domain) {
: domainDisplayName} 'text-sm': !showDomainHeading,
</Link> 'text-truncate': trimLink,
</Tooltip> },
); textClassName
)}
data-testid="domain-link"
to={getDomainPath(domain?.fullyQualifiedName)}>
{trimLink ? (
<Typography.Text
className="domain-link-name"
ellipsis={{ tooltip: false }}>
{displayName}
</Typography.Text>
) : (
<>{displayName}</>
)}
</Link>
</Tooltip>
);
};
export const initializeDomainEntityRef = ( export const initializeDomainEntityRef = (
domains: EntityReference[], domains: EntityReference[],