@raclettejs/core 0.1.36 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +32 -7
  2. package/dist/cli.js +1 -1
  3. package/dist/cli.js.map +2 -2
  4. package/package.json +2 -2
  5. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/composition.schema.ts +10 -4
  6. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/interactionLink.schema.ts +4 -1
  7. package/services/backend/src/shared/schemas/core/CompositionCreate.schema.ts +4 -0
  8. package/services/backend/src/shared/schemas/core/InteractionLinkCreate.schema.ts +4 -0
  9. package/services/backend/src/shared/types/core/CompositionCreate.types.ts +1 -0
  10. package/services/backend/src/shared/types/core/InteractionLinkCreate.types.ts +1 -0
  11. package/services/frontend/src/core/lib/scheduleTask.ts +35 -0
  12. package/services/frontend/src/core/store/index.ts +5 -1
  13. package/services/frontend/src/core/store/reducers/compositionSlots/reducers.ts +1 -5
  14. package/services/frontend/src/core/store/reducers/data/reducers.ts +101 -65
  15. package/services/frontend/src/core/store/reducers/metaReducer.ts +23 -0
  16. package/services/frontend/src/core/store/reducers/notifications/reducers.ts +39 -47
  17. package/services/frontend/src/core/store/reducers/queries/effects.ts +55 -2
  18. package/services/frontend/src/core/store/reducers/queries/reducers.ts +60 -62
  19. package/services/frontend/src/core/store/reducers/queriesCache/effects.ts +105 -0
  20. package/services/frontend/src/core/store/reducers/queriesCache/index.ts +2 -1
  21. package/services/frontend/src/core/store/reducers/queriesCache/queryCacheHelper.ts +79 -9
  22. package/services/frontend/src/core/store/reducers/queriesCache/reducers.ts +95 -100
  23. package/services/frontend/src/orchestrator/components/CompositionLoadingState.vue +28 -0
  24. package/services/frontend/src/orchestrator/components/composition/CompositionOverlay.vue +5 -7
  25. package/services/frontend/src/orchestrator/components/composition/WidgetsLayoutLoader.vue +171 -39
  26. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTable.vue +336 -56
  27. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTableConfirmDeleteBtn.vue +27 -0
  28. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTableFilterDrawerShell.vue +72 -0
  29. package/services/frontend/src/orchestrator/components/index.ts +4 -0
  30. package/services/frontend/src/orchestrator/composables/index.ts +1 -0
  31. package/services/frontend/src/orchestrator/composables/useBaseDataTableDeleteConfirm.ts +23 -0
  32. package/services/frontend/src/orchestrator/composables/useVueWriteOperationHelper.ts +16 -4
  33. package/services/frontend/src/orchestrator/composables/useWidgetLifecycle.ts +53 -23
  34. package/services/frontend/src/orchestrator/constants/widgetSlotType.ts +4 -0
  35. package/services/frontend/src/orchestrator/i18n/de-DE.json +7 -1
  36. package/services/frontend/src/orchestrator/i18n/en-EU.json +7 -1
  37. package/services/frontend/src/orchestrator/i18n/sk.json +7 -1
  38. package/services/frontend/src/orchestrator/router/routerHooks/afterEach.ts +15 -9
  39. package/services/frontend/src/orchestrator/types/DataApi.ts +2 -1
