@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.
- package/CHANGELOG.md +22 -9
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
- package/services/backend/package.json +1 -1
- package/services/frontend/src/core/lib/scheduleTask.ts +35 -0
- package/services/frontend/src/core/store/index.ts +5 -1
- package/services/frontend/src/core/store/reducers/compositionSlots/reducers.ts +1 -5
- package/services/frontend/src/core/store/reducers/data/reducers.ts +101 -65
- package/services/frontend/src/core/store/reducers/metaReducer.ts +23 -0
- package/services/frontend/src/core/store/reducers/notifications/reducers.ts +39 -47
- package/services/frontend/src/core/store/reducers/queries/effects.ts +55 -2
- package/services/frontend/src/core/store/reducers/queries/reducers.ts +60 -62
- package/services/frontend/src/core/store/reducers/queriesCache/effects.ts +105 -0
- package/services/frontend/src/core/store/reducers/queriesCache/index.ts +2 -1
- package/services/frontend/src/core/store/reducers/queriesCache/queryCacheHelper.ts +79 -9
- package/services/frontend/src/core/store/reducers/queriesCache/reducers.ts +95 -100
- package/services/frontend/src/orchestrator/components/CompositionLoadingState.vue +28 -0
- package/services/frontend/src/orchestrator/components/composition/CompositionOverlay.vue +5 -7
- package/services/frontend/src/orchestrator/components/composition/WidgetsLayoutLoader.vue +171 -39
- package/services/frontend/src/orchestrator/components/dataTable/BaseDataTable.vue +311 -54
- package/services/frontend/src/orchestrator/components/dataTable/BaseDataTableConfirmDeleteBtn.vue +27 -0
- package/services/frontend/src/orchestrator/components/dataTable/BaseDataTableFilterDrawerShell.vue +72 -0
- package/services/frontend/src/orchestrator/components/index.ts +4 -0
- package/services/frontend/src/orchestrator/composables/index.ts +1 -0
- package/services/frontend/src/orchestrator/composables/useBaseDataTableDeleteConfirm.ts +23 -0
- package/services/frontend/src/orchestrator/composables/useWidgetLifecycle.ts +53 -23
- package/services/frontend/src/orchestrator/constants/widgetSlotType.ts +4 -0
- package/services/frontend/src/orchestrator/i18n/de-DE.json +7 -1
- package/services/frontend/src/orchestrator/i18n/en-EU.json +7 -1
- package/services/frontend/src/orchestrator/i18n/sk.json +7 -1
- 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
|
-
<
|
|
264
|
+
<BaseDataTableFilterDrawerShell
|
|
265
|
+
v-if="showFilters"
|
|
224
266
|
v-model="isFilterDrawerOpen"
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
<
|
|
250
|
-
<
|
|
251
|
-
<
|
|
252
|
-
<
|
|
253
|
-
v-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
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
|
-
</
|
|
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 {
|
|
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
|
-
|
|
550
|
+
const { t } = useI18n()
|
|
435
551
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (
|
|
440
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
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")
|
|
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 || "
|
|
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>
|
package/services/frontend/src/orchestrator/components/dataTable/BaseDataTableConfirmDeleteBtn.vue
ADDED
|
@@ -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>
|
package/services/frontend/src/orchestrator/components/dataTable/BaseDataTableFilterDrawerShell.vue
ADDED
|
@@ -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,
|
|
@@ -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
|
+
}
|