runeforge 0.0.7 → 0.0.9
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 +28 -0
- package/dist/components/common/Header.svelte +1 -3
- package/dist/components/common/Header.svelte.d.ts +0 -1
- package/dist/components/crud/GenericCRUD.svelte +17 -10
- package/dist/components/crud/GenericCRUD.svelte.d.ts +1 -0
- package/dist/components/crud/utils/formatters.d.ts +1 -3
- package/dist/components/crud/utils/formatters.js +2 -2
- package/dist/components/crud/views/Create.svelte +0 -1
- package/dist/components/crud/views/List.svelte +3 -2
- package/dist/components/crud/views/List.svelte.d.ts +1 -0
- package/dist/components/crud/views/Read.svelte +4 -3
- package/dist/components/crud/views/Read.svelte.d.ts +1 -0
- package/dist/components/crud/views/Update.svelte +4 -3
- package/dist/components/crud/views/Update.svelte.d.ts +1 -0
- package/dist/components/navigation/Breadcrumbs.svelte +2 -3
- package/dist/components/navigation/Breadcrumbs.svelte.d.ts +0 -1
- package/dist/config/context.d.ts +5 -0
- package/dist/config/context.js +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ A SvelteKit toolkit that forges forms, tables, actions, and CRUD workflows from
|
|
|
23
23
|
- [Theming](#theming)
|
|
24
24
|
- [Tailwind source scanning](#tailwind-source-scanning)
|
|
25
25
|
- [CSS variables](#css-variables)
|
|
26
|
+
- [Configuration](#configuration)
|
|
26
27
|
- [Basic Usage](#basic-usage)
|
|
27
28
|
- [1. Define your interface and metadata](#1-define-your-interface-and-metadata)
|
|
28
29
|
- [2. Create the model](#2-create-the-model)
|
|
@@ -136,6 +137,25 @@ Responsive overrides work too:
|
|
|
136
137
|
|
|
137
138
|
---
|
|
138
139
|
|
|
140
|
+
## Configuration
|
|
141
|
+
|
|
142
|
+
Global settings are applied once in your root layout via `setConfig`. This avoids passing the same prop to every CRUD component.
|
|
143
|
+
|
|
144
|
+
```svelte
|
|
145
|
+
<!-- +layout.svelte -->
|
|
146
|
+
<script>
|
|
147
|
+
import { setConfig } from 'runeforge';
|
|
148
|
+
|
|
149
|
+
setConfig({ homeHref: '/admin' });
|
|
150
|
+
</script>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| Option | Default | Description |
|
|
154
|
+
| --- | --- | --- |
|
|
155
|
+
| `homeHref` | `'/'` | URL for the home crumb in every breadcrumb trail |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
139
159
|
## Basic Usage
|
|
140
160
|
|
|
141
161
|
### 1. Define your interface and metadata
|
|
@@ -284,6 +304,14 @@ The `load` function returns a single record when `?id=` is present (used by the
|
|
|
284
304
|
|
|
285
305
|
`dataKey` must match the key returned by the load function for the list. Each `endpoint` maps to a SvelteKit form action on the same page.
|
|
286
306
|
|
|
307
|
+
If your records use a different identifier field than `_id` (e.g. a plain `id`), pass the `idKey` prop:
|
|
308
|
+
|
|
309
|
+
```svelte
|
|
310
|
+
<GenericCRUD idKey="id" ... />
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
This propagates to navigation URLs, form submissions, deletion calls, and the auto-excluded column list, so no other changes are needed on your end.
|
|
314
|
+
|
|
287
315
|
---
|
|
288
316
|
|
|
289
317
|
## Components
|
|
@@ -7,19 +7,17 @@
|
|
|
7
7
|
title,
|
|
8
8
|
breadcrumbs = [],
|
|
9
9
|
buttons,
|
|
10
|
-
admin = false,
|
|
11
10
|
}: {
|
|
12
11
|
title: string;
|
|
13
12
|
breadcrumbs?: BreadcrumbItem[];
|
|
14
13
|
buttons?: Snippet;
|
|
15
|
-
admin?: boolean;
|
|
16
14
|
} = $props();
|
|
17
15
|
</script>
|
|
18
16
|
|
|
19
17
|
<div class="header-root flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
20
18
|
<div class="flex flex-col gap-1 flex-1">
|
|
21
19
|
<h1>{title}</h1>
|
|
22
|
-
<Breadcrumbs items={breadcrumbs}
|
|
20
|
+
<Breadcrumbs items={breadcrumbs} />
|
|
23
21
|
</div>
|
|
24
22
|
|
|
25
23
|
{#if buttons}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
let {
|
|
19
19
|
data = undefined as Record<string, unknown> | undefined,
|
|
20
20
|
dataKey = undefined as string | undefined,
|
|
21
|
+
idKey = '_id',
|
|
21
22
|
labelOne = '',
|
|
22
23
|
labelMany = '',
|
|
23
24
|
icon,
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
}: {
|
|
35
36
|
data?: Record<string, unknown>;
|
|
36
37
|
dataKey?: string;
|
|
38
|
+
idKey?: string;
|
|
37
39
|
labelOne?: string;
|
|
38
40
|
labelMany?: string;
|
|
39
41
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -61,9 +63,11 @@
|
|
|
61
63
|
const reading = $derived(idParam !== null && viewParam === null);
|
|
62
64
|
const editing = $derived(idParam !== null && viewParam === 'edit');
|
|
63
65
|
|
|
66
|
+
const excluded = $derived(new Set([...AUTO_EXCLUDED, idKey]));
|
|
67
|
+
|
|
64
68
|
const singleInstance = $derived<T | undefined>(
|
|
65
69
|
(Object.values(page.data as Record<string, unknown>).find(
|
|
66
|
-
(v) => v !== null && typeof v === 'object' && !Array.isArray(v) &&
|
|
70
|
+
(v) => v !== null && typeof v === 'object' && !Array.isArray(v) && idKey in (v as object)
|
|
67
71
|
) as T | undefined)
|
|
68
72
|
);
|
|
69
73
|
|
|
@@ -72,10 +76,10 @@
|
|
|
72
76
|
async function navList() { await goto('?'); }
|
|
73
77
|
async function navCreate() { await goto('?view=create'); }
|
|
74
78
|
async function navRead(item: T) {
|
|
75
|
-
await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)
|
|
79
|
+
await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)[idKey] ?? ''))}`);
|
|
76
80
|
}
|
|
77
81
|
async function navEdit(item: T) {
|
|
78
|
-
await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)
|
|
82
|
+
await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)[idKey] ?? ''))}&view=edit`);
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
const resolvedColumns: ColumnDefinition<T>[] = $derived(
|
|
@@ -93,7 +97,7 @@
|
|
|
93
97
|
}))
|
|
94
98
|
: entityData.length > 0
|
|
95
99
|
? (Object.keys(entityData[0]) as (keyof T & string)[])
|
|
96
|
-
.filter((k) => k
|
|
100
|
+
.filter((k) => !excluded.has(k))
|
|
97
101
|
.map((k) => ({ attribute: k, title: k }))
|
|
98
102
|
: [])
|
|
99
103
|
);
|
|
@@ -101,7 +105,7 @@
|
|
|
101
105
|
const resolvedFields: FieldDefinition<T>[] = $derived(
|
|
102
106
|
fields ?? (meta
|
|
103
107
|
? (Object.entries(meta) as [string, AttributeMetadata][])
|
|
104
|
-
.filter(([k, m]) => !
|
|
108
|
+
.filter(([k, m]) => !excluded.has(k) && !m.excludedFromCreate)
|
|
105
109
|
.map(([k, m]) => ({
|
|
106
110
|
attribute: k as keyof T & string,
|
|
107
111
|
title: m.label,
|
|
@@ -114,7 +118,7 @@
|
|
|
114
118
|
}))
|
|
115
119
|
: entityData.length > 0
|
|
116
120
|
? (Object.entries(entityData[0]) as [string, unknown][])
|
|
117
|
-
.filter(([k]) => !
|
|
121
|
+
.filter(([k]) => !excluded.has(k))
|
|
118
122
|
.map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
|
|
119
123
|
: [])
|
|
120
124
|
);
|
|
@@ -122,7 +126,7 @@
|
|
|
122
126
|
const resolvedReadFields: FieldDefinition<T>[] = $derived(
|
|
123
127
|
fields ?? (meta
|
|
124
128
|
? (Object.entries(meta) as [string, AttributeMetadata][])
|
|
125
|
-
.filter(([k, m]) => !
|
|
129
|
+
.filter(([k, m]) => !excluded.has(k) && !m.excludedFromRead)
|
|
126
130
|
.map(([k, m]) => ({
|
|
127
131
|
attribute: k as keyof T & string,
|
|
128
132
|
title: m.label,
|
|
@@ -135,7 +139,7 @@
|
|
|
135
139
|
}))
|
|
136
140
|
: entityData.length > 0
|
|
137
141
|
? (Object.entries(entityData[0]) as [string, unknown][])
|
|
138
|
-
.filter(([k]) => !
|
|
142
|
+
.filter(([k]) => !excluded.has(k))
|
|
139
143
|
.map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
|
|
140
144
|
: [])
|
|
141
145
|
);
|
|
@@ -143,7 +147,7 @@
|
|
|
143
147
|
const resolvedUpdateFields: FieldDefinition<T>[] = $derived(
|
|
144
148
|
fields ?? (meta
|
|
145
149
|
? (Object.entries(meta) as [string, AttributeMetadata][])
|
|
146
|
-
.filter(([k, m]) => !
|
|
150
|
+
.filter(([k, m]) => !excluded.has(k) && !m.excludedFromUpdate)
|
|
147
151
|
.map(([k, m]) => ({
|
|
148
152
|
attribute: k as keyof T & string,
|
|
149
153
|
title: m.label,
|
|
@@ -156,7 +160,7 @@
|
|
|
156
160
|
}))
|
|
157
161
|
: entityData.length > 0
|
|
158
162
|
? (Object.entries(entityData[0]) as [string, unknown][])
|
|
159
|
-
.filter(([k]) => !
|
|
163
|
+
.filter(([k]) => !excluded.has(k))
|
|
160
164
|
.map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
|
|
161
165
|
: [])
|
|
162
166
|
);
|
|
@@ -184,6 +188,7 @@
|
|
|
184
188
|
{labelOne}
|
|
185
189
|
{labelMany}
|
|
186
190
|
{icon}
|
|
191
|
+
{idKey}
|
|
187
192
|
fields={resolvedReadFields}
|
|
188
193
|
instance={singleInstance ?? {} as T}
|
|
189
194
|
{read}
|
|
@@ -194,6 +199,7 @@
|
|
|
194
199
|
{labelOne}
|
|
195
200
|
{labelMany}
|
|
196
201
|
{icon}
|
|
202
|
+
{idKey}
|
|
197
203
|
fields={resolvedUpdateFields}
|
|
198
204
|
instance={singleInstance ?? {} as T}
|
|
199
205
|
{update}
|
|
@@ -208,6 +214,7 @@
|
|
|
208
214
|
{labelMany}
|
|
209
215
|
{icon}
|
|
210
216
|
{pageSize}
|
|
217
|
+
{idKey}
|
|
211
218
|
{creation}
|
|
212
219
|
{update}
|
|
213
220
|
{read}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
export declare const formatBoolean: (trueLabel?: string, falseLabel?: string) => () => (value: boolean) => string;
|
|
2
2
|
export declare const formatDatetime: (format?: string) => () => (value: Date) => string;
|
|
3
3
|
export declare function formatTruncateTextUpTo(maxLength: number): () => (value: string) => string;
|
|
4
|
-
export declare function formatInstance<T extends
|
|
5
|
-
_id: string;
|
|
6
|
-
}>(attribute: keyof T & string, instances: T[], urlPath: string): (value: unknown) => string;
|
|
4
|
+
export declare function formatInstance<T extends Record<string, unknown>>(attribute: keyof T & string, instances: T[], urlPath: string, idKey?: keyof T & string): (value: unknown) => string;
|
|
@@ -30,8 +30,8 @@ function escapeHtml(str) {
|
|
|
30
30
|
.replace(/>/g, '>')
|
|
31
31
|
.replace(/"/g, '"');
|
|
32
32
|
}
|
|
33
|
-
export function formatInstance(attribute, instances, urlPath) {
|
|
34
|
-
const map = new Map(instances.map((i) => [i
|
|
33
|
+
export function formatInstance(attribute, instances, urlPath, idKey = '_id') {
|
|
34
|
+
const map = new Map(instances.map((i) => [String(i[idKey] ?? ''), i]));
|
|
35
35
|
return (value) => {
|
|
36
36
|
const id = String(value ?? '');
|
|
37
37
|
const instance = map.get(id);
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
labelMany = '',
|
|
23
23
|
icon,
|
|
24
24
|
pageSize = 10,
|
|
25
|
+
idKey = '_id',
|
|
25
26
|
creation = {} as ActionConfiguration<T>,
|
|
26
27
|
update = {} as ActionConfiguration<T>,
|
|
27
28
|
read = {} as ActionConfiguration<T>,
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
41
|
icon?: any;
|
|
41
42
|
pageSize?: number;
|
|
43
|
+
idKey?: string;
|
|
42
44
|
creation?: ActionConfiguration<T>;
|
|
43
45
|
update?: ActionConfiguration<T>;
|
|
44
46
|
read?: ActionConfiguration<T>;
|
|
@@ -67,7 +69,7 @@
|
|
|
67
69
|
async function runEndpointAction(endpoint: string, items: T[]) {
|
|
68
70
|
await Promise.all(items.map((item) => {
|
|
69
71
|
const fd = new FormData();
|
|
70
|
-
fd.set('id', String((item as Record<string, unknown>)
|
|
72
|
+
fd.set('id', String((item as Record<string, unknown>)[idKey] ?? ''));
|
|
71
73
|
return fetch(endpoint, { method: 'POST', body: fd });
|
|
72
74
|
}));
|
|
73
75
|
await invalidateAll();
|
|
@@ -124,7 +126,6 @@
|
|
|
124
126
|
<div class="flex flex-col gap-6">
|
|
125
127
|
|
|
126
128
|
<Header
|
|
127
|
-
admin
|
|
128
129
|
title={labelMany}
|
|
129
130
|
breadcrumbs={[
|
|
130
131
|
{ label: labelMany, icon: entityIcon, link: { href: '#' }, prominent: true },
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
labelOne = '',
|
|
16
16
|
labelMany = '',
|
|
17
17
|
icon,
|
|
18
|
+
idKey = '_id',
|
|
18
19
|
fields = [] as FieldDefinition<T>[],
|
|
19
20
|
instance = {} as T,
|
|
20
21
|
read = {} as ActionConfiguration<T>,
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
labelMany?: string;
|
|
25
26
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
27
|
icon?: any;
|
|
28
|
+
idKey?: string;
|
|
27
29
|
fields?: FieldDefinition<T>[];
|
|
28
30
|
instance?: T;
|
|
29
31
|
read?: ActionConfiguration<T>;
|
|
@@ -37,7 +39,7 @@
|
|
|
37
39
|
|
|
38
40
|
$effect(() => {
|
|
39
41
|
const endpoint = read.endpoint;
|
|
40
|
-
const id = (instance as Record<string, unknown>)
|
|
42
|
+
const id = (instance as Record<string, unknown>)[idKey];
|
|
41
43
|
if (!endpoint || id == null) return;
|
|
42
44
|
|
|
43
45
|
let cancelled = false;
|
|
@@ -50,7 +52,7 @@
|
|
|
50
52
|
const data = result.data as Record<string, unknown> | undefined;
|
|
51
53
|
const fetched = data
|
|
52
54
|
? Object.values(data).find(
|
|
53
|
-
(v) => v !== null && typeof v === 'object' && !Array.isArray(v) &&
|
|
55
|
+
(v) => v !== null && typeof v === 'object' && !Array.isArray(v) && idKey in (v as object)
|
|
54
56
|
)
|
|
55
57
|
: undefined;
|
|
56
58
|
if (fetched && typeof fetched === 'object') record = fetched as Record<string, unknown>;
|
|
@@ -63,7 +65,6 @@
|
|
|
63
65
|
<div class="flex flex-col gap-6">
|
|
64
66
|
|
|
65
67
|
<Header
|
|
66
|
-
admin
|
|
67
68
|
title={labelMany}
|
|
68
69
|
breadcrumbs={[
|
|
69
70
|
{ label: labelMany, icon: entityIcon, link: { href: '#', onclick: (e) => { e.preventDefault(); onCancel?.(); } }, prominent: true },
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
labelOne = '',
|
|
17
17
|
labelMany = '',
|
|
18
18
|
icon,
|
|
19
|
+
idKey = '_id',
|
|
19
20
|
fields = [] as FieldDefinition<T>[],
|
|
20
21
|
instance = {} as T,
|
|
21
22
|
update = {} as ActionConfiguration<T>,
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
labelMany?: string;
|
|
28
29
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
30
|
icon?: any;
|
|
31
|
+
idKey?: string;
|
|
30
32
|
fields?: FieldDefinition<T>[];
|
|
31
33
|
instance?: T;
|
|
32
34
|
update?: ActionConfiguration<T>;
|
|
@@ -56,7 +58,7 @@
|
|
|
56
58
|
);
|
|
57
59
|
|
|
58
60
|
$effect(() => {
|
|
59
|
-
const id = (instance as Record<string, unknown>)
|
|
61
|
+
const id = (instance as Record<string, unknown>)[idKey];
|
|
60
62
|
if (!id) return;
|
|
61
63
|
untrack(() => { record = seedFromInstance(instance as Record<string, unknown>); });
|
|
62
64
|
});
|
|
@@ -83,7 +85,6 @@
|
|
|
83
85
|
<div class="flex flex-col gap-6">
|
|
84
86
|
|
|
85
87
|
<Header
|
|
86
|
-
admin
|
|
87
88
|
title={labelMany}
|
|
88
89
|
breadcrumbs={[
|
|
89
90
|
{ label: labelMany, icon: entityIcon, link: { href: '#', onclick: (e) => { e.preventDefault(); onCancel?.(); } }, prominent: true },
|
|
@@ -130,7 +131,7 @@
|
|
|
130
131
|
};
|
|
131
132
|
}}
|
|
132
133
|
>
|
|
133
|
-
<input type="hidden" name="id" value={String(record
|
|
134
|
+
<input type="hidden" name="id" value={String(record[idKey] ?? '')} />
|
|
134
135
|
|
|
135
136
|
{#each fields as field (field.attribute)}
|
|
136
137
|
<Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} />
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getIconSet } from '../../icons/context.js';
|
|
3
3
|
import { defaultIconSet } from '../../icons/sets/default.js';
|
|
4
|
+
import { getConfig } from '../../config/context.js';
|
|
4
5
|
import type { BreadcrumbItem } from '../../types/breadcrumb.js';
|
|
5
6
|
|
|
6
7
|
let {
|
|
7
8
|
items = [],
|
|
8
|
-
admin = false,
|
|
9
9
|
homeHref,
|
|
10
10
|
}: {
|
|
11
11
|
items?: BreadcrumbItem[];
|
|
12
|
-
admin?: boolean;
|
|
13
12
|
homeHref?: string;
|
|
14
13
|
} = $props();
|
|
15
14
|
|
|
@@ -18,7 +17,7 @@
|
|
|
18
17
|
const home: BreadcrumbItem = $derived({
|
|
19
18
|
label: 'Inicio',
|
|
20
19
|
icon: icons.home,
|
|
21
|
-
link: { href: homeHref ?? (
|
|
20
|
+
link: { href: homeHref ?? getConfig().homeHref ?? '/' },
|
|
22
21
|
});
|
|
23
22
|
|
|
24
23
|
const allItems = $derived([home, ...items]);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const KEY = Symbol('runeforge-config');
|
|
3
|
+
export function setConfig(config) {
|
|
4
|
+
const existing = getContext(KEY) ?? {};
|
|
5
|
+
setContext(KEY, { ...existing, ...config });
|
|
6
|
+
}
|
|
7
|
+
export function getConfig() {
|
|
8
|
+
return getContext(KEY) ?? {};
|
|
9
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export { AttributeType } from './types/attribute.js';
|
|
|
3
3
|
export type { AttributeMetadata, InterfaceMetadata, SelectOption, OptionsResolver, FormatterResolver } from './types/attribute.js';
|
|
4
4
|
export type { CellProps, CellComponent, CellFormatter, SortDirection, IndexedRow, DistinctEntry } from './types/table.js';
|
|
5
5
|
export type { ColumnDefinition, FieldDefinition, ActionConfiguration, CustomAction, RowAction } from './types/crud.js';
|
|
6
|
+
export type { RuneforgeConfig } from './config/context.js';
|
|
7
|
+
export { setConfig, getConfig } from './config/context.js';
|
|
6
8
|
export type { CRUDIconSet, IconComponent } from './icons/types.js';
|
|
7
9
|
export { setIconSet, getIconSet } from './icons/context.js';
|
|
8
10
|
export { defaultIconSet } from './icons/sets/default.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { AttributeType } from './types/attribute.js';
|
|
2
|
+
export { setConfig, getConfig } from './config/context.js';
|
|
2
3
|
export { setIconSet, getIconSet } from './icons/context.js';
|
|
3
4
|
export { defaultIconSet } from './icons/sets/default.js';
|
|
4
5
|
export { bootstrapIconSet } from './icons/sets/bootstrap.js';
|