#22089: fix the table column selection not persisting for all action in dropdown (#22094)

* fix the table column selection not persisting for all action in dropdown

* added playwright test for the test

* move the utils changes to the component itself

* updated the code as the setter was not needed

* modify state name and remove unnecessary conditions
This commit is contained in:
Ashish Gupta 2025-07-04 14:43:49 +05:30 committed by GitHub
parent 5989df394d
commit 3e17b85f2e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 445 additions and 102 deletions

View File

@ -1262,6 +1262,10 @@ test.describe('Glossary tests', () => {
const checkboxLabels = ['Reviewer', 'Synonyms'];
await selectColumns(page, checkboxLabels);
await verifyColumnsVisibility(page, checkboxLabels, true);
await page.reload();
await page.waitForLoadState('networkidle');
await verifyColumnsVisibility(page, checkboxLabels, true);
}
);
@ -1272,6 +1276,10 @@ test.describe('Glossary tests', () => {
const checkboxLabels = ['Reviewer', 'Owners'];
await deselectColumns(page, checkboxLabels);
await verifyColumnsVisibility(page, checkboxLabels, false);
await page.reload();
await page.waitForLoadState('networkidle');
await verifyColumnsVisibility(page, checkboxLabels, false);
}
);
@ -1287,6 +1295,10 @@ test.describe('Glossary tests', () => {
'ACTIONS',
];
await verifyAllColumns(page, tableColumns, true);
await page.reload();
await page.waitForLoadState('networkidle');
await verifyAllColumns(page, tableColumns, true);
});
await test.step('Hide All columns selection', async () => {
@ -1299,6 +1311,10 @@ test.describe('Glossary tests', () => {
'STATUS',
];
await verifyAllColumns(page, tableColumns, false);
await page.reload();
await page.waitForLoadState('networkidle');
await verifyAllColumns(page, tableColumns, false);
});
} finally {
await glossaryTerm1.delete(apiContext);

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { render, screen } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { getCustomizeColumnDetails } from '../../../utils/CustomizeColumnUtils';
@ -25,10 +25,50 @@ jest.mock('../../../utils/CustomizeColumnUtils', () => ({
getReorderedColumns: jest.fn().mockImplementation((_, columns) => columns),
}));
jest.mock('../../../utils/TableUtils', () => ({
getTableExpandableConfig: jest.fn(),
}));
jest.mock('../SearchBarComponent/SearchBar.component', () =>
jest.fn().mockImplementation(() => <div>SearchBar</div>)
);
// Mock DraggableMenuItem component
jest.mock('./DraggableMenu/DraggableMenuItem.component', () =>
jest.fn().mockImplementation(({ currentItem, selectedOptions, onSelect }) => (
<div key={currentItem.value}>
<input
checked={selectedOptions.includes(currentItem.value)}
data-testid={`column-checkbox-${currentItem.value}`}
type="checkbox"
onChange={(e) => onSelect(currentItem.value, e.target.checked)}
/>
<label>{currentItem.label}</label>
</div>
))
);
// Mock hooks
const mockSetPreference = jest.fn();
const mockUseCurrentUserPreferences = {
preferences: {
selectedEntityTableColumns: {},
},
setPreference: mockSetPreference,
};
const mockUseGenericContext = {
type: 'table',
};
jest.mock('../../../hooks/currentUserStore/useCurrentUserStore', () => ({
useCurrentUserPreferences: jest.fn(() => mockUseCurrentUserPreferences),
}));
jest.mock('../../Customization/GenericProvider/GenericProvider', () => ({
useGenericContext: jest.fn(() => mockUseGenericContext),
}));
const mockColumns = [
{
title: 'Column 1',
@ -40,11 +80,16 @@ const mockColumns = [
dataIndex: 'col2',
key: 'col2',
},
{
title: 'Column 3',
dataIndex: 'col3',
key: 'col3',
},
];
const mockData = [
{ col1: 'Value 1', col2: 'Value 2' },
{ col1: 'Value 3', col2: 'Value 4' },
{ col1: 'Value 1', col2: 'Value 2', col3: 'Value 3' },
{ col1: 'Value 4', col2: 'Value 5', col3: 'Value 6' },
];
describe('Table component', () => {
@ -56,6 +101,11 @@ describe('Table component', () => {
);
};
beforeEach(() => {
jest.clearAllMocks();
mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {};
});
it('should display skeleton loader if loading is true', async () => {
renderComponent({ loading: true });
@ -105,4 +155,317 @@ describe('Table component', () => {
expect(screen.getByTestId('table-filters')).toBeInTheDocument();
});
describe('Column Selection Functionality', () => {
beforeEach(() => {
(getCustomizeColumnDetails as jest.Mock).mockReturnValue([
{ label: 'Column 1', value: 'col1' },
{ label: 'Column 2', value: 'col2' },
{ label: 'Column 3', value: 'col3' },
]);
});
it('should initialize column selections from existing user preferences', () => {
mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
table: ['col1', 'col2'],
};
renderComponent({
staticVisibleColumns: ['col1'],
defaultVisibleColumns: ['col2', 'col3'],
entityType: 'table',
});
// Component should use existing preferences
expect(mockSetPreference).not.toHaveBeenCalled();
});
it('should use default columns when no existing preferences and customization is enabled', () => {
mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {};
renderComponent({
staticVisibleColumns: ['col1'],
defaultVisibleColumns: ['col2', 'col3'],
entityType: 'table',
});
// Component should not automatically set preferences
expect(mockSetPreference).not.toHaveBeenCalled();
});
it('should require both static and default columns for customization', () => {
renderComponent({
staticVisibleColumns: ['col1'],
defaultVisibleColumns: ['col2'],
});
expect(screen.getByTestId('column-dropdown')).toBeInTheDocument();
});
it('should not render column dropdown when only staticVisibleColumns is provided', () => {
renderComponent({
staticVisibleColumns: ['col1'],
defaultVisibleColumns: undefined,
});
expect(screen.queryByTestId('column-dropdown')).not.toBeInTheDocument();
});
it('should not render column dropdown when only defaultVisibleColumns is provided', () => {
renderComponent({
staticVisibleColumns: undefined,
defaultVisibleColumns: ['col2'],
});
expect(screen.queryByTestId('column-dropdown')).not.toBeInTheDocument();
});
it('should not render column dropdown when both static and default columns are empty', () => {
renderComponent({
staticVisibleColumns: undefined,
defaultVisibleColumns: undefined,
});
expect(screen.queryByTestId('column-dropdown')).not.toBeInTheDocument();
});
it('should not enable customization when no static or default columns are provided', () => {
renderComponent();
expect(screen.queryByTestId('column-dropdown')).not.toBeInTheDocument();
});
it('should open column dropdown and show column options', async () => {
mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
table: ['col1', 'col2'],
};
renderComponent({
staticVisibleColumns: ['col1'],
defaultVisibleColumns: ['col2'],
});
const columnDropdown = screen.getByTestId('column-dropdown');
fireEvent.click(columnDropdown);
await waitFor(() => {
expect(screen.getByTestId('column-dropdown-title')).toBeInTheDocument();
expect(screen.getByText('label.column')).toBeInTheDocument();
});
});
it('should handle column selection when checkbox is clicked', async () => {
mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
table: ['col1', 'col2'],
};
renderComponent({
staticVisibleColumns: ['col1'],
defaultVisibleColumns: ['col2'],
entityType: 'table',
});
const columnDropdown = screen.getByTestId('column-dropdown');
fireEvent.click(columnDropdown);
await waitFor(() => {
expect(screen.getByTestId('column-checkbox-col2')).toBeInTheDocument();
});
const checkbox = screen.getByTestId('column-checkbox-col2');
fireEvent.click(checkbox);
// Verify that preferences are updated
expect(mockSetPreference).toHaveBeenCalledWith({
selectedEntityTableColumns: {
table: ['col1'],
},
});
});
it('should handle column addition when unchecked checkbox is clicked', async () => {
mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
table: ['col1'],
};
renderComponent({
staticVisibleColumns: ['col1'],
defaultVisibleColumns: ['col2', 'col3'],
entityType: 'table',
});
const columnDropdown = screen.getByTestId('column-dropdown');
fireEvent.click(columnDropdown);
await waitFor(() => {
expect(screen.getByTestId('column-checkbox-col3')).toBeInTheDocument();
});
const checkbox = screen.getByTestId('column-checkbox-col3');
fireEvent.click(checkbox);
expect(mockSetPreference).toHaveBeenCalledWith({
selectedEntityTableColumns: {
table: ['col1', 'col3'],
},
});
});
it('should show "View All" button when not all columns are selected', async () => {
mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
table: ['col1'],
};
renderComponent({
staticVisibleColumns: ['col1'],
defaultVisibleColumns: ['col2'],
});
const columnDropdown = screen.getByTestId('column-dropdown');
fireEvent.click(columnDropdown);
await waitFor(() => {
expect(
screen.getByTestId('column-dropdown-action-button')
).toBeInTheDocument();
expect(screen.getByText('label.view-all')).toBeInTheDocument();
});
});
it('should show "Hide All" button when all columns are selected', async () => {
mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
table: ['col1', 'col2', 'col3'],
};
renderComponent({
staticVisibleColumns: ['col1'],
defaultVisibleColumns: ['col2'],
});
const columnDropdown = screen.getByTestId('column-dropdown');
fireEvent.click(columnDropdown);
await waitFor(() => {
expect(
screen.getByTestId('column-dropdown-action-button')
).toBeInTheDocument();
expect(screen.getByText('label.hide-all')).toBeInTheDocument();
});
});
it('should select all columns when "View All" button is clicked', async () => {
mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
table: ['col1'],
};
renderComponent({
staticVisibleColumns: ['col1'],
defaultVisibleColumns: ['col2'],
entityType: 'table',
});
const columnDropdown = screen.getByTestId('column-dropdown');
fireEvent.click(columnDropdown);
await waitFor(() => {
expect(
screen.getByTestId('column-dropdown-action-button')
).toBeInTheDocument();
});
const viewAllButton = screen.getByTestId('column-dropdown-action-button');
fireEvent.click(viewAllButton);
expect(mockSetPreference).toHaveBeenCalledWith({
selectedEntityTableColumns: {
table: ['col1', 'col2', 'col3'],
},
});
});
it('should deselect all columns when "Hide All" button is clicked', async () => {
mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
table: ['col1', 'col2', 'col3'],
};
renderComponent({
staticVisibleColumns: ['col1'],
defaultVisibleColumns: ['col2'],
entityType: 'table',
});
const columnDropdown = screen.getByTestId('column-dropdown');
fireEvent.click(columnDropdown);
await waitFor(() => {
expect(
screen.getByTestId('column-dropdown-action-button')
).toBeInTheDocument();
});
const hideAllButton = screen.getByTestId('column-dropdown-action-button');
fireEvent.click(hideAllButton);
expect(mockSetPreference).toHaveBeenCalledWith({
selectedEntityTableColumns: {
table: [],
},
});
});
it('should preserve existing preferences for other entity types', async () => {
mockUseCurrentUserPreferences.preferences.selectedEntityTableColumns = {
dashboard: ['dash1', 'dash2'],
table: ['col1'],
};
renderComponent({
staticVisibleColumns: ['col1'],
defaultVisibleColumns: ['col2'],
entityType: 'table',
});
const columnDropdown = screen.getByTestId('column-dropdown');
fireEvent.click(columnDropdown);
await waitFor(() => {
expect(
screen.getByTestId('column-dropdown-action-button')
).toBeInTheDocument();
});
const viewAllButton = screen.getByTestId('column-dropdown-action-button');
fireEvent.click(viewAllButton);
expect(mockSetPreference).toHaveBeenCalledWith({
selectedEntityTableColumns: {
dashboard: ['dash1', 'dash2'],
table: ['col1', 'col2', 'col3'],
},
});
});
it('should render search bar when searchProps are provided', () => {
renderComponent({
staticVisibleColumns: ['col1'],
defaultVisibleColumns: ['col2'],
searchProps: {
placeholder: 'Search columns',
value: 'test',
onSearch: jest.fn(),
},
});
expect(screen.getByText('SearchBar')).toBeInTheDocument();
});
it('should not render column dropdown in full view mode', () => {
renderComponent({
staticVisibleColumns: undefined,
defaultVisibleColumns: undefined,
});
expect(screen.queryByTestId('column-dropdown')).not.toBeInTheDocument();
});
});
});

