@nocobase/flow-engine 2.1.0-beta.24 → 2.1.0-beta.26

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.
@@ -851,6 +851,18 @@ const findCellByPath = /* @__PURE__ */ __name((layout, path) => {
851
851
  }
852
852
  return null;
853
853
  }, "findCellByPath");
854
+ const findCellByPathOrClosestAncestor = /* @__PURE__ */ __name((layout, path) => {
855
+ if (!(path == null ? void 0 : path.length)) {
856
+ return null;
857
+ }
858
+ for (let length = path.length; length > 0; length -= 1) {
859
+ const target = findCellByPath(layout, path.slice(0, length));
860
+ if (target) {
861
+ return target;
862
+ }
863
+ }
864
+ return null;
865
+ }, "findCellByPathOrClosestAncestor");
854
866
  const removeItemFromGridLayout = /* @__PURE__ */ __name((layout, sourceUid) => {
855
867
  const removeFromRows = /* @__PURE__ */ __name((rows) => rows.map((row) => {
856
868
  const cellsWithSizes = row.cells.map((cell, index) => {
@@ -918,7 +930,7 @@ const simulateGridLayoutForSlot = /* @__PURE__ */ __name(({
918
930
  removeItemFromGridLayout(cloned, sourceUid);
919
931
  switch (slot.type) {
920
932
  case "column": {
921
- const target = findCellByPath(cloned, targetPath);
933
+ const target = findCellByPathOrClosestAncestor(cloned, targetPath);
922
934
  if (!target) {
923
935
  break;
924
936
  }
@@ -934,7 +946,7 @@ const simulateGridLayoutForSlot = /* @__PURE__ */ __name(({
934
946
  break;
935
947
  }
936
948
  case "empty-column": {
937
- const target = findCellByPath(cloned, targetPath);
949
+ const target = findCellByPathOrClosestAncestor(cloned, targetPath);
938
950
  if (target) {
939
951
  delete target.cell.rows;
940
952
  target.cell.items = [sourceUid];
@@ -942,7 +954,7 @@ const simulateGridLayoutForSlot = /* @__PURE__ */ __name(({
942
954
  break;
943
955
  }
944
956
  case "column-edge": {
945
- const target = findCellByPath(cloned, targetPath);
957
+ const target = findCellByPathOrClosestAncestor(cloned, targetPath);
946
958
  if (!target) {
947
959
  break;
948
960
  }
@@ -969,7 +981,7 @@ const simulateGridLayoutForSlot = /* @__PURE__ */ __name(({
969
981
  if (!targetItemUid) {
970
982
  break;
971
983
  }
972
- const target = findCellByPath(cloned, targetPath);
984
+ const target = findCellByPathOrClosestAncestor(cloned, targetPath);
973
985
  if (!(target == null ? void 0 : target.cell.items)) {
974
986
  break;
975
987
  }
@@ -15,5 +15,6 @@ export interface SelectWithTitleProps {
15
15
  itemKey?: string;
16
16
  onChange?: (...args: any[]) => void;
17
17
  dropdownRender?: any;
18
+ tooltip?: any;
18
19
  }
19
- export declare function SelectWithTitle({ title, getDefaultValue, onChange, options, fieldNames, itemKey, ...others }: SelectWithTitleProps): React.JSX.Element;
20
+ export declare function SelectWithTitle({ title, getDefaultValue, onChange, options, fieldNames, itemKey, tooltip, ...others }: SelectWithTitleProps): React.JSX.Element;
@@ -50,6 +50,7 @@ function SelectWithTitle({
50
50
  options,
51
51
  fieldNames,
52
52
  itemKey,
53
+ tooltip,
53
54
  ...others
54
55
  }) {
55
56
  const [open, setOpen] = (0, import_react.useState)(false);
@@ -80,6 +81,18 @@ function SelectWithTitle({
80
81
  setValue(val);
81
82
  onChange == null ? void 0 : onChange({ [itemKey]: val });
82
83
  }, "handleChange");
84
+ const titleNode = /* @__PURE__ */ import_react.default.createElement(
85
+ "span",
86
+ {
87
+ style: {
88
+ whiteSpace: "nowrap",
89
+ // 不换行
90
+ flexShrink: 0
91
+ // 不被挤压
92
+ }
93
+ },
94
+ title
95
+ );
83
96
  return /* @__PURE__ */ import_react.default.createElement(
84
97
  "div",
85
98
  {
@@ -94,18 +107,7 @@ function SelectWithTitle({
94
107
  }, 200);
95
108
  }
96
109
  },
97
- /* @__PURE__ */ import_react.default.createElement(
98
- "span",
99
- {
100
- style: {
101
- whiteSpace: "nowrap",
102
- // 不换行
103
- flexShrink: 0
104
- // 不被挤压
105
- }
106
- },
107
- title
108
- ),
110
+ tooltip ? /* @__PURE__ */ import_react.default.createElement(import_antd.Tooltip, { title: tooltip, placement: "top", destroyTooltipOnHide: true }, titleNode) : titleNode,
109
111
  /* @__PURE__ */ import_react.default.createElement(
110
112
  import_antd.Select,
111
113
  {
@@ -622,6 +622,7 @@ const _FlowModel = class _FlowModel {
622
622
  } else {
623
623
  this.props = { ...this.props, ...props };
624
624
  }
625
+ this._options.props = { ...this.props };
625
626
  }
626
627
  getProps() {
627
628
  return this.props;
@@ -1163,6 +1164,7 @@ const _FlowModel = class _FlowModel {
1163
1164
  const data = {
1164
1165
  uid: this.uid,
1165
1166
  ...import_lodash.default.omit(this._options, ["flowEngine"]),
1167
+ props: { ...this.props },
1166
1168
  stepParams: this.stepParams,
1167
1169
  sortIndex: this.sortIndex,
1168
1170
  flowRegistry: {}
@@ -32,6 +32,10 @@ __export(createCollectionContextMeta_exports, {
32
32
  module.exports = __toCommonJS(createCollectionContextMeta_exports);
33
33
  const RELATION_FIELD_TYPES = ["belongsTo", "hasOne", "hasMany", "belongsToMany", "belongsToArray"];
34
34
  const NUMERIC_FIELD_TYPES = ["integer", "float", "double", "decimal"];
35
+ function shouldShowFieldInMeta(field, includeNonFilterable) {
36
+ return Boolean(field.interface && (includeNonFilterable || field.filterable));
37
+ }
38
+ __name(shouldShowFieldInMeta, "shouldShowFieldInMeta");
35
39
  function createFieldMetadata(field, includeNonFilterable) {
36
40
  const baseProperties = createMetaBaseProperties(field);
37
41
  if (field.isAssociationField()) {
@@ -49,7 +53,7 @@ function createFieldMetadata(field, includeNonFilterable) {
49
53
  properties: /* @__PURE__ */ __name(async () => {
50
54
  const subProperties = {};
51
55
  targetCollection.fields.forEach((subField) => {
52
- if (includeNonFilterable || subField.filterable) {
56
+ if (shouldShowFieldInMeta(subField, includeNonFilterable)) {
53
57
  subProperties[subField.name] = createFieldMetadata(subField, includeNonFilterable);
54
58
  }
55
59
  });
@@ -104,7 +108,7 @@ function createCollectionContextMeta(collectionOrFactory, title, includeNonFilte
104
108
  properties: /* @__PURE__ */ __name(async () => {
105
109
  const properties = {};
106
110
  collection.fields.forEach((field) => {
107
- if (includeNonFilterable || field.filterable) {
111
+ if (shouldShowFieldInMeta(field, includeNonFilterable)) {
108
112
  properties[field.name] = createFieldMetadata(field, includeNonFilterable);
109
113
  }
110
114
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.1.0-beta.24",
3
+ "version": "2.1.0-beta.26",
4
4
  "private": false,
5
5
  "description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
6
6
  "main": "lib/index.js",
@@ -8,8 +8,8 @@
8
8
  "dependencies": {
9
9
  "@formily/antd-v5": "1.x",
10
10
  "@formily/reactive": "2.x",
11
- "@nocobase/sdk": "2.1.0-beta.24",
12
- "@nocobase/shared": "2.1.0-beta.24",
11
+ "@nocobase/sdk": "2.1.0-beta.26",
12
+ "@nocobase/shared": "2.1.0-beta.26",
13
13
  "ahooks": "^3.7.2",
14
14
  "axios": "^1.7.0",
15
15
  "dayjs": "^1.11.9",
@@ -37,5 +37,5 @@
37
37
  ],
38
38
  "author": "NocoBase Team",
39
39
  "license": "Apache-2.0",
40
- "gitHead": "f77b85530a2d127d9bfe4dca3a26fbb02c1139ba"
40
+ "gitHead": "b17e1a72057813fa27d8435bf0f2af67ea4b059f"
41
41
  }
@@ -26,6 +26,7 @@ function setupEngineWithCollections() {
26
26
  fields: [
27
27
  { name: 'id', type: 'integer', interface: 'number' },
28
28
  { name: 'name', type: 'string', interface: 'text' },
29
+ { name: 'rawUserPayload', type: 'json', filterable: true },
29
30
  ],
30
31
  });
31
32
  ds.addCollection({
@@ -41,6 +42,8 @@ function setupEngineWithCollections() {
41
42
  filterTargetKey: 'id',
42
43
  fields: [
43
44
  { name: 'title', type: 'string', interface: 'text' },
45
+ { name: 'internalName', type: 'string', interface: 'text' },
46
+ { name: 'rawPostPayload', type: 'json', filterable: true },
44
47
  { name: 'author', type: 'belongsTo', target: 'users', interface: 'm2o' },
45
48
  { name: 'tags', type: 'belongsToMany', target: 'tags', interface: 'm2m' },
46
49
  ],
@@ -91,6 +94,27 @@ describe('objectVariable utilities', () => {
91
94
  });
92
95
  });
93
96
 
97
+ it('createAssociationAwareObjectMetaFactory should hide fields without interface from object variable meta', async () => {
98
+ const { collection } = setupEngineWithCollections();
99
+ const obj = { title: 'hello', internalName: 'internal', rawPostPayload: { secret: true }, author: 1 };
100
+ const metaFactory = createAssociationAwareObjectMetaFactory(
101
+ () => collection,
102
+ 'Current object',
103
+ () => obj,
104
+ );
105
+
106
+ const meta = await metaFactory();
107
+ const props = await (meta?.properties as any)?.();
108
+ const authorFields = await props?.author?.properties?.();
109
+
110
+ expect(props).toHaveProperty('title');
111
+ expect(props).toHaveProperty('internalName');
112
+ expect(props).toHaveProperty('author');
113
+ expect(props).not.toHaveProperty('rawPostPayload');
114
+ expect(authorFields).toHaveProperty('name');
115
+ expect(authorFields).not.toHaveProperty('rawUserPayload');
116
+ });
117
+
94
118
  it('integrates with FlowContext.resolveJsonTemplate to call variables:resolve with flattened contextParams', async () => {
95
119
  const { engine, collection } = setupEngineWithCollections();
96
120
  const obj = { author: 1 };
@@ -829,6 +829,52 @@ describe('simulateLayoutForSlot', () => {
829
829
  expect(nestedRows[1].sizes).toEqual([12, 12]);
830
830
  });
831
831
 
832
+ it('keeps nested column insertion target when removing a sibling collapses the original path', () => {
833
+ const layout = createLayout(
834
+ {
835
+ vyvfw2jw071: [['6ad3ccaabd5', 'ff8b4b57f65']],
836
+ ablhoqw51gb: [['21b422021b8']],
837
+ },
838
+ {
839
+ vyvfw2jw071: [24],
840
+ ablhoqw51gb: [24],
841
+ },
842
+ ['vyvfw2jw071', 'ablhoqw51gb'],
843
+ );
844
+ layout.layout = normalizeGridLayout({
845
+ rows: layout.rows,
846
+ sizes: layout.sizes,
847
+ rowOrder: layout.rowOrder,
848
+ itemUids: ['6ad3ccaabd5', 'ff8b4b57f65', '21b422021b8'],
849
+ });
850
+
851
+ const slot: LayoutSlot = {
852
+ type: 'column',
853
+ rowId: 'll5vo5pzj3u',
854
+ columnIndex: 0,
855
+ insertIndex: 1,
856
+ position: 'after',
857
+ path: [
858
+ { rowId: 'vyvfw2jw071', cellId: 'vyvfw2jw071:cell:0' },
859
+ { rowId: 'll5vo5pzj3u', cellId: 'ghy612j5zzg' },
860
+ ],
861
+ rect,
862
+ };
863
+
864
+ const result = simulateLayoutForSlot({ slot, sourceUid: 'ff8b4b57f65', layout });
865
+
866
+ expect(result.layout!.rows).toMatchObject([
867
+ {
868
+ id: 'vyvfw2jw071',
869
+ cells: [{ items: ['6ad3ccaabd5', 'ff8b4b57f65'] }],
870
+ },
871
+ {
872
+ id: 'ablhoqw51gb',
873
+ cells: [{ items: ['21b422021b8'] }],
874
+ },
875
+ ]);
876
+ });
877
+
832
878
  it('treats dragging an item to its own item-edge as no-op', () => {
833
879
  const layout = createLayout(
834
880
  {
@@ -1146,6 +1146,21 @@ const findCellByPath = (layout: GridLayoutV2, path?: GridLayoutPath) => {
1146
1146
  return null;
1147
1147
  };
1148
1148
 
1149
+ const findCellByPathOrClosestAncestor = (layout: GridLayoutV2, path?: GridLayoutPath) => {
1150
+ if (!path?.length) {
1151
+ return null;
1152
+ }
1153
+
1154
+ for (let length = path.length; length > 0; length -= 1) {
1155
+ const target = findCellByPath(layout, path.slice(0, length));
1156
+ if (target) {
1157
+ return target;
1158
+ }
1159
+ }
1160
+
1161
+ return null;
1162
+ };
1163
+
1149
1164
  const removeItemFromGridLayout = (layout: GridLayoutV2, sourceUid: string) => {
1150
1165
  const removeFromRows = (rows: GridRowV2[]): GridRowV2[] =>
1151
1166
  rows
@@ -1231,7 +1246,7 @@ const simulateGridLayoutForSlot = ({
1231
1246
 
1232
1247
  switch (slot.type) {
1233
1248
  case 'column': {
1234
- const target = findCellByPath(cloned, targetPath);
1249
+ const target = findCellByPathOrClosestAncestor(cloned, targetPath);
1235
1250
  if (!target) {
1236
1251
  break;
1237
1252
  }
@@ -1247,7 +1262,7 @@ const simulateGridLayoutForSlot = ({
1247
1262
  break;
1248
1263
  }
1249
1264
  case 'empty-column': {
1250
- const target = findCellByPath(cloned, targetPath);
1265
+ const target = findCellByPathOrClosestAncestor(cloned, targetPath);
1251
1266
  if (target) {
1252
1267
  delete target.cell.rows;
1253
1268
  target.cell.items = [sourceUid];
@@ -1255,7 +1270,7 @@ const simulateGridLayoutForSlot = ({
1255
1270
  break;
1256
1271
  }
1257
1272
  case 'column-edge': {
1258
- const target = findCellByPath(cloned, targetPath);
1273
+ const target = findCellByPathOrClosestAncestor(cloned, targetPath);
1259
1274
  if (!target) {
1260
1275
  break;
1261
1276
  }
@@ -1282,7 +1297,7 @@ const simulateGridLayoutForSlot = ({
1282
1297
  if (!targetItemUid) {
1283
1298
  break;
1284
1299
  }
1285
- const target = findCellByPath(cloned, targetPath);
1300
+ const target = findCellByPathOrClosestAncestor(cloned, targetPath);
1286
1301
  if (!target?.cell.items) {
1287
1302
  break;
1288
1303
  }
@@ -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
  });