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