@mapomodule/form 2.0.0-beta.0 → 2.0.0-beta.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.
- package/README.md +2 -1
- package/dist/__tests__/__mocks__/nuxt-imports.d.ts +4 -0
- package/dist/__tests__/__mocks__/nuxt-imports.d.ts.map +1 -0
- package/dist/__tests__/camomillaRoundTrip.test.d.ts +2 -0
- package/dist/__tests__/camomillaRoundTrip.test.d.ts.map +1 -0
- package/dist/__tests__/registry.test.d.ts +2 -0
- package/dist/__tests__/registry.test.d.ts.map +1 -0
- package/dist/__tests__/useFormFromSchema.test.d.ts +2 -0
- package/dist/__tests__/useFormFromSchema.test.d.ts.map +1 -0
- package/dist/__tests__/useMapoForm.test.d.ts +2 -0
- package/dist/__tests__/useMapoForm.test.d.ts.map +1 -0
- package/dist/index.d.mts +9 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +7 -34
- package/dist/module.d.mts +20 -2
- package/dist/module.d.ts +12 -2
- package/dist/module.d.ts.map +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +187 -13
- package/dist/runtime/components/MapoFocusPortal.vue +115 -0
- package/dist/runtime/components/MapoFocusPortal.vue.d.ts +4 -0
- package/dist/runtime/components/MapoFocusPortal.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoForm.vue +168 -0
- package/dist/runtime/components/MapoForm.vue.d.ts +47 -0
- package/dist/runtime/components/MapoForm.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoFormField.vue +269 -0
- package/dist/runtime/components/MapoFormField.vue.d.ts +46 -0
- package/dist/runtime/components/MapoFormField.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoFormFlatSection.vue +149 -0
- package/dist/runtime/components/MapoFormFlatSection.vue.d.ts +15 -0
- package/dist/runtime/components/MapoFormFlatSection.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoFormGroup.vue +247 -0
- package/dist/runtime/components/MapoFormGroup.vue.d.ts +44 -0
- package/dist/runtime/components/MapoFormGroup.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoFormTabs.vue +110 -0
- package/dist/runtime/components/MapoFormTabs.vue.d.ts +15 -0
- package/dist/runtime/components/MapoFormTabs.vue.d.ts.map +1 -0
- package/dist/runtime/components/MapoUnknownField.vue +62 -0
- package/dist/runtime/components/MapoUnknownField.vue.d.ts +16 -0
- package/dist/runtime/components/MapoUnknownField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoDateField.vue +88 -0
- package/dist/runtime/components/fields/MapoDateField.vue.d.ts +17 -0
- package/dist/runtime/components/fields/MapoDateField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoDateTimeField.vue +159 -0
- package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts +17 -0
- package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoEnhancedMediaField.vue +130 -0
- package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoFileField.vue +99 -0
- package/dist/runtime/components/fields/MapoFileField.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoFileField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoFksField.vue +122 -0
- package/dist/runtime/components/fields/MapoFksField.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoFksField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoMapField.vue +99 -0
- package/dist/runtime/components/fields/MapoMapField.vue.d.ts +22 -0
- package/dist/runtime/components/fields/MapoMapField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoMapFieldClient.vue +67 -0
- package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts +20 -0
- package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoMediaField.vue +91 -0
- package/dist/runtime/components/fields/MapoMediaField.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoMediaField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoMediaM2mField.vue +110 -0
- package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoMediaUnavailable.vue +15 -0
- package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts +4 -0
- package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoRepeater.vue +459 -0
- package/dist/runtime/components/fields/MapoRepeater.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoRepeater.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoRepeaterItem.vue +346 -0
- package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts +55 -0
- package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoSeoPreview.vue +122 -0
- package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts +22 -0
- package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoTimeField.vue +44 -0
- package/dist/runtime/components/fields/MapoTimeField.vue.d.ts +17 -0
- package/dist/runtime/components/fields/MapoTimeField.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/MapoWygEditor.vue +252 -0
- package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts +16 -0
- package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiCheckbox.vue +9 -0
- package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiInput.vue +9 -0
- package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiSelectMenu.vue +18 -0
- package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiSlider.vue +9 -0
- package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiSwitch.vue +9 -0
- package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/NuiTextarea.vue +9 -0
- package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts +4 -0
- package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts +13 -0
- package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts.map +1 -0
- package/dist/runtime/components/fields/nui/useNuiFieldAttrs.js +19 -0
- package/dist/runtime/composables/defineFormField.d.ts +25 -0
- package/dist/runtime/composables/defineFormField.d.ts.map +1 -0
- package/dist/runtime/composables/defineFormField.js +27 -0
- package/dist/runtime/composables/useCurrentLang.d.ts +6 -0
- package/dist/runtime/composables/useCurrentLang.d.ts.map +1 -0
- package/dist/runtime/composables/useCurrentLang.js +9 -0
- package/dist/runtime/composables/useFocusMode.d.ts +40 -0
- package/dist/runtime/composables/useFocusMode.d.ts.map +1 -0
- package/dist/runtime/composables/useFocusMode.js +31 -0
- package/dist/runtime/composables/useFormDraft.d.ts +60 -0
- package/dist/runtime/composables/useFormDraft.d.ts.map +1 -0
- package/dist/runtime/composables/useFormDraft.js +52 -0
- package/dist/runtime/composables/useFormFromSchema.d.ts +21 -0
- package/dist/runtime/composables/useFormFromSchema.d.ts.map +1 -0
- package/dist/runtime/composables/useFormFromSchema.js +145 -0
- package/dist/runtime/composables/useMapoForm.d.ts +96 -0
- package/dist/runtime/composables/useMapoForm.d.ts.map +1 -0
- package/dist/runtime/composables/useMapoForm.js +325 -0
- package/dist/runtime/composables/useMediaManager.d.ts +17 -0
- package/dist/runtime/composables/useMediaManager.d.ts.map +1 -0
- package/dist/runtime/composables/useMediaManager.js +6 -0
- package/dist/runtime/composables/useProgressiveDisclosure.d.ts +44 -0
- package/dist/runtime/composables/useProgressiveDisclosure.d.ts.map +1 -0
- package/dist/runtime/composables/useProgressiveDisclosure.js +23 -0
- package/dist/runtime/composables/useUnsavedChangesGuard.d.ts +24 -0
- package/dist/runtime/composables/useUnsavedChangesGuard.d.ts.map +1 -0
- package/dist/runtime/composables/useUnsavedChangesGuard.js +28 -0
- package/dist/runtime/devtools/DevtoolsFormsPage.vue +235 -0
- package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts +4 -0
- package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts.map +1 -0
- package/dist/runtime/plugins/form-registry.d.ts +18 -0
- package/dist/runtime/plugins/form-registry.d.ts.map +1 -0
- package/dist/runtime/plugins/form-registry.js +19 -0
- package/dist/runtime/registry/defaults.d.ts +3 -0
- package/dist/runtime/registry/defaults.d.ts.map +1 -0
- package/dist/runtime/registry/defaults.js +93 -0
- package/dist/runtime/types/descriptor.d.ts +373 -0
- package/dist/runtime/types/descriptor.d.ts.map +1 -0
- package/dist/runtime/types/descriptor.js +26 -0
- package/dist/runtime/types/index.d.ts +12 -0
- package/dist/runtime/types/index.d.ts.map +1 -0
- package/dist/runtime/types/index.js +6 -0
- package/dist/runtime/types/layout.d.ts +21 -0
- package/dist/runtime/types/layout.d.ts.map +1 -0
- package/dist/runtime/types/layout.js +0 -0
- package/dist/runtime/types/media.d.ts +43 -0
- package/dist/runtime/types/media.d.ts.map +1 -0
- package/dist/runtime/types/media.js +0 -0
- package/dist/runtime/types/registry.d.ts +62 -0
- package/dist/runtime/types/registry.d.ts.map +1 -0
- package/dist/runtime/types/registry.js +19 -0
- package/dist/runtime/utils/flattenFieldGroups.d.ts +50 -0
- package/dist/runtime/utils/flattenFieldGroups.d.ts.map +1 -0
- package/dist/runtime/utils/flattenFieldGroups.js +24 -0
- package/dist/types.d.mts +6 -10
- package/package.json +10 -8
- package/src/__tests__/article.schema.json +193 -0
- package/src/__tests__/camomillaRoundTrip.test.ts +73 -0
- package/src/__tests__/useFormFromSchema.test.ts +24 -0
- package/src/__tests__/useMapoForm.test.ts +5 -0
- package/src/index.ts +5 -0
- package/src/module.ts +34 -0
- package/src/runtime/components/MapoFocusPortal.vue +3 -1
- package/src/runtime/components/MapoFormTabs.vue +4 -1
- package/src/runtime/components/fields/MapoDateField.vue +3 -1
- package/src/runtime/components/fields/MapoDateTimeField.vue +3 -1
- package/src/runtime/components/fields/MapoEnhancedMediaField.vue +153 -0
- package/src/runtime/components/fields/MapoFileField.vue +3 -1
- package/src/runtime/components/fields/MapoFksField.vue +7 -2
- package/src/runtime/components/fields/MapoMapField.vue +5 -2
- package/src/runtime/components/fields/MapoMediaField.vue +104 -0
- package/src/runtime/components/fields/MapoMediaM2mField.vue +130 -0
- package/src/runtime/components/fields/MapoMediaUnavailable.vue +19 -0
- package/src/runtime/components/fields/MapoRepeater.vue +33 -26
- package/src/runtime/components/fields/MapoRepeaterItem.vue +14 -8
- package/src/runtime/components/fields/MapoSeoPreview.vue +17 -13
- package/src/runtime/components/fields/MapoWygEditor.vue +47 -5
- package/src/runtime/composables/useFormFromSchema.ts +4 -1
- package/src/runtime/composables/useMapoForm.ts +4 -2
- package/src/runtime/composables/useMediaManager.ts +21 -0
- package/src/runtime/composables/useUnsavedChangesGuard.ts +3 -3
- package/src/runtime/registry/defaults.ts +9 -1
- package/src/runtime/types/descriptor.ts +22 -7
- package/src/runtime/types/index.ts +4 -0
- package/src/runtime/types/media.ts +44 -0
- package/dist/index.d.cts +0 -1
- package/dist/module.d.cts +0 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
4
|
+
import type {
|
|
5
|
+
EnhancedMediaDescriptor,
|
|
6
|
+
EnhancedMediaValue,
|
|
7
|
+
MediaItem,
|
|
8
|
+
} from "../../types/index.js";
|
|
9
|
+
import { useMediaManager } from "../../composables/useMediaManager.js";
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
modelValue: EnhancedMediaValue | null | undefined;
|
|
13
|
+
descriptor: EnhancedMediaDescriptor;
|
|
14
|
+
errors?: string[];
|
|
15
|
+
readonly?: boolean;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
"update:modelValue": [value: EnhancedMediaValue | null];
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
const { t } = useI18n();
|
|
24
|
+
|
|
25
|
+
const dialogOpen = ref(false);
|
|
26
|
+
|
|
27
|
+
// Picker dialog comes from @mapomodule/uikit, resolved at runtime.
|
|
28
|
+
const { MediaDialog, available } = useMediaManager();
|
|
29
|
+
|
|
30
|
+
const current = computed<MediaItem | null>(
|
|
31
|
+
() => props.modelValue?.media ?? null,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const altValue = computed({
|
|
35
|
+
get: () => props.modelValue?.alt ?? "",
|
|
36
|
+
set: (v: string) =>
|
|
37
|
+
emit("update:modelValue", {
|
|
38
|
+
...(props.modelValue ?? { media: null }),
|
|
39
|
+
alt: v,
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const captionValue = computed({
|
|
44
|
+
get: () => props.modelValue?.caption ?? "",
|
|
45
|
+
set: (v: string) =>
|
|
46
|
+
emit("update:modelValue", {
|
|
47
|
+
...(props.modelValue ?? { media: null }),
|
|
48
|
+
caption: v,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const mime = computed(() => props.descriptor.attrs?.mime as string | undefined);
|
|
53
|
+
|
|
54
|
+
function onConfirm(selection: MediaItem | MediaItem[]) {
|
|
55
|
+
const media = Array.isArray(selection) ? (selection[0] ?? null) : selection;
|
|
56
|
+
emit("update:modelValue", {
|
|
57
|
+
...(props.modelValue ?? {}),
|
|
58
|
+
media,
|
|
59
|
+
alt: props.modelValue?.alt ?? media?.alt_text ?? "",
|
|
60
|
+
caption: props.modelValue?.caption ?? "",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function clear() {
|
|
65
|
+
emit("update:modelValue", null);
|
|
66
|
+
}
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<div class="mapo-enhanced-media-field space-y-3">
|
|
71
|
+
<MapoMediaUnavailable v-if="!available" />
|
|
72
|
+
|
|
73
|
+
<template v-else>
|
|
74
|
+
<div class="flex gap-3">
|
|
75
|
+
<!-- Preview -->
|
|
76
|
+
<div class="shrink-0">
|
|
77
|
+
<div
|
|
78
|
+
class="flex size-24 cursor-pointer items-center justify-center overflow-hidden rounded-lg border-2 border-dashed transition-colors"
|
|
79
|
+
:class="
|
|
80
|
+
current
|
|
81
|
+
? 'border-default'
|
|
82
|
+
: 'border-accented hover:border-primary'
|
|
83
|
+
"
|
|
84
|
+
@click="!readonly && !disabled && (dialogOpen = true)"
|
|
85
|
+
>
|
|
86
|
+
<MapoMediaPreview v-if="current" :media="current" size="md" />
|
|
87
|
+
<div v-else class="flex flex-col items-center gap-1 text-dimmed">
|
|
88
|
+
<UIcon name="i-lucide-image-plus" class="size-7" />
|
|
89
|
+
<span class="text-[10px]">{{ t("mapo.select") }}</span>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- Metadata fields -->
|
|
95
|
+
<div class="flex-1 space-y-2">
|
|
96
|
+
<div v-if="current" class="flex items-start justify-between gap-2">
|
|
97
|
+
<p class="truncate text-xs text-muted">
|
|
98
|
+
{{ current.title || current.file?.split("/").pop() }}
|
|
99
|
+
</p>
|
|
100
|
+
<div class="flex gap-1">
|
|
101
|
+
<UButton
|
|
102
|
+
v-if="!readonly && !disabled"
|
|
103
|
+
icon="i-lucide-refresh-cw"
|
|
104
|
+
size="xs"
|
|
105
|
+
variant="ghost"
|
|
106
|
+
color="neutral"
|
|
107
|
+
:title="t('mapo.change')"
|
|
108
|
+
@click="dialogOpen = true"
|
|
109
|
+
/>
|
|
110
|
+
<UButton
|
|
111
|
+
v-if="!readonly && !disabled"
|
|
112
|
+
icon="i-lucide-x"
|
|
113
|
+
size="xs"
|
|
114
|
+
variant="ghost"
|
|
115
|
+
color="neutral"
|
|
116
|
+
:title="t('mapo.remove')"
|
|
117
|
+
@click="clear"
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<UInput
|
|
123
|
+
v-model="altValue"
|
|
124
|
+
size="sm"
|
|
125
|
+
:placeholder="t('mapo.enhancedMediaField.altText')"
|
|
126
|
+
:disabled="readonly || disabled || !current"
|
|
127
|
+
/>
|
|
128
|
+
<UInput
|
|
129
|
+
v-model="captionValue"
|
|
130
|
+
size="sm"
|
|
131
|
+
:placeholder="t('mapo.enhancedMediaField.caption')"
|
|
132
|
+
:disabled="readonly || disabled || !current"
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<!-- Dialog -->
|
|
138
|
+
<component
|
|
139
|
+
:is="MediaDialog"
|
|
140
|
+
v-model="dialogOpen"
|
|
141
|
+
selection-mode="single"
|
|
142
|
+
:mime="mime"
|
|
143
|
+
:selected="current"
|
|
144
|
+
@confirm="onConfirm"
|
|
145
|
+
/>
|
|
146
|
+
</template>
|
|
147
|
+
|
|
148
|
+
<!-- Errors -->
|
|
149
|
+
<p v-for="err in errors" :key="err" class="text-xs text-error" role="alert">
|
|
150
|
+
{{ err }}
|
|
151
|
+
</p>
|
|
152
|
+
</div>
|
|
153
|
+
</template>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
3
4
|
import type { FileDescriptor } from "../../types/index.js";
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
@@ -12,6 +13,7 @@ const props = defineProps<{
|
|
|
12
13
|
|
|
13
14
|
const emit = defineEmits<{ "update:modelValue": [value: File | null] }>();
|
|
14
15
|
|
|
16
|
+
const { t } = useI18n();
|
|
15
17
|
const inputRef = ref<HTMLInputElement | null>(null);
|
|
16
18
|
|
|
17
19
|
const currentFileName = computed<string | null>(() => {
|
|
@@ -78,7 +80,7 @@ const accept = computed(
|
|
|
78
80
|
variant="ghost"
|
|
79
81
|
color="neutral"
|
|
80
82
|
icon="i-lucide-x"
|
|
81
|
-
aria-label="
|
|
83
|
+
:aria-label="t('mapo.fileField.removeFile')"
|
|
82
84
|
@click="clear"
|
|
83
85
|
/>
|
|
84
86
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed, watch, onMounted } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
3
4
|
import { useNuxtApp } from "#app";
|
|
4
5
|
import { debounce } from "@mapomodule/utils";
|
|
5
6
|
import type { FksDescriptor } from "../../types/index.js";
|
|
@@ -14,6 +15,8 @@ const props = defineProps<{
|
|
|
14
15
|
|
|
15
16
|
const emit = defineEmits<{ "update:modelValue": [value: unknown] }>();
|
|
16
17
|
|
|
18
|
+
const { t } = useI18n();
|
|
19
|
+
|
|
17
20
|
const attrs = computed(() => props.descriptor.attrs);
|
|
18
21
|
const isMultiple = computed(
|
|
19
22
|
() => props.descriptor.type === "m2m" || !!attrs.value.multiple,
|
|
@@ -70,7 +73,7 @@ const internalValue = computed({
|
|
|
70
73
|
|
|
71
74
|
// Placeholder
|
|
72
75
|
const placeholder = computed(() =>
|
|
73
|
-
isMultiple.value ? "
|
|
76
|
+
isMultiple.value ? t("mapo.fksField.searchSelect") : t("mapo.search"),
|
|
74
77
|
);
|
|
75
78
|
|
|
76
79
|
// Chip text shown in the trigger.
|
|
@@ -144,7 +147,9 @@ function getItemKey(item: unknown): unknown {
|
|
|
144
147
|
<!-- Empty state -->
|
|
145
148
|
<template #empty>
|
|
146
149
|
<span class="text-sm text-gray-500">
|
|
147
|
-
{{
|
|
150
|
+
{{
|
|
151
|
+
searchTerm ? t("mapo.fksField.noResults") : t("mapo.fksField.noItems")
|
|
152
|
+
}}
|
|
148
153
|
</span>
|
|
149
154
|
</template>
|
|
150
155
|
</USelectMenu>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed, defineAsyncComponent } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
3
4
|
import type { MapDescriptor } from "../../types/index.js";
|
|
4
5
|
|
|
5
6
|
// Leaflet is loaded only on the client because it is not SSR-safe.
|
|
@@ -7,6 +8,8 @@ const LeafletMap = defineAsyncComponent(
|
|
|
7
8
|
() => import("./MapoMapFieldClient.vue"),
|
|
8
9
|
);
|
|
9
10
|
|
|
11
|
+
const { t } = useI18n();
|
|
12
|
+
|
|
10
13
|
/** Geographic coordinates stored by the map field. */
|
|
11
14
|
interface LatLng {
|
|
12
15
|
lat: number;
|
|
@@ -63,7 +66,7 @@ function clear() {
|
|
|
63
66
|
<div class="flex gap-2">
|
|
64
67
|
<UInput
|
|
65
68
|
v-model="latInput"
|
|
66
|
-
placeholder="
|
|
69
|
+
:placeholder="t('mapo.latitude')"
|
|
67
70
|
class="flex-1"
|
|
68
71
|
:readonly="readonly"
|
|
69
72
|
:disabled="disabled"
|
|
@@ -71,7 +74,7 @@ function clear() {
|
|
|
71
74
|
/>
|
|
72
75
|
<UInput
|
|
73
76
|
v-model="lngInput"
|
|
74
|
-
placeholder="
|
|
77
|
+
:placeholder="t('mapo.longitude')"
|
|
75
78
|
class="flex-1"
|
|
76
79
|
:readonly="readonly"
|
|
77
80
|
:disabled="disabled"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
4
|
+
import type { MediaDescriptor, MediaItem } from "../../types/index.js";
|
|
5
|
+
import { useMediaManager } from "../../composables/useMediaManager.js";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
modelValue: MediaItem | number | null | undefined;
|
|
9
|
+
descriptor: MediaDescriptor;
|
|
10
|
+
errors?: string[];
|
|
11
|
+
readonly?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}>();
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits<{
|
|
16
|
+
"update:modelValue": [value: MediaItem | null];
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
const { t } = useI18n();
|
|
20
|
+
|
|
21
|
+
const dialogOpen = ref(false);
|
|
22
|
+
|
|
23
|
+
// Picker dialog comes from @mapomodule/uikit, resolved at runtime.
|
|
24
|
+
const { MediaDialog, available } = useMediaManager();
|
|
25
|
+
|
|
26
|
+
const current = computed<MediaItem | null>(() => {
|
|
27
|
+
if (!props.modelValue) return null;
|
|
28
|
+
if (typeof props.modelValue === "object") return props.modelValue;
|
|
29
|
+
return null;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const mime = computed(() => props.descriptor.attrs?.mime as string | undefined);
|
|
33
|
+
|
|
34
|
+
function onConfirm(selection: MediaItem | MediaItem[]) {
|
|
35
|
+
emit(
|
|
36
|
+
"update:modelValue",
|
|
37
|
+
Array.isArray(selection) ? (selection[0] ?? null) : selection,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function clear() {
|
|
42
|
+
emit("update:modelValue", null);
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<div class="mapo-media-field space-y-2">
|
|
48
|
+
<MapoMediaUnavailable v-if="!available" />
|
|
49
|
+
|
|
50
|
+
<template v-else>
|
|
51
|
+
<!-- Selected media preview -->
|
|
52
|
+
<div
|
|
53
|
+
v-if="current"
|
|
54
|
+
class="flex items-center gap-3 rounded-lg border border-default p-2"
|
|
55
|
+
>
|
|
56
|
+
<MapoMediaPreview :media="current" size="sm" />
|
|
57
|
+
<div class="flex-1 min-w-0">
|
|
58
|
+
<p class="truncate text-xs font-medium">
|
|
59
|
+
{{ current.title || current.file?.split("/").pop() }}
|
|
60
|
+
</p>
|
|
61
|
+
<p class="text-xs text-dimmed">
|
|
62
|
+
{{ current.mime_type }}
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
<UButton
|
|
66
|
+
v-if="!readonly && !disabled"
|
|
67
|
+
icon="i-lucide-x"
|
|
68
|
+
size="xs"
|
|
69
|
+
variant="ghost"
|
|
70
|
+
color="neutral"
|
|
71
|
+
@click="clear"
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<!-- Trigger button -->
|
|
76
|
+
<UButton
|
|
77
|
+
v-if="!readonly && !disabled"
|
|
78
|
+
:icon="current ? 'i-lucide-refresh-cw' : 'i-lucide-image-plus'"
|
|
79
|
+
size="sm"
|
|
80
|
+
variant="soft"
|
|
81
|
+
color="neutral"
|
|
82
|
+
:block="!current"
|
|
83
|
+
@click="dialogOpen = true"
|
|
84
|
+
>
|
|
85
|
+
{{ current ? t("mapo.change") : t("mapo.mediaField.selectMedia") }}
|
|
86
|
+
</UButton>
|
|
87
|
+
|
|
88
|
+
<!-- Dialog -->
|
|
89
|
+
<component
|
|
90
|
+
:is="MediaDialog"
|
|
91
|
+
v-model="dialogOpen"
|
|
92
|
+
selection-mode="single"
|
|
93
|
+
:mime="mime"
|
|
94
|
+
:selected="current"
|
|
95
|
+
@confirm="onConfirm"
|
|
96
|
+
/>
|
|
97
|
+
</template>
|
|
98
|
+
|
|
99
|
+
<!-- Errors -->
|
|
100
|
+
<p v-for="err in errors" :key="err" class="text-xs text-error" role="alert">
|
|
101
|
+
{{ err }}
|
|
102
|
+
</p>
|
|
103
|
+
</div>
|
|
104
|
+
</template>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
4
|
+
import type { MediaM2mDescriptor, MediaItem } from "../../types/index.js";
|
|
5
|
+
import { useMediaManager } from "../../composables/useMediaManager.js";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
modelValue: MediaItem[] | number[] | null | undefined;
|
|
9
|
+
descriptor: MediaM2mDescriptor;
|
|
10
|
+
errors?: string[];
|
|
11
|
+
readonly?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}>();
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits<{
|
|
16
|
+
"update:modelValue": [value: MediaItem[]];
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
const { t } = useI18n();
|
|
20
|
+
|
|
21
|
+
const dialogOpen = ref(false);
|
|
22
|
+
|
|
23
|
+
// Picker dialog comes from @mapomodule/uikit, resolved at runtime.
|
|
24
|
+
const { MediaDialog, available } = useMediaManager();
|
|
25
|
+
|
|
26
|
+
const current = computed<MediaItem[]>(() => {
|
|
27
|
+
if (!props.modelValue) return [];
|
|
28
|
+
return props.modelValue.filter((v): v is MediaItem => typeof v === "object");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const mime = computed(() => props.descriptor.attrs?.mime as string | undefined);
|
|
32
|
+
|
|
33
|
+
function onConfirm(selection: MediaItem | MediaItem[]) {
|
|
34
|
+
emit("update:modelValue", Array.isArray(selection) ? selection : [selection]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function removeItem(id: number) {
|
|
38
|
+
emit(
|
|
39
|
+
"update:modelValue",
|
|
40
|
+
current.value.filter((m) => m.id !== id),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// HTML5 drag reorder — order matters for m2m relations (legacy parity).
|
|
45
|
+
const dragIndex = ref<number | null>(null);
|
|
46
|
+
|
|
47
|
+
function onDragStart(index: number) {
|
|
48
|
+
dragIndex.value = index;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function onDragOverItem(index: number) {
|
|
52
|
+
if (dragIndex.value === null || dragIndex.value === index) return;
|
|
53
|
+
const items = [...current.value];
|
|
54
|
+
const [moved] = items.splice(dragIndex.value, 1);
|
|
55
|
+
if (!moved) return;
|
|
56
|
+
items.splice(index, 0, moved);
|
|
57
|
+
dragIndex.value = index;
|
|
58
|
+
emit("update:modelValue", items);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function onDragEnd() {
|
|
62
|
+
dragIndex.value = null;
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<template>
|
|
67
|
+
<div class="mapo-media-m2m-field space-y-2">
|
|
68
|
+
<MapoMediaUnavailable v-if="!available" />
|
|
69
|
+
|
|
70
|
+
<template v-else>
|
|
71
|
+
<!-- Selected items grid -->
|
|
72
|
+
<div v-if="current.length > 0" class="flex flex-wrap gap-2">
|
|
73
|
+
<div
|
|
74
|
+
v-for="(media, i) in current"
|
|
75
|
+
:key="media.id"
|
|
76
|
+
class="group relative"
|
|
77
|
+
:class="{
|
|
78
|
+
'opacity-50': dragIndex === i,
|
|
79
|
+
'cursor-grab': !readonly && !disabled,
|
|
80
|
+
}"
|
|
81
|
+
:draggable="!readonly && !disabled"
|
|
82
|
+
@dragstart="onDragStart(i)"
|
|
83
|
+
@dragover.prevent="onDragOverItem(i)"
|
|
84
|
+
@dragend="onDragEnd"
|
|
85
|
+
>
|
|
86
|
+
<MapoMediaPreview :media="media" size="sm" />
|
|
87
|
+
<button
|
|
88
|
+
v-if="!readonly && !disabled"
|
|
89
|
+
class="absolute -right-1.5 -top-1.5 hidden size-4 items-center justify-center rounded-full bg-error text-inverted group-hover:flex"
|
|
90
|
+
type="button"
|
|
91
|
+
@click="removeItem(media.id)"
|
|
92
|
+
>
|
|
93
|
+
<UIcon name="i-lucide-x" class="size-2.5" />
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<!-- Add button -->
|
|
99
|
+
<UButton
|
|
100
|
+
v-if="!readonly && !disabled"
|
|
101
|
+
icon="i-lucide-plus"
|
|
102
|
+
size="sm"
|
|
103
|
+
variant="soft"
|
|
104
|
+
color="neutral"
|
|
105
|
+
@click="dialogOpen = true"
|
|
106
|
+
>
|
|
107
|
+
{{
|
|
108
|
+
current.length > 0
|
|
109
|
+
? t("mapo.mediaM2MField.addMedia")
|
|
110
|
+
: t("mapo.mediaField.selectMedia")
|
|
111
|
+
}}
|
|
112
|
+
</UButton>
|
|
113
|
+
|
|
114
|
+
<!-- Dialog -->
|
|
115
|
+
<component
|
|
116
|
+
:is="MediaDialog"
|
|
117
|
+
v-model="dialogOpen"
|
|
118
|
+
selection-mode="multi"
|
|
119
|
+
:mime="mime"
|
|
120
|
+
:selected="current"
|
|
121
|
+
@confirm="onConfirm"
|
|
122
|
+
/>
|
|
123
|
+
</template>
|
|
124
|
+
|
|
125
|
+
<!-- Errors -->
|
|
126
|
+
<p v-for="err in errors" :key="err" class="text-xs text-error" role="alert">
|
|
127
|
+
{{ err }}
|
|
128
|
+
</p>
|
|
129
|
+
</div>
|
|
130
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Placeholder rendered by the media fields when the Media Manager is not
|
|
4
|
+
* registered — i.e. `@mapomodule/form` is installed without `@mapomodule/uikit`.
|
|
5
|
+
* Better than a silently broken picker: it names the missing package.
|
|
6
|
+
*/
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<div
|
|
11
|
+
class="flex items-center gap-2 rounded-lg border border-dashed border-default px-3 py-2 text-xs text-muted"
|
|
12
|
+
>
|
|
13
|
+
<UIcon
|
|
14
|
+
name="i-lucide-triangle-alert"
|
|
15
|
+
class="size-4 shrink-0 text-warning"
|
|
16
|
+
/>
|
|
17
|
+
<span>Media fields require <code>@mapomodule/uikit</code>.</span>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed, watchEffect } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
3
4
|
import { VueDraggable } from "vue-draggable-plus";
|
|
4
5
|
import { useConfirmStore } from "@mapomodule/store/runtime/stores/confirm";
|
|
5
6
|
import type { RepeaterDescriptor } from "../../types/index.js";
|
|
@@ -17,6 +18,7 @@ const props = defineProps<{
|
|
|
17
18
|
const emit = defineEmits<{ "update:modelValue": [value: unknown[]] }>();
|
|
18
19
|
|
|
19
20
|
const form = injectMapoForm()!;
|
|
21
|
+
const { t } = useI18n();
|
|
20
22
|
|
|
21
23
|
const attrs = computed(() => props.descriptor.attrs ?? {});
|
|
22
24
|
|
|
@@ -108,10 +110,8 @@ function minBound(): number {
|
|
|
108
110
|
return attrs.value.minItems ?? 0;
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
async function askConfirm(
|
|
112
|
-
|
|
113
|
-
title = "Confirm",
|
|
114
|
-
): Promise<boolean> {
|
|
113
|
+
async function askConfirm(message: string, title?: string): Promise<boolean> {
|
|
114
|
+
title ??= t("mapo.confirm");
|
|
115
115
|
try {
|
|
116
116
|
// Falls back to native confirm when the Pinia store is not registered.
|
|
117
117
|
return await useConfirmStore().ask({ title, message });
|
|
@@ -135,8 +135,8 @@ async function deleteItem(index: number) {
|
|
|
135
135
|
if (!canDelete.value) return;
|
|
136
136
|
if (attrs.value.confirmDelete !== false) {
|
|
137
137
|
const confirmed = await askConfirm(
|
|
138
|
-
"
|
|
139
|
-
"
|
|
138
|
+
t("mapo.confirmDelete"),
|
|
139
|
+
t("mapo.repeater.deleteItem"),
|
|
140
140
|
);
|
|
141
141
|
if (!confirmed) return;
|
|
142
142
|
}
|
|
@@ -275,14 +275,14 @@ async function bulkDelete() {
|
|
|
275
275
|
// Enforce minItems: would deletion drop below the lower bound?
|
|
276
276
|
if (items.value.length - count < minBound()) {
|
|
277
277
|
await askConfirm(
|
|
278
|
-
|
|
279
|
-
"
|
|
278
|
+
t("mapo.repeater.minItemsBlock", { min: minBound() }),
|
|
279
|
+
t("mapo.repeater.actionBlocked"),
|
|
280
280
|
);
|
|
281
281
|
return;
|
|
282
282
|
}
|
|
283
283
|
const confirmed = await askConfirm(
|
|
284
|
-
|
|
285
|
-
"
|
|
284
|
+
t("mapo.repeater.confirmBulkDelete", { count }),
|
|
285
|
+
t("mapo.repeater.deleteItems"),
|
|
286
286
|
);
|
|
287
287
|
if (!confirmed) return;
|
|
288
288
|
pushUndo();
|
|
@@ -336,7 +336,7 @@ const errorPrefix = (index: number) =>
|
|
|
336
336
|
class="flex flex-wrap items-center gap-2 rounded border border-gray-200 bg-gray-50 px-3 py-2"
|
|
337
337
|
>
|
|
338
338
|
<span class="text-sm font-medium text-gray-700">
|
|
339
|
-
{{
|
|
339
|
+
{{ t("mapo.repeater.nItems", { n: items.length }, items.length) }}
|
|
340
340
|
<span v-if="attrs.maxItems" class="text-gray-400"
|
|
341
341
|
>/ {{ attrs.maxItems }}</span
|
|
342
342
|
>
|
|
@@ -344,10 +344,10 @@ const errorPrefix = (index: number) =>
|
|
|
344
344
|
|
|
345
345
|
<div class="flex flex-1 flex-wrap items-center justify-end gap-2">
|
|
346
346
|
<UButton size="xs" variant="ghost" color="neutral" @click="collapseAll">
|
|
347
|
-
|
|
347
|
+
{{ t("mapo.repeater.collapseAll") }}
|
|
348
348
|
</UButton>
|
|
349
349
|
<UButton size="xs" variant="ghost" color="neutral" @click="expandAll">
|
|
350
|
-
|
|
350
|
+
{{ t("mapo.repeater.expandAll") }}
|
|
351
351
|
</UButton>
|
|
352
352
|
<UButton
|
|
353
353
|
v-if="undoStack.length"
|
|
@@ -355,7 +355,7 @@ const errorPrefix = (index: number) =>
|
|
|
355
355
|
variant="ghost"
|
|
356
356
|
color="neutral"
|
|
357
357
|
icon="i-lucide-undo-2"
|
|
358
|
-
title="
|
|
358
|
+
:title="t('mapo.repeater.undoLastAction')"
|
|
359
359
|
@click="undo"
|
|
360
360
|
/>
|
|
361
361
|
|
|
@@ -365,8 +365,10 @@ const errorPrefix = (index: number) =>
|
|
|
365
365
|
:variant="selectionMode ? 'solid' : 'ghost'"
|
|
366
366
|
color="neutral"
|
|
367
367
|
icon="i-lucide-check-square"
|
|
368
|
-
title="
|
|
369
|
-
:label="
|
|
368
|
+
:title="t('mapo.repeater.bulkSelection')"
|
|
369
|
+
:label="
|
|
370
|
+
selectionMode ? t('mapo.repeater.exitSelection') : t('mapo.select')
|
|
371
|
+
"
|
|
370
372
|
@click="toggleSelectionMode"
|
|
371
373
|
/>
|
|
372
374
|
|
|
@@ -386,7 +388,7 @@ const errorPrefix = (index: number) =>
|
|
|
386
388
|
:disabled="!canAdd"
|
|
387
389
|
@click="addItem"
|
|
388
390
|
>
|
|
389
|
-
|
|
391
|
+
{{ t("mapo.add") }}
|
|
390
392
|
</UButton>
|
|
391
393
|
</div>
|
|
392
394
|
</div>
|
|
@@ -398,11 +400,11 @@ const errorPrefix = (index: number) =>
|
|
|
398
400
|
class="flex items-center gap-2 rounded border border-primary-200 bg-primary-50 px-3 py-2"
|
|
399
401
|
>
|
|
400
402
|
<span class="text-sm font-medium text-primary-700">
|
|
401
|
-
{{ selectionCount }}
|
|
403
|
+
{{ t("mapo.repeater.nSelected", { n: selectionCount }) }}
|
|
402
404
|
</span>
|
|
403
405
|
<div class="flex flex-1 flex-wrap items-center gap-2">
|
|
404
406
|
<UButton size="xs" variant="ghost" color="neutral" @click="selectAll">
|
|
405
|
-
|
|
407
|
+
{{ t("mapo.repeater.all") }}
|
|
406
408
|
</UButton>
|
|
407
409
|
<UButton
|
|
408
410
|
size="xs"
|
|
@@ -410,7 +412,7 @@ const errorPrefix = (index: number) =>
|
|
|
410
412
|
color="neutral"
|
|
411
413
|
@click="clearSelection"
|
|
412
414
|
>
|
|
413
|
-
|
|
415
|
+
{{ t("mapo.clear") }}
|
|
414
416
|
</UButton>
|
|
415
417
|
<UButton
|
|
416
418
|
size="xs"
|
|
@@ -420,12 +422,17 @@ const errorPrefix = (index: number) =>
|
|
|
420
422
|
:disabled="remainingSlots() <= 0"
|
|
421
423
|
:title="
|
|
422
424
|
remainingSlots() < selectionCount
|
|
423
|
-
?
|
|
424
|
-
|
|
425
|
+
? t('mapo.repeater.duplicateLimited', {
|
|
426
|
+
slots: remainingSlots(),
|
|
427
|
+
count: selectionCount,
|
|
428
|
+
})
|
|
429
|
+
: t('mapo.repeater.duplicateSelected')
|
|
425
430
|
"
|
|
426
431
|
@click="bulkDuplicate"
|
|
427
432
|
>
|
|
428
|
-
|
|
433
|
+
{{ t("mapo.duplicate") }} ({{
|
|
434
|
+
Math.min(selectionCount, remainingSlots())
|
|
435
|
+
}})
|
|
429
436
|
</UButton>
|
|
430
437
|
<UButton
|
|
431
438
|
size="xs"
|
|
@@ -434,7 +441,7 @@ const errorPrefix = (index: number) =>
|
|
|
434
441
|
icon="i-lucide-arrow-up"
|
|
435
442
|
@click="bulkMoveToTop"
|
|
436
443
|
>
|
|
437
|
-
|
|
444
|
+
{{ t("mapo.repeater.moveToTop") }}
|
|
438
445
|
</UButton>
|
|
439
446
|
<UButton
|
|
440
447
|
size="xs"
|
|
@@ -444,7 +451,7 @@ const errorPrefix = (index: number) =>
|
|
|
444
451
|
:disabled="!canDelete"
|
|
445
452
|
@click="bulkDelete"
|
|
446
453
|
>
|
|
447
|
-
|
|
454
|
+
{{ t("mapo.delete") }} ({{ selectionCount }})
|
|
448
455
|
</UButton>
|
|
449
456
|
</div>
|
|
450
457
|
</div>
|
|
@@ -527,7 +534,7 @@ const errorPrefix = (index: number) =>
|
|
|
527
534
|
v-if="items.length === 0"
|
|
528
535
|
class="rounded border border-dashed border-gray-300 py-8 text-center text-sm text-gray-400"
|
|
529
536
|
>
|
|
530
|
-
|
|
537
|
+
{{ t("mapo.repeater.empty") }}
|
|
531
538
|
</div>
|
|
532
539
|
</div>
|
|
533
540
|
</template>
|