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.
- package/CHANGELOG.md +60 -0
- package/ROADMAP.md +29 -0
- package/dist/admin/api/rest/handler.d.ts +7 -0
- package/dist/admin/api/rest/handler.js +116 -0
- package/dist/admin/api/rest/middleware/apiKey.d.ts +6 -0
- package/dist/admin/api/rest/middleware/apiKey.js +45 -0
- package/dist/admin/api/rest/routes/collections.d.ts +5 -0
- package/dist/admin/api/rest/routes/collections.js +104 -0
- package/dist/admin/api/rest/routes/entries.d.ts +2 -0
- package/dist/admin/api/rest/routes/entries.js +37 -0
- package/dist/admin/api/rest/routes/languages.d.ts +1 -0
- package/dist/admin/api/rest/routes/languages.js +5 -0
- package/dist/admin/api/rest/routes/schema.d.ts +2 -0
- package/dist/admin/api/rest/routes/schema.js +78 -0
- package/dist/admin/api/rest/routes/singletons.d.ts +3 -0
- package/dist/admin/api/rest/routes/singletons.js +60 -0
- package/dist/admin/auth-client.d.ts +7 -7
- package/dist/admin/client/collection/collection-entries.svelte +56 -5
- package/dist/admin/client/collection/data-table.svelte +127 -18
- package/dist/admin/client/collection/data-table.svelte.d.ts +2 -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 +28 -40
- 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/layout/nav-collections.svelte +2 -1
- package/dist/admin/components/layout/nav-forms.svelte +2 -1
- package/dist/admin/components/layout/nav-singletons.svelte +2 -1
- 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.d.ts +9 -1
- package/dist/admin/remote/entry.remote.js +30 -2
- 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/cli/scaffold/admin.js +8 -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/cms.d.ts +2 -1
- package/dist/core/cms.js +2 -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/entries/operations/get.js +2 -1
- package/dist/core/server/entries/operations/update.d.ts +1 -0
- package/dist/core/server/entries/operations/update.js +5 -1
- 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/db-postgres/schema/entry.d.ts +17 -0
- package/dist/db-postgres/schema/entry.js +4 -2
- package/dist/schemas/field/url.d.ts +2 -0
- package/dist/schemas/field/url.js +4 -2
- package/dist/server/auth.d.ts +6 -6
- package/dist/sveltekit/server/handle.js +1 -0
- package/dist/types/cms.d.ts +7 -0
- package/dist/types/collections.d.ts +2 -0
- package/dist/types/entries.d.ts +7 -1
- package/dist/types/fields.d.ts +9 -0
- package/dist/types/formFields.d.ts +15 -2
- package/dist/types/index.d.ts +2 -1
- 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/0.5.4/index.d.ts +2 -0
- package/dist/updates/0.5.4/index.js +15 -0
- package/dist/updates/0.5.5/index.d.ts +2 -0
- package/dist/updates/0.5.5/index.js +20 -0
- package/dist/updates/index.js +4 -1
- package/package.json +7 -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
|
@@ -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>
|
package/dist/core/cms.d.ts
CHANGED
|
@@ -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, {
|
|
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) =>
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
}
|