@raclettejs/core 0.1.37 → 0.1.39

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 (32) hide show
  1. package/CHANGELOG.md +22 -9
  2. package/dist/cli.js +1 -1
  3. package/dist/cli.js.map +1 -1
  4. package/package.json +2 -2
  5. package/services/backend/package.json +1 -1
  6. package/services/frontend/src/core/lib/scheduleTask.ts +35 -0
  7. package/services/frontend/src/core/store/index.ts +5 -1
  8. package/services/frontend/src/core/store/reducers/compositionSlots/reducers.ts +1 -5
  9. package/services/frontend/src/core/store/reducers/data/reducers.ts +101 -65
  10. package/services/frontend/src/core/store/reducers/metaReducer.ts +23 -0
  11. package/services/frontend/src/core/store/reducers/notifications/reducers.ts +39 -47
  12. package/services/frontend/src/core/store/reducers/queries/effects.ts +55 -2
  13. package/services/frontend/src/core/store/reducers/queries/reducers.ts +60 -62
  14. package/services/frontend/src/core/store/reducers/queriesCache/effects.ts +105 -0
  15. package/services/frontend/src/core/store/reducers/queriesCache/index.ts +2 -1
  16. package/services/frontend/src/core/store/reducers/queriesCache/queryCacheHelper.ts +79 -9
  17. package/services/frontend/src/core/store/reducers/queriesCache/reducers.ts +95 -100
  18. package/services/frontend/src/orchestrator/components/CompositionLoadingState.vue +28 -0
  19. package/services/frontend/src/orchestrator/components/composition/CompositionOverlay.vue +5 -7
  20. package/services/frontend/src/orchestrator/components/composition/WidgetsLayoutLoader.vue +171 -39
  21. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTable.vue +311 -54
  22. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTableConfirmDeleteBtn.vue +27 -0
  23. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTableFilterDrawerShell.vue +72 -0
  24. package/services/frontend/src/orchestrator/components/index.ts +4 -0
  25. package/services/frontend/src/orchestrator/composables/index.ts +1 -0
  26. package/services/frontend/src/orchestrator/composables/useBaseDataTableDeleteConfirm.ts +23 -0
  27. package/services/frontend/src/orchestrator/composables/useWidgetLifecycle.ts +53 -23
  28. package/services/frontend/src/orchestrator/constants/widgetSlotType.ts +4 -0
  29. package/services/frontend/src/orchestrator/i18n/de-DE.json +7 -1
  30. package/services/frontend/src/orchestrator/i18n/en-EU.json +7 -1
  31. package/services/frontend/src/orchestrator/i18n/sk.json +7 -1
  32. package/services/frontend/src/orchestrator/router/routerHooks/afterEach.ts +15 -9
@@ -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
@@ -220,14 +261,14 @@
220
261
  <!-- Additional dialogs slot -->
221
262
  <slot name="dialogs" />
222
263
 
223
- <v-navigation-drawer
264
+ <BaseDataTableFilterDrawerShell
265
+ v-if="showFilters"
224
266
  v-model="isFilterDrawerOpen"
225
- location="right"
226
- temporary
227
- width="360"
267
+ :contained="isFilterDrawerContained"
268
+ :width="filterDrawerWidth"
228
269
  :aria-label="$t('core.baseDataTable.filters')"
229
270
  >
230
- <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">
231
272
  <slot name="filters.summary" v-bind="filterSummarySlotProps" />
232
273
  <slot name="filters.drawer.header" v-bind="filterDrawerHeaderSlotProps">
233
274
  <div
@@ -246,35 +287,47 @@
246
287
  </div>
247
288
  </slot>
248
289
 
249
- <slot name="filters.drawer.body" v-bind="filterDrawerBodySlotProps">
250
- <div class="tw:px-4 tw:pb-4 tw:flex tw:flex-col tw:gap-3">
251
- <template v-for="header in filterableHeaders" :key="String(header.key)">
252
- <v-autocomplete
253
- v-if="columnUsesComboboxFilter(String(header.key))"
254
- v-model="columnFilters[String(header.key)]"
255
- @update:model-value="notifyFilterStateChange"
256
- :items="filterOptionsByKey[String(header.key)] ?? []"
257
- :label="header.title"
258
- clearable
259
- hide-details
260
- density="compact"
261
- variant="outlined"
262
- />
263
- <v-text-field
264
- v-else
265
- v-model="columnFilters[String(header.key)]"
266
- @update:model-value="notifyFilterStateChange"
267
- :label="header.title"
268
- clearable
269
- hide-details
270
- density="compact"
271
- variant="outlined"
272
- />
273
- </template>
274
- </div>
275
- </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>
276
322
 
