@raclettejs/core 0.1.40 → 0.1.41
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 +55 -1
- package/dist/cli.js +104 -104
- package/dist/cli.js.map +4 -4
- package/dist/index.js.map +2 -2
- package/package.json +2 -2
- package/services/frontend/eslint.config.js +31 -9
- package/services/frontend/src/core/setup/socketBootstrap.ts +4 -3
- package/services/frontend/src/core/store/state.ts +2 -7
- package/services/frontend/src/core/store/types/index.ts +2 -0
- package/services/frontend/src/orchestrator/ProductOrchestrator.vue +33 -22
- package/services/frontend/src/orchestrator/components/LoadingWidgetState.vue +7 -3
- package/services/frontend/src/orchestrator/components/composition/WidgetsLayoutLoader.vue +179 -195
- package/services/frontend/src/orchestrator/components/dataExport/DataExporter.vue +79 -24
- package/services/frontend/src/orchestrator/components/dataTable/BaseDataTable.vue +27 -14
- package/services/frontend/src/orchestrator/components/dataTable/BaseDataTableGroupSelectCheckbox.vue +42 -0
- package/services/frontend/src/orchestrator/components/index.ts +2 -0
- package/services/frontend/src/orchestrator/components/menu/ServerStatus.vue +2 -4
- package/services/frontend/src/orchestrator/components/menu/UserMenu.vue +1 -1
- package/services/frontend/src/orchestrator/components/welcomeScreen/AdminWelcomeScreen.vue +1 -1
- package/services/frontend/src/orchestrator/composables/index.ts +1 -0
- package/services/frontend/src/orchestrator/composables/useAppLoadingCoordinator.ts +77 -0
- package/services/frontend/src/orchestrator/composables/usePageNavigation.ts +1 -1
- package/services/frontend/src/orchestrator/composables/useUiState.ts +2 -0
- package/services/frontend/src/orchestrator/composables/useWidgetLifecycle.ts +65 -76
- package/services/frontend/src/orchestrator/composables/useWidgets/helperFunctions.ts +7 -4
- package/services/frontend/src/orchestrator/constants/index.ts +2 -0
- package/services/frontend/src/orchestrator/constants/widgetSlot.ts +7 -0
- package/services/frontend/src/orchestrator/exports.ts +1 -0
- package/services/frontend/src/orchestrator/helpers/staticWidgetRegistry.ts +9 -44
- package/services/frontend/src/orchestrator/helpers/widgetLayoutHelper.ts +37 -0
- package/services/frontend/src/orchestrator/helpers/widgetLoader.ts +0 -1
- package/services/frontend/src/orchestrator/helpers/widgetSlotHelper.ts +20 -0
- package/services/frontend/src/orchestrator/helpers/widgetVisuals.ts +11 -12
- package/services/frontend/src/orchestrator/helpers/wrapWidgetWithLifecycle.ts +32 -0
- package/services/frontend/src/orchestrator/router/routerHooks/afterEach.ts +1 -20
- package/services/frontend/src/orchestrator/types/Widgets.ts +12 -6
- package/src/types.ts +7 -1
|
@@ -2,21 +2,29 @@ import { ref, type Ref } from "vue"
|
|
|
2
2
|
import { isDevelopmentMode } from "@racletteCore/helpers/devMode"
|
|
3
3
|
import log from "@racletteCore/lib/logger"
|
|
4
4
|
import { scheduleAfterPaint } from "@racletteCore/lib/scheduleTask"
|
|
5
|
+
import { hasWidgetSlotContent } from "@racletteOrchestrator/helpers/widgetSlotHelper"
|
|
5
6
|
|
|
6
7
|
export type OnAllWidgetsReady = (callback: () => void) => void
|
|
7
8
|
|
|
8
|
-
interface WidgetLifecycleHooks {
|
|
9
|
+
export interface WidgetLifecycleHooks {
|
|
9
10
|
onAllWidgetsReady: OnAllWidgetsReady
|
|
10
|
-
|
|
11
|
+
onWidgetContainerMounted: (element: HTMLElement) => void
|
|
11
12
|
onWidgetUnmounted: () => void
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
interface WidgetLifecycleManager {
|
|
15
|
+
export interface WidgetLifecycleManager {
|
|
15
16
|
createLifecycleHooks: (widgetId: string) => WidgetLifecycleHooks
|
|
16
17
|
allWidgetsReady: Ref<boolean>
|
|
17
18
|
reset: () => void
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Tracks widget slot readiness for page navigation, modal overlays, and other
|
|
23
|
+
* `WidgetsLayoutLoader` instances with `trackReadiness`.
|
|
24
|
+
*
|
|
25
|
+
* Observes each widget slot container in `WidgetsLayoutLoader` and waits until
|
|
26
|
+
* plugin content appears in the DOM. Plugin developers do not interact with this.
|
|
27
|
+
*/
|
|
20
28
|
export function createWidgetLifecycleManager(
|
|
21
29
|
totalWidgets: number,
|
|
22
30
|
): WidgetLifecycleManager {
|
|
@@ -24,6 +32,7 @@ export function createWidgetLifecycleManager(
|
|
|
24
32
|
const allWidgetsReady = ref(false)
|
|
25
33
|
const readyCallbacks = new Set<() => void>()
|
|
26
34
|
const observers = new Map<string, MutationObserver>()
|
|
35
|
+
const observedContainers = new Map<string, HTMLElement>()
|
|
27
36
|
|
|
28
37
|
const DEV_MODE = isDevelopmentMode()
|
|
29
38
|
|
|
@@ -34,91 +43,75 @@ export function createWidgetLifecycleManager(
|
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
const checkAllReady = () => {
|
|
37
|
-
if (renderedWidgets.value.size
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (DEV_MODE) {
|
|
41
|
-
log.widgets(
|
|
42
|
-
"useWidgetLifecycle",
|
|
43
|
-
`✅ All ${totalWidgets} widgets are fully rendered`,
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
readyCallbacks.forEach((callback) => callback())
|
|
48
|
-
readyCallbacks.clear()
|
|
46
|
+
if (renderedWidgets.value.size !== totalWidgets || allWidgetsReady.value) {
|
|
47
|
+
return
|
|
49
48
|
}
|
|
50
|
-
}
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
if (element.childNodes.length === 0) {
|
|
54
|
-
return false
|
|
55
|
-
}
|
|
50
|
+
allWidgetsReady.value = true
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
if (DEV_MODE) {
|
|
53
|
+
log.widgets(
|
|
54
|
+
"useWidgetLifecycle",
|
|
55
|
+
`All ${totalWidgets} widgets are fully rendered`,
|
|
56
|
+
)
|
|
60
57
|
}
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
59
|
+
readyCallbacks.forEach((callback) => callback())
|
|
60
|
+
readyCallbacks.clear()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const disconnectObserver = (id: string) => {
|
|
64
|
+
observers.get(id)?.disconnect()
|
|
65
|
+
observers.delete(id)
|
|
66
|
+
observedContainers.delete(id)
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
const
|
|
72
|
-
|
|
69
|
+
const scheduleContainerCheck = (
|
|
70
|
+
container: HTMLElement,
|
|
73
71
|
onReady: () => void,
|
|
74
72
|
): (() => void) => {
|
|
75
73
|
let scheduled = false
|
|
76
74
|
|
|
77
75
|
const runCheck = () => {
|
|
78
76
|
scheduled = false
|
|
79
|
-
if (
|
|
77
|
+
if (hasWidgetSlotContent(container)) {
|
|
80
78
|
onReady()
|
|
81
79
|
}
|
|
82
80
|
}
|
|
83
81
|
|
|
84
|
-
|
|
82
|
+
return () => {
|
|
85
83
|
if (scheduled) {
|
|
86
84
|
return
|
|
87
85
|
}
|
|
88
86
|
scheduled = true
|
|
89
87
|
scheduleAfterPaint(runCheck)
|
|
90
88
|
}
|
|
91
|
-
|
|
92
|
-
return scheduleCheck
|
|
93
89
|
}
|
|
94
90
|
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
|
|
91
|
+
const registerWidgetAsReady = (id: string) => {
|
|
92
|
+
if (renderedWidgets.value.has(id)) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
98
95
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"useWidgetLifecycle",
|
|
102
|
-
`📦 Widget rendered: ${id} (${renderedWidgets.value.size}/${totalWidgets})`,
|
|
103
|
-
)
|
|
104
|
-
}
|
|
96
|
+
renderedWidgets.value.add(id)
|
|
97
|
+
disconnectObserver(id)
|
|
105
98
|
|
|
106
|
-
|
|
99
|
+
if (DEV_MODE) {
|
|
100
|
+
log.widgets(
|
|
101
|
+
"useWidgetLifecycle",
|
|
102
|
+
`Widget ready: ${id} (${renderedWidgets.value.size}/${totalWidgets})`,
|
|
103
|
+
)
|
|
107
104
|
}
|
|
105
|
+
|
|
106
|
+
checkAllReady()
|
|
108
107
|
}
|
|
109
108
|
|
|
110
109
|
const unregisterWidget = (id: string) => {
|
|
111
110
|
if (DEV_MODE) {
|
|
112
|
-
log.widgets("useWidgetLifecycle",
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Clean up observer
|
|
116
|
-
const observer = observers.get(id)
|
|
117
|
-
if (observer) {
|
|
118
|
-
observer.disconnect()
|
|
119
|
-
observers.delete(id)
|
|
111
|
+
log.widgets("useWidgetLifecycle", `Widget unregistered: ${id}`)
|
|
120
112
|
}
|
|
121
113
|
|
|
114
|
+
disconnectObserver(id)
|
|
122
115
|
renderedWidgets.value.delete(id)
|
|
123
116
|
|
|
124
117
|
if (renderedWidgets.value.size < totalWidgets) {
|
|
@@ -126,33 +119,30 @@ export function createWidgetLifecycleManager(
|
|
|
126
119
|
}
|
|
127
120
|
}
|
|
128
121
|
|
|
129
|
-
const
|
|
130
|
-
|
|
122
|
+
const observeWidgetContainer = (id: string, container: HTMLElement) => {
|
|
123
|
+
if (renderedWidgets.value.has(id)) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
131
126
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
observer = null
|
|
135
|
-
observers.delete(id)
|
|
136
|
-
registerWidgetAsRendered(id)
|
|
127
|
+
if (observedContainers.get(id) === container) {
|
|
128
|
+
return
|
|
137
129
|
}
|
|
138
130
|
|
|
139
|
-
|
|
131
|
+
disconnectObserver(id)
|
|
132
|
+
observedContainers.set(id, container)
|
|
140
133
|
|
|
141
|
-
|
|
142
|
-
|
|
134
|
+
const scheduleCheck = scheduleContainerCheck(container, () =>
|
|
135
|
+
registerWidgetAsReady(id),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const observer = new MutationObserver(scheduleCheck)
|
|
139
|
+
observer.observe(container, {
|
|
143
140
|
childList: true,
|
|
144
141
|
subtree: true,
|
|
145
142
|
characterData: true,
|
|
146
143
|
})
|
|
147
144
|
observers.set(id, observer)
|
|
148
145
|
scheduleCheck()
|
|
149
|
-
|
|
150
|
-
if (DEV_MODE) {
|
|
151
|
-
log.widgets(
|
|
152
|
-
"useWidgetLifecycle",
|
|
153
|
-
`👀 Observing widget for content: ${id}`,
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
146
|
}
|
|
157
147
|
|
|
158
148
|
const createLifecycleHooks = (widgetId: string): WidgetLifecycleHooks => ({
|
|
@@ -163,24 +153,23 @@ export function createWidgetLifecycleManager(
|
|
|
163
153
|
readyCallbacks.add(callback)
|
|
164
154
|
}
|
|
165
155
|
},
|
|
166
|
-
|
|
167
|
-
|
|
156
|
+
onWidgetContainerMounted: (element: HTMLElement) => {
|
|
157
|
+
observeWidgetContainer(widgetId, element)
|
|
168
158
|
},
|
|
169
159
|
onWidgetUnmounted: () => unregisterWidget(widgetId),
|
|
170
160
|
})
|
|
171
161
|
|
|
172
162
|
const reset = () => {
|
|
173
|
-
// Disconnect all observers
|
|
174
163
|
observers.forEach((observer) => observer.disconnect())
|
|
175
164
|
observers.clear()
|
|
165
|
+
observedContainers.clear()
|
|
176
166
|
renderedWidgets.value.clear()
|
|
177
167
|
allWidgetsReady.value = false
|
|
178
168
|
readyCallbacks.clear()
|
|
179
|
-
|
|
180
169
|
markReadyIfNoWidgets()
|
|
181
170
|
|
|
182
171
|
if (DEV_MODE) {
|
|
183
|
-
log.widgets("useWidgetLifecycle", "
|
|
172
|
+
log.widgets("useWidgetLifecycle", "Widget lifecycle manager reset")
|
|
184
173
|
}
|
|
185
174
|
}
|
|
186
175
|
|
|
@@ -6,7 +6,10 @@ import { forEachObjIndexed } from "ramda"
|
|
|
6
6
|
import type { ConfigurableParameters, WidgetConf } from "@racletteOrchestrator"
|
|
7
7
|
import { generatePluginKey } from "@shared/helper/pluginHelper"
|
|
8
8
|
import type { PluginMetadata } from "@core"
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
RegisteredWidgetDeclaration,
|
|
11
|
+
WidgetDeclaration,
|
|
12
|
+
} from "@racletteOrchestrator"
|
|
10
13
|
import { normalizeWidgetVisuals } from "@racletteOrchestrator/helpers"
|
|
11
14
|
|
|
12
15
|
// Plugin metadata files (unchanged)
|
|
@@ -134,9 +137,8 @@ export const getWidgetDeclacations = () => {
|
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
// Extract details and config (handle both default export and named exports)
|
|
137
|
-
const details = (setupModule.details ||
|
|
138
|
-
|
|
|
139
|
-
| undefined
|
|
140
|
+
const details = (setupModule.details ||
|
|
141
|
+
setupModule.default?.details) as WidgetDeclaration | undefined
|
|
140
142
|
const config = setupModule.config || setupModule.default?.config
|
|
141
143
|
|
|
142
144
|
if (!details) {
|
|
@@ -150,6 +152,7 @@ export const getWidgetDeclacations = () => {
|
|
|
150
152
|
const registeredWidget: RegisteredWidgetDeclaration = {
|
|
151
153
|
...details,
|
|
152
154
|
...normalizedVisuals,
|
|
155
|
+
description: details.description?.trim() ?? "",
|
|
153
156
|
pluginKey,
|
|
154
157
|
author: pluginMetadata.author,
|
|
155
158
|
name: widgetName,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal marker on {@link LoadingWidgetState} while a widget JS chunk loads.
|
|
3
|
+
* Not used in plugin widgets — only the orchestrator loading placeholder.
|
|
4
|
+
*/
|
|
5
|
+
export const WIDGET_CHUNK_LOADING_ATTR = "data-raclette-widget-chunk-loading"
|
|
6
|
+
|
|
7
|
+
export const WIDGET_CHUNK_LOADING_SELECTOR = `[${WIDGET_CHUNK_LOADING_ATTR}]`
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Component } from "vue"
|
|
2
|
-
import { h } from "vue"
|
|
3
2
|
import type { WidgetDeclaration } from "@racletteOrchestrator"
|
|
4
3
|
import { registeredPlugins } from "@racletteCore/setup/plugin-system"
|
|
5
4
|
import logger from "@racletteCore/lib/logger"
|
|
5
|
+
import { createEnhancedError } from "@racletteOrchestrator/helpers/widgetUtils"
|
|
6
6
|
|
|
7
7
|
export type WidgetInfo = {
|
|
8
8
|
widgetName: string
|
|
@@ -177,23 +177,12 @@ const initializeWidgetDiscovery = () => {
|
|
|
177
177
|
// =====================================================
|
|
178
178
|
|
|
179
179
|
const loadWidget = async (widgetInfo: WidgetInfo) => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const component = componentModule.default || componentModule
|
|
183
|
-
|
|
184
|
-
const setup = await loadWidgetSetup(widgetInfo)
|
|
180
|
+
const componentModule = await widgetInfo.componentLoader()
|
|
181
|
+
const component = componentModule.default || componentModule
|
|
185
182
|
|
|
186
|
-
|
|
187
|
-
} catch (error) {
|
|
188
|
-
logger.widgets("Failed to load widget", {
|
|
189
|
-
widgetName: widgetInfo.widgetName,
|
|
190
|
-
error: error instanceof Error ? error.message : String(error),
|
|
191
|
-
})
|
|
183
|
+
const setup = await loadWidgetSetup(widgetInfo)
|
|
192
184
|
|
|
193
|
-
|
|
194
|
-
component: createErrorComponent(widgetInfo.widgetName),
|
|
195
|
-
}
|
|
196
|
-
}
|
|
185
|
+
return { component, ...setup }
|
|
197
186
|
}
|
|
198
187
|
|
|
199
188
|
const loadWidgetSetup = async (widgetInfo: WidgetInfo) => {
|
|
@@ -216,16 +205,6 @@ const loadWidgetSetup = async (widgetInfo: WidgetInfo) => {
|
|
|
216
205
|
}
|
|
217
206
|
}
|
|
218
207
|
|
|
219
|
-
const createErrorComponent = (widgetName: string): Component => ({
|
|
220
|
-
name: `${widgetName}Error`,
|
|
221
|
-
setup: () => () =>
|
|
222
|
-
h(
|
|
223
|
-
"div",
|
|
224
|
-
{ class: "p-4 text-red-500 border border-red-300 rounded" },
|
|
225
|
-
`Failed to load widget: ${widgetName}`,
|
|
226
|
-
),
|
|
227
|
-
})
|
|
228
|
-
|
|
229
208
|
// =====================================================
|
|
230
209
|
// PATH RESOLUTION HELPERS
|
|
231
210
|
// =====================================================
|
|
@@ -318,7 +297,10 @@ export const loadWidgetComponent = async (
|
|
|
318
297
|
}
|
|
319
298
|
return component
|
|
320
299
|
} catch (error) {
|
|
321
|
-
const enhancedError =
|
|
300
|
+
const enhancedError = createEnhancedError(error, {
|
|
301
|
+
pluginKey,
|
|
302
|
+
name: widgetName,
|
|
303
|
+
})
|
|
322
304
|
logger.widgets("Widget component load failed", {
|
|
323
305
|
pluginKey,
|
|
324
306
|
widgetName,
|
|
@@ -328,22 +310,5 @@ export const loadWidgetComponent = async (
|
|
|
328
310
|
}
|
|
329
311
|
}
|
|
330
312
|
|
|
331
|
-
const createEnhancedLoadError = (
|
|
332
|
-
error: unknown,
|
|
333
|
-
pluginKey: string,
|
|
334
|
-
widgetName: string,
|
|
335
|
-
) => {
|
|
336
|
-
if (error instanceof Error) {
|
|
337
|
-
const enhancedError = new Error(
|
|
338
|
-
`Failed to load widget component ${pluginKey}/${widgetName}: ${error.message}`,
|
|
339
|
-
)
|
|
340
|
-
enhancedError.stack = error.stack
|
|
341
|
-
return enhancedError
|
|
342
|
-
}
|
|
343
|
-
return new Error(
|
|
344
|
-
`Failed to load widget component ${pluginKey}/${widgetName}: ${String(error)}`,
|
|
345
|
-
)
|
|
346
|
-
}
|
|
347
|
-
|
|
348
313
|
// Initialize on module load
|
|
349
314
|
initializeWidgetDiscovery()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Composition } from "@shared/types/core/Composition.types"
|
|
2
|
+
import { isDefined } from "@vueuse/core"
|
|
3
|
+
import { chain, filter, join, length, map, pipe, pickBy } from "ramda"
|
|
4
|
+
|
|
5
|
+
type WidgetsLayout = Composition["widgetsLayout"]
|
|
6
|
+
|
|
7
|
+
/** Count slots that reference a widget instance. */
|
|
8
|
+
export const countWidgetsInLayout = (layout: WidgetsLayout): number =>
|
|
9
|
+
pipe(
|
|
10
|
+
chain((column) => column),
|
|
11
|
+
filter((slot) => Boolean(slot.widget)),
|
|
12
|
+
length,
|
|
13
|
+
)(layout)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Stable signature of widget UUIDs in layout order.
|
|
17
|
+
* Used to detect structural layout changes without deep-reacting to mutations.
|
|
18
|
+
*/
|
|
19
|
+
export const getWidgetsLayoutSignature = (layout: WidgetsLayout): string =>
|
|
20
|
+
pipe(
|
|
21
|
+
chain((column) => map((slot) => slot.widget?.uuid ?? "", column)),
|
|
22
|
+
join("|"),
|
|
23
|
+
)(layout)
|
|
24
|
+
|
|
25
|
+
/** Drop undefined route param values before passing them to widgets. */
|
|
26
|
+
export const cleanRouteParams = (
|
|
27
|
+
params: Record<string, string | undefined> | undefined,
|
|
28
|
+
): Record<string, string> => {
|
|
29
|
+
if (!params) {
|
|
30
|
+
return {}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return pickBy(
|
|
34
|
+
(value) => isDefined(value) && value !== "undefined",
|
|
35
|
+
params,
|
|
36
|
+
) as Record<string, string>
|
|
37
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { WIDGET_CHUNK_LOADING_SELECTOR } from "@racletteOrchestrator/constants/widgetSlot"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* True when a widget slot container has rendered plugin content.
|
|
5
|
+
*
|
|
6
|
+
* Ignores the orchestrator's async chunk loader (`LoadingWidgetState`).
|
|
7
|
+
* Widget-owned loading UI (skeletons, spinners) inside the slot counts as content.
|
|
8
|
+
*/
|
|
9
|
+
export const hasWidgetSlotContent = (container: HTMLElement): boolean => {
|
|
10
|
+
if (container.querySelector(WIDGET_CHUNK_LOADING_SELECTOR)) {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT)
|
|
15
|
+
if (walker.nextNode()) {
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return Boolean(container.textContent?.trim())
|
|
20
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
images
|
|
6
|
-
|
|
1
|
+
import type { WidgetDeclaration } from "@racletteOrchestrator/types/Widgets"
|
|
2
|
+
|
|
3
|
+
export type WidgetVisualInput = Pick<
|
|
4
|
+
WidgetDeclaration,
|
|
5
|
+
"title" | "color" | "icon" | "images"
|
|
6
|
+
>
|
|
7
7
|
|
|
8
8
|
export type WidgetVisuals = {
|
|
9
9
|
color: string
|
|
@@ -27,10 +27,7 @@ export const generateColorFromName = (name: string): string => {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export const getWidgetInitials = (name: string): string => {
|
|
30
|
-
const parts = name
|
|
31
|
-
.trim()
|
|
32
|
-
.split(/\s+/)
|
|
33
|
-
.filter(Boolean)
|
|
30
|
+
const parts = name.trim().split(/\s+/).filter(Boolean)
|
|
34
31
|
|
|
35
32
|
if (parts.length === 0) {
|
|
36
33
|
return "W"
|
|
@@ -59,8 +56,10 @@ export const normalizeWidgetVisuals = (
|
|
|
59
56
|
: ""
|
|
60
57
|
|
|
61
58
|
const normalizedImages = Array.isArray(details.images)
|
|
62
|
-
// Ignore invalid/empty entries to keep consumers simple.
|
|
63
|
-
|
|
59
|
+
? // Ignore invalid/empty entries to keep consumers simple.
|
|
60
|
+
details.images.filter(
|
|
61
|
+
(image) => typeof image === "string" && image.trim(),
|
|
62
|
+
)
|
|
64
63
|
: []
|
|
65
64
|
|
|
66
65
|
return {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { WidgetBase } from "@racletteCore/store/types"
|
|
2
|
+
import type {
|
|
3
|
+
OnAllWidgetsReady,
|
|
4
|
+
WidgetLifecycleManager,
|
|
5
|
+
} from "@racletteOrchestrator/composables/useWidgetLifecycle"
|
|
6
|
+
import { h, type Component } from "vue"
|
|
7
|
+
|
|
8
|
+
const noopAllWidgetsReady: OnAllWidgetsReady = () => {}
|
|
9
|
+
|
|
10
|
+
/** Injects `onAllWidgetsReady` into loaded widget components. */
|
|
11
|
+
export const wrapWidgetWithLifecycle = (
|
|
12
|
+
widget: WidgetBase,
|
|
13
|
+
originalComponent: Component,
|
|
14
|
+
getLifecycleManager: () => WidgetLifecycleManager | null,
|
|
15
|
+
): Component => ({
|
|
16
|
+
name: `${originalComponent.name || "Widget"}Wrapper`,
|
|
17
|
+
setup(props, context) {
|
|
18
|
+
const onAllWidgetsReady =
|
|
19
|
+
getLifecycleManager()?.createLifecycleHooks(widget.uuid)
|
|
20
|
+
.onAllWidgetsReady ?? noopAllWidgetsReady
|
|
21
|
+
|
|
22
|
+
return () =>
|
|
23
|
+
h(
|
|
24
|
+
originalComponent,
|
|
25
|
+
{
|
|
26
|
+
...props,
|
|
27
|
+
onAllWidgetsReady,
|
|
28
|
+
},
|
|
29
|
+
context.slots,
|
|
30
|
+
)
|
|
31
|
+
},
|
|
32
|
+
})
|
|
@@ -1,31 +1,12 @@
|
|
|
1
|
-
import { loadingHold } from "@racletteCore/store/state"
|
|
2
|
-
import { $store } from "@racletteCore"
|
|
3
1
|
import { scheduleAfterPaint } from "@racletteCore/lib/scheduleTask"
|
|
4
2
|
import type { RouteLocationNormalized } from "vue-router"
|
|
5
3
|
import { handleRouteChange } from "../routeStore"
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
|
-
*
|
|
9
|
-
* @param { object } store, mini-rx store instance
|
|
10
|
-
*
|
|
11
|
-
* removeLoading Animation
|
|
6
|
+
* Runs deferred route-store work after navigation.
|
|
12
7
|
*/
|
|
13
8
|
export default (to: RouteLocationNormalized, from: RouteLocationNormalized) => {
|
|
14
9
|
scheduleAfterPaint(() => {
|
|
15
10
|
handleRouteChange(to, from)
|
|
16
11
|
})
|
|
17
|
-
|
|
18
|
-
const loading = $store.getState(["ui", "loading"])
|
|
19
|
-
if (loading) {
|
|
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)
|
|
30
|
-
}
|
|
31
12
|
}
|
|
@@ -38,22 +38,28 @@ export type ConfigurableParameters = Record<
|
|
|
38
38
|
EditorSettings & { defaultValue: any }
|
|
39
39
|
>
|
|
40
40
|
|
|
41
|
+
/** Catalog entry after registration (workbench picker, APIs). All fields are required. */
|
|
41
42
|
export type RegisteredWidgetDeclaration = {
|
|
42
43
|
pluginKey: string
|
|
43
44
|
author: string
|
|
45
|
+
name: string
|
|
44
46
|
title: string
|
|
47
|
+
description: string
|
|
45
48
|
color: string
|
|
46
49
|
icon: string
|
|
47
50
|
images: string[]
|
|
48
|
-
description: string
|
|
49
51
|
configurableParameters?: ConfigurableParameters
|
|
50
|
-
name: string
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Plugin-authored `details` in `frontend/widgets/<WidgetFolder>/setup.ts`.
|
|
56
|
+
*
|
|
57
|
+
* `pluginKey`, `author`, and `name` are added by the framework (from plugin metadata
|
|
58
|
+
* and the `*Widget.vue` filename). Optional visual fields may be omitted or left
|
|
59
|
+
* empty — `normalizeWidgetVisuals` fills defaults before producing a
|
|
60
|
+
* `RegisteredWidgetDeclaration`.
|
|
61
|
+
*/
|
|
62
|
+
export type WidgetDeclaration = {
|
|
57
63
|
title: string
|
|
58
64
|
description?: string
|
|
59
65
|
color?: string
|
package/src/types.ts
CHANGED
|
@@ -21,9 +21,15 @@ export type RacletteConfig = {
|
|
|
21
21
|
port: number
|
|
22
22
|
name?: string
|
|
23
23
|
volume?: string
|
|
24
|
+
/** Default Valkey DB index for all roles when role-specific value is unset (default: 0). */
|
|
24
25
|
db?: number
|
|
26
|
+
/** Per-stack Valkey DB overrides (app / workbench / core). */
|
|
27
|
+
dbs?: {
|
|
28
|
+
app?: number
|
|
29
|
+
workbench?: number
|
|
30
|
+
core?: number
|
|
31
|
+
}
|
|
25
32
|
volumes?: VolumeDefinition[] // Add custom volumes property
|
|
26
|
-
|
|
27
33
|
}
|
|
28
34
|
workbench?: {
|
|
29
35
|
enabled: boolean
|