rastack 0.0.42 → 0.0.44
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 +15 -0
- package/admin/AdminApp.tsx +308 -0
- package/admin/adapters.ts +247 -0
- package/admin/index.tsx +88 -0
- package/admin/useAdmin.ts +299 -0
- package/dist/admin.js +81 -0
- package/dist/dev/harness.d.ts +53 -0
- package/dist/dev/harness.js +151 -0
- package/dist/rastack-dev.d.ts +18 -0
- package/dist/rastack-dev.js +197 -0
- package/dist/rastack-init.d.ts +3 -2
- package/dist/rastack-init.js +23 -44
- package/dist/rastack-update.js +16 -8
- package/dist/rastack-wasm-build.d.ts +9 -4
- package/dist/rastack-wasm-build.js +44 -33
- package/dist/rastack.d.ts +1 -2
- package/dist/rastack.js +7 -10
- package/dist/update.d.ts +11 -0
- package/dist/update.js +14 -0
- package/dist/wasm/package.json +4 -0
- package/dist/wasm/rastack_wasm.d.ts +100 -0
- package/dist/wasm/rastack_wasm.js +745 -0
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/dist/wasm/rastack_wasm_bg.wasm.d.ts +28 -0
- package/package.json +6 -2
- package/scripts/build-admin.js +48 -0
- package/scripts/copy-wasm.js +35 -0
- package/src/dev/harness.ts +130 -0
- package/src/rastack-dev.ts +197 -0
- package/src/rastack-init.ts +26 -62
- package/src/rastack-update.ts +22 -9
- package/src/rastack-wasm-build.ts +52 -41
- package/src/rastack.ts +7 -10
- package/src/update.ts +14 -0
- package/test/dev.spec.ts +277 -0
- package/test/update.spec.ts +13 -0
- package/wasm/package.json +4 -0
- package/wasm/rastack_wasm.d.ts +100 -0
- package/wasm/rastack_wasm.js +745 -0
- package/wasm/rastack_wasm_bg.wasm +0 -0
- package/wasm/rastack_wasm_bg.wasm.d.ts +28 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hooks that bridge the WASM engine to the reusable `rastack/components`
|
|
3
|
+
* shapes. `useResourceTable` synthesizes a `TableQuery` for `<DataTable>` from
|
|
4
|
+
* `engine.handle('GET', …)`; `useResourceForm` synthesizes an `AutoFormLike` for
|
|
5
|
+
* `<AutoForm>` (loading FK options + the edited row, wiring create/update). No
|
|
6
|
+
* generated per-app hooks — everything is driven by the manifest at runtime, so
|
|
7
|
+
* one admin serves any app.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
11
|
+
import type { RastackEngine } from "../provider/types";
|
|
12
|
+
import type { AutoFormField, AutoFormLike, TableQuery } from "../components/types";
|
|
13
|
+
import type {
|
|
14
|
+
IPaginationActions,
|
|
15
|
+
ISearchingActions,
|
|
16
|
+
} from "../hooks/query/interfaces";
|
|
17
|
+
import type { FieldOption } from "../hooks/form/interfaces";
|
|
18
|
+
import {
|
|
19
|
+
AdminResource,
|
|
20
|
+
PAGE_SIZE,
|
|
21
|
+
buildWriteBody,
|
|
22
|
+
changelistHeaders,
|
|
23
|
+
detailPath,
|
|
24
|
+
errorDetail,
|
|
25
|
+
fieldControlType,
|
|
26
|
+
formFields,
|
|
27
|
+
initialFormValues,
|
|
28
|
+
listPath,
|
|
29
|
+
optionsFromRows,
|
|
30
|
+
pageCount,
|
|
31
|
+
readListEnvelope,
|
|
32
|
+
} from "./adapters";
|
|
33
|
+
|
|
34
|
+
interface EngineResult {
|
|
35
|
+
status: number;
|
|
36
|
+
body: any;
|
|
37
|
+
}
|
|
38
|
+
const ok = (r: EngineResult) => r.status >= 200 && r.status < 300;
|
|
39
|
+
|
|
40
|
+
/** Drive a `<DataTable>` for one resource, backed by the WASM engine. */
|
|
41
|
+
export function useResourceTable(
|
|
42
|
+
engine: RastackEngine,
|
|
43
|
+
resource: AdminResource,
|
|
44
|
+
): { query: TableQuery; reload: () => void } {
|
|
45
|
+
const [page, setPage] = useState(1);
|
|
46
|
+
const [search, setSearch] = useState<string | undefined>(undefined);
|
|
47
|
+
const [data, setData] = useState<Record<string, unknown>[] | undefined>(
|
|
48
|
+
undefined,
|
|
49
|
+
);
|
|
50
|
+
const [count, setCount] = useState(0);
|
|
51
|
+
const [isLoading, setLoading] = useState(true);
|
|
52
|
+
const [error, setError] = useState<unknown>(undefined);
|
|
53
|
+
const [tick, setTick] = useState(0);
|
|
54
|
+
|
|
55
|
+
// Reset paging/search when the resource changes.
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setPage(1);
|
|
58
|
+
setSearch(undefined);
|
|
59
|
+
}, [resource.key]);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
let cancelled = false;
|
|
63
|
+
setLoading(true);
|
|
64
|
+
setError(undefined);
|
|
65
|
+
const query: Record<string, string> = {
|
|
66
|
+
page: String(page),
|
|
67
|
+
page_size: String(PAGE_SIZE),
|
|
68
|
+
};
|
|
69
|
+
if (search) query.search = search;
|
|
70
|
+
engine
|
|
71
|
+
.handle("GET", listPath(resource.app, resource.model), query, undefined)
|
|
72
|
+
.then((res: EngineResult) => {
|
|
73
|
+
if (cancelled) return;
|
|
74
|
+
if (ok(res)) {
|
|
75
|
+
const env = readListEnvelope(res.body);
|
|
76
|
+
setData(env.results);
|
|
77
|
+
setCount(env.count);
|
|
78
|
+
} else {
|
|
79
|
+
setError(errorDetail(res.body));
|
|
80
|
+
setData([]);
|
|
81
|
+
}
|
|
82
|
+
setLoading(false);
|
|
83
|
+
})
|
|
84
|
+
.catch((e: unknown) => {
|
|
85
|
+
if (cancelled) return;
|
|
86
|
+
setError(e);
|
|
87
|
+
setData([]);
|
|
88
|
+
setLoading(false);
|
|
89
|
+
});
|
|
90
|
+
return () => {
|
|
91
|
+
cancelled = true;
|
|
92
|
+
};
|
|
93
|
+
}, [engine, resource.key, resource.app, resource.model, page, search, tick]);
|
|
94
|
+
|
|
95
|
+
const pagination: IPaginationActions = {
|
|
96
|
+
currentPage: page,
|
|
97
|
+
pageSize: PAGE_SIZE,
|
|
98
|
+
itemCount: count,
|
|
99
|
+
pageCount: pageCount(count, PAGE_SIZE),
|
|
100
|
+
nextPage: () =>
|
|
101
|
+
setPage((p) => Math.min(pageCount(count, PAGE_SIZE), p + 1)),
|
|
102
|
+
previousPage: () => setPage((p) => Math.max(1, p - 1)),
|
|
103
|
+
setPageSize: () => {
|
|
104
|
+
/* fixed page size, like the Rust admin */
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const searching: ISearchingActions<Record<string, unknown>> = {
|
|
108
|
+
searchText: search,
|
|
109
|
+
setSearchText: (v) => {
|
|
110
|
+
setSearch(v && v.length ? v : undefined);
|
|
111
|
+
setPage(1);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const query: TableQuery = {
|
|
116
|
+
data,
|
|
117
|
+
isLoading,
|
|
118
|
+
isError: Boolean(error),
|
|
119
|
+
error,
|
|
120
|
+
header: changelistHeaders(resource),
|
|
121
|
+
pagination,
|
|
122
|
+
// No `sorting`: the store orders by id only, so sortable headers would lie.
|
|
123
|
+
searching: resource.search.length ? searching : undefined,
|
|
124
|
+
};
|
|
125
|
+
return { query, reload: () => setTick((t) => t + 1) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** State a form screen needs beyond the `AutoFormLike` itself. */
|
|
129
|
+
export interface FormState {
|
|
130
|
+
model: AutoFormLike;
|
|
131
|
+
loading: boolean;
|
|
132
|
+
loadError?: string;
|
|
133
|
+
submitError?: string;
|
|
134
|
+
done: boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Drive an `<AutoForm>` for create (`id` undefined) or edit. */
|
|
138
|
+
export function useResourceForm(
|
|
139
|
+
engine: RastackEngine,
|
|
140
|
+
resource: AdminResource,
|
|
141
|
+
id?: string,
|
|
142
|
+
): FormState {
|
|
143
|
+
const fields = useMemo(() => formFields(resource), [resource.key]);
|
|
144
|
+
const [values, setValues] = useState<Record<string, unknown>>(() =>
|
|
145
|
+
initialFormValues(resource),
|
|
146
|
+
);
|
|
147
|
+
const [errors] = useState<Record<string, string>>({});
|
|
148
|
+
const [fkOptions, setFkOptions] = useState<Record<string, FieldOption[]>>({});
|
|
149
|
+
const [submitting, setSubmitting] = useState(false);
|
|
150
|
+
const [loading, setLoading] = useState(Boolean(id));
|
|
151
|
+
const [loadError, setLoadError] = useState<string | undefined>();
|
|
152
|
+
const [submitError, setSubmitError] = useState<string | undefined>();
|
|
153
|
+
const [done, setDone] = useState(false);
|
|
154
|
+
|
|
155
|
+
// Reset when switching between add/edit or resources.
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
setValues(initialFormValues(resource));
|
|
158
|
+
setDone(false);
|
|
159
|
+
setSubmitError(undefined);
|
|
160
|
+
setLoadError(undefined);
|
|
161
|
+
setLoading(Boolean(id));
|
|
162
|
+
}, [resource.key, id]);
|
|
163
|
+
|
|
164
|
+
// Load FK target options once per resource.
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
let cancelled = false;
|
|
167
|
+
const fks = fields.filter((f) => f.type === "fk" && f.relation);
|
|
168
|
+
Promise.all(
|
|
169
|
+
fks.map(async (f) => {
|
|
170
|
+
const res: EngineResult = await engine.handle(
|
|
171
|
+
"GET",
|
|
172
|
+
listPath(f.relation!.app, f.relation!.model),
|
|
173
|
+
{ page_size: "1000" },
|
|
174
|
+
undefined,
|
|
175
|
+
);
|
|
176
|
+
return [f.name, optionsFromRows(readListEnvelope(res.body).results)] as const;
|
|
177
|
+
}),
|
|
178
|
+
).then((entries) => {
|
|
179
|
+
if (!cancelled) setFkOptions(Object.fromEntries(entries));
|
|
180
|
+
});
|
|
181
|
+
return () => {
|
|
182
|
+
cancelled = true;
|
|
183
|
+
};
|
|
184
|
+
}, [engine, resource.key]);
|
|
185
|
+
|
|
186
|
+
// Load the existing row when editing.
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!id) return;
|
|
189
|
+
let cancelled = false;
|
|
190
|
+
engine
|
|
191
|
+
.handle("GET", detailPath(resource.app, resource.model, id), undefined, undefined)
|
|
192
|
+
.then((res: EngineResult) => {
|
|
193
|
+
if (cancelled) return;
|
|
194
|
+
if (ok(res) && res.body && typeof res.body === "object") {
|
|
195
|
+
const row = res.body as Record<string, unknown>;
|
|
196
|
+
const next: Record<string, unknown> = {};
|
|
197
|
+
for (const f of fields) {
|
|
198
|
+
next[f.name] =
|
|
199
|
+
row[f.name] ?? (f.type === "bool" ? false : "");
|
|
200
|
+
}
|
|
201
|
+
setValues(next);
|
|
202
|
+
} else {
|
|
203
|
+
setLoadError(errorDetail(res.body));
|
|
204
|
+
}
|
|
205
|
+
setLoading(false);
|
|
206
|
+
})
|
|
207
|
+
.catch((e: unknown) => {
|
|
208
|
+
if (!cancelled) {
|
|
209
|
+
setLoadError(String(e));
|
|
210
|
+
setLoading(false);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
return () => {
|
|
214
|
+
cancelled = true;
|
|
215
|
+
};
|
|
216
|
+
}, [engine, resource.key, id]);
|
|
217
|
+
|
|
218
|
+
const setValue = useCallback(
|
|
219
|
+
(name: string) => (v: unknown) =>
|
|
220
|
+
setValues((prev) => ({ ...prev, [name]: v })),
|
|
221
|
+
[],
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const getFormField = useCallback(
|
|
225
|
+
(name: string): AutoFormField => {
|
|
226
|
+
const field = fields.find((f) => f.name === name)!;
|
|
227
|
+
const value = values[name];
|
|
228
|
+
return {
|
|
229
|
+
value,
|
|
230
|
+
setValue: setValue(name),
|
|
231
|
+
error: errors[name] ?? "",
|
|
232
|
+
type: fieldControlType(field),
|
|
233
|
+
isChecked: value === true,
|
|
234
|
+
isError: Boolean(errors[name]),
|
|
235
|
+
isDisabled: submitting,
|
|
236
|
+
isRequired: !field.nullable,
|
|
237
|
+
setTouched: () => {
|
|
238
|
+
/* touched tracked by AutoForm itself */
|
|
239
|
+
},
|
|
240
|
+
maxLength: field.maxLength,
|
|
241
|
+
options: field.type === "fk" ? fkOptions[field.name] ?? [] : undefined,
|
|
242
|
+
relation: field.relation,
|
|
243
|
+
nullable: field.nullable,
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
[fields, values, errors, submitting, fkOptions, setValue],
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const onSubmit = useCallback(() => {
|
|
250
|
+
setSubmitting(true);
|
|
251
|
+
setSubmitError(undefined);
|
|
252
|
+
const body = buildWriteBody(resource, values);
|
|
253
|
+
const method = id ? "PUT" : "POST";
|
|
254
|
+
const path = id
|
|
255
|
+
? detailPath(resource.app, resource.model, id)
|
|
256
|
+
: listPath(resource.app, resource.model);
|
|
257
|
+
engine
|
|
258
|
+
.handle(method, path, undefined, body)
|
|
259
|
+
.then((res: EngineResult) => {
|
|
260
|
+
setSubmitting(false);
|
|
261
|
+
if (ok(res)) setDone(true);
|
|
262
|
+
else setSubmitError(errorDetail(res.body));
|
|
263
|
+
})
|
|
264
|
+
.catch((e: unknown) => {
|
|
265
|
+
setSubmitting(false);
|
|
266
|
+
setSubmitError(String(e));
|
|
267
|
+
});
|
|
268
|
+
}, [engine, resource, values, id]);
|
|
269
|
+
|
|
270
|
+
const model: AutoFormLike = {
|
|
271
|
+
formValues: values,
|
|
272
|
+
getFormField,
|
|
273
|
+
onSubmit,
|
|
274
|
+
resetForm: () => setValues(initialFormValues(resource)),
|
|
275
|
+
isSubmitDisabled: submitting,
|
|
276
|
+
isSubmitting: submitting,
|
|
277
|
+
errors,
|
|
278
|
+
};
|
|
279
|
+
return { model, loading, loadError, submitError, done };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Delete a record via the engine; resolves true on success. */
|
|
283
|
+
export function useDeleteRecord(
|
|
284
|
+
engine: RastackEngine,
|
|
285
|
+
resource: AdminResource,
|
|
286
|
+
): (id: string | number) => Promise<boolean> {
|
|
287
|
+
return useCallback(
|
|
288
|
+
async (id) => {
|
|
289
|
+
const res: EngineResult = await engine.handle(
|
|
290
|
+
"DELETE",
|
|
291
|
+
detailPath(resource.app, resource.model, id),
|
|
292
|
+
undefined,
|
|
293
|
+
undefined,
|
|
294
|
+
);
|
|
295
|
+
return ok(res);
|
|
296
|
+
},
|
|
297
|
+
[engine, resource.key],
|
|
298
|
+
);
|
|
299
|
+
}
|