runeforge 0.0.23 → 0.0.24
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/Modal.svelte +26 -1
- package/dist/components/Modal.svelte.d.ts +9 -0
- package/dist/components/crud/EmbeddedField.svelte +144 -0
- package/dist/components/crud/EmbeddedField.svelte.d.ts +29 -0
- package/dist/components/crud/Field.svelte +3 -0
- package/dist/components/crud/utils/embedded.d.ts +4 -0
- package/dist/components/crud/utils/embedded.js +40 -0
- package/dist/components/crud/utils/resolution.js +5 -1
- package/dist/components/crud/utils/validation.js +13 -0
- package/dist/components/crud/views/Create.svelte +3 -13
- package/dist/components/crud/views/Update.svelte +3 -4
- package/dist/i18n/en.js +3 -0
- package/dist/i18n/es.js +3 -0
- package/dist/i18n/types.d.ts +3 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/types/attribute.d.ts +9 -1
- package/dist/types/attribute.js +1 -0
- package/dist/types/crud.d.ts +5 -0
- package/package.json +1 -1
|
@@ -6,15 +6,40 @@
|
|
|
6
6
|
title = '',
|
|
7
7
|
onClose,
|
|
8
8
|
children,
|
|
9
|
+
class: additionalClass,
|
|
10
|
+
width,
|
|
11
|
+
maxWidth,
|
|
12
|
+
height,
|
|
13
|
+
maxHeight,
|
|
9
14
|
}: {
|
|
10
15
|
title?: string;
|
|
11
16
|
onClose?: () => void;
|
|
12
17
|
children: Snippet;
|
|
18
|
+
/** Extra classes merged onto the modal box, e.g. Tailwind size utilities
|
|
19
|
+
* like `max-w-4xl` or `w-11/12`. */
|
|
20
|
+
class?: string;
|
|
21
|
+
/** Explicit size overrides (any valid CSS length, e.g. '600px', '90vw').
|
|
22
|
+
* Applied as inline styles, so they take priority over `class` utilities. */
|
|
23
|
+
width?: string;
|
|
24
|
+
maxWidth?: string;
|
|
25
|
+
height?: string;
|
|
26
|
+
maxHeight?: string;
|
|
13
27
|
} = $props();
|
|
28
|
+
|
|
29
|
+
const boxStyle = $derived(
|
|
30
|
+
[
|
|
31
|
+
width ? `width:${width}` : '',
|
|
32
|
+
maxWidth ? `max-width:${maxWidth}` : '',
|
|
33
|
+
height ? `height:${height}` : '',
|
|
34
|
+
maxHeight ? `max-height:${maxHeight}` : '',
|
|
35
|
+
]
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.join(';')
|
|
38
|
+
);
|
|
14
39
|
</script>
|
|
15
40
|
|
|
16
41
|
<dialog class="modal" open>
|
|
17
|
-
<div class=
|
|
42
|
+
<div class={['modal-box', additionalClass]} style={boxStyle}>
|
|
18
43
|
<div class="flex items-center justify-between gap-4">
|
|
19
44
|
<h3 class="text-lg font-bold">{title}</h3>
|
|
20
45
|
{#if onClose}
|
|
@@ -3,6 +3,15 @@ type $$ComponentProps = {
|
|
|
3
3
|
title?: string;
|
|
4
4
|
onClose?: () => void;
|
|
5
5
|
children: Snippet;
|
|
6
|
+
/** Extra classes merged onto the modal box, e.g. Tailwind size utilities
|
|
7
|
+
* like `max-w-4xl` or `w-11/12`. */
|
|
8
|
+
class?: string;
|
|
9
|
+
/** Explicit size overrides (any valid CSS length, e.g. '600px', '90vw').
|
|
10
|
+
* Applied as inline styles, so they take priority over `class` utilities. */
|
|
11
|
+
width?: string;
|
|
12
|
+
maxWidth?: string;
|
|
13
|
+
height?: string;
|
|
14
|
+
maxHeight?: string;
|
|
6
15
|
};
|
|
7
16
|
declare const Modal: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
17
|
type Modal = ReturnType<typeof Modal>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
+
import Field from './Field.svelte';
|
|
3
|
+
import Button from '../form/Button.svelte';
|
|
4
|
+
import Modal from '../Modal.svelte';
|
|
5
|
+
import { emptyRecord, seedField, defaultItemLabel } from './utils/embedded.js';
|
|
6
|
+
import { fieldLabel } from './utils/misc.js';
|
|
7
|
+
import { validateAll } from './utils/validation.js';
|
|
8
|
+
import type { FieldDefinition } from '../../types/crud.js';
|
|
9
|
+
import { getStrings } from '../../i18n/context.js';
|
|
10
|
+
|
|
11
|
+
const strings = getStrings();
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
field,
|
|
15
|
+
record = $bindable({} as Record<string, unknown>),
|
|
16
|
+
readonly = false
|
|
17
|
+
}: {
|
|
18
|
+
field: FieldDefinition<T>;
|
|
19
|
+
record?: Record<string, unknown>;
|
|
20
|
+
readonly?: boolean;
|
|
21
|
+
} = $props();
|
|
22
|
+
|
|
23
|
+
const subFields = $derived(field.fields ?? []);
|
|
24
|
+
const items = $derived((record[field.attribute] as Record<string, unknown>[] | undefined) ?? []);
|
|
25
|
+
|
|
26
|
+
let modalOpen = $state(false);
|
|
27
|
+
// null while adding a new item; the item's index while editing an existing
|
|
28
|
+
// one, so saveItem knows whether to append or replace in place.
|
|
29
|
+
let editingIndex = $state<number | null>(null);
|
|
30
|
+
let draft = $state<Record<string, unknown>>({});
|
|
31
|
+
let draftErrors = $state<Record<string, string>>({});
|
|
32
|
+
|
|
33
|
+
function openCreateModal() {
|
|
34
|
+
editingIndex = null;
|
|
35
|
+
draft = emptyRecord(subFields);
|
|
36
|
+
draftErrors = {};
|
|
37
|
+
modalOpen = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function openEditModal(index: number) {
|
|
41
|
+
const item = items[index];
|
|
42
|
+
editingIndex = index;
|
|
43
|
+
draft = Object.fromEntries(
|
|
44
|
+
subFields.map((f) => [f.attribute, seedField(f, f.seed ? f.seed(item) : item[f.attribute])])
|
|
45
|
+
);
|
|
46
|
+
draftErrors = {};
|
|
47
|
+
modalOpen = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function closeModal() {
|
|
51
|
+
modalOpen = false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function coerce(f: FieldDefinition, raw: unknown): unknown {
|
|
55
|
+
if (f.type === 'number') return raw === '' || raw == null ? null : Number(raw);
|
|
56
|
+
return raw;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function saveItem() {
|
|
60
|
+
const fd = new FormData();
|
|
61
|
+
for (const f of subFields) fd.set(f.attribute, String(draft[f.attribute] ?? ''));
|
|
62
|
+
const errs = validateAll(subFields, fd, strings);
|
|
63
|
+
if (Object.keys(errs).length > 0) {
|
|
64
|
+
draftErrors = errs;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const item = Object.fromEntries(
|
|
68
|
+
subFields.map((f) => [f.attribute, coerce(f, draft[f.attribute])])
|
|
69
|
+
);
|
|
70
|
+
record[field.attribute] =
|
|
71
|
+
editingIndex == null
|
|
72
|
+
? [...items, item]
|
|
73
|
+
: items.map((existing, i) => (i === editingIndex ? item : existing));
|
|
74
|
+
modalOpen = false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function removeItem(index: number) {
|
|
78
|
+
record[field.attribute] = items.filter((_, i) => i !== index);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function itemLabel(item: Record<string, unknown>): string {
|
|
82
|
+
return field.itemLabel ? field.itemLabel(item) : defaultItemLabel(subFields, item);
|
|
83
|
+
}
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<div class="flex flex-col gap-2">
|
|
87
|
+
{#if !readonly}
|
|
88
|
+
<input type="hidden" name={field.attribute} value={JSON.stringify(items)} />
|
|
89
|
+
{/if}
|
|
90
|
+
|
|
91
|
+
{#if items.length === 0}
|
|
92
|
+
<p class="text-sm text-base-content/50">{strings.noItems}</p>
|
|
93
|
+
{:else}
|
|
94
|
+
<ul class="flex flex-col gap-1">
|
|
95
|
+
{#each items as item, i (i)}
|
|
96
|
+
<li
|
|
97
|
+
class="flex items-center justify-between gap-2 rounded-box border border-base-300 px-3 py-2 text-sm"
|
|
98
|
+
>
|
|
99
|
+
{#if readonly}
|
|
100
|
+
<span>{itemLabel(item)}</span>
|
|
101
|
+
{:else}
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
class="flex-1 text-left hover:underline"
|
|
105
|
+
onclick={() => openEditModal(i)}
|
|
106
|
+
>
|
|
107
|
+
{itemLabel(item)}
|
|
108
|
+
</button>
|
|
109
|
+
<Button
|
|
110
|
+
variant="ghost"
|
|
111
|
+
class="btn-xs btn-circle"
|
|
112
|
+
aria-label={strings.remove}
|
|
113
|
+
onclick={() => removeItem(i)}
|
|
114
|
+
>
|
|
115
|
+
✕
|
|
116
|
+
</Button>
|
|
117
|
+
{/if}
|
|
118
|
+
</li>
|
|
119
|
+
{/each}
|
|
120
|
+
</ul>
|
|
121
|
+
{/if}
|
|
122
|
+
|
|
123
|
+
{#if !readonly}
|
|
124
|
+
<Button variant="outline" class="btn-sm self-start" onclick={openCreateModal}>
|
|
125
|
+
+ {strings.add}
|
|
126
|
+
</Button>
|
|
127
|
+
{/if}
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{#if modalOpen}
|
|
131
|
+
<Modal title={fieldLabel(field)} onClose={closeModal}>
|
|
132
|
+
<div class="flex flex-col gap-4">
|
|
133
|
+
{#each subFields as f (f.attribute)}
|
|
134
|
+
<Field field={f} bind:record={draft} error={draftErrors[f.attribute] ?? ''} />
|
|
135
|
+
{/each}
|
|
136
|
+
<div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
|
|
137
|
+
<Button variant="ghost" onclick={closeModal}>{strings.cancel}</Button>
|
|
138
|
+
<Button variant="primary" onclick={saveItem}>
|
|
139
|
+
{editingIndex == null ? strings.add : strings.edit}
|
|
140
|
+
</Button>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</Modal>
|
|
144
|
+
{/if}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { FieldDefinition } from '../../types/crud.js';
|
|
2
|
+
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
3
|
+
props: {
|
|
4
|
+
field: FieldDefinition<T>;
|
|
5
|
+
record?: Record<string, unknown>;
|
|
6
|
+
readonly?: boolean;
|
|
7
|
+
};
|
|
8
|
+
exports: {};
|
|
9
|
+
bindings: "record";
|
|
10
|
+
slots: {};
|
|
11
|
+
events: {};
|
|
12
|
+
};
|
|
13
|
+
declare class __sveltets_Render<T extends object = Record<string, unknown>> {
|
|
14
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
15
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
16
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
17
|
+
bindings(): "record";
|
|
18
|
+
exports(): {};
|
|
19
|
+
}
|
|
20
|
+
interface $$IsomorphicComponent {
|
|
21
|
+
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']>> & {
|
|
22
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
23
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
24
|
+
<T extends object = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
25
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
26
|
+
}
|
|
27
|
+
declare const EmbeddedField: $$IsomorphicComponent;
|
|
28
|
+
type EmbeddedField<T extends object = Record<string, unknown>> = InstanceType<typeof EmbeddedField<T>>;
|
|
29
|
+
export default EmbeddedField;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import Avatar from '../Avatar.svelte';
|
|
4
4
|
import Label from '../form/Label.svelte';
|
|
5
5
|
import Select from '../form/Select.svelte';
|
|
6
|
+
import EmbeddedField from './EmbeddedField.svelte';
|
|
6
7
|
import { fieldLabel, initials } from './utils/misc.js';
|
|
7
8
|
import type { FieldDefinition } from '../../types/crud.js';
|
|
8
9
|
import { getStrings } from '../../i18n/context.js';
|
|
@@ -147,6 +148,8 @@
|
|
|
147
148
|
disabled={fieldDisabled}
|
|
148
149
|
></textarea>
|
|
149
150
|
{/if}
|
|
151
|
+
{:else if field.type === 'embedded'}
|
|
152
|
+
<EmbeddedField {field} bind:record {readonly} />
|
|
150
153
|
{:else if readonly}
|
|
151
154
|
<input
|
|
152
155
|
type={field.type ?? 'text'}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FieldDefinition } from '../../../types/crud.js';
|
|
2
|
+
export declare function seedField<T extends object = Record<string, unknown>>(f: FieldDefinition<T>, raw: unknown): unknown;
|
|
3
|
+
export declare function emptyRecord<T extends object = Record<string, unknown>>(fields: FieldDefinition<T>[]): Record<string, unknown>;
|
|
4
|
+
export declare function defaultItemLabel<T extends object = Record<string, unknown>>(fields: FieldDefinition<T>[], item: Record<string, unknown>): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { fieldLabel } from './misc.js';
|
|
2
|
+
// Shared by emptyRecord/Update.svelte's seedFromInstance/EmbeddedField's edit
|
|
3
|
+
// mode: converts one raw stored value into its editable draft form, using the
|
|
4
|
+
// same convention everywhere — booleans and embedded arrays keep their real
|
|
5
|
+
// type (Field.svelte's checkbox/list binds them directly), everything else
|
|
6
|
+
// becomes a string since Field.svelte's other inputs bind as strings.
|
|
7
|
+
export function seedField(f, raw) {
|
|
8
|
+
if (f.type === 'boolean')
|
|
9
|
+
return !!raw;
|
|
10
|
+
if (f.type === 'file')
|
|
11
|
+
return raw ?? null;
|
|
12
|
+
if (f.type === 'embedded')
|
|
13
|
+
return Array.isArray(raw) ? raw : [];
|
|
14
|
+
return String(raw ?? '');
|
|
15
|
+
}
|
|
16
|
+
// Shared by Create.svelte and EmbeddedField.svelte: both need an empty draft
|
|
17
|
+
// record seeded with each field's default, keyed the same way regardless of
|
|
18
|
+
// whether it ends up serialized as a top-level form or as one item inside an
|
|
19
|
+
// embedded list.
|
|
20
|
+
export function emptyRecord(fields) {
|
|
21
|
+
return Object.fromEntries(fields.map((f) => [f.attribute, seedField(f, f.default)]));
|
|
22
|
+
}
|
|
23
|
+
// Default summary shown per item in an embedded list when the field has no
|
|
24
|
+
// `itemLabel`: joins each sub-field's resolved display value so the list is
|
|
25
|
+
// at least readable out of the box.
|
|
26
|
+
export function defaultItemLabel(fields, item) {
|
|
27
|
+
return fields
|
|
28
|
+
.map((f) => {
|
|
29
|
+
const raw = item[f.attribute];
|
|
30
|
+
if (raw == null || raw === '')
|
|
31
|
+
return null;
|
|
32
|
+
if (f.type === 'select')
|
|
33
|
+
return f.options?.find((o) => o.value === String(raw))?.label ?? String(raw);
|
|
34
|
+
if (f.type === 'boolean')
|
|
35
|
+
return raw ? fieldLabel(f) : null;
|
|
36
|
+
return String(raw);
|
|
37
|
+
})
|
|
38
|
+
.filter((v) => !!v)
|
|
39
|
+
.join(' · ');
|
|
40
|
+
}
|
|
@@ -51,6 +51,10 @@ export function buildFieldDefinitions(meta, data, excludedFlag, excluded) {
|
|
|
51
51
|
integer: m.integer,
|
|
52
52
|
minLength: m.minLength,
|
|
53
53
|
maxLength: m.maxLength,
|
|
54
|
-
pattern: m.pattern
|
|
54
|
+
pattern: m.pattern,
|
|
55
|
+
fields: m.fields
|
|
56
|
+
? buildFieldDefinitions(m.fields, data, excludedFlag, new Set())
|
|
57
|
+
: undefined,
|
|
58
|
+
itemLabel: m.itemLabel
|
|
55
59
|
}));
|
|
56
60
|
}
|
|
@@ -6,6 +6,19 @@ import { fieldLabel } from './misc.js';
|
|
|
6
6
|
export function validateAll(fields, formData, strings) {
|
|
7
7
|
const errors = {};
|
|
8
8
|
for (const field of fields) {
|
|
9
|
+
if (field.type === 'embedded') {
|
|
10
|
+
let items;
|
|
11
|
+
try {
|
|
12
|
+
items = JSON.parse(String(formData.get(field.attribute) ?? '[]'));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
items = [];
|
|
16
|
+
}
|
|
17
|
+
if (field.required && (!Array.isArray(items) || items.length === 0)) {
|
|
18
|
+
errors[field.attribute] = strings.required(fieldLabel(field));
|
|
19
|
+
}
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
9
22
|
const val = String(formData.get(field.attribute) ?? '').trim();
|
|
10
23
|
if (field.required && !val) {
|
|
11
24
|
errors[field.attribute] = strings.required(fieldLabel(field));
|
|
@@ -8,6 +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
12
|
import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
|
|
12
13
|
import { getStrings } from '../../../i18n/context.js';
|
|
13
14
|
|
|
@@ -41,18 +42,7 @@
|
|
|
41
42
|
let internalError = $state('');
|
|
42
43
|
let continueCreating = $state(false);
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
return Object.fromEntries(
|
|
46
|
-
fields.map((f) => [
|
|
47
|
-
f.attribute,
|
|
48
|
-
f.type === 'boolean' ? !!f.default
|
|
49
|
-
: f.type === 'file' ? (f.default ?? null)
|
|
50
|
-
: String(f.default ?? ''),
|
|
51
|
-
])
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
let record = $state<Record<string, unknown>>(untrack(() => emptyRecord()));
|
|
45
|
+
let record = $state<Record<string, unknown>>(untrack(() => emptyRecord(fields)));
|
|
56
46
|
|
|
57
47
|
const groups = $derived(groupFields(fields));
|
|
58
48
|
const hasFileField = $derived(fields.some((f) => f.type === 'file'));
|
|
@@ -104,7 +94,7 @@
|
|
|
104
94
|
if (result.type === 'success' || result.type === 'redirect') {
|
|
105
95
|
await update({ reset: false });
|
|
106
96
|
if (continueCreating) {
|
|
107
|
-
record = emptyRecord();
|
|
97
|
+
record = emptyRecord(fields);
|
|
108
98
|
fieldErrors = {};
|
|
109
99
|
internalError = '';
|
|
110
100
|
} else {
|
|
@@ -8,6 +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 { seedField } from '../utils/embedded.js';
|
|
11
12
|
import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
|
|
12
13
|
import { getStrings } from '../../../i18n/context.js';
|
|
13
14
|
|
|
@@ -47,10 +48,8 @@
|
|
|
47
48
|
function seedFromInstance(inst: Record<string, unknown>): Record<string, unknown> {
|
|
48
49
|
const seeded: Record<string, unknown> = { ...inst };
|
|
49
50
|
for (const f of fields) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
seeded[f.attribute] = String(raw ?? '');
|
|
53
|
-
}
|
|
51
|
+
const raw = f.seed ? f.seed(inst) : inst[f.attribute];
|
|
52
|
+
seeded[f.attribute] = seedField(f, raw);
|
|
54
53
|
}
|
|
55
54
|
return seeded;
|
|
56
55
|
}
|
package/dist/i18n/en.js
CHANGED
|
@@ -24,6 +24,9 @@ export const en = {
|
|
|
24
24
|
saveAndContinue: 'Save and continue',
|
|
25
25
|
cancel: 'Cancel',
|
|
26
26
|
back: 'Back',
|
|
27
|
+
add: 'Add',
|
|
28
|
+
remove: 'Remove',
|
|
29
|
+
noItems: 'No items added',
|
|
27
30
|
confirm: 'Confirm',
|
|
28
31
|
deleteConfirm: (count, actionLabel) => `Are you sure you want to ${String(actionLabel).toLowerCase()} ${count} item${count === 1 ? '' : 's'}?`,
|
|
29
32
|
required: (field) => `${field} is required`,
|
package/dist/i18n/es.js
CHANGED
|
@@ -24,6 +24,9 @@ export const es = {
|
|
|
24
24
|
saveAndContinue: 'Guardar y continuar',
|
|
25
25
|
cancel: 'Cancelar',
|
|
26
26
|
back: 'Volver',
|
|
27
|
+
add: 'Agregar',
|
|
28
|
+
remove: 'Quitar',
|
|
29
|
+
noItems: 'Sin elementos agregados',
|
|
27
30
|
confirm: 'Confirmar',
|
|
28
31
|
deleteConfirm: (count, actionLabel) => `¿Seguro que querés ${String(actionLabel).toLowerCase()} ${count} elemento${count === 1 ? '' : 's'}?`,
|
|
29
32
|
required: (field) => `${field} es requerido`,
|
package/dist/i18n/types.d.ts
CHANGED
|
@@ -24,6 +24,9 @@ export interface RuneforgeStrings {
|
|
|
24
24
|
saveAndContinue: string;
|
|
25
25
|
cancel: string;
|
|
26
26
|
back: string;
|
|
27
|
+
add: string;
|
|
28
|
+
remove: string;
|
|
29
|
+
noItems: string;
|
|
27
30
|
confirm: string;
|
|
28
31
|
deleteConfirm: (count: number, actionLabel: string) => string;
|
|
29
32
|
required: (field: string) => string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { BreadcrumbItem } from './types/breadcrumb.js';
|
|
2
2
|
export { AttributeType } from './types/attribute.js';
|
|
3
|
-
export type { AttributeMetadata, InterfaceMetadata, SelectOption, OptionsResolver, FormatterResolver } from './types/attribute.js';
|
|
3
|
+
export type { AttributeMetadata, InterfaceMetadata, SelectOption, OptionsResolver, FormatterResolver, EmbeddedItemLabelResolver } from './types/attribute.js';
|
|
4
4
|
export type { CellProps, CellComponent, CellFormatter, SortDirection, IndexedRow, DistinctEntry, PaginatedEnvelope, ServerPagination, FilterSnapshot, TableQuery } from './types/table.js';
|
|
5
5
|
export type { ColumnDefinition, FieldDefinition, ActionConfiguration, CustomAction, CustomBulkAction, RowAction, SearchConfiguration } from './types/crud.js';
|
|
6
6
|
export type { RuneforgeConfig } from './config/context.js';
|
|
@@ -32,6 +32,7 @@ export { SortState, FilterState, snapshotFilter } from './components/table/state
|
|
|
32
32
|
export { cellRenderedText, isSortable, isFilterable, compare, distinctEntries } from './components/table/utils.js';
|
|
33
33
|
export { default as GenericCRUD } from './components/crud/GenericCRUD.svelte';
|
|
34
34
|
export { default as Field } from './components/crud/Field.svelte';
|
|
35
|
+
export { default as EmbeddedField } from './components/crud/EmbeddedField.svelte';
|
|
35
36
|
export { default as Header } from './components/common/Header.svelte';
|
|
36
37
|
export { default as AvatarCell } from './components/crud/columns/Avatar.svelte';
|
|
37
38
|
export { default as IconCell } from './components/crud/columns/Icon.svelte';
|
|
@@ -47,3 +48,4 @@ export { formatBoolean, formatDatetime, formatTruncateTextUpTo, formatInstance }
|
|
|
47
48
|
export { groupFields } from './components/crud/utils/grouping.js';
|
|
48
49
|
export type { FieldGroup } from './components/crud/utils/grouping.js';
|
|
49
50
|
export { validateAll } from './components/crud/utils/validation.js';
|
|
51
|
+
export { emptyRecord, defaultItemLabel } from './components/crud/utils/embedded.js';
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ export { cellRenderedText, isSortable, isFilterable, compare, distinctEntries }
|
|
|
29
29
|
// CRUD components
|
|
30
30
|
export { default as GenericCRUD } from './components/crud/GenericCRUD.svelte';
|
|
31
31
|
export { default as Field } from './components/crud/Field.svelte';
|
|
32
|
+
export { default as EmbeddedField } from './components/crud/EmbeddedField.svelte';
|
|
32
33
|
export { default as Header } from './components/common/Header.svelte';
|
|
33
34
|
export { default as AvatarCell } from './components/crud/columns/Avatar.svelte';
|
|
34
35
|
export { default as IconCell } from './components/crud/columns/Icon.svelte';
|
|
@@ -44,3 +45,4 @@ export { fieldLabel, initials } from './components/crud/utils/misc.js';
|
|
|
44
45
|
export { formatBoolean, formatDatetime, formatTruncateTextUpTo, formatInstance } from './components/crud/utils/formatters.js';
|
|
45
46
|
export { groupFields } from './components/crud/utils/grouping.js';
|
|
46
47
|
export { validateAll } from './components/crud/utils/validation.js';
|
|
48
|
+
export { emptyRecord, defaultItemLabel } from './components/crud/utils/embedded.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FullAutoFill } from 'svelte/elements';
|
|
2
2
|
import type { CellComponent, CellFormatter } from './table.js';
|
|
3
|
-
export type AttributeType = 'text' | 'email' | 'password' | 'number' | 'boolean' | 'textarea' | 'file' | 'select' | 'datetime';
|
|
3
|
+
export type AttributeType = 'text' | 'email' | 'password' | 'number' | 'boolean' | 'textarea' | 'file' | 'select' | 'datetime' | 'embedded';
|
|
4
4
|
export declare const AttributeType: {
|
|
5
5
|
readonly text: "text";
|
|
6
6
|
readonly email: "email";
|
|
@@ -11,6 +11,7 @@ export declare const AttributeType: {
|
|
|
11
11
|
readonly file: "file";
|
|
12
12
|
readonly select: "select";
|
|
13
13
|
readonly datetime: "datetime";
|
|
14
|
+
readonly embedded: "embedded";
|
|
14
15
|
};
|
|
15
16
|
export type InterfaceMetadata<T> = Partial<Record<keyof T, AttributeMetadata>>;
|
|
16
17
|
export type SelectOption = {
|
|
@@ -23,6 +24,9 @@ export type DependentOptionsResolver = (data: any, record: Record<string, unknow
|
|
|
23
24
|
export type SearchResolver = (query: string) => Promise<SelectOption[]>;
|
|
24
25
|
export type DisabledResolver = (record: Record<string, unknown>) => boolean;
|
|
25
26
|
export type SeedResolver = (instance: any) => unknown;
|
|
27
|
+
/** Embedded fields only: renders a short summary for one item in the list.
|
|
28
|
+
* Falls back to a dash-joined summary of the item's sub-field values. */
|
|
29
|
+
export type EmbeddedItemLabelResolver = (item: Record<string, unknown>) => string;
|
|
26
30
|
export type AttributeMetadata = {
|
|
27
31
|
label?: string;
|
|
28
32
|
type?: AttributeType;
|
|
@@ -56,4 +60,8 @@ export type AttributeMetadata = {
|
|
|
56
60
|
minLength?: number;
|
|
57
61
|
maxLength?: number;
|
|
58
62
|
pattern?: string;
|
|
63
|
+
/** Embedded fields only: schema for each item added through the "+" modal. */
|
|
64
|
+
fields?: InterfaceMetadata<any>;
|
|
65
|
+
/** Embedded fields only: short label for an item in the list. */
|
|
66
|
+
itemLabel?: EmbeddedItemLabelResolver;
|
|
59
67
|
};
|
package/dist/types/attribute.js
CHANGED
package/dist/types/crud.d.ts
CHANGED
|
@@ -39,6 +39,11 @@ export interface FieldDefinition<T extends object = Record<string, unknown>> {
|
|
|
39
39
|
minLength?: number;
|
|
40
40
|
maxLength?: number;
|
|
41
41
|
pattern?: string;
|
|
42
|
+
/** Embedded fields only: sub-field definitions for each item, built from
|
|
43
|
+
* the metadata's `fields`. */
|
|
44
|
+
fields?: FieldDefinition<Record<string, unknown>>[];
|
|
45
|
+
/** Embedded fields only: short label for an item in the list. */
|
|
46
|
+
itemLabel?: (item: Record<string, unknown>) => string;
|
|
42
47
|
}
|
|
43
48
|
export interface ActionConfiguration<T extends object = Record<string, unknown>> {
|
|
44
49
|
enabled?: boolean;
|