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.
Files changed (93) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/ROADMAP.md +13 -0
  3. package/dist/admin/client/entry/entry-form.svelte +1 -0
  4. package/dist/admin/client/entry/entry.svelte +130 -123
  5. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +92 -9
  6. package/dist/admin/components/fields/blocks-field.svelte +142 -112
  7. package/dist/admin/components/fields/blocks-field.svelte.d.ts +10 -30
  8. package/dist/admin/components/fields/boolean-field.svelte +28 -38
  9. package/dist/admin/components/fields/boolean-field.svelte.d.ts +5 -27
  10. package/dist/admin/components/fields/checkboxes-field.svelte +12 -24
  11. package/dist/admin/components/fields/checkboxes-field.svelte.d.ts +5 -27
  12. package/dist/admin/components/fields/content-field.svelte +4 -17
  13. package/dist/admin/components/fields/content-field.svelte.d.ts +5 -27
  14. package/dist/admin/components/fields/date-field.svelte +8 -21
  15. package/dist/admin/components/fields/date-field.svelte.d.ts +5 -27
  16. package/dist/admin/components/fields/datetime-field.svelte +8 -21
  17. package/dist/admin/components/fields/datetime-field.svelte.d.ts +5 -27
  18. package/dist/admin/components/fields/field-renderer.svelte +32 -19
  19. package/dist/admin/components/fields/field-renderer.svelte.d.ts +1 -1
  20. package/dist/admin/components/fields/field-value-bridge.svelte +21 -0
  21. package/dist/admin/components/fields/field-value-bridge.svelte.d.ts +31 -0
  22. package/dist/admin/components/fields/fields-form.svelte +13 -10
  23. package/dist/admin/components/fields/file-field.svelte +12 -27
  24. package/dist/admin/components/fields/file-field.svelte.d.ts +5 -27
  25. package/dist/admin/components/fields/image-field.svelte +13 -28
  26. package/dist/admin/components/fields/image-field.svelte.d.ts +5 -27
  27. package/dist/admin/components/fields/media-field.svelte +15 -30
  28. package/dist/admin/components/fields/media-field.svelte.d.ts +5 -27
  29. package/dist/admin/components/fields/number-field.svelte +6 -20
  30. package/dist/admin/components/fields/number-field.svelte.d.ts +5 -27
  31. package/dist/admin/components/fields/object-field.svelte +26 -29
  32. package/dist/admin/components/fields/object-field.svelte.d.ts +11 -31
  33. package/dist/admin/components/fields/radio-field.svelte +8 -20
  34. package/dist/admin/components/fields/radio-field.svelte.d.ts +5 -27
  35. package/dist/admin/components/fields/relation-field.svelte +15 -30
  36. package/dist/admin/components/fields/relation-field.svelte.d.ts +5 -27
  37. package/dist/admin/components/fields/richtext-field.svelte +4 -17
  38. package/dist/admin/components/fields/richtext-field.svelte.d.ts +5 -27
  39. package/dist/admin/components/fields/select-field.svelte +14 -28
  40. package/dist/admin/components/fields/select-field.svelte.d.ts +5 -27
  41. package/dist/admin/components/fields/seo-field.svelte +5 -12
  42. package/dist/admin/components/fields/seo-field.svelte.d.ts +8 -28
  43. package/dist/admin/components/fields/simple-array-field.svelte +29 -42
  44. package/dist/admin/components/fields/simple-array-field.svelte.d.ts +5 -27
  45. package/dist/admin/components/fields/slug-field.svelte +6 -11
  46. package/dist/admin/components/fields/slug-field.svelte.d.ts +6 -26
  47. package/dist/admin/components/fields/text-field-wrapper.svelte +22 -40
  48. package/dist/admin/components/fields/text-field.svelte +7 -19
  49. package/dist/admin/components/fields/text-field.svelte.d.ts +5 -27
  50. package/dist/admin/components/fields/url-field-wrapper.svelte +8 -3
  51. package/dist/admin/components/fields/url-field.svelte +294 -128
  52. package/dist/admin/components/fields/url-field.svelte.d.ts +5 -27
  53. package/dist/admin/components/layout/layout-renderer.svelte +8 -6
  54. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +221 -31
  55. package/dist/admin/components/tiptap/content-editor.svelte +13 -2
  56. package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
  57. package/dist/admin/components/tiptap/inline-block-node.js +18 -1
  58. package/dist/admin/components/tiptap/slash-command.js +2 -3
  59. package/dist/admin/components/tiptap/standalone-form.d.ts +7 -0
  60. package/dist/admin/components/tiptap/standalone-form.js +31 -0
  61. package/dist/admin/components/tiptap/tiptap-editor.svelte +7 -0
  62. package/dist/admin/remote/entry.remote.js +16 -0
  63. package/dist/admin/styles/admin.css +10 -0
  64. package/dist/admin/utils/fieldCondition.d.ts +6 -0
  65. package/dist/admin/utils/fieldCondition.js +20 -0
  66. package/dist/components/ui/switch/index.d.ts +2 -0
  67. package/dist/components/ui/switch/index.js +4 -0
  68. package/dist/components/ui/switch/switch.svelte +26 -0
  69. package/dist/components/ui/switch/switch.svelte.d.ts +4 -0
  70. package/dist/core/fields/fieldSchemaToTs.js +15 -3
  71. package/dist/core/fields/formFieldSchemaToTs.js +22 -6
  72. package/dist/core/fields/urlUtils.d.ts +14 -0
  73. package/dist/core/fields/urlUtils.js +21 -0
  74. package/dist/core/server/fields/populateEntry.js +43 -0
  75. package/dist/core/server/fields/resolveImageFields.js +33 -1
  76. package/dist/core/server/fields/resolveRelationFields.js +46 -0
  77. package/dist/core/server/fields/resolveRichtextLinks.js +15 -1
  78. package/dist/core/server/fields/resolveUrlFields.js +65 -0
  79. package/dist/core/server/generator/formFieldSchemaToString.js +40 -9
  80. package/dist/core/server/generator/formFields.js +2 -0
  81. package/dist/core/server/generator/generator.js +25 -1
  82. package/dist/schemas/field/url.d.ts +2 -0
  83. package/dist/schemas/field/url.js +4 -2
  84. package/dist/types/fields.d.ts +9 -0
  85. package/dist/types/formFields.d.ts +15 -2
  86. package/dist/types/index.d.ts +1 -0
  87. package/dist/types/index.js +1 -0
  88. package/dist/updates/0.5.3/index.d.ts +2 -0
  89. package/dist/updates/0.5.3/index.js +19 -0
  90. package/dist/updates/index.js +2 -1
  91. package/package.json +2 -1
  92. package/dist/admin/components/fields/standalone-field-renderer.svelte +0 -148
  93. 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