@@ -0,0 +1,72 @@
1
+ <template>
2
+ <v-navigation-drawer
3
+ v-if="!contained"
4
+ :model-value="modelValue"
5
+ location="right"
6
+ temporary
7
+ :width="width"
8
+ :aria-label="ariaLabel"
9
+ @update:model-value="emit('update:modelValue', $event)"
10
+ >
11
+ <slot />
12
+ </v-navigation-drawer>
13
+
14
+ <template v-else>
15
+ <div
16
+ v-if="modelValue"
17
+ class="base-data-table-filter-scrim"
18
+ aria-hidden="true"
19
+ @click="emit('update:modelValue', false)"
20
+ />
21
+ <aside
22
+ v-if="modelValue"
23
+ class="base-data-table-filter-panel"
24
+ :class="{ 'base-data-table-filter-panel--contained': contained }"
25
+ :style="contained ? undefined : { width: `${width}px` }"
26
+ :aria-label="ariaLabel"
27
+ >
28
+ <slot />
29
+ </aside>
30
+ </template>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ defineProps<{
35
+ modelValue: boolean
36
+ contained: boolean
37
+ width: number
38
+ ariaLabel: string
39
+ }>()
40
+
41
+ const emit = defineEmits<{
42
+ "update:modelValue": [value: boolean]
43
+ }>()
44
+ </script>
45
+
46
+ <style scoped>
47
+ .base-data-table-filter-scrim {
48
+ position: absolute;
49
+ inset: 0;
50
+ z-index: 3;
51
+ background: rgba(0, 0, 0, 0.4);
52
+ }
53
+
54
+ .base-data-table-filter-panel {
55
+ position: absolute;
56
+ top: 0;
57
+ right: 0;
58
+ bottom: 0;
59
+ z-index: 4;
60
+ display: flex;
61
+ flex-direction: column;
62
+ min-height: 0;
63
+ box-sizing: border-box;
64
+ background: rgb(var(--v-theme-surface));
65
+ box-shadow: -4px 0 16px rgba(0, 0, 0, 0.12);
66
+ }
67
+
68
+ .base-data-table-filter-panel--contained {
69
+ width: min(100%, 360px);
70
+ max-width: 100%;
71
+ }
72
+ </style>
@@ -3,6 +3,7 @@ import DefaultModal from "./modal/DefaultModal.vue"
3
3
  import DraggableElement from "./dragAndDrop/DraggableElement.vue"
4
4
  import DropzoneElement from "./dragAndDrop/DropzoneElement.vue"
5
5
  import ErrorWidgetState from "./ErrorWidgetState.vue"
6
+ import CompositionLoadingState from "./CompositionLoadingState.vue"
6
7
  import LoadingWidgetState from "./LoadingWidgetState.vue"
7
8
  import LoginForm from "./authentication/LoginForm.vue"
8
9
  import PageNavigationSidebar from "./navigation/PageNavigationSidebar.vue"
@@ -18,13 +19,16 @@ import DataExporter from "./dataExport/DataExporter.vue"
18
19
  import ImportSelectionDialog from "./dataImport/ImportSelectionDialog.vue"
19
20
  import ImportSummaryDialog from "./dataImport/ImportSummaryDialog.vue"
20
21
  import BaseDataTable from "./dataTable/BaseDataTable.vue"
22
+ import BaseDataTableConfirmDeleteBtn from "./dataTable/BaseDataTableConfirmDeleteBtn.vue"
21
23
  import FileUpload from "./input/FileUpload.vue"
22
24
 
23
25
  export {
24
26
  ImportSelectionDialog,
25
27
  ImportSummaryDialog,
26
28
  BaseDataTable,
29
+ BaseDataTableConfirmDeleteBtn,
27
30
  FileUpload,
31
+ CompositionLoadingState,
28
32
  CompositionOverlay,
29
33
  DataExporter,
30
34
  DefaultModal,
@@ -5,3 +5,4 @@ export * from "./useWidgets"
5
5
  export * from "./useInteractionLinks"
6
6
  export * from "./useWidgetLifecycle"
7
7
  export * from "./useExport"
8
+ export * from "./useBaseDataTableDeleteConfirm"
@@ -0,0 +1,23 @@
1
+ import { inject, type InjectionKey } from "vue"
2
+
3
+ export type RequestDeleteFn = <T>(
4
+ item: T,
5
+ onConfirm: () => void | Promise<void>,
6
+ ) => void
7
+
8
+ export const BASE_DATA_TABLE_REQUEST_DELETE_KEY: InjectionKey<RequestDeleteFn> =
9
+ Symbol("baseDataTableRequestDelete")
10
+
11
+ /**
12
+ * Call from `#row-actions` (or descendants) inside `BaseDataTable` to run delete
13
+ * through the table's optional confirmation dialog.
14
+ */
15
+ export const useBaseDataTableDeleteConfirm = () => {
16
+ const requestDelete = inject(BASE_DATA_TABLE_REQUEST_DELETE_KEY)
17
+ if (!requestDelete) {
18
+ throw new Error(
19
+ "useBaseDataTableDeleteConfirm must be used within BaseDataTable",
20
+ )
21
+ }
22
+ return { requestDelete }
23
+ }
@@ -87,10 +87,22 @@ export const useVueWriteOperationHelper = (
87
87
  return data.value ? Object.values(data.value) : []
88
88
  }),
