@raclettejs/core 0.1.33-canary.3 → 0.1.34

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,24 +1,28 @@
1
1
  <template>
2
2
  <div>
3
- <v-sheet border rounded>
4
- <v-data-table
5
- v-model="selected"
6
- v-model:search="search"
7
- density="compact"
8
- :items-per-page="itemsPerPage"
9
- sticky
10
- fixed-header
11
- :headers="computedHeaders"
12
- :items="items"
13
- :loading="loading"
14
- @click:row="handleRowClick"
15
- @update:current-items="(items) => emit('update:currentItems', items)"
16
- :class="{ itemsDeleted }"
17
- :show-select="showExport"
18
- :item-value
19
- >
20
- <template #top>
21
- <v-toolbar flat>
3
+ <v-data-table
4
+ v-model="selected"
5
+ v-model:search="search"
6
+ density="compact"
7
+ :items-per-page="itemsPerPage"
8
+ sticky
9
+ fixed-header
10
+ :headers="computedHeaders"
11
+ :items="filteredItems"
12
+ :loading="loading"
13
+ @click:row="handleRowClick"
14
+ @update:current-items="(items) => emit('update:currentItems', items)"
15
+ :class="{ itemsDeleted }"
16
+ :show-select="showExport && isExportMode"
17
+ :item-value
18
+ >
19
+ <template #top>
20
+ <v-toolbar
21
+ flat
22
+ density="compact"
23
+ class="base-data-table-toolbar tw:pr-2"
24
+ >
25
+ <slot name="toolbar-start">
22
26
  <v-toolbar-title v-if="dataName?.length">
23
27
  <v-icon
24
28
  color="medium-emphasis"
@@ -26,61 +30,82 @@
26
30
  size="x-small"
27
31
  start
28
32
  />
29
-
30
33
  {{ dataName }}
31
34
  </v-toolbar-title>
35
+ </slot>
32
36
 
33
- <template #append>
34
- <div
35
- class="tw:flex tw:flex-wrap tw:gap-2 tw:items-center tw:justify-end"
37
+ <template #append>
38
+ <div
39
+ class="tw:flex tw:min-w-0 tw:w-full tw:flex-wrap tw:items-center tw:justify-end tw:gap-2"
40
+ >
41
+ <slot name="toolbar-leading" />
42
+ <slot name="toolbar-end" />
43
+ <slot
44
+ name="toolbar-filters-trigger"
45
+ v-bind="filterTriggerSlotProps"
36
46
  >
37
- <slot name="actions" />
38
- <slot name="toolbar-leading" />
39
- <FileUpload
40
- v-if="onFileLoaded"
41
- ref="importFileUploadRef"
42
- :on-file-loaded="onFileLoaded"
43
- :accept="importAccept"
44
- :read-as="importReadAs"
47
+ <v-btn
48
+ v-if="showFilters"
49
+ variant="outlined"
50
+ prepend-icon="mdi-filter-variant"
51
+ :text="$t('core.baseDataTable.filters')"
52
+ @click="openFilters"
45
53
  />
