@xleddyl/nuxt-cms 0.1.23 → 0.1.25
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 +16 -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 +48 -78
- 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/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/utils/relations.d.ts +1 -0
- package/dist/runtime/server/utils/relations.js +25 -0
- package/dist/runtime/shared/index.d.ts +10 -3
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -10,11 +10,12 @@ import { introspectionFromSchema, buildSchema } from 'graphql';
|
|
|
10
10
|
import { minifyIntrospection, outputIntrospectionFile } from 'gql.tada/internal';
|
|
11
11
|
import { createJiti } from 'jiti';
|
|
12
12
|
import { typeName, blockTypeName, blockUnionName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
|
|
13
|
-
import {
|
|
13
|
+
import { isMultiSelect, isTranslatableField } from '../dist/runtime/shared/index.js';
|
|
14
14
|
|
|
15
15
|
const IDENTIFIER = /^[a-z_]\w*$/i;
|
|
16
16
|
const RESERVED_ENTRY_KEYS = ["admin", "auth", "login", "media", "graphql", "cms_media"];
|
|
17
17
|
const RESERVED_COLUMNS = ["id", "status", "created_at", "updated_at"];
|
|
18
|
+
const TITLE_FIELD_TYPES = ["text", "slug", "email", "number", "date", "select"];
|
|
18
19
|
const RESERVED_TYPE_NAMES = [
|
|
19
20
|
"Query",
|
|
20
21
|
"Mutation",
|
|
@@ -68,8 +69,20 @@ function validateConfig(config, i18n) {
|
|
|
68
69
|
errors.push(`${at}: drafts are only supported on collections`);
|
|
69
70
|
if (!entry.fields || !Object.keys(entry.fields).length)
|
|
70
71
|
errors.push(`${at}: fields must not be empty`);
|
|
71
|
-
if (entry.
|
|
72
|
-
errors.push(`${at}: titleField
|
|
72
|
+
if (entry.kind === "collection" && !entry.titleField) {
|
|
73
|
+
errors.push(`${at}: titleField is required on collections`);
|
|
74
|
+
} else if (entry.titleField) {
|
|
75
|
+
const titleField = entry.fields?.[entry.titleField];
|
|
76
|
+
if (!titleField) {
|
|
77
|
+
errors.push(`${at}: titleField '${entry.titleField}' is not a declared field`);
|
|
78
|
+
} else if (!TITLE_FIELD_TYPES.includes(titleField.type) || isMultiSelect(titleField)) {
|
|
79
|
+
errors.push(
|
|
80
|
+
`${at}: titleField '${entry.titleField}' must be one of ${TITLE_FIELD_TYPES.join(
|
|
81
|
+
", "
|
|
82
|
+
)} (got '${titleField.type}')`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
73
86
|
const columnNames = /* @__PURE__ */ new Set();
|
|
74
87
|
for (const [key, field] of Object.entries(entry.fields ?? {})) {
|
|
75
88
|
const fat = `${at}, field '${key}'`;
|
|
@@ -59,7 +59,6 @@
|
|
|
59
59
|
:key="blockKey"
|
|
60
60
|
:label="blockField.label"
|
|
61
61
|
:required="blockField.required"
|
|
62
|
-
:ui="CMS_FIELD_UI"
|
|
63
62
|
>
|
|
64
63
|
<CmsFieldInput
|
|
65
64
|
:model-value="item[blockKey]"
|
|
@@ -69,20 +68,14 @@
|
|
|
69
68
|
</CmsFormField>
|
|
70
69
|
</template>
|
|
71
70
|
</div>
|
|
72
|
-
<CmsDropdownMenu :items="addItems">
|
|
73
|
-
<CmsButton
|
|
74
|
-
label="Add block"
|
|
75
|
-
icon="plus"
|
|
76
|
-
variant="subtle"
|
|
77
|
-
class="self-start rounded-full px-4"
|
|
78
|
-
/>
|
|
71
|
+
<CmsDropdownMenu :items="addItems" class="self-start">
|
|
72
|
+
<CmsButton label="Add block" icon="plus" variant="subtle" />
|
|
79
73
|
</CmsDropdownMenu>
|
|
80
74
|
</div>
|
|
81
75
|
</template>
|
|
82
76
|
|
|
83
77
|
<script setup>
|
|
84
78
|
import { computed, ref } from "#imports";
|
|
85
|
-
import { CMS_FIELD_UI } from "../../utils/ui";
|
|
86
79
|
const props = defineProps({
|
|
87
80
|
field: { type: Object, required: true }
|
|
88
81
|
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
`cms-btn-${size ?? 'md'}`,
|
|
9
9
|
`cms-btn-${color ?? 'primary'}`,
|
|
10
10
|
`cms-btn-${variant ?? 'solid'}`,
|
|
11
|
-
{ 'is-block': block }
|
|
11
|
+
{ 'is-block': block, 'is-icon': iconOnly }
|
|
12
12
|
]"
|
|
13
13
|
>
|
|
14
14
|
<CmsIcon
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
</template>
|
|
29
29
|
|
|
30
30
|
<script setup>
|
|
31
|
-
import { computed } from "#imports";
|
|
31
|
+
import { computed, useSlots } from "#imports";
|
|
32
32
|
const props = defineProps({
|
|
33
33
|
label: { type: String, required: false },
|
|
34
34
|
type: { type: String, required: false },
|
|
@@ -43,4 +43,6 @@ const props = defineProps({
|
|
|
43
43
|
block: { type: Boolean, required: false }
|
|
44
44
|
});
|
|
45
45
|
const iconName = computed(() => props.loading ? "arrow-path" : props.icon);
|
|
46
|
+
const slots = useSlots();
|
|
47
|
+
const iconOnly = computed(() => !props.label && !props.block && !slots.default);
|
|
46
48
|
</script>
|
|
@@ -2,24 +2,17 @@
|
|
|
2
2
|
<CmsModal
|
|
3
3
|
v-model:open="state.open"
|
|
4
4
|
:title="state.title ?? 'Are you sure?'"
|
|
5
|
-
|
|
5
|
+
size="sm"
|
|
6
6
|
@after:leave="finish(false)"
|
|
7
7
|
>
|
|
8
8
|
<template #body>
|
|
9
|
-
<div class="
|
|
10
|
-
<p class="text-sm
|
|
11
|
-
<div class="
|
|
12
|
-
<CmsButton
|
|
13
|
-
label="Cancel"
|
|
14
|
-
variant="subtle"
|
|
15
|
-
color="neutral"
|
|
16
|
-
class="rounded-full px-4"
|
|
17
|
-
@click="finish(false)"
|
|
18
|
-
/>
|
|
9
|
+
<div class="cms-form">
|
|
10
|
+
<p class="text-sm">{{ state.message }}</p>
|
|
11
|
+
<div class="cms-actions is-end">
|
|
12
|
+
<CmsButton label="Cancel" variant="subtle" color="neutral" @click="finish(false)" />
|
|
19
13
|
<CmsButton
|
|
20
14
|
:label="state.confirmLabel ?? 'Confirm'"
|
|
21
15
|
color="error"
|
|
22
|
-
class="rounded-full px-4"
|
|
23
16
|
@click="finish(true)"
|
|
24
17
|
/>
|
|
25
18
|
</div>
|
|
@@ -30,9 +23,7 @@
|
|
|
30
23
|
|
|
31
24
|
<script setup>
|
|
32
25
|
import { useCmsConfirmState } from "../../composables/cms-confirm";
|
|
33
|
-
import { CMS_MODAL_UI } from "../../utils/ui";
|
|
34
26
|
const state = useCmsConfirmState();
|
|
35
|
-
const ui = { ...CMS_MODAL_UI, content: "rounded-2xl sm:max-w-md" };
|
|
36
27
|
function finish(value) {
|
|
37
28
|
const resolve = state.value.resolve;
|
|
38
29
|
if (!resolve) return;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
title?: string;
|
|
3
|
+
};
|
|
4
|
+
type __VLS_ModelProps = {
|
|
5
|
+
'open'?: boolean;
|
|
6
|
+
};
|
|
7
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
8
|
+
declare var __VLS_13: {}, __VLS_22: {}, __VLS_24: {};
|
|
9
|
+
type __VLS_Slots = {} & {
|
|
10
|
+
badge?: (props: typeof __VLS_13) => any;
|
|
11
|
+
} & {
|
|
12
|
+
default?: (props: typeof __VLS_22) => any;
|
|
13
|
+
} & {
|
|
14
|
+
footer?: (props: typeof __VLS_24) => any;
|
|
15
|
+
};
|
|
16
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
17
|
+
close: () => any;
|
|
18
|
+
"update:open": (value: boolean) => any;
|
|
19
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
20
|
+
onClose?: (() => any) | undefined;
|
|
21
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
22
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
23
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
24
|
+
declare const _default: typeof __VLS_export;
|
|
25
|
+
export default _default;
|
|
26
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
27
|
+
new (): {
|
|
28
|
+
$slots: S;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport to="body">
|
|
3
|
+
<Transition name="cms-drawer">
|
|
4
|
+
<div v-if="open" class="cms-scope cms-overlay cms-overlay-drawer" @click.self="close">
|
|
5
|
+
<aside class="cms-drawer" role="dialog" aria-modal="true">
|
|
6
|
+
<header class="cms-drawer-header">
|
|
7
|
+
<div class="cms-actions min-w-0">
|
|
8
|
+
<h2 class="cms-title cms-title-sm truncate">
|
|
9
|
+
{{ title }}
|
|
10
|
+
</h2>
|
|
11
|
+
<slot name="badge" />
|
|
12
|
+
</div>
|
|
13
|
+
<CmsButton
|
|
14
|
+
icon="x-mark"
|
|
15
|
+
variant="ghost"
|
|
16
|
+
color="neutral"
|
|
17
|
+
size="sm"
|
|
18
|
+
aria-label="Close"
|
|
19
|
+
@click="close"
|
|
20
|
+
/>
|
|
21
|
+
</header>
|
|
22
|
+
|
|
23
|
+
<div class="cms-drawer-body">
|
|
24
|
+
<slot />
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<footer v-if="$slots.footer" class="cms-drawer-footer">
|
|
28
|
+
<slot name="footer" />
|
|
29
|
+
</footer>
|
|
30
|
+
</aside>
|
|
31
|
+
</div>
|
|
32
|
+
</Transition>
|
|
33
|
+
</Teleport>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup>
|
|
37
|
+
import { onBeforeUnmount, watch } from "#imports";
|
|
38
|
+
defineProps({
|
|
39
|
+
title: { type: String, required: false }
|
|
40
|
+
});
|
|
41
|
+
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
42
|
+
const emit = defineEmits(["close"]);
|
|
43
|
+
function close() {
|
|
44
|
+
emit("close");
|
|
45
|
+
}
|
|
46
|
+
function onKeydown(event) {
|
|
47
|
+
if (event.key === "Escape") close();
|
|
48
|
+
}
|
|
49
|
+
watch(open, (value) => {
|
|
50
|
+
if (import.meta.server) return;
|
|
51
|
+
if (value) {
|
|
52
|
+
document.addEventListener("keydown", onKeydown);
|
|
53
|
+
document.body.style.overflow = "hidden";
|
|
54
|
+
} else {
|
|
55
|
+
document.removeEventListener("keydown", onKeydown);
|
|
56
|
+
document.body.style.overflow = "";
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
onBeforeUnmount(() => {
|
|
60
|
+
if (import.meta.client) {
|
|
61
|
+
document.removeEventListener("keydown", onKeydown);
|
|
62
|
+
document.body.style.overflow = "";
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
</script>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
title?: string;
|
|
3
|
+
};
|
|
4
|
+
type __VLS_ModelProps = {
|
|
5
|
+
'open'?: boolean;
|
|
6
|
+
};
|
|
7
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
8
|
+
declare var __VLS_13: {}, __VLS_22: {}, __VLS_24: {};
|
|
9
|
+
type __VLS_Slots = {} & {
|
|
10
|
+
badge?: (props: typeof __VLS_13) => any;
|
|
11
|
+
} & {
|
|
12
|
+
default?: (props: typeof __VLS_22) => any;
|
|
13
|
+
} & {
|
|
14
|
+
footer?: (props: typeof __VLS_24) => any;
|
|
15
|
+
};
|
|
16
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
17
|
+
close: () => any;
|
|
18
|
+
"update:open": (value: boolean) => any;
|
|
19
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
20
|
+
onClose?: (() => any) | undefined;
|
|
21
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
22
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
23
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
24
|
+
declare const _default: typeof __VLS_export;
|
|
25
|
+
export default _default;
|
|
26
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
27
|
+
new (): {
|
|
28
|
+
$slots: S;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
@@ -3,11 +3,7 @@
|
|
|
3
3
|
<div class="contents" @click="open = !open">
|
|
4
4
|
<slot />
|
|
5
5
|
</div>
|
|
6
|
-
<div
|
|
7
|
-
v-if="open"
|
|
8
|
-
class="cms-menu"
|
|
9
|
-
:class="[content?.align === 'end' && 'align-end', ui?.content]"
|
|
10
|
-
>
|
|
6
|
+
<div v-if="open" class="cms-menu" :class="{ 'align-end': content?.align === 'end' }">
|
|
11
7
|
<button
|
|
12
8
|
v-for="(item, index) in items"
|
|
13
9
|
:key="index"
|
|
@@ -30,8 +26,7 @@
|
|
|
30
26
|
import { onBeforeUnmount, ref, watch } from "#imports";
|
|
31
27
|
defineProps({
|
|
32
28
|
items: { type: Array, required: true },
|
|
33
|
-
content: { type: Object, required: false }
|
|
34
|
-
ui: { type: Object, required: false }
|
|
29
|
+
content: { type: Object, required: false }
|
|
35
30
|
});
|
|
36
31
|
const open = ref(false);
|
|
37
32
|
const root = ref(null);
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="cms-empty
|
|
3
|
-
<CmsIcon :name="icon" class="
|
|
4
|
-
<p class="cms-
|
|
5
|
-
|
|
6
|
-
</p>
|
|
7
|
-
<p v-if="body" class="max-w-md text-sm text-(--ui-text-muted)">
|
|
2
|
+
<div class="cms-empty" :class="{ 'is-fill': fill }">
|
|
3
|
+
<CmsIcon :name="icon" class="cms-empty-icon size-6" />
|
|
4
|
+
<p class="cms-title cms-title-sm">{{ title }}<span class="cms-accent">.</span></p>
|
|
5
|
+
<p v-if="body" class="cms-subtitle max-w-md">
|
|
8
6
|
{{ body }}
|
|
9
7
|
</p>
|
|
10
8
|
<slot />
|
|
@@ -15,6 +13,7 @@
|
|
|
15
13
|
defineProps({
|
|
16
14
|
icon: { type: String, required: true },
|
|
17
15
|
title: { type: String, required: true },
|
|
18
|
-
body: { type: String, required: false }
|
|
16
|
+
body: { type: String, required: false },
|
|
17
|
+
fill: { type: Boolean, required: false }
|
|
19
18
|
});
|
|
20
19
|
</script>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CmsEntry } from '#nuxt-cms';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
collection: string;
|
|
4
|
+
config: CmsEntry;
|
|
5
|
+
entryId: string | null;
|
|
6
|
+
};
|
|
7
|
+
type __VLS_ModelProps = {
|
|
8
|
+
'open'?: boolean;
|
|
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
|
+
saved: () => any;
|
|
13
|
+
deleted: () => any;
|
|
14
|
+
"update:open": (value: boolean) => any;
|
|
15
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
16
|
+
onSaved?: (() => any) | undefined;
|
|
17
|
+
onDeleted?: (() => any) | undefined;
|
|
18
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
19
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<CmsDrawer v-model:open="open" :title="title" @close="requestClose">
|
|
3
|
+
<template v-if="drafts && !loading" #badge>
|
|
4
|
+
<CmsStatusBadge :published="published" />
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<CmsSpinner v-if="loading" />
|
|
8
|
+
|
|
9
|
+
<CmsEntryForm
|
|
10
|
+
v-else
|
|
11
|
+
v-model="formState"
|
|
12
|
+
:fields="config.fields"
|
|
13
|
+
:drafts="drafts"
|
|
14
|
+
:form-id="FORM_ID"
|
|
15
|
+
:loading="saving"
|
|
16
|
+
:footer="false"
|
|
17
|
+
@submit="save"
|
|
18
|
+
@error="revertStatus"
|
|
19
|
+
/>
|
|
20
|
+
|
|
21
|
+
<template #footer>
|
|
22
|
+
<CmsButton
|
|
23
|
+
v-if="!isNew"
|
|
24
|
+
label="Delete"
|
|
25
|
+
icon="trash"
|
|
26
|
+
variant="ghost"
|
|
27
|
+
color="error"
|
|
28
|
+
@click="remove"
|
|
29
|
+
/>
|
|
30
|
+
<span v-else />
|
|
31
|
+
<div class="cms-actions">
|
|
32
|
+
<CmsButton
|
|
33
|
+
v-if="drafts"
|
|
34
|
+
type="submit"
|
|
35
|
+
:form="FORM_ID"
|
|
36
|
+
:label="published ? 'Make draft' : 'Publish'"
|
|
37
|
+
variant="subtle"
|
|
38
|
+
:loading="saving"
|
|
39
|
+
:disabled="loading"
|
|
40
|
+
@click="togglePublished"
|
|
41
|
+
/>
|
|
42
|
+
<CmsButton
|
|
43
|
+
type="submit"
|
|
44
|
+
:form="FORM_ID"
|
|
45
|
+
label="Save"
|
|
46
|
+
:loading="saving"
|
|
47
|
+
:disabled="loading"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
</CmsDrawer>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script setup>
|
|
55
|
+
import { isTranslatableField } from "#nuxt-cms";
|
|
56
|
+
import { computed, ref, watch } from "#imports";
|
|
57
|
+
import { useCmsConfirm } from "../../composables/cms-confirm";
|
|
58
|
+
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
59
|
+
import { useCmsToast } from "../../composables/cms-toast";
|
|
60
|
+
import { cmsApi } from "../../utils/api";
|
|
61
|
+
import { errorMessage } from "../../utils/ui";
|
|
62
|
+
const FORM_ID = "cms-entry-drawer-form";
|
|
63
|
+
const props = defineProps({
|
|
64
|
+
collection: { type: String, required: true },
|
|
65
|
+
config: { type: Object, required: true },
|
|
66
|
+
entryId: { type: [String, null], required: true }
|
|
67
|
+
});
|
|
68
|
+
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
69
|
+
const emit = defineEmits(["saved", "deleted"]);
|
|
70
|
+
const toast = useCmsToast();
|
|
71
|
+
const confirmAction = useCmsConfirm();
|
|
72
|
+
const endpoint = computed(() => `/api/cms/admin/${props.collection}`);
|
|
73
|
+
const isNew = computed(() => props.entryId === null);
|
|
74
|
+
const drafts = computed(() => !!props.config.drafts);
|
|
75
|
+
const formKeys = computed(
|
|
76
|
+
() => drafts.value ? [...Object.keys(props.config.fields), "status"] : Object.keys(props.config.fields)
|
|
77
|
+
);
|
|
78
|
+
const { i18n: contentI18n } = useCmsRuntime();
|
|
79
|
+
const loading = ref(false);
|
|
80
|
+
const saving = ref(false);
|
|
81
|
+
const formState = ref({});
|
|
82
|
+
const snapshot = ref("");
|
|
83
|
+
const entryName = computed(() => {
|
|
84
|
+
const key = props.config.titleField;
|
|
85
|
+
const field = key ? props.config.fields[key] : void 0;
|
|
86
|
+
const value = key ? formState.value[key] : void 0;
|
|
87
|
+
if (!field || value == null || value === "") return "";
|
|
88
|
+
if (isTranslatableField(field)) {
|
|
89
|
+
const record = value;
|
|
90
|
+
return record[contentI18n.defaultLocale] ?? Object.values(record)[0] ?? "";
|
|
91
|
+
}
|
|
92
|
+
return String(value);
|
|
93
|
+
});
|
|
94
|
+
const title = computed(() => {
|
|
95
|
+
if (isNew.value) return "Add entry";
|
|
96
|
+
return entryName.value || "Untitled";
|
|
97
|
+
});
|
|
98
|
+
function emptyState() {
|
|
99
|
+
return Object.fromEntries(formKeys.value.map((key) => [key, null]));
|
|
100
|
+
}
|
|
101
|
+
function pickFields(row) {
|
|
102
|
+
return Object.fromEntries(formKeys.value.map((key) => [key, row[key] ?? null]));
|
|
103
|
+
}
|
|
104
|
+
function setState(state) {
|
|
105
|
+
formState.value = state;
|
|
106
|
+
snapshot.value = JSON.stringify(state);
|
|
107
|
+
}
|
|
108
|
+
const dirty = computed(() => JSON.stringify(formState.value) !== snapshot.value);
|
|
109
|
+
const published = computed({
|
|
110
|
+
get: () => formState.value.status === "published",
|
|
111
|
+
set: (value) => {
|
|
112
|
+
formState.value.status = value ? "published" : "draft";
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
function togglePublished() {
|
|
116
|
+
published.value = !published.value;
|
|
117
|
+
}
|
|
118
|
+
function revertStatus() {
|
|
119
|
+
if (!drafts.value) return;
|
|
120
|
+
formState.value.status = JSON.parse(snapshot.value).status ?? null;
|
|
121
|
+
}
|
|
122
|
+
async function load() {
|
|
123
|
+
if (props.entryId === null) {
|
|
124
|
+
setState(emptyState());
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
loading.value = true;
|
|
128
|
+
try {
|
|
129
|
+
const entry = await cmsApi(`${endpoint.value}/${props.entryId}`);
|
|
130
|
+
setState(pickFields(entry));
|
|
131
|
+
} catch (error) {
|
|
132
|
+
toast.add({
|
|
133
|
+
title: "Could not load entry",
|
|
134
|
+
description: errorMessage(error),
|
|
135
|
+
color: "error"
|
|
136
|
+
});
|
|
137
|
+
open.value = false;
|
|
138
|
+
} finally {
|
|
139
|
+
loading.value = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
watch(
|
|
143
|
+
() => [open.value, props.entryId],
|
|
144
|
+
([isOpen]) => {
|
|
145
|
+
if (isOpen) load();
|
|
146
|
+
},
|
|
147
|
+
{ immediate: true }
|
|
148
|
+
);
|
|
149
|
+
async function requestClose() {
|
|
150
|
+
if (dirty.value && !await confirmAction("You have unsaved changes. Close anyway?")) return;
|
|
151
|
+
open.value = false;
|
|
152
|
+
}
|
|
153
|
+
async function save() {
|
|
154
|
+
if (saving.value) return;
|
|
155
|
+
saving.value = true;
|
|
156
|
+
try {
|
|
157
|
+
await (isNew.value ? cmsApi(endpoint.value, { method: "POST", body: formState.value }) : cmsApi(`${endpoint.value}/${props.entryId}`, {
|
|
158
|
+
method: "PUT",
|
|
159
|
+
body: formState.value
|
|
160
|
+
}));
|
|
161
|
+
snapshot.value = JSON.stringify(formState.value);
|
|
162
|
+
toast.add({ title: "Saved", color: "success" });
|
|
163
|
+
emit("saved");
|
|
164
|
+
open.value = false;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
toast.add({
|
|
167
|
+
title: "Save failed",
|
|
168
|
+
description: errorMessage(error),
|
|
169
|
+
color: "error"
|
|
170
|
+
});
|
|
171
|
+
} finally {
|
|
172
|
+
saving.value = false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function remove() {
|
|
176
|
+
if (!await confirmAction("Delete this row?")) return;
|
|
177
|
+
saving.value = true;
|
|
178
|
+
try {
|
|
179
|
+
await cmsApi(`${endpoint.value}/${props.entryId}`, { method: "DELETE" });
|
|
180
|
+
snapshot.value = JSON.stringify(formState.value);
|
|
181
|
+
emit("deleted");
|
|
182
|
+
open.value = false;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
toast.add({
|
|
185
|
+
title: "Delete failed",
|
|
186
|
+
description: errorMessage(error),
|
|
187
|
+
color: "error"
|
|
188
|
+
});
|
|
189
|
+
} finally {
|
|
190
|
+
saving.value = false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
</script>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CmsEntry } from '#nuxt-cms';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
collection: string;
|
|
4
|
+
config: CmsEntry;
|
|
5
|
+
entryId: string | null;
|
|
6
|
+
};
|
|
7
|
+
type __VLS_ModelProps = {
|
|
8
|
+
'open'?: boolean;
|
|
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
|
+
saved: () => any;
|
|
13
|
+
deleted: () => any;
|
|
14
|
+
"update:open": (value: boolean) => any;
|
|
15
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
16
|
+
onSaved?: (() => any) | undefined;
|
|
17
|
+
onDeleted?: (() => any) | undefined;
|
|
18
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
19
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
@@ -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
|
);
|