@bpmn-nova/react 0.3.2-preview → 0.3.4-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/README.md +70 -3
- package/dist/index.d.ts +39 -2
- package/dist/index.js +47 -5
- package/dist/styles.css +4 -3
- package/llms-full.txt +367 -59
- package/llms.txt +33 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ BPMN Nova 的 React 18+ Adapter,提供 Studio、Designer、Viewer、审批轨
|
|
|
4
4
|
|
|
5
5
|
> **English summary:** React 18+ components and refs for BPMN Nova process design, viewing, approval traces, properties, themes, and SVG export.
|
|
6
6
|
|
|
7
|
-
> 当前版本为 `0.3.
|
|
7
|
+
> 当前版本为 `0.3.4-preview`。React 与 React DOM 由宿主工程提供,本包不会替应用选择或升级框架版本。
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
@@ -20,6 +20,20 @@ import '@bpmn-nova/react/styles.css'
|
|
|
20
20
|
|
|
21
21
|
只安装本包;`@bpmn-nova/studio` 会作为内部依赖自动解析。容器必须有明确高度。
|
|
22
22
|
|
|
23
|
+
TypeScript 类型也从 React 入口导入,不要为了模型类型额外声明 Studio 直依赖:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import type {
|
|
27
|
+
BpmnNode,
|
|
28
|
+
EdgeType,
|
|
29
|
+
ElementSelection,
|
|
30
|
+
NodeType,
|
|
31
|
+
ProcessModel,
|
|
32
|
+
} from '@bpmn-nova/react'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
公共类型门面还包含 `BpmnEdge`、`EngineId` 和 `LayoutOptions`。
|
|
36
|
+
|
|
23
37
|
使用代码生成助手或 IDE Agent 接入时,从随包的 `llms.txt` 开始;需要完整接口和定制资料时再读取 `llms-full.txt`。两份文件都包含在 npm tarball 中,不依赖源码仓库可见性。
|
|
24
38
|
|
|
25
39
|
## 流程设计
|
|
@@ -50,7 +64,7 @@ export function WorkflowEditor({ initialXml }) {
|
|
|
50
64
|
}
|
|
51
65
|
```
|
|
52
66
|
|
|
53
|
-
Ref 提供 `actions`、`exportXml()`、`fitView()`、`setTheme()`、`exportSvg()` 和 `openSvgExportPreview()`。
|
|
67
|
+
Ref 提供 `actions`、`validate()`、`exportXml()`、`fitView()`、`setTheme()`、`exportSvg()` 和 `openSvgExportPreview()`。
|
|
54
68
|
|
|
55
69
|
`xml`/`model` 是外部替换输入,`onChange` 的 XML 是草稿输出。宿主回写完全相同的导出 XML 不会重新导入或清空撤销历史;切换服务器 revision 或流程时传入不同 XML 会替换模型。Activiti 宿主应显式使用 `engine="activiti"`。
|
|
56
70
|
|
|
@@ -72,17 +86,70 @@ Ref 提供 `actions`、`exportXml()`、`fitView()`、`setTheme()`、`exportSvg()
|
|
|
72
86
|
headerStart={({ state, actions }) => (
|
|
73
87
|
<HostProcessIdentity onBack={back} />
|
|
74
88
|
)}
|
|
89
|
+
headerActions={({ actions, mode }) => (
|
|
90
|
+
<HostWorkflowActions
|
|
91
|
+
disabled={mode !== 'design'}
|
|
92
|
+
onValidate={() => actions.validate()}
|
|
93
|
+
onSave={() => saveDraft(actions.exportXml())}
|
|
94
|
+
onPublish={() => publishProcess(actions)}
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
75
97
|
onChange={handleChange}
|
|
98
|
+
onValidation={(event) => handleValidation(event)}
|
|
76
99
|
onSelectionChange={(selection, element) => {
|
|
77
100
|
setSelectedElement(selection ? element : null)
|
|
78
101
|
}}
|
|
79
102
|
/>
|
|
80
103
|
```
|
|
81
104
|
|
|
82
|
-
`headerStart` 只替换 Nova Brand
|
|
105
|
+
`headerStart` 只替换 Nova Brand;`headerActions` 只替换默认“校验 / 导入 / 导出”动作组,适合组合“校验 / 保存 / 发布”;`header` 可完整替换 Header。它们都通过 React Portal 在当前组件树内渲染,保留 Context 和生命周期。默认 Header 的最佳视图只保留在底部缩放区。
|
|
106
|
+
|
|
107
|
+
`actions.validate()` 以 `toolbar` 来源先更新 Nova 默认状态栏,再调用 `onValidation` 并返回 issues;Ref `validate()` 使用 `api` 来源。事件的 `valid` 仅由 error 决定,warning 会保留在 `issues` 中但不默认阻止发布。保存与发布仍由宿主负责。
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
async function publishProcess(actions) {
|
|
111
|
+
const issues = actions.validate()
|
|
112
|
+
if (issues.some((issue) => issue.level === 'error')) return
|
|
113
|
+
await publishXml(actions.exportXml())
|
|
114
|
+
}
|
|
115
|
+
```
|
|
83
116
|
|
|
84
117
|
宿主属性面板以 `onSelectionChange` 为选择状态来源;它覆盖节点、连线、多选、清空与键盘选择。`onElementClick` 只是点击观察事件,不能代替选择状态。宿主通过稳定 BPMN Element ID 关联业务配置,并自行负责保存、发布、权限与服务端事务。
|
|
85
118
|
|
|
119
|
+
## Mode 双向同步与副标题刷新
|
|
120
|
+
|
|
121
|
+
```jsx
|
|
122
|
+
import { useCallback, useRef, useState } from 'react'
|
|
123
|
+
|
|
124
|
+
const [mode, setMode] = useState('design')
|
|
125
|
+
const summaries = useRef(new Map())
|
|
126
|
+
const resolveSubtitle = useCallback(
|
|
127
|
+
({ node }) => summaries.current.has(node.id)
|
|
128
|
+
? summaries.current.get(node.id)
|
|
129
|
+
: undefined,
|
|
130
|
+
[],
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
<BpmnStudio
|
|
134
|
+
ref={studioRef}
|
|
135
|
+
xml={xml}
|
|
136
|
+
mode={mode}
|
|
137
|
+
allowedModes={['design', 'viewer']}
|
|
138
|
+
onModeChange={(event) => setMode(event.mode)}
|
|
139
|
+
nodeSubtitleResolver={resolveSubtitle}
|
|
140
|
+
headerStart={({ mode: actualMode }) => (
|
|
141
|
+
<HostHeader mode={actualMode} />
|
|
142
|
+
)}
|
|
143
|
+
/>
|
|
144
|
+
|
|
145
|
+
function updateArchiveSummary() {
|
|
146
|
+
summaries.current.set('ServiceTask_Archive', '归档到采购系统')
|
|
147
|
+
studioRef.current?.refreshPresentation()
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Ref 的 `mode` Getter 与 `setMode()` 始终反映 Shell 实际状态。Resolver 返回 `undefined` 保留默认值、`null` 移除副标题行、字符串替换显示;函数引用变化会自动刷新,稳定闭包内部数据变化需显式调用 `refreshPresentation()`。Resolver 只影响 Design/Viewer 标准卡片和 SVG,不覆盖 Instance Runtime Presentation,也不修改 XML、History、Selection 或 Viewport。
|
|
152
|
+
|
|
86
153
|
## 只读展示与审批轨迹
|
|
87
154
|
|
|
88
155
|
```jsx
|
package/dist/index.d.ts
CHANGED
|
@@ -29,8 +29,13 @@ import type {
|
|
|
29
29
|
BpmnStudioShell,
|
|
30
30
|
ContextMenuRegistry,
|
|
31
31
|
InteractionController,
|
|
32
|
+
NodeSubtitleResolver,
|
|
32
33
|
StudioCommands,
|
|
33
34
|
StudioShellActions,
|
|
35
|
+
StudioMode,
|
|
36
|
+
StudioModeChangeEvent,
|
|
37
|
+
StudioValidationEvent,
|
|
38
|
+
StudioValidationIssue,
|
|
34
39
|
StudioShellOptions,
|
|
35
40
|
StudioShellRegions,
|
|
36
41
|
StudioShellSlots,
|
|
@@ -48,6 +53,16 @@ import type {
|
|
|
48
53
|
ViewerProjection,
|
|
49
54
|
} from '@bpmn-nova/studio/viewer'
|
|
50
55
|
|
|
56
|
+
export type {
|
|
57
|
+
BpmnEdge,
|
|
58
|
+
BpmnNode,
|
|
59
|
+
EdgeType,
|
|
60
|
+
ElementSelection,
|
|
61
|
+
EngineId,
|
|
62
|
+
LayoutOptions,
|
|
63
|
+
NodeType,
|
|
64
|
+
ProcessModel,
|
|
65
|
+
} from '@bpmn-nova/studio'
|
|
51
66
|
export {
|
|
52
67
|
createContextMenuRegistry,
|
|
53
68
|
createDefaultContextMenuRegistry,
|
|
@@ -67,6 +82,13 @@ export {
|
|
|
67
82
|
registerBpmnProperties,
|
|
68
83
|
registerFlowableProperties,
|
|
69
84
|
} from '@bpmn-nova/studio'
|
|
85
|
+
export type {
|
|
86
|
+
NodeSubtitleResolver,
|
|
87
|
+
NodeSubtitleResolverContext,
|
|
88
|
+
StudioValidationEvent,
|
|
89
|
+
StudioValidationIssue,
|
|
90
|
+
StudioValidationSource,
|
|
91
|
+
} from '@bpmn-nova/studio'
|
|
70
92
|
|
|
71
93
|
export interface BpmnNovaVisualProps {
|
|
72
94
|
theme?: NovaThemeInput
|
|
@@ -96,11 +118,13 @@ export interface BpmnCanvasProps extends BpmnNovaVisualProps {
|
|
|
96
118
|
iconRegistry?: IconRegistry
|
|
97
119
|
nodeRenderers?: Record<string, Function>
|
|
98
120
|
nodeRenderer?: Function
|
|
121
|
+
nodeSubtitleResolver?: NodeSubtitleResolver
|
|
99
122
|
svgExport?: ViewerOptions['svgExport']
|
|
100
123
|
}
|
|
101
124
|
export interface BpmnCanvasHandle {
|
|
102
125
|
readonly instance: CanvasInstance | null
|
|
103
126
|
fitView(padding?: number, options?: Record<string, unknown>): void
|
|
127
|
+
refreshPresentation(): void
|
|
104
128
|
clientToWorld(x: number, y: number): { x: number; y: number } | undefined
|
|
105
129
|
setTheme(theme: NovaThemeInput): NovaThemeState | undefined
|
|
106
130
|
exportSvg(options?: SvgExportOptions): Promise<SvgExportArtifact> | undefined
|
|
@@ -138,14 +162,16 @@ export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
|
|
|
138
162
|
contextMenuRegistry?: ContextMenuRegistry
|
|
139
163
|
nodeRenderers?: Record<string, Function>
|
|
140
164
|
nodeRenderer?: Function
|
|
165
|
+
nodeSubtitleResolver?: NodeSubtitleResolver
|
|
141
166
|
slots?: StudioShellSlots
|
|
142
167
|
layout?: StudioShellOptions['layout']
|
|
143
168
|
regions?: StudioShellRegions
|
|
144
169
|
headerStart?: BpmnStudioRenderSlot
|
|
170
|
+
headerActions?: BpmnStudioRenderSlot
|
|
145
171
|
header?: BpmnStudioRenderSlot
|
|
146
172
|
runtime?: ProcessInstanceSnapshot | null
|
|
147
|
-
mode?:
|
|
148
|
-
allowedModes?:
|
|
173
|
+
mode?: StudioMode
|
|
174
|
+
allowedModes?: readonly StudioMode[]
|
|
149
175
|
projection?: ViewerProjection
|
|
150
176
|
responsive?: boolean
|
|
151
177
|
projectionOptions?: Array<{ value: ViewerProjection; label: string }>
|
|
@@ -169,18 +195,28 @@ export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
|
|
|
169
195
|
onChange?: (model: ProcessModel, reason: string, xml: string) => void
|
|
170
196
|
onSelectionChange?: (selection: ElementSelection | null, element: StudioState['selectedElement']) => void
|
|
171
197
|
onScopeChange?: (activeScopeId: string, scopePath: StudioState['scopePath'], state: StudioState) => void
|
|
198
|
+
onModeChange?: (event: StudioModeChangeEvent) => void
|
|
199
|
+
onValidation?: (event: StudioValidationEvent) => void
|
|
172
200
|
}
|
|
173
201
|
export interface BpmnStudioRenderContext {
|
|
174
202
|
readonly studio: BpmnStudioController
|
|
175
203
|
readonly shell: BpmnStudioShell
|
|
176
204
|
readonly actions: StudioShellActions
|
|
177
205
|
readonly state: Readonly<StudioState>
|
|
206
|
+
readonly mode: StudioMode
|
|
178
207
|
}
|
|
179
208
|
export type BpmnStudioRenderSlot = ReactNode | ((context: BpmnStudioRenderContext) => ReactNode)
|
|
180
209
|
export interface BpmnStudioHandle {
|
|
181
210
|
readonly studio: BpmnStudioController
|
|
182
211
|
readonly shell: BpmnStudioShell | null
|
|
183
212
|
readonly actions: StudioShellActions | null
|
|
213
|
+
readonly mode: StudioMode | undefined
|
|
214
|
+
getMode(): StudioMode | undefined
|
|
215
|
+
getAllowedModes(): readonly StudioMode[]
|
|
216
|
+
setMode(mode: StudioMode): boolean
|
|
217
|
+
setAllowedModes(modes: readonly StudioMode[]): readonly StudioMode[]
|
|
218
|
+
refreshPresentation(): void
|
|
219
|
+
validate(): StudioValidationIssue[]
|
|
184
220
|
exportXml(engine?: EngineId): string
|
|
185
221
|
fitView(): void
|
|
186
222
|
setTheme(theme: NovaThemeInput): NovaThemeState | undefined
|
|
@@ -212,6 +248,7 @@ export interface BpmnViewerProps extends BpmnNovaVisualProps, BpmnModelProps, Om
|
|
|
212
248
|
export interface BpmnViewerHandle {
|
|
213
249
|
readonly instance: ViewerInstance | null
|
|
214
250
|
fitView(): void
|
|
251
|
+
refreshPresentation(): void
|
|
215
252
|
setProjection(projection: ViewerProjection): void
|
|
216
253
|
setRuntime(runtime: ProcessInstanceSnapshot | null): void
|
|
217
254
|
setDisplayOptions(options: Parameters<ViewerInstance['setDisplayOptions']>[0]): void
|
package/dist/index.js
CHANGED
|
@@ -62,9 +62,13 @@ export function useBpmnStudio(options = {}) {
|
|
|
62
62
|
export const BpmnCanvas = forwardRef(function BpmnCanvas(props, ref) {
|
|
63
63
|
const hostRef = useRef(null);
|
|
64
64
|
const instanceRef = useRef(null);
|
|
65
|
+
const subtitleResolverRef = useRef(props.nodeSubtitleResolver);
|
|
66
|
+
subtitleResolverRef.current = props.nodeSubtitleResolver;
|
|
67
|
+
const stableSubtitleResolverRef = useRef((context) => subtitleResolverRef.current?.(context));
|
|
65
68
|
useImperativeHandle(ref, () => ({
|
|
66
69
|
get instance() { return instanceRef.current; },
|
|
67
70
|
fitView(padding, options) { instanceRef.current?.fitView(padding, options); },
|
|
71
|
+
refreshPresentation() { instanceRef.current?.refreshPresentation(); },
|
|
68
72
|
clientToWorld(x, y) { return instanceRef.current?.clientToWorld(x, y); },
|
|
69
73
|
setTheme(theme) { return instanceRef.current?.setTheme(theme); },
|
|
70
74
|
exportSvg(options) { return instanceRef.current?.exportSvg(options); },
|
|
@@ -73,12 +77,13 @@ export const BpmnCanvas = forwardRef(function BpmnCanvas(props, ref) {
|
|
|
73
77
|
useEffect(() => {
|
|
74
78
|
if (!hostRef.current || !props.studio) return undefined;
|
|
75
79
|
const interactions = props.interactions || createInteractionController({ studio: props.studio, templates: props.templates || createTemplateRegistry() });
|
|
76
|
-
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 } });
|
|
80
|
+
const instance = new CoreCanvas({ container: hostRef.current, studio: props.studio, interactions, theme: props.theme, onThemeChange: props.onThemeChange, nodeSubtitleResolver: stableSubtitleResolverRef.current, rendererOptions: { iconRegistry: props.iconRegistry, nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, svgExport: props.svgExport } });
|
|
77
81
|
instanceRef.current = instance;
|
|
78
82
|
requestAnimationFrame(() => instance.fitView());
|
|
79
83
|
return () => { instance.destroy(); instanceRef.current = null; };
|
|
80
84
|
}, [props.studio]);
|
|
81
85
|
useEffect(() => { if (props.theme !== undefined) instanceRef.current?.setTheme(props.theme); }, [props.theme]);
|
|
86
|
+
useEffect(() => { instanceRef.current?.refreshPresentation(); }, [props.nodeSubtitleResolver]);
|
|
82
87
|
return React.createElement('div', { ref: hostRef, className: props.className, style: { width: '100%', height: '100%', minHeight: 320, ...props.style } });
|
|
83
88
|
});
|
|
84
89
|
|
|
@@ -105,15 +110,27 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
|
|
|
105
110
|
const mountedModelSyncRef = useRef(false);
|
|
106
111
|
const [headerPortal, setHeaderPortal] = useState(null);
|
|
107
112
|
const [headerStartPortal, setHeaderStartPortal] = useState(null);
|
|
113
|
+
const [headerActionsPortal, setHeaderActionsPortal] = useState(null);
|
|
108
114
|
const callbacksRef = useRef({});
|
|
109
115
|
callbacksRef.current = props;
|
|
110
116
|
if (!props.studio && !ownedStudioRef.current) ownedStudioRef.current = createStudioController({ model: resolveModel(props), propertiesProfile: props.propertiesProfile, allowedNodeTypes: props.allowedNodeTypes, allowedEdgeTypes: props.allowedEdgeTypes });
|
|
111
117
|
const studio = props.studio || ownedStudioRef.current;
|
|
112
118
|
const [studioState, setStudioState] = useState(() => studio.getState());
|
|
119
|
+
const [actualMode, setActualMode] = useState(() => props.mode || props.allowedModes?.[0] || 'design');
|
|
120
|
+
const subtitleResolverRef = useRef(props.nodeSubtitleResolver);
|
|
121
|
+
subtitleResolverRef.current = props.nodeSubtitleResolver;
|
|
122
|
+
const stableSubtitleResolverRef = useRef((context) => subtitleResolverRef.current?.(context));
|
|
113
123
|
useImperativeHandle(ref, () => ({
|
|
114
124
|
studio,
|
|
115
125
|
get shell() { return shellRef.current; },
|
|
116
126
|
get actions() { return shellRef.current?.actions || null; },
|
|
127
|
+
get mode() { return shellRef.current?.getMode(); },
|
|
128
|
+
getMode: () => shellRef.current?.getMode(),
|
|
129
|
+
getAllowedModes: () => shellRef.current?.getAllowedModes() || [],
|
|
130
|
+
setMode: (mode) => shellRef.current?.setMode(mode) || false,
|
|
131
|
+
setAllowedModes: (modes) => shellRef.current?.setAllowedModes(modes) || [],
|
|
132
|
+
refreshPresentation: () => shellRef.current?.refreshPresentation(),
|
|
133
|
+
validate: () => shellRef.current?.validate() || [],
|
|
117
134
|
exportXml: (engine) => studio.exportXml(engine),
|
|
118
135
|
exportSvg: (options) => shellRef.current?.exportSvg(options),
|
|
119
136
|
openSvgExportPreview: (options) => shellRef.current?.openSvgExportPreview(options),
|
|
@@ -122,6 +139,7 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
|
|
|
122
139
|
}), [studio]);
|
|
123
140
|
const hasNativeHeader = props.header !== undefined && props.header !== null;
|
|
124
141
|
const hasNativeHeaderStart = props.headerStart !== undefined && props.headerStart !== null;
|
|
142
|
+
const hasNativeHeaderActions = props.headerActions !== undefined && props.headerActions !== null;
|
|
125
143
|
useEffect(() => {
|
|
126
144
|
if (!hostRef.current) return undefined;
|
|
127
145
|
setStudioState(studio.getState());
|
|
@@ -132,7 +150,10 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
|
|
|
132
150
|
};
|
|
133
151
|
const shellSlots = { ...(props.slots || {}) };
|
|
134
152
|
if (hasNativeHeader) shellSlots.header = createPortalSlot(setHeaderPortal);
|
|
135
|
-
else if (!shellSlots.header
|
|
153
|
+
else if (!shellSlots.header) {
|
|
154
|
+
if (hasNativeHeaderStart) shellSlots.headerStart = createPortalSlot(setHeaderStartPortal);
|
|
155
|
+
if (hasNativeHeaderActions) shellSlots.headerActions = createPortalSlot(setHeaderActionsPortal);
|
|
156
|
+
}
|
|
136
157
|
const shell = new CoreStudioShell({
|
|
137
158
|
container: hostRef.current,
|
|
138
159
|
studio,
|
|
@@ -144,6 +165,7 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
|
|
|
144
165
|
rendererOptions: {
|
|
145
166
|
nodeRenderers: props.nodeRenderers,
|
|
146
167
|
nodeRenderer: props.nodeRenderer,
|
|
168
|
+
nodeSubtitleResolver: stableSubtitleResolverRef.current,
|
|
147
169
|
runtimePresenter: props.runtimePresenter,
|
|
148
170
|
runtimeTraceProjector: props.runtimeTraceProjector,
|
|
149
171
|
runtimeAssetResolver: props.runtimeAssetResolver,
|
|
@@ -176,14 +198,20 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
|
|
|
176
198
|
onThemeChange: (state) => callbacksRef.current.onThemeChange?.(state),
|
|
177
199
|
});
|
|
178
200
|
shellRef.current = shell;
|
|
201
|
+
setActualMode(shell.getMode());
|
|
202
|
+
const offMode = shell.subscribeMode((event) => {
|
|
203
|
+
setActualMode(event.mode);
|
|
204
|
+
callbacksRef.current.onModeChange?.(event);
|
|
205
|
+
});
|
|
206
|
+
const offValidation = shell.subscribeValidation((event) => callbacksRef.current.onValidation?.(event));
|
|
179
207
|
const off = studio.subscribe((event) => {
|
|
180
208
|
setStudioState(event.state);
|
|
181
209
|
if (event.type === 'modelChanged') callbacksRef.current.onChange?.(studio.model, event.reason, studio.exportXml());
|
|
182
210
|
if (event.type === 'selectionChanged') callbacksRef.current.onSelectionChange?.(studio.selection, studio.getSelectedElement());
|
|
183
211
|
if (event.type === 'scopeChanged') callbacksRef.current.onScopeChange?.(event.activeScopeId, event.scopePath, studio.getState());
|
|
184
212
|
});
|
|
185
|
-
return () => { off(); shell.destroy(); shellRef.current = null; };
|
|
186
|
-
}, [studio, hasNativeHeader, hasNativeHeaderStart]);
|
|
213
|
+
return () => { offMode(); offValidation(); off(); shell.destroy(); shellRef.current = null; };
|
|
214
|
+
}, [studio, hasNativeHeader, hasNativeHeaderStart, hasNativeHeaderActions]);
|
|
187
215
|
useEffect(() => () => {
|
|
188
216
|
if (!props.studio && ownedStudioRef.current === studio) {
|
|
189
217
|
studio.destroy();
|
|
@@ -199,11 +227,17 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
|
|
|
199
227
|
else if (props.xml && props.xml !== studio.exportXml(props.engine)) studio.importXml(props.xml, props.engine);
|
|
200
228
|
}, [studio, props.model, props.xml, props.engine]);
|
|
201
229
|
useEffect(() => { if (props.runtime !== undefined) shellRef.current?.setRuntime(props.runtime); }, [props.runtime]);
|
|
202
|
-
useEffect(() => {
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
const shell = shellRef.current;
|
|
232
|
+
if (!shell) return;
|
|
233
|
+
if (props.allowedModes !== undefined) shell.setAllowedModes(props.allowedModes);
|
|
234
|
+
if (props.mode) shell.setMode(props.mode);
|
|
235
|
+
}, [props.allowedModes, props.mode]);
|
|
203
236
|
useEffect(() => { if (props.projection) shellRef.current?.setProjection(props.projection); }, [props.projection]);
|
|
204
237
|
useEffect(() => { if (props.regions !== undefined) shellRef.current?.setRegions(props.regions); }, [props.regions]);
|
|
205
238
|
useEffect(() => { if (props.theme !== undefined) shellRef.current?.setTheme(props.theme); }, [props.theme]);
|
|
206
239
|
useEffect(() => { if (props.runtimeAppearance !== undefined) shellRef.current?.setRuntimeAppearance(props.runtimeAppearance); }, [props.runtimeAppearance]);
|
|
240
|
+
useEffect(() => { shellRef.current?.refreshPresentation(); }, [props.nodeSubtitleResolver]);
|
|
207
241
|
useEffect(() => {
|
|
208
242
|
const shell = shellRef.current;
|
|
209
243
|
if (!shell) return;
|
|
@@ -219,6 +253,7 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
|
|
|
219
253
|
shell: portal.context.shell,
|
|
220
254
|
actions: portal.context.actions,
|
|
221
255
|
state: studioState,
|
|
256
|
+
mode: actualMode,
|
|
222
257
|
};
|
|
223
258
|
return createPortal(typeof slot === 'function' ? slot(context) : slot, portal.target, key);
|
|
224
259
|
};
|
|
@@ -226,6 +261,7 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
|
|
|
226
261
|
React.createElement('div', { ref: hostRef, className: props.className, style: { width: '100%', height: '100%', minHeight: 520, ...props.style } }),
|
|
227
262
|
renderSlot(props.header, headerPortal, 'bpmn-nova-header'),
|
|
228
263
|
renderSlot(props.headerStart, headerStartPortal, 'bpmn-nova-header-start'),
|
|
264
|
+
renderSlot(props.headerActions, headerActionsPortal, 'bpmn-nova-header-actions'),
|
|
229
265
|
);
|
|
230
266
|
});
|
|
231
267
|
|
|
@@ -290,6 +326,9 @@ export const BpmnViewer = forwardRef(function BpmnViewer(props, ref) {
|
|
|
290
326
|
const hostRef = useRef(null);
|
|
291
327
|
const instanceRef = useRef(null);
|
|
292
328
|
const callbacksRef = useRef({});
|
|
329
|
+
const subtitleResolverRef = useRef(props.nodeSubtitleResolver);
|
|
330
|
+
subtitleResolverRef.current = props.nodeSubtitleResolver;
|
|
331
|
+
const stableSubtitleResolverRef = useRef((context) => subtitleResolverRef.current?.(context));
|
|
293
332
|
callbacksRef.current = {
|
|
294
333
|
onTraceClick: props.onTraceClick,
|
|
295
334
|
onElementClick: props.onElementClick,
|
|
@@ -303,6 +342,7 @@ export const BpmnViewer = forwardRef(function BpmnViewer(props, ref) {
|
|
|
303
342
|
useImperativeHandle(ref, () => ({
|
|
304
343
|
get instance() { return instanceRef.current; },
|
|
305
344
|
fitView() { instanceRef.current?.fitView(); },
|
|
345
|
+
refreshPresentation() { instanceRef.current?.refreshPresentation(); },
|
|
306
346
|
setProjection(value) { instanceRef.current?.setProjection(value); },
|
|
307
347
|
setRuntime(value) { instanceRef.current?.setRuntime(value); },
|
|
308
348
|
setDisplayOptions(value) { instanceRef.current?.setDisplayOptions(value); },
|
|
@@ -325,6 +365,7 @@ export const BpmnViewer = forwardRef(function BpmnViewer(props, ref) {
|
|
|
325
365
|
iconRegistry: props.iconRegistry,
|
|
326
366
|
nodeRenderers: props.nodeRenderers,
|
|
327
367
|
nodeRenderer: props.nodeRenderer,
|
|
368
|
+
nodeSubtitleResolver: stableSubtitleResolverRef.current,
|
|
328
369
|
runtimePresenter: props.runtimePresenter,
|
|
329
370
|
runtimeTraceProjector: props.runtimeTraceProjector,
|
|
330
371
|
runtimeAssetResolver: props.runtimeAssetResolver,
|
|
@@ -354,6 +395,7 @@ export const BpmnViewer = forwardRef(function BpmnViewer(props, ref) {
|
|
|
354
395
|
useEffect(() => { if (instanceRef.current && props.projection) instanceRef.current.setProjection(props.projection); }, [props.projection]);
|
|
355
396
|
useEffect(() => { if (props.theme !== undefined) instanceRef.current?.setTheme(props.theme); }, [props.theme]);
|
|
356
397
|
useEffect(() => { if (props.runtimeAppearance !== undefined) instanceRef.current?.setRuntimeAppearance(props.runtimeAppearance); }, [props.runtimeAppearance]);
|
|
398
|
+
useEffect(() => { instanceRef.current?.refreshPresentation(); }, [props.nodeSubtitleResolver]);
|
|
357
399
|
useEffect(() => { instanceRef.current?.setDisplayOptions({ timeline: props.timeline, runtimeDetails: props.runtimeDetails, runtimeTraceOptions: props.runtimeTraceOptions, runtimeAssetResolver: props.runtimeAssetResolver }); }, [props.timeline, props.runtimeDetails, props.runtimeTraceOptions, props.runtimeAssetResolver]);
|
|
358
400
|
useEffect(() => {
|
|
359
401
|
if (!instanceRef.current) return;
|
package/dist/styles.css
CHANGED
|
@@ -511,8 +511,8 @@
|
|
|
511
511
|
.mb-node-event,
|
|
512
512
|
.mb-node-boundary { display: grid; place-items: center; z-index: 7; }
|
|
513
513
|
.mb-event-shape {
|
|
514
|
-
width: 46px;
|
|
515
|
-
height: 46px;
|
|
514
|
+
width: var(--mb-event-shape-size, 46px);
|
|
515
|
+
height: var(--mb-event-shape-size, 46px);
|
|
516
516
|
box-sizing: border-box;
|
|
517
517
|
display: grid;
|
|
518
518
|
place-items: center;
|
|
@@ -523,7 +523,7 @@
|
|
|
523
523
|
box-shadow: var(--nova-shadow-sm);
|
|
524
524
|
position: relative;
|
|
525
525
|
}
|
|
526
|
-
.mb-node-boundary .mb-event-shape { width: 40px; height: 40px; border-width: 2px; }
|
|
526
|
+
.mb-node-boundary .mb-event-shape { width: var(--mb-event-shape-size, 40px); height: var(--mb-event-shape-size, 40px); border-width: 2px; }
|
|
527
527
|
.mb-event-shape[data-stage="intermediate"],
|
|
528
528
|
.mb-node-boundary .mb-event-shape { box-shadow: inset 0 0 0 3px var(--nova-color-surface), inset 0 0 0 4.5px var(--nova-color-text-muted, #8796a9), var(--nova-shadow-sm); }
|
|
529
529
|
.mb-event-shape[data-stage="end"] { border-width: 4px; border-color: var(--nova-color-text-secondary); }
|
|
@@ -1159,6 +1159,7 @@ textarea.property-control { resize: vertical; }
|
|
|
1159
1159
|
.nova-studio-projection-switch button:hover { color: var(--nova-tone-primary-foreground, #4d59ca); background: var(--nova-color-primary-soft, #f4f5ff); }.nova-studio-projection-switch button.is-active { color: var(--nova-tone-primary-foreground, #4654c8); background: var(--nova-color-primary-soft, #eef0ff); font-weight: var(--nova-font-weight-semibold); }
|
|
1160
1160
|
.nova-studio-tools { min-width: 0; overflow: visible; display: flex; align-items: center; justify-content: flex-end; gap: 5px; }
|
|
1161
1161
|
.nova-studio-tools::-webkit-scrollbar { display: none; }
|
|
1162
|
+
.nova-studio-header-actions { min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 5px; }
|
|
1162
1163
|
.nova-studio-tool-divider { flex: none; width: 1px; height: 20px; margin: 0 2px; background: var(--nova-color-divider, #e4e8f0); }
|
|
1163
1164
|
.nova-studio-tool { flex: none; min-height: 32px; border: 1px solid var(--nova-color-border, #e2e6ef); border-radius: 7px; padding: 0 10px; display: inline-flex; align-items: center; justify-content: center; gap: 5px; color: var(--nova-color-text-secondary, #4c586d); background: var(--nova-color-surface, #fff); font-size: var(--nova-font-size-sm); line-height: 18px; white-space: nowrap; cursor: pointer; }
|
|
1164
1165
|
.nova-studio-tool:focus { outline: none; }.nova-studio-tool:focus-visible { outline: 2px solid var(--nova-color-focus-ring, #aeb7ff); outline-offset: 1px; }
|