46
- <v-btn-group density="compact" divided>
47
- <DataExporter
48
- v-if="showExport"
49
- :data="exportPayloadData"
50
- :disabled="!selected.length"
51
- :formats="exportFormats"
52
- :default-format="exportDefaultFormat"
53
- :button-label="$t('core.export')"
54
- color="info"
55
- variant="elevated"
56
- />
57
- <v-btn
58
- v-if="onFileLoaded"
59
- color="teal"
60
- variant="elevated"
61
- density="compact"
62
- prepend-icon="mdi-database-import"
63
- :text="$t('core.import')"
64
- @click="importFileUploadRef?.openPicker()"
65
- />
66
- <v-btn
67
- v-if="showCreateButton"
68
- @click="handleCreate"
69
- prepend-icon="mdi-plus"
70
- density="compact"
71
- :text="
72
- $t('core.baseDataTable.createDataButton', {
73
- dataName,
74
- })
75
- "
76
- variant="elevated"
77
- color="success"
78
- />
79
- </v-btn-group>
80
- </div>
81
- </template>
82
- </v-toolbar>
83
-
54
+ </slot>
55
+ <slot
56
+ name="toolbar-actions-trigger"
57
+ v-bind="actionsTriggerSlotProps"
58
+ >
59
+ <v-menu
60
+ v-if="showActionsOverflowMenu"
61
+ v-model="isActionsMenuOpen"
62
+ location="bottom end"
63
+ >
64
+ <template #activator="{ props: menuProps }">
65
+ <v-btn
66
+ v-bind="menuProps"
67
+ icon="mdi-dots-vertical"
68
+ variant="text"
69
+ density="comfortable"
70
+ :aria-label="$t('core.baseDataTable.actionsMenu')"
71
+ />
72
+ </template>
73
+ <v-list density="compact" nav min-width="260">
74
+ <slot
75
+ name="action.export-mode"
76
+ v-bind="exportModeActionSlotProps"
77
+ >
78
+ <v-list-item
79
+ v-if="showExport"
80
+ :title="
81
+ isExportMode
82
+ ? $t('core.baseDataTable.disableExportMode')
83
+ : $t('core.baseDataTable.enableExportMode')
84
+ "
85
+ prepend-icon="mdi-checkbox-marked-outline"
86
+ @click="toggleExportMode"
87
+ />
88
+ </slot>
89
+ <slot name="action.import" v-bind="importActionSlotProps">
90
+ <v-list-item
91
+ v-if="onFileLoaded"
92
+ :title="$t('core.import')"
93
+ prepend-icon="mdi-database-import"
94
+ @click="importFileUploadRef?.openPicker()"
95
+ />
96
+ </slot>
97
+ <slot
98
+ name="toolbar-actions-menu"
99
+ v-bind="menuActionSlotProps"
100
+ />
101
+ </v-list>
102
+ </v-menu>
103
+ </slot>
104
+ </div>
105
+ </template>
106
+ </v-toolbar>
107
+
108
+ <slot name="toolbar-search">
84
109
  <v-text-field
85
110
  v-model="search"
86
111
  :label="
@@ -93,135 +118,169 @@
93
118
  hide-details
94
119
  single-line
95
120
  density="compact"
121
+ class="tw:mt-2 tw:mb-4"
96
122
  />
97
- </template>
123
+ </slot>
124
+
125
+ <FileUpload
126
+ v-if="onFileLoaded"
127
+ ref="importFileUploadRef"
128
+ :on-file-loaded="onFileLoaded"
129
+ :accept="importAccept"
130
+ :read-as="importReadAs"
131
+ />
98
132
 
99
- <template
100
- #item.color="{ item }"
101
- v-if="!customSlots.includes('item.color')"
102
- >
103
- <div class="tw:flex tw:gap-2" v-if="item.color?.length">
104
- <v-chip :color="item.color" label variant="outlined" size="small">
105
- {{ item.color }}
106
- </v-chip>
107
- </div>
108
- </template>
109
- <!-- Built-in tags column -->
110
- <template
111
- #item.tags="{ item }"
112
- v-if="!customSlots.includes('item.tags')"
113
- >
114
- <div class="tw:flex tw:gap-2" v-if="item.tags?.length">
115
- <v-chip
116
- v-for="tag in item.tags"
117
- :key="tag._id || tag.id || tag"
118
- :color="tag.color"
119
- label
120
- variant="outlined"
121
- size="small"
122
- >
123
- {{ tag.title || tag.name || tag }}
124
- </v-chip>
125
- </div>
126
- </template>
127
-
128
- <!-- Dynamic slots for custom column rendering -->
129
- <template v-for="slot in customSlots" :key="slot" #[slot]="slotProps">
130
- <slot :name="slot" v-bind="slotProps" />
131
- </template>
132
-
133
- <!-- Actions column template -->
134
- <template #item.actions="{ item }" v-if="showActionsColumn">
135
- <slot name="prepend-row-actions" :item="item"> </slot>
136
- <slot name="row-actions" :item="item">
137
- <!-- Default delete action -->
138
- <v-icon
139
- v-if="showDeleteAction && !isDeleteDisabled(item)"
140
- color="red"
141
- @click.stop.prevent="handleDelete(item)"
142
- class="mr-2"
143
- :title="
144
- $t('core.baseDataTable.deleteDataTitle', {
145
- dataName,
146
- })
147
- "
148
- icon="mdi-delete"
149
- />
150
- </slot>
151
- <slot name="append-row-actions" :item="item"> </slot>
152
- </template>
153
-
154
- <!-- Loading template -->
155
- <template #loader>
156
- <v-skeleton-loader
157
- class="mx-auto border"
158
- :height="500"
159
- width="100%"
160
- type="list-item"
161
- :loading="true"
133
+ <slot name="importer" v-bind="importerSlotProps" />
134
+ <slot name="exporter" v-bind="exporterSlotProps">
135
+ <DataExporter
136
+ v-if="showExport && isExportMode"
137
+ :data="exportPayloadData"
138
+ :disabled="!selected.length"
139
+ :formats="exportFormats"
140
+ :default-format="exportDefaultFormat"
141
+ :button-label="$t('core.export')"
162
142
  />
