@xleddyl/nuxt-cms 0.1.23 → 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.
Files changed (69) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +16 -3
  3. package/dist/runtime/app/components/cms/BlocksField.vue +2 -9
  4. package/dist/runtime/app/components/cms/Button.vue +4 -2
  5. package/dist/runtime/app/components/cms/ConfirmModal.vue +5 -14
  6. package/dist/runtime/app/components/cms/Drawer.d.vue.ts +30 -0
  7. package/dist/runtime/app/components/cms/Drawer.vue +65 -0
  8. package/dist/runtime/app/components/cms/Drawer.vue.d.ts +30 -0
  9. package/dist/runtime/app/components/cms/DropdownMenu.d.vue.ts +0 -3
  10. package/dist/runtime/app/components/cms/DropdownMenu.vue +2 -7
  11. package/dist/runtime/app/components/cms/DropdownMenu.vue.d.ts +0 -3
  12. package/dist/runtime/app/components/cms/EmptyState.d.vue.ts +1 -0
  13. package/dist/runtime/app/components/cms/EmptyState.vue +6 -7
  14. package/dist/runtime/app/components/cms/EmptyState.vue.d.ts +1 -0
  15. package/dist/runtime/app/components/cms/EntryDrawer.d.vue.ts +21 -0
  16. package/dist/runtime/app/components/cms/EntryDrawer.vue +193 -0
  17. package/dist/runtime/app/components/cms/EntryDrawer.vue.d.ts +21 -0
  18. package/dist/runtime/app/components/cms/EntryForm.vue +19 -7
  19. package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +1 -0
  20. package/dist/runtime/app/components/cms/FieldInput.vue +13 -27
  21. package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +1 -0
  22. package/dist/runtime/app/components/cms/Form.vue +1 -1
  23. package/dist/runtime/app/components/cms/FormField.d.vue.ts +4 -5
  24. package/dist/runtime/app/components/cms/FormField.vue +9 -7
  25. package/dist/runtime/app/components/cms/FormField.vue.d.ts +4 -5
  26. package/dist/runtime/app/components/cms/JsonField.vue +3 -4
  27. package/dist/runtime/app/components/cms/LocaleSwitch.d.vue.ts +14 -0
  28. package/dist/runtime/app/components/cms/LocaleSwitch.vue +29 -0
  29. package/dist/runtime/app/components/cms/LocaleSwitch.vue.d.ts +14 -0
  30. package/dist/runtime/app/components/cms/MediaField.vue +7 -17
  31. package/dist/runtime/app/components/cms/MediaGallery.vue +48 -78
  32. package/dist/runtime/app/components/cms/MediaThumb.d.vue.ts +6 -0
  33. package/dist/runtime/app/components/cms/MediaThumb.vue +40 -0
  34. package/dist/runtime/app/components/cms/MediaThumb.vue.d.ts +6 -0
  35. package/dist/runtime/app/components/cms/Modal.d.vue.ts +1 -4
  36. package/dist/runtime/app/components/cms/Modal.vue +8 -7
  37. package/dist/runtime/app/components/cms/Modal.vue.d.ts +1 -4
  38. package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +1 -1
  39. package/dist/runtime/app/components/cms/PageHeader.vue +6 -6
  40. package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +1 -1
  41. package/dist/runtime/app/components/cms/RelationField.vue +0 -4
  42. package/dist/runtime/app/components/cms/SelectMenu.vue +23 -3
  43. package/dist/runtime/app/components/cms/SlugField.vue +1 -2
  44. package/dist/runtime/app/components/cms/Spinner.d.vue.ts +3 -0
  45. package/dist/runtime/app/components/cms/Spinner.vue +5 -0
  46. package/dist/runtime/app/components/cms/Spinner.vue.d.ts +3 -0
  47. package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +6 -0
  48. package/dist/runtime/app/components/cms/StatusBadge.vue +11 -0
  49. package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +6 -0
  50. package/dist/runtime/app/components/cms/Table.d.vue.ts +3 -6
  51. package/dist/runtime/app/components/cms/Table.vue +72 -27
  52. package/dist/runtime/app/components/cms/Table.vue.d.ts +3 -6
  53. package/dist/runtime/app/components/cms/Toaster.vue +4 -4
  54. package/dist/runtime/app/components/cms/TranslatableField.d.vue.ts +1 -0
  55. package/dist/runtime/app/components/cms/TranslatableField.vue +7 -44
  56. package/dist/runtime/app/components/cms/TranslatableField.vue.d.ts +1 -0
  57. package/dist/runtime/app/layouts/cms-admin.vue +10 -20
  58. package/dist/runtime/app/pages/admin-collection.vue +130 -103
  59. package/dist/runtime/app/pages/admin-entry.vue +6 -24
  60. package/dist/runtime/app/pages/admin-login.vue +12 -31
  61. package/dist/runtime/app/pages/admin-media.vue +8 -2
  62. package/dist/runtime/app/utils/ui.d.ts +0 -7
  63. package/dist/runtime/app/utils/ui.js +0 -5
  64. package/dist/runtime/assets/main.css +1 -1
  65. package/dist/runtime/server/api/collection.get.js +8 -3
  66. package/dist/runtime/server/utils/relations.d.ts +1 -0
  67. package/dist/runtime/server/utils/relations.js +25 -0
  68. package/dist/runtime/shared/index.d.ts +10 -3
  69. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xleddyl/nuxt-cms",
