@bpmn-nova/react 0.3.1-preview

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/dist/index.js ADDED
@@ -0,0 +1,352 @@
1
+ import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
2
+ import { BpmnDesigner as CoreDesigner } from '@bpmn-nova/studio/designer';
3
+ import { BpmnViewer as CoreViewer } from '@bpmn-nova/studio/viewer';
4
+ import { importBpmn, exportBpmn } from '@bpmn-nova/studio';
5
+ import { createEmptyProcess } from '@bpmn-nova/studio';
6
+ import { BpmnCanvas as CoreCanvas, BpmnStudioShell as CoreStudioShell, createInteractionController, createStudioController, createTemplateRegistry } from '@bpmn-nova/studio';
7
+ import { createDefaultPaletteRegistry, PalettePanel as CorePalettePanel } from '@bpmn-nova/studio';
8
+
9
+ export function createReactRuntimeDetailsComponent(Component) {
10
+ return ({ container, ...context }) => {
11
+ const root = createRoot(container);
12
+ root.render(React.createElement(Component, context));
13
+ return () => root.unmount();
14
+ };
15
+ }
16
+
17
+ export function createReactRuntimeTransitionDetailsComponent(Component) {
18
+ return createReactRuntimeDetailsComponent(Component);
19
+ }
20
+
21
+ export function createReactRuntimeTimelineComponent(Component) {
22
+ return createReactRuntimeDetailsComponent(Component);
23
+ }
24
+
25
+ function resolveModel({ model, xml, engine = 'flowable' }) {
26
+ if (model) return model;
27
+ if (xml) return importBpmn(xml, engine);
28
+ return createEmptyProcess(engine);
29
+ }
30
+
31
+ export function useBpmnStudio(options = {}) {
32
+ const ownedRef = useRef(null);
33
+ if (!options.studio && !ownedRef.current) ownedRef.current = createStudioController({ model: resolveModel(options), propertiesProfile: options.propertiesProfile });
34
+ const studio = options.studio || ownedRef.current;
35
+ const [state, setState] = useState(() => studio.getState());
36
+ useEffect(() => studio.subscribe((event) => setState(event.state)), [studio]);
37
+ useEffect(() => () => { if (!options.studio) ownedRef.current?.destroy(); }, [studio]);
38
+ return { studio, state, commands: studio.commands };
39
+ }
40
+
41
+ export const BpmnCanvas = forwardRef(function BpmnCanvas(props, ref) {
42
+ const hostRef = useRef(null);
43
+ const instanceRef = useRef(null);
44
+ useImperativeHandle(ref, () => ({
45
+ get instance() { return instanceRef.current; },
46
+ fitView(padding, options) { instanceRef.current?.fitView(padding, options); },
47
+ clientToWorld(x, y) { return instanceRef.current?.clientToWorld(x, y); },
48
+ setTheme(theme) { return instanceRef.current?.setTheme(theme); },
49
+ exportSvg(options) { return instanceRef.current?.exportSvg(options); },
50
+ openSvgExportPreview(options) { return instanceRef.current?.openSvgExportPreview(options); },
51
+ }), []);
52
+ useEffect(() => {
53
+ if (!hostRef.current || !props.studio) return undefined;
54
+ const interactions = props.interactions || createInteractionController({ studio: props.studio, templates: props.templates || createTemplateRegistry() });
55
+ const instance = new CoreCanvas({ container: hostRef.current, studio: props.studio, interactions, theme: props.theme, onThemeChange: props.onThemeChange, rendererOptions: { iconRegistry: props.iconRegistry, nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, svgExport: props.svgExport } });
56
+ instanceRef.current = instance;
57
+ requestAnimationFrame(() => instance.fitView());
58
+ return () => { instance.destroy(); instanceRef.current = null; };
59
+ }, [props.studio]);
60
+ useEffect(() => { if (props.theme !== undefined) instanceRef.current?.setTheme(props.theme); }, [props.theme]);
61
+ return React.createElement('div', { ref: hostRef, className: props.className, style: { width: '100%', height: '100%', minHeight: 320, ...props.style } });
62
+ });
63
+
64
+ export const BpmnPalettePanel = forwardRef(function BpmnPalettePanel(props, ref) {
65
+ const hostRef = useRef(null);
66
+ const panelRef = useRef(null);
67
+ useImperativeHandle(ref, () => ({ get panel() { return panelRef.current; }, render() { panelRef.current?.render(); }, setTheme(theme) { return panelRef.current?.setTheme(theme); } }), []);
68
+ useEffect(() => {
69
+ if (!hostRef.current || !props.studio) return undefined;
70
+ const interactions = props.interactions || createInteractionController({ studio: props.studio, templates: props.templates || createTemplateRegistry() });
71
+ const registry = props.registry || createDefaultPaletteRegistry({ providers: props.providers || [] });
72
+ const panel = new CorePalettePanel({ container: hostRef.current, registry, studio: props.studio, interactions, canvas: props.canvas, iconRegistry: props.iconRegistry, renderItem: props.renderItem, renderSection: props.renderSection, theme: props.theme, onThemeChange: props.onThemeChange });
73
+ panelRef.current = panel;
74
+ return () => { panel.destroy(); panelRef.current = null; };
75
+ }, [props.studio, props.registry, props.interactions, props.canvas]);
76
+ useEffect(() => { if (props.theme !== undefined) panelRef.current?.setTheme(props.theme); }, [props.theme]);
77
+ return React.createElement('div', { ref: hostRef, className: props.className, style: { width: '100%', height: '100%', overflow: 'auto', ...props.style } });
78
+ });
79
+
80
+ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
81
+ const hostRef = useRef(null);
82
+ const ownedStudioRef = useRef(null);
83
+ const shellRef = useRef(null);
84
+ const callbacksRef = useRef({});
85
+ callbacksRef.current = props;
86
+ if (!props.studio && !ownedStudioRef.current) ownedStudioRef.current = createStudioController({ model: resolveModel(props), propertiesProfile: props.propertiesProfile });
87
+ const studio = props.studio || ownedStudioRef.current;
88
+ useImperativeHandle(ref, () => ({
89
+ studio,
90
+ get shell() { return shellRef.current; },
91
+ exportXml: (engine) => studio.exportXml(engine),
92
+ exportSvg: (options) => shellRef.current?.exportSvg(options),
93
+ openSvgExportPreview: (options) => shellRef.current?.openSvgExportPreview(options),
94
+ fitView: () => shellRef.current?.canvas?.fitView(),
95
+ setTheme: (theme) => shellRef.current?.setTheme(theme),
96
+ }), [studio]);
97
+ useEffect(() => {
98
+ if (!hostRef.current) return undefined;
99
+ const shell = new CoreStudioShell({
100
+ container: hostRef.current,
101
+ studio,
102
+ iconRegistry: props.iconRegistry,
103
+ paletteRegistry: props.paletteRegistry,
104
+ propertiesRegistry: props.propertiesRegistry,
105
+ templateRegistry: props.templateRegistry,
106
+ rendererOptions: {
107
+ nodeRenderers: props.nodeRenderers,
108
+ nodeRenderer: props.nodeRenderer,
109
+ runtimePresenter: props.runtimePresenter,
110
+ runtimeTraceProjector: props.runtimeTraceProjector,
111
+ runtimeAssetResolver: props.runtimeAssetResolver,
112
+ runtimeTraceOptions: props.runtimeTraceOptions,
113
+ runtimeTimelineRenderer: props.runtimeTimelineRenderer,
114
+ timeline: props.timeline,
115
+ runtimeDetails: props.runtimeDetails,
116
+ onRuntimeTraceItemClick: (payload) => callbacksRef.current.onRuntimeTraceItemClick?.(payload),
117
+ runtimeDetailsRenderer: props.runtimeDetailsRenderer,
118
+ onRuntimeDetailsOpen: (payload) => callbacksRef.current.onRuntimeDetailsOpen?.(payload),
119
+ runtimeTransitionDetailsRenderer: props.runtimeTransitionDetailsRenderer,
120
+ onRuntimeTransitionDetailsOpen: (payload) => callbacksRef.current.onRuntimeTransitionDetailsOpen?.(payload),
121
+ onElementClick: (payload) => callbacksRef.current.onElementClick?.(payload),
122
+ onTraceClick: (payload) => callbacksRef.current.onTraceClick?.(payload),
123
+ },
124
+ slots: props.slots || {},
125
+ layout: props.layout,
126
+ runtime: props.runtime,
127
+ mode: props.mode,
128
+ projection: props.projection,
129
+ responsive: props.responsive,
130
+ projectionOptions: props.projectionOptions,
131
+ leftWidth: props.leftWidth,
132
+ rightWidth: props.rightWidth,
133
+ theme: props.theme,
134
+ runtimeAppearance: props.runtimeAppearance,
135
+ svgExport: props.svgExport,
136
+ onThemeChange: (state) => callbacksRef.current.onThemeChange?.(state),
137
+ });
138
+ shellRef.current = shell;
139
+ const off = studio.subscribe((event) => {
140
+ if (event.type === 'modelChanged') callbacksRef.current.onChange?.(studio.model, event.reason, studio.exportXml());
141
+ if (event.type === 'selectionChanged') callbacksRef.current.onSelectionChange?.(studio.selection, studio.getSelectedElement());
142
+ if (event.type === 'scopeChanged') callbacksRef.current.onScopeChange?.(event.activeScopeId, event.scopePath, studio.getState());
143
+ });
144
+ return () => { off(); shell.destroy(); shellRef.current = null; if (!props.studio) { ownedStudioRef.current?.destroy(); ownedStudioRef.current = null; } };
145
+ }, [studio]);
146
+ useEffect(() => { if (props.model && props.model !== studio.model) studio.setModel(props.model); else if (props.xml) studio.importXml(props.xml, props.engine); }, [props.model, props.xml]);
147
+ useEffect(() => { if (props.runtime !== undefined) shellRef.current?.setRuntime(props.runtime); }, [props.runtime]);
148
+ useEffect(() => { if (props.mode) shellRef.current?.setMode(props.mode); }, [props.mode]);
149
+ useEffect(() => { if (props.projection) shellRef.current?.setProjection(props.projection); }, [props.projection]);
150
+ useEffect(() => { if (props.theme !== undefined) shellRef.current?.setTheme(props.theme); }, [props.theme]);
151
+ useEffect(() => { if (props.runtimeAppearance !== undefined) shellRef.current?.setRuntimeAppearance(props.runtimeAppearance); }, [props.runtimeAppearance]);
152
+ useEffect(() => {
153
+ const shell = shellRef.current;
154
+ if (!shell) return;
155
+ shell.rendererOptions.timeline = props.timeline;
156
+ shell.rendererOptions.runtimeDetails = props.runtimeDetails;
157
+ shell.rendererOptions.runtimeTraceOptions = props.runtimeTraceOptions;
158
+ shell.viewer?.setDisplayOptions({ timeline: props.timeline, runtimeDetails: props.runtimeDetails, runtimeTraceOptions: props.runtimeTraceOptions });
159
+ }, [props.timeline, props.runtimeDetails, props.runtimeTraceOptions]);
160
+ return React.createElement('div', { ref: hostRef, className: props.className, style: { width: '100%', height: '100%', minHeight: 520, ...props.style } });
161
+ });
162
+
163
+ export const BpmnDesigner = forwardRef(function BpmnDesigner(props, ref) {
164
+ const hostRef = useRef(null);
165
+ const instanceRef = useRef(null);
166
+
167
+ useImperativeHandle(ref, () => ({
168
+ get instance() { return instanceRef.current; },
169
+ get model() { return instanceRef.current?.model; },
170
+ exportXml(engine) { return instanceRef.current?.exportXml(engine) || ''; },
171
+ fitView() { instanceRef.current?.renderer.fitView(); },
172
+ beautify(options) { instanceRef.current?.beautify(options); },
173
+ rerouteEdges(options) { instanceRef.current?.rerouteEdges(options); },
174
+ validate() { return instanceRef.current?.validate() || []; },
175
+ setTheme(theme) { return instanceRef.current?.setTheme(theme); },
176
+ exportSvg(options) { return instanceRef.current?.exportSvg(options); },
177
+ openSvgExportPreview(options) { return instanceRef.current?.openSvgExportPreview(options); },
178
+ }), []);
179
+
180
+ useEffect(() => {
181
+ if (!hostRef.current) return undefined;
182
+ const instance = new CoreDesigner({
183
+ container: hostRef.current,
184
+ model: resolveModel(props),
185
+ onChange: (model, reason) => props.onChange?.(model, reason, exportBpmn(model, model.engine)),
186
+ onSelectionChange: props.onSelectionChange,
187
+ theme: props.theme,
188
+ svgExport: props.svgExport,
189
+ onThemeChange: props.onThemeChange,
190
+ });
191
+ instanceRef.current = instance;
192
+ requestAnimationFrame(() => instance.renderer.fitView());
193
+ return () => {
194
+ instance.destroy();
195
+ instanceRef.current = null;
196
+ };
197
+ }, []);
198
+ useEffect(() => { if (props.theme !== undefined) instanceRef.current?.setTheme(props.theme); }, [props.theme]);
199
+
200
+ useEffect(() => {
201
+ const instance = instanceRef.current;
202
+ if (!instance) return;
203
+ if (props.model && props.model !== instance.model) instance.setModel(props.model);
204
+ else if (props.xml) instance.setModel(importBpmn(props.xml, props.engine));
205
+ }, [props.model, props.xml]);
206
+
207
+ useEffect(() => {
208
+ if (props.engine && instanceRef.current && props.engine !== instanceRef.current.model.engine) {
209
+ instanceRef.current.setEngine(props.engine);
210
+ }
211
+ }, [props.engine]);
212
+
213
+ return React.createElement('div', {
214
+ ref: hostRef,
215
+ className: props.className,
216
+ style: { width: '100%', height: '100%', minHeight: 320, ...props.style },
217
+ });
218
+ });
219
+
220
+ export const BpmnViewer = forwardRef(function BpmnViewer(props, ref) {
221
+ const hostRef = useRef(null);
222
+ const instanceRef = useRef(null);
223
+ const callbacksRef = useRef({});
224
+ callbacksRef.current = {
225
+ onTraceClick: props.onTraceClick,
226
+ onElementClick: props.onElementClick,
227
+ onRuntimeTraceItemClick: props.onRuntimeTraceItemClick,
228
+ onProjectionChange: props.onProjectionChange,
229
+ onRuntimeDetailsOpen: props.onRuntimeDetailsOpen,
230
+ onRuntimeTransitionDetailsOpen: props.onRuntimeTransitionDetailsOpen,
231
+ onThemeChange: props.onThemeChange,
232
+ };
233
+
234
+ useImperativeHandle(ref, () => ({
235
+ get instance() { return instanceRef.current; },
236
+ fitView() { instanceRef.current?.fitView(); },
237
+ setProjection(value) { instanceRef.current?.setProjection(value); },
238
+ setRuntime(value) { instanceRef.current?.setRuntime(value); },
239
+ setDisplayOptions(value) { instanceRef.current?.setDisplayOptions(value); },
240
+ setTheme(value) { return instanceRef.current?.setTheme(value); },
241
+ exportSvg(options) { return instanceRef.current?.exportSvg(options); },
242
+ openSvgExportPreview(options) { return instanceRef.current?.openSvgExportPreview(options); },
243
+ }), []);
244
+
245
+ useEffect(() => {
246
+ if (!hostRef.current) return undefined;
247
+ const instance = new CoreViewer({
248
+ container: hostRef.current,
249
+ model: resolveModel(props),
250
+ runtime: props.runtime,
251
+ projection: props.projection,
252
+ responsive: props.responsive,
253
+ runtimeTraceOptions: props.runtimeTraceOptions,
254
+ timeline: props.timeline,
255
+ runtimeDetails: props.runtimeDetails,
256
+ iconRegistry: props.iconRegistry,
257
+ nodeRenderers: props.nodeRenderers,
258
+ nodeRenderer: props.nodeRenderer,
259
+ runtimePresenter: props.runtimePresenter,
260
+ runtimeTraceProjector: props.runtimeTraceProjector,
261
+ runtimeAssetResolver: props.runtimeAssetResolver,
262
+ runtimeTimelineRenderer: props.runtimeTimelineRenderer,
263
+ onRuntimeTraceItemClick: (payload) => callbacksRef.current.onRuntimeTraceItemClick?.(payload),
264
+ onProjectionChange: (payload) => callbacksRef.current.onProjectionChange?.(payload),
265
+ runtimeDetailsRenderer: props.runtimeDetailsRenderer,
266
+ onRuntimeDetailsOpen: (payload) => callbacksRef.current.onRuntimeDetailsOpen?.(payload),
267
+ runtimeTransitionDetailsRenderer: props.runtimeTransitionDetailsRenderer,
268
+ onRuntimeTransitionDetailsOpen: (payload) => callbacksRef.current.onRuntimeTransitionDetailsOpen?.(payload),
269
+ onElementClick: (payload) => callbacksRef.current.onElementClick?.(payload),
270
+ onTraceClick: (payload) => callbacksRef.current.onTraceClick?.(payload),
271
+ theme: props.theme,
272
+ runtimeAppearance: props.runtimeAppearance,
273
+ svgExport: props.svgExport,
274
+ onThemeChange: (state) => callbacksRef.current.onThemeChange?.(state),
275
+ });
276
+ instanceRef.current = instance;
277
+ requestAnimationFrame(() => instance.fitView());
278
+ return () => {
279
+ instance.destroy();
280
+ instanceRef.current = null;
281
+ };
282
+ }, []);
283
+
284
+ useEffect(() => { if (instanceRef.current && props.runtime !== undefined) instanceRef.current.setRuntime(props.runtime); }, [props.runtime]);
285
+ useEffect(() => { if (instanceRef.current && props.projection) instanceRef.current.setProjection(props.projection); }, [props.projection]);
286
+ useEffect(() => { if (props.theme !== undefined) instanceRef.current?.setTheme(props.theme); }, [props.theme]);
287
+ useEffect(() => { if (props.runtimeAppearance !== undefined) instanceRef.current?.setRuntimeAppearance(props.runtimeAppearance); }, [props.runtimeAppearance]);
288
+ useEffect(() => { instanceRef.current?.setDisplayOptions({ timeline: props.timeline, runtimeDetails: props.runtimeDetails, runtimeTraceOptions: props.runtimeTraceOptions, runtimeAssetResolver: props.runtimeAssetResolver }); }, [props.timeline, props.runtimeDetails, props.runtimeTraceOptions, props.runtimeAssetResolver]);
289
+ useEffect(() => {
290
+ if (!instanceRef.current) return;
291
+ if (props.model) instanceRef.current.setModel(props.model);
292
+ else if (props.xml) instanceRef.current.setModel(importBpmn(props.xml, props.engine));
293
+ }, [props.model, props.xml]);
294
+
295
+ return React.createElement('div', {
296
+ ref: hostRef,
297
+ className: props.className,
298
+ style: { width: '100%', height: '100%', minHeight: 260, ...props.style },
299
+ });
300
+ });
301
+
302
+ // Properties Panel -----------------------------------------------------------
303
+ import { createRoot } from 'react-dom/client';
304
+ import { createDefaultPropertiesRegistry, PropertiesPanel as CorePropertiesPanel } from '@bpmn-nova/studio';
305
+
306
+ export function createReactPropertyComponent(Component) {
307
+ return ({ container, entry, context, value, commit }) => {
308
+ const root = createRoot(container);
309
+ root.render(React.createElement(Component, { entry, context, value, onChange: commit }));
310
+ return () => root.unmount();
311
+ };
312
+ }
313
+
314
+ export const BpmnPropertiesPanel = forwardRef(function BpmnPropertiesPanel(props, ref) {
315
+ const hostRef = useRef(null);
316
+ const panelRef = useRef(null);
317
+ const registryRef = useRef(null);
318
+
319
+ useImperativeHandle(ref, () => ({
320
+ get panel() { return panelRef.current; },
321
+ get registry() { return registryRef.current; },
322
+ render() { panelRef.current?.render(); },
323
+ setTheme(theme) { return panelRef.current?.setTheme(theme); },
324
+ }), []);
325
+
326
+ useEffect(() => {
327
+ const designer = props.studio || props.designer;
328
+ if (!hostRef.current || !designer) return undefined;
329
+ const registry = props.registry || createDefaultPropertiesRegistry({
330
+ studio: designer,
331
+ dataProviders: props.dataProviders || {},
332
+ });
333
+ for (const provider of props.providers || []) registry.registerProvider(provider.priority ?? 500, provider);
334
+ for (const [name, Component] of Object.entries(props.components || {})) registry.registerComponent(name, createReactPropertyComponent(Component));
335
+ const panel = new CorePropertiesPanel({ container: hostRef.current, registry, studio: designer, theme: props.theme, onThemeChange: props.onThemeChange });
336
+ registryRef.current = registry;
337
+ panelRef.current = panel;
338
+ panel.render();
339
+ return () => {
340
+ panel.destroy();
341
+ panelRef.current = null;
342
+ registryRef.current = null;
343
+ };
344
+ }, [props.studio, props.designer, props.registry]);
345
+ useEffect(() => { if (props.theme !== undefined) panelRef.current?.setTheme(props.theme); }, [props.theme]);
346
+
347
+ return React.createElement('div', {
348
+ ref: hostRef,
349
+ className: props.className,
350
+ style: { width: '100%', height: '100%', overflow: 'auto', ...props.style },
351
+ });
352
+ });