@xleddyl/nuxt-cms 0.2.2 → 0.2.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 +3 -1
- package/dist/module.d.mts +4 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +319 -32
- package/dist/runtime/app/blocks/CmsBlocks.d.vue.ts +45 -0
- package/dist/runtime/app/blocks/CmsBlocks.vue +36 -0
- package/dist/runtime/app/blocks/CmsBlocks.vue.d.ts +45 -0
- package/dist/runtime/app/blocks/resolve.d.ts +21 -0
- package/dist/runtime/app/blocks/resolve.js +37 -0
- package/dist/runtime/app/components/cms/BlockOutline.d.vue.ts +17 -0
- package/dist/runtime/app/components/cms/BlockOutline.vue +99 -0
- package/dist/runtime/app/components/cms/BlockOutline.vue.d.ts +17 -0
- package/dist/runtime/app/components/cms/BlockPalette.d.vue.ts +12 -0
- package/dist/runtime/app/components/cms/BlockPalette.vue +29 -0
- package/dist/runtime/app/components/cms/BlockPalette.vue.d.ts +12 -0
- package/dist/runtime/app/components/cms/BlocksField.d.vue.ts +2 -0
- package/dist/runtime/app/components/cms/BlocksField.vue +83 -3
- package/dist/runtime/app/components/cms/BlocksField.vue.d.ts +2 -0
- package/dist/runtime/app/components/cms/EntryForm.vue +9 -4
- package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +2 -0
- package/dist/runtime/app/components/cms/FieldInput.vue +18 -1
- package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +2 -0
- package/dist/runtime/app/components/cms/SettingsEntry.vue +3 -2
- package/dist/runtime/app/components/cms/SettingsModal.vue +2 -1
- package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +2 -0
- package/dist/runtime/app/components/cms/StatusBadge.vue +9 -3
- package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +2 -0
- package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +37 -0
- package/dist/runtime/app/components/cms/VisualEditor.vue +533 -0
- package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +37 -0
- package/dist/runtime/app/composables/cms-entry-disabled.d.ts +7 -1
- package/dist/runtime/app/composables/cms-entry-disabled.js +15 -0
- package/dist/runtime/app/composables/cms-entry.d.ts +7 -1
- package/dist/runtime/app/composables/cms-entry.js +51 -1
- package/dist/runtime/app/composables/cms-runtime.d.ts +1 -0
- package/dist/runtime/app/composables/cms-settings.js +8 -2
- package/dist/runtime/app/layouts/cms-admin.vue +5 -0
- package/dist/runtime/app/layouts/cms-editor.d.vue.ts +13 -0
- package/dist/runtime/app/layouts/cms-editor.vue +14 -0
- package/dist/runtime/app/layouts/cms-editor.vue.d.ts +13 -0
- package/dist/runtime/app/pages/admin-collection.vue +33 -4
- package/dist/runtime/app/pages/admin-content.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-content.vue +284 -0
- package/dist/runtime/app/pages/admin-content.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin-preview.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-preview.vue +132 -0
- package/dist/runtime/app/pages/admin-preview.vue.d.ts +3 -0
- package/dist/runtime/app/preview/PreviewOverlay.d.vue.ts +15 -0
- package/dist/runtime/app/preview/PreviewOverlay.vue +362 -0
- package/dist/runtime/app/preview/PreviewOverlay.vue.d.ts +15 -0
- package/dist/runtime/app/utils/ui.js +1 -0
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/server/api/collection.get.js +3 -2
- package/dist/runtime/server/api/collection.post.js +4 -1
- package/dist/runtime/server/api/item.delete.js +2 -1
- package/dist/runtime/server/api/item.put.js +2 -1
- package/dist/runtime/server/utils/graphql.js +23 -8
- package/dist/runtime/server/utils/registry.d.ts +1 -0
- package/dist/runtime/server/utils/registry.js +6 -0
- package/dist/runtime/server/utils/visibility.d.ts +4 -0
- package/dist/runtime/server/utils/visibility.js +12 -0
- package/dist/runtime/shared/graphql-sdl.js +4 -1
- package/dist/runtime/shared/index.d.ts +59 -5
- package/dist/runtime/shared/index.js +133 -0
- package/dist/runtime/shared/preview.d.ts +85 -0
- package/dist/runtime/shared/preview.js +219 -0
- package/dist/runtime/shared/validation.d.ts +2 -0
- package/dist/runtime/shared/validation.js +15 -2
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import type { PgTable } from 'drizzle-orm/pg-core';
|
|
|
2
2
|
import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
|
|
3
3
|
import type { CmsLayoutBlock, CmsListInput } from './layout.js';
|
|
4
4
|
export type CmsTable = SQLiteTable | PgTable;
|
|
5
|
-
export type FieldType = 'text' | 'richtext' | 'number' | 'boolean' | 'date' | 'email' | 'slug' | 'select' | 'json' | 'media' | 'relation' | 'blocks';
|
|
5
|
+
export type FieldType = 'text' | 'richtext' | 'number' | 'boolean' | 'date' | 'datetime' | 'email' | 'slug' | 'select' | 'json' | 'media' | 'relation' | 'blocks';
|
|
6
6
|
export interface CmsI18n {
|
|
7
7
|
locales: string[];
|
|
8
8
|
defaultLocale: string;
|
|
@@ -76,6 +76,9 @@ export interface MediaUsageGroup {
|
|
|
76
76
|
export declare function mediaUsageGroups(usage: Record<string, MediaUsage[]>, keys: string[]): MediaUsageGroup[];
|
|
77
77
|
export interface BlockConfig {
|
|
78
78
|
label: string;
|
|
79
|
+
component?: string;
|
|
80
|
+
icon?: string;
|
|
81
|
+
description?: string;
|
|
79
82
|
fields: Record<string, FieldConfig>;
|
|
80
83
|
}
|
|
81
84
|
export type ConditionValue = string | number | boolean;
|
|
@@ -129,10 +132,14 @@ export declare function isMultiSelect(field: FieldConfig): boolean;
|
|
|
129
132
|
export declare function translatableFieldKeys(entry: EntryLike): string[];
|
|
130
133
|
export declare function translatableBlockFieldKeys(block: BlockConfig): string[];
|
|
131
134
|
export declare function hasTranslatableBlockFields(field: FieldConfig): boolean;
|
|
135
|
+
export declare const BLOCK_HIDDEN_KEY = "hidden";
|
|
136
|
+
export declare function declaresHiddenField(block: BlockConfig | undefined): boolean;
|
|
137
|
+
export declare function isHiddenBlock(item: unknown): boolean;
|
|
138
|
+
export declare function publicBlocks(field: FieldConfig, value: unknown): unknown;
|
|
132
139
|
export declare function localizeBlock(field: FieldConfig, item: unknown, locale: string, defaultLocale: string): unknown;
|
|
133
140
|
export declare function localizeBlocks(field: FieldConfig, value: unknown, locale: string, defaultLocale: string): unknown;
|
|
134
141
|
export declare const CMS_GRAPHQL_BATCH_LIMIT = 20;
|
|
135
|
-
export type CmsEntryKind = 'collection' | 'single' | 'page';
|
|
142
|
+
export type CmsEntryKind = 'collection' | 'single' | 'page' | 'content';
|
|
136
143
|
export interface CmsPageRoute {
|
|
137
144
|
path: string;
|
|
138
145
|
key: string;
|
|
@@ -158,10 +165,35 @@ export interface CmsEntry {
|
|
|
158
165
|
icon?: string;
|
|
159
166
|
layout?: CmsLayoutBlock[];
|
|
160
167
|
list?: CmsListInput;
|
|
168
|
+
blocks?: Record<string, BlockConfig>;
|
|
169
|
+
preview?: CmsPreviewOptions;
|
|
170
|
+
declared?: CmsContentDeclared;
|
|
171
|
+
}
|
|
172
|
+
export interface CmsPreviewOptions {
|
|
173
|
+
component?: string;
|
|
174
|
+
}
|
|
175
|
+
export interface CmsContentDeclared {
|
|
176
|
+
fields: string[];
|
|
177
|
+
titleField?: unknown;
|
|
178
|
+
drafts?: unknown;
|
|
161
179
|
}
|
|
162
180
|
export declare const MOBILE_MEDIA_SUFFIX = "Mobile";
|
|
163
181
|
export declare function mobileMediaKey(key: string): string;
|
|
164
182
|
export declare function expandMobileMedia<T extends Record<string, unknown>>(config: T): T;
|
|
183
|
+
export declare const CONTENT_TITLE_FIELD = "title";
|
|
184
|
+
export declare const CONTENT_SLUG_FIELD = "slug";
|
|
185
|
+
export declare const CONTENT_PUBLISHED_AT_FIELD = "publishedAt";
|
|
186
|
+
export declare const CONTENT_BODY_FIELD = "body";
|
|
187
|
+
export declare const CONTENT_SYSTEM_FIELDS: readonly ["title", "slug", "publishedAt", "excerpt", "cover", "body", "seoTitle", "seoDescription", "seoImage"];
|
|
188
|
+
export type CmsContentSystemField = (typeof CONTENT_SYSTEM_FIELDS)[number];
|
|
189
|
+
export declare const CONTENT_LIST_COLUMNS: string[];
|
|
190
|
+
export declare function isContentEntry(entry: Pick<CmsEntry, 'kind'>): boolean;
|
|
191
|
+
export declare function isCollectionKind(entry: Pick<CmsEntry, 'kind'>): boolean;
|
|
192
|
+
export declare function normalizeContentEntries<T extends Record<string, unknown>>(config: T, i18n?: Pick<CmsI18n, 'locales'>): T;
|
|
193
|
+
export declare function normalizeCmsConfig<T extends Record<string, unknown>>(config: T, i18n?: Pick<CmsI18n, 'locales'>): T;
|
|
194
|
+
export declare function slugSourceValue(source: FieldConfig | undefined, value: unknown, defaultLocale: string): string | null;
|
|
195
|
+
export declare function isScheduledContent(row: Record<string, unknown> | null | undefined, now?: Date): boolean;
|
|
196
|
+
export declare function resolveBlocksMedia(field: FieldConfig, value: unknown, resolve: (key: string) => unknown): unknown;
|
|
165
197
|
export declare function entryTabs(entry: Pick<CmsEntry, 'tabs'>): CmsTab[];
|
|
166
198
|
export declare function fieldTab(field: FieldConfig, tabs: CmsTab[]): string | undefined;
|
|
167
199
|
export declare function fieldsByTab(fields: Record<string, FieldConfig>, tabs: CmsTab[]): Record<string, Record<string, FieldConfig>>;
|
|
@@ -214,6 +246,9 @@ export interface BooleanFieldInput extends FieldInputBase {
|
|
|
214
246
|
export interface DateFieldInput extends FieldInputBase {
|
|
215
247
|
type: 'date';
|
|
216
248
|
}
|
|
249
|
+
export interface DateTimeFieldInput extends FieldInputBase {
|
|
250
|
+
type: 'datetime';
|
|
251
|
+
}
|
|
217
252
|
export interface EmailFieldInput extends FieldInputBase {
|
|
218
253
|
type: 'email';
|
|
219
254
|
}
|
|
@@ -243,16 +278,19 @@ export interface RelationFieldInput extends FieldInputBase {
|
|
|
243
278
|
onDelete?: 'set null' | 'cascade' | 'restrict';
|
|
244
279
|
}
|
|
245
280
|
type BlockField<T> = Omit<T, 'private' | 'showIf' | 'tab'>;
|
|
246
|
-
export type BlockFieldInput = BlockField<TextFieldInput> | BlockField<RichtextFieldInput> | BlockField<NumberFieldInput> | BlockField<BooleanFieldInput> | BlockField<DateFieldInput> | BlockField<EmailFieldInput> | BlockField<SelectFieldInput> | BlockField<JsonFieldInput> | Omit<BlockField<MediaFieldInput>, 'mobile'>;
|
|
281
|
+
export type BlockFieldInput = BlockField<TextFieldInput> | BlockField<RichtextFieldInput> | BlockField<NumberFieldInput> | BlockField<BooleanFieldInput> | BlockField<DateFieldInput> | BlockField<DateTimeFieldInput> | BlockField<EmailFieldInput> | BlockField<SelectFieldInput> | BlockField<JsonFieldInput> | Omit<BlockField<MediaFieldInput>, 'mobile'>;
|
|
247
282
|
export interface BlockInput {
|
|
248
283
|
label: string;
|
|
284
|
+
component?: string;
|
|
285
|
+
icon?: string;
|
|
286
|
+
description?: string;
|
|
249
287
|
fields: Record<string, BlockFieldInput>;
|
|
250
288
|
}
|
|
251
289
|
export interface BlocksFieldInput extends FieldInputBase {
|
|
252
290
|
type: 'blocks';
|
|
253
291
|
blocks: Record<string, BlockInput>;
|
|
254
292
|
}
|
|
255
|
-
export type CmsFieldInput = TextFieldInput | RichtextFieldInput | NumberFieldInput | BooleanFieldInput | DateFieldInput | EmailFieldInput | SlugFieldInput | SelectFieldInput | JsonFieldInput | MediaFieldInput | RelationFieldInput | BlocksFieldInput;
|
|
293
|
+
export type CmsFieldInput = TextFieldInput | RichtextFieldInput | NumberFieldInput | BooleanFieldInput | DateFieldInput | DateTimeFieldInput | EmailFieldInput | SlugFieldInput | SelectFieldInput | JsonFieldInput | MediaFieldInput | RelationFieldInput | BlocksFieldInput;
|
|
256
294
|
interface CmsEntryInputBase {
|
|
257
295
|
id: string;
|
|
258
296
|
label: string;
|
|
@@ -283,8 +321,24 @@ export interface CmsPageInput extends CmsEntryInputBase {
|
|
|
283
321
|
overrides?: Record<string, Record<string, CmsFieldInput | null>>;
|
|
284
322
|
columns?: string[];
|
|
285
323
|
}
|
|
286
|
-
export
|
|
324
|
+
export interface CmsContentInput {
|
|
325
|
+
id: string;
|
|
326
|
+
label: string;
|
|
327
|
+
kind: 'content';
|
|
328
|
+
icon?: string;
|
|
329
|
+
blocks: Record<string, BlockInput>;
|
|
330
|
+
fields?: Record<string, CmsFieldInput>;
|
|
331
|
+
labels?: Partial<Record<CmsContentSystemField, string>>;
|
|
332
|
+
tabs?: CmsTab[];
|
|
333
|
+
layout?: CmsLayoutBlock[];
|
|
334
|
+
list?: CmsListInput;
|
|
335
|
+
preview?: CmsPreviewOptions;
|
|
336
|
+
titleField?: never;
|
|
337
|
+
drafts?: never;
|
|
338
|
+
}
|
|
339
|
+
export type CmsEntryInput = CmsCollectionInput | CmsSingleInput | CmsPageInput | CmsContentInput;
|
|
287
340
|
export type CmsConfigInput = Record<string, CmsEntryInput>;
|
|
288
341
|
export declare function defineCmsConfig<T extends CmsConfigInput>(config: T): T;
|
|
342
|
+
export declare function defineCmsBlocks<T extends Record<string, BlockInput>>(blocks: T): T;
|
|
289
343
|
export * from './layout.js';
|
|
290
344
|
export * from './users.js';
|
|
@@ -219,6 +219,20 @@ export function hasTranslatableBlockFields(field) {
|
|
|
219
219
|
(block) => Object.values(block.fields).some(isTranslatableField)
|
|
220
220
|
);
|
|
221
221
|
}
|
|
222
|
+
export const BLOCK_HIDDEN_KEY = "hidden";
|
|
223
|
+
export function declaresHiddenField(block) {
|
|
224
|
+
return !!block && Object.hasOwn(block.fields, BLOCK_HIDDEN_KEY);
|
|
225
|
+
}
|
|
226
|
+
export function isHiddenBlock(item) {
|
|
227
|
+
return !!item && typeof item === "object" && item.hidden === true;
|
|
228
|
+
}
|
|
229
|
+
export function publicBlocks(field, value) {
|
|
230
|
+
if (!Array.isArray(value)) return value;
|
|
231
|
+
return value.filter((item) => {
|
|
232
|
+
if (!isHiddenBlock(item)) return true;
|
|
233
|
+
return declaresHiddenField(field.blocks?.[String(item.type)]);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
222
236
|
export function localizeBlock(field, item, locale, defaultLocale) {
|
|
223
237
|
if (!item || typeof item !== "object") return item;
|
|
224
238
|
const block = field.blocks?.[String(item.type)];
|
|
@@ -289,6 +303,122 @@ export function expandMobileMedia(config) {
|
|
|
289
303
|
}
|
|
290
304
|
return config;
|
|
291
305
|
}
|
|
306
|
+
export const CONTENT_TITLE_FIELD = "title";
|
|
307
|
+
export const CONTENT_SLUG_FIELD = "slug";
|
|
308
|
+
export const CONTENT_PUBLISHED_AT_FIELD = "publishedAt";
|
|
309
|
+
export const CONTENT_BODY_FIELD = "body";
|
|
310
|
+
export const CONTENT_SYSTEM_FIELDS = [
|
|
311
|
+
"title",
|
|
312
|
+
"slug",
|
|
313
|
+
"publishedAt",
|
|
314
|
+
"excerpt",
|
|
315
|
+
"cover",
|
|
316
|
+
"body",
|
|
317
|
+
"seoTitle",
|
|
318
|
+
"seoDescription",
|
|
319
|
+
"seoImage"
|
|
320
|
+
];
|
|
321
|
+
export const CONTENT_LIST_COLUMNS = ["title", "status", "publishedAt"];
|
|
322
|
+
export function isContentEntry(entry) {
|
|
323
|
+
return entry.kind === "content";
|
|
324
|
+
}
|
|
325
|
+
export function isCollectionKind(entry) {
|
|
326
|
+
return entry.kind === "collection" || entry.kind === "content";
|
|
327
|
+
}
|
|
328
|
+
function contentSystemFields(blocks, labels, translatable) {
|
|
329
|
+
const label = (key, fallback) => labels[key] || fallback;
|
|
330
|
+
const localized = translatable ? { translatable: true } : {};
|
|
331
|
+
return {
|
|
332
|
+
title: { label: label("title", "Title"), type: "text", required: true, ...localized },
|
|
333
|
+
slug: {
|
|
334
|
+
label: label("slug", "Slug"),
|
|
335
|
+
type: "slug",
|
|
336
|
+
from: CONTENT_TITLE_FIELD,
|
|
337
|
+
required: true,
|
|
338
|
+
description: "Filled from the title in the default language."
|
|
339
|
+
},
|
|
340
|
+
publishedAt: {
|
|
341
|
+
label: label("publishedAt", "Published at"),
|
|
342
|
+
type: "datetime",
|
|
343
|
+
description: "Empty means now when you publish. A future date schedules the item."
|
|
344
|
+
},
|
|
345
|
+
excerpt: {
|
|
346
|
+
label: label("excerpt", "Excerpt"),
|
|
347
|
+
type: "text",
|
|
348
|
+
textarea: true,
|
|
349
|
+
...localized
|
|
350
|
+
},
|
|
351
|
+
cover: { label: label("cover", "Cover"), type: "media", mediaType: "image" },
|
|
352
|
+
body: { label: label("body", "Body"), type: "blocks", blocks },
|
|
353
|
+
seoTitle: { label: label("seoTitle", "SEO title"), type: "text", ...localized },
|
|
354
|
+
seoDescription: {
|
|
355
|
+
label: label("seoDescription", "SEO description"),
|
|
356
|
+
type: "text",
|
|
357
|
+
textarea: true,
|
|
358
|
+
...localized
|
|
359
|
+
},
|
|
360
|
+
seoImage: { label: label("seoImage", "SEO image"), type: "media", mediaType: "image" }
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function normalizeContentEntry(entry, translatable) {
|
|
364
|
+
if (entry.declared) return entry;
|
|
365
|
+
const declared = entry.fields ?? {};
|
|
366
|
+
const system = contentSystemFields(entry.blocks ?? {}, entry.labels ?? {}, translatable);
|
|
367
|
+
const extra = Object.fromEntries(
|
|
368
|
+
Object.entries(declared).filter(([key]) => !Object.hasOwn(system, key))
|
|
369
|
+
);
|
|
370
|
+
const { body, seoTitle, seoDescription, seoImage, ...head } = system;
|
|
371
|
+
return Object.assign(entry, {
|
|
372
|
+
declared: {
|
|
373
|
+
fields: Object.keys(declared),
|
|
374
|
+
...entry.titleField !== void 0 ? { titleField: entry.titleField } : {},
|
|
375
|
+
...entry.drafts !== void 0 ? { drafts: entry.drafts } : {}
|
|
376
|
+
},
|
|
377
|
+
titleField: CONTENT_TITLE_FIELD,
|
|
378
|
+
drafts: true,
|
|
379
|
+
fields: { ...head, ...extra, body, seoTitle, seoDescription, seoImage },
|
|
380
|
+
list: entry.list ?? { columns: [...CONTENT_LIST_COLUMNS] }
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
export function normalizeContentEntries(config, i18n) {
|
|
384
|
+
const translatable = !!i18n?.locales.length;
|
|
385
|
+
for (const entry of Object.values(config)) {
|
|
386
|
+
if (entry?.kind === "content") normalizeContentEntry(entry, translatable);
|
|
387
|
+
}
|
|
388
|
+
return config;
|
|
389
|
+
}
|
|
390
|
+
export function normalizeCmsConfig(config, i18n) {
|
|
391
|
+
return expandMobileMedia(normalizeContentEntries(config, i18n));
|
|
392
|
+
}
|
|
393
|
+
export function slugSourceValue(source, value, defaultLocale) {
|
|
394
|
+
if (value == null) return null;
|
|
395
|
+
if (source && isTranslatableField(source) && typeof value === "object") {
|
|
396
|
+
return value[defaultLocale] ?? null;
|
|
397
|
+
}
|
|
398
|
+
return typeof value === "string" ? value : null;
|
|
399
|
+
}
|
|
400
|
+
export function isScheduledContent(row, now = /* @__PURE__ */ new Date()) {
|
|
401
|
+
if (!row || row.status !== "published") return false;
|
|
402
|
+
const at = row[CONTENT_PUBLISHED_AT_FIELD];
|
|
403
|
+
if (typeof at !== "string" || !at) return false;
|
|
404
|
+
const time = new Date(at).getTime();
|
|
405
|
+
return !Number.isNaN(time) && time > now.getTime();
|
|
406
|
+
}
|
|
407
|
+
export function resolveBlocksMedia(field, value, resolve) {
|
|
408
|
+
if (!Array.isArray(value)) return value;
|
|
409
|
+
return value.map((item) => {
|
|
410
|
+
if (!item || typeof item !== "object") return item;
|
|
411
|
+
const block = field.blocks?.[String(item.type)];
|
|
412
|
+
if (!block) return item;
|
|
413
|
+
const resolved = { ...item };
|
|
414
|
+
for (const [key, blockField] of Object.entries(block.fields)) {
|
|
415
|
+
if (blockField.type !== "media") continue;
|
|
416
|
+
const media = resolved[key];
|
|
417
|
+
resolved[key] = typeof media === "string" && media ? resolve(media) ?? null : null;
|
|
418
|
+
}
|
|
419
|
+
return resolved;
|
|
420
|
+
});
|
|
421
|
+
}
|
|
292
422
|
export function entryTabs(entry) {
|
|
293
423
|
return entry.tabs?.length ? entry.tabs : [];
|
|
294
424
|
}
|
|
@@ -392,5 +522,8 @@ export function blockTypeName(entryName, fieldKey, blockName) {
|
|
|
392
522
|
export function defineCmsConfig(config) {
|
|
393
523
|
return config;
|
|
394
524
|
}
|
|
525
|
+
export function defineCmsBlocks(blocks) {
|
|
526
|
+
return blocks;
|
|
527
|
+
}
|
|
395
528
|
export * from "./layout.js";
|
|
396
529
|
export * from "./users.js";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { BlockConfig, FieldConfig, MediaItem, MediaType } from './index.js';
|
|
2
|
+
export declare const CMS_PREVIEW_CHANNEL = "nuxt-cms:preview";
|
|
3
|
+
export declare const DEFAULT_CMS_PREVIEW_PATH = "/cms/preview";
|
|
4
|
+
export declare function previewPathErrors(path: string, entryNames: string[]): string[];
|
|
5
|
+
export type CmsBlockValue = Record<string, unknown>;
|
|
6
|
+
export interface CmsPreviewMedia {
|
|
7
|
+
key: string;
|
|
8
|
+
url: string | null;
|
|
9
|
+
type: MediaType;
|
|
10
|
+
alt: string | null;
|
|
11
|
+
folder: string | null;
|
|
12
|
+
mime: string | null;
|
|
13
|
+
size: number | null;
|
|
14
|
+
width: number | null;
|
|
15
|
+
height: number | null;
|
|
16
|
+
}
|
|
17
|
+
export interface CmsPreviewState {
|
|
18
|
+
entry: string;
|
|
19
|
+
field: string;
|
|
20
|
+
locale: string;
|
|
21
|
+
blocks: CmsBlockValue[];
|
|
22
|
+
item: Record<string, unknown> | null;
|
|
23
|
+
media: Record<string, CmsPreviewMedia>;
|
|
24
|
+
selected: number | null;
|
|
25
|
+
}
|
|
26
|
+
export type CmsPreviewHostMessage = {
|
|
27
|
+
type: 'render';
|
|
28
|
+
state: CmsPreviewState;
|
|
29
|
+
};
|
|
30
|
+
export type CmsPreviewFrameMessage = {
|
|
31
|
+
type: 'ready';
|
|
32
|
+
} | {
|
|
33
|
+
type: 'select';
|
|
34
|
+
index: number | null;
|
|
35
|
+
focus?: boolean;
|
|
36
|
+
} | {
|
|
37
|
+
type: 'insert';
|
|
38
|
+
index: number;
|
|
39
|
+
} | {
|
|
40
|
+
type: 'move';
|
|
41
|
+
from: number;
|
|
42
|
+
to: number;
|
|
43
|
+
focus?: boolean;
|
|
44
|
+
} | {
|
|
45
|
+
type: 'duplicate';
|
|
46
|
+
index: number;
|
|
47
|
+
} | {
|
|
48
|
+
type: 'toggle-hidden';
|
|
49
|
+
index: number;
|
|
50
|
+
} | {
|
|
51
|
+
type: 'remove';
|
|
52
|
+
index: number;
|
|
53
|
+
};
|
|
54
|
+
export type CmsPreviewEnvelope<T> = T & {
|
|
55
|
+
channel: typeof CMS_PREVIEW_CHANNEL;
|
|
56
|
+
};
|
|
57
|
+
export declare function previewEnvelope<T extends {
|
|
58
|
+
type: string;
|
|
59
|
+
}>(message: T): CmsPreviewEnvelope<T>;
|
|
60
|
+
export declare function parseFrameMessage(data: unknown): CmsPreviewFrameMessage | null;
|
|
61
|
+
export declare function parseHostMessage(data: unknown): CmsPreviewHostMessage | null;
|
|
62
|
+
export declare function isTrustedPreviewEvent(event: {
|
|
63
|
+
origin: string;
|
|
64
|
+
source: unknown;
|
|
65
|
+
}, origin: string, source: unknown): boolean;
|
|
66
|
+
export declare function emptyBlock(type: string, block: BlockConfig): CmsBlockValue;
|
|
67
|
+
export declare function insertBlock(list: readonly CmsBlockValue[], index: number, item: CmsBlockValue): CmsBlockValue[];
|
|
68
|
+
export declare function moveBlock(list: readonly CmsBlockValue[], from: number, to: number): CmsBlockValue[];
|
|
69
|
+
export declare function duplicateBlock(list: readonly CmsBlockValue[], index: number): CmsBlockValue[];
|
|
70
|
+
export declare function removeBlock(list: readonly CmsBlockValue[], index: number): CmsBlockValue[];
|
|
71
|
+
export declare function canHideBlock(block: BlockConfig | undefined): boolean;
|
|
72
|
+
export declare function toggleBlockHidden(list: readonly CmsBlockValue[], index: number, block: BlockConfig | undefined): CmsBlockValue[];
|
|
73
|
+
export declare function selectionAfterMove(selected: number | null, from: number, to: number): number | null;
|
|
74
|
+
export declare function selectionAfterRemove(selected: number | null, index: number): number | null;
|
|
75
|
+
export declare function moveTargetForInsertion(from: number, insertAt: number): number;
|
|
76
|
+
export declare function insertionIndex(blocks: readonly {
|
|
77
|
+
index: number;
|
|
78
|
+
top: number;
|
|
79
|
+
bottom: number;
|
|
80
|
+
}[], y: number, total: number): number;
|
|
81
|
+
export declare function collectMediaKeys(fields: Record<string, FieldConfig>, values: Record<string, unknown> | null | undefined): string[];
|
|
82
|
+
export declare function previewMedia(key: string, item: MediaItem | undefined, baseUrl: string | null | undefined): CmsPreviewMedia;
|
|
83
|
+
export declare function previewBlocks(field: FieldConfig, value: unknown, locale: string, defaultLocale: string, media: Record<string, CmsPreviewMedia>): CmsBlockValue[];
|
|
84
|
+
export declare function previewItem(fields: Record<string, FieldConfig>, values: Record<string, unknown>, locale: string, defaultLocale: string, media: Record<string, CmsPreviewMedia>): Record<string, unknown>;
|
|
85
|
+
export declare function blockSummary(block: BlockConfig | undefined, item: CmsBlockValue, locale: string, defaultLocale: string, max?: number): string;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BLOCK_HIDDEN_KEY,
|
|
3
|
+
declaresHiddenField,
|
|
4
|
+
decodeTranslatableValue,
|
|
5
|
+
isHiddenBlock,
|
|
6
|
+
isPrivateField,
|
|
7
|
+
isTranslatableField,
|
|
8
|
+
isTranslatableMediaField,
|
|
9
|
+
localizeBlocks,
|
|
10
|
+
mediaPublicUrl,
|
|
11
|
+
mediaTypeForKey,
|
|
12
|
+
pickTranslatedMedia,
|
|
13
|
+
resolveBlocksMedia
|
|
14
|
+
} from "./index.js";
|
|
15
|
+
export const CMS_PREVIEW_CHANNEL = "nuxt-cms:preview";
|
|
16
|
+
export const DEFAULT_CMS_PREVIEW_PATH = "/cms/preview";
|
|
17
|
+
const RESERVED_ADMIN_SEGMENTS = /* @__PURE__ */ new Set(["login", "media"]);
|
|
18
|
+
export function previewPathErrors(path, entryNames) {
|
|
19
|
+
const match = /^\/cms\/([a-z0-9][a-z0-9-]*)$/.exec(path);
|
|
20
|
+
if (!match)
|
|
21
|
+
return [
|
|
22
|
+
`preview.path '${path}' must be a single lowercase segment under /cms, like '${DEFAULT_CMS_PREVIEW_PATH}'`
|
|
23
|
+
];
|
|
24
|
+
const segment = match[1];
|
|
25
|
+
if (RESERVED_ADMIN_SEGMENTS.has(segment) || entryNames.includes(segment))
|
|
26
|
+
return [`preview.path '${path}' clashes with an admin route, pick another segment`];
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
export function previewEnvelope(message) {
|
|
30
|
+
return { ...message, channel: CMS_PREVIEW_CHANNEL };
|
|
31
|
+
}
|
|
32
|
+
function isRecord(value) {
|
|
33
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
function isIndex(value) {
|
|
36
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
37
|
+
}
|
|
38
|
+
function envelope(data) {
|
|
39
|
+
if (!isRecord(data) || data.channel !== CMS_PREVIEW_CHANNEL) return null;
|
|
40
|
+
return typeof data.type === "string" ? data : null;
|
|
41
|
+
}
|
|
42
|
+
export function parseFrameMessage(data) {
|
|
43
|
+
const message = envelope(data);
|
|
44
|
+
if (!message) return null;
|
|
45
|
+
const focus = message.focus === true ? { focus: true } : {};
|
|
46
|
+
switch (message.type) {
|
|
47
|
+
case "ready":
|
|
48
|
+
return { type: "ready" };
|
|
49
|
+
case "select":
|
|
50
|
+
return message.index === null || isIndex(message.index) ? { type: "select", index: message.index, ...focus } : null;
|
|
51
|
+
case "move":
|
|
52
|
+
return isIndex(message.from) && isIndex(message.to) ? { type: "move", from: message.from, to: message.to, ...focus } : null;
|
|
53
|
+
case "insert":
|
|
54
|
+
case "duplicate":
|
|
55
|
+
case "toggle-hidden":
|
|
56
|
+
case "remove":
|
|
57
|
+
return isIndex(message.index) ? { type: message.type, index: message.index } : null;
|
|
58
|
+
default:
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function parseHostMessage(data) {
|
|
63
|
+
const message = envelope(data);
|
|
64
|
+
if (!message || message.type !== "render" || !isRecord(message.state)) return null;
|
|
65
|
+
const state = message.state;
|
|
66
|
+
if (typeof state.entry !== "string" || typeof state.field !== "string") return null;
|
|
67
|
+
if (typeof state.locale !== "string" || !Array.isArray(state.blocks)) return null;
|
|
68
|
+
if (state.item !== null && !isRecord(state.item)) return null;
|
|
69
|
+
if (!isRecord(state.media)) return null;
|
|
70
|
+
if (state.selected !== null && !isIndex(state.selected)) return null;
|
|
71
|
+
return {
|
|
72
|
+
type: "render",
|
|
73
|
+
state: {
|
|
74
|
+
entry: state.entry,
|
|
75
|
+
field: state.field,
|
|
76
|
+
locale: state.locale,
|
|
77
|
+
blocks: state.blocks.filter(isRecord),
|
|
78
|
+
item: state.item,
|
|
79
|
+
media: state.media,
|
|
80
|
+
selected: state.selected
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function isTrustedPreviewEvent(event, origin, source) {
|
|
85
|
+
return !!source && event.origin === origin && event.source === source;
|
|
86
|
+
}
|
|
87
|
+
function clamp(index, length) {
|
|
88
|
+
return Math.min(Math.max(index, 0), length);
|
|
89
|
+
}
|
|
90
|
+
export function emptyBlock(type, block) {
|
|
91
|
+
return { type, ...Object.fromEntries(Object.keys(block.fields).map((key) => [key, null])) };
|
|
92
|
+
}
|
|
93
|
+
export function insertBlock(list, index, item) {
|
|
94
|
+
const next = [...list];
|
|
95
|
+
next.splice(clamp(index, list.length), 0, item);
|
|
96
|
+
return next;
|
|
97
|
+
}
|
|
98
|
+
export function moveBlock(list, from, to) {
|
|
99
|
+
if (from === to || from < 0 || from >= list.length || to < 0 || to >= list.length)
|
|
100
|
+
return [...list];
|
|
101
|
+
const next = [...list];
|
|
102
|
+
const [item] = next.splice(from, 1);
|
|
103
|
+
next.splice(to, 0, item);
|
|
104
|
+
return next;
|
|
105
|
+
}
|
|
106
|
+
export function duplicateBlock(list, index) {
|
|
107
|
+
const item = list[index];
|
|
108
|
+
if (!item) return [...list];
|
|
109
|
+
return insertBlock(list, index + 1, JSON.parse(JSON.stringify(item)));
|
|
110
|
+
}
|
|
111
|
+
export function removeBlock(list, index) {
|
|
112
|
+
return list.filter((_, i) => i !== index);
|
|
113
|
+
}
|
|
114
|
+
export function canHideBlock(block) {
|
|
115
|
+
return !!block && !declaresHiddenField(block);
|
|
116
|
+
}
|
|
117
|
+
export function toggleBlockHidden(list, index, block) {
|
|
118
|
+
const item = list[index];
|
|
119
|
+
if (!item || !canHideBlock(block)) return [...list];
|
|
120
|
+
const { [BLOCK_HIDDEN_KEY]: _hidden, ...rest } = item;
|
|
121
|
+
const updated = isHiddenBlock(item) ? rest : { ...rest, [BLOCK_HIDDEN_KEY]: true };
|
|
122
|
+
return list.map((current, i) => i === index ? updated : current);
|
|
123
|
+
}
|
|
124
|
+
export function selectionAfterMove(selected, from, to) {
|
|
125
|
+
if (selected === null) return null;
|
|
126
|
+
if (selected === from) return to;
|
|
127
|
+
if (from < selected && to >= selected) return selected - 1;
|
|
128
|
+
if (from > selected && to <= selected) return selected + 1;
|
|
129
|
+
return selected;
|
|
130
|
+
}
|
|
131
|
+
export function selectionAfterRemove(selected, index) {
|
|
132
|
+
if (selected === null || selected === index) return null;
|
|
133
|
+
return selected > index ? selected - 1 : selected;
|
|
134
|
+
}
|
|
135
|
+
export function moveTargetForInsertion(from, insertAt) {
|
|
136
|
+
return insertAt > from ? insertAt - 1 : insertAt;
|
|
137
|
+
}
|
|
138
|
+
export function insertionIndex(blocks, y, total) {
|
|
139
|
+
for (const block of blocks) {
|
|
140
|
+
if (y < (block.top + block.bottom) / 2) return block.index;
|
|
141
|
+
}
|
|
142
|
+
const last = blocks[blocks.length - 1];
|
|
143
|
+
return last ? last.index + 1 : total;
|
|
144
|
+
}
|
|
145
|
+
function pushMediaKeys(keys, field, value) {
|
|
146
|
+
if (field.type !== "media" || value == null) return;
|
|
147
|
+
const values = typeof value === "object" ? Object.values(value) : [value];
|
|
148
|
+
for (const key of values) if (typeof key === "string" && key) keys.add(key);
|
|
149
|
+
}
|
|
150
|
+
export function collectMediaKeys(fields, values) {
|
|
151
|
+
const keys = /* @__PURE__ */ new Set();
|
|
152
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
153
|
+
const value = values?.[key];
|
|
154
|
+
if (field.type === "media") {
|
|
155
|
+
pushMediaKeys(keys, field, value);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (field.type !== "blocks" || !Array.isArray(value)) continue;
|
|
159
|
+
for (const item of value) {
|
|
160
|
+
if (!isRecord(item)) continue;
|
|
161
|
+
const block = field.blocks?.[String(item.type)];
|
|
162
|
+
for (const [blockKey, blockField] of Object.entries(block?.fields ?? {}))
|
|
163
|
+
pushMediaKeys(keys, blockField, item[blockKey]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return [...keys];
|
|
167
|
+
}
|
|
168
|
+
export function previewMedia(key, item, baseUrl) {
|
|
169
|
+
return {
|
|
170
|
+
key,
|
|
171
|
+
url: item?.url ?? mediaPublicUrl(baseUrl, key),
|
|
172
|
+
type: item?.type ?? mediaTypeForKey(key),
|
|
173
|
+
alt: item?.alt ?? null,
|
|
174
|
+
folder: item?.folder ?? null,
|
|
175
|
+
mime: item?.mime ?? null,
|
|
176
|
+
size: item?.size ?? null,
|
|
177
|
+
width: item?.width ?? null,
|
|
178
|
+
height: item?.height ?? null
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export function previewBlocks(field, value, locale, defaultLocale, media) {
|
|
182
|
+
if (!Array.isArray(value)) return [];
|
|
183
|
+
const localized = localizeBlocks(field, value, locale, defaultLocale);
|
|
184
|
+
return resolveBlocksMedia(field, localized, (key) => media[key] ?? null);
|
|
185
|
+
}
|
|
186
|
+
export function previewItem(fields, values, locale, defaultLocale, media) {
|
|
187
|
+
const item = {};
|
|
188
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
189
|
+
if (isPrivateField(field)) continue;
|
|
190
|
+
const value = values[key] ?? null;
|
|
191
|
+
if (field.type === "blocks") {
|
|
192
|
+
item[key] = previewBlocks(field, value, locale, defaultLocale, media);
|
|
193
|
+
} else if (field.type === "media") {
|
|
194
|
+
const mediaKey = isTranslatableMediaField(field) ? pickTranslatedMedia(
|
|
195
|
+
decodeTranslatableValue(value, defaultLocale),
|
|
196
|
+
locale,
|
|
197
|
+
defaultLocale
|
|
198
|
+
) : value;
|
|
199
|
+
item[key] = typeof mediaKey === "string" && mediaKey ? media[mediaKey] ?? null : null;
|
|
200
|
+
} else if (isTranslatableField(field)) {
|
|
201
|
+
const translations = decodeTranslatableValue(value, defaultLocale);
|
|
202
|
+
item[key] = translations?.[locale] ?? translations?.[defaultLocale] ?? null;
|
|
203
|
+
} else {
|
|
204
|
+
item[key] = value;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return item;
|
|
208
|
+
}
|
|
209
|
+
export function blockSummary(block, item, locale, defaultLocale, max = 80) {
|
|
210
|
+
for (const [key, field] of Object.entries(block?.fields ?? {})) {
|
|
211
|
+
if (field.type !== "text" && field.type !== "richtext") continue;
|
|
212
|
+
const raw = item[key];
|
|
213
|
+
const translations = isRecord(raw) ? raw : null;
|
|
214
|
+
const value = typeof raw === "string" ? raw : translations?.[locale] || translations?.[defaultLocale] || "";
|
|
215
|
+
const text = value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
216
|
+
if (text) return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
217
|
+
}
|
|
218
|
+
return "";
|
|
219
|
+
}
|
|
@@ -4,11 +4,13 @@ export declare const objectKeySchema: z.ZodString;
|
|
|
4
4
|
export interface ValidationMessages {
|
|
5
5
|
required: string;
|
|
6
6
|
invalidDate: string;
|
|
7
|
+
invalidDateTime: string;
|
|
7
8
|
invalidEmail: string;
|
|
8
9
|
invalidSlug: string;
|
|
9
10
|
unknownLocale: string;
|
|
10
11
|
requiredLocale: (locale: string) => string;
|
|
11
12
|
}
|
|
13
|
+
export declare function datetimeSchema(message: string): z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
12
14
|
export declare function buildEntrySchema(entry: Pick<CmsEntry, 'fields'> & {
|
|
13
15
|
drafts?: boolean;
|
|
14
16
|
}, i18n: CmsI18n, messages?: Partial<ValidationMessages>): z.ZodObject<{
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { isFieldVisible, isTranslatableField } from "./index.js";
|
|
2
|
+
import { BLOCK_HIDDEN_KEY, declaresHiddenField, 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",
|
|
6
6
|
invalidDate: "Invalid date (yyyy-mm-dd)",
|
|
7
|
+
invalidDateTime: "Invalid date and time (ISO 8601 with a time zone)",
|
|
7
8
|
invalidEmail: "Invalid email",
|
|
8
9
|
invalidSlug: "Invalid slug (lowercase letters, numbers, dashes)",
|
|
9
10
|
unknownLocale: "Unknown locale key",
|
|
10
11
|
requiredLocale: (locale) => `Required field (locale '${locale}')`
|
|
11
12
|
};
|
|
13
|
+
const DATETIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
14
|
+
export function datetimeSchema(message) {
|
|
15
|
+
return z.string().trim().refine(
|
|
16
|
+
(value) => DATETIME_PATTERN.test(value) && !Number.isNaN(new Date(value).getTime()),
|
|
17
|
+
message
|
|
18
|
+
).transform((value) => new Date(value).toISOString());
|
|
19
|
+
}
|
|
12
20
|
function scalarSchema(field, m) {
|
|
13
21
|
switch (field.type) {
|
|
14
22
|
case "number":
|
|
@@ -19,6 +27,8 @@ function scalarSchema(field, m) {
|
|
|
19
27
|
return z.string().min(1);
|
|
20
28
|
case "date":
|
|
21
29
|
return z.string().regex(/^\d{4}-\d{2}-\d{2}$/, m.invalidDate);
|
|
30
|
+
case "datetime":
|
|
31
|
+
return datetimeSchema(m.invalidDateTime);
|
|
22
32
|
case "email":
|
|
23
33
|
return z.email(m.invalidEmail);
|
|
24
34
|
case "slug":
|
|
@@ -37,7 +47,9 @@ function optionalize(field, base, m) {
|
|
|
37
47
|
}
|
|
38
48
|
function translatableSchema(field, i18n, m) {
|
|
39
49
|
const { locales, defaultLocale } = i18n;
|
|
40
|
-
const record = z.record(z.string(), field.type === "media" ? objectKeySchema : z.string()
|
|
50
|
+
const record = z.record(z.string(), field.type === "media" ? objectKeySchema : z.string(), {
|
|
51
|
+
error: (issue) => issue.input == null ? m.required : void 0
|
|
52
|
+
}).refine((v) => Object.keys(v).every((k) => locales.includes(k)), m.unknownLocale);
|
|
41
53
|
return field.required ? record.refine((v) => !!v[defaultLocale]?.trim(), m.requiredLocale(defaultLocale)) : record.nullish().transform((v) => v ?? null);
|
|
42
54
|
}
|
|
43
55
|
function blockFieldSchema(blockField, i18n, m) {
|
|
@@ -49,6 +61,7 @@ function blocksSchema(field, i18n, m) {
|
|
|
49
61
|
const variants = Object.entries(field.blocks ?? {}).map(
|
|
50
62
|
([type, block]) => z.object({
|
|
51
63
|
type: z.literal(type),
|
|
64
|
+
...declaresHiddenField(block) ? {} : { [BLOCK_HIDDEN_KEY]: z.boolean().optional() },
|
|
52
65
|
...Object.fromEntries(
|
|
53
66
|
Object.entries(block.fields).map(([key, blockField]) => [
|
|
54
67
|
key,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xleddyl/nuxt-cms",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.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)",
|