includio-cms 0.5.2 → 0.5.5

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 (132) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/ROADMAP.md +29 -0
  3. package/dist/admin/api/rest/handler.d.ts +7 -0
  4. package/dist/admin/api/rest/handler.js +116 -0
  5. package/dist/admin/api/rest/middleware/apiKey.d.ts +6 -0
  6. package/dist/admin/api/rest/middleware/apiKey.js +45 -0
  7. package/dist/admin/api/rest/routes/collections.d.ts +5 -0
  8. package/dist/admin/api/rest/routes/collections.js +104 -0
  9. package/dist/admin/api/rest/routes/entries.d.ts +2 -0
  10. package/dist/admin/api/rest/routes/entries.js +37 -0
  11. package/dist/admin/api/rest/routes/languages.d.ts +1 -0
  12. package/dist/admin/api/rest/routes/languages.js +5 -0
  13. package/dist/admin/api/rest/routes/schema.d.ts +2 -0
  14. package/dist/admin/api/rest/routes/schema.js +78 -0
  15. package/dist/admin/api/rest/routes/singletons.d.ts +3 -0
  16. package/dist/admin/api/rest/routes/singletons.js +60 -0
  17. package/dist/admin/auth-client.d.ts +7 -7
  18. package/dist/admin/client/collection/collection-entries.svelte +56 -5
  19. package/dist/admin/client/collection/data-table.svelte +127 -18
  20. package/dist/admin/client/collection/data-table.svelte.d.ts +2 -0
  21. package/dist/admin/client/entry/entry-form.svelte +1 -0
  22. package/dist/admin/client/entry/entry.svelte +130 -123
  23. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +92 -9
  24. package/dist/admin/components/fields/blocks-field.svelte +142 -112
  25. package/dist/admin/components/fields/blocks-field.svelte.d.ts +10 -30
  26. package/dist/admin/components/fields/boolean-field.svelte +28 -38
  27. package/dist/admin/components/fields/boolean-field.svelte.d.ts +5 -27
  28. package/dist/admin/components/fields/checkboxes-field.svelte +12 -24
  29. package/dist/admin/components/fields/checkboxes-field.svelte.d.ts +5 -27
  30. package/dist/admin/components/fields/content-field.svelte +4 -17
  31. package/dist/admin/components/fields/content-field.svelte.d.ts +5 -27
  32. package/dist/admin/components/fields/date-field.svelte +8 -21
  33. package/dist/admin/components/fields/date-field.svelte.d.ts +5 -27
  34. package/dist/admin/components/fields/datetime-field.svelte +8 -21
  35. package/dist/admin/components/fields/datetime-field.svelte.d.ts +5 -27
  36. package/dist/admin/components/fields/field-renderer.svelte +32 -19
  37. package/dist/admin/components/fields/field-renderer.svelte.d.ts +1 -1
  38. package/dist/admin/components/fields/field-value-bridge.svelte +21 -0
  39. package/dist/admin/components/fields/field-value-bridge.svelte.d.ts +31 -0
  40. package/dist/admin/components/fields/fields-form.svelte +13 -10
  41. package/dist/admin/components/fields/file-field.svelte +12 -27
  42. package/dist/admin/components/fields/file-field.svelte.d.ts +5 -27
  43. package/dist/admin/components/fields/image-field.svelte +13 -28
  44. package/dist/admin/components/fields/image-field.svelte.d.ts +5 -27
  45. package/dist/admin/components/fields/media-field.svelte +15 -30
  46. package/dist/admin/components/fields/media-field.svelte.d.ts +5 -27
  47. package/dist/admin/components/fields/number-field.svelte +6 -20
  48. package/dist/admin/components/fields/number-field.svelte.d.ts +5 -27
  49. package/dist/admin/components/fields/object-field.svelte +26 -29
  50. package/dist/admin/components/fields/object-field.svelte.d.ts +11 -31
  51. package/dist/admin/components/fields/radio-field.svelte +8 -20
  52. package/dist/admin/components/fields/radio-field.svelte.d.ts +5 -27
  53. package/dist/admin/components/fields/relation-field.svelte +28 -40
  54. package/dist/admin/components/fields/relation-field.svelte.d.ts +5 -27
  55. package/dist/admin/components/fields/richtext-field.svelte +4 -17
  56. package/dist/admin/components/fields/richtext-field.svelte.d.ts +5 -27
  57. package/dist/admin/components/fields/select-field.svelte +14 -28
  58. package/dist/admin/components/fields/select-field.svelte.d.ts +5 -27
  59. package/dist/admin/components/fields/seo-field.svelte +5 -12
  60. package/dist/admin/components/fields/seo-field.svelte.d.ts +8 -28
  61. package/dist/admin/components/fields/simple-array-field.svelte +29 -42
  62. package/dist/admin/components/fields/simple-array-field.svelte.d.ts +5 -27
  63. package/dist/admin/components/fields/slug-field.svelte +6 -11
  64. package/dist/admin/components/fields/slug-field.svelte.d.ts +6 -26
  65. package/dist/admin/components/fields/text-field-wrapper.svelte +22 -40
  66. package/dist/admin/components/fields/text-field.svelte +7 -19
  67. package/dist/admin/components/fields/text-field.svelte.d.ts +5 -27
  68. package/dist/admin/components/fields/url-field-wrapper.svelte +8 -3
  69. package/dist/admin/components/fields/url-field.svelte +294 -128
  70. package/dist/admin/components/fields/url-field.svelte.d.ts +5 -27
  71. package/dist/admin/components/layout/layout-renderer.svelte +8 -6
  72. package/dist/admin/components/layout/nav-collections.svelte +2 -1
  73. package/dist/admin/components/layout/nav-forms.svelte +2 -1
  74. package/dist/admin/components/layout/nav-singletons.svelte +2 -1
  75. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +221 -31
  76. package/dist/admin/components/tiptap/content-editor.svelte +13 -2
  77. package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
  78. package/dist/admin/components/tiptap/inline-block-node.js +18 -1
  79. package/dist/admin/components/tiptap/slash-command.js +2 -3
  80. package/dist/admin/components/tiptap/standalone-form.d.ts +7 -0
  81. package/dist/admin/components/tiptap/standalone-form.js +31 -0
  82. package/dist/admin/components/tiptap/tiptap-editor.svelte +7 -0
  83. package/dist/admin/remote/entry.remote.d.ts +9 -1
  84. package/dist/admin/remote/entry.remote.js +30 -2
  85. package/dist/admin/styles/admin.css +10 -0
  86. package/dist/admin/utils/fieldCondition.d.ts +6 -0
  87. package/dist/admin/utils/fieldCondition.js +20 -0
  88. package/dist/cli/scaffold/admin.js +8 -0
  89. package/dist/components/ui/switch/index.d.ts +2 -0
  90. package/dist/components/ui/switch/index.js +4 -0
  91. package/dist/components/ui/switch/switch.svelte +26 -0
  92. package/dist/components/ui/switch/switch.svelte.d.ts +4 -0
  93. package/dist/core/cms.d.ts +2 -1
  94. package/dist/core/cms.js +2 -0
  95. package/dist/core/fields/fieldSchemaToTs.js +15 -3
  96. package/dist/core/fields/formFieldSchemaToTs.js +22 -6
  97. package/dist/core/fields/urlUtils.d.ts +14 -0
  98. package/dist/core/fields/urlUtils.js +21 -0
  99. package/dist/core/server/entries/operations/get.js +2 -1
  100. package/dist/core/server/entries/operations/update.d.ts +1 -0
  101. package/dist/core/server/entries/operations/update.js +5 -1
  102. package/dist/core/server/fields/populateEntry.js +43 -0
  103. package/dist/core/server/fields/resolveImageFields.js +33 -1
  104. package/dist/core/server/fields/resolveRelationFields.js +46 -0
  105. package/dist/core/server/fields/resolveRichtextLinks.js +15 -1
  106. package/dist/core/server/fields/resolveUrlFields.js +65 -0
  107. package/dist/core/server/generator/formFieldSchemaToString.js +40 -9
  108. package/dist/core/server/generator/formFields.js +2 -0
  109. package/dist/core/server/generator/generator.js +25 -1
  110. package/dist/db-postgres/schema/entry.d.ts +17 -0
  111. package/dist/db-postgres/schema/entry.js +4 -2
  112. package/dist/schemas/field/url.d.ts +2 -0
  113. package/dist/schemas/field/url.js +4 -2
  114. package/dist/server/auth.d.ts +6 -6
  115. package/dist/sveltekit/server/handle.js +1 -0
  116. package/dist/types/cms.d.ts +7 -0
  117. package/dist/types/collections.d.ts +2 -0
  118. package/dist/types/entries.d.ts +7 -1
  119. package/dist/types/fields.d.ts +9 -0
  120. package/dist/types/formFields.d.ts +15 -2
  121. package/dist/types/index.d.ts +2 -1
  122. package/dist/types/index.js +1 -0
  123. package/dist/updates/0.5.3/index.d.ts +2 -0
  124. package/dist/updates/0.5.3/index.js +19 -0
  125. package/dist/updates/0.5.4/index.d.ts +2 -0
  126. package/dist/updates/0.5.4/index.js +15 -0
  127. package/dist/updates/0.5.5/index.d.ts +2 -0
  128. package/dist/updates/0.5.5/index.js +20 -0
  129. package/dist/updates/index.js +4 -1
  130. package/package.json +7 -1
  131. package/dist/admin/components/fields/standalone-field-renderer.svelte +0 -148
  132. package/dist/admin/components/fields/standalone-field-renderer.svelte.d.ts +0 -9
