@nocobase/client-v2 2.2.0-beta.9 → 2.2.1

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 (224) hide show
  1. package/es/APIClient.d.ts +13 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  4. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  5. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  6. package/es/components/category-tabs/index.d.ts +9 -0
  7. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  8. package/es/components/form/TypedVariableInput.d.ts +31 -5
  9. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  10. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  11. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  12. package/es/components/form/filter/index.d.ts +2 -0
  13. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  14. package/es/components/index.d.ts +1 -0
  15. package/es/flow/actions/index.d.ts +1 -1
  16. package/es/flow/actions/linkageRules.d.ts +27 -0
  17. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  18. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  19. package/es/flow/components/FieldAssignValueInput.d.ts +7 -24
  20. package/es/flow/components/FlowRoute.d.ts +2 -2
  21. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  22. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  23. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  24. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  25. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  26. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  27. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  28. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  29. package/es/flow/index.d.ts +1 -0
  30. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  31. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  32. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  33. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  34. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  35. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  36. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  37. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  38. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  39. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  40. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  41. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  42. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  43. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  44. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  45. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  46. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  47. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  48. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  49. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  50. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  51. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  52. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  53. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  54. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  55. package/es/flow-compat/passwordUtils.d.ts +1 -1
  56. package/es/flow-compat/routeTypes.d.ts +1 -0
  57. package/es/index.d.ts +5 -0
  58. package/es/index.mjs +199 -161
  59. package/es/ui-operation/index.d.ts +15 -0
  60. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  61. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  62. package/es/utils/index.d.ts +1 -0
  63. package/lib/index.js +215 -177
  64. package/lib/locale/languageCodes.js +3 -1
  65. package/package.json +9 -7
  66. package/src/APIClient.ts +90 -7
  67. package/src/Application.tsx +7 -1
  68. package/src/BaseApplication.tsx +11 -6
  69. package/src/__tests__/APIClient.test.tsx +58 -0
  70. package/src/__tests__/app.test.tsx +73 -0
  71. package/src/__tests__/browserChecker.test.ts +61 -0
  72. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  73. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +35 -2
  74. package/src/__tests__/settings-center.test.tsx +270 -6
  75. package/src/acl/ACLProvider.tsx +2 -0
  76. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  77. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  78. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  79. package/src/collection-manager/field-validation.ts +1 -1
  80. package/src/components/README.md +7 -1
  81. package/src/components/README.zh-CN.md +6 -1
  82. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  83. package/src/components/category-tabs/index.ts +10 -0
  84. package/src/components/form/JsonTextArea.tsx +4 -0
  85. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  86. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  87. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  88. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  89. package/src/components/form/TypedVariableInput.tsx +452 -101
  90. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  91. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  92. package/src/components/form/filter/CollectionFilterItem.tsx +36 -9
  93. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  94. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +55 -0
  95. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  96. package/src/components/form/filter/index.ts +2 -0
  97. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  98. package/src/components/form/table/Table.tsx +81 -6
  99. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  100. package/src/components/index.ts +1 -0
  101. package/src/flow/FlowPage.tsx +9 -1
  102. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  103. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  104. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  105. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  106. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  107. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  108. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  109. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  110. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  111. package/src/flow/actions/index.ts +2 -0
  112. package/src/flow/actions/linkageRules.tsx +144 -26
  113. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  114. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
  115. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  116. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  117. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  118. package/src/flow/common/Markdown/style.ts +4 -0
  119. package/src/flow/components/BlockItemCard.tsx +2 -2
  120. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  121. package/src/flow/components/FieldAssignValueInput.tsx +72 -584
  122. package/src/flow/components/FlowRoute.tsx +78 -32
  123. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  124. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  125. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  126. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  127. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  128. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  129. package/src/flow/components/field-value-variable/index.ts +12 -0
  130. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  131. package/src/flow/components/filter/VariableFilterItem.tsx +170 -13
  132. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  133. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  134. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +195 -1
  135. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  136. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  137. package/src/flow/index.ts +3 -0
  138. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  139. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  140. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  141. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  142. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  143. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  144. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  145. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  146. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  147. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  148. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  149. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  152. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  153. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  154. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  155. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  156. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  157. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  158. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  159. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  160. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  161. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  162. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +155 -25
  163. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  164. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  165. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +395 -1
  166. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  167. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  168. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  169. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  170. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  171. package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
  172. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  173. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  174. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
  175. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  176. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  177. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  178. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  179. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  180. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  181. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  182. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  183. package/src/flow/models/blocks/table/TableBlockModel.tsx +29 -3
  184. package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
  185. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  186. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  187. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  188. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  189. package/src/flow/models/blocks/table/utils.ts +7 -0
  190. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  191. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +10 -1
  192. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  193. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  194. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
  195. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
  196. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
  197. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  198. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  199. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  200. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  201. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  202. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  203. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  204. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  205. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  206. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  207. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  208. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +26 -10
  209. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +51 -14
  210. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  211. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  212. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  213. package/src/flow-compat/routeTypes.ts +1 -0
  214. package/src/index.ts +5 -0
  215. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  216. package/src/layout-manager/LayoutRoute.tsx +2 -1
  217. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  218. package/src/locale/languageCodes.ts +3 -1
  219. package/src/nocobase-buildin-plugin/index.tsx +12 -0
  220. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  221. package/src/ui-operation/index.ts +33 -0
  222. package/src/ui-operation/ui-operation-codec.ts +54 -0
  223. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  224. package/src/utils/index.tsx +1 -0
