fix(ui): tab label with persona assigned (#22927)

* fix(ui): tab label with persona assigned

* fix tests

* fix tests

* add playwright to validate the same

* remove dndProvider from glossary term table

* fix unit tests

* fix tests

* fix data contract failure

(cherry picked from commit 0081ece0ac8344edb699e7a3541fd086dde591a3)
This commit is contained in:
Chirag Madlani 2025-08-25 12:58:23 +05:30
parent 3512549097
commit eb57f6d5cd
15 changed files with 438 additions and 262 deletions

View File

@ -32,138 +32,188 @@ export enum ECustomizedGovernance {
GLOSSARY_TERM = 'Glossary Term',
}
export enum EntityTabs {
SCHEMA = 'schema',
SCHEMAS = 'schemas',
ACTIVITY_FEED = 'activity_feed',
SAMPLE_DATA = 'sample_data',
TABLE_QUERIES = 'table_queries',
PROFILER = 'profiler',
LINEAGE = 'lineage',
KNOWLEDGE_GRAPH = 'knowledge_graph',
DBT = 'dbt',
VIEW_DEFINITION = 'view_definition',
SCHEMA_DEFINITION = 'schema_definition',
CUSTOM_PROPERTIES = 'custom_properties',
MODEL = 'model',
FEATURES = 'features',
TASKS = 'tasks',
CONFIG = 'config',
DETAILS = 'details',
CHILDREN = 'children',
EXECUTIONS = 'executions',
TABLE = 'table',
TEST_CASES = 'test-cases',
PIPELINE = 'pipeline',
DATA_Model = 'data-model',
AGENTS = 'agents',
CONNECTION = 'connection',
SQL = 'sql',
FIELDS = 'fields',
SEARCH_INDEX_SETTINGS = 'search-index-settings',
STORED_PROCEDURE = 'stored_procedure',
CODE = 'code',
API_COLLECTION = 'apiCollection',
API_ENDPOINT = 'apiEndpoint',
OVERVIEW = 'overview',
INCIDENTS = 'incidents',
TERMS = 'terms',
GLOSSARY_TERMS = 'glossary_terms',
ASSETS = 'assets',
EXPRESSION = 'expression',
INSIGHTS = 'insights',
DASHBOARD = 'dashboard',
DOCUMENTATION = 'documentation',
DATA_PRODUCTS = 'data_products',
SUBDOMAINS = 'subdomains',
CONTRACT = 'contract',
}
export const TABLE_DEFAULT_TABS = [
'Activity Feeds & Tasks',
'Contract',
'Custom Properties',
'Data Observability',
'Lineage',
'Queries',
'Sample Data',
'Schema',
'View Definition',
'dbt',
EntityTabs.ACTIVITY_FEED,
EntityTabs.CONTRACT,
EntityTabs.CUSTOM_PROPERTIES,
EntityTabs.PROFILER,
EntityTabs.LINEAGE,
EntityTabs.TABLE_QUERIES,
EntityTabs.SAMPLE_DATA,
EntityTabs.SCHEMA,
EntityTabs.VIEW_DEFINITION,
EntityTabs.DBT,
];
export const TOPIC_DEFAULT_TABS = [
'Schema',
'Activity Feeds & Tasks',
'Sample Data',
'Config',
'Lineage',
'Custom Properties',
EntityTabs.SCHEMA,
EntityTabs.ACTIVITY_FEED,
EntityTabs.SAMPLE_DATA,
EntityTabs.CONFIG,
EntityTabs.LINEAGE,
EntityTabs.CUSTOM_PROPERTIES,
];
export const DASHBOARD_DEFAULT_TABS = [
'Details',
'Activity Feeds & Tasks',
'Lineage',
'Custom Properties',
EntityTabs.DETAILS,
EntityTabs.ACTIVITY_FEED,
EntityTabs.LINEAGE,
EntityTabs.CUSTOM_PROPERTIES,
];
export const MLMODEL_DEFAULT_TABS = [
'Features',
'Activity Feeds & Tasks',
'Details',
'Lineage',
'Custom Properties',
EntityTabs.FEATURES,
EntityTabs.ACTIVITY_FEED,
EntityTabs.DETAILS,
EntityTabs.LINEAGE,
EntityTabs.CUSTOM_PROPERTIES,
];
export const PIPELINE_DEFAULT_TABS = [
'Tasks',
'Activity Feeds & Tasks',
'Executions',
'Lineage',
'Custom Properties',
EntityTabs.TASKS,
EntityTabs.ACTIVITY_FEED,
EntityTabs.EXECUTIONS,
EntityTabs.LINEAGE,
EntityTabs.CUSTOM_PROPERTIES,
];
export const DASHBOARD_DATAMODEL_DEFAULT_TABS = [
'Model',
'Activity Feeds & Tasks',
'SQL',
'Lineage',
'Custom Properties',
EntityTabs.MODEL,
EntityTabs.ACTIVITY_FEED,
EntityTabs.SQL,
EntityTabs.LINEAGE,
EntityTabs.CUSTOM_PROPERTIES,
];
export const API_COLLECTION_DEFAULT_TABS = [
'Endpoints',
'Activity Feeds & Tasks',
'Custom Properties',
EntityTabs.API_ENDPOINT,
EntityTabs.ACTIVITY_FEED,
EntityTabs.CUSTOM_PROPERTIES,
];
export const SEARCH_INDEX_DEFAULT_TABS = [
'Fields',
'Activity Feeds & Tasks',
'Sample Data',
'Lineage',
'Search Index Settings',
'Custom Properties',
EntityTabs.FIELDS,
EntityTabs.ACTIVITY_FEED,
EntityTabs.SAMPLE_DATA,
EntityTabs.LINEAGE,
EntityTabs.SEARCH_INDEX_SETTINGS,
EntityTabs.CUSTOM_PROPERTIES,
];
export const CONTAINER_DEFAULT_TABS = [
'Schema',
'Children',
'Activity Feeds & Tasks',
'Lineage',
'Custom Properties',
EntityTabs.SCHEMA,
EntityTabs.CHILDREN,
EntityTabs.ACTIVITY_FEED,
EntityTabs.LINEAGE,
EntityTabs.CUSTOM_PROPERTIES,
];
export const DATABASE_DEFAULT_TABS = [
'Schemas',
'Activity Feeds & Tasks',
'Custom Properties',
EntityTabs.SCHEMAS,
EntityTabs.ACTIVITY_FEED,
EntityTabs.CUSTOM_PROPERTIES,
];
export const DATABASE_SCHEMA_DEFAULT_TABS = [
'Tables',
'Stored Procedures',
'Activity Feeds & Tasks',
'Custom Properties',
EntityTabs.TABLE,
EntityTabs.STORED_PROCEDURE,
EntityTabs.ACTIVITY_FEED,
EntityTabs.CUSTOM_PROPERTIES,
];
export const STORED_PROCEDURE_DEFAULT_TABS = [
'Code',
'Activity Feeds & Tasks',
'Lineage',
'Custom Properties',
EntityTabs.CODE,
EntityTabs.ACTIVITY_FEED,
EntityTabs.LINEAGE,
EntityTabs.CUSTOM_PROPERTIES,
];
export const API_ENDPOINT_DEFAULT_TABS = [
'Schema',
'Activity Feeds & Tasks',
'Lineage',
'Custom Properties',
EntityTabs.SCHEMA,
EntityTabs.ACTIVITY_FEED,
EntityTabs.LINEAGE,
EntityTabs.CUSTOM_PROPERTIES,
];
export const DASHBOARD_DATA_MODEL_DEFAULT_TABS = [
'Model',
'Activity Feeds & Tasks',
'Lineage',
'Custom Properties',
EntityTabs.MODEL,
EntityTabs.ACTIVITY_FEED,
EntityTabs.LINEAGE,
EntityTabs.CUSTOM_PROPERTIES,
];
export const ML_MODEL_DEFAULT_TABS = [
'Features',
'Activity Feeds & Tasks',
'Details',
'Lineage',
'Custom Properties',
EntityTabs.FEATURES,
EntityTabs.ACTIVITY_FEED,
EntityTabs.DETAILS,
EntityTabs.LINEAGE,
EntityTabs.CUSTOM_PROPERTIES,
];
export const DOMAIN_DEFAULT_TABS = [
'Documentation',
'Sub Domains',
'Data Products',
'Assets',
'Custom Properties',
EntityTabs.DOCUMENTATION,
EntityTabs.SUBDOMAINS,
EntityTabs.DATA_PRODUCTS,
EntityTabs.ASSETS,
EntityTabs.CUSTOM_PROPERTIES,
];
export const GLOSSARY_DEFAULT_TABS = ['Terms', 'Activity Feeds & Tasks'];
export const GLOSSARY_DEFAULT_TABS = [
EntityTabs.TERMS,
EntityTabs.ACTIVITY_FEED,
];
export const GLOSSARY_TERM_DEFAULT_TABS = [
'Overview',
'Glossary Terms',
'Assets',
'Activity Feeds & Tasks',
'Custom Properties',
EntityTabs.OVERVIEW,
EntityTabs.GLOSSARY_TERMS,
EntityTabs.ASSETS,
EntityTabs.ACTIVITY_FEED,
EntityTabs.CUSTOM_PROPERTIES,
];

View File

@ -388,7 +388,7 @@ test.describe('Persona customization', () => {
.getByTestId('remove-widget-button')
.click();
await adminPage.getByTestId('tab-Custom Properties').click();
await adminPage.getByTestId('tab-custom_properties').click();
await adminPage.getByText('Hide', { exact: true }).click();
await adminPage.getByRole('button', { name: 'Add tab' }).click();
@ -470,9 +470,9 @@ test.describe('Persona customization', () => {
for (const tabName of expectedTabs) {
await expect(
adminPage.getByTestId('customize-tab-card').getByRole('button', {
name: tabName,
})
adminPage
.getByTestId('customize-tab-card')
.getByTestId(`tab-${tabName}`)
).toBeVisible();
}
}
@ -489,6 +489,9 @@ test.describe('Persona customization', () => {
.click();
await adminPage.getByRole('button', { name: 'Add tab' }).click();
await expect(adminPage.getByRole('dialog')).toBeVisible();
await adminPage
.getByRole('dialog')
.getByRole('button', { name: 'Add' })
@ -551,8 +554,8 @@ test.describe('Persona customization', () => {
state: 'detached',
});
const dragElement = adminPage.getByTestId('tab-Overview');
const dropTarget = adminPage.getByTestId('tab-Custom Properties');
const dragElement = adminPage.getByTestId('tab-overview');
const dropTarget = adminPage.getByTestId('tab-custom_properties');
await dragElement.dragTo(dropTarget);
@ -597,4 +600,98 @@ test.describe('Persona customization', () => {
).toBeVisible();
});
});
test("customize tab label should only render if it's customize by user", async ({
adminPage,
userPage,
}) => {
await test.step('apply tab label customization for Table', async () => {
await settingClick(adminPage, GlobalSettingOptions.PERSONA);
await adminPage.waitForLoadState('networkidle');
await adminPage
.getByTestId(`persona-details-card-${persona.data.name}`)
.click();
await adminPage.getByRole('tab', { name: 'Customize UI' }).click();
await adminPage.waitForLoadState('networkidle');
await adminPage.getByText('Data Assets').click();
await adminPage.getByText('Table', { exact: true }).click();
await adminPage.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
await expect(
adminPage
.getByTestId('customize-tab-card')
.getByTestId(`tab-sample_data`)
).toBeVisible();
await adminPage
.getByTestId('customize-tab-card')
.getByTestId(`tab-sample_data`)
.click();
await adminPage.getByRole('menuitem', { name: 'Rename' }).click();
await expect(adminPage.getByRole('dialog')).toBeVisible();
await adminPage.getByRole('dialog').getByRole('textbox').clear();
await adminPage
.getByRole('dialog')
.getByRole('textbox')
.fill('Sample Data Updated');
await adminPage
.getByRole('dialog')
.getByRole('button', { name: 'Ok' })
.click();
await expect(
adminPage
.getByTestId('customize-tab-card')
.getByTestId(`tab-sample_data`)
).toHaveText('Sample Data Updated');
await adminPage.getByTestId('save-button').click();
await toastNotification(
adminPage,
/^Page layout (created|updated) successfully\.$/
);
});
await test.step(
'validate applied label change and language support for page',
async () => {
await redirectToHomePage(userPage);
const entity = getCustomizeDetailsEntity(ECustomizedDataAssets.TABLE);
await entity.visitEntityPage(userPage);
await userPage.waitForLoadState('networkidle');
await userPage.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
// Change language to French
await userPage.getByRole('button', { name: 'EN' }).click();
await userPage.getByRole('menuitem', { name: 'Français - FR' }).click();
await userPage.waitForLoadState('networkidle');
await userPage.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
await expect(
userPage.getByRole('tab', { name: 'Sample Data Updated' })
).toBeVisible();
// Overview tab in French, only customized tab should be non-localized rest should be localized
await expect(
userPage.getByRole('tab', { name: 'Colonnes' })
).toBeVisible();
await expect(
userPage.getByRole('tab', { name: "Flux d'Activité & Tâches" })
).toBeVisible();
}
);
});
});

View File

@ -696,7 +696,7 @@ test.describe('Data Contracts', () => {
});
// Hide the Contract tab
await page.getByTestId('tab-Contract').click();
await page.getByTestId('tab-contract').click();
await page.getByText('Hide', { exact: true }).click();
// Save the customization

View File

@ -32,6 +32,8 @@ import { useApplicationStore } from './hooks/useApplicationStore';
import { getCustomUiThemePreference } from './rest/settingConfigAPI';
import { getBasePath } from './utils/HistoryUtils';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import i18n from './utils/i18next/LocalUtil';
import { getThemeConfig } from './utils/ThemeUtils';
@ -88,7 +90,9 @@ const App: FC = () => {
<AsyncDeleteProvider>
<EntityExportModalProvider>
<AirflowStatusProvider>
<AppRouter />
<DndProvider backend={HTML5Backend}>
<AppRouter />
</DndProvider>
</AirflowStatusProvider>
</EntityExportModalProvider>
</AsyncDeleteProvider>

View File

@ -13,17 +13,8 @@
import { EyeFilled, MoreOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Card, Col, Dropdown, Input, Modal, Space } from 'antd';
import {
cloneDeep,
isEmpty,
isNil,
isUndefined,
toString,
uniqueId,
} from 'lodash';
import { cloneDeep, isEmpty, isNil, isUndefined, uniqueId } from 'lodash';
import { useCallback, useMemo, useState } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import RGL, { Layout, WidthProvider } from 'react-grid-layout';
import { useTranslation } from 'react-i18next';
import {
@ -50,6 +41,7 @@ import {
getCustomizableWidgetByPage,
getDefaultTabs,
getDefaultWidgetForTab,
getTabDisplayName,
} from '../../../utils/CustomizePage/CustomizePageUtils';
import { getEntityName } from '../../../utils/EntityUtils';
import { TabItem } from '../../common/DraggableTabs/DraggableTabs';
@ -388,46 +380,44 @@ export const CustomizeTabWidget = () => {
</Button>
}
title={t('label.customize-tab-plural')}>
<DndProvider backend={HTML5Backend}>
<div className="d-flex flex-wrap gap-4">
{items.map((item, index) => (
<TabItem
index={index}
item={item}
key={item.id}
moveTab={moveTab}
shouldHide={systemTabIds.includes(item.id)}
onEdit={onChange}
onRemove={remove}
onRename={handleTabEditClick}
/>
))}
{hiddenTabs.map((item) => (
<Dropdown
key={item.id}
menu={{
items: [
{
label: t('label.show'),
key: 'show',
icon: <EyeFilled />,
},
],
onClick: () => add(item),
}}
trigger={['click']}>
<Button
className="draggable-hidden-tab-item bg-grey"
data-testid={`tab-${item.displayName}`}>
<Space>
{getEntityName(item)}
<MoreOutlined />
</Space>
</Button>
</Dropdown>
))}
</div>
</DndProvider>
<div className="d-flex flex-wrap gap-4">
{items.map((item, index) => (
<TabItem
index={index}
item={item}
key={item.id}
moveTab={moveTab}
shouldHide={systemTabIds.includes(item.id)}
onEdit={onChange}
onRemove={remove}
onRename={handleTabEditClick}
/>
))}
{hiddenTabs.map((item) => (
<Dropdown
key={item.id}
menu={{
items: [
{
label: t('label.show'),
key: 'show',
icon: <EyeFilled />,
},
],
onClick: () => add(item),
}}
trigger={['click']}>
<Button
className="draggable-hidden-tab-item bg-grey"
data-testid={`tab-${item.name}`}>
<Space>
{getTabDisplayName(item)}
<MoreOutlined />
</Space>
</Button>
</Dropdown>
))}
</div>
</Card>
</Col>
<Col span={24}>
@ -508,7 +498,7 @@ export const CustomizeTabWidget = () => {
onOk={handleRenameSave}>
<Input
autoFocus
value={toString(getEntityName(editableItem))}
value={getTabDisplayName(editableItem)}
onChange={handleChange}
/>
</Modal>

View File

@ -32,8 +32,6 @@ import classNames from 'classnames';
import { compare } from 'fast-json-patch';
import { cloneDeep, isEmpty, isUndefined } from 'lodash';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useTranslation } from 'react-i18next';
import { Link, useNavigate } from 'react-router-dom';
import { ReactComponent as IconDrag } from '../../../assets/svg/drag.svg';
@ -882,28 +880,26 @@ const GlossaryTermTab = ({ isGlossary, className }: GlossaryTermTabProps) => {
{/* Have use the col to set the width of the table, to only use the viewport width for the table columns */}
<Col className="w-full" ref={tableContainerRef} span={24}>
{glossaryTerms.length > 0 ? (
<DndProvider backend={HTML5Backend}>
<Table
resizableColumns
className={classNames('drop-over-background', {
'drop-over-table': isTableHovered,
})}
columns={columns}
components={TABLE_CONSTANTS}
data-testid="glossary-terms-table"
dataSource={filteredGlossaryTerms}
defaultVisibleColumns={DEFAULT_VISIBLE_COLUMNS}
expandable={expandableConfig}
extraTableFilters={extraTableFilters}
loading={isTableLoading || termsLoading}
pagination={false}
rowKey="fullyQualifiedName"
size="small"
staticVisibleColumns={STATIC_VISIBLE_COLUMNS}
onHeaderRow={onTableHeader}
onRow={onTableRow}
/>
</DndProvider>
<Table
resizableColumns
className={classNames('drop-over-background', {
'drop-over-table': isTableHovered,
})}
columns={columns}
components={TABLE_CONSTANTS}
data-testid="glossary-terms-table"
dataSource={filteredGlossaryTerms}
defaultVisibleColumns={DEFAULT_VISIBLE_COLUMNS}
expandable={expandableConfig}
extraTableFilters={extraTableFilters}
loading={isTableLoading || termsLoading}
pagination={false}
rowKey="fullyQualifiedName"
size="small"
staticVisibleColumns={STATIC_VISIBLE_COLUMNS}
onHeaderRow={onTableHeader}
onRow={onTableRow}
/>
) : (
<ErrorPlaceHolder />
)}

View File

@ -19,6 +19,8 @@ import {
getByText,
render,
} from '@testing-library/react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { MemoryRouter } from 'react-router-dom';
import {
mockedGlossaryTerms,
@ -104,6 +106,15 @@ jest.mock('../../Customization/GenericProvider/GenericProvider', () => ({
})),
}));
const renderWithDndProvider = (props = {}) => {
return render(
<DndProvider backend={HTML5Backend}>
<GlossaryTermTab {...mockProps} {...props} />
</DndProvider>,
{ wrapper: MemoryRouter }
);
};
describe('Test GlossaryTermTab component', () => {
it('should show the ErrorPlaceHolder component, if no glossary is present', () => {
const { container } = render(<GlossaryTermTab {...mockProps} />, {
@ -132,9 +143,7 @@ describe('Test GlossaryTermTab component', () => {
glossaryChildTerms: mockedGlossaryTerms,
updateActiveGlossary: jest.fn(),
}));
const { container } = render(<GlossaryTermTab {...mockProps} />, {
wrapper: MemoryRouter,
});
const { container } = renderWithDndProvider();
expect(getByTestId(container, 'Clothing')).toBeInTheDocument();
expect(

View File

@ -11,7 +11,9 @@
* limitations under the License.
*/
import { act, fireEvent, render, screen } from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { MemoryRouter } from 'react-router-dom';
import {
MOCK_CURRENT_TEAM,
@ -80,13 +82,18 @@ jest.mock('../../../common/SearchBarComponent/SearchBar.component', () =>
jest.fn().mockImplementation(() => <div>SearchBar</div>)
);
const renderComponent = (props = {}) => {
return render(
<DndProvider backend={HTML5Backend}>
<TeamHierarchy {...teamHierarchyPropsData} {...props} />
</DndProvider>,
{ wrapper: MemoryRouter }
);
};
describe('Team Hierarchy page', () => {
it('Initially, Table should load', async () => {
await act(async () => {
render(<TeamHierarchy {...teamHierarchyPropsData} />, {
wrapper: MemoryRouter,
});
});
renderComponent();
const table = await screen.findByTestId('team-hierarchy-table');
@ -94,11 +101,7 @@ describe('Team Hierarchy page', () => {
});
it('Should render all table columns', async () => {
await act(async () => {
render(<TeamHierarchy {...teamHierarchyPropsData} />, {
wrapper: MemoryRouter,
});
});
renderComponent();
const table = await screen.findByTestId('team-hierarchy-table');
const teamsColumn = await screen.findByText('label.team-plural');
@ -121,11 +124,7 @@ describe('Team Hierarchy page', () => {
});
it('Should render child row in table', async () => {
await act(async () => {
render(<TeamHierarchy {...teamHierarchyPropsData} />, {
wrapper: MemoryRouter,
});
});
renderComponent();
const table = await screen.findByTestId('team-hierarchy-table');

View File

@ -19,8 +19,6 @@ import classNames from 'classnames';
import { compare } from 'fast-json-patch';
import { isEmpty, isUndefined } from 'lodash';
import { FC, useCallback, useMemo, useState } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
@ -271,51 +269,49 @@ const TeamHierarchy: FC<TeamHierarchyProps> = ({
return (
<div className="team-list-container">
<DndProvider backend={HTML5Backend}>
<Table
className={classNames('teams-list-table drop-over-background', {
'drop-over-table': isTableHovered,
})}
columns={columns}
components={TABLE_CONSTANTS}
data-testid="team-hierarchy-table"
dataSource={data}
expandable={expandableConfig}
extraTableFilters={
<Space align="center">
<span>
<Switch
checked={showDeletedTeam}
data-testid="show-deleted"
onClick={onShowDeletedTeamChange}
/>
<Typography.Text className="m-l-xs">
{t('label.deleted')}
</Typography.Text>
</span>
<Table
className={classNames('teams-list-table drop-over-background', {
'drop-over-table': isTableHovered,
})}
columns={columns}
components={TABLE_CONSTANTS}
data-testid="team-hierarchy-table"
dataSource={data}
expandable={expandableConfig}
extraTableFilters={
<Space align="center">
<span>
<Switch
checked={showDeletedTeam}
data-testid="show-deleted"
onClick={onShowDeletedTeamChange}
/>
<Typography.Text className="m-l-xs">
{t('label.deleted')}
</Typography.Text>
</span>
{createTeamPermission && !isTeamDeleted && (
<Button
data-testid="add-team"
type="primary"
onClick={handleAddTeamButtonClick}>
{t('label.add-entity', { entity: t('label.team') })}
</Button>
)}
</Space>
}
loading={isTableLoading}
locale={{
emptyText: <FilterTablePlaceHolder />,
}}
pagination={false}
rowKey="name"
searchProps={searchProps}
size="small"
onHeaderRow={onTableHeader}
onRow={onTableRow}
/>
</DndProvider>
{createTeamPermission && !isTeamDeleted && (
<Button
data-testid="add-team"
type="primary"
onClick={handleAddTeamButtonClick}>
{t('label.add-entity', { entity: t('label.team') })}
</Button>
)}
</Space>
}
loading={isTableLoading}
locale={{
emptyText: <FilterTablePlaceHolder />,
}}
pagination={false}
rowKey="name"
searchProps={searchProps}
size="small"
onHeaderRow={onTableHeader}
onRow={onTableRow}
/>
<Modal
centered

View File

@ -15,9 +15,8 @@ import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { TabItem } from './DraggableTabs';
// Mock the getEntityName utility function
jest.mock('../../../utils/EntityUtils', () => ({
getEntityName: jest.fn().mockReturnValue('Test Tab'),
jest.mock('../../../utils/CustomizePage/CustomizePageUtils', () => ({
getTabDisplayName: jest.fn().mockReturnValue('Test Tab'),
}));
describe('TabItem', () => {
@ -53,12 +52,12 @@ describe('TabItem', () => {
it('renders tab item with correct name', () => {
renderComponent();
expect(screen.getByText('Test Tab')).toBeInTheDocument();
expect(screen.getByTestId('tab-Test Tab')).toBeInTheDocument();
});
it('calls onItemClick when tab is clicked', () => {
renderComponent();
fireEvent.click(screen.getByText('Test Tab'));
fireEvent.click(screen.getByTestId('tab-Test Tab'));
expect(defaultProps.onItemClick).toHaveBeenCalledWith(mockTab.id);
});

View File

@ -22,7 +22,7 @@ import React from 'react';
import { useDrag, useDrop } from 'react-dnd';
import { useTranslation } from 'react-i18next';
import { Tab } from '../../../generated/system/ui/tab';
import { getEntityName } from '../../../utils/EntityUtils';
import { getTabDisplayName } from '../../../utils/CustomizePage/CustomizePageUtils';
import './draggable-tabs.less';
type TargetKey = React.MouseEvent | React.KeyboardEvent | string;
@ -118,10 +118,10 @@ export const TabItem = ({
trigger={['click']}>
<Button
className="draggable-tab-item"
data-testid={`tab-${item.displayName}`}
data-testid={`tab-${item.name}`}
onClick={() => onItemClick?.(item.id)}>
<Space>
{getEntityName(item)}
{getTabDisplayName(item)}
<MoreOutlined />
</Space>
</Button>

View File

@ -33,8 +33,6 @@ import {
} from 'react';
import { useAntdColumnResize } from 'react-antd-column-resize';
import { Column } from 'react-antd-column-resize/dist/useAntdColumnResize/types';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useTranslation } from 'react-i18next';
import { ReactComponent as ColumnIcon } from '../../../assets/svg/ic-column.svg';
import { useCurrentUserPreferences } from '../../../hooks/currentUserStore/useCurrentUserStore';
@ -291,24 +289,22 @@ const Table = <T extends Record<string, unknown>>(
span={searchProps ? 12 : 24}>
{rest.extraTableFilters}
{isCustomizeColumnEnable && (
<DndProvider backend={HTML5Backend}>
<Dropdown
className="custom-column-dropdown-menu text-primary"
menu={menu}
open={isDropdownVisible}
placement="bottomRight"
trigger={['click']}
onOpenChange={setIsDropdownVisible}>
<Button
className="remove-button-background-hover"
data-testid="column-dropdown"
icon={<Icon component={ColumnIcon} />}
size="small"
type="text">
{t('label.column-plural')}
</Button>
</Dropdown>
</DndProvider>
<Dropdown
className="custom-column-dropdown-menu text-primary"
menu={menu}
open={isDropdownVisible}
placement="bottomRight"
trigger={['click']}
onOpenChange={setIsDropdownVisible}>
<Button
className="remove-button-background-hover"
data-testid="column-dropdown"
icon={<Icon component={ColumnIcon} />}
size="small"
type="text">
{t('label.column-plural')}
</Button>
</Dropdown>
)}
</Col>
)}

View File

@ -17,6 +17,7 @@ import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.inte
import {
checkIfExpandViewSupported,
getDefaultTabs,
getTabDisplayName,
getTabLabelFromId,
getTabLabelMapFromTabs,
sortTabs,
@ -24,6 +25,43 @@ import {
} from './CustomizePageUtils';
describe('CustomizePageUtils', () => {
describe('getTabDisplayName', () => {
it('should return displayName if present', () => {
const tab: Tab = {
id: EntityTabs.OVERVIEW,
name: EntityTabs.OVERVIEW,
displayName: 'Custom Overview',
layout: [],
};
expect(getTabDisplayName(tab)).toBe('Custom Overview');
});
it('should fallback to getTabLabelFromId if displayName is missing', () => {
const tab: Tab = {
id: EntityTabs.OVERVIEW,
name: EntityTabs.OVERVIEW,
layout: [],
};
const result = getTabDisplayName(tab);
expect(typeof result).toBe('string');
});
it('should return empty string if displayName and name are missing', () => {
const tab: Tab = {
id: EntityTabs.OVERVIEW,
name: '' as EntityTabs,
layout: [],
};
const result = getTabDisplayName(tab);
expect(typeof result).toBe('string');
});
});
describe('sortTabs', () => {
it('should sort tabs according to given order', () => {
const tabs: TabsProps['items'] = [

View File

@ -691,3 +691,7 @@ export const updateWidgetHeightRecursively = (
return acc;
}, [] as WidgetConfig[]);
export const getTabDisplayName = (item: Tab) => {
return item.displayName ?? getTabLabelFromId(item.name as EntityTabs);
};

View File

@ -28,7 +28,6 @@ import { Tab } from '../generated/system/ui/uiCustomization';
import { TestSummary } from '../generated/tests/testCase';
import { FeedCounts } from '../interface/feed.interface';
import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface';
import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils';
import i18n from './i18next/LocalUtil';
import {
getTableDetailPageBaseTabs,
@ -106,7 +105,6 @@ class TableClassBase {
].map((tab: EntityTabs) => ({
id: tab,
name: tab,
displayName: getTabLabelFromId(tab),
layout: this.getDefaultLayout(tab),
editable: tab === EntityTabs.SCHEMA,
}));