@xleddyl/nuxt-cms 0.2.0 → 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.
- package/dist/module.json +1 -1
- package/dist/module.mjs +17 -6
- package/dist/runtime/app/components/cms/BrandMark.vue +2 -3
- package/dist/runtime/app/components/cms/ConfirmModal.vue +14 -1
- package/dist/runtime/app/components/cms/Drawer.vue +5 -9
- package/dist/runtime/app/components/cms/EntryDrawer.vue +11 -3
- package/dist/runtime/app/components/cms/EntryForm.vue +0 -1
- package/dist/runtime/app/components/cms/Input.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/Input.vue +7 -4
- package/dist/runtime/app/components/cms/Input.vue.d.ts +0 -1
- package/dist/runtime/app/components/cms/LocaleSwitch.vue +1 -1
- package/dist/runtime/app/components/cms/MediaFolderPicker.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/MediaFolderPicker.vue +1 -3
- package/dist/runtime/app/components/cms/MediaFolderPicker.vue.d.ts +0 -1
- package/dist/runtime/app/components/cms/MediaGallery.vue +89 -11
- package/dist/runtime/app/components/cms/Modal.vue +5 -11
- package/dist/runtime/app/components/cms/PasswordInput.vue +9 -5
- package/dist/runtime/app/components/cms/RelationField.vue +7 -2
- package/dist/runtime/app/components/cms/SelectMenu.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/SelectMenu.vue +1 -6
- package/dist/runtime/app/components/cms/SelectMenu.vue.d.ts +0 -1
- package/dist/runtime/app/components/cms/SettingsEntry.vue +1 -8
- package/dist/runtime/app/components/cms/SettingsModal.vue +5 -10
- package/dist/runtime/app/components/cms/SettingsUsers.vue +8 -4
- package/dist/runtime/app/components/cms/Table.d.vue.ts +0 -7
- package/dist/runtime/app/components/cms/Table.vue +7 -49
- package/dist/runtime/app/components/cms/Table.vue.d.ts +0 -7
- package/dist/runtime/app/components/cms/Textarea.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/Textarea.vue +1 -2
- package/dist/runtime/app/components/cms/Textarea.vue.d.ts +0 -1
- package/dist/runtime/app/composables/cms-confirm.d.ts +16 -7
- package/dist/runtime/app/composables/cms-confirm.js +1 -0
- package/dist/runtime/app/composables/cms-entry-disabled.js +1 -1
- package/dist/runtime/app/composables/cms-media-keys.d.ts +1 -0
- package/dist/runtime/app/composables/cms-media-keys.js +5 -0
- package/dist/runtime/app/composables/cms-media-library.d.ts +1 -2
- package/dist/runtime/app/composables/cms-media-library.js +15 -19
- package/dist/runtime/app/composables/cms-media-uploader.d.ts +0 -1
- package/dist/runtime/app/composables/cms-media-uploader.js +1 -1
- package/dist/runtime/app/composables/cms-overlay.d.ts +4 -0
- package/dist/runtime/app/composables/cms-overlay.js +23 -0
- package/dist/runtime/app/composables/cms-settings.d.ts +0 -1
- package/dist/runtime/app/composables/cms-settings.js +1 -1
- package/dist/runtime/app/pages/admin-entry.vue +1 -0
- package/dist/runtime/app/utils/ui.d.ts +0 -2
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/generate.js +1 -1
- package/dist/runtime/seed.js +11 -1
- package/dist/runtime/server/api/collection.get.js +1 -1
- package/dist/runtime/server/api/collection.post.js +20 -14
- package/dist/runtime/server/api/collection.put.js +12 -12
- package/dist/runtime/server/api/item.put.js +14 -13
- package/dist/runtime/server/api/media-folder.patch.js +10 -16
- package/dist/runtime/server/api/media-move.post.js +12 -8
- package/dist/runtime/server/api/media.delete.js +3 -2
- package/dist/runtime/server/api/media.post.js +4 -2
- package/dist/runtime/server/api/media.put.js +4 -2
- package/dist/runtime/server/api/users.post.js +13 -9
- package/dist/runtime/server/plugins/migrate-sqlite.js +2 -2
- package/dist/runtime/server/routes/auth/login.post.js +30 -24
- package/dist/runtime/server/utils/db-d1.d.ts +0 -1
- package/dist/runtime/server/utils/db-d1.js +3 -4
- package/dist/runtime/server/utils/db-libsql.d.ts +0 -1
- package/dist/runtime/server/utils/db-libsql.js +3 -4
- package/dist/runtime/server/utils/db-postgres.d.ts +0 -1
- package/dist/runtime/server/utils/db-postgres.js +1 -4
- package/dist/runtime/server/utils/db-sqlite.d.ts +0 -1
- package/dist/runtime/server/utils/db-sqlite.js +4 -10
- package/dist/runtime/server/utils/graphql-depth.js +12 -5
- package/dist/runtime/server/utils/graphql.js +4 -3
- package/dist/runtime/server/utils/libsql-migrations.d.ts +1 -0
- package/dist/runtime/server/utils/libsql-migrations.js +1 -1
- package/dist/runtime/server/utils/media-folders.js +23 -24
- package/dist/runtime/server/utils/media.d.ts +1 -0
- package/dist/runtime/server/utils/media.js +6 -1
- package/dist/runtime/server/utils/migrate.d.ts +1 -0
- package/dist/runtime/server/utils/migrate.js +33 -1
- package/dist/runtime/server/utils/registry.d.ts +3 -0
- package/dist/runtime/server/utils/registry.js +10 -1
- package/dist/runtime/server/utils/relations.d.ts +1 -1
- package/dist/runtime/server/utils/relations.js +6 -9
- package/dist/runtime/server/utils/require-admin.d.ts +0 -2
- package/dist/runtime/server/utils/require-admin.js +0 -1
- package/dist/runtime/server/utils/timestamps.d.ts +12 -0
- package/dist/runtime/server/utils/timestamps.js +48 -0
- package/dist/runtime/server/utils/users.js +3 -2
- package/dist/runtime/server/utils/write-statements.d.ts +33 -0
- package/dist/runtime/server/utils/write-statements.js +75 -0
- package/dist/runtime/shared/index.d.ts +14 -0
- package/dist/runtime/shared/index.js +13 -0
- package/dist/runtime/shared/layout.d.ts +0 -1
- package/dist/runtime/shared/layout.js +0 -3
- package/dist/runtime/shared/timestamps.d.ts +4 -0
- package/dist/runtime/shared/timestamps.js +24 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -380,7 +380,7 @@ function validateConfig(config, i18n) {
|
|
|
380
380
|
errors.push(`${fat}: relation target '${field.to}' must be a collection`);
|
|
381
381
|
if (!entry.table && target.table)
|
|
382
382
|
errors.push(
|
|
383
|
-
`${fat}: relation target '${field.to}' uses a custom table
|
|
383
|
+
`${fat}: relation target '${field.to}' uses a custom table: derived relations can only target derived entries`
|
|
384
384
|
);
|
|
385
385
|
}
|
|
386
386
|
if (field.cardinality && !["many-to-one", "one-to-one", "many-to-many"].includes(field.cardinality)) {
|
|
@@ -835,7 +835,12 @@ ${lines.join(
|
|
|
835
835
|
function pageTypesTs(name, entry) {
|
|
836
836
|
const auto = `${typeName(name)}Auto`;
|
|
837
837
|
const lines = pageRoutes(entry).map((route) => {
|
|
838
|
-
const keys = [
|
|
838
|
+
const keys = [
|
|
839
|
+
"id",
|
|
840
|
+
"path",
|
|
841
|
+
"updatedAt",
|
|
842
|
+
...Object.entries(pageFields(entry, route.path)).filter(([, field]) => !isPrivateField(field)).map(([key]) => key)
|
|
843
|
+
];
|
|
839
844
|
return ` ${JSON.stringify(route.path)}: Pick<${auto}, ${quotedKeys(keys)}>`;
|
|
840
845
|
});
|
|
841
846
|
return [
|
|
@@ -1027,7 +1032,7 @@ async function loadCmsConfig(nuxt, resolver, configPathOption, i18n, logger) {
|
|
|
1027
1032
|
cmsConfig = expandMobileMedia(await jiti.import(configPath, { default: true }));
|
|
1028
1033
|
} else {
|
|
1029
1034
|
logger.warn(
|
|
1030
|
-
`[nuxt-cms] Config file not found: ${configPath}. Using an empty registry
|
|
1035
|
+
`[nuxt-cms] Config file not found: ${configPath}. Using an empty registry. Create a ${configPathOption}.ts with defineCmsConfig().`
|
|
1031
1036
|
);
|
|
1032
1037
|
nuxt.options.alias["#cms-config"] = resolver.resolve("./runtime/shared/empty-config");
|
|
1033
1038
|
}
|
|
@@ -1121,7 +1126,7 @@ function addCmsTypeTemplates(nuxt, cmsConfig) {
|
|
|
1121
1126
|
``,
|
|
1122
1127
|
`declare const graphql: CmsGraphql`,
|
|
1123
1128
|
``,
|
|
1124
|
-
`// @ts-ignore
|
|
1129
|
+
`// @ts-ignore`,
|
|
1125
1130
|
`export type CmsDocument<Query extends string> = ReturnType<typeof graphql<Query, []>>`,
|
|
1126
1131
|
``,
|
|
1127
1132
|
`export type CmsResult<Query extends string> = string extends Query`,
|
|
@@ -1288,7 +1293,13 @@ const module$1 = defineNuxtModule({
|
|
|
1288
1293
|
` dialect: '${dialect}',`,
|
|
1289
1294
|
` schema: '${toPosix(schemaTemplate.dst)}',`,
|
|
1290
1295
|
` out: '${toPosix(relativeMigrationsDir)}',`,
|
|
1291
|
-
driver === "postgres" ? ` dbCredentials: { url: process.env.NUXT_CMS_DATABASE_URL ??
|
|
1296
|
+
driver === "postgres" ? ` dbCredentials: { url: process.env.NUXT_CMS_DATABASE_URL ?? ${JSON.stringify(
|
|
1297
|
+
databaseUrl
|
|
1298
|
+
)} },` : driver === "libsql" ? ` dbCredentials: { url: process.env.NUXT_CMS_DATABASE_URL ?? ${JSON.stringify(
|
|
1299
|
+
databaseUrl || `file:${toPosix(resolvedDbPath)}`
|
|
1300
|
+
)}, authToken: (process.env.NUXT_CMS_DATABASE_AUTH_TOKEN ?? ${JSON.stringify(
|
|
1301
|
+
databaseAuthToken
|
|
1302
|
+
)}) || undefined },` : driver === "d1" ? `` : ` dbCredentials: { url: ${JSON.stringify(toPosix(resolvedDbPath))} },`,
|
|
1292
1303
|
`}`,
|
|
1293
1304
|
``
|
|
1294
1305
|
].join("\n")
|
|
@@ -1305,7 +1316,7 @@ const module$1 = defineNuxtModule({
|
|
|
1305
1316
|
const migrations = await collectMigrations(migrationsDir);
|
|
1306
1317
|
if (!migrations && !nuxt.options.dev && Object.keys(cmsConfig).length) {
|
|
1307
1318
|
logger.warn(
|
|
1308
|
-
`[nuxt-cms] No migrations found at ${migrationsDir}. The CMS tables will not be created
|
|
1319
|
+
`[nuxt-cms] No migrations found at ${migrationsDir}. The CMS tables will not be created. Run the dev server once to generate them and commit ${toPosix(
|
|
1309
1320
|
relativeMigrationsDir
|
|
1310
1321
|
)}.`
|
|
1311
1322
|
);
|
|
@@ -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.
|
|
5
|
+
d="M4.5 5.5h7M4.5 8h7M4.5 10.5h4"
|
|
6
6
|
stroke="currentColor"
|
|
7
|
-
stroke-width="
|
|
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
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
<script setup>
|
|
37
37
|
import { onBeforeUnmount, watch } from "#imports";
|
|
38
|
+
import { useCmsOverlay } from "../../composables/cms-overlay";
|
|
38
39
|
defineProps({
|
|
39
40
|
title: { type: String, required: false }
|
|
40
41
|
});
|
|
@@ -43,23 +44,18 @@ const emit = defineEmits(["close"]);
|
|
|
43
44
|
function close() {
|
|
44
45
|
emit("close");
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
-
if (event.key === "Escape") close();
|
|
48
|
-
}
|
|
47
|
+
const overlay = useCmsOverlay(close);
|
|
49
48
|
watch(open, (value) => {
|
|
50
49
|
if (import.meta.server) return;
|
|
51
50
|
if (value) {
|
|
52
|
-
|
|
51
|
+
overlay.activate();
|
|
53
52
|
document.body.style.overflow = "hidden";
|
|
54
53
|
} else {
|
|
55
|
-
|
|
54
|
+
overlay.deactivate();
|
|
56
55
|
document.body.style.overflow = "";
|
|
57
56
|
}
|
|
58
57
|
});
|
|
59
58
|
onBeforeUnmount(() => {
|
|
60
|
-
if (import.meta.client)
|
|
61
|
-
document.removeEventListener("keydown", onKeydown);
|
|
62
|
-
document.body.style.overflow = "";
|
|
63
|
-
}
|
|
59
|
+
if (import.meta.client) document.body.style.overflow = "";
|
|
64
60
|
});
|
|
65
61
|
</script>
|
|
@@ -121,16 +121,23 @@ function revertStatus() {
|
|
|
121
121
|
if (!drafts.value) return;
|
|
122
122
|
formState.value.status = JSON.parse(snapshot.value).status ?? null;
|
|
123
123
|
}
|
|
124
|
+
let loadRequest = 0;
|
|
124
125
|
async function load() {
|
|
125
|
-
|
|
126
|
+
const request = ++loadRequest;
|
|
127
|
+
const id = props.entryId;
|
|
128
|
+
if (id === null) {
|
|
129
|
+
loading.value = false;
|
|
126
130
|
setState(emptyState());
|
|
127
131
|
return;
|
|
128
132
|
}
|
|
133
|
+
const isStale = () => request !== loadRequest || props.entryId !== id || !open.value;
|
|
129
134
|
loading.value = true;
|
|
130
135
|
try {
|
|
131
|
-
const entry = await cmsApi(`${endpoint.value}/${
|
|
136
|
+
const entry = await cmsApi(`${endpoint.value}/${id}`);
|
|
137
|
+
if (isStale()) return;
|
|
132
138
|
setState(pickFields(entry));
|
|
133
139
|
} catch (error) {
|
|
140
|
+
if (isStale()) return;
|
|
134
141
|
toast.add({
|
|
135
142
|
title: "Could not load entry",
|
|
136
143
|
description: errorMessage(error),
|
|
@@ -138,7 +145,7 @@ async function load() {
|
|
|
138
145
|
});
|
|
139
146
|
open.value = false;
|
|
140
147
|
} finally {
|
|
141
|
-
loading.value = false;
|
|
148
|
+
if (request === loadRequest) loading.value = false;
|
|
142
149
|
}
|
|
143
150
|
}
|
|
144
151
|
watch(
|
|
@@ -165,6 +172,7 @@ async function save() {
|
|
|
165
172
|
emit("saved");
|
|
166
173
|
open.value = false;
|
|
167
174
|
} catch (error) {
|
|
175
|
+
revertStatus();
|
|
168
176
|
toast.add({
|
|
169
177
|
title: "Save failed",
|
|
170
178
|
description: errorMessage(error),
|
|
@@ -125,7 +125,6 @@ watch(tabList, (tabs) => {
|
|
|
125
125
|
if (!tabs.some((tab) => tab.id === activeTab.value)) activeTab.value = tabs[0]?.id ?? "";
|
|
126
126
|
});
|
|
127
127
|
const settings = useCmsSettingsState();
|
|
128
|
-
const fields = computed(() => props.fields);
|
|
129
128
|
const layout = computed(
|
|
130
129
|
() => props.entryName ? entryFormLayout(props.entryName, settings.value, props.fields) : resolveFormLayout(props.fields, void 0)
|
|
131
130
|
);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
class="cms-input-wrap"
|
|
4
|
+
:class="[$attrs.class, { 'has-lead': icon }]"
|
|
5
|
+
:style="$attrs.style"
|
|
6
|
+
>
|
|
3
7
|
<span v-if="icon" class="cms-input-lead">
|
|
4
8
|
<CmsIcon :name="icon" class="size-4" />
|
|
5
9
|
</span>
|
|
@@ -7,7 +11,7 @@
|
|
|
7
11
|
v-bind="inputAttrs"
|
|
8
12
|
:type="type ?? 'text'"
|
|
9
13
|
:value="model ?? ''"
|
|
10
|
-
:class="['cms-field', `cms-field-${size ?? 'md'}
|
|
14
|
+
:class="['cms-field', `cms-field-${size ?? 'md'}`]"
|
|
11
15
|
@input="onInput"
|
|
12
16
|
/>
|
|
13
17
|
<span v-if="$slots.trailing" class="cms-input-trail">
|
|
@@ -22,8 +26,7 @@ defineOptions({ inheritAttrs: false });
|
|
|
22
26
|
defineProps({
|
|
23
27
|
type: { type: String, required: false },
|
|
24
28
|
size: { type: String, required: false },
|
|
25
|
-
icon: { type: String, required: false }
|
|
26
|
-
color: { type: String, required: false }
|
|
29
|
+
icon: { type: String, required: false }
|
|
27
30
|
});
|
|
28
31
|
const [model, modifiers] = defineModel({ type: [String, Number, null] });
|
|
29
32
|
const attrs = useAttrs();
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
type="button"
|
|
7
7
|
role="tab"
|
|
8
8
|
:aria-selected="model === locale"
|
|
9
|
-
:title="filled(locale) ? locale : `${locale}
|
|
9
|
+
:title="filled(locale) ? locale : `${locale}: empty`"
|
|
10
10
|
class="cms-locale-option"
|
|
11
11
|
:class="{ 'is-active': model === locale, 'is-empty': !filled(locale) }"
|
|
12
12
|
@click="model = locale"
|
|
@@ -50,8 +50,7 @@ import { computed, ref } from "#imports";
|
|
|
50
50
|
import { isWithinMediaFolder, mediaFolderDepth, mediaFolderName } from "#nuxt-cms";
|
|
51
51
|
const props = defineProps({
|
|
52
52
|
folders: { type: Array, required: true },
|
|
53
|
-
exclude: { type: [String, null], required: false }
|
|
54
|
-
disabled: { type: Array, required: false }
|
|
53
|
+
exclude: { type: [String, null], required: false }
|
|
55
54
|
});
|
|
56
55
|
const model = defineModel({ type: [String, null], ...{ default: null } });
|
|
57
56
|
const query = ref("");
|
|
@@ -60,7 +59,6 @@ const visible = computed(() => {
|
|
|
60
59
|
return term ? props.folders.filter((folder) => folder.includes(term)) : props.folders;
|
|
61
60
|
});
|
|
62
61
|
function isDisabled(folder) {
|
|
63
|
-
if (props.disabled?.includes(folder)) return true;
|
|
64
62
|
return !!props.exclude && !!folder && isWithinMediaFolder(folder, props.exclude);
|
|
65
63
|
}
|
|
66
64
|
</script>
|
|
@@ -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> </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,
|
|
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
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
|
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
|
|
@@ -27,7 +25,8 @@
|
|
|
27
25
|
</template>
|
|
28
26
|
|
|
29
27
|
<script setup>
|
|
30
|
-
import {
|
|
28
|
+
import { watch } from "#imports";
|
|
29
|
+
import { useCmsOverlay } from "../../composables/cms-overlay";
|
|
31
30
|
defineProps({
|
|
32
31
|
title: { type: String, required: false },
|
|
33
32
|
size: { type: String, required: false }
|
|
@@ -37,19 +36,14 @@ const emit = defineEmits(["after:leave"]);
|
|
|
37
36
|
function close() {
|
|
38
37
|
open.value = false;
|
|
39
38
|
}
|
|
40
|
-
|
|
41
|
-
if (event.key === "Escape") close();
|
|
42
|
-
}
|
|
39
|
+
const overlay = useCmsOverlay(close);
|
|
43
40
|
watch(open, (value, previous) => {
|
|
44
41
|
if (import.meta.server) return;
|
|
45
42
|
if (value) {
|
|
46
|
-
|
|
43
|
+
overlay.activate();
|
|
47
44
|
} else {
|
|
48
|
-
|
|
45
|
+
overlay.deactivate();
|
|
49
46
|
if (previous) emit("after:leave");
|
|
50
47
|
}
|
|
51
48
|
});
|
|
52
|
-
onBeforeUnmount(() => {
|
|
53
|
-
if (import.meta.client) document.removeEventListener("keydown", onKeydown);
|
|
54
|
-
});
|
|
55
49
|
</script>
|
|
@@ -67,11 +67,15 @@ function regenerate() {
|
|
|
67
67
|
}
|
|
68
68
|
async function copyPassword() {
|
|
69
69
|
if (!model.value) return;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
try {
|
|
71
|
+
await navigator.clipboard.writeText(model.value);
|
|
72
|
+
copied.value = true;
|
|
73
|
+
clearTimeout(copiedTimer);
|
|
74
|
+
copiedTimer = setTimeout(() => {
|
|
75
|
+
copied.value = false;
|
|
76
|
+
}, 1600);
|
|
77
|
+
} catch {
|
|
74
78
|
copied.value = false;
|
|
75
|
-
}
|
|
79
|
+
}
|
|
76
80
|
}
|
|
77
81
|
</script>
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
<script setup>
|
|
24
24
|
import { isTranslatableField } from "#nuxt-cms";
|
|
25
|
-
import { computed, onMounted, ref, watch } from "#imports";
|
|
25
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from "#imports";
|
|
26
26
|
import cmsConfig from "#cms-config";
|
|
27
27
|
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
28
28
|
import { useCmsToast } from "../../composables/cms-toast";
|
|
@@ -40,22 +40,26 @@ const fetched = ref([]);
|
|
|
40
40
|
const selectedRows = ref(/* @__PURE__ */ new Map());
|
|
41
41
|
const loading = ref(true);
|
|
42
42
|
const searchTerm = ref("");
|
|
43
|
+
let fetchRequest = 0;
|
|
43
44
|
async function fetchOptions() {
|
|
45
|
+
const request = ++fetchRequest;
|
|
44
46
|
loading.value = true;
|
|
45
47
|
try {
|
|
46
48
|
const term = searchTerm.value.trim();
|
|
47
49
|
const page = await $fetch(endpoint, {
|
|
48
50
|
query: { light: "true", limit: 25, ...term ? { search: term } : {} }
|
|
49
51
|
});
|
|
52
|
+
if (request !== fetchRequest) return;
|
|
50
53
|
fetched.value = page.items;
|
|
51
54
|
} catch (err) {
|
|
55
|
+
if (request !== fetchRequest) return;
|
|
52
56
|
toast.add({
|
|
53
57
|
title: "Loading failed",
|
|
54
58
|
description: errorMessage(err),
|
|
55
59
|
color: "error"
|
|
56
60
|
});
|
|
57
61
|
} finally {
|
|
58
|
-
loading.value = false;
|
|
62
|
+
if (request === fetchRequest) loading.value = false;
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
65
|
let searchTimer;
|
|
@@ -63,6 +67,7 @@ watch(searchTerm, () => {
|
|
|
63
67
|
clearTimeout(searchTimer);
|
|
64
68
|
searchTimer = setTimeout(() => void fetchOptions(), 300);
|
|
65
69
|
});
|
|
70
|
+
onBeforeUnmount(() => clearTimeout(searchTimer));
|
|
66
71
|
const selectedIds = computed(() => {
|
|
67
72
|
if (props.multiple) return model.value ?? [];
|
|
68
73
|
return model.value ? [model.value] : [];
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div ref="root" class="relative w-full">
|
|
3
|
-
<div
|
|
4
|
-
class="cms-field flex flex-wrap items-center gap-1"
|
|
5
|
-
:class="`cms-field-${size ?? 'md'}`"
|
|
6
|
-
@click="focusInput"
|
|
7
|
-
>
|
|
3
|
+
<div class="cms-field cms-field-md flex flex-wrap items-center gap-1" @click="focusInput">
|
|
8
4
|
<template v-if="multiple">
|
|
9
5
|
<span v-for="value in modelArray" :key="value" class="cms-tag">
|
|
10
6
|
{{ labelFor(value) }}
|
|
@@ -70,7 +66,6 @@ const props = defineProps({
|
|
|
70
66
|
multiple: { type: Boolean, required: false },
|
|
71
67
|
ignoreFilter: { type: Boolean, required: false },
|
|
72
68
|
loading: { type: Boolean, required: false },
|
|
73
|
-
size: { type: String, required: false },
|
|
74
69
|
placeholder: { type: String, required: false }
|
|
75
70
|
});
|
|
76
71
|
const model = defineModel({ type: [String, Array, null] });
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
</template>
|
|
56
56
|
|
|
57
57
|
<script setup>
|
|
58
|
-
import { computed, ref
|
|
58
|
+
import { computed, ref } from "#imports";
|
|
59
59
|
import cmsConfig from "#cms-config";
|
|
60
60
|
import {
|
|
61
61
|
entryFields,
|
|
@@ -89,13 +89,6 @@ const parts = computed(() => [
|
|
|
89
89
|
{ id: "layout", label: "Form layout" }
|
|
90
90
|
]);
|
|
91
91
|
const activePart = ref(parts.value[0].id);
|
|
92
|
-
watch(
|
|
93
|
-
() => props.name,
|
|
94
|
-
() => {
|
|
95
|
-
activePart.value = parts.value[0].id;
|
|
96
|
-
status.value = "idle";
|
|
97
|
-
}
|
|
98
|
-
);
|
|
99
92
|
const columns = computed(() => entryListColumns(props.name, settings.value));
|
|
100
93
|
const layout = computed(() => entryFormLayout(props.name, settings.value, fields.value));
|
|
101
94
|
const isCustom = computed(() => {
|
|
@@ -132,10 +132,11 @@
|
|
|
132
132
|
</template>
|
|
133
133
|
|
|
134
134
|
<script setup>
|
|
135
|
-
import { computed,
|
|
135
|
+
import { computed, ref, watch } from "#imports";
|
|
136
136
|
import cmsConfig from "#cms-config";
|
|
137
137
|
import { CMS_THEMES, setCmsTheme } from "../../composables/cms-theme";
|
|
138
138
|
import { useCmsAccount } from "../../composables/cms-account";
|
|
139
|
+
import { useCmsOverlay } from "../../composables/cms-overlay";
|
|
139
140
|
const { isSuperAdmin } = useCmsAccount();
|
|
140
141
|
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
141
142
|
const theme = defineModel("theme", { type: String, ...{ required: true } });
|
|
@@ -165,15 +166,9 @@ function previewHalves(value) {
|
|
|
165
166
|
function close() {
|
|
166
167
|
open.value = false;
|
|
167
168
|
}
|
|
168
|
-
|
|
169
|
-
if (event.key === "Escape") close();
|
|
170
|
-
}
|
|
169
|
+
const overlay = useCmsOverlay(close);
|
|
171
170
|
watch(open, (value) => {
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
else document.removeEventListener("keydown", onKeydown);
|
|
175
|
-
});
|
|
176
|
-
onBeforeUnmount(() => {
|
|
177
|
-
if (import.meta.client) document.removeEventListener("keydown", onKeydown);
|
|
171
|
+
if (value) overlay.activate();
|
|
172
|
+
else overlay.deactivate();
|
|
178
173
|
});
|
|
179
174
|
</script>
|