@xleddyl/nuxt-cms 0.1.26 → 0.1.28

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 (42) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +5 -1
  3. package/dist/runtime/app/components/cms/Alert.d.vue.ts +1 -2
  4. package/dist/runtime/app/components/cms/Alert.vue +2 -3
  5. package/dist/runtime/app/components/cms/Alert.vue.d.ts +1 -2
  6. package/dist/runtime/app/components/cms/BlocksField.vue +2 -2
  7. package/dist/runtime/app/components/cms/Button.d.vue.ts +1 -1
  8. package/dist/runtime/app/components/cms/Button.vue.d.ts +1 -1
  9. package/dist/runtime/app/components/cms/ConfirmModal.vue +1 -1
  10. package/dist/runtime/app/components/cms/EntryDrawer.vue +1 -1
  11. package/dist/runtime/app/components/cms/MediaField.vue +20 -18
  12. package/dist/runtime/app/components/cms/MediaFolderPicker.d.vue.ts +16 -0
  13. package/dist/runtime/app/components/cms/MediaFolderPicker.vue +65 -0
  14. package/dist/runtime/app/components/cms/MediaFolderPicker.vue.d.ts +16 -0
  15. package/dist/runtime/app/components/cms/MediaGallery.vue +213 -49
  16. package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +1 -1
  17. package/dist/runtime/app/components/cms/MediaUpload.vue +9 -3
  18. package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +1 -1
  19. package/dist/runtime/app/components/cms/Modal.d.vue.ts +4 -4
  20. package/dist/runtime/app/components/cms/Modal.vue +4 -4
  21. package/dist/runtime/app/components/cms/Modal.vue.d.ts +4 -4
  22. package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +0 -1
  23. package/dist/runtime/app/components/cms/PageHeader.vue +0 -4
  24. package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +0 -1
  25. package/dist/runtime/app/components/cms/RichTextField.vue +1 -1
  26. package/dist/runtime/app/components/cms/Table.d.vue.ts +10 -2
  27. package/dist/runtime/app/components/cms/Table.vue +30 -2
  28. package/dist/runtime/app/components/cms/Table.vue.d.ts +10 -2
  29. package/dist/runtime/app/components/cms/Toaster.vue +6 -0
  30. package/dist/runtime/app/layouts/cms-admin.vue +2 -2
  31. package/dist/runtime/app/pages/admin-collection.vue +18 -8
  32. package/dist/runtime/app/pages/admin-entry.vue +2 -2
  33. package/dist/runtime/app/pages/admin-login.vue +5 -5
  34. package/dist/runtime/assets/main.css +1 -1
  35. package/dist/runtime/server/api/collection.get.js +14 -4
  36. package/dist/runtime/server/api/media-presign.post.d.ts +1 -0
  37. package/dist/runtime/server/api/media-presign.post.js +7 -4
  38. package/dist/runtime/server/api/media.post.js +2 -1
  39. package/dist/runtime/server/api/media.put.js +2 -1
  40. package/dist/runtime/shared/index.d.ts +2 -0
  41. package/dist/runtime/shared/index.js +6 -0
  42. package/package.json +4 -2
@@ -1,4 +1,4 @@
1
- import { count, desc, sql } from "drizzle-orm";
1
+ import { asc, count, desc, sql } from "drizzle-orm";
2
2
  import { defineEventHandler, getValidatedQuery } from "h3";
3
3
  import { z } from "zod";
4
4
  import { useDb } from "#cms-db";
@@ -9,6 +9,8 @@ const querySchema = z.object({
9
9
  limit: z.coerce.number().int().min(1).max(100).default(50),
10
10
  offset: z.coerce.number().int().min(0).default(0),
11
11
  search: z.string().trim().max(200).optional(),
12
+ sort: z.string().trim().max(64).optional(),
13
+ order: z.enum(["asc", "desc"]).default("desc"),
12
14
  light: z.stringbool().default(false)
13
15
  });