89
89
  response: responseRef,
90
- execute: (executeParams?: VueQueryParamsInput) =>
91
- executeParams === undefined
92
- ? writeOperation.execute()
93
- : writeOperation.execute(toQueryParamsSource(executeParams)),
90
+ execute: (executeParams?: VueQueryParamsInput) => {
91
+ if (executeParams === undefined) {
92
+ return writeOperation.execute()
93
+ }
94
+ // Bulk POST bodies (e.g. createBulk) must reach core dataApi as arrays.
95
+ if (Array.isArray(executeParams)) {
96
+ return writeOperation.execute(executeParams)
97
+ }
98
+ if (
99
+ typeof FormData !== "undefined" &&
100
+ executeParams instanceof FormData
101
+ ) {
102
+ return writeOperation.execute(executeParams)
103
+ }
104
+ return writeOperation.execute(toQueryParamsSource(executeParams))
105
+ },
94
106
  remove: writeOperation.remove,
95
107
  clear: writeOperation.clear,
96
108
  error: computed(() => {
@@ -1,6 +1,7 @@
1
1
  import { ref, type Ref } from "vue"
2
2
  import { isDevelopmentMode } from "@racletteCore/helpers/devMode"
3
3
  import log from "@racletteCore/lib/logger"
4
+ import { scheduleAfterPaint } from "@racletteCore/lib/scheduleTask"
4
5
 
5
6
  export type OnAllWidgetsReady = (callback: () => void) => void
6
7
 
@@ -26,6 +27,12 @@ export function createWidgetLifecycleManager(
26
27
 
27
28
  const DEV_MODE = isDevelopmentMode()
28
29
 
30
+ const markReadyIfNoWidgets = () => {
31
+ if (totalWidgets === 0) {
32
+ allWidgetsReady.value = true
33
+ }
34
+ }
35
+
29
36
  const checkAllReady = () => {
30
37
  if (renderedWidgets.value.size === totalWidgets && !allWidgetsReady.value) {
31
38
  allWidgetsReady.value = true
@@ -43,22 +50,46 @@ export function createWidgetLifecycleManager(
43
50
  }
44
51
 
45
52
  const hasVisibleContent = (element: HTMLElement): boolean => {
46
- // Check if element has any child nodes
47
53
  if (element.childNodes.length === 0) {
48
54
  return false
49
55
  }
50
56
 
51
- // Check if element has visible dimensions
52
- const rect = element.getBoundingClientRect()
53
- if (rect.width === 0 && rect.height === 0) {
54
- return false
57
+ const text = element.textContent?.trim()
58
+ if (text?.length) {
59
+ return true
60
+ }
61
+
62
+ // DOM-only checks — avoid getBoundingClientRect / offset* (forced reflow).
63
+ return (
64
+ element.children.length > 0 ||
65
+ element.querySelector(
66
+ "table, img, canvas, svg, video, iframe, input, button, select, textarea, [role='row']",
67
+ ) !== null
68
+ )
69
+ }
70
+
71
+ const scheduleContentCheck = (
72
+ element: HTMLElement,
73
+ onReady: () => void,
74
+ ): (() => void) => {
75
+ let scheduled = false
76
+
77
+ const runCheck = () => {
78
+ scheduled = false
79
+ if (hasVisibleContent(element)) {
80
+ onReady()
81
+ }
55
82
  }
56
83
 
57
- // Check for text content or child elements
58
- const hasTextContent = element.textContent?.trim().length > 0
59
- const hasChildElements = element.children.length > 0
84
+ const scheduleCheck = () => {
85
+ if (scheduled) {
86
+ return
87
+ }
88
+ scheduled = true
89
+ scheduleAfterPaint(runCheck)
90
+ }
60
91
 
61
- return hasTextContent || hasChildElements
92
+ return scheduleCheck
62
93
  }
63
94
 
64
95
  const registerWidgetAsRendered = (id: string) => {
@@ -96,30 +127,25 @@ export function createWidgetLifecycleManager(
96
127
  }
97
128
 
98
129
  const observeWidgetRendering = (id: string, element: HTMLElement) => {
99
- // Check immediately if content is already rendered
100
- if (hasVisibleContent(element)) {
130
+ let observer: MutationObserver | null = null
131
+
132
+ const markReady = () => {
133
+ observer?.disconnect()
134
+ observer = null
135
+ observers.delete(id)
101
136
  registerWidgetAsRendered(id)
102
- return
103
137
  }
104
138
 
105
- // Otherwise, observe for changes
106
- const observer = new MutationObserver(() => {
107
- if (hasVisibleContent(element)) {
108
- registerWidgetAsRendered(id)
109
- observer.disconnect()
110
- observers.delete(id)
111
- }
112
- })
139
+ const scheduleCheck = scheduleContentCheck(element, markReady)
113
140
 
141
+ observer = new MutationObserver(scheduleCheck)
114
142
  observer.observe(element, {
115
143
  childList: true,
116
144
  subtree: true,
117
145
  characterData: true,
118
- attributes: true,
119
- attributeFilter: ["style", "class"],
120
146
  })
121
-
122
147
  observers.set(id, observer)
148
+ scheduleCheck()
123
149
 
124
150
  if (DEV_MODE) {
125
151
  log.widgets(
@@ -151,11 +177,15 @@ export function createWidgetLifecycleManager(
151
177
  allWidgetsReady.value = false
152
178
  readyCallbacks.clear()
153
179
 
180
+ markReadyIfNoWidgets()
181
+
154
182
  if (DEV_MODE) {
155
183
  log.widgets("useWidgetLifecycle", "🔄 Widget lifecycle manager reset")
156
184
  }
157
185
  }
158
186
 
187
+ markReadyIfNoWidgets()
188
+
159
189
  return {
160
190
  createLifecycleHooks,
161
191
  allWidgetsReady,
@@ -0,0 +1,4 @@
1
+ /** Composition slot type from `WidgetsLayoutLoader` (`page` | `modal` | `inline`). */
2
+ export const WIDGET_SLOT_TYPE_KEY = Symbol("widgetSlotType")
3
+
4
+ export type WidgetSlotType = "page" | "modal" | "inline"
@@ -42,8 +42,14 @@
42
42
  "resetFilters": "Filter zurücksetzen",
43
43
  "applyFilters": "Filter anwenden",
44
44
  "actionsMenu": "Tabellenaktionen",
45
+ "actions": "Aktionen",
45
46
  "enableExportMode": "Exportmodus aktivieren",
46
- "disableExportMode": "Exportmodus deaktivieren"
47
+ "disableExportMode": "Exportmodus deaktivieren",
48
+ "confirmDeleteTitle": "Löschung bestätigen",
49
+ "confirmDeleteMessage": "Möchten Sie {name} wirklich löschen?",
50
+ "confirmHardDeleteMessage": "Möchten Sie {name} wirklich endgültig löschen?",
51
+ "delete": "Löschen",
52
+ "activeModesTooltip": "Aktiv: {modes}"
47
53
  },
48
54
  "dataExporter": {
49
55
  "lines": "Zeilen",
@@ -42,8 +42,14 @@
42
42
  "resetFilters": "Reset filters",
43
43
  "applyFilters": "Apply filters",
44
44
  "actionsMenu": "Table actions",
45
+ "actions": "Actions",
45
46
  "enableExportMode": "Enable export mode",
46
- "disableExportMode": "Disable export mode"
47
+ "disableExportMode": "Disable export mode",
48
+ "confirmDeleteTitle": "Confirm deletion",
49
+ "confirmDeleteMessage": "Are you sure you want to delete {name}?",
50
+ "confirmHardDeleteMessage": "Are you sure you want to permanently delete {name}?",
51
+ "delete": "Delete",
52
+ "activeModesTooltip": "Active: {modes}"
47
53
  },
48
54
  "dataExporter": {
49
55
  "lines": "Lines",
@@ -42,8 +42,14 @@
42
42
  "resetFilters": "Obnoviť filtre",
43
43
  "applyFilters": "Použiť filtre",
44
44
  "actionsMenu": "Akcie tabuľky",
45
+ "actions": "Akcie",
45
46
  "enableExportMode": "Zapnúť režim exportu",
46
- "disableExportMode": "Vypnúť režim exportu"
47
+ "disableExportMode": "Vypnúť režim exportu",
48
+ "confirmDeleteTitle": "Potvrdiť vymazanie",
49
+ "confirmDeleteMessage": "Naozaj chcete vymazať {name}?",
50
+ "confirmHardDeleteMessage": "Naozaj chcete natrvalo vymazať {name}?",
51
+ "delete": "Vymazať",
52
+ "activeModesTooltip": "Aktívne: {modes}"
47
53
  },
48
54
  "dataExporter": {
49
55
  "lines": "riadky",
@@ -1,5 +1,6 @@
1
1
  import { loadingHold } from "@racletteCore/store/state"
2
2
  import { $store } from "@racletteCore"
3
+ import { scheduleAfterPaint } from "@racletteCore/lib/scheduleTask"
3
4
  import type { RouteLocationNormalized } from "vue-router"
4
5
  import { handleRouteChange } from "../routeStore"
5
6
 
@@ -10,16 +11,21 @@ import { handleRouteChange } from "../routeStore"
10
11
  * removeLoading Animation
11
12
  */
12
13
  export default (to: RouteLocationNormalized, from: RouteLocationNormalized) => {
14
+ scheduleAfterPaint(() => {
15
+ handleRouteChange(to, from)
16
+ })
17
+
13
18
  const loading = $store.getState(["ui", "loading"])
14
19
  if (loading) {
15
- setTimeout(
16
- () =>
17
- $store.dispatch({
18
- type: "ui/update",
19
- payload: { loading: false },
20
- }),
21
- loadingHold,
22
- )
20
+ setTimeout(() => {
21
+ scheduleAfterPaint(() => {
22
+ if ($store.getState(["ui", "loading"])) {
23
+ $store.dispatch({
24
+ type: "ui/update",
25
+ payload: { loading: false },
26
+ })
27
+ }
28
+ })
29
+ }, loadingHold)
23
30
  }
24
- handleRouteChange(to, from)
25
31
  }
@@ -4,10 +4,11 @@ import type {
4
4
  CustomStoreActionsPayload,
5
5
  } from "@racletteCore/types"
6
6
 
7
- /** Params accepted by Vue plugin `$data` methods (refs/computed/getters allowed). */
7
+ /** Params accepted by Vue plugin `$data` write `execute()` (refs/computed/getters, or bulk body arrays). */
8
8
  export type VueQueryParamsInput =
9
9
  | MaybeRefOrGetter<CustomStoreActionParams | undefined>
10
10
  | CustomStoreActionParams
11
+ | unknown[]
11
12
  | undefined
12
13
 
13
14
  /** Vue orchestrator payload — extends core with reactive `params`. */