@xleddyl/nuxt-cms 0.2.1 → 0.2.2

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 (52) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +1 -1
  3. package/dist/runtime/app/components/cms/BrandMark.vue +2 -3
  4. package/dist/runtime/app/components/cms/ConfirmModal.vue +14 -1
  5. package/dist/runtime/app/components/cms/MediaGallery.vue +89 -11
  6. package/dist/runtime/app/components/cms/Modal.vue +0 -2
  7. package/dist/runtime/app/composables/cms-confirm.d.ts +16 -7
  8. package/dist/runtime/app/composables/cms-confirm.js +1 -0
  9. package/dist/runtime/app/composables/cms-media-library.d.ts +1 -2
  10. package/dist/runtime/app/composables/cms-media-library.js +12 -19
  11. package/dist/runtime/app/utils/ui.d.ts +0 -2
  12. package/dist/runtime/assets/main.css +1 -1
  13. package/dist/runtime/seed.js +11 -1
  14. package/dist/runtime/server/api/collection.post.js +20 -14
  15. package/dist/runtime/server/api/collection.put.js +12 -12
  16. package/dist/runtime/server/api/item.put.js +14 -13
  17. package/dist/runtime/server/api/media-folder.patch.js +10 -16
  18. package/dist/runtime/server/api/media-move.post.js +12 -8
  19. package/dist/runtime/server/api/media.delete.js +1 -1
  20. package/dist/runtime/server/api/media.post.js +2 -1
  21. package/dist/runtime/server/api/media.put.js +2 -1
  22. package/dist/runtime/server/plugins/migrate-sqlite.js +2 -2
  23. package/dist/runtime/server/routes/auth/login.post.js +28 -22
  24. package/dist/runtime/server/utils/db-d1.d.ts +0 -1
  25. package/dist/runtime/server/utils/db-d1.js +3 -4
  26. package/dist/runtime/server/utils/db-libsql.d.ts +0 -1
  27. package/dist/runtime/server/utils/db-libsql.js +3 -4
  28. package/dist/runtime/server/utils/db-postgres.d.ts +0 -1
  29. package/dist/runtime/server/utils/db-postgres.js +1 -4
  30. package/dist/runtime/server/utils/db-sqlite.d.ts +0 -1
  31. package/dist/runtime/server/utils/db-sqlite.js +4 -10
  32. package/dist/runtime/server/utils/graphql.js +2 -1
  33. package/dist/runtime/server/utils/libsql-migrations.d.ts +1 -0
  34. package/dist/runtime/server/utils/libsql-migrations.js +1 -1
  35. package/dist/runtime/server/utils/media-folders.js +14 -23
  36. package/dist/runtime/server/utils/media.js +2 -0
  37. package/dist/runtime/server/utils/migrate.d.ts +1 -0
  38. package/dist/runtime/server/utils/migrate.js +32 -0
  39. package/dist/runtime/server/utils/registry.d.ts +3 -0
  40. package/dist/runtime/server/utils/registry.js +10 -1
  41. package/dist/runtime/server/utils/relations.d.ts +1 -1
  42. package/dist/runtime/server/utils/relations.js +6 -9
  43. package/dist/runtime/server/utils/timestamps.d.ts +12 -0
  44. package/dist/runtime/server/utils/timestamps.js +48 -0
  45. package/dist/runtime/server/utils/users.js +3 -2
  46. package/dist/runtime/server/utils/write-statements.d.ts +33 -0
  47. package/dist/runtime/server/utils/write-statements.js +75 -0
  48. package/dist/runtime/shared/index.d.ts +14 -0
  49. package/dist/runtime/shared/index.js +13 -0
  50. package/dist/runtime/shared/timestamps.d.ts +4 -0
  51. package/dist/runtime/shared/timestamps.js +24 -0
  52. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xleddyl/nuxt-cms",
3
3
  "configKey": "cms",
4
- "version": "0.2.1",
4
+ "version": "0.2.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1126,7 +1126,7 @@ function addCmsTypeTemplates(nuxt, cmsConfig) {
1126
1126
  ``,
