@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
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- class="tw:relative tw:max-w-full tw:min-h-fit"
3
+ class="tw:relative tw:min-h-fit tw:max-w-full"
4
4
  :class="{
5
5
  'tw:min-h-[200px]': showLoadingOverlay,
6
6
  'tw:p-4':
@@ -32,59 +32,68 @@
32
32
  class="tw:max-w-full tw:self-start"
33
33
  :class="{ 'tw:h-full': slotLayout === 'full' }"
34
34
  >
35
- <v-col
36
- v-for="(column, colIndex) in props.widgetsLayout"
37
- :key="colIndex"
38
- :cols="getGridCols(props.widgetsLayout.length)"
39
- :class="{ 'tw:h-full': slotLayout === 'full' }"
40
- >
41
- <v-row
42
- :no-gutters="slotLayout === 'default' || slotLayout === 'full'"
43
- :class="[{ 'tw:h-full': slotLayout === 'full' }, rowClasses]"
35
+ <v-col
36
+ v-for="(column, colIndex) in props.widgetsLayout"
37
+ :key="colIndex"
38
+ :cols="getGridCols(props.widgetsLayout.length)"
39
+ :class="{ 'tw:h-full': slotLayout === 'full' }"
44
40
  >
45
- <TransitionGroup
46
- name="grid-item"
47
- tag="div"
48
- class="tw:w-full tw:flex tw:flex-wrap"
49
- appear
41
+ <v-row
42
+ :no-gutters="slotLayout === 'default' || slotLayout === 'full'"
43
+ :class="[{ 'tw:h-full': slotLayout === 'full' }, rowClasses]"
50
44
  >
51
- <v-col
52
- v-for="(slot, slotIndex) in column"
53
- :key="`${gridKey}-${colIndex}-${slotIndex}`"
54
- :cols="getSlotCols(slot)"
55
- :style="{
56
- 'transition-delay': `${slotIndex * 50}ms`,
57
- ...(slotLayout === 'default' || slotLayout === 'full'
58
- ? { padding: '0px' }
59
- : {}),
60
- }"
45
+ <TransitionGroup
46
+ name="grid-item"
47
+ tag="div"
48
+ class="tw:flex tw:w-full tw:flex-wrap"
49
+ appear
61
50
  >
62
- <div :class="widgetContainerClasses" class="tw:h-full">
63
- <component
51
+ <v-col
52
+ v-for="(slot, slotIndex) in column"
53
+ v-show="slot.widget"
54
+ :key="`${gridKey}-${colIndex}-${slotIndex}`"
55
+ :cols="getSlotCols(slot)"
56
+ :style="{
57
+ 'transition-delay': `${slotIndex * 50}ms`,
58
+ ...(slotLayout === 'default' || slotLayout === 'full'
59
+ ? { padding: '0px' }
60
+ : {}),
61
+ }"
62
+ >
63
+ <div
64
64
  v-if="slot.widget"
65
- :is="getSlotWidget(slot.widget)"
66
- :uuid="slot.widget.uuid"
67
- :class="[
68
- hasSpacing &&
69
- widgetStates[slot.widget.uuid].config
70
- ?.allowPaddingOverride
71
- ? 'tw:p-4'
72
- : '',
73
- ]"
74
- v-bind="{
75
- ...widgetStates[slot.widget.uuid].config,
76
- ...widgetStates[slot.widget.uuid].public,
77
- ...routeParams,
78
- compositionName: currentRouteConfig?.slotValue,
79
- slotType,
80
- }"
81
- />
82
- </div>
83
- </v-col>
84
- </TransitionGroup>
85
- </v-row>
86
- </v-col>
87
- </v-row>
65
+ :ref="
66
+ shouldTrackReadiness
67
+ ? (el) => bindWidgetContainer(slot.widget!.uuid, el)
68
+ : undefined
69
+ "
70
+ :class="widgetContainerClasses"
71
+ class="tw:h-full"
72
+ >
73
+ <component
74
+ :is="getSlotWidget(slot.widget)"
75
+ :uuid="slot.widget.uuid"
76
+ :class="[
77
+ hasSpacing &&
78
+ widgetStates[slot.widget.uuid].config
79
+ ?.allowPaddingOverride
80
+ ? 'tw:p-4'
81
+ : '',
82
+ ]"
83
+ v-bind="{
84
+ ...widgetStates[slot.widget.uuid].config,
85
+ ...widgetStates[slot.widget.uuid].public,
86
+ ...routeParams,
87
+ compositionName: currentRouteConfig?.slotValue,
88
+ slotType,
89
+ }"
90
+ />
91
+ </div>
92
+ </v-col>
93
+ </TransitionGroup>
94
+ </v-row>
95
+ </v-col>
96
+ </v-row>
88
97
  </Transition>
