@nocobase/flow-engine 2.1.0-alpha.21 → 2.1.0-alpha.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/lib/components/FieldModelRenderer.js +2 -2
  2. package/lib/components/FlowModelRenderer.d.ts +2 -0
  3. package/lib/components/FlowModelRenderer.js +2 -0
  4. package/lib/components/dnd/index.d.ts +19 -1
  5. package/lib/components/dnd/index.js +239 -21
  6. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +20 -1
  7. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +4 -0
  8. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +21 -8
  9. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +2 -0
  10. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +100 -32
  11. package/lib/components/subModel/index.d.ts +1 -0
  12. package/lib/components/subModel/index.js +19 -0
  13. package/lib/components/subModel/utils.d.ts +1 -1
  14. package/lib/data-source/index.d.ts +73 -0
  15. package/lib/data-source/index.js +205 -1
  16. package/lib/flowContext.d.ts +2 -0
  17. package/lib/flowI18n.js +2 -1
  18. package/lib/models/DisplayItemModel.d.ts +1 -1
  19. package/lib/models/EditableItemModel.d.ts +1 -1
  20. package/lib/models/FilterableItemModel.d.ts +1 -1
  21. package/lib/models/flowModel.d.ts +11 -9
  22. package/lib/models/flowModel.js +48 -9
  23. package/lib/provider.js +38 -23
  24. package/package.json +4 -4
  25. package/src/__tests__/provider.test.tsx +24 -2
  26. package/src/components/FieldModelRenderer.tsx +2 -1
  27. package/src/components/FlowModelRenderer.tsx +6 -0
  28. package/src/components/__tests__/FlowModelRenderer.test.tsx +22 -0
  29. package/src/components/__tests__/dnd.test.ts +44 -0
  30. package/src/components/dnd/index.tsx +286 -26
  31. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +25 -1
  32. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +24 -5
  33. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +94 -3
  34. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +171 -2
  35. package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +2 -0
  36. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +112 -32
  37. package/src/components/subModel/index.ts +1 -0
  38. package/src/data-source/__tests__/index.test.ts +34 -1
  39. package/src/data-source/index.ts +252 -2
  40. package/src/flowContext.ts +2 -0
  41. package/src/flowI18n.ts +2 -1
  42. package/src/models/DisplayItemModel.tsx +1 -1
  43. package/src/models/EditableItemModel.tsx +1 -1
  44. package/src/models/FilterableItemModel.tsx +1 -1
  45. package/src/models/flowModel.tsx +85 -23
  46. package/src/provider.tsx +41 -25
