@xleddyl/nuxt-cms 0.1.33 → 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
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) {
|
|
@@ -45,6 +45,7 @@ const RESERVED_ENTRY_KEYS = ["admin", "auth", "login", "media", "graphql", "cms_
|
|
|
45
45
|
const RESERVED_COLUMNS = ["id", "status", "created_at", "updated_at"];
|
|
46
46
|
const TITLE_FIELD_TYPES = ["text", "slug", "email", "number", "date", "select"];
|
|
47
47
|
const TRANSLATABLE_FIELD_TYPES = ["text", "richtext", "media"];
|
|
48
|
+
const CONDITION_FIELD_TYPES = ["select", "boolean", "text", "number", "date", "email", "slug"];
|
|
48
49
|
const RESERVED_TYPE_NAMES = [
|
|
49
50
|
"Query",
|
|
50
51
|
"Mutation",
|
|
@@ -138,6 +139,48 @@ function validateConfig(config, i18n) {
|
|
|
138
139
|
else if (new Set(field.options).size !== field.options.length)
|
|
139
140
|
errors.push(`${fat}: select options must be unique`);
|
|
140
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
|
+
}
|
|
141
184
|
if (field.type === "slug" && field.from) {
|
|
142
185
|
const source = entry.fields?.[field.from];
|
|
143
186
|
if (!source) errors.push(`${fat}: slug source '${field.from}' is not a declared field`);
|
|
@@ -180,6 +223,8 @@ function validateConfig(config, i18n) {
|
|
|
180
223
|
}
|
|
181
224
|
if (blockField.private)
|
|
182
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`);
|
|
183
228
|
if (blockField.type === "select" && !blockField.options?.length) {
|
|
184
229
|
errors.push(`${bfat}: select requires a non-empty options array`);
|
|
185
230
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
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"
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
</template>
|
|
44
44
|
|
|
45
45
|
<script setup>
|
|
46
|
-
import { hasTranslatableBlockFields, isTranslatableField } from "#nuxt-cms";
|
|
46
|
+
import { hasTranslatableBlockFields, isFieldVisible, isTranslatableField } from "#nuxt-cms";
|
|
47
47
|
import { computed, ref } from "#imports";
|
|
48
48
|
import { buildEntrySchema } from "../../../shared/validation";
|
|
49
49
|
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
@@ -58,6 +58,11 @@ const state = defineModel({ type: Object, ...{ required: true } });
|
|
|
58
58
|
const emit = defineEmits(["submit", "error"]);
|
|
59
59
|
const { i18n } = useCmsRuntime();
|
|
60
60
|
const activeLocale = ref({});
|
|
61
|
+
const visibleFields = computed(
|
|
62
|
+
() => Object.fromEntries(
|
|
63
|
+
Object.entries(props.fields).filter(([, field]) => isFieldVisible(field, state.value))
|
|
64
|
+
)
|
|
65
|
+
);
|
|
61
66
|
function hasLocaleSwitch(field) {
|
|
62
67
|
return isTranslatableField(field) || hasTranslatableBlockFields(field);
|
|
63
68
|
}
|
|
@@ -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,8 +65,11 @@ 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
75
|
export declare function decodeTranslatableValue(value: unknown, defaultLocale: string): Record<string, string> | null;
|
|
@@ -90,6 +99,7 @@ interface FieldInputBase {
|
|
|
90
99
|
label: string;
|
|
91
100
|
required?: boolean;
|
|
92
101
|
private?: boolean;
|
|
102
|
+
showIf?: FieldCondition | FieldCondition[];
|
|
93
103
|
}
|
|
94
104
|
export interface TextFieldInput extends FieldInputBase {
|
|
95
105
|
type: 'text';
|
|
@@ -137,7 +147,8 @@ export interface RelationFieldInput extends FieldInputBase {
|
|
|
137
147
|
cardinality?: 'many-to-one' | 'one-to-one' | 'many-to-many';
|
|
138
148
|
onDelete?: 'set null' | 'cascade' | 'restrict';
|
|
139
149
|
}
|
|
140
|
-
|
|
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>;
|
|
141
152
|
export interface BlockInput {
|
|
142
153
|
label: string;
|
|
143
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
|
}
|
|
@@ -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",
|
|
@@ -58,10 +58,23 @@ function blocksSchema(field, i18n, m) {
|
|
|
58
58
|
);
|
|
59
59
|
return z.array(z.discriminatedUnion("type", variants));
|
|
60
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
|
+
}
|
|
61
71
|
export function buildEntrySchema(entry, i18n, messages) {
|
|
62
72
|
const m = { ...DEFAULT_MESSAGES, ...messages };
|
|
63
73
|
const shape = {};
|
|
64
|
-
|
|
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]);
|
|
65
78
|
if (field.type === "relation" && field.cardinality === "many-to-many") {
|
|
66
79
|
const list = z.array(z.string().min(1));
|
|
67
80
|
shape[key] = field.required ? list.min(1, m.required) : list.nullish().transform((v) => v ?? []);
|
|
@@ -91,5 +104,14 @@ export function buildEntrySchema(entry, i18n, messages) {
|
|
|
91
104
|
if (entry.drafts) {
|
|
92
105
|
shape.status = z.enum(["draft", "published"]).nullish().transform((v) => v ?? void 0);
|
|
93
106
|
}
|
|
94
|
-
|
|
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
|
+
});
|
|
95
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)",
|