@xleddyl/nuxt-cms 0.1.22 → 0.1.24
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/README.md +2 -2
- package/dist/module.d.mts +2 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +20 -3
- package/dist/runtime/app/components/cms/BlocksField.vue +2 -9
- package/dist/runtime/app/components/cms/Button.vue +4 -2
- package/dist/runtime/app/components/cms/ConfirmModal.vue +5 -14
- package/dist/runtime/app/components/cms/Drawer.d.vue.ts +30 -0
- package/dist/runtime/app/components/cms/Drawer.vue +65 -0
- package/dist/runtime/app/components/cms/Drawer.vue.d.ts +30 -0
- package/dist/runtime/app/components/cms/DropdownMenu.d.vue.ts +0 -3
- package/dist/runtime/app/components/cms/DropdownMenu.vue +2 -7
- package/dist/runtime/app/components/cms/DropdownMenu.vue.d.ts +0 -3
- package/dist/runtime/app/components/cms/EmptyState.d.vue.ts +1 -0
- package/dist/runtime/app/components/cms/EmptyState.vue +6 -7
- package/dist/runtime/app/components/cms/EmptyState.vue.d.ts +1 -0
- package/dist/runtime/app/components/cms/EntryDrawer.d.vue.ts +21 -0
- package/dist/runtime/app/components/cms/EntryDrawer.vue +193 -0
- package/dist/runtime/app/components/cms/EntryDrawer.vue.d.ts +21 -0
- package/dist/runtime/app/components/cms/EntryForm.vue +19 -7
- package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +1 -0
- package/dist/runtime/app/components/cms/FieldInput.vue +13 -27
- package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +1 -0
- package/dist/runtime/app/components/cms/Form.vue +1 -1
- package/dist/runtime/app/components/cms/FormField.d.vue.ts +4 -5
- package/dist/runtime/app/components/cms/FormField.vue +9 -7
- package/dist/runtime/app/components/cms/FormField.vue.d.ts +4 -5
- package/dist/runtime/app/components/cms/JsonField.vue +3 -4
- package/dist/runtime/app/components/cms/LocaleSwitch.d.vue.ts +14 -0
- package/dist/runtime/app/components/cms/LocaleSwitch.vue +29 -0
- package/dist/runtime/app/components/cms/LocaleSwitch.vue.d.ts +14 -0
- package/dist/runtime/app/components/cms/MediaField.vue +7 -17
- package/dist/runtime/app/components/cms/MediaGallery.vue +57 -80
- package/dist/runtime/app/components/cms/MediaThumb.d.vue.ts +6 -0
- package/dist/runtime/app/components/cms/MediaThumb.vue +40 -0
- package/dist/runtime/app/components/cms/MediaThumb.vue.d.ts +6 -0
- package/dist/runtime/app/components/cms/Modal.d.vue.ts +1 -4
- package/dist/runtime/app/components/cms/Modal.vue +8 -7
- package/dist/runtime/app/components/cms/Modal.vue.d.ts +1 -4
- package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +1 -1
- package/dist/runtime/app/components/cms/PageHeader.vue +6 -6
- package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +1 -1
- package/dist/runtime/app/components/cms/RelationField.vue +0 -4
- package/dist/runtime/app/components/cms/SelectMenu.vue +23 -3
- package/dist/runtime/app/components/cms/SlugField.vue +1 -2
- package/dist/runtime/app/components/cms/Spinner.d.vue.ts +3 -0
- package/dist/runtime/app/components/cms/Spinner.vue +5 -0
- package/dist/runtime/app/components/cms/Spinner.vue.d.ts +3 -0
- package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +6 -0
- package/dist/runtime/app/components/cms/StatusBadge.vue +11 -0
- package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +6 -0
- package/dist/runtime/app/components/cms/Table.d.vue.ts +3 -6
- package/dist/runtime/app/components/cms/Table.vue +72 -27
- package/dist/runtime/app/components/cms/Table.vue.d.ts +3 -6
- package/dist/runtime/app/components/cms/Toaster.vue +4 -4
- package/dist/runtime/app/components/cms/TranslatableField.d.vue.ts +1 -0
- package/dist/runtime/app/components/cms/TranslatableField.vue +7 -44
- package/dist/runtime/app/components/cms/TranslatableField.vue.d.ts +1 -0
- package/dist/runtime/app/composables/cms-runtime.d.ts +2 -1
- package/dist/runtime/app/layouts/cms-admin.vue +10 -20
- package/dist/runtime/app/pages/admin-collection.vue +130 -103
- package/dist/runtime/app/pages/admin-entry.vue +6 -24
- package/dist/runtime/app/pages/admin-login.vue +12 -31
- package/dist/runtime/app/pages/admin-media.vue +8 -2
- package/dist/runtime/app/utils/ui.d.ts +0 -7
- package/dist/runtime/app/utils/ui.js +0 -5
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/server/api/collection.get.js +8 -3
- package/dist/runtime/server/api/media.post.js +8 -2
- package/dist/runtime/server/api/media.put.js +3 -2
- package/dist/runtime/server/utils/media.d.ts +4 -1
- package/dist/runtime/server/utils/media.js +16 -3
- package/dist/runtime/server/utils/relations.d.ts +1 -0
- package/dist/runtime/server/utils/relations.js +25 -0
- package/dist/runtime/shared/index.d.ts +11 -3
- package/package.json +1 -1
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
:id="formId"
|
|
4
4
|
:state="state"
|
|
5
5
|
:schema="schema"
|
|
6
|
-
class="flex flex-col gap-5"
|
|
7
6
|
@submit="emit('submit')"
|
|
8
7
|
@error="emit('error')"
|
|
9
8
|
>
|
|
@@ -13,22 +12,28 @@
|
|
|
13
12
|
:label="field.label"
|
|
14
13
|
:name="key"
|
|
15
14
|
:required="field.required"
|
|
16
|
-
:ui="CMS_FIELD_UI"
|
|
17
15
|
>
|
|
16
|
+
<template v-if="isTranslatableField(field) && i18n.locales.length > 1" #label-actions>
|
|
17
|
+
<CmsLocaleSwitch
|
|
18
|
+
:model-value="localeFor(key)"
|
|
19
|
+
:value="state[key]"
|
|
20
|
+
@update:model-value="(locale) => setLocale(key, locale)"
|
|
21
|
+
/>
|
|
22
|
+
</template>
|
|
18
23
|
<CmsFieldInput
|
|
19
24
|
v-model="state[key]"
|
|
20
25
|
:field="field"
|
|
26
|
+
:locale="localeFor(key)"
|
|
21
27
|
:slug-source="field.from ? state[field.from] : void 0"
|
|
22
28
|
/>
|
|
23
29
|
</CmsFormField>
|
|
24
|
-
<div v-if="footer" class="
|
|
25
|
-
<CmsButton type="submit" label="Save" :loading="loading"
|
|
30
|
+
<div v-if="footer" class="cms-form-actions">
|
|
31
|
+
<CmsButton type="submit" label="Save" :loading="loading" />
|
|
26
32
|
<CmsButton
|
|
27
33
|
v-if="drafts"
|
|
28
34
|
type="submit"
|
|
29
35
|
:label="published ? 'Make draft' : 'Publish'"
|
|
30
36
|
:loading="loading"
|
|
31
|
-
class="rounded-full px-6"
|
|
32
37
|
@click="togglePublished"
|
|
33
38
|
/>
|
|
34
39
|
</div>
|
|
@@ -36,10 +41,10 @@
|
|
|
36
41
|
</template>
|
|
37
42
|
|
|
38
43
|
<script setup>
|
|
39
|
-
import {
|
|
44
|
+
import { isTranslatableField } from "#nuxt-cms";
|
|
45
|
+
import { computed, ref } from "#imports";
|
|
40
46
|
import { buildEntrySchema } from "../../../shared/validation";
|
|
41
47
|
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
42
|
-
import { CMS_FIELD_UI } from "../../utils/ui";
|
|
43
48
|
const props = defineProps({
|
|
44
49
|
fields: { type: Object, required: true },
|
|
45
50
|
drafts: { type: Boolean, required: false },
|
|
@@ -50,6 +55,13 @@ const props = defineProps({
|
|
|
50
55
|
const state = defineModel({ type: Object, ...{ required: true } });
|
|
51
56
|
const emit = defineEmits(["submit", "error"]);
|
|
52
57
|
const { i18n } = useCmsRuntime();
|
|
58
|
+
const activeLocale = ref({});
|
|
59
|
+
function localeFor(key) {
|
|
60
|
+
return activeLocale.value[key] ?? i18n.defaultLocale;
|
|
61
|
+
}
|
|
62
|
+
function setLocale(key, locale) {
|
|
63
|
+
activeLocale.value = { ...activeLocale.value, [key]: locale };
|
|
64
|
+
}
|
|
53
65
|
const schema = computed(
|
|
54
66
|
() => buildEntrySchema({ fields: props.fields, drafts: props.drafts }, i18n)
|
|
55
67
|
);
|
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<CmsTranslatableField
|
|
2
|
+
<CmsTranslatableField
|
|
3
|
+
v-if="isTranslatableField(field)"
|
|
4
|
+
v-model="translatable"
|
|
5
|
+
:field="field"
|
|
6
|
+
:locale="locale"
|
|
7
|
+
/>
|
|
3
8
|
<CmsBlocksField v-else-if="field.type === 'blocks'" v-model="blocksValue" :field="field" />
|
|
4
9
|
<CmsRichTextField v-else-if="field.type === 'richtext'" v-model="strOrNull" />
|
|
5
|
-
<CmsTextarea
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
:rows="4"
|
|
9
|
-
size="lg"
|
|
10
|
-
class="w-full"
|
|
11
|
-
/>
|
|
12
|
-
<CmsInput v-else-if="field.type === 'text'" v-model="str" size="lg" class="w-full" />
|
|
13
|
-
<CmsInput
|
|
14
|
-
v-else-if="field.type === 'email'"
|
|
15
|
-
v-model="str"
|
|
16
|
-
type="email"
|
|
17
|
-
size="lg"
|
|
18
|
-
class="w-full"
|
|
19
|
-
/>
|
|
10
|
+
<CmsTextarea v-else-if="field.type === 'text' && field.textarea" v-model="str" :rows="8" />
|
|
11
|
+
<CmsInput v-else-if="field.type === 'text'" v-model="str" />
|
|
12
|
+
<CmsInput v-else-if="field.type === 'email'" v-model="str" type="email" />
|
|
20
13
|
<CmsSlugField v-else-if="field.type === 'slug'" v-model="strOrNull" :source="slugSource" />
|
|
21
14
|
<CmsSelectMenu
|
|
22
15
|
v-else-if="field.type === 'select'"
|
|
@@ -24,8 +17,6 @@
|
|
|
24
17
|
:multiple="field.multiple"
|
|
25
18
|
:items="selectItems"
|
|
26
19
|
placeholder="Select…"
|
|
27
|
-
size="lg"
|
|
28
|
-
class="w-full"
|
|
29
20
|
/>
|
|
30
21
|
<CmsSwitch
|
|
31
22
|
v-else-if="field.type === 'boolean'"
|
|
@@ -37,14 +28,8 @@
|
|
|
37
28
|
"
|
|
38
29
|
/>
|
|
39
30
|
<CmsJsonField v-else-if="field.type === 'json'" v-model="jsonValue" />
|
|
40
|
-
<CmsInput
|
|
41
|
-
|
|
42
|
-
v-model.number="num"
|
|
43
|
-
type="number"
|
|
44
|
-
size="lg"
|
|
45
|
-
class="w-full"
|
|
46
|
-
/>
|
|
47
|
-
<CmsInput v-else-if="field.type === 'date'" v-model="str" type="date" size="lg" class="w-full" />
|
|
31
|
+
<CmsInput v-else-if="field.type === 'number'" v-model.number="num" type="number" />
|
|
32
|
+
<CmsInput v-else-if="field.type === 'date'" v-model="str" type="date" />
|
|
48
33
|
<CmsRelationField
|
|
49
34
|
v-else-if="field.type === 'relation'"
|
|
50
35
|
v-model="relationValue"
|
|
@@ -60,7 +45,8 @@ import { isTranslatableField } from "#nuxt-cms";
|
|
|
60
45
|
import { computed } from "#imports";
|
|
61
46
|
const props = defineProps({
|
|
62
47
|
field: { type: Object, required: true },
|
|
63
|
-
slugSource: { type: [String, null], required: false }
|
|
48
|
+
slugSource: { type: [String, null], required: false },
|
|
49
|
+
locale: { type: String, required: false }
|
|
64
50
|
});
|
|
65
51
|
const model = defineModel({ type: null, ...{ required: true } });
|
|
66
52
|
const selectItems = computed(() => [
|
|
@@ -2,13 +2,12 @@ type __VLS_Props = {
|
|
|
2
2
|
label?: string;
|
|
3
3
|
name?: string;
|
|
4
4
|
required?: boolean;
|
|
5
|
-
ui?: {
|
|
6
|
-
label?: string;
|
|
7
|
-
};
|
|
8
5
|
};
|
|
9
|
-
declare var __VLS_1: {};
|
|
6
|
+
declare var __VLS_1: {}, __VLS_3: {};
|
|
10
7
|
type __VLS_Slots = {} & {
|
|
11
|
-
|
|
8
|
+
'label-actions'?: (props: typeof __VLS_1) => any;
|
|
9
|
+
} & {
|
|
10
|
+
default?: (props: typeof __VLS_3) => any;
|
|
12
11
|
};
|
|
13
12
|
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>;
|
|
14
13
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
<div class="cms-form-field">
|
|
3
|
+
<div v-if="label || $slots['label-actions']" class="cms-form-label-row">
|
|
4
|
+
<label v-if="label" class="cms-form-label">
|
|
5
|
+
{{ label }}<span v-if="required" class="cms-form-required"> *</span>
|
|
6
|
+
</label>
|
|
7
|
+
<slot name="label-actions" />
|
|
8
|
+
</div>
|
|
6
9
|
<slot />
|
|
7
|
-
<p v-if="error" class="
|
|
10
|
+
<p v-if="error" class="cms-form-error">{{ error }}</p>
|
|
8
11
|
</div>
|
|
9
12
|
</template>
|
|
10
13
|
|
|
@@ -14,8 +17,7 @@ import { CMS_FORM_ERRORS } from "../../utils/ui";
|
|
|
14
17
|
const props = defineProps({
|
|
15
18
|
label: { type: String, required: false },
|
|
16
19
|
name: { type: String, required: false },
|
|
17
|
-
required: { type: Boolean, required: false }
|
|
18
|
-
ui: { type: Object, required: false }
|
|
20
|
+
required: { type: Boolean, required: false }
|
|
19
21
|
});
|
|
20
22
|
const errors = inject(CMS_FORM_ERRORS, null);
|
|
21
23
|
const error = computed(() => props.name && errors ? errors[props.name] : void 0);
|
|
@@ -2,13 +2,12 @@ type __VLS_Props = {
|
|
|
2
2
|
label?: string;
|
|
3
3
|
name?: string;
|
|
4
4
|
required?: boolean;
|
|
5
|
-
ui?: {
|
|
6
|
-
label?: string;
|
|
7
|
-
};
|
|
8
5
|
};
|
|
9
|
-
declare var __VLS_1: {};
|
|
6
|
+
declare var __VLS_1: {}, __VLS_3: {};
|
|
10
7
|
type __VLS_Slots = {} & {
|
|
11
|
-
|
|
8
|
+
'label-actions'?: (props: typeof __VLS_1) => any;
|
|
9
|
+
} & {
|
|
10
|
+
default?: (props: typeof __VLS_3) => any;
|
|
12
11
|
};
|
|
13
12
|
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>;
|
|
14
13
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="
|
|
2
|
+
<div class="cms-form-field">
|
|
3
3
|
<CmsTextarea
|
|
4
4
|
v-model="text"
|
|
5
5
|
:rows="6"
|
|
6
|
-
|
|
7
|
-
class="w-full font-mono"
|
|
6
|
+
class="font-mono"
|
|
8
7
|
:color="invalid ? 'error' : void 0"
|
|
9
8
|
@blur="commit"
|
|
10
9
|
@update:model-value="invalid = false"
|
|
11
10
|
/>
|
|
12
|
-
<p v-if="invalid" class="
|
|
11
|
+
<p v-if="invalid" class="cms-form-error">Invalid JSON</p>
|
|
13
12
|
</div>
|
|
14
13
|
</template>
|
|
15
14
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
value?: Record<string, string> | null;
|
|
3
|
+
};
|
|
4
|
+
type __VLS_ModelProps = {
|
|
5
|
+
modelValue: string;
|
|
6
|
+
};
|
|
7
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
8
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
"update:modelValue": (value: string) => any;
|
|
10
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
11
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
12
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
|
+
declare const _default: typeof __VLS_export;
|
|
14
|
+
export default _default;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cms-locale-switch" role="tablist" aria-label="Content language">
|
|
3
|
+
<button
|
|
4
|
+
v-for="locale in i18n.locales"
|
|
5
|
+
:key="locale"
|
|
6
|
+
type="button"
|
|
7
|
+
role="tab"
|
|
8
|
+
:aria-selected="model === locale"
|
|
9
|
+
:title="filled(locale) ? locale : `${locale} \u2014 empty`"
|
|
10
|
+
class="cms-locale-option"
|
|
11
|
+
:class="{ 'is-active': model === locale, 'is-empty': !filled(locale) }"
|
|
12
|
+
@click="model = locale"
|
|
13
|
+
>
|
|
14
|
+
{{ locale }}
|
|
15
|
+
</button>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup>
|
|
20
|
+
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
21
|
+
const props = defineProps({
|
|
22
|
+
value: { type: [Object, null], required: false }
|
|
23
|
+
});
|
|
24
|
+
const model = defineModel({ type: String, ...{ required: true } });
|
|
25
|
+
const { i18n } = useCmsRuntime();
|
|
26
|
+
function filled(locale) {
|
|
27
|
+
return !!props.value?.[locale]?.trim();
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
value?: Record<string, string> | null;
|
|
3
|
+
};
|
|
4
|
+
type __VLS_ModelProps = {
|
|
5
|
+
modelValue: string;
|
|
6
|
+
};
|
|
7
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
8
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
"update:modelValue": (value: string) => any;
|
|
10
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
11
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
12
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
|
+
declare const _default: typeof __VLS_export;
|
|
14
|
+
export default _default;
|
|
@@ -1,30 +1,22 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div v-if="model" class="cms-card
|
|
3
|
-
<img
|
|
4
|
-
v-if="kind === 'image' && url"
|
|
5
|
-
:src="url"
|
|
6
|
-
alt=""
|
|
7
|
-
class="max-h-56 w-full bg-(--cms-field) object-cover"
|
|
8
|
-
/>
|
|
2
|
+
<div v-if="model" class="cms-card cms-media-tile">
|
|
3
|
+
<img v-if="kind === 'image' && url" :src="url" alt="" class="cms-media-preview-large" />
|
|
9
4
|
<video
|
|
10
5
|
v-else-if="kind === 'video' && url"
|
|
11
6
|
:src="url"
|
|
12
7
|
controls
|
|
13
8
|
preload="metadata"
|
|
14
9
|
playsinline
|
|
15
|
-
class="
|
|
10
|
+
class="cms-media-preview-large"
|
|
16
11
|
/>
|
|
17
|
-
<div class="
|
|
18
|
-
<CmsIcon :name="icon" class="
|
|
19
|
-
<span class="
|
|
20
|
-
mediaFilename(model)
|
|
21
|
-
}}</span>
|
|
12
|
+
<div class="cms-media-bar">
|
|
13
|
+
<CmsIcon :name="icon" class="cms-media-bar-icon size-4" />
|
|
14
|
+
<span class="cms-media-bar-name" :title="model">{{ mediaFilename(model) }}</span>
|
|
22
15
|
<CmsButton
|
|
23
16
|
icon="photo"
|
|
24
17
|
variant="ghost"
|
|
25
18
|
color="neutral"
|
|
26
19
|
size="xs"
|
|
27
|
-
class="rounded-full"
|
|
28
20
|
aria-label="Change"
|
|
29
21
|
@click="openGallery"
|
|
30
22
|
/>
|
|
@@ -33,7 +25,6 @@
|
|
|
33
25
|
variant="ghost"
|
|
34
26
|
color="neutral"
|
|
35
27
|
size="xs"
|
|
36
|
-
class="rounded-full"
|
|
37
28
|
aria-label="Remove"
|
|
38
29
|
@click="clear"
|
|
39
30
|
/>
|
|
@@ -45,7 +36,7 @@
|
|
|
45
36
|
<span class="text-sm font-medium">Choose from the gallery</span>
|
|
46
37
|
</button>
|
|
47
38
|
|
|
48
|
-
<CmsModal v-model:open="galleryOpen" title="Media"
|
|
39
|
+
<CmsModal v-model:open="galleryOpen" title="Media" size="lg">
|
|
49
40
|
<template #body>
|
|
50
41
|
<CmsMediaGallery selectable :media-type="mediaType" :accept="accept" @select="onSelect" />
|
|
51
42
|
</template>
|
|
@@ -56,7 +47,6 @@
|
|
|
56
47
|
import { computed, ref } from "#imports";
|
|
57
48
|
import { mediaFilename, mediaIconFor, mediaPublicUrl, mediaTypeForKey } from "#nuxt-cms";
|
|
58
49
|
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
59
|
-
import { CMS_MODAL_UI } from "../../utils/ui";
|
|
60
50
|
defineProps({
|
|
61
51
|
mediaType: { type: null, required: false },
|
|
62
52
|
accept: { type: Array, required: false }
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
body="Set cms.media in nuxt.config to enable uploads."
|
|
7
7
|
/>
|
|
8
8
|
|
|
9
|
-
<div v-else class="
|
|
9
|
+
<div v-else class="cms-media-gallery">
|
|
10
10
|
<CmsMediaUpload
|
|
11
|
+
v-if="!readOnly"
|
|
11
12
|
:multiple="!selectable"
|
|
12
13
|
:dense="selectable"
|
|
13
14
|
:media-type="mediaType"
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
@uploaded="onUploaded"
|
|
16
17
|
/>
|
|
17
18
|
|
|
18
|
-
<div v-if="!restricted && items.length" class="
|
|
19
|
+
<div v-if="!restricted && items.length" class="cms-media-filters">
|
|
19
20
|
<button
|
|
20
21
|
v-for="f in filters"
|
|
21
22
|
:key="f"
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
{{ filterLabels[f] }}
|
|
28
29
|
</button>
|
|
29
30
|
<template v-if="folders.length">
|
|
30
|
-
<span class="
|
|
31
|
+
<span class="cms-media-filter-separator" />
|
|
31
32
|
<button
|
|
32
33
|
v-for="f in folders"
|
|
33
34
|
:key="f"
|
|
@@ -36,40 +37,29 @@
|
|
|
36
37
|
:class="{ 'is-active': folder === f }"
|
|
37
38
|
@click="folder = folder === f ? null : f"
|
|
38
39
|
>
|
|
39
|
-
<CmsIcon name="folder" class="
|
|
40
|
+
<CmsIcon name="folder" class="size-3" />{{ f }}
|
|
40
41
|
</button>
|
|
41
42
|
</template>
|
|
42
43
|
</div>
|
|
43
44
|
|
|
44
|
-
<
|
|
45
|
-
<CmsIcon name="arrow-path" class="size-6 animate-spin text-(--ui-text-dimmed)" />
|
|
46
|
-
</div>
|
|
45
|
+
<CmsSpinner v-if="loading" />
|
|
47
46
|
|
|
48
|
-
<div
|
|
49
|
-
v-else-if="visible.length"
|
|
50
|
-
class="grid gap-4"
|
|
51
|
-
:class="
|
|
52
|
-
selectable ? 'grid-cols-2 sm:grid-cols-3' : 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4'
|
|
53
|
-
"
|
|
54
|
-
>
|
|
47
|
+
<div v-else-if="visible.length" class="cms-media-grid" :class="{ 'is-compact': selectable }">
|
|
55
48
|
<component
|
|
56
49
|
:is="selectable ? 'button' : 'div'"
|
|
57
|
-
v-for="item in
|
|
50
|
+
v-for="item in paged"
|
|
58
51
|
:key="item.key"
|
|
59
52
|
:type="selectable ? 'button' : void 0"
|
|
60
|
-
class="cms-card
|
|
61
|
-
:class="{
|
|
62
|
-
'hover:border-(--cms-line-strong)': selectable
|
|
63
|
-
}"
|
|
53
|
+
class="cms-card cms-media-tile"
|
|
54
|
+
:class="{ 'is-selectable': selectable }"
|
|
64
55
|
@click="selectable && emit('select', { key: item.key, url: item.url })"
|
|
65
56
|
>
|
|
66
|
-
<div class="
|
|
57
|
+
<div class="cms-media-preview">
|
|
67
58
|
<img
|
|
68
59
|
v-if="item.type === 'image' && item.url"
|
|
69
60
|
:src="item.url"
|
|
70
61
|
alt=""
|
|
71
62
|
loading="lazy"
|
|
72
|
-
class="absolute inset-0 size-full object-cover"
|
|
73
63
|
/>
|
|
74
64
|
<video
|
|
75
65
|
v-else-if="item.type === 'video' && item.url"
|
|
@@ -77,81 +67,66 @@
|
|
|
77
67
|
preload="metadata"
|
|
78
68
|
muted
|
|
79
69
|
playsinline
|
|
80
|
-
class="absolute inset-0 size-full object-cover"
|
|
81
|
-
/>
|
|
82
|
-
<CmsIcon
|
|
83
|
-
v-else
|
|
84
|
-
:name="mediaIconFor(item.type)"
|
|
85
|
-
class="size-8 text-(--ui-text-dimmed)"
|
|
86
70
|
/>
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
class="absolute top-2 right-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
|
|
90
|
-
>
|
|
71
|
+
<CmsIcon v-else :name="mediaIconFor(item.type)" class="size-8" />
|
|
72
|
+
<div v-if="!selectable" class="cms-media-actions">
|
|
91
73
|
<CmsButton
|
|
74
|
+
v-if="!readOnly"
|
|
92
75
|
icon="pencil-square"
|
|
93
76
|
size="xs"
|
|
94
77
|
color="neutral"
|
|
95
78
|
variant="solid"
|
|
96
|
-
class="rounded-full"
|
|
97
79
|
aria-label="Edit details"
|
|
98
80
|
@click="openEdit(item)"
|
|
99
81
|
/>
|
|
100
82
|
<CmsButton
|
|
101
|
-
|
|
102
|
-
size="xs"
|
|
103
|
-
color="neutral"
|
|
104
|
-
variant="solid"
|
|
105
|
-
class="rounded-full"
|
|
106
|
-
aria-label="Copy URL"
|
|
107
|
-
@click="copy(item)"
|
|
108
|
-
/>
|
|
109
|
-
<CmsButton
|
|
83
|
+
v-if="!readOnly"
|
|
110
84
|
icon="trash"
|
|
111
85
|
size="xs"
|
|
112
86
|
color="error"
|
|
113
87
|
variant="solid"
|
|
114
|
-
class="rounded-full"
|
|
115
88
|
aria-label="Delete"
|
|
116
89
|
@click="remove(item)"
|
|
117
90
|
/>
|
|
118
91
|
</div>
|
|
119
|
-
|
|
120
|
-
<div class="flex flex-col gap-1 px-3.5 py-2.5">
|
|
121
|
-
<div class="truncate text-sm font-medium" :title="item.key">
|
|
122
|
-
{{ mediaFilename(item.key) }}
|
|
123
|
-
</div>
|
|
124
|
-
<div class="cms-kicker">
|
|
92
|
+
<div class="cms-media-info">
|
|
125
93
|
{{ extension(item.key) }} · {{ formatSize(item.size)
|
|
126
94
|
}}<template v-if="item.width && item.height">
|
|
127
95
|
· {{ item.width }}×{{ item.height }}
|
|
128
96
|
</template>
|
|
129
97
|
</div>
|
|
130
98
|
</div>
|
|
99
|
+
<div class="cms-media-meta">
|
|
100
|
+
<div class="cms-media-name" :title="item.key">
|
|
101
|
+
{{ mediaFilename(item.key) }}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
131
104
|
</component>
|
|
132
105
|
</div>
|
|
133
106
|
|
|
134
107
|
<CmsEmptyState v-else-if="loadError" icon="exclamation-triangle" title="Could not load media">
|
|
135
|
-
<CmsButton
|
|
136
|
-
label="Retry"
|
|
137
|
-
icon="arrow-path"
|
|
138
|
-
variant="subtle"
|
|
139
|
-
class="mt-3 rounded-full px-4"
|
|
140
|
-
@click="reload"
|
|
141
|
-
/>
|
|
108
|
+
<CmsButton label="Retry" icon="arrow-path" variant="subtle" @click="reload" />
|
|
142
109
|
</CmsEmptyState>
|
|
143
110
|
|
|
144
111
|
<CmsEmptyState
|
|
145
112
|
v-else
|
|
146
113
|
icon="photo"
|
|
147
|
-
title="No media yet"
|
|
148
|
-
body="Files you upload will show up here."
|
|
114
|
+
:title="readOnly ? 'No media' : 'No media yet'"
|
|
115
|
+
:body="readOnly ? 'No media registered.' : 'Files you upload will show up here.'"
|
|
116
|
+
/>
|
|
117
|
+
|
|
118
|
+
<CmsPagination
|
|
119
|
+
v-if="!loading && visible.length"
|
|
120
|
+
v-model:page="page"
|
|
121
|
+
:total="visible.length"
|
|
122
|
+
:items-per-page="PAGE_SIZE"
|
|
149
123
|
/>
|
|
150
124
|
|
|
151
125
|
<CmsModal
|
|
126
|
+
v-if="!readOnly"
|
|
152
127
|
:open="!!editing"
|
|
153
128
|
:title="editing ? mediaFilename(editing.key) : ''"
|
|
154
|
-
|
|
129
|
+
size="lg"
|
|
155
130
|
@update:open="
|
|
156
131
|
(open) => {
|
|
157
132
|
if (!open) editing = null;
|
|
@@ -159,20 +134,15 @@
|
|
|
159
134
|
"
|
|
160
135
|
>
|
|
161
136
|
<template #body>
|
|
162
|
-
<div class="
|
|
137
|
+
<div class="cms-form">
|
|
163
138
|
<CmsFormField label="Alt text">
|
|
164
|
-
<CmsInput v-model="editAlt"
|
|
139
|
+
<CmsInput v-model="editAlt" />
|
|
165
140
|
</CmsFormField>
|
|
166
141
|
<CmsFormField label="Folder">
|
|
167
|
-
<CmsInput v-model="editFolder"
|
|
142
|
+
<CmsInput v-model="editFolder" />
|
|
168
143
|
</CmsFormField>
|
|
169
|
-
<div class="
|
|
170
|
-
<CmsButton
|
|
171
|
-
label="Save"
|
|
172
|
-
:loading="editSaving"
|
|
173
|
-
class="rounded-full px-6"
|
|
174
|
-
@click="saveEdit"
|
|
175
|
-
/>
|
|
144
|
+
<div class="cms-actions is-end">
|
|
145
|
+
<CmsButton label="Save" :loading="editSaving" @click="saveEdit" />
|
|
176
146
|
</div>
|
|
177
147
|
</div>
|
|
178
148
|
</template>
|
|
@@ -181,11 +151,12 @@
|
|
|
181
151
|
</template>
|
|
182
152
|
|
|
183
153
|
<script setup>
|
|
184
|
-
import { computed, onMounted, ref } from "#imports";
|
|
154
|
+
import { computed, onMounted, ref, watch } from "#imports";
|
|
185
155
|
import { MEDIA_TYPES, mediaFilename, mediaIconFor } from "#nuxt-cms";
|
|
186
156
|
import { useCmsConfirm } from "../../composables/cms-confirm";
|
|
157
|
+
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
187
158
|
import { useCmsToast } from "../../composables/cms-toast";
|
|
188
|
-
import {
|
|
159
|
+
import { errorMessage } from "../../utils/ui";
|
|
189
160
|
const filterLabels = {
|
|
190
161
|
all: "All",
|
|
191
162
|
image: "Images",
|
|
@@ -199,6 +170,8 @@ const props = defineProps({
|
|
|
199
170
|
});
|
|
200
171
|
const emit = defineEmits(["select"]);
|
|
201
172
|
const toast = useCmsToast();
|
|
173
|
+
const runtime = useCmsRuntime();
|
|
174
|
+
const readOnly = computed(() => runtime.mediaStorage === "local");
|
|
202
175
|
const endpoint = "/api/cms/admin/media";
|
|
203
176
|
const items = ref([]);
|
|
204
177
|
const loading = ref(true);
|
|
@@ -207,8 +180,8 @@ async function reload() {
|
|
|
207
180
|
loading.value = true;
|
|
208
181
|
errorCode.value = null;
|
|
209
182
|
try {
|
|
210
|
-
const
|
|
211
|
-
items.value =
|
|
183
|
+
const result = await $fetch(endpoint);
|
|
184
|
+
items.value = result.items;
|
|
212
185
|
} catch (err) {
|
|
213
186
|
errorCode.value = err.statusCode ?? 500;
|
|
214
187
|
} finally {
|
|
@@ -234,6 +207,18 @@ const visible = computed(() => {
|
|
|
234
207
|
if (folder.value) list = list.filter((item) => item.folder === folder.value);
|
|
235
208
|
return list;
|
|
236
209
|
});
|
|
210
|
+
const PAGE_SIZE = 24;
|
|
211
|
+
const page = ref(1);
|
|
212
|
+
watch([filter, folder], () => {
|
|
213
|
+
page.value = 1;
|
|
214
|
+
});
|
|
215
|
+
watch(visible, (list) => {
|
|
216
|
+
const pageCount = Math.max(1, Math.ceil(list.length / PAGE_SIZE));
|
|
217
|
+
if (page.value > pageCount) page.value = pageCount;
|
|
218
|
+
});
|
|
219
|
+
const paged = computed(
|
|
220
|
+
() => visible.value.slice((page.value - 1) * PAGE_SIZE, page.value * PAGE_SIZE)
|
|
221
|
+
);
|
|
237
222
|
function onUploaded(uploaded) {
|
|
238
223
|
if (props.selectable && uploaded[0]) {
|
|
239
224
|
emit("select", uploaded[0]);
|
|
@@ -288,14 +273,6 @@ function formatSize(bytes) {
|
|
|
288
273
|
}
|
|
289
274
|
return `${Math.round(value / 1024)} TB`;
|
|
290
275
|
}
|
|
291
|
-
async function copy(item) {
|
|
292
|
-
try {
|
|
293
|
-
await navigator.clipboard.writeText(item.url ?? item.key);
|
|
294
|
-
toast.add({ title: "Copied", color: "success" });
|
|
295
|
-
} catch {
|
|
296
|
-
toast.add({ title: "Copy failed", color: "error" });
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
276
|
const confirmAction = useCmsConfirm();
|
|
300
277
|
const removing = ref(false);
|
|
301
278
|
async function remove(item) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
value?: string | null;
|
|
3
|
+
};
|
|
4
|
+
declare const __VLS_export: 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>;
|
|
5
|
+
declare const _default: typeof __VLS_export;
|
|
6
|
+
export default _default;
|