runeforge 0.0.44 → 0.0.46
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 +73 -19
- package/dist/components/crud/utils/resolution.js +1 -0
- package/dist/components/crud/views/Create.svelte +46 -4
- package/dist/i18n/en.js +1 -0
- package/dist/i18n/es.js +1 -0
- package/dist/i18n/types.d.ts +1 -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"
|
|
@@ -295,13 +339,23 @@
|
|
|
295
339
|
/>
|
|
296
340
|
{/if}
|
|
297
341
|
{:else if readonly}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
342
|
+
{#if field.formatter}
|
|
343
|
+
<div
|
|
344
|
+
id={field.attribute}
|
|
345
|
+
class="input input-bordered w-full bg-base-200 border-base-200 text-base-content/40 shadow-none cursor-not-allowed"
|
|
346
|
+
>
|
|
347
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
348
|
+
{@html formattedValue}
|
|
349
|
+
</div>
|
|
350
|
+
{:else}
|
|
351
|
+
<input
|
|
352
|
+
type={field.type ?? 'text'}
|
|
353
|
+
id={field.attribute}
|
|
354
|
+
class="input input-bordered w-full"
|
|
355
|
+
value={formattedValue}
|
|
356
|
+
disabled
|
|
357
|
+
/>
|
|
358
|
+
{/if}
|
|
305
359
|
{:else}
|
|
306
360
|
<input
|
|
307
361
|
type={field.type ?? 'text'}
|
|
@@ -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,
|
|
@@ -41,12 +41,21 @@
|
|
|
41
41
|
let fieldErrors = $state<Record<string, string>>({});
|
|
42
42
|
let internalError = $state('');
|
|
43
43
|
let continueCreating = $state(false);
|
|
44
|
+
let duplicating = $state(false);
|
|
44
45
|
|
|
45
46
|
let record = $state<Record<string, unknown>>(untrack(() => emptyRecord(fields)));
|
|
46
47
|
|
|
47
48
|
const groups = $derived(groupFields(fields));
|
|
48
49
|
const hasFileField = $derived(fields.some((f) => f.type === 'file'));
|
|
49
50
|
|
|
51
|
+
const continueEnabled = $derived(creation.continue?.enabled ?? true);
|
|
52
|
+
const continueLabel = $derived(creation.continue?.label ?? strings.saveAndContinue);
|
|
53
|
+
const continueClass = $derived(creation.continue?.class ?? '');
|
|
54
|
+
|
|
55
|
+
const duplicationEnabled = $derived(creation.duplication?.enabled ?? false);
|
|
56
|
+
const duplicationLabel = $derived(creation.duplication?.label ?? strings.duplicate);
|
|
57
|
+
const duplicationClass = $derived(creation.duplication?.class ?? '');
|
|
58
|
+
|
|
50
59
|
const errorEntries = $derived([
|
|
51
60
|
...((serverError || internalError) ? [['_global', internalError || serverError] as [string, string]] : []),
|
|
52
61
|
...Object.entries(fieldErrors),
|
|
@@ -97,6 +106,9 @@
|
|
|
97
106
|
record = emptyRecord(fields);
|
|
98
107
|
fieldErrors = {};
|
|
99
108
|
internalError = '';
|
|
109
|
+
} else if (duplicating) {
|
|
110
|
+
fieldErrors = {};
|
|
111
|
+
internalError = '';
|
|
100
112
|
} else {
|
|
101
113
|
onSuccess?.();
|
|
102
114
|
}
|
|
@@ -145,10 +157,40 @@
|
|
|
145
157
|
<Button variant="ghost" onclick={() => onCancel?.()}>
|
|
146
158
|
{strings.cancel}
|
|
147
159
|
</Button>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
160
|
+
{#if duplicationEnabled}
|
|
161
|
+
<Button
|
|
162
|
+
type="submit"
|
|
163
|
+
variant="warning"
|
|
164
|
+
class={duplicationClass}
|
|
165
|
+
onclick={() => {
|
|
166
|
+
continueCreating = false;
|
|
167
|
+
duplicating = true;
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
{duplicationLabel}
|
|
171
|
+
</Button>
|
|
172
|
+
{/if}
|
|
173
|
+
{#if continueEnabled}
|
|
174
|
+
<Button
|
|
175
|
+
type="submit"
|
|
176
|
+
variant="secondary"
|
|
177
|
+
class={continueClass}
|
|
178
|
+
onclick={() => {
|
|
179
|
+
continueCreating = true;
|
|
180
|
+
duplicating = false;
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
{continueLabel}
|
|
184
|
+
</Button>
|
|
185
|
+
{/if}
|
|
186
|
+
<Button
|
|
187
|
+
type="submit"
|
|
188
|
+
variant="primary"
|
|
189
|
+
onclick={() => {
|
|
190
|
+
continueCreating = false;
|
|
191
|
+
duplicating = false;
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
152
194
|
{strings.save}
|
|
153
195
|
</Button>
|
|
154
196
|
</div>
|
package/dist/i18n/en.js
CHANGED
package/dist/i18n/es.js
CHANGED
package/dist/i18n/types.d.ts
CHANGED
|
@@ -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;
|