@raclettejs/core 0.1.36 → 0.1.38

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 (39) hide show
  1. package/CHANGELOG.md +32 -7
  2. package/dist/cli.js +1 -1
  3. package/dist/cli.js.map +2 -2
  4. package/package.json +2 -2
  5. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/composition.schema.ts +10 -4
  6. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/interactionLink.schema.ts +4 -1
  7. package/services/backend/src/shared/schemas/core/CompositionCreate.schema.ts +4 -0
  8. package/services/backend/src/shared/schemas/core/InteractionLinkCreate.schema.ts +4 -0
  9. package/services/backend/src/shared/types/core/CompositionCreate.types.ts +1 -0
  10. package/services/backend/src/shared/types/core/InteractionLinkCreate.types.ts +1 -0
  11. package/services/frontend/src/core/lib/scheduleTask.ts +35 -0
  12. package/services/frontend/src/core/store/index.ts +5 -1
  13. package/services/frontend/src/core/store/reducers/compositionSlots/reducers.ts +1 -5
  14. package/services/frontend/src/core/store/reducers/data/reducers.ts +101 -65
  15. package/services/frontend/src/core/store/reducers/metaReducer.ts +23 -0
  16. package/services/frontend/src/core/store/reducers/notifications/reducers.ts +39 -47
  17. package/services/frontend/src/core/store/reducers/queries/effects.ts +55 -2
  18. package/services/frontend/src/core/store/reducers/queries/reducers.ts +60 -62
  19. package/services/frontend/src/core/store/reducers/queriesCache/effects.ts +105 -0
  20. package/services/frontend/src/core/store/reducers/queriesCache/index.ts +2 -1
  21. package/services/frontend/src/core/store/reducers/queriesCache/queryCacheHelper.ts +79 -9
  22. package/services/frontend/src/core/store/reducers/queriesCache/reducers.ts +95 -100
  23. package/services/frontend/src/orchestrator/components/CompositionLoadingState.vue +28 -0
  24. package/services/frontend/src/orchestrator/components/composition/CompositionOverlay.vue +5 -7
  25. package/services/frontend/src/orchestrator/components/composition/WidgetsLayoutLoader.vue +171 -39
  26. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTable.vue +336 -56
  27. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTableConfirmDeleteBtn.vue +27 -0
  28. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTableFilterDrawerShell.vue +72 -0
  29. package/services/frontend/src/orchestrator/components/index.ts +4 -0
  30. package/services/frontend/src/orchestrator/composables/index.ts +1 -0
  31. package/services/frontend/src/orchestrator/composables/useBaseDataTableDeleteConfirm.ts +23 -0
  32. package/services/frontend/src/orchestrator/composables/useVueWriteOperationHelper.ts +16 -4
  33. package/services/frontend/src/orchestrator/composables/useWidgetLifecycle.ts +53 -23
  34. package/services/frontend/src/orchestrator/constants/widgetSlotType.ts +4 -0
  35. package/services/frontend/src/orchestrator/i18n/de-DE.json +7 -1
  36. package/services/frontend/src/orchestrator/i18n/en-EU.json +7 -1
  37. package/services/frontend/src/orchestrator/i18n/sk.json +7 -1
  38. package/services/frontend/src/orchestrator/router/routerHooks/afterEach.ts +15 -9
  39. package/services/frontend/src/orchestrator/types/DataApi.ts +2 -1
@@ -1,5 +1,11 @@
1
1
  <template>
2
- <div>
2
+ <div
3
+ class="base-data-table-host tw:relative tw:min-h-0"
4
+ :class="{
5
+ 'base-data-table-host--filter-open':
6
+ isFilterDrawerContained && isFilterDrawerOpen,
7
+ }"
8
+ >
3
9
  <v-data-table
4
10
  v-model="selected"
5
11
  :search="showSearch ? search : undefined"
@@ -43,6 +49,22 @@
43
49
  class="tw:flex tw:min-w-0 tw:w-full tw:flex-wrap tw:items-center tw:justify-end tw:gap-2"
