@xleddyl/nuxt-cms 0.1.60 → 0.1.61

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 (38) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +20 -0
  3. package/dist/runtime/app/components/cms/MediaDetail.d.vue.ts +16 -0
  4. package/dist/runtime/app/components/cms/MediaDetail.vue +169 -0
  5. package/dist/runtime/app/components/cms/MediaDetail.vue.d.ts +16 -0
  6. package/dist/runtime/app/components/cms/MediaField.vue +9 -2
  7. package/dist/runtime/app/components/cms/MediaFolderPicker.d.vue.ts +2 -2
  8. package/dist/runtime/app/components/cms/MediaFolderPicker.vue +55 -54
  9. package/dist/runtime/app/components/cms/MediaFolderPicker.vue.d.ts +2 -2
  10. package/dist/runtime/app/components/cms/MediaGallery.d.vue.ts +8 -2
  11. package/dist/runtime/app/components/cms/MediaGallery.vue +913 -336
  12. package/dist/runtime/app/components/cms/MediaGallery.vue.d.ts +8 -2
  13. package/dist/runtime/app/components/cms/MediaUpload.vue +10 -103
  14. package/dist/runtime/app/composables/cms-media-library.d.ts +75 -0
  15. package/dist/runtime/app/composables/cms-media-library.js +151 -0
  16. package/dist/runtime/app/composables/cms-media-uploader.d.ts +12 -0
  17. package/dist/runtime/app/composables/cms-media-uploader.js +117 -0
  18. package/dist/runtime/app/pages/admin-media.vue +14 -2
  19. package/dist/runtime/assets/main.css +1 -1
  20. package/dist/runtime/server/api/media-delete.post.d.ts +4 -0
  21. package/dist/runtime/server/api/media-delete.post.js +23 -0
  22. package/dist/runtime/server/api/media-folder.delete.js +6 -16
  23. package/dist/runtime/server/api/media-folder.patch.d.ts +5 -0
  24. package/dist/runtime/server/api/media-folder.patch.js +68 -0
  25. package/dist/runtime/server/api/media-folder.post.js +3 -15
  26. package/dist/runtime/server/api/media-move.post.d.ts +5 -0
  27. package/dist/runtime/server/api/media-move.post.js +28 -0
  28. package/dist/runtime/server/api/media-usage.post.d.ts +5 -0
  29. package/dist/runtime/server/api/media-usage.post.js +15 -0
  30. package/dist/runtime/server/utils/media-folders.d.ts +86 -0
  31. package/dist/runtime/server/utils/media-folders.js +37 -0
  32. package/dist/runtime/server/utils/media-references.d.ts +7 -1
  33. package/dist/runtime/server/utils/media-references.js +40 -0
  34. package/dist/runtime/server/utils/media-usage.d.ts +2 -0
  35. package/dist/runtime/server/utils/media-usage.js +18 -0
  36. package/dist/runtime/shared/index.d.ts +16 -0
  37. package/dist/runtime/shared/index.js +34 -2
  38. 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.1.60",
4
+ "version": "0.1.61",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1495,6 +1495,26 @@ const module$1 = defineNuxtModule({
1495
1495
  method: "delete",
1496
1496
  handler: resolver.resolve("./runtime/server/api/media-folder.delete")
1497
1497
  });