163
- </template>
164
-
165
- <!-- Additional table slots (this caused the table to be empty) -->
166
- <!-- <slot name="table-slots" /> -->
167
- </v-data-table>
168
- </v-sheet>
169
- <!-- Delete Confirmation Dialog -->
170
- <v-dialog
171
- v-model="deleteDialog"
172
- max-width="400"
173
- v-if="showDeleteAction && itemToDelete"
174
- >
175
- <v-card>
176
- <v-card-title class="text-h6">
177
- {{
178
- $t("core.baseDataTable.deleteDataTitle", {
179
- dataName,
180
- })
181
- }}
182
- </v-card-title>
183
-
184
- <v-card-text
185
- v-html="
186
- $t('core.baseDataTable.deleteDataConfirmMessage', {
187
- article: dataArticle,
188
- dataName,
189
- value:
190
- itemToDelete.email ||
191
- itemToDelete.title ||
192
- itemToDelete.name ||
193
- itemToDelete._id,
194
- })
195
- "
143
+ </slot>
144
+ </template>
145
+
146
+ <template
147
+ #item.color="{ item }"
148
+ v-if="!customSlots.includes('item.color')"
149
+ >
150
+ <div class="tw:flex tw:gap-2" v-if="item.color?.length">
151
+ <v-chip :color="item.color" label variant="outlined" size="small">
152
+ {{ item.color }}
153
+ </v-chip>
154
+ </div>
155
+ </template>
156
+ <!-- Built-in tags column -->
157
+ <template #item.tags="{ item }" v-if="!customSlots.includes('item.tags')">
158
+ <div class="tw:flex tw:gap-2" v-if="item.tags?.length">
159
+ <v-chip
160
+ v-for="tag in item.tags"
161
+ :key="tag._id || tag.id || tag"
162
+ :color="tag.color"
163
+ label
164
+ variant="outlined"
165
+ size="small"
166
+ >
167
+ {{ tag.title || tag.name || tag }}
168
+ </v-chip>
169
+ </div>
170
+ </template>
171
+
172
+ <!-- Dynamic slots for custom column rendering -->
173
+ <template v-for="slot in customSlots" :key="slot" #[slot]="slotProps">
174
+ <slot :name="slot" v-bind="slotProps" />
175
+ </template>
176
+
177
+ <!-- Actions column template -->
178
+ <template #item.actions="{ item }" v-if="showActionsColumn">
179
+ <slot name="prepend-row-actions" :item="item"> </slot>
180
+ <slot name="row-actions" :item="item" />
181
+ <slot name="append-row-actions" :item="item"> </slot>
182
+ </template>
183
+
184
+ <!-- Loading template -->
185
+ <template #loader>
186
+ <v-skeleton-loader
187
+ class="mx-auto border"
188
+ :height="500"
189
+ width="100%"
190
+ type="list-item"
191
+ :loading="true"
196
192
  />