44
50
  >
45
51
  <slot name="toolbar-leading" />
52
+ <slot
53
+ name="toolbar-indicators"
54
+ v-bind="toolbarIndicatorsSlotProps"
55
+ >
56
+ <v-chip
57
+ v-for="indicator in toolbarIndicators"
58
+ :key="indicator.id"
59
+ :color="indicator.color"
60
+ :prepend-icon="indicator.icon"
61
+ label
62
+ size="small"
63
+ variant="tonal"
64
+ >
65
+ {{ indicator.label }}
66
+ </v-chip>
67
+ </slot>
46
68
  <slot name="toolbar-end" />
47
69
  <slot
48
70
  name="toolbar-filters-trigger"
@@ -68,11 +90,30 @@
68
90
  <template #activator="{ props: menuProps }">
69
91
  <v-btn
70
92
  v-bind="menuProps"
71
- icon="mdi-dots-vertical"
72
93
  variant="text"
73
94
  density="comfortable"
74
95
  :aria-label="$t('core.baseDataTable.actionsMenu')"
75
- />
96
+ >
97
+ <v-badge
98
+ v-if="hasToolbarIndicators"
99
+ dot
100
+ :color="toolbarIndicators[0]?.color ?? 'primary'"
101
+ :content="
102
+ toolbarIndicators.length > 1
103
+ ? toolbarIndicators.length
104
+ : undefined
105
+ "
106
+ >
107
+ <v-icon icon="mdi-dots-vertical" />
108
+ </v-badge>
109
+ <v-icon v-else icon="mdi-dots-vertical" />
110
+ <v-tooltip
111
+ v-if="hasToolbarIndicators"
112
+ activator="parent"
113
+ location="bottom"
114
+ :text="activeModesTooltip"
115
+ />
116
+ </v-btn>
76
117
  </template>
77
118
  <v-list density="compact" nav min-width="260">
78
119
  <slot
@@ -142,6 +183,7 @@
142
183
  :disabled="!selected.length"
143
184
  :formats="exportFormats"
144
185
  :default-format="exportDefaultFormat"
186
+ :show-data-scope-switch="exportShowDataScopeSwitch"
145
187
  :button-label="$t('core.export')"
146
188
  />
147
189
  </slot>
@@ -219,14 +261,14 @@
219
261
  <!-- Additional dialogs slot -->
220
262
  <slot name="dialogs" />
221
263
 
222
- <v-navigation-drawer
264
+ <BaseDataTableFilterDrawerShell
265
+ v-if="showFilters"
223
266
  v-model="isFilterDrawerOpen"
224
- location="right"
225
- temporary
226
- width="360"
267
+ :contained="isFilterDrawerContained"
268
+ :width="filterDrawerWidth"
227
269
  :aria-label="$t('core.baseDataTable.filters')"
228
270
  >
229
- <div class="tw:flex tw:flex-col tw:h-full">
271
+ <div class="tw:flex tw:flex-col tw:h-full tw:min-h-0">
230
272
  <slot name="filters.summary" v-bind="filterSummarySlotProps" />
231
273
  <slot name="filters.drawer.header" v-bind="filterDrawerHeaderSlotProps">
232
274
  <div
@@ -245,35 +287,47 @@
245
287
  </div>
246
288
  </slot>
247
289
 
