@kahitsan/ksui 0.16.0 → 0.17.1
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/package.json +1 -1
- package/src/components/composite/resource/ResourceDetail.tsx +3 -4
- package/src/components/composite/resource/ResourceForm.tsx +3 -4
- package/src/components/composite/resource/ResourcePage.tsx +116 -65
- package/src/components/composite/resource/cells.tsx +2 -3
- package/src/components/composite/resource/spec.ts +12 -24
- package/src/index.ts +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kahitsan/ksui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "ksui is a standalone set of SolidJS UI components for KahitSan/Hilinga and any SolidJS app. Published to the public npm registry and consumed as a normal dependency. Ships source under a `solid` export condition so the consumer's vite-plugin-solid compiles it with only solid-js externalized; it depends on nothing but solid-js + lucide-solid and injects its own CSS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// reproducing hand-written payees' PayeeDetail.
|
|
1
|
+
// The read-only detail view (the non-editing face of the detail modal). Renders
|
|
2
|
+
// one DetailRow per declared detail row, deriving each value by its kind (raw
|
|
3
|
+
// field / enum label / status / formatted datetime).
|
|
5
4
|
import { For } from "solid-js";
|
|
6
5
|
import DetailRow from "../../base/DetailRow";
|
|
7
6
|
import type { ResourceRow, UiDetailRow, UiDetailValue } from "./spec";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
// and
|
|
4
|
-
// passed in, so a single instance backs both the create and edit modals.
|
|
1
|
+
// The create/edit form: one control per declared field (text / textarea /
|
|
2
|
+
// select) on the FormField + Button shell. Form state is owned by ResourcePage
|
|
3
|
+
// and passed in, so one instance backs both the create and edit modals.
|
|
5
4
|
import { For, Show } from "solid-js";
|
|
6
5
|
import FormField from "../../base/FormField";
|
|
7
6
|
import Button from "../../base/Button";
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// A config-driven CRUD page: a list (DataTable) with search, filters and paging,
|
|
2
|
+
// plus create / view / edit / archive / restore modals — all described by one
|
|
3
|
+
// declarative `ResourceUiSpec` (columns, fields, filters, labels, REST endpoints).
|
|
4
|
+
// It talks to a REST resource exposing list / create / get / update / delete /
|
|
5
|
+
// restore over `basePath`.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// Everything application-specific is injected via the `host` prop — the page-shell
|
|
8
|
+
// layout, a permission check, per-request init (auth headers / credentials), a
|
|
9
|
+
// refetch trigger, and any extra header actions — so the component carries no
|
|
10
|
+
// app, transport, or auth assumptions of its own.
|
|
11
11
|
import { createSignal, For, Show, type Component, type JSX } from "solid-js";
|
|
12
12
|
import { createStore } from "solid-js/store";
|
|
13
13
|
import Plus from "lucide-solid/icons/plus";
|
|
@@ -40,26 +40,35 @@ import { ResourceForm } from "./ResourceForm";
|
|
|
40
40
|
import { ResourceDetail } from "./ResourceDetail";
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* tree, so their reactive context resolves correctly.
|
|
43
|
+
* Application-specific dependencies injected by the consumer. The component holds
|
|
44
|
+
* no transport, auth, tenancy or layout assumptions of its own — they all arrive
|
|
45
|
+
* here. Only `PageShell` is required; the rest default to permissive no-ops.
|
|
47
46
|
*/
|
|
48
47
|
export interface ResourcePageHost {
|
|
48
|
+
/** Page-shell layout: a heading area + an actions slot wrapping the body. */
|
|
49
49
|
PageShell: Component<{
|
|
50
50
|
title: string;
|
|
51
51
|
subtitle?: string;
|
|
52
52
|
actions?: JSX.Element;
|
|
53
53
|
children: JSX.Element;
|
|
54
54
|
}>;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
/** Permission check against the spec's permission keys. Defaults to allow-all. */
|
|
56
|
+
can?: (permission: string) => boolean;
|
|
57
|
+
/**
|
|
58
|
+
* `RequestInit` merged into every request the page makes — the seam for auth
|
|
59
|
+
* headers, credentials, or any per-tenant header. Called per request so it can
|
|
60
|
+
* read live context. The component adds only `method` and (on writes) the JSON
|
|
61
|
+
* `Content-Type` + body on top of what this returns.
|
|
62
|
+
*/
|
|
63
|
+
requestInit?: () => RequestInit;
|
|
64
|
+
/** Reactive value; when it changes the list resets to page 1 and refetches. */
|
|
65
|
+
refetchKey?: () => unknown;
|
|
66
|
+
/**
|
|
67
|
+
* Render extra header actions before the built-in create button. A function
|
|
68
|
+
* (not a pre-created element) so it runs inside this component's render scope —
|
|
69
|
+
* a pre-created element would re-run outside it if the host object is rebuilt.
|
|
70
|
+
*/
|
|
71
|
+
headerActions?: () => JSX.Element;
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
export interface ResourcePageProps<T extends ResourceRow> {
|
|
@@ -71,26 +80,54 @@ export interface ResourcePageProps<T extends ResourceRow> {
|
|
|
71
80
|
|
|
72
81
|
type RefetchApi = { refetch: () => void; resetAndRefetch: () => void };
|
|
73
82
|
|
|
74
|
-
export function ResourcePage<T extends ResourceRow>(
|
|
83
|
+
export function ResourcePage<T extends ResourceRow>(
|
|
84
|
+
props: ResourcePageProps<T>
|
|
85
|
+
) {
|
|
75
86
|
const spec = props.spec;
|
|
76
87
|
const ep = endpoints(spec);
|
|
77
|
-
const doFetch =
|
|
88
|
+
const doFetch =
|
|
89
|
+
props.fetchImpl ?? ((...a: Parameters<typeof fetch>) => fetch(...a));
|
|
78
90
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
// Read the host config ONCE so the component is robust against a consumer
|
|
92
|
+
// passing an inline `host={{...}}` literal (which Solid re-evaluates on each
|
|
93
|
+
// `props.host.*` access — re-instantiating headerActions outside render scope).
|
|
94
|
+
const {
|
|
95
|
+
PageShell,
|
|
96
|
+
can: hostCan,
|
|
97
|
+
requestInit: hostRequestInit,
|
|
98
|
+
refetchKey,
|
|
99
|
+
headerActions,
|
|
100
|
+
} = props.host;
|
|
101
|
+
const can = (key: string) => hostCan?.(key) ?? true;
|
|
102
|
+
const canView = () => can(spec.permissions.view);
|
|
103
|
+
const canEdit = () => spec.permissions.edit.some(can);
|
|
104
|
+
const canDelete = () => can(spec.permissions.delete);
|
|
85
105
|
|
|
86
|
-
|
|
106
|
+
/** Merge the host's per-request init (headers/credentials) with method + body. */
|
|
107
|
+
function reqInit(extra?: RequestInit): RequestInit {
|
|
108
|
+
const base = hostRequestInit?.() ?? {};
|
|
109
|
+
return {
|
|
110
|
+
...base,
|
|
111
|
+
...extra,
|
|
112
|
+
headers: {
|
|
113
|
+
...(base.headers as Record<string, string> | undefined),
|
|
114
|
+
...(extra?.headers as Record<string, string> | undefined),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const [filterState, setFilterState] = createStore<Record<string, string>>(
|
|
120
|
+
initialFilterState(spec)
|
|
121
|
+
);
|
|
87
122
|
let refetchFn: RefetchApi | undefined;
|
|
88
123
|
|
|
89
124
|
const [detailRow, setDetailRow] = createSignal<ResourceRow | null>(null);
|
|
90
125
|
const [editing, setEditing] = createSignal(false);
|
|
91
126
|
const [createOpen, setCreateOpen] = createSignal(false);
|
|
92
127
|
|
|
93
|
-
const [form, setForm] = createStore<Record<string, string>>(
|
|
128
|
+
const [form, setForm] = createStore<Record<string, string>>(
|
|
129
|
+
emptyFormValues(spec)
|
|
130
|
+
);
|
|
94
131
|
const [saving, setSaving] = createSignal(false);
|
|
95
132
|
const [error, setError] = createSignal("");
|
|
96
133
|
const setValue = (key: string, value: string) => setForm(key, value);
|
|
@@ -104,14 +141,9 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
104
141
|
setError("");
|
|
105
142
|
}
|
|
106
143
|
|
|
107
|
-
function wsHeaders(): Record<string, string> {
|
|
108
|
-
const ws = activeWorkspace();
|
|
109
|
-
return ws ? { "X-Workspace-Id": String(ws.ws_id) } : {};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
144
|
async function openDetail(id: number) {
|
|
113
145
|
try {
|
|
114
|
-
const res = await doFetch(ep.one(id),
|
|
146
|
+
const res = await doFetch(ep.one(id), reqInit());
|
|
115
147
|
if (res.ok) {
|
|
116
148
|
setDetailRow(await res.json());
|
|
117
149
|
setEditing(false);
|
|
@@ -128,7 +160,12 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
128
160
|
setEditing(true);
|
|
129
161
|
}
|
|
130
162
|
|
|
131
|
-
async function submit(
|
|
163
|
+
async function submit(
|
|
164
|
+
method: "POST" | "PUT",
|
|
165
|
+
url: string,
|
|
166
|
+
fallback: string,
|
|
167
|
+
onOk: (row: ResourceRow) => void
|
|
168
|
+
) {
|
|
132
169
|
const msg = validateForm(spec, form);
|
|
133
170
|
if (msg) {
|
|
134
171
|
setError(msg);
|
|
@@ -137,12 +174,14 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
137
174
|
setSaving(true);
|
|
138
175
|
setError("");
|
|
139
176
|
try {
|
|
140
|
-
const res = await doFetch(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
177
|
+
const res = await doFetch(
|
|
178
|
+
url,
|
|
179
|
+
reqInit({
|
|
180
|
+
method,
|
|
181
|
+
headers: { "Content-Type": "application/json" },
|
|
182
|
+
body: JSON.stringify(formToBody(spec, form)),
|
|
183
|
+
})
|
|
184
|
+
);
|
|
146
185
|
// create allows the idempotent-200 path; both treat non-ok as an error
|
|
147
186
|
if (!res.ok && !(method === "POST" && res.status === 200)) {
|
|
148
187
|
const err = await res.json().catch(() => ({}));
|
|
@@ -168,10 +207,15 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
168
207
|
async function handleUpdate() {
|
|
169
208
|
const row = detailRow();
|
|
170
209
|
if (!row) return;
|
|
171
|
-
await submit(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
210
|
+
await submit(
|
|
211
|
+
"PUT",
|
|
212
|
+
ep.one(row.id),
|
|
213
|
+
spec.labels.updateErrorFallback,
|
|
214
|
+
(updated) => {
|
|
215
|
+
if (updated && typeof updated.id === "number") setDetailRow(updated);
|
|
216
|
+
setEditing(false);
|
|
217
|
+
}
|
|
218
|
+
);
|
|
175
219
|
}
|
|
176
220
|
|
|
177
221
|
async function handleArchive(id: number) {
|
|
@@ -185,7 +229,7 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
185
229
|
)
|
|
186
230
|
return;
|
|
187
231
|
try {
|
|
188
|
-
await doFetch(ep.one(id), { method: "DELETE"
|
|
232
|
+
await doFetch(ep.one(id), reqInit({ method: "DELETE" }));
|
|
189
233
|
setDetailRow(null);
|
|
190
234
|
refetchFn?.refetch();
|
|
191
235
|
} catch {
|
|
@@ -195,7 +239,7 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
195
239
|
|
|
196
240
|
async function handleRestore(id: number) {
|
|
197
241
|
try {
|
|
198
|
-
const res = await doFetch(ep.restore(id), { method: "PATCH"
|
|
242
|
+
const res = await doFetch(ep.restore(id), reqInit({ method: "PATCH" }));
|
|
199
243
|
if (res.ok) {
|
|
200
244
|
setDetailRow(await res.json());
|
|
201
245
|
refetchFn?.refetch();
|
|
@@ -219,13 +263,7 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
219
263
|
subtitle={spec.subtitle}
|
|
220
264
|
actions={
|
|
221
265
|
<>
|
|
222
|
-
|
|
223
|
-
{(s) =>
|
|
224
|
-
PageShareButton ? (
|
|
225
|
-
<PageShareButton module={s().module} moduleLabel={s().moduleLabel} />
|
|
226
|
-
) : null
|
|
227
|
-
}
|
|
228
|
-
</Show>
|
|
266
|
+
{headerActions?.()}
|
|
229
267
|
<Show when={canEdit()}>
|
|
230
268
|
<Button
|
|
231
269
|
intent="primary"
|
|
@@ -244,10 +282,12 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
244
282
|
}
|
|
245
283
|
>
|
|
246
284
|
<DataTable<ResourceRow>
|
|
247
|
-
refetchKey={
|
|
248
|
-
fetchFn={async (
|
|
285
|
+
refetchKey={refetchKey}
|
|
286
|
+
fetchFn={async (
|
|
287
|
+
params: FetchParams
|
|
288
|
+
): Promise<FetchResult<ResourceRow>> => {
|
|
249
289
|
const q = buildListQuery(spec, params, filterState);
|
|
250
|
-
const res = await doFetch(`${ep.list}?${q}`,
|
|
290
|
+
const res = await doFetch(`${ep.list}?${q}`, reqInit());
|
|
251
291
|
return res.json();
|
|
252
292
|
}}
|
|
253
293
|
columns={columns}
|
|
@@ -271,10 +311,14 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
271
311
|
) : (
|
|
272
312
|
<select
|
|
273
313
|
value={filterState[f.param]}
|
|
274
|
-
onChange={(e) =>
|
|
314
|
+
onChange={(e) =>
|
|
315
|
+
setFilterState(f.param, e.currentTarget.value)
|
|
316
|
+
}
|
|
275
317
|
class="rounded-lg border border-zinc-800/50 bg-zinc-900 px-3 py-1.5 text-xs text-zinc-400 cursor-pointer"
|
|
276
318
|
>
|
|
277
|
-
<For each={f.options}>
|
|
319
|
+
<For each={f.options}>
|
|
320
|
+
{(o) => <option value={o.value}>{o.label}</option>}
|
|
321
|
+
</For>
|
|
278
322
|
</select>
|
|
279
323
|
)
|
|
280
324
|
}
|
|
@@ -298,7 +342,9 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
298
342
|
>
|
|
299
343
|
<div data-testid={`${spec.testIdPrefix}-create-modal`}>
|
|
300
344
|
<div class="flex items-center justify-between mb-6">
|
|
301
|
-
<h2 class="text-lg font-semibold text-zinc-100">
|
|
345
|
+
<h2 class="text-lg font-semibold text-zinc-100">
|
|
346
|
+
{spec.labels.createTitle}
|
|
347
|
+
</h2>
|
|
302
348
|
<button
|
|
303
349
|
onClick={() => {
|
|
304
350
|
setCreateOpen(false);
|
|
@@ -340,7 +386,9 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
340
386
|
<div data-testid={`${spec.testIdPrefix}-detail-modal`}>
|
|
341
387
|
<div class="flex items-center justify-between mb-6">
|
|
342
388
|
<h2 class="text-lg font-semibold text-zinc-100">
|
|
343
|
-
{editing()
|
|
389
|
+
{editing()
|
|
390
|
+
? spec.labels.editTitle
|
|
391
|
+
: String(row()[spec.labels.titleField] ?? "")}
|
|
344
392
|
</h2>
|
|
345
393
|
<div class="flex items-center gap-2">
|
|
346
394
|
<Show when={!editing() && canEdit()}>
|
|
@@ -387,7 +435,10 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
387
435
|
</div>
|
|
388
436
|
</div>
|
|
389
437
|
|
|
390
|
-
<Show
|
|
438
|
+
<Show
|
|
439
|
+
when={editing()}
|
|
440
|
+
fallback={<ResourceDetail rows={spec.detail} row={row()} />}
|
|
441
|
+
>
|
|
391
442
|
<ResourceForm
|
|
392
443
|
spec={spec}
|
|
393
444
|
values={form}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
// Column-cell rendering for the
|
|
2
|
-
//
|
|
3
|
-
// the generated table is byte-for-behavior identical.
|
|
1
|
+
// Column-cell rendering for the datatable. One pure function maps a column's
|
|
2
|
+
// declared render hint (title / enum / status / text) to its cell markup.
|
|
4
3
|
import type { JSX } from "solid-js";
|
|
5
4
|
import StatusPill from "../../base/StatusPill";
|
|
6
5
|
import type { ResourceRow, ResourceUiSpec, UiColumn } from "./spec";
|
|
@@ -1,18 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// server-side `defineResource(spec)` runtime (kernel-base/resource/*): the SAME
|
|
7
|
-
// field/column declarations that drive the table + migration + CRUD routes also
|
|
8
|
-
// drive the page. Authored once as a `ResourceUiSpec`, rendered by `ResourcePage`
|
|
9
|
-
// (ResourcePage.tsx) into the exact ksui DataTable + Modal + FormField shell a
|
|
10
|
-
// hand-written base plugin ships.
|
|
11
|
-
//
|
|
12
|
-
// Built inside kplugin_payees as the make-or-break proof (byte-for-behavior ==
|
|
13
|
-
// hand-written payees). It imports only ksui + @kserp/host-ui + solid-js, so it
|
|
14
|
-
// lifts into the shared SDK / UI-kit later (that lift needs the plugin UI build
|
|
15
|
-
// to resolve the SDK — a vite alias + tsconfig.ui paths entry — out of scope here).
|
|
1
|
+
// The declarative contract for `ResourcePage` plus its PURE helpers (no solid-js
|
|
2
|
+
// or component imports, so they unit-test under plain node). A `ResourceUiSpec`
|
|
3
|
+
// describes a CRUD page's columns, form fields, filters, labels and REST endpoints
|
|
4
|
+
// once; `ResourcePage` renders it. The shape is transport- and framework-agnostic
|
|
5
|
+
// — it names columns and fields, never how requests are authed or sent.
|
|
16
6
|
|
|
17
7
|
/** A row the runtime can render: any record with a numeric surrogate id. */
|
|
18
8
|
export interface ResourceRow {
|
|
@@ -129,14 +119,12 @@ export interface UiDetailRow {
|
|
|
129
119
|
// ---- the spec --------------------------------------------------------------
|
|
130
120
|
|
|
131
121
|
export interface ResourceUiSpec {
|
|
132
|
-
/** REST base, e.g. "/api/
|
|
122
|
+
/** REST base, e.g. "/api/things"; the CRUD endpoints derive from it. */
|
|
133
123
|
readonly basePath: string;
|
|
134
124
|
/** Page title + framing. */
|
|
135
125
|
readonly title: string;
|
|
136
126
|
readonly subtitle?: string;
|
|
137
|
-
/**
|
|
138
|
-
readonly share?: { readonly module: string; readonly moduleLabel: string };
|
|
139
|
-
/** Capability codes. `edit` is satisfied by hasAny(...edit). */
|
|
127
|
+
/** Permission keys passed to `host.can`. `edit` passes if ANY of its keys do. */
|
|
140
128
|
readonly permissions: {
|
|
141
129
|
readonly view: string;
|
|
142
130
|
readonly edit: readonly string[];
|
|
@@ -166,7 +154,7 @@ export interface ResourceUiSpec {
|
|
|
166
154
|
readonly archiveMessage: string;
|
|
167
155
|
readonly archiveConfirm: string;
|
|
168
156
|
};
|
|
169
|
-
/** data-testid prefix (e.g. "
|
|
157
|
+
/** data-testid prefix (e.g. "things" → things-add-btn, things-row-3). */
|
|
170
158
|
readonly testIdPrefix: string;
|
|
171
159
|
}
|
|
172
160
|
|
|
@@ -201,9 +189,9 @@ export interface ListParams {
|
|
|
201
189
|
}
|
|
202
190
|
|
|
203
191
|
/**
|
|
204
|
-
* Build the list-request query string
|
|
205
|
-
*
|
|
206
|
-
*
|
|
192
|
+
* Build the list-request query string: page/limit/search/sortBy/sortDir are
|
|
193
|
+
* always present; a segmented filter is always sent; a select filter is sent
|
|
194
|
+
* only when its value is non-empty.
|
|
207
195
|
*/
|
|
208
196
|
export function buildListQuery(
|
|
209
197
|
spec: ResourceUiSpec,
|
|
@@ -269,7 +257,7 @@ export function cleanLabel(label: string): string {
|
|
|
269
257
|
|
|
270
258
|
/**
|
|
271
259
|
* Validate the form. Returns the first required-but-empty field's error message,
|
|
272
|
-
* or null when valid.
|
|
260
|
+
* or null when valid (e.g. "Name is required").
|
|
273
261
|
*/
|
|
274
262
|
export function validateForm(
|
|
275
263
|
spec: ResourceUiSpec,
|
package/src/index.ts
CHANGED
|
@@ -147,12 +147,12 @@ export type { VoucherOption } from "./components/composite/VoucherPicker";
|
|
|
147
147
|
|
|
148
148
|
export { default as NotFound, type NotFoundProps } from "./components/composite/NotFound";
|
|
149
149
|
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
// (DataTable + Modal + FormField
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
150
|
+
// Config-driven CRUD page: a list/create/view/edit/archive page over a REST
|
|
151
|
+
// resource, described by one declarative ResourceUiSpec and rendered through
|
|
152
|
+
// ResourcePage (DataTable + Modal + FormField). Everything app-specific — the
|
|
153
|
+
// page-shell layout, a permission check, per-request init, a refetch trigger and
|
|
154
|
+
// any extra header actions — is injected via the `host` prop, so the component
|
|
155
|
+
// carries no app, transport or auth assumptions of its own.
|
|
156
156
|
export { ResourcePage } from "./components/composite/resource/ResourcePage";
|
|
157
157
|
export type {
|
|
158
158
|
ResourcePageProps,
|