runeforge 0.0.45 → 0.0.47
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/Field.svelte +56 -12
- package/dist/components/crud/utils/resolution.js +1 -0
- package/dist/components/crud/views/Create.svelte +67 -4
- package/dist/i18n/en.js +2 -0
- package/dist/i18n/es.js +2 -0
- package/dist/i18n/types.d.ts +5 -0
- package/dist/types/attribute.d.ts +2 -0
- package/dist/types/crud.d.ts +23 -0
- package/package.json +1 -1
|
@@ -82,13 +82,46 @@
|
|
|
82
82
|
if (!calendarDateEl) return;
|
|
83
83
|
function handler() {
|
|
84
84
|
if (fieldDisabled) return;
|
|
85
|
-
|
|
85
|
+
setDateTime(calendarDateEl!.value, timePart);
|
|
86
86
|
datePopoverEl?.hidePopover();
|
|
87
87
|
}
|
|
88
88
|
calendarDateEl.addEventListener('change', handler);
|
|
89
89
|
return () => calendarDateEl?.removeEventListener('change', handler);
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
function pad(n: number): string {
|
|
93
|
+
return String(n).padStart(2, '0');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// `record[field.attribute]` may be empty, a bare "YYYY-MM-DD" (freshly
|
|
97
|
+
// picked, before any time is set) or a full ISO datetime (loaded from an
|
|
98
|
+
// existing instance) — parsed once here so the date button, the time
|
|
99
|
+
// input and cally's own `value` all derive from the same local-time
|
|
100
|
+
// breakdown instead of each re-parsing the raw string their own way.
|
|
101
|
+
const parsedDateTime = $derived.by(() => {
|
|
102
|
+
const raw = record[field.attribute];
|
|
103
|
+
if (raw == null || raw === '') return null;
|
|
104
|
+
const d = new Date(String(raw));
|
|
105
|
+
return isNaN(d.getTime()) ? null : d;
|
|
106
|
+
});
|
|
107
|
+
const datePart = $derived(
|
|
108
|
+
parsedDateTime
|
|
109
|
+
? `${parsedDateTime.getFullYear()}-${pad(parsedDateTime.getMonth() + 1)}-${pad(parsedDateTime.getDate())}`
|
|
110
|
+
: ''
|
|
111
|
+
);
|
|
112
|
+
const timePart = $derived(
|
|
113
|
+
parsedDateTime ? `${pad(parsedDateTime.getHours())}:${pad(parsedDateTime.getMinutes())}` : ''
|
|
114
|
+
);
|
|
115
|
+
const formattedDatePart = $derived(
|
|
116
|
+
parsedDateTime
|
|
117
|
+
? `${pad(parsedDateTime.getDate())}/${pad(parsedDateTime.getMonth() + 1)}/${parsedDateTime.getFullYear()}`
|
|
118
|
+
: ''
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
function setDateTime(nextDatePart: string, nextTimePart: string) {
|
|
122
|
+
record[field.attribute] = nextDatePart ? `${nextDatePart}T${nextTimePart || '00:00'}` : '';
|
|
123
|
+
}
|
|
124
|
+
|
|
92
125
|
$effect(() => {
|
|
93
126
|
if (!field.dependentOptions || isMultiValued) return;
|
|
94
127
|
const current = record[field.attribute];
|
|
@@ -191,16 +224,25 @@
|
|
|
191
224
|
value={String(record[field.attribute] ?? '')}
|
|
192
225
|
disabled={fieldDisabled}
|
|
193
226
|
/>
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
227
|
+
<div class="flex gap-2">
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
230
|
+
class="input input-bordered w-full text-left font-normal"
|
|
231
|
+
class:opacity-40={!record[field.attribute]}
|
|
232
|
+
disabled={fieldDisabled}
|
|
233
|
+
popovertarget={datePopId}
|
|
234
|
+
style="anchor-name:{dateAnchorName}"
|
|
235
|
+
>
|
|
236
|
+
{record[field.attribute] ? formattedDatePart : (field.placeholder ?? '')}
|
|
237
|
+
</button>
|
|
238
|
+
<input
|
|
239
|
+
type="time"
|
|
240
|
+
class="input input-bordered w-32 shrink-0"
|
|
241
|
+
value={timePart}
|
|
242
|
+
disabled={fieldDisabled || !record[field.attribute]}
|
|
243
|
+
onchange={(e) => setDateTime(datePart, (e.currentTarget as HTMLInputElement).value)}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
204
246
|
<div
|
|
205
247
|
popover="auto"
|
|
206
248
|
id={datePopId}
|
|
@@ -211,7 +253,7 @@
|
|
|
211
253
|
<calendar-date
|
|
212
254
|
class="cally"
|
|
213
255
|
bind:this={calendarDateEl}
|
|
214
|
-
value={
|
|
256
|
+
value={datePart}
|
|
215
257
|
>
|
|
216
258
|
<svg
|
|
217
259
|
aria-label={strings.previous}
|
|
@@ -235,6 +277,7 @@
|
|
|
235
277
|
{#if readonly}
|
|
236
278
|
<textarea
|
|
237
279
|
id={field.attribute}
|
|
280
|
+
rows={field.rows}
|
|
238
281
|
class="textarea textarea-bordered bg-base-100 w-full"
|
|
239
282
|
value={formattedValue}
|
|
240
283
|
disabled
|
|
@@ -243,6 +286,7 @@
|
|
|
243
286
|
<textarea
|
|
244
287
|
id={field.attribute}
|
|
245
288
|
{name}
|
|
289
|
+
rows={field.rows}
|
|
246
290
|
placeholder={field.placeholder ?? ''}
|
|
247
291
|
bind:value={record[field.attribute]}
|
|
248
292
|
class="textarea textarea-bordered bg-base-100 w-full"
|
|
@@ -57,6 +57,7 @@ export function buildFieldDefinitions(meta, data, excludedFlag, excluded) {
|
|
|
57
57
|
minLength: m.minLength,
|
|
58
58
|
maxLength: m.maxLength,
|
|
59
59
|
pattern: m.pattern,
|
|
60
|
+
rows: m.rows,
|
|
60
61
|
fields: m.fields ? buildFieldDefinitions(m.fields, data, excludedFlag, new Set()) : undefined,
|
|
61
62
|
itemLabel: m.itemLabel,
|
|
62
63
|
defaultExpanded: m.defaultExpanded,
|
|
@@ -40,13 +40,30 @@
|
|
|
40
40
|
|
|
41
41
|
let fieldErrors = $state<Record<string, string>>({});
|
|
42
42
|
let internalError = $state('');
|
|
43
|
+
let successMessage = $state('');
|
|
44
|
+
let successTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
43
45
|
let continueCreating = $state(false);
|
|
46
|
+
let duplicating = $state(false);
|
|
47
|
+
|
|
48
|
+
function flashSuccess() {
|
|
49
|
+
successMessage = strings.saveSuccess;
|
|
50
|
+
clearTimeout(successTimeout);
|
|
51
|
+
successTimeout = setTimeout(() => (successMessage = ''), 4000);
|
|
52
|
+
}
|
|
44
53
|
|
|
45
54
|
let record = $state<Record<string, unknown>>(untrack(() => emptyRecord(fields)));
|
|
46
55
|
|
|
47
56
|
const groups = $derived(groupFields(fields));
|
|
48
57
|
const hasFileField = $derived(fields.some((f) => f.type === 'file'));
|
|
49
58
|
|
|
59
|
+
const continueEnabled = $derived(creation.continue?.enabled ?? true);
|
|
60
|
+
const continueLabel = $derived(creation.continue?.label ?? strings.saveAndContinue);
|
|
61
|
+
const continueClass = $derived(creation.continue?.class ?? '');
|
|
62
|
+
|
|
63
|
+
const duplicationEnabled = $derived(creation.duplication?.enabled ?? false);
|
|
64
|
+
const duplicationLabel = $derived(creation.duplication?.label ?? strings.duplicate);
|
|
65
|
+
const duplicationClass = $derived(creation.duplication?.class ?? '');
|
|
66
|
+
|
|
50
67
|
const errorEntries = $derived([
|
|
51
68
|
...((serverError || internalError) ? [['_global', internalError || serverError] as [string, string]] : []),
|
|
52
69
|
...Object.entries(fieldErrors),
|
|
@@ -63,6 +80,15 @@
|
|
|
63
80
|
]}
|
|
64
81
|
/>
|
|
65
82
|
|
|
83
|
+
{#if successMessage}
|
|
84
|
+
<div role="alert" class="alert alert-success">
|
|
85
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
86
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
|
87
|
+
</svg>
|
|
88
|
+
<span class="text-sm">{successMessage}</span>
|
|
89
|
+
</div>
|
|
90
|
+
{/if}
|
|
91
|
+
|
|
66
92
|
{#if errorEntries.length > 0}
|
|
67
93
|
<div role="alert" class="alert alert-error">
|
|
68
94
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
@@ -84,6 +110,8 @@
|
|
|
84
110
|
use:enhance={({ formData, cancel }) => {
|
|
85
111
|
fieldErrors = {};
|
|
86
112
|
internalError = '';
|
|
113
|
+
successMessage = '';
|
|
114
|
+
clearTimeout(successTimeout);
|
|
87
115
|
const errs = validateAll(fields, formData, strings);
|
|
88
116
|
if (Object.keys(errs).length > 0) {
|
|
89
117
|
fieldErrors = errs;
|
|
@@ -97,6 +125,11 @@
|
|
|
97
125
|
record = emptyRecord(fields);
|
|
98
126
|
fieldErrors = {};
|
|
99
127
|
internalError = '';
|
|
128
|
+
flashSuccess();
|
|
129
|
+
} else if (duplicating) {
|
|
130
|
+
fieldErrors = {};
|
|
131
|
+
internalError = '';
|
|
132
|
+
flashSuccess();
|
|
100
133
|
} else {
|
|
101
134
|
onSuccess?.();
|
|
102
135
|
}
|
|
@@ -145,10 +178,40 @@
|
|
|
145
178
|
<Button variant="ghost" onclick={() => onCancel?.()}>
|
|
146
179
|
{strings.cancel}
|
|
147
180
|
</Button>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
181
|
+
{#if duplicationEnabled}
|
|
182
|
+
<Button
|
|
183
|
+
type="submit"
|
|
184
|
+
variant="warning"
|
|
185
|
+
class={duplicationClass}
|
|
186
|
+
onclick={() => {
|
|
187
|
+
continueCreating = false;
|
|
188
|
+
duplicating = true;
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
{duplicationLabel}
|
|
192
|
+
</Button>
|
|
193
|
+
{/if}
|
|
194
|
+
{#if continueEnabled}
|
|
195
|
+
<Button
|
|
196
|
+
type="submit"
|
|
197
|
+
variant="secondary"
|
|
198
|
+
class={continueClass}
|
|
199
|
+
onclick={() => {
|
|
200
|
+
continueCreating = true;
|
|
201
|
+
duplicating = false;
|
|
202
|
+
}}
|
|
203
|
+
>
|
|
204
|
+
{continueLabel}
|
|
205
|
+
</Button>
|
|
206
|
+
{/if}
|
|
207
|
+
<Button
|
|
208
|
+
type="submit"
|
|
209
|
+
variant="primary"
|
|
210
|
+
onclick={() => {
|
|
211
|
+
continueCreating = false;
|
|
212
|
+
duplicating = false;
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
152
215
|
{strings.save}
|
|
153
216
|
</Button>
|
|
154
217
|
</div>
|
package/dist/i18n/en.js
CHANGED
|
@@ -25,8 +25,10 @@ export const en = {
|
|
|
25
25
|
exportExcel: 'Export as Excel',
|
|
26
26
|
save: 'Save',
|
|
27
27
|
saveAndContinue: 'Save and continue',
|
|
28
|
+
duplicate: 'Duplicate',
|
|
28
29
|
cancel: 'Cancel',
|
|
29
30
|
back: 'Back',
|
|
31
|
+
saveSuccess: 'Saved successfully.',
|
|
30
32
|
add: 'Add',
|
|
31
33
|
remove: 'Remove',
|
|
32
34
|
noItems: 'No items added',
|
package/dist/i18n/es.js
CHANGED
|
@@ -25,8 +25,10 @@ export const es = {
|
|
|
25
25
|
exportExcel: 'Exportar a Excel',
|
|
26
26
|
save: 'Guardar',
|
|
27
27
|
saveAndContinue: 'Guardar y continuar',
|
|
28
|
+
duplicate: 'Duplicar',
|
|
28
29
|
cancel: 'Cancelar',
|
|
29
30
|
back: 'Volver',
|
|
31
|
+
saveSuccess: 'Guardado correctamente.',
|
|
30
32
|
add: 'Agregar',
|
|
31
33
|
remove: 'Quitar',
|
|
32
34
|
noItems: 'Sin elementos agregados',
|
package/dist/i18n/types.d.ts
CHANGED
|
@@ -25,8 +25,13 @@ export interface RuneforgeStrings {
|
|
|
25
25
|
exportExcel: string;
|
|
26
26
|
save: string;
|
|
27
27
|
saveAndContinue: string;
|
|
28
|
+
duplicate: string;
|
|
28
29
|
cancel: string;
|
|
29
30
|
back: string;
|
|
31
|
+
/** Create form only: shown after "Save and continue"/"Duplicate" save
|
|
32
|
+
* successfully but keep the user on the form (unlike a plain Save, which
|
|
33
|
+
* navigates away and needs no extra confirmation). */
|
|
34
|
+
saveSuccess: string;
|
|
30
35
|
add: string;
|
|
31
36
|
remove: string;
|
|
32
37
|
noItems: string;
|
|
@@ -74,6 +74,8 @@ export type AttributeMetadata = {
|
|
|
74
74
|
minLength?: number;
|
|
75
75
|
maxLength?: number;
|
|
76
76
|
pattern?: string;
|
|
77
|
+
/** Textarea fields only: the HTML `rows` attribute, controlling height. */
|
|
78
|
+
rows?: number;
|
|
77
79
|
/** Embedded fields only: schema for each item added through the "+" modal. */
|
|
78
80
|
fields?: InterfaceMetadata<any>;
|
|
79
81
|
/** Embedded fields only: short label for an item in the list. */
|
package/dist/types/crud.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface FieldDefinition<T extends object = Record<string, unknown>> {
|
|
|
43
43
|
minLength?: number;
|
|
44
44
|
maxLength?: number;
|
|
45
45
|
pattern?: string;
|
|
46
|
+
/** Textarea fields only: the HTML `rows` attribute, controlling height. */
|
|
47
|
+
rows?: number;
|
|
46
48
|
/** Embedded fields only: sub-field definitions for each item, built from
|
|
47
49
|
* the metadata's `fields`. */
|
|
48
50
|
fields?: FieldDefinition<Record<string, unknown>>[];
|
|
@@ -53,12 +55,33 @@ export interface FieldDefinition<T extends object = Record<string, unknown>> {
|
|
|
53
55
|
/** Fields sharing the same `row` string render side by side. */
|
|
54
56
|
row?: string;
|
|
55
57
|
}
|
|
58
|
+
/** A create-form button whose visibility, label and styling can all be
|
|
59
|
+
* overridden — `enabled` falls back to the button's own default (see
|
|
60
|
+
* `continue`/`duplication` on `ActionConfiguration`), `label` falls back to
|
|
61
|
+
* the matching i18n string, and `class` adds extra classes (e.g. Tailwind
|
|
62
|
+
* utilities) alongside the button's default variant. */
|
|
63
|
+
export interface CreateFormButtonConfiguration {
|
|
64
|
+
enabled?: boolean;
|
|
65
|
+
label?: string;
|
|
66
|
+
class?: string;
|
|
67
|
+
}
|
|
56
68
|
export interface ActionConfiguration<T extends object = Record<string, unknown>> {
|
|
57
69
|
enabled?: boolean;
|
|
58
70
|
label?: string;
|
|
59
71
|
endpoint?: string;
|
|
60
72
|
confirm?: boolean;
|
|
61
73
|
callback?: (items: T[]) => void | Promise<void>;
|
|
74
|
+
/** Create form only: the "Save and continue" button, which submits to the
|
|
75
|
+
* same `endpoint` and then blanks the form so the user can create another
|
|
76
|
+
* record from scratch. Enabled by default — pass `{ enabled: false }` to
|
|
77
|
+
* hide it. */
|
|
78
|
+
continue?: CreateFormButtonConfiguration;
|
|
79
|
+
/** Create form only: shows a "Duplicate" button alongside Save/Cancel that
|
|
80
|
+
* submits to the same `endpoint`, but — unlike "Save and continue", which
|
|
81
|
+
* blanks the form — leaves the just-submitted values in place so the user
|
|
82
|
+
* can tweak a few fields and save again as a new record. Disabled by
|
|
83
|
+
* default — pass `{ enabled: true }` to show it. */
|
|
84
|
+
duplication?: CreateFormButtonConfiguration;
|
|
62
85
|
}
|
|
63
86
|
export interface CustomAction<T extends object = Record<string, unknown>> {
|
|
64
87
|
label: string;
|