runeforge 0.0.1
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/LICENSE +21 -0
- package/README.md +551 -0
- package/dist/components/Avatar.svelte +31 -0
- package/dist/components/Avatar.svelte.d.ts +10 -0
- package/dist/components/IconRenderer.svelte +22 -0
- package/dist/components/IconRenderer.svelte.d.ts +8 -0
- package/dist/components/Modal.svelte +47 -0
- package/dist/components/Modal.svelte.d.ts +9 -0
- package/dist/components/common/Header.svelte +30 -0
- package/dist/components/common/Header.svelte.d.ts +11 -0
- package/dist/components/crud/Field.svelte +159 -0
- package/dist/components/crud/Field.svelte.d.ts +30 -0
- package/dist/components/crud/GenericCRUD.svelte +236 -0
- package/dist/components/crud/GenericCRUD.svelte.d.ts +44 -0
- package/dist/components/crud/columns/Avatar.svelte +15 -0
- package/dist/components/crud/columns/Avatar.svelte.d.ts +8 -0
- package/dist/components/crud/columns/Icon.svelte +8 -0
- package/dist/components/crud/columns/Icon.svelte.d.ts +4 -0
- package/dist/components/crud/utils/constants.d.ts +1 -0
- package/dist/components/crud/utils/constants.js +6 -0
- package/dist/components/crud/utils/formatters.d.ts +6 -0
- package/dist/components/crud/utils/formatters.js +42 -0
- package/dist/components/crud/utils/misc.d.ts +5 -0
- package/dist/components/crud/utils/misc.js +8 -0
- package/dist/components/crud/utils/resolution.d.ts +4 -0
- package/dist/components/crud/utils/resolution.js +22 -0
- package/dist/components/crud/views/Create.svelte +147 -0
- package/dist/components/crud/views/Create.svelte.d.ts +34 -0
- package/dist/components/crud/views/List.svelte +170 -0
- package/dist/components/crud/views/List.svelte.d.ts +41 -0
- package/dist/components/crud/views/Read.svelte +85 -0
- package/dist/components/crud/views/Read.svelte.d.ts +33 -0
- package/dist/components/crud/views/Update.svelte +148 -0
- package/dist/components/crud/views/Update.svelte.d.ts +35 -0
- package/dist/components/form/Button.svelte +27 -0
- package/dist/components/form/Button.svelte.d.ts +10 -0
- package/dist/components/form/Label.svelte +37 -0
- package/dist/components/form/Label.svelte.d.ts +14 -0
- package/dist/components/form/PasswordInput.svelte +61 -0
- package/dist/components/form/PasswordInput.svelte.d.ts +13 -0
- package/dist/components/form/Required.svelte +1 -0
- package/dist/components/form/Required.svelte.d.ts +26 -0
- package/dist/components/form/Select.svelte +114 -0
- package/dist/components/form/Select.svelte.d.ts +14 -0
- package/dist/components/navigation/Breadcrumbs.svelte +51 -0
- package/dist/components/navigation/Breadcrumbs.svelte.d.ts +9 -0
- package/dist/components/table/ColumnFilter.svelte +140 -0
- package/dist/components/table/ColumnFilter.svelte.d.ts +33 -0
- package/dist/components/table/PaginatedTable.svelte +106 -0
- package/dist/components/table/PaginatedTable.svelte.d.ts +35 -0
- package/dist/components/table/Paginator.svelte +62 -0
- package/dist/components/table/Paginator.svelte.d.ts +10 -0
- package/dist/components/table/SortHeader.svelte +43 -0
- package/dist/components/table/SortHeader.svelte.d.ts +10 -0
- package/dist/components/table/TableBody.svelte +70 -0
- package/dist/components/table/TableBody.svelte.d.ts +36 -0
- package/dist/components/table/TableHeader.svelte +83 -0
- package/dist/components/table/TableHeader.svelte.d.ts +39 -0
- package/dist/components/table/state.svelte.d.ts +30 -0
- package/dist/components/table/state.svelte.js +109 -0
- package/dist/components/table/utils.d.ts +7 -0
- package/dist/components/table/utils.js +44 -0
- package/dist/i18n/context.d.ts +3 -0
- package/dist/i18n/context.js +10 -0
- package/dist/i18n/en.d.ts +2 -0
- package/dist/i18n/en.js +24 -0
- package/dist/i18n/es.d.ts +2 -0
- package/dist/i18n/es.js +24 -0
- package/dist/i18n/types.d.ts +24 -0
- package/dist/i18n/types.js +1 -0
- package/dist/icons/bootstrapIconSet.d.ts +1 -0
- package/dist/icons/bootstrapIconSet.js +1 -0
- package/dist/icons/context.d.ts +3 -0
- package/dist/icons/context.js +9 -0
- package/dist/icons/defaultIconSet.d.ts +1 -0
- package/dist/icons/defaultIconSet.js +1 -0
- package/dist/icons/defaults/Create.svelte +6 -0
- package/dist/icons/defaults/Create.svelte.d.ts +7 -0
- package/dist/icons/defaults/Delete.svelte +6 -0
- package/dist/icons/defaults/Delete.svelte.d.ts +7 -0
- package/dist/icons/defaults/Edit.svelte +7 -0
- package/dist/icons/defaults/Edit.svelte.d.ts +7 -0
- package/dist/icons/defaults/Filter.svelte +6 -0
- package/dist/icons/defaults/Filter.svelte.d.ts +7 -0
- package/dist/icons/defaults/FilterActive.svelte +6 -0
- package/dist/icons/defaults/FilterActive.svelte.d.ts +7 -0
- package/dist/icons/defaults/Folder.svelte +6 -0
- package/dist/icons/defaults/Folder.svelte.d.ts +7 -0
- package/dist/icons/defaults/Home.svelte +6 -0
- package/dist/icons/defaults/Home.svelte.d.ts +7 -0
- package/dist/icons/defaults/PasswordHide.svelte +9 -0
- package/dist/icons/defaults/PasswordHide.svelte.d.ts +7 -0
- package/dist/icons/defaults/PasswordShow.svelte +7 -0
- package/dist/icons/defaults/PasswordShow.svelte.d.ts +7 -0
- package/dist/icons/defaults/SortAsc.svelte +6 -0
- package/dist/icons/defaults/SortAsc.svelte.d.ts +7 -0
- package/dist/icons/defaults/SortDesc.svelte +6 -0
- package/dist/icons/defaults/SortDesc.svelte.d.ts +7 -0
- package/dist/icons/defaults/SortNone.svelte +6 -0
- package/dist/icons/defaults/SortNone.svelte.d.ts +7 -0
- package/dist/icons/defaults/View.svelte +7 -0
- package/dist/icons/defaults/View.svelte.d.ts +7 -0
- package/dist/icons/sets/bootstrap.d.ts +2 -0
- package/dist/icons/sets/bootstrap.js +29 -0
- package/dist/icons/sets/default.d.ts +2 -0
- package/dist/icons/sets/default.js +28 -0
- package/dist/icons/types.d.ts +26 -0
- package/dist/icons/types.js +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +41 -0
- package/dist/types/attribute.d.ts +38 -0
- package/dist/types/attribute.js +11 -0
- package/dist/types/breadcrumb.d.ts +7 -0
- package/dist/types/breadcrumb.js +1 -0
- package/dist/types/crud.d.ts +48 -0
- package/dist/types/crud.js +1 -0
- package/dist/types/table.d.ts +16 -0
- package/dist/types/table.js +1 -0
- package/package.json +82 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { enhance } from '$app/forms';
|
|
4
|
+
import Field from '../Field.svelte';
|
|
5
|
+
import Button from '../../form/Button.svelte';
|
|
6
|
+
import Header from '../../common/Header.svelte';
|
|
7
|
+
import { getIconSet } from '../../../icons/context.js';
|
|
8
|
+
import { defaultIconSet } from '../../../icons/sets/default.js';
|
|
9
|
+
import { fieldLabel } from '../utils/misc.js';
|
|
10
|
+
import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
|
|
11
|
+
import { getStrings } from '../../../i18n/context.js';
|
|
12
|
+
|
|
13
|
+
const strings = getStrings();
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
labelOne = '',
|
|
17
|
+
labelMany = '',
|
|
18
|
+
icon,
|
|
19
|
+
fields = [] as FieldDefinition<T>[],
|
|
20
|
+
creation = {} as ActionConfiguration<T>,
|
|
21
|
+
serverError = '',
|
|
22
|
+
onCancel,
|
|
23
|
+
onSuccess,
|
|
24
|
+
}: {
|
|
25
|
+
labelOne?: string;
|
|
26
|
+
labelMany?: string;
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
icon?: any;
|
|
29
|
+
fields?: FieldDefinition<T>[];
|
|
30
|
+
creation?: ActionConfiguration<T>;
|
|
31
|
+
serverError?: string;
|
|
32
|
+
onCancel?: () => void;
|
|
33
|
+
onSuccess?: () => void;
|
|
34
|
+
} = $props();
|
|
35
|
+
|
|
36
|
+
const icons = $derived(getIconSet() ?? defaultIconSet);
|
|
37
|
+
const entityIcon = $derived(icon ?? icons.folder);
|
|
38
|
+
|
|
39
|
+
let fieldErrors = $state<Record<string, string>>({});
|
|
40
|
+
let internalError = $state('');
|
|
41
|
+
let continueCreating = $state(false);
|
|
42
|
+
|
|
43
|
+
function emptyRecord(): Record<string, unknown> {
|
|
44
|
+
return Object.fromEntries(
|
|
45
|
+
fields.map((f) => [
|
|
46
|
+
f.attribute,
|
|
47
|
+
f.type === 'boolean' ? !!f.default
|
|
48
|
+
: f.type === 'file' ? (f.default ?? null)
|
|
49
|
+
: String(f.default ?? ''),
|
|
50
|
+
])
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let record = $state<Record<string, unknown>>(untrack(() => emptyRecord()));
|
|
55
|
+
|
|
56
|
+
function validateAll(formData: FormData): Record<string, string> {
|
|
57
|
+
const errs: Record<string, string> = {};
|
|
58
|
+
for (const field of fields) {
|
|
59
|
+
if (field.required) {
|
|
60
|
+
const val = String(formData.get(field.attribute) ?? '').trim();
|
|
61
|
+
if (!val) errs[field.attribute] = strings.required(fieldLabel(field));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return errs;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const hasFileField = $derived(fields.some((f) => f.type === 'file'));
|
|
68
|
+
|
|
69
|
+
const errorEntries = $derived([
|
|
70
|
+
...((serverError || internalError) ? [['_global', internalError || serverError] as [string, string]] : []),
|
|
71
|
+
...Object.entries(fieldErrors),
|
|
72
|
+
]);
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<div class="flex flex-col gap-6">
|
|
76
|
+
|
|
77
|
+
<Header
|
|
78
|
+
admin
|
|
79
|
+
title={labelMany}
|
|
80
|
+
breadcrumbs={[
|
|
81
|
+
{ label: labelMany, icon: entityIcon, link: { href: '#', onclick: (e) => { e.preventDefault(); onCancel?.(); } }, prominent: true },
|
|
82
|
+
{ label: creation.label ?? labelOne, icon: icons.create },
|
|
83
|
+
]}
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
{#if errorEntries.length > 0}
|
|
87
|
+
<div role="alert" class="alert alert-error">
|
|
88
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
89
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
90
|
+
</svg>
|
|
91
|
+
<ul class="list-disc list-inside text-sm">
|
|
92
|
+
{#each errorEntries as [key, msg] (key)}
|
|
93
|
+
<li>{msg}</li>
|
|
94
|
+
{/each}
|
|
95
|
+
</ul>
|
|
96
|
+
</div>
|
|
97
|
+
{/if}
|
|
98
|
+
|
|
99
|
+
<form
|
|
100
|
+
method="POST"
|
|
101
|
+
action={creation.endpoint ?? '?/create'}
|
|
102
|
+
enctype={hasFileField ? 'multipart/form-data' : undefined}
|
|
103
|
+
class="mx-auto flex w-full max-w-lg flex-col gap-4 px-4"
|
|
104
|
+
use:enhance={({ formData, cancel }) => {
|
|
105
|
+
fieldErrors = {};
|
|
106
|
+
internalError = '';
|
|
107
|
+
const errs = validateAll(formData);
|
|
108
|
+
if (Object.keys(errs).length > 0) {
|
|
109
|
+
fieldErrors = errs;
|
|
110
|
+
cancel();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
return async ({ result, update }) => {
|
|
114
|
+
if (result.type === 'success' || result.type === 'redirect') {
|
|
115
|
+
await update({ reset: false });
|
|
116
|
+
if (continueCreating) {
|
|
117
|
+
record = emptyRecord();
|
|
118
|
+
fieldErrors = {};
|
|
119
|
+
internalError = '';
|
|
120
|
+
} else {
|
|
121
|
+
onSuccess?.();
|
|
122
|
+
}
|
|
123
|
+
} else if (result.type === 'error') {
|
|
124
|
+
internalError = result.error?.message ?? strings.serverError;
|
|
125
|
+
} else {
|
|
126
|
+
await update({ reset: false });
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
{#each fields as field (field.attribute)}
|
|
132
|
+
<Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} />
|
|
133
|
+
{/each}
|
|
134
|
+
|
|
135
|
+
<div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
|
|
136
|
+
<Button variant="ghost" onclick={() => onCancel?.()}>
|
|
137
|
+
{strings.cancel}
|
|
138
|
+
</Button>
|
|
139
|
+
<Button type="submit" variant="secondary" onclick={() => (continueCreating = true)}>
|
|
140
|
+
{strings.saveAndContinue}
|
|
141
|
+
</Button>
|
|
142
|
+
<Button type="submit" variant="primary" onclick={() => (continueCreating = false)}>
|
|
143
|
+
{strings.save}
|
|
144
|
+
</Button>
|
|
145
|
+
</div>
|
|
146
|
+
</form>
|
|
147
|
+
</div>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
|
|
2
|
+
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
3
|
+
props: {
|
|
4
|
+
labelOne?: string;
|
|
5
|
+
labelMany?: string;
|
|
6
|
+
icon?: any;
|
|
7
|
+
fields?: FieldDefinition<T>[];
|
|
8
|
+
creation?: ActionConfiguration<T>;
|
|
9
|
+
serverError?: string;
|
|
10
|
+
onCancel?: () => void;
|
|
11
|
+
onSuccess?: () => void;
|
|
12
|
+
};
|
|
13
|
+
exports: {};
|
|
14
|
+
bindings: "";
|
|
15
|
+
slots: {};
|
|
16
|
+
events: {};
|
|
17
|
+
};
|
|
18
|
+
declare class __sveltets_Render<T extends object = Record<string, unknown>> {
|
|
19
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
20
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
21
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
22
|
+
bindings(): "";
|
|
23
|
+
exports(): {};
|
|
24
|
+
}
|
|
25
|
+
interface $$IsomorphicComponent {
|
|
26
|
+
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']>> & {
|
|
27
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
28
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
29
|
+
<T extends object = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
30
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
31
|
+
}
|
|
32
|
+
declare const Create: $$IsomorphicComponent;
|
|
33
|
+
type Create<T extends object = Record<string, unknown>> = InstanceType<typeof Create<T>>;
|
|
34
|
+
export default Create;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
3
|
+
import { invalidateAll } from '$app/navigation';
|
|
4
|
+
import Button from '../../form/Button.svelte';
|
|
5
|
+
import Header from '../../common/Header.svelte';
|
|
6
|
+
import PaginatedTable from '../../table/PaginatedTable.svelte';
|
|
7
|
+
import { getIconSet } from '../../../icons/context.js';
|
|
8
|
+
import { defaultIconSet } from '../../../icons/sets/default.js';
|
|
9
|
+
import type {
|
|
10
|
+
ActionConfiguration,
|
|
11
|
+
ColumnDefinition,
|
|
12
|
+
CustomAction,
|
|
13
|
+
RowAction
|
|
14
|
+
} from '../../../types/crud.js';
|
|
15
|
+
import { getStrings } from '../../../i18n/context.js';
|
|
16
|
+
|
|
17
|
+
const strings = getStrings();
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
data = [] as T[],
|
|
21
|
+
labelOne = '',
|
|
22
|
+
labelMany = '',
|
|
23
|
+
icon,
|
|
24
|
+
pageSize = 10,
|
|
25
|
+
creation = {} as ActionConfiguration<T>,
|
|
26
|
+
update = {} as ActionConfiguration<T>,
|
|
27
|
+
read = {} as ActionConfiguration<T>,
|
|
28
|
+
deletion = {} as ActionConfiguration<T>,
|
|
29
|
+
actions = [] as CustomAction<T>[],
|
|
30
|
+
columns = [] as ColumnDefinition<T>[],
|
|
31
|
+
onCreate,
|
|
32
|
+
onEdit,
|
|
33
|
+
onView,
|
|
34
|
+
onAction,
|
|
35
|
+
}: {
|
|
36
|
+
data?: T[];
|
|
37
|
+
labelOne?: string;
|
|
38
|
+
labelMany?: string;
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
icon?: any;
|
|
41
|
+
pageSize?: number;
|
|
42
|
+
creation?: ActionConfiguration<T>;
|
|
43
|
+
update?: ActionConfiguration<T>;
|
|
44
|
+
read?: ActionConfiguration<T>;
|
|
45
|
+
deletion?: ActionConfiguration<T>;
|
|
46
|
+
actions?: CustomAction<T>[];
|
|
47
|
+
columns?: ColumnDefinition<T>[];
|
|
48
|
+
onCreate?: () => void;
|
|
49
|
+
onEdit?: (item: T) => void;
|
|
50
|
+
onView?: (item: T) => void;
|
|
51
|
+
onAction?: (action: CustomAction<T>, item: T) => void;
|
|
52
|
+
} = $props();
|
|
53
|
+
|
|
54
|
+
const icons = $derived(getIconSet() ?? defaultIconSet);
|
|
55
|
+
const entityIcon = $derived(icon ?? icons.folder);
|
|
56
|
+
|
|
57
|
+
const allowRead = $derived(read.enabled ?? true);
|
|
58
|
+
const allowUpdate = $derived(update.enabled ?? true);
|
|
59
|
+
const allowDelete = $derived(deletion.enabled ?? true);
|
|
60
|
+
const deleteLabel = $derived(deletion.label ?? strings.delete);
|
|
61
|
+
const updateLabel = $derived(update.label ?? strings.edit);
|
|
62
|
+
const readLabel = $derived(read.label ?? strings.view);
|
|
63
|
+
const showRowActions = $derived(allowRead || allowUpdate || allowDelete || actions.length > 0);
|
|
64
|
+
|
|
65
|
+
let selected = new SvelteSet<number>();
|
|
66
|
+
|
|
67
|
+
async function runEndpointAction(endpoint: string, items: T[]) {
|
|
68
|
+
await Promise.all(items.map((item) => {
|
|
69
|
+
const fd = new FormData();
|
|
70
|
+
fd.set('id', String((item as Record<string, unknown>)._id ?? ''));
|
|
71
|
+
return fetch(endpoint, { method: 'POST', body: fd });
|
|
72
|
+
}));
|
|
73
|
+
await invalidateAll();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function runDeletion(items: T[]) {
|
|
77
|
+
if (deletion.endpoint) {
|
|
78
|
+
await runEndpointAction(deletion.endpoint, items);
|
|
79
|
+
} else {
|
|
80
|
+
await deletion.callback?.(items);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function handleCreate() { onCreate?.(); }
|
|
85
|
+
|
|
86
|
+
async function handleDelete() {
|
|
87
|
+
const items = [...selected].map(i => data[i]);
|
|
88
|
+
selected.clear();
|
|
89
|
+
await runDeletion(items);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rowActions = $derived<RowAction<T>[]>([
|
|
93
|
+
...actions.map((action) => ({
|
|
94
|
+
label: action.label,
|
|
95
|
+
icon: action.icon,
|
|
96
|
+
condition: action.condition,
|
|
97
|
+
run: (item: T) => onAction?.(action, item),
|
|
98
|
+
})),
|
|
99
|
+
...(allowRead ? [{ label: readLabel, icon: icons.view, run: (item: T) => onView?.(item) }] : []),
|
|
100
|
+
...(allowUpdate ? [{ label: updateLabel, icon: icons.edit, run: (item: T) => onEdit?.(item) }] : []),
|
|
101
|
+
...(allowDelete ? [{ label: deleteLabel, icon: icons.delete, class: 'text-error', run: (item: T) => runDeletion([item]) }] : []),
|
|
102
|
+
]);
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
{#snippet actionsCell(item: T)}
|
|
106
|
+
<div class="flex justify-end gap-1">
|
|
107
|
+
{#each rowActions as action (action.label)}
|
|
108
|
+
{#if action.condition?.(item) ?? true}
|
|
109
|
+
{@const Icon = action.icon}
|
|
110
|
+
<Button
|
|
111
|
+
variant="ghost"
|
|
112
|
+
class={['btn-xs', action.class]}
|
|
113
|
+
title={action.label}
|
|
114
|
+
aria-label={action.label}
|
|
115
|
+
onclick={(e) => { e.stopPropagation(); action.run(item); }}
|
|
116
|
+
>
|
|
117
|
+
<Icon class="size-4" />
|
|
118
|
+
</Button>
|
|
119
|
+
{/if}
|
|
120
|
+
{/each}
|
|
121
|
+
</div>
|
|
122
|
+
{/snippet}
|
|
123
|
+
|
|
124
|
+
<div class="flex flex-col gap-6">
|
|
125
|
+
|
|
126
|
+
<Header
|
|
127
|
+
admin
|
|
128
|
+
title={labelMany}
|
|
129
|
+
breadcrumbs={[
|
|
130
|
+
{ label: labelMany, icon: entityIcon, link: { href: '#' }, prominent: true },
|
|
131
|
+
]}
|
|
132
|
+
>
|
|
133
|
+
{#snippet buttons()}
|
|
134
|
+
{#if allowDelete}
|
|
135
|
+
{@const DeleteIcon = icons.delete}
|
|
136
|
+
<Button
|
|
137
|
+
variant="error"
|
|
138
|
+
class="btn-outline"
|
|
139
|
+
disabled={selected.size === 0}
|
|
140
|
+
onclick={handleDelete}
|
|
141
|
+
>
|
|
142
|
+
<DeleteIcon class="size-4" />
|
|
143
|
+
{deleteLabel} ({selected.size})
|
|
144
|
+
</Button>
|
|
145
|
+
{/if}
|
|
146
|
+
|
|
147
|
+
{@const CreateIcon = icons.create}
|
|
148
|
+
<Button
|
|
149
|
+
variant="primary"
|
|
150
|
+
onclick={handleCreate}
|
|
151
|
+
>
|
|
152
|
+
<CreateIcon class="size-5" />
|
|
153
|
+
{#if creation.label}
|
|
154
|
+
<span>{creation.label}</span>
|
|
155
|
+
{:else}
|
|
156
|
+
<span>{strings.create}<span class="hidden sm:inline"> {labelOne}</span></span>
|
|
157
|
+
{/if}
|
|
158
|
+
</Button>
|
|
159
|
+
{/snippet}
|
|
160
|
+
</Header>
|
|
161
|
+
|
|
162
|
+
<PaginatedTable
|
|
163
|
+
{data}
|
|
164
|
+
{columns}
|
|
165
|
+
{pageSize}
|
|
166
|
+
selectable={allowDelete}
|
|
167
|
+
{selected}
|
|
168
|
+
rowActions={showRowActions ? actionsCell : undefined}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ActionConfiguration, ColumnDefinition, CustomAction } from '../../../types/crud.js';
|
|
2
|
+
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
3
|
+
props: {
|
|
4
|
+
data?: T[];
|
|
5
|
+
labelOne?: string;
|
|
6
|
+
labelMany?: string;
|
|
7
|
+
icon?: any;
|
|
8
|
+
pageSize?: number;
|
|
9
|
+
creation?: ActionConfiguration<T>;
|
|
10
|
+
update?: ActionConfiguration<T>;
|
|
11
|
+
read?: ActionConfiguration<T>;
|
|
12
|
+
deletion?: ActionConfiguration<T>;
|
|
13
|
+
actions?: CustomAction<T>[];
|
|
14
|
+
columns?: ColumnDefinition<T>[];
|
|
15
|
+
onCreate?: () => void;
|
|
16
|
+
onEdit?: (item: T) => void;
|
|
17
|
+
onView?: (item: T) => void;
|
|
18
|
+
onAction?: (action: CustomAction<T>, item: T) => void;
|
|
19
|
+
};
|
|
20
|
+
exports: {};
|
|
21
|
+
bindings: "";
|
|
22
|
+
slots: {};
|
|
23
|
+
events: {};
|
|
24
|
+
};
|
|
25
|
+
declare class __sveltets_Render<T extends object = Record<string, unknown>> {
|
|
26
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
27
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
28
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
29
|
+
bindings(): "";
|
|
30
|
+
exports(): {};
|
|
31
|
+
}
|
|
32
|
+
interface $$IsomorphicComponent {
|
|
33
|
+
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']>> & {
|
|
34
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
35
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
36
|
+
<T extends object = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
37
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
38
|
+
}
|
|
39
|
+
declare const List: $$IsomorphicComponent;
|
|
40
|
+
type List<T extends object = Record<string, unknown>> = InstanceType<typeof List<T>>;
|
|
41
|
+
export default List;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { deserialize } from '$app/forms';
|
|
4
|
+
import Field from '../Field.svelte';
|
|
5
|
+
import Button from '../../form/Button.svelte';
|
|
6
|
+
import Header from '../../common/Header.svelte';
|
|
7
|
+
import { getIconSet } from '../../../icons/context.js';
|
|
8
|
+
import { defaultIconSet } from '../../../icons/sets/default.js';
|
|
9
|
+
import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
|
|
10
|
+
import { getStrings } from '../../../i18n/context.js';
|
|
11
|
+
|
|
12
|
+
const strings = getStrings();
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
labelOne = '',
|
|
16
|
+
labelMany = '',
|
|
17
|
+
icon,
|
|
18
|
+
fields = [] as FieldDefinition<T>[],
|
|
19
|
+
instance = {} as T,
|
|
20
|
+
read = {} as ActionConfiguration<T>,
|
|
21
|
+
onCancel,
|
|
22
|
+
}: {
|
|
23
|
+
labelOne?: string;
|
|
24
|
+
labelMany?: string;
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
icon?: any;
|
|
27
|
+
fields?: FieldDefinition<T>[];
|
|
28
|
+
instance?: T;
|
|
29
|
+
read?: ActionConfiguration<T>;
|
|
30
|
+
onCancel?: () => void;
|
|
31
|
+
} = $props();
|
|
32
|
+
|
|
33
|
+
const icons = $derived(getIconSet() ?? defaultIconSet);
|
|
34
|
+
const entityIcon = $derived(icon ?? icons.folder);
|
|
35
|
+
|
|
36
|
+
let record = $state<Record<string, unknown>>(untrack(() => instance as Record<string, unknown>));
|
|
37
|
+
|
|
38
|
+
$effect(() => {
|
|
39
|
+
const endpoint = read.endpoint;
|
|
40
|
+
const id = (instance as Record<string, unknown>)._id;
|
|
41
|
+
if (!endpoint || id == null) return;
|
|
42
|
+
|
|
43
|
+
let cancelled = false;
|
|
44
|
+
(async () => {
|
|
45
|
+
const fd = new FormData();
|
|
46
|
+
fd.set('id', String(id));
|
|
47
|
+
const res = await fetch(endpoint, { method: 'POST', body: fd });
|
|
48
|
+
const result = deserialize(await res.text());
|
|
49
|
+
if (!cancelled && result.type === 'success') {
|
|
50
|
+
const data = result.data as Record<string, unknown> | undefined;
|
|
51
|
+
const fetched = data
|
|
52
|
+
? Object.values(data).find(
|
|
53
|
+
(v) => v !== null && typeof v === 'object' && !Array.isArray(v) && '_id' in (v as object)
|
|
54
|
+
)
|
|
55
|
+
: undefined;
|
|
56
|
+
if (fetched && typeof fetched === 'object') record = fetched as Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
return () => { cancelled = true; };
|
|
60
|
+
});
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<div class="flex flex-col gap-6">
|
|
64
|
+
|
|
65
|
+
<Header
|
|
66
|
+
admin
|
|
67
|
+
title={labelMany}
|
|
68
|
+
breadcrumbs={[
|
|
69
|
+
{ label: labelMany, icon: entityIcon, link: { href: '#', onclick: (e) => { e.preventDefault(); onCancel?.(); } }, prominent: true },
|
|
70
|
+
{ label: read.label ?? labelOne, icon: icons.view },
|
|
71
|
+
]}
|
|
72
|
+
/>
|
|
73
|
+
|
|
74
|
+
<div class="mx-auto flex w-full max-w-lg flex-col gap-4 px-4">
|
|
75
|
+
{#each fields as field (field.attribute)}
|
|
76
|
+
<Field {field} {record} readonly />
|
|
77
|
+
{/each}
|
|
78
|
+
|
|
79
|
+
<div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
|
|
80
|
+
<Button variant="ghost" onclick={() => onCancel?.()}>
|
|
81
|
+
{strings.back}
|
|
82
|
+
</Button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
|
|
2
|
+
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
3
|
+
props: {
|
|
4
|
+
labelOne?: string;
|
|
5
|
+
labelMany?: string;
|
|
6
|
+
icon?: any;
|
|
7
|
+
fields?: FieldDefinition<T>[];
|
|
8
|
+
instance?: T;
|
|
9
|
+
read?: ActionConfiguration<T>;
|
|
10
|
+
onCancel?: () => void;
|
|
11
|
+
};
|
|
12
|
+
exports: {};
|
|
13
|
+
bindings: "";
|
|
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(): "";
|
|
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 Read: $$IsomorphicComponent;
|
|
32
|
+
type Read<T extends object = Record<string, unknown>> = InstanceType<typeof Read<T>>;
|
|
33
|
+
export default Read;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { enhance } from '$app/forms';
|
|
4
|
+
import Field from '../Field.svelte';
|
|
5
|
+
import Button from '../../form/Button.svelte';
|
|
6
|
+
import Header from '../../common/Header.svelte';
|
|
7
|
+
import { getIconSet } from '../../../icons/context.js';
|
|
8
|
+
import { defaultIconSet } from '../../../icons/sets/default.js';
|
|
9
|
+
import { fieldLabel } from '../utils/misc.js';
|
|
10
|
+
import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
|
|
11
|
+
import { getStrings } from '../../../i18n/context.js';
|
|
12
|
+
|
|
13
|
+
const strings = getStrings();
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
labelOne = '',
|
|
17
|
+
labelMany = '',
|
|
18
|
+
icon,
|
|
19
|
+
fields = [] as FieldDefinition<T>[],
|
|
20
|
+
instance = {} as T,
|
|
21
|
+
update = {} as ActionConfiguration<T>,
|
|
22
|
+
serverError = '',
|
|
23
|
+
onCancel,
|
|
24
|
+
onSuccess,
|
|
25
|
+
}: {
|
|
26
|
+
labelOne?: string;
|
|
27
|
+
labelMany?: string;
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
icon?: any;
|
|
30
|
+
fields?: FieldDefinition<T>[];
|
|
31
|
+
instance?: T;
|
|
32
|
+
update?: ActionConfiguration<T>;
|
|
33
|
+
serverError?: string;
|
|
34
|
+
onCancel?: () => void;
|
|
35
|
+
onSuccess?: () => void;
|
|
36
|
+
} = $props();
|
|
37
|
+
|
|
38
|
+
const icons = $derived(getIconSet() ?? defaultIconSet);
|
|
39
|
+
const entityIcon = $derived(icon ?? icons.folder);
|
|
40
|
+
|
|
41
|
+
let fieldErrors = $state<Record<string, string>>({});
|
|
42
|
+
let internalError = $state('');
|
|
43
|
+
|
|
44
|
+
function seedFromInstance(inst: Record<string, unknown>): Record<string, unknown> {
|
|
45
|
+
const seeded: Record<string, unknown> = { ...inst };
|
|
46
|
+
for (const f of fields) {
|
|
47
|
+
if (f.type !== 'boolean' && f.type !== 'file') {
|
|
48
|
+
seeded[f.attribute] = String(inst[f.attribute] ?? '');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return seeded;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let record = $state<Record<string, unknown>>(
|
|
55
|
+
untrack(() => seedFromInstance(instance as Record<string, unknown>))
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
$effect(() => {
|
|
59
|
+
const id = (instance as Record<string, unknown>)._id;
|
|
60
|
+
if (!id) return;
|
|
61
|
+
untrack(() => { record = seedFromInstance(instance as Record<string, unknown>); });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
function validateAll(formData: FormData): Record<string, string> {
|
|
65
|
+
const errs: Record<string, string> = {};
|
|
66
|
+
for (const field of fields) {
|
|
67
|
+
if (field.required) {
|
|
68
|
+
const val = String(formData.get(field.attribute) ?? '').trim();
|
|
69
|
+
if (!val) errs[field.attribute] = strings.required(fieldLabel(field));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return errs;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const hasFileField = $derived(fields.some((f) => f.type === 'file'));
|
|
76
|
+
|
|
77
|
+
const errorEntries = $derived([
|
|
78
|
+
...((serverError || internalError) ? [['_global', internalError || serverError] as [string, string]] : []),
|
|
79
|
+
...Object.entries(fieldErrors),
|
|
80
|
+
]);
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<div class="flex flex-col gap-6">
|
|
84
|
+
|
|
85
|
+
<Header
|
|
86
|
+
admin
|
|
87
|
+
title={labelMany}
|
|
88
|
+
breadcrumbs={[
|
|
89
|
+
{ label: labelMany, icon: entityIcon, link: { href: '#', onclick: (e) => { e.preventDefault(); onCancel?.(); } }, prominent: true },
|
|
90
|
+
{ label: update.label ?? labelOne, icon: icons.edit },
|
|
91
|
+
]}
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
{#if errorEntries.length > 0}
|
|
95
|
+
<div role="alert" class="alert alert-error">
|
|
96
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
97
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
98
|
+
</svg>
|
|
99
|
+
<ul class="list-disc list-inside text-sm">
|
|
100
|
+
{#each errorEntries as [key, msg] (key)}
|
|
101
|
+
<li>{msg}</li>
|
|
102
|
+
{/each}
|
|
103
|
+
</ul>
|
|
104
|
+
</div>
|
|
105
|
+
{/if}
|
|
106
|
+
|
|
107
|
+
<form
|
|
108
|
+
method="POST"
|
|
109
|
+
action={update.endpoint ?? '?/update'}
|
|
110
|
+
enctype={hasFileField ? 'multipart/form-data' : undefined}
|
|
111
|
+
class="mx-auto flex w-full max-w-lg flex-col gap-4 px-4"
|
|
112
|
+
use:enhance={({ formData, cancel }) => {
|
|
113
|
+
fieldErrors = {};
|
|
114
|
+
internalError = '';
|
|
115
|
+
const errs = validateAll(formData);
|
|
116
|
+
if (Object.keys(errs).length > 0) {
|
|
117
|
+
fieldErrors = errs;
|
|
118
|
+
cancel();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
return async ({ result, update: updateForm }) => {
|
|
122
|
+
if (result.type === 'success' || result.type === 'redirect') {
|
|
123
|
+
await updateForm({ reset: false });
|
|
124
|
+
onSuccess?.();
|
|
125
|
+
} else if (result.type === 'error') {
|
|
126
|
+
internalError = result.error?.message ?? strings.serverError;
|
|
127
|
+
} else {
|
|
128
|
+
await updateForm({ reset: false });
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
<input type="hidden" name="id" value={String(record._id ?? '')} />
|
|
134
|
+
|
|
135
|
+
{#each fields as field (field.attribute)}
|
|
136
|
+
<Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} />
|
|
137
|
+
{/each}
|
|
138
|
+
|
|
139
|
+
<div class="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
|
|
140
|
+
<Button variant="ghost" onclick={() => onCancel?.()}>
|
|
141
|
+
{strings.cancel}
|
|
142
|
+
</Button>
|
|
143
|
+
<Button type="submit" variant="primary">
|
|
144
|
+
{strings.save}
|
|
145
|
+
</Button>
|
|
146
|
+
</div>
|
|
147
|
+
</form>
|
|
148
|
+
</div>
|