@raclettejs/core 0.1.40 → 0.1.42

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 (38) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/dist/cli.js +104 -104
  3. package/dist/cli.js.map +4 -4
  4. package/dist/index.js.map +2 -2
  5. package/package.json +2 -2
  6. package/services/frontend/eslint.config.js +31 -9
  7. package/services/frontend/src/core/setup/socketBootstrap.ts +4 -3
  8. package/services/frontend/src/core/store/state.ts +2 -7
  9. package/services/frontend/src/core/store/types/index.ts +2 -0
  10. package/services/frontend/src/orchestrator/ProductOrchestrator.vue +33 -22
  11. package/services/frontend/src/orchestrator/components/LoadingWidgetState.vue +7 -3
  12. package/services/frontend/src/orchestrator/components/composition/WidgetsLayoutLoader.vue +179 -195
  13. package/services/frontend/src/orchestrator/components/dataExport/DataExporter.vue +79 -24
  14. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTable.vue +27 -14
  15. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTableGroupSelectCheckbox.vue +42 -0
  16. package/services/frontend/src/orchestrator/components/index.ts +2 -0
  17. package/services/frontend/src/orchestrator/components/menu/ServerStatus.vue +2 -4
  18. package/services/frontend/src/orchestrator/components/menu/UserMenu.vue +1 -1
  19. package/services/frontend/src/orchestrator/components/welcomeScreen/AdminWelcomeScreen.vue +1 -1
  20. package/services/frontend/src/orchestrator/composables/index.ts +1 -0
  21. package/services/frontend/src/orchestrator/composables/useAppLoadingCoordinator.ts +77 -0
  22. package/services/frontend/src/orchestrator/composables/usePageNavigation.ts +1 -1
  23. package/services/frontend/src/orchestrator/composables/useUiState.ts +2 -0
  24. package/services/frontend/src/orchestrator/composables/useWidgetLifecycle.ts +65 -76
  25. package/services/frontend/src/orchestrator/composables/useWidgets/helperFunctions.ts +7 -4
  26. package/services/frontend/src/orchestrator/constants/index.ts +2 -0
  27. package/services/frontend/src/orchestrator/constants/widgetSlot.ts +7 -0
  28. package/services/frontend/src/orchestrator/exports.ts +1 -0
  29. package/services/frontend/src/orchestrator/helpers/staticWidgetRegistry.ts +9 -44
  30. package/services/frontend/src/orchestrator/helpers/widgetLayoutHelper.ts +37 -0
  31. package/services/frontend/src/orchestrator/helpers/widgetLoader.ts +0 -1
  32. package/services/frontend/src/orchestrator/helpers/widgetSlotHelper.ts +20 -0
  33. package/services/frontend/src/orchestrator/helpers/widgetVisuals.ts +11 -12
  34. package/services/frontend/src/orchestrator/helpers/wrapWidgetWithLifecycle.ts +32 -0
  35. package/services/frontend/src/orchestrator/router/routerHooks/afterEach.ts +4 -29
  36. package/services/frontend/src/orchestrator/router/routerHooks/beforeEach.ts +4 -1
  37. package/services/frontend/src/orchestrator/types/Widgets.ts +12 -6
  38. package/src/types.ts +7 -1
@@ -26,14 +26,23 @@
26
26
  <v-card rounded="lg" elevation="8" :style="{ width: '360px' }">
27
27
  <!-- Header -->
28
28
  <slot name="header" v-bind="headerSlotProps">
29
- <div class="tw:flex tw:items-center tw:justify-between tw:px-4 tw:pt-4 tw:pb-2">
29
+ <div
30
+ class="tw:flex tw:items-center tw:justify-between tw:px-4 tw:pt-4 tw:pb-2"
31
+ >
30
32
  <div class="tw:flex tw:items-center tw:gap-2">
31
33
  <v-icon icon="mdi-export-variant" size="18" color="primary" />
32
- <span class="tw:text-sm tw:font-semibold tw:tracking-wide tw:uppercase tw:opacity-70">
34
+ <span
35
+ class="tw:text-sm tw:font-semibold tw:tracking-wide tw:uppercase tw:opacity-70"
36
+ >
33
37
  {{ $t("core.dataExporter.title") }}
