@nocobase/flow-engine 2.1.0-beta.23 → 2.1.0-beta.25

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.
@@ -73,7 +73,7 @@ const resolveDraggableHostNode = (activatorNode: HTMLElement | null) => {
73
73
  `[data-has-float-menu="true"][data-float-menu-model-uid="${toolbarModelUid}"]`,
74
74
  ),
75
75
  );
76
- const popupRoot = floatToolbarContainer.closest<HTMLElement>(MENU_SUBMENU_POPUP_SELECTOR);
76
+ const popupRoot = floatToolbarContainer?.closest<HTMLElement>(MENU_SUBMENU_POPUP_SELECTOR);
77
77
 
78
78
  if (popupRoot) {
79
79
  return (
@@ -287,7 +287,9 @@ export const Droppable: FC<{ model: FlowModel<any>; children: React.ReactNode }>
287
287
  export const DndProvider: FC<DndContextProps & PersistOptions> = ({
288
288
  persist = true,
289
289
  children,
290
+ onDragStart,
290
291
  onDragEnd,
292
+ onDragCancel,
291
293
  ...restProps
292
294
  }) => {
293
295
  const [activeId, setActiveId] = useState<string | null>(null);
@@ -327,9 +329,10 @@ export const DndProvider: FC<DndContextProps & PersistOptions> = ({
327
329
 
328
330
  return (
329
331
  <DndContext
332
+ {...restProps}
330
333
  onDragStart={(event) => {
331
334
  setActiveId(event.active.id as string);
332
- restProps.onDragStart?.(event);
335
+ onDragStart?.(event);
333
336
  }}
334
337
  onDragEnd={(event) => {
335
338
  setActiveId(null);
@@ -347,7 +350,7 @@ export const DndProvider: FC<DndContextProps & PersistOptions> = ({
347
350
  onDragCancel={(event) => {
348
351
  setActiveId(null);
349
352
  setDragAnchorPoint(null);
350
- restProps.onDragCancel?.(event);
353
+ onDragCancel?.(event);
351
354
  }}
352
355
  {...restProps}
353
356
  >
@@ -362,6 +365,7 @@ export const DndProvider: FC<DndContextProps & PersistOptions> = ({
362
365
  >
363
366
  {activeId && (
364
367
  <span
368
+ data-testid="flow-drag-preview"
365
369
  style={{
366
370
  display: 'inline-flex',
367
371
  alignItems: 'center',
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { Select } from 'antd';
10
+ import { Select, Tooltip } from 'antd';
11
11
  import React, { useEffect, useRef, useState } from 'react';
12
12
  import { useFlowEngineContext } from '../../../../provider';
13
13
 
@@ -19,6 +19,7 @@ export interface SelectWithTitleProps {
19
19
  itemKey?: string;
20
20
  onChange?: (...args: any[]) => void;
21
21
  dropdownRender?: any;
22
+ tooltip?: any;
22
23
  }
23
24
 
24
25
  export function SelectWithTitle({
@@ -28,6 +29,7 @@ export function SelectWithTitle({
28
29
  options,
29
30
  fieldNames,
30
31
  itemKey,
32
+ tooltip,
31
33
  ...others
32
34
  }: SelectWithTitleProps) {
33
35
  const [open, setOpen] = useState(false);
@@ -66,6 +68,17 @@ export function SelectWithTitle({
66
68
  setValue(val);
67
69
  onChange?.({ [itemKey]: val });
68
70
  };
71
+ const titleNode = (
72
+ <span
73
+ style={{
74
+ whiteSpace: 'nowrap', // 不换行
75
+ flexShrink: 0, // 不被挤压
76
+ }}
77
+ >
78
+ {title}
79
+ </span>
80
+ );
81
+
69
82
  return (
70
83
  <div
71
84
  style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}
@@ -79,14 +92,13 @@ export function SelectWithTitle({
79
92
  }, 200);
80
93
  }}
81
94
  >
82
- <span
83
- style={{
84
- whiteSpace: 'nowrap', // 不换行
85
- flexShrink: 0, // 不被挤压
86
- }}
87
- >
88
- {title}
89
- </span>
95
+ {tooltip ? (
96
+ <Tooltip title={tooltip} placement="top" destroyTooltipOnHide>
97
+ {titleNode}
98
+ </Tooltip>
99
+ ) : (
100
+ titleNode
101
+ )}
90
102
  <Select
91
103
  {...others}
92
104
  open={open}
@@ -1855,7 +1855,7 @@ describe('FlowModel', () => {
1855
1855
  });
1856
1856
 
1857
1857
  describe('serialization', () => {
1858
- test('should serialize basic model data, excluding props and flowEngine', () => {
1858
+ test('should serialize basic model data with the latest props, excluding flowEngine', () => {
1859
1859
  model.sortIndex = 5;
1860
1860
  model.setProps({ name: 'Test Model', value: 42 });
1861
1861
  model.setStepParams({
@@ -1867,13 +1867,12 @@ describe('FlowModel', () => {
1867
1867
  expect(serialized).toEqual(
1868
1868
  expect.objectContaining({
1869
1869
  uid: model.uid,
1870
+ props: expect.objectContaining({ name: 'Test Model', value: 42 }),
1870
1871
  stepParams: expect.objectContaining({ flow1: { step1: { param1: 'value1' } } }),
1871
1872
  sortIndex: 5,
1872
1873
  subModels: expect.any(Object),
1873
1874
  }),
1874
1875
  );
1875
- // props should be excluded from serialization
1876
- expect(serialized.props).toBeUndefined();
1877
1876
  expect(serialized.flowEngine).toBeUndefined();
1878
1877
  });
1879
1878
 
@@ -1892,6 +1891,7 @@ describe('FlowModel', () => {
1892
1891
  expect(serialized).toEqual(
1893
1892
  expect.objectContaining({
1894
1893
  uid: 'empty-model',
1894
+ props: expect.objectContaining({ foo: 'bar' }),
1895
1895
  stepParams: expect.any(Object),
1896
1896
  sortIndex: expect.any(Number),
1897
1897
  subModels: expect.any(Object),
@@ -1899,6 +1899,22 @@ describe('FlowModel', () => {
1899
1899
  );
1900
1900
  expect(serialized.flowEngine).toBeUndefined();
1901
1901
  });
1902
+
1903
+ test('should serialize the latest props after multiple updates', () => {
1904
+ model.setProps({ fieldNames: { title: 'name' }, searchable: true });
1905
+ model.setProps({ fieldNames: { title: 'age' } });
1906
+ model.setProps('defaultExpandAll', false);
1907
+
1908
+ const serialized = model.serialize();
1909
+
1910
+ expect(serialized.props).toEqual(
1911
+ expect.objectContaining({
1912
+ fieldNames: { title: 'age' },
1913
+ searchable: true,
1914
+ defaultExpandAll: false,
1915
+ }),
1916
+ );
1917
+ });
1902
1918
  });
1903
1919
  });
1904
1920
 
@@ -762,6 +762,8 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
762
762
  } else {
763
763
  this.props = { ...this.props, ...props };
764
764
  }
765
+
766
+ this._options.props = { ...this.props };
765
767
  }
766
768
 
767
769
  getProps(): ReadonlyModelProps {
@@ -1509,6 +1511,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1509
1511
  const data = {
1510
1512
  uid: this.uid,
1511
1513
  ..._.omit(this._options, ['flowEngine']),
1514
+ props: { ...this.props },
1512
1515
  stepParams: this.stepParams,
1513
1516
  sortIndex: this.sortIndex,
1514
1517
  flowRegistry: {},
@@ -26,6 +26,7 @@ describe('createCollectionContextMeta', () => {
26
26
  { name: 'id', type: 'integer', interface: 'number', filterable: true },
27
27
  { name: 'email', type: 'string', interface: 'text', filterable: true },
28
28
  { name: 'nickname', type: 'string', interface: 'text' }, // 未声明 filterable
29
+ { name: 'rawUserPayload', type: 'json', filterable: true },
29
30
  ],
30
31
  });
31
32
 
@@ -34,6 +35,7 @@ describe('createCollectionContextMeta', () => {
34
35
  fields: [
35
36
  { name: 'title', type: 'string', interface: 'text', filterable: true },
36
37
  { name: 'author', type: 'belongsTo', target: 'users', interface: 'm2o', filterable: true },
38
+ { name: 'rawPostPayload', type: 'json', filterable: true },
37
39
  ],
38
40
  });
39
41
 
@@ -44,8 +46,54 @@ describe('createCollectionContextMeta', () => {
44
46
  const authorMeta: any = props?.author;
45
47
  const authorFields = await authorMeta?.properties?.();
46
48
 
49
+ expect(props).toHaveProperty('title');
50
+ expect(props).toHaveProperty('author');
51
+ expect(props).not.toHaveProperty('rawPostPayload');
47
52
  expect(authorFields).toBeTruthy();
48
53
  expect(authorFields).toHaveProperty('email');
49
54
  expect(authorFields).not.toHaveProperty('nickname');
55
+ expect(authorFields).not.toHaveProperty('rawUserPayload');
56
+ });
57
+
58
+ it('keeps interfaced non-filterable fields but hides fields without interface when includeNonFilterable is true', async () => {
59
+ const engine = new FlowEngine();
60
+ const dm = engine.dataSourceManager as any;
61
+ dm.collectionFieldInterfaceManager = new CollectionFieldInterfaceManager([], {}, dm);
62
+ engine.context.defineProperty('app', { value: { dataSourceManager: dm } });
63
+ const ds = dm.getDataSource('main')!;
64
+
65
+ ds.addCollection({
66
+ name: 'users',
67
+ fields: [
68
+ { name: 'id', type: 'integer', interface: 'number', filterable: true },
69
+ { name: 'email', type: 'string', interface: 'text', filterable: true },
70
+ { name: 'nickname', type: 'string', interface: 'text' },
71
+ { name: 'rawUserPayload', type: 'json', filterable: true },
72
+ ],
73
+ });
74
+
75
+ ds.addCollection({
76
+ name: 'posts',
77
+ fields: [
78
+ { name: 'title', type: 'string', interface: 'text', filterable: true },
79
+ { name: 'internalName', type: 'string', interface: 'text' },
80
+ { name: 'rawPostPayload', type: 'json', filterable: true },
81
+ { name: 'author', type: 'belongsTo', target: 'users', interface: 'm2o', filterable: true },
82
+ ],
83
+ });
84
+
85
+ const posts = ds.getCollection('posts')!;
86
+ const metaFactory = createCollectionContextMeta(posts, 'Posts', true);
87
+ const meta = await metaFactory();
88
+ const props = await (meta?.properties as any)?.();
89
+ const authorFields = await props?.author?.properties?.();
90
+
91
+ expect(props).toHaveProperty('title');
92
+ expect(props).toHaveProperty('internalName');
93
+ expect(props).toHaveProperty('author');
94
+ expect(props).not.toHaveProperty('rawPostPayload');
95
+ expect(authorFields).toHaveProperty('email');
96
+ expect(authorFields).toHaveProperty('nickname');
97
+ expect(authorFields).not.toHaveProperty('rawUserPayload');
50
98
  });
51
99
  });
@@ -14,6 +14,10 @@ import type { PropertyMetaFactory } from '../flowContext';
14
14
  const RELATION_FIELD_TYPES = ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany', 'belongsToArray'] as const;
15
15
  const NUMERIC_FIELD_TYPES = ['integer', 'float', 'double', 'decimal'] as const;
16
16
 
17
+ function shouldShowFieldInMeta(field: CollectionField, includeNonFilterable?: boolean) {
18
+ return Boolean(field.interface && (includeNonFilterable || field.filterable));
19
+ }
20
+
17
21
  /**
18
22
  * 创建字段的完整元数据(统一处理关联和非关联字段)
19
23
  */
@@ -36,7 +40,7 @@ function createFieldMetadata(field: CollectionField, includeNonFilterable?: bool
36
40
  properties: async () => {
37
41
  const subProperties: Record<string, any> = {};
38
42
  targetCollection.fields.forEach((subField) => {
39
- if (includeNonFilterable || subField.filterable) {
43
+ if (shouldShowFieldInMeta(subField, includeNonFilterable)) {
40
44
  subProperties[subField.name] = createFieldMetadata(subField, includeNonFilterable);
41
45
  }
42
46
  });
@@ -114,7 +118,7 @@ export function createCollectionContextMeta(
114
118
 
115
119
  // 添加所有字段
116
120
  collection.fields.forEach((field) => {
117
- if (includeNonFilterable || field.filterable) {
121
+ if (shouldShowFieldInMeta(field, includeNonFilterable)) {
118
122
  properties[field.name] = createFieldMetadata(field, includeNonFilterable);
119
123
  }
120
124
  });