View File

@ -37,16 +37,12 @@ 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 { useApplicationStore } from '../../../hooks/useApplicationStore';
import { useCurrentUserPreferences } from '../../../hooks/currentUserStore/useCurrentUserStore';
import {
getCustomizeColumnDetails,
getReorderedColumns,
} from '../../../utils/CustomizeColumnUtils';
import {
getTableColumnConfigSelections,
getTableExpandableConfig,
handleUpdateTableColumnSelections,
} from '../../../utils/TableUtils';
import { getTableExpandableConfig } from '../../../utils/TableUtils';
import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider';
import Loader from '../Loader/Loader';
import NextPrevious from '../NextPrevious/NextPrevious';
@ -73,7 +69,6 @@ const Table = <T extends Record<string, unknown>>(
) => {
const { t } = useTranslation();
const { type } = useGenericContext();
const { currentUser } = useApplicationStore();
const [propsColumns, setPropsColumns] = useState<ColumnsType<T>>([]);
const [isDropdownVisible, setIsDropdownVisible] = useState<boolean>(false);
const [dropdownColumnList, setDropdownColumnList] = useState<
@ -86,6 +81,10 @@ const Table = <T extends Record<string, unknown>>(
() => ({ columns: propsColumns as Column[], minWidth: 80 }),
[propsColumns]
);
const {
preferences: { selectedEntityTableColumns },
setPreference,
} = useCurrentUserPreferences();
const isLoading = useMemo(
() => (loading as SpinProps)?.spinning ?? (loading as boolean) ?? false,
@ -94,9 +93,10 @@ const Table = <T extends Record<string, unknown>>(
const entityKey = useMemo(() => entityType ?? type, [type, entityType]);
// Check if the table is in Full View mode, if so, the dropdown and Customize Column feature is not available
const isFullViewTable = useMemo(
() => isEmpty(rest.staticVisibleColumns) && isEmpty(defaultVisibleColumns),
// Check if the table is customizable, if so, the dropdown and Customize Column feature is available
const isCustomizeColumnEnable = useMemo(
() =>
!isEmpty(rest.staticVisibleColumns) && !isEmpty(defaultVisibleColumns),
[rest.staticVisibleColumns, defaultVisibleColumns]
);
@ -110,28 +110,47 @@ const Table = <T extends Record<string, unknown>>(
const handleColumnItemSelect = useCallback(
(key: string, selected: boolean) => {
const updatedSelections = handleUpdateTableColumnSelections(
selected,
key,
columnDropdownSelections,
currentUser?.fullyQualifiedName ?? '',
entityKey
);
const updatedSelections = selected
? [...columnDropdownSelections, key]
: columnDropdownSelections.filter((item) => item !== key);
setPreference({
selectedEntityTableColumns: {
...selectedEntityTableColumns,
[entityKey]: updatedSelections,
},
});
setColumnDropdownSelections(updatedSelections);
},
[columnDropdownSelections, entityKey]
[columnDropdownSelections, selectedEntityTableColumns, entityKey]
);
const handleBulkColumnAction = useCallback(() => {
if (dropdownColumnList.length === columnDropdownSelections.length) {
setColumnDropdownSelections([]);
setPreference({
selectedEntityTableColumns: {
...selectedEntityTableColumns,
[entityKey]: [],
},
});
} else {
setColumnDropdownSelections(
dropdownColumnList.map((option) => option.value)
);
const columns = dropdownColumnList.map((option) => option.value);
setColumnDropdownSelections(columns);
setPreference({
selectedEntityTableColumns: {
...selectedEntityTableColumns,
[entityKey]: columns,
},
});
}
}, [dropdownColumnList, columnDropdownSelections]);
}, [
dropdownColumnList,
columnDropdownSelections,
selectedEntityTableColumns,
entityKey,
]);
const menu = useMemo(
() => ({
@ -203,15 +222,15 @@ const Table = <T extends Record<string, unknown>>(
};
useEffect(() => {
if (!isFullViewTable) {
if (isCustomizeColumnEnable) {
setDropdownColumnList(
getCustomizeColumnDetails<T>(rest.columns, rest.staticVisibleColumns)
);
}
}, [isFullViewTable, rest.columns, rest.staticVisibleColumns]);
}, [isCustomizeColumnEnable, rest.columns, rest.staticVisibleColumns]);
useEffect(() => {
if (isFullViewTable) {
if (!isCustomizeColumnEnable) {
setPropsColumns(rest.columns ?? []);
} else {
const filteredColumns = (rest.columns ?? []).filter(
@ -223,28 +242,31 @@ const Table = <T extends Record<string, unknown>>(
setPropsColumns(getReorderedColumns(dropdownColumnList, filteredColumns));
}
}, [
isFullViewTable,
isCustomizeColumnEnable,
rest.columns,
columnDropdownSelections,
rest.staticVisibleColumns,
]);
useEffect(() => {
const selections = getTableColumnConfigSelections(
currentUser?.fullyQualifiedName ?? '',
entityKey,
isFullViewTable,
defaultVisibleColumns
);
setColumnDropdownSelections(selections);
}, [entityKey, defaultVisibleColumns, isFullViewTable]);
if (isCustomizeColumnEnable) {
setColumnDropdownSelections(
selectedEntityTableColumns?.[entityKey] ?? defaultVisibleColumns ?? []
);
}
}, [
isCustomizeColumnEnable,
selectedEntityTableColumns,
entityKey,
defaultVisibleColumns,
]);
return (
<Row className={classNames('table-container', rest.containerClassName)}>
<Col
className={classNames({
'p-y-md': searchProps ?? rest.extraTableFilters ?? !isFullViewTable,
'p-y-md':
searchProps ?? rest.extraTableFilters ?? isCustomizeColumnEnable,
})}
span={24}>
<Row className="p-x-md">
@ -260,7 +282,7 @@ const Table = <T extends Record<string, unknown>>(
/>
</Col>
) : null}
{(rest.extraTableFilters || !isFullViewTable) && (
{(rest.extraTableFilters || isCustomizeColumnEnable) && (
<Col
className={classNames(
'd-flex justify-end items-center gap-5',
@ -268,7 +290,7 @@ const Table = <T extends Record<string, unknown>>(
)}
span={searchProps ? 12 : 24}>
{rest.extraTableFilters}
{!isFullViewTable && (
{isCustomizeColumnEnable && (
<DndProvider backend={HTML5Backend}>
<Dropdown
className="custom-column-dropdown-menu text-primary"

View File

@ -15,8 +15,9 @@ import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import { useApplicationStore } from '../useApplicationStore';
interface UserPreferences {
export interface UserPreferences {
isSidebarCollapsed: boolean;
selectedEntityTableColumns: Record<string, string[]>;
}
interface Store {
@ -31,6 +32,7 @@ interface Store {
const defaultPreferences: UserPreferences = {
isSidebarCollapsed: false,
selectedEntityTableColumns: {},
// Add default values for other preferences
};

View File

@ -743,66 +743,6 @@ export const updateFieldDescription = <T extends TableFieldsInfoCommonEntities>(
});
};
export const getTableColumnConfigSelections = (
userFqn: string,
entityType: string | undefined,
isFullViewTable: boolean,
defaultColumns: string[] | undefined
) => {
if (!userFqn) {
return [];
}
const storageKey = `selectedColumns-${userFqn}`;
const selectedColumns = JSON.parse(localStorage.getItem(storageKey) ?? '{}');
if (entityType) {
if (selectedColumns[entityType]) {
return selectedColumns[entityType];
} else if (!isFullViewTable) {
localStorage.setItem(
storageKey,
JSON.stringify({
...selectedColumns,
[entityType]: defaultColumns,
})
);
return defaultColumns;
}
}
return [];
};
export const handleUpdateTableColumnSelections = (
selected: boolean,
key: string,
columnDropdownSelections: string[],
userFqn: string,
entityType: string | undefined
) => {
const updatedSelections = selected
? [...columnDropdownSelections, key]
: columnDropdownSelections.filter((item) => item !== key);
// Updating localStorage
const selectedColumns = JSON.parse(
localStorage.getItem(`selectedColumns-${userFqn}`) ?? '{}'
);
if (entityType) {
localStorage.setItem(
`selectedColumns-${userFqn}`,
JSON.stringify({
...selectedColumns,
[entityType]: updatedSelections,
})
);
}
return updatedSelections;
};
export const getTableDetailPageBaseTabs = ({
queryCount,
isTourOpen,