@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
|
@@ -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>;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="cms-page" :class="{ 'is-fill': config.kind === 'collection' && !error }">
|
|
3
|
-
<CmsPageHeader
|
|
3
|
+
<CmsPageHeader
|
|
4
|
+
:title="config.label"
|
|
5
|
+
:icon="kindMeta.icon"
|
|
6
|
+
:description="kindMeta.description"
|
|
7
|
+
>
|
|
8
|
+
<template v-if="config.kind !== 'single' && total" #badge>
|
|
9
|
+
<span :key="total" class="cms-count">{{ total }}</span>
|
|
10
|
+
</template>
|
|
4
11
|
<CmsButton
|
|
5
12
|
v-if="config.kind === 'single'"
|
|
6
13
|
type="submit"
|
|
@@ -25,6 +32,7 @@
|
|
|
25
32
|
v-model="formState"
|
|
26
33
|
:fields="config.fields"
|
|
27
34
|
:tabs="config.tabs"
|
|
35
|
+
:entry-name="name"
|
|
28
36
|
:form-id="FORM_ID"
|
|
29
37
|
:loading="saving"
|
|
30
38
|
@submit="saveSingle"
|
|
@@ -42,27 +50,33 @@
|
|
|
42
50
|
/>
|
|
43
51
|
</div>
|
|
44
52
|
|
|
45
|
-
<div v-if="pageTree.length" class="cms-card
|
|
53
|
+
<div v-if="pageTree.length" class="cms-card cms-tree">
|
|
46
54
|
<NuxtLink
|
|
47
|
-
v-for="node in pageTree"
|
|
55
|
+
v-for="(node, index) in pageTree"
|
|
48
56
|
:key="String(node.row.id)"
|
|
49
57
|
:to="`/cms/${name}/${node.row.id}`"
|
|
50
58
|
class="cms-tree-row"
|
|
51
|
-
:style="{
|
|
59
|
+
:style="{
|
|
60
|
+
paddingLeft: `${1 + node.depth * 1.5}rem`,
|
|
61
|
+
'--cms-row-delay': `${Math.min(index, 14) * 24}ms`
|
|
62
|
+
}"
|
|
52
63
|
>
|
|
53
64
|
<CmsIcon
|
|
54
65
|
v-if="node.depth"
|
|
55
66
|
name="arrow-turn-left-up"
|
|
56
67
|
class="cms-tree-branch size-3.5"
|
|
57
68
|
/>
|
|
69
|
+
<span class="cms-tree-doc">
|
|
70
|
+
<CmsIcon name="document-text" class="size-4" />
|
|
71
|
+
</span>
|
|
58
72
|
<div class="min-w-0 flex-1">
|
|
59
|
-
<div class="
|
|
73
|
+
<div class="cms-tree-title">
|
|
60
74
|
{{ node.row.label }}
|
|
61
75
|
</div>
|
|
62
|
-
<div class="cms-
|
|
76
|
+
<div class="cms-tree-path">{{ node.row.path }}</div>
|
|
63
77
|
</div>
|
|
64
|
-
<span class="cms-
|
|
65
|
-
<CmsIcon name="chevron-right" class="
|
|
78
|
+
<span class="cms-tree-meta">{{ lastEdit(node.row.updatedAt) }}</span>
|
|
79
|
+
<CmsIcon name="chevron-right" class="cms-tree-chevron size-4" />
|
|
66
80
|
</NuxtLink>
|
|
67
81
|
</div>
|
|
68
82
|
|
|
@@ -89,22 +103,54 @@
|
|
|
89
103
|
placeholder="Search…"
|
|
90
104
|
class="flex-1"
|
|
91
105
|
/>
|
|
92
|
-
<div class="cms-toolbar-actions">
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
106
|
+
<div v-if="selected.length" :key="'bulk'" class="cms-toolbar-actions cms-bulk-actions">
|
|
107
|
+
<span class="cms-bulk-count">{{ selected.length }} selected</span>
|
|
108
|
+
<CmsButton
|
|
109
|
+
icon="x-mark"
|
|
110
|
+
variant="ghost"
|
|
111
|
+
color="neutral"
|
|
112
|
+
size="sm"
|
|
113
|
+
aria-label="Clear selection"
|
|
114
|
+
title="Clear selection"
|
|
115
|
+
@click="selected = []"
|
|
116
|
+
/>
|
|
117
|
+
<template v-if="drafts">
|
|
118
|
+
<CmsButton
|
|
119
|
+
label="Draft"
|
|
120
|
+
icon="pencil-square"
|
|
121
|
+
variant="soft"
|
|
122
|
+
:disabled="saving"
|
|
123
|
+
@click="bulkStatus('draft')"
|
|
124
|
+
/>
|
|
125
|
+
<CmsButton
|
|
126
|
+
label="Publish"
|
|
127
|
+
icon="check-circle"
|
|
128
|
+
variant="soft"
|
|
129
|
+
:disabled="saving"
|
|
130
|
+
@click="bulkStatus('published')"
|
|
131
|
+
/>
|
|
132
|
+
</template>
|
|
133
|
+
<CmsButton
|
|
134
|
+
label="Delete"
|
|
135
|
+
icon="trash"
|
|
136
|
+
color="error"
|
|
137
|
+
:loading="saving"
|
|
138
|
+
@click="bulkDelete"
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
<div v-else :key="'default'" class="cms-toolbar-actions">
|
|
96
142
|
<CmsButton label="New entry" icon="plus" @click="openCreate" />
|
|
97
143
|
</div>
|
|
98
144
|
</div>
|
|
99
145
|
|
|
100
146
|
<CmsTable
|
|
101
147
|
v-if="rows.length"
|
|
102
|
-
v-model:
|
|
148
|
+
v-model:selected="selected"
|
|
149
|
+
selectable
|
|
103
150
|
v-model:sort="sort"
|
|
104
151
|
:data="rows"
|
|
105
152
|
:columns="columns"
|
|
106
153
|
@select="onSelect"
|
|
107
|
-
@reorder="reorderColumns"
|
|
108
154
|
>
|
|
109
155
|
<template v-for="key in mediaKeys" #[`${key}-cell`]="{ row }" :key="key">
|
|
110
156
|
<CmsMediaThumb :value="mediaThumbValue(key, row.original[key])" />
|
|
@@ -112,17 +158,6 @@
|
|
|
112
158
|
<template v-if="drafts" #status-cell="{ row }">
|
|
113
159
|
<CmsStatusBadge :published="row.original.status === 'published'" />
|
|
114
160
|
</template>
|
|
115
|
-
<template #actions-cell="{ row }">
|
|
116
|
-
<div class="cms-table-cell-actions">
|
|
117
|
-
<CmsButton
|
|
118
|
-
icon="trash"
|
|
119
|
-
variant="ghost"
|
|
120
|
-
color="error"
|
|
121
|
-
size="xs"
|
|
122
|
-
@click.stop="deleteRow(row.original)"
|
|
123
|
-
/>
|
|
124
|
-
</div>
|
|
125
|
-
</template>
|
|
126
161
|
</CmsTable>
|
|
127
162
|
|
|
128
163
|
<CmsSpinner v-else-if="status === 'pending'" />
|
|
@@ -178,6 +213,7 @@ import {
|
|
|
178
213
|
import cmsConfig from "#cms-config";
|
|
179
214
|
import { useCmsConfirm } from "../composables/cms-confirm";
|
|
180
215
|
import { useCmsRuntime } from "../composables/cms-runtime";
|
|
216
|
+
import { entryListColumns, useCmsSettingsState } from "../composables/cms-settings";
|
|
181
217
|
import { useCmsToast } from "../composables/cms-toast";
|
|
182
218
|
import { cmsApi } from "../utils/api";
|
|
183
219
|
import { errorMessage } from "../utils/ui";
|
|
@@ -195,6 +231,12 @@ const config = cmsConfig[name];
|
|
|
195
231
|
if (!config) {
|
|
196
232
|
throw createError({ statusCode: 404, statusMessage: "Unknown collection", fatal: true });
|
|
197
233
|
}
|
|
234
|
+
const KIND_META = {
|
|
235
|
+
collection: { icon: "square-3-stack-3d", description: "Collection of entries." },
|
|
236
|
+
single: { icon: "document-text", description: "Single entry, edited in place." },
|
|
237
|
+
page: { icon: "window", description: "Content for the routes of your app." }
|
|
238
|
+
};
|
|
239
|
+
const kindMeta = { ...KIND_META[config.kind], icon: config.icon ?? KIND_META[config.kind].icon };
|
|
198
240
|
const fieldKeys = Object.keys(config.fields);
|
|
199
241
|
const mediaKeys = fieldKeys.filter((key) => config.fields[key].type === "media");
|
|
200
242
|
const drafts = config.kind === "collection" && !!config.drafts;
|
|
@@ -347,82 +389,20 @@ function displayValue(field, key, value) {
|
|
|
347
389
|
return truncate(String(value));
|
|
348
390
|
}
|
|
349
391
|
}
|
|
350
|
-
const columnOrder = ref([...fieldKeys]);
|
|
351
|
-
const orderedKeys = computed(() => {
|
|
352
|
-
const known = columnOrder.value.filter((key) => fieldKeys.includes(key));
|
|
353
|
-
return [...known, ...fieldKeys.filter((key) => !known.includes(key))];
|
|
354
|
-
});
|
|
355
392
|
function isSortable(field) {
|
|
356
393
|
if (field.type === "blocks") return false;
|
|
357
394
|
return !(field.type === "relation" && field.cardinality === "many-to-many");
|
|
358
395
|
}
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
...drafts ? [{ accessorKey: "status", header: "Status", sortable: true }] : [],
|
|
368
|
-
{ id: "actions", header: "" }
|
|
369
|
-
]);
|
|
370
|
-
function reorderColumns(from, to) {
|
|
371
|
-
const next = [...orderedKeys.value];
|
|
372
|
-
const fromIndex = next.indexOf(from);
|
|
373
|
-
const toIndex = next.indexOf(to);
|
|
374
|
-
if (fromIndex === -1 || toIndex === -1) return;
|
|
375
|
-
next.splice(toIndex, 0, ...next.splice(fromIndex, 1));
|
|
376
|
-
columnOrder.value = next;
|
|
377
|
-
}
|
|
378
|
-
const DEFAULT_VISIBLE_COLUMNS = 4;
|
|
379
|
-
const columnStorageKey = `cms:columns:${name}`;
|
|
380
|
-
const columnOrderStorageKey = `cms:column-order:${name}`;
|
|
381
|
-
const columnVisibility = ref(
|
|
382
|
-
Object.fromEntries(fieldKeys.map((key, index) => [key, index < DEFAULT_VISIBLE_COLUMNS]))
|
|
383
|
-
);
|
|
384
|
-
function readStored(key) {
|
|
385
|
-
const stored = localStorage.getItem(key);
|
|
386
|
-
if (!stored) return void 0;
|
|
387
|
-
try {
|
|
388
|
-
return JSON.parse(stored);
|
|
389
|
-
} catch {
|
|
390
|
-
return void 0;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
onMounted(() => {
|
|
394
|
-
const storedVisibility = readStored(columnStorageKey);
|
|
395
|
-
if (storedVisibility) {
|
|
396
|
-
columnVisibility.value = Object.fromEntries(
|
|
397
|
-
fieldKeys.map((key) => [
|
|
398
|
-
key,
|
|
399
|
-
storedVisibility[key] ?? columnVisibility.value[key] ?? false
|
|
400
|
-
])
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
const storedOrder = readStored(columnOrderStorageKey);
|
|
404
|
-
if (Array.isArray(storedOrder)) {
|
|
405
|
-
columnOrder.value = storedOrder.filter((key) => typeof key === "string");
|
|
406
|
-
}
|
|
407
|
-
watch(
|
|
408
|
-
columnVisibility,
|
|
409
|
-
(value) => localStorage.setItem(columnStorageKey, JSON.stringify(value)),
|
|
410
|
-
{ deep: true }
|
|
411
|
-
);
|
|
412
|
-
watch(orderedKeys, (value) => localStorage.setItem(columnOrderStorageKey, JSON.stringify(value)));
|
|
413
|
-
});
|
|
414
|
-
const columnItems = computed(
|
|
415
|
-
() => orderedKeys.value.map((key) => ({
|
|
416
|
-
label: config.fields[key].label,
|
|
417
|
-
type: "checkbox",
|
|
418
|
-
checked: columnVisibility.value[key],
|
|
419
|
-
onUpdateChecked(checked) {
|
|
420
|
-
columnVisibility.value = { ...columnVisibility.value, [key]: checked };
|
|
421
|
-
},
|
|
422
|
-
onSelect(event) {
|
|
423
|
-
event.preventDefault();
|
|
396
|
+
const settings = useCmsSettingsState();
|
|
397
|
+
const columns = computed(
|
|
398
|
+
() => entryListColumns(name, settings.value).map(
|
|
399
|
+
(key) => key === "status" && !config.fields.status ? { accessorKey: "status", header: "Status", sortable: true } : {
|
|
400
|
+
id: key,
|
|
401
|
+
accessorFn: (row) => displayValue(config.fields[key], key, row[key]),
|
|
402
|
+
header: config.fields[key].label,
|
|
403
|
+
sortable: isSortable(config.fields[key])
|
|
424
404
|
}
|
|
425
|
-
|
|
405
|
+
)
|
|
426
406
|
);
|
|
427
407
|
const drawerOpen = ref(false);
|
|
428
408
|
const drawerEntryId = ref(null);
|
|
@@ -439,10 +419,56 @@ async function onDrawerDeleted() {
|
|
|
439
419
|
else await refresh();
|
|
440
420
|
}
|
|
441
421
|
const confirmAction = useCmsConfirm();
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
422
|
+
const selected = ref([]);
|
|
423
|
+
watch(rows, (current) => {
|
|
424
|
+
const ids = new Set(current.map((row) => String(row.id)));
|
|
425
|
+
selected.value = selected.value.filter((id) => ids.has(id));
|
|
426
|
+
});
|
|
427
|
+
function entriesLabel(count) {
|
|
428
|
+
return `${count} ${count === 1 ? "entry" : "entries"}`;
|
|
429
|
+
}
|
|
430
|
+
async function runBulk(ids, action) {
|
|
431
|
+
saving.value = true;
|
|
432
|
+
try {
|
|
433
|
+
const results = await Promise.allSettled(ids.map(action));
|
|
434
|
+
const failed = results.flatMap(
|
|
435
|
+
(result, index) => result.status === "rejected" ? [{ id: ids[index], reason: result.reason }] : []
|
|
436
|
+
);
|
|
437
|
+
await refresh();
|
|
438
|
+
selected.value = failed.map((f) => f.id);
|
|
439
|
+
return failed;
|
|
440
|
+
} finally {
|
|
441
|
+
saving.value = false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function reportBulk(done, total2, failed) {
|
|
445
|
+
const succeeded = total2 - failed.length;
|
|
446
|
+
if (succeeded) toast.add({ title: `${done}: ${entriesLabel(succeeded)}`, color: "success" });
|
|
447
|
+
if (failed.length) {
|
|
448
|
+
toast.add({
|
|
449
|
+
title: `${entriesLabel(failed.length)} not changed`,
|
|
450
|
+
description: errorMessage(failed[0].reason),
|
|
451
|
+
color: "error"
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async function bulkDelete() {
|
|
456
|
+
const ids = [...selected.value];
|
|
457
|
+
if (!ids.length || saving.value) return;
|
|
458
|
+
if (!await confirmAction(`Delete ${entriesLabel(ids.length)}?`)) return;
|
|
459
|
+
const emptiesPage = ids.length >= rows.value.length && page.value > 1;
|
|
460
|
+
const failed = await runBulk(ids, (id) => cmsApi(`${endpoint}/${id}`, { method: "DELETE" }));
|
|
461
|
+
reportBulk("Deleted", ids.length, failed);
|
|
462
|
+
if (!failed.length && emptiesPage) page.value -= 1;
|
|
463
|
+
}
|
|
464
|
+
async function bulkStatus(status2) {
|
|
465
|
+
const ids = [...selected.value];
|
|
466
|
+
if (!ids.length || saving.value) return;
|
|
467
|
+
const failed = await runBulk(ids, async (id) => {
|
|
468
|
+
const entry = await cmsApi(`${endpoint}/${id}`);
|
|
469
|
+
const body = Object.fromEntries(formKeys.map((key) => [key, entry[key] ?? null]));
|
|
470
|
+
return cmsApi(`${endpoint}/${id}`, { method: "PUT", body: { ...body, status: status2 } });
|
|
471
|
+
});
|
|
472
|
+
reportBulk(status2 === "published" ? "Published" : "Moved to draft", ids.length, failed);
|
|
447
473
|
}
|
|
448
474
|
</script>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="cms-scope cms-canvas cms-auth">
|
|
2
|
+
<div class="cms-scope cms-canvas cms-auth" :data-theme="theme">
|
|
3
3
|
<div class="cms-auth-panel cms-rise">
|
|
4
4
|
<header class="cms-auth-header">
|
|
5
|
-
<
|
|
5
|
+
<CmsBrandMark size="lg" />
|
|
6
|
+
<h1 class="cms-title cms-title-lg">Welcome back</h1>
|
|
6
7
|
<p class="cms-subtitle">Sign in to manage your content.</p>
|
|
7
8
|
</header>
|
|
8
9
|
|
|
@@ -13,6 +14,7 @@
|
|
|
13
14
|
<CmsInput
|
|
14
15
|
v-model="state.email"
|
|
15
16
|
type="email"
|
|
17
|
+
icon="envelope"
|
|
16
18
|
autocomplete="username"
|
|
17
19
|
placeholder="you@example.com"
|
|
18
20
|
/>
|
|
@@ -21,13 +23,24 @@
|
|
|
21
23
|
<CmsInput
|
|
22
24
|
v-model="state.password"
|
|
23
25
|
type="password"
|
|
26
|
+
icon="key"
|
|
24
27
|
autocomplete="current-password"
|
|
25
28
|
placeholder="••••••••"
|
|
26
29
|
/>
|
|
27
30
|
</CmsFormField>
|
|
28
|
-
<CmsButton
|
|
31
|
+
<CmsButton
|
|
32
|
+
type="submit"
|
|
33
|
+
label="Sign in"
|
|
34
|
+
icon="arrow-right"
|
|
35
|
+
trailing-icon
|
|
36
|
+
size="lg"
|
|
37
|
+
block
|
|
38
|
+
:loading="loading"
|
|
39
|
+
/>
|
|
29
40
|
</CmsForm>
|
|
30
41
|
</div>
|
|
42
|
+
|
|
43
|
+
<p class="cms-auth-footer">Powered by nuxt-cms</p>
|
|
31
44
|
</div>
|
|
32
45
|
</div>
|
|
33
46
|
</template>
|
|
@@ -35,7 +48,9 @@
|
|
|
35
48
|
<script setup>
|
|
36
49
|
import { definePageMeta, navigateTo, ref, useUserSession } from "#imports";
|
|
37
50
|
import { errorMessage } from "../utils/ui";
|
|
51
|
+
import { useCmsTheme } from "../composables/cms-theme";
|
|
38
52
|
definePageMeta({ layout: false });
|
|
53
|
+
const theme = useCmsTheme();
|
|
39
54
|
const { loggedIn } = useUserSession();
|
|
40
55
|
if (loggedIn.value) {
|
|
41
56
|
await navigateTo("/cms", { replace: true });
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="cms-page">
|
|
3
|
-
<CmsPageHeader
|
|
3
|
+
<CmsPageHeader
|
|
4
|
+
title="Media"
|
|
5
|
+
icon="photo"
|
|
6
|
+
description="Images, videos and files used across your content."
|
|
7
|
+
>
|
|
4
8
|
<template #badge>
|
|
5
9
|
<span class="cms-badge is-muted">{{ runtime.mediaStorage }}</span>
|
|
6
10
|
</template>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { InjectionKey, Reactive } from 'vue';
|
|
2
|
+
import type { FieldConfig } from '#nuxt-cms';
|
|
2
3
|
/** Shared reactive map of field-name -> error message, provided by CmsForm and
|
|
3
4
|
* read by CmsFormField to render inline validation errors. */
|
|
4
5
|
export declare const CMS_FORM_ERRORS: InjectionKey<Reactive<Record<string, string>>>;
|
|
5
6
|
export declare function errorMessage(error: unknown): string | undefined;
|
|
7
|
+
export declare function fieldIcon(field: FieldConfig): string;
|
|
@@ -3,3 +3,21 @@ export function errorMessage(error) {
|
|
|
3
3
|
const err = error;
|
|
4
4
|
return err.data?.message ?? err.message;
|
|
5
5
|
}
|
|
6
|
+
const FIELD_ICONS = {
|
|
7
|
+
text: "bars-3-bottom-left",
|
|
8
|
+
richtext: "document-text",
|
|
9
|
+
number: "hashtag",
|
|
10
|
+
boolean: "check-circle",
|
|
11
|
+
date: "calendar",
|
|
12
|
+
email: "at-symbol",
|
|
13
|
+
slug: "link",
|
|
14
|
+
select: "chevron-up-down",
|
|
15
|
+
json: "code-bracket",
|
|
16
|
+
media: "photo",
|
|
17
|
+
relation: "arrows-right-left",
|
|
18
|
+
blocks: "squares-2x2"
|
|
19
|
+
};
|
|
20
|
+
export function fieldIcon(field) {
|
|
21
|
+
if (field.type === "select" && field.multiple) return "list-bullet";
|
|
22
|
+
return FIELD_ICONS[field.type];
|
|
23
|
+
}
|