@xleddyl/nuxt-cms 0.2.1 → 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 +1 -1
- 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/MediaGallery.vue +89 -11
- package/dist/runtime/app/components/cms/Modal.vue +0 -2
- 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-media-library.d.ts +1 -2
- package/dist/runtime/app/composables/cms-media-library.js +12 -19
- package/dist/runtime/app/utils/ui.d.ts +0 -2
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/seed.js +11 -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 +1 -1
- package/dist/runtime/server/api/media.post.js +2 -1
- package/dist/runtime/server/api/media.put.js +2 -1
- package/dist/runtime/server/plugins/migrate-sqlite.js +2 -2
- package/dist/runtime/server/routes/auth/login.post.js +28 -22
- 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.js +2 -1
- 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 +14 -23
- package/dist/runtime/server/utils/media.js +2 -0
- package/dist/runtime/server/utils/migrate.d.ts +1 -0
- package/dist/runtime/server/utils/migrate.js +32 -0
- 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/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/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
|
@@ -1126,7 +1126,7 @@ function addCmsTypeTemplates(nuxt, cmsConfig) {
|
|
|
1126
1126
|
``,
|
|
1127
1127
|
`declare const graphql: CmsGraphql`,
|
|
1128
1128
|
``,
|
|
1129
|
-
`// @ts-ignore
|
|
1129
|
+
`// @ts-ignore`,
|
|
1130
1130
|
`export type CmsDocument<Query extends string> = ReturnType<typeof graphql<Query, []>>`,
|
|
1131
1131
|
``,
|
|
1132
1132
|
`export type CmsResult<Query extends string> = string extends Query`,
|
|
@@ -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
|
|
@@ -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
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
export interface
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export interface CmsConfirmLink {
|
|
2
|
+
title: string;
|
|
3
|
+
meta: string;
|
|
4
|
+
to: string;
|
|
5
|
+
}
|
|
6
|
+
export interface CmsConfirmGroup {
|
|
7
|
+
title: string;
|
|
8
|
+
links: CmsConfirmLink[];
|
|
9
|
+
}
|
|
10
|
+
export interface CmsConfirmOptions {
|
|
4
11
|
title?: string;
|
|
5
12
|
confirmLabel?: string;
|
|
13
|
+
groups?: CmsConfirmGroup[];
|
|
14
|
+
}
|
|
15
|
+
export interface CmsConfirmState extends CmsConfirmOptions {
|
|
16
|
+
open: boolean;
|
|
17
|
+
message: string;
|
|
6
18
|
resolve?: (value: boolean) => void;
|
|
7
19
|
}
|
|
8
20
|
export declare function useCmsConfirmState(): import("vue").Ref<CmsConfirmState, CmsConfirmState>;
|
|
9
|
-
export declare function useCmsConfirm(): (message: string, options?:
|
|
10
|
-
title?: string;
|
|
11
|
-
confirmLabel?: string;
|
|
12
|
-
}) => Promise<boolean>;
|
|
21
|
+
export declare function useCmsConfirm(): (message: string, options?: CmsConfirmOptions) => Promise<boolean>;
|
|
@@ -71,5 +71,4 @@ export declare function useCmsMediaLibrary(): {
|
|
|
71
71
|
usage: (keys: string[]) => Promise<Record<string, MediaUsage[]> | null>;
|
|
72
72
|
};
|
|
73
73
|
export type CmsMediaLibrary = ReturnType<typeof useCmsMediaLibrary>;
|
|
74
|
-
export declare function
|
|
75
|
-
export declare function usageLink(usage: MediaUsage): string;
|
|
74
|
+
export declare function usageLink(usage: Pick<MediaUsage, 'collection' | 'id'>): string;
|
|
@@ -9,6 +9,7 @@ import { errorMessage } from "../utils/ui.js";
|
|
|
9
9
|
import { invalidateCmsMediaKeys } from "./cms-media-keys.js";
|
|
10
10
|
import { useCmsToast } from "./cms-toast.js";
|
|
11
11
|
const endpoint = "/api/cms/admin/media";
|
|
12
|
+
const USAGE_CHUNK = 1e3;
|
|
12
13
|
export function useCmsMediaLibrary() {
|
|
13
14
|
const toast = useCmsToast();
|
|
14
15
|
const items = ref([]);
|
|
@@ -106,13 +107,18 @@ export function useCmsMediaLibrary() {
|
|
|
106
107
|
});
|
|
107
108
|
}
|
|
108
109
|
async function usage(keys) {
|
|
109
|
-
|
|
110
|
+
const unique = [...new Set(keys)];
|
|
111
|
+
if (!unique.length) return {};
|
|
110
112
|
try {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
const merged = {};
|
|
114
|
+
for (let index = 0; index < unique.length; index += USAGE_CHUNK) {
|
|
115
|
+
const result = await $fetch(
|
|
116
|
+
`${endpoint}/usage`,
|
|
117
|
+
{ method: "POST", body: { keys: unique.slice(index, index + USAGE_CHUNK) } }
|
|
118
|
+
);
|
|
119
|
+
Object.assign(merged, result.usage);
|
|
120
|
+
}
|
|
121
|
+
return merged;
|
|
116
122
|
} catch {
|
|
117
123
|
return null;
|
|
118
124
|
}
|
|
@@ -136,19 +142,6 @@ export function useCmsMediaLibrary() {
|
|
|
136
142
|
usage
|
|
137
143
|
};
|
|
138
144
|
}
|
|
139
|
-
export function usageSummary(usage, keys) {
|
|
140
|
-
if (!usage) return "";
|
|
141
|
-
const entries = /* @__PURE__ */ new Set();
|
|
142
|
-
let used = 0;
|
|
143
|
-
for (const key of keys) {
|
|
144
|
-
const list = usage[key] ?? [];
|
|
145
|
-
if (list.length) used++;
|
|
146
|
-
for (const item of list) entries.add(`${item.collection}:${item.id ?? ""}`);
|
|
147
|
-
}
|
|
148
|
-
if (!used) return "";
|
|
149
|
-
const files = keys.length === 1 ? "It is" : `${used} of them are`;
|
|
150
|
-
return `${files} used in ${entries.size} entr${entries.size === 1 ? "y" : "ies"}, which will keep a broken link.`;
|
|
151
|
-
}
|
|
152
145
|
export function usageLink(usage) {
|
|
153
146
|
return usage.id === null ? `/cms/${usage.collection}` : `/cms/${usage.collection}/${usage.id}`;
|
|
154
147
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { InjectionKey, Reactive } from 'vue';
|
|
2
2
|
import type { FieldConfig } from '#nuxt-cms';
|
|
3
|
-
/** Shared reactive map of field-name -> error message, provided by CmsForm and
|
|
4
|
-
* read by CmsFormField to render inline validation errors. */
|
|
5
3
|
export declare const CMS_FORM_ERRORS: InjectionKey<Reactive<Record<string, string>>>;
|
|
6
4
|
export declare function errorMessage(error: unknown): string | undefined;
|
|
7
5
|
export declare function fieldIcon(field: FieldConfig): string;
|