@xleddyl/nuxt-cms 0.1.9 → 0.1.10

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 (38) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +5 -1
  3. package/dist/runtime/app/components/cms/BlocksField.vue +59 -16
  4. package/dist/runtime/app/components/cms/ConfirmModal.d.vue.ts +20 -0
  5. package/dist/runtime/app/components/cms/ConfirmModal.vue +43 -0
  6. package/dist/runtime/app/components/cms/ConfirmModal.vue.d.ts +20 -0
  7. package/dist/runtime/app/components/cms/MediaGallery.vue +9 -2
  8. package/dist/runtime/app/components/cms/RelationField.vue +56 -10
  9. package/dist/runtime/app/components/cms/RichTextField.vue +63 -2
  10. package/dist/runtime/app/composables/cms-confirm.d.ts +4 -0
  11. package/dist/runtime/app/composables/cms-confirm.js +7 -0
  12. package/dist/runtime/app/i18n/en.json +32 -2
  13. package/dist/runtime/app/i18n/it.json +32 -2
  14. package/dist/runtime/app/pages/admin-collection.vue +89 -14
  15. package/dist/runtime/app/pages/admin-entry.vue +12 -5
  16. package/dist/runtime/server/api/collection.get.d.ts +1 -3
  17. package/dist/runtime/server/api/collection.get.js +34 -8
  18. package/dist/runtime/server/api/collection.post.js +11 -7
  19. package/dist/runtime/server/api/collection.put.js +11 -7
  20. package/dist/runtime/server/api/item.get.js +3 -2
  21. package/dist/runtime/server/api/item.put.js +12 -8
  22. package/dist/runtime/server/api/media-presign.post.js +2 -1
  23. package/dist/runtime/server/api/media.post.js +2 -1
  24. package/dist/runtime/server/plugins/migrate-postgres.js +9 -1
  25. package/dist/runtime/server/plugins/migrate-sqlite.js +9 -1
  26. package/dist/runtime/server/routes/auth/login.post.js +1 -1
  27. package/dist/runtime/server/utils/db-postgres.d.ts +4 -0
  28. package/dist/runtime/server/utils/db-postgres.js +3 -0
  29. package/dist/runtime/server/utils/db-sqlite.d.ts +2 -0
  30. package/dist/runtime/server/utils/db-sqlite.js +19 -0
  31. package/dist/runtime/server/utils/graphql.js +10 -4
  32. package/dist/runtime/server/utils/media.d.ts +1 -0
  33. package/dist/runtime/server/utils/media.js +27 -0
  34. package/dist/runtime/server/utils/relations.d.ts +3 -2
  35. package/dist/runtime/server/utils/relations.js +2 -4
  36. package/dist/runtime/server/utils/require-admin.js +15 -1
  37. package/dist/runtime/shared/index.d.ts +69 -1
  38. package/package.json +4 -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.9",
4
+ "version": "0.1.10",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -552,7 +552,11 @@ const module$1 = defineNuxtModule({
552
552
  ]
553
553
  });
554
554
  });
555
- const { driver = "sqlite", path: dbPath = "data/cms.db", url: databaseUrl = "" } = options.database;
555
+ const {
556
+ driver = "sqlite",
557
+ path: dbPath = "data/cms.db",
558
+ url: databaseUrl = ""
559
+ } = options.database;
556
560
  nuxt.options.alias["#cms-db"] = resolver.resolve(
557
561
  driver === "postgres" ? "./runtime/server/utils/db-postgres" : "./runtime/server/utils/db-sqlite"
558
562
  );
@@ -6,14 +6,26 @@
6
6
  class="cms-card flex flex-col gap-4 p-4"
7
7
  >
8
8
  <div class="flex items-center gap-1">
9
- <span class="cms-kicker grow">
9
+ <button
10
+ type="button"
11
+ class="cms-kicker flex grow items-center gap-1.5 text-left"
12
+ :aria-label="
13
+ isCollapsed(item) ? t('cms.form.expandBlock') : t('cms.form.collapseBlock')
14
+ "
15
+ @click="toggleCollapsed(item)"
16
+ >
17
+ <UIcon
18
+ :name="isCollapsed(item) ? 'i-lucide-chevron-right' : 'i-lucide-chevron-down'"
19
+ class="size-3.5"
20
+ />
10
21
  {{ blocks[item.type]?.label ?? item.type }}
