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,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import Breadcrumbs from '../navigation/Breadcrumbs.svelte';
|
|
4
|
+
import type { BreadcrumbItem } from '../../types/breadcrumb.js';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
title,
|
|
8
|
+
breadcrumbs = [],
|
|
9
|
+
buttons,
|
|
10
|
+
admin = false,
|
|
11
|
+
}: {
|
|
12
|
+
title: string;
|
|
13
|
+
breadcrumbs?: BreadcrumbItem[];
|
|
14
|
+
buttons?: Snippet;
|
|
15
|
+
admin?: boolean;
|
|
16
|
+
} = $props();
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
20
|
+
<div class="flex flex-col gap-1">
|
|
21
|
+
<h1>{title}</h1>
|
|
22
|
+
<Breadcrumbs items={breadcrumbs} {admin} />
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
{#if buttons}
|
|
26
|
+
<div class="flex items-center gap-2 pt-1">
|
|
27
|
+
{@render buttons()}
|
|
28
|
+
</div>
|
|
29
|
+
{/if}
|
|
30
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { BreadcrumbItem } from '../../types/breadcrumb.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
title: string;
|
|
5
|
+
breadcrumbs?: BreadcrumbItem[];
|
|
6
|
+
buttons?: Snippet;
|
|
7
|
+
admin?: boolean;
|
|
8
|
+
};
|
|
9
|
+
declare const Header: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type Header = ReturnType<typeof Header>;
|
|
11
|
+
export default Header;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
+
import Avatar from '../Avatar.svelte';
|
|
3
|
+
import Label from '../form/Label.svelte';
|
|
4
|
+
import Select from '../form/Select.svelte';
|
|
5
|
+
import { fieldLabel, initials } from './utils/misc.js';
|
|
6
|
+
import type { FieldDefinition } from '../../types/crud.js';
|
|
7
|
+
import { getStrings } from '../../i18n/context.js';
|
|
8
|
+
|
|
9
|
+
const strings = getStrings();
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
field,
|
|
13
|
+
record = $bindable({} as Record<string, unknown>),
|
|
14
|
+
error = '',
|
|
15
|
+
readonly = false,
|
|
16
|
+
}: {
|
|
17
|
+
field: FieldDefinition<T>;
|
|
18
|
+
record?: Record<string, unknown>;
|
|
19
|
+
error?: string;
|
|
20
|
+
readonly?: boolean;
|
|
21
|
+
} = $props();
|
|
22
|
+
|
|
23
|
+
const name = $derived(readonly ? undefined : field.attribute);
|
|
24
|
+
const labelText = $derived(fieldLabel(field));
|
|
25
|
+
let filePreview = $state<string | null>(null);
|
|
26
|
+
|
|
27
|
+
function onFileChange(e: Event & { currentTarget: HTMLInputElement }) {
|
|
28
|
+
const file = e.currentTarget.files?.[0];
|
|
29
|
+
if (filePreview) URL.revokeObjectURL(filePreview);
|
|
30
|
+
filePreview = file && file.type.startsWith('image/') ? URL.createObjectURL(file) : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
$effect(() => () => {
|
|
34
|
+
if (filePreview) URL.revokeObjectURL(filePreview);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const saved = $derived(record[field.attribute]);
|
|
38
|
+
const preview = $derived(filePreview ?? (typeof saved === 'string' && saved ? saved : null));
|
|
39
|
+
const avatarInitials = $derived(initials(record.firstName as string, record.lastName as string));
|
|
40
|
+
const displayValue = $derived(saved == null ? '' : String(saved));
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<div class="flex flex-col gap-1">
|
|
44
|
+
{#if field.type === 'file'}
|
|
45
|
+
<div class="flex justify-center">
|
|
46
|
+
<Avatar src={preview} text={avatarInitials} alt={labelText} class="w-32 rounded-full" textClass="text-3xl" />
|
|
47
|
+
</div>
|
|
48
|
+
{/if}
|
|
49
|
+
|
|
50
|
+
<Label
|
|
51
|
+
text={labelText}
|
|
52
|
+
for={field.attribute}
|
|
53
|
+
capitalize={true}
|
|
54
|
+
required={field.required && !readonly}
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
{#if field.type === 'boolean'}
|
|
58
|
+
<input
|
|
59
|
+
type="checkbox"
|
|
60
|
+
id={field.attribute}
|
|
61
|
+
{name}
|
|
62
|
+
class="toggle"
|
|
63
|
+
checked={!!saved}
|
|
64
|
+
disabled={readonly}
|
|
65
|
+
/>
|
|
66
|
+
{:else if field.type === 'file'}
|
|
67
|
+
{#if !readonly}
|
|
68
|
+
<fieldset class="fieldset w-full p-0">
|
|
69
|
+
<input
|
|
70
|
+
type="file"
|
|
71
|
+
id={field.attribute}
|
|
72
|
+
{name}
|
|
73
|
+
class="file-input w-full"
|
|
74
|
+
class:file-input-error={!!error}
|
|
75
|
+
onchange={onFileChange}
|
|
76
|
+
/>
|
|
77
|
+
{#if field.placeholder}
|
|
78
|
+
<Label text={field.placeholder} for={field.attribute} class="label" />
|
|
79
|
+
{/if}
|
|
80
|
+
</fieldset>
|
|
81
|
+
{/if}
|
|
82
|
+
<!-- read-only file value is shown as the avatar above -->
|
|
83
|
+
{:else if field.type === 'select'}
|
|
84
|
+
{#if readonly}
|
|
85
|
+
<input
|
|
86
|
+
type="text"
|
|
87
|
+
id={field.attribute}
|
|
88
|
+
class="input input-bordered w-full"
|
|
89
|
+
value={field.options?.find((o) => o.value === String(saved))?.label ?? displayValue}
|
|
90
|
+
disabled
|
|
91
|
+
/>
|
|
92
|
+
{:else}
|
|
93
|
+
<Select
|
|
94
|
+
name={field.attribute}
|
|
95
|
+
bind:value={record[field.attribute] as string}
|
|
96
|
+
options={field.options ?? []}
|
|
97
|
+
placeholder={field.placeholder}
|
|
98
|
+
{error}
|
|
99
|
+
/>
|
|
100
|
+
{/if}
|
|
101
|
+
{:else if field.type === 'datetime'}
|
|
102
|
+
{#if readonly}
|
|
103
|
+
<input
|
|
104
|
+
type="text"
|
|
105
|
+
id={field.attribute}
|
|
106
|
+
class="input input-bordered w-full"
|
|
107
|
+
value={displayValue}
|
|
108
|
+
disabled
|
|
109
|
+
/>
|
|
110
|
+
{:else}
|
|
111
|
+
<input type="hidden" {name} value={String(record[field.attribute] ?? '')} />
|
|
112
|
+
<calendar-date
|
|
113
|
+
class="cally rounded-box border border-base-300 bg-base-100 shadow-sm"
|
|
114
|
+
value={String(record[field.attribute] ?? '')}
|
|
115
|
+
onchange={(e: Event) => { record[field.attribute] = (e as Event & { detail: { value: string } }).detail.value; }}
|
|
116
|
+
>
|
|
117
|
+
<svg aria-label={strings.previous} class="fill-current size-4" {...{"slot": "previous"}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M15.75 19.5 8.25 12l7.5-7.5"/></svg>
|
|
118
|
+
<svg aria-label={strings.next} class="fill-current size-4" {...{"slot": "next"}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="m8.25 4.5 7.5 7.5-7.5 7.5"/></svg>
|
|
119
|
+
<calendar-month></calendar-month>
|
|
120
|
+
</calendar-date>
|
|
121
|
+
{/if}
|
|
122
|
+
{:else if field.type === 'textarea'}
|
|
123
|
+
{#if readonly}
|
|
124
|
+
<textarea id={field.attribute} class="textarea textarea-bordered w-full" value={displayValue} disabled></textarea>
|
|
125
|
+
{:else}
|
|
126
|
+
<textarea
|
|
127
|
+
id={field.attribute}
|
|
128
|
+
{name}
|
|
129
|
+
placeholder={field.placeholder ?? ''}
|
|
130
|
+
bind:value={record[field.attribute]}
|
|
131
|
+
class="textarea textarea-bordered w-full"
|
|
132
|
+
class:textarea-error={!!error}
|
|
133
|
+
></textarea>
|
|
134
|
+
{/if}
|
|
135
|
+
{:else if readonly}
|
|
136
|
+
<input
|
|
137
|
+
type={field.type ?? 'text'}
|
|
138
|
+
id={field.attribute}
|
|
139
|
+
class="input input-bordered w-full"
|
|
140
|
+
value={displayValue}
|
|
141
|
+
disabled
|
|
142
|
+
/>
|
|
143
|
+
{:else}
|
|
144
|
+
<input
|
|
145
|
+
type={field.type ?? 'text'}
|
|
146
|
+
id={field.attribute}
|
|
147
|
+
{name}
|
|
148
|
+
placeholder={field.placeholder ?? ''}
|
|
149
|
+
bind:value={record[field.attribute]}
|
|
150
|
+
autocomplete={field.autocomplete}
|
|
151
|
+
class="input input-bordered w-full"
|
|
152
|
+
class:input-error={!!error}
|
|
153
|
+
/>
|
|
154
|
+
{/if}
|
|
155
|
+
|
|
156
|
+
{#if error}
|
|
157
|
+
<span class="text-error text-xs">{error}</span>
|
|
158
|
+
{/if}
|
|
159
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
error?: string;
|
|
7
|
+
readonly?: boolean;
|
|
8
|
+
};
|
|
9
|
+
exports: {};
|
|
10
|
+
bindings: "record";
|
|
11
|
+
slots: {};
|
|
12
|
+
events: {};
|
|
13
|
+
};
|
|
14
|
+
declare class __sveltets_Render<T extends object = Record<string, unknown>> {
|
|
15
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
16
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
17
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
18
|
+
bindings(): "record";
|
|
19
|
+
exports(): {};
|
|
20
|
+
}
|
|
21
|
+
interface $$IsomorphicComponent {
|
|
22
|
+
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']>> & {
|
|
23
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
24
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
25
|
+
<T extends object = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
26
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
27
|
+
}
|
|
28
|
+
declare const Field: $$IsomorphicComponent;
|
|
29
|
+
type Field<T extends object = Record<string, unknown>> = InstanceType<typeof Field<T>>;
|
|
30
|
+
export default Field;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
+
import { page } from '$app/state';
|
|
3
|
+
import { goto } from '$app/navigation';
|
|
4
|
+
import List from './views/List.svelte';
|
|
5
|
+
import Read from './views/Read.svelte';
|
|
6
|
+
import Create from './views/Create.svelte';
|
|
7
|
+
import Update from './views/Update.svelte';
|
|
8
|
+
import { AUTO_EXCLUDED } from './utils/constants.js';
|
|
9
|
+
import { resolveOptions, resolveFormatter, inferType } from './utils/resolution.js';
|
|
10
|
+
import type { AttributeMetadata } from '../../types/attribute.js';
|
|
11
|
+
import type {
|
|
12
|
+
ActionConfiguration,
|
|
13
|
+
ColumnDefinition,
|
|
14
|
+
CustomAction,
|
|
15
|
+
FieldDefinition,
|
|
16
|
+
} from '../../types/crud.js';
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
data = undefined as Record<string, unknown> | undefined,
|
|
20
|
+
dataKey = undefined as string | undefined,
|
|
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 = undefined as ColumnDefinition<T>[] | undefined,
|
|
31
|
+
fields = undefined as FieldDefinition<T>[] | undefined,
|
|
32
|
+
meta = undefined as Partial<Record<string, AttributeMetadata>> | undefined,
|
|
33
|
+
form = null as { error?: string } | null,
|
|
34
|
+
}: {
|
|
35
|
+
data?: Record<string, unknown>;
|
|
36
|
+
dataKey?: string;
|
|
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
|
+
fields?: FieldDefinition<T>[];
|
|
49
|
+
meta?: Partial<Record<string, AttributeMetadata>>;
|
|
50
|
+
form?: { error?: string } | null;
|
|
51
|
+
} = $props();
|
|
52
|
+
|
|
53
|
+
const entityData = $derived<T[]>(
|
|
54
|
+
data && dataKey ? (data[dataKey] as T[] ?? []) : []
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const viewParam = $derived(page.url.searchParams.get('view'));
|
|
58
|
+
const idParam = $derived(page.url.searchParams.get('id'));
|
|
59
|
+
|
|
60
|
+
const creating = $derived(viewParam === 'create');
|
|
61
|
+
const reading = $derived(idParam !== null && viewParam === null);
|
|
62
|
+
const editing = $derived(idParam !== null && viewParam === 'edit');
|
|
63
|
+
|
|
64
|
+
const singleInstance = $derived<T | undefined>(
|
|
65
|
+
(Object.values(page.data as Record<string, unknown>).find(
|
|
66
|
+
(v) => v !== null && typeof v === 'object' && !Array.isArray(v) && '_id' in (v as object)
|
|
67
|
+
) as T | undefined)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
let activeAction = $state<{ action: CustomAction<T>; item: T } | null>(null);
|
|
71
|
+
|
|
72
|
+
async function navList() { await goto('?'); }
|
|
73
|
+
async function navCreate() { await goto('?view=create'); }
|
|
74
|
+
async function navRead(item: T) {
|
|
75
|
+
await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)._id ?? ''))}`);
|
|
76
|
+
}
|
|
77
|
+
async function navEdit(item: T) {
|
|
78
|
+
await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)._id ?? ''))}&view=edit`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const resolvedColumns: ColumnDefinition<T>[] = $derived(
|
|
82
|
+
columns ?? (meta
|
|
83
|
+
? (Object.entries(meta) as [string, AttributeMetadata][])
|
|
84
|
+
.filter(([, m]) => !m.excludedFromList)
|
|
85
|
+
.map(([k, m]) => ({
|
|
86
|
+
attribute: k as keyof T & string,
|
|
87
|
+
title: m.label ?? k,
|
|
88
|
+
type: m.type,
|
|
89
|
+
formatter: resolveFormatter(m, data),
|
|
90
|
+
component: m.component,
|
|
91
|
+
sortable: m.sortable,
|
|
92
|
+
filterable: m.filterable,
|
|
93
|
+
}))
|
|
94
|
+
: entityData.length > 0
|
|
95
|
+
? (Object.keys(entityData[0]) as (keyof T & string)[])
|
|
96
|
+
.filter((k) => k !== '_id')
|
|
97
|
+
.map((k) => ({ attribute: k, title: k }))
|
|
98
|
+
: [])
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const resolvedFields: FieldDefinition<T>[] = $derived(
|
|
102
|
+
fields ?? (meta
|
|
103
|
+
? (Object.entries(meta) as [string, AttributeMetadata][])
|
|
104
|
+
.filter(([k, m]) => !AUTO_EXCLUDED.has(k) && !m.excludedFromCreate)
|
|
105
|
+
.map(([k, m]) => ({
|
|
106
|
+
attribute: k as keyof T & string,
|
|
107
|
+
title: m.label,
|
|
108
|
+
type: m.type ?? inferType(k, undefined),
|
|
109
|
+
required: m.required,
|
|
110
|
+
autocomplete: m.autocomplete,
|
|
111
|
+
placeholder: m.placeholder,
|
|
112
|
+
default: m.default,
|
|
113
|
+
options: resolveOptions(m, data),
|
|
114
|
+
}))
|
|
115
|
+
: entityData.length > 0
|
|
116
|
+
? (Object.entries(entityData[0]) as [string, unknown][])
|
|
117
|
+
.filter(([k]) => !AUTO_EXCLUDED.has(k))
|
|
118
|
+
.map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
|
|
119
|
+
: [])
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const resolvedReadFields: FieldDefinition<T>[] = $derived(
|
|
123
|
+
fields ?? (meta
|
|
124
|
+
? (Object.entries(meta) as [string, AttributeMetadata][])
|
|
125
|
+
.filter(([k, m]) => !AUTO_EXCLUDED.has(k) && !m.excludedFromRead)
|
|
126
|
+
.map(([k, m]) => ({
|
|
127
|
+
attribute: k as keyof T & string,
|
|
128
|
+
title: m.label,
|
|
129
|
+
type: m.type ?? inferType(k, undefined),
|
|
130
|
+
required: m.required,
|
|
131
|
+
autocomplete: m.autocomplete,
|
|
132
|
+
placeholder: m.placeholder,
|
|
133
|
+
default: m.default,
|
|
134
|
+
options: resolveOptions(m, data),
|
|
135
|
+
}))
|
|
136
|
+
: entityData.length > 0
|
|
137
|
+
? (Object.entries(entityData[0]) as [string, unknown][])
|
|
138
|
+
.filter(([k]) => !AUTO_EXCLUDED.has(k))
|
|
139
|
+
.map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
|
|
140
|
+
: [])
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const resolvedUpdateFields: FieldDefinition<T>[] = $derived(
|
|
144
|
+
fields ?? (meta
|
|
145
|
+
? (Object.entries(meta) as [string, AttributeMetadata][])
|
|
146
|
+
.filter(([k, m]) => !AUTO_EXCLUDED.has(k) && !m.excludedFromUpdate)
|
|
147
|
+
.map(([k, m]) => ({
|
|
148
|
+
attribute: k as keyof T & string,
|
|
149
|
+
title: m.label,
|
|
150
|
+
type: m.type ?? inferType(k, undefined),
|
|
151
|
+
required: m.required,
|
|
152
|
+
autocomplete: m.autocomplete,
|
|
153
|
+
placeholder: m.placeholder,
|
|
154
|
+
default: m.default,
|
|
155
|
+
options: resolveOptions(m, data),
|
|
156
|
+
}))
|
|
157
|
+
: entityData.length > 0
|
|
158
|
+
? (Object.entries(entityData[0]) as [string, unknown][])
|
|
159
|
+
.filter(([k]) => !AUTO_EXCLUDED.has(k))
|
|
160
|
+
.map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
|
|
161
|
+
: [])
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const serverError = $derived(
|
|
165
|
+
creating || reading || editing || activeAction !== null
|
|
166
|
+
? (form?.error ?? '')
|
|
167
|
+
: ''
|
|
168
|
+
);
|
|
169
|
+
</script>
|
|
170
|
+
|
|
171
|
+
{#if creating}
|
|
172
|
+
<Create
|
|
173
|
+
{labelOne}
|
|
174
|
+
{labelMany}
|
|
175
|
+
{icon}
|
|
176
|
+
fields={resolvedFields}
|
|
177
|
+
{creation}
|
|
178
|
+
{serverError}
|
|
179
|
+
onCancel={navList}
|
|
180
|
+
onSuccess={navList}
|
|
181
|
+
/>
|
|
182
|
+
{:else if reading}
|
|
183
|
+
<Read
|
|
184
|
+
{labelOne}
|
|
185
|
+
{labelMany}
|
|
186
|
+
{icon}
|
|
187
|
+
fields={resolvedReadFields}
|
|
188
|
+
instance={singleInstance ?? {} as T}
|
|
189
|
+
{read}
|
|
190
|
+
onCancel={navList}
|
|
191
|
+
/>
|
|
192
|
+
{:else if editing}
|
|
193
|
+
<Update
|
|
194
|
+
{labelOne}
|
|
195
|
+
{labelMany}
|
|
196
|
+
{icon}
|
|
197
|
+
fields={resolvedUpdateFields}
|
|
198
|
+
instance={singleInstance ?? {} as T}
|
|
199
|
+
{update}
|
|
200
|
+
{serverError}
|
|
201
|
+
onCancel={navList}
|
|
202
|
+
onSuccess={navList}
|
|
203
|
+
/>
|
|
204
|
+
{:else}
|
|
205
|
+
<List
|
|
206
|
+
data={entityData}
|
|
207
|
+
{labelOne}
|
|
208
|
+
{labelMany}
|
|
209
|
+
{icon}
|
|
210
|
+
{pageSize}
|
|
211
|
+
{creation}
|
|
212
|
+
{update}
|
|
213
|
+
{read}
|
|
214
|
+
{deletion}
|
|
215
|
+
{actions}
|
|
216
|
+
columns={resolvedColumns}
|
|
217
|
+
onCreate={navCreate}
|
|
218
|
+
onEdit={navEdit}
|
|
219
|
+
onView={navRead}
|
|
220
|
+
onAction={(action, item) => {
|
|
221
|
+
if (action.condition?.(item) ?? true) activeAction = { action, item };
|
|
222
|
+
}}
|
|
223
|
+
/>
|
|
224
|
+
{/if}
|
|
225
|
+
|
|
226
|
+
{#if activeAction !== null}
|
|
227
|
+
{@const ActionView = activeAction.action.view}
|
|
228
|
+
<ActionView
|
|
229
|
+
instance={activeAction.item}
|
|
230
|
+
label={activeAction.action.label}
|
|
231
|
+
endpoint={activeAction.action.endpoint}
|
|
232
|
+
{serverError}
|
|
233
|
+
onCancel={() => (activeAction = null)}
|
|
234
|
+
onSuccess={() => (activeAction = null)}
|
|
235
|
+
/>
|
|
236
|
+
{/if}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AttributeMetadata } from '../../types/attribute.js';
|
|
2
|
+
import type { ActionConfiguration, ColumnDefinition, CustomAction, FieldDefinition } from '../../types/crud.js';
|
|
3
|
+
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
4
|
+
props: {
|
|
5
|
+
data?: Record<string, unknown>;
|
|
6
|
+
dataKey?: string;
|
|
7
|
+
labelOne?: string;
|
|
8
|
+
labelMany?: string;
|
|
9
|
+
icon?: any;
|
|
10
|
+
pageSize?: number;
|
|
11
|
+
creation?: ActionConfiguration<T>;
|
|
12
|
+
update?: ActionConfiguration<T>;
|
|
13
|
+
read?: ActionConfiguration<T>;
|
|
14
|
+
deletion?: ActionConfiguration<T>;
|
|
15
|
+
actions?: CustomAction<T>[];
|
|
16
|
+
columns?: ColumnDefinition<T>[];
|
|
17
|
+
fields?: FieldDefinition<T>[];
|
|
18
|
+
meta?: Partial<Record<string, AttributeMetadata>>;
|
|
19
|
+
form?: {
|
|
20
|
+
error?: string;
|
|
21
|
+
} | null;
|
|
22
|
+
};
|
|
23
|
+
exports: {};
|
|
24
|
+
bindings: "";
|
|
25
|
+
slots: {};
|
|
26
|
+
events: {};
|
|
27
|
+
};
|
|
28
|
+
declare class __sveltets_Render<T extends object = Record<string, unknown>> {
|
|
29
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
30
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
31
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
32
|
+
bindings(): "";
|
|
33
|
+
exports(): {};
|
|
34
|
+
}
|
|
35
|
+
interface $$IsomorphicComponent {
|
|
36
|
+
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']>> & {
|
|
37
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
38
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
39
|
+
<T extends object = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
40
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
41
|
+
}
|
|
42
|
+
declare const GenericCRUD: $$IsomorphicComponent;
|
|
43
|
+
type GenericCRUD<T extends object = Record<string, unknown>> = InstanceType<typeof GenericCRUD<T>>;
|
|
44
|
+
export default GenericCRUD;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import AvatarComponent from '../../Avatar.svelte';
|
|
3
|
+
import { initials } from '../utils/misc.js';
|
|
4
|
+
import type { CellProps } from '../../../types/table.js';
|
|
5
|
+
|
|
6
|
+
type AvatarRow = { firstName?: string; lastName?: string; email?: string };
|
|
7
|
+
|
|
8
|
+
let { value, row }: CellProps<AvatarRow, string | null | undefined> = $props();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<AvatarComponent
|
|
12
|
+
src={value ?? null}
|
|
13
|
+
text={initials(row.firstName, row.lastName)}
|
|
14
|
+
alt={row.email ?? ''}
|
|
15
|
+
/>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CellProps } from '../../../types/table.js';
|
|
2
|
+
declare const Avatar: import("svelte").Component<CellProps<{
|
|
3
|
+
firstName?: string;
|
|
4
|
+
lastName?: string;
|
|
5
|
+
email?: string;
|
|
6
|
+
}, string | null | undefined>, {}, "">;
|
|
7
|
+
type Avatar = ReturnType<typeof Avatar>;
|
|
8
|
+
export default Avatar;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import IconRenderer from '../../IconRenderer.svelte';
|
|
3
|
+
import type { CellProps } from '../../../types/table.js';
|
|
4
|
+
|
|
5
|
+
let { value }: CellProps<Record<string, unknown>, string> = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<IconRenderer name={String(value ?? '')} />
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const AUTO_EXCLUDED: Set<string>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const formatBoolean: (trueLabel?: string, falseLabel?: string) => () => (value: boolean) => string;
|
|
2
|
+
export declare const formatDatetime: (format?: string) => () => (value: Date) => string;
|
|
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;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const formatBoolean = (trueLabel = 'Sí', falseLabel = 'No') => () => (value) => (value ? trueLabel : falseLabel);
|
|
2
|
+
// Supported tokens: dd · mm · YYYY · HH · MM · ss
|
|
3
|
+
const TOKENS = {
|
|
4
|
+
dd: (d) => String(d.getDate()).padStart(2, '0'),
|
|
5
|
+
mm: (d) => String(d.getMonth() + 1).padStart(2, '0'),
|
|
6
|
+
YYYY: (d) => String(d.getFullYear()),
|
|
7
|
+
HH: (d) => String(d.getHours()).padStart(2, '0'),
|
|
8
|
+
MM: (d) => String(d.getMinutes()).padStart(2, '0'),
|
|
9
|
+
ss: (d) => String(d.getSeconds()).padStart(2, '0'),
|
|
10
|
+
};
|
|
11
|
+
export const formatDatetime = (format = 'dd/mm/YYYY HH:MM') => {
|
|
12
|
+
const fmt = (value) => {
|
|
13
|
+
const d = new Date(value);
|
|
14
|
+
if (isNaN(d.getTime()))
|
|
15
|
+
return '';
|
|
16
|
+
return format.replace(/dd|mm|YYYY|HH|MM|ss/g, (token) => TOKENS[token](d));
|
|
17
|
+
};
|
|
18
|
+
return () => fmt;
|
|
19
|
+
};
|
|
20
|
+
export function formatTruncateTextUpTo(maxLength) {
|
|
21
|
+
return () => (value) => {
|
|
22
|
+
const str = String(value ?? '');
|
|
23
|
+
return str.length > maxLength ? str.slice(0, maxLength) + '…' : str;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function escapeHtml(str) {
|
|
27
|
+
return str
|
|
28
|
+
.replace(/&/g, '&')
|
|
29
|
+
.replace(/</g, '<')
|
|
30
|
+
.replace(/>/g, '>')
|
|
31
|
+
.replace(/"/g, '"');
|
|
32
|
+
}
|
|
33
|
+
export function formatInstance(attribute, instances, urlPath) {
|
|
34
|
+
const map = new Map(instances.map((i) => [i._id, i]));
|
|
35
|
+
return (value) => {
|
|
36
|
+
const id = String(value ?? '');
|
|
37
|
+
const instance = map.get(id);
|
|
38
|
+
const label = instance ? String(instance[attribute] ?? id) : id;
|
|
39
|
+
const href = `${urlPath}?id=${encodeURIComponent(id)}`;
|
|
40
|
+
return `<a href="${href}" class="link link-primary">${escapeHtml(label)}</a>`;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function fieldLabel(field) {
|
|
2
|
+
return (field.title ?? field.attribute).replace(/_/g, ' ');
|
|
3
|
+
}
|
|
4
|
+
export function initials(firstName, lastName) {
|
|
5
|
+
const a = (firstName ?? '').trim().charAt(0);
|
|
6
|
+
const b = (lastName ?? '').trim().charAt(0);
|
|
7
|
+
return (a + b).toUpperCase() || '?';
|
|
8
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AttributeMetadata, AttributeType, SelectOption } from '../../../types/attribute.js';
|
|
2
|
+
export declare function resolveOptions(m: AttributeMetadata, d: unknown): SelectOption[] | undefined;
|
|
3
|
+
export declare function resolveFormatter(m: AttributeMetadata, d: unknown): import("../../../index.ts").CellFormatter<any, any> | undefined;
|
|
4
|
+
export declare function inferType(key: string, value: unknown): AttributeType;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function resolveOptions(m, d) {
|
|
2
|
+
if (!m.options)
|
|
3
|
+
return undefined;
|
|
4
|
+
return typeof m.options === 'function' ? m.options(d) : m.options;
|
|
5
|
+
}
|
|
6
|
+
export function resolveFormatter(m, d) {
|
|
7
|
+
return m.formatter?.(d);
|
|
8
|
+
}
|
|
9
|
+
export function inferType(key, value) {
|
|
10
|
+
if (typeof value === 'boolean')
|
|
11
|
+
return 'boolean';
|
|
12
|
+
if (typeof value === 'number')
|
|
13
|
+
return 'number';
|
|
14
|
+
const k = key.toLowerCase();
|
|
15
|
+
if (k.includes('email'))
|
|
16
|
+
return 'email';
|
|
17
|
+
if (k.includes('password') || k.includes('hash'))
|
|
18
|
+
return 'password';
|
|
19
|
+
if (k.includes('description') || k.includes('bio') || k.includes('notes') || k.includes('content'))
|
|
20
|
+
return 'textarea';
|
|
21
|
+
return 'text';
|
|
22
|
+
}
|