@svadmin/lite 0.3.12 → 0.3.14
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 +30 -5
- package/dist/components/LiteArrayItem.svelte +31 -10
- package/dist/components/LiteAuditLog.svelte +8 -17
- package/dist/components/LiteAuditLog.svelte.d.ts +5 -13
- package/dist/components/LiteForm.svelte +37 -7
- package/dist/components/LiteForm.svelte.d.ts +1 -1
- package/dist/components/LiteLayout.svelte +12 -7
- package/dist/components/LiteLayout.svelte.d.ts +2 -0
- package/dist/components/LitePermissionMatrix.svelte +48 -15
- package/dist/components/LitePermissionMatrix.svelte.d.ts +10 -9
- package/dist/components/LiteShow.svelte +3 -2
- package/dist/components/LiteShowField.svelte +5 -3
- package/dist/components/LiteTable.svelte +19 -9
- package/dist/components/LiteTable.svelte.d.ts +1 -0
- package/dist/components/advanced/LiteVirtualTable.svelte +5 -2
- package/dist/components/advanced/LiteVirtualTable.svelte.d.ts +1 -0
- package/dist/components/buttons/LiteCloneButton.svelte +2 -2
- package/dist/components/buttons/LiteCreateButton.svelte +2 -2
- package/dist/components/buttons/LiteDeleteButton.svelte +2 -2
- package/dist/components/buttons/LiteEditButton.svelte +2 -2
- package/dist/components/buttons/LiteExportButton.svelte +2 -2
- package/dist/components/buttons/LiteImportButton.svelte +2 -2
- package/dist/components/buttons/LiteListButton.svelte +2 -2
- package/dist/components/buttons/LiteRefreshButton.svelte +7 -11
- package/dist/components/buttons/LiteSaveButton.svelte +2 -2
- package/dist/components/buttons/LiteShowButton.svelte +2 -2
- package/dist/components/fields/LiteBooleanField.svelte +9 -8
- package/dist/components/fields/LiteDateField.svelte +2 -2
- package/dist/components/fields/LiteEmailField.svelte +1 -2
- package/dist/components/fields/LiteImageField.svelte +20 -11
- package/dist/components/fields/LiteNumberField.svelte +1 -2
- package/dist/components/fields/LiteRelationField.svelte +15 -7
- package/dist/components/fields/LiteSelectField.svelte +2 -1
- package/dist/components/fields/LiteTextField.svelte +1 -2
- package/dist/components/fields/LiteUrlField.svelte +1 -5
- package/dist/components/layout/LiteCatchAllNavigate.svelte +3 -9
- package/dist/components/layout/LiteNavigateToResource.svelte +5 -16
- package/dist/components/layout/LiteSidebar.svelte +9 -5
- package/dist/components/layout/LiteSidebar.svelte.d.ts +1 -0
- package/dist/components/pages/LiteEditPage.svelte +6 -4
- package/dist/components/pages/LiteListPage.svelte +14 -6
- package/dist/components/pages/LiteListPage.svelte.d.ts +1 -0
- package/dist/components/pages/LiteShowPage.svelte +6 -4
- package/dist/components/widgets/LiteAnomalyBadge.svelte +5 -1
- package/dist/index.js +1 -1
- package/dist/lite.css +2 -1
- package/dist/menu-visibility.d.ts +4 -0
- package/dist/menu-visibility.js +34 -0
- package/dist/schema-generator.d.ts +1 -1
- package/dist/schema-generator.js +112 -51
- package/dist/server-adapter.d.ts +53 -4
- package/dist/server-adapter.js +241 -67
- package/dist/value-normalization.d.ts +2 -0
- package/dist/value-normalization.js +21 -0
- package/package.json +6 -14
|
@@ -20,23 +20,25 @@
|
|
|
20
20
|
record,
|
|
21
21
|
errors = {},
|
|
22
22
|
basePath = '/lite',
|
|
23
|
-
canDelete
|
|
24
|
-
canShow
|
|
23
|
+
canDelete,
|
|
24
|
+
canShow,
|
|
25
25
|
}: Props = $props();
|
|
26
26
|
|
|
27
27
|
let pk = $derived(resource.primaryKey ?? 'id');
|
|
28
28
|
let idStr = $derived(String(record[pk]));
|
|
29
|
+
const showDelete = $derived(canDelete ?? resource.canDelete !== false);
|
|
30
|
+
const showView = $derived(canShow ?? resource.canShow !== false);
|
|
29
31
|
</script>
|
|
30
32
|
|
|
31
33
|
<div class="lite-page">
|
|
32
34
|
<div class="lite-page-header">
|
|
33
35
|
<h1 class="lite-page-title">{t('common.edit') || 'Edit'} {resource.label || resource.name} #{idStr}</h1>
|
|
34
36
|
<div class="lite-page-actions">
|
|
35
|
-
{#if
|
|
37
|
+
{#if showView}
|
|
36
38
|
<LiteShowButton resource={resource.name} recordItemId={idStr} {basePath} />
|
|
37
39
|
{/if}
|
|
38
40
|
<LiteListButton resource={resource.name} {basePath} />
|
|
39
|
-
{#if
|
|
41
|
+
{#if showDelete}
|
|
40
42
|
<LiteDeleteButton resource={resource.name} recordItemId={idStr} redirectUrl={`${basePath}/${resource.name}`} {basePath} />
|
|
41
43
|
{/if}
|
|
42
44
|
</div>
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
currentSearch?: string;
|
|
18
18
|
basePath?: string;
|
|
19
19
|
canCreate?: boolean;
|
|
20
|
+
canShow?: boolean;
|
|
20
21
|
canEdit?: boolean;
|
|
21
22
|
canDelete?: boolean;
|
|
22
23
|
}
|
|
@@ -30,17 +31,23 @@
|
|
|
30
31
|
currentOrder = 'asc',
|
|
31
32
|
currentSearch,
|
|
32
33
|
basePath = '/lite',
|
|
33
|
-
canCreate
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
canCreate,
|
|
35
|
+
canShow,
|
|
36
|
+
canEdit,
|
|
37
|
+
canDelete,
|
|
36
38
|
}: Props = $props();
|
|
39
|
+
|
|
40
|
+
const showCreate = $derived(canCreate ?? resource.canCreate !== false);
|
|
41
|
+
const showView = $derived(canShow ?? resource.canShow !== false);
|
|
42
|
+
const showEdit = $derived(canEdit ?? resource.canEdit !== false);
|
|
43
|
+
const showDelete = $derived(canDelete ?? resource.canDelete !== false);
|
|
37
44
|
</script>
|
|
38
45
|
|
|
39
46
|
<div class="lite-page">
|
|
40
47
|
<div class="lite-page-header">
|
|
41
48
|
<h1 class="lite-page-title">{resource.label || resource.name} {t('common.list') || 'List'}</h1>
|
|
42
49
|
<div class="lite-page-actions">
|
|
43
|
-
{#if
|
|
50
|
+
{#if showCreate}
|
|
44
51
|
<LiteCreateButton resource={resource.name} {basePath} />
|
|
45
52
|
{/if}
|
|
46
53
|
<LiteRefreshButton hideText />
|
|
@@ -62,8 +69,9 @@
|
|
|
62
69
|
{currentOrder}
|
|
63
70
|
{currentSearch}
|
|
64
71
|
{basePath}
|
|
65
|
-
{
|
|
66
|
-
{
|
|
72
|
+
canShow={showView}
|
|
73
|
+
canEdit={showEdit}
|
|
74
|
+
canDelete={showDelete}
|
|
67
75
|
/>
|
|
68
76
|
|
|
69
77
|
{#if total > pagination.perPage}
|
|
@@ -18,12 +18,14 @@
|
|
|
18
18
|
resource,
|
|
19
19
|
record,
|
|
20
20
|
basePath = '/lite',
|
|
21
|
-
canEdit
|
|
22
|
-
canDelete
|
|
21
|
+
canEdit,
|
|
22
|
+
canDelete,
|
|
23
23
|
}: Props = $props();
|
|
24
24
|
|
|
25
25
|
let pk = $derived(resource.primaryKey ?? 'id');
|
|
26
26
|
let idStr = $derived(String(record[pk]));
|
|
27
|
+
const showEdit = $derived(canEdit ?? resource.canEdit !== false);
|
|
28
|
+
const showDelete = $derived(canDelete ?? resource.canDelete !== false);
|
|
27
29
|
|
|
28
30
|
const showFields = $derived(
|
|
29
31
|
resource.fields.filter(f => f.showInShow !== false)
|
|
@@ -34,11 +36,11 @@
|
|
|
34
36
|
<div class="lite-page-header">
|
|
35
37
|
<h1 class="lite-page-title">{t('common.show') || 'Show'} {resource.label || resource.name} #{idStr}</h1>
|
|
36
38
|
<div class="lite-page-actions">
|
|
37
|
-
{#if
|
|
39
|
+
{#if showEdit}
|
|
38
40
|
<LiteEditButton resource={resource.name} recordItemId={idStr} {basePath} />
|
|
39
41
|
{/if}
|
|
40
42
|
<LiteListButton resource={resource.name} {basePath} />
|
|
41
|
-
{#if
|
|
43
|
+
{#if showDelete}
|
|
42
44
|
<LiteDeleteButton resource={resource.name} recordItemId={idStr} redirectUrl={`${basePath}/${resource.name}`} {basePath} />
|
|
43
45
|
{/if}
|
|
44
46
|
</div>
|
|
@@ -20,7 +20,11 @@
|
|
|
20
20
|
lowerIsBetter = false,
|
|
21
21
|
}: Props = $props();
|
|
22
22
|
|
|
23
|
-
const diff = $derived(
|
|
23
|
+
const diff = $derived(
|
|
24
|
+
baseline === 0
|
|
25
|
+
? value === 0 ? 0 : value > 0 ? Infinity : -Infinity
|
|
26
|
+
: (value - baseline) / Math.abs(baseline),
|
|
27
|
+
);
|
|
24
28
|
const isAnomaly = $derived(Math.abs(diff) >= threshold);
|
|
25
29
|
const isGood = $derived(!isAnomaly ? null : (lowerIsBetter ? diff < 0 : diff > 0));
|
|
26
30
|
const percentLabel = $derived(`${Math.abs(diff * 100).toFixed(1)}%`);
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Server-rendered by default, with optional progressive enhancement.
|
|
3
3
|
// Server utilities (use in +page.server.ts / hooks.server.ts)
|
|
4
4
|
export { createListLoader, createDetailLoader, createCrudActions, createAuthGuard, createAuthActions, createLegacyRedirectHook, isLegacyBrowser, } from './server-adapter';
|
|
5
|
-
// Schema generator (
|
|
5
|
+
// Schema generator (used by Lite actions; also reusable by other Zod consumers)
|
|
6
6
|
export { fieldsToZodSchema, resourceToZodSchema, fieldToInputType, fieldToPlaceholder, } from './schema-generator';
|
|
7
7
|
// UI Components (use in +page.svelte with csr = false)
|
|
8
8
|
export { default as LiteLayout } from './components/LiteLayout.svelte';
|
package/dist/lite.css
CHANGED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MenuItem, ResourceDefinition } from '@svadmin/core';
|
|
2
|
+
export type MenuAccessCheck = (resource: string, action: string) => boolean;
|
|
3
|
+
export declare function filterVisibleResources(resources: ResourceDefinition[], canAccess?: MenuAccessCheck): ResourceDefinition[];
|
|
4
|
+
export declare function filterVisibleMenu(menuEntries: MenuItem[], canAccess?: MenuAccessCheck): MenuItem[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function passesMenuAccess(resource, action, canAccess) {
|
|
2
|
+
if (!canAccess)
|
|
3
|
+
return true;
|
|
4
|
+
try {
|
|
5
|
+
return canAccess(resource, action);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
// Permission callbacks fail closed so an evaluator error cannot expose navigation.
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function filterVisibleResources(resources, canAccess) {
|
|
13
|
+
return resources.filter((resource) => resource.showInMenu !== false
|
|
14
|
+
&& passesMenuAccess(resource.name, 'list', canAccess));
|
|
15
|
+
}
|
|
16
|
+
export function filterVisibleMenu(menuEntries, canAccess) {
|
|
17
|
+
const visibleEntries = [];
|
|
18
|
+
for (const menuEntry of menuEntries) {
|
|
19
|
+
if (menuEntry.meta?.hidden)
|
|
20
|
+
continue;
|
|
21
|
+
if (menuEntry.meta?.resource
|
|
22
|
+
&& !passesMenuAccess(menuEntry.meta.resource, menuEntry.meta.action ?? 'list', canAccess)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const visibleChildren = menuEntry.children
|
|
26
|
+
? filterVisibleMenu(menuEntry.children, canAccess)
|
|
27
|
+
: undefined;
|
|
28
|
+
if (!menuEntry.href && menuEntry.children && menuEntry.children.length > 0
|
|
29
|
+
&& visibleChildren?.length === 0)
|
|
30
|
+
continue;
|
|
31
|
+
visibleEntries.push({ ...menuEntry, ...(visibleChildren ? { children: visibleChildren } : {}) });
|
|
32
|
+
}
|
|
33
|
+
return visibleEntries;
|
|
34
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @svadmin/lite — Schema Generator
|
|
3
3
|
*
|
|
4
4
|
* Auto-generates Zod schemas from @svadmin/core FieldDefinitions.
|
|
5
|
-
* Used
|
|
5
|
+
* Used by the Lite server actions and compatible with other Zod consumers.
|
|
6
6
|
*/
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import type { FieldDefinition, ResourceDefinition } from '@svadmin/core';
|
package/dist/schema-generator.js
CHANGED
|
@@ -2,15 +2,23 @@
|
|
|
2
2
|
* @svadmin/lite — Schema Generator
|
|
3
3
|
*
|
|
4
4
|
* Auto-generates Zod schemas from @svadmin/core FieldDefinitions.
|
|
5
|
-
* Used
|
|
5
|
+
* Used by the Lite server actions and compatible with other Zod consumers.
|
|
6
6
|
*/
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
+
import { parseExplicitBoolean } from './value-normalization';
|
|
8
9
|
function isNativeFile(value) {
|
|
9
10
|
return typeof File !== 'undefined' && value instanceof File;
|
|
10
11
|
}
|
|
11
12
|
function isNonEmptyNativeFile(value) {
|
|
12
13
|
return isNativeFile(value) && value.size > 0 && value.name !== '';
|
|
13
14
|
}
|
|
15
|
+
function normalizeSingleUpload(submittedUpload) {
|
|
16
|
+
if (submittedUpload === undefined || submittedUpload === null || submittedUpload === '')
|
|
17
|
+
return undefined;
|
|
18
|
+
if (isNativeFile(submittedUpload) && !isNonEmptyNativeFile(submittedUpload))
|
|
19
|
+
return undefined;
|
|
20
|
+
return submittedUpload;
|
|
21
|
+
}
|
|
14
22
|
function hasRequiredValue(value) {
|
|
15
23
|
if (value === undefined || value === null)
|
|
16
24
|
return false;
|
|
@@ -22,6 +30,17 @@ function hasRequiredValue(value) {
|
|
|
22
30
|
return isNonEmptyNativeFile(value);
|
|
23
31
|
return true;
|
|
24
32
|
}
|
|
33
|
+
function applyCustomValidation(schema, field) {
|
|
34
|
+
if (!field.validate)
|
|
35
|
+
return schema;
|
|
36
|
+
return schema.superRefine((parsedFieldValue, context) => {
|
|
37
|
+
if (parsedFieldValue === undefined || parsedFieldValue === null || parsedFieldValue === '')
|
|
38
|
+
return;
|
|
39
|
+
const message = field.validate?.(parsedFieldValue);
|
|
40
|
+
if (message)
|
|
41
|
+
context.addIssue({ code: 'custom', message });
|
|
42
|
+
});
|
|
43
|
+
}
|
|
25
44
|
function numberFieldToZod(field) {
|
|
26
45
|
const numberSchema = z.coerce.number({ message: `${field.label} must be a number` });
|
|
27
46
|
const targetSchema = field.required ? numberSchema : numberSchema.optional();
|
|
@@ -33,6 +52,32 @@ function numberFieldToZod(field) {
|
|
|
33
52
|
return value;
|
|
34
53
|
}, targetSchema);
|
|
35
54
|
}
|
|
55
|
+
function booleanFieldToZod(field) {
|
|
56
|
+
const booleanSchema = z.boolean();
|
|
57
|
+
const targetSchema = field.required ? booleanSchema : booleanSchema.optional();
|
|
58
|
+
return z.preprocess((rawBoolean) => {
|
|
59
|
+
if (rawBoolean === undefined || rawBoolean === null || rawBoolean === '')
|
|
60
|
+
return undefined;
|
|
61
|
+
return parseExplicitBoolean(rawBoolean) ?? rawBoolean;
|
|
62
|
+
}, targetSchema);
|
|
63
|
+
}
|
|
64
|
+
function restoreOptionValue(field, submittedOption) {
|
|
65
|
+
const option = field.options?.find((candidate) => String(candidate.value) === String(submittedOption));
|
|
66
|
+
return option?.value ?? submittedOption;
|
|
67
|
+
}
|
|
68
|
+
function optionValueSchema(field) {
|
|
69
|
+
const options = field.options ?? [];
|
|
70
|
+
return z.union([z.string(), z.number()]).refine((parsedOption) => options.length === 0
|
|
71
|
+
|| options.some((option) => Object.is(option.value, parsedOption)), { message: `${field.label} must be one of the options` });
|
|
72
|
+
}
|
|
73
|
+
function singleOptionFieldToZod(field) {
|
|
74
|
+
return z.preprocess((submittedOption) => restoreOptionValue(field, submittedOption), optionValueSchema(field));
|
|
75
|
+
}
|
|
76
|
+
function multipleOptionFieldToZod(field) {
|
|
77
|
+
return z.preprocess((submittedOptions) => Array.isArray(submittedOptions)
|
|
78
|
+
? submittedOptions.map((submittedOption) => restoreOptionValue(field, submittedOption))
|
|
79
|
+
: submittedOptions, z.array(optionValueSchema(field)).default([]));
|
|
80
|
+
}
|
|
36
81
|
function singleFileFieldToZod(field, options) {
|
|
37
82
|
const fileSchema = z.custom(isNonEmptyNativeFile, {
|
|
38
83
|
message: `${field.label} must be a non-empty file`,
|
|
@@ -40,36 +85,44 @@ function singleFileFieldToZod(field, options) {
|
|
|
40
85
|
const referenceSchema = z.string().trim().min(1, `${field.label} must reference an existing file`);
|
|
41
86
|
const uploadSchema = options.allowReference ? z.union([fileSchema, referenceSchema]) : fileSchema;
|
|
42
87
|
const targetSchema = options.required ? uploadSchema : uploadSchema.optional();
|
|
43
|
-
return z.preprocess(
|
|
44
|
-
if (value === undefined || value === null || value === '')
|
|
45
|
-
return undefined;
|
|
46
|
-
if (isNativeFile(value) && !isNonEmptyNativeFile(value))
|
|
47
|
-
return undefined;
|
|
48
|
-
return value;
|
|
49
|
-
}, targetSchema);
|
|
88
|
+
return z.preprocess(normalizeSingleUpload, targetSchema);
|
|
50
89
|
}
|
|
51
|
-
function
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
function imageFieldSchemas(field) {
|
|
91
|
+
const imageSchema = z.union([
|
|
92
|
+
z.string().trim().min(1, `${field.label} must reference an image`),
|
|
93
|
+
z.custom(isNonEmptyNativeFile, {
|
|
94
|
+
message: `${field.label} must reference an image or contain a non-empty file`,
|
|
95
|
+
}),
|
|
96
|
+
]);
|
|
97
|
+
return {
|
|
98
|
+
required: z.preprocess(normalizeSingleUpload, imageSchema),
|
|
99
|
+
optional: z.preprocess(normalizeSingleUpload, imageSchema.optional()),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function normalizeImageEntries(submittedImages) {
|
|
103
|
+
if (submittedImages === undefined || submittedImages === null || submittedImages === '')
|
|
104
|
+
return undefined;
|
|
105
|
+
const entries = Array.isArray(submittedImages) ? submittedImages : [submittedImages];
|
|
106
|
+
const normalized = entries.flatMap((entry) => {
|
|
107
|
+
if (typeof entry !== 'string')
|
|
108
|
+
return [entry];
|
|
109
|
+
return entry.split(/[\r\n]+/u).map((reference) => reference.trim()).filter(Boolean);
|
|
110
|
+
}).filter((entry) => !isNativeFile(entry) || isNonEmptyNativeFile(entry));
|
|
111
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
112
|
+
}
|
|
113
|
+
function imagesFieldSchemas(field) {
|
|
114
|
+
const imageEntrySchema = z.union([
|
|
115
|
+
z.string().trim().min(1, `${field.label} contains an empty image reference`),
|
|
116
|
+
z.custom(isNonEmptyNativeFile, {
|
|
117
|
+
message: `${field.label} contains an empty or invalid file`,
|
|
118
|
+
}),
|
|
119
|
+
]);
|
|
120
|
+
const requiredImages = z.array(imageEntrySchema)
|
|
121
|
+
.min(1, `${field.label} must contain at least one image`);
|
|
122
|
+
return {
|
|
123
|
+
required: z.preprocess(normalizeImageEntries, requiredImages),
|
|
124
|
+
optional: z.preprocess(normalizeImageEntries, z.array(imageEntrySchema).optional()),
|
|
125
|
+
};
|
|
73
126
|
}
|
|
74
127
|
/**
|
|
75
128
|
* Convert a single FieldDefinition to its corresponding Zod type.
|
|
@@ -80,8 +133,7 @@ function fieldToZod(field, mode, withinArray = false) {
|
|
|
80
133
|
case 'number':
|
|
81
134
|
return numberFieldToZod(field);
|
|
82
135
|
case 'boolean':
|
|
83
|
-
|
|
84
|
-
break;
|
|
136
|
+
return booleanFieldToZod(field);
|
|
85
137
|
case 'email':
|
|
86
138
|
schema = z.string().email(`${field.label} must be a valid email`);
|
|
87
139
|
break;
|
|
@@ -92,20 +144,18 @@ function fieldToZod(field, mode, withinArray = false) {
|
|
|
92
144
|
schema = z.string().refine((v) => !v || !isNaN(Date.parse(v)), { message: `${field.label} must be a valid date` });
|
|
93
145
|
break;
|
|
94
146
|
case 'select':
|
|
95
|
-
|
|
96
|
-
schema = z.enum(field.options.map((o) => String(o.value)), { message: `${field.label} must be one of the options` });
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
schema = z.string();
|
|
100
|
-
}
|
|
147
|
+
schema = singleOptionFieldToZod(field);
|
|
101
148
|
break;
|
|
102
149
|
case 'multiselect':
|
|
103
|
-
schema =
|
|
150
|
+
schema = multipleOptionFieldToZod(field);
|
|
151
|
+
break;
|
|
152
|
+
case 'relation':
|
|
153
|
+
schema = field.options?.length ? singleOptionFieldToZod(field) : z.string();
|
|
104
154
|
break;
|
|
105
155
|
case 'array': {
|
|
106
156
|
const shape = {};
|
|
107
157
|
for (const subField of field.subFields ?? []) {
|
|
108
|
-
shape[subField.key] = fieldToZod(subField, mode, true);
|
|
158
|
+
shape[subField.key] = applyCustomValidation(fieldToZod(subField, mode, true), subField);
|
|
109
159
|
}
|
|
110
160
|
const arraySchema = z.array(z.object(shape));
|
|
111
161
|
return field.required
|
|
@@ -120,6 +170,7 @@ function fieldToZod(field, mode, withinArray = false) {
|
|
|
120
170
|
break;
|
|
121
171
|
case 'textarea':
|
|
122
172
|
case 'richtext':
|
|
173
|
+
case 'markdown':
|
|
123
174
|
schema = z.string().max(50000, `${field.label} is too long`);
|
|
124
175
|
break;
|
|
125
176
|
case 'json':
|
|
@@ -139,16 +190,22 @@ function fieldToZod(field, mode, withinArray = false) {
|
|
|
139
190
|
schema = z.string().regex(/^[+\d\s()-]*$/, `${field.label} must be a valid phone number`);
|
|
140
191
|
break;
|
|
141
192
|
case 'file':
|
|
142
|
-
case 'image':
|
|
143
193
|
return singleFileFieldToZod(field, {
|
|
144
194
|
required: field.required === true && (mode === 'create' || withinArray),
|
|
145
195
|
allowReference: mode === 'edit' && withinArray,
|
|
146
196
|
});
|
|
147
|
-
case '
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
197
|
+
case 'image': {
|
|
198
|
+
const imageSchemas = imageFieldSchemas(field);
|
|
199
|
+
return field.required === true && (mode === 'create' || withinArray)
|
|
200
|
+
? imageSchemas.required
|
|
201
|
+
: imageSchemas.optional;
|
|
202
|
+
}
|
|
203
|
+
case 'images': {
|
|
204
|
+
const imagesSchemas = imagesFieldSchemas(field);
|
|
205
|
+
return field.required === true && (mode === 'create' || withinArray)
|
|
206
|
+
? imagesSchemas.required
|
|
207
|
+
: imagesSchemas.optional;
|
|
208
|
+
}
|
|
152
209
|
default:
|
|
153
210
|
schema = z.string();
|
|
154
211
|
}
|
|
@@ -177,7 +234,7 @@ export function fieldsToZodSchema(fields, mode = 'create') {
|
|
|
177
234
|
continue;
|
|
178
235
|
if (mode === 'edit' && field.showInEdit === false)
|
|
179
236
|
continue;
|
|
180
|
-
shape[field.key] = fieldToZod(field, mode);
|
|
237
|
+
shape[field.key] = applyCustomValidation(fieldToZod(field, mode), field);
|
|
181
238
|
}
|
|
182
239
|
return z.object(shape);
|
|
183
240
|
}
|
|
@@ -186,7 +243,8 @@ export function fieldsToZodSchema(fields, mode = 'create') {
|
|
|
186
243
|
* Convenience wrapper around fieldsToZodSchema.
|
|
187
244
|
*/
|
|
188
245
|
export function resourceToZodSchema(resource, mode = 'create') {
|
|
189
|
-
|
|
246
|
+
const primaryKey = resource.primaryKey ?? 'id';
|
|
247
|
+
return fieldsToZodSchema(resource.fields.filter((field) => field.key !== primaryKey), mode);
|
|
190
248
|
}
|
|
191
249
|
/**
|
|
192
250
|
* Determine a conservative HTML input type for server-rendered forms.
|
|
@@ -201,13 +259,16 @@ export function fieldToInputType(field) {
|
|
|
201
259
|
case 'boolean': return 'checkbox';
|
|
202
260
|
case 'date': return 'text';
|
|
203
261
|
case 'textarea':
|
|
204
|
-
case 'richtext':
|
|
262
|
+
case 'richtext':
|
|
263
|
+
case 'markdown':
|
|
264
|
+
case 'images': return 'textarea';
|
|
265
|
+
case 'json': return 'textarea';
|
|
205
266
|
case 'select':
|
|
206
267
|
case 'multiselect': return 'select';
|
|
268
|
+
case 'relation': return field.options?.length ? 'select' : 'text';
|
|
207
269
|
case 'array': return 'text';
|
|
208
|
-
case 'image':
|
|
209
|
-
case 'images':
|
|
210
270
|
case 'file': return 'file';
|
|
271
|
+
case 'password': return 'password';
|
|
211
272
|
default: return 'text';
|
|
212
273
|
}
|
|
213
274
|
}
|
package/dist/server-adapter.d.ts
CHANGED
|
@@ -16,6 +16,14 @@ export interface ListLoaderResult {
|
|
|
16
16
|
sort?: string;
|
|
17
17
|
order?: 'asc' | 'desc';
|
|
18
18
|
search?: string;
|
|
19
|
+
/** Aliases consumed directly by LiteListPage. */
|
|
20
|
+
pagination: {
|
|
21
|
+
page: number;
|
|
22
|
+
perPage: number;
|
|
23
|
+
};
|
|
24
|
+
currentSort?: string;
|
|
25
|
+
currentOrder?: 'asc' | 'desc';
|
|
26
|
+
currentSearch?: string;
|
|
19
27
|
resource: ResourceDefinition;
|
|
20
28
|
}
|
|
21
29
|
/**
|
|
@@ -43,6 +51,11 @@ export declare function createCrudActions(dp: DataProvider, resource: ResourceDe
|
|
|
43
51
|
error: string;
|
|
44
52
|
values: Record<string, unknown>;
|
|
45
53
|
errors: Record<string, string[]>;
|
|
54
|
+
} | {
|
|
55
|
+
success: boolean;
|
|
56
|
+
error: string;
|
|
57
|
+
id?: undefined;
|
|
58
|
+
values?: undefined;
|
|
46
59
|
} | {
|
|
47
60
|
success: boolean;
|
|
48
61
|
id: unknown;
|
|
@@ -59,6 +72,10 @@ export declare function createCrudActions(dp: DataProvider, resource: ResourceDe
|
|
|
59
72
|
error: string;
|
|
60
73
|
values: Record<string, unknown>;
|
|
61
74
|
errors: Record<string, string[]>;
|
|
75
|
+
} | {
|
|
76
|
+
success: boolean;
|
|
77
|
+
error: string;
|
|
78
|
+
values?: undefined;
|
|
62
79
|
} | {
|
|
63
80
|
success: boolean;
|
|
64
81
|
error?: undefined;
|
|
@@ -70,10 +87,10 @@ export declare function createCrudActions(dp: DataProvider, resource: ResourceDe
|
|
|
70
87
|
}>;
|
|
71
88
|
delete: ({ request }: RequestEvent) => Promise<{
|
|
72
89
|
success: boolean;
|
|
73
|
-
error
|
|
90
|
+
error: string;
|
|
74
91
|
} | {
|
|
75
92
|
success: boolean;
|
|
76
|
-
error
|
|
93
|
+
error?: undefined;
|
|
77
94
|
}>;
|
|
78
95
|
};
|
|
79
96
|
/**
|
|
@@ -85,15 +102,47 @@ export declare function createAuthGuard(authProvider: AuthProvider, loginPath?:
|
|
|
85
102
|
resolve: (event: RequestEvent) => Promise<Response>;
|
|
86
103
|
}) => Promise<Response>;
|
|
87
104
|
/**
|
|
88
|
-
* Creates
|
|
105
|
+
* Creates the form actions used by all exported Lite authentication pages.
|
|
89
106
|
*/
|
|
90
107
|
export declare function createAuthActions(authProvider: AuthProvider): {
|
|
91
|
-
login: ({ request, cookies }: RequestEvent) => Promise<{
|
|
108
|
+
login: ({ request, cookies, url }: RequestEvent) => Promise<{
|
|
92
109
|
success: boolean;
|
|
93
110
|
error: string;
|
|
94
111
|
}>;
|
|
95
112
|
logout: ({ cookies }: RequestEvent) => Promise<{
|
|
96
113
|
success: boolean;
|
|
114
|
+
error: string;
|
|
115
|
+
} | {
|
|
116
|
+
success: boolean;
|
|
117
|
+
error?: undefined;
|
|
118
|
+
}>;
|
|
119
|
+
register: ({ request }: RequestEvent) => Promise<{
|
|
120
|
+
success: boolean;
|
|
121
|
+
error: string;
|
|
122
|
+
} | {
|
|
123
|
+
success: boolean;
|
|
124
|
+
error?: undefined;
|
|
125
|
+
}>;
|
|
126
|
+
forgot_password: ({ request }: RequestEvent) => Promise<{
|
|
127
|
+
success: boolean;
|
|
128
|
+
error: string;
|
|
129
|
+
} | {
|
|
130
|
+
success: boolean;
|
|
131
|
+
error?: undefined;
|
|
132
|
+
}>;
|
|
133
|
+
update_password: ({ request }: RequestEvent) => Promise<{
|
|
134
|
+
success: boolean;
|
|
135
|
+
error: string;
|
|
136
|
+
} | {
|
|
137
|
+
success: boolean;
|
|
138
|
+
error?: undefined;
|
|
139
|
+
}>;
|
|
140
|
+
update_profile: ({ request }: RequestEvent) => Promise<{
|
|
141
|
+
success: boolean;
|
|
142
|
+
error: string;
|
|
143
|
+
} | {
|
|
144
|
+
success: boolean;
|
|
145
|
+
error?: undefined;
|
|
97
146
|
}>;
|
|
98
147
|
};
|
|
99
148
|
/**
|