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,
})