@raclettejs/core 0.1.34 → 0.1.36-canary.0
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 +30 -1
- package/dist/cli.js +86 -82
- package/dist/cli.js.map +3 -3
- package/package.json +5 -5
- package/services/.dockerignore +2 -0
- package/services/backend/.dockerignore +3 -1
- package/services/backend/src/core/payload/payloadTypes.ts +1 -0
- package/services/backend/src/core/pluginSystem/configGenerator/index.ts +12 -11
- package/services/backend/src/core/pluginSystem/pluginFastifyInstance.ts +28 -3
- package/services/backend/src/core/pluginSystem/pluginLoader.ts +11 -2
- package/services/backend/src/core/pluginSystem/pluginTypes.ts +39 -1
- package/services/backend/src/core/sockets/createSystemEventHandlers.ts +4 -4
- package/services/backend/src/core/sockets/socketManager.ts +80 -32
- package/services/backend/src/core/sockets/socketTypes.ts +6 -0
- package/services/backend/src/core/validation/schemaGenerator.ts +10 -3
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/project.service.ts +1 -1
- package/services/backend/src/corePlugins/raclette__core/backend/index.ts +0 -2
- package/services/backend/src/modules/authentication/index.ts +15 -23
- package/services/backend/src/shared/helper/adminTag.ts +34 -0
- package/services/backend/src/shared/types/plugins/index.ts +1 -9
- package/services/backend/src/utils/request.utils.ts +1 -1
- package/services/frontend/.dockerignore +3 -1
- package/services/frontend/provision/entrypoint.sh +2 -2
- package/services/frontend/src/core/helpers/devMode.ts +1 -3
- package/services/frontend/src/core/lib/data/dataApi.ts +27 -16
- package/services/frontend/src/core/lib/data/queryApi.ts +17 -8
- package/services/frontend/src/core/lib/data/responseTypeHandler.ts +17 -8
- package/services/frontend/src/core/lib/data/writeDataHandler.ts +3 -1
- package/services/frontend/src/core/lib/dataHelper.ts +51 -12
- package/services/frontend/src/core/lib/eventWhitelist.ts +2 -0
- package/services/frontend/src/core/lib/httpClient.ts +16 -2
- package/services/frontend/src/core/lib/logger.ts +2 -3
- package/services/frontend/src/core/lib/userNotifier.ts +20 -0
- package/services/frontend/src/core/lib/workSessionUrl.ts +16 -0
- package/services/frontend/src/core/main.ts +82 -15
- package/services/frontend/src/core/setup/socketBootstrap.ts +83 -0
- package/services/frontend/src/core/setup/socketClient.ts +39 -39
- package/services/frontend/src/core/setup/socketEvents.ts +152 -158
- package/services/frontend/src/core/setup/socketRegistry.ts +19 -0
- package/services/frontend/src/core/store/index.ts +2 -4
- package/services/frontend/src/core/types/DataApi.ts +10 -2
- package/services/frontend/src/core/types/PluginRegistrar.ts +1 -1
- package/services/frontend/src/orchestrator/ProductOrchestrator.vue +15 -1
- package/services/frontend/src/orchestrator/assets/styles/layers.css +4 -1
- package/services/frontend/src/orchestrator/components/dataTable/BaseDataTable.vue +267 -35
- package/services/frontend/src/orchestrator/components/menu/ServerStatus.vue +11 -2
- package/services/frontend/src/orchestrator/composables/useVueQueryObservableHelper.ts +12 -7
- package/services/frontend/src/orchestrator/composables/useVueWriteOperationHelper.ts +12 -6
- package/services/frontend/src/orchestrator/composables/useWidgetLifecycle.ts +2 -4
- package/services/frontend/src/orchestrator/i18n/de-DE.json +1 -0
- package/services/frontend/src/orchestrator/i18n/en-EU.json +1 -0
- package/services/frontend/src/orchestrator/i18n/sk.json +1 -0
- package/services/frontend/src/orchestrator/i18n/tl-TL.json +1 -0
- package/services/frontend/src/orchestrator/index.ts +7 -24
- package/services/frontend/src/orchestrator/lib/queryParamsSource.ts +48 -0
- package/services/frontend/src/orchestrator/main.ts +2 -2
- package/services/frontend/src/orchestrator/router/index.ts +0 -19
- package/services/frontend/src/orchestrator/router/registerEventHandlers.ts +29 -0
- package/services/frontend/src/orchestrator/router/routeStore.ts +92 -17
- package/services/frontend/src/orchestrator/router/routes.ts +5 -0
- package/services/frontend/src/orchestrator/setup/application.ts +11 -2
- package/services/frontend/src/orchestrator/types/DataApi.ts +19 -0
- package/services/frontend/src/orchestrator/types/PluginApi.ts +4 -7
- package/services/frontend/src/orchestrator/types/Widgets.ts +2 -0
- package/services/frontend/src/orchestrator/types/index.ts +1 -0
- package/templates/docker-compose.yml +1 -4
- package/templates/fs.yml +1 -1
- package/package/dist/index.js +0 -452
- package/services/backend/.yarn/install-state.gz +0 -0
- package/services/backend/dist/core/eventBus/index.js +0 -7
- package/services/backend/dist/core/pluginSystem/configGenerator/index.js +0 -346
- package/services/backend/dist/core/sockets/index.js +0 -95
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/account/events/index.js +0 -31
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/account/index.js +0 -48
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/account/routes/index.js +0 -15
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/composition/events/index.js +0 -22
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/composition/index.js +0 -51
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/composition/routes/index.js +0 -19
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/interactionLink/events/index.js +0 -22
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/interactionLink/index.js +0 -48
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/index.js +0 -19
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/project/events/index.js +0 -31
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/project/index.js +0 -49
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/project/routes/index.js +0 -16
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/tag/events/index.js +0 -39
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/tag/index.js +0 -50
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/tag/routes/index.js +0 -19
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/user/events/index.js +0 -31
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/user/index.js +0 -51
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/user/routes/index.js +0 -21
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/workSession/events/index.js +0 -31
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/workSession/index.js +0 -48
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/workSession/routes/index.js +0 -15
- package/services/backend/dist/corePlugins/raclette__core/backend/index.js +0 -34
- package/services/backend/dist/domains/index.js +0 -11
- package/services/backend/dist/domains/system/index.js +0 -11
- package/services/backend/dist/domains/system/routes/index.js +0 -17
- package/services/backend/dist/helpers/index.js +0 -14
- package/services/backend/dist/index.js +0 -3
- package/services/backend/dist/modules/authentication/index.js +0 -253
- package/services/backend/dist/shared/types/core/index.js +0 -8
- package/services/backend/dist/shared/types/dataTypes/index.js +0 -6
- package/services/backend/dist/shared/types/index.js +0 -8
- package/services/backend/dist/shared/types/plugins/index.js +0 -8
- package/services/backend/dist/types/index.js +0 -12
- package/services/backend/dist/utils/index.js +0 -2
- package/services/frontend/.yarn/install-state.gz +0 -0
|
@@ -65,15 +65,28 @@
|
|
|
65
65
|
<v-main
|
|
66
66
|
class="tw:flex tw:flex-col tw:min-h-0 tw:overflow-y-auto tw:h-screen"
|
|
67
67
|
id="appMainContent"
|
|
68
|
+
:aria-busy="loading"
|
|
68
69
|
>
|
|
69
70
|
<div class="tw:w-full tw-h-1 tw:top-[49px]">
|
|
70
71
|
<v-progress-linear
|
|
71
72
|
:height="2"
|
|
72
73
|
:indeterminate="loading"
|
|
73
74
|
color="primary"
|
|
75
|
+
role="progressbar"
|
|
76
|
+
:aria-hidden="!loading"
|
|
77
|
+
:aria-label="$t('core.app_loading')"
|
|
74
78
|
/>
|
|
75
79
|
</div>
|
|
76
80
|
|
|
81
|
+
<span
|
|
82
|
+
v-if="loading"
|
|
83
|
+
class="tw:sr-only"
|
|
84
|
+
role="status"
|
|
85
|
+
aria-live="polite"
|
|
86
|
+
>
|
|
87
|
+
{{ $t("core.app_loading") }}
|
|
88
|
+
</span>
|
|
89
|
+
|
|
77
90
|
<component v-if="showWelcomeScreen" :is="welcomeScreenComponent" />
|
|
78
91
|
|
|
79
92
|
<template v-else>
|
|
@@ -81,6 +94,7 @@
|
|
|
81
94
|
v-if="widgetsLayout"
|
|
82
95
|
class="tw:min-h-0 tw:pb-0"
|
|
83
96
|
:class="{ '!tw:invisible': loading }"
|
|
97
|
+
:aria-hidden="loading"
|
|
84
98
|
:widgets-layout="widgetsLayout"
|
|
85
99
|
slot-type="page"
|
|
86
100
|
:slot-layout="slotLayout"
|
|
@@ -259,7 +273,7 @@ const setTheme = (theme: "dark" | "light") => {
|
|
|
259
273
|
onMounted(async () => {
|
|
260
274
|
setTheme(theme.value)
|
|
261
275
|
vuetifyLocale.current.value = locale.value
|
|
262
|
-
isDevelopment.value =
|
|
276
|
+
isDevelopment.value = isDevelopmentMode()
|
|
263
277
|
logoExists.value = await checkImageExists(logoSrc.value)
|
|
264
278
|
// check if we have a message from checkToken to display as a snackbar
|
|
265
279
|
loginUserNotifications()
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
@layer tailwind-theme;
|
|
2
|
+
@layer tailwind-reset;
|
|
3
|
+
|
|
1
4
|
@layer vuetify-core;
|
|
2
5
|
@layer vuetify-components;
|
|
3
6
|
@layer vuetify-overrides;
|
|
4
7
|
@layer vuetify-utilities;
|
|
5
8
|
|
|
6
|
-
@layer tailwind;
|
|
9
|
+
@layer tailwind-utilities;
|
|
7
10
|
|
|
8
11
|
@layer vuetify-final;
|
|
@@ -2,22 +2,26 @@
|
|
|
2
2
|
<div>
|
|
3
3
|
<v-data-table
|
|
4
4
|
v-model="selected"
|
|
5
|
-
|
|
5
|
+
:search="showSearch ? search : undefined"
|
|
6
6
|
density="compact"
|
|
7
7
|
:items-per-page="itemsPerPage"
|
|
8
8
|
sticky
|
|
9
9
|
fixed-header
|
|
10
10
|
:headers="computedHeaders"
|
|
11
11
|
:items="filteredItems"
|
|
12
|
-
:loading="loading"
|
|
12
|
+
:loading="showLoading ? loading : false"
|
|
13
|
+
:row-props="resolveRowProps"
|
|
13
14
|
@click:row="handleRowClick"
|
|
14
15
|
@update:current-items="(items) => emit('update:currentItems', items)"
|
|
15
16
|
:class="{ itemsDeleted }"
|
|
16
|
-
:show-select="
|
|
17
|
+
:show-select="showSelectColumn"
|
|
17
18
|
:item-value
|
|
19
|
+
:group-by="resolvedGroupBy"
|
|
20
|
+
:sort-by="sortBy"
|
|
18
21
|
>
|
|
19
22
|
<template #top>
|
|
20
23
|
<v-toolbar
|
|
24
|
+
v-if="showTopToolbar"
|
|
21
25
|
flat
|
|
22
26
|
density="compact"
|
|
23
27
|
class="base-data-table-toolbar tw:pr-2"
|
|
@@ -105,7 +109,7 @@
|
|
|
105
109
|
</template>
|
|
106
110
|
</v-toolbar>
|
|
107
111
|
|
|
108
|
-
<slot name="toolbar-search">
|
|
112
|
+
<slot v-if="showSearch" name="toolbar-search">
|
|
109
113
|
<v-text-field
|
|
110
114
|
v-model="search"
|
|
111
115
|
:label="
|
|
@@ -192,8 +196,25 @@
|
|
|
192
196
|
/>
|
|
193
197
|
</template>
|
|
194
198
|
|
|
195
|
-
<!--
|
|
196
|
-
|
|
199
|
+
<!-- Always bridge tbody summary slots so v-data-table receives a stable slot (dynamic v-for is not reliable here). -->
|
|
200
|
+
<template #body.prepend="slotProps">
|
|
201
|
+
<slot name="body.prepend" v-bind="slotProps ?? {}" />
|
|
202
|
+
</template>
|
|
203
|
+
<template #body.append="slotProps">
|
|
204
|
+
<slot name="body.append" v-bind="slotProps ?? {}" />
|
|
205
|
+
</template>
|
|
206
|
+
<template #tfoot="slotProps">
|
|
207
|
+
<slot name="tfoot" v-bind="slotProps ?? {}" />
|
|
208
|
+
</template>
|
|
209
|
+
|
|
210
|
+
<!-- Forward remaining v-data-table slots (e.g. bottom, thead). -->
|
|
211
|
+
<template
|
|
212
|
+
v-for="slotName in vDataTablePassthroughSlots"
|
|
213
|
+
:key="slotName"
|
|
214
|
+
#[slotName]="slotProps"
|
|
215
|
+
>
|
|
216
|
+
<slot :name="slotName" v-bind="slotProps ?? {}" />
|
|
217
|
+
</template>
|
|
197
218
|
</v-data-table>
|
|
198
219
|
<!-- Additional dialogs slot -->
|
|
199
220
|
<slot name="dialogs" />
|
|
@@ -226,17 +247,29 @@
|
|
|
226
247
|
|
|
227
248
|
<slot name="filters.drawer.body" v-bind="filterDrawerBodySlotProps">
|
|
228
249
|
<div class="tw:px-4 tw:pb-4 tw:flex tw:flex-col tw:gap-3">
|
|
229
|
-
<v-
|
|
230
|
-
v-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
250
|
+
<template v-for="header in filterableHeaders" :key="String(header.key)">
|
|
251
|
+
<v-autocomplete
|
|
252
|
+
v-if="columnUsesComboboxFilter(String(header.key))"
|
|
253
|
+
v-model="columnFilters[String(header.key)]"
|
|
254
|
+
@update:model-value="notifyFilterStateChange"
|
|
255
|
+
:items="filterOptionsByKey[String(header.key)] ?? []"
|
|
256
|
+
:label="header.title"
|
|
257
|
+
clearable
|
|
258
|
+
hide-details
|
|
259
|
+
density="compact"
|
|
260
|
+
variant="outlined"
|
|
261
|
+
/>
|
|
262
|
+
<v-text-field
|
|
263
|
+
v-else
|
|
264
|
+
v-model="columnFilters[String(header.key)]"
|
|
265
|
+
@update:model-value="notifyFilterStateChange"
|
|
266
|
+
:label="header.title"
|
|
267
|
+
clearable
|
|
268
|
+
hide-details
|
|
269
|
+
density="compact"
|
|
270
|
+
variant="outlined"
|
|
271
|
+
/>
|
|
272
|
+
</template>
|
|
240
273
|
</div>
|
|
241
274
|
</slot>
|
|
242
275
|
|
|
@@ -266,7 +299,7 @@
|
|
|
266
299
|
</template>
|
|
267
300
|
|
|
268
301
|
<script setup lang="ts" generic="T extends Record<string, any>">
|
|
269
|
-
import { computed, ref, useSlots, useTemplateRef } from "vue"
|
|
302
|
+
import { Comment, computed, ref, useSlots, useTemplateRef, watch } from "vue"
|
|
270
303
|
import DataExporter from "@racletteOrchestrator/components/dataExport/DataExporter.vue"
|
|
271
304
|
import FileUpload from "@racletteOrchestrator/components/input/FileUpload.vue"
|
|
272
305
|
import type {
|
|
@@ -296,19 +329,33 @@ export interface BaseDataTableProps<T> {
|
|
|
296
329
|
width?: string | number
|
|
297
330
|
}>
|
|
298
331
|
itemsPerPage?: number
|
|
332
|
+
/** Initial / default column sort (Vuetify `{ key, order }[]`). */
|
|
333
|
+
sortBy?: ReadonlyArray<{ key: string; order: "asc" | "desc" }>
|
|
299
334
|
|
|
300
335
|
// Row actions
|
|
301
336
|
showActionsColumn?: boolean
|
|
302
337
|
|
|
303
338
|
// Item processing
|
|
304
339
|
rowClickHandler?: (item: T, context?: BaseDataTableActionContext<T>) => void
|
|
340
|
+
/** When set with `rowClickHandler`, only matching rows are clickable and show a pointer cursor. */
|
|
341
|
+
isRowClickable?: (item: T) => boolean
|
|
305
342
|
|
|
306
343
|
dataName: string
|
|
307
344
|
|
|
308
345
|
showExport?: boolean
|
|
346
|
+
/** Checkbox column for row selection (independent of export mode). */
|
|
347
|
+
showSelect?: boolean
|
|
309
348
|
showFilters?: boolean
|
|
349
|
+
/** Show the search field above the table (default true). */
|
|
350
|
+
showSearch?: boolean
|
|
351
|
+
/** Pass loading state through to v-data-table (default true). */
|
|
352
|
+
showLoading?: boolean
|
|
353
|
+
/** Max distinct values per column before filter falls back to a text field. */
|
|
354
|
+
filterMaxSelectOptions?: number
|
|
310
355
|
|
|
311
356
|
itemValue?: string
|
|
357
|
+
/** Vuetify group-by key(s), e.g. `"project"` for project-grouped rows. */
|
|
358
|
+
groupBy?: string | readonly string[]
|
|
312
359
|
actionsHeaderTitle?: string
|
|
313
360
|
|
|
314
361
|
/**
|
|
@@ -345,11 +392,17 @@ const props = withDefaults(defineProps<BaseDataTableProps<T>>(), {
|
|
|
345
392
|
uuid: undefined,
|
|
346
393
|
items: () => [],
|
|
347
394
|
itemsDeleted: false,
|
|
348
|
-
itemsPerPage:
|
|
395
|
+
itemsPerPage: 10,
|
|
396
|
+
sortBy: undefined,
|
|
349
397
|
showActionsColumn: true,
|
|
350
398
|
showExport: true,
|
|
399
|
+
showSelect: false,
|
|
351
400
|
showFilters: false,
|
|
401
|
+
showSearch: true,
|
|
402
|
+
showLoading: true,
|
|
403
|
+
filterMaxSelectOptions: 300,
|
|
352
404
|
itemValue: "_id",
|
|
405
|
+
groupBy: undefined,
|
|
353
406
|
actionsHeaderTitle: "",
|
|
354
407
|
exportableItems: undefined,
|
|
355
408
|
exportMeta: undefined,
|
|
@@ -368,8 +421,65 @@ const emit = defineEmits<{
|
|
|
368
421
|
"update:currentItems": [items: readonly DataTableItem<T>[]]
|
|
369
422
|
}>()
|
|
370
423
|
|
|
424
|
+
type VuetifyGroupByItem = { key: string; order?: "asc" | "desc" }
|
|
425
|
+
|
|
426
|
+
/** Vuetify expects `{ key }[]`; accept plain column key(s) from callers. */
|
|
427
|
+
const resolvedGroupBy = computed((): readonly VuetifyGroupByItem[] | undefined => {
|
|
428
|
+
const groupBy = props.groupBy
|
|
429
|
+
if (!groupBy) return undefined
|
|
430
|
+
if (typeof groupBy === "string") {
|
|
431
|
+
return [{ key: groupBy, order: "asc" }]
|
|
432
|
+
}
|
|
433
|
+
if (groupBy.length === 0) return undefined
|
|
434
|
+
if (typeof groupBy[0] === "string") {
|
|
435
|
+
return groupBy.map((key) => ({ key, order: "asc" as const }))
|
|
436
|
+
}
|
|
437
|
+
return groupBy as readonly VuetifyGroupByItem[]
|
|
438
|
+
})
|
|
439
|
+
|
|
371
440
|
const slots = useSlots()
|
|
372
441
|
|
|
442
|
+
/** Slots owned by BaseDataTable chrome (toolbar, filters drawer, row actions, export). */
|
|
443
|
+
const BASE_DATA_TABLE_RESERVED_SLOTS = new Set([
|
|
444
|
+
"toolbar-start",
|
|
445
|
+
"toolbar-leading",
|
|
446
|
+
"toolbar-end",
|
|
447
|
+
"toolbar-filters-trigger",
|
|
448
|
+
"toolbar-actions-trigger",
|
|
449
|
+
"action.export-mode",
|
|
450
|
+
"action.import",
|
|
451
|
+
"toolbar-actions-menu",
|
|
452
|
+
"toolbar-search",
|
|
453
|
+
"importer",
|
|
454
|
+
"exporter",
|
|
455
|
+
"prepend-row-actions",
|
|
456
|
+
"row-actions",
|
|
457
|
+
"append-row-actions",
|
|
458
|
+
"dialogs",
|
|
459
|
+
"filters.summary",
|
|
460
|
+
"filters.drawer.header",
|
|
461
|
+
"filters.drawer.body",
|
|
462
|
+
"filters.drawer.footer",
|
|
463
|
+
])
|
|
464
|
+
|
|
465
|
+
const V_DATA_TABLE_BRIDGED_SLOTS = new Set([
|
|
466
|
+
"body.prepend",
|
|
467
|
+
"body.append",
|
|
468
|
+
"tfoot",
|
|
469
|
+
])
|
|
470
|
+
|
|
471
|
+
/** v-data-table slots supplied by the parent (e.g. bottom), excluding built-in handling. */
|
|
472
|
+
const vDataTablePassthroughSlots = computed(() =>
|
|
473
|
+
Object.keys(slots).filter(
|
|
474
|
+
(name) =>
|
|
475
|
+
!BASE_DATA_TABLE_RESERVED_SLOTS.has(name) &&
|
|
476
|
+
!V_DATA_TABLE_BRIDGED_SLOTS.has(name) &&
|
|
477
|
+
!name.startsWith("item.") &&
|
|
478
|
+
name !== "loader" &&
|
|
479
|
+
name !== "top",
|
|
480
|
+
),
|
|
481
|
+
)
|
|
482
|
+
|
|
373
483
|
/** Hide ⋮ when the menu would be empty (no export toggle, import, or slot content). */
|
|
374
484
|
const showActionsOverflowMenu = computed(
|
|
375
485
|
() =>
|
|
@@ -380,17 +490,58 @@ const showActionsOverflowMenu = computed(
|
|
|
380
490
|
Boolean(slots["action.import"]),
|
|
381
491
|
)
|
|
382
492
|
|
|
493
|
+
const hasSlotContent = (name: string) => {
|
|
494
|
+
const slot = slots[name]
|
|
495
|
+
if (!slot) return false
|
|
496
|
+
return slot().some((vnode) => {
|
|
497
|
+
if (vnode.type === Comment) return false
|
|
498
|
+
if (typeof vnode.children === "string") return vnode.children.trim().length > 0
|
|
499
|
+
if (Array.isArray(vnode.children)) return vnode.children.length > 0
|
|
500
|
+
return true
|
|
501
|
+
})
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Toolbar hidden when no title, filters, export menu, import, or custom slot content. */
|
|
505
|
+
const showTopToolbar = computed(
|
|
506
|
+
() =>
|
|
507
|
+
Boolean(props.dataName?.length) ||
|
|
508
|
+
Boolean(props.showFilters) ||
|
|
509
|
+
showActionsOverflowMenu.value ||
|
|
510
|
+
Boolean(props.onFileLoaded) ||
|
|
511
|
+
hasSlotContent("toolbar-start") ||
|
|
512
|
+
hasSlotContent("toolbar-leading") ||
|
|
513
|
+
hasSlotContent("toolbar-end"),
|
|
514
|
+
)
|
|
515
|
+
|
|
383
516
|
const importFileUploadRef = useTemplateRef<{
|
|
384
517
|
openPicker: () => void
|
|
385
518
|
}>("importFileUploadRef")
|
|
386
519
|
|
|
387
520
|
// Reactive data
|
|
388
521
|
const search = ref("")
|
|
389
|
-
const selected =
|
|
522
|
+
const selected = defineModel<Array<string | number>>("selected", {
|
|
523
|
+
default: () => [],
|
|
524
|
+
})
|
|
390
525
|
const isExportMode = ref(false)
|
|
526
|
+
const showSelectColumn = computed(
|
|
527
|
+
() => props.showSelect || (props.showExport && isExportMode.value),
|
|
528
|
+
)
|
|
391
529
|
const isActionsMenuOpen = ref(false)
|
|
392
530
|
const isFilterDrawerOpen = ref(false)
|
|
531
|
+
/** Draft values edited in the filter drawer (applied only on Apply). */
|
|
393
532
|
const columnFilters = ref<Record<string, string>>({})
|
|
533
|
+
/** Values currently filtering the table. */
|
|
534
|
+
const appliedColumnFilters = ref<Record<string, string>>({})
|
|
535
|
+
|
|
536
|
+
const syncDraftFiltersFromApplied = () => {
|
|
537
|
+
columnFilters.value = { ...appliedColumnFilters.value }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
watch(isFilterDrawerOpen, (open) => {
|
|
541
|
+
if (open) {
|
|
542
|
+
syncDraftFiltersFromApplied()
|
|
543
|
+
}
|
|
544
|
+
})
|
|
394
545
|
|
|
395
546
|
const computedHeaders = computed(() => {
|
|
396
547
|
if (!props.headers || !props.headers?.length) {
|
|
@@ -400,7 +551,8 @@ const computedHeaders = computed(() => {
|
|
|
400
551
|
const headers = [...props.headers].map((header) => {
|
|
401
552
|
const key = String(header.key)
|
|
402
553
|
const hasFilter =
|
|
403
|
-
props.showFilters &&
|
|
554
|
+
props.showFilters &&
|
|
555
|
+
appliedColumnFilters.value[key]?.trim()?.length > 0
|
|
404
556
|
return {
|
|
405
557
|
...header,
|
|
406
558
|
title: header.title,
|
|
@@ -449,13 +601,42 @@ const filterableHeaders = computed(() =>
|
|
|
449
601
|
}),
|
|
450
602
|
)
|
|
451
603
|
|
|
604
|
+
const filterOptionsByKey = computed(() => {
|
|
605
|
+
const rows = props.items ?? []
|
|
606
|
+
const options: Record<string, string[]> = {}
|
|
607
|
+
|
|
608
|
+
for (const header of filterableHeaders.value) {
|
|
609
|
+
const key = String(header.key)
|
|
610
|
+
const values = new Set<string>()
|
|
611
|
+
for (const row of rows) {
|
|
612
|
+
const raw = row[key as keyof T]
|
|
613
|
+
if (raw === undefined || raw === null) {
|
|
614
|
+
continue
|
|
615
|
+
}
|
|
616
|
+
const text = String(raw)
|
|
617
|
+
if (text.length === 0) {
|
|
618
|
+
continue
|
|
619
|
+
}
|
|
620
|
+
values.add(text)
|
|
621
|
+
}
|
|
622
|
+
options[key] = [...values].sort((a, b) => a.localeCompare(b))
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return options
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
const columnUsesComboboxFilter = (key: string) => {
|
|
629
|
+
const count = filterOptionsByKey.value[key]?.length ?? 0
|
|
630
|
+
return count <= props.filterMaxSelectOptions
|
|
631
|
+
}
|
|
632
|
+
|
|
452
633
|
const filteredItems = computed(() => {
|
|
453
634
|
const rows = props.items ?? []
|
|
454
635
|
if (!props.showFilters) {
|
|
455
636
|
return rows
|
|
456
637
|
}
|
|
457
638
|
|
|
458
|
-
const activeFilters = Object.entries(
|
|
639
|
+
const activeFilters = Object.entries(appliedColumnFilters.value).filter(
|
|
459
640
|
([, value]) => value?.trim().length > 0,
|
|
460
641
|
)
|
|
461
642
|
if (!activeFilters.length) {
|
|
@@ -468,7 +649,11 @@ const filteredItems = computed(() => {
|
|
|
468
649
|
if (raw === undefined || raw === null) {
|
|
469
650
|
return false
|
|
470
651
|
}
|
|
471
|
-
|
|
652
|
+
const cell = String(raw)
|
|
653
|
+
if (columnUsesComboboxFilter(key)) {
|
|
654
|
+
return cell.toLowerCase() === filterValue.toLowerCase()
|
|
655
|
+
}
|
|
656
|
+
return cell.toLowerCase().includes(filterValue.toLowerCase())
|
|
472
657
|
}),
|
|
473
658
|
)
|
|
474
659
|
})
|
|
@@ -551,20 +736,57 @@ const exportPayloadData = computed<ExportPayload>(() => {
|
|
|
551
736
|
// Methods
|
|
552
737
|
const toggleExportMode = () => {
|
|
553
738
|
isExportMode.value = !isExportMode.value
|
|
554
|
-
if (!isExportMode.value) {
|
|
739
|
+
if (!isExportMode.value && !props.showSelect) {
|
|
555
740
|
selected.value = []
|
|
556
741
|
}
|
|
557
742
|
}
|
|
558
743
|
|
|
559
744
|
const toggleFilterDrawer = () => {
|
|
745
|
+
if (!isFilterDrawerOpen.value) {
|
|
746
|
+
syncDraftFiltersFromApplied()
|
|
747
|
+
}
|
|
560
748
|
isFilterDrawerOpen.value = !isFilterDrawerOpen.value
|
|
561
749
|
}
|
|
562
750
|
|
|
563
751
|
const openFilters = () => {
|
|
752
|
+
syncDraftFiltersFromApplied()
|
|
564
753
|
isFilterDrawerOpen.value = true
|
|
565
754
|
}
|
|
566
755
|
|
|
567
|
-
const
|
|
756
|
+
const isItemRowClickable = (item: T) =>
|
|
757
|
+
props.isRowClickable ? props.isRowClickable(item) : true
|
|
758
|
+
|
|
759
|
+
const resolveRowProps = (data: { item: T }) => {
|
|
760
|
+
if (!props.rowClickHandler) {
|
|
761
|
+
return {}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return {
|
|
765
|
+
class: isItemRowClickable(data.item)
|
|
766
|
+
? "tw:cursor-pointer"
|
|
767
|
+
: "tw:cursor-default",
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const isSelectionControlClick = (event: PointerEvent) => {
|
|
772
|
+
const target = event.target
|
|
773
|
+
if (!(target instanceof Element)) return false
|
|
774
|
+
return Boolean(
|
|
775
|
+
target.closest(
|
|
776
|
+
".v-selection-control, .v-data-table__checkbox, .v-data-table-column--select",
|
|
777
|
+
),
|
|
778
|
+
)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const handleRowClick = (event: PointerEvent, row: { item: T }) => {
|
|
782
|
+
if (showSelectColumn.value && isSelectionControlClick(event)) {
|
|
783
|
+
return
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (props.rowClickHandler && !isItemRowClickable(row.item)) {
|
|
787
|
+
return
|
|
788
|
+
}
|
|
789
|
+
|
|
568
790
|
const context = actionContext(row.item)
|
|
569
791
|
if (props.rowClickHandler) {
|
|
570
792
|
props.rowClickHandler(row.item, context)
|
|
@@ -573,31 +795,39 @@ const handleRowClick = (_: PointerEvent, row: { item: T }) => {
|
|
|
573
795
|
emit("rowClick", row.item)
|
|
574
796
|
}
|
|
575
797
|
|
|
576
|
-
const
|
|
798
|
+
const draftFilterPayload = computed(() => ({ ...columnFilters.value }))
|
|
799
|
+
const appliedFilterPayload = computed(() => ({
|
|
800
|
+
...appliedColumnFilters.value,
|
|
801
|
+
}))
|
|
577
802
|
const actionContext = (item?: T): BaseDataTableActionContext<T> => ({
|
|
578
803
|
item,
|
|
579
|
-
filters:
|
|
804
|
+
filters: appliedFilterPayload.value,
|
|
580
805
|
selectedIds: [...selected.value],
|
|
581
806
|
})
|
|
582
807
|
|
|
583
808
|
const notifyFilterStateChange = () => {
|
|
584
809
|
if (props.onFilterStateChange) {
|
|
585
|
-
props.onFilterStateChange(
|
|
810
|
+
props.onFilterStateChange(draftFilterPayload.value, {
|
|
811
|
+
...actionContext(),
|
|
812
|
+
filters: draftFilterPayload.value,
|
|
813
|
+
})
|
|
586
814
|
} else {
|
|
587
|
-
console.log("[BaseDataTable] filter-state-change",
|
|
815
|
+
console.log("[BaseDataTable] filter-state-change", draftFilterPayload.value)
|
|
588
816
|
}
|
|
589
817
|
}
|
|
590
818
|
|
|
591
819
|
const applyFilters = () => {
|
|
820
|
+
appliedColumnFilters.value = { ...columnFilters.value }
|
|
592
821
|
if (props.onFilterApply) {
|
|
593
|
-
props.onFilterApply(
|
|
822
|
+
props.onFilterApply(appliedFilterPayload.value, actionContext())
|
|
594
823
|
} else {
|
|
595
|
-
console.log("[BaseDataTable] filter-apply",
|
|
824
|
+
console.log("[BaseDataTable] filter-apply", appliedFilterPayload.value)
|
|
596
825
|
}
|
|
597
826
|
}
|
|
598
827
|
|
|
599
828
|
const resetFilters = () => {
|
|
600
829
|
columnFilters.value = {}
|
|
830
|
+
appliedColumnFilters.value = {}
|
|
601
831
|
if (props.onFilterReset) {
|
|
602
832
|
props.onFilterReset({}, actionContext())
|
|
603
833
|
} else {
|
|
@@ -658,8 +888,8 @@ const importerSlotProps = computed(() => ({
|
|
|
658
888
|
}))
|
|
659
889
|
|
|
660
890
|
const filterSummarySlotProps = computed(() => ({
|
|
661
|
-
filters:
|
|
662
|
-
activeCount: Object.values(
|
|
891
|
+
filters: appliedFilterPayload.value,
|
|
892
|
+
activeCount: Object.values(appliedColumnFilters.value).filter(
|
|
663
893
|
(value) => value?.trim().length > 0,
|
|
664
894
|
).length,
|
|
665
895
|
}))
|
|
@@ -668,12 +898,14 @@ const filterDrawerHeaderSlotProps = computed(() => ({
|
|
|
668
898
|
close: () => {
|
|
669
899
|
isFilterDrawerOpen.value = false
|
|
670
900
|
},
|
|
671
|
-
filters:
|
|
901
|
+
filters: draftFilterPayload.value,
|
|
672
902
|
}))
|
|
673
903
|
|
|
674
904
|
const filterDrawerBodySlotProps = computed(() => ({
|
|
675
905
|
filters: columnFilters.value,
|
|
676
906
|
headers: filterableHeaders.value,
|
|
907
|
+
filterOptionsByKey: filterOptionsByKey.value,
|
|
908
|
+
columnUsesComboboxFilter,
|
|
677
909
|
setFilter: (key: string, value: string) => {
|
|
678
910
|
columnFilters.value[key] = value
|
|
679
911
|
notifyFilterStateChange()
|
|
@@ -683,7 +915,7 @@ const filterDrawerBodySlotProps = computed(() => ({
|
|
|
683
915
|
const filterDrawerFooterSlotProps = computed(() => ({
|
|
684
916
|
applyFilters,
|
|
685
917
|
resetFilters,
|
|
686
|
-
filters:
|
|
918
|
+
filters: draftFilterPayload.value,
|
|
687
919
|
context: actionContext(),
|
|
688
920
|
}))
|
|
689
921
|
</script>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
v-bind="props"
|
|
11
11
|
:icon="currentState.icon"
|
|
12
12
|
:color="currentState.color"
|
|
13
|
+
:aria-label="serverStatusLabel"
|
|
13
14
|
/>
|
|
14
15
|
</template>
|
|
15
16
|
|
|
@@ -64,13 +65,21 @@ const theme = computed(() =>
|
|
|
64
65
|
vuetifyTheme.global.name.value === "light" ? "dark" : "light",
|
|
65
66
|
)
|
|
66
67
|
|
|
68
|
+
const socketStatus = computed(
|
|
69
|
+
() => backend.value?.socket?.status ?? 2,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const serverStatusLabel = computed(() =>
|
|
73
|
+
i18n.t(`core.socket_status_${socketStatus.value || 0}`),
|
|
74
|
+
)
|
|
75
|
+
|
|
67
76
|
const currentState = computed(() => {
|
|
68
|
-
const status =
|
|
77
|
+
const status = socketStatus.value
|
|
69
78
|
|
|
70
79
|
const stateMapping = [
|
|
71
80
|
{ icon: "mdi-alert-circle", color: "error" }, // status 0
|
|
72
81
|
{ icon: "mdi-checkbox-marked-circle", color: "success" }, // status 1
|
|
73
|
-
{ icon: "mdi-alert-circle", color: "warning" }, // status 2
|
|
82
|
+
{ icon: "mdi-alert-circle", color: "warning" }, // status 2 — connecting
|
|
74
83
|
]
|
|
75
84
|
|
|
76
85
|
return stateMapping[status] ?? stateMapping[2]
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { computed, ref, watch, onUnmounted } from "vue"
|
|
2
2
|
import { useObservable } from "@vueuse/rxjs"
|
|
3
3
|
import { $queryApi } from "@racletteCore/lib/data/dataApi"
|
|
4
|
+
import type { QueryApiQuery, QueryMethods } from "@racletteCore/types"
|
|
4
5
|
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "
|
|
6
|
+
VueQueryParamsInput,
|
|
7
|
+
VueStoreActionsPayload,
|
|
8
|
+
} from "../types/DataApi"
|
|
9
|
+
import { toQueryParamsSource } from "../lib/queryParamsSource"
|
|
9
10
|
import { difference } from "ramda"
|
|
10
11
|
import getStateObservable from "@racletteCore/lib/observableHelpers/getStateObservable"
|
|
11
12
|
import type { Observable } from "rxjs"
|
|
@@ -16,8 +17,9 @@ const subscriberCounter = {}
|
|
|
16
17
|
export const useVueQueryObservableHelper = (
|
|
17
18
|
instanceActionId,
|
|
18
19
|
target,
|
|
19
|
-
{ params = {}, options = {} }:
|
|
20
|
+
{ params = {}, options = {} }: VueStoreActionsPayload = {},
|
|
20
21
|
): QueryMethods => {
|
|
22
|
+
const paramsSource = toQueryParamsSource(params)
|
|
21
23
|
const dataObservable$ = ref<Observable<unknown>>()
|
|
22
24
|
|
|
23
25
|
const data = computed(() => {
|
|
@@ -36,7 +38,7 @@ export const useVueQueryObservableHelper = (
|
|
|
36
38
|
coreQuery = subscriber$[instanceActionId]
|
|
37
39
|
} else {
|
|
38
40
|
subscriberCounter[instanceActionId] = 1
|
|
39
|
-
coreQuery = $queryApi(instanceActionId, target, { params, options })
|
|
41
|
+
coreQuery = $queryApi(instanceActionId, target, { params: paramsSource, options })
|
|
40
42
|
}
|
|
41
43
|
subscriber$[instanceActionId] = coreQuery
|
|
42
44
|
const queryRef = useObservable<QueryApiQuery>(coreQuery.query$)
|
|
@@ -77,7 +79,10 @@ export const useVueQueryObservableHelper = (
|
|
|
77
79
|
return data.value ? Object.values(data.value) : []
|
|
78
80
|
}),
|
|
79
81
|
response: responseRef,
|
|
80
|
-
execute:
|
|
82
|
+
execute: (executeParams?: VueQueryParamsInput) =>
|
|
83
|
+
executeParams === undefined
|
|
84
|
+
? coreQuery.execute()
|
|
85
|
+
: coreQuery.execute(toQueryParamsSource(executeParams)),
|
|
81
86
|
uuids: coreQuery.uuids$,
|
|
82
87
|
remove: coreQuery.remove,
|
|
83
88
|
clear: coreQuery.clear,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { computed, ref, watch, onUnmounted } from "vue"
|
|
2
2
|
import { useObservable } from "@vueuse/rxjs"
|
|
3
3
|
import { $writeDataApi } from "@racletteCore/lib/data/dataApi"
|
|
4
|
+
import type { DataApiMethods } from "@racletteCore/types"
|
|
4
5
|
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "
|
|
6
|
+
VueQueryParamsInput,
|
|
7
|
+
VueStoreActionsPayload,
|
|
8
|
+
} from "../types/DataApi"
|
|
9
|
+
import { toQueryParamsSource } from "../lib/queryParamsSource"
|
|
8
10
|
import getStateObservable from "@racletteCore/lib/observableHelpers/getStateObservable"
|
|
9
11
|
import type { Observable } from "rxjs"
|
|
10
12
|
|
|
@@ -14,8 +16,9 @@ const subscriberCounter = {}
|
|
|
14
16
|
export const useVueWriteOperationHelper = (
|
|
15
17
|
instanceActionId,
|
|
16
18
|
operationDefinition,
|
|
17
|
-
{ params = {}, options = {} }:
|
|
19
|
+
{ params = {}, options = {} }: VueStoreActionsPayload = {},
|
|
18
20
|
): DataApiMethods => {
|
|
21
|
+
const paramsSource = toQueryParamsSource(params)
|
|
19
22
|
let writeOperation
|
|
20
23
|
const dataStoreObservable$ = ref<Observable<any>>()
|
|
21
24
|
|
|
@@ -34,7 +37,7 @@ export const useVueWriteOperationHelper = (
|
|
|
34
37
|
} else {
|
|
35
38
|
subscriberCounter[instanceActionId] = 1
|
|
36
39
|
writeOperation = $writeDataApi(instanceActionId, operationDefinition, {
|
|
37
|
-
params,
|
|
40
|
+
params: paramsSource,
|
|
38
41
|
options,
|
|
39
42
|
})
|
|
40
43
|
}
|
|
@@ -84,7 +87,10 @@ export const useVueWriteOperationHelper = (
|
|
|
84
87
|
return data.value ? Object.values(data.value) : []
|
|
85
88
|
}),
|
|
86
89
|
response: responseRef,
|
|
87
|
-
execute:
|
|
90
|
+
execute: (executeParams?: VueQueryParamsInput) =>
|
|
91
|
+
executeParams === undefined
|
|
92
|
+
? writeOperation.execute()
|
|
93
|
+
: writeOperation.execute(toQueryParamsSource(executeParams)),
|
|
88
94
|
remove: writeOperation.remove,
|
|
89
95
|
clear: writeOperation.clear,
|
|
90
96
|
error: computed(() => {
|