277
- <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
+ >
278
331
  <slot
279
332
  name="filters.drawer.footer"
280
333
  v-bind="filterDrawerFooterSlotProps"
@@ -283,6 +336,7 @@
283
336
  variant="outlined"
284
337
  prepend-icon="mdi-filter-remove"
285
338
  :text="$t('core.baseDataTable.resetFilters')"
339
+ class="base-data-table-filter-drawer-footer-btn"
286
340
  @click="resetFilters"
287
341
  />
288
342
  <v-btn
@@ -290,25 +344,66 @@
290
344
  variant="flat"
291
345
  prepend-icon="mdi-check"
292
346
  :text="$t('core.baseDataTable.applyFilters')"
347
+ class="base-data-table-filter-drawer-footer-btn"
293
348
  @click="applyFilters"
294
349
  />
295
350
  </slot>
296
351
  </div>
297
352
  </div>
298
- </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>
299
370
  </div>
300
371
  </template>
301
372
 
302
373
  <script setup lang="ts" generic="T extends Record<string, any>">
303
- 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"
304
385
  import DataExporter from "@racletteOrchestrator/components/dataExport/DataExporter.vue"
305
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"
306
394
  import type {
307
395
  ExportPayload,
308
396
  FormatEntry,
309
397
  } from "@racletteOrchestrator/composables/useExport/types"
310
398
  import type { DataTableItem } from "vuetify/lib/components/VDataTable/types"
311
399
 
400
+ export interface BaseDataTableToolbarIndicator {
401
+ id: string
402
+ label: string
403
+ color?: string
404
+ icon?: string
405
+ }
406
+
312
407
  export interface BaseDataTableActionContext<T> {
313
408
  item?: T
314
409
  filters: Record<string, string>
@@ -353,10 +448,18 @@ export interface BaseDataTableProps<T> {
353
448
  showLoading?: boolean
354
449
  /** Max distinct values per column before filter falls back to a text field. */
355
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[]
356
458
 
357
459
  itemValue?: string
358
460
  /** Vuetify group-by key(s), e.g. `"project"` for project-grouped rows. */
359
461
  groupBy?: string | readonly string[]
462
+ /** Overrides `core.baseDataTable.actions` for the row-actions column header. */
360
463
  actionsHeaderTitle?: string
361
464
 
362
465
  /**
@@ -394,6 +497,13 @@ export interface BaseDataTableProps<T> {
394
497
  payload: Record<string, string>,
395
498
  context: BaseDataTableActionContext<T>,
396
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
397
507
  }
398
508
 
399
509
  const props = withDefaults(defineProps<BaseDataTableProps<T>>(), {
@@ -409,6 +519,8 @@ const props = withDefaults(defineProps<BaseDataTableProps<T>>(), {
409
519
  showSearch: true,
410
520
  showLoading: true,
411
521
  filterMaxSelectOptions: 300,
522
+ filterDrawerAttach: "auto",
523
+ toolbarIndicators: () => [],
412
524
  itemValue: "_id",
413
525
  groupBy: undefined,
414
526
  actionsHeaderTitle: "",
@@ -424,6 +536,10 @@ const props = withDefaults(defineProps<BaseDataTableProps<T>>(), {
424
536
  onFilterStateChange: undefined,
425
537
  onFilterApply: undefined,
426
538
  onFilterReset: undefined,
539
+ confirmDelete: true,
540
+ deleteConfirmTitle: undefined,
541
+ getDeleteConfirmMessage: undefined,
542
+ getHardDeleteConfirmMessage: undefined,
427
543
  })
428
544
 
429
545
  const emit = defineEmits<{
@@ -431,28 +547,130 @@ const emit = defineEmits<{
431
547
  "update:currentItems": [items: readonly DataTableItem<T>[]]
432
548
  }>()
433
549
 
434
- type VuetifyGroupByItem = { key: string; order?: "asc" | "desc" }
550
+ const { t } = useI18n()
435
551
 
436
- /** Vuetify expects `{ key }[]`; accept plain column key(s) from callers. */
437
- const resolvedGroupBy = computed((): readonly VuetifyGroupByItem[] | undefined => {
438
- const groupBy = props.groupBy
439
- if (!groupBy) return undefined
440
- if (typeof groupBy === "string") {
441
- 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
442
557
  }
443
- if (groupBy.length === 0) return undefined
444
- if (typeof groupBy[0] === "string") {
445
- return groupBy.map((key) => ({ key, order: "asc" as const }))
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"
446
579
  }
447
- return groupBy as readonly VuetifyGroupByItem[]
580
+ return style
448
581
  })
449
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)
609
+ }
610
+ if (props.getDeleteConfirmMessage) {
611
+ return props.getDeleteConfirmMessage(item)
612
+ }
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 })
618
+ })
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
+
450
667
  const slots = useSlots()
