@raclettejs/workbench 0.1.18 → 0.1.19

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.
@@ -1,62 +1,142 @@
1
1
  <template>
2
- <BaseDataTable
3
- :items="parsedTags"
4
- :loading="tagsLoading"
5
- :headers="headers"
6
- create-interaction-link-id="tagCreateInteractionLink"
7
- edit-interaction-link-id="tagEditInteractionLink"
8
- :data-name="$t('workbench.tagList.dataName')"
9
- :data-article="$t('workbench.tagList.dataArticle')"
10
- @delete="deleteTag"
11
- :itemsDeleted="showDeleted"
12
- >
13
- <template #actions>
14
- <v-tooltip
15
- :text="showDeleted ? $t('core.hideDeleted') : $t('core.showDeleted')"
16
- >
17
- <template v-slot:activator="{ props }">
18
- <v-btn-toggle
19
- density="compact"
20
- v-bind="props"
21
- v-model="showDeleted"
22
- mandatory
23
- :color="showDeleted ? 'error' : 'success'"
24
- >
25
- <v-btn :value="true">
26
- <v-icon>mdi-table-eye-off</v-icon>
27
- </v-btn>
28
- <v-btn :value="false"><v-icon>mdi-table-eye</v-icon></v-btn>
29
- </v-btn-toggle>
30
- </template>
31
- </v-tooltip>
32
- </template>
33
- <template #prepend-row-actions="{ item }">
34
- <v-icon
35
- v-if="showDeleted"
36
- color="green"
37
- class="mr-2"
38
- @click="restore(item._id)"
39
- :title="
40
- $t('workbench.baseDataTable.restoreDataTitle', {
41
- dataName: item.title,
42
- })
43
- "
44
- icon="mdi-delete-restore"
45
- />
46
- </template>
47
- </BaseDataTable>
2
+ <div>
3
+ <BaseDataTable
4
+ :items="parsedTags"
5
+ :loading="tagsLoading"
6
+ :headers="headers"
7
+ create-interaction-link-id="tagCreateInteractionLink"
8
+ edit-interaction-link-id="tagEditInteractionLink"
9
+ :data-name="$t('workbench.tagList.dataName')"
10
+ :data-article="$t('workbench.tagList.dataArticle')"
11
+ @delete="deleteTag"
12
+ :itemsDeleted="showDeleted"
13
+ >
14
+ <template #actions>
15
+ <v-tooltip
16
+ :text="showDeleted ? $t('core.hideDeleted') : $t('core.showDeleted')"
17
+ >
18
+ <template v-slot:activator="{ props }">
19
+ <v-btn-toggle
20
+ density="compact"
21
+ v-bind="props"
22
+ v-model="showDeleted"
23
+ mandatory
24
+ :color="showDeleted ? 'error' : 'success'"
25
+ >
26
+ <v-btn :value="true">
27
+ <v-icon>mdi-table-eye-off</v-icon>
28
+ </v-btn>
29
+ <v-btn :value="false"><v-icon>mdi-table-eye</v-icon></v-btn>
30
+ </v-btn-toggle>
31
+ </template>
32
+ </v-tooltip>
33
+ <v-btn-group density="compact">
34
+ <v-tooltip :text="$t('core.export')">
35
+ <template v-slot:activator="{ props }">
36
+ <v-btn color="info" v-bind="props" flat @click="openExportDialog">
37
+ <v-icon>mdi-database-export</v-icon>
38
+ </v-btn>
39
+ </template>
40
+ </v-tooltip>
41
+ <v-tooltip :text="$t('core.import')">
42
+ <template v-slot:activator="{ props }">
43
+ <v-btn v-bind="props" color="warning">
44
+ <FileUpload
45
+ ref="uploadRef"
46
+ :onFileLoaded="handleFileUpload"
47
+ accept=".json"
48
+ readAs="text"
49
+ /></v-btn>
50
+ </template>
51
+ </v-tooltip>
52
+ </v-btn-group>
53
+ </template>
54
+ <template #prepend-row-actions="{ item }">
55
+ <v-icon
56
+ v-if="showDeleted"
57
+ color="green"
58
+ class="mr-2"
59
+ @click="restore(item._id)"
60
+ :title="
61
+ $t('workbench.baseDataTable.restoreDataTitle', {
62
+ dataName: item.title,
63
+ })
64
+ "
65
+ icon="mdi-delete-restore"
66
+ />
67
+ </template>
68
+ </BaseDataTable>
69
+ <!-- Export Selection Dialog -->
70
+ <SelectionDialog
71
+ v-model="showExportDialog"
72
+ :items="exportableItems"
73
+ display-attribute="title"
74
+ :title="$t('core.selectItemsToExport')"
75
+ :confirm-text="$t('core.export')"
76
+ @confirm="handleExportConfirm"
77
+ />
78
+
79
+ <!-- Import Selection Dialog -->
80
+ <SelectionDialog
81
+ v-model="showImportDialog"
82
+ :items="importableItems"
83
+ display-attribute="title"
84
+ :title="$t('core.selectItemsToImport')"
85
+ :confirm-text="$t('core.import')"
86
+ @confirm="handleImportConfirm"
87
+ />
88
+
89
+ <!-- Export Summary Dialog -->
90
+ <SummaryDialog
91
+ v-model="showExportSummary"
92
+ display-attribute="title"
93
+ :items="exportedItems"
94
+ :title="$t('core.exportSummary')"
95
+ :message="$t('core.exportSuccessMessage')"
96
+ type="success"
97
+ />
98
+
99
+ <!-- Import Summary Dialog -->
100
+ <SummaryDialog
101
+ v-model="showImportSummary"
102
+ display-attribute="title"
103
+ :items="importedItems"
104
+ :title="$t('core.importSummary')"
105
+ :message="$t('core.importSuccessMessage')"
106
+ type="success"
107
+ :show-confirm="true"
108
+ :confirm-text="$t('core.saveImported')"
109
+ @confirm="finalizeImport"
110
+ />
111
+ </div>
48
112
  </template>
