@xleddyl/nuxt-cms 0.4.1 → 0.4.3
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 +74 -96
- package/dist/module.json +1 -1
- package/dist/module.mjs +86 -22
- package/dist/runtime/app/components/cms/BlocksField.vue +30 -5
- package/dist/runtime/app/components/cms/DropdownMenu.vue +1 -5
- package/dist/runtime/app/components/cms/EntryDrawer.d.vue.ts +3 -0
- package/dist/runtime/app/components/cms/EntryDrawer.vue +32 -13
- package/dist/runtime/app/components/cms/EntryDrawer.vue.d.ts +3 -0
- package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +1 -0
- package/dist/runtime/app/components/cms/EntryForm.vue +20 -3
- package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +1 -0
- package/dist/runtime/app/components/cms/FieldInput.vue +2 -2
- package/dist/runtime/app/components/cms/Form.d.vue.ts +8 -6
- package/dist/runtime/app/components/cms/Form.vue +26 -5
- package/dist/runtime/app/components/cms/Form.vue.d.ts +8 -6
- package/dist/runtime/app/components/cms/FormField.vue +6 -1
- package/dist/runtime/app/components/cms/ImportModal.d.vue.ts +18 -0
- package/dist/runtime/app/components/cms/ImportModal.vue +252 -0
- package/dist/runtime/app/components/cms/ImportModal.vue.d.ts +18 -0
- package/dist/runtime/app/components/cms/ListFilters.d.vue.ts +18 -0
- package/dist/runtime/app/components/cms/ListFilters.vue +151 -0
- package/dist/runtime/app/components/cms/ListFilters.vue.d.ts +18 -0
- package/dist/runtime/app/components/cms/RelationField.vue +3 -1
- package/dist/runtime/app/components/cms/VisualEditor.vue +81 -12
- package/dist/runtime/app/pages/admin-collection.vue +158 -7
- package/dist/runtime/app/pages/admin-content.vue +18 -7
- package/dist/runtime/app/pages/admin-entry.vue +1 -0
- package/dist/runtime/app/pages/admin-preview.vue +6 -3
- package/dist/runtime/app/preview/PreviewOverlay.vue +4 -0
- package/dist/runtime/app/utils/ui.d.ts +2 -1
- package/dist/runtime/app/utils/ui.js +1 -0
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/server/api/collection-export.get.d.ts +2 -0
- package/dist/runtime/server/api/collection-export.get.js +67 -0
- package/dist/runtime/server/api/collection-import.post.d.ts +2 -0
- package/dist/runtime/server/api/collection-import.post.js +85 -0
- package/dist/runtime/server/api/collection.get.js +7 -15
- package/dist/runtime/server/utils/import.d.ts +31 -0
- package/dist/runtime/server/utils/import.js +202 -0
- package/dist/runtime/server/utils/list-query.d.ts +15 -0
- package/dist/runtime/server/utils/list-query.js +54 -0
- package/dist/runtime/server/utils/media-check.d.ts +2 -0
- package/dist/runtime/server/utils/media-check.js +1 -1
- package/dist/runtime/server/utils/media-references.js +4 -1
- package/dist/runtime/server/utils/registry.js +1 -1
- package/dist/runtime/server/utils/search.js +23 -4
- package/dist/runtime/shared/duplicate.d.ts +10 -0
- package/dist/runtime/shared/duplicate.js +54 -0
- package/dist/runtime/shared/graphql-sdl.js +4 -7
- package/dist/runtime/shared/index.d.ts +49 -2
- package/dist/runtime/shared/index.js +33 -0
- package/dist/runtime/shared/layout.d.ts +3 -0
- package/dist/runtime/shared/layout.js +35 -2
- package/dist/runtime/shared/list-filters.d.ts +20 -0
- package/dist/runtime/shared/list-filters.js +83 -0
- package/dist/runtime/shared/transfer.d.ts +70 -0
- package/dist/runtime/shared/transfer.js +256 -0
- package/dist/runtime/shared/validation.d.ts +15 -2
- package/dist/runtime/shared/validation.js +102 -9
- package/package.json +1 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { isCollectionKind, isMultiSelect, isTranslatableField, selectOptionLabel } from "./index.js";
|
|
2
|
+
export const TRANSFER_FORMATS = ["json", "csv"];
|
|
3
|
+
export const IMPORT_MODES = ["create", "upsert"];
|
|
4
|
+
export const IMPORT_MAX_BYTES = 5 * 1024 * 1024;
|
|
5
|
+
export const IMPORT_MAX_ROWS = 2e3;
|
|
6
|
+
export const IMPORT_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
7
|
+
export const TRANSFER_ID_KEY = "id";
|
|
8
|
+
export const TRANSFER_TIMESTAMP_KEYS = ["createdAt", "updatedAt"];
|
|
9
|
+
const STATUS_KEY = "status";
|
|
10
|
+
const CSV_BOM = "\uFEFF";
|
|
11
|
+
function hasStatus(entry) {
|
|
12
|
+
return isCollectionKind(entry) && !!entry.drafts && !Object.hasOwn(entry.fields, STATUS_KEY);
|
|
13
|
+
}
|
|
14
|
+
export function canTransferCsv(entry) {
|
|
15
|
+
return Object.values(entry.fields).every(
|
|
16
|
+
(field) => field.type !== "blocks" && !(field.type === "relation" && field.cardinality === "many-to-many")
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
export function transferKeys(entry) {
|
|
20
|
+
return [
|
|
21
|
+
TRANSFER_ID_KEY,
|
|
22
|
+
...Object.keys(entry.fields),
|
|
23
|
+
...hasStatus(entry) ? [STATUS_KEY] : [],
|
|
24
|
+
...TRANSFER_TIMESTAMP_KEYS
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
export function exportRow(entry, row) {
|
|
28
|
+
return Object.fromEntries(
|
|
29
|
+
transferKeys(entry).filter((key) => Object.hasOwn(row, key) || Object.hasOwn(entry.fields, key)).map((key) => [key, row[key] ?? null])
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
export function exportFileName(name, format, now = /* @__PURE__ */ new Date()) {
|
|
33
|
+
return `${name}-${now.toISOString().slice(0, 10)}.${format}`;
|
|
34
|
+
}
|
|
35
|
+
function isLocalized(field, i18n) {
|
|
36
|
+
return isTranslatableField(field) && i18n.locales.length > 0;
|
|
37
|
+
}
|
|
38
|
+
export function csvColumns(entry, i18n) {
|
|
39
|
+
return transferKeys(entry).flatMap((key) => {
|
|
40
|
+
const field = entry.fields[key];
|
|
41
|
+
if (field && isLocalized(field, i18n)) return i18n.locales.map((locale) => `${key}.${locale}`);
|
|
42
|
+
return [key];
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export function encodeCsvCell(value) {
|
|
46
|
+
return /[",\r\n]|^\s|\s$/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
|
|
47
|
+
}
|
|
48
|
+
export function encodeCsv(rows) {
|
|
49
|
+
return rows.map((row) => row.map(encodeCsvCell).join(",")).join("\r\n") + "\r\n";
|
|
50
|
+
}
|
|
51
|
+
export function parseCsv(text) {
|
|
52
|
+
const input = text.startsWith(CSV_BOM) ? text.slice(1) : text;
|
|
53
|
+
const rows = [];
|
|
54
|
+
let row = [];
|
|
55
|
+
let cell = "";
|
|
56
|
+
let quoted = false;
|
|
57
|
+
let index = 0;
|
|
58
|
+
while (index < input.length) {
|
|
59
|
+
const char = input[index];
|
|
60
|
+
if (quoted) {
|
|
61
|
+
if (char === '"' && input[index + 1] === '"') {
|
|
62
|
+
cell += '"';
|
|
63
|
+
index += 2;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (char === '"') quoted = false;
|
|
67
|
+
else cell += char;
|
|
68
|
+
index++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (char === '"' && cell === "") {
|
|
72
|
+
quoted = true;
|
|
73
|
+
} else if (char === ",") {
|
|
74
|
+
row.push(cell);
|
|
75
|
+
cell = "";
|
|
76
|
+
} else if (char === "\n" || char === "\r") {
|
|
77
|
+
row.push(cell);
|
|
78
|
+
rows.push(row);
|
|
79
|
+
row = [];
|
|
80
|
+
cell = "";
|
|
81
|
+
if (char === "\r" && input[index + 1] === "\n") index++;
|
|
82
|
+
} else {
|
|
83
|
+
cell += char;
|
|
84
|
+
}
|
|
85
|
+
index++;
|
|
86
|
+
}
|
|
87
|
+
if (quoted) throw new Error("A quoted cell is not closed");
|
|
88
|
+
if (cell !== "" || row.length) {
|
|
89
|
+
row.push(cell);
|
|
90
|
+
rows.push(row);
|
|
91
|
+
}
|
|
92
|
+
return rows.filter((cells) => cells.some((value) => value !== ""));
|
|
93
|
+
}
|
|
94
|
+
function csvCellText(field, value) {
|
|
95
|
+
if (value === null || value === void 0) return "";
|
|
96
|
+
if (field?.type === "json" || isMultiSelectField(field)) return JSON.stringify(value);
|
|
97
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
98
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
99
|
+
return String(value);
|
|
100
|
+
}
|
|
101
|
+
function isMultiSelectField(field) {
|
|
102
|
+
return !!field && isMultiSelect(field);
|
|
103
|
+
}
|
|
104
|
+
export function entryRowsToCsv(entry, rows, i18n) {
|
|
105
|
+
const keys = transferKeys(entry);
|
|
106
|
+
const lines = rows.map(
|
|
107
|
+
(row) => keys.flatMap((key) => {
|
|
108
|
+
const field = entry.fields[key];
|
|
109
|
+
if (field && isLocalized(field, i18n)) {
|
|
110
|
+
const value = row[key];
|
|
111
|
+
const values = value && typeof value === "object" ? value : typeof value === "string" && value ? { [i18n.defaultLocale]: value } : {};
|
|
112
|
+
return i18n.locales.map((locale) => csvCellText(void 0, values[locale]));
|
|
113
|
+
}
|
|
114
|
+
return [csvCellText(field, row[key])];
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
return CSV_BOM + encodeCsv([csvColumns(entry, i18n), ...lines]);
|
|
118
|
+
}
|
|
119
|
+
const TRUE_VALUES = ["true", "1", "yes"];
|
|
120
|
+
const FALSE_VALUES = ["false", "0", "no"];
|
|
121
|
+
export function csvCellValue(field, text) {
|
|
122
|
+
if (text === "") return { value: null };
|
|
123
|
+
if (field.type === "number") {
|
|
124
|
+
const number = Number(text.trim());
|
|
125
|
+
return text.trim() && Number.isFinite(number) ? { value: number } : { error: "Not a number" };
|
|
126
|
+
}
|
|
127
|
+
if (field.type === "boolean") {
|
|
128
|
+
const lower = text.trim().toLowerCase();
|
|
129
|
+
if (TRUE_VALUES.includes(lower)) return { value: true };
|
|
130
|
+
if (FALSE_VALUES.includes(lower)) return { value: false };
|
|
131
|
+
return { error: "Not a boolean (true or false)" };
|
|
132
|
+
}
|
|
133
|
+
if (field.type === "json" || isMultiSelect(field)) {
|
|
134
|
+
try {
|
|
135
|
+
return { value: JSON.parse(text) };
|
|
136
|
+
} catch {
|
|
137
|
+
return { error: "Invalid JSON" };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { value: text };
|
|
141
|
+
}
|
|
142
|
+
function rowTooMany(count) {
|
|
143
|
+
return `The file has ${count} rows; the limit is ${IMPORT_MAX_ROWS}`;
|
|
144
|
+
}
|
|
145
|
+
export function csvImportSource(entry, text, i18n) {
|
|
146
|
+
let table;
|
|
147
|
+
try {
|
|
148
|
+
table = parseCsv(text);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return { rows: [], errors: [error.message] };
|
|
151
|
+
}
|
|
152
|
+
const [header, ...lines] = table;
|
|
153
|
+
if (!header) return { rows: [], errors: ["The file has no header row"] };
|
|
154
|
+
const names = header.map((name) => name.trim());
|
|
155
|
+
const known = new Set(csvColumns(entry, i18n));
|
|
156
|
+
const errors = [];
|
|
157
|
+
const seen = /* @__PURE__ */ new Set();
|
|
158
|
+
for (const name of names) {
|
|
159
|
+
if (!known.has(name)) errors.push(`Unknown column '${name}'`);
|
|
160
|
+
else if (seen.has(name)) errors.push(`Column '${name}' appears twice`);
|
|
161
|
+
seen.add(name);
|
|
162
|
+
}
|
|
163
|
+
if (lines.length > IMPORT_MAX_ROWS) errors.push(rowTooMany(lines.length));
|
|
164
|
+
if (errors.length) return { rows: [], errors };
|
|
165
|
+
const rows = lines.map((cells, position) => {
|
|
166
|
+
const index = position + 1;
|
|
167
|
+
const issues = [];
|
|
168
|
+
if (cells.length !== names.length) {
|
|
169
|
+
issues.push({
|
|
170
|
+
path: "",
|
|
171
|
+
message: `Expected ${names.length} cells, found ${cells.length}`
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const cellOf = (name) => {
|
|
175
|
+
const at = names.indexOf(name);
|
|
176
|
+
return at === -1 ? void 0 : cells[at] ?? "";
|
|
177
|
+
};
|
|
178
|
+
const values = {};
|
|
179
|
+
for (const key of transferKeys(entry)) {
|
|
180
|
+
const field = entry.fields[key];
|
|
181
|
+
if (field && isLocalized(field, i18n)) {
|
|
182
|
+
const localized = {};
|
|
183
|
+
let present = false;
|
|
184
|
+
for (const locale of i18n.locales) {
|
|
185
|
+
const text3 = cellOf(`${key}.${locale}`);
|
|
186
|
+
if (text3 === void 0) continue;
|
|
187
|
+
present = true;
|
|
188
|
+
if (text3 !== "") localized[locale] = text3;
|
|
189
|
+
}
|
|
190
|
+
if (present) values[key] = Object.keys(localized).length ? localized : null;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const text2 = cellOf(key);
|
|
194
|
+
if (text2 === void 0) continue;
|
|
195
|
+
if (!field) {
|
|
196
|
+
values[key] = text2 === "" ? null : text2;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const result = csvCellValue(field, text2);
|
|
200
|
+
if ("error" in result) issues.push({ path: key, message: result.error });
|
|
201
|
+
else values[key] = result.value;
|
|
202
|
+
}
|
|
203
|
+
return { index, values, errors: issues };
|
|
204
|
+
});
|
|
205
|
+
return { rows, errors: [] };
|
|
206
|
+
}
|
|
207
|
+
export function jsonImportSource(entry, text) {
|
|
208
|
+
let data;
|
|
209
|
+
try {
|
|
210
|
+
data = JSON.parse(text.startsWith(CSV_BOM) ? text.slice(1) : text);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
return { rows: [], errors: [`Invalid JSON: ${error.message}`] };
|
|
213
|
+
}
|
|
214
|
+
const items = Array.isArray(data) ? data : data && typeof data === "object" && Array.isArray(data.items) ? data.items : null;
|
|
215
|
+
if (!items)
|
|
216
|
+
return {
|
|
217
|
+
rows: [],
|
|
218
|
+
errors: ["Expected an array of entries or an object with an items array"]
|
|
219
|
+
};
|
|
220
|
+
if (items.length > IMPORT_MAX_ROWS) return { rows: [], errors: [rowTooMany(items.length)] };
|
|
221
|
+
const allowed = new Set(transferKeys(entry));
|
|
222
|
+
const rows = items.map((item, position) => {
|
|
223
|
+
const index = position + 1;
|
|
224
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
225
|
+
return { index, values: {}, errors: [{ path: "", message: "Not an object" }] };
|
|
226
|
+
}
|
|
227
|
+
const values = item;
|
|
228
|
+
const errors = Object.keys(values).filter((key) => !allowed.has(key)).map((key) => ({ path: key, message: `Unknown field '${key}'` }));
|
|
229
|
+
return { index, values: { ...values }, errors };
|
|
230
|
+
});
|
|
231
|
+
return { rows, errors: [] };
|
|
232
|
+
}
|
|
233
|
+
export function importSource(entry, format, text, i18n) {
|
|
234
|
+
if (format === "csv") {
|
|
235
|
+
if (!canTransferCsv(entry))
|
|
236
|
+
return { rows: [], errors: ["CSV needs an entry without blocks and many-to-many fields"] };
|
|
237
|
+
return csvImportSource(entry, text, i18n);
|
|
238
|
+
}
|
|
239
|
+
return jsonImportSource(entry, text);
|
|
240
|
+
}
|
|
241
|
+
export function importRowTitle(entry, values, defaultLocale) {
|
|
242
|
+
const key = entry.titleField;
|
|
243
|
+
const field = key ? entry.fields[key] : void 0;
|
|
244
|
+
const value = key ? values[key] : void 0;
|
|
245
|
+
if (!field || value === null || value === void 0 || value === "") return null;
|
|
246
|
+
if (typeof value === "object") {
|
|
247
|
+
const record = value;
|
|
248
|
+
const text = record[defaultLocale] ?? Object.values(record).find(Boolean);
|
|
249
|
+
return typeof text === "string" && text ? text : null;
|
|
250
|
+
}
|
|
251
|
+
if (field.type === "select") return selectOptionLabel(field, value);
|
|
252
|
+
return String(value);
|
|
253
|
+
}
|
|
254
|
+
export function importIssuePath(path) {
|
|
255
|
+
return path.map((part) => String(part)).join(".");
|
|
256
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type { CmsEntry, CmsI18n } from './index.js';
|
|
2
|
+
import type { CmsEntry, CmsI18n, CmsValidationLevel, FieldConfig } from './index.js';
|
|
3
3
|
export declare const objectKeySchema: z.ZodString;
|
|
4
4
|
export interface ValidationMessages {
|
|
5
5
|
required: string;
|
|
@@ -9,10 +9,23 @@ export interface ValidationMessages {
|
|
|
9
9
|
invalidSlug: string;
|
|
10
10
|
unknownLocale: string;
|
|
11
11
|
requiredLocale: (locale: string) => string;
|
|
12
|
+
minBlocks: (min: number) => string;
|
|
13
|
+
maxBlocks: (max: number) => string;
|
|
12
14
|
}
|
|
13
15
|
export declare function datetimeSchema(message: string): z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
14
|
-
export declare function
|
|
16
|
+
export declare function blocksCountMessage(field: Pick<FieldConfig, 'min' | 'max'>, count: number, messages?: Partial<ValidationMessages>): string | null;
|
|
17
|
+
export interface EntryValidationIssue {
|
|
18
|
+
path: (string | number)[];
|
|
19
|
+
message: string;
|
|
20
|
+
level: CmsValidationLevel;
|
|
21
|
+
}
|
|
22
|
+
type ValidatedEntry = Pick<CmsEntry, 'fields'> & Partial<Pick<CmsEntry, 'validate'>>;
|
|
23
|
+
export declare function entryValidationIssues(entry: ValidatedEntry, values: Record<string, unknown>, i18n: CmsI18n, path?: string): EntryValidationIssue[];
|
|
24
|
+
export declare function entryValidationWarnings(entry: ValidatedEntry, values: Record<string, unknown>, i18n: CmsI18n, path?: string): EntryValidationIssue[];
|
|
25
|
+
export declare function buildEntrySchema(entry: ValidatedEntry & {
|
|
15
26
|
drafts?: boolean;
|
|
27
|
+
path?: string;
|
|
16
28
|
}, i18n: CmsI18n, messages?: Partial<ValidationMessages>): z.ZodObject<{
|
|
17
29
|
[x: string]: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
18
30
|
}, z.core.$strip>;
|
|
31
|
+
export {};
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BLOCK_HIDDEN_KEY,
|
|
4
|
+
declaresHiddenField,
|
|
5
|
+
isFieldVisible,
|
|
6
|
+
isTranslatableField,
|
|
7
|
+
selectOptionValues
|
|
8
|
+
} from "./index.js";
|
|
3
9
|
export const objectKeySchema = z.string().min(1).max(1024).refine((k) => !k.includes("..") && !k.startsWith("/"), "Invalid object key");
|
|
4
10
|
const DEFAULT_MESSAGES = {
|
|
5
11
|
required: "Required field",
|
|
@@ -8,7 +14,9 @@ const DEFAULT_MESSAGES = {
|
|
|
8
14
|
invalidEmail: "Invalid email",
|
|
9
15
|
invalidSlug: "Invalid slug (lowercase letters, numbers, dashes)",
|
|
10
16
|
unknownLocale: "Unknown locale key",
|
|
11
|
-
requiredLocale: (locale) => `Required field (locale '${locale}')
|
|
17
|
+
requiredLocale: (locale) => `Required field (locale '${locale}')`,
|
|
18
|
+
minBlocks: (min) => `Add at least ${min} ${min === 1 ? "block" : "blocks"}`,
|
|
19
|
+
maxBlocks: (max) => `Add at most ${max} ${max === 1 ? "block" : "blocks"}`
|
|
12
20
|
};
|
|
13
21
|
const DATETIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
14
22
|
export function datetimeSchema(message) {
|
|
@@ -34,9 +42,11 @@ function scalarSchema(field, m) {
|
|
|
34
42
|
case "slug":
|
|
35
43
|
return z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, m.invalidSlug);
|
|
36
44
|
case "select":
|
|
37
|
-
return z.enum(field
|
|
45
|
+
return z.enum(selectOptionValues(field));
|
|
38
46
|
case "media":
|
|
39
47
|
return objectKeySchema;
|
|
48
|
+
case "json":
|
|
49
|
+
return z.unknown();
|
|
40
50
|
default:
|
|
41
51
|
return field.required ? z.string().trim().min(1, m.required) : z.string();
|
|
42
52
|
}
|
|
@@ -72,6 +82,72 @@ function blocksSchema(field, i18n, m) {
|
|
|
72
82
|
);
|
|
73
83
|
return z.array(z.discriminatedUnion("type", variants));
|
|
74
84
|
}
|
|
85
|
+
export function blocksCountMessage(field, count, messages) {
|
|
86
|
+
const m = { ...DEFAULT_MESSAGES, ...messages };
|
|
87
|
+
if (field.max !== void 0 && count > field.max) return m.maxBlocks(field.max);
|
|
88
|
+
if (field.min !== void 0 && count > 0 && count < field.min) return m.minBlocks(field.min);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
function boundedBlocks(list, field, m) {
|
|
92
|
+
if (field.min === void 0 && field.max === void 0) return list;
|
|
93
|
+
return list.superRefine((items, ctx) => {
|
|
94
|
+
const message = blocksCountMessage(field, items.length, m);
|
|
95
|
+
if (message) ctx.addIssue({ code: "custom", message });
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function hookIssues(result) {
|
|
99
|
+
if (!Array.isArray(result)) return [];
|
|
100
|
+
return result.filter(
|
|
101
|
+
(issue) => !!issue && typeof issue === "object" && typeof issue.message === "string"
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
function hasValidateHooks(entry) {
|
|
105
|
+
if (entry.validate) return true;
|
|
106
|
+
return Object.values(entry.fields).some(
|
|
107
|
+
(field) => field.type === "blocks" && Object.values(field.blocks ?? {}).some((block) => block.validate)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
export function entryValidationIssues(entry, values, i18n, path) {
|
|
111
|
+
const context = {
|
|
112
|
+
locales: i18n.locales,
|
|
113
|
+
defaultLocale: i18n.defaultLocale,
|
|
114
|
+
...path ? { path } : {}
|
|
115
|
+
};
|
|
116
|
+
const issues = [];
|
|
117
|
+
if (entry.validate) {
|
|
118
|
+
for (const issue of hookIssues(entry.validate(values, context))) {
|
|
119
|
+
issues.push({
|
|
120
|
+
path: issue.field ? [issue.field] : [],
|
|
121
|
+
message: issue.message,
|
|
122
|
+
level: issue.level === "warning" ? "warning" : "error"
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const [key, field] of Object.entries(entry.fields)) {
|
|
127
|
+
const list = values[key];
|
|
128
|
+
if (field.type !== "blocks" || !Array.isArray(list)) continue;
|
|
129
|
+
list.forEach((item, index) => {
|
|
130
|
+
if (!item || typeof item !== "object") return;
|
|
131
|
+
const block = field.blocks?.[String(item.type)];
|
|
132
|
+
if (!block?.validate) return;
|
|
133
|
+
const result = block.validate(item, { ...context, index });
|
|
134
|
+
for (const issue of hookIssues(result)) {
|
|
135
|
+
issues.push({
|
|
136
|
+
path: issue.field ? [key, index, issue.field] : [key, index],
|
|
137
|
+
message: issue.message,
|
|
138
|
+
level: issue.level === "warning" ? "warning" : "error"
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return issues;
|
|
144
|
+
}
|
|
145
|
+
export function entryValidationWarnings(entry, values, i18n, path) {
|
|
146
|
+
if (!hasValidateHooks(entry)) return [];
|
|
147
|
+
return entryValidationIssues(entry, values, i18n, path).filter(
|
|
148
|
+
(issue) => issue.level === "warning"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
75
151
|
function isEmptyValue(field, value, defaultLocale) {
|
|
76
152
|
if (value == null) return true;
|
|
77
153
|
if (Array.isArray(value)) return !value.length;
|
|
@@ -86,22 +162,27 @@ export function buildEntrySchema(entry, i18n, messages) {
|
|
|
86
162
|
const m = { ...DEFAULT_MESSAGES, ...messages };
|
|
87
163
|
const shape = {};
|
|
88
164
|
const conditional = [];
|
|
165
|
+
const conditionalBounds = [];
|
|
89
166
|
for (const [key, declared] of Object.entries(entry.fields)) {
|
|
90
167
|
const field = declared.showIf ? { ...declared, required: false } : declared;
|
|
91
168
|
if (declared.showIf && declared.required) conditional.push([key, declared]);
|
|
169
|
+
if (declared.showIf && declared.type === "blocks" && (declared.min !== void 0 || declared.max !== void 0))
|
|
170
|
+
conditionalBounds.push([key, declared]);
|
|
92
171
|
if (field.type === "relation" && field.cardinality === "many-to-many") {
|
|
93
172
|
const list = z.array(z.string().min(1));
|
|
94
173
|
shape[key] = field.required ? list.min(1, m.required) : list.nullish().transform((v) => v ?? []);
|
|
95
174
|
continue;
|
|
96
175
|
}
|
|
97
176
|
if (field.type === "select" && field.multiple) {
|
|
98
|
-
const list = z.array(z.enum(field
|
|
177
|
+
const list = z.array(z.enum(selectOptionValues(field)));
|
|
99
178
|
shape[key] = field.required ? list.min(1, m.required) : list.nullish().transform((v) => v ?? []);
|
|
100
179
|
continue;
|
|
101
180
|
}
|
|
102
181
|
if (field.type === "blocks") {
|
|
103
182
|
const list = blocksSchema(field, i18n, m);
|
|
104
|
-
|
|
183
|
+
const base = field.required ? list.min(1, m.required) : list;
|
|
184
|
+
const bounded = declared.showIf ? base : boundedBlocks(base, field, m);
|
|
185
|
+
shape[key] = field.required ? bounded : bounded.nullish().transform((v) => v ?? null);
|
|
105
186
|
continue;
|
|
106
187
|
}
|
|
107
188
|
if (isTranslatableField(field) && i18n.locales.length) {
|
|
@@ -119,13 +200,25 @@ export function buildEntrySchema(entry, i18n, messages) {
|
|
|
119
200
|
shape.status = z.enum(["draft", "published"]).nullish().transform((v) => v ?? void 0);
|
|
120
201
|
}
|
|
121
202
|
const schema = z.object(shape);
|
|
122
|
-
|
|
123
|
-
|
|
203
|
+
const hooks = hasValidateHooks(entry);
|
|
204
|
+
if (!conditional.length && !conditionalBounds.length && !hooks) return schema;
|
|
205
|
+
return schema.superRefine((parsed, ctx) => {
|
|
206
|
+
const values = parsed;
|
|
124
207
|
for (const [key, field] of conditional) {
|
|
125
208
|
if (!isFieldVisible(field, values)) continue;
|
|
126
|
-
if (!isEmptyValue(field, values[key], i18n.defaultLocale))
|
|
127
|
-
continue;
|
|
209
|
+
if (!isEmptyValue(field, values[key], i18n.defaultLocale)) continue;
|
|
128
210
|
ctx.addIssue({ code: "custom", path: [key], message: m.required });
|
|
129
211
|
}
|
|
212
|
+
for (const [key, field] of conditionalBounds) {
|
|
213
|
+
const list = values[key];
|
|
214
|
+
if (!isFieldVisible(field, values) || !Array.isArray(list)) continue;
|
|
215
|
+
const message = blocksCountMessage(field, list.length, m);
|
|
216
|
+
if (message) ctx.addIssue({ code: "custom", path: [key], message });
|
|
217
|
+
}
|
|
218
|
+
if (!hooks) return;
|
|
219
|
+
for (const issue of entryValidationIssues(entry, values, i18n, entry.path)) {
|
|
220
|
+
if (issue.level === "error")
|
|
221
|
+
ctx.addIssue({ code: "custom", path: issue.path, message: issue.message });
|
|
222
|
+
}
|
|
130
223
|
});
|
|
131
224
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xleddyl/nuxt-cms",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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)",
|