@xleddyl/nuxt-cms 0.2.2 → 0.3.0

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.
Files changed (92) hide show
  1. package/README.md +4 -2
  2. package/dist/module.d.mts +4 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +329 -32
  5. package/dist/runtime/app/blocks/CmsBlocks.d.vue.ts +45 -0
  6. package/dist/runtime/app/blocks/CmsBlocks.vue +36 -0
  7. package/dist/runtime/app/blocks/CmsBlocks.vue.d.ts +45 -0
  8. package/dist/runtime/app/blocks/resolve.d.ts +21 -0
  9. package/dist/runtime/app/blocks/resolve.js +37 -0
  10. package/dist/runtime/app/components/cms/BlockOutline.d.vue.ts +17 -0
  11. package/dist/runtime/app/components/cms/BlockOutline.vue +99 -0
  12. package/dist/runtime/app/components/cms/BlockOutline.vue.d.ts +17 -0
  13. package/dist/runtime/app/components/cms/BlockPalette.d.vue.ts +12 -0
  14. package/dist/runtime/app/components/cms/BlockPalette.vue +29 -0
  15. package/dist/runtime/app/components/cms/BlockPalette.vue.d.ts +12 -0
  16. package/dist/runtime/app/components/cms/BlocksField.d.vue.ts +2 -0
  17. package/dist/runtime/app/components/cms/BlocksField.vue +83 -3
  18. package/dist/runtime/app/components/cms/BlocksField.vue.d.ts +2 -0
  19. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +2 -0
  20. package/dist/runtime/app/components/cms/EntryForm.vue +43 -7
  21. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +2 -0
  22. package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +2 -0
  23. package/dist/runtime/app/components/cms/FieldInput.vue +18 -1
  24. package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +2 -0
  25. package/dist/runtime/app/components/cms/MediaDetail.vue +25 -13
  26. package/dist/runtime/app/components/cms/Search.d.vue.ts +7 -0
  27. package/dist/runtime/app/components/cms/Search.vue +347 -0
  28. package/dist/runtime/app/components/cms/Search.vue.d.ts +7 -0
  29. package/dist/runtime/app/components/cms/SettingsEntry.vue +3 -2
  30. package/dist/runtime/app/components/cms/SettingsModal.vue +2 -1
  31. package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +2 -0
  32. package/dist/runtime/app/components/cms/StatusBadge.vue +9 -3
  33. package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +2 -0
  34. package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +39 -0
  35. package/dist/runtime/app/components/cms/VisualEditor.vue +553 -0
  36. package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +39 -0
  37. package/dist/runtime/app/composables/cms-entry-disabled.d.ts +7 -1
  38. package/dist/runtime/app/composables/cms-entry-disabled.js +15 -0
  39. package/dist/runtime/app/composables/cms-entry.d.ts +7 -1
  40. package/dist/runtime/app/composables/cms-entry.js +51 -1
  41. package/dist/runtime/app/composables/cms-field-focus.d.ts +4 -0
  42. package/dist/runtime/app/composables/cms-field-focus.js +20 -0
  43. package/dist/runtime/app/composables/cms-runtime.d.ts +1 -0
  44. package/dist/runtime/app/composables/cms-settings.js +8 -2
  45. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
  46. package/dist/runtime/app/layouts/cms-admin.vue +7 -0
  47. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
  48. package/dist/runtime/app/layouts/cms-editor.d.vue.ts +13 -0
  49. package/dist/runtime/app/layouts/cms-editor.vue +14 -0
  50. package/dist/runtime/app/layouts/cms-editor.vue.d.ts +13 -0
  51. package/dist/runtime/app/pages/admin-collection.vue +36 -4
  52. package/dist/runtime/app/pages/admin-content.d.vue.ts +3 -0
  53. package/dist/runtime/app/pages/admin-content.vue +287 -0
  54. package/dist/runtime/app/pages/admin-content.vue.d.ts +3 -0
  55. package/dist/runtime/app/pages/admin-entry.vue +3 -0
  56. package/dist/runtime/app/pages/admin-preview.d.vue.ts +3 -0
  57. package/dist/runtime/app/pages/admin-preview.vue +132 -0
  58. package/dist/runtime/app/pages/admin-preview.vue.d.ts +3 -0
  59. package/dist/runtime/app/preview/PreviewOverlay.d.vue.ts +15 -0
  60. package/dist/runtime/app/preview/PreviewOverlay.vue +362 -0
  61. package/dist/runtime/app/preview/PreviewOverlay.vue.d.ts +15 -0
  62. package/dist/runtime/app/utils/ui.js +1 -0
  63. package/dist/runtime/assets/main.css +1 -1
  64. package/dist/runtime/server/api/collection.get.js +4 -5
  65. package/dist/runtime/server/api/collection.post.js +4 -1
  66. package/dist/runtime/server/api/item.delete.js +2 -1
  67. package/dist/runtime/server/api/item.put.js +2 -1
  68. package/dist/runtime/server/api/media-download.get.d.ts +2 -0
  69. package/dist/runtime/server/api/media-download.get.js +90 -0
  70. package/dist/runtime/server/api/search.get.d.ts +3 -0
  71. package/dist/runtime/server/api/search.get.js +24 -0
  72. package/dist/runtime/server/utils/graphql.js +23 -8
  73. package/dist/runtime/server/utils/media-download.d.ts +1 -0
  74. package/dist/runtime/server/utils/media-download.js +8 -0
  75. package/dist/runtime/server/utils/media.d.ts +1 -0
  76. package/dist/runtime/server/utils/media.js +18 -3
  77. package/dist/runtime/server/utils/registry.d.ts +1 -0
  78. package/dist/runtime/server/utils/registry.js +6 -0
  79. package/dist/runtime/server/utils/search.d.ts +20 -0
  80. package/dist/runtime/server/utils/search.js +233 -0
  81. package/dist/runtime/server/utils/visibility.d.ts +4 -0
  82. package/dist/runtime/server/utils/visibility.js +12 -0
  83. package/dist/runtime/shared/graphql-sdl.js +4 -1
  84. package/dist/runtime/shared/index.d.ts +59 -5
  85. package/dist/runtime/shared/index.js +133 -0
  86. package/dist/runtime/shared/preview.d.ts +85 -0
  87. package/dist/runtime/shared/preview.js +219 -0
  88. package/dist/runtime/shared/search.d.ts +52 -0
  89. package/dist/runtime/shared/search.js +207 -0
  90. package/dist/runtime/shared/validation.d.ts +2 -0
  91. package/dist/runtime/shared/validation.js +15 -2
  92. package/package.json +2 -2