248
- <slot name="filters.drawer.body" v-bind="filterDrawerBodySlotProps">
249
- <div class="tw:px-4 tw:pb-4 tw:flex tw:flex-col tw:gap-3">
250
- <template v-for="header in filterableHeaders" :key="String(header.key)">
251
- <v-autocomplete
252
- v-if="columnUsesComboboxFilter(String(header.key))"
253
- v-model="columnFilters[String(header.key)]"
254
- @update:model-value="notifyFilterStateChange"
255
- :items="filterOptionsByKey[String(header.key)] ?? []"
256
- :label="header.title"
257
- clearable
258
- hide-details
259
- density="compact"
260
- variant="outlined"
261
- />
262
- <v-text-field
263
- v-else
264
- v-model="columnFilters[String(header.key)]"
265
- @update:model-value="notifyFilterStateChange"
266
- :label="header.title"
267
- clearable
268
- hide-details
269
- density="compact"
270
- variant="outlined"
271
- />
272
- </template>
273
- </div>
274
- </slot>
290
+ <div class="tw:flex-1 tw:min-h-0 tw:overflow-y-auto">
291
+ <slot name="filters.drawer.body" v-bind="filterDrawerBodySlotProps">
292
+ <div class="tw:px-4 tw:pb-4 tw:flex tw:flex-col tw:gap-3">
293
+ <template
294
+ v-for="header in filterableHeaders"
295
+ :key="String(header.key)"
296
+ >
297
+ <v-autocomplete
298
+ v-if="columnUsesComboboxFilter(String(header.key))"
299
+ v-model="columnFilters[String(header.key)]"
300
+ @update:model-value="notifyFilterStateChange"
301
+ :items="filterOptionsByKey[String(header.key)] ?? []"
302
+ :label="header.title"
303
+ clearable
304
+ hide-details
305
+ density="compact"
306
+ variant="outlined"
307
+ />
308
+ <v-text-field
309
+ v-else
310
+ v-model="columnFilters[String(header.key)]"
311
+ @update:model-value="notifyFilterStateChange"
312
+ :label="header.title"
313
+ clearable
314
+ hide-details
315
+ density="compact"
316
+ variant="outlined"
317
+ />
318
+ </template>
319
+ </div>
320
+ </slot>
321
+ </div>
275
322
 
276
- <div class="tw:mt-auto tw:px-4 tw:pb-4 tw:pt-2 tw:flex tw:gap-2">
323
+ <div
324
+ class="base-data-table-filter-drawer-footer tw:mt-auto tw:px-4 tw:pt-2 tw:flex tw:gap-2 tw:bg-surface tw:z-10"
325
+ :class="{
326
+ 'base-data-table-filter-drawer-footer--contained':
327
+ isFilterDrawerContained,
328
+ }"
329
+ :style="filterDrawerFooterStyle"
330
+ >
277
331
  <slot
278
332
  name="filters.drawer.footer"
279
333
  v-bind="filterDrawerFooterSlotProps"
@@ -282,6 +336,7 @@
282
336
  variant="outlined"
283
337
  prepend-icon="mdi-filter-remove"
284
338
  :text="$t('core.baseDataTable.resetFilters')"
339
+ class="base-data-table-filter-drawer-footer-btn"
285
340
  @click="resetFilters"
286
341
  />
287
342
  <v-btn
@@ -289,25 +344,66 @@
289
344
  variant="flat"
290
345
  prepend-icon="mdi-check"
291
346
  :text="$t('core.baseDataTable.applyFilters')"
347
+ class="base-data-table-filter-drawer-footer-btn"
292
348
  @click="applyFilters"
293
349
  />
294
350
  </slot>
295
351
  </div>
296
352
  </div>
297
- </v-navigation-drawer>
353
+ </BaseDataTableFilterDrawerShell>
354
+
355
+ <v-dialog v-model="deleteDialogOpen" max-width="400">
356
+ <v-card>
357
+ <v-card-title>{{ resolvedDeleteConfirmTitle }}</v-card-title>
358
+ <v-card-text>{{ deleteConfirmMessageText }}</v-card-text>
359
+ <v-card-actions>
360
+ <v-spacer />
361
+ <v-btn variant="text" @click="cancelDelete">
362
+ {{ $t("core.cancel") }}
363
+ </v-btn>
364
+ <v-btn color="error" @click="confirmDeleteAction">
365
+ {{ $t("core.baseDataTable.delete") }}
366
+ </v-btn>
367
+ </v-card-actions>
368
+ </v-card>
369
+ </v-dialog>
298
370
  </div>
299
371
  </template>
300
372
 
301
373
  <script setup lang="ts" generic="T extends Record<string, any>">