3
3
  "configKey": "cms",
4
- "version": "0.1.23",
4
+ "version": "0.1.24",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
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 { isTranslatableField, isMultiSelect } from '../dist/runtime/shared/index.js';
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.titleField && !entry.fields?.[entry.titleField])
72
- errors.push(`${at}: titleField '${entry.titleField}' is not a declared field`);
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
- :ui="ui"
5
+ size="sm"
6
6
  @after:leave="finish(false)"
7
7
  >
8
8
  <template #body>
9
- <div class="flex flex-col gap-5">
10
- <p class="text-sm text-(--ui-text)">{{ state.message }}</p>
11
- <div class="flex justify-end gap-2">
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
+ };
@@ -10,9 +10,6 @@ type __VLS_Props = {
10
10
  content?: {
11
11
  align?: 'start' | 'end';
12
12
  };
13
- ui?: {
14
- content?: string;
15
- };
16
13
  };
17
14
  declare var __VLS_1: {};
18
15
  type __VLS_Slots = {} & {
@@ -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);
@@ -10,9 +10,6 @@ type __VLS_Props = {
10
10
  content?: {
11
11
  align?: 'start' | 'end';
12
12
  };
13
- ui?: {
14
- content?: string;
15
- };
16
13
  };
17
14
  declare var __VLS_1: {};
18
15
  type __VLS_Slots = {} & {
@@ -2,6 +2,7 @@ type __VLS_Props = {
2
2
  icon: string;
3
3
  title: string;
4
4
  body?: string;
5
+ fill?: boolean;
5
6
  };
6
7
  declare var __VLS_6: {};
7
8
  type __VLS_Slots = {} & {
@@ -1,10 +1,8 @@
1
1
  <template>
2
- <div class="cms-empty flex flex-col items-center gap-2 p-14 text-center">
3
- <CmsIcon :name="icon" class="mb-1 size-6 text-(--cms-fern)" />
4
- <p class="cms-display text-xl text-(--ui-text-highlighted)">
5
- {{ title }}<span class="text-(--cms-fern)">.</span>
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>
@@ -2,6 +2,7 @@ type __VLS_Props = {
2
2
  icon: string;
3
3
  title: string;
4
4
  body?: string;
5
+ fill?: boolean;
5
6
  };
6
7
  declare var __VLS_6: {};
7
8
  type __VLS_Slots = {} & {
@@ -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="flex items-center justify-end gap-3 pt-1">
25
- <CmsButton type="submit" label="Save" :loading="loading" class="rounded-full px-6" />
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 { computed } from "#imports";
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
  );
@@ -2,6 +2,7 @@ import type { FieldConfig } from '#nuxt-cms';
2
2
  type __VLS_Props = {
3
3
  field: FieldConfig;
4
4
  slugSource?: string | null;
5
+ locale?: string;
5
6
  };
6
7
  type __VLS_ModelProps = {
7
8
  modelValue: unknown;