@xleddyl/nuxt-cms 0.1.61 → 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 +117 -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/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/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-uploader.d.ts +11 -1
- package/dist/runtime/app/composables/cms-media-uploader.js +138 -11
- 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 +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/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/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/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 +13 -1
- package/dist/runtime/shared/index.js +2 -0
- 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
|
@@ -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;
|
|
@@ -14,25 +14,29 @@ interface TableSort {
|
|
|
14
14
|
type __VLS_Props = {
|
|
15
15
|
data: Row[];
|
|
16
16
|
columns: Column[];
|
|
17
|
+
selectable?: boolean;
|
|
18
|
+
rowKey?: string;
|
|
17
19
|
};
|
|
18
20
|
type __VLS_ModelProps = {
|
|
21
|
+
'selected'?: string[];
|
|
19
22
|
'columnVisibility'?: Record<string, boolean>;
|
|
20
23
|
'sort'?: TableSort | null;
|
|
21
24
|
};
|
|
22
25
|
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
23
|
-
declare var
|
|
26
|
+
declare var __VLS_21: `${string}-cell`, __VLS_22: {
|
|
24
27
|
row: {
|
|
25
28
|
original: Row;
|
|
26
29
|
};
|
|
27
30
|
};
|
|
28
31
|
type __VLS_Slots = {} & {
|
|
29
|
-
[K in NonNullable<typeof
|
|
32
|
+
[K in NonNullable<typeof __VLS_21>]?: (props: typeof __VLS_22) => any;
|
|
30
33
|
};
|
|
31
34
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
32
35
|
select: (event: Event, row: {
|
|
33
36
|
original: Row;
|
|
34
37
|
}) => any;
|
|
35
38
|
reorder: (from: string, to: string) => any;
|
|
39
|
+
"update:selected": (value: string[]) => any;
|
|
36
40
|
"update:columnVisibility": (value: Record<string, boolean>) => any;
|
|
37
41
|
"update:sort": (value: TableSort | null) => any;
|
|
38
42
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
@@ -40,6 +44,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
40
44
|
original: Row;
|
|
41
45
|
}) => any) | undefined;
|
|
42
46
|
onReorder?: ((from: string, to: string) => any) | undefined;
|
|
47
|
+
"onUpdate:selected"?: ((value: string[]) => any) | undefined;
|
|
43
48
|
"onUpdate:columnVisibility"?: ((value: Record<string, boolean>) => any) | undefined;
|
|
44
49
|
"onUpdate:sort"?: ((value: TableSort | null) => any) | undefined;
|
|
45
50
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
<table class="cms-table">
|
|
5
5
|
<thead>
|
|
6
6
|
<tr>
|
|
7
|
+
<th v-if="selectable" class="cms-table-check-cell">
|
|
8
|
+
<CmsCheckbox
|
|
9
|
+
:checked="allSelected"
|
|
10
|
+
:indeterminate="someSelected"
|
|
11
|
+
aria-label="Select all rows"
|
|
12
|
+
@toggle="toggleAll"
|
|
13
|
+
/>
|
|
14
|
+
</th>
|
|
7
15
|
<th
|
|
8
16
|
v-for="column in visibleColumns"
|
|
9
17
|
:key="columnId(column)"
|
|
@@ -38,8 +46,17 @@
|
|
|
38
46
|
v-for="(row, index) in data"
|
|
39
47
|
:key="index"
|
|
40
48
|
class="__clickable"
|
|
49
|
+
:class="{ 'is-selected': isSelected(row) }"
|
|
50
|
+
:style="{ '--cms-row-delay': `${Math.min(index, 14) * 24}ms` }"
|
|
41
51
|
@click="emit('select', $event, { original: row })"
|
|
42
52
|
>
|
|
53
|
+
<td v-if="selectable" class="cms-table-check-cell" @click.stop="toggleRow(row)">
|
|
54
|
+
<CmsCheckbox
|
|
55
|
+
:checked="isSelected(row)"
|
|
56
|
+
aria-label="Select row"
|
|
57
|
+
@toggle="toggleRow(row)"
|
|
58
|
+
/>
|
|
59
|
+
</td>
|
|
43
60
|
<td v-for="column in visibleColumns" :key="columnId(column)">
|
|
44
61
|
<slot :name="`${columnId(column)}-cell`" :row="{ original: row }">
|
|
45
62
|
{{ cellValue(column, row) }}
|
|
@@ -56,8 +73,28 @@
|
|
|
56
73
|
import { computed, ref } from "#imports";
|
|
57
74
|
const props = defineProps({
|
|
58
75
|
data: { type: Array, required: true },
|
|
59
|
-
columns: { type: Array, required: true }
|
|
76
|
+
columns: { type: Array, required: true },
|
|
77
|
+
selectable: { type: Boolean, required: false },
|
|
78
|
+
rowKey: { type: String, required: false }
|
|
60
79
|
});
|
|
80
|
+
const selected = defineModel("selected", { type: Array, ...{ default: () => [] } });
|
|
81
|
+
function keyOf(row) {
|
|
82
|
+
return String(row[props.rowKey ?? "id"]);
|
|
83
|
+
}
|
|
84
|
+
function isSelected(row) {
|
|
85
|
+
return selected.value.includes(keyOf(row));
|
|
86
|
+
}
|
|
87
|
+
function toggleRow(row) {
|
|
88
|
+
const key = keyOf(row);
|
|
89
|
+
selected.value = isSelected(row) ? selected.value.filter((k) => k !== key) : [...selected.value, key];
|
|
90
|
+
}
|
|
91
|
+
const allSelected = computed(
|
|
92
|
+
() => props.data.length > 0 && props.data.every((row) => isSelected(row))
|
|
93
|
+
);
|
|
94
|
+
const someSelected = computed(() => !allSelected.value && selected.value.length > 0);
|
|
95
|
+
function toggleAll() {
|
|
96
|
+
selected.value = allSelected.value ? [] : props.data.map(keyOf);
|
|
97
|
+
}
|
|
61
98
|
const visibility = defineModel("columnVisibility", { type: Object, ...{ default: () => ({}) } });
|
|
62
99
|
const sort = defineModel("sort", { type: [Object, null], ...{ default: null } });
|
|
63
100
|
const emit = defineEmits(["select", "reorder"]);
|
|
@@ -14,25 +14,29 @@ interface TableSort {
|
|
|
14
14
|
type __VLS_Props = {
|
|
15
15
|
data: Row[];
|
|
16
16
|
columns: Column[];
|
|
17
|
+
selectable?: boolean;
|
|
18
|
+
rowKey?: string;
|
|
17
19
|
};
|
|
18
20
|
type __VLS_ModelProps = {
|
|
21
|
+
'selected'?: string[];
|
|
19
22
|
'columnVisibility'?: Record<string, boolean>;
|
|
20
23
|
'sort'?: TableSort | null;
|
|
21
24
|
};
|
|
22
25
|
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
23
|
-
declare var
|
|
26
|
+
declare var __VLS_21: `${string}-cell`, __VLS_22: {
|
|
24
27
|
row: {
|
|
25
28
|
original: Row;
|
|
26
29
|
};
|
|
27
30
|
};
|
|
28
31
|
type __VLS_Slots = {} & {
|
|
29
|
-
[K in NonNullable<typeof
|
|
32
|
+
[K in NonNullable<typeof __VLS_21>]?: (props: typeof __VLS_22) => any;
|
|
30
33
|
};
|
|
31
34
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
32
35
|
select: (event: Event, row: {
|
|
33
36
|
original: Row;
|
|
34
37
|
}) => any;
|
|
35
38
|
reorder: (from: string, to: string) => any;
|
|
39
|
+
"update:selected": (value: string[]) => any;
|
|
36
40
|
"update:columnVisibility": (value: Record<string, boolean>) => any;
|
|
37
41
|
"update:sort": (value: TableSort | null) => any;
|
|
38
42
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
@@ -40,6 +44,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
40
44
|
original: Row;
|
|
41
45
|
}) => any) | undefined;
|
|
42
46
|
onReorder?: ((from: string, to: string) => any) | undefined;
|
|
47
|
+
"onUpdate:selected"?: ((value: string[]) => any) | undefined;
|
|
43
48
|
"onUpdate:columnVisibility"?: ((value: Record<string, boolean>) => any) | undefined;
|
|
44
49
|
"onUpdate:sort"?: ((value: TableSort | null) => any) | undefined;
|
|
45
50
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -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,69 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<CmsModal :open="open" size="sm" title="Welcome to nuxt-cms" @update:open="(v) => !v && keep()">
|
|
3
|
+
<template #body>
|
|
4
|
+
<CmsForm :state="state" @submit="save">
|
|
5
|
+
<p class="cms-settings-hint">
|
|
6
|
+
You signed in with a password that someone shared with you. Set your own password
|
|
7
|
+
now, or keep this one. You can change it later in Settings, Account.
|
|
8
|
+
</p>
|
|
9
|
+
<CmsAlert v-if="error" color="error" :title="error" />
|
|
10
|
+
<CmsFormField label="New password" name="password">
|
|
11
|
+
<CmsPasswordInput v-model="state.password" generate copy />
|
|
12
|
+
</CmsFormField>
|
|
13
|
+
<div class="cms-actions is-end">
|
|
14
|
+
<CmsButton
|
|
15
|
+
label="Keep current password"
|
|
16
|
+
variant="ghost"
|
|
17
|
+
color="neutral"
|
|
18
|
+
:disabled="busy"
|
|
19
|
+
@click="keep"
|
|
20
|
+
/>
|
|
21
|
+
<CmsButton
|
|
22
|
+
type="submit"
|
|
23
|
+
label="Set password"
|
|
24
|
+
:loading="busy"
|
|
25
|
+
:disabled="state.password.length < MIN_PASSWORD_LENGTH"
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
</CmsForm>
|
|
29
|
+
</template>
|
|
30
|
+
</CmsModal>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup>
|
|
34
|
+
import { MIN_PASSWORD_LENGTH } from "#nuxt-cms";
|
|
35
|
+
import { computed, ref } from "#imports";
|
|
36
|
+
import { useCmsAccount } from "../../composables/cms-account";
|
|
37
|
+
import { useCmsToast } from "../../composables/cms-toast";
|
|
38
|
+
import { cmsApi } from "../../utils/api";
|
|
39
|
+
import { errorMessage } from "../../utils/ui";
|
|
40
|
+
const { user, refresh } = useCmsAccount();
|
|
41
|
+
const toast = useCmsToast();
|
|
42
|
+
const dismissed = ref(false);
|
|
43
|
+
const open = computed(() => !!user.value?.firstLogin && !dismissed.value);
|
|
44
|
+
const state = ref({ password: "" });
|
|
45
|
+
const error = ref(null);
|
|
46
|
+
const busy = ref(false);
|
|
47
|
+
async function keep() {
|
|
48
|
+
dismissed.value = true;
|
|
49
|
+
await cmsApi("/api/cms/account/welcome", { method: "POST" }).catch(() => void 0);
|
|
50
|
+
await refresh();
|
|
51
|
+
}
|
|
52
|
+
async function save() {
|
|
53
|
+
busy.value = true;
|
|
54
|
+
error.value = null;
|
|
55
|
+
try {
|
|
56
|
+
await cmsApi("/api/cms/account/password", {
|
|
57
|
+
method: "PUT",
|
|
58
|
+
body: { newPassword: state.value.password }
|
|
59
|
+
});
|
|
60
|
+
dismissed.value = true;
|
|
61
|
+
await refresh();
|
|
62
|
+
toast.add({ title: "Password set", color: "success" });
|
|
63
|
+
} catch (e) {
|
|
64
|
+
error.value = errorMessage(e) ?? "Password not set.";
|
|
65
|
+
} finally {
|
|
66
|
+
busy.value = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
</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,11 @@
|
|
|
1
|
+
import type { CmsSessionUser } from '#nuxt-cms';
|
|
2
|
+
export declare function generatePassword(length?: number): string;
|
|
3
|
+
export declare function initialOf(name: string | null | undefined, email: string): string;
|
|
4
|
+
export declare function useCmsAccount(): {
|
|
5
|
+
user: import("vue").ComputedRef<CmsSessionUser | null>;
|
|
6
|
+
email: import("vue").ComputedRef<string>;
|
|
7
|
+
isSuperAdmin: import("vue").ComputedRef<boolean>;
|
|
8
|
+
displayName: import("vue").ComputedRef<string>;
|
|
9
|
+
initial: import("vue").ComputedRef<string>;
|
|
10
|
+
refresh: () => Promise<void>;
|
|
11
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { MediaItem } from '../../shared/index.js';
|
|
2
2
|
export declare function matchesAccept(file: File, accept: string | undefined): boolean;
|
|
3
|
+
export interface UploadEntry {
|
|
4
|
+
file: File;
|
|
5
|
+
directory: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface UploadTree {
|
|
8
|
+
files: UploadEntry[];
|
|
9
|
+
directories: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function uploadTreeFromFiles(list: FileList | File[]): UploadTree;
|
|
12
|
+
export declare function uploadTreeFromDrop(dataTransfer: DataTransfer): Promise<UploadTree>;
|
|
3
13
|
export declare function useCmsMediaUploader(): {
|
|
4
|
-
upload: (
|
|
14
|
+
upload: (source: FileList | File[] | UploadTree, options: {
|
|
5
15
|
folder: string | null;
|
|
6
16
|
accept?: string;
|
|
7
17
|
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";
|
|
@@ -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,13 @@
|
|
|
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 defaultFormLayout(name: string, fields?: Record<string, import("#nuxt-cms").FieldConfig>): CmsFormLayout;
|
|
11
|
+
export declare function entryFormLayout(name: string, settings: CmsSettingsMap, fields?: Record<string, import("#nuxt-cms").FieldConfig>): CmsFormLayout;
|
|
12
|
+
export declare function listExtraColumns(name: string): string[];
|
|
13
|
+
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
|
+
export 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>;
|