@@ -0,0 +1,4 @@
1
+ import Root from "./switch.svelte";
2
+ export { Root,
3
+ //
4
+ Root as Switch, };
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ import { Switch as SwitchPrimitive } from "bits-ui";
3
+ import { cn, type WithoutChildrenOrChild } from "../../../utils.js";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ checked = $bindable(false),
8
+ class: className,
9
+ ...restProps
10
+ }: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
11
+ </script>
12
+
13
+ <SwitchPrimitive.Root
14
+ bind:ref
15
+ bind:checked
16
+ data-slot="switch"
17
+ class={cn(
18
+ "peer inline-flex h-[22px] w-[38px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-border",
19
+ className
20
+ )}
21
+ {...restProps}
22
+ >
23
+ <SwitchPrimitive.Thumb
24
+ class="pointer-events-none block size-[18px] rounded-full bg-white shadow-sm ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
25
+ />
26
+ </SwitchPrimitive.Root>
@@ -0,0 +1,4 @@
1
+ import { Switch as SwitchPrimitive } from "bits-ui";
2
+ declare const Switch: import("svelte").Component<Omit<Omit<SwitchPrimitive.RootProps, "child">, "children">, {}, "ref" | "checked">;
3
+ type Switch = ReturnType<typeof Switch>;
4
+ export default Switch;
@@ -1,6 +1,6 @@
1
1
  import type { DatabaseAdapter } from '../types/adapters/db.js';