11
- </span>
22
+ </button>
12
23
  <UButton
13
24
  icon="i-lucide-chevron-up"
14
25
  size="xs"
15
26
  variant="ghost"
16
27
  color="neutral"
28
+ :aria-label="t('cms.form.moveUp')"
17
29
  :disabled="index === 0"
18
30
  @click="move(index, -1)"
19
31
  />
@@ -22,30 +34,42 @@
22
34
  size="xs"
23
35
  variant="ghost"
24
36
  color="neutral"
37
+ :aria-label="t('cms.form.moveDown')"
25
38
  :disabled="index === items.length - 1"
26
39
  @click="move(index, 1)"
27
40
  />
41
+ <UButton
42
+ icon="i-lucide-copy"
43
+ size="xs"
44
+ variant="ghost"
45
+ color="neutral"
46
+ :aria-label="t('cms.form.duplicateBlock')"
47
+ @click="duplicate(index)"
48
+ />
28
49
  <UButton
29
50
  icon="i-lucide-trash-2"
30
51
  size="xs"
31
52
  variant="ghost"
32
53
  color="error"
54
+ :aria-label="t('cms.form.removeBlock')"
33
55
  @click="remove(index)"
34
56
  />
35
57
  </div>
36
- <UFormField
37
- v-for="(blockField, blockKey) in blocks[item.type]?.fields"
38
- :key="blockKey"
39
- :label="blockField.label"
40
- :required="blockField.required"
41
- :ui="CMS_FIELD_UI"
42
- >
43
- <CmsFieldInput
44
- :model-value="item[blockKey]"
45
- :field="blockField"
46
- @update:model-value="(value) => updateField(index, blockKey, value)"
47
- />
48
- </UFormField>
58
+ <template v-if="!isCollapsed(item)">
59
+ <UFormField
60
+ v-for="(blockField, blockKey) in blocks[item.type]?.fields"
61
+ :key="blockKey"
62
+ :label="blockField.label"
63
+ :required="blockField.required"
64
+ :ui="CMS_FIELD_UI"
65
+ >
66
+ <CmsFieldInput
67
+ :model-value="item[blockKey]"
68
+ :field="blockField"
69
+ @update:model-value="(value) => updateField(index, blockKey, value)"
70
+ />
71
+ </UFormField>
72
+ </template>
49
73
  </div>
50
74
  <UDropdownMenu :items="addItems">
51
75
  <UButton
@@ -59,7 +83,7 @@
59
83
  </template>
60
84
 
61
85
  <script setup>
62
- import { computed, useI18n } from "#imports";
86
+ import { computed, ref, useI18n } from "#imports";
63
87
  import { CMS_FIELD_UI } from "../../utils/ui";
