@raclettejs/core 0.1.33-canary.2 → 0.1.33

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,23 +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
- :class="{ itemsDeleted }"
16
- :show-select="showExport"
17
- :item-value
18
- >
19
- <template #top>
20
- <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">
21
26
  <v-toolbar-title v-if="dataName?.length">
22
27
  <v-icon
23
28
  color="medium-emphasis"
@@ -25,61 +30,82 @@
25
30
  size="x-small"
26
31
  start
27
32
  />
28
-
29
33
  {{ dataName }}
30
34
  </v-toolbar-title>
35
+ </slot>
31
36
 
32
- <template #append>
33
- <div
34
- 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"
35
46
  >
36
- <slot name="actions" />
37
- <slot name="toolbar-leading" />
38
- <FileUpload
39
- v-if="onFileLoaded"
40
- ref="importFileUploadRef"
41
- :on-file-loaded="onFileLoaded"
42
- :accept="importAccept"
43
- :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"
44
53
  />
45
- <v-btn-group density="compact" divided>
46
- <DataExporter
47
- v-if="showExport"
48
- :data="exportPayloadData"
49
- :disabled="!selected.length"
50
- :formats="exportFormats"
51
- :default-format="exportDefaultFormat"
52
- :button-label="$t('core.export')"
53
- color="info"
54
- variant="elevated"
55
- />
56
- <v-btn
57
- v-if="onFileLoaded"
58
- color="teal"
59
- variant="elevated"
60
- density="compact"
61
- prepend-icon="mdi-database-import"
62
- :text="$t('core.import')"
63
- @click="importFileUploadRef?.openPicker()"
64
- />
65
- <v-btn
66
- v-if="showCreateButton"
67
- @click="handleCreate"
68
- prepend-icon="mdi-plus"
69
- density="compact"
70
- :text="
71
- $t('core.baseDataTable.createDataButton', {
72
- dataName,
73
- })
74
- "
75
- variant="elevated"
76
- color="success"
77
- />
78
- </v-btn-group>
79
- </div>
80
- </template>
81
- </v-toolbar>
82
-
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">
83
109
  <v-text-field
84
110
  v-model="search"
85
111
  :label="
@@ -92,133 +118,168 @@
92
118
  hide-details
93
119
  single-line
94
120
  density="compact"
121
+ class="tw:mt-2 tw:mb-4"
95
122
  />
96
- </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
+ />
97
132
 
98
- <template
99
- #item.color="{ item }"
100
- v-if="!customSlots.includes('item.color')"
101
- >
102
- <div class="tw:flex tw:gap-2" v-if="item.color?.length">
103
- <v-chip :color="item.color" label variant="outlined" size="small">
104
- {{ item.color }}
105
- </v-chip>
106
- </div>
107
- </template>
108
- <!-- Built-in tags column -->
109
- <template
110
- #item.tags="{ item }"
111
- v-if="!customSlots.includes('item.tags')"
112
- >
113
- <div class="tw:flex tw:gap-2" v-if="item.tags?.length">
114
- <v-chip
115
- v-for="tag in item.tags"
116
- :key="tag._id || tag.id || tag"
117
- :color="tag.color"
118
- label
119
- variant="outlined"
120
- size="small"
121
- >
122
- {{ tag.title || tag.name || tag }}
123
- </v-chip>
124
- </div>
125
- </template>
126
-
127
- <!-- Dynamic slots for custom column rendering -->
128
- <template v-for="slot in customSlots" :key="slot" #[slot]="slotProps">
129
- <slot :name="slot" v-bind="slotProps" />
130
- </template>
131
-
132
- <!-- Actions column template -->
133
- <template #item.actions="{ item }" v-if="showActionsColumn">
134
- <slot name="prepend-row-actions" :item="item"> </slot>
135
- <slot name="row-actions" :item="item">
136
- <!-- Default delete action -->
137
- <v-icon
138
- v-if="showDeleteAction && !isDeleteDisabled(item)"
139
- color="red"
140
- @click.stop.prevent="handleDelete(item)"
141
- class="mr-2"
142
- :title="
143
- $t('core.baseDataTable.deleteDataTitle', {
144
- dataName,
145
- })
146
- "
147
- icon="mdi-delete"
148
- />
149
- </slot>
150
- <slot name="append-row-actions" :item="item"> </slot>
151
- </template>
152
-
153
- <!-- Loading template -->
154
- <template #loader>
155
- <v-skeleton-loader
156
- class="mx-auto border"
157
- :height="500"
158
- width="100%"
159
- type="list-item"
160
- :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')"
161
142
  />
