runeforge 0.0.57 → 0.0.59
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/README.md +51 -0
- package/dist/components/crud/EmbeddedField.svelte +33 -5
- package/dist/components/crud/EmbeddedField.svelte.d.ts +6 -1
- package/dist/components/crud/Field.svelte +76 -423
- package/dist/components/crud/Field.svelte.d.ts +5 -1
- package/dist/components/crud/GenericCRUD.svelte +4 -4
- package/dist/components/crud/GenericCRUD.svelte.d.ts +4 -1
- package/dist/components/crud/SearchInput.svelte +1 -1
- package/dist/components/crud/SearchInput.svelte.d.ts +1 -1
- package/dist/components/crud/fields/BooleanField.svelte +38 -0
- package/dist/components/crud/fields/BooleanField.svelte.d.ts +33 -0
- package/dist/components/crud/fields/DatetimeField.svelte +250 -0
- package/dist/components/crud/fields/DatetimeField.svelte.d.ts +34 -0
- package/dist/components/crud/fields/DefaultField.svelte +67 -0
- package/dist/components/crud/fields/DefaultField.svelte.d.ts +34 -0
- package/dist/components/crud/fields/FileField.svelte +70 -0
- package/dist/components/crud/fields/FileField.svelte.d.ts +34 -0
- package/dist/components/crud/fields/MultiSelectField.svelte +70 -0
- package/dist/components/crud/fields/MultiSelectField.svelte.d.ts +34 -0
- package/dist/components/crud/fields/SelectField.svelte +67 -0
- package/dist/components/crud/fields/SelectField.svelte.d.ts +34 -0
- package/dist/components/crud/fields/TextareaField.svelte +55 -0
- package/dist/components/crud/fields/TextareaField.svelte.d.ts +34 -0
- package/dist/components/crud/fields/TreeField.svelte +63 -0
- package/dist/components/crud/fields/TreeField.svelte.d.ts +33 -0
- package/dist/components/crud/utils/embedded.d.ts +1 -1
- package/dist/components/crud/utils/grouping.d.ts +1 -1
- package/dist/components/crud/utils/resolution.d.ts +1 -1
- package/dist/components/crud/utils/resolution.js +6 -2
- package/dist/components/crud/utils/validation.d.ts +1 -1
- package/dist/components/crud/utils/validation.js +10 -0
- package/dist/components/crud/views/Create.svelte +37 -30
- package/dist/components/crud/views/Create.svelte.d.ts +2 -1
- package/dist/components/crud/views/Read.svelte +19 -12
- package/dist/components/crud/views/Read.svelte.d.ts +2 -1
- package/dist/components/crud/views/Update.svelte +28 -21
- package/dist/components/crud/views/Update.svelte.d.ts +2 -1
- package/dist/components/crud/views/list/List.svelte +3 -3
- package/dist/components/crud/views/list/List.svelte.d.ts +3 -1
- package/dist/components/crud/views/list/Modals.svelte +1 -1
- package/dist/components/crud/views/list/Modals.svelte.d.ts +1 -1
- package/dist/components/crud/views/list/Table.svelte +3 -6
- package/dist/components/crud/views/list/Table.svelte.d.ts +3 -1
- package/dist/components/crud/views/list/Toolbar.svelte +3 -4
- package/dist/components/crud/views/list/Toolbar.svelte.d.ts +3 -1
- package/dist/components/table/ColumnFilter.svelte +1 -1
- package/dist/components/table/ColumnFilter.svelte.d.ts +1 -1
- package/dist/components/table/PaginatedTable.svelte +1 -1
- package/dist/components/table/PaginatedTable.svelte.d.ts +1 -1
- package/dist/components/table/TableBody.svelte +1 -1
- package/dist/components/table/TableBody.svelte.d.ts +1 -1
- package/dist/components/table/TableHeader.svelte +1 -1
- package/dist/components/table/TableHeader.svelte.d.ts +1 -1
- package/dist/components/table/export.d.ts +1 -1
- package/dist/components/table/state.svelte.d.ts +1 -1
- package/dist/components/table/utils.d.ts +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/types/attribute.d.ts +56 -4
- package/dist/types/{crud.d.ts → crud/actions.d.ts} +0 -123
- package/dist/types/crud/columns.d.ts +23 -0
- package/dist/types/crud/columns.js +1 -0
- package/dist/types/crud/fields.d.ts +48 -0
- package/dist/types/crud/fields.js +1 -0
- package/dist/types/crud/list-config.d.ts +23 -0
- package/dist/types/crud/list-config.js +1 -0
- package/dist/types/crud/reorder.d.ts +45 -0
- package/dist/types/crud/reorder.js +1 -0
- package/package.json +1 -1
- /package/dist/types/{crud.js → crud/actions.js} +0 -0
|
@@ -1,469 +1,122 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
-
import
|
|
3
|
-
import Avatar from '../Avatar.svelte';
|
|
2
|
+
import Button from '../form/Button.svelte';
|
|
4
3
|
import Label from '../form/Label.svelte';
|
|
5
|
-
import Select from '../form/Select.svelte';
|
|
6
|
-
import MultiSelect from '../form/MultiSelect.svelte';
|
|
7
|
-
import Tree from '../form/Tree.svelte';
|
|
8
4
|
import EmbeddedField from './EmbeddedField.svelte';
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
5
|
+
import BooleanField from './fields/BooleanField.svelte';
|
|
6
|
+
import FileField from './fields/FileField.svelte';
|
|
7
|
+
import SelectField from './fields/SelectField.svelte';
|
|
8
|
+
import MultiSelectField from './fields/MultiSelectField.svelte';
|
|
9
|
+
import TreeField from './fields/TreeField.svelte';
|
|
10
|
+
import DatetimeField from './fields/DatetimeField.svelte';
|
|
11
|
+
import TextareaField from './fields/TextareaField.svelte';
|
|
12
|
+
import DefaultField from './fields/DefaultField.svelte';
|
|
13
|
+
import { fieldLabel } from './utils/misc.js';
|
|
14
|
+
import type { FieldDefinition } from '../../types/crud/fields.js';
|
|
18
15
|
|
|
19
16
|
let {
|
|
20
17
|
field,
|
|
21
18
|
record = $bindable({} as Record<string, unknown>),
|
|
19
|
+
parentRecord = undefined,
|
|
22
20
|
error = '',
|
|
23
21
|
readonly = false,
|
|
24
22
|
class: className = ''
|
|
25
23
|
}: {
|
|
26
24
|
field: FieldDefinition<T>;
|
|
27
25
|
record?: Record<string, unknown>;
|
|
26
|
+
/** Set only when this Field renders a sub-field of an `embedded` field
|
|
27
|
+
* — the outer form's own record, passed through to `hidden`/`disabled`/
|
|
28
|
+
* `required`/`dependentOptions` as their second argument. */
|
|
29
|
+
parentRecord?: Record<string, unknown>;
|
|
28
30
|
error?: string;
|
|
29
31
|
readonly?: boolean;
|
|
30
32
|
class?: string;
|
|
31
33
|
} = $props();
|
|
32
34
|
|
|
33
|
-
const datePopId = $props.id();
|
|
34
|
-
const dateAnchorName = `--date-anchor-${datePopId}`;
|
|
35
|
-
let datePopoverEl: HTMLElement | undefined = $state();
|
|
36
|
-
let calendarDateEl: (HTMLElement & { value: string }) | undefined = $state();
|
|
37
|
-
|
|
38
|
-
const name = $derived(readonly ? undefined : field.attribute);
|
|
39
35
|
const labelText = $derived(fieldLabel(field));
|
|
40
|
-
let filePreview = $state<string | null>(null);
|
|
41
|
-
|
|
42
|
-
function onFileChange(e: Event & { currentTarget: HTMLInputElement }) {
|
|
43
|
-
const file = e.currentTarget.files?.[0];
|
|
44
|
-
if (filePreview) URL.revokeObjectURL(filePreview);
|
|
45
|
-
filePreview = file && file.type.startsWith('image/') ? URL.createObjectURL(file) : null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
$effect(() => () => {
|
|
49
|
-
if (filePreview) URL.revokeObjectURL(filePreview);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const saved = $derived(record[field.attribute]);
|
|
53
|
-
const preview = $derived(filePreview ?? (typeof saved === 'string' && saved ? saved : null));
|
|
54
|
-
const avatarInitials = $derived(initials(record.firstName as string, record.lastName as string));
|
|
55
|
-
const displayValue = $derived(saved == null ? '' : String(saved));
|
|
56
|
-
// Only for readonly branches with no other resolution mechanism (select/
|
|
57
|
-
// multiselect/tree already resolve their own label from `options`, and
|
|
58
|
-
// some of their formatters return HTML meant for a table cell, not plain
|
|
59
|
-
// input text). Falls back to `displayValue` so a value with no formatter
|
|
60
|
-
// still renders instead of going blank.
|
|
61
|
-
const formattedValue = $derived(
|
|
62
|
-
readonly && field.formatter ? field.formatter(saved, record) : displayValue
|
|
63
|
-
);
|
|
64
|
-
const selectOptions = $derived(
|
|
65
|
-
field.dependentOptions ? field.dependentOptions(record) : (field.options ?? [])
|
|
66
|
-
);
|
|
67
|
-
const fieldDisabled = $derived(field.disabled ? field.disabled(record) : false);
|
|
68
36
|
const fieldRequired = $derived(
|
|
69
|
-
typeof field.required === 'function' ? field.required(record) : !!field.required
|
|
37
|
+
typeof field.required === 'function' ? field.required(record, parentRecord) : !!field.required
|
|
70
38
|
);
|
|
71
39
|
const fieldHidden = $derived(
|
|
72
|
-
typeof field.hidden === 'function' ? field.hidden(record) : !!field.hidden
|
|
40
|
+
typeof field.hidden === 'function' ? field.hidden(record, parentRecord) : !!field.hidden
|
|
73
41
|
);
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
// cally's `change` event doesn't bubble, so the usual `onchange={...}` prop
|
|
77
|
-
// never fires — Svelte 5 delegates events like `change` to a listener on
|
|
78
|
-
// a shared ancestor, which only ever sees events that bubble up to it.
|
|
79
|
-
// Attaching directly on the element (same fix ColumnFilter's date-range
|
|
80
|
-
// picker already uses) sidesteps delegation entirely.
|
|
81
|
-
$effect(() => {
|
|
82
|
-
if (!calendarDateEl) return;
|
|
83
|
-
function handler() {
|
|
84
|
-
if (fieldDisabled) return;
|
|
85
|
-
setDateTime(calendarDateEl!.value, timePart);
|
|
86
|
-
datePopoverEl?.hidePopover();
|
|
87
|
-
}
|
|
88
|
-
calendarDateEl.addEventListener('change', handler);
|
|
89
|
-
return () => calendarDateEl?.removeEventListener('change', handler);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
function pad(n: number): string {
|
|
93
|
-
return String(n).padStart(2, '0');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ISO (yyyy-mm-dd) <-> display (dd/mm/yyyy) conversion and typing mask.
|
|
97
|
-
function isoDateToDisplay(iso: string): string {
|
|
98
|
-
const [year, month, day] = iso.split('-');
|
|
99
|
-
return `${day}/${month}/${year}`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function displayToIsoDate(display: string): string | null {
|
|
103
|
-
const match = display.trim().match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
|
104
|
-
if (!match) return null;
|
|
105
|
-
const [, dd, mm, yyyy] = match;
|
|
106
|
-
const day = Number(dd);
|
|
107
|
-
const month = Number(mm);
|
|
108
|
-
const year = Number(yyyy);
|
|
109
|
-
// Rejects invalid dates (e.g. 31/02).
|
|
110
|
-
const date = new Date(Date.UTC(year, month - 1, day));
|
|
111
|
-
if (
|
|
112
|
-
date.getUTCFullYear() !== year ||
|
|
113
|
-
date.getUTCMonth() !== month - 1 ||
|
|
114
|
-
date.getUTCDate() !== day
|
|
115
|
-
) {
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
return `${yyyy}-${mm.padStart(2, '0')}-${dd.padStart(2, '0')}`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Inserts "/" as digits are typed: "21" -> "21/", "211" -> "21/1".
|
|
122
|
-
function maskDateDigits(digits: string): string {
|
|
123
|
-
const day = digits.slice(0, 2);
|
|
124
|
-
const month = digits.slice(2, 4);
|
|
125
|
-
const year = digits.slice(4, 8);
|
|
126
|
-
let out = day;
|
|
127
|
-
if (digits.length >= 2) out += '/';
|
|
128
|
-
out += month;
|
|
129
|
-
if (digits.length >= 4) out += '/';
|
|
130
|
-
return out + year;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// `record[field.attribute]` may be empty, a bare "YYYY-MM-DD" (freshly
|
|
134
|
-
// picked, before any time is set) or a full ISO datetime (loaded from an
|
|
135
|
-
// existing instance) — parsed once here so the date button, the time
|
|
136
|
-
// input and cally's own `value` all derive from the same local-time
|
|
137
|
-
// breakdown instead of each re-parsing the raw string their own way.
|
|
138
|
-
const parsedDateTime = $derived.by(() => {
|
|
139
|
-
const raw = record[field.attribute];
|
|
140
|
-
if (raw == null || raw === '') return null;
|
|
141
|
-
const d = new Date(String(raw));
|
|
142
|
-
return isNaN(d.getTime()) ? null : d;
|
|
143
|
-
});
|
|
144
|
-
const datePart = $derived(
|
|
145
|
-
parsedDateTime
|
|
146
|
-
? `${parsedDateTime.getFullYear()}-${pad(parsedDateTime.getMonth() + 1)}-${pad(parsedDateTime.getDate())}`
|
|
147
|
-
: ''
|
|
148
|
-
);
|
|
149
|
-
const timePart = $derived(
|
|
150
|
-
parsedDateTime ? `${pad(parsedDateTime.getHours())}:${pad(parsedDateTime.getMinutes())}` : ''
|
|
42
|
+
const fieldActions = $derived(
|
|
43
|
+
(field.actions ?? []).filter((action) => action.condition?.(record) ?? true)
|
|
151
44
|
);
|
|
152
|
-
|
|
153
|
-
function setDateTime(nextDatePart: string, nextTimePart: string) {
|
|
154
|
-
record[field.attribute] = nextDatePart ? `${nextDatePart}T${nextTimePart || '00:00'}` : '';
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Typed display text for the date part, synced from `datePart`.
|
|
158
|
-
// eslint-disable-next-line svelte/prefer-writable-derived
|
|
159
|
-
let dateTextValue = $state('');
|
|
160
|
-
$effect(() => {
|
|
161
|
-
dateTextValue = datePart ? isoDateToDisplay(datePart) : '';
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
function onDateTextInput(event: Event) {
|
|
165
|
-
const raw = (event.currentTarget as HTMLInputElement).value;
|
|
166
|
-
const prevDigits = dateTextValue.replace(/\D/g, '');
|
|
167
|
-
let digits = raw.replace(/\D/g, '');
|
|
168
|
-
// Drops one extra digit when backspacing over an inserted "/".
|
|
169
|
-
if (raw.length < dateTextValue.length && digits.length === prevDigits.length) {
|
|
170
|
-
digits = digits.slice(0, -1);
|
|
171
|
-
}
|
|
172
|
-
digits = digits.slice(0, 8);
|
|
173
|
-
dateTextValue = maskDateDigits(digits);
|
|
174
|
-
if (!digits) {
|
|
175
|
-
setDateTime('', timePart);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
const parsed = displayToIsoDate(dateTextValue);
|
|
179
|
-
if (parsed) setDateTime(parsed, timePart);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function onDateTextBlur() {
|
|
183
|
-
if (!dateTextValue.trim()) {
|
|
184
|
-
setDateTime('', timePart);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
if (!displayToIsoDate(dateTextValue)) {
|
|
188
|
-
// Discards invalid input.
|
|
189
|
-
dateTextValue = datePart ? isoDateToDisplay(datePart) : '';
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
$effect(() => {
|
|
194
|
-
if (!field.dependentOptions || isMultiValued) return;
|
|
195
|
-
const current = record[field.attribute];
|
|
196
|
-
if (current && !selectOptions.some((o) => o.value === String(current))) {
|
|
197
|
-
record[field.attribute] = '';
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
$effect(() => {
|
|
202
|
-
if (!field.dependentOptions || !isMultiValued) return;
|
|
203
|
-
const current = record[field.attribute];
|
|
204
|
-
if (!Array.isArray(current)) return;
|
|
205
|
-
const validValues = new Set(selectOptions.map((o) => o.value));
|
|
206
|
-
const pruned = current.filter((v) => validValues.has(String(v)));
|
|
207
|
-
if (pruned.length !== current.length) {
|
|
208
|
-
record[field.attribute] = pruned;
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
45
|
</script>
|
|
212
46
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
text={avatarInitials}
|
|
220
|
-
alt={labelText}
|
|
221
|
-
class="w-20 rounded-full"
|
|
222
|
-
textClass="text-xl"
|
|
223
|
-
/>
|
|
224
|
-
</div>
|
|
225
|
-
{/if}
|
|
226
|
-
|
|
47
|
+
<!-- Shared by every field type below, always rendered in the same spot
|
|
48
|
+
(right after an optional type-specific lead-in, e.g. FileField's avatar
|
|
49
|
+
preview) so each type-specific component just brackets its own markup
|
|
50
|
+
with these two instead of re-declaring the label/error chrome itself. -->
|
|
51
|
+
{#snippet labelRow()}
|
|
52
|
+
<div class="flex items-center justify-between gap-2">
|
|
227
53
|
<Label
|
|
228
54
|
text={labelText}
|
|
229
55
|
for={field.attribute}
|
|
230
56
|
capitalize={true}
|
|
231
57
|
required={fieldRequired && !readonly}
|
|
232
58
|
/>
|
|
59
|
+
{#if !readonly && fieldActions.length > 0}
|
|
60
|
+
<div class="flex items-center gap-1">
|
|
61
|
+
{#each fieldActions as action (action.label)}
|
|
62
|
+
<Button
|
|
63
|
+
type="button"
|
|
64
|
+
variant="ghost"
|
|
65
|
+
class={['btn-xs', action.class]}
|
|
66
|
+
title={action.label}
|
|
67
|
+
onclick={() =>
|
|
68
|
+
action.run(record[field.attribute], record, (attribute, value) => {
|
|
69
|
+
record[attribute] = value;
|
|
70
|
+
})}
|
|
71
|
+
>
|
|
72
|
+
{#if action.icon}
|
|
73
|
+
{@const Icon = action.icon}
|
|
74
|
+
<Icon class="size-4" />
|
|
75
|
+
{/if}
|
|
76
|
+
{action.label}
|
|
77
|
+
</Button>
|
|
78
|
+
{/each}
|
|
79
|
+
</div>
|
|
80
|
+
{/if}
|
|
81
|
+
</div>
|
|
82
|
+
{/snippet}
|
|
83
|
+
|
|
84
|
+
{#snippet errorRow()}
|
|
85
|
+
{#if error}
|
|
86
|
+
<span class="text-error text-xs">{error}</span>
|
|
87
|
+
{/if}
|
|
88
|
+
{/snippet}
|
|
233
89
|
|
|
90
|
+
{#if !fieldHidden}
|
|
91
|
+
<div class="flex flex-col gap-1 {className}">
|
|
234
92
|
{#if field.type === 'boolean'}
|
|
235
|
-
<
|
|
236
|
-
type="checkbox"
|
|
237
|
-
id={field.attribute}
|
|
238
|
-
{name}
|
|
239
|
-
class="toggle toggle-primary"
|
|
240
|
-
checked={!!saved}
|
|
241
|
-
disabled={readonly || fieldDisabled}
|
|
242
|
-
onchange={(e) => {
|
|
243
|
-
record[field.attribute] = (e.currentTarget as HTMLInputElement).checked;
|
|
244
|
-
}}
|
|
245
|
-
/>
|
|
93
|
+
<BooleanField {field} bind:record {parentRecord} {readonly} {labelRow} {errorRow} />
|
|
246
94
|
{:else if field.type === 'file'}
|
|
247
|
-
{
|
|
248
|
-
<input
|
|
249
|
-
type="file"
|
|
250
|
-
id={field.attribute}
|
|
251
|
-
{name}
|
|
252
|
-
class="file-input file-input-bordered w-full"
|
|
253
|
-
class:file-input-error={!!error}
|
|
254
|
-
disabled={fieldDisabled}
|
|
255
|
-
onchange={onFileChange}
|
|
256
|
-
/>
|
|
257
|
-
{/if}
|
|
258
|
-
<!-- read-only file value is shown as the avatar above -->
|
|
95
|
+
<FileField {field} bind:record {parentRecord} {error} {readonly} {labelRow} {errorRow} />
|
|
259
96
|
{:else if field.type === 'select'}
|
|
260
|
-
{
|
|
261
|
-
<input
|
|
262
|
-
type="text"
|
|
263
|
-
id={field.attribute}
|
|
264
|
-
class="input input-bordered w-full"
|
|
265
|
-
value={selectOptions.find((o) => o.value === String(saved))?.label ?? displayValue}
|
|
266
|
-
disabled
|
|
267
|
-
/>
|
|
268
|
-
{:else}
|
|
269
|
-
<Select
|
|
270
|
-
id={field.attribute}
|
|
271
|
-
name={field.attribute}
|
|
272
|
-
bind:value={record[field.attribute] as string}
|
|
273
|
-
options={selectOptions}
|
|
274
|
-
search={field.search}
|
|
275
|
-
placeholder={field.placeholder}
|
|
276
|
-
disabled={fieldDisabled}
|
|
277
|
-
{error}
|
|
278
|
-
/>
|
|
279
|
-
{/if}
|
|
97
|
+
<SelectField {field} bind:record {parentRecord} {error} {readonly} {labelRow} {errorRow} />
|
|
280
98
|
{:else if field.type === 'datetime'}
|
|
281
|
-
{
|
|
282
|
-
<input
|
|
283
|
-
type="text"
|
|
284
|
-
id={field.attribute}
|
|
285
|
-
class="input input-bordered w-full"
|
|
286
|
-
value={formattedValue}
|
|
287
|
-
disabled
|
|
288
|
-
/>
|
|
289
|
-
{:else}
|
|
290
|
-
<input
|
|
291
|
-
type="hidden"
|
|
292
|
-
{name}
|
|
293
|
-
value={String(record[field.attribute] ?? '')}
|
|
294
|
-
disabled={fieldDisabled}
|
|
295
|
-
/>
|
|
296
|
-
<div class="flex gap-2">
|
|
297
|
-
<div class="relative flex-1" style="anchor-name:{dateAnchorName}">
|
|
298
|
-
<input
|
|
299
|
-
type="text"
|
|
300
|
-
id={field.attribute}
|
|
301
|
-
placeholder={field.placeholder ?? 'dd/mm/aaaa'}
|
|
302
|
-
value={dateTextValue}
|
|
303
|
-
oninput={onDateTextInput}
|
|
304
|
-
onblur={onDateTextBlur}
|
|
305
|
-
autocomplete="off"
|
|
306
|
-
disabled={fieldDisabled}
|
|
307
|
-
class="input input-bordered w-full pr-9"
|
|
308
|
-
class:input-error={!!error}
|
|
309
|
-
/>
|
|
310
|
-
<button
|
|
311
|
-
type="button"
|
|
312
|
-
popovertarget={datePopId}
|
|
313
|
-
aria-label={strings.chooseDate}
|
|
314
|
-
disabled={fieldDisabled}
|
|
315
|
-
class="absolute inset-y-0 right-0 flex items-center px-2.5 text-base-content/50 hover:text-base-content disabled:pointer-events-none disabled:opacity-40"
|
|
316
|
-
>
|
|
317
|
-
<svg
|
|
318
|
-
class="size-4"
|
|
319
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
320
|
-
viewBox="0 0 24 24"
|
|
321
|
-
fill="none"
|
|
322
|
-
stroke="currentColor"
|
|
323
|
-
stroke-width="2"
|
|
324
|
-
stroke-linecap="round"
|
|
325
|
-
stroke-linejoin="round"
|
|
326
|
-
>
|
|
327
|
-
<rect x="3" y="4" width="18" height="18" rx="2" />
|
|
328
|
-
<path d="M16 2v4M8 2v4M3 10h18" />
|
|
329
|
-
</svg>
|
|
330
|
-
</button>
|
|
331
|
-
</div>
|
|
332
|
-
<input
|
|
333
|
-
type="time"
|
|
334
|
-
class="input input-bordered w-32 shrink-0"
|
|
335
|
-
value={timePart}
|
|
336
|
-
disabled={fieldDisabled || !record[field.attribute]}
|
|
337
|
-
onchange={(e) => setDateTime(datePart, (e.currentTarget as HTMLInputElement).value)}
|
|
338
|
-
/>
|
|
339
|
-
</div>
|
|
340
|
-
<div
|
|
341
|
-
popover="auto"
|
|
342
|
-
id={datePopId}
|
|
343
|
-
bind:this={datePopoverEl}
|
|
344
|
-
style="position-anchor:{dateAnchorName}; position-try-fallbacks:flip-block;"
|
|
345
|
-
class="dropdown mt-1 rounded-box border border-base-content/10 bg-base-100 p-2 shadow-lg"
|
|
346
|
-
>
|
|
347
|
-
<calendar-date class="cally" bind:this={calendarDateEl} value={datePart}>
|
|
348
|
-
<svg
|
|
349
|
-
aria-label={strings.previous}
|
|
350
|
-
class="fill-current size-4"
|
|
351
|
-
{...{ slot: 'previous' }}
|
|
352
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
353
|
-
viewBox="0 0 24 24"><path fill="currentColor" d="M15.75 19.5 8.25 12l7.5-7.5" /></svg
|
|
354
|
-
>
|
|
355
|
-
<svg
|
|
356
|
-
aria-label={strings.next}
|
|
357
|
-
class="fill-current size-4"
|
|
358
|
-
{...{ slot: 'next' }}
|
|
359
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
360
|
-
viewBox="0 0 24 24"><path fill="currentColor" d="m8.25 4.5 7.5 7.5-7.5 7.5" /></svg
|
|
361
|
-
>
|
|
362
|
-
<calendar-month></calendar-month>
|
|
363
|
-
</calendar-date>
|
|
364
|
-
</div>
|
|
365
|
-
{/if}
|
|
99
|
+
<DatetimeField {field} bind:record {parentRecord} {error} {readonly} {labelRow} {errorRow} />
|
|
366
100
|
{:else if field.type === 'textarea'}
|
|
367
|
-
{
|
|
368
|
-
<textarea
|
|
369
|
-
id={field.attribute}
|
|
370
|
-
rows={field.rows}
|
|
371
|
-
class="textarea textarea-bordered bg-base-100 w-full"
|
|
372
|
-
value={formattedValue}
|
|
373
|
-
disabled
|
|
374
|
-
></textarea>
|
|
375
|
-
{:else}
|
|
376
|
-
<textarea
|
|
377
|
-
id={field.attribute}
|
|
378
|
-
{name}
|
|
379
|
-
rows={field.rows}
|
|
380
|
-
placeholder={field.placeholder ?? ''}
|
|
381
|
-
bind:value={record[field.attribute]}
|
|
382
|
-
class="textarea textarea-bordered bg-base-100 w-full"
|
|
383
|
-
class:textarea-error={!!error}
|
|
384
|
-
disabled={fieldDisabled}
|
|
385
|
-
></textarea>
|
|
386
|
-
{/if}
|
|
101
|
+
<TextareaField {field} bind:record {parentRecord} {error} {readonly} {labelRow} {errorRow} />
|
|
387
102
|
{:else if field.type === 'embedded'}
|
|
103
|
+
{@render labelRow()}
|
|
388
104
|
<EmbeddedField {field} bind:record {readonly} />
|
|
105
|
+
{@render errorRow()}
|
|
389
106
|
{:else if field.type === 'multiselect'}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
/>
|
|
400
|
-
{:else}
|
|
401
|
-
<MultiSelect
|
|
402
|
-
id={field.attribute}
|
|
403
|
-
name={field.attribute}
|
|
404
|
-
bind:value={record[field.attribute] as string[]}
|
|
405
|
-
options={selectOptions}
|
|
406
|
-
search={field.search}
|
|
407
|
-
placeholder={field.placeholder}
|
|
408
|
-
disabled={fieldDisabled}
|
|
409
|
-
{error}
|
|
410
|
-
/>
|
|
411
|
-
{/if}
|
|
107
|
+
<MultiSelectField
|
|
108
|
+
{field}
|
|
109
|
+
bind:record
|
|
110
|
+
{parentRecord}
|
|
111
|
+
{error}
|
|
112
|
+
{readonly}
|
|
113
|
+
{labelRow}
|
|
114
|
+
{errorRow}
|
|
115
|
+
/>
|
|
412
116
|
{:else if field.type === 'tree'}
|
|
413
|
-
{
|
|
414
|
-
<input
|
|
415
|
-
type="text"
|
|
416
|
-
id={field.attribute}
|
|
417
|
-
class="input input-bordered w-full"
|
|
418
|
-
value={(Array.isArray(saved) ? saved : [])
|
|
419
|
-
.map((v) => selectOptions.find((o) => o.value === String(v))?.label ?? String(v))
|
|
420
|
-
.join(', ')}
|
|
421
|
-
disabled
|
|
422
|
-
/>
|
|
423
|
-
{:else}
|
|
424
|
-
<Tree
|
|
425
|
-
name={field.attribute}
|
|
426
|
-
bind:value={record[field.attribute] as string[]}
|
|
427
|
-
options={selectOptions}
|
|
428
|
-
disabled={fieldDisabled}
|
|
429
|
-
defaultExpanded={field.defaultExpanded ?? true}
|
|
430
|
-
/>
|
|
431
|
-
{/if}
|
|
432
|
-
{:else if readonly}
|
|
433
|
-
{#if field.formatter}
|
|
434
|
-
<div
|
|
435
|
-
id={field.attribute}
|
|
436
|
-
class="input input-bordered w-full bg-base-200 border-base-200 text-base-content/40 shadow-none cursor-not-allowed"
|
|
437
|
-
>
|
|
438
|
-
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
439
|
-
{@html formattedValue}
|
|
440
|
-
</div>
|
|
441
|
-
{:else}
|
|
442
|
-
<input
|
|
443
|
-
type={field.type ?? 'text'}
|
|
444
|
-
id={field.attribute}
|
|
445
|
-
class="input input-bordered w-full"
|
|
446
|
-
value={formattedValue}
|
|
447
|
-
disabled
|
|
448
|
-
/>
|
|
449
|
-
{/if}
|
|
117
|
+
<TreeField {field} bind:record {parentRecord} {readonly} {labelRow} {errorRow} />
|
|
450
118
|
{:else}
|
|
451
|
-
<
|
|
452
|
-
type={field.type ?? 'text'}
|
|
453
|
-
id={field.attribute}
|
|
454
|
-
{name}
|
|
455
|
-
placeholder={field.placeholder ?? ''}
|
|
456
|
-
bind:value={record[field.attribute]}
|
|
457
|
-
autocomplete={field.autocomplete}
|
|
458
|
-
step={field.type === 'number' ? 'any' : undefined}
|
|
459
|
-
class="input input-bordered w-full"
|
|
460
|
-
class:input-error={!!error}
|
|
461
|
-
disabled={fieldDisabled}
|
|
462
|
-
/>
|
|
463
|
-
{/if}
|
|
464
|
-
|
|
465
|
-
{#if error}
|
|
466
|
-
<span class="text-error text-xs">{error}</span>
|
|
119
|
+
<DefaultField {field} bind:record {parentRecord} {error} {readonly} {labelRow} {errorRow} />
|
|
467
120
|
{/if}
|
|
468
121
|
</div>
|
|
469
122
|
{/if}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import type { FieldDefinition } from '../../types/crud.js';
|
|
1
|
+
import type { FieldDefinition } from '../../types/crud/fields.js';
|
|
2
2
|
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
3
3
|
props: {
|
|
4
4
|
field: FieldDefinition<T>;
|
|
5
5
|
record?: Record<string, unknown>;
|
|
6
|
+
/** Set only when this Field renders a sub-field of an `embedded` field
|
|
7
|
+
* — the outer form's own record, passed through to `hidden`/`disabled`/
|
|
8
|
+
* `required`/`dependentOptions` as their second argument. */
|
|
9
|
+
parentRecord?: Record<string, unknown>;
|
|
6
10
|
error?: string;
|
|
7
11
|
readonly?: boolean;
|
|
8
12
|
class?: string;
|
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
import type { AttributeMetadata } from '../../types/attribute.js';
|
|
18
18
|
import type {
|
|
19
19
|
ActionConfiguration,
|
|
20
|
-
ColumnDefinition,
|
|
21
20
|
CustomAction,
|
|
22
|
-
FieldDefinition,
|
|
23
21
|
ListActions,
|
|
24
|
-
ListConfig,
|
|
25
22
|
ViewBasedCustomBulkAction
|
|
26
|
-
} from '../../types/crud.js';
|
|
23
|
+
} from '../../types/crud/actions.js';
|
|
24
|
+
import type { ColumnDefinition } from '../../types/crud/columns.js';
|
|
25
|
+
import type { FieldDefinition } from '../../types/crud/fields.js';
|
|
26
|
+
import type { ListConfig } from '../../types/crud/list-config.js';
|
|
27
27
|
import type {
|
|
28
28
|
FilterSnapshot,
|
|
29
29
|
PaginatedEnvelope,
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { AttributeMetadata } from '../../types/attribute.js';
|
|
2
|
-
import type { ActionConfiguration,
|
|
2
|
+
import type { ActionConfiguration, ListActions } from '../../types/crud/actions.js';
|
|
3
|
+
import type { ColumnDefinition } from '../../types/crud/columns.js';
|
|
4
|
+
import type { FieldDefinition } from '../../types/crud/fields.js';
|
|
5
|
+
import type { ListConfig } from '../../types/crud/list-config.js';
|
|
3
6
|
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
4
7
|
props: {
|
|
5
8
|
data?: Record<string, unknown>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { page } from '$app/state';
|
|
3
3
|
import { goto } from '$app/navigation';
|
|
4
4
|
import { getStrings } from '../../i18n/context.js';
|
|
5
|
-
import type { SearchConfiguration } from '../../types/crud.js';
|
|
5
|
+
import type { SearchConfiguration } from '../../types/crud/list-config.js';
|
|
6
6
|
|
|
7
7
|
const strings = getStrings();
|
|
8
8
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { FieldDefinition } from '../../../types/crud/fields.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
field,
|
|
7
|
+
record = $bindable({} as Record<string, unknown>),
|
|
8
|
+
parentRecord = undefined,
|
|
9
|
+
readonly = false,
|
|
10
|
+
labelRow,
|
|
11
|
+
errorRow
|
|
12
|
+
}: {
|
|
13
|
+
field: FieldDefinition<T>;
|
|
14
|
+
record?: Record<string, unknown>;
|
|
15
|
+
parentRecord?: Record<string, unknown>;
|
|
16
|
+
readonly?: boolean;
|
|
17
|
+
labelRow: Snippet;
|
|
18
|
+
errorRow: Snippet;
|
|
19
|
+
} = $props();
|
|
20
|
+
|
|
21
|
+
const name = $derived(readonly ? undefined : field.attribute);
|
|
22
|
+
const saved = $derived(record[field.attribute]);
|
|
23
|
+
const fieldDisabled = $derived(field.disabled ? field.disabled(record, parentRecord) : false);
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
{@render labelRow()}
|
|
27
|
+
<input
|
|
28
|
+
type="checkbox"
|
|
29
|
+
id={field.attribute}
|
|
30
|
+
{name}
|
|
31
|
+
class="toggle toggle-primary"
|
|
32
|
+
checked={!!saved}
|
|
33
|
+
disabled={readonly || fieldDisabled}
|
|
34
|
+
onchange={(e) => {
|
|
35
|
+
record[field.attribute] = (e.currentTarget as HTMLInputElement).checked;
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
{@render errorRow()}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { FieldDefinition } from '../../../types/crud/fields.js';
|
|
3
|
+
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
4
|
+
props: {
|
|
5
|
+
field: FieldDefinition<T>;
|
|
6
|
+
record?: Record<string, unknown>;
|
|
7
|
+
parentRecord?: Record<string, unknown>;
|
|
8
|
+
readonly?: boolean;
|
|
9
|
+
labelRow: Snippet;
|
|
10
|
+
errorRow: Snippet;
|
|
11
|
+
};
|
|
12
|
+
exports: {};
|
|
13
|
+
bindings: "record";
|
|
14
|
+
slots: {};
|
|
15
|
+
events: {};
|
|
16
|
+
};
|
|
17
|
+
declare class __sveltets_Render<T extends object = Record<string, unknown>> {
|
|
18
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
19
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
20
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
21
|
+
bindings(): "record";
|
|
22
|
+
exports(): {};
|
|
23
|
+
}
|
|
24
|
+
interface $$IsomorphicComponent {
|
|
25
|
+
new <T extends object = Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
26
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
27
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
28
|
+
<T extends object = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
29
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
30
|
+
}
|
|
31
|
+
declare const BooleanField: $$IsomorphicComponent;
|
|
32
|
+
type BooleanField<T extends object = Record<string, unknown>> = InstanceType<typeof BooleanField<T>>;
|
|
33
|
+
export default BooleanField;
|