@bpmn-nova/vue 0.3.1-preview → 0.3.3-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 CHANGED
@@ -1,10 +1,10 @@
1
1
  # @bpmn-nova/vue
2
2
 
3
- BPMN Nova 的 Vue 3.3+ Adapter,提供 Studio、Designer、Viewer、审批轨迹、Properties 和 Expose 实例方法。
3
+ BPMN Nova 的 Vue 3.3+ Adapter,提供 Studio、Designer、Viewer、审批轨迹、Palette、Properties 和 Expose 实例方法。
4
4
 
5
5
  > **English summary:** Vue 3.3+ components and exposed instance methods for BPMN Nova process design, viewing, approval traces, properties, themes, and SVG export.
6
6
 
7
- > 当前版本为 `0.3.1-preview`。Vue 由宿主工程提供,本包不会替应用选择或升级框架版本。
7
+ > 当前版本为 `0.3.3-preview`。Vue 由宿主工程提供,本包不会替应用选择或升级框架版本。
8
8
 
9
9
  ![BPMN Nova Vue 流程工作台](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/docs/assets/bpmn-nova-designer.jpg)
10
10
 
@@ -20,6 +20,8 @@ import '@bpmn-nova/vue/styles.css'
20
20
 
21
21
  只安装本包;`@bpmn-nova/studio` 会作为内部依赖自动解析。容器必须有明确高度。
22
22
 
23
+ 使用代码生成助手或 IDE Agent 接入时,从随包的 `llms.txt` 开始;需要完整接口和定制资料时再读取 `llms-full.txt`。两份文件都包含在 npm tarball 中,不依赖源码仓库可见性。
24
+
23
25
  ## 流程设计
24
26
 
25
27
  ```vue
@@ -28,7 +30,7 @@ import { ref } from 'vue'
28
30
  import { BpmnStudio } from '@bpmn-nova/vue'
29
31
  import '@bpmn-nova/vue/styles.css'
30
32
 
31
- defineProps({ xml: String })
33
+ const props = defineProps({ initialXml: String })
32
34
  const studioRef = ref(null)
33
35
 
34
36
  function handleChange(model, reason, nextXml) {
@@ -40,17 +42,126 @@ function handleChange(model, reason, nextXml) {
40
42
  <div style="height: 720px">
41
43
  <BpmnStudio
42
44
  ref="studioRef"
43
- :xml="xml"
45
+ :xml="props.initialXml"
44
46
  engine="flowable"
45
47
  mode="design"
48
+ :allowed-modes="['design']"
49
+ theme="auto"
50
+ @change="handleChange"
51
+ />
52
+ </div>
53
+ </template>
54
+ ```
55
+
56
+ Expose 提供 `getActions()`、`validate()`、`exportXml()`、`fitView()`、`setTheme()`、`exportSvg()` 和 `openSvgExportPreview()`。
57
+
58
+ `xml`/`model` 是外部替换输入,`change` 的 XML 是草稿输出。宿主回写完全相同的导出 XML 不会重新导入或清空撤销历史;切换服务器 revision 或流程时传入不同 XML 会替换模型。Activiti 宿主应显式使用 `engine="activiti"`。
59
+
60
+ ## 组合宿主头部与业务属性面板
61
+
62
+ 默认 `BpmnStudio` 是包含 Header、Palette、Canvas、Properties 和 Statusbar 的完整工作台。下面的接入显式隐藏 Nova Properties,保留 Nova Palette,并只替换 Header 左侧品牌区:
63
+
64
+ ```vue
65
+ <script setup>
66
+ import { ref } from 'vue'
67
+ import { BpmnStudio } from '@bpmn-nova/vue'
68
+ import '@bpmn-nova/vue/styles.css'
69
+
70
+ const studioRef = ref(null)
71
+ const selectedElement = ref(null)
72
+ const supportedNodeTypes = ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent']
73
+
74
+ function handleSelectionChange(selection, element) {
75
+ selectedElement.value = selection ? element : null
76
+ }
77
+ </script>
78
+
79
+ <template>
80
+ <div class="process-design-workbench">
81
+ <BpmnStudio
82
+ ref="studioRef"
83
+ :xml="xml"
84
+ engine="activiti"
85
+ mode="design"
86
+ :allowed-modes="['design']"
87
+ :allowed-node-types="supportedNodeTypes"
88
+ :allowed-edge-types="['sequenceFlow']"
89
+ :regions="{ right: 'hidden' }"
46
90
  theme="auto"
47
91
  @change="handleChange"
92
+ @selection-change="handleSelectionChange"
93
+ @validation="handleValidation"
94
+ >
95
+ <template #header-start="{ state, actions }">
96
+ <button type="button" @click="back">← 返回审批列表</button>
97
+ <ApprovalIcon :icon="icon" />
98
+ <strong>{{ processName }}</strong>
99
+ <span>{{ processType }}</span>
100
+ </template>
101
+ <template #header-actions="{ actions, mode }">
102
+ <button type="button" :disabled="mode !== 'design'" @click="actions.validate()">校验</button>
103
+ <button type="button" :disabled="mode !== 'design'" @click="saveDraft(actions.exportXml())">保存</button>
104
+ <button type="button" :disabled="mode !== 'design'" @click="publishProcess(actions)">发布</button>
105
+ </template>
106
+ </BpmnStudio>
107
+
108
+ <DxProcessProperties
109
+ v-if="selectedElement"
110
+ :element-id="selectedElement.id"
48
111
  />
49
112
  </div>
50
113
  </template>
51
114
  ```
52
115
 
