@xleddyl/nuxt-cms 0.2.1 → 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 +320 -33
- 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/BrandMark.vue +2 -3
- package/dist/runtime/app/components/cms/ConfirmModal.vue +14 -1
- 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/MediaGallery.vue +89 -11
- package/dist/runtime/app/components/cms/Modal.vue +0 -2
- 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-confirm.d.ts +16 -7
- package/dist/runtime/app/composables/cms-confirm.js +1 -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-media-library.d.ts +1 -2
- package/dist/runtime/app/composables/cms-media-library.js +12 -19
- 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.d.ts +0 -2
- package/dist/runtime/app/utils/ui.js +1 -0
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/seed.js +11 -1
- package/dist/runtime/server/api/collection.get.js +3 -2
- package/dist/runtime/server/api/collection.post.js +24 -15
- package/dist/runtime/server/api/collection.put.js +12 -12
- package/dist/runtime/server/api/item.delete.js +2 -1
- package/dist/runtime/server/api/item.put.js +16 -14
- package/dist/runtime/server/api/media-folder.patch.js +10 -16
- package/dist/runtime/server/api/media-move.post.js +12 -8
- package/dist/runtime/server/api/media.delete.js +1 -1
- package/dist/runtime/server/api/media.post.js +2 -1
- package/dist/runtime/server/api/media.put.js +2 -1
- package/dist/runtime/server/plugins/migrate-sqlite.js +2 -2
- package/dist/runtime/server/routes/auth/login.post.js +28 -22
- package/dist/runtime/server/utils/db-d1.d.ts +0 -1
- package/dist/runtime/server/utils/db-d1.js +3 -4
- package/dist/runtime/server/utils/db-libsql.d.ts +0 -1
- package/dist/runtime/server/utils/db-libsql.js +3 -4
- package/dist/runtime/server/utils/db-postgres.d.ts +0 -1
- package/dist/runtime/server/utils/db-postgres.js +1 -4
- package/dist/runtime/server/utils/db-sqlite.d.ts +0 -1
- package/dist/runtime/server/utils/db-sqlite.js +4 -10
- package/dist/runtime/server/utils/graphql.js +25 -9
- package/dist/runtime/server/utils/libsql-migrations.d.ts +1 -0
- package/dist/runtime/server/utils/libsql-migrations.js +1 -1
- package/dist/runtime/server/utils/media-folders.js +14 -23
- package/dist/runtime/server/utils/media.js +2 -0
- package/dist/runtime/server/utils/migrate.d.ts +1 -0
- package/dist/runtime/server/utils/migrate.js +32 -0
- package/dist/runtime/server/utils/registry.d.ts +4 -0
- package/dist/runtime/server/utils/registry.js +16 -1
- package/dist/runtime/server/utils/relations.d.ts +1 -1
- package/dist/runtime/server/utils/relations.js +6 -9
- package/dist/runtime/server/utils/timestamps.d.ts +12 -0
- package/dist/runtime/server/utils/timestamps.js +48 -0
- package/dist/runtime/server/utils/users.js +3 -2
- package/dist/runtime/server/utils/visibility.d.ts +4 -0
- package/dist/runtime/server/utils/visibility.js +12 -0
- package/dist/runtime/server/utils/write-statements.d.ts +33 -0
- package/dist/runtime/server/utils/write-statements.js +75 -0
- package/dist/runtime/shared/graphql-sdl.js +4 -1
- package/dist/runtime/shared/index.d.ts +73 -5
- package/dist/runtime/shared/index.js +146 -0
- package/dist/runtime/shared/preview.d.ts +85 -0
- package/dist/runtime/shared/preview.js +219 -0
- package/dist/runtime/shared/timestamps.d.ts +4 -0
- package/dist/runtime/shared/timestamps.js +24 -0
- package/dist/runtime/shared/validation.d.ts +2 -0
- package/dist/runtime/shared/validation.js +15 -2
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const SQLITE_ISO_FORMAT = "%Y-%m-%dT%H:%M:%fZ";
|
|
2
|
+
export declare function nowTimestamp(): string;
|
|
3
|
+
export declare function normalizeTimestamp<T>(value: T): T | string;
|
|
4
|
+
export declare function normalizeTimestampFields<T extends Record<string, unknown>>(row: T, keys?: readonly string[]): T;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const SQLITE_ISO_FORMAT = "%Y-%m-%dT%H:%M:%fZ";
|
|
2
|
+
export function nowTimestamp() {
|
|
3
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
4
|
+
}
|
|
5
|
+
const TIMESTAMP_PATTERN = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(?:\.(\d+))?(Z|[+-]\d{2}(?::?\d{2})?)?$/;
|
|
6
|
+
export function normalizeTimestamp(value) {
|
|
7
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? value : value.toISOString();
|
|
8
|
+
if (typeof value !== "string") return value;
|
|
9
|
+
const match = TIMESTAMP_PATTERN.exec(value.trim());
|
|
10
|
+
if (!match) return value;
|
|
11
|
+
const [, date, time, fraction = "", zone] = match;
|
|
12
|
+
const millis = fraction.padEnd(3, "0").slice(0, 3);
|
|
13
|
+
const utc = `${date}T${time}.${millis}Z`;
|
|
14
|
+
if (!zone || zone === "Z") return utc;
|
|
15
|
+
const offset = zone.length === 3 ? `${zone}:00` : zone.includes(":") ? zone : `${zone.slice(0, 3)}:${zone.slice(3)}`;
|
|
16
|
+
const parsed = /* @__PURE__ */ new Date(`${date}T${time}.${millis}${offset}`);
|
|
17
|
+
return Number.isNaN(parsed.getTime()) ? value : parsed.toISOString();
|
|
18
|
+
}
|
|
19
|
+
export function normalizeTimestampFields(row, keys = ["createdAt", "updatedAt"]) {
|
|
20
|
+
for (const key of keys) {
|
|
21
|
+
if (Object.hasOwn(row, key)) row[key] = normalizeTimestamp(row[key]);
|
|
22
|
+
}
|
|
23
|
+
return row;
|
|
24
|
+
}
|
|
@@ -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)",
|