@raclettejs/core 0.1.32 → 0.1.33-canary.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 (33) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cli.js +4 -4
  3. package/dist/cli.js.map +2 -2
  4. package/dist/index.js +3 -3
  5. package/dist/index.js.map +1 -1
  6. package/package.json +2 -2
  7. package/services/backend/.yarn/install-state.gz +0 -0
  8. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.patchOwn.ts +1 -1
  9. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/user.service.ts +22 -1
  10. package/services/backend/yarn.lock +1 -1
  11. package/services/frontend/.yarn/install-state.gz +0 -0
  12. package/services/frontend/package.json +2 -2
  13. package/services/frontend/src/core/lib/data/dataApi.ts +1 -2
  14. package/services/frontend/src/orchestrator/ProductOrchestrator.vue +8 -1
  15. package/services/frontend/src/orchestrator/assets/styles/layers.css +1 -4
  16. package/services/frontend/src/orchestrator/assets/styles/vuetifyStyles.scss +4 -1
  17. package/services/frontend/src/orchestrator/components/dataExport/DataExporter.vue +303 -0
  18. package/services/frontend/src/orchestrator/components/dataImport/ImportSelectionDialog.vue +169 -0
  19. package/services/frontend/src/orchestrator/components/dataImport/ImportSummaryDialog.vue +211 -0
  20. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTable.vue +473 -0
  21. package/services/frontend/src/orchestrator/components/index.ts +10 -0
  22. package/services/frontend/src/orchestrator/components/input/FileUpload.vue +83 -0
  23. package/services/frontend/src/orchestrator/composables/index.ts +1 -0
  24. package/services/frontend/src/orchestrator/composables/useExport/formats/builtins.ts +124 -0
  25. package/services/frontend/src/orchestrator/composables/useExport/index.ts +176 -0
  26. package/services/frontend/src/orchestrator/composables/useExport/types.ts +141 -0
  27. package/services/frontend/src/orchestrator/i18n/de-DE.json +26 -1
  28. package/services/frontend/src/orchestrator/i18n/en-EU.json +26 -1
  29. package/services/frontend/src/orchestrator/i18n/sk.json +26 -1
  30. package/services/frontend/src/orchestrator/i18n/tl-TL.json +14 -0
  31. package/services/frontend/src/orchestrator/setup/vuetify.ts +0 -1
  32. package/services/frontend/yarn.lock +4854 -0
  33. package/types/index.ts +1 -1
@@ -135,7 +135,8 @@ export class UserService {
135
135
  options: QueryOptions = {},
136
136
  ): Promise<UserType[]> {
137
137
  try {
138
- if (filter.isDeleted !== true && filter.isDeleted !=='true') filter.isDeleted = false
138
+ if (filter.isDeleted !== true && filter.isDeleted !== "true")
139
+ filter.isDeleted = false
139
140
  // Start building the query
140
141
  let query = this.userModel.find(filter)
141
142
 
@@ -319,7 +320,27 @@ export class UserService {
319
320
  throw error
320
321
  }
321
322
  }
323
+ /**
324
+ * Hard delete a user with payload wrapping and event emission
325
+ */
326
+ async hardDeleteUser(
327
+ fastify: PluginFastifyInstance,
328
+ requestData: FrontendPayloadRequestData,
329
+ id: string,
330
+ ): Promise<FrontendPayload<UserType[]>> {
331
+ const user = await this._hardDeleteUser(fastify, id)
332
+
333
+ const payload = await createUserPayload(fastify, [user], {
334
+ ...requestData,
335
+ type: "dataHardDeleted",
336
+ })
337
+
338
+ payload.body.deleted = [user._id]
322
339
 
340
+ fastify.emit("userHardDeleted", payload)
341
+
342
+ return payload
343
+ }
323
344
  /**
324
345
  * Core function to update user's last accessed project (returns raw data)
325
346
  */
@@ -2,7 +2,7 @@
2
2
  # Manual changes might be lost - proceed with caution!
3
3
 
4
4
  __metadata:
5
- version: 8
5
+ version: 9
6
6
  cacheKey: 10c0
7
7
 
8
8
  "@colors/colors@npm:1.5.0":
@@ -41,12 +41,12 @@
41
41
  "vuetify": "4.0.5"
42
42
  },
43
43
  "devDependencies": {
44
+ "@eslint/js": "9.35.0",
44
45
  "@types/node": "24.5.1",
45
46
  "@types/ramda": "0.31.1",
46
47
  "@vitejs/plugin-vue": "6.0.5",
47
48
  "@vue/tsconfig": "0.9.0",
48
49
  "eslint": "9.35.0",
49
- "@eslint/js": "9.35.0",
50
50
  "eslint-config-prettier": "9.1.2",
51
51
  "eslint-plugin-import": "2.32.0",
52
52
  "eslint-plugin-prefer-arrow-functions": "3.9.1",
@@ -60,4 +60,4 @@
60
60
  "vue-eslint-parser": "9.4.3",
61
61
  "vue-tsc": "3.2.6"
62
62
  }
63
- }
63
+ }
@@ -79,10 +79,9 @@ const $writeDataApi = (
79
79
  responseSubject$.complete()
80
80
  },
81
81
  clear: () => {
82
- dataSubject$.next({})
82
+ dataSubject$.next([])
83
83
  errorSubject$.next({})
84
84
  isLoadingSubject$.next(false)
85
- dataSubject$.next({})
86
85
  },
87
86
  }
88
87
 
@@ -112,13 +112,14 @@
112
112
  rel="noopener noreferrer"
113
113
  target="_blank"
114
114
  title="raclette"
115
- class="tw:text-[0.9rem]"
115
+ class="tw:text-[0.9rem] tw:text-neutral-500 tw:no-underline hover:tw:no-underline hover:tw:text-neutral-700 tw:transition-colors"
116
116
  >
117
117
  Cooked with <span class="raclette">raclette 🧀</span></a
118
118
  >
119
119
  <span
120
120
  v-if="configService.getAdvertiseYourself().length"
121
121
  v-html="configService.getAdvertiseYourself()"
122
+ class="footerLink tw:text-[0.9rem] tw:text-neutral-500 tw:no-underline hover:tw:no-underline hover:tw:text-neutral-700 tw:transition-colors"
122
123
  ></span>
123
124
  </v-footer>
124
125
  </v-main>
@@ -285,3 +286,9 @@ window
285
286
  }
286
287
  })
287
288
  </script>
289
+ <style lang="css">
290
+ .footerLink a {
291
+ text-decoration: none;
292
+ color: var(--tw-color-neutral-500);
293
+ }
294
+ </style>
@@ -1,11 +1,8 @@
1
- @layer tailwind-theme;
2
- @layer tailwind-reset;
3
-
4
1
  @layer vuetify-core;
5
2
  @layer vuetify-components;
6
3
  @layer vuetify-overrides;
7
4
  @layer vuetify-utilities;
8
5
 
9
- @layer tailwind;
6
+ @layer tailwind; /* <-- our new utilities */
10
7
 
11
8
  @layer vuetify-final;
@@ -1,5 +1,8 @@
1
1
  @use "@mdi/font/css/materialdesignicons.css";
2
-
2
+ @use "vuetify/settings" with (
3
+ $color-pack: false,
4
+ $utilities: false
5
+ );
3
6
  // The styles below are picked from vuetify's reset css (which is disabled)
4
7
 
5
8
  /* Apply cursor pointer to button elements */
@@ -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>