49
113
 
50
114
  <script setup lang="ts">
51
115
  import { usePluginApi } from "@raclettejs/core/orchestrator/composables"
52
- import { computed, ref, watch } from "vue"
116
+ import { computed, ref, watch, useTemplateRef } from "vue"
53
117
  import type { Tag } from "@raclettejs/core"
54
118
  import { useI18n } from "vue-i18n"
55
119
  import BaseDataTable from "@app/components/BaseDataTable.vue"
56
120
  import { formatDistance } from "date-fns"
121
+ import { downloadJson, mapJsonValues } from "@app/lib/jsonTools"
122
+ import FileUpload from "@app/components/FileUpload.vue"
123
+ import SelectionDialog from "@app/components/SelectionDialog.vue"
124
+ import SummaryDialog from "@app/components/SummaryDialog.vue"
57
125
  const showDeleted = ref(false)
58
126
  const { t } = useI18n()
127
+ // Dialog states
128
+ const showExportDialog = ref(false)
129
+ const showImportDialog = ref(false)
130
+ const showExportSummary = ref(false)
131
+ const showImportSummary = ref(false)
59
132
 
133
+ // Data for dialogs
134
+ const exportableItems = ref<any[]>([])
135
+ const importableItems = ref<any[]>([])
136
+ const exportedItems = ref<any[]>([])
137
+ const importedItems = ref<any[]>([])
138
+ const pendingImportData = ref<any>(null)
139
+ const uploadRef = useTemplateRef("uploadRef")
60
140
  const headers = computed<{ title: string; key: keyof Tag }[]>(() => [
61
141
  {
62
142
  title: t("workbench.tagList.tableHeaders.title"),
@@ -80,7 +160,7 @@ const props = defineProps<{
80
160
  uuid: string
81
161
  }>()
82
162
 
83
- const { $data, $socket } = usePluginApi("raclette__core")
163
+ const { $data, $socket, $user } = usePluginApi("raclette__core")
84
164
 
85
165
  const {
86
166
  data: tags,
@@ -94,6 +174,9 @@ const {
94
174
  const { execute: executeDelete } = $data.tag.delete({
95
175
  options: { cb: () => execute({ isDeleted: showDeleted.value }) },
96
176
  })
177
+ const { execute: executeBulkCreate } = await $data.tag.createBulk({
178
+ options: { notify: false, cb: () => execute() },
179
+ })
97
180
  const { execute: executeHardDelete } = await $data.tag.deleteHard({
98
181
  options: { cb: () => execute({ isDeleted: showDeleted.value }) },
99
182
  })
@@ -118,6 +201,73 @@ const deleteTag = async (tag: Tag) => {
118
201
  await executeDelete({ _id: tag._id })
119
202
  }
120
203
  }
204
+ const openExportDialog = () => {
205
+ const allTags = Object.values(tags.value || {})
206
+ const exportableTags = []
207
+ allTags.forEach((tag) => {
208
+ if (!tag.locked) {
209
+ exportableTags.push(tag)
210
+ }
211
+ })
212
+ exportableItems.value = exportableTags
213
+ showExportDialog.value = true
214
+ }
215
+
216
+ const handleExportConfirm = (selectedIds: string[]) => {
217
+ const dataToExport = selectedIds.reduce((acc, id) => {
218
+ if (tags.value?.[id]) {
219
+ acc[id] = tags.value[id]
220
+ }
221
+ return acc
222
+ }, {} as any)
223
+
224
+ exportedItems.value = Object.values(dataToExport)
225
+ downloadJson(dataToExport, "tags")
226
+ showExportSummary.value = true
227
+ }
228
+
229
+ // Import functions
230
+ const handleFileUpload = (content: string, file: File) => {
231
+ try {
232
+ const parsedData = JSON.parse(content)
233
+ const itemsArray = Object.values(parsedData)
234
+
235
+ importableItems.value = itemsArray
236
+ pendingImportData.value = parsedData
237
+ showImportDialog.value = true
238
+ } catch (error) {
239
+ console.error("Failed to parse JSON:", error)
240
+ }
241
+ }
242
+
243
+ const handleImportConfirm = (selectedIds: string[]) => {
244
+ const selectedItems = selectedIds.map((id) => pendingImportData.value[id])
245
+ importedItems.value = selectedItems
246
+ showImportSummary.value = true
247
+ }
248
+
249
+ const finalizeImport = async () => {
250
+ const mapping = {
251
+ owner: $user.value._id,
252
+ lastEditor: $user.value._id,
253
+ createdBy: $user.value._id,
254
+ }
255
+
256
+ const dataToImport = importedItems.value.reduce((acc, item) => {
257
+ const mappedItem = mapJsonValues(item, mapping)
258
+ acc[item._id] = mappedItem
259
+ return acc
260
+ }, {} as any)
261
+
262
+ await executeBulkCreate(Object.values(dataToImport))
263
+ // TODO: Add your actual import logic here
264
+ // e.g., call an API endpoint to save the imported compositions
265
+
266
+ // Reset states
267
+ pendingImportData.value = null
268
+ importableItems.value = []
269
+ importedItems.value = []
270
+ }
121
271
  const restore = async (_id: string) => {
122
272
  executeRestore({ _id })
123
273
  }
@@ -0,0 +1,98 @@
1
+ <template>
2
+ <v-file-input
3
+ v-model="files"
4
+ :accept="accept"
5
+ ref="fileInput"
6
+ hide-input
7
+ flat
8
+ prepend-icon="mdi-database-import"
9
+ @update:model-value="handleFileChange"
10
+ >
11
+ </v-file-input>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { ref, useTemplateRef } from "vue"
16
+ const fileInput = useTemplateRef("fileInput")
17
+ const props = defineProps({
18
+ /**
19
+ * Function to call when file content is loaded.
20
+ * Receives: (content: string | ArrayBuffer, file: File)
21
+ */
22
+ onFileLoaded: {
23
+ type: Function,
24
+ required: true,
25
+ },
26
+
27
+ /**
28
+ * Optional: accept only certain file types
29
+ */
30
+ accept: {
31
+ type: String,
32
+ default: "*/*",
33
+ },
34
+
35
+ /**
36
+ * How to read the file: "text" or "arrayBuffer"
37
+ */
38
+ readAs: {
39
+ type: String,
40
+ default: "text",
41
+ validator: (v: string) => ["text", "arrayBuffer"].includes(v),
42
+ },
43
+ })
44
+
45
+ const files = ref<File[]>([])
46
+ const trigger = () => {
47
+ fileInput.value?.click()
48
+ }
49
+ const handleFileChange = (newFile: File | null) => {
50
+ console.log("File change triggered:", newFile)
51
+ console.log(files.value)
52
+
53
+ if (!newFile) {
54
+ console.log("No file selected")
55
+ return
56
+ }
57
+
58
+ const selectedFile = newFile
59
+ if (!selectedFile) {
60
+ console.log("Selected file is null")
61
+ return
62
+ }
63
+
64
+ console.log(
65
+ "Reading file:",
66
+ selectedFile.name,
67
+ selectedFile.type,
68
+ selectedFile.size,
69
+ )
70
+
71
+ const reader = new FileReader()
72
+
73
+ reader.onload = () => {
74
+ console.log(
75
+ "File loaded successfully, length:",
76
+ (reader.result as string)?.length,
77
+ )
78
+ // Pass content first, then file
79
+ props.onFileLoaded(reader.result, selectedFile)
80
+ }
81
+
82
+ reader.onerror = () => {
83
+ console.error("Error reading file:", reader.error)
84
+ }
85
+
86
+ reader.onprogress = (e) => {
87
+ console.log("Reading progress:", e.loaded, "/", e.total)
88
+ }
89
+
90
+ // Read the file from the parameter, not from refs
91
+ if (props.readAs === "arrayBuffer") {
92
+ reader.readAsArrayBuffer(selectedFile)
93
+ } else {
94
+ reader.readAsText(selectedFile)
95
+ }
96
+ }
97
+ defineExpose({ trigger })
98
+ </script>
@@ -0,0 +1,182 @@
1
+ <template>
2
+ <!-- Dialog overlay -->
3
+ <div
4
+ v-if="dialog"
5
+ class="tw:fixed tw:inset-0 tw:flex tw:items-center tw:justify-center tw:bg-black/50 tw:z-50"
6
+ >
7
+ <!-- Dialog panel -->
8
+ <div
9
+ class="tw:bg-white tw:rounded-lg tw:shadow-xl tw:max-w-xl tw:w-full tw:overflow-auto"
10
+ >
11
+ <!-- Title bar -->
12
+ <div
13
+ class="tw:flex tw:justify-between tw:items-center tw:px-4 tw:py-3 tw:border-b tw:border-gray-200"
14
+ >
15
+ <span class="tw:text-lg tw:font-semibold">{{ title }}</span>
16
+
17
+ <button
18
+ @click="close"
19
+ class="tw:text-gray-500 hover:tw:text-gray-700 tw:p-1 tw:rounded tw:hover:bg-gray-100"
20
+ >
21
+ <i class="mdi mdi-close tw:text-xl"></i>
22
+ </button>
23
+ </div>
24
+
25
+ <!-- Content -->
26
+ <div class="tw:px-4 tw:py-4">
27
+ <!-- Header row -->
28
+ <div class="tw:flex tw:justify-between tw:items-center tw:mb-4">
29
+ <!-- Select all -->
30
+ <label class="tw:flex tw:items-center tw:space-x-2 tw:cursor-pointer">
31
+ <input
32
+ type="checkbox"
33
+ v-model="selectAll"
34
+ @change="toggleSelectAll"
35
+ class="tw:w-4 tw:h-4 tw:rounded tw:border-gray-300 tw:text-primary tw:focus:ring-primary"
36
+ />
37
+ <span class="tw:text-sm tw:text-gray-700">
38
+ {{ $t("core.selectAll") }}
39
+ </span>
40
+ </label>
41
+
42
+ <!-- Chip -->
43
+ <div
44
+ class="tw:px-3 tw:py-1 tw:bg-primary tw:text-white tw:rounded-full tw:text-sm tw:font-medium"
45
+ >
46
+ {{ selectedCount }} / {{ items.length }} {{ $t("core.selected") }}
47
+ </div>
48
+ </div>
49
+
50
+ <!-- List -->
51
+ <ul class="tw:max-h-96 tw:overflow-y-auto tw:space-y-1">
52
+ <li
53
+ v-for="item in items"
54
+ :key="item._id"
55
+ @click="toggleSelection(item._id)"
56
+ class="tw:flex tw:items-center tw:space-x-3 tw:p-2 tw:rounded tw:cursor-pointer hover:tw:bg-gray-50"
57
+ >
58
+ <!-- Checkbox -->
59
+ <input
60
+ type="checkbox"
61
+ :checked="selected.includes(item._id)"
62
+ @click.stop="toggleSelection(item._id)"
63
+ class="tw:w-4 tw:h-4 tw:rounded tw:border-gray-300 tw:text-primary tw:focus:ring-primary"
64
+ />
65
+
66
+ <!-- Label -->
67
+ <span class="tw:text-sm tw:text-gray-800">
68
+ {{ getDisplayValue(item) }}
69
+ </span>
70
+ </li>
71
+ </ul>
72
+
73
+ <!-- Warning for no selection -->
74
+ <div
75
+ v-if="selectedCount === 0"
76
+ class="tw:mt-4 tw:bg-yellow-50 tw:text-yellow-700 tw:p-3 tw:rounded tw:text-sm"
77
+ >
78
+ {{ $t("core.noItemsSelected") }}
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Actions -->
83
+ <div
84
+ class="tw:flex tw:justify-end tw:space-x-2 tw:px-4 tw:py-3 tw:border-t tw:border-gray-200"
85
+ >
86
+ <button
87
+ @click="close"
88
+ class="tw:px-4 tw:py-2 tw:text-sm tw:text-gray-700 tw:rounded"
89
+ >
90
+ {{ $t("core.cancel") }}
91
+ </button>
92
+
93
+ <button
94
+ v-if="selectedCount"
95
+ @click="confirm"
96
+ :disabled="selectedCount === 0"
97
+ class="tw:px-4 tw:py-2 tw:text-sm tw:rounded tw:text-gray-700 tw:bg-primary"
98
+ >
99
+ {{ confirmText }}
100
+ </button>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </template>
105
+
106
+ <script setup lang="ts">
107
+ import { ref, computed, watch } from "vue"
108
+
109
+ interface Props {
110
+ modelValue: boolean
111
+ items: any[]
112
+ displayAttribute: string
113
+ title: string
114
+ confirmText: string
115
+ }
116
+
117
+ const props = defineProps<Props>()
118
+
119
+ const emit = defineEmits<{
120
+ "update:modelValue": [value: boolean]
121
+ confirm: [selectedIds: string[]]
122
+ }>()
123
+
124
+ const dialog = computed({
125
+ get: () => props.modelValue,
126
+ set: (value) => emit("update:modelValue", value),
127
+ })
128
+
129
+ const selected = ref<string[]>([])
130
+ const selectAll = ref(false)
131
+
132
+ const selectedCount = computed(() => selected.value.length)
133
+
134
+ const getDisplayValue = (item: any) => {
135
+ return item[props.displayAttribute] || item._id
136
+ }
137
+
138
+ const toggleSelection = (id: string) => {
139
+ const index = selected.value.indexOf(id)
140
+ if (index > -1) {
141
+ selected.value.splice(index, 1)
142
+ } else {
143
+ selected.value.push(id)
144
+ }
145
+ updateSelectAllState()
146
+ }
147
+
148
+ const toggleSelectAll = (value: boolean) => {
149
+ if (value) {
150
+ selected.value = props.items.map((item) => item._id)
151
+ } else {
152
+ selected.value = []
153
+ }
154
+ }
155
+
156
+ const updateSelectAllState = () => {
157
+ selectAll.value =
158
+ selected.value.length === props.items.length && props.items.length > 0
159
+ }
160
+
161
+ const close = () => {
162
+ dialog.value = false
163
+ selected.value = []
164
+ selectAll.value = false
165
+ }
166
+
167
+ const confirm = () => {
168
+ if (selectedCount.value > 0) {
169
+ emit("confirm", selected.value)
170
+ close()
171
+ }
172
+ }
173
+
174
+ // Initialize selectAll state when items change
175
+ watch(
176
+ () => props.items,
177
+ () => {
178
+ updateSelectAllState()
179
+ },
180
+ { immediate: true },
181
+ )
182
+ </script>
@@ -0,0 +1,140 @@
1
+ <template>
2
+ <!-- Dialog overlay -->
3
+ <div
4
+ v-if="dialog"
5
+ class="tw:fixed tw:inset-0 tw:flex tw:items-center tw:justify-center tw:bg-black/50 tw:z-50"
6
+ >
7
+ <!-- Dialog panel -->
8
+ <div
9
+ class="tw:bg-white tw:rounded-lg tw:shadow-xl tw:max-w-md tw:w-full tw:overflow-auto tw:h-[80%]"
10
+ >
11
+ <!-- Title bar -->
12
+ <div
13
+ class="tw:flex tw:justify-between tw:items-center tw:px-4 tw:py-3 tw:border-b tw:border-gray-200"
14
+ >
15
+ <span class="tw:text-lg tw:font-semibold">{{ title }}</span>
16
+
17
+ <button
18
+ @click="close"
19
+ class="tw:text-gray-500 hover:tw:text-gray-700 tw:p-1 tw:rounded tw:hover:bg-gray-100"
20
+ >
21
+ <i class="mdi mdi-close tw:text-xl"></i>
22
+ </button>
23
+ </div>
24
+
25
+ <!-- Content -->
26
+ <div class="tw:px-4 tw:py-4">
27
+ <!-- Alert -->
28
+ <div
29
+ :class="[
30
+ 'tw:rounded tw:p-4 tw:mb-4',
31
+ type === 'error' && 'tw:bg-red-50 tw:text-red-700',
32
+ type === 'warning' && 'tw:bg-yellow-50 tw:text-yellow-700',
33
+ type === 'info' && 'tw:bg-blue-50 tw:text-blue-700',
34
+ type === 'success' && 'tw:bg-green-50 tw:text-green-700',
35
+ ]"
36
+ >
37
+ <div class="tw:text-lg tw:font-medium tw:mb-2">
38
+ {{ message }}
39
+ </div>
40
+
41
+ <div class="tw:text-sm">
42
+ {{ $t("core.itemsCount") }}:
43
+ <strong>{{ itemCount }}</strong>
44
+ </div>
45
+ </div>
46
+
47
+ <!-- List -->
48
+ <div v-if="showItems && items.length > 0" class="tw:mt-4">
49
+ <div
50
+ class="tw:text-xs tw:uppercase tw:font-semibold tw:text-gray-500 tw:mb-2"
51
+ >
52
+ {{ $t("core.selectedItems") }}
53
+ </div>
54
+
55
+ <ul class="tw:space-y-1">
56
+ <li
57
+ v-for="item in items"
58
+ :key="item._id"
59
+ class="tw:px-3 tw:py-2 tw:bg-gray-50 tw:rounded tw:text-sm tw:text-gray-700"
60
+ >
61
+ {{ getDisplayValue(item) }}
62
+ </li>
63
+ </ul>
64
+ </div>
65
+ </div>
66
+
67
+ <!-- Actions -->
68
+ <div
69
+ class="tw:flex tw:justify-end tw:space-x-2 tw:px-4 tw:py-3 tw:border-t tw:border-gray-200"
70
+ >
71
+ <button
72
+ @click="close"
73
+ class="tw:px-4 tw:py-2 tw:text-sm tw:text-gray-700 hover:tw:bg-gray-100 tw:rounded"
74
+ >
75
+ {{ cancelText || $t("core.close") }}
76
+ </button>
77
+
78
+ <button
79
+ v-if="showConfirm"
80
+ @click="confirm"
81
+ :class="[
82
+ 'tw:px-4 tw:py-2 tw:text-sm tw:rounded tw:text-white',
83
+ type === 'error' && 'tw:bg-red-600 hover:tw:bg-red-700',
84
+ type === 'warning' && 'tw:bg-yellow-600 hover:tw:bg-yellow-700',
85
+ type === 'info' && 'tw:bg-blue-600 hover:tw:bg-blue-700',
86
+ type === 'success' && 'tw:bg-green-600 hover:tw:bg-green-700',
87
+ ]"
88
+ >
89
+ {{ confirmText }}
90
+ </button>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </template>
95
+
96
+ <script setup lang="ts">
97
+ import { computed } from "vue"
98
+
99
+ interface Props {
100
+ modelValue: boolean
101
+ items: any[]
102
+ title: string
103
+ message: string
104
+ type?: "info" | "success" | "warning" | "error"
105
+ showItems?: boolean
106
+ showConfirm?: boolean
107
+ displayAttribute: string
108
+ confirmText?: string
109
+ cancelText?: string
110
+ }
111
+
112
+ const props = withDefaults(defineProps<Props>(), {
113
+ type: "info",
114
+ showItems: true,
115
+ showConfirm: false,
116
+ })
117
+
118
+ const emit = defineEmits<{
119
+ "update:modelValue": [value: boolean]
120
+ confirm: []
121
+ }>()
122
+
123
+ const dialog = computed({
124
+ get: () => props.modelValue,
125
+ set: (value) => emit("update:modelValue", value),
126
+ })
127
+
128
+ const itemCount = computed(() => props.items.length)
129
+
130
+ const close = () => {
131
+ dialog.value = false
132
+ }
133
+ const getDisplayValue = (item: any) => {
134
+ return item[props.displayAttribute] || item._id
135
+ }
136
+ const confirm = () => {
137
+ emit("confirm")
138
+ close()
139
+ }
140
+ </script>