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