From 3e17b85f2e69a646f093d015f2682a91c44f751c Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Fri, 4 Jul 2025 14:43:49 +0530 Subject: [PATCH] #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 --- .../ui/playwright/e2e/Pages/Glossary.spec.ts | 16 + .../components/common/Table/Table.test.tsx | 369 +++++++++++++++++- .../ui/src/components/common/Table/Table.tsx | 98 +++-- .../currentUserStore/useCurrentUserStore.ts | 4 +- .../resources/ui/src/utils/TableUtils.tsx | 60 --- 5 files changed, 445 insertions(+), 102 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 7a290862369..9f1d8bc493f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -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); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.test.tsx index 05451efca46..1c41207ec26 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.test.tsx @@ -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(() =>
SearchBar
) ); +// Mock DraggableMenuItem component +jest.mock('./DraggableMenu/DraggableMenuItem.component', () => + jest.fn().mockImplementation(({ currentItem, selectedOptions, onSelect }) => ( +
+ onSelect(currentItem.value, e.target.checked)} + /> + +
+ )) +); + +// 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(); + }); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.tsx index 09afc2a4aad..776911096cd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Table/Table.tsx @@ -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 = >( ) => { const { t } = useTranslation(); const { type } = useGenericContext(); - const { currentUser } = useApplicationStore(); const [propsColumns, setPropsColumns] = useState>([]); const [isDropdownVisible, setIsDropdownVisible] = useState(false); const [dropdownColumnList, setDropdownColumnList] = useState< @@ -86,6 +81,10 @@ const Table = >( () => ({ 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 = >( 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 = >( 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 = >( }; useEffect(() => { - if (!isFullViewTable) { + if (isCustomizeColumnEnable) { setDropdownColumnList( getCustomizeColumnDetails(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 = >( 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 ( @@ -260,7 +282,7 @@ const Table = >( /> ) : null} - {(rest.extraTableFilters || !isFullViewTable) && ( + {(rest.extraTableFilters || isCustomizeColumnEnable) && ( >( )} span={searchProps ? 12 : 24}> {rest.extraTableFilters} - {!isFullViewTable && ( + {isCustomizeColumnEnable && ( ; } interface Store { @@ -31,6 +32,7 @@ interface Store { const defaultPreferences: UserPreferences = { isSidebarCollapsed: false, + selectedEntityTableColumns: {}, // Add default values for other preferences }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index cab6d8a2bc9..4fa7a98db00 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -743,66 +743,6 @@ export const updateFieldDescription = ( }); }; -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,