@@ -15,7 +15,9 @@ import { FilterFormItemModel } from '../FilterFormItemModel';
15
15
  import { FilterFormGridModel } from '../FilterFormGridModel';
16
16
  import { InputFieldModel } from '../../../fields/InputFieldModel';
17
17
  import { NumberFieldModel } from '../../../fields/NumberFieldModel';
18
+ import { CascadeSelectFieldModel } from '../../../fields/AssociationFieldModel/CascadeSelectFieldModel';
18
19
  import { RecordSelectFieldModel } from '../../../fields/AssociationFieldModel/RecordSelectFieldModel';
20
+ import { FilterFormRecordSelectFieldModel } from '../fields/FilterFormRecordSelectFieldModel';
19
21
 
20
22
  class DummyResource {
21
23
  supportsFilter = true;
@@ -41,7 +43,9 @@ function createEngine() {
41
43
  DummyCollectionBlockModel,
42
44
  InputFieldModel,
43
45
  NumberFieldModel,
46
+ CascadeSelectFieldModel,
44
47
  RecordSelectFieldModel,
48
+ FilterFormRecordSelectFieldModel,
45
49
  });
46
50
 
47
51
  return engine;
@@ -76,7 +80,13 @@ function createGridModel(engine: FlowEngine) {
76
80
  return { gridModel, saveConnectFieldsConfig };
77
81
  }
78
82
 
79
- function createFilterItemModel(engine: FlowEngine, dataBlockModel: DummyCollectionBlockModel, fieldPath: string) {
83
+ function createFilterItemModel(
84
+ engine: FlowEngine,
85
+ dataBlockModel: DummyCollectionBlockModel,
86
+ fieldPath: string,
87
+ fieldModel?: string,
88
+ fieldProps?: Record<string, unknown>,
89
+ ) {
80
90
  const subModel = engine.createModel<FilterFormItemModel>({
81
91
  uid: `filter-item-${fieldPath}`,
82
92
  use: 'FilterFormItemModel',
@@ -94,6 +104,14 @@ function createFilterItemModel(engine: FlowEngine, dataBlockModel: DummyCollecti
94
104
  },
95
105
  },
96
106
  },
107
+ subModels: fieldModel
108
+ ? {
109
+ field: {
110
+ use: fieldModel,
111
+ props: fieldProps,
112
+ },
113
+ }
114
+ : undefined,
97
115
  });
98
116
 
99
117
  subModel.context.defineProperty('blockGridModel', {
@@ -244,4 +262,159 @@ describe('FilterFormGridModel onModelCreated', () => {
244
262
  ],
245
263
  });
246
264
  });
