diff --git a/frontend/src/components/TableView/TableView.test.tsx b/frontend/src/components/TableView/TableView.test.tsx index 80366567..43fe3f2b 100644 --- a/frontend/src/components/TableView/TableView.test.tsx +++ b/frontend/src/components/TableView/TableView.test.tsx @@ -1,15 +1,24 @@ import { describe, expect, it, jest, beforeEach } from '@jest/globals' import '@testing-library/jest-dom' -import { fireEvent, render, waitFor } from '@testing-library/react' +import { act, fireEvent, render, waitFor } from '@testing-library/react' import { Role } from '@/shared/types' import { TableView } from './TableView' import { usePageContext } from '../Page' import { useUser } from '@/hooks/user' +type CapturedTableOptions = { + onColumnFiltersChange?: (filters: Array<{ id: string; value: unknown }>) => void +} + +let lastMaterialReactTableOptions: CapturedTableOptions | null = null + jest.mock('material-react-table', () => ({ - useMaterialReactTable: () => ({ - getPrePaginationRowModel: () => ({ rows: [] }), - }), + useMaterialReactTable: (options: CapturedTableOptions) => { + lastMaterialReactTableOptions = options + return { + getPrePaginationRowModel: () => ({ rows: [] }), + } + }, MaterialReactTable: () =>
, })) @@ -48,6 +57,8 @@ describe('TableView table help integration', () => { mockLocationSearch = '' mockNavigate = jest.fn() window.sessionStorage.clear() + window.localStorage.clear() + lastMaterialReactTableOptions = null mockUsePageContext.mockReturnValue({ editRights: {}, idList: [], @@ -233,6 +244,90 @@ describe('TableView table help integration', () => { ) }) + it('restores persisted column filters for normal table visits', async () => { + const setSqlColumnFilters = jest.fn() + const persistedFilters = [{ id: 'name', value: 'Alpha' }] + + window.localStorage.setItem('nowdatabase-table-columnfilters:test', JSON.stringify(persistedFilters)) + mockUsePageContext.mockReturnValue({ + editRights: {}, + idList: [], + idFieldName: 'id', + viewName: 'test', + previousTableUrls: [], + createTitle: () => '', + createSubtitle: () => '', + sqlLimit: 25, + sqlOffset: 0, + sqlColumnFilters: [], + sqlOrderBy: [], + setIdList: jest.fn(), + setSqlLimit: jest.fn(), + setSqlOffset: jest.fn(), + setSqlColumnFilters, + setSqlOrderBy: jest.fn(), + setPreviousTableUrls: jest.fn(), + }) + + render( + + title="Test Table" + idFieldName="id" + columns={[ + { header: 'Name', accessorKey: 'name' }, + { header: 'Id', accessorKey: 'id' }, + ]} + visibleColumns={{ name: true, id: true }} + data={[ + { + id: '1', + name: 'Alpha', + full_count: 1, + }, + ]} + url="test" + isFetching={false} + /> + ) + + await waitFor(() => { + expect(setSqlColumnFilters).toHaveBeenLastCalledWith(persistedFilters) + }) + }) + + it('persists changed column filters for later table visits', async () => { + render( + + title="Test Table" + idFieldName="id" + columns={[ + { header: 'Name', accessorKey: 'name' }, + { header: 'Id', accessorKey: 'id' }, + ]} + visibleColumns={{ name: true, id: true }} + data={[ + { + id: '1', + name: 'Alpha', + full_count: 1, + }, + ]} + url="test" + isFetching={false} + /> + ) + + act(() => { + lastMaterialReactTableOptions?.onColumnFiltersChange?.([{ id: 'name', value: 'Alpha' }]) + }) + + await waitFor(() => { + expect(window.localStorage.getItem('nowdatabase-table-columnfilters:test')).toEqual( + JSON.stringify([{ id: 'name', value: 'Alpha' }]) + ) + }) + }) + it('shows help with multi-sort guidance for regular tables', () => { const { getByRole, getByText } = render( diff --git a/frontend/src/components/TableView/TableView.tsx b/frontend/src/components/TableView/TableView.tsx index df461fce..6a93275c 100755 --- a/frontend/src/components/TableView/TableView.tsx +++ b/frontend/src/components/TableView/TableView.tsx @@ -41,6 +41,7 @@ const TEXT_FILTER_MODE_OPTIONS = ['equals', 'contains', 'startsWith'] as const type TextFilterModeOption = (typeof TEXT_FILTER_MODE_OPTIONS)[number] const TABLE_STATE_URL_PARAM = 'tableState' const TABLE_STATE_STORAGE_PREFIX = 'nowdatabase-table-state' +const TABLE_FILTERS_STORAGE_PREFIX = 'nowdatabase-table-columnfilters' const isEmptyFilterValue = (value: unknown): boolean => { if (Array.isArray(value)) { @@ -341,6 +342,7 @@ export const TableView = ({ } const getTableStateStorageKey = (stateId: string) => `${TABLE_STATE_STORAGE_PREFIX}:${stateId}` + const getPersistentColumnFiltersStorageKey = () => `${TABLE_FILTERS_STORAGE_PREFIX}:${url ?? location.pathname}` const loadStoredTableState = (stateId: string) => { const storedValue = window.sessionStorage.getItem(getTableStateStorageKey(stateId)) @@ -350,10 +352,28 @@ export const TableView = ({ return isStoredTableState(parsed) ? parsed : undefined } + const loadPersistentColumnFilters = () => { + const storedValue = window.localStorage.getItem(getPersistentColumnFiltersStorageKey()) + if (!storedValue) return + + const parsed = safeJsonParse(storedValue) + return isColumnFiltersState(parsed) ? normalizeColumnFilters(parsed) : undefined + } + const saveStoredTableState = (state: StoredTableState) => { window.sessionStorage.setItem(getTableStateStorageKey(tableStateId), JSON.stringify(state)) } + const savePersistentColumnFilters = (filters: MRT_ColumnFiltersState) => { + const storageKey = getPersistentColumnFiltersStorageKey() + if (filters.length === 0) { + window.localStorage.removeItem(storageKey) + return + } + + window.localStorage.setItem(storageKey, JSON.stringify(filters)) + } + const buildTableStateUrl = () => `${location.pathname}?${TABLE_STATE_URL_PARAM}=${encodeURIComponent(tableStateId)}` const loadStateFromUrl = ( @@ -379,7 +399,13 @@ export const TableView = ({ return stateFromStorage as unknown as TState } - if (!stateFromUrl) return defaultState + if (!stateFromUrl) { + if (state === 'columnfilters') { + return (loadPersistentColumnFilters() ?? defaultState) as TState + } + + return defaultState + } const parsed = safeJsonParse(stateFromUrl) if (parsed === undefined) { return defaultState @@ -645,6 +671,7 @@ export const TableView = ({ if (selectorFn || !hasLoadedTableState) return const sanitizedFilters = sanitizeColumnFilters(columnFilters) saveStoredTableState({ columnfilters: sanitizedFilters, sorting, pagination }) + savePersistentColumnFilters(sanitizedFilters) navigate(buildTableStateUrl(), { replace: true, })