@xleddyl/nuxt-cms 0.1.61 → 0.2.1
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 +133 -14
- 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/Drawer.vue +5 -9
- package/dist/runtime/app/components/cms/EmptyState.vue +4 -2
- package/dist/runtime/app/components/cms/EntryDrawer.vue +12 -3
- package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +1 -0
- package/dist/runtime/app/components/cms/EntryForm.vue +104 -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/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 +24 -10
- package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +1 -0
- package/dist/runtime/app/components/cms/MediaUpload.vue +71 -41
- package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +1 -0
- package/dist/runtime/app/components/cms/Modal.vue +5 -9
- 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 +81 -0
- package/dist/runtime/app/components/cms/PasswordInput.vue.d.ts +17 -0
- 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 +4 -11
- package/dist/runtime/app/components/cms/SelectMenu.vue.d.ts +0 -1
- 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 +126 -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 +174 -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 +274 -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 +6 -8
- package/dist/runtime/app/components/cms/Table.vue +42 -47
- package/dist/runtime/app/components/cms/Table.vue.d.ts +6 -8
- 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/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-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.js +3 -0
- package/dist/runtime/app/composables/cms-media-uploader.d.ts +11 -2
- package/dist/runtime/app/composables/cms-media-uploader.js +139 -12
- 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 +12 -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 +2 -0
- package/dist/runtime/app/pages/admin-login.vue +18 -3
- package/dist/runtime/app/pages/admin-media.vue +5 -1
- 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/generate.js +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/collection.get.js +1 -1
- package/dist/runtime/server/api/media.delete.js +2 -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/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 +36 -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 +20 -10
- package/dist/runtime/server/utils/graphql-depth.js +12 -5
- package/dist/runtime/server/utils/graphql.js +2 -2
- package/dist/runtime/server/utils/media-folders.js +10 -2
- package/dist/runtime/server/utils/media.d.ts +1 -0
- package/dist/runtime/server/utils/media.js +4 -1
- package/dist/runtime/server/utils/migrate.js +1 -1
- 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 +4 -2
- package/dist/runtime/server/utils/require-admin.js +32 -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 +13 -1
- package/dist/runtime/shared/index.js +2 -0
- package/dist/runtime/shared/layout.d.ts +33 -0
- package/dist/runtime/shared/layout.js +127 -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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { computed, useUserSession } from "#imports";
|
|
2
|
+
const PASSWORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
|
|
3
|
+
export function generatePassword(length = 16) {
|
|
4
|
+
const values = new Uint32Array(length);
|
|
5
|
+
crypto.getRandomValues(values);
|
|
6
|
+
return Array.from(values, (value) => PASSWORD_ALPHABET[value % PASSWORD_ALPHABET.length]).join(
|
|
7
|
+
""
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
export function initialOf(name, email) {
|
|
11
|
+
return (name?.trim() || email).charAt(0).toUpperCase() || "A";
|
|
12
|
+
}
|
|
13
|
+
export function useCmsAccount() {
|
|
14
|
+
const session = useUserSession();
|
|
15
|
+
const user = computed(() => session.user.value ?? null);
|
|
16
|
+
const email = computed(() => user.value?.email ?? "");
|
|
17
|
+
const isSuperAdmin = computed(() => !!user.value && !user.value.id);
|
|
18
|
+
const displayName = computed(
|
|
19
|
+
() => user.value?.name?.trim() || (isSuperAdmin.value ? "Super admin" : email.value)
|
|
20
|
+
);
|
|
21
|
+
const initial = computed(() => initialOf(user.value?.name, email.value));
|
|
22
|
+
return { user, email, isSuperAdmin, displayName, initial, refresh: session.fetch };
|
|
23
|
+
}
|
|
@@ -17,7 +17,7 @@ export function useCmsCollection(name, options = {}) {
|
|
|
17
17
|
export function useCmsPage(path, options = {}) {
|
|
18
18
|
const { locale, key, default: fallback } = options;
|
|
19
19
|
return useAsyncData(
|
|
20
|
-
`cms-page:${String(path)}:${locale ?? ""}`,
|
|
20
|
+
key ?? `cms-page:${String(path)}:${locale ?? ""}`,
|
|
21
21
|
async () => fallback ? fallback() : null
|
|
22
22
|
);
|
|
23
23
|
}
|
|
@@ -6,9 +6,14 @@ function load() {
|
|
|
6
6
|
known.value = result.source.kind === "none" ? null : new Set(result.items.map((item) => item.key));
|
|
7
7
|
}).catch(() => {
|
|
8
8
|
known.value = null;
|
|
9
|
+
loading = null;
|
|
9
10
|
});
|
|
10
11
|
return loading;
|
|
11
12
|
}
|
|
13
|
+
export function invalidateCmsMediaKeys() {
|
|
14
|
+
loading = null;
|
|
15
|
+
known.value = null;
|
|
16
|
+
}
|
|
12
17
|
export function useCmsMediaKeys() {
|
|
13
18
|
if (import.meta.client) void load();
|
|
14
19
|
function isMissing(key) {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "../../shared/index.js";
|
|
7
7
|
import { computed, ref } from "#imports";
|
|
8
8
|
import { errorMessage } from "../utils/ui.js";
|
|
9
|
+
import { invalidateCmsMediaKeys } from "./cms-media-keys.js";
|
|
9
10
|
import { useCmsToast } from "./cms-toast.js";
|
|
10
11
|
const endpoint = "/api/cms/admin/media";
|
|
11
12
|
export function useCmsMediaLibrary() {
|
|
@@ -81,6 +82,7 @@ export function useCmsMediaLibrary() {
|
|
|
81
82
|
method: "DELETE",
|
|
82
83
|
query: { name, recursive: "true" }
|
|
83
84
|
});
|
|
85
|
+
invalidateCmsMediaKeys();
|
|
84
86
|
await reload({ quiet: true });
|
|
85
87
|
});
|
|
86
88
|
}
|
|
@@ -93,6 +95,7 @@ export function useCmsMediaLibrary() {
|
|
|
93
95
|
function deleteItems(keys) {
|
|
94
96
|
return attempt("Delete failed", async () => {
|
|
95
97
|
await $fetch(`${endpoint}/delete`, { method: "POST", body: { keys } });
|
|
98
|
+
invalidateCmsMediaKeys();
|
|
96
99
|
await reload({ quiet: true });
|
|
97
100
|
});
|
|
98
101
|
}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { MediaItem } from '../../shared/index.js';
|
|
2
|
-
export
|
|
2
|
+
export interface UploadEntry {
|
|
3
|
+
file: File;
|
|
4
|
+
directory: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface UploadTree {
|
|
7
|
+
files: UploadEntry[];
|
|
8
|
+
directories: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function uploadTreeFromFiles(list: FileList | File[]): UploadTree;
|
|
11
|
+
export declare function uploadTreeFromDrop(dataTransfer: DataTransfer): Promise<UploadTree>;
|
|
3
12
|
export declare function useCmsMediaUploader(): {
|
|
4
|
-
upload: (
|
|
13
|
+
upload: (source: FileList | File[] | UploadTree, options: {
|
|
5
14
|
folder: string | null;
|
|
6
15
|
accept?: string;
|
|
7
16
|
multiple?: boolean;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
formatFileSize,
|
|
3
|
+
MEDIA_FOLDER_MAX_DEPTH,
|
|
4
|
+
mediaFolderSegments,
|
|
5
|
+
normalizeMediaFolder
|
|
6
|
+
} from "../../shared/index.js";
|
|
2
7
|
import { ref } from "#imports";
|
|
3
8
|
import { useCmsRuntime } from "./cms-runtime.js";
|
|
4
9
|
import { useCmsToast } from "./cms-toast.js";
|
|
@@ -34,7 +39,7 @@ async function mediaDimensions(file) {
|
|
|
34
39
|
if (file.type.startsWith("video/")) return videoDimensions(file);
|
|
35
40
|
return {};
|
|
36
41
|
}
|
|
37
|
-
|
|
42
|
+
function matchesAccept(file, accept) {
|
|
38
43
|
if (!accept) return true;
|
|
39
44
|
return accept.split(",").some((raw) => {
|
|
40
45
|
const pattern = raw.trim().toLowerCase();
|
|
@@ -43,6 +48,90 @@ export function matchesAccept(file, accept) {
|
|
|
43
48
|
return file.type.toLowerCase() === pattern;
|
|
44
49
|
});
|
|
45
50
|
}
|
|
51
|
+
const UPLOAD_CONCURRENCY = 4;
|
|
52
|
+
function isHiddenName(name) {
|
|
53
|
+
return name.startsWith(".");
|
|
54
|
+
}
|
|
55
|
+
function joinPath(parent, name) {
|
|
56
|
+
return parent ? `${parent}/${name}` : name;
|
|
57
|
+
}
|
|
58
|
+
export function uploadTreeFromFiles(list) {
|
|
59
|
+
const files = [];
|
|
60
|
+
const directories = /* @__PURE__ */ new Set();
|
|
61
|
+
for (const file of Array.from(list)) {
|
|
62
|
+
const segments = (file.webkitRelativePath || file.name).split("/").filter(Boolean);
|
|
63
|
+
if (segments.some(isHiddenName)) continue;
|
|
64
|
+
const directory = segments.slice(0, -1).join("/") || null;
|
|
65
|
+
if (directory) directories.add(directory);
|
|
66
|
+
files.push({ file, directory });
|
|
67
|
+
}
|
|
68
|
+
return { files, directories: [...directories] };
|
|
69
|
+
}
|
|
70
|
+
function readDirectory(entry) {
|
|
71
|
+
const reader = entry.createReader();
|
|
72
|
+
const all = [];
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const next = () => reader.readEntries((batch) => {
|
|
75
|
+
if (!batch.length) return resolve(all);
|
|
76
|
+
all.push(...batch);
|
|
77
|
+
next();
|
|
78
|
+
}, reject);
|
|
79
|
+
next();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function entryFile(entry) {
|
|
83
|
+
return new Promise((resolve, reject) => entry.file(resolve, reject));
|
|
84
|
+
}
|
|
85
|
+
async function walkEntry(entry, parent, tree) {
|
|
86
|
+
if (isHiddenName(entry.name)) return;
|
|
87
|
+
if (entry.isFile) {
|
|
88
|
+
tree.files.push({ file: await entryFile(entry), directory: parent });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!entry.isDirectory) return;
|
|
92
|
+
const directory = joinPath(parent, entry.name);
|
|
93
|
+
tree.directories.push(directory);
|
|
94
|
+
const children = await readDirectory(entry);
|
|
95
|
+
for (const child of children) await walkEntry(child, directory, tree);
|
|
96
|
+
}
|
|
97
|
+
export function uploadTreeFromDrop(dataTransfer) {
|
|
98
|
+
const files = Array.from(dataTransfer.files);
|
|
99
|
+
const entries = Array.from(dataTransfer.items ?? []).filter((item) => item.kind === "file").map((item) => item.webkitGetAsEntry?.() ?? null).filter((entry) => !!entry);
|
|
100
|
+
if (!entries.some((entry) => entry.isDirectory)) {
|
|
101
|
+
return Promise.resolve(uploadTreeFromFiles(files));
|
|
102
|
+
}
|
|
103
|
+
return (async () => {
|
|
104
|
+
const tree = { files: [], directories: [] };
|
|
105
|
+
for (const entry of entries) await walkEntry(entry, null, tree);
|
|
106
|
+
return tree;
|
|
107
|
+
})();
|
|
108
|
+
}
|
|
109
|
+
async function mapWithConcurrency(list, limit, task) {
|
|
110
|
+
const results = new Array(list.length);
|
|
111
|
+
let cursor = 0;
|
|
112
|
+
const worker = async () => {
|
|
113
|
+
while (cursor < list.length) {
|
|
114
|
+
const index = cursor++;
|
|
115
|
+
try {
|
|
116
|
+
results[index] = { status: "fulfilled", value: await task(list[index]) };
|
|
117
|
+
} catch (reason) {
|
|
118
|
+
results[index] = { status: "rejected", reason };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
await Promise.all(Array.from({ length: Math.min(limit, list.length) }, worker));
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
function targetFolder(base, directory) {
|
|
126
|
+
const joined = [base, directory].filter(Boolean).join("/");
|
|
127
|
+
return {
|
|
128
|
+
folder: normalizeMediaFolder(joined),
|
|
129
|
+
flattened: mediaFolderSegments(joined).length > MEDIA_FOLDER_MAX_DEPTH
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function isWithinOrAbove(folder, other) {
|
|
133
|
+
return other === folder || other.startsWith(`${folder}/`);
|
|
134
|
+
}
|
|
46
135
|
export function useCmsMediaUploader() {
|
|
47
136
|
const toast = useCmsToast();
|
|
48
137
|
const { mediaMaxFileSize } = useCmsRuntime();
|
|
@@ -78,9 +167,26 @@ export function useCmsMediaUploader() {
|
|
|
78
167
|
done.value++;
|
|
79
168
|
return item;
|
|
80
169
|
}
|
|
81
|
-
async function
|
|
82
|
-
const
|
|
83
|
-
|
|
170
|
+
async function createFolders(folders) {
|
|
171
|
+
const results = await mapWithConcurrency(
|
|
172
|
+
folders,
|
|
173
|
+
UPLOAD_CONCURRENCY,
|
|
174
|
+
(name) => $fetch("/api/cms/admin/media/folders", {
|
|
175
|
+
method: "POST",
|
|
176
|
+
body: { name }
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
const failed = results.filter((r) => r.status === "rejected").length;
|
|
180
|
+
if (failed)
|
|
181
|
+
toast.add({
|
|
182
|
+
title: `${failed} folder${failed > 1 ? "s" : ""} could not be created`,
|
|
183
|
+
color: "error"
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async function upload(source, options) {
|
|
187
|
+
const tree = Array.isArray(source) || source instanceof FileList ? uploadTreeFromFiles(source) : source;
|
|
188
|
+
const all = tree.files;
|
|
189
|
+
let files = all.filter((entry) => matchesAccept(entry.file, options.accept));
|
|
84
190
|
const rejected = all.length - files.length;
|
|
85
191
|
if (rejected) {
|
|
86
192
|
toast.add({
|
|
@@ -88,21 +194,42 @@ export function useCmsMediaUploader() {
|
|
|
88
194
|
color: "error"
|
|
89
195
|
});
|
|
90
196
|
}
|
|
91
|
-
for (const file of files.filter((
|
|
197
|
+
for (const { file } of files.filter((entry) => entry.file.size > mediaMaxFileSize)) {
|
|
92
198
|
toast.add({
|
|
93
199
|
title: `File too large (max ${formatFileSize(mediaMaxFileSize)}): ${file.name}`,
|
|
94
200
|
color: "error"
|
|
95
201
|
});
|
|
96
202
|
}
|
|
97
|
-
files = files.filter((
|
|
98
|
-
|
|
99
|
-
if (!files
|
|
203
|
+
files = files.filter((entry) => entry.file.size <= mediaMaxFileSize);
|
|
204
|
+
const nested = options.multiple !== false;
|
|
205
|
+
if (!nested) files = files.slice(0, 1).map((entry) => ({ ...entry, directory: null }));
|
|
206
|
+
const directories = nested ? tree.directories : [];
|
|
207
|
+
if (!files.length && !directories.length || uploading.value) return [];
|
|
208
|
+
const planned = files.map((entry) => ({
|
|
209
|
+
file: entry.file,
|
|
210
|
+
...targetFolder(options.folder, entry.directory)
|
|
211
|
+
}));
|
|
212
|
+
const fileFolders = planned.map((entry) => entry.folder).filter((f) => !!f);
|
|
213
|
+
const emptyFolders = [
|
|
214
|
+
...new Set(
|
|
215
|
+
directories.map((directory) => targetFolder(options.folder, directory).folder).filter((folder) => !!folder)
|
|
216
|
+
)
|
|
217
|
+
].filter((folder) => !fileFolders.some((other) => isWithinOrAbove(folder, other)));
|
|
218
|
+
if (planned.some((entry) => entry.flattened)) {
|
|
219
|
+
toast.add({
|
|
220
|
+
title: `Folders nest up to ${MEDIA_FOLDER_MAX_DEPTH} levels`,
|
|
221
|
+
description: "Files in deeper folders were uploaded to the deepest allowed folder."
|
|
222
|
+
});
|
|
223
|
+
}
|
|
100
224
|
uploading.value = true;
|
|
101
225
|
done.value = 0;
|
|
102
|
-
total.value =
|
|
226
|
+
total.value = planned.length;
|
|
103
227
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
228
|
+
if (emptyFolders.length) await createFolders(emptyFolders);
|
|
229
|
+
const results = await mapWithConcurrency(
|
|
230
|
+
planned,
|
|
231
|
+
UPLOAD_CONCURRENCY,
|
|
232
|
+
(entry) => uploadOne(entry.file, entry.folder)
|
|
106
233
|
);
|
|
107
234
|
const ok = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
108
235
|
const failed = results.length - ok.length;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { onBeforeUnmount } from "#imports";
|
|
2
|
+
const stack = [];
|
|
3
|
+
function onKeydown(event) {
|
|
4
|
+
if (event.key !== "Escape") return;
|
|
5
|
+
stack[stack.length - 1]?.close();
|
|
6
|
+
}
|
|
7
|
+
export function useCmsOverlay(close) {
|
|
8
|
+
const entry = { close };
|
|
9
|
+
function activate() {
|
|
10
|
+
if (import.meta.server || stack.includes(entry)) return;
|
|
11
|
+
stack.push(entry);
|
|
12
|
+
if (stack.length === 1) document.addEventListener("keydown", onKeydown);
|
|
13
|
+
}
|
|
14
|
+
function deactivate() {
|
|
15
|
+
if (import.meta.server) return;
|
|
16
|
+
const index = stack.indexOf(entry);
|
|
17
|
+
if (index === -1) return;
|
|
18
|
+
stack.splice(index, 1);
|
|
19
|
+
if (stack.length === 0) document.removeEventListener("keydown", onKeydown);
|
|
20
|
+
}
|
|
21
|
+
onBeforeUnmount(deactivate);
|
|
22
|
+
return { activate, deactivate };
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CmsEntrySettings, CmsFormLayout, CmsSettingsMap } from '#nuxt-cms';
|
|
2
|
+
export declare function useCmsSettingsState(): import("vue").Ref<CmsSettingsMap, CmsSettingsMap>;
|
|
3
|
+
export declare function loadCmsSettings(): Promise<void>;
|
|
4
|
+
export declare function useCmsSettings(): {
|
|
5
|
+
settings: import("vue").Ref<CmsSettingsMap, CmsSettingsMap>;
|
|
6
|
+
save: (name: string, value: CmsEntrySettings) => Promise<void>;
|
|
7
|
+
reset: (name: string) => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export declare function entryFields(name: string): Record<string, import("#nuxt-cms").FieldConfig>;
|
|
10
|
+
export declare function entryFormLayout(name: string, settings: CmsSettingsMap, fields?: Record<string, import("#nuxt-cms").FieldConfig>): CmsFormLayout;
|
|
11
|
+
export declare function listExtraColumns(name: string): string[];
|
|
12
|
+
export declare function entryListColumns(name: string, settings: CmsSettingsMap): string[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { layoutFromConfig, pageAllFields, resolveFormLayout, resolveListColumns } from "#nuxt-cms";
|
|
2
|
+
import { useFetch, useState } from "#imports";
|
|
3
|
+
import cmsConfig from "#cms-config";
|
|
4
|
+
import { cmsApi } from "../utils/api.js";
|
|
5
|
+
const SETTINGS_ENDPOINT = "/api/cms/admin/settings";
|
|
6
|
+
export function useCmsSettingsState() {
|
|
7
|
+
return useState("cms-settings", () => ({}));
|
|
8
|
+
}
|
|
9
|
+
export async function loadCmsSettings() {
|
|
10
|
+
const state = useCmsSettingsState();
|
|
11
|
+
const { data } = await useFetch(SETTINGS_ENDPOINT, { key: "cms-settings" });
|
|
12
|
+
if (data.value) state.value = data.value;
|
|
13
|
+
}
|
|
14
|
+
export function useCmsSettings() {
|
|
15
|
+
const settings = useCmsSettingsState();
|
|
16
|
+
async function save(name, value) {
|
|
17
|
+
const saved = await cmsApi(`${SETTINGS_ENDPOINT}/${name}`, {
|
|
18
|
+
method: "PUT",
|
|
19
|
+
body: value
|
|
20
|
+
});
|
|
21
|
+
settings.value = { ...settings.value, [name]: saved };
|
|
22
|
+
}
|
|
23
|
+
async function reset(name) {
|
|
24
|
+
await cmsApi(`${SETTINGS_ENDPOINT}/${name}`, { method: "DELETE" });
|
|
25
|
+
const next = { ...settings.value };
|
|
26
|
+
delete next[name];
|
|
27
|
+
settings.value = next;
|
|
28
|
+
}
|
|
29
|
+
return { settings, save, reset };
|
|
30
|
+
}
|
|
31
|
+
function entryOf(name) {
|
|
32
|
+
return cmsConfig[name];
|
|
33
|
+
}
|
|
34
|
+
export function entryFields(name) {
|
|
35
|
+
const entry = entryOf(name);
|
|
36
|
+
if (!entry) return {};
|
|
37
|
+
return entry.kind === "page" ? pageAllFields(entry) : entry.fields;
|
|
38
|
+
}
|
|
39
|
+
function defaultFormLayout(name, fields = entryFields(name)) {
|
|
40
|
+
return resolveFormLayout(fields, layoutFromConfig(entryOf(name)?.layout));
|
|
41
|
+
}
|
|
42
|
+
export function entryFormLayout(name, settings, fields = entryFields(name)) {
|
|
43
|
+
const saved = settings[name]?.layout;
|
|
44
|
+
return saved ? resolveFormLayout(fields, saved) : defaultFormLayout(name, fields);
|
|
45
|
+
}
|
|
46
|
+
export function listExtraColumns(name) {
|
|
47
|
+
const entry = entryOf(name);
|
|
48
|
+
return entry?.kind === "collection" && entry.drafts ? ["status"] : [];
|
|
49
|
+
}
|
|
50
|
+
export function entryListColumns(name, settings) {
|
|
51
|
+
const entry = entryOf(name);
|
|
52
|
+
if (!entry) return [];
|
|
53
|
+
return resolveListColumns(
|
|
54
|
+
entry.fields,
|
|
55
|
+
settings[name]?.columns,
|
|
56
|
+
entry.list,
|
|
57
|
+
listExtraColumns(name)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
export type CmsTheme = 'light' | 'dark' | 'system';
|
|
3
|
+
export declare const CMS_THEMES: {
|
|
4
|
+
value: CmsTheme;
|
|
5
|
+
label: string;
|
|
6
|
+
icon: string;
|
|
7
|
+
}[];
|
|
8
|
+
export declare function useCmsTheme(): import("nuxt/app").CookieRef<CmsTheme>;
|
|
9
|
+
export declare function setCmsTheme(theme: Ref<CmsTheme>, value: CmsTheme): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { nextTick, useCookie } from "#imports";
|
|
2
|
+
export const CMS_THEMES = [
|
|
3
|
+
{ value: "light", label: "Light", icon: "sun" },
|
|
4
|
+
{ value: "dark", label: "Dark", icon: "moon" },
|
|
5
|
+
{ value: "system", label: "System", icon: "computer-desktop" }
|
|
6
|
+
];
|
|
7
|
+
export function useCmsTheme() {
|
|
8
|
+
return useCookie("cms-theme", {
|
|
9
|
+
default: () => "system",
|
|
10
|
+
maxAge: 60 * 60 * 24 * 365,
|
|
11
|
+
sameSite: "lax",
|
|
12
|
+
path: "/"
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export function setCmsTheme(theme, value) {
|
|
16
|
+
if (value === theme.value) return;
|
|
17
|
+
const doc = document;
|
|
18
|
+
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
19
|
+
if (!doc.startViewTransition || reduced) {
|
|
20
|
+
theme.value = value;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
doc.startViewTransition(async () => {
|
|
24
|
+
theme.value = value;
|
|
25
|
+
await nextTick();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
declare var
|
|
1
|
+
declare var __VLS_34: {};
|
|
2
2
|
type __VLS_Slots = {} & {
|
|
3
|
-
default?: (props: typeof
|
|
3
|
+
default?: (props: typeof __VLS_34) => any;
|
|
4
4
|
};
|
|
5
5
|
declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
6
6
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="cms-scope cms-canvas min-h-screen">
|
|
3
|
-
<div class="cms-shell">
|
|
2
|
+
<div class="cms-scope cms-canvas min-h-screen" :data-theme="theme">
|
|
3
|
+
<div class="cms-shell" :class="{ 'is-nav-open': navOpen }">
|
|
4
4
|
<aside class="cms-sidebar">
|
|
5
|
-
<div class="cms-sidebar-brand">
|
|
5
|
+
<div class="cms-sidebar-brand">
|
|
6
|
+
<CmsBrandMark />
|
|
7
|
+
<div class="cms-brand-text">
|
|
8
|
+
<span class="cms-brand-name">nuxt-cms</span>
|
|
9
|
+
<span class="cms-brand-meta">Content studio</span>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
6
12
|
|
|
7
13
|
<nav class="cms-sidebar-nav">
|
|
8
14
|
<div v-for="group in groups" :key="group.title" class="cms-sidebar-group">
|
|
@@ -14,49 +20,81 @@
|
|
|
14
20
|
:key="link.name"
|
|
15
21
|
:to="link.to"
|
|
16
22
|
class="cms-navlink"
|
|
17
|
-
:class="{ 'is-active':
|
|
23
|
+
:class="{ 'is-active': isActive(link.to) }"
|
|
18
24
|
>
|
|
19
|
-
<CmsIcon
|
|
25
|
+
<CmsIcon
|
|
26
|
+
:name="link.icon ?? group.icon"
|
|
27
|
+
class="cms-navlink-icon size-4 shrink-0"
|
|
28
|
+
/>
|
|
20
29
|
<span class="truncate">{{ link.label }}</span>
|
|
21
30
|
</NuxtLink>
|
|
22
31
|
</div>
|
|
23
32
|
</nav>
|
|
24
33
|
|
|
25
34
|
<div class="cms-sidebar-footer">
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
class="cms-account"
|
|
38
|
+
:class="{ 'is-open': settingsOpen }"
|
|
39
|
+
aria-label="Open settings"
|
|
40
|
+
@click="openSettings"
|
|
41
|
+
>
|
|
42
|
+
<span class="cms-avatar">{{ initial }}</span>
|
|
43
|
+
<span class="cms-account-text">
|
|
44
|
+
<span class="cms-account-name">{{ displayName }}</span>
|
|
45
|
+
<span class="cms-account-email" :title="email">{{ email }}</span>
|
|
46
|
+
</span>
|
|
47
|
+
<CmsIcon name="ellipsis-horizontal" class="cms-account-more size-4" />
|
|
48
|
+
</button>
|
|
36
49
|
</div>
|
|
37
50
|
</aside>
|
|
38
51
|
|
|
52
|
+
<div class="cms-sidebar-scrim" @click="navOpen = false" />
|
|
53
|
+
|
|
39
54
|
<main class="cms-main">
|
|
55
|
+
<header class="cms-topbar">
|
|
56
|
+
<CmsButton
|
|
57
|
+
icon="bars-3"
|
|
58
|
+
variant="ghost"
|
|
59
|
+
color="neutral"
|
|
60
|
+
size="sm"
|
|
61
|
+
aria-label="Open navigation"
|
|
62
|
+
@click="navOpen = true"
|
|
63
|
+
/>
|
|
64
|
+
<CmsBrandMark />
|
|
65
|
+
<span class="cms-brand-name">nuxt-cms</span>
|
|
66
|
+
</header>
|
|
40
67
|
<div class="cms-main-inner">
|
|
41
68
|
<slot />
|
|
42
69
|
</div>
|
|
43
70
|
</main>
|
|
44
71
|
</div>
|
|
45
72
|
|
|
73
|
+
<CmsSettingsModal v-model:open="settingsOpen" v-model:theme="theme" @logout="logout" />
|
|
74
|
+
<CmsWelcomeModal />
|
|
46
75
|
<CmsToaster />
|
|
47
76
|
<CmsConfirmModal />
|
|
48
77
|
</div>
|
|
49
78
|
</template>
|
|
50
79
|
|
|
51
80
|
<script setup>
|
|
52
|
-
import { computed, navigateTo, useRoute, useUserSession } from "#imports";
|
|
81
|
+
import { computed, navigateTo, ref, useRoute, useUserSession, watch } from "#imports";
|
|
53
82
|
import cmsConfig from "#cms-config";
|
|
83
|
+
import { useCmsTheme } from "../composables/cms-theme";
|
|
84
|
+
import { useCmsAccount } from "../composables/cms-account";
|
|
85
|
+
import { loadCmsSettings } from "../composables/cms-settings";
|
|
54
86
|
const route = useRoute();
|
|
55
87
|
const { clear } = useUserSession();
|
|
88
|
+
const { email, displayName, initial } = useCmsAccount();
|
|
89
|
+
const theme = useCmsTheme();
|
|
90
|
+
const navOpen = ref(false);
|
|
91
|
+
const settingsOpen = ref(false);
|
|
92
|
+
await loadCmsSettings();
|
|
56
93
|
const links = Object.entries(cmsConfig).map(([name, entry]) => ({
|
|
57
94
|
name,
|
|
58
95
|
label: entry.label,
|
|
59
96
|
kind: entry.kind,
|
|
97
|
+
icon: entry.icon,
|
|
60
98
|
to: `/cms/${name}`
|
|
61
99
|
}));
|
|
62
100
|
const groups = computed(
|
|
@@ -79,11 +117,27 @@ const groups = computed(
|
|
|
79
117
|
{
|
|
80
118
|
title: "Library",
|
|
81
119
|
icon: "photo",
|
|
82
|
-
links: [
|
|
120
|
+
links: [
|
|
121
|
+
{ name: "media", label: "Media", kind: "media", icon: void 0, to: "/cms/media" }
|
|
122
|
+
]
|
|
83
123
|
}
|
|
84
124
|
].filter((g) => g.links.length)
|
|
85
125
|
);
|
|
126
|
+
function isActive(to) {
|
|
127
|
+
return route.path === to || route.path.startsWith(`${to}/`);
|
|
128
|
+
}
|
|
129
|
+
watch(
|
|
130
|
+
() => route.fullPath,
|
|
131
|
+
() => {
|
|
132
|
+
navOpen.value = false;
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
function openSettings() {
|
|
136
|
+
navOpen.value = false;
|
|
137
|
+
settingsOpen.value = true;
|
|
138
|
+
}
|
|
86
139
|
async function logout() {
|
|
140
|
+
settingsOpen.value = false;
|
|
87
141
|
await clear();
|
|
88
142
|
await navigateTo("/cms/login");
|
|
89
143
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
declare var
|
|
1
|
+
declare var __VLS_34: {};
|
|
2
2
|
type __VLS_Slots = {} & {
|
|
3
|
-
default?: (props: typeof
|
|
3
|
+
default?: (props: typeof __VLS_34) => any;
|
|
4
4
|
};
|
|
5
5
|
declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
6
6
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|