265
+
266
+ it('keeps the target collection filter key for record select relation fields with targetKey', async () => {
267
+ const engine = createEngine();
268
+ const ds = engine.dataSourceManager.getDataSource('main');
269
+
270
+ ds.addCollection({
271
+ name: 'departments',
272
+ filterTargetKey: 'id',
273
+ fields: [
274
+ { name: 'id', type: 'integer', interface: 'number', filterable: { operators: [] } },
275
+ { name: 'code', type: 'string', interface: 'input', filterable: { operators: [] } },
276
+ { name: 'name', type: 'string', interface: 'input', filterable: { operators: [] } },
277
+ ],
278
+ });
279
+ ds.addCollection({
280
+ name: 'users',
281
+ filterTargetKey: 'id',
282
+ fields: [
283
+ { name: 'id', type: 'integer', interface: 'number', filterable: { operators: [] } },
284
+ {
285
+ name: 'department',
286
+ type: 'belongsTo',
287
+ interface: 'm2o',
288
+ target: 'departments',
289
+ targetKey: 'code',
290
+ filterable: { operators: [] },
291
+ },
292
+ ],
293
+ });
294
+
295
+ const dataBlockModel = createDataBlockModel(engine);
296
+ const { gridModel, saveConnectFieldsConfig } = createGridModel(engine);
297
+ const subModel = createFilterItemModel(engine, dataBlockModel, 'department', 'FilterFormRecordSelectFieldModel');
298
+
299
+ await gridModel.onModelCreated(subModel);
300
+
301
+ expect(saveConnectFieldsConfig).toHaveBeenCalledTimes(1);
302
+ const [, payload] = saveConnectFieldsConfig.mock.calls[0] as unknown as [any, any];
303
+ expect(payload).toEqual({
304
+ targets: [
305
+ {
306
+ targetId: dataBlockModel.uid,
307
+ filterPaths: ['department.id'],
308
+ },
309
+ ],
310
+ });
311
+ });
312
+
313
+ it('falls back to association targetKey for non-record-select filter fields when target collection filter key is id', async () => {
314
+ const engine = createEngine();
315
+ const ds = engine.dataSourceManager.getDataSource('main');
316
+
317
+ ds.addCollection({
318
+ name: 'chinaRegions',
319
+ fields: [
320
+ { name: 'code', type: 'string', interface: 'input', filterable: { operators: [] } },
321
+ { name: 'name', type: 'string', interface: 'input', filterable: { operators: [] } },
322
+ ],
323
+ });
324
+ ds.addCollection({
325
+ name: 'users',
326
+ filterTargetKey: 'id',
327
+ fields: [
328
+ { name: 'id', type: 'integer', interface: 'number', filterable: { operators: [] } },
329
+ {
330
+ name: 'birthPlace',
331
+ type: 'belongsToMany',
332
+ interface: 'chinaRegion',
333
+ target: 'chinaRegions',
334
+ targetKey: 'code',
335
+ filterable: { operators: [] },
336
+ },
337
+ ],
338
+ });
339
+
340
+ const dataBlockModel = createDataBlockModel(engine);
341
+ const { gridModel, saveConnectFieldsConfig } = createGridModel(engine);
342
+ const subModel = createFilterItemModel(engine, dataBlockModel, 'birthPlace', 'InputFieldModel');
343
+
344
+ await gridModel.onModelCreated(subModel);
345
+
346
+ expect(saveConnectFieldsConfig).toHaveBeenCalledTimes(1);
347
+ const [, payload] = saveConnectFieldsConfig.mock.calls[0] as unknown as [any, any];
348
+ expect(payload).toEqual({
349
+ targets: [
350
+ {
351
+ targetId: dataBlockModel.uid,
352
+ filterPaths: ['birthPlace.code'],
353
+ },
354
+ ],
355
+ });
356
+ });
357
+
358
+ it('uses the Cascader value key for both the filter value and connected field path', async () => {
359
+ const engine = createEngine();
360
+ const ds = engine.dataSourceManager.getDataSource('main');
361
+
362
+ ds.addCollection({
363
+ name: 'organizations',
364
+ template: 'tree',
365
+ filterTargetKey: 'id',
366
+ fields: [
367
+ { name: 'id', type: 'integer', interface: 'number', filterable: { operators: [] } },
368
+ { name: 'code', type: 'string', interface: 'input', filterable: { operators: [] } },
369
+ { name: 'name', type: 'string', interface: 'input', filterable: { operators: [] } },
370
+ ],
371
+ });
372
+ ds.addCollection({
373
+ name: 'users',
374
+ filterTargetKey: 'id',
375
+ fields: [
376
+ { name: 'id', type: 'integer', interface: 'number', filterable: { operators: [] } },
377
+ {
378
+ name: 'organization',
379
+ type: 'belongsTo',
380
+ interface: 'm2o',
381
+ target: 'organizations',
382
+ targetKey: 'code',
383
+ filterable: { operators: [] },
384
+ },
385
+ ],
386
+ });
387
+
388
+ const dataBlockModel = createDataBlockModel(engine);
389
+ const { gridModel, saveConnectFieldsConfig } = createGridModel(engine);
390
+ const subModel = createFilterItemModel(engine, dataBlockModel, 'organization', 'CascadeSelectFieldModel', {
391
+ fieldNames: { label: 'name', value: 'id' },
392
+ value: {
393
+ id: 52,
394
+ code: 'ORG-52',
395
+ name: 'Leaf organization',
396
+ parent: { id: 1, code: 'ORG-1', name: 'Root organization', parent: null },
397
+ },
398
+ });
399
+ subModel.mounted = true;
400
+ subModel.context.defineProperty('form', {
401
+ value: {
402
+ getFieldValue: () => subModel.subModels.field.props.value,
403
+ },
404
+ });
405
+
406
+ await gridModel.onModelCreated(subModel);
407
+
408
+ expect(subModel.getFilterValue()).toBe(52);
409
+ expect(saveConnectFieldsConfig).toHaveBeenCalledTimes(1);
410
+ const [, payload] = saveConnectFieldsConfig.mock.calls[0] as unknown as [unknown, unknown];
411
+ expect(payload).toEqual({
412
+ targets: [
413
+ {
414
+ targetId: dataBlockModel.uid,
415
+ filterPaths: ['organization.id'],
416
+ },
417
+ ],
418
+ });
419
+ });
247
420
  });
