@raclettejs/core 0.1.31 → 0.1.32-nightly-1

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 (83) hide show
  1. package/CHANGELOG.md +31 -4
  2. package/dist/cli.js +94 -94
  3. package/dist/cli.js.map +3 -3
  4. package/dist/index.js +3 -3
  5. package/dist/index.js.map +1 -1
  6. package/package.json +4 -4
  7. package/services/backend/.yarn/install-state.gz +0 -0
  8. package/services/backend/dist/core/eventBus/index.js +7 -0
  9. package/services/backend/dist/core/pluginSystem/configGenerator/index.js +346 -0
  10. package/services/backend/dist/core/sockets/index.js +95 -0
  11. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/account/events/index.js +31 -0
  12. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/account/index.js +48 -0
  13. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/account/routes/index.js +15 -0
  14. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/composition/events/index.js +22 -0
  15. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/composition/index.js +51 -0
  16. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/composition/routes/index.js +19 -0
  17. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/interactionLink/events/index.js +22 -0
  18. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/interactionLink/index.js +48 -0
  19. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/index.js +19 -0
  20. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/project/events/index.js +31 -0
  21. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/project/index.js +49 -0
  22. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/project/routes/index.js +16 -0
  23. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/tag/events/index.js +39 -0
  24. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/tag/index.js +50 -0
  25. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/tag/routes/index.js +19 -0
  26. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/user/events/index.js +31 -0
  27. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/user/index.js +51 -0
  28. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/user/routes/index.js +21 -0
  29. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/workSession/events/index.js +31 -0
  30. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/workSession/index.js +48 -0
  31. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/workSession/routes/index.js +15 -0
  32. package/services/backend/dist/corePlugins/raclette__core/backend/index.js +34 -0
  33. package/services/backend/dist/domains/index.js +11 -0
  34. package/services/backend/dist/domains/system/index.js +11 -0
  35. package/services/backend/dist/domains/system/routes/index.js +17 -0
  36. package/services/backend/dist/helpers/index.js +14 -0
  37. package/services/backend/dist/index.js +3 -0
  38. package/services/backend/dist/modules/authentication/index.js +253 -0
  39. package/services/backend/dist/shared/types/core/index.js +8 -0
  40. package/services/backend/dist/shared/types/dataTypes/index.js +6 -0
  41. package/services/backend/dist/shared/types/index.js +8 -0
  42. package/services/backend/dist/shared/types/plugins/index.js +8 -0
  43. package/services/backend/dist/types/index.js +12 -0
  44. package/services/backend/dist/utils/index.js +2 -0
  45. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.patchOwn.ts +1 -1
  46. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/user.service.ts +22 -1
  47. package/services/backend/yarn.lock +1 -1
  48. package/services/frontend/.yarn/install-state.gz +0 -0
  49. package/services/frontend/package.json +2 -2
  50. package/services/frontend/src/core/lib/data/dataApi.ts +1 -2
  51. package/services/frontend/src/orchestrator/ProductOrchestrator.vue +8 -1
  52. package/services/frontend/src/orchestrator/assets/styles/layers.css +1 -4
  53. package/services/frontend/src/orchestrator/assets/styles/tailwindStyles.css +3 -3
  54. package/services/frontend/src/orchestrator/assets/styles/vuetifyStyles.scss +4 -1
  55. package/services/frontend/src/orchestrator/components/dataExport/DataExporter.vue +303 -0
  56. package/services/frontend/src/orchestrator/components/dataImport/ImportSelectionDialog.vue +169 -0
  57. package/services/frontend/src/orchestrator/components/dataImport/ImportSummaryDialog.vue +211 -0
  58. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTable.vue +473 -0
  59. package/services/frontend/src/orchestrator/components/index.ts +8 -0
  60. package/services/frontend/src/orchestrator/components/input/FileUpload.vue +83 -0
  61. package/services/frontend/src/orchestrator/composables/index.ts +1 -0
  62. package/services/frontend/src/orchestrator/composables/useExport/formats/builtins.ts +124 -0
  63. package/services/frontend/src/orchestrator/composables/useExport/index.ts +176 -0
  64. package/services/frontend/src/orchestrator/composables/useExport/types.ts +141 -0
  65. package/services/frontend/src/orchestrator/i18n/de-DE.json +26 -1
  66. package/services/frontend/src/orchestrator/i18n/en-EU.json +26 -1
  67. package/services/frontend/src/orchestrator/i18n/sk.json +26 -1
  68. package/services/frontend/src/orchestrator/i18n/tl-TL.json +14 -0
  69. package/services/frontend/src/orchestrator/setup/vuetify.ts +0 -1
  70. package/services/frontend/yarn.lock +4854 -0
  71. package/types/index.ts +1 -1
  72. package/package/LICENSE.md +0 -699
  73. package/package/README.md +0 -49
  74. package/package/dev/README.md +0 -4
  75. package/package/index.js +0 -11
  76. package/package/raclette.default.config.yaml +0 -60
  77. package/package/services/backend/README.md +0 -172
  78. package/package/services/backend/yarn.lock +0 -5320
  79. package/package/services/frontend/README.md +0 -511
  80. package/package/services/frontend/src/core/lib/eggs/readme.md +0 -75
  81. package/package/services/frontend/yarn.lock +0 -0
  82. package/package/src/README.md +0 -404
  83. package/package/templates/README.md +0 -0
