@nocobase/client-v2 2.3.0-alpha.1 → 2.3.0-beta.11

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 (173) hide show
  1. package/es/APIClient.d.ts +12 -0
  2. package/es/components/form/ScanInput/useCodeScanner.d.ts +12 -2
  3. package/es/components/form/ScanInput/zxingWasmDecoder.d.ts +9 -0
  4. package/es/flow/actions/linkageRules.d.ts +25 -0
  5. package/es/flow/components/FieldAssignValueInput.d.ts +6 -30
  6. package/es/flow/components/FlowRoute.d.ts +2 -2
  7. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  8. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +32 -0
  9. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  10. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  11. package/es/flow/components/filter/VariableFilterItem.d.ts +7 -0
  12. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  13. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  14. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  15. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  16. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  17. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  18. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  19. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  20. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  21. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  22. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  23. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  24. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  25. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  26. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  27. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  28. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  29. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  30. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  31. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  32. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  33. package/es/flow-compat/passwordUtils.d.ts +1 -1
  34. package/es/index.d.ts +1 -0
  35. package/es/index.mjs +148 -145
  36. package/es/ui-operation/index.d.ts +15 -0
  37. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  38. package/lib/index.js +170 -167
  39. package/lib/locale/languageCodes.js +1 -0
  40. package/package.json +10 -8
  41. package/src/APIClient.ts +92 -0
  42. package/src/Application.tsx +3 -1
  43. package/src/__tests__/APIClient.test.tsx +58 -0
  44. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
  45. package/src/__tests__/settings-center.test.tsx +270 -6
  46. package/src/acl/ACLProvider.tsx +2 -0
  47. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  48. package/src/collection-manager/__tests__/field-configure.test.ts +52 -0
  49. package/src/collection-manager/field-configure.ts +2 -2
  50. package/src/components/form/ScanInput/CodeScanner.tsx +64 -35
  51. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +107 -0
  52. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +294 -24
  53. package/src/components/form/ScanInput/__tests__/zxingWasmDecoder.test.ts +78 -0
  54. package/src/components/form/ScanInput/useCodeScanner.ts +281 -23
  55. package/src/components/form/ScanInput/zxingWasmDecoder.ts +64 -0
  56. package/src/components/form/filter/CollectionFilterItem.tsx +4 -2
  57. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +17 -0
  58. package/src/components/form/table/Table.tsx +81 -6
  59. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  60. package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
  61. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  62. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  63. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  64. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  65. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  66. package/src/flow/actions/__tests__/validation.test.ts +48 -0
  67. package/src/flow/actions/linkageRules.tsx +59 -16
  68. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  69. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  70. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
  71. package/src/flow/common/Markdown/style.ts +4 -0
  72. package/src/flow/components/BlockItemCard.tsx +2 -2
  73. package/src/flow/components/FieldAssignValueInput.tsx +42 -621
  74. package/src/flow/components/FlowRoute.tsx +71 -29
  75. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  76. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +335 -0
  77. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +389 -0
  78. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  79. package/src/flow/components/field-value-variable/index.ts +12 -0
  80. package/src/flow/components/filter/VariableFilterItem.tsx +23 -6
  81. package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
  82. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
  83. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  84. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  85. package/src/flow/index.ts +2 -0
  86. package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
  87. package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
  88. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  89. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  90. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  91. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  92. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  93. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  94. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  95. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  96. package/src/flow/models/base/GridModel.tsx +0 -1
  97. package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
  98. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
  99. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  100. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  101. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  102. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  103. package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
  104. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +36 -50
  105. package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
  106. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  107. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  108. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
  109. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  110. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
  111. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  112. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  113. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
  114. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
  115. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  116. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  117. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
  118. package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
  119. package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
  120. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  121. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  122. package/src/flow/models/blocks/form/FormBlockModel.tsx +73 -5
  123. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  124. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +153 -2
  125. package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
  126. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  127. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  128. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  129. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  130. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  131. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  132. package/src/flow/models/blocks/table/TableBlockModel.tsx +28 -2
  133. package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
  134. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  135. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  136. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  137. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  138. package/src/flow/models/blocks/table/utils.ts +7 -0
  139. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  140. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +12 -3
  141. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  142. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  143. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +29 -3
  144. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
  145. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +5 -1
  146. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +191 -3
  147. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  148. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  149. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  150. package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
  151. package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
  152. package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
  153. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  154. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  155. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  156. package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
  157. package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
  158. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  159. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  160. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  161. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
  162. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
  163. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  164. package/src/flow/models/topbar/TopbarActionModel.tsx +5 -5
  165. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  166. package/src/index.ts +1 -0
  167. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  168. package/src/layout-manager/LayoutRoute.tsx +2 -1
  169. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  170. package/src/locale/languageCodes.ts +1 -0
  171. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  172. package/src/ui-operation/index.ts +33 -0
  173. package/src/ui-operation/ui-operation-codec.ts +54 -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
  });
@@ -21,6 +21,35 @@ describe('FilterFormGridModel.toggleFormFieldsCollapse', () => {
21
21
  engine.registerModels({ FilterFormGridModel });
22
22
  });
23
23
 
24
+ it.each([false, true])('preserves collapsed fields in mobile layout with settings enabled: %s', (settingsEnabled) => {
25
+ engine.flowSettings[settingsEnabled ? 'enable' : 'disable']();
26
+ const rows = {
27
+ first: [['field-1']],
28
+ second: [['field-2']],
29
+ third: [['field-3']],
30
+ };
31
+ const model = engine.createModel<FilterFormGridModel>({
32
+ uid: 'mobile-filter-grid',
33
+ use: 'FilterFormGridModel',
34
+ props: { rows },
35
+ subModels: {
36
+ items: ['field-1', 'field-2', 'field-3'].map((uid) => ({ use: 'FlowModel', uid })),
37
+ },
38
+ });
39
+ model.context.defineProperty('isMobileLayout', { value: true });
40
+ model.setStepParams(GRID_FLOW_KEY, GRID_STEP, { rows });
41
+ type VisibleLayoutReader = { getVisibleLayout(): { rows: Record<string, string[][]> } };
42
+ const renderedItems = () =>
43
+ Object.values((model as unknown as VisibleLayoutReader).getVisibleLayout().rows).flat(2);
44
+
45
+ expect(renderedItems()).toEqual(['field-1', 'field-2', 'field-3']);
46
+ model.toggleFormFieldsCollapse(true, 1);
47
+ expect(renderedItems()).toEqual(['field-1']);
48
+ model.toggleFormFieldsCollapse(false, 1);
49
+ expect(renderedItems()).toEqual(['field-1', 'field-2', 'field-3']);
50
+ expect(model.getStepParams(GRID_FLOW_KEY, GRID_STEP).rows).toEqual(rows);
51
+ });
52
+
24
53
  it('uses rowOrder from the full layout when collapsing after reorder', () => {
25
54
  const model = engine.createModel<FilterFormGridModel>({
26
55
  uid: 'filter-grid-collapse-order',
@@ -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,6 +34,20 @@ 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', () => {
36
52
  it('hides default operator setting for association filter fields', () => {
37
53
  const engine = new FlowEngine();
@@ -191,6 +207,348 @@ describe('FilterFormItemModel defineChildren association fields', () => {
191
207
  expect(filterItem.collectionField).toBeTruthy();
192
208
  });
193
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
+
194
552
  it('provides fallback field metadata for sql fields without collection context', async () => {
195
553
  const engine = new FlowEngine();
196
554
  engine.registerModels({