@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.
Files changed (110) hide show
  1. package/README.md +3 -1
  2. package/dist/module.d.mts +4 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +320 -33
  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/BrandMark.vue +2 -3
  20. package/dist/runtime/app/components/cms/ConfirmModal.vue +14 -1
  21. package/dist/runtime/app/components/cms/EntryForm.vue +9 -4
  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/MediaGallery.vue +89 -11
  26. package/dist/runtime/app/components/cms/Modal.vue +0 -2
  27. package/dist/runtime/app/components/cms/SettingsEntry.vue +3 -2
  28. package/dist/runtime/app/components/cms/SettingsModal.vue +2 -1
  29. package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +2 -0
  30. package/dist/runtime/app/components/cms/StatusBadge.vue +9 -3
  31. package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +2 -0
  32. package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +37 -0
  33. package/dist/runtime/app/components/cms/VisualEditor.vue +533 -0
  34. package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +37 -0
  35. package/dist/runtime/app/composables/cms-confirm.d.ts +16 -7
  36. package/dist/runtime/app/composables/cms-confirm.js +1 -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-media-library.d.ts +1 -2
  42. package/dist/runtime/app/composables/cms-media-library.js +12 -19
  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.vue +5 -0
  46. package/dist/runtime/app/layouts/cms-editor.d.vue.ts +13 -0
  47. package/dist/runtime/app/layouts/cms-editor.vue +14 -0
  48. package/dist/runtime/app/layouts/cms-editor.vue.d.ts +13 -0
  49. package/dist/runtime/app/pages/admin-collection.vue +33 -4
  50. package/dist/runtime/app/pages/admin-content.d.vue.ts +3 -0
  51. package/dist/runtime/app/pages/admin-content.vue +284 -0
  52. package/dist/runtime/app/pages/admin-content.vue.d.ts +3 -0
  53. package/dist/runtime/app/pages/admin-preview.d.vue.ts +3 -0
  54. package/dist/runtime/app/pages/admin-preview.vue +132 -0
  55. package/dist/runtime/app/pages/admin-preview.vue.d.ts +3 -0
  56. package/dist/runtime/app/preview/PreviewOverlay.d.vue.ts +15 -0
  57. package/dist/runtime/app/preview/PreviewOverlay.vue +362 -0
  58. package/dist/runtime/app/preview/PreviewOverlay.vue.d.ts +15 -0
  59. package/dist/runtime/app/utils/ui.d.ts +0 -2
  60. package/dist/runtime/app/utils/ui.js +1 -0
  61. package/dist/runtime/assets/main.css +1 -1
  62. package/dist/runtime/seed.js +11 -1
  63. package/dist/runtime/server/api/collection.get.js +3 -2
  64. package/dist/runtime/server/api/collection.post.js +24 -15
  65. package/dist/runtime/server/api/collection.put.js +12 -12
  66. package/dist/runtime/server/api/item.delete.js +2 -1
  67. package/dist/runtime/server/api/item.put.js +16 -14
  68. package/dist/runtime/server/api/media-folder.patch.js +10 -16
  69. package/dist/runtime/server/api/media-move.post.js +12 -8
  70. package/dist/runtime/server/api/media.delete.js +1 -1
  71. package/dist/runtime/server/api/media.post.js +2 -1
  72. package/dist/runtime/server/api/media.put.js +2 -1
  73. package/dist/runtime/server/plugins/migrate-sqlite.js +2 -2
  74. package/dist/runtime/server/routes/auth/login.post.js +28 -22
  75. package/dist/runtime/server/utils/db-d1.d.ts +0 -1
  76. package/dist/runtime/server/utils/db-d1.js +3 -4
  77. package/dist/runtime/server/utils/db-libsql.d.ts +0 -1
  78. package/dist/runtime/server/utils/db-libsql.js +3 -4
  79. package/dist/runtime/server/utils/db-postgres.d.ts +0 -1
  80. package/dist/runtime/server/utils/db-postgres.js +1 -4
  81. package/dist/runtime/server/utils/db-sqlite.d.ts +0 -1
  82. package/dist/runtime/server/utils/db-sqlite.js +4 -10
  83. package/dist/runtime/server/utils/graphql.js +25 -9
  84. package/dist/runtime/server/utils/libsql-migrations.d.ts +1 -0
  85. package/dist/runtime/server/utils/libsql-migrations.js +1 -1
  86. package/dist/runtime/server/utils/media-folders.js +14 -23
  87. package/dist/runtime/server/utils/media.js +2 -0
  88. package/dist/runtime/server/utils/migrate.d.ts +1 -0
  89. package/dist/runtime/server/utils/migrate.js +32 -0
  90. package/dist/runtime/server/utils/registry.d.ts +4 -0
  91. package/dist/runtime/server/utils/registry.js +16 -1
  92. package/dist/runtime/server/utils/relations.d.ts +1 -1
  93. package/dist/runtime/server/utils/relations.js +6 -9
  94. package/dist/runtime/server/utils/timestamps.d.ts +12 -0
  95. package/dist/runtime/server/utils/timestamps.js +48 -0
  96. package/dist/runtime/server/utils/users.js +3 -2
  97. package/dist/runtime/server/utils/visibility.d.ts +4 -0
  98. package/dist/runtime/server/utils/visibility.js +12 -0
  99. package/dist/runtime/server/utils/write-statements.d.ts +33 -0
  100. package/dist/runtime/server/utils/write-statements.js +75 -0
  101. package/dist/runtime/shared/graphql-sdl.js +4 -1
  102. package/dist/runtime/shared/index.d.ts +73 -5
  103. package/dist/runtime/shared/index.js +146 -0
  104. package/dist/runtime/shared/preview.d.ts +85 -0
  105. package/dist/runtime/shared/preview.js +219 -0
  106. package/dist/runtime/shared/timestamps.d.ts +4 -0
  107. package/dist/runtime/shared/timestamps.js +24 -0
  108. package/dist/runtime/shared/validation.d.ts +2 -0
  109. package/dist/runtime/shared/validation.js +15 -2
  110. package/package.json +1 -1