@@ -0,0 +1,303 @@
1
+ <template>
2
+ <v-menu
3
+ v-model="isOpen"
4
+ :close-on-content-click="false"
5
+ location="bottom end"
6
+ offset="8"
7
+ :max-width="520"
8
+ :min-width="360"
9
+ >
10
+ <template #activator="{ props: menuProps }">
11
+ <slot v-if="$slots.default" v-bind="triggerSlotProps" />
12
+ <v-btn
13
+ v-else
14
+ v-bind="{ ...menuProps, ...$attrs }"
15
+ :disabled="disabled"
16
+ :variant="$attrs.variant || 'tonal'"
17
+ color="info"
18
+ prepend-icon="mdi-export-variant"
19
+ class="tw:font-medium"
20
+ density="compact"
21
+ @click="open"
22
+ >
23
+ {{ buttonLabel }}
24
+ </v-btn>
25
+ </template>
26
+
27
+ <v-card rounded="lg" elevation="8" :style="{ width: '360px' }">
28
+ <!-- Header -->
29
+ <div class="tw:flex tw:items-center tw:justify-between tw:px-4 tw:pt-4 tw:pb-2">
30
+ <div class="tw:flex tw:items-center tw:gap-2">
31
+ <v-icon icon="mdi-export-variant" size="18" color="primary" />
32
+ <span class="tw:text-sm tw:font-semibold tw:tracking-wide tw:uppercase tw:opacity-70">
33
+ {{ $t("core.dataExporter.title") }}
34
+ </span>
35
+ </div>
36
+ <v-btn icon="mdi-close" variant="text" size="x-small" @click="isOpen = false" />
37
+ </div>
38
+
39
+ <v-divider />
40
+
41
+ <!-- Format tabs -->
42
+ <div class="tw:px-4 tw:pt-3 tw:pb-1">
43
+ <v-chip-group
44
+ v-model="activeFormatId"
45
+ mandatory
46
+ selected-class="text-primary"
47
+ class="tw:gap-1"
48
+ >
49
+ <v-chip
50
+ v-for="fmt in resolvedFormats"
51
+ :key="fmt.id"
52
+ :value="fmt.id"
53
+ size="small"
54
+ variant="tonal"
55
+ class="tw:font-mono tw:text-xs"
56
+ >
57
+ {{ fmt.label }}
58
+ </v-chip>
59
+ </v-chip-group>
60
+ </div>
61
+
62
+ <!-- Data scope switch — only shown when both items AND meta exist -->
63
+ <div
64
+ v-if="showScopeSwitch"
65
+ class="tw:px-4 tw:py-1 tw:flex tw:items-center tw:justify-between tw:gap-2"
66
+ >
67
+ <span class="tw:text-xs tw:opacity-60 tw:uppercase tw:tracking-wide">
68
+ {{ $t("core.dataExporter.scopeLabel") }}
69
+ </span>
70
+ <v-btn-toggle v-model="selectedScope" divided density="compact" mandatory>
71
+ <v-btn value="combined" size="x-small">
72
+ {{ $t("core.dataExporter.scopeCombined") }}
73
+ </v-btn>
74
+ <v-btn value="items" size="x-small">
75
+ {{ $t("core.dataExporter.scopeItems") }}
76
+ </v-btn>
77
+ <v-btn value="meta" size="x-small">
78
+ {{ $t("core.dataExporter.scopeMeta") }}
79
+ </v-btn>
80
+ </v-btn-toggle>
81
+ </div>
82
+
83
+ <!-- No formats warning -->
84
+ <div v-if="resolvedFormats.length === 0" class="tw:px-4 tw:py-6 tw:text-center">
85
+ <v-icon icon="mdi-alert-circle-outline" color="warning" class="tw:mb-2" />
86
+ <p class="tw:text-sm tw:opacity-60">{{ $t("core.dataExporter.noValidFormats") }}</p>
87
+ </div>
88
+
89
+ <!-- Preview pane -->
90
+ <div v-else class="tw:px-4 tw:pt-1 tw:pb-4">
91
+ <!-- Preview toggle -->
92
+ <v-btn
93
+ variant="text"
94
+ density="compact"
95
+ class="tw:mb-1.5 tw:w-full tw:justify-start tw:normal-case tw:text-xs tw:opacity-60 hover:tw:opacity-90"
96
+ @click="previewOpen = !previewOpen"
97
+ >
98
+ <template #prepend>
99
+ <v-icon
100
+ :icon="previewOpen ? 'mdi-chevron-down' : 'mdi-chevron-right'"
101
+ size="14"
102
+ />
103
+ </template>
104
+ {{ $t("core.dataExporter.preview") }}
105
+ </v-btn>
106
+
107
+ <!-- Fixed-height container prevents width reflow when toggling -->
108
+ <div class="tw:overflow-hidden" :style="{ height: previewOpen ? 'auto' : '0px', minHeight: previewOpen ? '0px' : undefined }">
109
+ <v-expand-transition>
110
+ <div v-show="previewOpen">
111
+ <!-- Serialization error -->
112
+ <v-alert
113
+ v-if="!serializeResult.ok"
114
+ type="error"
115
+ variant="tonal"
116
+ density="compact"
117
+ class="tw:mb-2 tw:font-mono tw:text-xs"
118
+ >
119
+ <pre class="tw:whitespace-pre-wrap tw:break-all">{{ serializeResult.error }}</pre>
120
+ </v-alert>
121
+
122
+ <!-- Preview code block -->
123
+ <div
124
+ v-else
125
+ class="tw:rounded-lg tw:overflow-hidden tw:border tw:border-black/10 dark:tw:border-white/10"
126
+ >
127
+ <!-- Toolbar -->
128
+ <div
129
+ class="tw:flex tw:items-center tw:justify-between tw:px-3 tw:py-1.5 tw:bg-black/5 dark:tw:bg-white/5 tw:border-b tw:border-black/10 dark:tw:border-white/10"
130
+ >
131
+ <span class="tw:font-mono tw:text-[10px] tw:uppercase tw:tracking-widest tw:opacity-50">
132
+ {{ activeFormatId }}
133
+ </span>
134
+ <span class="tw:font-mono tw:text-[10px] tw:opacity-40">
135
+ {{ previewLines.lineCount }}
136
+ {{ $t("core.dataExporter.lines") }}
137
+ </span>
138
+ </div>
139
+
140
+ <!-- Code content -->
141
+ <pre
142
+ class="tw:text-xs tw:font-mono tw:p-3 tw:max-h-52 tw:overflow-auto tw:leading-relaxed tw:bg-black/[0.03] dark:tw:bg-white/[0.03] tw:m-0"
143
+ >{{ previewLines.text }}</pre>
144
+
145
+ <!-- Truncation notice -->
146
+ <div
147
+ v-if="previewLines.truncated"
148
+ class="tw:text-[10px] tw:text-center tw:py-1 tw:opacity-40 tw:bg-black/5 dark:tw:bg-white/5 tw:border-t tw:border-black/10 dark:tw:border-white/10"
149
+ >
150
+ {{ $t("core.dataExporter.previewTruncatedAt", { count: PREVIEW_LINE_LIMIT }) }}
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </v-expand-transition>
155
+ </div>
156
+
157
+ <!-- Action Buttons -->
158
+ <div class="tw:flex tw:gap-2 tw:mt-3">
159
+ <v-btn
160
+ v-if="clipboardSupported"
161
+ :disabled="!serializeResult.ok"
162
+ :color="copyState === 'success' ? 'success' : copyState === 'error' ? 'error' : 'primary'"
163
+ variant="tonal"
164
+ size="small"
165
+ class="tw:flex-1 tw:font-medium"
166
+ :prepend-icon="
167
+ copyState === 'success' ? 'mdi-check'
168
+ : copyState === 'error' ? 'mdi-alert-circle'
169
+ : 'mdi-content-copy'
170
+ "
171
+ @click="copyToClipboard"
172
+ >
173
+ {{
174
+ copyState === "success" ? $t("core.dataExporter.copied")
175
+ : copyState === "error" ? $t("core.dataExporter.copyFailed")
176
+ : $t("core.dataExporter.copy")
177
+ }}
178
+ </v-btn>
179
+ <v-btn
180
+ :disabled="!serializeResult.ok"
181
+ color="primary"
182
+ variant="flat"
183
+ size="small"
184
+ class="tw:flex-1 tw:font-medium"
185
+ prepend-icon="mdi-download"
186
+ @click="downloadFile"
187
+ >
188
+ {{ $t("core.dataExporter.download") }}
189
+ </v-btn>
190
+ </div>
191
+ </div>
192
+ </v-card>
193
+ </v-menu>
194
+ </template>
195
+
196
+ <script setup lang="ts">
197
+ import { ref, computed, watch } from "vue"
198
+ import type {
199
+ RacletteExportProps,
200
+ FormatEntry,
201
+ ExportSuccessPayload,
202
+ ExportErrorPayload,
203
+ TriggerSlotProps,
204
+ ExportDataScope,
205
+ } from "@racletteOrchestrator/composables/useExport/types"
206
+ import { useExport } from "@racletteOrchestrator/composables/useExport"
207
+
208
+ defineOptions({ inheritAttrs: false })
209
+
210
+ const props = withDefaults(defineProps<RacletteExportProps>(), {
211
+ filename: "export",
212
+ formats: () => ["json", "yaml", "csv", "text"] as FormatEntry[],
213
+ buttonLabel: "Export",
214
+ disabled: false,
215
+ previewCollapsed: true,
216
+ showDataScopeSwitch: true,
217
+ })
218
+
219
+ const emit = defineEmits<{
220
+ "export-success": [payload: ExportSuccessPayload]
221
+ "export-error": [payload: ExportErrorPayload]
222
+ }>()
223
+
224
+ // ─── Popover State ────────────────────────────────────────────────────────────
225
+
226
+ const isOpen = ref(false)
227
+ const previewOpen = ref(!props.previewCollapsed)
228
+
229
+ const hasItems = computed(() => props.data.items !== undefined)
230
+ const hasMeta = computed(() => props.data.meta !== undefined)
231
+
232
+ // Only show the toggle when BOTH sources exist and the prop allows it
233
+ const showScopeSwitch = computed(
234
+ () => props.showDataScopeSwitch && hasItems.value && hasMeta.value,
235
+ )
236
+
237
+ // Derive the correct default: items if available, else meta, else combined
238
+ const defaultScope = computed<ExportDataScope>(() => {
239
+ if (hasItems.value) return "items"
240
+ if (hasMeta.value) return "meta"
241
+ return "combined"
242
+ })
243
+
244
+ const selectedScope = ref<ExportDataScope>(defaultScope.value)
245
+
246
+ // Keep scope valid when data shape changes at runtime
247
+ watch(defaultScope, (next) => {
248
+ selectedScope.value = next
249
+ })
250
+
251
+ const scopedData = computed(() => {
252
+ if (selectedScope.value === "items") return props.data.items
253
+ if (selectedScope.value === "meta") return props.data.meta ?? {}
254
+ return props.data
255
+ })
256
+
257
+ const open = () => {
258
+ if (!props.disabled) isOpen.value = true
259
+ }
260
+
261
+ // ─── Export Logic ─────────────────────────────────────────────────────────────
262
+
263
+ const {
264
+ resolvedFormats,
265
+ activeFormatId,
266
+ serializeResult,
267
+ copyToClipboard,
268
+ downloadFile,
269
+ copyState,
270
+ clipboardSupported,
271
+ } = useExport({
272
+ data: () => scopedData.value,
273
+ formats: () => props.formats,
274
+ defaultFormat: () => props.defaultFormat,
275
+ filename: () => props.filename,
276
+ enabled: () => isOpen.value,
277
+ onSuccess: (p) => emit("export-success", p),
278
+ onError: (p) => emit("export-error", p),
279
+ })
280
+
281
+ // ─── Trigger Slot Props ───────────────────────────────────────────────────────
282
+
283
+ const triggerSlotProps = computed<TriggerSlotProps>(() => ({
284
+ open,
285
+ isOpen: isOpen.value,
286
+ disabled: props.disabled,
287
+ }))
288
+
289
+ // ─── Preview lines ────────────────────────────────────────────────────────────
290
+
291
+ const PREVIEW_LINE_LIMIT = 200
292
+
293
+ const previewLines = computed<{ text: string; lineCount: number; truncated: boolean }>(() => {
294
+ if (!serializeResult.value.ok) return { text: "", lineCount: 0, truncated: false }
295
+ const lines = serializeResult.value.value.split("\n")
296
+ const truncated = lines.length > PREVIEW_LINE_LIMIT
297
+ return {
298
+ text: truncated ? lines.slice(0, PREVIEW_LINE_LIMIT).join("\n") : serializeResult.value.value,
299
+ lineCount: lines.length,
300
+ truncated,
301
+ }
302
+ })
303
+ </script>
@@ -0,0 +1,169 @@
1
+ <template>
2
+ <v-dialog v-model="dialog" max-width="760" scrollable>
3
+ <v-card>
4
+ <v-card-title class="tw:flex tw:justify-between tw:items-center tw:gap-2">
5
+ <span class="tw:text-lg tw:font-semibold">{{ title }}</span>
6
+ <v-btn icon="mdi-close" size="small" variant="text" @click="close" />
7
+ </v-card-title>
8
+
9
+ <v-divider />
10
+
11
+ <v-card-text class="tw:py-4">
12
+ <div class="tw:flex tw:justify-between tw:items-center tw:mb-4 tw:gap-2">
13
+ <v-checkbox
14
+ :model-value="selectAll"
15
+ @update:model-value="toggleSelectAll"
16
+ :label="$t('core.selectAll')"
17
+ hide-details
18
+ density="compact"
19
+ />
20
+
21
+ <v-chip color="primary" variant="flat" size="small">
22
+ {{ selectedCount }} / {{ items.length }} {{ $t("core.selected") }}
23
+ </v-chip>
24
+ </div>
25
+
26
+ <v-list class="tw:max-h-96 tw:overflow-y-auto tw:border tw:rounded-md tw:py-0">
27
+ <v-list-item
28
+ v-for="item in items"
29
+ :key="item._id"
30
+ @click="toggleSelection(String(item._id))"
31
+ class="tw:cursor-pointer"
32
+ >
33
+ <template #prepend>
34
+ <v-checkbox-btn
35
+ :model-value="selected.includes(String(item._id))"
36
+ @update:model-value="(checked) => setSelection(String(item._id), checked)"
37
+ @click.stop
38
+ />
39
+ </template>
40
+
41
+ <v-list-item-title class="tw:text-sm">
42
+ {{ getDisplayValue(item) }}
43
+ </v-list-item-title>
44
+ </v-list-item>
45
+ </v-list>
46
+
47
+ <v-alert
48
+ v-if="selectedCount === 0"
49
+ type="warning"
50
+ variant="tonal"
51
+ density="compact"
52
+ class="tw:mt-4"
53
+ >
54
+ {{ $t("core.noItemsSelected") }}
55
+ </v-alert>
56
+ </v-card-text>
57
+
58
+ <v-divider />
59
+
60
+ <v-card-actions class="tw:px-4 tw:py-3">
61
+ <v-spacer />
62
+ <v-btn variant="text" @click="close">
63
+ {{ $t("core.cancel") }}
64
+ </v-btn>
65
+
66
+ <v-btn
67
+ v-if="selectedCount"
68
+ @click="confirm"
69
+ :disabled="selectedCount === 0"
70
+ color="primary"
71
+ variant="flat"
72
+ >
73
+ {{ confirmText }}
74
+ </v-btn>
75
+ </v-card-actions>
76
+ </v-card>
77
+ </v-dialog>
78
+ </template>
79
+
80
+ <script setup lang="ts">
81
+ import { ref, computed, watch } from "vue"
82
+
83
+ interface Props {
84
+ modelValue: boolean
85
+ items: any[]
86
+ displayAttribute: string
87
+ title: string
88
+ confirmText: string
89
+ }
90
+
91
+ const props = defineProps<Props>()
92
+
93
+ const emit = defineEmits<{
94
+ "update:modelValue": [value: boolean]
95
+ confirm: [selectedIds: string[]]
96
+ }>()
97
+
98
+ const dialog = computed({
99
+ get: () => props.modelValue,
100
+ set: (value) => emit("update:modelValue", value),
101
+ })
102
+
103
+ const selected = ref<string[]>([])
104
+ const selectAll = ref(false)
105
+
106
+ const selectedCount = computed(() => selected.value.length)
107
+
108
+ const getDisplayValue = (item: any) => {
109
+ return item[props.displayAttribute] || item._id
110
+ }
111
+
112
+ const toggleSelection = (id: string) => {
113
+ const index = selected.value.indexOf(id)
114
+ if (index > -1) {
115
+ selected.value.splice(index, 1)
116
+ } else {
117
+ selected.value.push(id)
118
+ }
119
+ updateSelectAllState()
120
+ }
121
+
122
+ const setSelection = (id: string, checked: boolean | null) => {
123
+ const isChecked = Boolean(checked)
124
+ const exists = selected.value.includes(id)
125
+ if (isChecked && !exists) {
126
+ selected.value.push(id)
127
+ } else if (!isChecked && exists) {
128
+ selected.value = selected.value.filter((selectedId) => selectedId !== id)
129
+ }
130
+ updateSelectAllState()
131
+ }
132
+
133
+ const toggleSelectAll = (value: boolean | null) => {
134
+ const enabled = Boolean(value)
135
+ if (enabled) {
136
+ selected.value = props.items.map((item) => String(item._id))
137
+ } else {
138
+ selected.value = []
139
+ }
140
+ selectAll.value = enabled
141
+ }
142
+
143
+ const updateSelectAllState = () => {
144
+ selectAll.value =
145
+ selected.value.length === props.items.length && props.items.length > 0
146
+ }
147
+
148
+ const close = () => {
149
+ dialog.value = false
150
+ selected.value = []
151
+ selectAll.value = false
152
+ }
153
+
154
+ const confirm = () => {
155
+ if (selectedCount.value > 0) {
156
+ emit("confirm", selected.value)
157
+ close()
158
+ }
159
+ }
160
+
161
+ // Initialize selectAll state when items change
162
+ watch(
163
+ () => props.items,
164
+ () => {
165
+ updateSelectAllState()
166
+ },
167
+ { immediate: true },
168
+ )
169
+ </script>
@@ -0,0 +1,211 @@
1
+ <template>
2
+ <v-dialog v-model="dialog" max-width="800" scrollable>
3
+ <v-card class="tw:overflow-hidden">
4
+ <v-card-title class="tw:flex tw:items-center tw:justify-between tw:gap-2">
5
+ <span class="tw:text-lg tw:font-semibold">{{ title }}</span>
6
+ <v-btn icon="mdi-close" size="small" variant="text" @click="close" />
7
+ </v-card-title>
8
+
9
+ <v-divider />
10
+
11
+ <v-card-text class="tw:py-4">
12
+ <v-alert :type="type" variant="tonal" class="tw:mb-4">
13
+ <div class="tw:text-base tw:font-medium tw:mb-2">
14
+ {{ message }}
15
+ </div>
16
+ <div class="tw:text-sm">
17
+ {{ $t("core.itemsCount") }}:
18
+ <strong>{{ itemCount }}</strong>
19
+ </div>
20
+ </v-alert>
21
+
22
+ <div v-if="showItems && renderedItems.length > 0">
23
+ <div class="tw:text-xs tw:uppercase tw:font-semibold tw:opacity-60 tw:mb-2">
24
+ {{ $t("core.selectedItems") }}
25
+ </div>
26
+
27
+ <v-list density="compact" class="tw:border tw:rounded-md tw:py-0">
28
+ <v-list-item
29
+ v-for="item in renderedItems"
30
+ :key="item.key"
31
+ class="tw:py-2"
32
+ >
33
+ <template #title>
34
+ <span class="tw:text-sm tw:font-medium">{{ item.title }}</span>
35
+ </template>
36
+
37
+ <template #subtitle>
38
+ <pre
39
+ v-if="item.kind === 'pre'"
40
+ class="tw:mt-1 tw:text-xs tw:font-mono tw:p-2 tw:max-h-44 tw:overflow-auto tw:rounded tw:bg-black/5 dark:tw:bg-white/5 tw:whitespace-pre-wrap tw:break-all"
41
+ >{{ item.text }}</pre>
42
+ <div v-else class="tw:mt-1 tw:text-sm tw:opacity-80 tw:break-all">
43
+ {{ item.text }}
44
+ </div>
45
+ </template>
46
+ </v-list-item>
47
+ </v-list>
48
+ </div>
49
+ </v-card-text>
50
+
51
+ <v-divider />
52
+
53
+ <v-card-actions class="tw:px-4 tw:py-3">
54
+ <v-spacer />
55
+ <v-btn variant="text" @click="close">
56
+ {{ cancelText || $t("core.close") }}
57
+ </v-btn>
58
+ <v-btn
59
+ v-if="showConfirm"
60
+ :color="type === 'warning' ? 'warning' : type === 'error' ? 'error' : 'primary'"
61
+ variant="flat"
62
+ @click="confirm"
63
+ >
64
+ {{ confirmText }}
65
+ </v-btn>
66
+ </v-card-actions>
67
+ </v-card>
68
+ </v-dialog>
69
+ </template>
70
+
71
+ <script setup lang="ts">
72
+ import { computed } from "vue"
73
+ import { useI18n } from "vue-i18n"
74
+
75
+ interface Props {
76
+ modelValue: boolean
77
+ items: any[]
78
+ title: string
79
+ message: string
80
+ type?: "info" | "success" | "warning" | "error"
81
+ showItems?: boolean
82
+ showConfirm?: boolean
83
+ displayAttribute: string
84
+ confirmText?: string
85
+ cancelText?: string
86
+ }
87
+
88
+ const props = withDefaults(defineProps<Props>(), {
89
+ type: "info",
90
+ showItems: true,
91
+ showConfirm: false,
92
+ })
93
+
94
+ const emit = defineEmits<{
95
+ "update:modelValue": [value: boolean]
96
+ confirm: []
97
+ }>()
98
+ const { t } = useI18n()
99
+
100
+ const dialog = computed({
101
+ get: () => props.modelValue,
102
+ set: (value) => emit("update:modelValue", value),
103
+ })
104
+
105
+ const itemCount = computed(() => props.items.length)
106
+
107
+ const close = () => {
108
+ dialog.value = false
109
+ }
110
+
111
+ type SummaryRenderKind = "text" | "pre"
112
+
113
+ interface SummaryRenderItem {
114
+ key: string
115
+ title: string
116
+ kind: SummaryRenderKind
117
+ text: string
118
+ }
119
+
120
+ interface SummaryFormatter {
121
+ kind: SummaryRenderKind
122
+ canHandle: (value: unknown) => boolean
123
+ format: (value: unknown) => string
124
+ }
125
+
126
+ const formatAsText = (value: unknown): string => {
127
+ if (value === null || value === undefined) return "-"
128
+ return String(value)
129
+ }
130
+
131
+ const objectJsonFormatter: SummaryFormatter = {
132
+ kind: "pre",
133
+ canHandle: (value) =>
134
+ value !== null && typeof value === "object" && !Array.isArray(value),
135
+ format: (value) => JSON.stringify(value, null, 2),
136
+ }
137
+
138
+ const arrayJsonFormatter: SummaryFormatter = {
139
+ kind: "pre",
140
+ canHandle: (value) => Array.isArray(value),
141
+ format: (value) => JSON.stringify(value, null, 2),
142
+ }
143
+
144
+ const primitiveFormatter: SummaryFormatter = {
145
+ kind: "text",
146
+ canHandle: () => true,
147
+ format: (value) => formatAsText(value),
148
+ }
149
+
150
+ // TODO(summary-formatters): Add XML formatter when import/export surfaces XML payloads.
151
+ // TODO(summary-formatters): Add CSV formatter with row/column-aware preview rendering.
152
+ // TODO(summary-formatters): Add image formatter for URL/blob metadata and thumbnail previews.
153
+ const summaryFormatters: SummaryFormatter[] = [
154
+ objectJsonFormatter,
155
+ arrayJsonFormatter,
156
+ primitiveFormatter,
157
+ ]
158
+
159
+ const resolveFormatter = (value: unknown): SummaryFormatter => {
160
+ return (
161
+ summaryFormatters.find((formatter) => formatter.canHandle(value)) ??
162
+ primitiveFormatter
163
+ )
164
+ }
165
+
166
+ const formatDisplayTitle = (item: unknown, index: number): string => {
167
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
168
+ return `${t("core.selectedItems")} #${index + 1}`
169
+ }
170
+
171
+ const record = item as Record<string, unknown>
172
+ const attributeValue = record[props.displayAttribute]
173
+ if (attributeValue !== undefined && attributeValue !== null) {
174
+ return formatAsText(attributeValue)
175
+ }
176
+ if (record._id !== undefined && record._id !== null) {
177
+ return formatAsText(record._id)
178
+ }
179
+ return `${t("core.selectedItems")} #${index + 1}`
180
+ }
181
+
182
+ const getDisplayPayload = (item: unknown): unknown => {
183
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
184
+ return item
185
+ }
186
+
187
+ const record = item as Record<string, unknown>
188
+ const payload = { ...record }
189
+ delete payload[props.displayAttribute]
190
+ return payload
191
+ }
192
+
193
+ const renderedItems = computed<SummaryRenderItem[]>(() =>
194
+ props.items.map((item, index) => {
195
+ const payload = getDisplayPayload(item)
196
+ const formatter = resolveFormatter(payload)
197
+
198
+ return {
199
+ key: `${index}-${formatDisplayTitle(item, index)}`,
200
+ title: formatDisplayTitle(item, index),
201
+ kind: formatter.kind,
202
+ text: formatter.format(payload),
203
+ }
204
+ }),
205
+ )
206
+
207
+ const confirm = () => {
208
+ emit("confirm")
209
+ close()
210
+ }
211
+ </script>