162
- </template>
163
-
164
- <!-- Additional table slots (this caused the table to be empty) -->
165
- <!-- <slot name="table-slots" /> -->
166
- </v-data-table>
167
- </v-sheet>
168
- <!-- Delete Confirmation Dialog -->
169
- <v-dialog
170
- v-model="deleteDialog"
171
- max-width="400"
172
- v-if="showDeleteAction && itemToDelete"
173
- >
174
- <v-card>
175
- <v-card-title class="text-h6">
176
- {{
177
- $t("core.baseDataTable.deleteDataTitle", {
178
- dataName,
179
- })
180
- }}
181
- </v-card-title>
182
-
183
- <v-card-text
184
- v-html="
185
- $t('core.baseDataTable.deleteDataConfirmMessage', {
186
- article: dataArticle,
187
- dataName,
188
- value:
189
- itemToDelete.email ||
190
- itemToDelete.title ||
191
- itemToDelete.name ||
192
- itemToDelete._id,
193
- })
194
- "
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"
195
192
  />
196
- <v-card-actions>
197
- <v-spacer />
198
- <v-btn variant="text" @click="deleteDialog = false">
199
- {{ $t("core.baseDataTable.deleteDataConfirmCancel") }}
200
- </v-btn>
201
- <v-btn color="red" variant="flat" @click="confirmDelete">
202
- {{ $t("core.baseDataTable.deleteDataConfirmButton") }}
203
- </v-btn>
204
- </v-card-actions>
205
- </v-card>
206
- </v-dialog>
193
+ </template>
207
194
 
195
+ <!-- Additional table slots (this caused the table to be empty) -->
196
+ <!-- <slot name="table-slots" /> -->
197
+ </v-data-table>
208
198
  <!-- Additional dialogs slot -->
209
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>
210
265
  </div>
211
266
  </template>
212
267
 
213
268
  <script setup lang="ts" generic="T extends Record<string, any>">
214
269
  import { computed, ref, useSlots, useTemplateRef } from "vue"
215
- import { useRouteState } from "@raclettejs/core/orchestrator/composables"
216
- import DataExporter from "@raclettejs/core/orchestrator/components/dataExport/DataExporter.vue"
217
- 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"
218
272
  import type {
219
273
  ExportPayload,
220
274
  FormatEntry,
221
275
  } from "@racletteOrchestrator/composables/useExport/types"
276
+ import type { DataTableItem } from "vuetify/lib/components/VDataTable/types"
277
+
278
+ export interface BaseDataTableActionContext<T> {
279
+ item?: T
280
+ filters: Record<string, string>
281
+ selectedIds: Array<string | number>
282
+ }
222
283
 