53
- Expose 提供 `exportXml()`、`fitView()`、`setTheme()`、`exportSvg()` `openSvgExportPreview()`。
116
+ `header-start` `header-actions` 使用 Vue Teleport 在当前应用内渲染,因此保留 provide/inject、响应式状态与组件生命周期。`header-actions` 只替换默认“校验 / 导入 / 导出”动作组,完整 `header` 的优先级更高。默认 Header 的最佳视图只保留在底部缩放区;若整个 Header 都不需要,配置 `regions.header = 'hidden'`。
117
+
118
+ `actions.validate()` 以 `toolbar` 来源先更新 Nova 默认状态栏,再发送 `validation` 事件并返回 issues;Expose `validate()` 使用 `api` 来源。事件的 `valid` 仅在存在 error 时为 `false`,warning 不默认阻止发布。保存与发布仍由宿主负责。
119
+
120
+ ```js
121
+ async function publishProcess(actions) {
122
+ const issues = actions.validate()
123
+ if (issues.some((issue) => issue.level === 'error')) return
124
+ await publishXml(actions.exportXml())
125
+ }
126
+ ```
127
+
128
+ 编辑器属性面板应订阅 `selection-change`,该事件覆盖节点、连线、多选、清空和键盘选择。`element-click` 是点击观察事件,不能代替选择状态。宿主使用稳定 BPMN Element ID 关联业务配置,并自行拥有保存、发布、权限和服务端事务。
129
+
130
+ ## Mode 双向同步与副标题刷新
131
+
132
+ ```vue
133
+ <script setup>
134
+ import { ref } from 'vue'
135
+
136
+ const studioRef = ref(null)
137
+ const mode = ref('design')
138
+ const summaries = new Map()
139
+ const resolveSubtitle = ({ node }) => summaries.has(node.id)
140
+ ? summaries.get(node.id)
141
+ : undefined
142
+
143
+ function updateSummary() {
144
+ summaries.set('ServiceTask_Archive', '归档到采购系统')
145
+ studioRef.value?.refreshPresentation()
146
+ }
147
+ </script>
148
+
149
+ <template>
150
+ <BpmnStudio
151
+ ref="studioRef"
152
+ v-model:mode="mode"
153
+ :allowed-modes="['design', 'viewer']"
154
+ :node-subtitle-resolver="resolveSubtitle"
155
+ @mode-change="event => console.log(event.source)"
156
+ >
157
+ <template #header-start="{ mode: actualMode }">
158
+ <HostHeader :mode="actualMode" />
159
+ </template>
160
+ </BpmnStudio>
161
+ </template>
162
+ ```
163
+
164
+ Expose 的 `getMode()` / `setMode()` 使用 Shell 实际状态。Resolver 返回 `undefined` 保留默认值、`null` 移除副标题行、字符串替换显示;函数引用变化会自动刷新,稳定闭包内部数据变化需显式调用 `refreshPresentation()`。Resolver 只影响 Design/Viewer 标准卡片和 SVG,不覆盖 Instance Runtime Presentation,也不修改 XML、History、Selection 或 Viewport。
54
165
 
55
166
  ## 只读展示与审批轨迹
56
167
 
