@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
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type CmsTheme } from '../../composables/cms-theme.js';
|
|
2
|
+
type __VLS_ModelProps = {
|
|
3
|
+
'open'?: boolean;
|
|
4
|
+
'theme': CmsTheme;
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
logout: () => any;
|
|
8
|
+
"update:open": (value: boolean) => any;
|
|
9
|
+
"update:theme": (value: CmsTheme) => any;
|
|
10
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
11
|
+
onLogout?: (() => any) | undefined;
|
|
12
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
13
|
+
"onUpdate:theme"?: ((value: CmsTheme) => any) | undefined;
|
|
14
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
15
|
+
declare const _default: typeof __VLS_export;
|
|
16
|
+
export default _default;
|
|
@@ -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,270 @@
|
|
|
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">Users</h2>
|
|
6
|
+
<p class="cms-settings-hint">
|
|
7
|
+
Users can do everything in the panel, except manage users.
|
|
8
|
+
</p>
|
|
9
|
+
</div>
|
|
10
|
+
<CmsButton
|
|
11
|
+
v-if="!creating"
|
|
12
|
+
label="Add user"
|
|
13
|
+
icon="user-plus"
|
|
14
|
+
size="sm"
|
|
15
|
+
@click="startCreate"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div v-if="created" class="cms-credentials">
|
|
20
|
+
<div class="cms-settings-heading">
|
|
21
|
+
<p class="cms-settings-label">{{ created.title }}</p>
|
|
22
|
+
<p class="cms-settings-hint">
|
|
23
|
+
Share these credentials with the user. The password does not show again.
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
<dl class="cms-credentials-list">
|
|
27
|
+
<dt>Email</dt>
|
|
28
|
+
<dd>{{ created.email }}</dd>
|
|
29
|
+
<dt>Password</dt>
|
|
30
|
+
<dd class="cms-mono">{{ created.password }}</dd>
|
|
31
|
+
</dl>
|
|
32
|
+
<div class="cms-actions is-end">
|
|
33
|
+
<CmsButton
|
|
34
|
+
:label="copied ? 'Copied' : 'Copy credentials'"
|
|
35
|
+
:icon="copied ? 'clipboard-document-check' : 'clipboard-document'"
|
|
36
|
+
variant="soft"
|
|
37
|
+
size="sm"
|
|
38
|
+
@click="copyCredentials"
|
|
39
|
+
/>
|
|
40
|
+
<CmsButton label="Done" size="sm" @click="created = null" />
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div v-if="creating" class="cms-credentials">
|
|
45
|
+
<p class="cms-settings-label">New user</p>
|
|
46
|
+
<CmsForm :state="draft" @submit="create">
|
|
47
|
+
<CmsAlert v-if="formError" color="error" :title="formError" />
|
|
48
|
+
<div class="cms-form-row" style="--cms-row-cols: 2">
|
|
49
|
+
<CmsFormField label="Email" name="email" required>
|
|
50
|
+
<CmsInput
|
|
51
|
+
v-model="draft.email"
|
|
52
|
+
type="email"
|
|
53
|
+
icon="envelope"
|
|
54
|
+
placeholder="name@example.com"
|
|
55
|
+
autocomplete="off"
|
|
56
|
+
/>
|
|
57
|
+
</CmsFormField>
|
|
58
|
+
<CmsFormField label="Name" name="name">
|
|
59
|
+
<CmsInput v-model="draft.name" placeholder="Optional" autocomplete="off" />
|
|
60
|
+
</CmsFormField>
|
|
61
|
+
</div>
|
|
62
|
+
<CmsFormField label="Password" name="password" required>
|
|
63
|
+
<CmsPasswordInput v-model="draft.password" generate copy />
|
|
64
|
+
</CmsFormField>
|
|
65
|
+
<div class="cms-actions is-end">
|
|
66
|
+
<CmsButton
|
|
67
|
+
label="Cancel"
|
|
68
|
+
variant="ghost"
|
|
69
|
+
color="neutral"
|
|
70
|
+
size="sm"
|
|
71
|
+
@click="cancel"
|
|
72
|
+
/>
|
|
73
|
+
<CmsButton
|
|
74
|
+
type="submit"
|
|
75
|
+
label="Create user"
|
|
76
|
+
size="sm"
|
|
77
|
+
:loading="busy"
|
|
78
|
+
:disabled="!draft.email || draft.password.length < MIN_PASSWORD_LENGTH"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</CmsForm>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<CmsSpinner v-if="loading" />
|
|
85
|
+
|
|
86
|
+
<ul v-else-if="users.length" class="cms-settings-list">
|
|
87
|
+
<li v-for="account in users" :key="account.id" class="cms-user-row">
|
|
88
|
+
<div class="cms-user-main">
|
|
89
|
+
<span class="cms-avatar">{{ initialOf(account.name, account.email) }}</span>
|
|
90
|
+
<div class="cms-account-text">
|
|
91
|
+
<span class="cms-account-name">{{ account.name || account.email }}</span>
|
|
92
|
+
<span class="cms-account-email">
|
|
93
|
+
{{ account.name ? `${account.email} \xB7 ` : "" }}{{ lastLogin(account) }}
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="cms-user-actions">
|
|
97
|
+
<CmsButton
|
|
98
|
+
icon="key"
|
|
99
|
+
variant="ghost"
|
|
100
|
+
color="neutral"
|
|
101
|
+
size="sm"
|
|
102
|
+
title="Set a new password"
|
|
103
|
+
aria-label="Set a new password"
|
|
104
|
+
@click="startReset(account)"
|
|
105
|
+
/>
|
|
106
|
+
<CmsButton
|
|
107
|
+
icon="trash"
|
|
108
|
+
variant="ghost"
|
|
109
|
+
color="error"
|
|
110
|
+
size="sm"
|
|
111
|
+
title="Delete user"
|
|
112
|
+
aria-label="Delete user"
|
|
113
|
+
@click="remove(account)"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<CmsForm
|
|
118
|
+
v-if="resetting === account.id"
|
|
119
|
+
:state="reset"
|
|
120
|
+
class="cms-user-reset"
|
|
121
|
+
@submit="saveReset(account)"
|
|
122
|
+
>
|
|
123
|
+
<CmsFormField label="New password" name="password">
|
|
124
|
+
<CmsPasswordInput v-model="reset.password" generate copy />
|
|
125
|
+
</CmsFormField>
|
|
126
|
+
<div class="cms-actions is-end">
|
|
127
|
+
<CmsButton
|
|
128
|
+
label="Cancel"
|
|
129
|
+
variant="ghost"
|
|
130
|
+
color="neutral"
|
|
131
|
+
size="sm"
|
|
132
|
+
@click="resetting = null"
|
|
133
|
+
/>
|
|
134
|
+
<CmsButton
|
|
135
|
+
type="submit"
|
|
136
|
+
label="Set password"
|
|
137
|
+
size="sm"
|
|
138
|
+
:loading="busy"
|
|
139
|
+
:disabled="reset.password.length < MIN_PASSWORD_LENGTH"
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
</CmsForm>
|
|
143
|
+
</li>
|
|
144
|
+
</ul>
|
|
145
|
+
|
|
146
|
+
<CmsEmptyState
|
|
147
|
+
v-else-if="!creating"
|
|
148
|
+
icon="users"
|
|
149
|
+
title="No users yet"
|
|
150
|
+
body="Add a user to give someone access to the panel."
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
</template>
|
|
154
|
+
|
|
155
|
+
<script setup>
|
|
156
|
+
import { MIN_PASSWORD_LENGTH } from "#nuxt-cms";
|
|
157
|
+
import { onMounted, ref } from "#imports";
|
|
158
|
+
import { generatePassword, initialOf } from "../../composables/cms-account";
|
|
159
|
+
import { useCmsConfirm } from "../../composables/cms-confirm";
|
|
160
|
+
import { useCmsToast } from "../../composables/cms-toast";
|
|
161
|
+
import { cmsApi } from "../../utils/api";
|
|
162
|
+
import { errorMessage } from "../../utils/ui";
|
|
163
|
+
const toast = useCmsToast();
|
|
164
|
+
const confirmAction = useCmsConfirm();
|
|
165
|
+
const users = ref([]);
|
|
166
|
+
const loading = ref(true);
|
|
167
|
+
const busy = ref(false);
|
|
168
|
+
async function load() {
|
|
169
|
+
try {
|
|
170
|
+
users.value = await cmsApi("/api/cms/users");
|
|
171
|
+
} catch (error) {
|
|
172
|
+
toast.add({ title: "Could not load users", description: errorMessage(error), color: "error" });
|
|
173
|
+
} finally {
|
|
174
|
+
loading.value = false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
onMounted(load);
|
|
178
|
+
function lastLogin(account) {
|
|
179
|
+
if (!account.lastLoginAt) return "Never signed in";
|
|
180
|
+
return `Last sign-in ${new Date(account.lastLoginAt).toLocaleString(void 0, {
|
|
181
|
+
dateStyle: "medium",
|
|
182
|
+
timeStyle: "short"
|
|
183
|
+
})}`;
|
|
184
|
+
}
|
|
185
|
+
const creating = ref(false);
|
|
186
|
+
const draft = ref({ email: "", name: "", password: "" });
|
|
187
|
+
const formError = ref(null);
|
|
188
|
+
const created = ref(null);
|
|
189
|
+
const copied = ref(false);
|
|
190
|
+
function startCreate() {
|
|
191
|
+
created.value = null;
|
|
192
|
+
formError.value = null;
|
|
193
|
+
draft.value = { email: "", name: "", password: generatePassword() };
|
|
194
|
+
creating.value = true;
|
|
195
|
+
}
|
|
196
|
+
function cancel() {
|
|
197
|
+
creating.value = false;
|
|
198
|
+
}
|
|
199
|
+
async function create() {
|
|
200
|
+
busy.value = true;
|
|
201
|
+
formError.value = null;
|
|
202
|
+
try {
|
|
203
|
+
const account = await cmsApi("/api/cms/users", {
|
|
204
|
+
method: "POST",
|
|
205
|
+
body: {
|
|
206
|
+
email: draft.value.email,
|
|
207
|
+
name: draft.value.name || null,
|
|
208
|
+
password: draft.value.password
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
users.value = [...users.value, account];
|
|
212
|
+
created.value = {
|
|
213
|
+
title: "User created",
|
|
214
|
+
email: account.email,
|
|
215
|
+
password: draft.value.password
|
|
216
|
+
};
|
|
217
|
+
copied.value = false;
|
|
218
|
+
creating.value = false;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
formError.value = errorMessage(error) ?? "User not created.";
|
|
221
|
+
} finally {
|
|
222
|
+
busy.value = false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function copyCredentials() {
|
|
226
|
+
if (!created.value) return;
|
|
227
|
+
await navigator.clipboard.writeText(
|
|
228
|
+
`Email: ${created.value.email}
|
|
229
|
+
Password: ${created.value.password}`
|
|
230
|
+
);
|
|
231
|
+
copied.value = true;
|
|
232
|
+
}
|
|
233
|
+
const resetting = ref(null);
|
|
234
|
+
const reset = ref({ password: "" });
|
|
235
|
+
function startReset(account) {
|
|
236
|
+
reset.value = { password: generatePassword() };
|
|
237
|
+
resetting.value = resetting.value === account.id ? null : account.id;
|
|
238
|
+
}
|
|
239
|
+
async function saveReset(account) {
|
|
240
|
+
busy.value = true;
|
|
241
|
+
try {
|
|
242
|
+
const updated = await cmsApi(`/api/cms/users/${account.id}`, {
|
|
243
|
+
method: "PUT",
|
|
244
|
+
body: { password: reset.value.password }
|
|
245
|
+
});
|
|
246
|
+
users.value = users.value.map((u) => u.id === updated.id ? updated : u);
|
|
247
|
+
created.value = {
|
|
248
|
+
title: "Password changed",
|
|
249
|
+
email: account.email,
|
|
250
|
+
password: reset.value.password
|
|
251
|
+
};
|
|
252
|
+
copied.value = false;
|
|
253
|
+
resetting.value = null;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
toast.add({ title: "Password not changed", description: errorMessage(error), color: "error" });
|
|
256
|
+
} finally {
|
|
257
|
+
busy.value = false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function remove(account) {
|
|
261
|
+
if (!await confirmAction(`Delete ${account.email}? They lose access to the panel.`)) return;
|
|
262
|
+
try {
|
|
263
|
+
await cmsApi(`/api/cms/users/${account.id}`, { method: "DELETE" });
|
|
264
|
+
users.value = users.value.filter((u) => u.id !== account.id);
|
|
265
|
+
toast.add({ title: "User deleted", color: "success" });
|
|
266
|
+
} catch (error) {
|
|
267
|
+
toast.add({ title: "User not deleted", description: errorMessage(error), color: "error" });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
</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;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { MediaItem, MediaSourceInfo, MediaUsage } from '../../shared/index.js';
|
|
2
|
+
export interface MediaFolderStats {
|
|
3
|
+
files: number;
|
|
4
|
+
folders: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function useCmsMediaLibrary(): {
|
|
7
|
+
items: import("vue").Ref<{
|
|
8
|
+
id?: number | undefined;
|
|
9
|
+
key: string;
|
|
10
|
+
alt: string | null;
|
|
11
|
+
folder: string | null;
|
|
12
|
+
mime: string | null;
|
|
13
|
+
size: number | null;
|
|
14
|
+
width: number | null;
|
|
15
|
+
height: number | null;
|
|
16
|
+
createdAt: string | null;
|
|
17
|
+
type: import("../../shared/index.js").MediaType;
|
|
18
|
+
url: string | null;
|
|
19
|
+
}[], MediaItem[] | {
|
|
20
|
+
id?: number | undefined;
|
|
21
|
+
key: string;
|
|
22
|
+
alt: string | null;
|
|
23
|
+
folder: string | null;
|
|
24
|
+
mime: string | null;
|
|
25
|
+
size: number | null;
|
|
26
|
+
width: number | null;
|
|
27
|
+
height: number | null;
|
|
28
|
+
createdAt: string | null;
|
|
29
|
+
type: import("../../shared/index.js").MediaType;
|
|
30
|
+
url: string | null;
|
|
31
|
+
}[]>;
|
|
32
|
+
source: import("vue").Ref<{
|
|
33
|
+
kind: import("../../shared/index.js").MediaSourceKind;
|
|
34
|
+
root: string;
|
|
35
|
+
builtAt: string | null;
|
|
36
|
+
} | null, MediaSourceInfo | {
|
|
37
|
+
kind: import("../../shared/index.js").MediaSourceKind;
|
|
38
|
+
root: string;
|
|
39
|
+
builtAt: string | null;
|
|
40
|
+
} | null>;
|
|
41
|
+
loading: import("vue").Ref<boolean, boolean>;
|
|
42
|
+
errorCode: import("vue").Ref<number | null, number | null>;
|
|
43
|
+
folders: import("vue").ComputedRef<string[]>;
|
|
44
|
+
stats: import("vue").ComputedRef<Map<string, MediaFolderStats>>;
|
|
45
|
+
reload: (options?: {
|
|
46
|
+
quiet?: boolean;
|
|
47
|
+
}) => Promise<void>;
|
|
48
|
+
childFolders: (parent: string | null) => string[];
|
|
49
|
+
itemsWithin: (folder: string) => {
|
|
50
|
+
id?: number | undefined;
|
|
51
|
+
key: string;
|
|
52
|
+
alt: string | null;
|
|
53
|
+
folder: string | null;
|
|
54
|
+
mime: string | null;
|
|
55
|
+
size: number | null;
|
|
56
|
+
width: number | null;
|
|
57
|
+
height: number | null;
|
|
58
|
+
createdAt: string | null;
|
|
59
|
+
type: import("../../shared/index.js").MediaType;
|
|
60
|
+
url: string | null;
|
|
61
|
+
}[];
|
|
62
|
+
createFolder: (name: string) => Promise<boolean>;
|
|
63
|
+
moveFolder: (from: string, to: string) => Promise<boolean>;
|
|
64
|
+
deleteFolder: (name: string) => Promise<boolean>;
|
|
65
|
+
moveItems: (keys: string[], folder: string | null) => Promise<boolean>;
|
|
66
|
+
deleteItems: (keys: string[]) => Promise<boolean>;
|
|
67
|
+
updateItem: (key: string, changes: {
|
|
68
|
+
alt?: string | null;
|
|
69
|
+
folder?: string | null;
|
|
70
|
+
}) => Promise<boolean>;
|
|
71
|
+
usage: (keys: string[]) => Promise<Record<string, MediaUsage[]> | null>;
|
|
72
|
+
};
|
|
73
|
+
export type CmsMediaLibrary = ReturnType<typeof useCmsMediaLibrary>;
|
|
74
|
+
export declare function usageSummary(usage: Record<string, MediaUsage[]> | null, keys: string[]): string;
|
|
75
|
+
export declare function usageLink(usage: MediaUsage): string;
|