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', 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 = [ export const TABLE_DEFAULT_TABS = [
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'Contract', EntityTabs.CONTRACT,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
'Data Observability', EntityTabs.PROFILER,
'Lineage', EntityTabs.LINEAGE,
'Queries', EntityTabs.TABLE_QUERIES,
'Sample Data', EntityTabs.SAMPLE_DATA,
'Schema', EntityTabs.SCHEMA,
'View Definition', EntityTabs.VIEW_DEFINITION,
'dbt', EntityTabs.DBT,
]; ];
export const TOPIC_DEFAULT_TABS = [ export const TOPIC_DEFAULT_TABS = [
'Schema', EntityTabs.SCHEMA,
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'Sample Data', EntityTabs.SAMPLE_DATA,
'Config', EntityTabs.CONFIG,
'Lineage', EntityTabs.LINEAGE,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
]; ];
export const DASHBOARD_DEFAULT_TABS = [ export const DASHBOARD_DEFAULT_TABS = [
'Details', EntityTabs.DETAILS,
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'Lineage', EntityTabs.LINEAGE,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
]; ];
export const MLMODEL_DEFAULT_TABS = [ export const MLMODEL_DEFAULT_TABS = [
'Features', EntityTabs.FEATURES,
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'Details', EntityTabs.DETAILS,
'Lineage', EntityTabs.LINEAGE,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
]; ];
export const PIPELINE_DEFAULT_TABS = [ export const PIPELINE_DEFAULT_TABS = [
'Tasks', EntityTabs.TASKS,
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'Executions', EntityTabs.EXECUTIONS,
'Lineage', EntityTabs.LINEAGE,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
]; ];
export const DASHBOARD_DATAMODEL_DEFAULT_TABS = [ export const DASHBOARD_DATAMODEL_DEFAULT_TABS = [
'Model', EntityTabs.MODEL,
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'SQL', EntityTabs.SQL,
'Lineage', EntityTabs.LINEAGE,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
]; ];
export const API_COLLECTION_DEFAULT_TABS = [ export const API_COLLECTION_DEFAULT_TABS = [
'Endpoints', EntityTabs.API_ENDPOINT,
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
]; ];
export const SEARCH_INDEX_DEFAULT_TABS = [ export const SEARCH_INDEX_DEFAULT_TABS = [
'Fields', EntityTabs.FIELDS,
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'Sample Data', EntityTabs.SAMPLE_DATA,
'Lineage', EntityTabs.LINEAGE,
'Search Index Settings', EntityTabs.SEARCH_INDEX_SETTINGS,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
]; ];
export const CONTAINER_DEFAULT_TABS = [ export const CONTAINER_DEFAULT_TABS = [
'Schema', EntityTabs.SCHEMA,
'Children', EntityTabs.CHILDREN,
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'Lineage', EntityTabs.LINEAGE,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
]; ];
export const DATABASE_DEFAULT_TABS = [ export const DATABASE_DEFAULT_TABS = [
'Schemas', EntityTabs.SCHEMAS,
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
]; ];
export const DATABASE_SCHEMA_DEFAULT_TABS = [ export const DATABASE_SCHEMA_DEFAULT_TABS = [
'Tables', EntityTabs.TABLE,
'Stored Procedures', EntityTabs.STORED_PROCEDURE,
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
]; ];
export const STORED_PROCEDURE_DEFAULT_TABS = [ export const STORED_PROCEDURE_DEFAULT_TABS = [
'Code', EntityTabs.CODE,
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'Lineage', EntityTabs.LINEAGE,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
]; ];
export const API_ENDPOINT_DEFAULT_TABS = [ export const API_ENDPOINT_DEFAULT_TABS = [
'Schema', EntityTabs.SCHEMA,
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'Lineage', EntityTabs.LINEAGE,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
]; ];
export const DASHBOARD_DATA_MODEL_DEFAULT_TABS = [ export const DASHBOARD_DATA_MODEL_DEFAULT_TABS = [
'Model', EntityTabs.MODEL,
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'Lineage', EntityTabs.LINEAGE,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
]; ];
export const ML_MODEL_DEFAULT_TABS = [ export const ML_MODEL_DEFAULT_TABS = [
'Features', EntityTabs.FEATURES,
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'Details', EntityTabs.DETAILS,
'Lineage', EntityTabs.LINEAGE,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
]; ];
export const DOMAIN_DEFAULT_TABS = [ export const DOMAIN_DEFAULT_TABS = [
'Documentation', EntityTabs.DOCUMENTATION,
'Sub Domains', EntityTabs.SUBDOMAINS,
'Data Products', EntityTabs.DATA_PRODUCTS,
'Assets', EntityTabs.ASSETS,
'Custom Properties', 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 = [ export const GLOSSARY_TERM_DEFAULT_TABS = [
'Overview', EntityTabs.OVERVIEW,
'Glossary Terms', EntityTabs.GLOSSARY_TERMS,
'Assets', EntityTabs.ASSETS,
'Activity Feeds & Tasks', EntityTabs.ACTIVITY_FEED,
'Custom Properties', EntityTabs.CUSTOM_PROPERTIES,
]; ];

View File

@ -388,7 +388,7 @@ test.describe('Persona customization', () => {
.getByTestId('remove-widget-button') .getByTestId('remove-widget-button')
.click(); .click();
await adminPage.getByTestId('tab-Custom Properties').click(); await adminPage.getByTestId('tab-custom_properties').click();
await adminPage.getByText('Hide', { exact: true }).click(); await adminPage.getByText('Hide', { exact: true }).click();
await adminPage.getByRole('button', { name: 'Add tab' }).click(); await adminPage.getByRole('button', { name: 'Add tab' }).click();
@ -470,9 +470,9 @@ test.describe('Persona customization', () => {
for (const tabName of expectedTabs) { for (const tabName of expectedTabs) {
await expect( await expect(
adminPage.getByTestId('customize-tab-card').getByRole('button', { adminPage
name: tabName, .getByTestId('customize-tab-card')
}) .getByTestId(`tab-${tabName}`)
).toBeVisible(); ).toBeVisible();
} }
} }
@ -489,6 +489,9 @@ test.describe('Persona customization', () => {
.click(); .click();
await adminPage.getByRole('button', { name: 'Add tab' }).click(); await adminPage.getByRole('button', { name: 'Add tab' }).click();
await expect(adminPage.getByRole('dialog')).toBeVisible();
await adminPage await adminPage
.getByRole('dialog') .getByRole('dialog')
.getByRole('button', { name: 'Add' }) .getByRole('button', { name: 'Add' })
@ -551,8 +554,8 @@ test.describe('Persona customization', () => {
state: 'detached', state: 'detached',
}); });
const dragElement = adminPage.getByTestId('tab-Overview'); const dragElement = adminPage.getByTestId('tab-overview');
const dropTarget = adminPage.getByTestId('tab-Custom Properties'); const dropTarget = adminPage.getByTestId('tab-custom_properties');
await dragElement.dragTo(dropTarget); await dragElement.dragTo(dropTarget);
@ -597,4 +600,98 @@ test.describe('Persona customization', () => {
).toBeVisible(); ).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 // Hide the Contract tab
await page.getByTestId('tab-Contract').click(); await page.getByTestId('tab-contract').click();
await page.getByText('Hide', { exact: true }).click(); await page.getByText('Hide', { exact: true }).click();
// Save the customization // Save the customization

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,9 @@
* limitations under the License. * 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 { MemoryRouter } from 'react-router-dom';
import { import {
MOCK_CURRENT_TEAM, MOCK_CURRENT_TEAM,
@ -80,13 +82,18 @@ jest.mock('../../../common/SearchBarComponent/SearchBar.component', () =>
jest.fn().mockImplementation(() => <div>SearchBar</div>) jest.fn().mockImplementation(() => <div>SearchBar</div>)
); );
const renderComponent = (props = {}) => {
return render(
<DndProvider backend={HTML5Backend}>
<TeamHierarchy {...teamHierarchyPropsData} {...props} />
</DndProvider>,
{ wrapper: MemoryRouter }
);
};
describe('Team Hierarchy page', () => { describe('Team Hierarchy page', () => {
it('Initially, Table should load', async () => { it('Initially, Table should load', async () => {
await act(async () => { renderComponent();
render(<TeamHierarchy {...teamHierarchyPropsData} />, {
wrapper: MemoryRouter,
});
});
const table = await screen.findByTestId('team-hierarchy-table'); const table = await screen.findByTestId('team-hierarchy-table');
@ -94,11 +101,7 @@ describe('Team Hierarchy page', () => {
}); });
it('Should render all table columns', async () => { it('Should render all table columns', async () => {
await act(async () => { renderComponent();
render(<TeamHierarchy {...teamHierarchyPropsData} />, {
wrapper: MemoryRouter,
});
});
const table = await screen.findByTestId('team-hierarchy-table'); const table = await screen.findByTestId('team-hierarchy-table');
const teamsColumn = await screen.findByText('label.team-plural'); const teamsColumn = await screen.findByText('label.team-plural');
@ -121,11 +124,7 @@ describe('Team Hierarchy page', () => {
}); });
it('Should render child row in table', async () => { it('Should render child row in table', async () => {
await act(async () => { renderComponent();
render(<TeamHierarchy {...teamHierarchyPropsData} />, {
wrapper: MemoryRouter,
});
});
const table = await screen.findByTestId('team-hierarchy-table'); 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 { compare } from 'fast-json-patch';
import { isEmpty, isUndefined } from 'lodash'; import { isEmpty, isUndefined } from 'lodash';
import { FC, useCallback, useMemo, useState } from 'react'; 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 { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { import {
@ -271,51 +269,49 @@ const TeamHierarchy: FC<TeamHierarchyProps> = ({
return ( return (
<div className="team-list-container"> <div className="team-list-container">
<DndProvider backend={HTML5Backend}> <Table
<Table className={classNames('teams-list-table drop-over-background', {
className={classNames('teams-list-table drop-over-background', { 'drop-over-table': isTableHovered,
'drop-over-table': isTableHovered, })}
})} columns={columns}
columns={columns} components={TABLE_CONSTANTS}
components={TABLE_CONSTANTS} data-testid="team-hierarchy-table"
data-testid="team-hierarchy-table" dataSource={data}
dataSource={data} expandable={expandableConfig}
expandable={expandableConfig} extraTableFilters={
extraTableFilters={ <Space align="center">
<Space align="center"> <span>
<span> <Switch
<Switch checked={showDeletedTeam}
checked={showDeletedTeam} data-testid="show-deleted"
data-testid="show-deleted" onClick={onShowDeletedTeamChange}
onClick={onShowDeletedTeamChange} />
/> <Typography.Text className="m-l-xs">
<Typography.Text className="m-l-xs"> {t('label.deleted')}
{t('label.deleted')} </Typography.Text>
</Typography.Text> </span>
</span>
{createTeamPermission && !isTeamDeleted && ( {createTeamPermission && !isTeamDeleted && (
<Button <Button
data-testid="add-team" data-testid="add-team"
type="primary" type="primary"
onClick={handleAddTeamButtonClick}> onClick={handleAddTeamButtonClick}>
{t('label.add-entity', { entity: t('label.team') })} {t('label.add-entity', { entity: t('label.team') })}
</Button> </Button>
)} )}
</Space> </Space>
} }
loading={isTableLoading} loading={isTableLoading}
locale={{ locale={{
emptyText: <FilterTablePlaceHolder />, emptyText: <FilterTablePlaceHolder />,
}} }}
pagination={false} pagination={false}
rowKey="name" rowKey="name"
searchProps={searchProps} searchProps={searchProps}
size="small" size="small"
onHeaderRow={onTableHeader} onHeaderRow={onTableHeader}
onRow={onTableRow} onRow={onTableRow}
/> />
</DndProvider>
<Modal <Modal
centered centered

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ import { WidgetConfig } from '../../pages/CustomizablePage/CustomizablePage.inte
import { import {
checkIfExpandViewSupported, checkIfExpandViewSupported,
getDefaultTabs, getDefaultTabs,
getTabDisplayName,
getTabLabelFromId, getTabLabelFromId,
getTabLabelMapFromTabs, getTabLabelMapFromTabs,
sortTabs, sortTabs,
@ -24,6 +25,43 @@ import {
} from './CustomizePageUtils'; } from './CustomizePageUtils';
describe('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', () => { describe('sortTabs', () => {
it('should sort tabs according to given order', () => { it('should sort tabs according to given order', () => {
const tabs: TabsProps['items'] = [ const tabs: TabsProps['items'] = [

View File

@ -691,3 +691,7 @@ export const updateWidgetHeightRecursively = (
return acc; return acc;
}, [] as WidgetConfig[]); }, [] 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 { TestSummary } from '../generated/tests/testCase';
import { FeedCounts } from '../interface/feed.interface'; import { FeedCounts } from '../interface/feed.interface';
import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface';
import { getTabLabelFromId } from './CustomizePage/CustomizePageUtils';
import i18n from './i18next/LocalUtil'; import i18n from './i18next/LocalUtil';
import { import {
getTableDetailPageBaseTabs, getTableDetailPageBaseTabs,
@ -106,7 +105,6 @@ class TableClassBase {
].map((tab: EntityTabs) => ({ ].map((tab: EntityTabs) => ({
id: tab, id: tab,
name: tab, name: tab,
displayName: getTabLabelFromId(tab),
layout: this.getDefaultLayout(tab), layout: this.getDefaultLayout(tab),
editable: tab === EntityTabs.SCHEMA, editable: tab === EntityTabs.SCHEMA,
})); }));