34
38
  </span>
35
39
  </div>
36
- <v-btn icon="mdi-close" variant="text" size="x-small" @click="isOpen = false" />
40
+ <v-btn
41
+ icon="mdi-close"
42
+ variant="text"
43
+ size="x-small"
44
+ @click="isOpen = false"
45
+ />
37
46
  </div>
38
47
  </slot>
39
48
 
@@ -71,7 +80,12 @@
71
80
  <span class="tw:text-xs tw:opacity-60 tw:uppercase tw:tracking-wide">
72
81
  {{ $t("core.dataExporter.scopeLabel") }}
73
82
  </span>
74
- <v-btn-toggle v-model="selectedScope" divided density="compact" mandatory>
83
+ <v-btn-toggle
84
+ v-model="selectedScope"
85
+ divided
86
+ density="compact"
87
+ mandatory
88
+ >
75
89
  <v-btn value="combined" size="x-small">
76
90
  {{ $t("core.dataExporter.scopeCombined") }}
77
91
  </v-btn>
@@ -86,9 +100,18 @@
86
100
  </div>
87
101
 
88
102
  <!-- No formats warning -->
89
- <div v-if="resolvedFormats.length === 0" class="tw:px-4 tw:py-6 tw:text-center">
90
- <v-icon icon="mdi-alert-circle-outline" color="warning" class="tw:mb-2" />
91
- <p class="tw:text-sm tw:opacity-60">{{ $t("core.dataExporter.noValidFormats") }}</p>
103
+ <div
104
+ v-if="resolvedFormats.length === 0"
105
+ class="tw:px-4 tw:py-6 tw:text-center"
106
+ >
107
+ <v-icon
108
+ icon="mdi-alert-circle-outline"
109
+ color="warning"
110
+ class="tw:mb-2"
111
+ />
112
+ <p class="tw:text-sm tw:opacity-60">
113
+ {{ $t("core.dataExporter.noValidFormats") }}
114
+ </p>
92
115
  </div>
93
116
 
94
117
  <!-- Preview pane -->
@@ -98,7 +121,7 @@
98
121
  <v-btn
99
122
  variant="text"
100
123
  density="compact"
101
- class="tw:mb-1.5 tw:w-full tw:justify-start tw:normal-case tw:text-xs tw:opacity-60 hover:tw:opacity-90"
124
+ class="tw:mb-1.5 tw:w-full tw:justify-start tw:normal-case tw:text-xs tw:opacity-60 tw:hover:opacity-90"
102
125
  @click="previewOpen = !previewOpen"
103
126
  >
104
127
  <template #prepend>
@@ -112,7 +135,13 @@
112
135
  </slot>
113
136
 
114
137
  <!-- Fixed-height container prevents width reflow when toggling -->
115
- <div class="tw:overflow-hidden" :style="{ height: previewOpen ? 'auto' : '0px', minHeight: previewOpen ? '0px' : undefined }">
138
+ <div
139
+ class="tw:overflow-hidden"
140
+ :style="{
141
+ height: previewOpen ? 'auto' : '0px',
142
+ minHeight: previewOpen ? '0px' : undefined,
143
+ }"
144
+ >
116
145
  <v-expand-transition>
117
146
  <div v-show="previewOpen">
118
147
  <slot name="preview-content" v-bind="previewContentSlotProps">
@@ -124,7 +153,9 @@
124
153
  density="compact"
125
154
  class="tw:mb-2 tw:font-mono tw:text-xs"
126
155
  >
127
- <pre class="tw:whitespace-pre-wrap tw:break-all">{{ serializeResult.error }}</pre>
156
+ <pre class="tw:whitespace-pre-wrap tw:break-all">{{
157
+ serializeResult.error
158
+ }}</pre>
128
159
  </v-alert>
129
160
 
130
161
  <!-- Preview code block -->
@@ -136,7 +167,9 @@
136
167
  <div
137
168
  class="tw:flex tw:items-center tw:justify-between tw:px-3 tw:py-1.5 tw:bg-black/5 dark:tw:bg-white/5 tw:border-b tw:border-black/10 dark:tw:border-white/10"