1127
1127
  `declare const graphql: CmsGraphql`,
1128
1128
  ``,
1129
- `// @ts-ignore: gql.tada's cache overload rejects this instantiation, but the parse overload still resolves`,
1129
+ `// @ts-ignore`,
1130
1130
  `export type CmsDocument<Query extends string> = ReturnType<typeof graphql<Query, []>>`,
1131
1131
  ``,
1132
1132
  `export type CmsResult<Query extends string> = string extends Query`,
@@ -2,12 +2,11 @@
2
2
  <span class="cms-brand-mark" :class="{ 'is-lg': size === 'lg' }" aria-hidden="true">
3
3
  <svg viewBox="0 0 16 16" fill="none">
4
4
  <path
5
- d="M4.25 12.25V7.5a3.75 3.75 0 0 1 7.5 0v4.75"
5
+ d="M4.5 5.5h7M4.5 8h7M4.5 10.5h4"
6
6
  stroke="currentColor"
7
- stroke-width="2"
7
+ stroke-width="1.5"
8
8
  stroke-linecap="round"
9
9
  />
10
- <circle cx="8" cy="12.25" r="1.25" fill="currentColor" />
11
10
  </svg>
12
11
  </span>
13
12
  </template>
@@ -2,12 +2,25 @@
2
2
  <CmsModal
3
3
  v-model:open="state.open"
4
4
  :title="state.title ?? 'Are you sure?'"
5
- size="sm"
5
+ :size="state.groups?.length ? void 0 : 'sm'"
6
6
  @after:leave="finish(false)"
7
7
  >
8
8
  <template #body>
9
9
  <div class="cms-form">
10
10
  <p class="text-sm">{{ state.message }}</p>
11
+ <div v-if="state.groups?.length" class="cms-confirm-groups">
12
+ <section v-for="group in state.groups" :key="group.title" class="cms-confirm-group">
13
+ <h3 class="cms-confirm-group-title" :title="group.title">{{ group.title }}</h3>
14
+ <ul class="cms-media-usage-list">
15
+ <li v-for="link in group.links" :key="link.to + link.meta">
16
+ <NuxtLink :to="link.to" class="cms-media-usage-link" @click="finish(false)">
17
+ <span class="cms-media-usage-title">{{ link.title }}</span>
18
+ <span class="cms-media-usage-meta">{{ link.meta }}</span>
19
+ </NuxtLink>
20
+ </li>
21
+ </ul>
22
+ </section>
23
+ </div>
11
24
  <div class="cms-actions is-end">
12
25
  <CmsButton label="Cancel" variant="soft" color="neutral" @click="finish(false)" />
13
26
  <CmsButton
@@ -336,6 +336,19 @@
336
336
  <div v-if="query && item.folder" class="cms-media-folder">
337
337
  <CmsIcon name="folder" class="size-3" />{{ item.folder }}
338
338
  </div>
339
+ <span
340
+ v-if="!selectable"
341
+ class="cms-media-usage-badge"
342
+ :class="{ 'is-unused': usageCount(item.key) === 0 }"
343
+ >
344
+ <template v-if="usageCount(item.key) !== null">
345
+ <CmsIcon
346
+ :name="usageCount(item.key) ? 'link' : 'link-slash'"
347
+ class="size-3"
348
+ />{{ usageLabel(item.key) }}
349
+ </template>
350
+ <template v-else>&nbsp;</template>
351
+ </span>
339
352
  </div>
340
353
  </div>
341
354
  </div>
@@ -378,6 +391,18 @@
378
391
  <CmsIcon name="folder" class="size-3" />{{ item.folder }}
379
392
  </span>
380
393
  </span>