1498
+ addServerHandler({
1499
+ route: "/api/cms/admin/media/folders",
1500
+ method: "patch",
1501
+ handler: resolver.resolve("./runtime/server/api/media-folder.patch")
1502
+ });
1503
+ addServerHandler({
1504
+ route: "/api/cms/admin/media/move",
1505
+ method: "post",
1506
+ handler: resolver.resolve("./runtime/server/api/media-move.post")
1507
+ });
1508
+ addServerHandler({
1509
+ route: "/api/cms/admin/media/delete",
1510
+ method: "post",
1511
+ handler: resolver.resolve("./runtime/server/api/media-delete.post")
1512
+ });
1513
+ addServerHandler({
1514
+ route: "/api/cms/admin/media/usage",
1515
+ method: "post",
1516
+ handler: resolver.resolve("./runtime/server/api/media-usage.post")
1517
+ });
1498
1518
  if (mediaFilesystemBase) {
1499
1519
  addServerHandler({
1500
1520
  route: "/api/cms/admin/media/upload",
@@ -0,0 +1,16 @@
1
+ import type { MediaItem } from '#nuxt-cms';
2
+ import type { CmsMediaLibrary } from '../../composables/cms-media-library.js';
3
+ type __VLS_Props = {
4
+ item: MediaItem | null;
5
+ library: CmsMediaLibrary;
6
+ readOnly: boolean;
7
+ };
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
+ delete: (item: MediaItem) => any;
10
+ close: () => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ onDelete?: ((item: MediaItem) => any) | undefined;
13
+ onClose?: (() => any) | undefined;
14
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;
@@ -0,0 +1,169 @@
1
+ <template>
2
+ <CmsDrawer :open="!!item" :title="item ? mediaFilename(item.key) : ''" @close="emit('close')">
3
+ <div v-if="item" class="cms-media-detail">
4
+ <div class="cms-media-detail-preview">
5
+ <img v-if="item.type === 'image' && item.url" :src="item.url" :alt="item.alt ?? ''" />
6
+ <video
7
+ v-else-if="item.type === 'video' && item.url"
8
+ :src="item.url"
9
+ controls
10
+ preload="metadata"
11
+ playsinline
12
+ />
13
+ <CmsIcon v-else :name="mediaIconFor(item.type)" class="size-12" />
14
+ </div>
15
+
16
+ <div v-if="item.url" class="cms-actions">
17
+ <CmsButton
18
+ :label="copied ? 'Copied' : 'Copy URL'"
19
+ :icon="copied ? 'check' : 'clipboard-document'"
20
+ variant="soft"
21
+ color="neutral"
22
+ size="sm"
23
+ @click="copyUrl"
24
+ />
25
+ <a
26
+ :href="item.url"
27
+ target="_blank"
28
+ rel="noopener"
29
+ class="cms-btn cms-btn-sm cms-btn-neutral cms-btn-ghost"
30
+ >
31
+ <CmsIcon name="arrow-top-right-on-square" class="size-4 shrink-0" />Open
32
+ </a>
33
+ </div>
34
+
35
+ <dl class="cms-media-detail-facts">
36
+ <template v-for="fact in facts" :key="fact.label">
37
+ <dt>{{ fact.label }}</dt>
38
+ <dd :class="{ 'is-mono': fact.mono }" :title="fact.value">{{ fact.value }}</dd>
39
+ </template>
40
+ </dl>
41
+
42
+ <div class="cms-form">
43
+ <CmsFormField label="Alt text">
44
+ <CmsInput
45
+ v-model="alt"
46
+ placeholder="Describe the image for screen readers"
47
+ @keydown.enter.prevent="save"
48
+ />
49
+ </CmsFormField>
50
+ <CmsFormField v-if="!readOnly" label="Folder">
51
+ <CmsMediaFolderPicker v-model="folder" :folders="folders" />
52
+ </CmsFormField>
53
+ </div>
54
+
55
+ <section class="cms-media-detail-usage">
56
+ <h3 class="cms-media-section-title">Used in</h3>
57
+ <CmsSpinner v-if="usageLoading" />
58
+ <p v-else-if="usageList === null" class="cms-form-hint">
59
+ Could not check where this file is used.
60
+ </p>
61
+ <p v-else-if="!usageList.length" class="cms-form-hint">Not used in any entry.</p>
62
+ <ul v-else class="cms-media-usage-list">
63
+ <li v-for="(entry, index) in usageList" :key="index">
64
+ <NuxtLink
65
+ :to="usageLink(entry)"
66
+ class="cms-media-usage-link"
67
+ @click="emit('close')"
68
+ >
69
+ <span class="cms-media-usage-title">{{ entry.title ?? entry.label }}</span>
70
+ <span class="cms-media-usage-meta">{{ entry.label }} · {{ entry.field }}</span>
71
+ </NuxtLink>
72
+ </li>
73
+ </ul>
74
+ </section>
75
+ </div>
76
+
77
+ <template #footer>
78
+ <CmsButton
79
+ v-if="!readOnly"
80
+ label="Delete"
81
+ icon="trash"
82
+ variant="ghost"
83
+ color="error"
84
+ @click="item && emit('delete', item)"
85
+ />
86
+ <span v-else />
87
+ <div class="cms-actions">
88
+ <CmsButton label="Close" variant="ghost" color="neutral" @click="emit('close')" />
89
+ <CmsButton label="Save" :loading="saving" :disabled="!dirty" @click="save" />
90
+ </div>
91
+ </template>
92
+ </CmsDrawer>
93
+ </template>
94
+
95
+ <script setup>
96
+ import { computed, ref, watch } from "#imports";
97
+ import { formatFileSize, mediaFilename, mediaIconFor } from "#nuxt-cms";
98
+ import { usageLink } from "../../composables/cms-media-library";
99
+ const props = defineProps({
100
+ item: { type: [Object, null], required: true },
101
+ library: { type: null, required: true },
102
+ readOnly: { type: Boolean, required: true }
103
+ });
104
+ const emit = defineEmits(["close", "delete"]);
105
+ const folders = computed(() => props.library.folders.value);
106
+ const alt = ref("");
107
+ const folder = ref(null);
108
+ const saving = ref(false);
109
+ const copied = ref(false);
110
+ const usageList = ref([]);
111
+ const usageLoading = ref(false);
112
+ watch(
113
+ () => props.item?.key,
114
+ async (key) => {
115
+ if (!props.item || !key) return;
116
+ alt.value = props.item.alt ?? "";
117
+ folder.value = props.item.folder ?? null;
118
+ copied.value = false;
119
+ usageLoading.value = true;
120
+ const usage = await props.library.usage([key]);
121
+ if (props.item?.key !== key) return;
122
+ usageList.value = usage ? usage[key] ?? [] : null;
123
+ usageLoading.value = false;
124
+ },
125
+ { immediate: true }
126
+ );
127
+ const dirty = computed(() => {
128
+ if (!props.item) return false;
129
+ return (alt.value.trim() || null) !== (props.item.alt ?? null) || !props.readOnly && folder.value !== (props.item.folder ?? null);
130
+ });
131
+ const facts = computed(() => {
132
+ const item = props.item;
133
+ if (!item) return [];
134
+ const list = [
135
+ { label: "Folder", value: item.folder ?? "Library root" },
136
+ { label: "Type", value: item.mime ?? item.type }
137
+ ];
138
+ if (item.size != null) list.push({ label: "Size", value: formatFileSize(item.size) });
139
+ if (item.width && item.height) {
140
+ list.push({ label: "Dimensions", value: `${item.width} \xD7 ${item.height}` });
141
+ }
142
+ if (item.createdAt) {
143
+ list.push({ label: "Uploaded", value: new Date(item.createdAt).toLocaleString() });
144
+ }
145
+ list.push({ label: "Key", value: item.key, mono: true });
146
+ return list;
147
+ });
148
+ async function copyUrl() {
149
+ if (!props.item?.url) return;
150
+ const url = new URL(props.item.url, window.location.origin).toString();
151
+ try {
152
+ await navigator.clipboard.writeText(url);
153
+ copied.value = true;
154
+ setTimeout(() => copied.value = false, 1500);
155
+ } catch {
156
+ copied.value = false;
157
+ }
158
+ }
159
+ async function save() {
160
+ if (!props.item || !dirty.value || saving.value) return;
161
+ saving.value = true;
162
+ const ok = await props.library.updateItem(props.item.key, {
163
+ alt: alt.value.trim() || null,
164
+ ...props.readOnly ? {} : { folder: folder.value }
165
+ });
166
+ saving.value = false;
167
+ if (ok) emit("close");
168
+ }
169
+ </script>
@@ -0,0 +1,16 @@
1
+ import type { MediaItem } from '#nuxt-cms';
2
+ import type { CmsMediaLibrary } from '../../composables/cms-media-library.js';
3
+ type __VLS_Props = {
4
+ item: MediaItem | null;
5
+ library: CmsMediaLibrary;
6
+ readOnly: boolean;
7
+ };
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
+ delete: (item: MediaItem) => any;
10
+ close: () => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ onDelete?: ((item: MediaItem) => any) | undefined;
13
+ onClose?: (() => any) | undefined;
14
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;
@@ -55,13 +55,19 @@
55
55
 
56
56
  <CmsModal v-model:open="galleryOpen" title="Media" size="lg">
57
57
  <template #body>
58
- <CmsMediaGallery selectable :media-type="mediaType" :accept="accept" @select="onSelect" />
58
+ <CmsMediaGallery
59
+ v-model:folder="pickerFolder"
60
+ selectable
61
+ :media-type="mediaType"
62
+ :accept="accept"
63
+ @select="onSelect"
64
+ />
59
65
  </template>
60
66
  </CmsModal>
61
67
  </template>
62
68
 
63
69
  <script setup>
64
- import { computed, ref } from "#imports";
70
+ import { computed, ref, useState } from "#imports";
65
71
  import { mediaFilename, mediaIconFor, mediaPublicUrl, mediaTypeForKey } from "#nuxt-cms";
66
72
  import { useCmsMediaKeys } from "../../composables/cms-media-keys";
67
73
  import { useCmsRuntime } from "../../composables/cms-runtime";
@@ -73,6 +79,7 @@ const model = defineModel({ type: [String, null], ...{ required: true } });
73
79
  const { mediaBaseUrl } = useCmsRuntime();
74
80
  const mediaKeys = useCmsMediaKeys();
75
81
  const galleryOpen = ref(false);
82
+ const pickerFolder = useState("cms-media-picker-folder", () => null);
76
83
  function onSelect(item) {
77
84
  mediaKeys.remember(item.key);
78
85
  model.value = item.key;
@@ -1,5 +1,7 @@
1
1
  type __VLS_Props = {
2
2
  folders: string[];
3
+ exclude?: string | null;
4
+ disabled?: (string | null)[];
3
5
  };
4
6
  type __VLS_ModelProps = {
5
7
  modelValue?: string | null;
@@ -7,10 +9,8 @@ type __VLS_ModelProps = {
7
9
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
8
10
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
11
  "update:modelValue": (value: string | null) => any;
10
- create: (folder: string) => any;
11
12
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
12
13
  "onUpdate:modelValue"?: ((value: string | null) => any) | undefined;
13
- onCreate?: ((folder: string) => any) | undefined;
14
14
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
15
  declare const _default: typeof __VLS_export;
16
16
  export default _default;
@@ -1,65 +1,66 @@
1
1
  <template>
2
2
  <div class="cms-folder-picker">
3
- <button
4
- type="button"
5
- class="cms-pill"
6
- :class="{ 'is-active': model === null }"
7
- @click="model = null"
8
- >
9
- <CmsIcon name="circle-stack" class="size-3" />Root
10
- </button>
11
- <button
12
- v-for="name in folders"
13
- :key="name"
14
- type="button"
15
- class="cms-pill"
16
- :class="{ 'is-active': model === name }"
17
- @click="model = name"
18
- >
19
- <CmsIcon name="folder" class="size-3" />{{ name }}
20
- </button>
21
- <template v-if="creating">
22
- <CmsInput
23
- v-model="draft"
24
- size="sm"
25
- autofocus
26
- placeholder="Folder name"
27
- class="cms-folder-picker-input"
28
- @keydown.enter.prevent="commit"
29
- @keydown.esc="cancel"
30
- />
31
- <CmsButton label="Add" size="xs" :disabled="!normalized" @click="commit" />
32
- <CmsButton label="Cancel" size="xs" variant="ghost" color="neutral" @click="cancel" />
33
- </template>
34
- <button v-else type="button" class="cms-pill" @click="startCreating">
35
- <CmsIcon name="folder-plus" class="size-3" />New folder
36
- </button>
3
+ <CmsInput
4
+ v-if="folders.length > 8"
5
+ v-model="query"
6
+ size="sm"
7
+ icon="magnifying-glass"
8
+ placeholder="Find a folder…"
9
+ />
10
+ <div class="cms-folder-picker-list" role="listbox">
11
+ <button
12
+ v-if="!query"
13
+ type="button"
14
+ role="option"
15
+ class="cms-folder-picker-row"
16
+ :class="{ 'is-active': model === null }"
17
+ :aria-selected="model === null"
18
+ :disabled="isDisabled(null)"
19
+ @click="model = null"
20
+ >
21
+ <CmsIcon name="home" class="size-4" />
22
+ <span class="truncate">Library root</span>
23
+ <CmsIcon v-if="model === null" name="check" class="cms-folder-picker-check size-4" />
24
+ </button>
25
+ <button
26
+ v-for="folder in visible"
27
+ :key="folder"
28
+ type="button"
29
+ role="option"
30
+ class="cms-folder-picker-row"
31
+ :class="{ 'is-active': model === folder }"
32
+ :style="
33
+ query ? void 0 : { paddingLeft: `${0.625 + mediaFolderDepth(folder) * 1}rem` }
34
+ "
35
+ :aria-selected="model === folder"
36
+ :disabled="isDisabled(folder)"
37
+ @click="model = folder"
38
+ >
39
+ <CmsIcon :name="model === folder ? 'folder-open' : 'folder'" class="size-4" />
40
+ <span class="truncate">{{ query ? folder : mediaFolderName(folder) }}</span>
41
+ <CmsIcon v-if="model === folder" name="check" class="cms-folder-picker-check size-4" />
42
+ </button>
43
+ <p v-if="query && !visible.length" class="cms-folder-picker-empty">No matching folders</p>
44
+ </div>
37
45
  </div>
38
46
  </template>
39
47
 
40
48
  <script setup>
41
49
  import { computed, ref } from "#imports";
42
- import { normalizeMediaFolder } from "#nuxt-cms";
43
- defineProps({
44
- folders: { type: Array, required: true }
50
+ import { isWithinMediaFolder, mediaFolderDepth, mediaFolderName } from "#nuxt-cms";
51
+ const props = defineProps({
52
+ folders: { type: Array, required: true },
53
+ exclude: { type: [String, null], required: false },
54
+ disabled: { type: Array, required: false }
45
55
  });
46
- const emit = defineEmits(["create"]);
47
56
  const model = defineModel({ type: [String, null], ...{ default: null } });
48
- const creating = ref(false);
49
- const draft = ref("");
50
- const normalized = computed(() => normalizeMediaFolder(draft.value));
51
- function startCreating() {
52
- creating.value = true;
53
- }
54
- function cancel() {
55
- creating.value = false;
56
- draft.value = "";
57
- }
58
- function commit() {
59
- const folder = normalized.value;
60
- if (!folder) return;
61
- emit("create", folder);
62
- model.value = folder;
63
- cancel();
57
+ const query = ref("");
58
+ const visible = computed(() => {
59
+ const term = query.value.trim().toLowerCase();
60
+ return term ? props.folders.filter((folder) => folder.includes(term)) : props.folders;
61
+ });
62
+ function isDisabled(folder) {
63
+ if (props.disabled?.includes(folder)) return true;
64
+ return !!props.exclude && !!folder && isWithinMediaFolder(folder, props.exclude);
64
65
  }
65
66
  </script>
@@ -1,5 +1,7 @@
1
1
  type __VLS_Props = {
2
2
  folders: string[];
3
+ exclude?: string | null;
4
+ disabled?: (string | null)[];
3
5
  };
4
6
  type __VLS_ModelProps = {
5
7
  modelValue?: string | null;
@@ -7,10 +9,8 @@ type __VLS_ModelProps = {
7
9
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
8
10
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
11
  "update:modelValue": (value: string | null) => any;
10
- create: (folder: string) => any;
11
12
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
12
13
  "onUpdate:modelValue"?: ((value: string | null) => any) | undefined;
13
- onCreate?: ((folder: string) => any) | undefined;
14
14
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
15
  declare const _default: typeof __VLS_export;
16
16
  export default _default;
@@ -4,16 +4,22 @@ type __VLS_Props = {
4
4
  mediaType?: MediaType | MediaType[];
5
5
  accept?: string[];
6
6
  };
7
- declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
+ type __VLS_ModelProps = {
8
+ 'folder'?: string | null;
9
+ };
10
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
11
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
12
  select: (item: {
9
13
  key: string;
10
14
  url: string | null;
11
15
  }) => any;
12
- }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
+ "update:folder": (value: string | null) => any;
17
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
13
18
  onSelect?: ((item: {
14
19
  key: string;
15
20
  url: string | null;
16
21
  }) => any) | undefined;
22
+ "onUpdate:folder"?: ((value: string | null) => any) | undefined;
17
23
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
24
  declare const _default: typeof __VLS_export;
19
25
  export default _default;