138
169
  >
139
- <span class="tw:font-mono tw:text-[10px] tw:uppercase tw:tracking-widest tw:opacity-50">
170
+ <span
171
+ class="tw:font-mono tw:text-[10px] tw:uppercase tw:tracking-widest tw:opacity-50"
172
+ >
140
173
  {{ activeFormatId }}
141
174
  </span>
142
175
  <span class="tw:font-mono tw:text-[10px] tw:opacity-40">
@@ -148,14 +181,19 @@
148
181
  <!-- Code content -->
149
182
  <pre
150
183
  class="tw:text-xs tw:font-mono tw:p-3 tw:max-h-52 tw:overflow-auto tw:leading-relaxed tw:bg-black/[0.03] dark:tw:bg-white/[0.03] tw:m-0"
151
- >{{ previewLines.text }}</pre>
184
+ >{{ previewLines.text }}</pre
185
+ >
152
186
 
153
187
  <!-- Truncation notice -->
154
188
  <div
155
189
  v-if="previewLines.truncated"
156
190
  class="tw:text-[10px] tw:text-center tw:py-1 tw:opacity-40 tw:bg-black/5 dark:tw:bg-white/5 tw:border-t tw:border-black/10 dark:tw:border-white/10"
157
191
  >
158
- {{ $t("core.dataExporter.previewTruncatedAt", { count: PREVIEW_LINE_LIMIT }) }}
192
+ {{
193
+ $t("core.dataExporter.previewTruncatedAt", {
194
+ count: PREVIEW_LINE_LIMIT,
195
+ })
196
+ }}
159
197
  </div>
160
198
  </div>
161
199
  </slot>
@@ -169,21 +207,31 @@
169
207
  <v-btn
170
208
  v-if="clipboardSupported"
171
209
  :disabled="!serializeResult.ok"
172
- :color="copyState === 'success' ? 'success' : copyState === 'error' ? 'error' : 'primary'"
210
+ :color="
211
+ copyState === 'success'
212
+ ? 'success'
213
+ : copyState === 'error'
214
+ ? 'error'
215
+ : 'primary'
216
+ "
173
217
  variant="tonal"
174
218
  size="small"
175
219
  class="tw:flex-1 tw:font-medium"
176
220
  :prepend-icon="
177
- copyState === 'success' ? 'mdi-check'
178
- : copyState === 'error' ? 'mdi-alert-circle'
179
- : 'mdi-content-copy'
221
+ copyState === 'success'
222
+ ? 'mdi-check'
223
+ : copyState === 'error'
224
+ ? 'mdi-alert-circle'
225
+ : 'mdi-content-copy'
180
226
  "
181
227
  @click="copyToClipboard"
182
228
  >
183
229
  {{
184
- copyState === "success" ? $t("core.dataExporter.copied")
185
- : copyState === "error" ? $t("core.dataExporter.copyFailed")
186
- : $t("core.dataExporter.copy")
230
+ copyState === "success"
231
+ ? $t("core.dataExporter.copied")
232
+ : copyState === "error"
233
+ ? $t("core.dataExporter.copyFailed")
234
+ : $t("core.dataExporter.copy")
187
235
  }}
188
236
  </v-btn>
189
237
  <v-btn
