@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
|
@@ -14,29 +14,72 @@
|
|
|
14
14
|
{{ tab.label }}
|
|
15
15
|
</button>
|
|
16
16
|
</div>
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
<div class="cms-form-layout">
|
|
18
|
+
<div class="cms-form-main">
|
|
19
|
+
<component
|
|
20
|
+
:is="section.title ? 'section' : 'div'"
|
|
21
|
+
v-for="section in sections"
|
|
22
|
+
:key="section.id"
|
|
23
|
+
class="cms-form-section"
|
|
24
|
+
:class="{ 'is-titled': !!section.title, 'is-collapsed': isCollapsed(section) }"
|
|
25
|
+
>
|
|
26
|
+
<button
|
|
27
|
+
v-if="section.title"
|
|
28
|
+
type="button"
|
|
29
|
+
class="cms-form-section-header"
|
|
30
|
+
:aria-expanded="!isCollapsed(section)"
|
|
31
|
+
@click="toggleSection(section.id)"
|
|
32
|
+
>
|
|
33
|
+
<span class="cms-form-section-text">
|
|
34
|
+
<span class="cms-form-section-title">{{ section.title }}</span>
|
|
35
|
+
<span v-if="section.description" class="cms-form-section-description">
|
|
36
|
+
{{ section.description }}
|
|
37
|
+
</span>
|
|
38
|
+
</span>
|
|
39
|
+
<CmsIcon name="chevron-down" class="cms-form-section-chevron size-4" />
|
|
40
|
+
</button>
|
|
41
|
+
<div v-show="!isCollapsed(section)" class="cms-form-rows">
|
|
42
|
+
<div
|
|
43
|
+
v-for="row in section.rows"
|
|
44
|
+
:key="row.join('|')"
|
|
45
|
+
class="cms-form-row"
|
|
46
|
+
:style="{ '--cms-row-cols': row.length }"
|
|
47
|
+
>
|
|
48
|
+
<CmsFormField
|
|
49
|
+
v-for="key in row"
|
|
50
|
+
:key="key"
|
|
51
|
+
:label="fields[key].label"
|
|
52
|
+
:description="fields[key].description"
|
|
53
|
+
:icon="fieldIcon(fields[key])"
|
|
54
|
+
:name="key"
|
|
55
|
+
:required="fields[key].required"
|
|
56
|
+
>
|
|
57
|
+
<template
|
|
58
|
+
v-if="hasLocaleSwitch(fields[key]) && i18n.locales.length > 1"
|
|
59
|
+
#label-actions
|
|
60
|
+
>
|
|
61
|
+
<CmsLocaleSwitch
|
|
62
|
+
:model-value="localeFor(key)"
|
|
63
|
+
:value="
|
|
64
|
+
isTranslatableField(fields[key]) ? state[key] : null
|
|
29
65
|
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
66
|
+
@update:model-value="(locale) => setLocale(key, locale)"
|
|
67
|
+
/>
|
|
68
|
+
</template>
|
|
69
|
+
<CmsFieldInput
|
|
70
|
+
v-model="state[key]"
|
|
71
|
+
:field="fields[key]"
|
|
72
|
+
:locale="localeFor(key)"
|
|
73
|
+
:slug-source="
|
|
74
|
+
fields[key].from ? state[fields[key].from] : void 0
|
|
75
|
+
"
|
|
76
|
+
/>
|
|
77
|
+
</CmsFormField>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</component>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
40
83
|
<div v-if="footer" class="cms-form-actions">
|
|
41
84
|
<CmsButton type="submit" label="Save" :loading="loading" />
|
|
42
85
|
<CmsButton
|
|
@@ -55,14 +98,18 @@ import {
|
|
|
55
98
|
fieldTab,
|
|
56
99
|
hasTranslatableBlockFields,
|
|
57
100
|
isFieldVisible,
|
|
58
|
-
isTranslatableField
|
|
101
|
+
isTranslatableField,
|
|
102
|
+
resolveFormLayout
|
|
59
103
|
} from "#nuxt-cms";
|
|
60
|
-
import { computed, ref, watch } from "#imports";
|
|
104
|
+
import { computed, nextTick, ref, watch } from "#imports";
|
|
61
105
|
import { buildEntrySchema } from "../../../shared/validation";
|
|
62
106
|
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
107
|
+
import { entryFormLayout, useCmsSettingsState } from "../../composables/cms-settings";
|
|
108
|
+
import { fieldIcon } from "../../utils/ui";
|
|
63
109
|
const props = defineProps({
|
|
64
110
|
fields: { type: Object, required: true },
|
|
65
111
|
tabs: { type: Array, required: false },
|
|
112
|
+
entryName: { type: String, required: false },
|
|
66
113
|
drafts: { type: Boolean, required: false },
|
|
67
114
|
loading: { type: Boolean, required: false },
|
|
68
115
|
formId: { type: String, required: false },
|
|
@@ -77,13 +124,40 @@ const activeTab = ref(tabList.value[0]?.id ?? "");
|
|
|
77
124
|
watch(tabList, (tabs) => {
|
|
78
125
|
if (!tabs.some((tab) => tab.id === activeTab.value)) activeTab.value = tabs[0]?.id ?? "";
|
|
79
126
|
});
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
127
|
+
const settings = useCmsSettingsState();
|
|
128
|
+
const fields = computed(() => props.fields);
|
|
129
|
+
const layout = computed(
|
|
130
|
+
() => props.entryName ? entryFormLayout(props.entryName, settings.value, props.fields) : resolveFormLayout(props.fields, void 0)
|
|
131
|
+
);
|
|
132
|
+
function isShown(key) {
|
|
133
|
+
const field = props.fields[key];
|
|
134
|
+
if (!field || !isFieldVisible(field, state.value)) return false;
|
|
135
|
+
if (!tabList.value.length) return true;
|
|
136
|
+
return fieldTab(field, tabList.value) === activeTab.value;
|
|
137
|
+
}
|
|
138
|
+
const sections = computed(
|
|
139
|
+
() => layout.value.main.map((section, index) => ({
|
|
140
|
+
...section,
|
|
141
|
+
id: `${index}-${section.title ?? ""}`,
|
|
142
|
+
rows: section.rows.map((row) => row.filter((key) => isShown(key))).filter((row) => row.length)
|
|
143
|
+
})).filter((section) => section.rows.length)
|
|
86
144
|
);
|
|
145
|
+
const toggled = ref({});
|
|
146
|
+
function isCollapsed(section) {
|
|
147
|
+
if (!section.title) return false;
|
|
148
|
+
return toggled.value[section.id] ?? !!section.collapsed;
|
|
149
|
+
}
|
|
150
|
+
function toggleSection(id) {
|
|
151
|
+
const section = sections.value.find((s) => s.id === id);
|
|
152
|
+
if (section) toggled.value = { ...toggled.value, [id]: !isCollapsed(section) };
|
|
153
|
+
}
|
|
154
|
+
function revealField(key) {
|
|
155
|
+
for (const section of sections.value) {
|
|
156
|
+
if (section.rows.some((row) => row.includes(key)) && isCollapsed(section)) {
|
|
157
|
+
toggled.value = { ...toggled.value, [section.id]: false };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
87
161
|
function onError() {
|
|
88
162
|
const issue = schema.value.safeParse(state.value).error?.issues[0];
|
|
89
163
|
const key = issue ? String(issue.path[0] ?? "") : "";
|
|
@@ -92,6 +166,7 @@ function onError() {
|
|
|
92
166
|
const tab = fieldTab(field, tabList.value);
|
|
93
167
|
if (tab) activeTab.value = tab;
|
|
94
168
|
}
|
|
169
|
+
if (key) void nextTick(() => revealField(key));
|
|
95
170
|
emit("error");
|
|
96
171
|
}
|
|
97
172
|
function hasLocaleSwitch(field) {
|
|
@@ -12,16 +12,26 @@
|
|
|
12
12
|
:locale="locale"
|
|
13
13
|
/>
|
|
14
14
|
<CmsRichTextField v-else-if="field.type === 'richtext'" v-model="strOrNull" />
|
|
15
|
-
<CmsTextarea
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
<CmsTextarea
|
|
16
|
+
v-else-if="field.type === 'text' && field.textarea"
|
|
17
|
+
v-model="str"
|
|
18
|
+
:rows="8"
|
|
19
|
+
:placeholder="field.placeholder"
|
|
20
|
+
/>
|
|
21
|
+
<CmsInput v-else-if="field.type === 'text'" v-model="str" :placeholder="field.placeholder" />
|
|
22
|
+
<CmsInput
|
|
23
|
+
v-else-if="field.type === 'email'"
|
|
24
|
+
v-model="str"
|
|
25
|
+
type="email"
|
|
26
|
+
:placeholder="field.placeholder"
|
|
27
|
+
/>
|
|
18
28
|
<CmsSlugField v-else-if="field.type === 'slug'" v-model="strOrNull" :source="slugSource" />
|
|
19
29
|
<CmsSelectMenu
|
|
20
30
|
v-else-if="field.type === 'select'"
|
|
21
31
|
v-model="selValue"
|
|
22
32
|
:multiple="field.multiple"
|
|
23
33
|
:items="selectItems"
|
|
24
|
-
placeholder="Select
|
|
34
|
+
:placeholder="field.placeholder ?? 'Select\u2026'"
|
|
25
35
|
/>
|
|
26
36
|
<CmsSwitch
|
|
27
37
|
v-else-if="field.type === 'boolean'"
|
|
@@ -33,7 +43,12 @@
|
|
|
33
43
|
"
|
|
34
44
|
/>
|
|
35
45
|
<CmsJsonField v-else-if="field.type === 'json'" v-model="jsonValue" />
|
|
36
|
-
<CmsInput
|
|
46
|
+
<CmsInput
|
|
47
|
+
v-else-if="field.type === 'number'"
|
|
48
|
+
v-model.number="num"
|
|
49
|
+
type="number"
|
|
50
|
+
:placeholder="field.placeholder"
|
|
51
|
+
/>
|
|
37
52
|
<CmsInput v-else-if="field.type === 'date'" v-model="str" type="date" />
|
|
38
53
|
<CmsRelationField
|
|
39
54
|
v-else-if="field.type === 'relation'"
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
label?: string;
|
|
3
|
+
icon?: string;
|
|
4
|
+
description?: string;
|
|
3
5
|
name?: string;
|
|
4
6
|
required?: boolean;
|
|
5
7
|
};
|
|
6
|
-
declare var
|
|
8
|
+
declare var __VLS_6: {}, __VLS_8: {};
|
|
7
9
|
type __VLS_Slots = {} & {
|
|
8
|
-
'label-actions'?: (props: typeof
|
|
10
|
+
'label-actions'?: (props: typeof __VLS_6) => any;
|
|
9
11
|
} & {
|
|
10
|
-
default?: (props: typeof
|
|
12
|
+
default?: (props: typeof __VLS_8) => any;
|
|
11
13
|
};
|
|
12
14
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
15
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="cms-form-field">
|
|
2
|
+
<div class="cms-form-field" :class="{ 'is-error': error }">
|
|
3
3
|
<div v-if="label || $slots['label-actions']" class="cms-form-label-row">
|
|
4
4
|
<label v-if="label" class="cms-form-label">
|
|
5
|
+
<CmsIcon v-if="icon" :name="icon" class="cms-form-label-icon size-3.5" />
|
|
5
6
|
{{ label }}<span v-if="required" class="cms-form-required"> *</span>
|
|
6
7
|
</label>
|
|
7
8
|
<slot name="label-actions" />
|
|
8
9
|
</div>
|
|
9
10
|
<slot />
|
|
10
|
-
<p v-if="
|
|
11
|
+
<p v-if="description" class="cms-form-description">{{ description }}</p>
|
|
12
|
+
<p v-if="error" class="cms-form-error">
|
|
13
|
+
<CmsIcon name="exclamation-circle" class="size-3.5 shrink-0" />{{ error }}
|
|
14
|
+
</p>
|
|
11
15
|
</div>
|
|
12
16
|
</template>
|
|
13
17
|
|
|
@@ -16,6 +20,8 @@ import { computed, inject } from "#imports";
|
|
|
16
20
|
import { CMS_FORM_ERRORS } from "../../utils/ui";
|
|
17
21
|
const props = defineProps({
|
|
18
22
|
label: { type: String, required: false },
|
|
23
|
+
icon: { type: String, required: false },
|
|
24
|
+
description: { type: String, required: false },
|
|
19
25
|
name: { type: String, required: false },
|
|
20
26
|
required: { type: Boolean, required: false }
|
|
21
27
|
});
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
label?: string;
|
|
3
|
+
icon?: string;
|
|
4
|
+
description?: string;
|
|
3
5
|
name?: string;
|
|
4
6
|
required?: boolean;
|
|
5
7
|
};
|
|
6
|
-
declare var
|
|
8
|
+
declare var __VLS_6: {}, __VLS_8: {};
|
|
7
9
|
type __VLS_Slots = {} & {
|
|
8
|
-
'label-actions'?: (props: typeof
|
|
10
|
+
'label-actions'?: (props: typeof __VLS_6) => any;
|
|
9
11
|
} & {
|
|
10
|
-
default?: (props: typeof
|
|
12
|
+
default?: (props: typeof __VLS_8) => any;
|
|
11
13
|
};
|
|
12
14
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
15
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -431,10 +431,11 @@
|
|
|
431
431
|
:media-type="mediaType"
|
|
432
432
|
:accept="accept"
|
|
433
433
|
:folder="folder"
|
|
434
|
+
:directories="!selectable"
|
|
434
435
|
@uploaded="onUploaded"
|
|
435
436
|
/>
|
|
436
437
|
<p class="cms-form-hint cms-media-upload-hint">
|
|
437
|
-
Tip: you can also drop files anywhere on the library, or on a folder.
|
|
438
|
+
Tip: you can also drop files or folders anywhere on the library, or on a folder.
|
|
438
439
|
</p>
|
|
439
440
|
</template>
|
|
440
441
|
</CmsModal>
|
|
@@ -562,7 +563,11 @@ import {
|
|
|
562
563
|
} from "#nuxt-cms";
|
|
563
564
|
import { useCmsConfirm } from "../../composables/cms-confirm";
|
|
564
565
|
import { useCmsMediaLibrary, usageSummary } from "../../composables/cms-media-library";
|
|
565
|
-
import {
|
|
566
|
+
import {
|
|
567
|
+
uploadTreeFromDrop,
|
|
568
|
+
uploadTreeFromFiles,
|
|
569
|
+
useCmsMediaUploader
|
|
570
|
+
} from "../../composables/cms-media-uploader";
|
|
566
571
|
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
567
572
|
import { useCmsToast } from "../../composables/cms-toast";
|
|
568
573
|
const MEDIA_DRAG = "application/x-cms-media";
|
|
@@ -946,7 +951,10 @@ async function moveSelection() {
|
|
|
946
951
|
}
|
|
947
952
|
const uploadOpen = ref(false);
|
|
948
953
|
async function afterUpload(uploaded) {
|
|
949
|
-
if (!uploaded.length)
|
|
954
|
+
if (!uploaded.length) {
|
|
955
|
+
if (!props.selectable) await library.reload({ quiet: true });
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
950
958
|
if (props.selectable && uploaded[0]) {
|
|
951
959
|
emitSelect(uploaded[0]);
|
|
952
960
|
return;
|
|
@@ -961,13 +969,19 @@ function onUploaded(uploaded) {
|
|
|
961
969
|
uploadOpen.value = false;
|
|
962
970
|
void afterUpload(uploaded);
|
|
963
971
|
}
|
|
964
|
-
|
|
965
|
-
const
|
|
972
|
+
function droppedTree(event) {
|
|
973
|
+
const data = event.dataTransfer;
|
|
974
|
+
if (!data?.files.length) return null;
|
|
975
|
+
return props.selectable ? Promise.resolve(uploadTreeFromFiles(data.files)) : uploadTreeFromDrop(data);
|
|
976
|
+
}
|
|
977
|
+
async function uploadTree(pending, target) {
|
|
978
|
+
const tree = await pending;
|
|
979
|
+
const uploaded = await uploader.upload(tree, {
|
|
966
980
|
folder: target,
|
|
967
981
|
accept: acceptAttr.value,
|
|
968
982
|
multiple: !props.selectable
|
|
969
983
|
});
|
|
970
|
-
await afterUpload(uploaded);
|
|
984
|
+
if (uploaded.length || tree.directories.length) await afterUpload(uploaded);
|
|
971
985
|
}
|
|
972
986
|
const draggingKeys = ref([]);
|
|
973
987
|
const draggingFolder = ref(null);
|
|
@@ -1036,7 +1050,7 @@ async function onTargetDrop(event, target) {
|
|
|
1036
1050
|
event.stopPropagation();
|
|
1037
1051
|
const keys = [...draggingKeys.value];
|
|
1038
1052
|
const path = draggingFolder.value;
|
|
1039
|
-
const
|
|
1053
|
+
const tree = hasFiles(event) ? droppedTree(event) : null;
|
|
1040
1054
|
onDragEnd();
|
|
1041
1055
|
fileDragDepth.value = 0;
|
|
1042
1056
|
if (keys.length) {
|
|
@@ -1051,7 +1065,7 @@ async function onTargetDrop(event, target) {
|
|
|
1051
1065
|
}
|
|
1052
1066
|
return;
|
|
1053
1067
|
}
|
|
1054
|
-
if (
|
|
1068
|
+
if (tree) await uploadTree(tree, target);
|
|
1055
1069
|
}
|
|
1056
1070
|
function onGalleryDragEnter(event) {
|
|
1057
1071
|
if (!hasFiles(event) || !canUpload.value) return;
|
|
@@ -1071,8 +1085,8 @@ async function onGalleryDrop(event) {
|
|
|
1071
1085
|
event.preventDefault();
|
|
1072
1086
|
fileDragDepth.value = 0;
|
|
1073
1087
|
dropTarget.value = null;
|
|
1074
|
-
const
|
|
1075
|
-
if (
|
|
1088
|
+
const tree = droppedTree(event);
|
|
1089
|
+
if (tree) await uploadTree(tree, folder.value);
|
|
1076
1090
|
}
|
|
1077
1091
|
function tileInfo(item) {
|
|
1078
1092
|
const parts = [extension(item.key)];
|
|
@@ -4,6 +4,7 @@ type __VLS_Props = {
|
|
|
4
4
|
mediaType?: MediaType | MediaType[];
|
|
5
5
|
accept?: string[];
|
|
6
6
|
folder?: string | null;
|
|
7
|
+
directories?: boolean;
|
|
7
8
|
};
|
|
8
9
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
10
|
uploaded: (items: MediaItem[]) => any;
|
|
@@ -1,72 +1,102 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
2
|
+
<div class="flex flex-col items-center gap-2">
|
|
3
|
+
<button
|
|
4
|
+
type="button"
|
|
5
|
+
class="cms-dropzone"
|
|
6
|
+
:class="{ 'is-over': dragOver, 'is-busy': uploading }"
|
|
7
|
+
:disabled="uploading"
|
|
8
|
+
@click="input?.click()"
|
|
9
|
+
@dragover.prevent="dragOver = true"
|
|
10
|
+
@dragleave.prevent="dragOver = false"
|
|
11
|
+
@drop.prevent.stop="onDrop"
|
|
12
|
+
>
|
|
13
|
+
<input
|
|
14
|
+
ref="input"
|
|
15
|
+
type="file"
|
|
16
|
+
class="hidden"
|
|
17
|
+
:accept="acceptAttr"
|
|
18
|
+
:multiple="multiple"
|
|
19
|
+
@change="onChange"
|
|
20
|
+
@click.stop
|
|
21
|
+
/>
|
|
22
|
+
<CmsIcon
|
|
23
|
+
:name="uploading ? 'arrow-path' : 'cloud-arrow-up'"
|
|
24
|
+
class="size-5 shrink-0"
|
|
25
|
+
:class="{ 'animate-spin': uploading }"
|
|
26
|
+
/>
|
|
27
|
+
<span class="text-sm font-medium">
|
|
28
|
+
{{
|
|
29
|
+
uploading ? `Uploading ${Math.min(done + 1, total)} of ${total}\u2026` : multiple ? "Drop files here or click to browse" : "Drop a file here or click to browse"
|
|
30
|
+
}}
|
|
31
|
+
</span>
|
|
32
|
+
<span v-if="!uploading" class="cms-dropzone-hint">
|
|
33
|
+
<CmsIcon :name="folder ? 'folder' : 'circle-stack'" class="size-3.5" />
|
|
34
|
+
{{ folder ?? "Library root" }}
|
|
35
|
+
</span>
|
|
36
|
+
</button>
|
|
37
|
+
<template v-if="directories && multiple && !uploading">
|
|
38
|
+
<input
|
|
39
|
+
ref="directoryInput"
|
|
40
|
+
type="file"
|
|
41
|
+
class="hidden"
|
|
42
|
+
webkitdirectory
|
|
43
|
+
multiple
|
|
44
|
+
@change="onChange"
|
|
45
|
+
/>
|
|
46
|
+
<CmsButton
|
|
47
|
+
label="Upload a folder"
|
|
48
|
+
icon="folder-arrow-down"
|
|
49
|
+
variant="ghost"
|
|
50
|
+
color="neutral"
|
|
51
|
+
size="sm"
|
|
52
|
+
@click="directoryInput?.click()"
|
|
53
|
+
/>
|
|
54
|
+
</template>
|
|
55
|
+
</div>
|
|
36
56
|
</template>
|
|
37
57
|
|
|
38
58
|
<script setup>
|
|
39
59
|
import { mediaTypeAccept } from "#nuxt-cms";
|
|
40
60
|
import { computed, ref } from "#imports";
|
|
41
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
uploadTreeFromDrop,
|
|
63
|
+
uploadTreeFromFiles,
|
|
64
|
+
useCmsMediaUploader
|
|
65
|
+
} from "../../composables/cms-media-uploader";
|
|
42
66
|
const props = defineProps({
|
|
43
67
|
multiple: { type: Boolean, required: false },
|
|
44
68
|
mediaType: { type: null, required: false, default: "file" },
|
|
45
69
|
accept: { type: Array, required: false },
|
|
46
|
-
folder: { type: [String, null], required: false, default: null }
|
|
70
|
+
folder: { type: [String, null], required: false, default: null },
|
|
71
|
+
directories: { type: Boolean, required: false }
|
|
47
72
|
});
|
|
48
73
|
const emit = defineEmits(["uploaded"]);
|
|
49
74
|
const { upload, uploading, done, total } = useCmsMediaUploader();
|
|
50
75
|
const input = ref(null);
|
|
76
|
+
const directoryInput = ref(null);
|
|
51
77
|
const dragOver = ref(false);
|
|
52
78
|
const acceptAttr = computed(
|
|
53
79
|
() => (props.accept?.length ? props.accept : mediaTypeAccept(props.mediaType))?.join(",")
|
|
54
80
|
);
|
|
55
|
-
async function
|
|
56
|
-
const
|
|
81
|
+
async function handleTree(pending) {
|
|
82
|
+
const tree = await pending;
|
|
83
|
+
const items = await upload(tree, {
|
|
57
84
|
folder: props.folder,
|
|
58
85
|
accept: acceptAttr.value,
|
|
59
86
|
multiple: props.multiple
|
|
60
87
|
});
|
|
61
|
-
if (items.length) emit("uploaded", items);
|
|
88
|
+
if (items.length || props.directories && tree.directories.length) emit("uploaded", items);
|
|
62
89
|
}
|
|
63
90
|
function onChange(event) {
|
|
64
91
|
const el = event.target;
|
|
65
|
-
if (el.files?.length) void
|
|
92
|
+
if (el.files?.length) void handleTree(uploadTreeFromFiles(el.files));
|
|
66
93
|
el.value = "";
|
|
67
94
|
}
|
|
68
95
|
function onDrop(event) {
|
|
69
96
|
dragOver.value = false;
|
|
70
|
-
if (event.dataTransfer?.files.length)
|
|
97
|
+
if (!event.dataTransfer?.files.length) return;
|
|
98
|
+
void handleTree(
|
|
99
|
+
props.directories ? uploadTreeFromDrop(event.dataTransfer) : uploadTreeFromFiles(event.dataTransfer.files)
|
|
100
|
+
);
|
|
71
101
|
}
|
|
72
102
|
</script>
|
|
@@ -4,6 +4,7 @@ type __VLS_Props = {
|
|
|
4
4
|
mediaType?: MediaType | MediaType[];
|
|
5
5
|
accept?: string[];
|
|
6
6
|
folder?: string | null;
|
|
7
|
+
directories?: boolean;
|
|
7
8
|
};
|
|
8
9
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
10
|
uploaded: (items: MediaItem[]) => any;
|
|
@@ -4,13 +4,15 @@ export interface CmsBreadcrumbLink {
|
|
|
4
4
|
}
|
|
5
5
|
type __VLS_Props = {
|
|
6
6
|
title: string;
|
|
7
|
+
icon?: string;
|
|
8
|
+
description?: string;
|
|
7
9
|
links?: CmsBreadcrumbLink[];
|
|
8
10
|
};
|
|
9
|
-
declare var
|
|
11
|
+
declare var __VLS_17: {}, __VLS_19: {};
|
|
10
12
|
type __VLS_Slots = {} & {
|
|
11
|
-
badge?: (props: typeof
|
|
13
|
+
badge?: (props: typeof __VLS_17) => any;
|
|
12
14
|
} & {
|
|
13
|
-
default?: (props: typeof
|
|
15
|
+
default?: (props: typeof __VLS_19) => any;
|
|
14
16
|
};
|
|
15
17
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
18
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -4,14 +4,22 @@
|
|
|
4
4
|
<nav v-if="links?.length" class="cms-breadcrumb">
|
|
5
5
|
<template v-for="link in links" :key="link.to">
|
|
6
6
|
<NuxtLink :to="link.to" class="cms-breadcrumb-link">{{ link.label }}</NuxtLink>
|
|
7
|
-
<CmsIcon name="chevron-right" class="cms-breadcrumb-separator size-
|
|
7
|
+
<CmsIcon name="chevron-right" class="cms-breadcrumb-separator size-3" />
|
|
8
8
|
</template>
|
|
9
9
|
</nav>
|
|
10
|
-
<div class="cms-
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
</
|
|
14
|
-
<
|
|
10
|
+
<div class="cms-page-title-row">
|
|
11
|
+
<span v-if="icon" class="cms-page-icon">
|
|
12
|
+
<CmsIcon :name="icon" class="size-[18px]" />
|
|
13
|
+
</span>
|
|
14
|
+
<div class="cms-page-title-text">
|
|
15
|
+
<div class="cms-actions">
|
|
16
|
+
<h1 class="cms-title cms-title-md">
|
|
17
|
+
{{ title }}
|
|
18
|
+
</h1>
|
|
19
|
+
<slot name="badge" />
|
|
20
|
+
</div>
|
|
21
|
+
<p v-if="description" class="cms-page-description">{{ description }}</p>
|
|
22
|
+
</div>
|
|
15
23
|
</div>
|
|
16
24
|
</div>
|
|
17
25
|
<slot />
|
|
@@ -21,6 +29,8 @@
|
|
|
21
29
|
<script setup>
|
|
22
30
|
defineProps({
|
|
23
31
|
title: { type: String, required: true },
|
|
32
|
+
icon: { type: String, required: false },
|
|
33
|
+
description: { type: String, required: false },
|
|
24
34
|
links: { type: Array, required: false }
|
|
25
35
|
});
|
|
26
36
|
</script>
|
|
@@ -4,13 +4,15 @@ export interface CmsBreadcrumbLink {
|
|
|
4
4
|
}
|
|
5
5
|
type __VLS_Props = {
|
|
6
6
|
title: string;
|
|
7
|
+
icon?: string;
|
|
8
|
+
description?: string;
|
|
7
9
|
links?: CmsBreadcrumbLink[];
|
|
8
10
|
};
|
|
9
|
-
declare var
|
|
11
|
+
declare var __VLS_17: {}, __VLS_19: {};
|
|
10
12
|
type __VLS_Slots = {} & {
|
|
11
|
-
badge?: (props: typeof
|
|
13
|
+
badge?: (props: typeof __VLS_17) => any;
|
|
12
14
|
} & {
|
|
13
|
-
default?: (props: typeof
|
|
15
|
+
default?: (props: typeof __VLS_19) => any;
|
|
14
16
|
};
|
|
15
17
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
18
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
generate?: boolean;
|
|
3
|
+
copy?: boolean;
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
autocomplete?: string;
|
|
6
|
+
};
|
|
7
|
+
type __VLS_ModelProps = {
|
|
8
|
+
modelValue: string;
|
|
9
|
+
};
|
|
10
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
11
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
|
+
"update:modelValue": (value: string) => any;
|
|
13
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
14
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
15
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
|
+
declare const _default: typeof __VLS_export;
|
|
17
|
+
export default _default;
|