@@ -164,12 +275,16 @@ studioRef.value?.openSvgExportPreview({
164
275
 
165
276
  本包还导出:
166
277
 
167
- - `BpmnDesigner`、`BpmnCanvas`、`BpmnPropertiesPanel`
278
+ - `BpmnDesigner`、`BpmnCanvas`、`BpmnPalettePanel`、`BpmnPropertiesPanel`
168
279
  - `useBpmnStudio`
169
280
  - `createVuePropertyComponent`
170
281
  - `createVueRuntimeDetailsComponent`
171
282
  - `createVueRuntimeTransitionDetailsComponent`
172
283
  - `createVueRuntimeTimelineComponent`
284
+ - `createStudioController`、`createEmptyProcess`、`importBpmn`、`exportBpmn`
285
+ - Palette、Properties、Context Menu、Icon 和 Template Registry 的公开创建函数
286
+
287
+ 完整工作台可通过 `allowedNodeTypes` 与 `allowedEdgeTypes` 限制宿主支持的图元类型,通过 `allowedModes` 限制可切换的工作台模式。图元限制同时作用于 XML 导入、Palette、连接、快捷新增、模板和节点类型转换。
173
288
 
174
289
  ## 常见问题
175
290
 
@@ -181,11 +296,11 @@ studioRef.value?.openSvgExportPreview({
181
296
 
182
297
  ## 文档与 AI
183
298
 
299
+ - npm 包内 `llms.txt`:可执行安装入口
300
+ - npm 包内 `llms-full.txt`:完整 AI 上下文
184
301
  - [完整项目能力](https://github.com/daxiangme/bpmn-nova)
185
302
  - [React/Vue 组件说明](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/COMPONENTS.md)
186
303
  - [公开 Interface](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/API.md)
187
- - [AI 接入入口](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/llms.txt)
188
- - [AI 完整上下文](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/llms-full.txt)
189
304
 
190
305
  ## License
191
306
 
package/dist/index.d.ts CHANGED
@@ -1,8 +1,14 @@
1
- import type { Component, DefineComponent } from 'vue'
2
- import type { BpmnEdge, BpmnNode, ElementSelection, EngineId, LayoutOptions, ProcessModel } from '@bpmn-nova/studio'
1
+ import type { Component, DefineComponent, VNodeChild } from 'vue'
2
+ import type { BpmnEdge, BpmnNode, EdgeType, ElementSelection, EngineId, LayoutOptions, NodeType, ProcessModel } from '@bpmn-nova/studio'
3
3
  import type { BpmnDesigner as DesignerInstance } from '@bpmn-nova/studio/designer'
4
4
  import type { IconRegistry } from '@bpmn-nova/studio'
5
- import type { PaletteRegistry } from '@bpmn-nova/studio'
5
+ import type {
6
+ PaletteItemRenderer,
7
+ PalettePanel as PalettePanelInstance,
8
+ PaletteProvider,
9
+ PaletteRegistry,
10
+ PaletteSectionRenderer,
11
+ } from '@bpmn-nova/studio'
6
12
  import type { PropertiesContext, PropertiesProvider, PropertiesRegistry, PropertyEntry } from '@bpmn-nova/studio'
7
13
  import type { ProcessInstanceSnapshot } from '@bpmn-nova/studio/runtime'
8
14
  import type { SvgExportArtifact, SvgExportOptions, SvgExportPreviewController } from '@bpmn-nova/studio/export-svg'
@@ -10,9 +16,17 @@ import type {
10
16
  BpmnCanvas as CanvasInstance,
11
17
  BpmnStudioController,
12
18
  BpmnStudioShell,
19
+ ContextMenuRegistry,
13
20
  InteractionController,
21
+ NodeSubtitleResolver,
14
22
  StudioCommands,
23
+ StudioMode,
24
+ StudioModeChangeEvent,
25
+ StudioValidationEvent,
26
+ StudioValidationIssue,
27
+ StudioShellActions,
15
28
  StudioShellOptions,
29
+ StudioShellRegions,
16
30
  StudioShellSlots,
17
31
  StudioState,
18
32
  TemplateRegistry,
@@ -28,13 +42,41 @@ import type {
28
42
  ViewerProjection,
29
43
  } from '@bpmn-nova/studio/viewer'
30
44
 
45
+ export type {
46
+ NodeSubtitleResolver,
47
+ NodeSubtitleResolverContext,
48
+ StudioValidationEvent,
49
+ StudioValidationIssue,
50
+ StudioValidationSource,
51
+ } from '@bpmn-nova/studio'
52
+
53
+ export {
54
+ createContextMenuRegistry,
55
+ createDefaultContextMenuRegistry,
56
+ createDefaultIconRegistry,
57
+ createDefaultPaletteRegistry,
58
+ createDefaultPropertiesRegistry,
59
+ createEmptyProcess,
60
+ createPaletteRegistry,
61
+ createPropertiesRegistry,
62
+ createStudioController,
63
+ createTemplateRegistry,
64
+ exportBpmn,
65
+ importBpmn,
66
+ propertyEntry,
67
+ propertyGroup,
68
+ registerActivitiProperties,
69
+ registerBpmnProperties,
70
+ registerFlowableProperties,
71
+ } from '@bpmn-nova/studio'
72
+
31
73
  export interface BpmnNovaVisualProps {
32
74
  theme?: NovaThemeInput
33
75
  runtimeAppearance?: RuntimeAppearanceOptions
34
76
  onThemeChange?: (state: NovaThemeState) => void
35
77
  }
36
78
  export interface BpmnModelProps { model?: ProcessModel; xml?: string; engine?: EngineId }
37
- export interface UseBpmnStudioOptions extends BpmnModelProps { studio?: BpmnStudioController; propertiesProfile?: 'business' | 'developer' }
79
+ export interface UseBpmnStudioOptions extends BpmnModelProps { studio?: BpmnStudioController; propertiesProfile?: 'business' | 'developer'; allowedNodeTypes?: NodeType[]; allowedEdgeTypes?: EdgeType[] }
38
80
  export interface UseBpmnStudioResult { studio: BpmnStudioController; state: StudioState; commands: StudioCommands }
39
81
  export function useBpmnStudio(options?: UseBpmnStudioOptions): UseBpmnStudioResult
40
82
 
@@ -45,28 +87,53 @@ export interface BpmnCanvasProps extends BpmnNovaVisualProps {
45
87
  iconRegistry?: IconRegistry
46
88
  nodeRenderers?: Record<string, Function>
47
89
  nodeRenderer?: Function
90
+ nodeSubtitleResolver?: NodeSubtitleResolver
48
91
  svgExport?: ViewerOptions['svgExport']
49
92
  }
50
93
  export interface BpmnCanvasExposed {
51
94
  getCanvas(): CanvasInstance | null
52
95
  fitView(): void
96
+ refreshPresentation(): void
53
97
  setTheme(theme: NovaThemeInput): NovaThemeState | undefined
54
98
  exportSvg(options?: SvgExportOptions): Promise<SvgExportArtifact> | undefined
55
99
  openSvgExportPreview(options?: SvgExportOptions & { previewTitle?: string; onDownload?: (artifact: SvgExportArtifact) => void }): SvgExportPreviewController | undefined
56
100
  }
57
101
  export const BpmnCanvas: DefineComponent<BpmnCanvasProps>
58
102
 
103
+ export interface BpmnPalettePanelProps extends BpmnNovaVisualProps {
104
+ studio: BpmnStudioController
105
+ interactions?: InteractionController
106
+ templates?: TemplateRegistry
107
+ registry?: PaletteRegistry
108
+ providers?: PaletteProvider[]
109
+ canvas?: CanvasInstance | null
110
+ iconRegistry?: IconRegistry
111
+ renderItem?: PaletteItemRenderer
112
+ renderSection?: PaletteSectionRenderer
113
+ }
114
+ export interface BpmnPalettePanelExposed {
115
+ getPanel(): PalettePanelInstance | null
116
+ render(): void
117
+ setTheme(theme: NovaThemeInput): NovaThemeState | undefined
118
+ }
119
+ export const BpmnPalettePanel: DefineComponent<BpmnPalettePanelProps>
120
+
59
121
  export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
60
122
  studio?: BpmnStudioController
61
123
  propertiesProfile?: 'business' | 'developer'
124
+ allowedNodeTypes?: NodeType[]
125
+ allowedEdgeTypes?: EdgeType[]
62
126
  iconRegistry?: IconRegistry
63
127
  paletteRegistry?: PaletteRegistry
64
128
  propertiesRegistry?: PropertiesRegistry
65
129
  templateRegistry?: TemplateRegistry
130
+ contextMenuRegistry?: ContextMenuRegistry
66
131
  nodeRenderers?: Record<string, Function>
67
132
  nodeRenderer?: Function
133
+ nodeSubtitleResolver?: NodeSubtitleResolver
68
134
  slotsConfig?: StudioShellSlots
69
135
  layout?: StudioShellOptions['layout']
136
+ regions?: StudioShellRegions
70
137
  leftWidth?: number
71
138
  rightWidth?: number
72
139
  runtime?: ProcessInstanceSnapshot | null
@@ -85,7 +152,8 @@ export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
85
152
  runtimeTraceOptions?: ViewerOptions['runtimeTraceOptions']
86
153
  onTraceClick?: (event: RuntimeTraceClickEvent) => void
87
154
  onElementClick?: ViewerOptions['onElementClick']
88
- mode?: 'design' | 'viewer' | 'instance'
155
+ mode?: StudioMode
156
+ allowedModes?: readonly StudioMode[]
89
157
  projection?: ViewerProjection
90
158
  responsive?: boolean
91
159
  projectionOptions?: Array<{ value: ViewerProjection; label: string }>
@@ -93,13 +161,34 @@ export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
93
161
  export interface BpmnStudioExposed {
94
162
  getStudio(): BpmnStudioController
95
163
  getShell(): BpmnStudioShell | null
164
+ getActions(): StudioShellActions | null
165
+ getMode(): StudioMode | undefined
166
+ getAllowedModes(): readonly StudioMode[]
167
+ setMode(mode: StudioMode): boolean
168
+ setAllowedModes(modes: readonly StudioMode[]): readonly StudioMode[]
169
+ refreshPresentation(): void
170
+ validate(): StudioValidationIssue[]
96
171
  exportXml(engine?: EngineId): string
97
172
  fitView(): void
98
173
  setTheme(theme: NovaThemeInput): NovaThemeState | undefined
99
174
  exportSvg(options?: SvgExportOptions): Promise<SvgExportArtifact> | undefined
100
175
  openSvgExportPreview(options?: SvgExportOptions & { previewTitle?: string; onDownload?: (artifact: SvgExportArtifact) => void }): SvgExportPreviewController | null | undefined
101
176
  }
102
- export const BpmnStudio: DefineComponent<BpmnStudioProps>
177
+ export interface BpmnStudioSlotContext {
178
+ readonly studio: BpmnStudioController
179
+ readonly shell: BpmnStudioShell
180
+ readonly actions: StudioShellActions
181
+ readonly state: Readonly<StudioState>
182
+ readonly mode: StudioMode
183
+ }
184
+ export interface BpmnStudioSlots {
185
+ 'header-start'?(context: BpmnStudioSlotContext): VNodeChild
186
+ 'header-actions'?(context: BpmnStudioSlotContext): VNodeChild
187
+ header?(context: BpmnStudioSlotContext): VNodeChild
188
+ }
189
+ export const BpmnStudio: DefineComponent<BpmnStudioProps> & {
190
+ new(): { $slots: BpmnStudioSlots }
191
+ }
103
192
 
104
193
  export interface BpmnDesignerProps extends BpmnNovaVisualProps, BpmnModelProps { svgExport?: ViewerOptions['svgExport'] }
105
194
  export interface BpmnDesignerExposed {
@@ -119,6 +208,7 @@ export interface BpmnViewerProps extends BpmnNovaVisualProps, BpmnModelProps, Om
119
208
  export interface BpmnViewerExposed {
120
209
  getInstance(): ViewerInstance | null
121
210
  fitView(): void
211
+ refreshPresentation(): void
122
212
  setProjection(projection: ViewerProjection): void
123
213
  setDisplayOptions(options: Parameters<ViewerInstance['setDisplayOptions']>[0]): void
124
214
  setTheme(theme: NovaThemeInput): NovaThemeState | undefined
@@ -160,6 +250,9 @@ export interface BpmnStudioEmits {
160
250
  'scope-change': [activeScopeId: string, scopePath: StudioState['scopePath'], state: StudioState]
161
251
  'element-click': [payload: Parameters<NonNullable<ViewerOptions['onElementClick']>>[0]]
162
252
  'trace-click': [payload: RuntimeTraceClickEvent]
253
+ 'update:mode': [mode: StudioMode]
254
+ 'mode-change': [event: StudioModeChangeEvent]
255
+ validation: [event: StudioValidationEvent]
163
256
  }
164
257
  export interface BpmnDesignerEmits {
165
258
  change: [model: ProcessModel, reason: string, xml: string]
package/dist/index.js CHANGED
@@ -1,9 +1,30 @@
1
- import { createApp, defineComponent, h, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
1
+ import { Fragment, Teleport, createApp, defineComponent, h, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
2
2
  import { BpmnDesigner as CoreDesigner } from '@bpmn-nova/studio/designer';
3
3
  import { BpmnViewer as CoreViewer } from '@bpmn-nova/studio/viewer';
4
4
  import { importBpmn, exportBpmn } from '@bpmn-nova/studio';
5
5
  import { createEmptyProcess } from '@bpmn-nova/studio';
6
6
  import { BpmnCanvas as CoreCanvas, BpmnStudioShell as CoreStudioShell, createInteractionController, createStudioController, createTemplateRegistry } from '@bpmn-nova/studio';
7
+ import { PalettePanel as CorePalettePanel, createDefaultPaletteRegistry as createCoreDefaultPaletteRegistry } from '@bpmn-nova/studio';
8
+
9
+ export {
10
+ createContextMenuRegistry,
11
+ createDefaultContextMenuRegistry,
12
+ createDefaultIconRegistry,
13
+ createDefaultPaletteRegistry,
14
+ createDefaultPropertiesRegistry,
15
+ createEmptyProcess,
16
+ createPaletteRegistry,
17
+ createPropertiesRegistry,
18
+ createStudioController,
19
+ createTemplateRegistry,
20
+ exportBpmn,
21
+ importBpmn,
22
+ propertyEntry,
23
+ propertyGroup,
24
+ registerActivitiProperties,
25
+ registerBpmnProperties,
26
+ registerFlowableProperties,
27
+ } from '@bpmn-nova/studio';
7
28
 
8
29
  export function createVueRuntimeDetailsComponent(Component) {
9
30
  return ({ container, ...context }) => {
@@ -28,7 +49,7 @@ function resolveModel(props) {
28
49
  }
29
50
 
30
51
  export function useBpmnStudio(options = {}) {
31
- const studio = options.studio || createStudioController({ model: resolveModel(options), propertiesProfile: options.propertiesProfile });
52
+ const studio = options.studio || createStudioController({ model: resolveModel(options), propertiesProfile: options.propertiesProfile, allowedNodeTypes: options.allowedNodeTypes, allowedEdgeTypes: options.allowedEdgeTypes });
32
53
  const state = reactive(studio.getState());
33
54
  const off = studio.subscribe((event) => Object.assign(state, event.state));
34
55
  onBeforeUnmount(() => { off(); if (!options.studio) studio.destroy(); });
@@ -37,56 +58,148 @@ export function useBpmnStudio(options = {}) {
37
58
 
38
59
  export const BpmnCanvas = defineComponent({
39
60
  name: 'BpmnNovaCanvas',
40
- props: { studio: { type: Object, required: true }, interactions: Object, templates: Object, iconRegistry: Object, nodeRenderers: Object, nodeRenderer: Function, svgExport: Object, theme: [Object, String], onThemeChange: Function },
61
+ props: { studio: { type: Object, required: true }, interactions: Object, templates: Object, iconRegistry: Object, nodeRenderers: Object, nodeRenderer: Function, nodeSubtitleResolver: Function, svgExport: Object, theme: [Object, String], onThemeChange: Function },
41
62
  setup(props, { expose, attrs }) {
42
63
  const host = ref(null);
43
64
  let canvas = null;
65
+ const nodeSubtitleResolver = (context) => props.nodeSubtitleResolver?.(context);
44
66
  onMounted(() => {
45
67
  const interactions = props.interactions || createInteractionController({ studio: props.studio, templates: props.templates || createTemplateRegistry() });
46
- canvas = new CoreCanvas({ container: host.value, studio: props.studio, interactions, theme: props.theme, onThemeChange: props.onThemeChange, rendererOptions: { iconRegistry: props.iconRegistry, nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, svgExport: props.svgExport } });
68
+ canvas = new CoreCanvas({ container: host.value, studio: props.studio, interactions, theme: props.theme, onThemeChange: props.onThemeChange, nodeSubtitleResolver, rendererOptions: { iconRegistry: props.iconRegistry, nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, svgExport: props.svgExport } });
47
69
  requestAnimationFrame(() => canvas.fitView());
48
70
  });
49
71
  onBeforeUnmount(() => canvas?.destroy());
50
72
  watch(() => props.theme, (value) => { if (value !== undefined) canvas?.setTheme(value); });
51
- expose({ getCanvas: () => canvas, fitView: () => canvas?.fitView(), setTheme: (value) => canvas?.setTheme(value), exportSvg: (options) => canvas?.exportSvg(options), openSvgExportPreview: (options) => canvas?.openSvgExportPreview(options) });
73
+ watch(() => props.nodeSubtitleResolver, () => canvas?.refreshPresentation());
74
+ expose({ getCanvas: () => canvas, fitView: () => canvas?.fitView(), refreshPresentation: () => canvas?.refreshPresentation(), setTheme: (value) => canvas?.setTheme(value), exportSvg: (options) => canvas?.exportSvg(options), openSvgExportPreview: (options) => canvas?.openSvgExportPreview(options) });
52
75
  return () => h('div', { ...attrs, ref: host, style: { width: '100%', height: '100%', minHeight: '320px', ...(attrs.style || {}) } });
53
76
  },
54
77
  });
55
78
 
79
+ export const BpmnPalettePanel = defineComponent({
80
+ name: 'BpmnNovaPalettePanel',
81
+ props: {
82
+ studio: { type: Object, required: true },
83
+ interactions: Object,
84
+ templates: Object,
85
+ registry: Object,
86
+ providers: Array,
87
+ canvas: Object,
88
+ iconRegistry: Object,
89
+ renderItem: Function,
90
+ renderSection: Function,
91
+ theme: [Object, String],
92
+ onThemeChange: Function,
93
+ },
94
+ setup(props, { expose, attrs }) {
95
+ const host = ref(null);
96
+ let panel = null;
97
+
98
+ const mountPanel = () => {
99
+ if (!host.value || !props.studio) return;
100
+ panel?.destroy();
101
+ const interactions = props.interactions || createInteractionController({ studio: props.studio, templates: props.templates || createTemplateRegistry() });
102
+ const registry = props.registry || createCoreDefaultPaletteRegistry({ providers: props.providers || [] });
103
+ panel = new CorePalettePanel({
104
+ container: host.value,
105
+ registry,
106
+ studio: props.studio,
107
+ interactions,
108
+ canvas: props.canvas || null,
109
+ iconRegistry: props.iconRegistry,
110
+ renderItem: props.renderItem,
111
+ renderSection: props.renderSection,
112
+ theme: props.theme,
113
+ onThemeChange: props.onThemeChange,
114
+ });
115
+ };
116
+
117
+ onMounted(mountPanel);
118
+ watch(() => [props.studio, props.registry, props.interactions, props.templates, props.canvas, props.providers, props.iconRegistry, props.renderItem, props.renderSection], mountPanel);
119
+ watch(() => props.theme, (value) => { if (value !== undefined) panel?.setTheme(value); });
120
+ onBeforeUnmount(() => panel?.destroy());
121
+ expose({ getPanel: () => panel, render: () => panel?.render(), setTheme: (value) => panel?.setTheme(value) });
122
+ return () => h('div', { ...attrs, ref: host, style: { width: '100%', height: '100%', overflow: 'auto', ...(attrs.style || {}) } });
123
+ },
124
+ });
125
+
56
126
  export const BpmnStudio = defineComponent({
57
127
  name: 'BpmnNovaStudio',
58
128
  props: {
59
- studio: Object, model: Object, xml: String, engine: { type: String, default: 'flowable' }, propertiesProfile: String,
60
- iconRegistry: Object, paletteRegistry: Object, propertiesRegistry: Object, templateRegistry: Object,
61
- nodeRenderers: Object, nodeRenderer: Function, slotsConfig: Object, layout: Function, leftWidth: Number, rightWidth: Number,
129
+ studio: Object, model: Object, xml: String, engine: { type: String, default: 'flowable' }, propertiesProfile: String, allowedNodeTypes: Array, allowedEdgeTypes: Array,
130
+ iconRegistry: Object, paletteRegistry: Object, propertiesRegistry: Object, templateRegistry: Object, contextMenuRegistry: Object,
131
+ nodeRenderers: Object, nodeRenderer: Function, nodeSubtitleResolver: Function, slotsConfig: Object, layout: Function, regions: Object, leftWidth: Number, rightWidth: Number,
62
132
  runtime: Object, runtimePresenter: Function, runtimeTraceProjector: Function, runtimeAssetResolver: Function, runtimeTimelineRenderer: Function, onRuntimeTraceItemClick: Function, runtimeDetailsRenderer: Function, onRuntimeDetailsOpen: Function,
63
133
  runtimeTransitionDetailsRenderer: Function, onRuntimeTransitionDetailsOpen: Function,
64
134
  timeline: Object, runtimeDetails: Object, runtimeTraceOptions: Object, onTraceClick: Function, onElementClick: Function,
65
- mode: String, projection: String, responsive: { type: Boolean, default: false }, projectionOptions: Array,
135
+ mode: String, allowedModes: Array, projection: String, responsive: { type: Boolean, default: false }, projectionOptions: Array,
66
136
  theme: [Object, String], runtimeAppearance: Object, svgExport: Object, onThemeChange: Function,
67
137
  },
68
- emits: ['change', 'selection-change', 'scope-change', 'element-click', 'trace-click'],
69
- setup(props, { emit, expose, attrs }) {
138
+ emits: ['change', 'selection-change', 'scope-change', 'element-click', 'trace-click', 'update:mode', 'mode-change', 'validation'],
139
+ setup(props, { emit, expose, attrs, slots }) {
70
140
  const host = ref(null);
141
+ const headerTarget = ref(null);
142
+ const headerStartTarget = ref(null);
143
+ const headerActionsTarget = ref(null);
144
+ const headerContext = ref(null);
145
+ const headerStartContext = ref(null);
146
+ const headerActionsContext = ref(null);
71
147
  const owned = !props.studio;
72
- const studio = props.studio || createStudioController({ model: resolveModel(props), propertiesProfile: props.propertiesProfile });
148
+ const studio = props.studio || createStudioController({ model: resolveModel(props), propertiesProfile: props.propertiesProfile, allowedNodeTypes: props.allowedNodeTypes, allowedEdgeTypes: props.allowedEdgeTypes });
149
+ const state = reactive(studio.getState());
150
+ const actualMode = ref(props.mode || props.allowedModes?.[0] || 'design');
151
+ const nodeSubtitleResolver = (context) => props.nodeSubtitleResolver?.(context);
73
152
  let shell = null;
74
153
  let off = null;
154
+ let offMode = null;
155
+ let offValidation = null;
156
+
157
+ const createTeleportSlot = (target, contextRef) => (context) => {
158
+ target.value = context.container;
159
+ contextRef.value = context;
160
+ return () => {
161
+ if (target.value === context.container) target.value = null;
162
+ if (contextRef.value === context) contextRef.value = null;
163
+ };
164
+ };
165
+
75
166
  onMounted(() => {
76
- shell = new CoreStudioShell({ container: host.value, studio, iconRegistry: props.iconRegistry, paletteRegistry: props.paletteRegistry, propertiesRegistry: props.propertiesRegistry, templateRegistry: props.templateRegistry, rendererOptions: { nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, runtimePresenter: props.runtimePresenter, runtimeTraceProjector: props.runtimeTraceProjector, runtimeAssetResolver: props.runtimeAssetResolver, runtimeTraceOptions: props.runtimeTraceOptions, runtimeTimelineRenderer: props.runtimeTimelineRenderer, timeline: props.timeline, runtimeDetails: props.runtimeDetails, onRuntimeTraceItemClick: (payload) => props.onRuntimeTraceItemClick?.(payload), runtimeDetailsRenderer: props.runtimeDetailsRenderer, onRuntimeDetailsOpen: (payload) => props.onRuntimeDetailsOpen?.(payload), runtimeTransitionDetailsRenderer: props.runtimeTransitionDetailsRenderer, onRuntimeTransitionDetailsOpen: (payload) => props.onRuntimeTransitionDetailsOpen?.(payload), onElementClick: (payload) => emit('element-click', payload), onTraceClick: (payload) => emit('trace-click', payload) }, slots: props.slotsConfig || {}, layout: props.layout, runtime: props.runtime, mode: props.mode, projection: props.projection, responsive: props.responsive, projectionOptions: props.projectionOptions, leftWidth: props.leftWidth, rightWidth: props.rightWidth, theme: props.theme, runtimeAppearance: props.runtimeAppearance, svgExport: props.svgExport, onThemeChange: props.onThemeChange });
167
+ const shellSlots = { ...(props.slotsConfig || {}) };
168
+ if (slots.header) shellSlots.header = createTeleportSlot(headerTarget, headerContext);
169
+ else if (!shellSlots.header) {
170
+ if (slots['header-start']) shellSlots.headerStart = createTeleportSlot(headerStartTarget, headerStartContext);
171
+ if (slots['header-actions']) shellSlots.headerActions = createTeleportSlot(headerActionsTarget, headerActionsContext);
172
+ }
173
+ shell = new CoreStudioShell({ container: host.value, studio, iconRegistry: props.iconRegistry, paletteRegistry: props.paletteRegistry, propertiesRegistry: props.propertiesRegistry, templateRegistry: props.templateRegistry, contextMenuRegistry: props.contextMenuRegistry, rendererOptions: { nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, nodeSubtitleResolver, runtimePresenter: props.runtimePresenter, runtimeTraceProjector: props.runtimeTraceProjector, runtimeAssetResolver: props.runtimeAssetResolver, runtimeTraceOptions: props.runtimeTraceOptions, runtimeTimelineRenderer: props.runtimeTimelineRenderer, timeline: props.timeline, runtimeDetails: props.runtimeDetails, onRuntimeTraceItemClick: (payload) => props.onRuntimeTraceItemClick?.(payload), runtimeDetailsRenderer: props.runtimeDetailsRenderer, onRuntimeDetailsOpen: (payload) => props.onRuntimeDetailsOpen?.(payload), runtimeTransitionDetailsRenderer: props.runtimeTransitionDetailsRenderer, onRuntimeTransitionDetailsOpen: (payload) => props.onRuntimeTransitionDetailsOpen?.(payload), onElementClick: (payload) => emit('element-click', payload), onTraceClick: (payload) => emit('trace-click', payload) }, slots: shellSlots, layout: props.layout, regions: props.regions, runtime: props.runtime, mode: props.mode, allowedModes: props.allowedModes, projection: props.projection, responsive: props.responsive, projectionOptions: props.projectionOptions, leftWidth: props.leftWidth, rightWidth: props.rightWidth, theme: props.theme, runtimeAppearance: props.runtimeAppearance, svgExport: props.svgExport, onThemeChange: props.onThemeChange });
174
+ actualMode.value = shell.getMode();
175
+ offMode = shell.subscribeMode((event) => {
176
+ actualMode.value = event.mode;
177
+ emit('update:mode', event.mode);
178
+ emit('mode-change', event);
179
+ });
180
+ offValidation = shell.subscribeValidation((event) => emit('validation', event));
77
181
  off = studio.subscribe((event) => {
182
+ Object.assign(state, event.state);
78
183
  if (event.type === 'modelChanged') emit('change', studio.model, event.reason, studio.exportXml());
79
184
  if (event.type === 'selectionChanged') emit('selection-change', studio.selection, studio.getSelectedElement());
80
185
  if (event.type === 'scopeChanged') emit('scope-change', event.activeScopeId, event.scopePath, studio.getState());
81
186
  });
82
187
  });
83
188
  watch(() => props.model, (value) => { if (value && value !== studio.model) studio.setModel(value); });
84
- watch(() => props.xml, (value) => { if (value) studio.importXml(value, props.engine); });
189
+ watch(() => props.xml, (value) => {
190
+ if (value && value !== studio.exportXml(props.engine)) studio.importXml(value, props.engine);
191
+ });
85
192
  watch(() => props.runtime, (value) => { if (value !== undefined) shell?.setRuntime(value); });
86
- watch(() => props.mode, (value) => { if (value) shell?.setMode(value); });
193
+ watch(() => [props.allowedModes, props.mode], ([allowedModes, mode]) => {
194
+ if (!shell) return;
195
+ if (allowedModes !== undefined) shell.setAllowedModes(allowedModes);
196
+ if (mode) shell.setMode(mode);
197
+ }, { deep: true });
87
198
  watch(() => props.projection, (value) => { if (value) shell?.setProjection(value); });
199
+ watch(() => props.regions, (value) => { if (value !== undefined) shell?.setRegions(value); }, { deep: true });
88
200
  watch(() => props.theme, (value) => { if (value !== undefined) shell?.setTheme(value); });
89
201
  watch(() => props.runtimeAppearance, (value) => { if (value !== undefined) shell?.setRuntimeAppearance(value); });
202
+ watch(() => props.nodeSubtitleResolver, () => shell?.refreshPresentation());
90
203
  watch(() => [props.timeline, props.runtimeDetails, props.runtimeTraceOptions], ([timeline, runtimeDetails, runtimeTraceOptions]) => {
91
204
  if (!shell) return;
92
205
  shell.rendererOptions.timeline = timeline;
@@ -94,9 +207,24 @@ export const BpmnStudio = defineComponent({
94
207
  shell.rendererOptions.runtimeTraceOptions = runtimeTraceOptions;
95
208
  shell.viewer?.setDisplayOptions({ timeline, runtimeDetails, runtimeTraceOptions });
96
209
  });
97
- onBeforeUnmount(() => { off?.(); shell?.destroy(); if (owned) studio.destroy(); });
98
- expose({ getStudio: () => studio, getShell: () => shell, exportXml: (engine) => studio.exportXml(engine), exportSvg: (options) => shell?.exportSvg(options), openSvgExportPreview: (options) => shell?.openSvgExportPreview(options), fitView: () => shell?.canvas?.fitView(), setTheme: (value) => shell?.setTheme(value) });
99
- return () => h('div', { ...attrs, ref: host, style: { width: '100%', height: '100%', minHeight: '520px', ...(attrs.style || {}) } });
210
+ onBeforeUnmount(() => { offMode?.(); offValidation?.(); off?.(); shell?.destroy(); if (owned) studio.destroy(); });
211
+ expose({ getStudio: () => studio, getShell: () => shell, getActions: () => shell?.actions || null, getMode: () => shell?.getMode(), getAllowedModes: () => shell?.getAllowedModes() || [], setMode: (value) => shell?.setMode(value) || false, setAllowedModes: (value) => shell?.setAllowedModes(value) || [], refreshPresentation: () => shell?.refreshPresentation(), validate: () => shell?.validate() || [], exportXml: (engine) => studio.exportXml(engine), exportSvg: (options) => shell?.exportSvg(options), openSvgExportPreview: (options) => shell?.openSvgExportPreview(options), fitView: () => shell?.fitView(), setTheme: (value) => shell?.setTheme(value) });
212
+ const renderSlot = (name, target, contextRef) => {
213
+ if (!slots[name] || !target.value || !contextRef.value) return null;
214
+ return h(Teleport, { to: target.value }, slots[name]({
215
+ studio: contextRef.value.studio,
216
+ shell: contextRef.value.shell,
217
+ actions: contextRef.value.actions,
218
+ state,
219
+ mode: actualMode.value,
220
+ }));
221
+ };
222
+ return () => h(Fragment, null, [
223
+ h('div', { ...attrs, ref: host, style: { width: '100%', height: '100%', minHeight: '520px', ...(attrs.style || {}) } }),
224
+ renderSlot('header', headerTarget, headerContext),
225
+ renderSlot('header-start', headerStartTarget, headerStartContext),
226
+ renderSlot('header-actions', headerActionsTarget, headerActionsContext),
227
+ ]);
100
228
  },
101
229
  });
102
230
 
@@ -162,6 +290,7 @@ export const BpmnViewer = defineComponent({
162
290
  iconRegistry: Object,
163
291
  nodeRenderers: Object,
164
292
  nodeRenderer: Function,
293
+ nodeSubtitleResolver: Function,
165
294
  timeline: Object,
166
295
  runtimeDetails: Object,
167
296
  runtimePresenter: Function,
@@ -187,6 +316,7 @@ export const BpmnViewer = defineComponent({
187
316
  setup(props, { emit, expose, attrs }) {
188
317
  const host = ref(null);
189
318
  let instance = null;
319
+ const nodeSubtitleResolver = (context) => props.nodeSubtitleResolver?.(context);
190
320
  onMounted(() => {
191
321
  instance = new CoreViewer({
192
322
  container: host.value,
@@ -195,6 +325,7 @@ export const BpmnViewer = defineComponent({
195
325
  iconRegistry: props.iconRegistry,
196
326
  nodeRenderers: props.nodeRenderers,
197
327
  nodeRenderer: props.nodeRenderer,
328
+ nodeSubtitleResolver,
198
329
  timeline: props.timeline,
199
330
  runtimeDetails: props.runtimeDetails,
200
331
  runtimePresenter: props.runtimePresenter,
@@ -224,10 +355,11 @@ export const BpmnViewer = defineComponent({
224
355
  watch(() => props.projection, (value) => instance?.setProjection(value));
225
356
  watch(() => props.theme, (value) => { if (value !== undefined) instance?.setTheme(value); });
226
357
  watch(() => props.runtimeAppearance, (value) => { if (value !== undefined) instance?.setRuntimeAppearance(value); });
358
+ watch(() => props.nodeSubtitleResolver, () => instance?.refreshPresentation());
227
359
  watch(() => [props.timeline, props.runtimeDetails, props.runtimeTraceOptions, props.runtimeAssetResolver], ([timeline, runtimeDetails, runtimeTraceOptions, runtimeAssetResolver]) => instance?.setDisplayOptions({ timeline, runtimeDetails, runtimeTraceOptions, runtimeAssetResolver }));
228
360
  watch(() => props.model, (value) => { if (value) instance?.setModel(value); });
229
361
  watch(() => props.xml, (value) => { if (value) instance?.setModel(importBpmn(value, props.engine)); });
230
- expose({ getInstance: () => instance, fitView: () => instance?.fitView(), setProjection: (p) => instance?.setProjection(p), setDisplayOptions: (value) => instance?.setDisplayOptions(value), setTheme: (value) => instance?.setTheme(value), exportSvg: (options) => instance?.exportSvg(options), openSvgExportPreview: (options) => instance?.openSvgExportPreview(options) });
362
+ expose({ getInstance: () => instance, fitView: () => instance?.fitView(), refreshPresentation: () => instance?.refreshPresentation(), setProjection: (p) => instance?.setProjection(p), setDisplayOptions: (value) => instance?.setDisplayOptions(value), setTheme: (value) => instance?.setTheme(value), exportSvg: (options) => instance?.exportSvg(options), openSvgExportPreview: (options) => instance?.openSvgExportPreview(options) });
231
363
  return () => h('div', { ...attrs, ref: host, style: { width: '100%', height: '100%', minHeight: '260px', ...(attrs.style || {}) } });
232
364
  },
233
365
  });