89
98
  </div>
90
99
 
@@ -118,38 +127,67 @@ import {
118
127
  computed,
119
128
  onMounted,
120
129
  onUnmounted,
121
- nextTick,
122
130
  provide,
123
131
  toRef,
124
132
  type Component,
125
- type ComponentPublicInstance,
126
133
  } from "vue"
127
134
  import { widgetLoader } from "@racletteOrchestrator/helpers/widgetLoader"
135
+ import { wrapWidgetWithLifecycle } from "@racletteOrchestrator/helpers/wrapWidgetWithLifecycle"
136
+ import {
137
+ cleanRouteParams,
138
+ countWidgetsInLayout,
139
+ getWidgetsLayoutSignature,
140
+ } from "@racletteOrchestrator/helpers/widgetLayoutHelper"
128
141
  import { WIDGET_SLOT_TYPE_KEY } from "@racletteOrchestrator/constants/widgetSlotType"
129
142
  import { useDisplay } from "vuetify"
130
143
  import useStateSubscriber from "@racletteOrchestrator/composables/useStateSubscriber"
131
144
  import useRouteState from "@racletteOrchestrator/composables/useRouteState"
132
- import { createWidgetLifecycleManager } from "@racletteOrchestrator/composables/useWidgetLifecycle"
133
- import { isDefined } from "@vueuse/core"
145
+ import {
146
+ createWidgetLifecycleManager,
147
+ type WidgetLifecycleManager,
148
+ } from "@racletteOrchestrator/composables/useWidgetLifecycle"
134
149
  import CompositionLoadingState from "@racletteOrchestrator/components/CompositionLoadingState.vue"
150
+ import ErrorWidgetState from "@racletteOrchestrator/components/ErrorWidgetState.vue"
151
+ import { createEnhancedError } from "@racletteOrchestrator/helpers/widgetUtils"
152
+ import log from "@racletteCore/lib/logger"
135
153
 
136
154
  const props = withDefaults(
137
155
  defineProps<{
138
156
  widgetsLayout: Composition["widgetsLayout"]
139
157
  slotType: string
140
158
  slotLayout: string
159
+ /**
160
+ * Observe widget slot containers for readiness. Defaults to page slots and
161
+ * modal overlays (`revealWhenReady`).
162
+ */
163
+ trackReadiness?: boolean
164
+ /** When true, show a local loading overlay until widgets are ready (modals). */
141
165
  revealWhenReady?: boolean
142
166
  loadingSize?: number | string
143
167
  loadingStrokeWidth?: number | string
144
168
  }>(),
145
169
  {
170
+ trackReadiness: undefined,
146
171
  revealWhenReady: false,
147
172
  loadingSize: 32,
148
173
  loadingStrokeWidth: 3,
149
174
  },
150
175
  )
151
176
 
152
- provide(WIDGET_SLOT_TYPE_KEY, toRef(() => props.slotType))
177
+ const emit = defineEmits<{
178
+ "all-widgets-ready": [ready: boolean]
179
+ }>()
180
+
181
+ const shouldTrackReadiness = computed(
182
+ () =>
183
+ props.trackReadiness ??
184
+ (props.slotType === "page" || props.revealWhenReady),
185
+ )
186
+
187
+ provide(
188
+ WIDGET_SLOT_TYPE_KEY,
189
+ toRef(() => props.slotType),
190
+ )
153
191
 
