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

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 (171) 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/linkageRules.tsx +59 -16
  67. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  68. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  69. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
  70. package/src/flow/common/Markdown/style.ts +4 -0
  71. package/src/flow/components/BlockItemCard.tsx +2 -2
  72. package/src/flow/components/FieldAssignValueInput.tsx +42 -621
  73. package/src/flow/components/FlowRoute.tsx +71 -29
  74. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  75. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +335 -0
  76. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +389 -0
  77. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  78. package/src/flow/components/field-value-variable/index.ts +12 -0
  79. package/src/flow/components/filter/VariableFilterItem.tsx +23 -6
  80. package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
  81. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
  82. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  83. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  84. package/src/flow/index.ts +2 -0
  85. package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
  86. package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
  87. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  88. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  89. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  90. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  91. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  92. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  93. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  94. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  95. package/src/flow/models/base/GridModel.tsx +0 -1
  96. package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
  97. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
  98. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  99. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  100. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  101. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  102. package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
  103. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +36 -50
  104. package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
  105. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  106. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  107. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
  108. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  109. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
  110. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  111. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  112. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
  113. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
  114. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  115. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  116. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
  117. package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
  118. package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
  119. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  120. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  121. package/src/flow/models/blocks/form/FormBlockModel.tsx +73 -5
  122. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  123. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +153 -2
  124. package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
  125. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  126. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  127. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  128. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  129. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  130. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  131. package/src/flow/models/blocks/table/TableBlockModel.tsx +28 -2
  132. package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
  133. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  134. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  135. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  136. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  137. package/src/flow/models/blocks/table/utils.ts +7 -0
  138. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  139. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
  140. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  141. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  142. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +29 -3
  143. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
  144. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +191 -3
  145. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  146. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  147. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  148. package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
  149. package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
  150. package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
  151. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  152. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  153. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  154. package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
  155. package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
  156. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  157. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  158. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  159. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
  160. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
  161. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  162. package/src/flow/models/topbar/TopbarActionModel.tsx +5 -5
  163. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  164. package/src/index.ts +1 -0
  165. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  166. package/src/layout-manager/LayoutRoute.tsx +2 -1
  167. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  168. package/src/locale/languageCodes.ts +1 -0
  169. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  170. package/src/ui-operation/index.ts +33 -0
  171. package/src/ui-operation/ui-operation-codec.ts +54 -0
@@ -20,6 +20,7 @@ import {
20
20
  SingleRecordResource,
21
21
  createCurrentRecordMetaFactory,
22
22
  createRecordResolveOnServerWithLocal,
23
+ observer,
23
24
  tExpr,
24
25
  } from '@nocobase/flow-engine';
25
26
  import { Pagination, Space } from 'antd';
@@ -154,8 +155,8 @@ export class DetailsBlockModel extends CollectionBlockModel<{
154
155
  simple
155
156
  pageSize={1}
156
157
  showSizeChanger={false}
157
- defaultCurrent={resource.getPage()}
158
- total={resource.getTotalPage()}
158
+ current={resource.getPage()}
159
+ total={resource.getCount()}
159
160
  onChange={this.handlePageChange}
160
161
  style={{ display: 'inline-block' }}
161
162
  />
@@ -226,6 +227,8 @@ export class DetailsBlockModel extends CollectionBlockModel<{
226
227
  }
227
228
  }
228
229
 
