@xleddyl/nuxt-cms 0.1.59 → 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 +53 -23
  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 +23 -1
  37. package/dist/runtime/shared/index.js +86 -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.59",
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
@@ -11,7 +11,7 @@ import { minifyIntrospection, outputIntrospectionFile } from 'gql.tada/internal'
11
11
  import { createJiti } from 'jiti';
12
12
  import { typeName, blockTypeName, blocksFieldTypeName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
13
13
  import { migrationsDirFor } from '../dist/runtime/shared/migrations-dir.js';
14
- import { pageColumnFields, isTranslatableField, isTranslatableMediaField, isMultiSelect, pageAllFields, PAGE_PATH_FIELD, pageRoutes, fieldConditions, pageFieldsTableName, pageMediaTableName, isRequiredField, pageFields, isPrivateField, entryFieldsFor, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE } from '../dist/runtime/shared/index.js';
14
+ import { pageColumnFields, isTranslatableField, isTranslatableMediaField, isMultiSelect, pageAllFields, PAGE_PATH_FIELD, pageRoutes, mobileMediaKey, fieldConditions, pageFieldsTableName, pageMediaTableName, isRequiredField, pageFields, isPrivateField, entryFieldsFor, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE, expandMobileMedia } from '../dist/runtime/shared/index.js';
15
15
  import { scanMediaDirectory, readMediaFileMeta } from '../dist/runtime/server/utils/media-sync.js';
16
16
  import { createHash } from 'node:crypto';
17
17
  import { readFile } from 'node:fs/promises';
@@ -245,6 +245,13 @@ function validateConfig(config, i18n) {
245
245
  if (!locales.length)
246
246
  errors.push(`${fat}: translatable requires cms.i18n.locales in nuxt.config`);
247
247
  }
248
+ if (field.mobile) {
249
+ const mobileKey = mobileMediaKey(key);
250
+ if (field.type !== "media")
251
+ errors.push(`${fat}: mobile is only supported on media fields`);
252
+ else if (allFields[mobileKey] && allFields[mobileKey].mobileOf !== key)
253
+ errors.push(`${fat}: mobile needs the key '${mobileKey}', which is already declared`);
254
+ }
248
255
  if (field.type === "select") {
249
256
  if (!field.options?.length)
250
257
  errors.push(`${fat}: select requires a non-empty options array`);
@@ -337,6 +344,8 @@ function validateConfig(config, i18n) {
337
344
  errors.push(`${bfat}: private fields are not supported inside blocks`);
338
345
  if (blockField.showIf)
339
346
  errors.push(`${bfat}: showIf is not supported inside blocks`);
347
+ if (blockField.mobile)
348
+ errors.push(`${bfat}: mobile is not supported inside blocks`);
340
349
  if (blockField.type === "select" && !blockField.options?.length) {
341
350
  errors.push(`${bfat}: select requires a non-empty options array`);
342
351
  }
@@ -966,7 +975,7 @@ async function loadCmsConfig(nuxt, resolver, configPathOption, i18n, logger) {
966
975
  moduleCache: false,
967
976
  alias: { "#nuxt-cms": resolver.resolve("./runtime/shared/index") }
968
977
  });
969
- cmsConfig = await jiti.import(configPath, { default: true });
978
+ cmsConfig = expandMobileMedia(await jiti.import(configPath, { default: true }));
970
979
  } else {
971
980
  logger.warn(
972
981
  `[nuxt-cms] Config file not found: ${configPath}. Using an empty registry \u2014 create a ${configPathOption}.ts with defineCmsConfig().`
@@ -984,27 +993,28 @@ async function loadCmsConfig(nuxt, resolver, configPathOption, i18n, logger) {
984
993
  entry.pages = resolvePageRoutes(entry, discoveredRoutes);
985
994
  routesByEntry[name] = entry.pages;
986
995
  }
987
- if (Object.keys(routesByEntry).length) {
988
- const source = (nuxt.options.alias["#cms-config"] ?? "").replace(/\\/g, "/").replace(/\.[cm]?[jt]s$/, "");
989
- const wrapper = addTemplate({
990
- filename: "cms/config.ts",
991
- write: true,
992
- getContents: () => [
993
- `import config from '${source}'`,
994
- ``,
995
- `const pageRoutes = ${JSON.stringify(routesByEntry, null, 3)}`,
996
- ``,
997
- `for (const [name, routes] of Object.entries(pageRoutes)) {`,
998
- ` const entry = Object(config)[name]`,
999
- ` if (entry) entry.pages = routes`,
1000
- `}`,
1001
- ``,
1002
- `export default config`,
1003
- ``
1004
- ].join("\n")
1005
- });
1006
- nuxt.options.alias["#cms-config"] = wrapper.dst;
1007
- }
996
+ const source = (nuxt.options.alias["#cms-config"] ?? "").replace(/\\/g, "/").replace(/\.[cm]?[jt]s$/, "");
997
+ const wrapper = addTemplate({
998
+ filename: "cms/config.ts",
999
+ write: true,
1000
+ getContents: () => [
1001
+ `import { expandMobileMedia } from '#nuxt-cms'`,
1002
+ `import config from '${source}'`,
1003
+ ``,
1004
+ `const pageRoutes = ${JSON.stringify(routesByEntry, null, 3)}`,
1005
+ ``,
1006
+ `expandMobileMedia(config)`,
1007
+ ``,
1008
+ `for (const [name, routes] of Object.entries(pageRoutes)) {`,
1009
+ ` const entry = Object(config)[name]`,
1010
+ ` if (entry) entry.pages = routes`,
1011
+ `}`,
1012
+ ``,
1013
+ `export default config`,
1014
+ ``
1015
+ ].join("\n")
1016
+ });
1017
+ nuxt.options.alias["#cms-config"] = wrapper.dst;
1008
1018
  const configErrors = validateConfig(cmsConfig, i18n);
1009
1019
  if (configErrors.length) {
1010
1020
  for (const error of configErrors) logger.error(error);
@@ -1485,6 +1495,26 @@ const module$1 = defineNuxtModule({
1485
1495
  method: "delete",
1486
1496
  handler: resolver.resolve("./runtime/server/api/media-folder.delete")
1487
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
+ });
1488
1518
  if (mediaFilesystemBase) {
1489
1519
  addServerHandler({
1490
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;