@raclettejs/workbench 0.1.32 → 0.1.33-canary.0
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/.raclette/backend/raclette.config.js +2 -2
- package/.raclette/backend/yarn.lock +71 -80
- package/.raclette/frontend/raclette.config.js +2 -2
- package/.raclette/frontend/yarn.lock +517 -471
- package/.raclette/virtual/backend/raclette.config.js +2 -2
- package/.raclette/virtual/backend/yarn.lock +71 -80
- package/.raclette/virtual/frontend/raclette.config.js +2 -2
- package/.raclette/virtual/frontend/yarn.lock +517 -471
- package/CHANGELOG.md +2 -0
- package/i18n/de-DE.json +1 -10
- package/i18n/en-EU.json +1 -10
- package/i18n/sk-SK.json +1 -10
- package/package.json +5 -3
- package/plugins/pacifico__compositions/frontend/widgets/CompositionListWidget.vue +63 -107
- package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkListWidget.vue +64 -122
- package/plugins/pacifico__plugins/frontend/widgets/PluginListWidget.vue +2 -2
- package/plugins/pacifico__tags/frontend/widgets/TagListWidget.vue +47 -104
- package/plugins/pacifico__users/frontend/widgets/UserListWidget.vue +24 -16
- package/services/frontend/src/app/lib/jsonTools.ts +69 -3
- package/services/frontend/src/app/components/BaseDataTable.vue +0 -303
- package/services/frontend/src/app/components/FileUpload.vue +0 -98
- package/services/frontend/src/app/components/SelectionDialog.vue +0 -182
- package/services/frontend/src/app/components/SummaryDialog.vue +0 -140
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div>
|
|
3
|
-
<!-- Search and Actions Bar -->
|
|
4
|
-
<div class="tw:flex tw:gap-2 tw:items-center tw:mb-6">
|
|
5
|
-
<v-text-field
|
|
6
|
-
v-model="search"
|
|
7
|
-
variant="outlined"
|
|
8
|
-
hide-details
|
|
9
|
-
prepend-inner-icon="mdi-magnify"
|
|
10
|
-
clearable
|
|
11
|
-
:placeholder="
|
|
12
|
-
$t('workbench.baseDataTable.searchPlaceholder', {
|
|
13
|
-
dataName,
|
|
14
|
-
})
|
|
15
|
-
"
|
|
16
|
-
density="compact"
|
|
17
|
-
/>
|
|
18
|
-
|
|
19
|
-
<AppButton
|
|
20
|
-
v-if="showCreateButton"
|
|
21
|
-
@click="handleCreate"
|
|
22
|
-
variant="secondary"
|
|
23
|
-
class="!tw:h-10"
|
|
24
|
-
>
|
|
25
|
-
<template #prepend-icon>
|
|
26
|
-
<v-icon icon="mdi-plus-circle-outline" />
|
|
27
|
-
</template>
|
|
28
|
-
{{
|
|
29
|
-
$t("workbench.baseDataTable.createDataButton", {
|
|
30
|
-
dataName,
|
|
31
|
-
})
|
|
32
|
-
}}
|
|
33
|
-
</AppButton>
|
|
34
|
-
|
|
35
|
-
<!-- Additional action buttons slot -->
|
|
36
|
-
<slot name="actions" />
|
|
37
|
-
</div>
|
|
38
|
-
<!-- Data Table -->
|
|
39
|
-
<v-data-table
|
|
40
|
-
v-model:search="search"
|
|
41
|
-
density="comfortable"
|
|
42
|
-
:items-per-page="itemsPerPage"
|
|
43
|
-
sticky
|
|
44
|
-
fixed-header
|
|
45
|
-
:headers="computedHeaders"
|
|
46
|
-
:items="items"
|
|
47
|
-
:loading="loading"
|
|
48
|
-
@click:row="handleRowClick"
|
|
49
|
-
:class="{ itemsDeleted }"
|
|
50
|
-
>
|
|
51
|
-
<template
|
|
52
|
-
#item.color="{ item }"
|
|
53
|
-
v-if="!customSlots.includes('item.color')"
|
|
54
|
-
>
|
|
55
|
-
<div class="tw:flex tw:gap-2" v-if="item.color?.length">
|
|
56
|
-
<v-chip :color="item.color" label variant="outlined" size="small">
|
|
57
|
-
{{ item.color }}
|
|
58
|
-
</v-chip>
|
|
59
|
-
</div>
|
|
60
|
-
</template>
|
|
61
|
-
<!-- Built-in tags column -->
|
|
62
|
-
<template #item.tags="{ item }" v-if="!customSlots.includes('item.tags')">
|
|
63
|
-
<div class="tw:flex tw:gap-2" v-if="item.tags?.length">
|
|
64
|
-
<v-chip
|
|
65
|
-
v-for="tag in item.tags"
|
|
66
|
-
:key="tag._id || tag.id || tag"
|
|
67
|
-
:color="tag.color"
|
|
68
|
-
label
|
|
69
|
-
variant="outlined"
|
|
70
|
-
size="small"
|
|
71
|
-
>
|
|
72
|
-
{{ tag.title || tag.name || tag }}
|
|
73
|
-
</v-chip>
|
|
74
|
-
</div>
|
|
75
|
-
</template>
|
|
76
|
-
|
|
77
|
-
<!-- Dynamic slots for custom column rendering -->
|
|
78
|
-
<template v-for="slot in customSlots" :key="slot" #[slot]="slotProps">
|
|
79
|
-
<slot :name="slot" v-bind="slotProps" />
|
|
80
|
-
</template>
|
|
81
|
-
|
|
82
|
-
<!-- Actions column template -->
|
|
83
|
-
<template #item.actions="{ item }" v-if="showActionsColumn">
|
|
84
|
-
<slot name="prepend-row-actions" :item="item"> </slot>
|
|
85
|
-
<slot name="row-actions" :item="item">
|
|
86
|
-
<!-- Default delete action -->
|
|
87
|
-
<v-icon
|
|
88
|
-
v-if="showDeleteAction && !isDeleteDisabled(item)"
|
|
89
|
-
color="red"
|
|
90
|
-
@click.stop.prevent="handleDelete(item)"
|
|
91
|
-
class="mr-2"
|
|
92
|
-
:title="
|
|
93
|
-
$t('workbench.baseDataTable.deleteDataTitle', {
|
|
94
|
-
dataName,
|
|
95
|
-
})
|
|
96
|
-
"
|
|
97
|
-
icon="mdi-delete"
|
|
98
|
-
/>
|
|
99
|
-
</slot>
|
|
100
|
-
<slot name="append-row-actions" :item="item"> </slot>
|
|
101
|
-
</template>
|
|
102
|
-
|
|
103
|
-
<!-- Loading template -->
|
|
104
|
-
<template #loader>
|
|
105
|
-
<v-skeleton-loader
|
|
106
|
-
class="mx-auto border"
|
|
107
|
-
:height="500"
|
|
108
|
-
width="100%"
|
|
109
|
-
type="list-item"
|
|
110
|
-
:loading="true"
|
|
111
|
-
/>
|
|
112
|
-
</template>
|
|
113
|
-
|
|
114
|
-
<!-- Additional table slots (this caused the table to be empty) -->
|
|
115
|
-
<!-- <slot name="table-slots" /> -->
|
|
116
|
-
</v-data-table>
|
|
117
|
-
|
|
118
|
-
<!-- Delete Confirmation Dialog -->
|
|
119
|
-
<v-dialog
|
|
120
|
-
v-model="deleteDialog"
|
|
121
|
-
max-width="400"
|
|
122
|
-
v-if="showDeleteAction && itemToDelete"
|
|
123
|
-
>
|
|
124
|
-
<v-card>
|
|
125
|
-
<v-card-title class="text-h6">
|
|
126
|
-
{{
|
|
127
|
-
$t("workbench.baseDataTable.deleteDataTitle", {
|
|
128
|
-
dataName,
|
|
129
|
-
})
|
|
130
|
-
}}
|
|
131
|
-
</v-card-title>
|
|
132
|
-
|
|
133
|
-
<v-card-text
|
|
134
|
-
v-html="
|
|
135
|
-
$t('workbench.baseDataTable.deleteDataConfirmMessage', {
|
|
136
|
-
article: dataArticle,
|
|
137
|
-
dataName,
|
|
138
|
-
value:
|
|
139
|
-
itemToDelete.email ||
|
|
140
|
-
itemToDelete.title ||
|
|
141
|
-
itemToDelete.name ||
|
|
142
|
-
itemToDelete._id,
|
|
143
|
-
})
|
|
144
|
-
"
|
|
145
|
-
/>
|
|
146
|
-
<v-card-actions>
|
|
147
|
-
<v-spacer />
|
|
148
|
-
<v-btn variant="text" @click="deleteDialog = false">
|
|
149
|
-
{{ $t("workbench.baseDataTable.deleteDataConfirmCancel") }}
|
|
150
|
-
</v-btn>
|
|
151
|
-
<v-btn color="red" variant="flat" @click="confirmDelete">
|
|
152
|
-
{{ $t("workbench.baseDataTable.deleteDataConfirmButton") }}
|
|
153
|
-
</v-btn>
|
|
154
|
-
</v-card-actions>
|
|
155
|
-
</v-card>
|
|
156
|
-
</v-dialog>
|
|
157
|
-
|
|
158
|
-
<!-- Additional dialogs slot -->
|
|
159
|
-
<slot name="dialogs" />
|
|
160
|
-
</div>
|
|
161
|
-
</template>
|
|
162
|
-
|
|
163
|
-
<script setup lang="ts" generic="T extends Record<string, any>">
|
|
164
|
-
import { computed, ref, useSlots } from "vue"
|
|
165
|
-
import { useRouteState } from "@raclettejs/core/orchestrator/composables"
|
|
166
|
-
import AppButton from "../../orchestrator/components/layout/AppButton.vue"
|
|
167
|
-
|
|
168
|
-
export interface BaseDataTableProps<T> {
|
|
169
|
-
uuid?: string
|
|
170
|
-
// Data props
|
|
171
|
-
items?: T[]
|
|
172
|
-
loading?: boolean
|
|
173
|
-
itemsDeleted?: boolean
|
|
174
|
-
// Table configuration
|
|
175
|
-
headers: Array<{
|
|
176
|
-
title: string
|
|
177
|
-
key: keyof T | string
|
|
178
|
-
align?: "start" | "center" | "end"
|
|
179
|
-
sortable?: boolean
|
|
180
|
-
width?: string | number
|
|
181
|
-
}>
|
|
182
|
-
itemsPerPage?: number
|
|
183
|
-
|
|
184
|
-
// Create button
|
|
185
|
-
showCreateButton?: boolean
|
|
186
|
-
createInteractionLinkId?: string
|
|
187
|
-
|
|
188
|
-
// Row actions
|
|
189
|
-
showActionsColumn?: boolean
|
|
190
|
-
showDeleteAction?: boolean
|
|
191
|
-
|
|
192
|
-
// Navigation
|
|
193
|
-
editInteractionLinkId?: string
|
|
194
|
-
|
|
195
|
-
// Item processing
|
|
196
|
-
deleteValidator?: (item: T) => boolean
|
|
197
|
-
rowClickHandler?: (item: T) => void
|
|
198
|
-
|
|
199
|
-
dataName: string
|
|
200
|
-
dataArticle: string
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const props = withDefaults(defineProps<BaseDataTableProps<T>>(), {
|
|
204
|
-
uuid: undefined,
|
|
205
|
-
items: () => [],
|
|
206
|
-
itemsDeleted: false,
|
|
207
|
-
itemsPerPage: 50,
|
|
208
|
-
showCreateButton: true,
|
|
209
|
-
showActionsColumn: true,
|
|
210
|
-
showDeleteAction: true,
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
const emit = defineEmits<{
|
|
214
|
-
create: []
|
|
215
|
-
edit: [item: T]
|
|
216
|
-
delete: [item: T]
|
|
217
|
-
rowClick: [item: T]
|
|
218
|
-
}>()
|
|
219
|
-
|
|
220
|
-
const slots = useSlots()
|
|
221
|
-
const { triggerInteractionLinkById } = useRouteState()
|
|
222
|
-
|
|
223
|
-
// Reactive data
|
|
224
|
-
const search = ref("")
|
|
225
|
-
const deleteDialog = ref(false)
|
|
226
|
-
const itemToDelete = ref<T | null>(null)
|
|
227
|
-
|
|
228
|
-
const computedHeaders = computed(() => {
|
|
229
|
-
if (!props.headers || !props.headers?.length) {
|
|
230
|
-
return null
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const headers = [...props.headers]
|
|
234
|
-
|
|
235
|
-
// Add actions column if needed
|
|
236
|
-
if (props.showActionsColumn) {
|
|
237
|
-
headers.push({
|
|
238
|
-
title: "Actions",
|
|
239
|
-
key: "actions",
|
|
240
|
-
align: "end" as const,
|
|
241
|
-
sortable: false,
|
|
242
|
-
})
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return headers
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
const customSlots = computed(() => {
|
|
249
|
-
return Object.keys(slots).filter(
|
|
250
|
-
(slot) => slot.startsWith("item.") && slot !== "item.actions",
|
|
251
|
-
)
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
// Methods
|
|
255
|
-
const handleCreate = () => {
|
|
256
|
-
if (props.createInteractionLinkId) {
|
|
257
|
-
triggerInteractionLinkById(props.createInteractionLinkId)
|
|
258
|
-
}
|
|
259
|
-
emit("create")
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const handleRowClick = (_: PointerEvent, row: { item: T }) => {
|
|
263
|
-
if (props.rowClickHandler) {
|
|
264
|
-
props.rowClickHandler(row.item)
|
|
265
|
-
return
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (props.editInteractionLinkId) {
|
|
269
|
-
const item = row.item as any
|
|
270
|
-
|
|
271
|
-
triggerInteractionLinkById(props.editInteractionLinkId, {
|
|
272
|
-
id: item._id,
|
|
273
|
-
})
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
emit("edit", row.item)
|
|
277
|
-
emit("rowClick", row.item)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const handleDelete = (item: T) => {
|
|
281
|
-
itemToDelete.value = item
|
|
282
|
-
deleteDialog.value = true
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const confirmDelete = () => {
|
|
286
|
-
if (itemToDelete.value) {
|
|
287
|
-
emit("delete", itemToDelete.value)
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
itemToDelete.value = null
|
|
291
|
-
deleteDialog.value = false
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const isDeleteDisabled = (item: T): boolean => {
|
|
295
|
-
return props.deleteValidator ? !props.deleteValidator(item) : false
|
|
296
|
-
}
|
|
297
|
-
</script>
|
|
298
|
-
<style lang="css">
|
|
299
|
-
.itemsDeleted .v-data-table__tr {
|
|
300
|
-
background-color: rgb(233, 190, 190);
|
|
301
|
-
color: #000;
|
|
302
|
-
}
|
|
303
|
-
</style>
|
|
@@ -1,98 +0,0 @@
|
|
|
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>
|
|
@@ -1,182 +0,0 @@
|
|
|
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>
|