@xleddyl/nuxt-cms 0.1.32 → 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/dist/module.json +1 -1
- package/dist/module.mjs +12 -3
- 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 +6 -0
- package/dist/runtime/shared/index.d.ts +7 -2
- package/dist/runtime/shared/index.js +28 -1
- package/dist/runtime/shared/validation.js +15 -7
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -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,16 @@ 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
|
+
}
|
|
172
181
|
if (blockField.private)
|
|
173
182
|
errors.push(`${bfat}: private fields are not supported inside blocks`);
|
|
174
183
|
if (blockField.type === "select" && !blockField.options?.length) {
|
|
@@ -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,8 +21,10 @@ import * as cmsTables from "#cms-tables";
|
|
|
21
21
|
import { useRuntimeConfig } from "#imports";
|
|
22
22
|
import {
|
|
23
23
|
decodeTranslatableMedia,
|
|
24
|
+
hasTranslatableBlockFields,
|
|
24
25
|
isPrivateField,
|
|
25
26
|
isTranslatableMediaField,
|
|
27
|
+
localizeBlocks,
|
|
26
28
|
mediaPublicUrl,
|
|
27
29
|
mediaTypeFor,
|
|
28
30
|
pickTranslatedMedia,
|
|
@@ -104,6 +106,10 @@ function localizeRow(entry, row, locale) {
|
|
|
104
106
|
const value = row[key];
|
|
105
107
|
result[key] = value?.[locale] ?? value?.[defaultLocale] ?? null;
|
|
106
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
|
+
}
|
|
107
113
|
return result;
|
|
108
114
|
}
|
|
109
115
|
const OPERATORS = {
|
|
@@ -63,7 +63,8 @@ export interface FieldConfig {
|
|
|
63
63
|
export declare function isPrivateField(field: FieldConfig): boolean;
|
|
64
64
|
export declare function isTranslatableField(field: FieldConfig): boolean;
|
|
65
65
|
export declare function isTranslatableMediaField(field: FieldConfig): boolean;
|
|
66
|
-
export declare function
|
|
66
|
+
export declare function decodeTranslatableValue(value: unknown, defaultLocale: string): Record<string, string> | null;
|
|
67
|
+
export declare const decodeTranslatableMedia: typeof decodeTranslatableValue;
|
|
67
68
|
export declare function encodeTranslatableMedia(value: unknown): string | null;
|
|
68
69
|
export declare function pickTranslatedMedia(values: Record<string, string> | null | undefined, locale: string, defaultLocale: string): string | null;
|
|
69
70
|
export declare function translatableMediaKeys(entry: Pick<CmsEntry, 'fields'>): string[];
|
|
@@ -71,6 +72,10 @@ export declare function encodeEntryTranslatableMedia(entry: Pick<CmsEntry, 'fiel
|
|
|
71
72
|
export declare function decodeEntryTranslatableMedia<T extends Record<string, unknown>>(entry: Pick<CmsEntry, 'fields'>, rows: T[], defaultLocale: string): T[];
|
|
72
73
|
export declare function isMultiSelect(field: FieldConfig): boolean;
|
|
73
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;
|
|
74
79
|
export interface CmsEntry {
|
|
75
80
|
id: string;
|
|
76
81
|
label: string;
|
|
@@ -132,7 +137,7 @@ export interface RelationFieldInput extends FieldInputBase {
|
|
|
132
137
|
cardinality?: 'many-to-one' | 'one-to-one' | 'many-to-many';
|
|
133
138
|
onDelete?: 'set null' | 'cascade' | 'restrict';
|
|
134
139
|
}
|
|
135
|
-
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'>;
|
|
136
141
|
export interface BlockInput {
|
|
137
142
|
label: string;
|
|
138
143
|
fields: Record<string, BlockFieldInput>;
|
|
@@ -81,13 +81,14 @@ function parseJsonObject(raw) {
|
|
|
81
81
|
}
|
|
82
82
|
return null;
|
|
83
83
|
}
|
|
84
|
-
export function
|
|
84
|
+
export function decodeTranslatableValue(value, defaultLocale) {
|
|
85
85
|
if (value == null) return null;
|
|
86
86
|
if (typeof value === "object") return value;
|
|
87
87
|
const raw = String(value).trim();
|
|
88
88
|
if (!raw) return null;
|
|
89
89
|
return (raw.startsWith("{") ? parseJsonObject(raw) : null) ?? { [defaultLocale]: raw };
|
|
90
90
|
}
|
|
91
|
+
export const decodeTranslatableMedia = decodeTranslatableValue;
|
|
91
92
|
export function encodeTranslatableMedia(value) {
|
|
92
93
|
if (value == null) return null;
|
|
93
94
|
if (typeof value === "string") return value.trim() || null;
|
|
@@ -131,6 +132,32 @@ export function isMultiSelect(field) {
|
|
|
131
132
|
export function translatableFieldKeys(entry) {
|
|
132
133
|
return Object.entries(entry.fields).filter(([, field]) => isTranslatableField(field)).map(([key]) => key);
|
|
133
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
|
+
}
|
|
134
161
|
export function defineCmsConfig(config) {
|
|
135
162
|
return config;
|
|
136
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)",
|