@xleddyl/nuxt-cms 0.1.60 → 0.2.0
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 +137 -9
- package/dist/runtime/app/components/cms/BlocksField.vue +86 -10
- package/dist/runtime/app/components/cms/BrandMark.d.vue.ts +6 -0
- package/dist/runtime/app/components/cms/BrandMark.vue +19 -0
- package/dist/runtime/app/components/cms/BrandMark.vue.d.ts +6 -0
- package/dist/runtime/app/components/cms/Checkbox.d.vue.ts +11 -0
- package/dist/runtime/app/components/cms/Checkbox.vue +21 -0
- package/dist/runtime/app/components/cms/Checkbox.vue.d.ts +11 -0
- package/dist/runtime/app/components/cms/EmptyState.vue +4 -2
- package/dist/runtime/app/components/cms/EntryDrawer.vue +1 -0
- package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +1 -0
- package/dist/runtime/app/components/cms/EntryForm.vue +105 -30
- package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +1 -0
- package/dist/runtime/app/components/cms/FieldInput.vue +20 -5
- package/dist/runtime/app/components/cms/FormField.d.vue.ts +5 -3
- package/dist/runtime/app/components/cms/FormField.vue +8 -2
- package/dist/runtime/app/components/cms/FormField.vue.d.ts +5 -3
- package/dist/runtime/app/components/cms/Icon.vue +1 -1
- 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 +927 -336
- package/dist/runtime/app/components/cms/MediaGallery.vue.d.ts +8 -2
- package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +1 -0
- package/dist/runtime/app/components/cms/MediaUpload.vue +76 -139
- package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +1 -0
- package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +5 -3
- package/dist/runtime/app/components/cms/PageHeader.vue +16 -6
- package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +5 -3
- package/dist/runtime/app/components/cms/PasswordInput.d.vue.ts +17 -0
- package/dist/runtime/app/components/cms/PasswordInput.vue +77 -0
- package/dist/runtime/app/components/cms/PasswordInput.vue.d.ts +17 -0
- package/dist/runtime/app/components/cms/SelectMenu.vue +3 -5
- package/dist/runtime/app/components/cms/SettingsAccount.d.vue.ts +3 -0
- package/dist/runtime/app/components/cms/SettingsAccount.vue +138 -0
- package/dist/runtime/app/components/cms/SettingsAccount.vue.d.ts +3 -0
- package/dist/runtime/app/components/cms/SettingsColumns.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsColumns.vue +102 -0
- package/dist/runtime/app/components/cms/SettingsColumns.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsEntry.d.vue.ts +6 -0
- package/dist/runtime/app/components/cms/SettingsEntry.vue +133 -0
- package/dist/runtime/app/components/cms/SettingsEntry.vue.d.ts +6 -0
- package/dist/runtime/app/components/cms/SettingsLayout.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsLayout.vue +177 -0
- package/dist/runtime/app/components/cms/SettingsLayout.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsModal.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsModal.vue +179 -0
- package/dist/runtime/app/components/cms/SettingsModal.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsUsers.d.vue.ts +3 -0
- package/dist/runtime/app/components/cms/SettingsUsers.vue +270 -0
- package/dist/runtime/app/components/cms/SettingsUsers.vue.d.ts +3 -0
- package/dist/runtime/app/components/cms/Spinner.vue +2 -2
- package/dist/runtime/app/components/cms/Table.d.vue.ts +7 -2
- package/dist/runtime/app/components/cms/Table.vue +38 -1
- package/dist/runtime/app/components/cms/Table.vue.d.ts +7 -2
- package/dist/runtime/app/components/cms/Toaster.vue +1 -1
- package/dist/runtime/app/components/cms/WelcomeModal.d.vue.ts +3 -0
- package/dist/runtime/app/components/cms/WelcomeModal.vue +69 -0
- package/dist/runtime/app/components/cms/WelcomeModal.vue.d.ts +3 -0
- package/dist/runtime/app/composables/cms-account.d.ts +11 -0
- package/dist/runtime/app/composables/cms-account.js +23 -0
- 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 +22 -0
- package/dist/runtime/app/composables/cms-media-uploader.js +244 -0
- package/dist/runtime/app/composables/cms-settings.d.ts +13 -0
- package/dist/runtime/app/composables/cms-settings.js +59 -0
- package/dist/runtime/app/composables/cms-theme.d.ts +9 -0
- package/dist/runtime/app/composables/cms-theme.js +27 -0
- package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
- package/dist/runtime/app/layouts/cms-admin.vue +71 -17
- package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
- package/dist/runtime/app/pages/admin-collection.vue +127 -101
- package/dist/runtime/app/pages/admin-entry.vue +1 -0
- package/dist/runtime/app/pages/admin-login.vue +18 -3
- package/dist/runtime/app/pages/admin-media.vue +19 -3
- package/dist/runtime/app/utils/ui.d.ts +2 -0
- package/dist/runtime/app/utils/ui.js +18 -0
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/server/api/account-password.put.d.ts +4 -0
- package/dist/runtime/server/api/account-password.put.js +38 -0
- package/dist/runtime/server/api/account-welcome.post.d.ts +4 -0
- package/dist/runtime/server/api/account-welcome.post.js +14 -0
- package/dist/runtime/server/api/account.put.d.ts +3 -0
- package/dist/runtime/server/api/account.put.js +25 -0
- 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/api/settings.delete.d.ts +4 -0
- package/dist/runtime/server/api/settings.delete.js +12 -0
- package/dist/runtime/server/api/settings.get.d.ts +3 -0
- package/dist/runtime/server/api/settings.get.js +15 -0
- package/dist/runtime/server/api/settings.put.d.ts +3 -0
- package/dist/runtime/server/api/settings.put.js +14 -0
- package/dist/runtime/server/api/user.delete.d.ts +4 -0
- package/dist/runtime/server/api/user.delete.js +12 -0
- package/dist/runtime/server/api/user.put.d.ts +3 -0
- package/dist/runtime/server/api/user.put.js +27 -0
- package/dist/runtime/server/api/users.get.d.ts +3 -0
- package/dist/runtime/server/api/users.get.js +11 -0
- package/dist/runtime/server/api/users.post.d.ts +3 -0
- package/dist/runtime/server/api/users.post.js +32 -0
- package/dist/runtime/server/plugins/session-validate.d.ts +2 -0
- package/dist/runtime/server/plugins/session-validate.js +13 -0
- package/dist/runtime/server/routes/auth/login.post.js +18 -8
- 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/server/utils/password.d.ts +3 -0
- package/dist/runtime/server/utils/password.js +31 -0
- package/dist/runtime/server/utils/require-admin.d.ts +6 -2
- package/dist/runtime/server/utils/require-admin.js +33 -28
- package/dist/runtime/server/utils/same-origin.d.ts +2 -0
- package/dist/runtime/server/utils/same-origin.js +24 -0
- package/dist/runtime/server/utils/settings.d.ts +16 -0
- package/dist/runtime/server/utils/settings.js +31 -0
- package/dist/runtime/server/utils/users.d.ts +15 -0
- package/dist/runtime/server/utils/users.js +34 -0
- package/dist/runtime/shared/index.d.ts +29 -1
- package/dist/runtime/shared/index.js +36 -2
- package/dist/runtime/shared/layout.d.ts +34 -0
- package/dist/runtime/shared/layout.js +130 -0
- package/dist/runtime/shared/users.d.ts +18 -0
- package/dist/runtime/shared/users.js +1 -0
- package/dist/runtime/shared/validation.js +5 -4
- package/package.json +3 -3
|
@@ -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;
|
|
@@ -4,6 +4,7 @@ type __VLS_Props = {
|
|
|
4
4
|
mediaType?: MediaType | MediaType[];
|
|
5
5
|
accept?: string[];
|
|
6
6
|
folder?: string | null;
|
|
7
|
+
directories?: boolean;
|
|
7
8
|
};
|
|
8
9
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
10
|
uploaded: (items: MediaItem[]) => any;
|
|
@@ -1,165 +1,102 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
2
|
+
<div class="flex flex-col items-center gap-2">
|
|
3
|
+
<button
|
|
4
|
+
type="button"
|
|
5
|
+
class="cms-dropzone"
|
|
6
|
+
:class="{ 'is-over': dragOver, 'is-busy': uploading }"
|
|
7
|
+
:disabled="uploading"
|
|
8
|
+
@click="input?.click()"
|
|
9
|
+
@dragover.prevent="dragOver = true"
|
|
10
|
+
@dragleave.prevent="dragOver = false"
|
|
11
|
+
@drop.prevent.stop="onDrop"
|
|
12
|
+
>
|
|
13
|
+
<input
|
|
14
|
+
ref="input"
|
|
15
|
+
type="file"
|
|
16
|
+
class="hidden"
|
|
17
|
+
:accept="acceptAttr"
|
|
18
|
+
:multiple="multiple"
|
|
19
|
+
@change="onChange"
|
|
20
|
+
@click.stop
|
|
21
|
+
/>
|
|
22
|
+
<CmsIcon
|
|
23
|
+
:name="uploading ? 'arrow-path' : 'cloud-arrow-up'"
|
|
24
|
+
class="size-5 shrink-0"
|
|
25
|
+
:class="{ 'animate-spin': uploading }"
|
|
26
|
+
/>
|
|
27
|
+
<span class="text-sm font-medium">
|
|
28
|
+
{{
|
|
29
|
+
uploading ? `Uploading ${Math.min(done + 1, total)} of ${total}\u2026` : multiple ? "Drop files here or click to browse" : "Drop a file here or click to browse"
|
|
30
|
+
}}
|
|
31
|
+
</span>
|
|
32
|
+
<span v-if="!uploading" class="cms-dropzone-hint">
|
|
33
|
+
<CmsIcon :name="folder ? 'folder' : 'circle-stack'" class="size-3.5" />
|
|
34
|
+
{{ folder ?? "Library root" }}
|
|
35
|
+
</span>
|
|
36
|
+
</button>
|
|
37
|
+
<template v-if="directories && multiple && !uploading">
|
|
38
|
+
<input
|
|
39
|
+
ref="directoryInput"
|
|
40
|
+
type="file"
|
|
41
|
+
class="hidden"
|
|
42
|
+
webkitdirectory
|
|
43
|
+
multiple
|
|
44
|
+
@change="onChange"
|
|
45
|
+
/>
|
|
46
|
+
<CmsButton
|
|
47
|
+
label="Upload a folder"
|
|
48
|
+
icon="folder-arrow-down"
|
|
49
|
+
variant="ghost"
|
|
50
|
+
color="neutral"
|
|
51
|
+
size="sm"
|
|
52
|
+
@click="directoryInput?.click()"
|
|
53
|
+
/>
|
|
54
|
+
</template>
|
|
55
|
+
</div>
|
|
36
56
|
</template>
|
|
37
57
|
|
|
38
58
|
<script setup>
|
|
39
|
-
import {
|
|
59
|
+
import { mediaTypeAccept } from "#nuxt-cms";
|
|
40
60
|
import { computed, ref } from "#imports";
|
|
41
|
-
import {
|
|
42
|
-
|
|
61
|
+
import {
|
|
62
|
+
uploadTreeFromDrop,
|
|
63
|
+
uploadTreeFromFiles,
|
|
64
|
+
useCmsMediaUploader
|
|
65
|
+
} from "../../composables/cms-media-uploader";
|
|
43
66
|
const props = defineProps({
|
|
44
67
|
multiple: { type: Boolean, required: false },
|
|
45
68
|
mediaType: { type: null, required: false, default: "file" },
|
|
46
69
|
accept: { type: Array, required: false },
|
|
47
|
-
folder: { type: [String, null], required: false, default: null }
|
|
70
|
+
folder: { type: [String, null], required: false, default: null },
|
|
71
|
+
directories: { type: Boolean, required: false }
|
|
48
72
|
});
|
|
49
73
|
const emit = defineEmits(["uploaded"]);
|
|
50
|
-
const
|
|
51
|
-
const { mediaMaxFileSize } = useCmsRuntime();
|
|
52
|
-
async function imageDimensions(file) {
|
|
53
|
-
try {
|
|
54
|
-
const bitmap = await createImageBitmap(file);
|
|
55
|
-
const dims = { width: bitmap.width, height: bitmap.height };
|
|
56
|
-
bitmap.close();
|
|
57
|
-
return dims;
|
|
58
|
-
} catch {
|
|
59
|
-
return {};
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function videoDimensions(file) {
|
|
63
|
-
return new Promise((resolve) => {
|
|
64
|
-
const url = URL.createObjectURL(file);
|
|
65
|
-
const video = document.createElement("video");
|
|
66
|
-
const settle = (dims) => {
|
|
67
|
-
URL.revokeObjectURL(url);
|
|
68
|
-
resolve(dims);
|
|
69
|
-
};
|
|
70
|
-
video.preload = "metadata";
|
|
71
|
-
video.muted = true;
|
|
72
|
-
video.onloadedmetadata = () => settle(
|
|
73
|
-
video.videoWidth && video.videoHeight ? { width: video.videoWidth, height: video.videoHeight } : {}
|
|
74
|
-
);
|
|
75
|
-
video.onerror = () => settle({});
|
|
76
|
-
video.src = url;
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
async function mediaDimensions(file) {
|
|
80
|
-
if (file.type.startsWith("image/")) return imageDimensions(file);
|
|
81
|
-
if (file.type.startsWith("video/")) return videoDimensions(file);
|
|
82
|
-
return {};
|
|
83
|
-
}
|
|
74
|
+
const { upload, uploading, done, total } = useCmsMediaUploader();
|
|
84
75
|
const input = ref(null);
|
|
76
|
+
const directoryInput = ref(null);
|
|
85
77
|
const dragOver = ref(false);
|
|
86
|
-
const uploading = ref(false);
|
|
87
|
-
const done = ref(0);
|
|
88
|
-
const total = ref(0);
|
|
89
78
|
const acceptAttr = computed(
|
|
90
79
|
() => (props.accept?.length ? props.accept : mediaTypeAccept(props.mediaType))?.join(",")
|
|
91
80
|
);
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return file.type.toLowerCase() === pattern;
|
|
81
|
+
async function handleTree(pending) {
|
|
82
|
+
const tree = await pending;
|
|
83
|
+
const items = await upload(tree, {
|
|
84
|
+
folder: props.folder,
|
|
85
|
+
accept: acceptAttr.value,
|
|
86
|
+
multiple: props.multiple
|
|
99
87
|
});
|
|
100
|
-
|
|
101
|
-
async function uploadOne(file) {
|
|
102
|
-
const presign = await $fetch("/api/cms/admin/media/presign", {
|
|
103
|
-
method: "POST",
|
|
104
|
-
body: {
|
|
105
|
-
filename: file.name,
|
|
106
|
-
contentType: file.type || "application/octet-stream",
|
|
107
|
-
size: file.size,
|
|
108
|
-
folder: props.folder
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
const res = await fetch(presign.uploadUrl, {
|
|
112
|
-
method: "PUT",
|
|
113
|
-
headers: presign.headers,
|
|
114
|
-
body: file
|
|
115
|
-
});
|
|
116
|
-
if (!res.ok) throw new Error(`Upload failed (${res.status})`);
|
|
117
|
-
const item = await $fetch("/api/cms/admin/media", {
|
|
118
|
-
method: "POST",
|
|
119
|
-
body: {
|
|
120
|
-
key: presign.key,
|
|
121
|
-
folder: presign.folder,
|
|
122
|
-
mime: file.type || null,
|
|
123
|
-
size: file.size,
|
|
124
|
-
...await mediaDimensions(file)
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
done.value++;
|
|
128
|
-
return item;
|
|
129
|
-
}
|
|
130
|
-
async function handleFiles(list) {
|
|
131
|
-
let files = Array.from(list).filter(matchesAccept);
|
|
132
|
-
const tooLarge = files.filter((file) => file.size > mediaMaxFileSize);
|
|
133
|
-
for (const file of tooLarge) {
|
|
134
|
-
toast.add({
|
|
135
|
-
title: `File too large (max ${formatFileSize(mediaMaxFileSize)}): ${file.name}`,
|
|
136
|
-
color: "error"
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
files = files.filter((file) => file.size <= mediaMaxFileSize);
|
|
140
|
-
if (!props.multiple) files = files.slice(0, 1);
|
|
141
|
-
if (!files.length || uploading.value) return;
|
|
142
|
-
uploading.value = true;
|
|
143
|
-
done.value = 0;
|
|
144
|
-
total.value = files.length;
|
|
145
|
-
try {
|
|
146
|
-
const results = await Promise.allSettled(files.map(uploadOne));
|
|
147
|
-
const ok = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
148
|
-
const failed = results.length - ok.length;
|
|
149
|
-
if (failed)
|
|
150
|
-
toast.add({ title: `${failed} upload${failed > 1 ? "s" : ""} failed`, color: "error" });
|
|
151
|
-
if (ok.length) emit("uploaded", ok);
|
|
152
|
-
} finally {
|
|
153
|
-
uploading.value = false;
|
|
154
|
-
}
|
|
88
|
+
if (items.length || props.directories && tree.directories.length) emit("uploaded", items);
|
|
155
89
|
}
|
|
156
90
|
function onChange(event) {
|
|
157
91
|
const el = event.target;
|
|
158
|
-
if (el.files?.length) void
|
|
92
|
+
if (el.files?.length) void handleTree(uploadTreeFromFiles(el.files));
|
|
159
93
|
el.value = "";
|
|
160
94
|
}
|
|
161
95
|
function onDrop(event) {
|
|
162
96
|
dragOver.value = false;
|
|
163
|
-
if (event.dataTransfer?.files.length)
|
|
97
|
+
if (!event.dataTransfer?.files.length) return;
|
|
98
|
+
void handleTree(
|
|
99
|
+
props.directories ? uploadTreeFromDrop(event.dataTransfer) : uploadTreeFromFiles(event.dataTransfer.files)
|
|
100
|
+
);
|
|
164
101
|
}
|
|
165
102
|
</script>
|
|
@@ -4,6 +4,7 @@ type __VLS_Props = {
|
|
|
4
4
|
mediaType?: MediaType | MediaType[];
|
|
5
5
|
accept?: string[];
|
|
6
6
|
folder?: string | null;
|
|
7
|
+
directories?: boolean;
|
|
7
8
|
};
|
|
8
9
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
10
|
uploaded: (items: MediaItem[]) => any;
|
|
@@ -4,13 +4,15 @@ export interface CmsBreadcrumbLink {
|
|
|
4
4
|
}
|
|
5
5
|
type __VLS_Props = {
|
|
6
6
|
title: string;
|
|
7
|
+
icon?: string;
|
|
8
|
+
description?: string;
|
|
7
9
|
links?: CmsBreadcrumbLink[];
|
|
8
10
|
};
|
|
9
|
-
declare var
|
|
11
|
+
declare var __VLS_17: {}, __VLS_19: {};
|
|
10
12
|
type __VLS_Slots = {} & {
|
|
11
|
-
badge?: (props: typeof
|
|
13
|
+
badge?: (props: typeof __VLS_17) => any;
|
|
12
14
|
} & {
|
|
13
|
-
default?: (props: typeof
|
|
15
|
+
default?: (props: typeof __VLS_19) => any;
|
|
14
16
|
};
|
|
15
17
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
18
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -4,14 +4,22 @@
|
|
|
4
4
|
<nav v-if="links?.length" class="cms-breadcrumb">
|
|
5
5
|
<template v-for="link in links" :key="link.to">
|
|
6
6
|
<NuxtLink :to="link.to" class="cms-breadcrumb-link">{{ link.label }}</NuxtLink>
|
|
7
|
-
<CmsIcon name="chevron-right" class="cms-breadcrumb-separator size-
|
|
7
|
+
<CmsIcon name="chevron-right" class="cms-breadcrumb-separator size-3" />
|
|
8
8
|
</template>
|
|
9
9
|
</nav>
|
|
10
|
-
<div class="cms-
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
</
|
|
14
|
-
<
|
|
10
|
+
<div class="cms-page-title-row">
|
|
11
|
+
<span v-if="icon" class="cms-page-icon">
|
|
12
|
+
<CmsIcon :name="icon" class="size-[18px]" />
|
|
13
|
+
</span>
|
|
14
|
+
<div class="cms-page-title-text">
|
|
15
|
+
<div class="cms-actions">
|
|
16
|
+
<h1 class="cms-title cms-title-md">
|
|
17
|
+
{{ title }}
|
|
18
|
+
</h1>
|
|
19
|
+
<slot name="badge" />
|
|
20
|
+
</div>
|
|
21
|
+
<p v-if="description" class="cms-page-description">{{ description }}</p>
|
|
22
|
+
</div>
|
|
15
23
|
</div>
|
|
16
24
|
</div>
|
|
17
25
|
<slot />
|
|
@@ -21,6 +29,8 @@
|
|
|
21
29
|
<script setup>
|
|
22
30
|
defineProps({
|
|
23
31
|
title: { type: String, required: true },
|
|
32
|
+
icon: { type: String, required: false },
|
|
33
|
+
description: { type: String, required: false },
|
|
24
34
|
links: { type: Array, required: false }
|
|
25
35
|
});
|
|
26
36
|
</script>
|
|
@@ -4,13 +4,15 @@ export interface CmsBreadcrumbLink {
|
|
|
4
4
|
}
|
|
5
5
|
type __VLS_Props = {
|
|
6
6
|
title: string;
|
|
7
|
+
icon?: string;
|
|
8
|
+
description?: string;
|
|
7
9
|
links?: CmsBreadcrumbLink[];
|
|
8
10
|
};
|
|
9
|
-
declare var
|
|
11
|
+
declare var __VLS_17: {}, __VLS_19: {};
|
|
10
12
|
type __VLS_Slots = {} & {
|
|
11
|
-
badge?: (props: typeof
|
|
13
|
+
badge?: (props: typeof __VLS_17) => any;
|
|
12
14
|
} & {
|
|
13
|
-
default?: (props: typeof
|
|
15
|
+
default?: (props: typeof __VLS_19) => any;
|
|
14
16
|
};
|
|
15
17
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
18
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
generate?: boolean;
|
|
3
|
+
copy?: boolean;
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
autocomplete?: string;
|
|
6
|
+
};
|
|
7
|
+
type __VLS_ModelProps = {
|
|
8
|
+
modelValue: string;
|
|
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, {
|
|
12
|
+
"update:modelValue": (value: string) => any;
|
|
13
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
14
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
15
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
|
+
declare const _default: typeof __VLS_export;
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<CmsInput
|
|
3
|
+
v-model="model"
|
|
4
|
+
:type="visible ? 'text' : 'password'"
|
|
5
|
+
icon="key"
|
|
6
|
+
class="cms-password-input"
|
|
7
|
+
:autocomplete="autocomplete ?? 'new-password'"
|
|
8
|
+
:placeholder="placeholder"
|
|
9
|
+
>
|
|
10
|
+
<template #trailing>
|
|
11
|
+
<span class="cms-password-actions">
|
|
12
|
+
<button
|
|
13
|
+
type="button"
|
|
14
|
+
class="cms-password-action"
|
|
15
|
+
:title="visible ? 'Hide password' : 'Show password'"
|
|
16
|
+
:aria-label="visible ? 'Hide password' : 'Show password'"
|
|
17
|
+
@click="visible = !visible"
|
|
18
|
+
>
|
|
19
|
+
<CmsIcon :name="visible ? 'eye-slash' : 'eye'" class="size-4" />
|
|
20
|
+
</button>
|
|
21
|
+
<button
|
|
22
|
+
v-if="generate"
|
|
23
|
+
type="button"
|
|
24
|
+
class="cms-password-action"
|
|
25
|
+
title="Generate a password"
|
|
26
|
+
aria-label="Generate a password"
|
|
27
|
+
@click="regenerate"
|
|
28
|
+
>
|
|
29
|
+
<CmsIcon name="sparkles" class="size-4" />
|
|
30
|
+
</button>
|
|
31
|
+
<button
|
|
32
|
+
v-if="copy"
|
|
33
|
+
type="button"
|
|
34
|
+
class="cms-password-action"
|
|
35
|
+
:class="{ 'is-done': copied }"
|
|
36
|
+
:title="copied ? 'Copied' : 'Copy password'"
|
|
37
|
+
:aria-label="copied ? 'Copied' : 'Copy password'"
|
|
38
|
+
:disabled="!model"
|
|
39
|
+
@click="copyPassword"
|
|
40
|
+
>
|
|
41
|
+
<CmsIcon
|
|
42
|
+
:name="copied ? 'clipboard-document-check' : 'clipboard-document'"
|
|
43
|
+
class="size-4"
|
|
44
|
+
/>
|
|
45
|
+
</button>
|
|
46
|
+
</span>
|
|
47
|
+
</template>
|
|
48
|
+
</CmsInput>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script setup>
|
|
52
|
+
import { ref } from "#imports";
|
|
53
|
+
import { generatePassword } from "../../composables/cms-account";
|
|
54
|
+
defineProps({
|
|
55
|
+
generate: { type: Boolean, required: false },
|
|
56
|
+
copy: { type: Boolean, required: false },
|
|
57
|
+
placeholder: { type: String, required: false },
|
|
58
|
+
autocomplete: { type: String, required: false }
|
|
59
|
+
});
|
|
60
|
+
const model = defineModel({ type: String, ...{ required: true } });
|
|
61
|
+
const visible = ref(false);
|
|
62
|
+
const copied = ref(false);
|
|
63
|
+
let copiedTimer;
|
|
64
|
+
function regenerate() {
|
|
65
|
+
model.value = generatePassword();
|
|
66
|
+
visible.value = true;
|
|
67
|
+
}
|
|
68
|
+
async function copyPassword() {
|
|
69
|
+
if (!model.value) return;
|
|
70
|
+
await navigator.clipboard.writeText(model.value);
|
|
71
|
+
copied.value = true;
|
|
72
|
+
clearTimeout(copiedTimer);
|
|
73
|
+
copiedTimer = setTimeout(() => {
|
|
74
|
+
copied.value = false;
|
|
75
|
+
}, 1600);
|
|
76
|
+
}
|
|
77
|
+
</script>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
generate?: boolean;
|
|
3
|
+
copy?: boolean;
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
autocomplete?: string;
|
|
6
|
+
};
|
|
7
|
+
type __VLS_ModelProps = {
|
|
8
|
+
modelValue: string;
|
|
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, {
|
|
12
|
+
"update:modelValue": (value: string) => any;
|
|
13
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
14
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
15
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
|
+
declare const _default: typeof __VLS_export;
|
|
17
|
+
export default _default;
|
|
@@ -25,11 +25,9 @@
|
|
|
25
25
|
@input="onInput"
|
|
26
26
|
@focus="onFocus"
|
|
27
27
|
/>
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class="cms-selectmenu-affix ml-auto size-4 animate-spin"
|
|
32
|
-
/>
|
|
28
|
+
<span v-if="loading" class="cms-selectmenu-affix ml-auto flex">
|
|
29
|
+
<CmsIcon name="arrow-path" class="size-4 animate-spin" />
|
|
30
|
+
</span>
|
|
33
31
|
<button
|
|
34
32
|
v-else
|
|
35
33
|
type="button"
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cms-settings-entry">
|
|
3
|
+
<div class="cms-settings-entry-header">
|
|
4
|
+
<div class="cms-settings-heading">
|
|
5
|
+
<h2 class="cms-title cms-title-sm">Account</h2>
|
|
6
|
+
<p class="cms-settings-hint">Your profile and your password.</p>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="cms-account-card">
|
|
11
|
+
<span class="cms-avatar is-lg">{{ initial }}</span>
|
|
12
|
+
<div class="cms-account-text">
|
|
13
|
+
<span class="cms-account-name">{{ displayName }}</span>
|
|
14
|
+
<span class="cms-account-email">{{ email }}</span>
|
|
15
|
+
</div>
|
|
16
|
+
<span class="cms-badge is-muted">{{ isSuperAdmin ? "Super admin" : "Admin" }}</span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<CmsAlert v-if="isSuperAdmin" color="neutral" title="Environment account">
|
|
20
|
+
This account signs in with the credentials of the environment variables
|
|
21
|
+
(NUXT_CMS_ADMIN_EMAIL and NUXT_CMS_ADMIN_PASSWORD). Change them there. Only this account
|
|
22
|
+
can manage users.
|
|
23
|
+
</CmsAlert>
|
|
24
|
+
|
|
25
|
+
<template v-else>
|
|
26
|
+
<div class="cms-settings-group">
|
|
27
|
+
<div class="cms-settings-heading">
|
|
28
|
+
<p class="cms-settings-label">Profile</p>
|
|
29
|
+
<p class="cms-settings-hint">The name shows in the sidebar.</p>
|
|
30
|
+
</div>
|
|
31
|
+
<CmsForm :state="profile" @submit="saveProfile">
|
|
32
|
+
<div class="cms-form-row" style="--cms-row-cols: 2">
|
|
33
|
+
<CmsFormField label="Name" name="name">
|
|
34
|
+
<CmsInput v-model="profile.name" placeholder="Your name" autocomplete="name" />
|
|
35
|
+
</CmsFormField>
|
|
36
|
+
<CmsFormField label="Email" name="email">
|
|
37
|
+
<CmsInput :model-value="email" disabled />
|
|
38
|
+
</CmsFormField>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="cms-actions is-end">
|
|
41
|
+
<CmsButton
|
|
42
|
+
type="submit"
|
|
43
|
+
label="Save profile"
|
|
44
|
+
size="sm"
|
|
45
|
+
:loading="savingProfile"
|
|
46
|
+
:disabled="profile.name === (user?.name ?? '')"
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
</CmsForm>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="cms-settings-group">
|
|
53
|
+
<div class="cms-settings-heading">
|
|
54
|
+
<p class="cms-settings-label">Password</p>
|
|
55
|
+
<p class="cms-settings-hint">
|
|
56
|
+
At least {{ MIN_PASSWORD_LENGTH }} characters. Use the sparkle button to generate
|
|
57
|
+
one.
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
<CmsForm :state="password" @submit="savePassword">
|
|
61
|
+
<CmsAlert v-if="passwordError" color="error" :title="passwordError" />
|
|
62
|
+
<CmsFormField label="Current password" name="current">
|
|
63
|
+
<CmsPasswordInput
|
|
64
|
+
v-model="password.current"
|
|
65
|
+
autocomplete="current-password"
|
|
66
|
+
placeholder="••••••••"
|
|
67
|
+
/>
|
|
68
|
+
</CmsFormField>
|
|
69
|
+
<CmsFormField label="New password" name="next">
|
|
70
|
+
<CmsPasswordInput v-model="password.next" generate copy />
|
|
71
|
+
</CmsFormField>
|
|
72
|
+
<div class="cms-actions is-end">
|
|
73
|
+
<CmsButton
|
|
74
|
+
type="submit"
|
|
75
|
+
label="Change password"
|
|
76
|
+
size="sm"
|
|
77
|
+
:loading="savingPassword"
|
|
78
|
+
:disabled="!password.current || password.next.length < MIN_PASSWORD_LENGTH"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</CmsForm>
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
|
84
|
+
</div>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<script setup>
|
|
88
|
+
import { MIN_PASSWORD_LENGTH } from "#nuxt-cms";
|
|
89
|
+
import { ref, watch } from "#imports";
|
|
90
|
+
import { useCmsAccount } from "../../composables/cms-account";
|
|
91
|
+
import { useCmsToast } from "../../composables/cms-toast";
|
|
92
|
+
import { cmsApi } from "../../utils/api";
|
|
93
|
+
import { errorMessage } from "../../utils/ui";
|
|
94
|
+
const { user, email, isSuperAdmin, displayName, initial, refresh } = useCmsAccount();
|
|
95
|
+
const toast = useCmsToast();
|
|
96
|
+
const profile = ref({ name: user.value?.name ?? "" });
|
|
97
|
+
watch(
|
|
98
|
+
() => user.value?.name,
|
|
99
|
+
(name) => {
|
|
100
|
+
profile.value.name = name ?? "";
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
const savingProfile = ref(false);
|
|
104
|
+
async function saveProfile() {
|
|
105
|
+
savingProfile.value = true;
|
|
106
|
+
try {
|
|
107
|
+
await cmsApi("/api/cms/account", {
|
|
108
|
+
method: "PUT",
|
|
109
|
+
body: { name: profile.value.name || null }
|
|
110
|
+
});
|
|
111
|
+
await refresh();
|
|
112
|
+
toast.add({ title: "Profile saved", color: "success" });
|
|
113
|
+
} catch (error) {
|
|
114
|
+
toast.add({ title: "Profile not saved", description: errorMessage(error), color: "error" });
|
|
115
|
+
} finally {
|
|
116
|
+
savingProfile.value = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const password = ref({ current: "", next: "" });
|
|
120
|
+
const passwordError = ref(null);
|
|
121
|
+
const savingPassword = ref(false);
|
|
122
|
+
async function savePassword() {
|
|
123
|
+
savingPassword.value = true;
|
|
124
|
+
passwordError.value = null;
|
|
125
|
+
try {
|
|
126
|
+
await cmsApi("/api/cms/account/password", {
|
|
127
|
+
method: "PUT",
|
|
128
|
+
body: { currentPassword: password.value.current, newPassword: password.value.next }
|
|
129
|
+
});
|
|
130
|
+
password.value = { current: "", next: "" };
|
|
131
|
+
toast.add({ title: "Password changed", color: "success" });
|
|
132
|
+
} catch (error) {
|
|
133
|
+
passwordError.value = errorMessage(error) ?? "Password not changed.";
|
|
134
|
+
} finally {
|
|
135
|
+
savingPassword.value = false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
</script>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FieldConfig } from '#nuxt-cms';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
fields: Record<string, FieldConfig>;
|
|
4
|
+
extra: string[];
|
|
5
|
+
};
|
|
6
|
+
type __VLS_ModelProps = {
|
|
7
|
+
modelValue: string[];
|
|
8
|
+
};
|
|
9
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
+
"update:modelValue": (value: string[]) => any;
|
|
12
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
13
|
+
"onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
|
|
14
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
15
|
+
declare const _default: typeof __VLS_export;
|
|
16
|
+
export default _default;
|