From 9300f127c77e5c74be609bc09087a8765f4616fb Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Mon, 6 Apr 2026 14:33:06 +0530 Subject: [PATCH 1/7] fix(teams): prevent team name truncation and align action buttons on header row --- .../Team/TeamDetails/TeamDetailsV1.tsx | 77 +++++++++---------- .../TeamsHeadingLabel.component.tsx | 33 +++++--- 2 files changed, 59 insertions(+), 51 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx index d15d5b5212b5..64b07f036251 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx @@ -946,15 +946,10 @@ const TeamDetailsV1 = ({ const teamsCollapseHeader = useMemo( () => ( <> - - - {!isOrganization && ( - - )} -
+
+ {!isOrganization && } +
+
@@ -970,45 +965,47 @@ const TeamDetailsV1 = ({ title={t('label.team-plural')} />
- - - {teamActionButton} - {!isOrganization ? ( - entityPermissions.EditAll && ( + + {teamActionButton} + {!isOrganization ? ( + entityPermissions.EditAll && ( + + ) + ) : ( - ) - ) : ( - - )} - - + )} + +
+
<> {heading ? ( - - {heading} - + +
+ + {heading} + +
+
) : ( - {t('label.no-entity', { entity: t('label.display-name'), })} - + )} {(hasAccess || isCurrentTeamOwner) && !currentTeam.deleted && ( {teamHeadingRender}; + return ( +
+ {teamHeadingRender} +
+ ); }; export default TeamsHeadingLabel; From a5f6d3534869504d1d7ef3ba7938f4147e6df650 Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Mon, 6 Apr 2026 14:33:16 +0530 Subject: [PATCH 2/7] lint --- .../TeamsHeaderSection/TeamsHeadingLabel.component.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.component.tsx index f1cd78151f97..17f0b43d6293 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.component.tsx @@ -200,7 +200,9 @@ const TeamsHeadingLabel = ({ return (
+ className={`d-flex items-center tw:gap-1${ + isHeadingEditing ? '' : ' tw:max-w-1/3' + }`}> {teamHeadingRender}
); From 0b15356b0ce56138774053c6d5e7d44d75aec5e5 Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Mon, 6 Apr 2026 15:51:11 +0530 Subject: [PATCH 3/7] fix empty placeholder bug & description bg change --- .../Team/TeamDetails/TeamDetailsV1.tsx | 9 +++- .../Team/TeamDetails/TeamHierarchy.tsx | 45 +++++++++++++------ .../Settings/Team/TeamDetails/teams.less | 44 +++++++++++++++++- 3 files changed, 82 insertions(+), 16 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx index 64b07f036251..9362ba1fe9b0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx @@ -636,7 +636,13 @@ const TeamDetailsV1 = ({ ); } - return currentTeam.childrenCount === 0 && !searchTerm ? ( + const showEmptyTeamPlaceholder = + isEmpty(searchTerm) && + isEmpty(childTeamList) && + (currentTeam.childrenCount ?? 0) === 0 && + !isTeamBasicDataLoading; + + return showEmptyTeamPlaceholder ? ( } @@ -696,6 +702,7 @@ const TeamDetailsV1 = ({ entityPermissions.Create, isFetchingAllTeamAdvancedDetails, isSearchLoading, + isTeamBasicDataLoading, onTeamExpand, handleAddTeamButtonClick, handleTeamSearch, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx index b00f32c531aa..85f784f7e65d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx @@ -11,7 +11,15 @@ * limitations under the License. */ -import { Button, Modal, Skeleton, Space, Switch, Typography } from 'antd'; +import { + Button, + Modal, + Skeleton, + Space, + Switch, + Tooltip, + Typography, +} from 'antd'; import { ColumnsType, TableProps } from 'antd/lib/table'; import { ExpandableConfig } from 'antd/lib/table/interface'; import { AxiosError } from 'axios'; @@ -82,18 +90,29 @@ const TeamHierarchy: FC = ({ { title: t('label.team-plural'), dataIndex: 'teams', - className: 'whitespace-nowrap', + className: 'teams-hierarchy-name-column', key: 'teams', - render: (_, record) => ( - - {stringToHTML( - highlightSearchText(getEntityName(record), searchTerm) - )} - - ), + width: '32%', + render: (_, record) => { + const displayName = getEntityName(record); + + return ( + + + + + {stringToHTML(highlightSearchText(displayName, searchTerm))} + + + + + ); + }, }, { title: t('label.type'), @@ -152,7 +171,7 @@ const TeamHierarchy: FC = ({ }, ...descriptionTableObject({ width: 300 }), ]; - }, [data, isFetchingAllTeamAdvancedDetails, onTeamExpand, teamAssetCounts]); + }, [isFetchingAllTeamAdvancedDetails, searchTerm, t, teamAssetCounts]); const handleTableHover = useCallback( (value: boolean) => setIsTableHovered(value), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/teams.less b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/teams.less index f64902d03b7d..9700c8d73b9d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/teams.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/teams.less @@ -42,6 +42,43 @@ } .teams-list-table { + .ant-table-content > table, + .ant-table-container .ant-table-content > table { + table-layout: fixed; + width: 100%; + } + + td.teams-hierarchy-name-column.ant-table-cell-with-append { + display: flex; + flex-wrap: nowrap; + align-items: center; + gap: 4px; + } + + .teams-hierarchy-team-name-cell { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + } + + .teams-hierarchy-team-name-tooltip-trigger { + display: block; + min-width: 0; + max-width: 100%; + } + + .teams-hierarchy-team-name-link { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + th.teams-hierarchy-name-column { + overflow: hidden; + text-overflow: ellipsis; + } + .ant-table-thead { tr > th { border-right: 0px !important; @@ -85,11 +122,14 @@ } .teams-profile-container { + .ant-card { + background: white; + } .collapse-panel-container { padding: 20px; } - .ant-card { - background: none; + .ant-card-head { + background: white; } .ant-collapse-header, From e2d0007f82fb45ec8ed3579401ec0c921464ebcd Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Mon, 6 Apr 2026 16:02:35 +0530 Subject: [PATCH 4/7] address gitar --- .../TeamsHeadingLabel.component.tsx | 21 +++++----- .../TeamsHeadingLabel.test.tsx | 39 +++++++++++++++++++ 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.component.tsx index 17f0b43d6293..da52a3e40ab7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.component.tsx @@ -126,18 +126,15 @@ const TeamsHeadingLabel = ({ <> <> {heading ? ( - -
- - {heading} - -
-
+ + {heading} + ) : ( { + const actual = jest.requireActual('@openmetadata/ui-core-components'); + + const TypographyMock = (props: { + as?: ElementType; + children?: ReactNode; + className?: string; + 'data-testid'?: string; + ellipsis?: boolean | { rows?: number; tooltip?: ReactNode }; + }) => { + const { + as: Component = 'span', + children, + className, + 'data-testid': dataTestId, + ellipsis, + } = props; + const truncateClass = + ellipsis === true || (typeof ellipsis === 'object' && ellipsis) + ? ' tw:truncate' + : ''; + + return createElement( + Component, + { + className: `${className ?? ''}${truncateClass}`.trim() || undefined, + 'data-testid': dataTestId, + }, + children + ); + }; + + return { + ...actual, + Typography: TypographyMock, + }; +}); + jest.mock('../../../../../hooks/authHooks', () => ({ useAuth: jest.fn().mockReturnValue({ isAdminUser: true }), })); From c66ef2d039f9b26b38185030df7cc6db6a800731 Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Mon, 6 Apr 2026 23:29:57 +0530 Subject: [PATCH 5/7] fix failing spec --- .../src/main/resources/ui/playwright/utils/dragDrop.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/dragDrop.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/dragDrop.ts index 8fc97411c1b3..a80b5e7a2795 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/dragDrop.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/dragDrop.ts @@ -37,10 +37,10 @@ export const dragAndDropElement = async ( }; export const openDragDropDropdown = async (page: Page, name: string) => { - const dropdownIcon = page.locator( - `[data-row-key=${name}] > .whitespace-nowrap > [data-testid="expand-icon"] > svg` - ); - await dropdownIcon.click(); + await page + .locator(`[data-row-key="${name}"]`) + .getByTestId('expand-icon') + .click(); }; export const confirmationDragAndDropTeam = async ( From e6fcad73479aac3e327789c7ed5355792f978c59 Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Tue, 7 Apr 2026 12:16:31 +0530 Subject: [PATCH 6/7] refactor code to remove core-component depedency --- .../Team/TeamDetails/TeamDetailsV1.tsx | 12 +- .../Team/TeamDetails/TeamHierarchy.test.tsx | 26 ++- .../Team/TeamDetails/TeamHierarchy.tsx | 42 +--- .../TeamHierarchyNameCell.test.tsx | 203 ++++++++++++++++++ .../TeamHierarchyNameCell.tsx | 75 +++++++ .../TeamsHeadingLabel.component.tsx | 36 ++-- .../TeamsHeadingLabel.test.tsx | 53 +---- .../TeamsInfo.component.tsx | 16 +- .../Settings/Team/TeamDetails/teams.less | 17 +- 9 files changed, 353 insertions(+), 127 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.test.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx index 9362ba1fe9b0..057e851f82cd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx @@ -956,7 +956,7 @@ const TeamDetailsV1 = ({
{!isOrganization && }
-
+
@@ -967,10 +967,12 @@ const TeamDetailsV1 = ({ updateTeamHandler={updateTeamHandler} /> - +
+ +
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.test.tsx index 337821155d61..f9847a9229d2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.test.tsx @@ -12,6 +12,7 @@ */ import { fireEvent, render, screen } from '@testing-library/react'; +import { forwardRef, type ReactNode } from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { MemoryRouter } from 'react-router-dom'; @@ -26,6 +27,8 @@ import TeamHierarchy from './TeamHierarchy'; const teamHierarchyPropsData: TeamHierarchyProps = { data: MOCK_TABLE_DATA, currentTeam: MOCK_CURRENT_TEAM, + isSearchLoading: false, + isTeamBasicDataLoading: false, onTeamExpand: jest.fn(), isFetchingAllTeamAdvancedDetails: false, showDeletedTeam: false, @@ -38,12 +41,23 @@ const teamHierarchyPropsData: TeamHierarchyProps = { const mockShowErrorToast = jest.fn(); -// mock library imports -jest.mock('react-router-dom', () => ({ - Link: jest - .fn() - .mockImplementation(({ children }) => {children}), -})); +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + + const MockLink = forwardRef< + HTMLAnchorElement, + { children?: ReactNode; to?: unknown } + >(({ children, to: _to, ...props }, ref) => ( + + {children} + + )); + + return { + ...actual, + Link: MockLink, + }; +}); jest.mock('../../../../utils/TeamUtils', () => ({ getMovedTeamData: jest.fn().mockReturnValue([]), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx index 85f784f7e65d..c45f34549ddb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx @@ -11,15 +11,7 @@ * limitations under the License. */ -import { - Button, - Modal, - Skeleton, - Space, - Switch, - Tooltip, - Typography, -} from 'antd'; +import { Button, Modal, Skeleton, Space, Switch, Typography } from 'antd'; import { ColumnsType, TableProps } from 'antd/lib/table'; import { ExpandableConfig } from 'antd/lib/table/interface'; import { AxiosError } from 'axios'; @@ -28,19 +20,13 @@ import { compare } from 'fast-json-patch'; import { isEmpty, isUndefined } from 'lodash'; import { FC, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; import { TABLE_CONSTANTS } from '../../../../constants/Teams.constants'; import { TabSpecificField } from '../../../../enums/entity.enum'; import { Team } from '../../../../generated/entity/teams/team'; import { Include } from '../../../../generated/type/include'; import { getTeamByName, patchTeamDetail } from '../../../../rest/teamsAPI'; import { Transi18next } from '../../../../utils/CommonUtils'; -import { - getEntityName, - highlightSearchText, -} from '../../../../utils/EntityUtils'; -import { getTeamsWithFqnPath } from '../../../../utils/RouterUtils'; -import { stringToHTML } from '../../../../utils/StringsUtils'; +import { getEntityName } from '../../../../utils/EntityUtils'; import { descriptionTableObject } from '../../../../utils/TableColumn.util'; import { getTableExpandableConfig } from '../../../../utils/TableUtils'; import { isDropRestricted } from '../../../../utils/TeamUtils'; @@ -50,6 +36,7 @@ import FilterTablePlaceHolder from '../../../common/ErrorWithPlaceholder/FilterT import Table from '../../../common/Table/Table'; import { MovedTeamProps, TeamHierarchyProps } from './team.interface'; import './teams.less'; +import { TeamHierarchyNameCell } from './TeamsHeaderSection/TeamHierarchyNameCell'; const TeamHierarchy: FC = ({ currentTeam, @@ -93,26 +80,9 @@ const TeamHierarchy: FC = ({ className: 'teams-hierarchy-name-column', key: 'teams', width: '32%', - render: (_, record) => { - const displayName = getEntityName(record); - - return ( - - - - - {stringToHTML(highlightSearchText(displayName, searchTerm))} - - - - - ); - }, + render: (_, record) => ( + + ), }, { title: t('label.type'), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.test.tsx new file mode 100644 index 000000000000..8b71cb9b8da9 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.test.tsx @@ -0,0 +1,203 @@ +/* + * Copyright 2026 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 { render, screen } from '@testing-library/react'; +import type { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { Team } from '../../../../../generated/entity/teams/team'; +import { + getEntityName, + highlightSearchText +} from '../../../../../utils/EntityUtils'; +import { getTeamsWithFqnPath } from '../../../../../utils/RouterUtils'; +import { stringToHTML } from '../../../../../utils/StringsUtils'; +import { TeamHierarchyNameCell } from './TeamHierarchyNameCell'; + +jest.mock('antd', () => { + const actual = jest.requireActual('antd'); + + return { + ...actual, + Tooltip: ({ + children, + title, + }: { + children: ReactNode; + title?: ReactNode; + }) => ( + + {children} + + ), + }; +}); + +jest.mock('../../../../../utils/EntityUtils', () => ({ + getEntityName: jest.fn(), + highlightSearchText: jest.fn((text: string) => text), +})); + +jest.mock('../../../../../utils/RouterUtils', () => ({ + getTeamsWithFqnPath: jest.fn(), +})); + +jest.mock('../../../../../utils/StringsUtils', () => ({ + stringToHTML: jest.fn((html: string) => html), +})); + +const mockGetEntityName = getEntityName as jest.MockedFunction; +const mockHighlightSearchText = + highlightSearchText as jest.MockedFunction; +const mockGetTeamsWithFqnPath = + getTeamsWithFqnPath as jest.MockedFunction; +const mockStringToHTML = stringToHTML as jest.MockedFunction; + +const mockTeam = { + fullyQualifiedName: 'Organization.Engineering', + id: '49d060a2-ad14-48a7-840a-836cd99aaffb', + name: 'Engineering', +} as Team; + +const renderCell = (props: { record: Team; searchTerm?: string }) => + render( + + + + ); + +describe('TeamHierarchyNameCell', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetEntityName.mockReturnValue('Engineering'); + mockGetTeamsWithFqnPath.mockReturnValue( + '/settings/members/teams/Engineering' + ); + jest + .spyOn(HTMLAnchorElement.prototype, 'scrollWidth', 'get') + .mockReturnValue(100); + jest + .spyOn(HTMLAnchorElement.prototype, 'clientWidth', 'get') + .mockReturnValue(100); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('renders team link with expected test id and route', () => { + renderCell({ record: mockTeam }); + + const link = screen.getByTestId('team-name-Engineering'); + + expect(link).toHaveClass('teams-hierarchy-team-name-link'); + expect(link).toHaveAttribute('href', '/settings/members/teams/Engineering'); + expect(mockGetTeamsWithFqnPath).toHaveBeenCalledWith( + 'Organization.Engineering' + ); + }); + + it('uses team name when fullyQualifiedName is missing', () => { + const teamWithoutFqn = { + name: 'LocalTeam', + } as Team; + + mockGetEntityName.mockReturnValue('LocalTeam'); + mockGetTeamsWithFqnPath.mockReturnValue('/teams/LocalTeam'); + + renderCell({ record: teamWithoutFqn }); + + expect(mockGetTeamsWithFqnPath).toHaveBeenCalledWith('LocalTeam'); + expect(screen.getByTestId('team-name-LocalTeam')).toBeInTheDocument(); + }); + + it('passes display name and search term through highlight and stringToHTML', () => { + mockHighlightSearchText.mockReturnValue('Engineering'); + + renderCell({ record: mockTeam, searchTerm: 'Eng' }); + + expect(mockHighlightSearchText).toHaveBeenCalledWith('Engineering', 'Eng'); + expect(mockStringToHTML).toHaveBeenCalledWith('Engineering'); + }); + + it('does not wrap with Tooltip when text is not truncated', () => { + jest + .spyOn(HTMLAnchorElement.prototype, 'scrollWidth', 'get') + .mockReturnValue(80); + jest + .spyOn(HTMLAnchorElement.prototype, 'clientWidth', 'get') + .mockReturnValue(100); + + renderCell({ record: mockTeam }); + + expect( + screen.queryByTestId('hierarchy-name-tooltip') + ).not.toBeInTheDocument(); + expect(screen.getByTestId('team-name-Engineering')).toBeInTheDocument(); + }); + + it('wraps link with Tooltip when text is truncated', () => { + jest + .spyOn(HTMLAnchorElement.prototype, 'scrollWidth', 'get') + .mockReturnValue(400); + jest + .spyOn(HTMLAnchorElement.prototype, 'clientWidth', 'get') + .mockReturnValue(50); + + renderCell({ record: mockTeam }); + + const tooltip = screen.getByTestId('hierarchy-name-tooltip'); + + expect(tooltip).toHaveAttribute('data-tooltip-title', 'Engineering'); + expect( + tooltip.querySelector('[data-testid="team-name-Engineering"]') + ).toBeInTheDocument(); + }); + + it('re-evaluates truncation when display name changes', () => { + jest + .spyOn(HTMLAnchorElement.prototype, 'scrollWidth', 'get') + .mockReturnValue(400); + jest + .spyOn(HTMLAnchorElement.prototype, 'clientWidth', 'get') + .mockReturnValue(50); + + const { rerender } = render( + + + + ); + + expect(screen.getByTestId('hierarchy-name-tooltip')).toBeInTheDocument(); + + mockGetEntityName.mockReturnValue('Short'); + jest + .spyOn(HTMLAnchorElement.prototype, 'scrollWidth', 'get') + .mockReturnValue(40); + jest + .spyOn(HTMLAnchorElement.prototype, 'clientWidth', 'get') + .mockReturnValue(100); + + rerender( + + + + ); + + expect( + screen.queryByTestId('hierarchy-name-tooltip') + ).not.toBeInTheDocument(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.tsx new file mode 100644 index 000000000000..4deefc8bcefd --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.tsx @@ -0,0 +1,75 @@ +import { useLayoutEffect, useCallback, useRef, useState, FC } from 'react'; +import { + getEntityName, + highlightSearchText, +} from '../../../../../utils/EntityUtils'; +import { Link } from 'react-router-dom'; +import { getTeamsWithFqnPath } from '../../../../../utils/RouterUtils'; +import { stringToHTML } from '../../../../../utils/StringsUtils'; +import { Tooltip } from 'antd'; +import { Team } from '../../../../../generated/entity/teams/team'; + +type TeamHierarchyNameCellProps = { + record: Team; + searchTerm?: string; +}; + +export const TeamHierarchyNameCell: FC = ({ + record, + searchTerm = '', +}) => { + const displayName = getEntityName(record); + const linkRef = useRef(null); + const [isTruncated, setIsTruncated] = useState(false); + + const updateTruncation = useCallback(() => { + const el = linkRef.current; + if (!el) { + return; + } + setIsTruncated(el.scrollWidth > el.clientWidth + 1); + }, []); + + useLayoutEffect(() => { + updateTruncation(); + }, [displayName, searchTerm, updateTruncation]); + + useLayoutEffect(() => { + const el = linkRef.current; + if (!el || typeof ResizeObserver === 'undefined') { + return undefined; + } + const observer = new ResizeObserver(() => { + updateTruncation(); + }); + observer.observe(el); + + return () => observer.disconnect(); + }, [updateTruncation]); + + const link = ( + + {stringToHTML(highlightSearchText(displayName, searchTerm))} + + ); + + return ( + + {isTruncated ? ( + + + {link} + + + ) : ( + + {link} + + )} + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.component.tsx index da52a3e40ab7..624794d2d7bb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.component.tsx @@ -15,8 +15,7 @@ import { CloseOutlined, ExclamationCircleFilled, } from '@ant-design/icons'; -import { Typography } from '@openmetadata/ui-core-components'; -import { Button, Input, Space, Tooltip } from 'antd'; +import { Button, Input, Space, Tooltip, Typography } from 'antd'; import { isEmpty } from 'lodash'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -92,9 +91,11 @@ const TeamsHeadingLabel = ({ isHeadingEditing ? ( // Used onClick stop click propagation event anywhere in the component to parent // TeamDetailsV1 component collapsible panel - e.stopPropagation()}> +
e.stopPropagation()}> setHeading(e.target.value)} /> - + - +
) : ( <> <> {heading ? ( - + ellipsis={{ tooltip: true }} + level={5}> {heading} - + ) : ( - {t('label.no-entity', { entity: t('label.display-name'), })} - + )} {(hasAccess || isCurrentTeamOwner) && !currentTeam.deleted && ( +
{teamHeadingRender}
); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.test.tsx index 0d16a0e24158..18748061604e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsHeadingLabel.test.tsx @@ -10,58 +10,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - act, - fireEvent, - render, - screen, - waitFor, -} from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { createElement, type ElementType, type ReactNode } from 'react'; import { TeamType } from '../../../../../generated/entity/teams/team'; import { useAuth } from '../../../../../hooks/authHooks'; import { ENTITY_PERMISSIONS } from '../../../../../mocks/Permissions.mock'; import TeamsHeadingLabel from './TeamsHeadingLabel.component'; -jest.mock('@openmetadata/ui-core-components', () => { - const actual = jest.requireActual('@openmetadata/ui-core-components'); - - const TypographyMock = (props: { - as?: ElementType; - children?: ReactNode; - className?: string; - 'data-testid'?: string; - ellipsis?: boolean | { rows?: number; tooltip?: ReactNode }; - }) => { - const { - as: Component = 'span', - children, - className, - 'data-testid': dataTestId, - ellipsis, - } = props; - const truncateClass = - ellipsis === true || (typeof ellipsis === 'object' && ellipsis) - ? ' tw:truncate' - : ''; - - return createElement( - Component, - { - className: `${className ?? ''}${truncateClass}`.trim() || undefined, - 'data-testid': dataTestId, - }, - children - ); - }; - - return { - ...actual, - Typography: TypographyMock, - }; -}); - jest.mock('../../../../../hooks/authHooks', () => ({ useAuth: jest.fn().mockReturnValue({ isAdminUser: true }), })); @@ -107,10 +62,8 @@ const teamProps = { }; describe('TeamsHeadingLabel', () => { - it('should render Teams Heading Label', async () => { - await act(async () => { - render(); - }); + it('should render Teams Heading Label', () => { + render(); const teamHeading = screen.getByTestId('team-heading'); expect(teamHeading).toHaveTextContent('Test Team'); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsInfo.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsInfo.component.tsx index 4643315c5317..84fa4f3abfee 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsInfo.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamsInfo.component.tsx @@ -175,7 +175,7 @@ const TeamsInfo = ({ () => (
- {`${t( + {`${t( 'label.email' )}`} {hasEditPermission && ( @@ -257,7 +257,7 @@ const TeamsInfo = ({ ) : ( {email ?? NO_DATA_PLACEHOLDER} @@ -279,7 +279,7 @@ const TeamsInfo = ({
- + {`${t('label.type')}`} {hasEditPermission && !showTypeSelector && !isGroupType && ( @@ -321,7 +321,7 @@ const TeamsInfo = ({ /> ) : ( {teamType} @@ -351,7 +351,7 @@ const TeamsInfo = ({
- + {t('label.persona')} ) : ( - + {t('message.no-persona-assigned')} )} @@ -433,7 +433,7 @@ const TeamsInfo = ({ {t('label.total-user-plural')} {currentTeam.userCount} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/teams.less b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/teams.less index 9700c8d73b9d..2d7f26d90fdb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/teams.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/teams.less @@ -123,13 +123,13 @@ .teams-profile-container { .ant-card { - background: white; + background: @white; } .collapse-panel-container { padding: 20px; } .ant-card-head { - background: white; + background: @white; } .ant-collapse-header, @@ -185,6 +185,19 @@ display: flex; } } + +.teams-heading-label-container { + flex: 0 1 auto; + max-width: 50%; + min-width: 0; +} + +.teams-heading-label-edit-row { + box-sizing: border-box; + max-width: 100%; + min-width: 0; +} + .team-assets-right-panel { .summary-panel-container { height: 100%; From 48b7d86f3371fb48c0595cbac4f547e44141b8c6 Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Tue, 7 Apr 2026 12:47:16 +0530 Subject: [PATCH 7/7] fix lint --- .../TeamHierarchyNameCell.test.tsx | 22 ++++++++++++------- .../TeamHierarchyNameCell.tsx | 20 +++++++++++++---- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.test.tsx index 8b71cb9b8da9..8b1a898e844f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.test.tsx @@ -15,8 +15,8 @@ import type { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { Team } from '../../../../../generated/entity/teams/team'; import { - getEntityName, - highlightSearchText + getEntityName, + highlightSearchText, } from '../../../../../utils/EntityUtils'; import { getTeamsWithFqnPath } from '../../../../../utils/RouterUtils'; import { stringToHTML } from '../../../../../utils/StringsUtils'; @@ -58,12 +58,18 @@ jest.mock('../../../../../utils/StringsUtils', () => ({ stringToHTML: jest.fn((html: string) => html), })); -const mockGetEntityName = getEntityName as jest.MockedFunction; -const mockHighlightSearchText = - highlightSearchText as jest.MockedFunction; -const mockGetTeamsWithFqnPath = - getTeamsWithFqnPath as jest.MockedFunction; -const mockStringToHTML = stringToHTML as jest.MockedFunction; +const mockGetEntityName = getEntityName as jest.MockedFunction< + typeof getEntityName +>; +const mockHighlightSearchText = highlightSearchText as jest.MockedFunction< + typeof highlightSearchText +>; +const mockGetTeamsWithFqnPath = getTeamsWithFqnPath as jest.MockedFunction< + typeof getTeamsWithFqnPath +>; +const mockStringToHTML = stringToHTML as jest.MockedFunction< + typeof stringToHTML +>; const mockTeam = { fullyQualifiedName: 'Organization.Engineering', diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.tsx index 4deefc8bcefd..523af04bec89 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamsHeaderSection/TeamHierarchyNameCell.tsx @@ -1,13 +1,25 @@ -import { useLayoutEffect, useCallback, useRef, useState, FC } from 'react'; +/* + * Copyright 2026 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 { Tooltip } from 'antd'; +import { FC, useCallback, useLayoutEffect, useRef, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { Team } from '../../../../../generated/entity/teams/team'; import { getEntityName, highlightSearchText, } from '../../../../../utils/EntityUtils'; -import { Link } from 'react-router-dom'; import { getTeamsWithFqnPath } from '../../../../../utils/RouterUtils'; import { stringToHTML } from '../../../../../utils/StringsUtils'; -import { Tooltip } from 'antd'; -import { Team } from '../../../../../generated/entity/teams/team'; type TeamHierarchyNameCellProps = { record: Team;