feat(webui): synchronize external DataGrid queries

This commit is contained in:
2026-07-22 09:03:11 +02:00
parent 22e8183846
commit cece71d945
3 changed files with 49 additions and 1 deletions

View File

@@ -239,6 +239,9 @@ instead of reproducing their behavior.
and the backend applies every emitted filter/sort before pagination while and the backend applies every emitted filter/sort before pagination while
returning `totalRows` for the filtered result. Server list filters declare returning `totalRows` for the filtered result. Server list filters declare
their complete option domain instead of deriving it from the loaded page. their complete option domain instead of deriving it from the loaded page.
External filter affordances such as summary-count shortcuts update the
grid's `query` contract; the grid header controls and backend query therefore
always display and execute the same filter state.
- Feedback and confirmation use `Dialog`, `ConfirmDialog`, or - Feedback and confirmation use `Dialog`, `ConfirmDialog`, or
`DismissibleAlert`. They never fall back to `window.alert`. `DismissibleAlert`. They never fall back to `window.alert`.

View File

@@ -116,6 +116,12 @@ type DataGridBaseProps<T> = {
storageKey?: string; storageKey?: string;
initialFilters?: Record<string, string | string[]>; initialFilters?: Record<string, string | string[]>;
initialSort?: {columnId: string;direction: DataGridSortDirection;}; initialSort?: {columnId: string;direction: DataGridSortDirection;};
/**
* Synchronize a query selected outside the grid, for example by a summary
* count shortcut. Server grids should feed `onQueryChange` back into this
* value so the header controls and backend query remain in agreement.
*/
query?: DataGridQueryState;
}; };
/** /**
@@ -204,6 +210,7 @@ export default function DataGrid<T>({
storageKey, storageKey,
initialFilters = {}, initialFilters = {},
initialSort, initialSort,
query,
pagination, pagination,
onQueryChange onQueryChange
}: DataGridProps<T>) { }: DataGridProps<T>) {
@@ -230,7 +237,15 @@ export default function DataGrid<T>({
const normalizedInitialFilters = useMemo(() => normalizeInitialFilters(initialFilters), [initialFiltersKey]); const normalizedInitialFilters = useMemo(() => normalizeInitialFilters(initialFilters), [initialFiltersKey]);
const initialSortKey = initialSort ? `${initialSort.columnId}:${initialSort.direction}` : ""; const initialSortKey = initialSort ? `${initialSort.columnId}:${initialSort.direction}` : "";
const normalizedInitialSort = useMemo(() => initialSort ? { ...initialSort } : undefined, [initialSortKey]); const normalizedInitialSort = useMemo(() => initialSort ? { ...initialSort } : undefined, [initialSortKey]);
const [state, setState] = useState<DataGridState>(() => mergeInitialSort(mergeInitialFilters(loadState(localStorageKey), normalizedInitialFilters), normalizedInitialSort)); const externalQueryKey = query ? JSON.stringify(query) : "";
const normalizedExternalQuery = useMemo(
() => query ? { sort: query.sort ? { ...query.sort } : null, filters: { ...query.filters } } : undefined,
[externalQueryKey]
);
const [state, setState] = useState<DataGridState>(() => mergeExternalQuery(
mergeInitialSort(mergeInitialFilters(loadState(localStorageKey), normalizedInitialFilters), normalizedInitialSort),
normalizedExternalQuery
));
const [resizeState, setResizeState] = useState<ColumnResizeState | null>(null); const [resizeState, setResizeState] = useState<ColumnResizeState | null>(null);
const [openFilterColumnId, setOpenFilterColumnId] = useState<string | null>(null); const [openFilterColumnId, setOpenFilterColumnId] = useState<string | null>(null);
const [filterPosition, setFilterPosition] = useState<FilterPosition | null>(null); const [filterPosition, setFilterPosition] = useState<FilterPosition | null>(null);
@@ -265,6 +280,10 @@ export default function DataGrid<T>({
setState((current) => mergeInitialSort(current, normalizedInitialSort)); setState((current) => mergeInitialSort(current, normalizedInitialSort));
}, [normalizedInitialSort]); }, [normalizedInitialSort]);
useEffect(() => {
setState((current) => mergeExternalQuery(current, normalizedExternalQuery));
}, [normalizedExternalQuery]);
useEffect(() => { useEffect(() => {
try { try {
window.localStorage.setItem(localStorageKey, JSON.stringify(state)); window.localStorage.setItem(localStorageKey, JSON.stringify(state));
@@ -1241,6 +1260,20 @@ initialSort?: {columnId: string;direction: DataGridSortDirection;})
return { ...state, sort: initialSort }; return { ...state, sort: initialSort };
} }
function mergeExternalQuery(state: DataGridState, query?: DataGridQueryState): DataGridState {
if (!query) return state;
const current = {
sort: state.sort ?? null,
filters: state.filters ?? {}
};
if (dataGridQueriesEqual(current, query)) return state;
return {
...state,
sort: query.sort ? { ...query.sort } : null,
filters: { ...query.filters }
};
}
function formatListFilter(values: string[]): string { function formatListFilter(values: string[]): string {
return `list:${JSON.stringify([...new Set(values)])}`; return `list:${JSON.stringify([...new Set(values)])}`;
} }

View File

@@ -117,3 +117,15 @@ const clearedFilterMarkup = renderToStaticMarkup(
); );
assertEqual(clearedFilterMarkup.includes("Ordinary row"), true, "clearing filters restores the first unfiltered row"); assertEqual(clearedFilterMarkup.includes("Ordinary row"), true, "clearing filters restores the first unfiltered row");
assertEqual(/of(?:<!-- -->)?\s*(?:<!-- -->)?6/.test(clearedFilterMarkup), true, "clearing filters restores the full pagination total"); assertEqual(/of(?:<!-- -->)?\s*(?:<!-- -->)?6/.test(clearedFilterMarkup), true, "clearing filters restores the full pagination total");
const externalShortcutMarkup = renderToStaticMarkup(
<DataGrid
id="external-query-shortcut-regression"
rows={filterRows}
columns={filterColumns}
getRowKey={(row) => row.id}
query={{ sort: null, filters: { name: "target" } }}
/>
);
assertEqual(externalShortcutMarkup.includes("Target alpha"), true, "an external count shortcut synchronizes the grid query");
assertEqual(externalShortcutMarkup.includes("Ordinary row"), false, "the synchronized query does not disagree with visible rows");