@xleddyl/nuxt-cms 0.1.31 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +18 -5
- package/dist/runtime/app/components/cms/BlocksField.d.vue.ts +1 -0
- package/dist/runtime/app/components/cms/BlocksField.vue +3 -1
- package/dist/runtime/app/components/cms/BlocksField.vue.d.ts +1 -0
- package/dist/runtime/app/components/cms/EntryForm.vue +8 -3
- package/dist/runtime/app/components/cms/FieldInput.vue +6 -1
- package/dist/runtime/server/utils/graphql.js +10 -1
- package/dist/runtime/shared/graphql-sdl.js +5 -3
- package/dist/runtime/shared/index.d.ts +10 -2
- package/dist/runtime/shared/index.js +31 -1
- package/dist/runtime/shared/validation.js +15 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ nuxt-cms is a Nuxt module that leverages the Nitro server to ship a lightweight
|
|
|
5
5
|
- **Zero extra infrastructure**: the CMS runs inside your app's Nitro server; you deploy one thing.
|
|
6
6
|
- **Content types in code**: a `cms.config.ts` with `defineCmsConfig()` declares collections, single documents, relations, blocks and translatable fields; database schema, migrations and TypeScript types are generated from it.
|
|
7
7
|
- **Admin panel at `/cms`**: entry editing with validation, drafts, media library (S3-compatible storage, or a local mode backed directly by your `public/` folder), single-admin auth from env credentials.
|
|
8
|
-
- **Public GraphQL API**: read-only, typed end-to-end via gql.tada, with filtering, sorting and pagination.
|
|
8
|
+
- **Public GraphQL API**: read-only, typed end-to-end via gql.tada, with filtering, sorting and pagination; fields marked `private` stay out of it.
|
|
9
9
|
- **SQLite, Postgres or libSQL/Turso**: a local file database by default, one config line to switch (including remote SQLite over the network).
|
|
10
10
|
|
|
11
11
|
## Installation
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { introspectionFromSchema, buildSchema } from 'graphql';
|
|
|
10
10
|
import { minifyIntrospection, outputIntrospectionFile } from 'gql.tada/internal';
|
|
11
11
|
import { createJiti } from 'jiti';
|
|
12
12
|
import { typeName, blockTypeName, blockUnionName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
|
|
13
|
-
import { isMultiSelect, isTranslatableField, isTranslatableMediaField, DEFAULT_MEDIA_MAX_FILE_SIZE } from '../dist/runtime/shared/index.js';
|
|
13
|
+
import { isMultiSelect, isTranslatableField, isTranslatableMediaField, isPrivateField, DEFAULT_MEDIA_MAX_FILE_SIZE } from '../dist/runtime/shared/index.js';
|
|
14
14
|
import { scanMediaDirectory, readMediaFileMeta } from '../dist/runtime/server/utils/media-sync.js';
|
|
15
15
|
|
|
16
16
|
async function collectMediaManifest(root) {
|
|
@@ -44,6 +44,7 @@ const IDENTIFIER = /^[a-z_]\w*$/i;
|
|
|
44
44
|
const RESERVED_ENTRY_KEYS = ["admin", "auth", "login", "media", "graphql", "cms_media"];
|
|
45
45
|
const RESERVED_COLUMNS = ["id", "status", "created_at", "updated_at"];
|
|
46
46
|
const TITLE_FIELD_TYPES = ["text", "slug", "email", "number", "date", "select"];
|
|
47
|
+
const TRANSLATABLE_FIELD_TYPES = ["text", "richtext", "media"];
|
|
47
48
|
const RESERVED_TYPE_NAMES = [
|
|
48
49
|
"Query",
|
|
49
50
|
"Mutation",
|
|
@@ -124,7 +125,7 @@ function validateConfig(config, i18n) {
|
|
|
124
125
|
columnNames.add(column);
|
|
125
126
|
}
|
|
126
127
|
if (field.translatable) {
|
|
127
|
-
if (
|
|
128
|
+
if (!TRANSLATABLE_FIELD_TYPES.includes(field.type))
|
|
128
129
|
errors.push(
|
|
129
130
|
`${fat}: translatable is only supported on text, richtext and media fields`
|
|
130
131
|
);
|
|
@@ -167,8 +168,18 @@ function validateConfig(config, i18n) {
|
|
|
167
168
|
`${bfat}: ${blockField.type} fields are not supported inside blocks`
|
|
168
169
|
);
|
|
169
170
|
}
|
|
170
|
-
if (blockField.translatable)
|
|
171
|
-
|
|
171
|
+
if (blockField.translatable) {
|
|
172
|
+
if (!TRANSLATABLE_FIELD_TYPES.includes(blockField.type))
|
|
173
|
+
errors.push(
|
|
174
|
+
`${bfat}: translatable is only supported on text, richtext and media fields`
|
|
175
|
+
);
|
|
176
|
+
if (!locales.length)
|
|
177
|
+
errors.push(
|
|
178
|
+
`${bfat}: translatable requires cms.i18n.locales in nuxt.config`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (blockField.private)
|
|
182
|
+
errors.push(`${bfat}: private fields are not supported inside blocks`);
|
|
172
183
|
if (blockField.type === "select" && !blockField.options?.length) {
|
|
173
184
|
errors.push(`${bfat}: select requires a non-empty options array`);
|
|
174
185
|
}
|
|
@@ -416,6 +427,7 @@ ${lines.join("\n")}
|
|
|
416
427
|
function entryTs(config, name, entry) {
|
|
417
428
|
const lines = [" id: string"];
|
|
418
429
|
for (const [key, field] of Object.entries(entry.fields)) {
|
|
430
|
+
if (isPrivateField(field)) continue;
|
|
419
431
|
lines.push(` ${key}: ${fieldTsType(config, name, key, field)}`);
|
|
420
432
|
}
|
|
421
433
|
if (entry.kind === "collection") lines.push(" createdAt: string");
|
|
@@ -440,7 +452,8 @@ function renderTypesFile(config) {
|
|
|
440
452
|
];
|
|
441
453
|
for (const [name, entry] of Object.entries(config)) {
|
|
442
454
|
for (const [key, field] of Object.entries(entry.fields)) {
|
|
443
|
-
if (field.type === "blocks"
|
|
455
|
+
if (field.type === "blocks" && !isPrivateField(field))
|
|
456
|
+
parts.push(...blockTypesTs(name, key, field));
|
|
444
457
|
}
|
|
445
458
|
parts.push(entryTs(config, name, entry));
|
|
446
459
|
}
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
<CmsFieldInput
|
|
64
64
|
:model-value="item[blockKey]"
|
|
65
65
|
:field="blockField"
|
|
66
|
+
:locale="locale"
|
|
66
67
|
@update:model-value="(value) => updateField(index, blockKey, value)"
|
|
67
68
|
/>
|
|
68
69
|
</CmsFormField>
|
|
@@ -77,7 +78,8 @@
|
|
|
77
78
|
<script setup>
|
|
78
79
|
import { computed, ref } from "#imports";
|
|
79
80
|
const props = defineProps({
|
|
80
|
-
field: { type: Object, required: true }
|
|
81
|
+
field: { type: Object, required: true },
|
|
82
|
+
locale: { type: String, required: false }
|
|
81
83
|
});
|
|
82
84
|
const model = defineModel({ type: [Array, null], ...{ required: true } });
|
|
83
85
|
const blocks = computed(() => props.field.blocks ?? {});
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
:name="key"
|
|
14
14
|
:required="field.required"
|
|
15
15
|
>
|
|
16
|
-
<template v-if="
|
|
16
|
+
<template v-if="hasLocaleSwitch(field) && i18n.locales.length > 1" #label-actions>
|
|
17
17
|
<CmsLocaleSwitch
|
|
18
18
|
:model-value="localeFor(key)"
|
|
19
|
-
:value="
|
|
19
|
+
:value="
|
|
20
|
+
isTranslatableField(field) ? state[key] : null
|
|
21
|
+
"
|
|
20
22
|
@update:model-value="(locale) => setLocale(key, locale)"
|
|
21
23
|
/>
|
|
22
24
|
</template>
|
|
@@ -41,7 +43,7 @@
|
|
|
41
43
|
</template>
|
|
42
44
|
|
|
43
45
|
<script setup>
|
|
44
|
-
import { isTranslatableField } from "#nuxt-cms";
|
|
46
|
+
import { hasTranslatableBlockFields, isTranslatableField } from "#nuxt-cms";
|
|
45
47
|
import { computed, ref } from "#imports";
|
|
46
48
|
import { buildEntrySchema } from "../../../shared/validation";
|
|
47
49
|
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
@@ -56,6 +58,9 @@ const state = defineModel({ type: Object, ...{ required: true } });
|
|
|
56
58
|
const emit = defineEmits(["submit", "error"]);
|
|
57
59
|
const { i18n } = useCmsRuntime();
|
|
58
60
|
const activeLocale = ref({});
|
|
61
|
+
function hasLocaleSwitch(field) {
|
|
62
|
+
return isTranslatableField(field) || hasTranslatableBlockFields(field);
|
|
63
|
+
}
|
|
59
64
|
function localeFor(key) {
|
|
60
65
|
return activeLocale.value[key] ?? i18n.defaultLocale;
|
|
61
66
|
}
|
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
:field="field"
|
|
6
6
|
:locale="locale"
|
|
7
7
|
/>
|
|
8
|
-
<CmsBlocksField
|
|
8
|
+
<CmsBlocksField
|
|
9
|
+
v-else-if="field.type === 'blocks'"
|
|
10
|
+
v-model="blocksValue"
|
|
11
|
+
:field="field"
|
|
12
|
+
:locale="locale"
|
|
13
|
+
/>
|
|
9
14
|
<CmsRichTextField v-else-if="field.type === 'richtext'" v-model="strOrNull" />
|
|
10
15
|
<CmsTextarea v-else-if="field.type === 'text' && field.textarea" v-model="str" :rows="8" />
|
|
11
16
|
<CmsInput v-else-if="field.type === 'text'" v-model="str" />
|
|
@@ -21,7 +21,10 @@ import * as cmsTables from "#cms-tables";
|
|
|
21
21
|
import { useRuntimeConfig } from "#imports";
|
|
22
22
|
import {
|
|
23
23
|
decodeTranslatableMedia,
|
|
24
|
+
hasTranslatableBlockFields,
|
|
25
|
+
isPrivateField,
|
|
24
26
|
isTranslatableMediaField,
|
|
27
|
+
localizeBlocks,
|
|
25
28
|
mediaPublicUrl,
|
|
26
29
|
mediaTypeFor,
|
|
27
30
|
pickTranslatedMedia,
|
|
@@ -103,6 +106,10 @@ function localizeRow(entry, row, locale) {
|
|
|
103
106
|
const value = row[key];
|
|
104
107
|
result[key] = value?.[locale] ?? value?.[defaultLocale] ?? null;
|
|
105
108
|
}
|
|
109
|
+
for (const [key, field] of Object.entries(entry.fields)) {
|
|
110
|
+
if (isPrivateField(field) || !hasTranslatableBlockFields(field)) continue;
|
|
111
|
+
result[key] = localizeBlocks(field, result[key], locale, defaultLocale);
|
|
112
|
+
}
|
|
106
113
|
return result;
|
|
107
114
|
}
|
|
108
115
|
const OPERATORS = {
|
|
@@ -257,6 +264,7 @@ function mediaObject(key, row) {
|
|
|
257
264
|
function entryResolvers(config, name, entry) {
|
|
258
265
|
const resolvers = {};
|
|
259
266
|
for (const [key, field] of Object.entries(entry.fields)) {
|
|
267
|
+
if (isPrivateField(field)) continue;
|
|
260
268
|
if (field.type === "relation" && field.cardinality === "many-to-many") {
|
|
261
269
|
resolvers[key] = async (parent, _args, ctx) => {
|
|
262
270
|
const rows = await loadManyToMany(ctx, name, key, field).load(parent.id);
|
|
@@ -301,7 +309,8 @@ export function buildCmsSchema() {
|
|
|
301
309
|
const fieldLevel = entryResolvers(config, name, entry);
|
|
302
310
|
if (Object.keys(fieldLevel).length) typeResolvers[gqlType] = fieldLevel;
|
|
303
311
|
for (const [key, field] of Object.entries(entry.fields)) {
|
|
304
|
-
if (field.type === "blocks"
|
|
312
|
+
if (field.type === "blocks" && !isPrivateField(field))
|
|
313
|
+
Object.assign(typeResolvers, blockResolvers(name, key, field));
|
|
305
314
|
}
|
|
306
315
|
if (entry.kind === "single") {
|
|
307
316
|
queryResolvers[name] = async (_, args) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isTranslatableField } from "./index.js";
|
|
1
|
+
import { isPrivateField, isTranslatableField } from "./index.js";
|
|
2
2
|
export function typeName(name) {
|
|
3
3
|
return name.replace(/(?:^|_)([a-z0-9])/gi, (_, c) => c.toUpperCase());
|
|
4
4
|
}
|
|
@@ -21,7 +21,7 @@ function scalarFor(field) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
function filterScalarFor(field) {
|
|
24
|
-
if (isTranslatableField(field)) return null;
|
|
24
|
+
if (isPrivateField(field) || isTranslatableField(field)) return null;
|
|
25
25
|
switch (field.type) {
|
|
26
26
|
case "json":
|
|
27
27
|
return null;
|
|
@@ -64,6 +64,7 @@ function fieldSdl(config, entryName, key, field) {
|
|
|
64
64
|
function entrySdl(config, name, entry) {
|
|
65
65
|
const lines = [" id: ID!"];
|
|
66
66
|
for (const [key, field] of Object.entries(entry.fields)) {
|
|
67
|
+
if (isPrivateField(field)) continue;
|
|
67
68
|
lines.push(fieldSdl(config, name, key, field));
|
|
68
69
|
}
|
|
69
70
|
if (entry.kind === "collection") lines.push(" createdAt: String!");
|
|
@@ -127,7 +128,8 @@ export function renderGraphqlSdl(config) {
|
|
|
127
128
|
const gqlType = typeName(name);
|
|
128
129
|
types.push(entrySdl(config, name, entry));
|
|
129
130
|
for (const [key, field] of Object.entries(entry.fields)) {
|
|
130
|
-
if (field.type === "blocks"
|
|
131
|
+
if (field.type === "blocks" && !isPrivateField(field))
|
|
132
|
+
types.push(...blocksSdl(name, key, field));
|
|
131
133
|
}
|
|
132
134
|
if (entry.kind === "single") {
|
|
133
135
|
queryLines.push(` ${name}(locale: String): ${gqlType}`);
|
|
@@ -46,6 +46,7 @@ export interface FieldConfig {
|
|
|
46
46
|
label: string;
|
|
47
47
|
type: FieldType;
|
|
48
48
|
required?: boolean;
|
|
49
|
+
private?: boolean;
|
|
49
50
|
textarea?: boolean;
|
|
50
51
|
integer?: boolean;
|
|
51
52
|
translatable?: boolean;
|
|
@@ -59,9 +60,11 @@ export interface FieldConfig {
|
|
|
59
60
|
cardinality?: 'many-to-one' | 'one-to-one' | 'many-to-many';
|
|
60
61
|
onDelete?: 'set null' | 'cascade' | 'restrict';
|
|
61
62
|
}
|
|
63
|
+
export declare function isPrivateField(field: FieldConfig): boolean;
|
|
62
64
|
export declare function isTranslatableField(field: FieldConfig): boolean;
|
|
63
65
|
export declare function isTranslatableMediaField(field: FieldConfig): boolean;
|
|
64
|
-
export declare function
|
|
66
|
+
export declare function decodeTranslatableValue(value: unknown, defaultLocale: string): Record<string, string> | null;
|
|
67
|
+
export declare const decodeTranslatableMedia: typeof decodeTranslatableValue;
|
|
65
68
|
export declare function encodeTranslatableMedia(value: unknown): string | null;
|
|
66
69
|
export declare function pickTranslatedMedia(values: Record<string, string> | null | undefined, locale: string, defaultLocale: string): string | null;
|
|
67
70
|
export declare function translatableMediaKeys(entry: Pick<CmsEntry, 'fields'>): string[];
|
|
@@ -69,6 +72,10 @@ export declare function encodeEntryTranslatableMedia(entry: Pick<CmsEntry, 'fiel
|
|
|
69
72
|
export declare function decodeEntryTranslatableMedia<T extends Record<string, unknown>>(entry: Pick<CmsEntry, 'fields'>, rows: T[], defaultLocale: string): T[];
|
|
70
73
|
export declare function isMultiSelect(field: FieldConfig): boolean;
|
|
71
74
|
export declare function translatableFieldKeys(entry: CmsEntry): string[];
|
|
75
|
+
export declare function translatableBlockFieldKeys(block: BlockConfig): string[];
|
|
76
|
+
export declare function hasTranslatableBlockFields(field: FieldConfig): boolean;
|
|
77
|
+
export declare function localizeBlock(field: FieldConfig, item: unknown, locale: string, defaultLocale: string): unknown;
|
|
78
|
+
export declare function localizeBlocks(field: FieldConfig, value: unknown, locale: string, defaultLocale: string): unknown;
|
|
72
79
|
export interface CmsEntry {
|
|
73
80
|
id: string;
|
|
74
81
|
label: string;
|
|
@@ -82,6 +89,7 @@ export type CmsConfig = Record<string, CmsEntry>;
|
|
|
82
89
|
interface FieldInputBase {
|
|
83
90
|
label: string;
|
|
84
91
|
required?: boolean;
|
|
92
|
+
private?: boolean;
|
|
85
93
|
}
|
|
86
94
|
export interface TextFieldInput extends FieldInputBase {
|
|
87
95
|
type: 'text';
|
|
@@ -129,7 +137,7 @@ export interface RelationFieldInput extends FieldInputBase {
|
|
|
129
137
|
cardinality?: 'many-to-one' | 'one-to-one' | 'many-to-many';
|
|
130
138
|
onDelete?: 'set null' | 'cascade' | 'restrict';
|
|
131
139
|
}
|
|
132
|
-
export type BlockFieldInput = Omit<TextFieldInput, '
|
|
140
|
+
export type BlockFieldInput = Omit<TextFieldInput, 'private'> | Omit<RichtextFieldInput, 'private'> | Omit<NumberFieldInput, 'private'> | Omit<BooleanFieldInput, 'private'> | Omit<DateFieldInput, 'private'> | Omit<EmailFieldInput, 'private'> | Omit<SelectFieldInput, 'private'> | Omit<JsonFieldInput, 'private'> | Omit<MediaFieldInput, 'private'>;
|
|
133
141
|
export interface BlockInput {
|
|
134
142
|
label: string;
|
|
135
143
|
fields: Record<string, BlockFieldInput>;
|
|
@@ -61,6 +61,9 @@ export function normalizeMediaFolder(value) {
|
|
|
61
61
|
const segments = value.split("/").map(slugify).filter(Boolean).slice(0, MEDIA_FOLDER_MAX_DEPTH);
|
|
62
62
|
return segments.length ? segments.join("/") : null;
|
|
63
63
|
}
|
|
64
|
+
export function isPrivateField(field) {
|
|
65
|
+
return !!field.private;
|
|
66
|
+
}
|
|
64
67
|
export function isTranslatableField(field) {
|
|
65
68
|
return !!field.translatable && (field.type === "text" || field.type === "richtext" || field.type === "media");
|
|
66
69
|
}
|
|
@@ -78,13 +81,14 @@ function parseJsonObject(raw) {
|
|
|
78
81
|
}
|
|
79
82
|
return null;
|
|
80
83
|
}
|
|
81
|
-
export function
|
|
84
|
+
export function decodeTranslatableValue(value, defaultLocale) {
|
|
82
85
|
if (value == null) return null;
|
|
83
86
|
if (typeof value === "object") return value;
|
|
84
87
|
const raw = String(value).trim();
|
|
85
88
|
if (!raw) return null;
|
|
86
89
|
return (raw.startsWith("{") ? parseJsonObject(raw) : null) ?? { [defaultLocale]: raw };
|
|
87
90
|
}
|
|
91
|
+
export const decodeTranslatableMedia = decodeTranslatableValue;
|
|
88
92
|
export function encodeTranslatableMedia(value) {
|
|
89
93
|
if (value == null) return null;
|
|
90
94
|
if (typeof value === "string") return value.trim() || null;
|
|
@@ -128,6 +132,32 @@ export function isMultiSelect(field) {
|
|
|
128
132
|
export function translatableFieldKeys(entry) {
|
|
129
133
|
return Object.entries(entry.fields).filter(([, field]) => isTranslatableField(field)).map(([key]) => key);
|
|
130
134
|
}
|
|
135
|
+
export function translatableBlockFieldKeys(block) {
|
|
136
|
+
return Object.entries(block.fields).filter(([, field]) => isTranslatableField(field)).map(([key]) => key);
|
|
137
|
+
}
|
|
138
|
+
export function hasTranslatableBlockFields(field) {
|
|
139
|
+
if (field.type !== "blocks") return false;
|
|
140
|
+
return Object.values(field.blocks ?? {}).some(
|
|
141
|
+
(block) => Object.values(block.fields).some(isTranslatableField)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
export function localizeBlock(field, item, locale, defaultLocale) {
|
|
145
|
+
if (!item || typeof item !== "object") return item;
|
|
146
|
+
const block = field.blocks?.[String(item.type)];
|
|
147
|
+
if (!block) return item;
|
|
148
|
+
const keys = translatableBlockFieldKeys(block);
|
|
149
|
+
if (!keys.length) return item;
|
|
150
|
+
const localized = { ...item };
|
|
151
|
+
for (const key of keys) {
|
|
152
|
+
const values = decodeTranslatableValue(localized[key], defaultLocale);
|
|
153
|
+
localized[key] = isTranslatableMediaField(block.fields[key]) ? pickTranslatedMedia(values, locale, defaultLocale) : values?.[locale] ?? values?.[defaultLocale] ?? null;
|
|
154
|
+
}
|
|
155
|
+
return localized;
|
|
156
|
+
}
|
|
157
|
+
export function localizeBlocks(field, value, locale, defaultLocale) {
|
|
158
|
+
if (!Array.isArray(value)) return value;
|
|
159
|
+
return value.map((item) => localizeBlock(field, item, locale, defaultLocale));
|
|
160
|
+
}
|
|
131
161
|
export function defineCmsConfig(config) {
|
|
132
162
|
return config;
|
|
133
163
|
}
|
|
@@ -34,14 +34,24 @@ function scalarSchema(field, m) {
|
|
|
34
34
|
function optionalize(field, base) {
|
|
35
35
|
return field.required ? base : base.nullish().transform((v) => v ?? null);
|
|
36
36
|
}
|
|
37
|
-
function
|
|
37
|
+
function translatableSchema(field, i18n, m) {
|
|
38
|
+
const { locales, defaultLocale } = i18n;
|
|
39
|
+
const record = z.record(z.string(), field.type === "media" ? objectKeySchema : z.string()).refine((v) => Object.keys(v).every((k) => locales.includes(k)), m.unknownLocale);
|
|
40
|
+
return field.required ? record.refine((v) => !!v[defaultLocale]?.trim(), m.requiredLocale(defaultLocale)) : record.nullish().transform((v) => v ?? null);
|
|
41
|
+
}
|
|
42
|
+
function blockFieldSchema(blockField, i18n, m) {
|
|
43
|
+
if (isTranslatableField(blockField) && i18n.locales.length)
|
|
44
|
+
return translatableSchema(blockField, i18n, m);
|
|
45
|
+
return optionalize(blockField, scalarSchema(blockField, m));
|
|
46
|
+
}
|
|
47
|
+
function blocksSchema(field, i18n, m) {
|
|
38
48
|
const variants = Object.entries(field.blocks ?? {}).map(
|
|
39
49
|
([type, block]) => z.object({
|
|
40
50
|
type: z.literal(type),
|
|
41
51
|
...Object.fromEntries(
|
|
42
52
|
Object.entries(block.fields).map(([key, blockField]) => [
|
|
43
53
|
key,
|
|
44
|
-
|
|
54
|
+
blockFieldSchema(blockField, i18n, m)
|
|
45
55
|
])
|
|
46
56
|
)
|
|
47
57
|
})
|
|
@@ -50,7 +60,6 @@ function blocksSchema(field, m) {
|
|
|
50
60
|
}
|
|
51
61
|
export function buildEntrySchema(entry, i18n, messages) {
|
|
52
62
|
const m = { ...DEFAULT_MESSAGES, ...messages };
|
|
53
|
-
const { locales, defaultLocale } = i18n;
|
|
54
63
|
const shape = {};
|
|
55
64
|
for (const [key, field] of Object.entries(entry.fields)) {
|
|
56
65
|
if (field.type === "relation" && field.cardinality === "many-to-many") {
|
|
@@ -64,13 +73,12 @@ export function buildEntrySchema(entry, i18n, messages) {
|
|
|
64
73
|
continue;
|
|
65
74
|
}
|
|
66
75
|
if (field.type === "blocks") {
|
|
67
|
-
const list = blocksSchema(field, m);
|
|
76
|
+
const list = blocksSchema(field, i18n, m);
|
|
68
77
|
shape[key] = field.required ? list.min(1, m.required) : list.nullish().transform((v) => v ?? null);
|
|
69
78
|
continue;
|
|
70
79
|
}
|
|
71
|
-
if (isTranslatableField(field) && locales.length) {
|
|
72
|
-
|
|
73
|
-
shape[key] = field.required ? record.refine((v) => !!v[defaultLocale]?.trim(), m.requiredLocale(defaultLocale)) : record.nullish().transform((v) => v ?? null);
|
|
80
|
+
if (isTranslatableField(field) && i18n.locales.length) {
|
|
81
|
+
shape[key] = translatableSchema(field, i18n, m);
|
|
74
82
|
continue;
|
|
75
83
|
}
|
|
76
84
|
if (field.type === "json") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xleddyl/nuxt-cms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "Lightweight CMS that ships with your Nuxt app: runs on the Nitro server, content types defined in code, /cms admin panel, GraphQL API, SQLite or Postgres. No external CMS needed!",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Edoardo Alberti (https://github.com/xleddyl)",
|