- if (field.required)
7
- schema = schema.min(1);
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
- if (field.required)
19
- schema = schema.min(1);
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
- resolved[lang] = resolveContentDoc(doc, slugMap);
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
- if (field.required)
7
- code += '.min(1)'; // Ensure non-empty string if required
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
- case 'email':
23
+ }
24
+ case 'email': {
10
25
  code = 'z.string()';
11
- if (field.required)
12
- code += '.min(1).email()'; // Ensure non-empty and valid email if required
26
+ const msg = getErrorMsg(field);
27
+ if (field.required) {
28
+ code += '.min(1)';
29
+ code += msg ? `.email(${msg})` : '.email()';
30
+ }
13
31
  break;
14
- case 'textarea':
32
+ }
33
+ case 'textarea': {
15
34
  code = 'z.string()';
16
- if (field.required)
17
- code += '.min(1)'; // Ensure non-empty string if required
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
  }
@@ -6,6 +6,8 @@ function getFormFieldTypeAsString(field) {
6
6
  return 'string';
7
7
  case 'checkbox':
8
8
  return 'boolean';
9
+ case 'select':
10
+ return field.options.map((o) => JSON.stringify(o.value)).join(' | ');
9
11
  default:
10
12
  return 'any';
11
13
  }
@@ -55,7 +55,7 @@ function generateTypesStringForForms(records) {
55
55
  .join('\n')}
56
56
  `;
57
57
  code += `
58
- export type ${recordTypeString}Map = {
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
  });
@@ -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 type FormField = FormTextField | FormEmailField | FormTextareaField | FormCheckboxField;
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;
@@ -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';