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/CHANGELOG.md +7 -0
- package/admin/AdminApp.tsx +311 -158
- package/admin/adapters.ts +99 -0
- package/admin/httpEngine.ts +56 -0
- package/admin/index.tsx +91 -39
- package/admin/ui/FilterBar.tsx +231 -0
- package/admin/ui/Popover.tsx +54 -0
- package/admin/ui/Sidebar.tsx +86 -0
- package/admin/ui/icons.tsx +146 -0
- package/admin/ui/styles.ts +398 -0
- package/admin/useAdmin.ts +149 -14
- package/dist/admin.js +395 -17
- package/dist/dev/harness.d.ts +17 -0
- package/dist/dev/harness.js +39 -1
- package/dist/rastack-admin.d.ts +14 -0
- package/dist/rastack-admin.js +128 -0
- package/dist/rastack.d.ts +1 -1
- package/dist/rastack.js +7 -23
- package/dist/wasm/rastack_wasm.js +1 -1
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/src/dev/harness.ts +44 -0
- package/src/rastack-admin.ts +112 -0
- package/src/rastack.ts +8 -28
- package/test/dev.spec.ts +96 -0
- package/wasm/rastack_wasm.js +1 -1
- package/wasm/rastack_wasm_bg.wasm +0 -0
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
|
-
/**
|
|
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
|
-
):
|
|
66
|
+
): ResourceTable {
|
|
45
67
|
const [page, setPage] = useState(1);
|
|
46
|
-
const [search,
|
|
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
|
-
|
|
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
|
|
90
|
+
const reserved: Record<string, string> = {
|
|
66
91
|
page: String(page),
|
|
67
92
|
page_size: String(PAGE_SIZE),
|
|
68
93
|
};
|
|
69
|
-
if (search)
|
|
94
|
+
if (search) reserved.search = search;
|
|
70
95
|
engine
|
|
71
|
-
.handle(
|
|
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 {
|
|
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. */
|