runeforge 0.0.53 → 0.0.55
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/LICENSE +21 -21
- package/README.md +1342 -1222
- package/dist/components/Avatar.svelte +31 -31
- package/dist/components/IconRenderer.svelte +22 -22
- package/dist/components/Modal.svelte +75 -75
- package/dist/components/common/Header.svelte +40 -40
- package/dist/components/crud/EmbeddedField.svelte +175 -175
- package/dist/components/crud/Field.svelte +376 -376
- package/dist/components/crud/GenericCRUD.svelte +426 -435
- package/dist/components/crud/GenericCRUD.svelte.d.ts +6 -9
- package/dist/components/crud/SearchInput.svelte +53 -53
- package/dist/components/crud/columns/Avatar.svelte +15 -15
- package/dist/components/crud/columns/Icon.svelte +8 -8
- package/dist/components/crud/utils/reorder.d.ts +4 -0
- package/dist/components/crud/utils/reorder.js +13 -0
- package/dist/components/crud/views/Create.svelte +232 -232
- package/dist/components/crud/views/Read.svelte +124 -124
- package/dist/components/crud/views/Update.svelte +208 -208
- package/dist/components/crud/views/list/List.svelte +291 -0
- package/dist/components/crud/views/{List.svelte.d.ts → list/List.svelte.d.ts} +4 -14
- package/dist/components/crud/views/list/Modals.svelte +56 -0
- package/dist/components/crud/views/list/Modals.svelte.d.ts +36 -0
- package/dist/components/crud/views/list/Table.svelte +176 -0
- package/dist/components/crud/views/list/Table.svelte.d.ts +59 -0
- package/dist/components/crud/views/list/Toolbar.svelte +341 -0
- package/dist/components/crud/views/list/Toolbar.svelte.d.ts +46 -0
- package/dist/components/form/Button.svelte +27 -27
- package/dist/components/form/Label.svelte +37 -37
- package/dist/components/form/MultiSelect.svelte +248 -248
- package/dist/components/form/PasswordInput.svelte +68 -68
- package/dist/components/form/Required.svelte +1 -1
- package/dist/components/form/Select.svelte +209 -209
- package/dist/components/form/Tree.svelte +62 -62
- package/dist/components/form/TreeNode.svelte +66 -66
- package/dist/components/navigation/Breadcrumbs.svelte +111 -111
- package/dist/components/table/ColumnFilter.svelte +168 -168
- package/dist/components/table/PaginatedTable.svelte +536 -215
- package/dist/components/table/PaginatedTable.svelte.d.ts +11 -2
- package/dist/components/table/Paginator.svelte +113 -113
- package/dist/components/table/SortHeader.svelte +43 -43
- package/dist/components/table/TableBody.svelte +150 -70
- package/dist/components/table/TableBody.svelte.d.ts +14 -1
- package/dist/components/table/TableHeader.svelte +88 -83
- package/dist/components/table/TableHeader.svelte.d.ts +1 -0
- package/dist/components/table/sortable.d.ts +27 -0
- package/dist/components/table/sortable.js +55 -0
- package/dist/components/table/utils.d.ts +28 -1
- package/dist/components/table/utils.js +75 -0
- package/dist/i18n/en.js +2 -0
- package/dist/i18n/es.js +2 -0
- package/dist/i18n/types.d.ts +2 -0
- package/dist/icons/defaults/Clear.svelte +6 -6
- package/dist/icons/defaults/Create.svelte +6 -6
- package/dist/icons/defaults/Delete.svelte +6 -6
- package/dist/icons/defaults/Download.svelte +7 -7
- package/dist/icons/defaults/Edit.svelte +7 -7
- package/dist/icons/defaults/Filter.svelte +6 -6
- package/dist/icons/defaults/FilterActive.svelte +6 -6
- package/dist/icons/defaults/Folder.svelte +6 -6
- package/dist/icons/defaults/Grip.svelte +7 -0
- package/dist/icons/defaults/Grip.svelte.d.ts +7 -0
- package/dist/icons/defaults/Home.svelte +6 -6
- package/dist/icons/defaults/PasswordHide.svelte +9 -9
- package/dist/icons/defaults/PasswordShow.svelte +7 -7
- package/dist/icons/defaults/SortAsc.svelte +6 -6
- package/dist/icons/defaults/SortDesc.svelte +6 -6
- package/dist/icons/defaults/SortNone.svelte +6 -6
- package/dist/icons/defaults/View.svelte +7 -7
- package/dist/icons/sets/bootstrap.js +2 -1
- package/dist/icons/sets/default.js +2 -0
- package/dist/icons/types.d.ts +3 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/types/attribute.d.ts +9 -0
- package/dist/types/crud.d.ts +72 -1
- package/dist/types/table.d.ts +56 -0
- package/package.json +3 -1
- package/dist/components/crud/views/List.svelte +0 -584
|
@@ -1,435 +1,426 @@
|
|
|
1
|
-
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
-
import { page } from '$app/state';
|
|
3
|
-
import { goto } from '$app/navigation';
|
|
4
|
-
import List from './views/List.svelte';
|
|
5
|
-
import Read from './views/Read.svelte';
|
|
6
|
-
import Create from './views/Create.svelte';
|
|
7
|
-
import Update from './views/Update.svelte';
|
|
8
|
-
import { AUTO_EXCLUDED } from './utils/constants.js';
|
|
9
|
-
import {
|
|
10
|
-
resolveFormatter,
|
|
11
|
-
truncateFormatter,
|
|
12
|
-
inferType,
|
|
13
|
-
buildFieldDefinitions
|
|
14
|
-
} from './utils/resolution.js';
|
|
15
|
-
import { defaultItemLabel } from './utils/embedded.js';
|
|
16
|
-
import { isFilterable } from '../table/utils.js';
|
|
17
|
-
import type {
|
|
18
|
-
import type {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
await goto(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
m.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
params.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
for (const
|
|
286
|
-
params.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
{
|
|
346
|
-
|
|
347
|
-
{
|
|
348
|
-
{
|
|
349
|
-
{
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
{
|
|
358
|
-
|
|
359
|
-
{
|
|
360
|
-
{
|
|
361
|
-
{
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
{
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
{
|
|
369
|
-
|
|
370
|
-
{
|
|
371
|
-
{
|
|
372
|
-
{
|
|
373
|
-
{
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
{
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
{
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
{
|
|
387
|
-
{
|
|
388
|
-
{
|
|
389
|
-
{
|
|
390
|
-
{
|
|
391
|
-
{
|
|
392
|
-
{
|
|
393
|
-
{
|
|
394
|
-
{
|
|
395
|
-
{
|
|
396
|
-
{
|
|
397
|
-
{
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
{
|
|
401
|
-
{
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
{
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
{
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
{
|
|
425
|
-
|
|
426
|
-
{
|
|
427
|
-
{@const BulkActionView = activeBulkAction.action.view}
|
|
428
|
-
<BulkActionView
|
|
429
|
-
items={activeBulkAction.items}
|
|
430
|
-
label={activeBulkAction.action.label}
|
|
431
|
-
{serverError}
|
|
432
|
-
onCancel={() => (activeBulkAction = null)}
|
|
433
|
-
onSuccess={() => (activeBulkAction = null)}
|
|
434
|
-
/>
|
|
435
|
-
{/if}
|
|
1
|
+
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
+
import { page } from '$app/state';
|
|
3
|
+
import { goto } from '$app/navigation';
|
|
4
|
+
import List from './views/list/List.svelte';
|
|
5
|
+
import Read from './views/Read.svelte';
|
|
6
|
+
import Create from './views/Create.svelte';
|
|
7
|
+
import Update from './views/Update.svelte';
|
|
8
|
+
import { AUTO_EXCLUDED } from './utils/constants.js';
|
|
9
|
+
import {
|
|
10
|
+
resolveFormatter,
|
|
11
|
+
truncateFormatter,
|
|
12
|
+
inferType,
|
|
13
|
+
buildFieldDefinitions
|
|
14
|
+
} from './utils/resolution.js';
|
|
15
|
+
import { defaultItemLabel } from './utils/embedded.js';
|
|
16
|
+
import { isFilterable } from '../table/utils.js';
|
|
17
|
+
import type { AttributeMetadata } from '../../types/attribute.js';
|
|
18
|
+
import type {
|
|
19
|
+
ActionConfiguration,
|
|
20
|
+
ColumnDefinition,
|
|
21
|
+
CustomAction,
|
|
22
|
+
FieldDefinition,
|
|
23
|
+
ListActions,
|
|
24
|
+
ListConfig,
|
|
25
|
+
ViewBasedCustomBulkAction
|
|
26
|
+
} from '../../types/crud.js';
|
|
27
|
+
import type {
|
|
28
|
+
FilterSnapshot,
|
|
29
|
+
PaginatedEnvelope,
|
|
30
|
+
ServerPagination,
|
|
31
|
+
SortDirection,
|
|
32
|
+
TableQuery
|
|
33
|
+
} from '../../types/table.js';
|
|
34
|
+
|
|
35
|
+
let {
|
|
36
|
+
data = undefined as Record<string, unknown> | undefined,
|
|
37
|
+
dataKey = undefined as string | undefined,
|
|
38
|
+
idKey = '_id',
|
|
39
|
+
labelOne = '',
|
|
40
|
+
labelMany = '',
|
|
41
|
+
icon,
|
|
42
|
+
pageSize = 10,
|
|
43
|
+
creation = {} as ActionConfiguration<T>,
|
|
44
|
+
update = {} as ActionConfiguration<T>,
|
|
45
|
+
read = {} as ActionConfiguration<T>,
|
|
46
|
+
deletion = {} as ActionConfiguration<T>,
|
|
47
|
+
actions = {} as ListActions<T>,
|
|
48
|
+
config = {} as ListConfig<T>,
|
|
49
|
+
columns = undefined as ColumnDefinition<T>[] | undefined,
|
|
50
|
+
fields = undefined as FieldDefinition<T>[] | undefined,
|
|
51
|
+
meta = undefined as Partial<Record<string, AttributeMetadata>> | undefined,
|
|
52
|
+
form = null as { error?: string } | null
|
|
53
|
+
}: {
|
|
54
|
+
data?: Record<string, unknown>;
|
|
55
|
+
dataKey?: string;
|
|
56
|
+
idKey?: string;
|
|
57
|
+
labelOne?: string;
|
|
58
|
+
labelMany?: string;
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
+
icon?: any;
|
|
61
|
+
pageSize?: number;
|
|
62
|
+
creation?: ActionConfiguration<T>;
|
|
63
|
+
update?: ActionConfiguration<T>;
|
|
64
|
+
read?: ActionConfiguration<T>;
|
|
65
|
+
deletion?: ActionConfiguration<T>;
|
|
66
|
+
/** Extra per-row (`custom`) and per-selection (`bulk`) actions. */
|
|
67
|
+
actions?: ListActions<T>;
|
|
68
|
+
/** Opt-in list behaviors: free-text search, CSV/Excel export, and
|
|
69
|
+
* drag-to-reorder. */
|
|
70
|
+
config?: ListConfig<T>;
|
|
71
|
+
columns?: ColumnDefinition<T>[];
|
|
72
|
+
fields?: FieldDefinition<T>[];
|
|
73
|
+
meta?: Partial<Record<string, AttributeMetadata>>;
|
|
74
|
+
form?: { error?: string } | null;
|
|
75
|
+
} = $props();
|
|
76
|
+
|
|
77
|
+
function isEnvelope(value: unknown): value is PaginatedEnvelope<T> {
|
|
78
|
+
return (
|
|
79
|
+
!!value &&
|
|
80
|
+
typeof value === 'object' &&
|
|
81
|
+
!Array.isArray(value) &&
|
|
82
|
+
Array.isArray((value as Record<string, unknown>).results) &&
|
|
83
|
+
typeof (value as Record<string, unknown>).count === 'number'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const rawSlice = $derived(data && dataKey ? data[dataKey] : undefined);
|
|
88
|
+
const envelope = $derived(isEnvelope(rawSlice) ? rawSlice : undefined);
|
|
89
|
+
const entityData = $derived<T[]>(
|
|
90
|
+
envelope ? envelope.results : Array.isArray(rawSlice) ? (rawSlice as T[]) : []
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const serverPagination = $derived<ServerPagination | undefined>(
|
|
94
|
+
envelope
|
|
95
|
+
? {
|
|
96
|
+
page: envelope.page,
|
|
97
|
+
pageSize: envelope.pageSize,
|
|
98
|
+
totalPages: Math.max(1, Math.ceil(envelope.count / envelope.pageSize)),
|
|
99
|
+
total: envelope.count
|
|
100
|
+
}
|
|
101
|
+
: undefined
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const viewParam = $derived(page.url.searchParams.get('view'));
|
|
105
|
+
const idParam = $derived(page.url.searchParams.get('id'));
|
|
106
|
+
|
|
107
|
+
const creating = $derived(viewParam === 'create');
|
|
108
|
+
const reading = $derived(idParam !== null && viewParam === null);
|
|
109
|
+
const editing = $derived(idParam !== null && viewParam === 'edit');
|
|
110
|
+
|
|
111
|
+
const excluded = $derived(new Set([...AUTO_EXCLUDED, idKey]));
|
|
112
|
+
|
|
113
|
+
const singleInstance = $derived<T | undefined>(
|
|
114
|
+
Object.values(page.data as Record<string, unknown>).find(
|
|
115
|
+
(v) => v !== null && typeof v === 'object' && !Array.isArray(v) && idKey in (v as object)
|
|
116
|
+
) as T | undefined
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
let activeAction = $state<{ action: CustomAction<T>; item: T } | null>(null);
|
|
120
|
+
let activeBulkAction = $state<{ action: ViewBasedCustomBulkAction<T>; items: T[] } | null>(
|
|
121
|
+
null
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
async function runAction(action: CustomAction<T>, item: T) {
|
|
125
|
+
if (!(action.condition?.(item) ?? true)) return;
|
|
126
|
+
if (action.href) {
|
|
127
|
+
await goto(action.href(item));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
activeAction = { action, item };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function runBulkAction(action: ViewBasedCustomBulkAction<T>, items: T[]) {
|
|
134
|
+
activeBulkAction = { action, items };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let duplicateSeed = $state<Record<string, unknown> | undefined>(undefined);
|
|
138
|
+
|
|
139
|
+
async function navList() {
|
|
140
|
+
duplicateSeed = undefined;
|
|
141
|
+
await goto(lastListState.search || '?');
|
|
142
|
+
}
|
|
143
|
+
async function navCreate() {
|
|
144
|
+
duplicateSeed = undefined;
|
|
145
|
+
await goto('?view=create');
|
|
146
|
+
}
|
|
147
|
+
async function navDuplicate(record: Record<string, unknown>) {
|
|
148
|
+
duplicateSeed = record;
|
|
149
|
+
await goto('?view=create');
|
|
150
|
+
}
|
|
151
|
+
async function navRead(item: T) {
|
|
152
|
+
await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)[idKey] ?? ''))}`);
|
|
153
|
+
}
|
|
154
|
+
async function navEdit(item: T) {
|
|
155
|
+
await goto(
|
|
156
|
+
`?id=${encodeURIComponent(String((item as Record<string, unknown>)[idKey] ?? ''))}&view=edit`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let lastListState = $state<{ data: T[]; search: string }>({ data: [], search: '' });
|
|
161
|
+
$effect(() => {
|
|
162
|
+
if (!creating && !reading && !editing) {
|
|
163
|
+
lastListState = { data: entityData, search: page.url.search };
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// "Save and continue" on the Update form: jumps to editing the record that
|
|
168
|
+
// follows the current one in the last loaded list, falling back to
|
|
169
|
+
// re-editing the same instance when it's the last one (or isn't found).
|
|
170
|
+
function nextInstance(current: T): T {
|
|
171
|
+
const currentId = (current as Record<string, unknown>)[idKey];
|
|
172
|
+
const idx = lastListState.data.findIndex(
|
|
173
|
+
(item) => (item as Record<string, unknown>)[idKey] === currentId
|
|
174
|
+
);
|
|
175
|
+
return (idx !== -1 ? lastListState.data[idx + 1] : undefined) ?? current;
|
|
176
|
+
}
|
|
177
|
+
async function navContinueEdit() {
|
|
178
|
+
if (!singleInstance) return;
|
|
179
|
+
await navEdit(nextInstance(singleInstance));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const resolvedColumns: ColumnDefinition<T>[] = $derived(
|
|
183
|
+
columns ??
|
|
184
|
+
(meta
|
|
185
|
+
? (Object.entries(meta) as [string, AttributeMetadata][])
|
|
186
|
+
.filter(([, m]) => !m.excludedFromList)
|
|
187
|
+
.map(([k, m]) => {
|
|
188
|
+
const embeddedFields =
|
|
189
|
+
m.type === 'embedded' && m.fields
|
|
190
|
+
? buildFieldDefinitions(m.fields, data, 'excludedFromList', new Set())
|
|
191
|
+
: undefined;
|
|
192
|
+
// Arrays of objects have no sensible raw cell value, so an
|
|
193
|
+
// embedded column without an explicit formatter falls back to
|
|
194
|
+
// joining each item's label (itemLabel, or the same
|
|
195
|
+
// sub-field-joining summary the embedded form list uses).
|
|
196
|
+
const resolvedFormatter =
|
|
197
|
+
resolveFormatter(m, data) ??
|
|
198
|
+
(embeddedFields
|
|
199
|
+
? (value: unknown) =>
|
|
200
|
+
Array.isArray(value)
|
|
201
|
+
? value
|
|
202
|
+
.map((item) =>
|
|
203
|
+
m.itemLabel
|
|
204
|
+
? m.itemLabel(item as Record<string, unknown>)
|
|
205
|
+
: defaultItemLabel(embeddedFields, item as Record<string, unknown>)
|
|
206
|
+
)
|
|
207
|
+
.join(', ')
|
|
208
|
+
: ''
|
|
209
|
+
: undefined);
|
|
210
|
+
const formatter =
|
|
211
|
+
m.truncateUpTo != null
|
|
212
|
+
? truncateFormatter(resolvedFormatter, m.truncateUpTo)
|
|
213
|
+
: resolvedFormatter;
|
|
214
|
+
return {
|
|
215
|
+
attribute: k as keyof T & string,
|
|
216
|
+
title: m.label ?? k,
|
|
217
|
+
type: m.type,
|
|
218
|
+
formatter,
|
|
219
|
+
component: m.component,
|
|
220
|
+
sortable: m.sortable,
|
|
221
|
+
filterable: m.filterable,
|
|
222
|
+
filterOptions: m.filterOptions,
|
|
223
|
+
fields: embeddedFields,
|
|
224
|
+
itemLabel: m.itemLabel
|
|
225
|
+
};
|
|
226
|
+
})
|
|
227
|
+
: entityData.length > 0
|
|
228
|
+
? (Object.keys(entityData[0]) as (keyof T & string)[])
|
|
229
|
+
.filter((k) => !excluded.has(k))
|
|
230
|
+
.map((k) => ({ attribute: k, title: k }))
|
|
231
|
+
: [])
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Server-pagination mode only: GenericCRUD owns `page`/`ordering`/per-column
|
|
235
|
+
// filter query params the same way it already owns `view`/`id`, so pagination
|
|
236
|
+
// state survives reloads/direct links and `+page.svelte` never has to change.
|
|
237
|
+
const orderingParam = $derived(page.url.searchParams.get('ordering'));
|
|
238
|
+
const initialSort = $derived(
|
|
239
|
+
orderingParam
|
|
240
|
+
? {
|
|
241
|
+
column: orderingParam.startsWith('-') ? orderingParam.slice(1) : orderingParam,
|
|
242
|
+
direction: (orderingParam.startsWith('-') ? 'desc' : 'asc') as SortDirection
|
|
243
|
+
}
|
|
244
|
+
: undefined
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const initialFilters = $derived.by<Partial<FilterSnapshot> | undefined>(() => {
|
|
248
|
+
if (!envelope) return undefined;
|
|
249
|
+
const text: Record<string, string> = {};
|
|
250
|
+
const values: Record<string, string[]> = {};
|
|
251
|
+
const dateRanges: Record<string, { from: string; to: string }> = {};
|
|
252
|
+
for (const col of resolvedColumns) {
|
|
253
|
+
if (!isFilterable(col)) continue;
|
|
254
|
+
const raw = page.url.searchParams.get(col.attribute);
|
|
255
|
+
if (raw != null) {
|
|
256
|
+
if (col.type === 'boolean') values[col.attribute] = raw.split(',').filter(Boolean);
|
|
257
|
+
else text[col.attribute] = raw;
|
|
258
|
+
}
|
|
259
|
+
const from = page.url.searchParams.get(`${col.attribute}_from`);
|
|
260
|
+
const to = page.url.searchParams.get(`${col.attribute}_to`);
|
|
261
|
+
if (from || to) dateRanges[col.attribute] = { from: from ?? '', to: to ?? '' };
|
|
262
|
+
}
|
|
263
|
+
return { text, values, dateRanges };
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
async function handlePaginationChange(query: TableQuery) {
|
|
267
|
+
// Local, synchronous query-string builder consumed immediately by goto();
|
|
268
|
+
// not rendered/reactive state, so plain URLSearchParams is correct here.
|
|
269
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
270
|
+
const params = new URLSearchParams(page.url.searchParams);
|
|
271
|
+
params.delete('view');
|
|
272
|
+
params.delete('id');
|
|
273
|
+
|
|
274
|
+
if (query.page > 1) params.set('page', String(query.page));
|
|
275
|
+
else params.delete('page');
|
|
276
|
+
|
|
277
|
+
if (query.ordering) params.set('ordering', query.ordering);
|
|
278
|
+
else params.delete('ordering');
|
|
279
|
+
|
|
280
|
+
for (const col of resolvedColumns) {
|
|
281
|
+
params.delete(col.attribute);
|
|
282
|
+
params.delete(`${col.attribute}_from`);
|
|
283
|
+
params.delete(`${col.attribute}_to`);
|
|
284
|
+
}
|
|
285
|
+
for (const [k, v] of Object.entries(query.filters.text)) {
|
|
286
|
+
if (v) params.set(k, v);
|
|
287
|
+
}
|
|
288
|
+
for (const [k, vs] of Object.entries(query.filters.values)) {
|
|
289
|
+
if (vs.length) params.set(k, vs.join(','));
|
|
290
|
+
}
|
|
291
|
+
for (const [k, r] of Object.entries(query.filters.dateRanges)) {
|
|
292
|
+
if (r.from) params.set(`${k}_from`, r.from);
|
|
293
|
+
if (r.to) params.set(`${k}_to`, r.to);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const qs = params.toString();
|
|
297
|
+
await goto(qs ? `?${qs}` : '?', { keepFocus: true, noScroll: true });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const resolvedFields: FieldDefinition<T>[] = $derived(
|
|
301
|
+
fields ??
|
|
302
|
+
(meta
|
|
303
|
+
? buildFieldDefinitions<T>(meta, data, 'excludedFromCreate', excluded)
|
|
304
|
+
: entityData.length > 0
|
|
305
|
+
? (Object.entries(entityData[0]) as [string, unknown][])
|
|
306
|
+
.filter(([k]) => !excluded.has(k))
|
|
307
|
+
.map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
|
|
308
|
+
: [])
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const resolvedReadFields: FieldDefinition<T>[] = $derived(
|
|
312
|
+
fields ??
|
|
313
|
+
(meta
|
|
314
|
+
? buildFieldDefinitions<T>(meta, data, 'excludedFromRead', excluded)
|
|
315
|
+
: entityData.length > 0
|
|
316
|
+
? (Object.entries(entityData[0]) as [string, unknown][])
|
|
317
|
+
.filter(([k]) => !excluded.has(k))
|
|
318
|
+
.map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
|
|
319
|
+
: [])
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const resolvedUpdateFields: FieldDefinition<T>[] = $derived(
|
|
323
|
+
fields ??
|
|
324
|
+
(meta
|
|
325
|
+
? buildFieldDefinitions<T>(meta, data, 'excludedFromUpdate', excluded)
|
|
326
|
+
: entityData.length > 0
|
|
327
|
+
? (Object.entries(entityData[0]) as [string, unknown][])
|
|
328
|
+
.filter(([k]) => !excluded.has(k))
|
|
329
|
+
.map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
|
|
330
|
+
: [])
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
const serverError = $derived(
|
|
334
|
+
creating || reading || editing || activeAction !== null || activeBulkAction !== null
|
|
335
|
+
? (form?.error ?? '')
|
|
336
|
+
: ''
|
|
337
|
+
);
|
|
338
|
+
</script>
|
|
339
|
+
|
|
340
|
+
{#if creating}
|
|
341
|
+
<Create
|
|
342
|
+
{labelOne}
|
|
343
|
+
{labelMany}
|
|
344
|
+
{icon}
|
|
345
|
+
fields={resolvedFields}
|
|
346
|
+
{creation}
|
|
347
|
+
{serverError}
|
|
348
|
+
seed={duplicateSeed}
|
|
349
|
+
onCancel={navList}
|
|
350
|
+
onSuccess={navList}
|
|
351
|
+
/>
|
|
352
|
+
{:else if reading}
|
|
353
|
+
<Read
|
|
354
|
+
{labelOne}
|
|
355
|
+
{labelMany}
|
|
356
|
+
{icon}
|
|
357
|
+
{idKey}
|
|
358
|
+
fields={resolvedReadFields}
|
|
359
|
+
instance={singleInstance ?? ({} as T)}
|
|
360
|
+
{read}
|
|
361
|
+
onCancel={navList}
|
|
362
|
+
/>
|
|
363
|
+
{:else if editing}
|
|
364
|
+
<Update
|
|
365
|
+
{labelOne}
|
|
366
|
+
{labelMany}
|
|
367
|
+
{icon}
|
|
368
|
+
{idKey}
|
|
369
|
+
fields={resolvedUpdateFields}
|
|
370
|
+
instance={singleInstance ?? ({} as T)}
|
|
371
|
+
{update}
|
|
372
|
+
{serverError}
|
|
373
|
+
onCancel={navList}
|
|
374
|
+
onSuccess={navList}
|
|
375
|
+
onContinue={navContinueEdit}
|
|
376
|
+
onDuplicate={navDuplicate}
|
|
377
|
+
/>
|
|
378
|
+
{:else}
|
|
379
|
+
<List
|
|
380
|
+
data={entityData}
|
|
381
|
+
{labelOne}
|
|
382
|
+
{labelMany}
|
|
383
|
+
{icon}
|
|
384
|
+
{pageSize}
|
|
385
|
+
{idKey}
|
|
386
|
+
{creation}
|
|
387
|
+
{update}
|
|
388
|
+
{read}
|
|
389
|
+
{deletion}
|
|
390
|
+
{actions}
|
|
391
|
+
{config}
|
|
392
|
+
columns={resolvedColumns}
|
|
393
|
+
pagination={serverPagination}
|
|
394
|
+
{initialSort}
|
|
395
|
+
{initialFilters}
|
|
396
|
+
onPaginationChange={handlePaginationChange}
|
|
397
|
+
onCreate={navCreate}
|
|
398
|
+
onEdit={navEdit}
|
|
399
|
+
onView={navRead}
|
|
400
|
+
onAction={runAction}
|
|
401
|
+
onBulkAction={runBulkAction}
|
|
402
|
+
/>
|
|
403
|
+
{/if}
|
|
404
|
+
|
|
405
|
+
{#if activeAction !== null}
|
|
406
|
+
{@const ActionView = activeAction.action.view}
|
|
407
|
+
<ActionView
|
|
408
|
+
instance={activeAction.item}
|
|
409
|
+
label={activeAction.action.label}
|
|
410
|
+
endpoint={activeAction.action.endpoint}
|
|
411
|
+
{serverError}
|
|
412
|
+
onCancel={() => (activeAction = null)}
|
|
413
|
+
onSuccess={() => (activeAction = null)}
|
|
414
|
+
/>
|
|
415
|
+
{/if}
|
|
416
|
+
|
|
417
|
+
{#if activeBulkAction !== null}
|
|
418
|
+
{@const BulkActionView = activeBulkAction.action.view}
|
|
419
|
+
<BulkActionView
|
|
420
|
+
items={activeBulkAction.items}
|
|
421
|
+
label={activeBulkAction.action.label}
|
|
422
|
+
{serverError}
|
|
423
|
+
onCancel={() => (activeBulkAction = null)}
|
|
424
|
+
onSuccess={() => (activeBulkAction = null)}
|
|
425
|
+
/>
|
|
426
|
+
{/if}
|