mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-25 17:37:57 +00:00
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:
parent
3ea4dbd10e
commit
9d19bd3eca
@ -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)
|
||||||
|
@ -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"
|
||||||
|
@ -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', {
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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)}
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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)
|
||||||
|
@ -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}
|
||||||
|
@ -25,4 +25,8 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
&.ant-popover-placement-top {
|
||||||
|
padding-top: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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[],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user