@kahitsan/ksui 0.17.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kahitsan/ksui",
|
|
3
|
-
"version": "0.17.
|
|
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",
|
|
@@ -63,8 +63,12 @@ export interface ResourcePageHost {
|
|
|
63
63
|
requestInit?: () => RequestInit;
|
|
64
64
|
/** Reactive value; when it changes the list resets to page 1 and refetches. */
|
|
65
65
|
refetchKey?: () => unknown;
|
|
66
|
-
/**
|
|
67
|
-
|
|
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;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
export interface ResourcePageProps<T extends ResourceRow> {
|
|
@@ -76,20 +80,32 @@ export interface ResourcePageProps<T extends ResourceRow> {
|
|
|
76
80
|
|
|
77
81
|
type RefetchApi = { refetch: () => void; resetAndRefetch: () => void };
|
|
78
82
|
|
|
79
|
-
export function ResourcePage<T extends ResourceRow>(
|
|
83
|
+
export function ResourcePage<T extends ResourceRow>(
|
|
84
|
+
props: ResourcePageProps<T>
|
|
85
|
+
) {
|
|
80
86
|
const spec = props.spec;
|
|
81
87
|
const ep = endpoints(spec);
|
|
82
|
-
const doFetch =
|
|
88
|
+
const doFetch =
|
|
89
|
+
props.fetchImpl ?? ((...a: Parameters<typeof fetch>) => fetch(...a));
|
|
83
90
|
|
|
84
|
-
|
|
85
|
-
|
|
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;
|
|
86
102
|
const canView = () => can(spec.permissions.view);
|
|
87
103
|
const canEdit = () => spec.permissions.edit.some(can);
|
|
88
104
|
const canDelete = () => can(spec.permissions.delete);
|
|
89
105
|
|
|
90
106
|
/** Merge the host's per-request init (headers/credentials) with method + body. */
|
|
91
107
|
function reqInit(extra?: RequestInit): RequestInit {
|
|
92
|
-
const base =
|
|
108
|
+
const base = hostRequestInit?.() ?? {};
|
|
93
109
|
return {
|
|
94
110
|
...base,
|
|
95
111
|
...extra,
|
|
@@ -100,14 +116,18 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
100
116
|
};
|
|
101
117
|
}
|
|
102
118
|
|
|
103
|
-
const [filterState, setFilterState] = createStore<Record<string, string>>(
|
|
119
|
+
const [filterState, setFilterState] = createStore<Record<string, string>>(
|
|
120
|
+
initialFilterState(spec)
|
|
121
|
+
);
|
|
104
122
|
let refetchFn: RefetchApi | undefined;
|
|
105
123
|
|
|
106
124
|
const [detailRow, setDetailRow] = createSignal<ResourceRow | null>(null);
|
|
107
125
|
const [editing, setEditing] = createSignal(false);
|
|
108
126
|
const [createOpen, setCreateOpen] = createSignal(false);
|
|
109
127
|
|
|
110
|
-
const [form, setForm] = createStore<Record<string, string>>(
|
|
128
|
+
const [form, setForm] = createStore<Record<string, string>>(
|
|
129
|
+
emptyFormValues(spec)
|
|
130
|
+
);
|
|
111
131
|
const [saving, setSaving] = createSignal(false);
|
|
112
132
|
const [error, setError] = createSignal("");
|
|
113
133
|
const setValue = (key: string, value: string) => setForm(key, value);
|
|
@@ -140,7 +160,12 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
140
160
|
setEditing(true);
|
|
141
161
|
}
|
|
142
162
|
|
|
143
|
-
async function submit(
|
|
163
|
+
async function submit(
|
|
164
|
+
method: "POST" | "PUT",
|
|
165
|
+
url: string,
|
|
166
|
+
fallback: string,
|
|
167
|
+
onOk: (row: ResourceRow) => void
|
|
168
|
+
) {
|
|
144
169
|
const msg = validateForm(spec, form);
|
|
145
170
|
if (msg) {
|
|
146
171
|
setError(msg);
|
|
@@ -155,7 +180,7 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
155
180
|
method,
|
|
156
181
|
headers: { "Content-Type": "application/json" },
|
|
157
182
|
body: JSON.stringify(formToBody(spec, form)),
|
|
158
|
-
})
|
|
183
|
+
})
|
|
159
184
|
);
|
|
160
185
|
// create allows the idempotent-200 path; both treat non-ok as an error
|
|
161
186
|
if (!res.ok && !(method === "POST" && res.status === 200)) {
|
|
@@ -182,10 +207,15 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
182
207
|
async function handleUpdate() {
|
|
183
208
|
const row = detailRow();
|
|
184
209
|
if (!row) return;
|
|
185
|
-
await submit(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
);
|
|
189
219
|
}
|
|
190
220
|
|
|
191
221
|
async function handleArchive(id: number) {
|
|
@@ -233,7 +263,7 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
233
263
|
subtitle={spec.subtitle}
|
|
234
264
|
actions={
|
|
235
265
|
<>
|
|
236
|
-
{
|
|
266
|
+
{headerActions?.()}
|
|
237
267
|
<Show when={canEdit()}>
|
|
238
268
|
<Button
|
|
239
269
|
intent="primary"
|
|
@@ -252,8 +282,10 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
252
282
|
}
|
|
253
283
|
>
|
|
254
284
|
<DataTable<ResourceRow>
|
|
255
|
-
refetchKey={
|
|
256
|
-
fetchFn={async (
|
|
285
|
+
refetchKey={refetchKey}
|
|
286
|
+
fetchFn={async (
|
|
287
|
+
params: FetchParams
|
|
288
|
+
): Promise<FetchResult<ResourceRow>> => {
|
|
257
289
|
const q = buildListQuery(spec, params, filterState);
|
|
258
290
|
const res = await doFetch(`${ep.list}?${q}`, reqInit());
|
|
259
291
|
return res.json();
|
|
@@ -279,10 +311,14 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
279
311
|
) : (
|
|
280
312
|
<select
|
|
281
313
|
value={filterState[f.param]}
|
|
282
|
-
onChange={(e) =>
|
|
314
|
+
onChange={(e) =>
|
|
315
|
+
setFilterState(f.param, e.currentTarget.value)
|
|
316
|
+
}
|
|
283
317
|
class="rounded-lg border border-zinc-800/50 bg-zinc-900 px-3 py-1.5 text-xs text-zinc-400 cursor-pointer"
|
|
284
318
|
>
|
|
285
|
-
<For each={f.options}>
|
|
319
|
+
<For each={f.options}>
|
|
320
|
+
{(o) => <option value={o.value}>{o.label}</option>}
|
|
321
|
+
</For>
|
|
286
322
|
</select>
|
|
287
323
|
)
|
|
288
324
|
}
|
|
@@ -306,7 +342,9 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
306
342
|
>
|
|
307
343
|
<div data-testid={`${spec.testIdPrefix}-create-modal`}>
|
|
308
344
|
<div class="flex items-center justify-between mb-6">
|
|
309
|
-
<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>
|
|
310
348
|
<button
|
|
311
349
|
onClick={() => {
|
|
312
350
|
setCreateOpen(false);
|
|
@@ -348,7 +386,9 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
348
386
|
<div data-testid={`${spec.testIdPrefix}-detail-modal`}>
|
|
349
387
|
<div class="flex items-center justify-between mb-6">
|
|
350
388
|
<h2 class="text-lg font-semibold text-zinc-100">
|
|
351
|
-
{editing()
|
|
389
|
+
{editing()
|
|
390
|
+
? spec.labels.editTitle
|
|
391
|
+
: String(row()[spec.labels.titleField] ?? "")}
|
|
352
392
|
</h2>
|
|
353
393
|
<div class="flex items-center gap-2">
|
|
354
394
|
<Show when={!editing() && canEdit()}>
|
|
@@ -395,7 +435,10 @@ export function ResourcePage<T extends ResourceRow>(props: ResourcePageProps<T>)
|
|
|
395
435
|
</div>
|
|
396
436
|
</div>
|
|
397
437
|
|
|
398
|
-
<Show
|
|
438
|
+
<Show
|
|
439
|
+
when={editing()}
|
|
440
|
+
fallback={<ResourceDetail rows={spec.detail} row={row()} />}
|
|
441
|
+
>
|
|
399
442
|
<ResourceForm
|
|
400
443
|
spec={spec}
|
|
401
444
|
values={form}
|