feat(webui): synchronize external DataGrid queries
This commit is contained in:
@@ -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`.
|
||||||
|
|
||||||
|
|||||||
@@ -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)])}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user