rastack 0.0.44 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/admin/useAdmin.ts CHANGED
@@ -17,18 +17,25 @@ import type {
17
17
  import type { FieldOption } from "../hooks/form/interfaces";
18
18
  import {
19
19
  AdminResource,
20
+ FilterKind,
21
+ FilterOption,
20
22
  PAGE_SIZE,
23
+ boolFilterOptions,
21
24
  buildWriteBody,
22
25
  changelistHeaders,
23
26
  detailPath,
27
+ distinctValueOptions,
24
28
  errorDetail,
25
29
  fieldControlType,
30
+ filterableFields,
31
+ fkFilterOptions,
26
32
  formFields,
27
33
  initialFormValues,
28
34
  listPath,
29
35
  optionsFromRows,
30
36
  pageCount,
31
37
  readListEnvelope,
38
+ withFilters,
32
39
  } from "./adapters";
33
40
 
34
41
  interface EngineResult {
@@ -37,13 +44,29 @@ interface EngineResult {
37
44
  }
38
45
  const ok = (r: EngineResult) => r.status >= 200 && r.status < 300;
39
46
 
40
- /** Drive a `<DataTable>` for one resource, backed by the WASM engine. */
47
+ /** What `useResourceTable` returns: a `TableQuery` plus changelist controls. */
48
+ export interface ResourceTable {
49
+ query: TableQuery;
50
+ reload: () => void;
51
+ /** Active `field → value` equality filters (the filter-bar chips). */
52
+ filters: Record<string, string>;
53
+ setFilter: (field: string, value: string) => void;
54
+ removeFilter: (field: string) => void;
55
+ clearFilters: () => void;
56
+ /** Current search text and its setter (rendered in the filter bar). */
57
+ search: string | undefined;
58
+ setSearch: (value: string | undefined) => void;
59
+ itemCount: number;
60
+ }
61
+
62
+ /** Drive a `<DataTable>` for one resource, backed by any `RastackEngine`. */
41
63
  export function useResourceTable(
42
64
  engine: RastackEngine,
43
65
  resource: AdminResource,
44
- ): { query: TableQuery; reload: () => void } {
66
+ ): ResourceTable {
45
67
  const [page, setPage] = useState(1);
46
- const [search, setSearch] = useState<string | undefined>(undefined);
68
+ const [search, setSearchState] = useState<string | undefined>(undefined);
69
+ const [filters, setFilters] = useState<Record<string, string>>({});
47
70
  const [data, setData] = useState<Record<string, unknown>[] | undefined>(
48
71
  undefined,
49
72
  );
@@ -52,23 +75,30 @@ export function useResourceTable(
52
75
  const [error, setError] = useState<unknown>(undefined);
53
76
  const [tick, setTick] = useState(0);
54
77
 
55
- // Reset paging/search when the resource changes.
78
+ // Reset paging/search/filters when the resource changes.
56
79
  useEffect(() => {
57
80
  setPage(1);
58
- setSearch(undefined);
81
+ setSearchState(undefined);
82
+ setFilters({});
59
83
  }, [resource.key]);
60
84
 
85
+ const filterKey = JSON.stringify(filters);
61
86
  useEffect(() => {
62
87
  let cancelled = false;
63
88
  setLoading(true);
64
89
  setError(undefined);
65
- const query: Record<string, string> = {
90
+ const reserved: Record<string, string> = {
66
91
  page: String(page),
67
92
  page_size: String(PAGE_SIZE),
68
93
  };
69
- if (search) query.search = search;
94
+ if (search) reserved.search = search;
70
95
  engine
71
- .handle("GET", listPath(resource.app, resource.model), query, undefined)
96
+ .handle(
97
+ "GET",
98
+ listPath(resource.app, resource.model),
99
+ withFilters(reserved, filters),
100
+ undefined,
101
+ )
72
102
  .then((res: EngineResult) => {
73
103
  if (cancelled) return;
74
104
  if (ok(res)) {
@@ -90,7 +120,28 @@ export function useResourceTable(
90
120
  return () => {
91
121
  cancelled = true;
92
122
  };
93
- }, [engine, resource.key, resource.app, resource.model, page, search, tick]);
123
+ }, [engine, resource.key, resource.app, resource.model, page, search, filterKey, tick]);
124
+
125
+ const setSearch = (v: string | undefined) => {
126
+ setSearchState(v && v.length ? v : undefined);
127
+ setPage(1);
128
+ };
129
+ const setFilter = (field: string, value: string) => {
130
+ setFilters((f) => ({ ...f, [field]: value }));
131
+ setPage(1);
132
+ };
133
+ const removeFilter = (field: string) => {
134
+ setFilters((f) => {
135
+ const next = { ...f };
136
+ delete next[field];
137
+ return next;
138
+ });
139
+ setPage(1);
140
+ };
141
+ const clearFilters = () => {
142
+ setFilters({});
143
+ setPage(1);
144
+ };
94
145
 
95
146
  const pagination: IPaginationActions = {
96
147
  currentPage: page,
@@ -106,10 +157,7 @@ export function useResourceTable(
106
157
  };
107
158
  const searching: ISearchingActions<Record<string, unknown>> = {
108
159
  searchText: search,
109
- setSearchText: (v) => {
110
- setSearch(v && v.length ? v : undefined);
111
- setPage(1);
112
- },
160
+ setSearchText: (v) => setSearch(v),
113
161
  };
114
162
 
115
163
  const query: TableQuery = {
@@ -122,7 +170,94 @@ export function useResourceTable(
122
170
  // No `sorting`: the store orders by id only, so sortable headers would lie.
123
171
  searching: resource.search.length ? searching : undefined,
124
172
  };
125
- return { query, reload: () => setTick((t) => t + 1) };
173
+ return {
174
+ query,
175
+ reload: () => setTick((t) => t + 1),
176
+ filters,
177
+ setFilter,
178
+ removeFilter,
179
+ clearFilters,
180
+ search,
181
+ setSearch,
182
+ itemCount: count,
183
+ };
184
+ }
185
+
186
+ /** A filter facet ready for the filter bar: its label, kind and value options. */
187
+ export interface FilterFieldModel {
188
+ name: string;
189
+ label: string;
190
+ kind: FilterKind;
191
+ options: FilterOption[];
192
+ }
193
+
194
+ /**
195
+ * Discover the changelist's filter facets and their value options from the
196
+ * engine: FK fields list their target table, `choice` (plain-string) fields are
197
+ * scanned for distinct values, booleans are True/False. Only facets that have
198
+ * at least one option are surfaced.
199
+ */
200
+ export function useFilterModel(
201
+ engine: RastackEngine,
202
+ resource: AdminResource,
203
+ ): { fields: FilterFieldModel[]; loading: boolean } {
204
+ const specs = useMemo(() => filterableFields(resource), [resource.key]);
205
+ const [fields, setFields] = useState<FilterFieldModel[]>([]);
206
+ const [loading, setLoading] = useState(specs.length > 0);
207
+
208
+ useEffect(() => {
209
+ let cancelled = false;
210
+ if (!specs.length) {
211
+ setFields([]);
212
+ setLoading(false);
213
+ return;
214
+ }
215
+ setLoading(true);
216
+ (async () => {
217
+ // Distinct-value facets need a scan of this table's rows (once).
218
+ let rows: Record<string, unknown>[] = [];
219
+ if (specs.some((s) => s.kind === "choice")) {
220
+ const res: EngineResult = await engine.handle(
221
+ "GET",
222
+ listPath(resource.app, resource.model),
223
+ { page_size: "1000" },
224
+ undefined,
225
+ );
226
+ rows = readListEnvelope(res.body).results;
227
+ }
228
+ const built = await Promise.all(
229
+ specs.map(async (s): Promise<FilterFieldModel> => {
230
+ let options: FilterOption[] = [];
231
+ if (s.kind === "bool") options = boolFilterOptions();
232
+ else if (s.kind === "choice") options = distinctValueOptions(rows, s.name);
233
+ else if (s.kind === "fk" && s.relation) {
234
+ const res: EngineResult = await engine.handle(
235
+ "GET",
236
+ listPath(s.relation.app, s.relation.model),
237
+ { page_size: "1000" },
238
+ undefined,
239
+ );
240
+ options = fkFilterOptions(readListEnvelope(res.body).results);
241
+ }
242
+ return { name: s.name, label: s.label, kind: s.kind, options };
243
+ }),
244
+ );
245
+ if (!cancelled) {
246
+ setFields(built.filter((f) => f.options.length > 0));
247
+ setLoading(false);
248
+ }
249
+ })().catch(() => {
250
+ if (!cancelled) {
251
+ setFields([]);
252
+ setLoading(false);
253
+ }
254
+ });
255
+ return () => {
256
+ cancelled = true;
257
+ };
258
+ }, [engine, resource.key]);
259
+
260
+ return { fields, loading };
126
261
  }
127
262
 
128
263
  /** State a form screen needs beyond the `AutoFormLike` itself. */