@xleddyl/nuxt-cms 0.1.32 → 0.1.34
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 +58 -4
- 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 +14 -4
- 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 +18 -2
- package/dist/runtime/shared/index.js +41 -1
- package/dist/runtime/shared/validation.js +40 -10
- package/package.json +1 -1
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, isPrivateField, DEFAULT_MEDIA_MAX_FILE_SIZE } from '../dist/runtime/shared/index.js';
|
|
13
|
+
import { isMultiSelect, fieldConditions, 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,8 @@ 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"];
|
|
48
|
+
const CONDITION_FIELD_TYPES = ["select", "boolean", "text", "number", "date", "email", "slug"];
|
|
47
49
|
const RESERVED_TYPE_NAMES = [
|
|
48
50
|
"Query",
|
|
49
51
|
"Mutation",
|
|
@@ -124,7 +126,7 @@ function validateConfig(config, i18n) {
|
|
|
124
126
|
columnNames.add(column);
|
|
125
127
|
}
|
|
126
128
|
if (field.translatable) {
|
|
127
|
-
if (
|
|
129
|
+
if (!TRANSLATABLE_FIELD_TYPES.includes(field.type))
|
|
128
130
|
errors.push(
|
|
129
131
|
`${fat}: translatable is only supported on text, richtext and media fields`
|
|
130
132
|
);
|
|
@@ -137,6 +139,48 @@ function validateConfig(config, i18n) {
|
|
|
137
139
|
else if (new Set(field.options).size !== field.options.length)
|
|
138
140
|
errors.push(`${fat}: select options must be unique`);
|
|
139
141
|
}
|
|
142
|
+
if (field.showIf) {
|
|
143
|
+
if (entry.titleField === key)
|
|
144
|
+
errors.push(`${fat}: the titleField cannot be conditional`);
|
|
145
|
+
for (const condition of fieldConditions(field)) {
|
|
146
|
+
const cat = `${fat}, showIf on '${condition.field}'`;
|
|
147
|
+
const target = entry.fields?.[condition.field];
|
|
148
|
+
if (!condition.field || !target) {
|
|
149
|
+
errors.push(`${cat}: '${condition.field}' is not a declared field`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (condition.field === key) {
|
|
153
|
+
errors.push(`${cat}: a field cannot depend on itself`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!CONDITION_FIELD_TYPES.includes(target.type) || isMultiSelect(target)) {
|
|
157
|
+
errors.push(
|
|
158
|
+
`${cat}: showIf can only depend on ${CONDITION_FIELD_TYPES.join(
|
|
159
|
+
", "
|
|
160
|
+
)} fields (got '${target.type}')`
|
|
161
|
+
);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (isTranslatableField(target)) {
|
|
165
|
+
errors.push(`${cat}: showIf cannot depend on a translatable field`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const values = condition.in ?? (condition.eq === void 0 ? null : [condition.eq]);
|
|
169
|
+
if (!values || !values.length) {
|
|
170
|
+
errors.push(`${cat}: showIf requires 'eq' or a non-empty 'in'`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (condition.in && condition.eq !== void 0)
|
|
174
|
+
errors.push(`${cat}: showIf accepts either 'eq' or 'in', not both`);
|
|
175
|
+
if (target.type === "select") {
|
|
176
|
+
const unknown = values.filter((value) => !target.options?.includes(String(value)));
|
|
177
|
+
if (unknown.length)
|
|
178
|
+
errors.push(
|
|
179
|
+
`${cat}: ${unknown.map((value) => `'${String(value)}'`).join(", ")} not in the options of '${condition.field}'`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
140
184
|
if (field.type === "slug" && field.from) {
|
|
141
185
|
const source = entry.fields?.[field.from];
|
|
142
186
|
if (!source) errors.push(`${fat}: slug source '${field.from}' is not a declared field`);
|
|
@@ -167,10 +211,20 @@ function validateConfig(config, i18n) {
|
|
|
167
211
|
`${bfat}: ${blockField.type} fields are not supported inside blocks`
|
|
168
212
|
);
|
|
169
213
|
}
|
|
170
|
-
if (blockField.translatable)
|
|
171
|
-
|
|
214
|
+
if (blockField.translatable) {
|
|
215
|
+
if (!TRANSLATABLE_FIELD_TYPES.includes(blockField.type))
|
|
216
|
+
errors.push(
|
|
217
|
+
`${bfat}: translatable is only supported on text, richtext and media fields`
|
|
218
|
+
);
|
|
219
|
+
if (!locales.length)
|
|
220
|
+
errors.push(
|
|
221
|
+
`${bfat}: translatable requires cms.i18n.locales in nuxt.config`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
172
224
|
if (blockField.private)
|
|
173
225
|
errors.push(`${bfat}: private fields are not supported inside blocks`);
|
|
226
|
+
if (blockField.showIf)
|
|
227
|
+
errors.push(`${bfat}: showIf is not supported inside blocks`);
|
|
174
228
|
if (blockField.type === "select" && !blockField.options?.length) {
|
|
175
229
|
errors.push(`${bfat}: select requires a non-empty options array`);
|
|
176
230
|
}
|
|
@@ -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 ?? {});
|
|
@@ -7,16 +7,18 @@
|
|
|
7
7
|
@error="emit('error')"
|
|
8
8
|
>
|
|
9
9
|
<CmsFormField
|
|
10
|
-
v-for="(field, key) in
|
|
10
|
+
v-for="(field, key) in visibleFields"
|
|
11
11
|
:key="key"
|
|
12
12
|
:label="field.label"
|
|
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, isFieldVisible, 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,14 @@ 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
|
+
const visibleFields = computed(
|
|
62
|
+
() => Object.fromEntries(
|
|
63
|
+
Object.entries(props.fields).filter(([, field]) => isFieldVisible(field, state.value))
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
function hasLocaleSwitch(field) {
|
|
67
|
+
return isTranslatableField(field) || hasTranslatableBlockFields(field);
|
|
68
|
+
}
|
|
59
69
|
function localeFor(key) {
|
|
60
70
|
return activeLocale.value[key] ?? i18n.defaultLocale;
|
|
61
71
|
}
|
|
@@ -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 = {
|
|
@@ -42,6 +42,12 @@ export interface BlockConfig {
|
|
|
42
42
|
label: string;
|
|
43
43
|
fields: Record<string, FieldConfig>;
|
|
44
44
|
}
|
|
45
|
+
export type ConditionValue = string | number | boolean;
|
|
46
|
+
export interface FieldCondition {
|
|
47
|
+
field: string;
|
|
48
|
+
eq?: ConditionValue;
|
|
49
|
+
in?: ConditionValue[];
|
|
50
|
+
}
|
|
45
51
|
export interface FieldConfig {
|
|
46
52
|
label: string;
|
|
47
53
|
type: FieldType;
|
|
@@ -59,11 +65,15 @@ export interface FieldConfig {
|
|
|
59
65
|
to?: string;
|
|
60
66
|
cardinality?: 'many-to-one' | 'one-to-one' | 'many-to-many';
|
|
61
67
|
onDelete?: 'set null' | 'cascade' | 'restrict';
|
|
68
|
+
showIf?: FieldCondition | FieldCondition[];
|
|
62
69
|
}
|
|
63
70
|
export declare function isPrivateField(field: FieldConfig): boolean;
|
|
71
|
+
export declare function fieldConditions(field: FieldConfig): FieldCondition[];
|
|
72
|
+
export declare function isFieldVisible(field: FieldConfig, values: Record<string, unknown> | null | undefined): boolean;
|
|
64
73
|
export declare function isTranslatableField(field: FieldConfig): boolean;
|
|
65
74
|
export declare function isTranslatableMediaField(field: FieldConfig): boolean;
|
|
66
|
-
export declare function
|
|
75
|
+
export declare function decodeTranslatableValue(value: unknown, defaultLocale: string): Record<string, string> | null;
|
|
76
|
+
export declare const decodeTranslatableMedia: typeof decodeTranslatableValue;
|
|
67
77
|
export declare function encodeTranslatableMedia(value: unknown): string | null;
|
|
68
78
|
export declare function pickTranslatedMedia(values: Record<string, string> | null | undefined, locale: string, defaultLocale: string): string | null;
|
|
69
79
|
export declare function translatableMediaKeys(entry: Pick<CmsEntry, 'fields'>): string[];
|
|
@@ -71,6 +81,10 @@ export declare function encodeEntryTranslatableMedia(entry: Pick<CmsEntry, 'fiel
|
|
|
71
81
|
export declare function decodeEntryTranslatableMedia<T extends Record<string, unknown>>(entry: Pick<CmsEntry, 'fields'>, rows: T[], defaultLocale: string): T[];
|
|
72
82
|
export declare function isMultiSelect(field: FieldConfig): boolean;
|
|
73
83
|
export declare function translatableFieldKeys(entry: CmsEntry): string[];
|
|
84
|
+
export declare function translatableBlockFieldKeys(block: BlockConfig): string[];
|
|
85
|
+
export declare function hasTranslatableBlockFields(field: FieldConfig): boolean;
|
|
86
|
+
export declare function localizeBlock(field: FieldConfig, item: unknown, locale: string, defaultLocale: string): unknown;
|
|
87
|
+
export declare function localizeBlocks(field: FieldConfig, value: unknown, locale: string, defaultLocale: string): unknown;
|
|
74
88
|
export interface CmsEntry {
|
|
75
89
|
id: string;
|
|
76
90
|
label: string;
|
|
@@ -85,6 +99,7 @@ interface FieldInputBase {
|
|
|
85
99
|
label: string;
|
|
86
100
|
required?: boolean;
|
|
87
101
|
private?: boolean;
|
|
102
|
+
showIf?: FieldCondition | FieldCondition[];
|
|
88
103
|
}
|
|
89
104
|
export interface TextFieldInput extends FieldInputBase {
|
|
90
105
|
type: 'text';
|
|
@@ -132,7 +147,8 @@ export interface RelationFieldInput extends FieldInputBase {
|
|
|
132
147
|
cardinality?: 'many-to-one' | 'one-to-one' | 'many-to-many';
|
|
133
148
|
onDelete?: 'set null' | 'cascade' | 'restrict';
|
|
134
149
|
}
|
|
135
|
-
|
|
150
|
+
type BlockField<T> = Omit<T, 'private' | 'showIf'>;
|
|
151
|
+
export type BlockFieldInput = BlockField<TextFieldInput> | BlockField<RichtextFieldInput> | BlockField<NumberFieldInput> | BlockField<BooleanFieldInput> | BlockField<DateFieldInput> | BlockField<EmailFieldInput> | BlockField<SelectFieldInput> | BlockField<JsonFieldInput> | BlockField<MediaFieldInput>;
|
|
136
152
|
export interface BlockInput {
|
|
137
153
|
label: string;
|
|
138
154
|
fields: Record<string, BlockFieldInput>;
|
|
@@ -64,6 +64,19 @@ export function normalizeMediaFolder(value) {
|
|
|
64
64
|
export function isPrivateField(field) {
|
|
65
65
|
return !!field.private;
|
|
66
66
|
}
|
|
67
|
+
export function fieldConditions(field) {
|
|
68
|
+
if (!field.showIf) return [];
|
|
69
|
+
return Array.isArray(field.showIf) ? field.showIf : [field.showIf];
|
|
70
|
+
}
|
|
71
|
+
function matchesCondition(condition, value) {
|
|
72
|
+
if (condition.in) return condition.in.some((option) => option === value);
|
|
73
|
+
return condition.eq === value;
|
|
74
|
+
}
|
|
75
|
+
export function isFieldVisible(field, values) {
|
|
76
|
+
const conditions = fieldConditions(field);
|
|
77
|
+
if (!conditions.length) return true;
|
|
78
|
+
return conditions.every((condition) => matchesCondition(condition, values?.[condition.field]));
|
|
79
|
+
}
|
|
67
80
|
export function isTranslatableField(field) {
|
|
68
81
|
return !!field.translatable && (field.type === "text" || field.type === "richtext" || field.type === "media");
|
|
69
82
|
}
|
|
@@ -81,13 +94,14 @@ function parseJsonObject(raw) {
|
|
|
81
94
|
}
|
|
82
95
|
return null;
|
|
83
96
|
}
|
|
84
|
-
export function
|
|
97
|
+
export function decodeTranslatableValue(value, defaultLocale) {
|
|
85
98
|
if (value == null) return null;
|
|
86
99
|
if (typeof value === "object") return value;
|
|
87
100
|
const raw = String(value).trim();
|
|
88
101
|
if (!raw) return null;
|
|
89
102
|
return (raw.startsWith("{") ? parseJsonObject(raw) : null) ?? { [defaultLocale]: raw };
|
|
90
103
|
}
|
|
104
|
+
export const decodeTranslatableMedia = decodeTranslatableValue;
|
|
91
105
|
export function encodeTranslatableMedia(value) {
|
|
92
106
|
if (value == null) return null;
|
|
93
107
|
if (typeof value === "string") return value.trim() || null;
|
|
@@ -131,6 +145,32 @@ export function isMultiSelect(field) {
|
|
|
131
145
|
export function translatableFieldKeys(entry) {
|
|
132
146
|
return Object.entries(entry.fields).filter(([, field]) => isTranslatableField(field)).map(([key]) => key);
|
|
133
147
|
}
|
|
148
|
+
export function translatableBlockFieldKeys(block) {
|
|
149
|
+
return Object.entries(block.fields).filter(([, field]) => isTranslatableField(field)).map(([key]) => key);
|
|
150
|
+
}
|
|
151
|
+
export function hasTranslatableBlockFields(field) {
|
|
152
|
+
if (field.type !== "blocks") return false;
|
|
153
|
+
return Object.values(field.blocks ?? {}).some(
|
|
154
|
+
(block) => Object.values(block.fields).some(isTranslatableField)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
export function localizeBlock(field, item, locale, defaultLocale) {
|
|
158
|
+
if (!item || typeof item !== "object") return item;
|
|
159
|
+
const block = field.blocks?.[String(item.type)];
|
|
160
|
+
if (!block) return item;
|
|
161
|
+
const keys = translatableBlockFieldKeys(block);
|
|
162
|
+
if (!keys.length) return item;
|
|
163
|
+
const localized = { ...item };
|
|
164
|
+
for (const key of keys) {
|
|
165
|
+
const values = decodeTranslatableValue(localized[key], defaultLocale);
|
|
166
|
+
localized[key] = isTranslatableMediaField(block.fields[key]) ? pickTranslatedMedia(values, locale, defaultLocale) : values?.[locale] ?? values?.[defaultLocale] ?? null;
|
|
167
|
+
}
|
|
168
|
+
return localized;
|
|
169
|
+
}
|
|
170
|
+
export function localizeBlocks(field, value, locale, defaultLocale) {
|
|
171
|
+
if (!Array.isArray(value)) return value;
|
|
172
|
+
return value.map((item) => localizeBlock(field, item, locale, defaultLocale));
|
|
173
|
+
}
|
|
134
174
|
export function defineCmsConfig(config) {
|
|
135
175
|
return config;
|
|
136
176
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { isTranslatableField } from "./index.js";
|
|
2
|
+
import { isFieldVisible, isTranslatableField } from "./index.js";
|
|
3
3
|
export const objectKeySchema = z.string().min(1).max(1024).refine((k) => !k.includes("..") && !k.startsWith("/"), "Invalid object key");
|
|
4
4
|
const DEFAULT_MESSAGES = {
|
|
5
5
|
required: "Required field",
|
|
@@ -34,25 +34,47 @@ 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
|
})
|
|
48
58
|
);
|
|
49
59
|
return z.array(z.discriminatedUnion("type", variants));
|
|
50
60
|
}
|
|
61
|
+
function isEmptyValue(field, value, defaultLocale) {
|
|
62
|
+
if (value == null) return true;
|
|
63
|
+
if (Array.isArray(value)) return !value.length;
|
|
64
|
+
if (isTranslatableField(field)) {
|
|
65
|
+
const values = value;
|
|
66
|
+
return !values[defaultLocale]?.trim();
|
|
67
|
+
}
|
|
68
|
+
if (typeof value === "string") return !value.trim();
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
51
71
|
export function buildEntrySchema(entry, i18n, messages) {
|
|
52
72
|
const m = { ...DEFAULT_MESSAGES, ...messages };
|
|
53
|
-
const { locales, defaultLocale } = i18n;
|
|
54
73
|
const shape = {};
|
|
55
|
-
|
|
74
|
+
const conditional = [];
|
|
75
|
+
for (const [key, declared] of Object.entries(entry.fields)) {
|
|
76
|
+
const field = declared.showIf ? { ...declared, required: false } : declared;
|
|
77
|
+
if (declared.showIf && declared.required) conditional.push([key, declared]);
|
|
56
78
|
if (field.type === "relation" && field.cardinality === "many-to-many") {
|
|
57
79
|
const list = z.array(z.string().min(1));
|
|
58
80
|
shape[key] = field.required ? list.min(1, m.required) : list.nullish().transform((v) => v ?? []);
|
|
@@ -64,13 +86,12 @@ export function buildEntrySchema(entry, i18n, messages) {
|
|
|
64
86
|
continue;
|
|
65
87
|
}
|
|
66
88
|
if (field.type === "blocks") {
|
|
67
|
-
const list = blocksSchema(field, m);
|
|
89
|
+
const list = blocksSchema(field, i18n, m);
|
|
68
90
|
shape[key] = field.required ? list.min(1, m.required) : list.nullish().transform((v) => v ?? null);
|
|
69
91
|
continue;
|
|
70
92
|
}
|
|
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);
|
|
93
|
+
if (isTranslatableField(field) && i18n.locales.length) {
|
|
94
|
+
shape[key] = translatableSchema(field, i18n, m);
|
|
74
95
|
continue;
|
|
75
96
|
}
|
|
76
97
|
if (field.type === "json") {
|
|
@@ -83,5 +104,14 @@ export function buildEntrySchema(entry, i18n, messages) {
|
|
|
83
104
|
if (entry.drafts) {
|
|
84
105
|
shape.status = z.enum(["draft", "published"]).nullish().transform((v) => v ?? void 0);
|
|
85
106
|
}
|
|
86
|
-
|
|
107
|
+
const schema = z.object(shape);
|
|
108
|
+
if (!conditional.length) return schema;
|
|
109
|
+
return schema.superRefine((values, ctx) => {
|
|
110
|
+
for (const [key, field] of conditional) {
|
|
111
|
+
if (!isFieldVisible(field, values)) continue;
|
|
112
|
+
if (!isEmptyValue(field, values[key], i18n.defaultLocale))
|
|
113
|
+
continue;
|
|
114
|
+
ctx.addIssue({ code: "custom", path: [key], message: m.required });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
87
117
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xleddyl/nuxt-cms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
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)",
|