@@ -9,11 +9,13 @@
9
9
 
10
10
  import { describe, expect, it } from 'vitest';
11
11
  import '../../../../index';
12
- import { FlowEngine } from '@nocobase/flow-engine';
12
+ import { FilterableItemModel, FlowEngine } from '@nocobase/flow-engine';
13
13
  import { CollectionBlockModel } from '../../../base';
14
+ import { CascadeSelectFieldModel } from '../../../fields/AssociationFieldModel/CascadeSelectFieldModel';
14
15
  import { InputFieldModel } from '../../../fields/InputFieldModel';
15
16
  import { NumberFieldModel } from '../../../fields/NumberFieldModel';
16
17
  import { FilterFormRecordSelectFieldModel } from '../fields/FilterFormRecordSelectFieldModel';
18
+ import { FilterFormFieldModel } from '../fields/FilterFormFieldModel';
17
19
  import { FilterFormItemModel } from '../FilterFormItemModel';
18
20
 
19
21
  class DummyResource {
@@ -32,7 +34,57 @@ class DummyCollectionBlockModel extends CollectionBlockModel {
32
34
  }
33
35
  }
34
36
 
37
+ class TestCascaderFilterFieldModel extends FilterFormFieldModel {}
38
+
39
+ FilterableItemModel.bindModelToInterface('TestCascaderFilterFieldModel', ['testChinaRegion'], {
40
+ isDefault: true,
41
+ defaultProps: {
42
+ fieldNames: {
43
+ label: 'name',
44
+ value: 'code',
45
+ },
46
+ labelInValue: true,
47
+ multiple: false,
48
+ },
49
+ });
50
+
35
51
  describe('FilterFormItemModel defineChildren association fields', () => {
52
+ it('hides default operator setting for association filter fields', () => {
53
+ const engine = new FlowEngine();
54
+ engine.registerModels({
55
+ FilterFormItemModel,
56
+ });
57
+
58
+ const filterItem = engine.createModel<FilterFormItemModel>({
59
+ uid: 'association-filter-item-settings',
60
+ use: 'FilterFormItemModel',
61
+ });
62
+
63
+ const defaultOperatorStep = filterItem.getFlow('filterFormItemSettings')?.steps?.defaultOperator as {
64
+ hideInSettings?: (ctx: {
65
+ collectionField?: unknown;
66
+ model?: {
67
+ subModels?: {
68
+ field?: {
69
+ context?: {
70
+ collectionField?: unknown;
71
+ };
72
+ };
73
+ };
74
+ };
75
+ }) => boolean;
76
+ };
77
+ expect(defaultOperatorStep?.hideInSettings?.({ collectionField: { isAssociationField: () => true } })).toBe(true);
78
+ expect(
79
+ defaultOperatorStep?.hideInSettings?.({
80
+ model: { subModels: { field: { context: { collectionField: { target: 'departments' } } } } },
81
+ }),
82
+ ).toBe(true);
83
+ expect(defaultOperatorStep?.hideInSettings?.({ collectionField: { interface: 'input', type: 'string' } })).toBe(
84
+ false,
85
+ );
86
+ });
87
+
36
88
  it('groups association target fields and supports recursive paths', async () => {
37
89
  const engine = new FlowEngine();
38
90
  engine.registerModels({
@@ -155,6 +207,348 @@ describe('FilterFormItemModel defineChildren association fields', () => {
155
207
  expect(filterItem.collectionField).toBeTruthy();
156
208
  });
157
209
 
210
+ it('lists filterable children from association field interfaces', async () => {
211
+ const engine = new FlowEngine();
212
+ engine.registerModels({
213
+ FilterFormItemModel: FilterFormItemModel as any,
214
+ DummyCollectionBlockModel,
215
+ InputFieldModel,
216
+ FilterFormRecordSelectFieldModel,
217
+ });
218
+ engine.context.defineProperty('t', {
219
+ value: (value: string) => (value === '{{t("Province/city/area name")}}' ? '省/市/区名称' : value),
220
+ });
221
+
222
+ const ds = engine.dataSourceManager.getDataSource('main');
223
+ ds?.addCollection({
224
+ name: 'students',
225
+ filterTargetKey: 'id',
226
+ fields: [
227
+ { name: 'id', type: 'integer', interface: 'number', filterable: { operators: [] } },
228
+ {
229
+ name: 'studentName',
230
+ title: 'Student name',
231
+ type: 'string',
232
+ interface: 'input',
233
+ filterable: { operators: [] },
234
+ },
235
+ {
236
+ name: 'birthPlace',
237
+ title: 'Birth place',
238
+ type: 'belongsToMany',
239
+ interface: 'chinaRegion',
240
+ target: 'chinaRegions',
241
+ filterable: {
242
+ children: [
243
+ {
244
+ name: 'name',
245
+ title: '{{t("Province/city/area name")}}',
246
+ operators: 'string',
247
+ schema: {
248
+ title: '{{t("Province/city/area name")}}',
249
+ type: 'string',
250
+ 'x-component': 'Input',
251
+ },
252
+ },
253
+ ],
254
+ },
255
+ },
256
+ ],
257
+ });
258
+
259
+ const model = engine.createModel<DummyCollectionBlockModel>({
260
+ uid: 'students-block',
261
+ use: 'DummyCollectionBlockModel',
262
+ stepParams: {
263
+ resourceSettings: {
264
+ init: {
265
+ dataSourceKey: 'main',
266
+ collectionName: 'students',
267
+ },
268
+ },
269
+ },
270
+ });
271
+ const getFilterFields = model.getFilterFields.bind(model);
272
+ model.getFilterFields = async () => {
273
+ const fields = await getFilterFields();
274
+ return fields.map((field: any) =>
275
+ field.name === 'birthPlace'
276
+ ? {
277
+ name: field.name,
278
+ title: field.title,
279
+ type: field.type,
280
+ interface: field.interface,
281
+ target: field.target,
282
+ filterable: field.filterable,
283
+ isAssociationField: () => true,
284
+ }
285
+ : field,
286
+ ) as any;
287
+ };
288
+
289
+ const children = (await FilterFormItemModel.defineChildren({
290
+ blockGridModel: {
291
+ filterSubModels: (_key: string, predicate: (item: any) => boolean) => [model].filter(predicate),
292
+ },
293
+ t: (value: string) => value,
294
+ } as any)) as any[];
295
+
296
+ const groups = await children[0].children();
297
+ const fieldsGroup = groups.find((group: any) => group.key === 'fields');
298
+ const associationGroup = groups.find((group: any) => group.key === 'relation-fields');
299
+ const fieldKeys = (fieldsGroup?.children || []).map((item: any) => item.key);
300
+ const associationKeys = (associationGroup?.children || []).map((item: any) => item.key);
301
+
302
+ expect(fieldKeys).toContain('studentName');
303
+ expect(associationKeys).toContain('birthPlace-associationField');
304
+ expect(associationKeys).not.toContain('studentName-associationField');
305
+
306
+ const regionAssociation = associationGroup?.children?.find(
307
+ (item: any) => item.key === 'birthPlace-associationField',
308
+ );
309
+ const regionGroups = await regionAssociation.children();
310
+ const regionFieldsGroup = regionGroups.find((group: any) => group.key === 'birthPlace-fields');
311
+ const regionNameItem = regionFieldsGroup?.children?.find((item: any) => item.key === 'birthPlace.name');
312
+
313
+ expect(regionNameItem?.label).toBe('Birth place / 省/市/区名称');
314
+
315
+ const createOptions = await regionNameItem.createModelOptions();
316
+ const filterItem = engine.createModel({
317
+ uid: 'filter-item-region-name',
318
+ ...createOptions,
319
+ } as any) as unknown as FilterFormItemModel;
320
+
321
+ expect(filterItem.fieldPath).toBe('birthPlace.name');
322
+ expect(filterItem.context.filterField).toMatchObject({
323
+ name: 'name',
324
+ title: 'Birth place / 省/市/区名称',
325
+ interface: 'input',
326
+ type: 'string',
327
+ });
328
+ expect(filterItem.context.filterField?.filterable?.operators).toBe('string');
329
+ expect(filterItem.collectionField).toMatchObject({
330
+ name: 'name',
331
+ title: 'Birth place / 省/市/区名称',
332
+ interface: 'input',
333
+ type: 'string',
334
+ });
335
+ expect(filterItem.collectionField?.filterable?.operators).toBe('string');
336
+ expect(filterItem.subModels.field).toBeInstanceOf(InputFieldModel);
337
+ });
338
+
339
+ it('uses explicit filterable binding for association fields that provide a filter model', async () => {
340
+ const engine = new FlowEngine();
341
+ engine.registerModels({
342
+ FilterFormItemModel: FilterFormItemModel as any,
343
+ DummyCollectionBlockModel,
344
+ InputFieldModel,
345
+ TestCascaderFilterFieldModel,
346
+ FilterFormRecordSelectFieldModel,
347
+ });
348
+
349
+ const ds = engine.dataSourceManager.getDataSource('main');
350
+ ds?.addCollection({
351
+ name: 'departments',
352
+ filterTargetKey: 'id',
353
+ fields: [
354
+ { name: 'id', type: 'integer', interface: 'number', filterable: { operators: [] } },
355
+ { name: 'name', type: 'string', interface: 'input', filterable: { operators: [] } },
356
+ ],
357
+ });
358
+ ds?.addCollection({
359
+ name: 'chinaRegions',
360
+ fields: [
361
+ { name: 'code', type: 'string', interface: 'input', filterable: { operators: [] } },
362
+ { name: 'name', type: 'string', interface: 'input', filterable: { operators: [] } },
363
+ ],
364
+ });
365
+ ds?.addCollection({
366
+ name: 'students',
367
+ filterTargetKey: 'id',
368
+ fields: [
369
+ {
370
+ name: 'department',
371
+ title: 'Department',
372
+ type: 'belongsTo',
373
+ interface: 'm2o',
374
+ target: 'departments',
375
+ filterable: { operators: [] },
376
+ },
377
+ {
378
+ name: 'birthPlace',
379
+ title: 'Birth place',
380
+ type: 'belongsToMany',
381
+ interface: 'testChinaRegion',
382
+ target: 'chinaRegions',
383
+ targetKey: 'code',
384
+ filterable: { operators: [] },
385
+ },
386
+ ],
387
+ });
388
+
389
+ const model = engine.createModel<DummyCollectionBlockModel>({
390
+ uid: 'students-direct-region-block',
391
+ use: 'DummyCollectionBlockModel',
392
+ stepParams: {
393
+ resourceSettings: {
394
+ init: {
395
+ dataSourceKey: 'main',
396
+ collectionName: 'students',
397
+ },
398
+ },
399
+ },
400
+ });
401
+
402
+ const children = (await FilterFormItemModel.defineChildren({
403
+ blockGridModel: {
404
+ filterSubModels: (_key: string, predicate: (item: any) => boolean) => [model].filter(predicate),
405
+ },
406
+ t: (value: string) => value,
407
+ } as any)) as any[];
408
+ const groups = await children[0].children();
409
+ const fieldsGroup = groups.find((group: any) => group.key === 'fields');
410
+ const departmentItem = fieldsGroup?.children?.find((item: any) => item.key === 'department');
411
+ const birthPlaceItem = fieldsGroup?.children?.find((item: any) => item.key === 'birthPlace');
412
+
413
+ const departmentCreateOptions = await departmentItem.createModelOptions();
414
+ const birthPlaceCreateOptions = await birthPlaceItem.createModelOptions();
415
+
416
+ expect(departmentCreateOptions.subModels.field.use).toBe('FilterFormRecordSelectFieldModel');
417
+ expect(birthPlaceCreateOptions.subModels.field.use).toBe('TestCascaderFilterFieldModel');
418
+ expect(birthPlaceCreateOptions.subModels.field.props).toMatchObject({
419
+ fieldNames: {
420
+ label: 'name',
421
+ value: 'code',
422
+ },
423
+ labelInValue: true,
424
+ multiple: false,
425
+ });
426
+
427
+ const filterItem = engine.createModel({
428
+ uid: 'filter-item-birth-place-direct',
429
+ ...birthPlaceCreateOptions,
430
+ } as any) as unknown as FilterFormItemModel;
431
+
432
+ expect(filterItem.fieldPath).toBe('birthPlace');
433
+ expect(filterItem.subModels.field).toBeInstanceOf(TestCascaderFilterFieldModel);
434
+ });
435
+
436
+ it('uses Cascader by default for tree to-one associations and keeps Dropdown for other associations', async () => {
437
+ const engine = new FlowEngine();
438
+ engine.registerModels({
439
+ FilterFormItemModel: FilterFormItemModel as any,
440
+ DummyCollectionBlockModel,
441
+ CascadeSelectFieldModel,
442
+ FilterFormRecordSelectFieldModel,
443
+ });
444
+
445
+ const ds = engine.dataSourceManager.getDataSource('main');
446
+ ds?.addCollection({
447
+ name: 'organizations',
448
+ template: 'tree',
449
+ filterTargetKey: 'id',
450
+ titleField: 'name',
451
+ fields: [
452
+ { name: 'id', type: 'integer', interface: 'number', filterable: { operators: [] } },
453
+ { name: 'name', type: 'string', interface: 'input', filterable: { operators: [] } },
454
+ ],
455
+ });
456
+ ds?.addCollection({
457
+ name: 'departments',
458
+ filterTargetKey: 'id',
459
+ fields: [
460
+ { name: 'id', type: 'integer', interface: 'number', filterable: { operators: [] } },
461
+ { name: 'name', type: 'string', interface: 'input', filterable: { operators: [] } },
462
+ ],
463
+ });
464
+ ds?.addCollection({
465
+ name: 'users',
466
+ filterTargetKey: 'id',
467
+ fields: [
468
+ {
469
+ name: 'organization',
470
+ title: 'Organization',
471
+ type: 'belongsTo',
472
+ interface: 'm2o',
473
+ target: 'organizations',
474
+ filterable: { operators: [] },
475
+ },
476
+ {
477
+ name: 'department',
478
+ title: 'Department',
479
+ type: 'belongsTo',
480
+ interface: 'm2o',
481
+ target: 'departments',
482
+ filterable: { operators: [] },
483
+ },
484
+ {
485
+ name: 'organizations',
486
+ title: 'Organizations',
487
+ type: 'belongsToMany',
488
+ interface: 'm2m',
489
+ target: 'organizations',
490
+ filterable: { operators: [] },
491
+ },
492
+ ],
493
+ });
494
+
495
+ const model = engine.createModel<DummyCollectionBlockModel>({
496
+ uid: 'users-tree-association-block',
497
+ use: 'DummyCollectionBlockModel',
498
+ stepParams: {
499
+ resourceSettings: {
500
+ init: {
501
+ dataSourceKey: 'main',
502
+ collectionName: 'users',
503
+ },
504
+ },
505
+ },
506
+ });
507
+
508
+ const children = (await FilterFormItemModel.defineChildren({
509
+ blockGridModel: {
510
+ filterSubModels: (_key: string, predicate: (item: any) => boolean) => [model].filter(predicate),
511
+ },
512
+ t: (value: string) => value,
513
+ } as any)) as any[];
514
+ const groups = await children[0].children();
515
+ const fieldsGroup = groups.find((group: any) => group.key === 'fields');
516
+ const organizationItem = fieldsGroup?.children?.find((item: any) => item.key === 'organization');
517
+ const departmentItem = fieldsGroup?.children?.find((item: any) => item.key === 'department');
518
+ const organizationsItem = fieldsGroup?.children?.find((item: any) => item.key === 'organizations');
519
+
520
+ const organizationCreateOptions = await organizationItem.createModelOptions();
521
+ const departmentCreateOptions = await departmentItem.createModelOptions();
522
+ const organizationsCreateOptions = await organizationsItem.createModelOptions();
523
+
524
+ expect(organizationCreateOptions.subModels.field.use).toBe('CascadeSelectFieldModel');
525
+ expect(organizationCreateOptions.subModels.field.props).toMatchObject({
526
+ fieldNames: { label: 'name', value: 'id' },
527
+ });
528
+ expect(departmentCreateOptions.subModels.field.use).toBe('FilterFormRecordSelectFieldModel');
529
+ expect(organizationsCreateOptions.subModels.field.use).toBe('FilterFormRecordSelectFieldModel');
530
+
531
+ const filterItem = engine.createModel<FilterFormItemModel>({
532
+ uid: 'filter-item-tree-organization',
533
+ ...organizationCreateOptions,
534
+ });
535
+ const modelStep = filterItem.getFlow('filterFormItemSettings')?.steps?.model as any;
536
+ const settingsContext = {
537
+ engine,
538
+ collectionField: ds?.getCollection('users')?.getField('organization'),
539
+ t: (value: string) => engine.translate(value),
540
+ };
541
+ const options = modelStep.uiMode(settingsContext).props.options;
542
+
543
+ expect(modelStep.hideInSettings(settingsContext)).not.toBe(true);
544
+ expect(options).toEqual(
545
+ expect.arrayContaining([
546
+ { label: 'Dropdown select', value: 'FilterFormRecordSelectFieldModel' },
547
+ { label: 'Cascader', value: 'CascadeSelectFieldModel' },
548
+ ]),
549
+ );
550
+ });
551
+
158
552
  it('provides fallback field metadata for sql fields without collection context', async () => {
159
553
  const engine = new FlowEngine();
160
554
  engine.registerModels({