@@ -0,0 +1,207 @@
1
+ import { entryTabs, fieldTab, pageAllFields, pageRoutes } from "./index.js";
2
+ export const CMS_SEARCH_MIN_LENGTH = 2;
3
+ export const CMS_SEARCH_MAX_LENGTH = 200;
4
+ export const CMS_SEARCH_LABEL_LIMIT = 20;
5
+ export const CMS_SEARCH_VALUE_LIMIT = 50;
6
+ export const CMS_SEARCH_ROWS_PER_ENTRY = 10;
7
+ export const CMS_SEARCH_HITS_PER_ROW = 5;
8
+ export const CMS_SEARCH_PATH_SEPARATOR = " \u203A ";
9
+ const SNIPPET_BEFORE = 40;
10
+ const SNIPPET_AFTER = 90;
11
+ const ENTITIES = {
12
+ amp: "&",
13
+ lt: "<",
14
+ gt: ">",
15
+ quot: '"',
16
+ apos: "'",
17
+ nbsp: " "
18
+ };
19
+ export function normalizeSearchQuery(value) {
20
+ return value.replace(/\s+/g, " ").trim().slice(0, CMS_SEARCH_MAX_LENGTH);
21
+ }
22
+ export function isSearchableQuery(value) {
23
+ return normalizeSearchQuery(value).length >= CMS_SEARCH_MIN_LENGTH;
24
+ }
25
+ export function searchLikePattern(term) {
26
+ return `%${term.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
27
+ }
28
+ export function decodeHtmlEntities(value) {
29
+ return value.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (entity, code) => {
30
+ if (code[0] === "#") {
31
+ const point = code[1] === "x" || code[1] === "X" ? Number.parseInt(code.slice(2), 16) : Number.parseInt(code.slice(1), 10);
32
+ return Number.isFinite(point) && point > 0 && point <= 1114111 ? String.fromCodePoint(point) : entity;
33
+ }
34
+ return ENTITIES[code.toLowerCase()] ?? entity;
35
+ });
36
+ }
37
+ export function stripMarkup(html) {
38
+ return decodeHtmlEntities(
39
+ html.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, " ").replace(/<\/?(p|div|li|ul|ol|h[1-6]|blockquote|pre|br|hr|tr|td|th)\b[^>]*>/gi, " ").replace(/<[^>]*>/g, "")
40
+ );
41
+ }
42
+ export function searchSnippet(text, term) {
43
+ const normalized = text.replace(/\s+/g, " ").trim();
44
+ const needle = normalizeSearchQuery(term).toLowerCase();
45
+ if (!needle) return null;
46
+ const index = normalized.toLowerCase().indexOf(needle);
47
+ if (index === -1) return null;
48
+ const end = index + needle.length;
49
+ const from = Math.max(0, index - SNIPPET_BEFORE);
50
+ const to = Math.min(normalized.length, end + SNIPPET_AFTER);
51
+ return {
52
+ before: `${from > 0 ? "\u2026" : ""}${normalized.slice(from, index).trimStart()}`,
53
+ match: normalized.slice(index, end),
54
+ after: `${normalized.slice(end, to).trimEnd()}${to < normalized.length ? "\u2026" : ""}`
55
+ };
56
+ }
57
+ export function searchFieldPath(...labels) {
58
+ return labels.join(CMS_SEARCH_PATH_SEPARATOR);
59
+ }
60
+ export function cmsSearchLink(path, focus = {}) {
61
+ const params = new URLSearchParams();
62
+ if (focus.field) params.set("field", focus.field);
63
+ if (focus.locale) params.set("locale", focus.locale);
64
+ if (focus.block != null) params.set("block", String(focus.block));
65
+ if (focus.tab) params.set("tab", focus.tab);
66
+ const query = params.toString();
67
+ return query ? `${path}?${query}` : path;
68
+ }
69
+ export function readSearchFocus(query) {
70
+ const text = (value) => {
71
+ const first = Array.isArray(value) ? value[0] : value;
72
+ return typeof first === "string" && first ? first : void 0;
73
+ };
74
+ const field = text(query.field);
75
+ const tab = text(query.tab);
76
+ if (!field && !tab) return null;
77
+ const block = Number.parseInt(text(query.block) ?? "", 10);
78
+ return {
79
+ ...field ? { field } : {},
80
+ ...tab ? { tab } : {},
81
+ locale: text(query.locale) ?? null,
82
+ block: Number.isInteger(block) && block >= 0 ? block : null
83
+ };
84
+ }
85
+ function entryPath(name) {
86
+ return `/cms/${name}`;
87
+ }
88
+ function fieldLink(name, entry, key, overridePaths) {
89
+ if (entry.kind === "single") {
90
+ return { to: cmsSearchLink(entryPath(name), { field: key }), id: null, title: null };
91
+ }
92
+ if (entry.kind === "page" && !Object.hasOwn(entry.fields, key) && overridePaths.length === 1) {
93
+ const route = pageRoutes(entry).find((candidate) => candidate.path === overridePaths[0]);
94
+ if (route) {
95
+ return {
96
+ to: cmsSearchLink(`${entryPath(name)}/${route.key}`, { field: key }),
97
+ id: route.key,
98
+ title: route.label
99
+ };
100
+ }
101
+ }
102
+ return { to: entryPath(name), id: null, title: null };
103
+ }
104
+ function overridePathsOf(entry, key) {
105
+ return Object.entries(entry.overrides ?? {}).filter(([, fields]) => !!fields[key]).map(([path]) => path);
106
+ }
107
+ function blockCandidates(key, field, link) {
108
+ const candidates = [];
109
+ for (const block of Object.values(field.blocks ?? {})) {
110
+ candidates.push({
111
+ type: "block",
112
+ ...link,
113
+ field: key,
114
+ fieldLabel: searchFieldPath(field.label, block.label),
115
+ texts: [block.label]
116
+ });
117
+ for (const blockField of Object.values(block.fields)) {
118
+ candidates.push({
119
+ type: "field",
120
+ ...link,
121
+ field: key,
122
+ fieldLabel: searchFieldPath(field.label, block.label, blockField.label),
123
+ texts: [blockField.label]
124
+ });
125
+ }
126
+ }
127
+ return candidates;
128
+ }
129
+ function labelCandidates(name, entry) {
130
+ const candidates = [
131
+ { type: "entry", texts: [entry.label], to: entryPath(name) }
132
+ ];
133
+ if (entry.kind === "page") {
134
+ for (const route of pageRoutes(entry)) {
135
+ candidates.push({
136
+ type: "page",
137
+ id: route.key,
138
+ title: route.label,
139
+ texts: [route.label, route.path],
140
+ to: `${entryPath(name)}/${route.key}`
141
+ });
142
+ }
143
+ }
144
+ for (const tab of entryTabs(entry)) {
145
+ candidates.push({
146
+ type: "tab",
147
+ fieldLabel: tab.label,
148
+ texts: [tab.label],
149
+ to: entry.kind === "single" ? cmsSearchLink(entryPath(name), { tab: tab.id }) : entryPath(name)
150
+ });
151
+ }
152
+ const fields = entry.kind === "page" ? pageAllFields(entry) : entry.fields;
153
+ const tabs = entryTabs(entry);
154
+ for (const [key, field] of Object.entries(fields)) {
155
+ const link = fieldLink(name, entry, key, overridePathsOf(entry, key));
156
+ candidates.push({
157
+ type: "field",
158
+ ...link,
159
+ field: key,
160
+ fieldLabel: tabs.length ? searchFieldPath(
161
+ tabs.find((tab) => tab.id === fieldTab(field, tabs))?.label ?? "",
162
+ field.label
163
+ ) : field.label,
164
+ texts: [field.label]
165
+ });
166
+ if (field.type === "blocks") candidates.push(...blockCandidates(key, field, link));
167
+ }
168
+ return candidates;
169
+ }
170
+ export function searchCmsLabels(config, query, limit = CMS_SEARCH_LABEL_LIMIT) {
171
+ const term = normalizeSearchQuery(query);
172
+ if (term.length < CMS_SEARCH_MIN_LENGTH) return { hits: [], truncated: false };
173
+ const needle = term.toLowerCase();
174
+ const scored = [];
175
+ const seen = /* @__PURE__ */ new Set();
176
+ for (const [name, entry] of Object.entries(config)) {
177
+ for (const candidate of labelCandidates(name, entry)) {
178
+ const text = candidate.texts.find((value) => value.toLowerCase().includes(needle));
179
+ if (!text) continue;
180
+ const snippet = searchSnippet(text, term);
181
+ if (!snippet) continue;
182
+ const identity = [candidate.type, name, candidate.fieldLabel ?? "", candidate.to].join(
183
+ "\0"
184
+ );
185
+ if (seen.has(identity)) continue;
186
+ seen.add(identity);
187
+ scored.push({
188
+ score: text.toLowerCase().startsWith(needle) ? 0 : 1,
189
+ hit: {
190
+ type: candidate.type,
191
+ collection: name,
192
+ entryLabel: entry.label,
193
+ kind: entry.kind,
194
+ id: candidate.id ?? null,
195
+ title: candidate.title ?? null,
196
+ field: candidate.field ?? null,
197
+ fieldLabel: candidate.fieldLabel ?? null,
198
+ locale: null,
199
+ snippet,
200
+ to: candidate.to
201
+ }
202
+ });
203
+ }
204
+ }
205
+ const sorted = scored.map((item, index) => ({ ...item, index })).sort((a, b) => a.score - b.score || a.index - b.index).map((item) => item.hit);
206
+ return { hits: sorted.slice(0, limit), truncated: sorted.length > limit };
207
+ }
@@ -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()).refine((v) => Object.keys(v).every((k) => locales.includes(k)), m.unknownLocale);
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.2",
3
+ "version": "0.3.0",
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)",
@@ -124,6 +124,6 @@
124
124
  "test:docker": "vitest run --config test/docker/vitest.config.ts",
125
125
  "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
126
126
  "format": "prettier --write \"**/*.{ts,tsx,js,jsx,md,html,json,sql,vue}\" --log-level error",
127
- "release": "sh -c 'changelogen --release --no-github ${1:+--$1} && git push origin main --follow-tags' --"
127
+ "release": "node scripts/release.mjs"
128
128
  }
129
129
  }