154
192
  const { compositionSlots } = useRouteState()
155
193
 
@@ -164,7 +202,7 @@ const widgetContainerClasses = computed(() => {
164
202
  case "spaced":
165
203
  return `${baseClasses} tw:rounded-lg`
166
204
  case "card":
167
- return `${baseClasses} tw:rounded-lg tw:shadow-lg hover:tw:shadow-xl tw:transition-shadow tw:duration-200`
205
+ return `${baseClasses} tw:rounded-lg tw:shadow-lg tw:hover:shadow-xl tw:transition-shadow tw:duration-200`
168
206
  case "border":
169
207
  return `${baseClasses} tw:rounded-lg tw:border tw:border-gray-300 tw:dark:border-gray-700`
170
208
  default:
@@ -176,49 +214,36 @@ const hasSpacing = computed(() =>
176
214
  ["spaced", "card", "border"].includes(props.slotLayout),
177
215
  )
178
216
 
179
- const rowClasses = computed(() => {
180
- return hasSpacing.value ? "tw:gap-2" : ""
181
- })
217
+ const rowClasses = computed(() => (hasSpacing.value ? "tw:gap-2" : ""))
182
218
 
183
219
  const { mdAndDown } = useDisplay()
184
-
185
220
  const { state: widgetStates } = useStateSubscriber("widgets")
186
- const routeParams = computed(() => {
187
- const cleanedParams = {}
188
- if (currentRouteConfig.value?.slotParams) {
189
- for (const [key, value] of Object.entries(
190
- currentRouteConfig.value.slotParams,
191
- )) {
192
- if (isDefined(value) && value !== "undefined") {
193
- cleanedParams[key] = value
194
- }
195
- }
196
- }
197
221
 
198
- return cleanedParams
199
- })
222
+ const routeParams = computed(() =>
223
+ cleanRouteParams(currentRouteConfig.value?.slotParams),
224
+ )
200
225
 
201
- // Create a reactive key that changes when widgetsLayout changes
202
226
  const gridKey = ref(0)
203
-
204
- // Calculate total number of widgets (only slots that actually contain a widget)
205
- const totalWidgets = computed(() =>
206
- props.widgetsLayout.reduce(
207
- (sum, column) => sum + column.filter((slot) => slot.widget).length,
208
- 0,
209
- ),
227
+ const totalWidgets = computed(() => countWidgetsInLayout(props.widgetsLayout))
228
+ const widgetsLayoutSignature = computed(() =>
229
+ getWidgetsLayoutSignature(props.widgetsLayout),
210
230
  )
211
231
 
212
- // Widget lifecycle manager
213
- const lifecycleManager = ref(
214
- createWidgetLifecycleManager(totalWidgets.value),
215
- )
232
+ const lifecycleManager = ref<WidgetLifecycleManager | null>(null)
216
233
  const allWidgetsReady = ref(false)
217
234
  let stopReadyWatch: (() => void) | null = null
218
235
 
219
- const attachLifecycleManager = (widgetCount: number) => {
236
+ const syncLifecycleManager = () => {
220
237
  stopReadyWatch?.()
221
- const manager = createWidgetLifecycleManager(widgetCount)
238
+ lifecycleManager.value?.reset()
239
+
240
+ if (!shouldTrackReadiness.value) {
241
+ lifecycleManager.value = null
242
+ allWidgetsReady.value = true
243
+ return
244
+ }
245
+
246
+ const manager = createWidgetLifecycleManager(totalWidgets.value)
222
247
  stopReadyWatch = watch(
223
248
  manager.allWidgetsReady,
224
249
  (ready) => {
@@ -229,58 +254,49 @@ const attachLifecycleManager = (widgetCount: number) => {
229
254
  lifecycleManager.value = manager
230
255
  }
231
256
 
232
- attachLifecycleManager(totalWidgets.value)
257
+ syncLifecycleManager()
258
+
259
+ watch(shouldTrackReadiness, syncLifecycleManager)
260
+
261
+ watch(
262
+ allWidgetsReady,
263
+ (ready) => {
264
+ if (shouldTrackReadiness.value) {
265
+ emit("all-widgets-ready", ready)
266
+ }
267
+ },
268
+ { immediate: true },
269
+ )
233
270
 
234
271
  const isContentReady = computed(
235
272
  () =>
236
- !props.revealWhenReady ||
237
- totalWidgets.value === 0 ||
238
- allWidgetsReady.value,
273
+ !props.revealWhenReady || totalWidgets.value === 0 || allWidgetsReady.value,
239
274
  )
240
275
 
241
276
  const showLoadingOverlay = computed(
242
277
  () => props.revealWhenReady && !isContentReady.value,
243
278
  )
244
279
 
245
- const resolveWidgetElement = (
246
- target: Element | ComponentPublicInstance | null,
247
- ): HTMLElement | null => {
248
- if (!target) {
249
- return null
280
+ /** Observe widget slot containers when readiness tracking is active. */
281
+ const bindWidgetContainer = (uuid: string, el: Element | null) => {
282
+ const manager = lifecycleManager.value
283
+ if (!manager) {
284
+ return
250
285
  }
251
286
 
252
- if (target instanceof HTMLElement) {
253
- return target
254
- }
287
+ const hooks = manager.createLifecycleHooks(uuid)
255
288
 
256
- const root = target.$el
257
- if (root instanceof HTMLElement) {
258
- return root
289
+ if (!el) {
290
+ hooks.onWidgetUnmounted()
291
+ return
259
292
  }
260
293
 
261
- if (root instanceof Text && root.parentElement instanceof HTMLElement) {
262
- return root.parentElement
294
+ if (el instanceof HTMLElement) {
295
+ hooks.onWidgetContainerMounted(el)
263
296
  }
264
-
265
- return null
266
- }
267
-
268
- const isWidgetLoadingPlaceholder = (
269
- target: Element | ComponentPublicInstance | null,
270
- ): boolean => {
271
- if (!target || target instanceof HTMLElement) {
272
- return false
273
- }
274
-
275
- const componentName =
276
- target.$.type?.name ?? (target.$options?.name as string | undefined)
277
-
278
- return (
279
- componentName === "LoadingWidgetState" || componentName === "ErrorWidgetState"
280
- )
281
297
  }
282
298
 
283
- // Widget component cache
299
+ // TODO: Should cacheKey include shouldTrackReadiness (or other props) when we support toggling it at runtime?
284
300
  const widgetComponentCache = new Map<string, Component>()
285
301
 
286
302
  const getSlotWidget = (widget: WidgetBase) => {
@@ -289,67 +305,31 @@ const getSlotWidget = (widget: WidgetBase) => {
289
305
  if (!widgetComponentCache.has(cacheKey)) {
290
306
  try {
291
307
  const originalComponent = widgetLoader(widget)
292
-
293
- // Wrap the component to inject lifecycle hooks
294
- const wrappedComponent = {
295
- name: `${originalComponent.name || "Widget"}Wrapper`,
296
- setup(props, context) {
297
- const { onAllWidgetsReady, onWidgetRendered, onWidgetUnmounted } =
298
- lifecycleManager.value.createLifecycleHooks(widget.uuid)
299
-
300
- const widgetRoot = ref<Element | ComponentPublicInstance | null>(null)
301
-
302
- const reportRendered = () => {
303
- nextTick(() => {
304
- const instance = widgetRoot.value
305
- if (isWidgetLoadingPlaceholder(instance)) {
306
- return
307
- }
308
-
309
- const element = resolveWidgetElement(instance)
310
- if (element) {
311
- onWidgetRendered(element)
312
- }
313
- })
314
- }
315
-
316
- onMounted(reportRendered)
317
-
318
- onUnmounted(() => {
319
- onWidgetUnmounted()
320
- })
321
-
322
- return () =>
323
- h(
324
- originalComponent,
325
- {
326
- ...props,
327
- onAllWidgetsReady,
328
- ref: (instance: Element | ComponentPublicInstance | null) => {
329
- widgetRoot.value = instance
330
- if (instance) {
331
- reportRendered()
332
- }
333
- },
334
- },
335
- context.slots,
336
- )
337
- },
338
- }
339
-
340
- widgetComponentCache.set(cacheKey, wrappedComponent)
308
+ const component = shouldTrackReadiness.value
309
+ ? wrapWidgetWithLifecycle(
310
+ widget,
311
+ originalComponent,
312
+ () => lifecycleManager.value,
313
+ )
314
+ : originalComponent
315
+ widgetComponentCache.set(cacheKey, component)
341
316
  } catch (error) {
342
- console.error("🚨 Error creating widget component:", error)
343
- // Cache error component too
317
+ const enhancedError = createEnhancedError(error, widget)
318
+ log.widgets("Error creating widget component", {
319
+ pluginKey: widget.pluginKey,
320
+ name: widget.name,
321
+ error: enhancedError.message,
322
+ })
344
323
  widgetComponentCache.set(cacheKey, {
345
324
  name: "ComponentCreationError",
346
325
  setup() {
347
326
  return () =>
348
- h(
349
- "div",
350
- { class: "p-4 text-red-500 border border-red-300 rounded" },
351
- "Error creating component",
352
- )
327
+ h(ErrorWidgetState, {
328
+ uuid: widget.uuid,
329
+ pluginKey: widget.pluginKey,
330
+ widgetName: widget.name,
331
+ error: enhancedError,
332
+ })
353
333
  },
354
334
  })
355
335
  }
@@ -358,29 +338,33 @@ const getSlotWidget = (widget: WidgetBase) => {
358
338
  return widgetComponentCache.get(cacheKey)
359
339
  }
360
340
 
361
- const getGridCols = (rowLength) => (mdAndDown.value ? 12 : 12 / rowLength)
341
+ const getGridCols = (rowLength: number) =>
342
+ mdAndDown.value ? 12 : 12 / rowLength
362
343
 
363
- const getSlotCols = (slot) => (mdAndDown.value ? 12 : slot.column)
344
+ const getSlotCols = (slot: { column: number }) =>
345
+ mdAndDown.value ? 12 : slot.column
364
346
 
365
- // Watch for changes in widgetsLayout to trigger transition and reset lifecycle
366
- watch(
367
- () => props.widgetsLayout,
368
- () => {
369
- // Reset lifecycle manager
370
- lifecycleManager.value.reset()
371
-
372
- // Clear component cache to force re-wrapping with new lifecycle manager
373
- widgetComponentCache.clear()
347
+ const resetLayout = () => {
348
+ widgetComponentCache.clear()
349
+ syncLifecycleManager()
350
+ gridKey.value++
351
+ }
374
352
 
375
- attachLifecycleManager(totalWidgets.value)
353
+ watch(widgetsLayoutSignature, (next, prev) => {
354
+ if (!prev || next === prev) {
355
+ return
356
+ }
357
+ resetLayout()
358
+ })
376
359
 
377
- // Trigger grid transition
378
- gridKey.value++
379
- },
380
- { deep: true },
381
- )
360
+ onMounted(() => {
361
+ if (shouldTrackReadiness.value) {
362
+ emit("all-widgets-ready", allWidgetsReady.value)
363
+ }
364
+ })
382
365
 
383
366
  onUnmounted(() => {
384
367
  stopReadyWatch?.()
368
+ lifecycleManager.value?.reset()
385
369
  })
386
370
  </script>