14
16
  function likePattern(term) {
@@ -23,7 +25,10 @@ export default defineEventHandler(async (event) => {
23
25
  await attachManyToMany(db, name, entry, rows);
24
26
  return rows[0] ?? null;
25
27
  }
26
- const { limit, offset, search, light } = await getValidatedQuery(event, querySchema.parse);
28
+ const { limit, offset, search, sort, order, light } = await getValidatedQuery(
29
+ event,
30
+ querySchema.parse
31
+ );
27
32
  const columns = tableColumns(table);
28
33
  const titleColumn = entry.titleField && Object.hasOwn(columns, entry.titleField) ? columns[entry.titleField] : void 0;
29
34
  let where;
@@ -36,9 +41,14 @@ export default defineEventHandler(async (event) => {
36
41
  ...entry.titleField && titleColumn ? [[entry.titleField, titleColumn]] : [],
37
42
  ...entry.drafts && Object.hasOwn(columns, "status") ? [["status", columns.status]] : []
38
43
  ]) : void 0;
39
- const orderBy = columns.createdAt ?? idColumn(table);
44
+ const sortColumn = sort && Object.hasOwn(columns, sort) ? columns[sort] : void 0;
45
+ const orderBy = sortColumn ?? columns.createdAt ?? idColumn(table);
46
+ const direction = sortColumn && order === "asc" ? asc : desc;
47
+ const tiebreaker = idColumn(table);
40
48
  const base = selection ? db.select(selection).from(table) : db.select().from(table);
41
- const items = await (where ? base.where(where) : base).orderBy(desc(orderBy)).limit(limit).offset(offset);
49
+ const items = await (where ? base.where(where) : base).orderBy(
50
+ ...orderBy === tiebreaker ? [direction(orderBy)] : [direction(orderBy), desc(tiebreaker)]
51
+ ).limit(limit).offset(offset);
42
52
  const [counted] = await (where ? db.select({ total: count() }).from(table).where(where) : db.select({ total: count() }).from(table));
43
53
  if (light) return { items, total: counted?.total ?? 0, relations: {} };
44
54
  await attachManyToMany(db, name, entry, items);
@@ -1,5 +1,6 @@
1
1
  declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
2
  key: string;
3
+ folder: string | null;
3
4
  uploadUrl: string;
4
5
  method: string;
5
6
  headers: {
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createError, defineEventHandler, readValidatedBody } from "h3";
3
3
  import { z } from "zod";
4
- import { slugify } from "../../shared/index.js";
4
+ import { normalizeMediaFolder, slugify } from "../../shared/index.js";
5
5
  import { assertUploadContentType, useMediaStorage } from "../utils/media.js";
6
6
  import { requireAdmin } from "../utils/require-admin.js";
7
7
  const MAX_BASE_LENGTH = 80;
@@ -10,7 +10,8 @@ const MAX_FILE_SIZE = 10 * 1024 * 1024;
10
10
  const bodySchema = z.object({
11
11
  filename: z.string().trim().min(1).max(255),
12
12
  contentType: z.string().regex(/^[-\w.+]+\/[-\w.+]+$/, "Invalid content type"),
13
- size: z.number().int().positive()
13
+ size: z.number().int().positive(),
14
+ folder: z.string().max(255).nullish()
14
15
  });
15
16
  function slugifyFilename(filename) {
16
17
  const dot = filename.lastIndexOf(".");
@@ -21,7 +22,7 @@ function slugifyFilename(filename) {
21
22
  export default defineEventHandler(async (event) => {
22
23
  await requireAdmin(event);
23
24
  const { media, client, bucketUrl, publicUrl } = useMediaStorage(event);
24
- const { filename, contentType, size } = await readValidatedBody(event, bodySchema.parse);
25
+ const { filename, contentType, size, folder } = await readValidatedBody(event, bodySchema.parse);
25
26
  assertUploadContentType(contentType);
26
27
  if (size > MAX_FILE_SIZE) {
27
28
  throw createError({
@@ -30,7 +31,8 @@ export default defineEventHandler(async (event) => {
30
31
  });
31
32
  }
32
33
  const now = /* @__PURE__ */ new Date();
33
- const prefix = `${now.getUTCFullYear()}/${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
34
+ const normalizedFolder = normalizeMediaFolder(folder);
35
+ const prefix = normalizedFolder ?? `${now.getUTCFullYear()}/${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
34
36
  const key = `${prefix}/${randomUUID()}-${slugifyFilename(filename)}`;
35
37
  const url = new URL(`${bucketUrl}/${key}`);
36
38
  url.searchParams.set("X-Amz-Expires", String(media.presignExpiry));
@@ -43,6 +45,7 @@ export default defineEventHandler(async (event) => {
43
45
  );
44
46
  return {
45
47
  key,
48
+ folder: normalizedFolder,
46
49
  uploadUrl: signed.url,
47
50
  method: "PUT",
48
51
  headers: { "content-type": contentType },
@@ -2,6 +2,7 @@ import { defineEventHandler, readValidatedBody } from "h3";
2
2
  import { z } from "zod";
3
3
  import { useDb } from "#cms-db";
4
4
  import { cms_media } from "#cms-tables";
5
+ import { normalizeMediaFolder } from "../../shared/index.js";
5
6
  import { objectKeySchema } from "../../shared/validation.js";
6
7
  import {
7
8
  assertMediaWritable,
@@ -32,7 +33,7 @@ export default defineEventHandler(async (event) => {
32
33
  width: body.width ?? null,
33
34
  height: body.height ?? null,
34
35
  alt: body.alt ?? null,
35
- folder: body.folder ?? null
36
+ folder: normalizeMediaFolder(body.folder)
36
37
  };
37
38
  const [row] = await useDb().insert(cms_media).values(values).onConflictDoUpdate({ target: cms_media.key, set: values }).returning();
38
39
  return toMediaItem(row, publicUrl);
@@ -3,6 +3,7 @@ import { createError, defineEventHandler, readValidatedBody } from "h3";
3
3
  import { z } from "zod";
4
4
  import { useDb } from "#cms-db";
5
5
  import { cms_media } from "#cms-tables";
6
+ import { normalizeMediaFolder } from "../../shared/index.js";
6
7
  import { assertMediaWritable, toMediaItem, useMediaConfig } from "../utils/media.js";
7
8
  import { requireAdmin } from "../utils/require-admin.js";
8
9
  const bodySchema = z.object({
@@ -17,7 +18,7 @@ export default defineEventHandler(async (event) => {
17
18
  const body = await readValidatedBody(event, bodySchema.parse);
18
19
  const updates = {};
19
20
  if (body.alt !== void 0) updates.alt = body.alt;
20
- if (body.folder !== void 0) updates.folder = body.folder;
21
+ if (body.folder !== void 0) updates.folder = normalizeMediaFolder(body.folder);
21
22
  if (Object.keys(updates).length === 0) {
22
23
  throw createError({ statusCode: 400, statusMessage: "No fields to update" });
23
24
  }
@@ -15,6 +15,8 @@ export declare function mediaTypeFor(mime: string | null | undefined, key: strin
15
15
  export declare function mediaIconFor(type: MediaType): string;
16
16
  export declare function mediaPublicUrl(baseUrl: string | null | undefined, key: string): string | null;
17
17
  export declare function slugify(value: string): string;
18
+ export declare const MEDIA_FOLDER_MAX_DEPTH = 4;
19
+ export declare function normalizeMediaFolder(value: string | null | undefined): string | null;
18
20
  export interface MediaItem {
19
21
  id: number;
20
22
  key: string;
@@ -44,6 +44,12 @@ export function mediaPublicUrl(baseUrl, key) {
44
44
  export function slugify(value) {
45
45
  return value.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036F]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
46
46
  }
47
+ export const MEDIA_FOLDER_MAX_DEPTH = 4;
48
+ export function normalizeMediaFolder(value) {
49
+ if (!value) return null;
50
+ const segments = value.split("/").map(slugify).filter(Boolean).slice(0, MEDIA_FOLDER_MAX_DEPTH);
51
+ return segments.length ? segments.join("/") : null;
52
+ }
47
53
  export function isTranslatableField(field) {
48
54
  return !!field.translatable && (field.type === "text" || field.type === "richtext");
49
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xleddyl/nuxt-cms",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
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)",
@@ -60,12 +60,13 @@
60
60
  "dist"
61
61
  ],
62
62
  "dependencies": {
63
+ "@fontsource-variable/hanken-grotesk": "^5.3.0",
64
+ "@fontsource/fragment-mono": "^5.3.0",
63
65
  "@libsql/client": "^0.17.4",
64
66
  "@nuxt/kit": "^4.4.8",
65
67
  "@tailwindcss/vite": "^4.3.2",
66
68
  "@tiptap/starter-kit": "^3.0.0",
67
69
  "@tiptap/vue-3": "^3.0.0",
68
- "tailwindcss": "^4.3.2",
69
70
  "aws4fetch": "^1.0.20",
70
71
  "better-sqlite3": "^12.2.0",
71
72
  "drizzle-kit": "^0.31.0",
@@ -76,6 +77,7 @@
76
77
  "jiti": "^2.7.0",
77
78
  "nuxt-auth-utils": "^0.5.29",
78
79
  "pg": "^8.16.0",
80
+ "tailwindcss": "^4.3.2",
79
81
  "vite-svg-loader": "^5.1.0",
80
82
  "zod": "^4.0.0"
81
83
  },