runeforge 0.0.51 → 0.0.52
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/dist/components/crud/GenericCRUD.svelte +10 -0
- package/dist/components/crud/utils/embedded.d.ts +1 -0
- package/dist/components/crud/utils/embedded.js +13 -0
- package/dist/components/crud/views/Create.svelte +8 -2
- package/dist/components/crud/views/Create.svelte.d.ts +3 -0
- package/dist/components/crud/views/Update.svelte +25 -14
- package/dist/components/crud/views/Update.svelte.d.ts +1 -0
- package/dist/types/crud.d.ts +9 -5
- package/package.json +1 -1
|
@@ -139,10 +139,18 @@
|
|
|
139
139
|
activeBulkAction = { action, items };
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
let duplicateSeed = $state<Record<string, unknown> | undefined>(undefined);
|
|
143
|
+
|
|
142
144
|
async function navList() {
|
|
145
|
+
duplicateSeed = undefined;
|
|
143
146
|
await goto(lastListState.search || '?');
|
|
144
147
|
}
|
|
145
148
|
async function navCreate() {
|
|
149
|
+
duplicateSeed = undefined;
|
|
150
|
+
await goto('?view=create');
|
|
151
|
+
}
|
|
152
|
+
async function navDuplicate(record: Record<string, unknown>) {
|
|
153
|
+
duplicateSeed = record;
|
|
146
154
|
await goto('?view=create');
|
|
147
155
|
}
|
|
148
156
|
async function navRead(item: T) {
|
|
@@ -337,6 +345,7 @@
|
|
|
337
345
|
fields={resolvedFields}
|
|
338
346
|
{creation}
|
|
339
347
|
{serverError}
|
|
348
|
+
seed={duplicateSeed}
|
|
340
349
|
onCancel={navList}
|
|
341
350
|
onSuccess={navList}
|
|
342
351
|
/>
|
|
@@ -364,6 +373,7 @@
|
|
|
364
373
|
onCancel={navList}
|
|
365
374
|
onSuccess={navList}
|
|
366
375
|
onContinue={navContinueEdit}
|
|
376
|
+
onDuplicate={navDuplicate}
|
|
367
377
|
/>
|
|
368
378
|
{:else}
|
|
369
379
|
<List
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FieldDefinition } from '../../../types/crud.js';
|
|
2
2
|
export declare function seedField<T extends object = Record<string, unknown>>(f: FieldDefinition<T>, raw: unknown): unknown;
|
|
3
3
|
export declare function emptyRecord<T extends object = Record<string, unknown>>(fields: FieldDefinition<T>[]): Record<string, unknown>;
|
|
4
|
+
export declare function seedRecord<T extends object = Record<string, unknown>>(fields: FieldDefinition<T>[], source: Record<string, unknown>): Record<string, unknown>;
|
|
4
5
|
export declare function formatEmbeddedFieldValue<T extends object = Record<string, unknown>>(f: FieldDefinition<T>, raw: unknown): string;
|
|
5
6
|
export declare function defaultItemLabel<T extends object = Record<string, unknown>>(fields: FieldDefinition<T>[], item: Record<string, unknown>): string;
|
|
@@ -26,6 +26,19 @@ export function seedField(f, raw) {
|
|
|
26
26
|
export function emptyRecord(fields) {
|
|
27
27
|
return Object.fromEntries(fields.map((f) => [f.attribute, seedField(f, f.default)]));
|
|
28
28
|
}
|
|
29
|
+
// Shared by Update.svelte (loading the instance being edited) and Create.svelte
|
|
30
|
+
// (pre-filling a new record from a duplicated instance): builds a draft record
|
|
31
|
+
// from an arbitrary source object, running each field's `seed` resolver
|
|
32
|
+
// (falling back to the source's raw value) through the same seedField
|
|
33
|
+
// normalization as emptyRecord.
|
|
34
|
+
export function seedRecord(fields, source) {
|
|
35
|
+
const seeded = { ...source };
|
|
36
|
+
for (const f of fields) {
|
|
37
|
+
const raw = f.seed ? f.seed(source) : source[f.attribute];
|
|
38
|
+
seeded[f.attribute] = seedField(f, raw);
|
|
39
|
+
}
|
|
40
|
+
return seeded;
|
|
41
|
+
}
|
|
29
42
|
// Shared by defaultItemLabel and the CSV/XLSX export column expansion: renders
|
|
30
43
|
// one sub-field's raw stored value as display text, resolving a select's
|
|
31
44
|
// option label instead of its stored value. Booleans are left to the caller
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { defaultIconSet } from '../../../icons/sets/default.js';
|
|
9
9
|
import { validateAll } from '../utils/validation.js';
|
|
10
10
|
import { groupFields } from '../utils/grouping.js';
|
|
11
|
-
import { emptyRecord } from '../utils/embedded.js';
|
|
11
|
+
import { emptyRecord, seedRecord } from '../utils/embedded.js';
|
|
12
12
|
import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
|
|
13
13
|
import { getStrings } from '../../../i18n/context.js';
|
|
14
14
|
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
fields = [] as FieldDefinition<T>[],
|
|
22
22
|
creation = {} as ActionConfiguration<T>,
|
|
23
23
|
serverError = '',
|
|
24
|
+
seed = undefined as Record<string, unknown> | undefined,
|
|
24
25
|
onCancel,
|
|
25
26
|
onSuccess,
|
|
26
27
|
}: {
|
|
@@ -31,6 +32,9 @@
|
|
|
31
32
|
fields?: FieldDefinition<T>[];
|
|
32
33
|
creation?: ActionConfiguration<T>;
|
|
33
34
|
serverError?: string;
|
|
35
|
+
/** Pre-fills the form from another instance's values — used when the
|
|
36
|
+
* create view is reached via an Update form's "Duplicate" button. */
|
|
37
|
+
seed?: Record<string, unknown>;
|
|
34
38
|
onCancel?: () => void;
|
|
35
39
|
onSuccess?: () => void;
|
|
36
40
|
} = $props();
|
|
@@ -51,7 +55,9 @@
|
|
|
51
55
|
successTimeout = setTimeout(() => (successMessage = ''), 4000);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
let record = $state<Record<string, unknown>>(
|
|
58
|
+
let record = $state<Record<string, unknown>>(
|
|
59
|
+
untrack(() => (seed ? seedRecord(fields, seed) : emptyRecord(fields)))
|
|
60
|
+
);
|
|
55
61
|
|
|
56
62
|
const groups = $derived(groupFields(fields));
|
|
57
63
|
const hasFileField = $derived(fields.some((f) => f.type === 'file'));
|
|
@@ -7,6 +7,9 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
|
7
7
|
fields?: FieldDefinition<T>[];
|
|
8
8
|
creation?: ActionConfiguration<T>;
|
|
9
9
|
serverError?: string;
|
|
10
|
+
/** Pre-fills the form from another instance's values — used when the
|
|
11
|
+
* create view is reached via an Update form's "Duplicate" button. */
|
|
12
|
+
seed?: Record<string, unknown>;
|
|
10
13
|
onCancel?: () => void;
|
|
11
14
|
onSuccess?: () => void;
|
|
12
15
|
};
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { defaultIconSet } from '../../../icons/sets/default.js';
|
|
9
9
|
import { validateAll } from '../utils/validation.js';
|
|
10
10
|
import { groupFields } from '../utils/grouping.js';
|
|
11
|
-
import {
|
|
11
|
+
import { seedRecord } from '../utils/embedded.js';
|
|
12
12
|
import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
|
|
13
13
|
import { getStrings } from '../../../i18n/context.js';
|
|
14
14
|
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
onCancel,
|
|
27
27
|
onSuccess,
|
|
28
28
|
onContinue,
|
|
29
|
+
onDuplicate,
|
|
29
30
|
}: {
|
|
30
31
|
labelOne?: string;
|
|
31
32
|
labelMany?: string;
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
onCancel?: () => void;
|
|
40
41
|
onSuccess?: () => void;
|
|
41
42
|
onContinue?: () => void;
|
|
43
|
+
onDuplicate?: (record: Record<string, unknown>) => void;
|
|
42
44
|
} = $props();
|
|
43
45
|
|
|
44
46
|
const icons = $derived(getIconSet() ?? defaultIconSet);
|
|
@@ -47,24 +49,16 @@
|
|
|
47
49
|
let fieldErrors = $state<Record<string, string>>({});
|
|
48
50
|
let internalError = $state('');
|
|
49
51
|
let continuing = $state(false);
|
|
50
|
-
|
|
51
|
-
function seedFromInstance(inst: Record<string, unknown>): Record<string, unknown> {
|
|
52
|
-
const seeded: Record<string, unknown> = { ...inst };
|
|
53
|
-
for (const f of fields) {
|
|
54
|
-
const raw = f.seed ? f.seed(inst) : inst[f.attribute];
|
|
55
|
-
seeded[f.attribute] = seedField(f, raw);
|
|
56
|
-
}
|
|
57
|
-
return seeded;
|
|
58
|
-
}
|
|
52
|
+
let duplicating = $state(false);
|
|
59
53
|
|
|
60
54
|
let record = $state<Record<string, unknown>>(
|
|
61
|
-
untrack(() =>
|
|
55
|
+
untrack(() => seedRecord(fields, instance as Record<string, unknown>))
|
|
62
56
|
);
|
|
63
57
|
|
|
64
58
|
$effect(() => {
|
|
65
59
|
const id = (instance as Record<string, unknown>)[idKey];
|
|
66
60
|
if (!id) return;
|
|
67
|
-
untrack(() => { record =
|
|
61
|
+
untrack(() => { record = seedRecord(fields, instance as Record<string, unknown>); });
|
|
68
62
|
});
|
|
69
63
|
|
|
70
64
|
const groups = $derived(groupFields(fields));
|
|
@@ -74,6 +68,10 @@
|
|
|
74
68
|
const continueLabel = $derived(update.continue?.label ?? strings.saveAndContinue);
|
|
75
69
|
const continueClass = $derived(update.continue?.class ?? '');
|
|
76
70
|
|
|
71
|
+
const duplicationEnabled = $derived(update.duplication?.enabled ?? false);
|
|
72
|
+
const duplicationLabel = $derived(update.duplication?.label ?? strings.duplicate);
|
|
73
|
+
const duplicationClass = $derived(update.duplication?.class ?? '');
|
|
74
|
+
|
|
77
75
|
const errorEntries = $derived([
|
|
78
76
|
...((serverError || internalError) ? [['_global', internalError || serverError] as [string, string]] : []),
|
|
79
77
|
...Object.entries(fieldErrors),
|
|
@@ -123,6 +121,9 @@
|
|
|
123
121
|
if (continuing) {
|
|
124
122
|
continuing = false;
|
|
125
123
|
onContinue?.();
|
|
124
|
+
} else if (duplicating) {
|
|
125
|
+
duplicating = false;
|
|
126
|
+
onDuplicate?.(record);
|
|
126
127
|
} else {
|
|
127
128
|
onSuccess?.();
|
|
128
129
|
}
|
|
@@ -173,17 +174,27 @@
|
|
|
173
174
|
<Button variant="ghost" onclick={() => onCancel?.()}>
|
|
174
175
|
{strings.cancel}
|
|
175
176
|
</Button>
|
|
177
|
+
{#if duplicationEnabled}
|
|
178
|
+
<Button
|
|
179
|
+
type="submit"
|
|
180
|
+
variant="warning"
|
|
181
|
+
class={duplicationClass}
|
|
182
|
+
onclick={() => { continuing = false; duplicating = true; }}
|
|
183
|
+
>
|
|
184
|
+
{duplicationLabel}
|
|
185
|
+
</Button>
|
|
186
|
+
{/if}
|
|
176
187
|
{#if continueEnabled}
|
|
177
188
|
<Button
|
|
178
189
|
type="submit"
|
|
179
190
|
variant="secondary"
|
|
180
191
|
class={continueClass}
|
|
181
|
-
onclick={() => { continuing = true; }}
|
|
192
|
+
onclick={() => { continuing = true; duplicating = false; }}
|
|
182
193
|
>
|
|
183
194
|
{continueLabel}
|
|
184
195
|
</Button>
|
|
185
196
|
{/if}
|
|
186
|
-
<Button type="submit" variant="primary" onclick={() => { continuing = false; }}>
|
|
197
|
+
<Button type="submit" variant="primary" onclick={() => { continuing = false; duplicating = false; }}>
|
|
187
198
|
{strings.save}
|
|
188
199
|
</Button>
|
|
189
200
|
</div>
|
package/dist/types/crud.d.ts
CHANGED
|
@@ -81,11 +81,15 @@ export interface ActionConfiguration<T extends object = Record<string, unknown>>
|
|
|
81
81
|
* Falls back to reloading the current instance when there is no next
|
|
82
82
|
* one. Disabled by default — pass `{ enabled: true }` to show it. */
|
|
83
83
|
continue?: CreateFormButtonConfiguration;
|
|
84
|
-
/**
|
|
85
|
-
* submits to the same `endpoint`, but — unlike "Save and
|
|
86
|
-
* blanks the form — leaves the just-submitted values in
|
|
87
|
-
* can tweak a few fields and save again as a new
|
|
88
|
-
*
|
|
84
|
+
/** Shows a "Duplicate" button alongside Save/Cancel.
|
|
85
|
+
* - Create form: submits to the same `endpoint`, but — unlike "Save and
|
|
86
|
+
* continue", which blanks the form — leaves the just-submitted values in
|
|
87
|
+
* place so the user can tweak a few fields and save again as a new
|
|
88
|
+
* record.
|
|
89
|
+
* - Update form: submits the edit to the same `endpoint`, then opens the
|
|
90
|
+
* create form pre-filled with the just-saved instance's values, so a new
|
|
91
|
+
* record can be started from it without retyping everything.
|
|
92
|
+
* Disabled by default — pass `{ enabled: true }` to show it. */
|
|
89
93
|
duplication?: CreateFormButtonConfiguration;
|
|
90
94
|
}
|
|
91
95
|
export interface CustomAction<T extends object = Record<string, unknown>> {
|