197
- <v-card-actions>
198
- <v-spacer />
199
- <v-btn variant="text" @click="deleteDialog = false">
200
- {{ $t("core.baseDataTable.deleteDataConfirmCancel") }}
201
- </v-btn>
202
- <v-btn color="red" variant="flat" @click="confirmDelete">
203
- {{ $t("core.baseDataTable.deleteDataConfirmButton") }}
204
- </v-btn>
205
- </v-card-actions>
206
- </v-card>
207
- </v-dialog>
193
+ </template>
208
194
 
195
+ <!-- Additional table slots (this caused the table to be empty) -->
196
+ <!-- <slot name="table-slots" /> -->
197
+ </v-data-table>
209
198
  <!-- Additional dialogs slot -->
210
199
  <slot name="dialogs" />
200
+
201
+ <v-navigation-drawer
202
+ v-model="isFilterDrawerOpen"
203
+ location="right"
204
+ temporary
205
+ width="360"
206
+ :aria-label="$t('core.baseDataTable.filters')"
207
+ >
208
+ <div class="tw:flex tw:flex-col tw:h-full">
209
+ <slot name="filters.summary" v-bind="filterSummarySlotProps" />
210
+ <slot name="filters.drawer.header" v-bind="filterDrawerHeaderSlotProps">
211
+ <div
212
+ class="tw:flex tw:items-center tw:justify-between tw:px-4 tw:py-3"
213
+ >
214
+ <span class="tw:font-semibold">{{
215
+ $t("core.baseDataTable.filters")
216
+ }}</span>
217
+ <v-btn
218
+ icon="mdi-close"
219
+ size="small"
220
+ variant="text"
221
+ :aria-label="$t('core.baseDataTable.closeFilters')"
222
+ @click="isFilterDrawerOpen = false"
223
+ />
224
+ </div>
225
+ </slot>
226
+
227
+ <slot name="filters.drawer.body" v-bind="filterDrawerBodySlotProps">
228
+ <div class="tw:px-4 tw:pb-4 tw:flex tw:flex-col tw:gap-3">
229
+ <v-text-field
230
+ v-for="header in filterableHeaders"
231
+ :key="String(header.key)"
232
+ v-model="columnFilters[String(header.key)]"
233
+ @update:model-value="notifyFilterStateChange"
234
+ :label="header.title"
235
+ clearable
236
+ hide-details
237
+ density="compact"
238
+ variant="outlined"
239
+ />
240
+ </div>
241
+ </slot>
242
+
243
+ <div class="tw:mt-auto tw:px-4 tw:pb-4 tw:pt-2 tw:flex tw:gap-2">
244
+ <slot
245
+ name="filters.drawer.footer"
246
+ v-bind="filterDrawerFooterSlotProps"
247
+ >
248
+ <v-btn
249
+ variant="outlined"
250
+ prepend-icon="mdi-filter-remove"
251
+ :text="$t('core.baseDataTable.resetFilters')"
252
+ @click="resetFilters"
253
+ />
254
+ <v-btn
255
+ color="primary"
256
+ variant="flat"
257
+ prepend-icon="mdi-check"
258
+ :text="$t('core.baseDataTable.applyFilters')"
259
+ @click="applyFilters"
260
+ />
261
+ </slot>
262
+ </div>
263
+ </div>
264
+ </v-navigation-drawer>
211
265
  </div>
212
266
  </template>
213
267
 
214
268
  <script setup lang="ts" generic="T extends Record<string, any>">
215
269
  import { computed, ref, useSlots, useTemplateRef } from "vue"
216
- import { useRouteState } from "@raclettejs/core/orchestrator/composables"
217
- import DataExporter from "@raclettejs/core/orchestrator/components/dataExport/DataExporter.vue"
218
- import FileUpload from "@raclettejs/core/orchestrator/components/input/FileUpload.vue"
270
+ import DataExporter from "@racletteOrchestrator/components/dataExport/DataExporter.vue"
271
+ import FileUpload from "@racletteOrchestrator/components/input/FileUpload.vue"
219
272
  import type {
220
273
  ExportPayload,
221
274
  FormatEntry,
222
275
  } from "@racletteOrchestrator/composables/useExport/types"