302
- import { Comment, computed, ref, useSlots, useTemplateRef, watch } from "vue"
374
+ import {
375
+ Comment,
376
+ computed,
377
+ inject,
378
+ provide,
379
+ ref,
380
+ useSlots,
381
+ useTemplateRef,
382
+ watch,
383
+ } from "vue"
384
+ import { useI18n } from "vue-i18n"
303
385
  import DataExporter from "@racletteOrchestrator/components/dataExport/DataExporter.vue"
304
386
  import FileUpload from "@racletteOrchestrator/components/input/FileUpload.vue"
387
+ import BaseDataTableFilterDrawerShell from "@racletteOrchestrator/components/dataTable/BaseDataTableFilterDrawerShell.vue"
388
+ import {
389
+ WIDGET_SLOT_TYPE_KEY,
390
+ type WidgetSlotType,
391
+ } from "@racletteOrchestrator/constants/widgetSlotType"
392
+ import { BASE_DATA_TABLE_REQUEST_DELETE_KEY } from "@racletteOrchestrator/composables/useBaseDataTableDeleteConfirm"
393
+ import configService from "@racletteCore/lib/configService"
305
394
  import type {
306
395
  ExportPayload,
307
396
  FormatEntry,
308
397
  } from "@racletteOrchestrator/composables/useExport/types"
309
398
  import type { DataTableItem } from "vuetify/lib/components/VDataTable/types"
310
399
 
