includio-cms 0.5.2 → 0.5.3
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/CHANGELOG.md +19 -0
- package/ROADMAP.md +13 -0
- package/dist/admin/client/entry/entry-form.svelte +1 -0
- package/dist/admin/client/entry/entry.svelte +130 -123
- package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +92 -9
- package/dist/admin/components/fields/blocks-field.svelte +142 -112
- package/dist/admin/components/fields/blocks-field.svelte.d.ts +10 -30
- package/dist/admin/components/fields/boolean-field.svelte +28 -38
- package/dist/admin/components/fields/boolean-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/checkboxes-field.svelte +12 -24
- package/dist/admin/components/fields/checkboxes-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/content-field.svelte +4 -17
- package/dist/admin/components/fields/content-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/date-field.svelte +8 -21
- package/dist/admin/components/fields/date-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/datetime-field.svelte +8 -21
- package/dist/admin/components/fields/datetime-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/field-renderer.svelte +32 -19
- package/dist/admin/components/fields/field-renderer.svelte.d.ts +1 -1
- package/dist/admin/components/fields/field-value-bridge.svelte +21 -0
- package/dist/admin/components/fields/field-value-bridge.svelte.d.ts +31 -0
- package/dist/admin/components/fields/fields-form.svelte +13 -10
- package/dist/admin/components/fields/file-field.svelte +12 -27
- package/dist/admin/components/fields/file-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/image-field.svelte +13 -28
- package/dist/admin/components/fields/image-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/media-field.svelte +15 -30
- package/dist/admin/components/fields/media-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/number-field.svelte +6 -20
- package/dist/admin/components/fields/number-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/object-field.svelte +26 -29
- package/dist/admin/components/fields/object-field.svelte.d.ts +11 -31
- package/dist/admin/components/fields/radio-field.svelte +8 -20
- package/dist/admin/components/fields/radio-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/relation-field.svelte +15 -30
- package/dist/admin/components/fields/relation-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/richtext-field.svelte +4 -17
- package/dist/admin/components/fields/richtext-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/select-field.svelte +14 -28
- package/dist/admin/components/fields/select-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/seo-field.svelte +5 -12
- package/dist/admin/components/fields/seo-field.svelte.d.ts +8 -28
- package/dist/admin/components/fields/simple-array-field.svelte +29 -42
- package/dist/admin/components/fields/simple-array-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/slug-field.svelte +6 -11
- package/dist/admin/components/fields/slug-field.svelte.d.ts +6 -26
- package/dist/admin/components/fields/text-field-wrapper.svelte +22 -40
- package/dist/admin/components/fields/text-field.svelte +7 -19
- package/dist/admin/components/fields/text-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/url-field-wrapper.svelte +8 -3
- package/dist/admin/components/fields/url-field.svelte +294 -128
- package/dist/admin/components/fields/url-field.svelte.d.ts +5 -27
- package/dist/admin/components/layout/layout-renderer.svelte +8 -6
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +221 -31
- package/dist/admin/components/tiptap/content-editor.svelte +13 -2
- package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
- package/dist/admin/components/tiptap/inline-block-node.js +18 -1
- package/dist/admin/components/tiptap/slash-command.js +2 -3
- package/dist/admin/components/tiptap/standalone-form.d.ts +7 -0
- package/dist/admin/components/tiptap/standalone-form.js +31 -0
- package/dist/admin/components/tiptap/tiptap-editor.svelte +7 -0
- package/dist/admin/remote/entry.remote.js +16 -0
- package/dist/admin/styles/admin.css +10 -0
- package/dist/admin/utils/fieldCondition.d.ts +6 -0
- package/dist/admin/utils/fieldCondition.js +20 -0
- package/dist/components/ui/switch/index.d.ts +2 -0
- package/dist/components/ui/switch/index.js +4 -0
- package/dist/components/ui/switch/switch.svelte +26 -0
- package/dist/components/ui/switch/switch.svelte.d.ts +4 -0
- package/dist/core/fields/fieldSchemaToTs.js +15 -3
- package/dist/core/fields/formFieldSchemaToTs.js +22 -6
- package/dist/core/fields/urlUtils.d.ts +14 -0
- package/dist/core/fields/urlUtils.js +21 -0
- package/dist/core/server/fields/populateEntry.js +43 -0
- package/dist/core/server/fields/resolveImageFields.js +33 -1
- package/dist/core/server/fields/resolveRelationFields.js +46 -0
- package/dist/core/server/fields/resolveRichtextLinks.js +15 -1
- package/dist/core/server/fields/resolveUrlFields.js +65 -0
- package/dist/core/server/generator/formFieldSchemaToString.js +40 -9
- package/dist/core/server/generator/formFields.js +2 -0
- package/dist/core/server/generator/generator.js +25 -1
- package/dist/schemas/field/url.d.ts +2 -0
- package/dist/schemas/field/url.js +4 -2
- package/dist/types/fields.d.ts +9 -0
- package/dist/types/formFields.d.ts +15 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/updates/0.5.3/index.d.ts +2 -0
- package/dist/updates/0.5.3/index.js +19 -0
- package/dist/updates/index.js +2 -1
- package/package.json +2 -1
- package/dist/admin/components/fields/standalone-field-renderer.svelte +0 -148
- package/dist/admin/components/fields/standalone-field-renderer.svelte.d.ts +0 -9
|
@@ -1,27 +1,43 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export function generateZodSchemaFromFormField(field) {
|
|
3
|
+
const errorMsg = field.errorMessage ? Object.values(field.errorMessage)[0] : undefined;
|
|
3
4
|
switch (field.type) {
|
|
4
5
|
case 'text': {
|
|
5
6
|
let schema = z.string();
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
const minLen = field.minLength ?? (field.required ? 1 : undefined);
|
|
8
|
+
if (minLen) {
|
|
9
|
+
schema = schema.min(minLen, errorMsg || undefined);
|
|
10
|
+
}
|
|
11
|
+
if (field.maxLength) {
|
|
12
|
+
schema = schema.max(field.maxLength);
|
|
13
|
+
}
|
|
8
14
|
return schema;
|
|
9
15
|
}
|
|
10
16
|
case 'email': {
|
|
11
17
|
let schema = z.string();
|
|
12
|
-
if (field.required)
|
|
13
|
-
schema = schema.min(1).email();
|
|
18
|
+
if (field.required) {
|
|
19
|
+
schema = schema.min(1).email(errorMsg || undefined);
|
|
20
|
+
}
|
|
14
21
|
return schema;
|
|
15
22
|
}
|
|
16
23
|
case 'textarea': {
|
|
17
24
|
let schema = z.string();
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
const minLen = field.minLength ?? (field.required ? 1 : undefined);
|
|
26
|
+
if (minLen) {
|
|
27
|
+
schema = schema.min(minLen, errorMsg || undefined);
|
|
28
|
+
}
|
|
29
|
+
if (field.maxLength) {
|
|
30
|
+
schema = schema.max(field.maxLength);
|
|
31
|
+
}
|
|
20
32
|
return schema;
|
|
21
33
|
}
|
|
22
34
|
case 'checkbox': {
|
|
23
35
|
return field.required ? z.literal(true) : z.boolean();
|
|
24
36
|
}
|
|
37
|
+
case 'select': {
|
|
38
|
+
const values = field.options.map((o) => o.value);
|
|
39
|
+
return z.enum(values);
|
|
40
|
+
}
|
|
25
41
|
default:
|
|
26
42
|
return z.any();
|
|
27
43
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if URL is external (starts with http:// or https://).
|
|
3
|
+
*/
|
|
4
|
+
export declare function isExternalUrl(url: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Build rel attribute string from individual tokens.
|
|
7
|
+
* Deduplicates and returns space-separated string or empty string.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildRel(tokens: string[]): string;
|
|
10
|
+
/**
|
|
11
|
+
* Merge user-provided rel tokens with auto-generated ones.
|
|
12
|
+
* Returns space-separated string.
|
|
13
|
+
*/
|
|
14
|
+
export declare function mergeRel(userRel: string | undefined, autoTokens: string[]): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if URL is external (starts with http:// or https://).
|
|
3
|
+
*/
|
|
4
|
+
export function isExternalUrl(url) {
|
|
5
|
+
return /^https?:\/\//i.test(url);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Build rel attribute string from individual tokens.
|
|
9
|
+
* Deduplicates and returns space-separated string or empty string.
|
|
10
|
+
*/
|
|
11
|
+
export function buildRel(tokens) {
|
|
12
|
+
return [...new Set(tokens.filter(Boolean))].join(' ');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Merge user-provided rel tokens with auto-generated ones.
|
|
16
|
+
* Returns space-separated string.
|
|
17
|
+
*/
|
|
18
|
+
export function mergeRel(userRel, autoTokens) {
|
|
19
|
+
const userTokens = userRel ? userRel.split(/\s+/).filter(Boolean) : [];
|
|
20
|
+
return buildRel([...autoTokens, ...userTokens]);
|
|
21
|
+
}
|
|
@@ -1,13 +1,56 @@
|
|
|
1
|
+
import { walkInlineBlockNodes } from '../../../admin/components/tiptap/structured-content-utils.js';
|
|
1
2
|
import { translateObject } from '../entries/utils/getEntryTranslation.js';
|
|
2
3
|
import { resolveMediaFields } from './resolveImageFields.js';
|
|
3
4
|
import { resolveRelationFields } from './resolveRelationFields.js';
|
|
4
5
|
import { resolveRichtextLinks } from './resolveRichtextLinks.js';
|
|
5
6
|
import { resolveUrlFields } from './resolveUrlFields.js';
|
|
7
|
+
function translateInlineBlockData(data, fields, language) {
|
|
8
|
+
for (const field of fields) {
|
|
9
|
+
const val = data[field.slug];
|
|
10
|
+
if (val == null)
|
|
11
|
+
continue;
|
|
12
|
+
switch (field.type) {
|
|
13
|
+
case 'content': {
|
|
14
|
+
const cf = field;
|
|
15
|
+
if (cf.inlineBlocks?.length &&
|
|
16
|
+
val &&
|
|
17
|
+
typeof val === 'object' &&
|
|
18
|
+
'type' in val &&
|
|
19
|
+
val.type === 'doc') {
|
|
20
|
+
walkInlineBlockNodes(val, (node) => {
|
|
21
|
+
if (node.attrs?.blockData && typeof node.attrs.blockData === 'object') {
|
|
22
|
+
node.attrs.blockData = translateObject(node.attrs.blockData, language);
|
|
23
|
+
const def = cf.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
|
|
24
|
+
if (def) {
|
|
25
|
+
translateInlineBlockData(node.attrs.blockData, def.fields, language);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
case 'object':
|
|
33
|
+
if (val && typeof val === 'object' && 'data' in val)
|
|
34
|
+
translateInlineBlockData(val.data, field.fields, language);
|
|
35
|
+
break;
|
|
36
|
+
case 'blocks':
|
|
37
|
+
if (Array.isArray(val)) {
|
|
38
|
+
for (const item of val) {
|
|
39
|
+
const def = field.of.find((d) => d.slug === item.slug);
|
|
40
|
+
if (def)
|
|
41
|
+
translateInlineBlockData(item.data, def.fields, language);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
6
48
|
export async function populateEntryData(data, fields, language) {
|
|
7
49
|
let populatedData = await resolveRelationFields(data, fields, language);
|
|
8
50
|
populatedData = await resolveUrlFields(populatedData, fields, language);
|
|
9
51
|
populatedData = await resolveMediaFields(populatedData, fields);
|
|
10
52
|
populatedData = await resolveRichtextLinks(populatedData, fields, language);
|
|
11
53
|
populatedData = translateObject(populatedData, language);
|
|
54
|
+
translateInlineBlockData(populatedData, fields, language);
|
|
12
55
|
return populatedData;
|
|
13
56
|
}
|
|
@@ -3,7 +3,7 @@ const PUBLIC_URL = env.PUBLIC_URL ?? '';
|
|
|
3
3
|
import { getCMS } from '../../cms.js';
|
|
4
4
|
import z from 'zod';
|
|
5
5
|
import { getImageStyles } from './utils/imageStyles.js';
|
|
6
|
-
import { extractMediaIds as extractMediaIdsFromDoc, walkMediaNodes, cloneDoc } from '../../../admin/components/tiptap/structured-content-utils.js';
|
|
6
|
+
import { extractMediaIds as extractMediaIdsFromDoc, walkMediaNodes, walkInlineBlockNodes, cloneDoc } from '../../../admin/components/tiptap/structured-content-utils.js';
|
|
7
7
|
export async function resolveMediaFields(data, fields) {
|
|
8
8
|
const mediaIds = [];
|
|
9
9
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -52,11 +52,24 @@ export async function resolveMediaFields(data, fields) {
|
|
|
52
52
|
}
|
|
53
53
|
break;
|
|
54
54
|
case 'content': {
|
|
55
|
+
const contentField = field;
|
|
55
56
|
// Content field is localized: { lang: StructuredContentDoc }
|
|
56
57
|
if (typeof val === 'object' && val !== null) {
|
|
57
58
|
for (const doc of Object.values(val)) {
|
|
58
59
|
if (doc && typeof doc === 'object' && doc.type === 'doc') {
|
|
60
|
+
// Regular media nodes (figure/video/image)
|
|
59
61
|
mediaIds.push(...extractMediaIdsFromDoc(doc));
|
|
62
|
+
// Inline block fields (image/file/media inside blocks/objects)
|
|
63
|
+
if (contentField.inlineBlocks?.length) {
|
|
64
|
+
walkInlineBlockNodes(doc, (node) => {
|
|
65
|
+
const bd = node.attrs?.blockData;
|
|
66
|
+
if (!bd || typeof bd !== 'object')
|
|
67
|
+
return;
|
|
68
|
+
const def = contentField.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
|
|
69
|
+
if (def)
|
|
70
|
+
collectIds(bd, def.fields);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
75
|
}
|
|
@@ -208,6 +221,7 @@ export async function resolveMediaFields(data, fields) {
|
|
|
208
221
|
break;
|
|
209
222
|
case 'content': {
|
|
210
223
|
// Content field is localized: { lang: StructuredContentDoc }
|
|
224
|
+
const contentField = field;
|
|
211
225
|
if (typeof val === 'object' && val !== null) {
|
|
212
226
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
213
227
|
const resolved = {};
|
|
@@ -233,6 +247,24 @@ export async function resolveMediaFields(data, fields) {
|
|
|
233
247
|
...(mediaFile.type === 'image' ? { blurDataUrl: mediaFile.blurDataUrl } : {})
|
|
234
248
|
};
|
|
235
249
|
});
|
|
250
|
+
// Resolve inline block fields
|
|
251
|
+
if (contentField.inlineBlocks?.length) {
|
|
252
|
+
const promises = [];
|
|
253
|
+
walkInlineBlockNodes(cloned, (node) => {
|
|
254
|
+
const bd = node.attrs?.blockData;
|
|
255
|
+
if (!bd || typeof bd !== 'object')
|
|
256
|
+
return;
|
|
257
|
+
const def = contentField.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
|
|
258
|
+
if (!def)
|
|
259
|
+
return;
|
|
260
|
+
promises.push(resolveValues(bd, def.fields).then((resolvedBd) => {
|
|
261
|
+
node.attrs.blockData = resolvedBd;
|
|
262
|
+
}));
|
|
263
|
+
});
|
|
264
|
+
if (promises.length > 0) {
|
|
265
|
+
await Promise.all(promises);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
236
268
|
resolved[lang] = cloned;
|
|
237
269
|
}
|
|
238
270
|
else {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { walkInlineBlockNodes, cloneDoc } from '../../../admin/components/tiptap/structured-content-utils.js';
|
|
1
2
|
import z from 'zod';
|
|
2
3
|
export async function resolveRelationFields(data, fields, language) {
|
|
3
4
|
const entriesIds = [];
|
|
@@ -37,6 +38,24 @@ export async function resolveRelationFields(data, fields, language) {
|
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
40
|
break;
|
|
41
|
+
case 'content': {
|
|
42
|
+
const cf = field;
|
|
43
|
+
if (typeof val === 'object' && val !== null && cf.inlineBlocks?.length) {
|
|
44
|
+
for (const doc of Object.values(val)) {
|
|
45
|
+
if (doc && typeof doc === 'object' && doc.type === 'doc') {
|
|
46
|
+
walkInlineBlockNodes(doc, (node) => {
|
|
47
|
+
const bd = node.attrs?.blockData;
|
|
48
|
+
if (!bd || typeof bd !== 'object')
|
|
49
|
+
return;
|
|
50
|
+
const def = cf.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
|
|
51
|
+
if (def)
|
|
52
|
+
collectIds(bd, def.fields);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
40
59
|
}
|
|
41
60
|
}
|
|
42
61
|
};
|
|
@@ -93,6 +112,33 @@ export async function resolveRelationFields(data, fields, language) {
|
|
|
93
112
|
return item;
|
|
94
113
|
});
|
|
95
114
|
break;
|
|
115
|
+
case 'content': {
|
|
116
|
+
const cf = field;
|
|
117
|
+
if (typeof val === 'object' && val !== null && cf.inlineBlocks?.length) {
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
119
|
+
const resolved = {};
|
|
120
|
+
for (const [lang, doc] of Object.entries(val)) {
|
|
121
|
+
if (doc && typeof doc === 'object' && doc.type === 'doc') {
|
|
122
|
+
const cloned = cloneDoc(doc);
|
|
123
|
+
walkInlineBlockNodes(cloned, (node) => {
|
|
124
|
+
const bd = node.attrs?.blockData;
|
|
125
|
+
if (!bd || typeof bd !== 'object')
|
|
126
|
+
return;
|
|
127
|
+
const def = cf.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
|
|
128
|
+
if (!def)
|
|
129
|
+
return;
|
|
130
|
+
node.attrs.blockData = resolveValues(bd, def.fields);
|
|
131
|
+
});
|
|
132
|
+
resolved[lang] = cloned;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
resolved[lang] = doc;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
result[field.slug] = resolved;
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
96
142
|
default:
|
|
97
143
|
result[field.slug] = val;
|
|
98
144
|
}
|
|
@@ -160,12 +160,26 @@ export async function resolveRichtextLinks(data, fields, language) {
|
|
|
160
160
|
break;
|
|
161
161
|
}
|
|
162
162
|
case 'content': {
|
|
163
|
+
const contentField = field;
|
|
163
164
|
if (typeof val === 'object' && val !== null) {
|
|
164
165
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
165
166
|
const resolved = {};
|
|
166
167
|
for (const [lang, doc] of Object.entries(val)) {
|
|
167
168
|
if (doc && typeof doc === 'object' && doc.type === 'doc') {
|
|
168
|
-
|
|
169
|
+
const cloned = resolveContentDoc(doc, slugMap);
|
|
170
|
+
// Resolve inline block fields
|
|
171
|
+
if (contentField.inlineBlocks?.length) {
|
|
172
|
+
walkInlineBlockNodes(cloned, (node) => {
|
|
173
|
+
const bd = node.attrs?.blockData;
|
|
174
|
+
if (!bd || typeof bd !== 'object')
|
|
175
|
+
return;
|
|
176
|
+
const def = contentField.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
|
|
177
|
+
if (!def)
|
|
178
|
+
return;
|
|
179
|
+
node.attrs.blockData = resolveValues(bd, def.fields);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
resolved[lang] = cloned;
|
|
169
183
|
}
|
|
170
184
|
else {
|
|
171
185
|
resolved[lang] = doc;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { urlFieldDataSchema, urlFieldDataWithRelationSchema } from '../../../schemas/field/url.js';
|
|
2
|
+
import { walkInlineBlockNodes, cloneDoc } from '../../../admin/components/tiptap/structured-content-utils.js';
|
|
2
3
|
import { getEntries } from '../entries/operations/get.js';
|
|
3
4
|
import { getEntrySlugPath, getSlugFromEntryData } from './slugResolver.js';
|
|
5
|
+
import { isExternalUrl, mergeRel } from '../../fields/urlUtils.js';
|
|
6
|
+
function applyExternalAutoDetect(resolvedUrl, extras) {
|
|
7
|
+
const url = typeof resolvedUrl === 'string' ? resolvedUrl : '';
|
|
8
|
+
if (url && isExternalUrl(url)) {
|
|
9
|
+
extras.isExternal = true;
|
|
10
|
+
extras.rel = mergeRel(extras.rel, ['noopener', 'noreferrer']);
|
|
11
|
+
if (extras.newTab === undefined) {
|
|
12
|
+
extras.newTab = true;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
4
16
|
export async function resolveUrlFields(data, fields, language) {
|
|
5
17
|
const entriesIds = [];
|
|
6
18
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -38,6 +50,24 @@ export async function resolveUrlFields(data, fields, language) {
|
|
|
38
50
|
});
|
|
39
51
|
}
|
|
40
52
|
break;
|
|
53
|
+
case 'content': {
|
|
54
|
+
const cf = field;
|
|
55
|
+
if (typeof val === 'object' && val !== null && cf.inlineBlocks?.length) {
|
|
56
|
+
for (const doc of Object.values(val)) {
|
|
57
|
+
if (doc && typeof doc === 'object' && doc.type === 'doc') {
|
|
58
|
+
walkInlineBlockNodes(doc, (node) => {
|
|
59
|
+
const bd = node.attrs?.blockData;
|
|
60
|
+
if (!bd || typeof bd !== 'object')
|
|
61
|
+
return;
|
|
62
|
+
const def = cf.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
|
|
63
|
+
if (def)
|
|
64
|
+
collectIds(bd, def.fields);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
41
71
|
case 'array':
|
|
42
72
|
if (field.of === 'url' && Array.isArray(val)) {
|
|
43
73
|
for (const item of val) {
|
|
@@ -98,6 +128,9 @@ export async function resolveUrlFields(data, fields, language) {
|
|
|
98
128
|
if (parsed.newTab !== undefined) {
|
|
99
129
|
extras.newTab = parsed.newTab;
|
|
100
130
|
}
|
|
131
|
+
if (parsed.rel) {
|
|
132
|
+
extras.rel = parsed.rel;
|
|
133
|
+
}
|
|
101
134
|
if (parsedValWithRelation.success) {
|
|
102
135
|
const slug = slugMap[parsedValWithRelation.data.id];
|
|
103
136
|
if (slug) {
|
|
@@ -108,6 +141,7 @@ export async function resolveUrlFields(data, fields, language) {
|
|
|
108
141
|
const resolvedUrl = language
|
|
109
142
|
? parsed.url[language]
|
|
110
143
|
: parsed.url;
|
|
144
|
+
applyExternalAutoDetect(resolvedUrl, extras);
|
|
111
145
|
result[field.slug] = { url: resolvedUrl, ...extras };
|
|
112
146
|
break;
|
|
113
147
|
}
|
|
@@ -132,6 +166,33 @@ export async function resolveUrlFields(data, fields, language) {
|
|
|
132
166
|
return item;
|
|
133
167
|
});
|
|
134
168
|
break;
|
|
169
|
+
case 'content': {
|
|
170
|
+
const cf = field;
|
|
171
|
+
if (typeof val === 'object' && val !== null && cf.inlineBlocks?.length) {
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
173
|
+
const resolved = {};
|
|
174
|
+
for (const [lang, doc] of Object.entries(val)) {
|
|
175
|
+
if (doc && typeof doc === 'object' && doc.type === 'doc') {
|
|
176
|
+
const cloned = cloneDoc(doc);
|
|
177
|
+
walkInlineBlockNodes(cloned, (node) => {
|
|
178
|
+
const bd = node.attrs?.blockData;
|
|
179
|
+
if (!bd || typeof bd !== 'object')
|
|
180
|
+
return;
|
|
181
|
+
const def = cf.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
|
|
182
|
+
if (!def)
|
|
183
|
+
return;
|
|
184
|
+
node.attrs.blockData = resolveValues(bd, def.fields);
|
|
185
|
+
});
|
|
186
|
+
resolved[lang] = cloned;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
resolved[lang] = doc;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
result[field.slug] = resolved;
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
135
196
|
case 'array':
|
|
136
197
|
if (field.of === 'url' && Array.isArray(val)) {
|
|
137
198
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -152,6 +213,9 @@ export async function resolveUrlFields(data, fields, language) {
|
|
|
152
213
|
if (parsed.newTab !== undefined) {
|
|
153
214
|
extras.newTab = parsed.newTab;
|
|
154
215
|
}
|
|
216
|
+
if (parsed.rel) {
|
|
217
|
+
extras.rel = parsed.rel;
|
|
218
|
+
}
|
|
155
219
|
if (parsedValWithRelation.success) {
|
|
156
220
|
const slug = slugMap[parsedValWithRelation.data.id];
|
|
157
221
|
if (slug) {
|
|
@@ -161,6 +225,7 @@ export async function resolveUrlFields(data, fields, language) {
|
|
|
161
225
|
const resolvedUrl = language
|
|
162
226
|
? parsed.url[language]
|
|
163
227
|
: parsed.url;
|
|
228
|
+
applyExternalAutoDetect(resolvedUrl, extras);
|
|
164
229
|
return { url: resolvedUrl, ...extras };
|
|
165
230
|
});
|
|
166
231
|
}
|
|
@@ -1,24 +1,55 @@
|
|
|
1
|
+
function getErrorMsg(field) {
|
|
2
|
+
if (field.errorMessage) {
|
|
3
|
+
const msg = Object.values(field.errorMessage)[0];
|
|
4
|
+
if (msg)
|
|
5
|
+
return JSON.stringify(msg);
|
|
6
|
+
}
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
1
9
|
export function generateZodSchemaFromFormFieldAsString(field) {
|
|
2
10
|
let code = '';
|
|
3
11
|
switch (field.type) {
|
|
4
|
-
case 'text':
|
|
12
|
+
case 'text': {
|
|
5
13
|
code = 'z.string()';
|
|
6
|
-
|
|
7
|
-
|
|
14
|
+
const msg = getErrorMsg(field);
|
|
15
|
+
const minLen = field.minLength ?? (field.required ? 1 : undefined);
|
|
16
|
+
if (minLen) {
|
|
17
|
+
code += msg ? `.min(${minLen}, ${msg})` : `.min(${minLen})`;
|
|
18
|
+
}
|
|
19
|
+
if (field.maxLength) {
|
|
20
|
+
code += `.max(${field.maxLength})`;
|
|
21
|
+
}
|
|
8
22
|
break;
|
|
9
|
-
|
|
23
|
+
}
|
|
24
|
+
case 'email': {
|
|
10
25
|
code = 'z.string()';
|
|
11
|
-
|
|
12
|
-
|
|
26
|
+
const msg = getErrorMsg(field);
|
|
27
|
+
if (field.required) {
|
|
28
|
+
code += '.min(1)';
|
|
29
|
+
code += msg ? `.email(${msg})` : '.email()';
|
|
30
|
+
}
|
|
13
31
|
break;
|
|
14
|
-
|
|
32
|
+
}
|
|
33
|
+
case 'textarea': {
|
|
15
34
|
code = 'z.string()';
|
|
16
|
-
|
|
17
|
-
|
|
35
|
+
const msg = getErrorMsg(field);
|
|
36
|
+
const minLen = field.minLength ?? (field.required ? 1 : undefined);
|
|
37
|
+
if (minLen) {
|
|
38
|
+
code += msg ? `.min(${minLen}, ${msg})` : `.min(${minLen})`;
|
|
39
|
+
}
|
|
40
|
+
if (field.maxLength) {
|
|
41
|
+
code += `.max(${field.maxLength})`;
|
|
42
|
+
}
|
|
18
43
|
break;
|
|
44
|
+
}
|
|
19
45
|
case 'checkbox':
|
|
20
46
|
code = field.required ? 'z.literal(true)' : 'z.boolean()';
|
|
21
47
|
break;
|
|
48
|
+
case 'select': {
|
|
49
|
+
const values = field.options.map((o) => JSON.stringify(o.value));
|
|
50
|
+
code = `z.enum([${values.join(', ')}])`;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
22
53
|
default:
|
|
23
54
|
code = 'z.any()';
|
|
24
55
|
}
|
|
@@ -55,7 +55,7 @@ function generateTypesStringForForms(records) {
|
|
|
55
55
|
.join('\n')}
|
|
56
56
|
`;
|
|
57
57
|
code += `
|
|
58
|
-
export type ${recordTypeString}
|
|
58
|
+
export type ${recordTypeString}EntryMap = {
|
|
59
59
|
${records
|
|
60
60
|
.map((single) => {
|
|
61
61
|
return `${single.slug}: ${toPascalCase(single.slug)}`;
|
|
@@ -168,9 +168,33 @@ function generateSchemas(config) {
|
|
|
168
168
|
});
|
|
169
169
|
writeFileSync(filePath, code);
|
|
170
170
|
}
|
|
171
|
+
function generateRemote(config) {
|
|
172
|
+
if (!config.forms || config.forms.length === 0)
|
|
173
|
+
return;
|
|
174
|
+
const cmsDir = join(process.cwd(), 'src/lib/cms/runtime');
|
|
175
|
+
const filePath = join(cmsDir, 'remote.ts');
|
|
176
|
+
let code = `// This file is auto-generated. Do not edit directly.\n\n`;
|
|
177
|
+
code += `import { command } from '$app/server';\n`;
|
|
178
|
+
code += `import { submitForm } from './api';\n`;
|
|
179
|
+
const schemaImports = config.forms
|
|
180
|
+
.map((form) => `${form.slug}FormSchema`)
|
|
181
|
+
.join(', ');
|
|
182
|
+
code += `import { ${schemaImports} } from './schemas';\n\n`;
|
|
183
|
+
config.forms.forEach((form) => {
|
|
184
|
+
const pascalSlug = toPascalCase(form.slug);
|
|
185
|
+
code += `export const submit${pascalSlug}Command = command(\n`;
|
|
186
|
+
code += `\t${form.slug}FormSchema,\n`;
|
|
187
|
+
code += `\tasync (data) => {\n`;
|
|
188
|
+
code += `\t\tawait submitForm('${form.slug}', data);\n`;
|
|
189
|
+
code += `\t}\n`;
|
|
190
|
+
code += `);\n\n`;
|
|
191
|
+
});
|
|
192
|
+
writeFileSync(filePath, code);
|
|
193
|
+
}
|
|
171
194
|
export function generateRuntime(config) {
|
|
172
195
|
createCmsRuntimeDir();
|
|
173
196
|
generateTypes(config);
|
|
174
197
|
generateAPI(config);
|
|
175
198
|
generateSchemas(config);
|
|
199
|
+
generateRemote(config);
|
|
176
200
|
}
|
|
@@ -4,10 +4,12 @@ export declare const urlFieldDataSchema: z.ZodObject<{
|
|
|
4
4
|
url: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
5
5
|
text: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
6
6
|
newTab: z.ZodOptional<z.ZodBoolean>;
|
|
7
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
7
8
|
}, z.z.core.$strip>;
|
|
8
9
|
export declare const urlFieldDataWithRelationSchema: z.ZodObject<{
|
|
9
10
|
id: z.ZodString;
|
|
10
11
|
url: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
11
12
|
text: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
12
13
|
newTab: z.ZodOptional<z.ZodBoolean>;
|
|
14
|
+
rel: z.ZodOptional<z.ZodString>;
|
|
13
15
|
}, z.z.core.$strip>;
|
|
@@ -3,11 +3,13 @@ export const urlFieldDataSchema = z.object({
|
|
|
3
3
|
id: z.string().optional(),
|
|
4
4
|
url: z.record(z.string(), z.string()),
|
|
5
5
|
text: z.record(z.string(), z.string()).optional(),
|
|
6
|
-
newTab: z.boolean().optional()
|
|
6
|
+
newTab: z.boolean().optional(),
|
|
7
|
+
rel: z.string().optional()
|
|
7
8
|
});
|
|
8
9
|
export const urlFieldDataWithRelationSchema = z.object({
|
|
9
10
|
id: z.string().uuid(),
|
|
10
11
|
url: z.record(z.string(), z.string()),
|
|
11
12
|
text: z.record(z.string(), z.string()).optional(),
|
|
12
|
-
newTab: z.boolean().optional()
|
|
13
|
+
newTab: z.boolean().optional(),
|
|
14
|
+
rel: z.string().optional()
|
|
13
15
|
});
|
package/dist/types/fields.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ import type { ImageStyle, MediaFile } from './media.js';
|
|
|
3
3
|
import type { Localized } from './languages.js';
|
|
4
4
|
import type { StructuredContentDoc } from './structured-content.js';
|
|
5
5
|
export type FieldType = 'text' | 'richtext' | 'content' | 'number' | 'boolean' | 'date' | 'datetime' | 'file' | 'image' | 'media' | 'select' | 'radio' | 'checkboxes' | 'relation' | 'object' | 'array' | 'blocks' | 'slug' | 'seo' | 'url';
|
|
6
|
+
export interface FieldCondition {
|
|
7
|
+
field: string;
|
|
8
|
+
equals?: string | string[];
|
|
9
|
+
notEquals?: string | string[];
|
|
10
|
+
}
|
|
6
11
|
export interface BaseField {
|
|
7
12
|
slug: string;
|
|
8
13
|
label?: Localized;
|
|
@@ -10,6 +15,7 @@ export interface BaseField {
|
|
|
10
15
|
description?: Localized;
|
|
11
16
|
defaultValue?: any;
|
|
12
17
|
localized?: boolean;
|
|
18
|
+
showWhen?: FieldCondition;
|
|
13
19
|
}
|
|
14
20
|
export interface TextField extends BaseField {
|
|
15
21
|
type: 'text';
|
|
@@ -144,6 +150,7 @@ export interface ObjectField extends BaseField {
|
|
|
144
150
|
thumbnail?: string;
|
|
145
151
|
}
|
|
146
152
|
export interface ObjectFieldData {
|
|
153
|
+
_id?: string;
|
|
147
154
|
slug?: string;
|
|
148
155
|
data: Record<string, unknown>;
|
|
149
156
|
}
|
|
@@ -188,11 +195,13 @@ export interface UrlField extends BaseField {
|
|
|
188
195
|
placeholder?: Localized;
|
|
189
196
|
text?: boolean;
|
|
190
197
|
newTab?: boolean;
|
|
198
|
+
rel?: boolean;
|
|
191
199
|
}
|
|
192
200
|
export type UrlFieldData = {
|
|
193
201
|
id?: string;
|
|
194
202
|
url: Record<string, string>;
|
|
195
203
|
text?: Record<string, string>;
|
|
196
204
|
newTab?: boolean;
|
|
205
|
+
rel?: string;
|
|
197
206
|
};
|
|
198
207
|
export type Field = TextField | RichtextField | ContentField | NumberField | BooleanField | DateField | DateTimeField | FileField | ImageField | MediaField | SelectField | RadioField | CheckboxesField | RelationField | ObjectField | ArrayField | BlocksField | SlugField | SeoField | UrlField;
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import type { Localized } from './languages.js';
|
|
2
|
-
export type FormFieldType = 'text' | 'email' | 'textarea' | 'checkbox';
|
|
2
|
+
export type FormFieldType = 'text' | 'email' | 'textarea' | 'checkbox' | 'select';
|
|
3
3
|
export interface FormBaseField {
|
|
4
4
|
slug: string;
|
|
5
5
|
label?: Localized;
|
|
6
6
|
required?: boolean;
|
|
7
7
|
description?: Localized;
|
|
8
|
+
errorMessage?: Localized;
|
|
8
9
|
defaultValue?: any;
|
|
9
10
|
showInDataTable?: boolean;
|
|
10
11
|
}
|
|
11
12
|
export interface FormTextField extends FormBaseField {
|
|
12
13
|
type: 'text';
|
|
13
14
|
defaultValue?: string;
|
|
15
|
+
minLength?: number;
|
|
16
|
+
maxLength?: number;
|
|
14
17
|
}
|
|
15
18
|
export interface FormEmailField extends FormBaseField {
|
|
16
19
|
type: 'email';
|
|
@@ -19,9 +22,19 @@ export interface FormEmailField extends FormBaseField {
|
|
|
19
22
|
export interface FormTextareaField extends FormBaseField {
|
|
20
23
|
type: 'textarea';
|
|
21
24
|
defaultValue?: string;
|
|
25
|
+
minLength?: number;
|
|
26
|
+
maxLength?: number;
|
|
22
27
|
}
|
|
23
28
|
export interface FormCheckboxField extends FormBaseField {
|
|
24
29
|
type: 'checkbox';
|
|
25
30
|
defaultValue?: boolean;
|
|
26
31
|
}
|
|
27
|
-
export
|
|
32
|
+
export interface FormSelectField extends FormBaseField {
|
|
33
|
+
type: 'select';
|
|
34
|
+
options: {
|
|
35
|
+
value: string;
|
|
36
|
+
label?: Localized;
|
|
37
|
+
}[];
|
|
38
|
+
defaultValue?: string;
|
|
39
|
+
}
|
|
40
|
+
export type FormField = FormTextField | FormEmailField | FormTextareaField | FormCheckboxField | FormSelectField;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { type ConfigBase } from './config.js';
|
|
|
7
7
|
export { type CollectionConfig } from './collections.js';
|
|
8
8
|
export { type SingleConfig } from './singles.js';
|
|
9
9
|
export { type FormConfig, type FormSubmission } from './forms.js';
|
|
10
|
+
export { type FormField, type FormFieldType, type FormBaseField, type FormTextField, type FormEmailField, type FormTextareaField, type FormCheckboxField, type FormSelectField } from './formFields.js';
|
|
10
11
|
export { type CMSConfig } from './cms.js';
|
|
11
12
|
export { type Language, type Localized } from './languages.js';
|
|
12
13
|
export { type Layout, type LayoutNode, type LayoutPreset, type LayoutNodeType, type ColumnRatio, type SectionNode, type ColumnsNode, type CardNode, type AccordionNode, type StackNode } from './layout.js';
|