@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
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ nuxt-cms is a Nuxt module that leverages the Nitro server to ship a lightweight
|
|
|
4
4
|
|
|
5
5
|
- **Zero extra infrastructure**: the CMS runs inside your app's Nitro server; you deploy one thing.
|
|
6
6
|
- **Content types in code**: a `cms.config.ts` with `defineCmsConfig()` declares collections, single documents, relations, blocks and translatable fields; database schema, migrations and TypeScript types are generated from it.
|
|
7
|
-
- **Admin panel at `/cms`**: entry editing with validation, drafts, media library (S3-compatible storage), single-admin auth from env credentials.
|
|
7
|
+
- **Admin panel at `/cms`**: entry editing with validation, drafts, media library (S3-compatible storage, or a read-only local mode), single-admin auth from env credentials.
|
|
8
8
|
- **Public GraphQL API**: read-only, typed end-to-end via gql.tada, with filtering, sorting and pagination.
|
|
9
9
|
- **SQLite, Postgres or libSQL/Turso**: a local file database by default, one config line to switch (including remote SQLite over the network).
|
|
10
10
|
|
|
@@ -78,7 +78,7 @@ Full documentation lives in [`docs/`](docs/README.md):
|
|
|
78
78
|
- [Schema](docs/schema.md) — `defineCmsConfig`, entries, field types, relations, blocks, i18n.
|
|
79
79
|
- [Querying content](docs/querying.md) — GraphQL API, `useCms` / `$cmsQuery`, filters, sorting, pagination.
|
|
80
80
|
- [Admin panel & security](docs/admin.md) — pages, authentication, sessions, admin REST API.
|
|
81
|
-
- [Media](docs/media.md) — S3-compatible storage, upload flow, allowed file types.
|
|
81
|
+
- [Media](docs/media.md) — S3-compatible storage or read-only local mode, upload flow, allowed file types.
|
|
82
82
|
|
|
83
83
|
## LLM guide
|
|
84
84
|
|
package/dist/module.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
import { MediaStorageMode } from '../dist/runtime/shared/index.js';
|
|
2
3
|
|
|
3
4
|
type Dialect = 'sqlite' | 'postgres';
|
|
4
5
|
type Driver = Dialect | 'libsql';
|
|
@@ -17,6 +18,7 @@ interface ModuleOptions {
|
|
|
17
18
|
authToken?: string;
|
|
18
19
|
};
|
|
19
20
|
media: {
|
|
21
|
+
storage: MediaStorageMode;
|
|
20
22
|
endpoint: string;
|
|
21
23
|
region: string;
|
|
22
24
|
bucket: string;
|
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}'`;
|
|
@@ -437,6 +450,7 @@ const module$1 = defineNuxtModule({
|
|
|
437
450
|
authToken: ""
|
|
438
451
|
},
|
|
439
452
|
media: {
|
|
453
|
+
storage: "s3",
|
|
440
454
|
endpoint: "",
|
|
441
455
|
region: "auto",
|
|
442
456
|
bucket: "",
|
|
@@ -464,6 +478,7 @@ const module$1 = defineNuxtModule({
|
|
|
464
478
|
]);
|
|
465
479
|
nuxt.options.runtimeConfig.public.cms = {
|
|
466
480
|
mediaBaseUrl: options.media.publicBaseUrl,
|
|
481
|
+
mediaStorage: options.media.storage,
|
|
467
482
|
i18n: options.i18n
|
|
468
483
|
};
|
|
469
484
|
logger.info(
|
|
@@ -643,6 +658,7 @@ const module$1 = defineNuxtModule({
|
|
|
643
658
|
...existingConfig.graphql ?? {}
|
|
644
659
|
},
|
|
645
660
|
media: {
|
|
661
|
+
storage: options.media.storage,
|
|
646
662
|
endpoint: options.media.endpoint,
|
|
647
663
|
region: options.media.region,
|
|
648
664
|
bucket: options.media.bucket,
|
|
@@ -654,6 +670,7 @@ const module$1 = defineNuxtModule({
|
|
|
654
670
|
};
|
|
655
671
|
nuxt.options.runtimeConfig.public.cms = {
|
|
656
672
|
mediaBaseUrl: options.media.publicBaseUrl,
|
|
673
|
+
mediaStorage: options.media.storage,
|
|
657
674
|
i18n: options.i18n
|
|
658
675
|
};
|
|
659
676
|
addTypeTemplate(
|
|
@@ -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;
|