@@ -10,6 +10,8 @@
10
10
  v-model="blocksValue"
11
11
  :field="field"
12
12
  :locale="locale"
13
+ :entry-name="entryName"
14
+ :field-key="fieldKey"
13
15
  />
14
16
  <CmsRichTextField v-else-if="field.type === 'richtext'" v-model="strOrNull" />
15
17
  <CmsTextarea
@@ -50,6 +52,7 @@
50
52
  :placeholder="field.placeholder"
51
53
  />
52
54
  <CmsInput v-else-if="field.type === 'date'" v-model="str" type="date" />
55
+ <CmsInput v-else-if="field.type === 'datetime'" v-model="localDateTime" type="datetime-local" />
53
56
  <CmsRelationField
54
57
  v-else-if="field.type === 'relation'"
55
58
  v-model="relationValue"
@@ -66,7 +69,9 @@ import { computed } from "#imports";
66
69
  const props = defineProps({
67
70
  field: { type: Object, required: true },
68
71
  slugSource: { type: [String, null], required: false },
69
- locale: { type: String, required: false }
72
+ locale: { type: String, required: false },
73
+ entryName: { type: String, required: false },
74
+ fieldKey: { type: String, required: false }
70
75
  });
71
76
  const model = defineModel({ type: null, ...{ required: true } });
