@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
|
@@ -4,11 +4,13 @@ import cmsConfig from "#cms-config";
|
|
|
4
4
|
import * as cmsTables from "#cms-tables";
|
|
5
5
|
import { useRuntimeConfig } from "#imports";
|
|
6
6
|
import {
|
|
7
|
+
CONTENT_PUBLISHED_AT_FIELD,
|
|
7
8
|
decodeEntryTranslatableMedia,
|
|
8
9
|
encodeEntryTranslatableMedia,
|
|
9
10
|
pageFields,
|
|
10
11
|
pageRouteOf
|
|
11
12
|
} from "../../shared/index.js";
|
|
13
|
+
import { normalizeTimestampFields, nowTimestamp } from "../../shared/timestamps.js";
|
|
12
14
|
import { buildEntrySchema } from "../../shared/validation.js";
|
|
13
15
|
let contentI18n;
|
|
14
16
|
export function getContentI18n() {
|
|
@@ -30,9 +32,16 @@ export function idColumn(table) {
|
|
|
30
32
|
}
|
|
31
33
|
export function withUpdatedAt(table, values) {
|
|
32
34
|
const set = { ...values };
|
|
33
|
-
if ("updatedAt" in table) set.updatedAt = (
|
|
35
|
+
if ("updatedAt" in table) set.updatedAt = nowTimestamp();
|
|
34
36
|
return set;
|
|
35
37
|
}
|
|
38
|
+
export function withTimestamps(table, values) {
|
|
39
|
+
const now = nowTimestamp();
|
|
40
|
+
const stamped = { ...values };
|
|
41
|
+
if ("createdAt" in table) stamped.createdAt ??= now;
|
|
42
|
+
if ("updatedAt" in table) stamped.updatedAt ??= now;
|
|
43
|
+
return stamped;
|
|
44
|
+
}
|
|
36
45
|
export function getRegistryEntry(event) {
|
|
37
46
|
const name = getRouterParam(event, "collection");
|
|
38
47
|
const config = cmsConfig;
|
|
@@ -58,8 +67,14 @@ export function encodeColumnValues(entry, values) {
|
|
|
58
67
|
return encodeEntryTranslatableMedia(entry, values);
|
|
59
68
|
}
|
|
60
69
|
export function decodeRows(entry, rows) {
|
|
70
|
+
for (const row of rows) normalizeTimestampFields(row);
|
|
61
71
|
return decodeEntryTranslatableMedia(entry, rows, getContentI18n().defaultLocale);
|
|
62
72
|
}
|
|
73
|
+
export function withContentDefaults(entry, values) {
|
|
74
|
+
if (entry.kind !== "content" || values.status !== "published") return values;
|
|
75
|
+
if (!values[CONTENT_PUBLISHED_AT_FIELD]) values[CONTENT_PUBLISHED_AT_FIELD] = nowTimestamp();
|
|
76
|
+
return values;
|
|
77
|
+
}
|
|
63
78
|
export function parseId(event) {
|
|
64
79
|
return z.string().min(1).parse(getRouterParam(event, "id"));
|
|
65
80
|
}
|
|
@@ -8,7 +8,7 @@ export declare function splitRelationValues(entry: CmsEntry, body: Row): {
|
|
|
8
8
|
lists: Record<string, string[]>;
|
|
9
9
|
};
|
|
10
10
|
export declare function assertRelationTargets(entry: CmsEntry, lists: Record<string, string[]>): Promise<void>;
|
|
11
|
-
export declare function
|
|
11
|
+
export declare function manyToManyStatements(db: CmsDb, name: string, sourceId: string, lists: Record<string, string[]>): PromiseLike<unknown>[];
|
|
12
12
|
export declare function relationTitles(db: CmsDb, entry: CmsEntry, rows: Row[]): Promise<Record<string, Record<string, unknown>>>;
|
|
13
13
|
export declare function attachManyToMany<T extends Row>(db: CmsDb, name: string, entry: CmsEntry, rows: T[]): Promise<T[]>;
|
|
14
14
|
export {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { asc,
|
|
1
|
+
import { asc, inArray } from "drizzle-orm";
|
|
2
2
|
import { createError } from "h3";
|
|
3
3
|
import cmsConfig from "#cms-config";
|
|
4
4
|
import { useDb } from "#cms-db";
|
|
5
5
|
import * as cmsTables from "#cms-tables";
|
|
6
6
|
import { entryFieldsFor } from "../../shared/index.js";
|
|
7
7
|
import { resolveTable, tableColumns } from "./registry.js";
|
|
8
|
+
import { joinWriteStatements } from "./write-statements.js";
|
|
8
9
|
function manyToManyKeys(entry) {
|
|
9
10
|
return Object.entries(entryFieldsFor(entry)).filter(([, field]) => field.type === "relation" && field.cardinality === "many-to-many").map(([key]) => key);
|
|
10
11
|
}
|
|
@@ -50,14 +51,10 @@ export async function assertRelationTargets(entry, lists) {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
|
-
export
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (ids.length) {
|
|
58
|
-
await db.insert(join.table).values(ids.map((targetId, position) => ({ sourceId, targetId, position })));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
54
|
+
export function manyToManyStatements(db, name, sourceId, lists) {
|
|
55
|
+
return Object.entries(lists).flatMap(
|
|
56
|
+
([key, ids]) => joinWriteStatements(db, joinTable(name, key), sourceId, ids)
|
|
57
|
+
);
|
|
61
58
|
}
|
|
62
59
|
export async function relationTitles(db, entry, rows) {
|
|
63
60
|
const config = cmsConfig;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface TimestampColumn {
|
|
2
|
+
table: string;
|
|
3
|
+
column: string;
|
|
4
|
+
}
|
|
5
|
+
export interface TimestampBackfillDriver {
|
|
6
|
+
select: (query: string) => Promise<unknown[]>;
|
|
7
|
+
write: (statements: string[]) => Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
export declare function timestampColumns(tables: Record<string, unknown>): TimestampColumn[];
|
|
10
|
+
export declare function timestampProbeQuery(columns: TimestampColumn[]): string;
|
|
11
|
+
export declare function timestampBackfillStatement({ table, column }: TimestampColumn): string;
|
|
12
|
+
export declare function backfillTimestamps(tables: Record<string, unknown>, driver: TimestampBackfillDriver): Promise<number>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { is } from "drizzle-orm";
|
|
2
|
+
import { SQLiteTable, getTableConfig } from "drizzle-orm/sqlite-core";
|
|
3
|
+
import { SQLITE_ISO_FORMAT } from "../../shared/timestamps.js";
|
|
4
|
+
const TIMESTAMP_COLUMNS = /* @__PURE__ */ new Set(["created_at", "updated_at"]);
|
|
5
|
+
function quoteIdentifier(name) {
|
|
6
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
7
|
+
}
|
|
8
|
+
function legacyCondition(column) {
|
|
9
|
+
const target = quoteIdentifier(column);
|
|
10
|
+
return `${target} NOT LIKE '%T%' AND strftime('${SQLITE_ISO_FORMAT}', ${target}) IS NOT NULL`;
|
|
11
|
+
}
|
|
12
|
+
export function timestampColumns(tables) {
|
|
13
|
+
const found = [];
|
|
14
|
+
for (const table of Object.values(tables)) {
|
|
15
|
+
if (!is(table, SQLiteTable)) continue;
|
|
16
|
+
const { name, columns } = getTableConfig(table);
|
|
17
|
+
for (const column of columns) {
|
|
18
|
+
if (TIMESTAMP_COLUMNS.has(column.name)) found.push({ table: name, column: column.name });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return found;
|
|
22
|
+
}
|
|
23
|
+
export function timestampProbeQuery(columns) {
|
|
24
|
+
return columns.map(
|
|
25
|
+
({ table, column }, index) => `SELECT ${index} AS target WHERE EXISTS (SELECT 1 FROM ${quoteIdentifier(
|
|
26
|
+
table
|
|
27
|
+
)} WHERE ${legacyCondition(column)})`
|
|
28
|
+
).join(" UNION ALL ");
|
|
29
|
+
}
|
|
30
|
+
export function timestampBackfillStatement({ table, column }) {
|
|
31
|
+
const target = quoteIdentifier(column);
|
|
32
|
+
return `UPDATE ${quoteIdentifier(
|
|
33
|
+
table
|
|
34
|
+
)} SET ${target} = strftime('${SQLITE_ISO_FORMAT}', ${target}) WHERE ${legacyCondition(column)}`;
|
|
35
|
+
}
|
|
36
|
+
function targetIndex(row) {
|
|
37
|
+
if (Array.isArray(row)) return Number(row[0]);
|
|
38
|
+
return Number(row.target);
|
|
39
|
+
}
|
|
40
|
+
export async function backfillTimestamps(tables, driver) {
|
|
41
|
+
const columns = timestampColumns(tables);
|
|
42
|
+
if (!columns.length) return 0;
|
|
43
|
+
const rows = await driver.select(timestampProbeQuery(columns));
|
|
44
|
+
const stale = rows.map((row) => columns[targetIndex(row)]).filter((column) => !!column);
|
|
45
|
+
if (!stale.length) return 0;
|
|
46
|
+
await driver.write(stale.map(timestampBackfillStatement));
|
|
47
|
+
return stale.length;
|
|
48
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { eq } from "drizzle-orm";
|
|
2
2
|
import { useDb } from "#cms-db";
|
|
3
3
|
import { cms_users } from "#cms-tables";
|
|
4
|
+
import { normalizeTimestamp } from "../../shared/timestamps.js";
|
|
4
5
|
export async function findUserByEmail(email) {
|
|
5
6
|
const [row] = await useDb().select().from(cms_users).where(eq(cms_users.email, email.toLowerCase()));
|
|
6
7
|
return row;
|
|
@@ -28,7 +29,7 @@ export function toAccount(row) {
|
|
|
28
29
|
email: row.email,
|
|
29
30
|
name: row.name ?? null,
|
|
30
31
|
role: row.role,
|
|
31
|
-
lastLoginAt: row.lastLoginAt ?? null,
|
|
32
|
-
createdAt: String(row.createdAt)
|
|
32
|
+
lastLoginAt: normalizeTimestamp(row.lastLoginAt ?? null),
|
|
33
|
+
createdAt: String(normalizeTimestamp(row.createdAt))
|
|
33
34
|
};
|
|
34
35
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SQL } from 'drizzle-orm';
|
|
2
|
+
import type { AnySQLiteColumn } from 'drizzle-orm/sqlite-core';
|
|
3
|
+
import type { CmsEntry } from '../../shared/index.js';
|
|
4
|
+
export declare function visibilityConditions(entry: Pick<CmsEntry, 'kind' | 'drafts'>, columns: Record<string, AnySQLiteColumn>, now?: string): SQL[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { eq, isNotNull, lte } from "drizzle-orm";
|
|
2
|
+
import { CONTENT_PUBLISHED_AT_FIELD } from "../../shared/index.js";
|
|
3
|
+
import { nowTimestamp } from "../../shared/timestamps.js";
|
|
4
|
+
export function visibilityConditions(entry, columns, now = nowTimestamp()) {
|
|
5
|
+
if (!entry.drafts) return [];
|
|
6
|
+
const conditions = [];
|
|
7
|
+
if (columns.status) conditions.push(eq(columns.status, "published"));
|
|
8
|
+
const publishedAt = columns[CONTENT_PUBLISHED_AT_FIELD];
|
|
9
|
+
if (entry.kind === "content" && publishedAt)
|
|
10
|
+
conditions.push(isNotNull(publishedAt), lte(publishedAt, now));
|
|
11
|
+
return conditions;
|
|
12
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AnySQLiteColumn, BaseSQLiteDatabase, SQLiteTable } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
export type WriteDb = Pick<BaseSQLiteDatabase<'sync' | 'async', unknown>, 'insert' | 'update' | 'delete'>;
|
|
3
|
+
export interface JoinTable {
|
|
4
|
+
table: SQLiteTable;
|
|
5
|
+
sourceId: AnySQLiteColumn;
|
|
6
|
+
targetId: AnySQLiteColumn;
|
|
7
|
+
position: AnySQLiteColumn;
|
|
8
|
+
}
|
|
9
|
+
export interface RebasedMediaRow {
|
|
10
|
+
key: string;
|
|
11
|
+
folder: string | null;
|
|
12
|
+
target: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface MediaFolderRename {
|
|
15
|
+
folders: [string, string | null][];
|
|
16
|
+
markers: string[];
|
|
17
|
+
staleMarkers: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare const KEY_CHUNK = 90;
|
|
20
|
+
export declare function chunks<T>(items: T[], size: number): T[][];
|
|
21
|
+
export declare function joinWriteStatements(db: WriteDb, join: JoinTable, sourceId: string, ids: string[]): PromiseLike<unknown>[];
|
|
22
|
+
export declare function mediaFolderMarkerValues(folder: string): {
|
|
23
|
+
key: string;
|
|
24
|
+
mime: string;
|
|
25
|
+
size: number;
|
|
26
|
+
width: null;
|
|
27
|
+
height: null;
|
|
28
|
+
alt: null;
|
|
29
|
+
folder: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function planMediaFolderRename(rows: RebasedMediaRow[]): MediaFolderRename;
|
|
32
|
+
export declare function mediaFolderRenameStatements(db: WriteDb, table: SQLiteTable, plan: MediaFolderRename): PromiseLike<unknown>[];
|
|
33
|
+
export declare function mediaDeleteStatements(db: WriteDb, table: SQLiteTable, keys: string[]): PromiseLike<unknown>[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { eq, getTableColumns, inArray } from "drizzle-orm";
|
|
2
|
+
import { isMediaFolderMarker, mediaFolderDepth, mediaFolderMarkerKey } from "../../shared/index.js";
|
|
3
|
+
import { nowTimestamp } from "../../shared/timestamps.js";
|
|
4
|
+
export const KEY_CHUNK = 90;
|
|
5
|
+
const JOIN_INSERT_CHUNK = 30;
|
|
6
|
+
export function chunks(items, size) {
|
|
7
|
+
const out = [];
|
|
8
|
+
for (let index = 0; index < items.length; index += size) {
|
|
9
|
+
out.push(items.slice(index, index + size));
|
|
10
|
+
}
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
export function joinWriteStatements(db, join, sourceId, ids) {
|
|
14
|
+
const statements = [
|
|
15
|
+
db.delete(join.table).where(eq(join.sourceId, sourceId))
|
|
16
|
+
];
|
|
17
|
+
const rows = ids.map((targetId, position) => ({ sourceId, targetId, position }));
|
|
18
|
+
for (const chunk of chunks(rows, JOIN_INSERT_CHUNK)) {
|
|
19
|
+
statements.push(db.insert(join.table).values(chunk));
|
|
20
|
+
}
|
|
21
|
+
return statements;
|
|
22
|
+
}
|
|
23
|
+
export function mediaFolderMarkerValues(folder) {
|
|
24
|
+
return {
|
|
25
|
+
key: mediaFolderMarkerKey(folder),
|
|
26
|
+
mime: "application/x-empty",
|
|
27
|
+
size: 0,
|
|
28
|
+
width: null,
|
|
29
|
+
height: null,
|
|
30
|
+
alt: null,
|
|
31
|
+
folder
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function planMediaFolderRename(rows) {
|
|
35
|
+
const folders = /* @__PURE__ */ new Map();
|
|
36
|
+
const markers = /* @__PURE__ */ new Set();
|
|
37
|
+
for (const row of rows) {
|
|
38
|
+
if (!isMediaFolderMarker(row.key)) {
|
|
39
|
+
if (row.folder) folders.set(row.folder, row.target);
|
|
40
|
+
} else if (row.target) {
|
|
41
|
+
markers.add(row.target);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const markerKeys = new Set([...markers].map(mediaFolderMarkerKey));
|
|
45
|
+
return {
|
|
46
|
+
folders: [...folders].sort(([a], [b]) => mediaFolderDepth(a) - mediaFolderDepth(b)),
|
|
47
|
+
markers: [...markers],
|
|
48
|
+
staleMarkers: rows.filter((row) => isMediaFolderMarker(row.key) && !markerKeys.has(row.key)).map((row) => row.key)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function mediaColumns(table) {
|
|
52
|
+
const columns = getTableColumns(table);
|
|
53
|
+
return { key: columns.key, folder: columns.folder };
|
|
54
|
+
}
|
|
55
|
+
export function mediaFolderRenameStatements(db, table, plan) {
|
|
56
|
+
const columns = mediaColumns(table);
|
|
57
|
+
const statements = [];
|
|
58
|
+
for (const [folder, target] of plan.folders) {
|
|
59
|
+
statements.push(db.update(table).set({ folder: target }).where(eq(columns.folder, folder)));
|
|
60
|
+
}
|
|
61
|
+
for (const folder of plan.markers) {
|
|
62
|
+
const values = mediaFolderMarkerValues(folder);
|
|
63
|
+
statements.push(
|
|
64
|
+
db.insert(table).values({ ...values, createdAt: nowTimestamp() }).onConflictDoUpdate({ target: columns.key, set: values })
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
for (const keys of chunks(plan.staleMarkers, KEY_CHUNK)) {
|
|
68
|
+
statements.push(db.delete(table).where(inArray(columns.key, keys)));
|
|
69
|
+
}
|
|
70
|
+
return statements;
|
|
71
|
+
}
|
|
72
|
+
export function mediaDeleteStatements(db, table, keys) {
|
|
73
|
+
const { key } = mediaColumns(table);
|
|
74
|
+
return chunks(keys, KEY_CHUNK).map((chunk) => db.delete(table).where(inArray(key, chunk)));
|
|
75
|
+
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
PAGE_PATH_FIELD,
|
|
3
3
|
blockTypeName,
|
|
4
4
|
blocksFieldTypeName,
|
|
5
|
+
isCollectionKind,
|
|
5
6
|
isPrivateField,
|
|
6
7
|
isRequiredField,
|
|
7
8
|
isTranslatableField,
|
|
@@ -72,7 +73,7 @@ function entrySdl(config, name, entry) {
|
|
|
72
73
|
if (isPrivateField(field)) continue;
|
|
73
74
|
lines.push(fieldSdl(config, name, key, field));
|
|
74
75
|
}
|
|
75
|
-
if (entry
|
|
76
|
+
if (isCollectionKind(entry)) lines.push(" createdAt: String!");
|
|
76
77
|
lines.push(" updatedAt: String!");
|
|
77
78
|
return `type ${typeName(name)} {
|
|
78
79
|
${lines.join("\n")}
|
|
@@ -171,6 +172,8 @@ export function renderGraphqlSdl(config) {
|
|
|
171
172
|
);
|
|
172
173
|
queryLines.push(` ${name}ById(id: ID!, locale: String): ${gqlType}`);
|
|
173
174
|
queryLines.push(` ${name}Count(filters: ${gqlType}Filters): Int!`);
|
|
175
|
+
if (entry.kind === "content")
|
|
176
|
+
queryLines.push(` ${name}BySlug(slug: String!, locale: String): ${gqlType}`);
|
|
174
177
|
}
|
|
175
178
|
return [
|
|
176
179
|
...COMMON_SDL,
|
|
@@ -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;
|
|
@@ -60,8 +60,25 @@ export interface MediaUsage {
|
|
|
60
60
|
title: string | null;
|
|
61
61
|
field: string;
|
|
62
62
|
}
|
|
63
|
+
export interface MediaUsageEntry {
|
|
64
|
+
collection: string;
|
|
65
|
+
label: string;
|
|
66
|
+
kind: CmsEntryKind;
|
|
67
|
+
id: string | null;
|
|
68
|
+
title: string | null;
|
|
69
|
+
fields: string[];
|
|
70
|
+
}
|
|
71
|
+
export declare function groupMediaUsage(usage: MediaUsage[]): MediaUsageEntry[];
|
|
72
|
+
export interface MediaUsageGroup {
|
|
73
|
+
key: string;
|
|
74
|
+
entries: MediaUsageEntry[];
|
|
75
|
+
}
|
|
76
|
+
export declare function mediaUsageGroups(usage: Record<string, MediaUsage[]>, keys: string[]): MediaUsageGroup[];
|
|
63
77
|
export interface BlockConfig {
|
|
64
78
|
label: string;
|
|
79
|
+
component?: string;
|
|
80
|
+
icon?: string;
|
|
81
|
+
description?: string;
|
|
65
82
|
fields: Record<string, FieldConfig>;
|
|
66
83
|
}
|
|
67
84
|
export type ConditionValue = string | number | boolean;
|
|
@@ -115,10 +132,14 @@ export declare function isMultiSelect(field: FieldConfig): boolean;
|
|
|
115
132
|
export declare function translatableFieldKeys(entry: EntryLike): string[];
|
|
116
133
|
export declare function translatableBlockFieldKeys(block: BlockConfig): string[];
|
|
117
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;
|
|
118
139
|
export declare function localizeBlock(field: FieldConfig, item: unknown, locale: string, defaultLocale: string): unknown;
|
|
119
140
|
export declare function localizeBlocks(field: FieldConfig, value: unknown, locale: string, defaultLocale: string): unknown;
|
|
120
141
|
export declare const CMS_GRAPHQL_BATCH_LIMIT = 20;
|
|
121
|
-
export type CmsEntryKind = 'collection' | 'single' | 'page';
|
|
142
|
+
export type CmsEntryKind = 'collection' | 'single' | 'page' | 'content';
|
|
122
143
|
export interface CmsPageRoute {
|
|
123
144
|
path: string;
|
|
124
145
|
key: string;
|
|
@@ -144,10 +165,35 @@ export interface CmsEntry {
|
|
|
144
165
|
icon?: string;
|
|
145
166
|
layout?: CmsLayoutBlock[];
|
|
146
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;
|
|
147
179
|
}
|
|
148
180
|
export declare const MOBILE_MEDIA_SUFFIX = "Mobile";
|
|
149
181
|
export declare function mobileMediaKey(key: string): string;
|
|
150
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;
|
|
151
197
|
export declare function entryTabs(entry: Pick<CmsEntry, 'tabs'>): CmsTab[];
|
|
152
198
|
export declare function fieldTab(field: FieldConfig, tabs: CmsTab[]): string | undefined;
|
|
153
199
|
export declare function fieldsByTab(fields: Record<string, FieldConfig>, tabs: CmsTab[]): Record<string, Record<string, FieldConfig>>;
|
|
@@ -200,6 +246,9 @@ export interface BooleanFieldInput extends FieldInputBase {
|
|
|
200
246
|
export interface DateFieldInput extends FieldInputBase {
|
|
201
247
|
type: 'date';
|
|
202
248
|
}
|
|
249
|
+
export interface DateTimeFieldInput extends FieldInputBase {
|
|
250
|
+
type: 'datetime';
|
|
251
|
+
}
|
|
203
252
|
export interface EmailFieldInput extends FieldInputBase {
|
|
204
253
|
type: 'email';
|
|
205
254
|
}
|
|
@@ -229,16 +278,19 @@ export interface RelationFieldInput extends FieldInputBase {
|
|
|
229
278
|
onDelete?: 'set null' | 'cascade' | 'restrict';
|
|
230
279
|
}
|
|
231
280
|
type BlockField<T> = Omit<T, 'private' | 'showIf' | 'tab'>;
|
|
232
|
-
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'>;
|
|
233
282
|
export interface BlockInput {
|
|
234
283
|
label: string;
|
|
284
|
+
component?: string;
|
|
285
|
+
icon?: string;
|
|
286
|
+
description?: string;
|
|
235
287
|
fields: Record<string, BlockFieldInput>;
|
|
236
288
|
}
|
|
237
289
|
export interface BlocksFieldInput extends FieldInputBase {
|
|
238
290
|
type: 'blocks';
|
|
239
291
|
blocks: Record<string, BlockInput>;
|
|
240
292
|
}
|
|
241
|
-
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;
|
|
242
294
|
interface CmsEntryInputBase {
|
|
243
295
|
id: string;
|
|
244
296
|
label: string;
|
|
@@ -269,8 +321,24 @@ export interface CmsPageInput extends CmsEntryInputBase {
|
|
|
269
321
|
overrides?: Record<string, Record<string, CmsFieldInput | null>>;
|
|
270
322
|
columns?: string[];
|
|
271
323
|
}
|
|
272
|
-
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;
|
|
273
340
|
export type CmsConfigInput = Record<string, CmsEntryInput>;
|
|
274
341
|
export declare function defineCmsConfig<T extends CmsConfigInput>(config: T): T;
|
|
342
|
+
export declare function defineCmsBlocks<T extends Record<string, BlockInput>>(blocks: T): T;
|
|
275
343
|
export * from './layout.js';
|
|
276
344
|
export * from './users.js';
|
|
@@ -110,6 +110,19 @@ export function expandMediaFolders(folders) {
|
|
|
110
110
|
}
|
|
111
111
|
return [...expanded].sort();
|
|
112
112
|
}
|
|
113
|
+
export function groupMediaUsage(usage) {
|
|
114
|
+
const entries = /* @__PURE__ */ new Map();
|
|
115
|
+
for (const { field, ...rest } of usage) {
|
|
116
|
+
const id = `${rest.collection}\0${rest.id ?? ""}`;
|
|
117
|
+
const entry = entries.get(id);
|
|
118
|
+
if (!entry) entries.set(id, { ...rest, fields: [field] });
|
|
119
|
+
else if (!entry.fields.includes(field)) entry.fields.push(field);
|
|
120
|
+
}
|
|
121
|
+
return [...entries.values()];
|
|
122
|
+
}
|
|
123
|
+
export function mediaUsageGroups(usage, keys) {
|
|
124
|
+
return [...new Set(keys)].map((key) => ({ key, entries: groupMediaUsage(usage[key] ?? []) })).filter((group) => group.entries.length > 0);
|
|
125
|
+
}
|
|
113
126
|
export function isPrivateField(field) {
|
|
114
127
|
return !!field.private;
|
|
115
128
|
}
|
|
@@ -206,6 +219,20 @@ export function hasTranslatableBlockFields(field) {
|
|
|
206
219
|
(block) => Object.values(block.fields).some(isTranslatableField)
|
|
207
220
|
);
|
|
208
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
|
+
}
|
|
209
236
|
export function localizeBlock(field, item, locale, defaultLocale) {
|
|
210
237
|
if (!item || typeof item !== "object") return item;
|
|
211
238
|
const block = field.blocks?.[String(item.type)];
|
|
@@ -276,6 +303,122 @@ export function expandMobileMedia(config) {
|
|
|
276
303
|
}
|
|
277
304
|
return config;
|
|
278
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
|
+
}
|
|
279
422
|
export function entryTabs(entry) {
|
|
280
423
|
return entry.tabs?.length ? entry.tabs : [];
|
|
281
424
|
}
|
|
@@ -379,5 +522,8 @@ export function blockTypeName(entryName, fieldKey, blockName) {
|
|
|
379
522
|
export function defineCmsConfig(config) {
|
|
380
523
|
return config;
|
|
381
524
|
}
|
|
525
|
+
export function defineCmsBlocks(blocks) {
|
|
526
|
+
return blocks;
|
|
527
|
+
}
|
|
382
528
|
export * from "./layout.js";
|
|
383
529
|
export * from "./users.js";
|