451
668
 
452
669
  /** Slots owned by BaseDataTable chrome (toolbar, filters drawer, row actions, export). */
453
670
  const BASE_DATA_TABLE_RESERVED_SLOTS = new Set([
454
671
  "toolbar-start",
455
672
  "toolbar-leading",
673
+ "toolbar-indicators",
456
674
  "toolbar-end",
457
675
  "toolbar-filters-trigger",
458
676
  "toolbar-actions-trigger",
@@ -505,7 +723,8 @@ const hasSlotContent = (name: string) => {
505
723
  if (!slot) return false
506
724
  return slot().some((vnode) => {
507
725
  if (vnode.type === Comment) return false
508
- if (typeof vnode.children === "string") return vnode.children.trim().length > 0
726
+ if (typeof vnode.children === "string")
727
+ return vnode.children.trim().length > 0
509
728
  if (Array.isArray(vnode.children)) return vnode.children.length > 0
510
729
  return true
511
730
  })
@@ -520,6 +739,8 @@ const showTopToolbar = computed(
520
739
  Boolean(props.onFileLoaded) ||
521
740
  hasSlotContent("toolbar-start") ||
522
741
  hasSlotContent("toolbar-leading") ||
742
+ hasSlotContent("toolbar-indicators") ||
743
+ hasToolbarIndicators.value ||
523
744
  hasSlotContent("toolbar-end"),
524
745
  )
525
746
 
@@ -561,8 +782,7 @@ const computedHeaders = computed(() => {
561
782
  const headers = [...props.headers].map((header) => {
562
783
  const key = String(header.key)
563
784
  const hasFilter =
564
- props.showFilters &&
565
- appliedColumnFilters.value[key]?.trim()?.length > 0
785
+ props.showFilters && appliedColumnFilters.value[key]?.trim()?.length > 0
566
786
  return {
567
787
  ...header,
568
788
  title: header.title,
@@ -578,11 +798,10 @@ const computedHeaders = computed(() => {
578
798
  : {}),
579
799
  }
580
800
  })
581
-
582
801
  // Add actions column if needed
583
802
  if (props.showActionsColumn) {
584
803
  headers.push({
585
- title: props.actionsHeaderTitle || "Actions",
804
+ title: props.actionsHeaderTitle || t("core.baseDataTable.actions"),
586
805
  key: "actions",
587
806
  align: "end" as const,
588
807
  sortable: false,
@@ -793,11 +1012,25 @@ const isSelectionControlClick = (event: PointerEvent) => {
793
1012
  )
794
1013
  }
795
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
+
796
1025
  const handleRowClick = (event: PointerEvent, row: { item: T }) => {
797
1026
  if (showSelectColumn.value && isSelectionControlClick(event)) {
798
1027
  return
799
1028
  }
800
1029
 
1030
+ if (isInteractiveRowClick(event)) {
1031
+ return
1032
+ }
1033
+
801
1034
  if (props.rowClickHandler && !isItemRowClickable(row.item)) {
802
1035
  return
803
1036
  }
@@ -941,6 +1174,10 @@ const filterDrawerFooterSlotProps = computed(() => ({
941
1174
  filters: draftFilterPayload.value,
942
1175
  context: actionContext(),
943
1176
  }))
1177
+
1178
+ const toolbarIndicatorsSlotProps = computed(() => ({
1179
+ indicators: props.toolbarIndicators ?? [],
1180
+ }))
944
1181
  </script>
945
1182
 
946
1183
  <style lang="css">
@@ -1005,4 +1242,24 @@ const filterDrawerFooterSlotProps = computed(() => ({
1005
1242
  .filtered-column-cell {
1006
1243
  background-color: rgba(var(--v-theme-info), 0.1);
1007
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
+ }
1008
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>
@@ -0,0 +1,72 @@
1
+ <template>
2
+ <v-navigation-drawer
3
+ v-if="!contained"
4
+ :model-value="modelValue"
5
+ location="right"
6
+ temporary
7
+ :width="width"
8
+ :aria-label="ariaLabel"
9
+ @update:model-value="emit('update:modelValue', $event)"
10
+ >
11
+ <slot />
12
+ </v-navigation-drawer>
13
+
14
+ <template v-else>
15
+ <div
16
+ v-if="modelValue"
17
+ class="base-data-table-filter-scrim"
18
+ aria-hidden="true"
19
+ @click="emit('update:modelValue', false)"
20
+ />
21
+ <aside
22
+ v-if="modelValue"
23
+ class="base-data-table-filter-panel"
24
+ :class="{ 'base-data-table-filter-panel--contained': contained }"
25
+ :style="contained ? undefined : { width: `${width}px` }"
26
+ :aria-label="ariaLabel"
27
+ >
28
+ <slot />
29
+ </aside>
30
+ </template>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ defineProps<{
35
+ modelValue: boolean
36
+ contained: boolean
37
+ width: number
38
+ ariaLabel: string
39
+ }>()
40
+
41
+ const emit = defineEmits<{
42
+ "update:modelValue": [value: boolean]
43
+ }>()
44
+ </script>
45
+
46
+ <style scoped>
47
+ .base-data-table-filter-scrim {
48
+ position: absolute;
49
+ inset: 0;
50
+ z-index: 3;
51
+ background: rgba(0, 0, 0, 0.4);
52
+ }
53
+
54
+ .base-data-table-filter-panel {
55
+ position: absolute;
56
+ top: 0;
57
+ right: 0;
58
+ bottom: 0;
59
+ z-index: 4;
60
+ display: flex;
61
+ flex-direction: column;
62
+ min-height: 0;
63
+ box-sizing: border-box;
64
+ background: rgb(var(--v-theme-surface));
65
+ box-shadow: -4px 0 16px rgba(0, 0, 0, 0.12);
66
+ }
67
+
68
+ .base-data-table-filter-panel--contained {
69
+ width: min(100%, 360px);
70
+ max-width: 100%;
71
+ }
72
+ </style>
@@ -3,6 +3,7 @@ import DefaultModal from "./modal/DefaultModal.vue"
3
3
  import DraggableElement from "./dragAndDrop/DraggableElement.vue"
4
4
  import DropzoneElement from "./dragAndDrop/DropzoneElement.vue"
5
5
  import ErrorWidgetState from "./ErrorWidgetState.vue"
6
+ import CompositionLoadingState from "./CompositionLoadingState.vue"
6
7
  import LoadingWidgetState from "./LoadingWidgetState.vue"
7
8
  import LoginForm from "./authentication/LoginForm.vue"
8
9
  import PageNavigationSidebar from "./navigation/PageNavigationSidebar.vue"
@@ -18,13 +19,16 @@ import DataExporter from "./dataExport/DataExporter.vue"
18
19
  import ImportSelectionDialog from "./dataImport/ImportSelectionDialog.vue"
19
20
  import ImportSummaryDialog from "./dataImport/ImportSummaryDialog.vue"
20
21
  import BaseDataTable from "./dataTable/BaseDataTable.vue"
22
+ import BaseDataTableConfirmDeleteBtn from "./dataTable/BaseDataTableConfirmDeleteBtn.vue"
21
23
  import FileUpload from "./input/FileUpload.vue"
22
24
 
23
25
  export {
24
26
  ImportSelectionDialog,
25
27
  ImportSummaryDialog,
26
28
  BaseDataTable,
29
+ BaseDataTableConfirmDeleteBtn,
27
30
  FileUpload,
31
+ CompositionLoadingState,
28
32
  CompositionOverlay,
29
33
  DataExporter,
30
34
  DefaultModal,
@@ -5,3 +5,4 @@ export * from "./useWidgets"
5
5
  export * from "./useInteractionLinks"
6
6
  export * from "./useWidgetLifecycle"
7
7
  export * from "./useExport"
8
+ export * from "./useBaseDataTableDeleteConfirm"
@@ -0,0 +1,23 @@
1
+ import { inject, type InjectionKey } from "vue"
2
+
3
+ export type RequestDeleteFn = <T>(
4
+ item: T,
5
+ onConfirm: () => void | Promise<void>,
6
+ ) => void
7
+
8
+ export const BASE_DATA_TABLE_REQUEST_DELETE_KEY: InjectionKey<RequestDeleteFn> =
9
+ Symbol("baseDataTableRequestDelete")
10
+
11
+ /**
12
+ * Call from `#row-actions` (or descendants) inside `BaseDataTable` to run delete
13
+ * through the table's optional confirmation dialog.
14
+ */
15
+ export const useBaseDataTableDeleteConfirm = () => {
16
+ const requestDelete = inject(BASE_DATA_TABLE_REQUEST_DELETE_KEY)
17
+ if (!requestDelete) {
18
+ throw new Error(
19
+ "useBaseDataTableDeleteConfirm must be used within BaseDataTable",
20
+ )
21
+ }
22
+ return { requestDelete }
23
+ }