@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.
- package/dist/module.json +1 -1
- package/dist/module.mjs +53 -23
- package/dist/runtime/app/components/cms/MediaDetail.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/MediaDetail.vue +169 -0
- package/dist/runtime/app/components/cms/MediaDetail.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/MediaField.vue +9 -2
- package/dist/runtime/app/components/cms/MediaFolderPicker.d.vue.ts +2 -2
- package/dist/runtime/app/components/cms/MediaFolderPicker.vue +55 -54
- package/dist/runtime/app/components/cms/MediaFolderPicker.vue.d.ts +2 -2
- package/dist/runtime/app/components/cms/MediaGallery.d.vue.ts +8 -2
- package/dist/runtime/app/components/cms/MediaGallery.vue +913 -336
- package/dist/runtime/app/components/cms/MediaGallery.vue.d.ts +8 -2
- package/dist/runtime/app/components/cms/MediaUpload.vue +10 -103
- package/dist/runtime/app/composables/cms-media-library.d.ts +75 -0
- package/dist/runtime/app/composables/cms-media-library.js +151 -0
- package/dist/runtime/app/composables/cms-media-uploader.d.ts +12 -0
- package/dist/runtime/app/composables/cms-media-uploader.js +117 -0
- package/dist/runtime/app/pages/admin-media.vue +14 -2
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/server/api/media-delete.post.d.ts +4 -0
- package/dist/runtime/server/api/media-delete.post.js +23 -0
- package/dist/runtime/server/api/media-folder.delete.js +6 -16
- package/dist/runtime/server/api/media-folder.patch.d.ts +5 -0
- package/dist/runtime/server/api/media-folder.patch.js +68 -0
- package/dist/runtime/server/api/media-folder.post.js +3 -15
- package/dist/runtime/server/api/media-move.post.d.ts +5 -0
- package/dist/runtime/server/api/media-move.post.js +28 -0
- package/dist/runtime/server/api/media-usage.post.d.ts +5 -0
- package/dist/runtime/server/api/media-usage.post.js +15 -0
- package/dist/runtime/server/utils/media-folders.d.ts +86 -0
- package/dist/runtime/server/utils/media-folders.js +37 -0
- package/dist/runtime/server/utils/media-references.d.ts +7 -1
- package/dist/runtime/server/utils/media-references.js +40 -0
- package/dist/runtime/server/utils/media-usage.d.ts +2 -0
- package/dist/runtime/server/utils/media-usage.js +18 -0
- package/dist/runtime/shared/index.d.ts +23 -1
- package/dist/runtime/shared/index.js +86 -2
- package/package.json +1 -1
package/dist/module.json
CHANGED
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
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
|
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
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
v-model="
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 {
|
|
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
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|