400
+ export interface BaseDataTableToolbarIndicator {
401
+ id: string
402
+ label: string
403
+ color?: string
404
+ icon?: string
405
+ }
406
+
311
407
  export interface BaseDataTableActionContext<T> {
312
408
  item?: T
313
409
  filters: Record<string, string>
@@ -352,10 +448,18 @@ export interface BaseDataTableProps<T> {
352
448
  showLoading?: boolean
353
449
  /** Max distinct values per column before filter falls back to a text field. */
354
450
  filterMaxSelectOptions?: number
451
+ /**
452
+ * Where the filter drawer attaches: `auto` uses parent containment when
453
+ * `WidgetsLayoutLoader` slotType is `modal`, otherwise full viewport.
454
+ */
455
+ filterDrawerAttach?: "auto" | "viewport" | "parent"
456
+ /** Generic toolbar mode indicators (badge on ⋮ menu, optional chips). */
457
+ toolbarIndicators?: BaseDataTableToolbarIndicator[]
355
458
 
356
459
  itemValue?: string
357
460
  /** Vuetify group-by key(s), e.g. `"project"` for project-grouped rows. */
358
461
  groupBy?: string | readonly string[]
462
+ /** Overrides `core.baseDataTable.actions` for the row-actions column header. */
359
463
  actionsHeaderTitle?: string
360
464
 
361
465
  /**
@@ -364,6 +468,13 @@ export interface BaseDataTableProps<T> {
364
468
  */
365
469
  exportableItems?: Record<string, unknown>[]
366
470
  exportMeta?: Record<string, unknown>
471
+ /**
472
+ * `visible-columns` (default): export only keys present in `headers`.
473
+ * `full-records`: export matched rows as-is (for round-trip import of canonical records).
474
+ */
475
+ exportRowShape?: "visible-columns" | "full-records"
476
+ /** When false, hides the DataExporter items/meta/combined scope toggle. */
477
+ exportShowDataScopeSwitch?: boolean
367
478
 
368
479
  /** Formats offered in the export menu (workbench list tables default to JSON only). */
369
480
  exportFormats?: FormatEntry[]
@@ -386,6 +497,13 @@ export interface BaseDataTableProps<T> {
386
497
  payload: Record<string, string>,
387
498
  context: BaseDataTableActionContext<T>,
388
499
  ) => void
500
+
501
+ /** When true (default), row deletes via `requestDelete` show a confirmation dialog. */
502
+ confirmDelete?: boolean
503
+ deleteConfirmTitle?: string
504
+ getDeleteConfirmMessage?: (item: T) => string
505
+ /** Used when `itemsDeleted` is true (hard-delete mode). Falls back to soft-delete message. */
506
+ getHardDeleteConfirmMessage?: (item: T) => string
389
507
  }
390
508
 
391
509
  const props = withDefaults(defineProps<BaseDataTableProps<T>>(), {
@@ -401,11 +519,15 @@ const props = withDefaults(defineProps<BaseDataTableProps<T>>(), {
401
519
  showSearch: true,
402
520
  showLoading: true,
403
521
  filterMaxSelectOptions: 300,
522
+ filterDrawerAttach: "auto",
523
+ toolbarIndicators: () => [],
404
524
  itemValue: "_id",
405
525
  groupBy: undefined,
406
526
  actionsHeaderTitle: "",
407
527
  exportableItems: undefined,
408
528
  exportMeta: undefined,
529
+ exportRowShape: "visible-columns",
530
+ exportShowDataScopeSwitch: true,
409
531
  exportFormats: () => ["json"] as FormatEntry[],
410
532
  exportDefaultFormat: "json",
411
533
  onFileLoaded: undefined,
@@ -414,6 +536,10 @@ const props = withDefaults(defineProps<BaseDataTableProps<T>>(), {
414
536
  onFilterStateChange: undefined,
415
537
  onFilterApply: undefined,
416
538
  onFilterReset: undefined,
539
+ confirmDelete: true,
540
+ deleteConfirmTitle: undefined,
541
+ getDeleteConfirmMessage: undefined,
542
+ getHardDeleteConfirmMessage: undefined,
417
543
  })
418
544
 
419
545
  const emit = defineEmits<{
@@ -421,28 +547,130 @@ const emit = defineEmits<{
421
547
  "update:currentItems": [items: readonly DataTableItem<T>[]]
422
548
  }>()
423
549
 
424
- type VuetifyGroupByItem = { key: string; order?: "asc" | "desc" }
550
+ const { t } = useI18n()
425
551
 
426
- /** Vuetify expects `{ key }[]`; accept plain column key(s) from callers. */
427
- const resolvedGroupBy = computed((): readonly VuetifyGroupByItem[] | undefined => {
428
- const groupBy = props.groupBy
429
- if (!groupBy) return undefined
430
- if (typeof groupBy === "string") {
431
- return [{ key: groupBy, order: "asc" }]
552
+ const widgetSlotType = inject(WIDGET_SLOT_TYPE_KEY, ref<WidgetSlotType>("page"))
553
+
554
+ const resolvedFilterDrawerAttach = computed(() => {
555
+ if (props.filterDrawerAttach !== "auto") {
556
+ return props.filterDrawerAttach
557
+ }
558
+ return widgetSlotType.value === "modal" ? "parent" : "viewport"
559
+ })
560
+
561
+ const isFilterDrawerContained = computed(
562
+ () => resolvedFilterDrawerAttach.value === "parent",
563
+ )
564
+
565
+ const filterDrawerWidth = computed(() =>
566
+ isFilterDrawerContained.value ? 320 : 360,
567
+ )
568
+
569
+ const showRacletteFooterAdvert = computed(() =>
570
+ configService.getPleaseLetUsAdvertiseOurselves(),
571
+ )
572
+
573
+ const filterDrawerFooterStyle = computed(() => {
574
+ const style: Record<string, string> = {
575
+ paddingBottom: "1rem",
576
+ }
577
+ if (!isFilterDrawerContained.value && showRacletteFooterAdvert.value) {
578
+ style.paddingBottom = "4.5rem"
579
+ }
580
+ return style
581
+ })
582
+
583
+ const hasToolbarIndicators = computed(
584
+ () => (props.toolbarIndicators?.length ?? 0) > 0,
585
+ )
586
+
587
+ const activeModesTooltip = computed(() => {
588
+ if (!hasToolbarIndicators.value) {
589
+ return ""
590
+ }
591
+ return t("core.baseDataTable.activeModesTooltip", {
592
+ modes: (props.toolbarIndicators ?? []).map((indicator) => indicator.label).join(", "),
593
+ })
594
+ })
595
+
596
+ const deleteDialogOpen = ref(false)
597
+ const pendingDeleteItem = ref<T | null>(null)
598
+ let pendingDeleteCallback: (() => void | Promise<void>) | null = null
599
+
600
+ const resolvedDeleteConfirmTitle = computed(
601
+ () => props.deleteConfirmTitle ?? t("core.baseDataTable.confirmDeleteTitle"),
602
+ )
603
+
604
+ const deleteConfirmMessageText = computed(() => {
605
+ const item = pendingDeleteItem.value
606
+ if (!item) return ""
607
+ if (props.itemsDeleted && props.getHardDeleteConfirmMessage) {
608
+ return props.getHardDeleteConfirmMessage(item)
432
609
  }
433
- if (groupBy.length === 0) return undefined
434
- if (typeof groupBy[0] === "string") {
435
- return groupBy.map((key) => ({ key, order: "asc" as const }))
610
+ if (props.getDeleteConfirmMessage) {
611
+ return props.getDeleteConfirmMessage(item)
436
612
  }
437
- return groupBy as readonly VuetifyGroupByItem[]
613
+ const name = String((item as Record<string, unknown>)._id ?? "")
614
+ const key = props.itemsDeleted
615
+ ? "core.baseDataTable.confirmHardDeleteMessage"
616
+ : "core.baseDataTable.confirmDeleteMessage"
617
+ return t(key, { name })
438
618
  })
439
619
 
620
+ const requestDelete = <Item,>(
621
+ item: Item,
622
+ onConfirm: () => void | Promise<void>,
623
+ ) => {
624
+ if (!props.confirmDelete) {
625
+ void onConfirm()
626
+ return
627
+ }
628
+ pendingDeleteItem.value = item as T
629
+ pendingDeleteCallback = onConfirm
630
+ deleteDialogOpen.value = true
631
+ }
632
+
633
+ const cancelDelete = () => {
634
+ deleteDialogOpen.value = false
635
+ pendingDeleteItem.value = null
636
+ pendingDeleteCallback = null
637
+ }
638
+
639
+ const confirmDeleteAction = () => {
640
+ const callback = pendingDeleteCallback
641
+ cancelDelete()
642
+ if (callback) {
643
+ void callback()
644
+ }
645
+ }
646
+
647
+ provide(BASE_DATA_TABLE_REQUEST_DELETE_KEY, requestDelete)
648
+
649
+ type VuetifyGroupByItem = { key: string; order?: "asc" | "desc" }
650
+
651
+ /** Vuetify expects `{ key }[]`; accept plain column key(s) from callers. */
652
+ const resolvedGroupBy = computed(
653
+ (): readonly VuetifyGroupByItem[] | undefined => {
654
+ const groupBy = props.groupBy
655
+ if (!groupBy) return undefined
656
+ if (typeof groupBy === "string") {
657
+ return [{ key: groupBy, order: "asc" }]
658
+ }
659
+ if (groupBy.length === 0) return undefined
660
+ if (typeof groupBy[0] === "string") {
661
+ return groupBy.map((key) => ({ key, order: "asc" as const }))
662
+ }
663
+ return groupBy as readonly VuetifyGroupByItem[]
664
+ },
665
+ )
666
+
440
667
  const slots = useSlots()
441
668
 
442
669
  /** Slots owned by BaseDataTable chrome (toolbar, filters drawer, row actions, export). */
443
670
  const BASE_DATA_TABLE_RESERVED_SLOTS = new Set([
444
671
  "toolbar-start",
445
672
  "toolbar-leading",
673
+ "toolbar-indicators",
446
674
  "toolbar-end",
447
675
  "toolbar-filters-trigger",
448
676
  "toolbar-actions-trigger",
@@ -495,7 +723,8 @@ const hasSlotContent = (name: string) => {
495
723
  if (!slot) return false
496
724
  return slot().some((vnode) => {
497
725
  if (vnode.type === Comment) return false
498
- if (typeof vnode.children === "string") return vnode.children.trim().length > 0
726
+ if (typeof vnode.children === "string")
727
+ return vnode.children.trim().length > 0
499
728
  if (Array.isArray(vnode.children)) return vnode.children.length > 0
500
729
  return true
501
730
  })
@@ -510,6 +739,8 @@ const showTopToolbar = computed(
510
739
  Boolean(props.onFileLoaded) ||
511
740
  hasSlotContent("toolbar-start") ||
512
741
  hasSlotContent("toolbar-leading") ||
742
+ hasSlotContent("toolbar-indicators") ||
743
+ hasToolbarIndicators.value ||
513
744
  hasSlotContent("toolbar-end"),
514
745
  )
515
746
 
@@ -551,8 +782,7 @@ const computedHeaders = computed(() => {
551
782
  const headers = [...props.headers].map((header) => {
552
783
  const key = String(header.key)
553
784
  const hasFilter =
554
- props.showFilters &&
555
- appliedColumnFilters.value[key]?.trim()?.length > 0
785
+ props.showFilters && appliedColumnFilters.value[key]?.trim()?.length > 0
556
786
  return {
557
787
  ...header,
558
788
  title: header.title,
@@ -568,11 +798,10 @@ const computedHeaders = computed(() => {
568
798
  : {}),
569
799
  }
570
800
  })
571
-
572
801
  // Add actions column if needed
573
802
  if (props.showActionsColumn) {
574
803
  headers.push({
575
- title: props.actionsHeaderTitle || "Actions",
804
+ title: props.actionsHeaderTitle || t("core.baseDataTable.actions"),
576
805
  key: "actions",
577
806
  align: "end" as const,
578
807
  sortable: false,
@@ -676,6 +905,11 @@ const projectRowsToVisibleColumns = <TRow extends Record<string, unknown>>(
676
905
  })
677
906
  }
678
907
 
908
+ const shapeExportRows = <TRow extends Record<string, unknown>>(rows: TRow[]) =>
909
+ props.exportRowShape === "full-records"
910
+ ? rows
911
+ : projectRowsToVisibleColumns(rows)
912
+
679
913
  /** Rows passed to DataExporter when something is selected. */
680
914
  const exportPayloadData = computed<ExportPayload>(() => {
681
915
  const idKey = props.itemValue as keyof T
@@ -709,7 +943,7 @@ const exportPayloadData = computed<ExportPayload>(() => {
709
943
  .filter((row): row is Record<string, unknown> => row !== undefined)
710
944
 
711
945
  return {
712
- items: projectRowsToVisibleColumns(selectedExportRows),
946
+ items: shapeExportRows(selectedExportRows),
713
947
  ...(props.exportMeta ? { meta: props.exportMeta } : {}),
714
948
  }
715
949
  }
@@ -728,7 +962,7 @@ const exportPayloadData = computed<ExportPayload>(() => {
728
962
  }) as Record<string, unknown>[]
729
963
 
730
964
  return {
731
- items: projectRowsToVisibleColumns(selectedRows),
965
+ items: shapeExportRows(selectedRows),
732
966
  ...(props.exportMeta ? { meta: props.exportMeta } : {}),
733
967
  }
734
968
  })
@@ -778,11 +1012,25 @@ const isSelectionControlClick = (event: PointerEvent) => {
778
1012
  )
779
1013
  }
780
1014
 
1015
+ const isInteractiveRowClick = (event: PointerEvent) => {
1016
+ const target = event.target
1017
+ if (!(target instanceof Element)) return false
1018
+ return Boolean(
1019
+ target.closest(
1020
+ "button, a, input, textarea, select, label, [role='button'], .v-btn, .v-btn-group",
1021
+ ),
1022
+ )
1023
+ }
1024
+
781
1025
  const handleRowClick = (event: PointerEvent, row: { item: T }) => {
782
1026
  if (showSelectColumn.value && isSelectionControlClick(event)) {
783
1027
  return
784
1028
  }
785
1029
 
1030
+ if (isInteractiveRowClick(event)) {
1031
+ return
1032
+ }
1033
+
786
1034
  if (props.rowClickHandler && !isItemRowClickable(row.item)) {
787
1035
  return
788
1036
  }
@@ -816,8 +1064,14 @@ const notifyFilterStateChange = () => {
816
1064
  }
817
1065
  }
818
1066
 
1067
+ /** Selection is tied to visible rows; clear when applied filters change. */
1068
+ const clearExportSelection = () => {
1069
+ selected.value = []
1070
+ }
1071
+
819
1072
  const applyFilters = () => {
820
1073
  appliedColumnFilters.value = { ...columnFilters.value }
1074
+ clearExportSelection()
821
1075
  if (props.onFilterApply) {
822
1076
  props.onFilterApply(appliedFilterPayload.value, actionContext())
823
1077
  } else {
@@ -828,6 +1082,7 @@ const applyFilters = () => {
828
1082
  const resetFilters = () => {
829
1083
  columnFilters.value = {}
830
1084
  appliedColumnFilters.value = {}
1085
+ clearExportSelection()
831
1086
  if (props.onFilterReset) {
832
1087
  props.onFilterReset({}, actionContext())
833
1088
  } else {
@@ -876,6 +1131,7 @@ const exporterSlotProps = computed(() => ({
876
1131
  disabled: !selected.value.length,
877
1132
  formats: props.exportFormats,
878
1133
  defaultFormat: props.exportDefaultFormat,
1134
+ showDataScopeSwitch: props.exportShowDataScopeSwitch,
879
1135
  isExportMode: isExportMode.value,
880
1136
  selectedCount: selected.value.length,
881
1137
  }))
@@ -918,6 +1174,10 @@ const filterDrawerFooterSlotProps = computed(() => ({
918
1174
  filters: draftFilterPayload.value,
919
1175
  context: actionContext(),
920
1176
  }))
1177
+
1178
+ const toolbarIndicatorsSlotProps = computed(() => ({
1179
+ indicators: props.toolbarIndicators ?? [],
1180
+ }))
921
1181
  </script>
922
1182
 
923
1183
  <style lang="css">
@@ -982,4 +1242,24 @@ const filterDrawerFooterSlotProps = computed(() => ({
982
1242
  .filtered-column-cell {
983
1243
  background-color: rgba(var(--v-theme-info), 0.1);
984
1244
  }
1245
+
1246
+ .base-data-table-host--filter-open {
1247
+ overflow: hidden;
1248
+ }
1249
+
1250
+ .base-data-table-filter-drawer-footer {
1251
+ position: sticky;
1252
+ bottom: 0;
1253
+ }
1254
+
1255
+ .base-data-table-filter-drawer-footer--contained {
1256
+ flex-direction: column;
1257
+ align-items: stretch;
1258
+ }
1259
+
1260
+ .base-data-table-filter-drawer-footer--contained
1261
+ :deep(.base-data-table-filter-drawer-footer-btn) {
1262
+ width: 100%;
1263
+ max-width: 100%;
1264
+ }
985
1265
  </style>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <v-btn
3
+ icon="mdi-delete"
4
+ size="small"
5
+ variant="text"
6
+ color="error"
7
+ @click.stop.prevent="handleClick"
8
+ />
9
+ </template>
10
+
11
+ <script setup lang="ts" generic="T">
12
+ import { useBaseDataTableDeleteConfirm } from "@racletteOrchestrator/composables/useBaseDataTableDeleteConfirm"
13
+
14
+ const props = defineProps<{
15
+ item: T
16
+ }>()
17
+
18
+ const emit = defineEmits<{
19
+ delete: [item: T]
20
+ }>()
21
+
22
+ const { requestDelete } = useBaseDataTableDeleteConfirm()
23
+
24
+ const handleClick = () => {
25
+ requestDelete(props.item, () => emit("delete", props.item))
26
+ }
27
+ </script>