-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlist-context.tsx
More file actions
102 lines (93 loc) · 3.38 KB
/
list-context.tsx
File metadata and controls
102 lines (93 loc) · 3.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// useList — fetch + cache the paginated list endpoint.
//
// Thin wrapper around @dar/api's list call + the shared SWR cache
// helper. Page packages MUST consume this hook (never @dar/api
// directly — see CLAUDE.md §7).
import { useMemo } from 'react';
import type { ApiClient, ListResponse } from '@dar/api';
import { type SwrState, useSwrCache } from './swr-cache';
export interface UseListParams {
client: ApiClient;
appLabel: string;
modelName: string;
q?: string;
page?: number;
pageSize?: number;
ordering?: string;
/** "Show all N" flag (Django `ALL_VAR`, #385): drop pagination and
* fetch every row when `total <= list_max_show_all`. */
all?: boolean;
/** `list_filter` query params keyed by descriptor name. */
filters?: Record<string, string>;
}
export type ListState = SwrState<ListResponse>;
function serializeFilters(filters?: Record<string, string>): string {
if (!filters) return '';
return Object.keys(filters)
.filter((k) => filters[k] !== '' && filters[k] != null)
.sort()
.map((k) => `${k}=${filters[k]}`)
.join('&');
}
function cacheKeyFor(p: UseListParams): string {
// Versioned + parameter-discriminated so two queries with different
// pagination don't trample each other in localStorage.
return [
'dar:list:v1',
p.appLabel,
p.modelName,
p.q ?? '',
p.page ?? 1,
p.pageSize ?? 0,
p.ordering ?? '',
p.all ? 'all' : '',
serializeFilters(p.filters),
].join('|');
}
export function useList(params: UseListParams): ListState {
const cacheKey = cacheKeyFor(params);
const filtersKey = serializeFilters(params.filters);
const fetcher = useMemo(
() => () => {
const query: Parameters<ApiClient['list']>[2] = {};
if (params.q !== undefined) query.q = params.q;
if (params.page !== undefined) query.page = params.page;
if (params.pageSize !== undefined) query.page_size = params.pageSize;
if (params.ordering !== undefined) query.ordering = params.ordering;
if (params.all !== undefined) query.all = params.all;
if (params.filters !== undefined) query.filters = params.filters;
return params.client.list(params.appLabel, params.modelName, query);
},
// `params.filters` tracked via its serialised form (`filtersKey`)
// so a new object identity with identical contents doesn't refetch.
// eslint-disable-next-line react-hooks/exhaustive-deps
[
params.client,
params.appLabel,
params.modelName,
params.q,
params.page,
params.pageSize,
params.ordering,
params.all,
filtersKey,
],
);
return useSwrCache<ListResponse>({
cacheKey,
fetcher,
deps: [cacheKey],
// Keep the list live without a manual reload: poll in the
// background and re-validate on focus (the hook's default).
refetchInterval: LIST_REFETCH_INTERVAL_MS,
// On a filter / page / search change the cache key changes; keep the
// prior response on screen so the page chrome (columns, header) stays
// put and only the table shows a loading skeleton, instead of the
// whole page blanking to a full-page skeleton (#368).
keepPreviousData: true,
});
}
// 15s background poll — a sensible "live enough" cadence for an admin
// list without hammering the backend. Focus re-validation (the hook
// default) covers the come-back-to-the-tab case between polls.
const LIST_REFETCH_INTERVAL_MS = 15_000;