72
77
  const selectItems = computed(() => [
@@ -97,6 +102,18 @@ const translatable = proxy((v) => v);
97
102
  const blocksValue = proxy(
98
103
  (v) => v
99
104
  );
105
+ function toLocalDateTime(value) {
106
+ if (typeof value !== "string" || !value) return "";
107
+ const date = new Date(value);
108
+ if (Number.isNaN(date.getTime())) return "";
109
+ return new Date(date.getTime() - date.getTimezoneOffset() * 6e4).toISOString().slice(0, 16);
110
+ }
111
+ function fromLocalDateTime(value) {
112
+ if (!value) return null;
113
+ const date = new Date(value);
114
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
115
+ }
116
+ const localDateTime = proxy(toLocalDateTime, fromLocalDateTime);
100
117
  const relationValue = proxy((v) => v);
101
118
  const jsonValue = proxy((v) => v);
102
119
  </script>
@@ -3,6 +3,8 @@ type __VLS_Props = {
3
3
  field: FieldConfig;
4
4
  slugSource?: string | null;
5
5
  locale?: string;
6
+ entryName?: string;
7
+ fieldKey?: string;
6
8
  };
7
9
  type __VLS_ModelProps = {
8
10
  modelValue: unknown;
@@ -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
@@ -69,7 +69,8 @@ import { errorMessage } from "../../utils/ui";
69
69
  const KIND_LABELS = {
70
70
  collection: "Collection",
71
71
  single: "Single",
72
- page: "Pages"
72
+ page: "Pages",
73
+ content: "Content"
73
74
  };
74
75
  const STATUS_LABELS = {
75
76
  idle: "Changes save automatically",
@@ -85,7 +86,7 @@ const toast = useCmsToast();
85
86
  const entry = computed(() => cmsConfig[props.name]);
86
87
  const fields = computed(() => entryFields(props.name));
87
88
  const parts = computed(() => [
88
- ...entry.value.kind === "collection" ? [{ id: "columns", label: "Table columns" }] : [],
89
+ ...entry.value.kind === "collection" || entry.value.kind === "content" ? [{ id: "columns", label: "Table columns" }] : [],
89
90
  { id: "layout", label: "Form layout" }
90
91
  ]);
91
92
  const activePart = ref(parts.value[0].id);
@@ -144,7 +144,8 @@ const emit = defineEmits(["logout"]);
144
144
  const KIND_ICONS = {
145
145
  collection: "square-3-stack-3d",
146
146
  single: "document-text",
147
- page: "window"
147
+ page: "window",
148
+ content: "newspaper"
148
149
  };
149
150
  const ENTRY_PREFIX = "entry:";
150
151
  const entries = Object.entries(cmsConfig).map(([name, entry]) => ({
@@ -1,5 +1,7 @@
1
1
  type __VLS_Props = {
2
2
  published?: boolean;
3
+ scheduled?: boolean;
4
+ at?: string;
3
5
  };
4
6
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
5
7
  declare const _default: typeof __VLS_export;
@@ -1,11 +1,17 @@
1
1
  <template>
2
- <span class="cms-badge" :class="published ? 'is-published' : 'is-draft'">
3
- {{ published ? "Published" : "Draft" }}
2
+ <span
3
+ class="cms-badge"
4
+ :class="scheduled ? 'is-scheduled' : published ? 'is-published' : 'is-draft'"
5
+ :title="scheduled && at ? `Goes live ${at}` : void 0"
6
+ >
7
+ {{ scheduled ? "Scheduled" : published ? "Published" : "Draft" }}
4
8
  </span>
5
9
  </template>
6
10
 
7
11
  <script setup>
8
12
  defineProps({
9
- published: { type: Boolean, required: false }
13
+ published: { type: Boolean, required: false },
14
+ scheduled: { type: Boolean, required: false },
15
+ at: { type: String, required: false }
10
16
  });
11
17
  </script>
@@ -1,5 +1,7 @@
1
1
  type __VLS_Props = {
2
2
  published?: boolean;
3
+ scheduled?: boolean;
4
+ at?: string;
3
5
  };
4
6
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
5
7
  declare const _default: typeof __VLS_export;
@@ -0,0 +1,37 @@
1
+ import type { FieldConfig } from '#nuxt-cms';
2
+ type __VLS_Props = {
3
+ entryName: string;
4
+ fieldKey: string;
5
+ fields: Record<string, FieldConfig>;
6
+ title: string;
7
+ content?: boolean;
8
+ };
9
+ declare function validate(): boolean;
10
+ type __VLS_ModelProps = {
11
+ 'state': Record<string, unknown>;
12
+ };
13
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
14
+ declare var __VLS_1: {}, __VLS_3: {}, __VLS_15: {};
15
+ type __VLS_Slots = {} & {
16
+ start?: (props: typeof __VLS_1) => any;
17
+ } & {
18
+ status?: (props: typeof __VLS_3) => any;
19
+ } & {
20
+ actions?: (props: typeof __VLS_15) => any;
21
+ };
22
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
23
+ validate: typeof validate;
24
+ locale: import("vue").Ref<string, string>;
25
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
26
+ "update:state": (value: Record<string, unknown>) => any;
27
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
28
+ "onUpdate:state"?: ((value: Record<string, unknown>) => any) | undefined;
29
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
30
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
31
+ declare const _default: typeof __VLS_export;
32
+ export default _default;
33
+ type __VLS_WithSlots<T, S> = T & {
34
+ new (): {
35
+ $slots: S;
36
+ };
37
+ };