223
284
  export interface BaseDataTableProps<T> {
224
285
  uuid?: string
@@ -236,27 +297,19 @@ export interface BaseDataTableProps<T> {
236
297
  }>
237
298
  itemsPerPage?: number
238
299
 
239
- // Create button
240
- showCreateButton?: boolean
241
- createInteractionLinkId?: string
242
-
243
300
  // Row actions
244
301
  showActionsColumn?: boolean
245
- showDeleteAction?: boolean
246
-
247
- // Navigation
248
- editInteractionLinkId?: string
249
302
 
250
303
  // Item processing
251
- deleteValidator?: (item: T) => boolean
252
- rowClickHandler?: (item: T) => void
304
+ rowClickHandler?: (item: T, context?: BaseDataTableActionContext<T>) => void
253
305
 
254
306
  dataName: string
255
- dataArticle: string
256
307
 
257
308
  showExport?: boolean
309
+ showFilters?: boolean
258
310
 
259
311
  itemValue?: string
312
+ actionsHeaderTitle?: string
260
313
 
261
314
  /**
262
315
  * When set, exported rows come from here (matched to table selection via `itemValue`, default `_id`).
@@ -273,6 +326,19 @@ export interface BaseDataTableProps<T> {
273
326
  onFileLoaded?: (content: string | ArrayBuffer, file: File) => void
274
327
  importAccept?: string
275
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
276
342
  }
277
343
 
278
344
  const props = withDefaults(defineProps<BaseDataTableProps<T>>(), {
@@ -280,11 +346,11 @@ const props = withDefaults(defineProps<BaseDataTableProps<T>>(), {
280
346
  items: () => [],
281
347
  itemsDeleted: false,
282
348
  itemsPerPage: 50,
283
- showCreateButton: true,
284
349
  showActionsColumn: true,
285
- showDeleteAction: true,
286
350
  showExport: true,
351
+ showFilters: false,
287
352
  itemValue: "_id",
353
+ actionsHeaderTitle: "",
288
354
  exportableItems: undefined,
289
355
  exportMeta: undefined,
290
356
  exportFormats: () => ["json"] as FormatEntry[],
@@ -292,17 +358,28 @@ const props = withDefaults(defineProps<BaseDataTableProps<T>>(), {
292
358
  onFileLoaded: undefined,
293
359
  importAccept: ".json",
294
360
  importReadAs: "text",
361
+ onFilterStateChange: undefined,
362
+ onFilterApply: undefined,
363
+ onFilterReset: undefined,
295
364
  })
296
365
 
297
366
  const emit = defineEmits<{
298
- create: []
299
- edit: [item: T]
300
- delete: [item: T]
301
367
  rowClick: [item: T]
368
+ "update:currentItems": [items: readonly DataTableItem<T>[]]
302
369
  }>()
303
370
 
304
371
  const slots = useSlots()
305
- 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
+
306
383
  const importFileUploadRef = useTemplateRef<{
307
384
  openPicker: () => void
308
385
  }>("importFileUploadRef")
@@ -310,20 +387,40 @@ const importFileUploadRef = useTemplateRef<{
310
387
  // Reactive data
311
388
  const search = ref("")
312
389
  const selected = ref<Array<string | number>>([])
313
- const deleteDialog = ref(false)
314
- 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>>({})
315
394
 
316
395
  const computedHeaders = computed(() => {
317
396
  if (!props.headers || !props.headers?.length) {
318
397
  return null
319
398
  }
320
399
 
321
- 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
+ })
322
419
 
323
420
  // Add actions column if needed
324
421
  if (props.showActionsColumn) {
325
422
  headers.push({
326
- title: "Actions",
423
+ title: props.actionsHeaderTitle || "Actions",
327
424
  key: "actions",
328
425
  align: "end" as const,
329
426
  sortable: false,
@@ -345,6 +442,37 @@ const exportColumnKeys = computed(() => {
345
442
  .filter((key) => key.length > 0 && key !== "actions")
346
443
  })
347
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
+
348
476
  const projectRowsToVisibleColumns = <TRow extends Record<string, unknown>>(
349
477
  rows: TRow[],
350
478
  ) => {
@@ -421,53 +549,205 @@ const exportPayloadData = computed<ExportPayload>(() => {
421
549
  })
422
550
 
423
551
  // Methods
424
- const handleCreate = () => {
425
- if (props.createInteractionLinkId) {
426
- triggerInteractionLinkById(props.createInteractionLinkId)
552
+ const toggleExportMode = () => {
553
+ isExportMode.value = !isExportMode.value
554
+ if (!isExportMode.value) {
555
+ selected.value = []
427
556
  }
428
- emit("create")
429
557
  }
430
558
 
431
- const handleRowClick = (_: PointerEvent, row: { item: T }) => {
432
- if (props.rowClickHandler) {
433
- props.rowClickHandler(row.item)
434
- return
435
- }
559
+ const toggleFilterDrawer = () => {
560
+ isFilterDrawerOpen.value = !isFilterDrawerOpen.value
561
+ }
436
562
 
437
- if (props.editInteractionLinkId) {
438
- const item = row.item as any
563
+ const openFilters = () => {
564
+ isFilterDrawerOpen.value = true
565
+ }
439
566
 
440
- triggerInteractionLinkById(props.editInteractionLinkId, {
441
- id: item._id,
442
- })
567
+ const handleRowClick = (_: PointerEvent, row: { item: T }) => {
568
+ const context = actionContext(row.item)
569
+ if (props.rowClickHandler) {
570
+ props.rowClickHandler(row.item, context)
443
571
  }
444
572
 
445
- emit("edit", row.item)
446
573
  emit("rowClick", row.item)
447
574
  }
448
575
 
449
- const handleDelete = (item: T) => {
450
- itemToDelete.value = item
451
- deleteDialog.value = true
452
- }
576
+ const filterPayload = computed(() => ({ ...columnFilters.value }))
577
+ const actionContext = (item?: T): BaseDataTableActionContext<T> => ({
578
+ item,
579
+ filters: filterPayload.value,
580
+ selectedIds: [...selected.value],
581
+ })
453
582
 
454
- const confirmDelete = () => {
455
- if (itemToDelete.value) {
456
- 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)
457
588
  }
589
+ }
458
590
 
459
- itemToDelete.value = null
460
- 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
+ }
461
597
  }
462
598
 
463
- const isDeleteDisabled = (item: T): boolean => {
464
- 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
+ }
465
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
+ }))
466
689
  </script>
467
690
 
468
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
+
469
740
  .itemsDeleted .v-data-table__tr {
470
741
  background-color: rgb(233, 190, 190);
471
742
  color: #000;
472
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
+ }
473
753
  </style>