@@ -90,11 +90,21 @@ describe('FlowModelRenderer', () => {
90
90
 
91
91
  test('should clear stale beforeRender state after unmount when reusing the same model', async () => {
92
92
  const statefulEngine = new FlowEngine();
93
+ const onMountSpy = vi.fn();
94
+ const onUnmountSpy = vi.fn();
93
95
 
94
96
  class StatefulModel extends FlowModel {
95
97
  render(): any {
96
98
  return <div>Stateful Content</div>;
97
99
  }
100
+
101
+ protected onMount(): void {
102
+ onMountSpy();
103
+ }
104
+
105
+ protected onUnmount(): void {
106
+ onUnmountSpy();
107
+ }
98
108
  }
99
109
 
100
110
  const statefulModel = new StatefulModel({
@@ -107,8 +117,14 @@ describe('FlowModelRenderer', () => {
107
117
  await waitFor(() => {
108
118
  expect(executorSpy).toHaveBeenCalledTimes(1);
109
119
  });
120
+ await waitFor(() => {
121
+ expect(onMountSpy).toHaveBeenCalledTimes(1);
122
+ });
110
123
 
111
124
  firstRender.unmount();
125
+ await waitFor(() => {
126
+ expect(onUnmountSpy).toHaveBeenCalledTimes(1);
127
+ });
112
128
 
113
129
  executorSpy.mockClear();
114
130
  statefulModel.setStepParams('anyFlow', 'anyStep', { x: 1 });
@@ -119,6 +135,9 @@ describe('FlowModelRenderer', () => {
119
135
  await waitFor(() => {
120
136
  expect(executorSpy).toHaveBeenCalledTimes(1);
121
137
  });
138
+ await waitFor(() => {
139
+ expect(onMountSpy).toHaveBeenCalledTimes(2);
140
+ });
122
141
  const [target, eventName, inputArgs, options] = executorSpy.mock.calls[0];
123
142
  expect(target).toBe(statefulModel);
124
143
  expect(eventName).toBe('beforeRender');
@@ -126,5 +145,8 @@ describe('FlowModelRenderer', () => {
126
145
  expect(options).toMatchObject({ useCache: true });
127
146
 
128
147
  secondRender.unmount();
148
+ await waitFor(() => {
149
+ expect(onUnmountSpy).toHaveBeenCalledTimes(2);
150
+ });
129
151
  });
130
152
  });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, expect, it } from 'vitest';
11
+ import { resolveOverlayAnchorTransform } from '../dnd';
12
+
13
+ describe('resolveOverlayAnchorTransform', () => {
14
+ it('should keep the original transform when anchor point is missing', () => {
15
+ const transform = { x: 24, y: 36, scaleX: 1, scaleY: 1 };
16
+
17
+ expect(
18
+ resolveOverlayAnchorTransform({
19
+ activeId: 'menu-item-1',
20
+ active: { id: 'menu-item-1' },
21
+ transform,
22
+ activeNodeRect: { top: 80, left: 120 },
23
+ dragAnchorPoint: null,
24
+ }),
25
+ ).toEqual(transform);
26
+ });
27
+
28
+ it('should align the overlay origin to the pointer position when dragging from toolbar handle', () => {
29
+ expect(
30
+ resolveOverlayAnchorTransform({
31
+ activeId: 'menu-item-1',
32
+ active: { id: 'menu-item-1' },
33
+ transform: { x: 20, y: 30, scaleX: 1, scaleY: 1 },
34
+ activeNodeRect: { top: 200, left: 100 },
35
+ dragAnchorPoint: { x: 180, y: 260 },
36
+ }),
37
+ ).toEqual({
38
+ x: 100,
39
+ y: 90,
40
+ scaleX: 1,
41
+ scaleY: 1,
42
+ });
43
+ });
44
+ });
@@ -8,8 +8,10 @@
8
8
  */
9
9
 
10
10
  import { DragOutlined } from '@ant-design/icons';
11
+ import type { Modifier } from '@dnd-kit/core';
11
12
  import { DndContext, DndContextProps, DragOverlay, useDraggable, useDroppable } from '@dnd-kit/core';
12
- import React, { FC, useState } from 'react';
13
+ import type { Transform } from '@dnd-kit/utilities';
14
+ import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
13
15
  import { createPortal } from 'react-dom';
14
16
  import { FlowModel } from '../../models';
15
17
  import { useFlowEngine } from '../../provider';
@@ -19,18 +21,229 @@ export * from './findModelUidPosition';
19
21
  export * from './gridDragPlanner';
20
22
 
21
23
  export const EMPTY_COLUMN_UID = 'EMPTY_COLUMN';
24
+ export const TOOLBAR_DRAG_ACTIVITY_EVENT = 'nb-toolbar-drag-activity';
25
+ const TOOLBAR_DRAG_ANCHOR_EVENT = 'nb-toolbar-drag-anchor';
26
+ const MENU_SUBMENU_POPUP_SELECTOR = '.ant-menu-submenu-popup';
27
+
28
+ type ToolbarDragAnchorPoint = {
29
+ x: number;
30
+ y: number;
31
+ };
32
+
33
+ type ToolbarDragAnchorDetail = {
34
+ modelUid: string;
35
+ point: ToolbarDragAnchorPoint | null;
36
+ };
37
+
38
+ export const resolveOverlayAnchorTransform = ({
39
+ activeId,
40
+ active,
41
+ transform,
42
+ activeNodeRect,
43
+ dragAnchorPoint,
44
+ }: {
45
+ activeId: string | null;
46
+ active: { id: string | number } | null | undefined;
47
+ transform: Transform;
48
+ activeNodeRect: { top: number; left: number } | null;
49
+ dragAnchorPoint: ToolbarDragAnchorPoint | null;
50
+ }): Transform => {
51
+ if (!activeId || active?.id !== activeId || !dragAnchorPoint || !activeNodeRect) {
52
+ return transform;
53
+ }
54
+
55
+ return {
56
+ ...transform,
57
+ x: transform.x + dragAnchorPoint.x - activeNodeRect.left,
58
+ y: transform.y + dragAnchorPoint.y - activeNodeRect.top,
59
+ };
60
+ };
61
+
62
+ const resolveDraggableHostNode = (activatorNode: HTMLElement | null) => {
63
+ const ownerDocument = activatorNode?.ownerDocument;
64
+ const floatToolbarContainer = activatorNode?.closest<HTMLElement>('.nb-toolbar-container[data-model-uid]');
65
+ const toolbarModelUid = floatToolbarContainer?.getAttribute('data-model-uid');
66
+
67
+ if (!ownerDocument || !toolbarModelUid) {
68
+ return activatorNode;
69
+ }
70
+
71
+ const matchedHosts = Array.from(
72
+ ownerDocument.querySelectorAll<HTMLElement>(
73
+ `[data-has-float-menu="true"][data-float-menu-model-uid="${toolbarModelUid}"]`,
74
+ ),
75
+ );
76
+ const popupRoot = floatToolbarContainer.closest<HTMLElement>(MENU_SUBMENU_POPUP_SELECTOR);
77
+
78
+ if (popupRoot) {
79
+ return (
80
+ matchedHosts.find((hostNode) => hostNode.closest(MENU_SUBMENU_POPUP_SELECTOR) === popupRoot) || activatorNode
81
+ );
82
+ }
83
+
84
+ return (
85
+ matchedHosts.find((hostNode) => !hostNode.closest(MENU_SUBMENU_POPUP_SELECTOR)) || matchedHosts[0] || activatorNode
86
+ );
87
+ };
22
88
 
23
89
  // 可拖拽图标组件
24
- export const DragHandler: FC<{ model: FlowModel; children: React.ReactNode }> = ({
90
+ export const DragHandler: FC<{ model: FlowModel; children?: React.ReactNode }> = ({
25
91
  model,
26
92
  children = <DragOutlined />,
27
93
  }) => {
28
- const { attributes, listeners, setNodeRef } = useDraggable({ id: model.uid });
94
+ const { attributes, isDragging, listeners, setActivatorNodeRef, setNodeRef } = useDraggable({ id: model.uid });
95
+ const dragHandlerRef = useRef<HTMLSpanElement | null>(null);
96
+ const draggableNodeRef = useRef<HTMLElement | null>(null);
97
+ const pointerPressCleanupRef = useRef<(() => void) | null>(null);
98
+ const isDraggingRef = useRef(isDragging);
99
+ const isPointerPressActiveRef = useRef(false);
100
+ const isToolbarDragActiveRef = useRef(false);
101
+ const syncDraggableNodeRef = useCallback(
102
+ (activatorNode: HTMLElement | null) => {
103
+ const nextNode = resolveDraggableHostNode(activatorNode);
104
+
105
+ if (draggableNodeRef.current === nextNode) {
106
+ return;
107
+ }
108
+
109
+ draggableNodeRef.current = nextNode;
110
+ setNodeRef(nextNode);
111
+ },
112
+ [setNodeRef],
113
+ );
114
+ const setDragHandlerNodeRef = useCallback(
115
+ (node: HTMLSpanElement | null) => {
116
+ dragHandlerRef.current = node;
117
+ setActivatorNodeRef(node);
118
+ syncDraggableNodeRef(node);
119
+ },
120
+ [setActivatorNodeRef, syncDraggableNodeRef],
121
+ );
122
+ const dispatchToolbarDragActivity = useCallback(
123
+ (active: boolean) => {
124
+ const ownerDocument = dragHandlerRef.current?.ownerDocument;
125
+ if (!ownerDocument) {
126
+ return;
127
+ }
128
+
129
+ ownerDocument.dispatchEvent(
130
+ new CustomEvent(TOOLBAR_DRAG_ACTIVITY_EVENT, {
131
+ detail: { active, modelUid: model.uid },
132
+ }),
133
+ );
134
+ },
135
+ [model.uid],
136
+ );
137
+
138
+ const dispatchToolbarDragAnchor = useCallback(
139
+ (detail: Pick<ToolbarDragAnchorDetail, 'point'>) => {
140
+ const ownerDocument = dragHandlerRef.current?.ownerDocument;
141
+ if (!ownerDocument) {
142
+ return;
143
+ }
144
+
145
+ ownerDocument.dispatchEvent(
146
+ new CustomEvent<ToolbarDragAnchorDetail>(TOOLBAR_DRAG_ANCHOR_EVENT, {
147
+ detail: { modelUid: model.uid, point: detail.point },
148
+ }),
149
+ );
150
+ },
151
+ [model.uid],
152
+ );
153
+
154
+ const clearPointerPressListeners = useCallback(() => {
155
+ pointerPressCleanupRef.current?.();
156
+ pointerPressCleanupRef.current = null;
157
+ }, []);
158
+
159
+ const syncToolbarDragActivity = useCallback(() => {
160
+ const nextActive = isPointerPressActiveRef.current || isDraggingRef.current;
161
+ if (nextActive === isToolbarDragActiveRef.current) {
162
+ return;
163
+ }
164
+
165
+ isToolbarDragActiveRef.current = nextActive;
166
+ dispatchToolbarDragActivity(nextActive);
167
+ }, [dispatchToolbarDragActivity]);
168
+
169
+ const handlePointerPressEnd = useCallback(() => {
170
+ isPointerPressActiveRef.current = false;
171
+ clearPointerPressListeners();
172
+ syncToolbarDragActivity();
173
+ }, [clearPointerPressListeners, syncToolbarDragActivity]);
174
+
175
+ const registerPointerPressListeners = useCallback(() => {
176
+ const ownerDocument = dragHandlerRef.current?.ownerDocument;
177
+ const ownerWindow = ownerDocument?.defaultView;
178
+ if (!ownerDocument) {
179
+ return;
180
+ }
181
+
182
+ clearPointerPressListeners();
183
+
184
+ const handlePointerEnd = () => {
185
+ handlePointerPressEnd();
186
+ };
187
+ const handleKeyDown = (event: KeyboardEvent) => {
188
+ if (event.key === 'Escape') {
189
+ handlePointerPressEnd();
190
+ }
191
+ };
192
+
193
+ ownerDocument.addEventListener('pointerup', handlePointerEnd, true);
194
+ ownerDocument.addEventListener('pointercancel', handlePointerEnd, true);
195
+ ownerDocument.addEventListener('keydown', handleKeyDown, true);
196
+ ownerWindow?.addEventListener('blur', handlePointerEnd);
197
+
198
+ pointerPressCleanupRef.current = () => {
199
+ ownerDocument.removeEventListener('pointerup', handlePointerEnd, true);
200
+ ownerDocument.removeEventListener('pointercancel', handlePointerEnd, true);
201
+ ownerDocument.removeEventListener('keydown', handleKeyDown, true);
202
+ ownerWindow?.removeEventListener('blur', handlePointerEnd);
203
+ };
204
+ }, [clearPointerPressListeners, handlePointerPressEnd]);
205
+
206
+ useEffect(() => {
207
+ syncDraggableNodeRef(dragHandlerRef.current);
208
+ }, [syncDraggableNodeRef]);
209
+
210
+ useEffect(() => {
211
+ isDraggingRef.current = isDragging;
212
+ syncToolbarDragActivity();
213
+ }, [isDragging, syncToolbarDragActivity]);
214
+
215
+ useEffect(() => {
216
+ return () => {
217
+ if (isToolbarDragActiveRef.current) {
218
+ dispatchToolbarDragActivity(false);
219
+ }
220
+ isPointerPressActiveRef.current = false;
221
+ isDraggingRef.current = false;
222
+ isToolbarDragActiveRef.current = false;
223
+ clearPointerPressListeners();
224
+ };
225
+ }, [clearPointerPressListeners, dispatchToolbarDragActivity]);
226
+
29
227
  return (
30
228
  <span
31
- ref={setNodeRef}
229
+ ref={setDragHandlerNodeRef}
32
230
  {...listeners}
33
231
  {...attributes}
232
+ onPointerDownCapture={(event) => {
233
+ if (event.button !== 0) {
234
+ return;
235
+ }
236
+
237
+ dispatchToolbarDragAnchor({
238
+ point: {
239
+ x: event.clientX,
240
+ y: event.clientY,
241
+ },
242
+ });
243
+ isPointerPressActiveRef.current = true;
244
+ syncToolbarDragActivity();
245
+ registerPointerPressListeners();
246
+ }}
34
247
  style={{
35
248
  cursor: 'grab',
36
249
  }}
@@ -78,7 +291,40 @@ export const DndProvider: FC<DndContextProps & PersistOptions> = ({
78
291
  ...restProps
79
292
  }) => {
80
293
  const [activeId, setActiveId] = useState<string | null>(null);
294
+ const [dragAnchorPoint, setDragAnchorPoint] = useState<ToolbarDragAnchorDetail['point']>(null);
81
295
  const flowEngine = useFlowEngine();
296
+
297
+ useEffect(() => {
298
+ if (typeof document === 'undefined') {
299
+ return;
300
+ }
301
+
302
+ const handleToolbarDragAnchor = (event: Event) => {
303
+ const customEvent = event as CustomEvent<ToolbarDragAnchorDetail>;
304
+ setDragAnchorPoint(customEvent.detail?.point || null);
305
+ };
306
+
307
+ document.addEventListener(TOOLBAR_DRAG_ANCHOR_EVENT, handleToolbarDragAnchor as EventListener);
308
+ return () => {
309
+ document.removeEventListener(TOOLBAR_DRAG_ANCHOR_EVENT, handleToolbarDragAnchor as EventListener);
310
+ };
311
+ }, []);
312
+
313
+ const overlayAnchorModifier = useCallback<Modifier>(
314
+ ({ active, activeNodeRect, transform }) => {
315
+ const nextTransform: Transform = resolveOverlayAnchorTransform({
316
+ activeId,
317
+ active,
318
+ transform,
319
+ activeNodeRect,
320
+ dragAnchorPoint,
321
+ });
322
+
323
+ return nextTransform;
324
+ },
325
+ [activeId, dragAnchorPoint],
326
+ );
327
+
82
328
  return (
83
329
  <DndContext
84
330
  onDragStart={(event) => {
@@ -87,6 +333,7 @@ export const DndProvider: FC<DndContextProps & PersistOptions> = ({
87
333
  }}
88
334
  onDragEnd={(event) => {
89
335
  setActiveId(null);
336
+ setDragAnchorPoint(null);
90
337
  // 如果没有 onDragEnd 回调,则默认调用 flowEngine 的 moveModel 方法
91
338
  if (!onDragEnd) {
92
339
  if (event.over) {
@@ -97,32 +344,45 @@ export const DndProvider: FC<DndContextProps & PersistOptions> = ({
97
344
  onDragEnd(event);
98
345
  }
99
346
  }}
347
+ onDragCancel={(event) => {
348
+ setActiveId(null);
349
+ setDragAnchorPoint(null);
350
+ restProps.onDragCancel?.(event);
351
+ }}
100
352
  {...restProps}
101
353
  >
102
354
  {children}
103
- {createPortal(
104
- <DragOverlay dropAnimation={null} zIndex={2000}>
105
- {activeId && (
106
- <span
107
- style={{
108
- display: 'inline-flex',
109
- alignItems: 'center',
110
- whiteSpace: 'nowrap',
111
- background: '#fff',
112
- border: '1px solid #1890ff',
113
- borderRadius: 4,
114
- padding: '4px 12px',
115
- color: '#1890ff',
116
- // fontSize: 18,
117
- boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
118
- }}
355
+ {typeof document !== 'undefined'
356
+ ? createPortal(
357
+ <DragOverlay
358
+ dropAnimation={null}
359
+ modifiers={[overlayAnchorModifier]}
360
+ zIndex={2000}
361
+ style={{ pointerEvents: 'none' }}
119
362
  >
120
- {flowEngine.translate('Dragging')}
121
- </span>
122
- )}
123
- </DragOverlay>,
124
- document.body,
125
- )}
363
+ {activeId && (
364
+ <span
365
+ style={{
366
+ display: 'inline-flex',
367
+ alignItems: 'center',
368
+ whiteSpace: 'nowrap',
369
+ background: '#fff',
370
+ border: '1px solid #1890ff',
371
+ borderRadius: 4,
372
+ padding: '4px 12px',
373
+ color: '#1890ff',
374
+ pointerEvents: 'none',
375
+ // fontSize: 18,
376
+ boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
377
+ }}
378
+ >
379
+ {flowEngine.translate('Dragging')}
380
+ </span>
381
+ )}
382
+ </DragOverlay>,
383
+ document.body,
384
+ )
385
+ : null}
126
386
  </DndContext>
127
387
  );
128
388
  };
@@ -27,6 +27,26 @@ import { useNiceDropdownMaxHeight } from '../../../../hooks';
27
27
  import { SwitchWithTitle } from '../component/SwitchWithTitle';
28
28
  import { SelectWithTitle } from '../component/SelectWithTitle';
29
29
  import type { FlowSettingsContext } from '../../../../flowContext';
30
+
31
+ const findExtraMenuItemByKey = (
32
+ items: FlowModelExtraMenuItem[],
33
+ targetKey: string,
34
+ ): FlowModelExtraMenuItem | undefined => {
35
+ for (const item of items) {
36
+ const itemKey = String(item?.key ?? '');
37
+ if (itemKey === targetKey) {
38
+ return item;
39
+ }
40
+ if (item.children?.length) {
41
+ const matched = findExtraMenuItemByKey(item.children, targetKey);
42
+ if (matched) {
43
+ return matched;
44
+ }
45
+ }
46
+ }
47
+ return undefined;
48
+ };
49
+
30
50
  // Type definitions for better type safety
31
51
  interface StepInfo {
32
52
  stepKey: string;
@@ -473,7 +493,11 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
473
493
  return;
474
494
  }
475
495
 
476
- const extra = extraMenuItems.find((it) => it?.key === originalKey || it?.key === cleanKey);
496
+ const extra =
497
+ findExtraMenuItemByKey(extraMenuItems, originalKey) || findExtraMenuItemByKey(extraMenuItems, cleanKey);
498
+ if (extra?.disabled) {
499
+ return;
500
+ }
477
501
  if (extra?.onClick) {
478
502
  closeDropdown();
479
503
  extra.onClick();
@@ -27,9 +27,9 @@ import {
27
27
  } from './useFloatToolbarPortal';
28
28
  import { useFloatToolbarVisibility } from './useFloatToolbarVisibility';
29
29
 
30
- const TOOLBAR_Z_INDEX = 999;
31
-
32
30
  type ToolbarPosition = 'inside' | 'above' | 'below';
31
+ const TOOLBAR_ITEM_WIDTH = 19;
32
+ const DEFAULT_POPUP_BASE_Z_INDEX = 1000;
33
33
 
34
34
  interface BaseFloatContextMenuProps {
35
35
  children?: React.ReactNode;
@@ -64,6 +64,10 @@ interface BaseFloatContextMenuProps {
64
64
  * Extra toolbar items to add to this context menu instance
65
65
  */
66
66
  extraToolbarItems?: ToolbarItemConfig[];
67
+ /**
68
+ * @default true
69
+ */
70
+ showDynamicFlowsEditor?: boolean;
67
71
  /**
68
72
  * @default 'inside'
69
73
  */
@@ -97,12 +101,14 @@ const toolbarContainerStyles = ({
97
101
  showBackground,
98
102
  showBorder,
99
103
  ctx,
104
+ toolbarZIndex,
100
105
  }: {
101
106
  showBackground: boolean;
102
107
  showBorder: boolean;
103
108
  ctx: any;
109
+ toolbarZIndex: number;
104
110
  }) => css`
105
- z-index: ${TOOLBAR_Z_INDEX};
111
+ z-index: ${toolbarZIndex};
106
112
  opacity: 0;
107
113
  pointer-events: none;
108
114
  overflow: visible;
@@ -294,6 +300,7 @@ const renderToolbarItems = (
294
300
  flowEngine: FlowEngine,
295
301
  settingsMenuLevel?: number,
296
302
  extraToolbarItems?: ToolbarItemConfig[],
303
+ showDynamicFlowsEditor = true,
297
304
  onSettingsMenuOpenChange?: (open: boolean) => void,
298
305
  getPopupContainer?: (triggerNode?: HTMLElement) => HTMLElement,
299
306
  ) => {
@@ -304,6 +311,9 @@ const renderToolbarItems = (
304
311
 
305
312
  return allToolbarItems
306
313
  .filter((itemConfig: ToolbarItemConfig) => {
314
+ if (itemConfig.key === 'dynamic-flows-editor' && showDynamicFlowsEditor === false) {
315
+ return false;
316
+ }
307
317
  return itemConfig.visible ? itemConfig.visible(model) : true;
308
318
  })
309
319
  .map((itemConfig: ToolbarItemConfig) => {
@@ -332,6 +342,7 @@ const buildToolbarContainerClassName = ({
332
342
  showBackground,
333
343
  showBorder,
334
344
  ctx,
345
+ toolbarZIndex,
335
346
  portalRenderSnapshot,
336
347
  isToolbarVisible,
337
348
  className,
@@ -339,12 +350,13 @@ const buildToolbarContainerClassName = ({
339
350
  showBackground: boolean;
340
351
  showBorder: boolean;
341
352
  ctx: any;
353
+ toolbarZIndex: number;
342
354
  portalRenderSnapshot: ToolbarPortalRenderSnapshot | null;
343
355
  isToolbarVisible: boolean;
344
356
  className?: string;
345
357
  }) =>
346
358
  [
347
- toolbarContainerStyles({ showBackground, showBorder, ctx }),
359
+ toolbarContainerStyles({ showBackground, showBorder, ctx, toolbarZIndex }),
348
360
  'nb-toolbar-portal',
349
361
  portalRenderSnapshot?.positioningMode === 'absolute' ? 'nb-toolbar-portal-absolute' : 'nb-toolbar-portal-fixed',
350
362
  isToolbarVisible ? 'nb-toolbar-visible' : '',
@@ -356,11 +368,13 @@ const buildToolbarContainerClassName = ({
356
368
  const buildToolbarContainerStyle = (
357
369
  portalRect: ToolbarPortalRect,
358
370
  toolbarStyle?: React.CSSProperties,
371
+ toolbarItemCount = 0,
359
372
  ): React.CSSProperties => ({
360
373
  top: `${portalRect.top}px`,
361
374
  left: `${portalRect.left}px`,
362
375
  width: `${portalRect.width}px`,
363
376
  height: `${portalRect.height}px`,
377
+ minWidth: toolbarItemCount ? `${TOOLBAR_ITEM_WIDTH * toolbarItemCount}px` : undefined,
364
378
  ...omitToolbarPortalInsetStyle(toolbarStyle),
365
379
  });
366
380
 
@@ -514,6 +528,7 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
514
528
  showDragHandle = false,
515
529
  settingsMenuLevel,
516
530
  extraToolbarItems,
531
+ showDynamicFlowsEditor = true,
517
532
  toolbarStyle,
518
533
  toolbarPosition = 'inside',
519
534
  }: ModelProvidedProps) => {
@@ -575,6 +590,7 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
575
590
  flowEngine,
576
591
  settingsMenuLevel,
577
592
  extraToolbarItems,
593
+ showDynamicFlowsEditor,
578
594
  handleSettingsMenuOpenChange,
579
595
  getPopupContainer,
580
596
  )
@@ -588,6 +604,7 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
588
604
  settingsMenuLevel,
589
605
  showCopyUidButton,
590
606
  showDeleteButton,
607
+ showDynamicFlowsEditor,
591
608
  ],
592
609
  );
593
610
 
@@ -625,15 +642,17 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
625
642
  return <>{children}</>;
626
643
  }
627
644
 
645
+ const toolbarZIndex = (model.context.themeToken?.zIndexPopupBase || DEFAULT_POPUP_BASE_Z_INDEX) + 1;
628
646
  const toolbarContainerClassName = buildToolbarContainerClassName({
629
647
  showBackground,
630
648
  showBorder,
631
649
  ctx: model.context,
650
+ toolbarZIndex,
632
651
  portalRenderSnapshot,
633
652
  isToolbarVisible,
634
653
  className,
635
654
  });
636
- const toolbarContainerStyle = buildToolbarContainerStyle(portalRect, toolbarStyle);
655
+ const toolbarContainerStyle = buildToolbarContainerStyle(portalRect, toolbarStyle, toolbarItems.length);
637
656
 
638
657
  const toolbarNode = shouldRenderToolbar ? (
639
658
  <div