2
2
  import type { FilesAdapter } from '../types/adapters/files.js';
3
- import type { CMSConfig, ICMS, MediaConfig } from '../types/cms.js';
3
+ import type { ApiKeyConfig, CMSConfig, ICMS, MediaConfig } from '../types/cms.js';
4
4
  import type { CollectionConfigWithType } from '../types/collections.js';
5
5
  import type { Language } from '../types/languages.js';
6
6
  import type { SingleConfigWithType } from '../types/singles.js';
@@ -22,6 +22,7 @@ export declare class CMS implements ICMS {
22
22
  languages: Language[];
23
23
  mediaConfig: MediaConfig;
24
24
  plugins: PluginConfig[];
25
+ apiKeys: ApiKeyConfig[];
25
26
  constructor(config: CMSConfig);
26
27
  getBySlug(slug: string): CollectionConfigWithType | SingleConfigWithType;
27
28
  getFormBySlug(slug: string): FormConfig;
package/dist/core/cms.js CHANGED
@@ -11,6 +11,7 @@ export class CMS {
11
11
  languages;
12
12
  mediaConfig;
13
13
  plugins = [];
14
+ apiKeys = [];
14
15
  constructor(config) {
15
16
  this.config = config;
16
17
  this.databaseAdapter = config.db;
@@ -40,6 +41,7 @@ export class CMS {
40
41
  };
41
42
  });
42
43
  this.languages = config.languages || [];
44
+ this.apiKeys = config.apiKeys || [];
43
45
  if (config.plugins) {
44
46
  this.plugins = config.plugins;
45
47
  }
@@ -139,7 +139,8 @@ export function generateZodSchemaFromField(field, languages, options = {
139
139
  }
140
140
  case 'object': {
141
141
  const data = generateZodSchemaFromFields(field.fields, languages, {
142
- parentRequired: field.required
142
+ parentRequired: field.required,
143
+ localized: options.localized
143
144
  });
144
145
  const finalData = !field.required ? data.partial().optional() : data;
145
146
  return z.object({
@@ -148,7 +149,10 @@ export function generateZodSchemaFromField(field, languages, options = {
148
149
  });
149
150
  }
150
151
  case 'blocks': {
151
- const schemas = field.of.map((f) => generateZodSchemaFromField(f, languages, { parentRequired: field.required }));
152
+ const schemas = field.of.map((f) => generateZodSchemaFromField(f, languages, {
153
+ parentRequired: field.required,
154
+ localized: options.localized
155
+ }));
152
156
  const itemSchema = schemas.length > 1
153
157
  ? z.discriminatedUnion('slug', schemas)
154
158
  : schemas[0];
@@ -283,6 +287,14 @@ export function generateZodSchemaFromField(field, languages, options = {
283
287
  }
284
288
  export function generateZodSchemaFromFields(fields, languages, options = {}) {
285
289
  return z.object({
286
- ...Object.fromEntries(fields.map((f) => [f.slug, generateZodSchemaFromField(f, languages, options)]))
290
+ ...Object.fromEntries(fields.map((f) => {
291
+ let schema = generateZodSchemaFromField(f, languages, options);
292
+ // Fields with showWhen are conditionally visible — make them optional
293
+ // so hidden fields don't block validation
294
+ if (f.showWhen) {
295
+ schema = schema.optional();
296
+ }
297
+ return [f.slug, schema];
298
+ }))
287
299
  });
288
300
  }
@@ -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
+ }
@@ -161,7 +161,8 @@ export const getEntries = async (options = {}) => {
161
161
  // Get entries that match the slug/ids filter
162
162
  const dbEntries = await getCMS().databaseAdapter.getEntries({
163
163
  ids,
164
- slug
164
+ slug,
165
+ orderBy: options.orderBy
165
166
  });
166
167
  if (dbEntries.length === 0) {
167
168
  return [];
@@ -6,6 +6,7 @@ export declare const updateEntrySchema: z.ZodObject<{
6
6
  publishedAt: z.ZodOptional<z.ZodNullable<z.ZodDate>>;
7
7
  publishedVersionId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
8
8
  publishedBy: z.ZodOptional<z.ZodNullable<z.ZodString>>;
9
+ sortOrder: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
9
10
  }, z.z.core.$strip>;
10
11
  export declare const updateEntry: (id: string, data: Partial<DbEntry>) => Promise<DbEntry>;
11
12
  export declare const updateEntryVersionSchema: z.ZodObject<{
@@ -9,7 +9,8 @@ export const updateEntrySchema = z.object({
9
9
  archivedAt: z.date().nullable().optional(),
10
10
  publishedAt: z.date().nullable().optional(),
11
11
  publishedVersionId: z.string().uuid().nullable().optional(),
12
- publishedBy: z.string().nullable().optional()
12
+ publishedBy: z.string().nullable().optional(),
13
+ sortOrder: z.number().int().nullable().optional()
13
14
  });
14
15
  export const updateEntry = async (id, data) => {
15
16
  const filteredDataParse = updateEntrySchema.safeParse(data);
@@ -33,6 +34,9 @@ export const updateEntry = async (id, data) => {
33
34
  if (filteredData.publishedBy !== undefined) {
34
35
  dataToUpdate.publishedBy = filteredData.publishedBy;
35
36
  }
37
+ if (filteredData.sortOrder !== undefined) {
38
+ dataToUpdate.sortOrder = filteredData.sortOrder;
39
+ }
36
40
  const updatedEntry = await getCMS().databaseAdapter.updateEntry({
37
41
  id,
38
42
  data: {
@@ -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
  }