230
+ export const DetailsPagination = observer(({ model }: { model: DetailsBlockModel }) => model.renderPagination());
231
+
229
232
  const DetailsBlockContent = ({
230
233
  model,
231
234
  gridModel,
@@ -297,7 +300,9 @@ const DetailsBlockContent = ({
297
300
  model={gridModel}
298
301
  showFlowSettings={false}
299
302
  />
300
- <div ref={paginationRef}>{model.renderPagination()}</div>
303
+ <div ref={paginationRef}>
304
+ <DetailsPagination model={model} />
305
+ </div>
301
306
  </div>
302
307
  </FormComponent>
303
308
  );
@@ -7,10 +7,12 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
+ import { act, render, screen } from '@testing-library/react';
10
11
  import { beforeEach, describe, expect, it, vi } from 'vitest';
11
12
  import { FlowEngine, MultiRecordResource, SingleRecordResource } from '@nocobase/flow-engine';
13
+ import React from 'react';
12
14
  import '../../../../index';
13
- import { DetailsBlockModel } from '../DetailsBlockModel';
15
+ import { DetailsBlockModel, DetailsPagination } from '../DetailsBlockModel';
14
16
  import { DetailsGridModel } from '../DetailsGridModel';
15
17
 
16
18
  function setupDetailsBlockModel(options?: { filterByTk?: string | number }) {
@@ -93,6 +95,35 @@ describe('DetailsBlockModel initial pagination refresh', () => {
93
95
  expect(dispatchSpy.mock.calls.filter(([eventName]) => eventName === 'paginationChange')).toHaveLength(1);
94
96
  });
95
97
 
98
+ it('renders pagination from the current count and page after filtering', () => {
99
+ const { model, resource } = setupDetailsBlockModel();
100
+ resource.setData([{ id: 3, name: 'CC', title: 'filtered' }] as any);
101
+ resource.setMeta({ count: 7, page: 3, pageSize: 1, totalPage: 20 });
102
+
103
+ const paginationContainer = model.renderPagination() as any;
104
+ const pagination = paginationContainer.props.children;
105
+
106
+ expect(pagination.props.total).toBe(7);
107
+ expect(pagination.props.current).toBe(3);
108
+ expect(pagination.props.defaultCurrent).toBeUndefined();
109
+ });
110
+
111
+ it('updates the rendered pagination when filtering changes only resource metadata', () => {
112
+ const { model, resource } = setupDetailsBlockModel();
113
+ resource.setData([{ id: 1, name: 'AA', title: 'foo' }] as any);
114
+ resource.setMeta({ count: 7, page: 1, pageSize: 1, totalPage: 7 });
115
+
116
+ render(React.createElement(DetailsPagination, { model }));
117
+
118
+ expect(screen.getByRole('list')).toBeInTheDocument();
119
+
120
+ act(() => {
121
+ resource.setMeta({ count: 1, page: 1, pageSize: 1, totalPage: 1 });
122
+ });
123
+
124
+ expect(screen.queryByRole('list')).not.toBeInTheDocument();
125
+ });
126
+
96
127
  it('uses a real single-record details model and does not emit root paginationChange on initial render', async () => {
97
128
  const { model, resource } = setupDetailsBlockModel({ filterByTk: 1 });
98
129
  expect(resource).toBeInstanceOf(SingleRecordResource);
@@ -15,8 +15,10 @@ import {
15
15
  DndProvider,
16
16
  DragHandler,
17
17
  Droppable,
18
+ isCtxDateExpression,
18
19
  isRunJSValue,
19
20
  normalizeRunJSValue,
21
+ parseCtxDateExpression,
20
22
  tExpr,
21
23
  FlowModelRenderer,
22
24
  FlowSettingsButton,
@@ -44,6 +46,15 @@ import { normalizeFilterValueByOperator } from './valueNormalization';
44
46
 
45
47
  const RELATION_FIELD_TYPES = ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany', 'belongsToArray'];
46
48
  const NUMERIC_FIELD_TYPES = ['integer', 'float', 'double', 'decimal'];
49
+ const DATE_FILTER_OPERATORS = new Set([
50
+ '$dateOn',
51
+ '$dateNotOn',
52
+ '$dateBefore',
53
+ '$dateAfter',
54
+ '$dateNotBefore',
55
+ '$dateNotAfter',
56
+ '$dateBetween',
57
+ ]);
47
58
 
48
59
  function getFilterFormFieldMetaType(field: CollectionField) {
49
60
  if (RELATION_FIELD_TYPES.includes(field.type)) {
@@ -66,6 +77,14 @@ function getFilterFormFieldMetaType(field: CollectionField) {
66
77
  }
67
78
  }
68
79
 
80
+ function parseDateFilterDefaultValue(operator: string | undefined, rawValue: unknown) {
81
+ if (!operator || !DATE_FILTER_OPERATORS.has(operator) || !isCtxDateExpression(rawValue)) {
82
+ return undefined;
83
+ }
84
+
85
+ return parseCtxDateExpression(rawValue);
86
+ }
87
+
69
88
  function shouldShowFilterFormFieldMeta(field: CollectionField) {
70
89
  return Boolean(field?.interface);
71
90
  }
@@ -489,7 +508,7 @@ export class FilterFormBlockModel extends FilterBlockModel<{
489
508
  const rules = (params?.value || []) as any[];
490
509
  if (!Array.isArray(rules) || rules.length === 0) return appliedValues;
491
510
 
492
- const resolveValue = async (raw: any) => {
511
+ const resolveValue = async (raw: any, operator?: string) => {
493
512
  // RunJS support
494
513
  if (isRunJSValue(raw)) {
495
514
  const { code, version } = normalizeRunJSValue(raw);
@@ -497,6 +516,11 @@ export class FilterFormBlockModel extends FilterBlockModel<{
497
516
  return ret?.success ? ret.value : undefined;
498
517
  }
499
518
 
519
+ const parsedDateFilterValue = parseDateFilterDefaultValue(operator, raw);
520
+ if (typeof parsedDateFilterValue !== 'undefined') {
521
+ return parsedDateFilterValue;
522
+ }
523
+
500
524
  return await (this.context as any).resolveJsonTemplate?.(raw);
501
525
  };
502
526
 
@@ -520,11 +544,11 @@ export class FilterFormBlockModel extends FilterBlockModel<{
520
544
 
521
545
  const current = (form as any).getFieldValue?.(name);
522
546
 
523
- const resolved = await resolveValue(rule.value);
547
+ const operator = getDefaultOperator(itemModel as any);
548
+ const resolved = await resolveValue(rule.value, operator);
524
549
  if (options?.refreshSeq && options.refreshSeq !== this.defaultValuesRefreshSeq) return appliedValues;
525
550
  if (typeof resolved === 'undefined') continue;
526
551
 
527
- const operator = getDefaultOperator(itemModel as any);
528
552
  const normalized = normalizeFilterValueByOperator(operator, resolved);
529
553
  const mode = this.normalizeFieldValueMode(rule.mode);
530
554
  if (mode === 'default' && !this.canApplyFormDefaultValue(String(name), current, force)) continue;
@@ -59,11 +59,42 @@ export class FilterFormGridModel extends GridModel {
59
59
 
60
60
  readonly loading = observable.ref(false);
61
61
 
62
- private getAssociationFilterTargetKey(field: any) {
63
- const filterTargetKey = field?.targetCollection?.filterTargetKey;
62
+ private isRecordSelectFilterField(subModel: FilterFormItemModel) {
63
+ const fieldModel = subModel.subModels?.field;
64
+ if (!fieldModel) {
65
+ return true;
66
+ }
67
+
68
+ const RecordSelectModel = this.flowEngine.getModelClass('FilterFormRecordSelectFieldModel');
69
+ if (!RecordSelectModel) {
70
+ return fieldModel.constructor?.name === 'FilterFormRecordSelectFieldModel';
71
+ }
72
+
73
+ return fieldModel instanceof RecordSelectModel;
74
+ }
75
+
76
+ private getAssociationFilterTargetKey(field: any, subModel: FilterFormItemModel) {
77
+ const normalizeKey = (key: any) => {
78
+ if (Array.isArray(key)) {
79
+ return key[0];
80
+ }
81
+ return key;
82
+ };
83
+
84
+ const fieldModelValueKey = normalizeKey(subModel.subModels?.field?.props?.fieldNames?.value);
85
+ if (fieldModelValueKey) {
86
+ return fieldModelValueKey;
87
+ }
88
+
89
+ const filterTargetKey = normalizeKey(field?.targetCollection?.filterTargetKey);
90
+ const fieldTargetKey = normalizeKey(field?.targetKey);
91
+
92
+ if (filterTargetKey && filterTargetKey !== 'id') {
93
+ return filterTargetKey;
94
+ }
64
95
 
65
- if (Array.isArray(filterTargetKey)) {
66
- return filterTargetKey[0] || 'id';
96
+ if (!this.isRecordSelectFilterField(subModel) && fieldTargetKey) {
97
+ return fieldTargetKey;
67
98
  }
68
99
 
69
100
  return filterTargetKey || 'id';
@@ -370,7 +401,7 @@ export class FilterFormGridModel extends GridModel {
370
401
  if (field?.target) {
371
402
  return {
372
403
  targetId: model.uid,
373
- filterPaths: [`${fieldPath}.${this.getAssociationFilterTargetKey(field)}`],
404
+ filterPaths: [`${fieldPath}.${this.getAssociationFilterTargetKey(field, subModel)}`],
374
405
  };
375
406
  }
376
407
  }
@@ -31,6 +31,12 @@ import {
31
31
  import { FilterFormFieldModel } from './fields';
32
32
  import { normalizeFilterValueByOperator } from './valueNormalization';
33
33
 
34
+ type CollectionWithManager = Collection & {
35
+ collectionManager?: {
36
+ getCollection?: (name: string) => Collection | undefined;
37
+ };
38
+ };
39
+
34
40
  const getAssociationTargetCollection = (field: any, collection?: Collection, model?: CollectionBlockModel) => {
35
41
  if (field?.targetCollection) {
36
42
  return field.targetCollection;
@@ -42,6 +48,10 @@ const getAssociationTargetCollection = (field: any, collection?: Collection, mod
42
48
  if (collection?.dataSource?.getCollection) {
43
49
  return collection.dataSource.getCollection(targetName);
44
50
  }
51
+ const collectionManager = (collection as CollectionWithManager | undefined)?.collectionManager;
52
+ if (collectionManager?.getCollection) {
53
+ return collectionManager.getCollection(targetName);
54
+ }
45
55
  const dataSourceKey = collection?.dataSourceKey;
46
56
  if (dataSourceKey && model?.context?.dataSourceManager?.getCollection) {
47
57
  return model.context.dataSourceManager.getCollection(dataSourceKey, targetName);
@@ -168,6 +178,74 @@ const buildVirtualFilterCollectionField = (ctx: FlowModelContext, filterField: a
168
178
  return result;
169
179
  };
170
180
 
181
+ const getFilterableChildren = (field: any) => {
182
+ const children = field?.filterable?.children;
183
+ return Array.isArray(children) ? children : [];
184
+ };
185
+
186
+ const hasAssociationChildren = (field: any, collection?: Collection, model?: CollectionBlockModel) => {
187
+ return (
188
+ isAssociationCollectionField(field) &&
189
+ (getFilterableChildren(field).length > 0 || !!getAssociationTargetCollection(field, collection, model))
190
+ );
191
+ };
192
+
193
+ const inferFilterableChildInterface = (child: any) => {
194
+ if (child?.interface) {
195
+ return child.interface;
196
+ }
197
+
198
+ const schema = child?.schema || child?.uiSchema;
199
+ const component = schema?.['x-component'];
200
+ if (component === 'Select') {
201
+ return 'select';
202
+ }
203
+ if (component === 'Checkbox.Group') {
204
+ return 'checkboxGroup';
205
+ }
206
+ if (component === 'Checkbox') {
207
+ return 'checkbox';
208
+ }
209
+
210
+ const type = child?.type || schema?.type;
211
+ if (type === 'number' || type === 'integer') {
212
+ return 'number';
213
+ }
214
+ return 'input';
215
+ };
216
+
217
+ const translateFilterableChildTitle = (title: unknown, t: (value: string) => string) => {
218
+ return typeof title === 'string' ? t(title) : title;
219
+ };
220
+
221
+ const buildFilterableChildField = (child: any, t: (value: string) => string) => {
222
+ const name = child?.name || child?.value;
223
+ if (!name) {
224
+ return;
225
+ }
226
+
227
+ const schema = child?.schema || child?.uiSchema || {};
228
+ const rawTitle = child?.title || child?.label || schema?.title || name;
229
+ const title = translateFilterableChildTitle(rawTitle, t);
230
+ const filterable = child?.filterable || (child?.operators ? { operators: child.operators } : undefined);
231
+
232
+ return {
233
+ ...child,
234
+ name,
235
+ title,
236
+ type: child?.type || schema?.type || 'string',
237
+ interface: inferFilterableChildInterface(child),
238
+ uiSchema: {
239
+ ...schema,
240
+ title: translateFilterableChildTitle(schema?.title || title, t),
241
+ },
242
+ filterable,
243
+ __filterFormVirtual: true,
244
+ isAssociationField: () => false,
245
+ getComponentProps: () => schema?.['x-component-props'] || {},
246
+ };
247
+ };
248
+
171
249
  const buildFilterFormFieldItem = ({
172
250
  model,
173
251
  collection,
@@ -187,11 +265,7 @@ const buildFilterFormFieldItem = ({
187
265
  if (!binding) {
188
266
  return;
189
267
  }
190
- const isAssociation = isAssociationCollectionField(field);
191
- const fieldModel =
192
- isAssociation && ctxWithFlags.engine?.getModelClass?.('FilterFormRecordSelectFieldModel')
193
- ? 'FilterFormRecordSelectFieldModel'
194
- : binding.modelName;
268
+ const fieldModel = binding.modelName;
195
269
  const label = field.title || field.name;
196
270
  const displayLabel = labelPrefix ? `${labelPrefix} / ${label}` : label;
197
271
  return {
@@ -211,7 +285,10 @@ const buildFilterFormFieldItem = ({
211
285
  },
212
286
  filterFormItemSettings: {
213
287
  init: {
214
- filterField: _.pick(field, ['name', 'title', 'interface', 'type']),
288
+ filterField: {
289
+ ..._.pick(field, ['name', 'title', 'interface', 'type', 'filterable', 'uiSchema', '__filterFormVirtual']),
290
+ title: displayLabel,
291
+ },
215
292
  defaultTargetUid: model.uid,
216
293
  },
217
294
  },
@@ -245,7 +322,8 @@ const buildAssociationFieldMenuItem = ({
245
322
  depth: number;
246
323
  }) => {
247
324
  const targetCollection = getAssociationTargetCollection(field, collection, model);
248
- if (!targetCollection) {
325
+ const filterableChildren = getFilterableChildren(field);
326
+ if (!targetCollection && !filterableChildren.length) {
249
327
  return;
250
328
  }
251
329
  if (depth > MAX_ASSOCIATION_DEPTH) {
@@ -262,10 +340,35 @@ const buildAssociationFieldMenuItem = ({
262
340
  const targetFields = getTargetFilterableFields(field, collection, model);
263
341
  const fieldItems: any[] = [];
264
342
  const associationItems: any[] = [];
343
+ const addedFieldPaths = new Set<string>();
344
+
345
+ filterableChildren.forEach((child: any) => {
346
+ const virtualChildField = buildFilterableChildField(child, t);
347
+ if (!virtualChildField) {
348
+ return;
349
+ }
350
+
351
+ const childFieldPath = `${fieldPath}.${virtualChildField.name}`;
352
+ const childItem = buildFilterFormFieldItem({
353
+ model,
354
+ collection,
355
+ ctxWithFlags,
356
+ field: virtualChildField,
357
+ fieldPath: childFieldPath,
358
+ labelPrefix: label,
359
+ });
360
+ if (childItem) {
361
+ addedFieldPaths.add(childFieldPath);
362
+ fieldItems.push(childItem);
363
+ }
364
+ });
265
365
 
266
366
  targetFields.forEach((targetField: any) => {
267
367
  const targetFieldPath = `${fieldPath}.${targetField.name}`;
268
- if (targetField?.targetCollection) {
368
+ if (addedFieldPaths.has(targetFieldPath)) {
369
+ return;
370
+ }
371
+ if (hasAssociationChildren(targetField, collection, model)) {
269
372
  const associationItem = buildAssociationFieldMenuItem({
270
373
  model,
271
374
  collection,
@@ -358,7 +461,7 @@ const getModelFieldGroups = async (model: CollectionBlockModel) => {
358
461
  baseItems.push(baseItem);
359
462
  }
360
463
 
361
- if (field?.targetCollection) {
464
+ if (hasAssociationChildren(field, collection, model)) {
362
465
  const associationItem = buildAssociationFieldMenuItem({
363
466
  model,
364
467
  collection,
@@ -489,6 +592,14 @@ export class FilterFormItemModel extends FilterableItemModel<{
489
592
  title: initFilterField?.title || initFilterField?.name,
490
593
  },
491
594
  });
595
+ if (initFilterField?.__filterFormVirtual) {
596
+ const virtualField = buildVirtualFilterCollectionField(this.context, initFilterField);
597
+ if (virtualField) {
598
+ this.context.defineProperty('collectionField', {
599
+ value: virtualField,
600
+ });
601
+ }
602
+ }
492
603
  }
493
604
  }
494
605
 
@@ -692,7 +803,8 @@ FilterFormItemModel.registerFlow({
692
803
  // @ts-ignore
693
804
  (fieldSettingsInitParams?.dataSourceKey && fieldSettingsInitParams?.collectionName)
694
805
  );
695
- if (!hasCollectionContext && !collectionField && normalizedFilterField) {
806
+ const shouldUseVirtualFilterField = !!normalizedFilterField?.__filterFormVirtual;
807
+ if ((shouldUseVirtualFilterField || (!hasCollectionContext && !collectionField)) && normalizedFilterField) {
696
808
  const virtualField = buildVirtualFilterCollectionField(ctx, normalizedFilterField);
697
809
  if (virtualField) {
698
810
  ctx.model.context.defineProperty('collectionField', {
@@ -787,10 +899,13 @@ FilterFormItemModel.registerFlow({
787
899
  type: 'select',
788
900
  key: 'use',
789
901
  props: {
790
- options: classes.map((model) => ({
791
- label: t(model.modelName),
792
- value: model.modelName,
793
- })),
902
+ options: classes.map((model) => {
903
+ const ModelClass = ctx.engine.getModelClass(model.modelName);
904
+ return {
905
+ label: t(ModelClass?.meta?.label || model.modelName),
906
+ value: model.modelName,
907
+ };
908
+ }),
794
909
  },
795
910
  };
796
911
  },
@@ -8,23 +8,76 @@
8
8
  */
9
9
 
10
10
  import type { ButtonProps } from 'antd/es/button';
11
+ import { reaction } from '@formily/reactive';
11
12
  import { tExpr } from '@nocobase/flow-engine';
12
13
  import { FilterFormActionModel } from './FilterFormActionModel';
13
14
 
15
+ type FilterFormBlockState = {
16
+ autoTriggerFilter: boolean;
17
+ };
18
+
19
+ const mountedSubmitActions = new WeakSet<object>();
20
+ const visibleSubmitActionsByBlock = new WeakMap<FilterFormBlockState, Set<object>>();
21
+ const hiddenReactionDisposers = new WeakMap<object, () => void>();
22
+
23
+ const syncAutoTriggerFilter = (blockModel: FilterFormBlockState, lifecycleAction: object & { hidden?: boolean }) => {
24
+ let visibleSubmitActions = visibleSubmitActionsByBlock.get(blockModel);
25
+
26
+ if (mountedSubmitActions.has(lifecycleAction) && !lifecycleAction.hidden) {
27
+ if (!visibleSubmitActions) {
28
+ visibleSubmitActions = new Set<object>();
29
+ visibleSubmitActionsByBlock.set(blockModel, visibleSubmitActions);
30
+ }
31
+ visibleSubmitActions.add(lifecycleAction);
32
+ } else {
33
+ visibleSubmitActions?.delete(lifecycleAction);
34
+ }
35
+
36
+ blockModel.autoTriggerFilter = Boolean(!visibleSubmitActions?.size);
37
+ if (!visibleSubmitActions?.size) {
38
+ visibleSubmitActionsByBlock.delete(blockModel);
39
+ }
40
+ };
41
+
14
42
  export class FilterFormSubmitActionModel extends FilterFormActionModel {
15
43
  defaultProps: ButtonProps = {
16
44
  title: tExpr('Filter'),
17
45
  type: 'primary',
18
46
  };
19
47
 
20
- onInit(options): void {
21
- super.onInit(options);
22
- this.context.blockModel.autoTriggerFilter = false;
48
+ private getLifecycleAction(): object & { hidden?: boolean } {
49
+ return this.context.model || this;
50
+ }
51
+
52
+ private syncAutoTriggerFilter(): void {
53
+ const blockModel = this.context.blockModel as FilterFormBlockState;
54
+ const lifecycleAction = this.getLifecycleAction();
55
+ syncAutoTriggerFilter(blockModel, lifecycleAction);
56
+ }
57
+
58
+ onMount(): void {
59
+ super.onMount();
60
+ const blockModel = this.context.blockModel as FilterFormBlockState;
61
+ const lifecycleAction = this.getLifecycleAction();
62
+ mountedSubmitActions.add(lifecycleAction);
63
+ hiddenReactionDisposers.get(lifecycleAction)?.();
64
+ hiddenReactionDisposers.set(
65
+ lifecycleAction,
66
+ reaction(
67
+ () => lifecycleAction.hidden,
68
+ () => syncAutoTriggerFilter(blockModel, lifecycleAction),
69
+ ),
70
+ );
71
+ this.syncAutoTriggerFilter();
23
72
  }
24
73
 
25
74
  onUnmount(): void {
26
75
  super.onUnmount();
27
- this.context.blockModel.autoTriggerFilter = true;
76
+ const lifecycleAction = this.getLifecycleAction();
77
+ hiddenReactionDisposers.get(lifecycleAction)?.();
78
+ hiddenReactionDisposers.delete(lifecycleAction);
79
+ mountedSubmitActions.delete(lifecycleAction);
80
+ this.syncAutoTriggerFilter();
28
81
  }
29
82
  }
30
83