@@ -340,14 +388,21 @@ const actionsSlotProps = computed(() => ({
340
388
 
341
389
  const PREVIEW_LINE_LIMIT = 200
342
390
 
343
- const previewLines = computed<{ text: string; lineCount: number; truncated: boolean }>(() => {
344
- if (!serializeResult.value.ok) return { text: "", lineCount: 0, truncated: false }
391
+ const previewLines = computed<{
392
+ text: string
393
+ lineCount: number
394
+ truncated: boolean
395
+ }>(() => {
396
+ if (!serializeResult.value.ok)
397
+ return { text: "", lineCount: 0, truncated: false }
345
398
  const lines = serializeResult.value.value.split("\n")
346
399
  const truncated = lines.length > PREVIEW_LINE_LIMIT
347
400
  return {
348
- text: truncated ? lines.slice(0, PREVIEW_LINE_LIMIT).join("\n") : serializeResult.value.value,
401
+ text: truncated
402
+ ? lines.slice(0, PREVIEW_LINE_LIMIT).join("\n")
403
+ : serializeResult.value.value,
349
404
  lineCount: lines.length,
350
405
  truncated,
351
406
  }
352
407
  })
353
- </script>
408
+ </script>
@@ -217,7 +217,7 @@
217
217
 
218
218
  <!-- Dynamic slots for custom column rendering -->
219
219
  <template v-for="slot in customSlots" :key="slot" #[slot]="slotProps">
220
- <slot :name="slot" v-bind="slotProps" />
220
+ <slot :name="slot" v-bind="slotProps ?? {}" />
221
221
  </template>
222
222
 
223
223
  <!-- Actions column template -->
@@ -249,6 +249,16 @@
249
249
  <slot name="tfoot" v-bind="slotProps ?? {}" />
250
250
  </template>
251
251
 
252
+ <template v-if="slots['group-header']" #group-header="slotProps">
253
+ <slot
254
+ name="group-header"
255
+ v-bind="{
256
+ ...(slotProps ?? {}),
257
+ selectionActive: showSelectColumn,
258
+ }"
259
+ />
260
+ </template>
261
+
252
262
  <!-- Forward remaining v-data-table slots (e.g. bottom, thead). -->
253
263
  <template
254
264
  v-for="slotName in vDataTablePassthroughSlots"
@@ -694,20 +704,9 @@ const V_DATA_TABLE_BRIDGED_SLOTS = new Set([
694
704
  "body.prepend",
695
705
  "body.append",
696
706
  "tfoot",
707
+ "group-header",
697
708
  ])
698
709
 
699
- /** v-data-table slots supplied by the parent (e.g. bottom), excluding built-in handling. */
700
- const vDataTablePassthroughSlots = computed(() =>
701
- Object.keys(slots).filter(
702
- (name) =>
703
- !BASE_DATA_TABLE_RESERVED_SLOTS.has(name) &&
704
- !V_DATA_TABLE_BRIDGED_SLOTS.has(name) &&
705
- !name.startsWith("item.") &&
706
- name !== "loader" &&
707
- name !== "top",
708
- ),
709
- )
710
-
711
710
  /** Hide ⋮ when the menu would be empty (no export toggle, import, or slot content). */
712
711
  const showActionsOverflowMenu = computed(
713
712
  () =>
@@ -721,7 +720,8 @@ const showActionsOverflowMenu = computed(
721
720
  const hasSlotContent = (name: string) => {
722
721
  const slot = slots[name]
723
722
  if (!slot) return false
724
- return slot().some((vnode) => {
723
+ // Scoped slots may destructure props; never probe-call them without a scope object.
724
+ return slot({}).some((vnode) => {
725
725
  if (vnode.type === Comment) return false
726
726
  if (typeof vnode.children === "string")
727
727
  return vnode.children.trim().length > 0
@@ -757,6 +757,19 @@ const isExportMode = ref(false)
757
757
  const showSelectColumn = computed(
758
758
  () => props.showSelect || (props.showExport && isExportMode.value),
759
759
  )
760
+
761
+ /** v-data-table slots supplied by the parent (e.g. bottom), excluding built-in handling. */
762
+ const vDataTablePassthroughSlots = computed(() =>
763
+ Object.keys(slots).filter(
764
+ (name) =>
765
+ !BASE_DATA_TABLE_RESERVED_SLOTS.has(name) &&
766
+ !V_DATA_TABLE_BRIDGED_SLOTS.has(name) &&
767
+ !name.startsWith("item.") &&
768
+ name !== "loader" &&
769
+ name !== "top",
770
+ ),
771
+ )
772
+
760
773
  const isActionsMenuOpen = ref(false)
761
774
  const isFilterDrawerOpen = ref(false)
762
775
  /** Draft values edited in the filter drawer (applied only on Apply). */
@@ -0,0 +1,42 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Group-row select checkbox for BaseDataTable when using a custom `#group-header` slot.
4
+ *
5
+ * Vuetify does not publicly export `useSelection` / `useGroupBy`, updates can break this
6
+ * TODO: we should find a further solution for this!
7
+ * the built-in group checkbox only appears on the default group header row. This component mirrors
8
+ * VDataTableGroupHeaderRow and must run inside `v-data-table` (inject context).
9
+ *
10
+ */
11
+ import { computed } from "vue"
12
+ import { useGroupBy } from "vuetify/lib/components/VDataTable/composables/group.js"
13
+ import { useSelection } from "vuetify/lib/components/VDataTable/composables/select.js"
14
+
15
+ const props = defineProps<{
16
+ group: { items?: readonly unknown[] }
17
+ }>()
18
+
19
+ const { isSelected, isSomeSelected, select } = useSelection()
20
+ const { extractRows } = useGroupBy()
21
+
22
+ const selectableRows = computed(() =>
23
+ extractRows([props.group]).filter((row) => row.selectable),
24
+ )
25
+
26
+ const modelValue = computed(
27
+ () => selectableRows.value.length > 0 && isSelected(selectableRows.value),
28
+ )
29
+
30
+ const indeterminate = computed(
31
+ () => isSomeSelected(selectableRows.value) && !modelValue.value,
32
+ )
33
+ </script>
34
+
35
+ <template>
36
+ <v-checkbox-btn
37
+ :disabled="selectableRows.length === 0"
38
+ :model-value="modelValue"
39
+ :indeterminate="indeterminate"
40
+ @update:model-value="select(selectableRows, !!$event)"
41
+ />
42
+ </template>
@@ -20,6 +20,7 @@ import ImportSelectionDialog from "./dataImport/ImportSelectionDialog.vue"
20
20
  import ImportSummaryDialog from "./dataImport/ImportSummaryDialog.vue"
21
21
  import BaseDataTable from "./dataTable/BaseDataTable.vue"
22
22
  import BaseDataTableConfirmDeleteBtn from "./dataTable/BaseDataTableConfirmDeleteBtn.vue"
23
+ import BaseDataTableGroupSelectCheckbox from "./dataTable/BaseDataTableGroupSelectCheckbox.vue"
23
24
  import FileUpload from "./input/FileUpload.vue"
24
25
 
25
26
  export {
@@ -27,6 +28,7 @@ export {
27
28
  ImportSummaryDialog,
28
29
  BaseDataTable,
29
30
  BaseDataTableConfirmDeleteBtn,
31
+ BaseDataTableGroupSelectCheckbox,
30
32
  FileUpload,
31
33
  CompositionLoadingState,
32
34
  CompositionOverlay,
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <v-tooltip
3
- content-class="!tw:p-1"
3
+ content-class="tw:!p-1"
4
4
  location="bottom"
5
5
  max-width="400px"
6
6
  v-model="show"
@@ -65,9 +65,7 @@ const theme = computed(() =>
65
65
  vuetifyTheme.global.name.value === "light" ? "dark" : "light",
66
66
  )
67
67
 
68
- const socketStatus = computed(
69
- () => backend.value?.socket?.status ?? 2,
70
- )
68
+ const socketStatus = computed(() => backend.value?.socket?.status ?? 2)
71
69
 
72
70
  const serverStatusLabel = computed(() =>
73
71
  i18n.t(`core.socket_status_${socketStatus.value || 0}`),
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <v-menu :close-on-content-click="false" v-model="isOpen">
3
3
  <template #activator="{ props }">
4
- <v-btn class="!tw:me-0" v-bind="props">
4
+ <v-btn class="tw:!me-0" v-bind="props">
5
5
  <v-avatar density="compact" color="primary" class="tw:mr-2">
6
6
  {{ avatar }}
7
7
  </v-avatar>
@@ -57,7 +57,7 @@
57
57
  >
58
58
  <v-card
59
59
  variant="outlined"
60
- class="tw:h-full tw:transition-all hover:tw:shadow-lg"
60
+ class="tw:h-full tw:transition-all tw:hover:shadow-lg"
61
61
  >
62
62
  <v-card-item>
63
63
  <template v-slot:prepend>
@@ -4,5 +4,6 @@ export * from "./useCurrentComposition"
4
4
  export * from "./useWidgets"
5
5
  export * from "./useInteractionLinks"
6
6
  export * from "./useWidgetLifecycle"
7
+ export * from "./useAppLoadingCoordinator"
7
8
  export * from "./useExport"
8
9
  export * from "./useBaseDataTableDeleteConfirm"
@@ -0,0 +1,77 @@
1
+ import { computed, ref, watch, type Ref } from "vue"
2
+ import { scheduleAfterPaint } from "@racletteCore/lib/scheduleTask"
3
+ import useUiState from "./useUiState"
4
+
5
+ /**
6
+ * Gates global `ui.loading` until socket bootstrap and page widget slots are ready.
7
+ * Owns page-loader remount session so readiness re-reports stay in sync with loading.
8
+ * Used only from `ProductOrchestrator`.
9
+ */
10
+ export const useAppLoadingCoordinator = (
11
+ compositionId: Ref<string | undefined | null>,
12
+ ) => {
13
+ const { loading, bootstrapComplete } = useUiState()
14
+ const pageWidgetsReady = ref(false)
15
+ const pageLoaderSession = ref(0)
16
+
17
+ const armPageLoadingGate = () => {
18
+ pageLoaderSession.value++
19
+ pageWidgetsReady.value = false
20
+ }
21
+
22
+ const pageLoaderKey = computed(
23
+ () => `${compositionId.value ?? "none"}-s${pageLoaderSession.value}`,
24
+ )
25
+
26
+ const tryClearLoading = () => {
27
+ if (!loading.value) {
28
+ return
29
+ }
30
+ if (!bootstrapComplete.value || !pageWidgetsReady.value) {
31
+ return
32
+ }
33
+
34
+ scheduleAfterPaint(() => {
35
+ if (loading.value && bootstrapComplete.value && pageWidgetsReady.value) {
36
+ loading.value = false
37
+ }
38
+ })
39
+ }
40
+
41
+ const onPageWidgetsReady = (ready: boolean) => {
42
+ pageWidgetsReady.value = ready
43
+ tryClearLoading()
44
+ }
45
+
46
+ /** Welcome screen, 404, or layouts with nothing to wait for. */
47
+ const markPageReadyWithoutWidgets = () => {
48
+ onPageWidgetsReady(true)
49
+ }
50
+
51
+ watch(
52
+ bootstrapComplete,
53
+ (complete, wasComplete) => {
54
+ if (!complete) {
55
+ if (wasComplete) {
56
+ pageLoaderSession.value++
57
+ }
58
+ pageWidgetsReady.value = false
59
+ return
60
+ }
61
+ tryClearLoading()
62
+ },
63
+ { immediate: true },
64
+ )
65
+
66
+ watch(loading, (isLoading, wasLoading) => {
67
+ if (isLoading && wasLoading === false) {
68
+ armPageLoadingGate()
69
+ }
70
+ })
71
+
72
+ return {
73
+ pageLoaderKey,
74
+ onPageWidgetsReady,
75
+ markPageReadyWithoutWidgets,
76
+ }
77
+ }
@@ -41,7 +41,7 @@ export default () => {
41
41
  interactionLink.triggers
42
42
 
43
43
  .sort(
44
- (a, b) => (a.settings.sortOrder ?? 0) - (b.settings.sortOrder ?? 0),
44
+ (a, b) => (b.settings.sortOrder ?? 0) - (a.settings.sortOrder ?? 0),
45
45
  )
46
46
  .forEach((trigger) => {
47
47
  if (trigger.settings) {
@@ -11,6 +11,7 @@ export default () => {
11
11
  const theme = createUiStateBinding<"light" | "dark">("theme")
12
12
  const mobile = createUiStateBinding<boolean>("mobile")
13
13
  const loading = createUiStateBinding<boolean>("loading")
14
+ const bootstrapComplete = createUiStateBinding<boolean>("bootstrapComplete")
14
15
  const landingPage = createUiStateBinding<string>("landingPage")
15
16
  const availableInterfaces = createUiStateBinding<
16
17
  UIState["availableInterfaces"]
@@ -22,6 +23,7 @@ export default () => {
22
23
  theme,
23
24
  mobile,
24
25
  loading,
26
+ bootstrapComplete,
25
27
  landingPage,
26
28
  availableInterfaces,
27
29
  }