64
88
  const props = defineProps({
65
89
  field: { type: Object, required: true }
@@ -95,6 +119,25 @@ function remove(index) {
95
119
  const next = items.value.filter((_, i) => i !== index);
96
120
  model.value = next.length ? next : null;
97
121
  }
122
+ function duplicate(index) {
123
+ const current = items.value[index];
124
+ if (!current) return;
125
+ const copy = JSON.parse(JSON.stringify(current));
126
+ const next = [...items.value];
127
+ next.splice(index + 1, 0, copy);
128
+ model.value = next;
129
+ }
130
+ const collapsedUids = ref(/* @__PURE__ */ new Set());
131
+ function isCollapsed(item) {
132
+ return collapsedUids.value.has(uidFor(item));
133
+ }
134
+ function toggleCollapsed(item) {
135
+ const uid = uidFor(item);
136
+ const next = new Set(collapsedUids.value);
137
+ if (next.has(uid)) next.delete(uid);
138
+ else next.add(uid);
139
+ collapsedUids.value = next;
140
+ }
98
141
  function move(index, delta) {
99
142
  const next = [...items.value];
100
143
  const [item] = next.splice(index, 1);
@@ -0,0 +1,20 @@
1
+ type __VLS_Props = {
2
+ message: string;
3
+ title?: string;
4
+ confirmLabel?: string;
5
+ };
6
+ type __VLS_ModelProps = {
7
+ 'open'?: boolean;
8
+ };
9
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
+ close: (value: boolean) => any;
12
+ "after:leave": () => any;
13
+ "update:open": (value: boolean) => any;
14
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
15
+ onClose?: ((value: boolean) => any) | undefined;
16
+ "onAfter:leave"?: (() => any) | undefined;
17
+ "onUpdate:open"?: ((value: boolean) => any) | undefined;
18
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <UModal
3
+ v-model:open="open"
4
+ :title="title ?? t('cms.confirm.title')"
5
+ :ui="ui"
6
+ @after:leave="emit('after:leave')"
7
+ >
8
+ <template #body>
9
+ <div class="flex flex-col gap-5">
10
+ <p class="text-(--ui-text) text-sm">{{ message }}</p>
11
+ <div class="flex justify-end gap-2">
12
+ <UButton
13
+ :label="t('cms.confirm.cancel')"
14
+ variant="subtle"
15
+ color="neutral"
16
+ class="rounded-full px-4"
17
+ @click="emit('close', false)"
18
+ />
19
+ <UButton
20
+ :label="confirmLabel ?? t('cms.confirm.confirm')"
21
+ color="error"
22
+ class="rounded-full px-4"
23
+ @click="emit('close', true)"
24
+ />
25
+ </div>
26
+ </div>
27
+ </template>
28
+ </UModal>
29
+ </template>
30
+
31
+ <script setup>
32
+ import { useI18n } from "#imports";
33
+ import { CMS_MODAL_UI } from "../../utils/ui";
34
+ defineProps({
35
+ message: { type: String, required: true },
36
+ title: { type: String, required: false },
37
+ confirmLabel: { type: String, required: false }
38
+ });
39
+ const open = defineModel("open", { type: Boolean, ...{ default: true } });
40
+ const emit = defineEmits(["close", "after:leave"]);
41
+ const { t } = useI18n();
42
+ const ui = { ...CMS_MODAL_UI, content: "rounded-2xl shadow-xl sm:max-w-md" };
43
+ </script>
@@ -0,0 +1,20 @@
1
+ type __VLS_Props = {
2
+ message: string;
3
+ title?: string;
4
+ confirmLabel?: string;
5
+ };
6
+ type __VLS_ModelProps = {
7
+ 'open'?: boolean;
8
+ };
9
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
+ close: (value: boolean) => any;
12
+ "after:leave": () => any;
13
+ "update:open": (value: boolean) => any;
14
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
15
+ onClose?: ((value: boolean) => any) | undefined;
16
+ "onAfter:leave"?: (() => any) | undefined;
17
+ "onUpdate:open"?: ((value: boolean) => any) | undefined;
18
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;
@@ -86,7 +86,7 @@
86
86
  />
87
87
  <div
88
88
  v-if="!selectable"
89
- class="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100"
89
+ class="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100"
90
90
  >
91
91
  <UButton
92
92
  icon="i-lucide-pencil"
@@ -187,6 +187,7 @@
187
187
  <script setup>
188
188
  import { computed, onMounted, ref, useI18n, useToast } from "#imports";
189
189
  import { MEDIA_TYPES, mediaFilename, mediaIconFor } from "#nuxt-cms";
190
+ import { useCmsConfirm } from "../../composables/cms-confirm";
190
191
  import { CMS_MODAL_UI, errorMessage } from "../../utils/ui";
191
192
  const props = defineProps({
192
193
  selectable: { type: Boolean, required: false },
@@ -293,8 +294,12 @@ async function copy(item) {
293
294
  toast.add({ title: t("cms.media.copyError"), color: "error" });
294
295
  }
295
296
  }
297
+ const confirmAction = useCmsConfirm();
298
+ const removing = ref(false);
296
299
  async function remove(item) {
297
- if (!confirm(t("cms.media.deleteConfirm"))) return;
300
+ if (removing.value) return;
301
+ if (!await confirmAction(t("cms.media.deleteConfirm"))) return;
302
+ removing.value = true;
298
303
  try {
299
304
  await $fetch(endpoint, { method: "DELETE", query: { key: item.key } });
300
305
  await reload();
@@ -304,6 +309,8 @@ async function remove(item) {
304
309
  description: errorMessage(err),
305
310
  color: "error"
306
311
  });
312
+ } finally {
313
+ removing.value = false;
307
314
  }
308
315
  }
309
316
  </script>
@@ -2,7 +2,9 @@
2
2
  <USelectMenu
3
3
  v-if="multiple"
4
4
  v-model="many"
5
+ v-model:search-term="searchTerm"
5
6
  multiple
7
+ ignore-filter
6
8
  :items="options"
7
9
  value-key="value"
8
10
  :loading="loading"
@@ -12,6 +14,8 @@
12
14
  <USelectMenu
13
15
  v-else
14
16
  v-model="single"
17
+ v-model:search-term="searchTerm"
18
+ ignore-filter
15
19
  :items="singleItems"
16
20
  value-key="value"
17
21
  :loading="loading"
@@ -22,7 +26,7 @@
22
26
 
23
27
  <script setup>
24
28
  import { isTranslatableField } from "#nuxt-cms";
25
- import { computed, onMounted, ref, useI18n, useToast } from "#imports";
29
+ import { computed, onMounted, ref, useI18n, useToast, watch } from "#imports";
26
30
  import cmsConfig from "#cms-config";
27
31
  import { useCmsRuntime } from "../../composables/cms-runtime";
28
32
  import { errorMessage } from "../../utils/ui";
@@ -35,11 +39,19 @@ const model = defineModel({ type: [String, Array, null], ...{ required: true } }
35
39
  const { t } = useI18n();
36
40
  const toast = useToast();
37
41
  const target = cmsConfig[props.to];
38
- const rows = ref([]);
42
+ const endpoint = `/api/cms/admin/${props.to}`;
43
+ const fetched = ref([]);
44
+ const selectedRows = ref(/* @__PURE__ */ new Map());
39
45
  const loading = ref(true);
40
- onMounted(async () => {
46
+ const searchTerm = ref("");
47
+ async function fetchOptions() {
48
+ loading.value = true;
41
49
  try {
42
- rows.value = await $fetch(`/api/cms/admin/${props.to}`);
50
+ const term = searchTerm.value.trim();
51
+ const page = await $fetch(endpoint, {
52
+ query: { light: "true", limit: 25, ...term ? { search: term } : {} }
53
+ });
54
+ fetched.value = page.items;
43
55
  } catch (err) {
44
56
  toast.add({
45
57
  title: t("cms.toast.loadFailed"),
@@ -49,6 +61,31 @@ onMounted(async () => {
49
61
  } finally {
50
62
  loading.value = false;
51
63
  }
64
+ }
65
+ let searchTimer;
66
+ watch(searchTerm, () => {
67
+ clearTimeout(searchTimer);
68
+ searchTimer = setTimeout(() => void fetchOptions(), 300);
69
+ });
70
+ const selectedIds = computed(() => {
71
+ if (props.multiple) return model.value ?? [];
72
+ return model.value ? [model.value] : [];
73
+ });
74
+ async function resolveSelected() {
75
+ const missing = selectedIds.value.filter((id) => !selectedRows.value.has(id));
76
+ await Promise.all(
77
+ missing.map(async (id) => {
78
+ try {
79
+ const row = await $fetch(`${endpoint}/${id}`);
80
+ selectedRows.value.set(id, row);
81
+ } catch {
82
+ }
83
+ })
84
+ );
85
+ }
86
+ onMounted(() => {
87
+ void fetchOptions();
88
+ void resolveSelected();
52
89
  });
53
90
  const { i18n: contentI18n } = useCmsRuntime();
54
91
  const titleField = target?.titleField ?? "id";
@@ -62,12 +99,21 @@ function optionLabel(row) {
62
99
  }
63
100
  return String(value ?? `#${row.id}`);
64
101
  }
65
- const options = computed(
66
- () => rows.value.map((row) => ({
67
- label: optionLabel(row),
68
- value: row.id
69
- }))
70
- );
102
+ const options = computed(() => {
103
+ const seen = /* @__PURE__ */ new Set();
104
+ const list = [];
105
+ for (const row of fetched.value) {
106
+ const id = row.id;
107
+ list.push({ label: optionLabel(row), value: id });
108
+ seen.add(id);
109
+ }
110
+ for (const id of selectedIds.value) {
111
+ if (seen.has(id)) continue;
112
+ const row = selectedRows.value.get(id);
113
+ list.push({ label: row ? optionLabel(row) : `#${id}`, value: id });
114
+ }
115
+ return list;
116
+ });
71
117
  const singleItems = computed(() => [
72
118
  ...props.required ? [] : [{ label: "\u2014", value: null }],
73
119
  ...options.value
@@ -8,14 +8,48 @@
8
8
  size="xs"
9
9
  :variant="editor?.isActive(action.mark, action.attrs) ? 'soft' : 'ghost'"
10
10
  color="neutral"
11
+ :aria-label="t(action.label)"
12
+ :title="t(action.label)"
11
13
  @click="action.run"
12
14
  />
15
+ <UPopover v-model:open="linkOpen">
16
+ <UButton
17
+ icon="i-lucide-link"
18
+ size="xs"
19
+ :variant="editor?.isActive('link') ? 'soft' : 'ghost'"
20
+ color="neutral"
21
+ :aria-label="t('cms.richtext.link')"
22
+ :title="t('cms.richtext.link')"
23
+ />
24
+ <template #content>
25
+ <div class="flex items-center gap-2 p-2">
26
+ <UInput
27
+ v-model="linkUrl"
28
+ size="sm"
29
+ placeholder="https://"
30
+ class="w-64"
31
+ @keydown.enter.prevent="applyLink"
32
+ />
33
+ <UButton size="xs" :label="t('cms.richtext.setLink')" @click="applyLink" />
34
+ <UButton
35
+ v-if="editor?.isActive('link')"
36
+ size="xs"
37
+ variant="subtle"
38
+ color="error"
39
+ :label="t('cms.richtext.unsetLink')"
40
+ @click="removeLink"
41
+ />
42
+ </div>
43
+ </template>
44
+ </UPopover>
13
45
  <span class="grow" />
14
46
  <UButton
15
47
  icon="i-lucide-undo-2"
16
48
  size="xs"
17
49
  variant="ghost"
18
50
  color="neutral"
51
+ :aria-label="t('cms.richtext.undo')"
52
+ :title="t('cms.richtext.undo')"
19
53
  :disabled="!editor?.can().undo()"
20
54
  @click="undo"
21
55
  />
@@ -24,6 +58,8 @@
24
58
  size="xs"
25
59
  variant="ghost"
26
60
  color="neutral"
61
+ :aria-label="t('cms.richtext.redo')"
62
+ :title="t('cms.richtext.redo')"
27
63
  :disabled="!editor?.can().redo()"
28
64
  @click="redo"
29
65
  />
@@ -35,11 +71,12 @@
35
71
  <script setup>
36
72
  import StarterKit from "@tiptap/starter-kit";
37
73
  import { EditorContent, useEditor } from "@tiptap/vue-3";
38
- import { watch } from "#imports";
74
+ import { ref, useI18n, watch } from "#imports";
39
75
  const model = defineModel({ type: [String, null], ...{ required: true } });
76
+ const { t } = useI18n();
40
77
  const editor = useEditor({
41
78
  content: model.value ?? "",
42
- extensions: [StarterKit],
79
+ extensions: [StarterKit.configure({ link: { openOnClick: false } })],
43
80
  editorProps: {
44
81
  attributes: { class: "cms-richtext-body" }
45
82
  },
@@ -58,6 +95,7 @@ const actions = [
58
95
  {
59
96
  icon: "i-lucide-bold",
60
97
  mark: "bold",
98
+ label: "cms.richtext.bold",
61
99
  run: () => {
62
100
  editor.value?.chain().focus().toggleBold().run();
63
101
  }
@@ -65,6 +103,7 @@ const actions = [
65
103
  {
66
104
  icon: "i-lucide-italic",
67
105
  mark: "italic",
106
+ label: "cms.richtext.italic",
68
107
  run: () => {
69
108
  editor.value?.chain().focus().toggleItalic().run();
70
109
  }
@@ -72,6 +111,7 @@ const actions = [
72
111
  {
73
112
  icon: "i-lucide-strikethrough",
74
113
  mark: "strike",
114
+ label: "cms.richtext.strike",
75
115
  run: () => {
76
116
  editor.value?.chain().focus().toggleStrike().run();
77
117
  }
@@ -80,6 +120,7 @@ const actions = [
80
120
  icon: "i-lucide-heading-2",
81
121
  mark: "heading",
82
122
  attrs: { level: 2 },
123
+ label: "cms.richtext.h2",
83
124
  run: () => {
84
125
  editor.value?.chain().focus().toggleHeading({ level: 2 }).run();
85
126
  }
@@ -88,6 +129,7 @@ const actions = [
88
129
  icon: "i-lucide-heading-3",
89
130
  mark: "heading",
90
131
  attrs: { level: 3 },
132
+ label: "cms.richtext.h3",
91
133
  run: () => {
92
134
  editor.value?.chain().focus().toggleHeading({ level: 3 }).run();
93
135
  }
@@ -95,6 +137,7 @@ const actions = [
95
137
  {
96
138
  icon: "i-lucide-list",
97
139
  mark: "bulletList",
140
+ label: "cms.richtext.bulletList",
98
141
  run: () => {
99
142
  editor.value?.chain().focus().toggleBulletList().run();
100
143
  }
@@ -102,6 +145,7 @@ const actions = [
102
145
  {
103
146
  icon: "i-lucide-list-ordered",
104
147
  mark: "orderedList",
148
+ label: "cms.richtext.orderedList",
105
149
  run: () => {
106
150
  editor.value?.chain().focus().toggleOrderedList().run();
107
151
  }
@@ -109,11 +153,28 @@ const actions = [
109
153
  {
110
154
  icon: "i-lucide-text-quote",
111
155
  mark: "blockquote",
156
+ label: "cms.richtext.blockquote",
112
157
  run: () => {
113
158
  editor.value?.chain().focus().toggleBlockquote().run();
114
159
  }
115
160
  }
116
161
  ];
162
+ const linkOpen = ref(false);
163
+ const linkUrl = ref("");
164
+ watch(linkOpen, (open) => {
165
+ if (open) linkUrl.value = editor.value?.getAttributes("link").href ?? "";
166
+ });
167
+ function applyLink() {
168
+ const url = linkUrl.value.trim();
169
+ const chain = editor.value?.chain().focus().extendMarkRange("link");
170
+ if (url) chain?.setLink({ href: url }).run();
171
+ else chain?.unsetLink().run();
172
+ linkOpen.value = false;
173
+ }
174
+ function removeLink() {
175
+ editor.value?.chain().focus().extendMarkRange("link").unsetLink().run();
176
+ linkOpen.value = false;
177
+ }
117
178
  function undo() {
118
179
  editor.value?.chain().focus().undo().run();
119
180
  }
@@ -0,0 +1,4 @@
1
+ export declare function useCmsConfirm(): (message: string, options?: {
2
+ title?: string;
3
+ confirmLabel?: string;
4
+ }) => Promise<boolean>;
@@ -0,0 +1,7 @@
1
+ import { useOverlay } from "#imports";
2
+ import ConfirmModal from "../components/cms/ConfirmModal.vue";
3
+ export function useCmsConfirm() {
4
+ const overlay = useOverlay();
5
+ const modal = overlay.create(ConfirmModal);
6
+ return async (message, options) => await modal.open({ message, ...options }) === true;
7
+ }
@@ -27,7 +27,16 @@
27
27
  "emptyBody": "Entries you create will show up here.",
28
28
  "deleteConfirm": "Delete this row?",
29
29
  "status": "Status",
30
- "columns": "Columns"
30
+ "columns": "Columns",
31
+ "loadError": "Could not load entries",
32
+ "retry": "Retry",
33
+ "searchPlaceholder": "Search…",
34
+ "noResults": "No matching entries"
35
+ },
36
+ "confirm": {
37
+ "title": "Are you sure?",
38
+ "confirm": "Confirm",
39
+ "cancel": "Cancel"
31
40
  },
32
41
  "entry": {
33
42
  "unsavedChanges": "You have unsaved changes. Leave anyway?"
@@ -79,7 +88,28 @@
79
88
  "mediaChange": "Change",
80
89
  "mediaRemove": "Remove",
81
90
  "invalidJson": "Invalid JSON",
82
- "addBlock": "Add block"
91
+ "addBlock": "Add block",
92
+ "moveUp": "Move up",
93
+ "moveDown": "Move down",
94
+ "duplicateBlock": "Duplicate block",
95
+ "removeBlock": "Remove block",
96
+ "collapseBlock": "Collapse block",
97
+ "expandBlock": "Expand block"
98
+ },
99
+ "richtext": {
100
+ "bold": "Bold",
101
+ "italic": "Italic",
102
+ "strike": "Strikethrough",
103
+ "h2": "Heading 2",
104
+ "h3": "Heading 3",
105
+ "bulletList": "Bullet list",
106
+ "orderedList": "Numbered list",
107
+ "blockquote": "Quote",
108
+ "link": "Link",
109
+ "setLink": "Apply",
110
+ "unsetLink": "Remove link",
111
+ "undo": "Undo",
112
+ "redo": "Redo"
83
113
  },
84
114
  "validation": {
85
115
  "required": "Required field",
@@ -27,7 +27,16 @@
27
27
  "emptyBody": "Le voci che crei appariranno qui.",
28
28
  "deleteConfirm": "Eliminare questa voce?",
29
29
  "status": "Stato",
30
- "columns": "Colonne"
30
+ "columns": "Colonne",
31
+ "loadError": "Impossibile caricare le voci",
32
+ "retry": "Riprova",
33
+ "searchPlaceholder": "Cerca…",
34
+ "noResults": "Nessuna voce corrispondente"
35
+ },
36
+ "confirm": {
37
+ "title": "Sei sicuro?",
38
+ "confirm": "Conferma",
39
+ "cancel": "Annulla"
31
40
  },
32
41
  "entry": {
33
42
  "unsavedChanges": "Ci sono modifiche non salvate. Uscire comunque?"
@@ -79,7 +88,28 @@
79
88
  "mediaChange": "Cambia",
80
89
  "mediaRemove": "Rimuovi",
81
90
  "invalidJson": "JSON non valido",
82
- "addBlock": "Aggiungi blocco"
91
+ "addBlock": "Aggiungi blocco",
92
+ "moveUp": "Sposta su",
93
+ "moveDown": "Sposta giù",
94
+ "duplicateBlock": "Duplica blocco",
95
+ "removeBlock": "Rimuovi blocco",
96
+ "collapseBlock": "Comprimi blocco",
97
+ "expandBlock": "Espandi blocco"
98
+ },
99
+ "richtext": {
100
+ "bold": "Grassetto",
101
+ "italic": "Corsivo",
102
+ "strike": "Barrato",
103
+ "h2": "Titolo 2",
104
+ "h3": "Titolo 3",
105
+ "bulletList": "Elenco puntato",
106
+ "orderedList": "Elenco numerato",
107
+ "blockquote": "Citazione",
108
+ "link": "Link",
109
+ "setLink": "Applica",
110
+ "unsetLink": "Rimuovi link",
111
+ "undo": "Annulla",
112
+ "redo": "Ripristina"
83
113
  },
84
114
  "validation": {
85
115
  "required": "Campo obbligatorio",