223
276
  import type { DataTableItem } from "vuetify/lib/components/VDataTable/types"
224
277
 
278
+ export interface BaseDataTableActionContext<T> {
279
+ item?: T
280
+ filters: Record<string, string>
281
+ selectedIds: Array<string | number>
282
+ }
283
+
225
284
  export interface BaseDataTableProps<T> {
226
285
  uuid?: string
227
286
  // Data props
@@ -238,27 +297,19 @@ export interface BaseDataTableProps<T> {
238
297
  }>
239
298
  itemsPerPage?: number
240
299
 
241
- // Create button
242
- showCreateButton?: boolean
243
- createInteractionLinkId?: string
244
-
245
300
  // Row actions
246
301
  showActionsColumn?: boolean
247
- showDeleteAction?: boolean
248
-
249
- // Navigation
250
- editInteractionLinkId?: string
251
302
 
252
303
  // Item processing
253
- deleteValidator?: (item: T) => boolean
254
- rowClickHandler?: (item: T) => void
304
+ rowClickHandler?: (item: T, context?: BaseDataTableActionContext<T>) => void
255
305
 
256
306
  dataName: string
257
- dataArticle: string
258
307
 
259
308
  showExport?: boolean
309
+ showFilters?: boolean
260
310
 
261
311
  itemValue?: string
312
+ actionsHeaderTitle?: string
262
313
 
263
314
  /**
264
315
  * When set, exported rows come from here (matched to table selection via `itemValue`, default `_id`).
@@ -275,6 +326,19 @@ export interface BaseDataTableProps<T> {
275
326
  onFileLoaded?: (content: string | ArrayBuffer, file: File) => void
276
327
  importAccept?: string
277
328
  importReadAs?: "text" | "arrayBuffer"
329
+
330
+ onFilterStateChange?: (
331
+ payload: Record<string, string>,
332
+ context: BaseDataTableActionContext<T>,
333
+ ) => void
334
+ onFilterApply?: (
335
+ payload: Record<string, string>,
336
+ context: BaseDataTableActionContext<T>,
337
+ ) => void
338
+ onFilterReset?: (
339
+ payload: Record<string, string>,
340
+ context: BaseDataTableActionContext<T>,
341
+ ) => void
278
342
  }
279
343
 
280
344
  const props = withDefaults(defineProps<BaseDataTableProps<T>>(), {
@@ -282,11 +346,11 @@ const props = withDefaults(defineProps<BaseDataTableProps<T>>(), {
282
346
  items: () => [],
283
347
  itemsDeleted: false,
284
348
  itemsPerPage: 50,
285
- showCreateButton: true,
286
349
  showActionsColumn: true,
287
- showDeleteAction: true,
288
350
  showExport: true,
351
+ showFilters: false,
289
352
  itemValue: "_id",
353
+ actionsHeaderTitle: "",
290
354
  exportableItems: undefined,
291
355
  exportMeta: undefined,
292
356
  exportFormats: () => ["json"] as FormatEntry[],
@@ -294,18 +358,28 @@ const props = withDefaults(defineProps<BaseDataTableProps<T>>(), {
294
358
  onFileLoaded: undefined,
295
359
  importAccept: ".json",
296
360
  importReadAs: "text",
361
+ onFilterStateChange: undefined,
362
+ onFilterApply: undefined,
363
+ onFilterReset: undefined,
297
364
  })
298
365
 
299
366
  const emit = defineEmits<{
300
- create: []
301
- edit: [item: T]
302
- delete: [item: T]
303
367
  rowClick: [item: T]
304
368
  "update:currentItems": [items: readonly DataTableItem<T>[]]
305
369
  }>()
306
370
 
307
371
  const slots = useSlots()
308
- const { triggerInteractionLinkById } = useRouteState()
372
+
373
+ /** Hide ⋮ when the menu would be empty (no export toggle, import, or slot content). */
374
+ const showActionsOverflowMenu = computed(
375
+ () =>
376
+ Boolean(props.showExport) ||
377
+ Boolean(props.onFileLoaded) ||
378
+ Boolean(slots["toolbar-actions-menu"]) ||
379
+ Boolean(slots["action.export-mode"]) ||
380
+ Boolean(slots["action.import"]),
381
+ )
382
+
309
383
  const importFileUploadRef = useTemplateRef<{
310
384
  openPicker: () => void
311
385
  }>("importFileUploadRef")
@@ -313,20 +387,40 @@ const importFileUploadRef = useTemplateRef<{
313
387
  // Reactive data
314
388
  const search = ref("")
315
389
  const selected = ref<Array<string | number>>([])
316
- const deleteDialog = ref(false)
317
- const itemToDelete = ref<T | null>(null)
390
+ const isExportMode = ref(false)
391
+ const isActionsMenuOpen = ref(false)
392
+ const isFilterDrawerOpen = ref(false)
393
+ const columnFilters = ref<Record<string, string>>({})
318
394
 
319
395
  const computedHeaders = computed(() => {
320
396
  if (!props.headers || !props.headers?.length) {
321
397
  return null
322
398
  }
323
399
 
324
- const headers = [...props.headers]
400
+ const headers = [...props.headers].map((header) => {
401
+ const key = String(header.key)
402
+ const hasFilter =
403
+ props.showFilters && columnFilters.value[key]?.trim()?.length > 0
404
+ return {
405
+ ...header,
406
+ title: header.title,
407
+ ...(hasFilter
408
+ ? {
409
+ headerProps: {
410
+ class: "filtered-column-header",
411
+ },
412
+ cellProps: {
413
+ class: "filtered-column-cell",
414
+ },
415
+ }
416
+ : {}),
417
+ }
418
+ })
325
419
 
326
420
  // Add actions column if needed
327
421
  if (props.showActionsColumn) {
328
422
  headers.push({
329
- title: "Actions",
423
+ title: props.actionsHeaderTitle || "Actions",
330
424
  key: "actions",
331
425
  align: "end" as const,
332
426
  sortable: false,
@@ -348,6 +442,37 @@ const exportColumnKeys = computed(() => {
348
442
  .filter((key) => key.length > 0 && key !== "actions")
349
443
  })
350
444
 
445
+ const filterableHeaders = computed(() =>
446
+ (props.headers ?? []).filter((header) => {
447
+ const key = String(header.key)
448
+ return key.length > 0 && key !== "actions"
449
+ }),
450
+ )
451
+
452
+ const filteredItems = computed(() => {
453
+ const rows = props.items ?? []
454
+ if (!props.showFilters) {
455
+ return rows
456
+ }
457
+
458
+ const activeFilters = Object.entries(columnFilters.value).filter(
459
+ ([, value]) => value?.trim().length > 0,
460
+ )
461
+ if (!activeFilters.length) {
462
+ return rows
463
+ }
464
+
465
+ return rows.filter((row) =>
466
+ activeFilters.every(([key, filterValue]) => {
467
+ const raw = row[key as keyof T]
468
+ if (raw === undefined || raw === null) {
469
+ return false
470
+ }
471
+ return String(raw).toLowerCase().includes(filterValue.toLowerCase())
472
+ }),
473
+ )
474
+ })
475
+
351
476
  const projectRowsToVisibleColumns = <TRow extends Record<string, unknown>>(
352
477
  rows: TRow[],
353
478
  ) => {
@@ -424,53 +549,205 @@ const exportPayloadData = computed<ExportPayload>(() => {
424
549
  })
425
550
 
426
551
  // Methods
427
- const handleCreate = () => {
428
- if (props.createInteractionLinkId) {
429
- triggerInteractionLinkById(props.createInteractionLinkId)
552
+ const toggleExportMode = () => {
553
+ isExportMode.value = !isExportMode.value
554
+ if (!isExportMode.value) {
555
+ selected.value = []
430
556
  }
431
- emit("create")
432
557
  }
433
558
 
434
- const handleRowClick = (_: PointerEvent, row: { item: T }) => {
435
- if (props.rowClickHandler) {
436
- props.rowClickHandler(row.item)
437
- return
438
- }
559
+ const toggleFilterDrawer = () => {
560
+ isFilterDrawerOpen.value = !isFilterDrawerOpen.value
561
+ }
439
562
 
440
- if (props.editInteractionLinkId) {
441
- const item = row.item as any
563
+ const openFilters = () => {
564
+ isFilterDrawerOpen.value = true
565
+ }
442
566
 
443
- triggerInteractionLinkById(props.editInteractionLinkId, {
444
- id: item._id,
445
- })
567
+ const handleRowClick = (_: PointerEvent, row: { item: T }) => {
568
+ const context = actionContext(row.item)
569
+ if (props.rowClickHandler) {
570
+ props.rowClickHandler(row.item, context)
446
571
  }
447
572
 
448
- emit("edit", row.item)
449
573
  emit("rowClick", row.item)
450
574
  }
451
575
 
452
- const handleDelete = (item: T) => {
453
- itemToDelete.value = item
454
- deleteDialog.value = true
455
- }
576
+ const filterPayload = computed(() => ({ ...columnFilters.value }))
577
+ const actionContext = (item?: T): BaseDataTableActionContext<T> => ({
578
+ item,
579
+ filters: filterPayload.value,
580
+ selectedIds: [...selected.value],
581
+ })
456
582
 
457
- const confirmDelete = () => {
458
- if (itemToDelete.value) {
459
- emit("delete", itemToDelete.value)
583
+ const notifyFilterStateChange = () => {
584
+ if (props.onFilterStateChange) {
585
+ props.onFilterStateChange(filterPayload.value, actionContext())
586
+ } else {
587
+ console.log("[BaseDataTable] filter-state-change", filterPayload.value)
460
588
  }
589
+ }
461
590
 
462
- itemToDelete.value = null
463
- deleteDialog.value = false
591
+ const applyFilters = () => {
592
+ if (props.onFilterApply) {
593
+ props.onFilterApply(filterPayload.value, actionContext())
594
+ } else {
595
+ console.log("[BaseDataTable] filter-apply", filterPayload.value)
596
+ }
464
597
  }
465
598
 
466
- const isDeleteDisabled = (item: T): boolean => {
467
- return props.deleteValidator ? !props.deleteValidator(item) : false
599
+ const resetFilters = () => {
600
+ columnFilters.value = {}
601
+ if (props.onFilterReset) {
602
+ props.onFilterReset({}, actionContext())
603
+ } else {
604
+ console.log("[BaseDataTable] filter-reset", {})
605
+ }
468
606
  }
607
+
608
+ const actionsTriggerSlotProps = computed(() => ({
609
+ isOpen: isActionsMenuOpen.value,
610
+ toggle: () => {
611
+ isActionsMenuOpen.value = !isActionsMenuOpen.value
612
+ },
613
+ }))
614
+
615
+ const menuActionSlotProps = computed(() => ({
616
+ isExportMode: isExportMode.value,
617
+ toggleExportMode,
618
+ isFilterDrawerOpen: isFilterDrawerOpen.value,
619
+ toggleFilterDrawer,
620
+ openFilters,
621
+ canExport: props.showExport,
622
+ canImport: Boolean(props.onFileLoaded),
623
+ openImportPicker: () => importFileUploadRef.value?.openPicker(),
624
+ context: actionContext(),
625
+ }))
626
+
627
+ const exportModeActionSlotProps = computed(() => ({
628
+ active: isExportMode.value,
629
+ enabled: props.showExport,
630
+ toggle: toggleExportMode,
631
+ }))
632
+
633
+ const importActionSlotProps = computed(() => ({
634
+ enabled: Boolean(props.onFileLoaded),
635
+ openPicker: () => importFileUploadRef.value?.openPicker(),
636
+ }))
637
+
638
+ const filterTriggerSlotProps = computed(() => ({
639
+ isFilterDrawerOpen: isFilterDrawerOpen.value,
640
+ openFilters,
641
+ toggleFilterDrawer,
642
+ }))
643
+
644
+ const exporterSlotProps = computed(() => ({
645
+ data: exportPayloadData.value,
646
+ disabled: !selected.value.length,
647
+ formats: props.exportFormats,
648
+ defaultFormat: props.exportDefaultFormat,
649
+ isExportMode: isExportMode.value,
650
+ selectedCount: selected.value.length,
651
+ }))
652
+
653
+ const importerSlotProps = computed(() => ({
654
+ openPicker: () => importFileUploadRef.value?.openPicker(),
655
+ accept: props.importAccept,
656
+ readAs: props.importReadAs,
657
+ enabled: Boolean(props.onFileLoaded),
658
+ }))
659
+
660
+ const filterSummarySlotProps = computed(() => ({
661
+ filters: filterPayload.value,
662
+ activeCount: Object.values(columnFilters.value).filter(
663
+ (value) => value?.trim().length > 0,
664
+ ).length,
665
+ }))
666
+
667
+ const filterDrawerHeaderSlotProps = computed(() => ({
668
+ close: () => {
669
+ isFilterDrawerOpen.value = false
670
+ },
671
+ filters: filterPayload.value,
672
+ }))
673
+
674
+ const filterDrawerBodySlotProps = computed(() => ({
675
+ filters: columnFilters.value,
676
+ headers: filterableHeaders.value,
677
+ setFilter: (key: string, value: string) => {
678
+ columnFilters.value[key] = value
679
+ notifyFilterStateChange()
680
+ },
681
+ }))
682
+
683
+ const filterDrawerFooterSlotProps = computed(() => ({
684
+ applyFilters,
685
+ resetFilters,
686
+ filters: filterPayload.value,
687
+ context: actionContext(),
688
+ }))
469
689
  </script>
470
690
 
471
691
  <style lang="css">
692
+ /*
693
+ * VToolbar pins .v-toolbar__content to a fixed height (density-compact ≈ 48px)
694
+ * and sets overflow: hidden, which clips wrapped toolbar controls when the
695
+ * table lives in a narrow container (not just small viewports).
696
+ */
697
+ .base-data-table-toolbar .v-toolbar__content {
698
+ height: auto !important;
699
+ min-height: 48px;
700
+ overflow: visible;
701
+ flex-wrap: wrap;
702
+ row-gap: 8px;
703
+ column-gap: 8px;
704
+ justify-content: space-between;
705
+ align-items: center;
706
+ }
707
+
708
+ /*
709
+ * Vuetify VToolbar.sass: .v-toolbar__content > .v-toolbar-title {
710
+ * margin-inline-start: $toolbar-title-margin // _variables.scss → 20px
711
+ * }
712
+ */
713
+ .base-data-table-toolbar .v-toolbar__content > .v-toolbar-title {
714
+ margin-inline-start: 0;
715
+ }
716
+
717
+ .base-data-table-toolbar .v-toolbar__append {
718
+ flex: 1 1 auto;
719
+ min-width: 0;
720
+ }
721
+
722
+ /*
723
+ * Default VToolbar gives the title flex: 1 1 so it steals shrink and ellipsizes
724
+ * before the append block wraps. Prefer capping the title width so actions keep
725
+ * horizontal room; headline text aligns to the end of the title cell.
726
+ */
727
+ .base-data-table-toolbar .v-toolbar-title {
728
+ display: flex;
729
+ flex-direction: row;
730
+ align-items: center;
731
+ justify-content: flex-end;
732
+ flex: 0 1 auto;
733
+ min-width: 0;
734
+ max-width: min(45%, 448px);
735
+ overflow: hidden;
736
+ text-overflow: ellipsis;
737
+ white-space: nowrap;
738
+ }
739
+
472
740
  .itemsDeleted .v-data-table__tr {
473
741
  background-color: rgb(233, 190, 190);
474
742
  color: #000;
475
743
  }
744
+
745
+ .filtered-column-header {
746
+ background-color: rgba(var(--v-theme-info), 0.18);
747
+ box-shadow: inset 0 -2px 0 rgba(var(--v-theme-info), 0.9);
748
+ }
749
+
750
+ .filtered-column-cell {
751
+ background-color: rgba(var(--v-theme-info), 0.1);
752
+ }
476
753
  </style>