394
+ <span v-if="!selectable" class="cms-media-row-cell is-usage">
395
+ <span
396
+ v-if="usageCount(item.key) !== null"
397
+ class="cms-media-usage-badge"
398
+ :class="{ 'is-unused': usageCount(item.key) === 0 }"
399
+ >
400
+ <CmsIcon
401
+ :name="usageCount(item.key) ? 'link' : 'link-slash'"
402
+ class="size-3"
403
+ />{{ usageLabel(item.key) }}
404
+ </span>
405
+ </span>
381
406
  <span class="cms-media-row-cell is-mono">{{ extension(item.key) }}</span>
382
407
  <span class="cms-media-row-cell">{{
383
408
  item.size != null ? formatFileSize(item.size) : ""
@@ -556,13 +581,15 @@ import {
556
581
  mediaFolderName,
557
582
  mediaFolderParent,
558
583
  mediaIconFor,
584
+ groupMediaUsage,
585
+ mediaUsageGroups,
559
586
  mediaTypeAccept,
560
587
  mediaTypeFilter,
561
588
  rebaseMediaFolder,
562
589
  slugify
563
590
  } from "#nuxt-cms";
564
591
  import { useCmsConfirm } from "../../composables/cms-confirm";
565
- import { useCmsMediaLibrary, usageSummary } from "../../composables/cms-media-library";
592
+ import { useCmsMediaLibrary, usageLink } from "../../composables/cms-media-library";
566
593
  import {
567
594
  uploadTreeFromDrop,
568
595
  uploadTreeFromFiles,
@@ -707,6 +734,35 @@ watch(visibleFiles, (list) => {
707
734
  const paged = computed(
708
735
  () => visibleFiles.value.slice((page.value - 1) * PAGE_SIZE, page.value * PAGE_SIZE)
709
736
  );
737
+ const usageByKey = ref({});
738
+ let usageRequest = 0;
739
+ watch(
740
+ () => [props.selectable, library.loading.value, paged.value],
741
+ async ([selectable, loading, items]) => {
742
+ const request = ++usageRequest;
743
+ if (selectable || loading || !items.length) return;
744
+ const usage = await library.usage(items.map((item) => item.key));
745
+ if (request !== usageRequest) return;
746
+ usageByKey.value = usage ?? {};
747
+ },
748
+ { immediate: true }
749
+ );
750
+ const usageCounts = computed(
751
+ () => new Map(
752
+ Object.entries(usageByKey.value).map(([key, list]) => [key, groupMediaUsage(list).length])
753
+ )
754
+ );
755
+ function usageCount(key) {
756
+ return usageCounts.value.get(key) ?? null;
757
+ }
758
+ function entriesLabel(count) {
759
+ return `${count} entr${count === 1 ? "y" : "ies"}`;
760
+ }
761
+ function usageLabel(key) {
762
+ const count = usageCount(key);
763
+ if (count === null) return "";
764
+ return count ? `Used in ${entriesLabel(count)}` : "Unused";
765
+ }
710
766
  const emptyState = computed(() => {
711
767
  if (query.value) {
712
768
  return {
@@ -890,6 +946,31 @@ async function renameFolder() {
890
946
  renameOpen.value = false;
891
947
  openFolder(to);
892
948
  }
949
+ async function confirmDelete(keys, question, title) {
950
+ const usage = await library.usage(keys);
951
+ const groups = usage ? mediaUsageGroups(usage, keys) : [];
952
+ if (!groups.length) {
953
+ const warning = usage ? "" : " Could not check where the files are used.";
954
+ return confirmAction(`${question}${warning}`, { title, confirmLabel: "Delete" });
955
+ }
956
+ const single = keys.length === 1;
957
+ const usedIn = single ? `This file is used in ${entriesLabel(groups[0].entries.length)}.` : `${groups.length} of ${keys.length} files are used in your content.`;
958
+ return confirmAction(
959
+ `${question} ${usedIn} Deleting ${single ? "it" : "them"} leaves broken links there.`,
960
+ {
961
+ title: single ? "File in use" : "Files in use",
962
+ confirmLabel: "Delete anyway",
963
+ groups: groups.map((group) => ({
964
+ title: group.key,
965
+ links: group.entries.map((entry) => ({
966
+ title: entry.title ?? entry.label,
967
+ meta: `${entry.label} \xB7 ${entry.fields.join(", ")}`,
968
+ to: usageLink(entry)
969
+ }))
970
+ }))
971
+ }
972
+ );
973
+ }
893
974
  async function removeFolder(path) {
894
975
  const files = library.itemsWithin(path);
895
976
  const subfolders = library.folders.value.filter(
@@ -898,10 +979,12 @@ async function removeFolder(path) {
898
979
  const contents = [];
899
980
  if (subfolders) contents.push(`${subfolders} subfolder${subfolders > 1 ? "s" : ""}`);
900
981
  if (files.length) contents.push(`${files.length} file${files.length > 1 ? "s" : ""}`);
901
- const keys = files.map((item) => item.key);
902
- const summary = usageSummary(await library.usage(keys), keys);
903
- const message = contents.length ? `Delete "${mediaFolderName(path)}" with ${contents.join(" and ")}? ${summary}` : `Delete the empty folder "${mediaFolderName(path)}"?`;
904
- if (!await confirmAction(message.trim(), { title: "Delete folder", confirmLabel: "Delete" }))
982
+ const question = contents.length ? `Delete "${mediaFolderName(path)}" with ${contents.join(" and ")}?` : `Delete the empty folder "${mediaFolderName(path)}"?`;
983
+ if (!await confirmDelete(
984
+ files.map((item) => item.key),
985
+ question,
986
+ "Delete folder"
987
+ ))
905
988
  return;
906
989
  const ok = await run(() => library.deleteFolder(path));
907
990
  if (ok && folder.value && isWithinMediaFolder(folder.value, path)) {
@@ -911,13 +994,8 @@ async function removeFolder(path) {
911
994
  async function removeItems(items) {
912
995
  if (!items.length) return;
913
996
  const keys = items.map((item) => item.key);
914
- const summary = usageSummary(await library.usage(keys), keys);
915
997
  const question = keys.length === 1 ? `Delete "${mediaFilename(keys[0])}"?` : `Delete ${keys.length} files?`;
916
- if (!await confirmAction(`${question} ${summary}`.trim(), {
917
- title: "Delete",
918
- confirmLabel: "Delete"
919
- }))
920
- return;
998
+ if (!await confirmDelete(keys, question, "Delete")) return;
921
999
  const ok = await run(() => library.deleteItems(keys));
922
1000
  if (!ok) return;
923
1001
  if (detailKey.value && keys.includes(detailKey.value)) detailKey.value = null;
@@ -1,7 +1,5 @@
1
1
  <template>
2
2
  <Teleport to="body">
3
- <!-- Wrapped in .cms-scope so the vendored Tailwind utilities still apply
4
- once teleported outside the admin layout root. -->
5
3
  <Transition name="cms-modal">
6
4
  <div v-if="open" class="cms-scope cms-overlay" @click.self="close">
7
5
  <div
@@ -1,12 +1,21 @@
1
- export interface CmsConfirmState {
2
- open: boolean;
3
- message: string;
1
+ export interface CmsConfirmLink {
2
+ title: string;
3
+ meta: string;
4
+ to: string;
5
+ }
6
+ export interface CmsConfirmGroup {
7
+ title: string;
8
+ links: CmsConfirmLink[];
9
+ }
10
+ export interface CmsConfirmOptions {
4
11
  title?: string;
5
12
  confirmLabel?: string;
13
+ groups?: CmsConfirmGroup[];
14
+ }
15
+ export interface CmsConfirmState extends CmsConfirmOptions {
16
+ open: boolean;
17
+ message: string;
6
18
  resolve?: (value: boolean) => void;
7
19
  }
8
20
  export declare function useCmsConfirmState(): import("vue").Ref<CmsConfirmState, CmsConfirmState>;
9
- export declare function useCmsConfirm(): (message: string, options?: {
10
- title?: string;
11
- confirmLabel?: string;
12
- }) => Promise<boolean>;
21
+ export declare function useCmsConfirm(): (message: string, options?: CmsConfirmOptions) => Promise<boolean>;
@@ -10,6 +10,7 @@ export function useCmsConfirm() {
10
10
  message,
11
11
  title: options?.title,
12
12
  confirmLabel: options?.confirmLabel,
13
+ groups: options?.groups,
13
14
  resolve
14
15
  };
15
16
  });
@@ -71,5 +71,4 @@ export declare function useCmsMediaLibrary(): {
71
71
  usage: (keys: string[]) => Promise<Record<string, MediaUsage[]> | null>;
72
72
  };
73
73
  export type CmsMediaLibrary = ReturnType<typeof useCmsMediaLibrary>;
74
- export declare function usageSummary(usage: Record<string, MediaUsage[]> | null, keys: string[]): string;
75
- export declare function usageLink(usage: MediaUsage): string;
74
+ export declare function usageLink(usage: Pick<MediaUsage, 'collection' | 'id'>): string;
@@ -9,6 +9,7 @@ import { errorMessage } from "../utils/ui.js";
9
9
  import { invalidateCmsMediaKeys } from "./cms-media-keys.js";
10
10
  import { useCmsToast } from "./cms-toast.js";
11
11
  const endpoint = "/api/cms/admin/media";
12
+ const USAGE_CHUNK = 1e3;
12
13
  export function useCmsMediaLibrary() {
13
14
  const toast = useCmsToast();
14
15
  const items = ref([]);
@@ -106,13 +107,18 @@ export function useCmsMediaLibrary() {
106
107
  });
107
108
  }
108
109
  async function usage(keys) {
109
- if (!keys.length) return {};
110
+ const unique = [...new Set(keys)];
111
+ if (!unique.length) return {};
110
112
  try {
111
- const result = await $fetch(`${endpoint}/usage`, {
112
- method: "POST",
113
- body: { keys }
114
- });
115
- return result.usage;
113
+ const merged = {};
114
+ for (let index = 0; index < unique.length; index += USAGE_CHUNK) {
115
+ const result = await $fetch(
116
+ `${endpoint}/usage`,
117
+ { method: "POST", body: { keys: unique.slice(index, index + USAGE_CHUNK) } }
118
+ );
119
+ Object.assign(merged, result.usage);
120
+ }
121
+ return merged;
116
122
  } catch {
117
123
  return null;
118
124
  }
@@ -136,19 +142,6 @@ export function useCmsMediaLibrary() {
136
142
  usage
137
143
  };
138
144
  }
139
- export function usageSummary(usage, keys) {
140
- if (!usage) return "";
141
- const entries = /* @__PURE__ */ new Set();
142
- let used = 0;
143
- for (const key of keys) {
144
- const list = usage[key] ?? [];
145
- if (list.length) used++;
146
- for (const item of list) entries.add(`${item.collection}:${item.id ?? ""}`);
147
- }
148
- if (!used) return "";
149
- const files = keys.length === 1 ? "It is" : `${used} of them are`;
150
- return `${files} used in ${entries.size} entr${entries.size === 1 ? "y" : "ies"}, which will keep a broken link.`;
151
- }
152
145
  export function usageLink(usage) {
153
146
  return usage.id === null ? `/cms/${usage.collection}` : `/cms/${usage.collection}/${usage.id}`;
154
147
  }
@@ -1,7 +1,5 @@
1
1
  import type { InjectionKey, Reactive } from 'vue';
2
2
  import type { FieldConfig } from '#nuxt-cms';
3
- /** Shared reactive map of field-name -> error message, provided by CmsForm and
4
- * read by CmsFormField to render inline validation errors. */
5
3
  export declare const CMS_FORM_ERRORS: InjectionKey<Reactive<Record<string, string>>>;
6
4
  export declare function errorMessage(error: unknown): string | undefined;
7
5
  export declare function fieldIcon(field: FieldConfig): string;