@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
@@ -9,6 +9,8 @@
9
9
 
10
10
  import { describe, expect, it, vi } from 'vitest';
11
11
  import '../../../../index';
12
+ import { FlowEngine } from '@nocobase/flow-engine';
13
+ import { CascadeSelectFieldModel } from '../../../fields/AssociationFieldModel/CascadeSelectFieldModel';
12
14
  import { FilterFormItemModel } from '../FilterFormItemModel';
13
15
 
14
16
  describe('FilterFormItemModel getFilterValue', () => {
@@ -174,4 +176,74 @@ describe('FilterFormItemModel getFilterValue', () => {
174
176
  const value = FilterFormItemModel.prototype.getFilterValue.call(model as any);
175
177
  expect(value).toBe(25);
176
178
  });
179
+
180
+ it('uses the tree Cascader leaf target key with the equality operator', () => {
181
+ const engine = new FlowEngine();
182
+ engine.registerModels({ CascadeSelectFieldModel });
183
+
184
+ const ds = engine.dataSourceManager.getDataSource('main');
185
+ ds?.addCollection({
186
+ name: 'organizations',
187
+ template: 'tree',
188
+ filterTargetKey: 'code',
189
+ fields: [
190
+ { name: 'code', type: 'string', interface: 'input', filterable: { operators: [] } },
191
+ { name: 'name', type: 'string', interface: 'input', filterable: { operators: [] } },
192
+ ],
193
+ });
194
+ ds?.addCollection({
195
+ name: 'users',
196
+ fields: [
197
+ {
198
+ name: 'organization',
199
+ type: 'belongsTo',
200
+ interface: 'm2o',
201
+ target: 'organizations',
202
+ targetKey: 'code',
203
+ filterable: { operators: [] },
204
+ },
205
+ ],
206
+ });
207
+
208
+ const fieldModel = engine.createModel<CascadeSelectFieldModel>({
209
+ uid: 'tree-cascader-filter-field',
210
+ use: 'CascadeSelectFieldModel',
211
+ stepParams: {
212
+ fieldSettings: {
213
+ init: {
214
+ dataSourceKey: 'main',
215
+ collectionName: 'users',
216
+ fieldPath: 'organization',
217
+ },
218
+ },
219
+ },
220
+ props: {
221
+ fieldNames: { label: 'name', value: 'code' },
222
+ value: {
223
+ code: 'leaf',
224
+ name: 'Leaf organization',
225
+ parent: { code: 'root', name: 'Root organization', parent: null },
226
+ },
227
+ },
228
+ });
229
+ const model = {
230
+ mounted: true,
231
+ props: { name: 'organization' },
232
+ subModels: { field: fieldModel },
233
+ context: {
234
+ form: {
235
+ getFieldValue: vi.fn(() => fieldModel.props.value),
236
+ },
237
+ },
238
+ getDefaultValue: vi.fn(() => undefined),
239
+ getStepParams: vi.fn(() => undefined),
240
+ getCurrentOperatorMeta: vi.fn(() => null),
241
+ normalizeAssociationFilterValue: FilterFormItemModel.prototype.normalizeAssociationFilterValue,
242
+ };
243
+
244
+ const value = FilterFormItemModel.prototype.getFilterValue.call(model as unknown as FilterFormItemModel);
245
+
246
+ expect(fieldModel.operator).toBe('$eq');
247
+ expect(value).toBe('leaf');
248
+ });
177
249
  });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { define, observable } from '@formily/reactive';
11
+ import { describe, expect, it } from 'vitest';
12
+ import { FilterFormSubmitActionModel } from '../FilterFormSubmitActionModel';
13
+
14
+ describe('FilterFormSubmitActionModel lifecycle', () => {
15
+ const createAction = (blockModel: { autoTriggerFilter: boolean }, hidden = false, lifecycleAction?: object) => {
16
+ const action = Object.create(FilterFormSubmitActionModel.prototype) as FilterFormSubmitActionModel;
17
+
18
+ Object.defineProperty(action, 'props', {
19
+ value: {},
20
+ });
21
+ Object.defineProperty(action, 'context', {
22
+ value: { blockModel, model: lifecycleAction || action, defineProperty: () => undefined },
23
+ });
24
+ action.hidden = hidden;
25
+ define(action, {
26
+ hidden: observable,
27
+ });
28
+ action.onInit({});
29
+
30
+ return action;
31
+ };
32
+
33
+ it('keeps automatic filtering disabled when the submit action remounts', () => {
34
+ const blockModel = { autoTriggerFilter: true };
35
+ const action = createAction(blockModel);
36
+
37
+ action.onMount();
38
+ expect(blockModel.autoTriggerFilter).toBe(false);
39
+
40
+ action.onUnmount();
41
+ expect(blockModel.autoTriggerFilter).toBe(true);
42
+
43
+ action.onMount();
44
+ expect(blockModel.autoTriggerFilter).toBe(false);
45
+ });
46
+
47
+ it('keeps automatic filtering disabled until every mounted submit action unmounts', () => {
48
+ const blockModel = { autoTriggerFilter: true };
49
+ const firstAction = createAction(blockModel);
50
+ const secondAction = createAction(blockModel);
51
+
52
+ firstAction.onMount();
53
+ secondAction.onMount();
54
+ firstAction.onUnmount();
55
+ expect(blockModel.autoTriggerFilter).toBe(false);
56
+
57
+ secondAction.onUnmount();
58
+ expect(blockModel.autoTriggerFilter).toBe(true);
59
+ });
60
+
61
+ it('only disables automatic filtering while a visible submit action is mounted', () => {
62
+ const blockModel = { autoTriggerFilter: true };
63
+ const action = createAction(blockModel, true);
64
+
65
+ action.onMount();
66
+ expect(blockModel.autoTriggerFilter).toBe(true);
67
+
68
+ action.hidden = false;
69
+ expect(blockModel.autoTriggerFilter).toBe(false);
70
+
71
+ action.hidden = true;
72
+ expect(blockModel.autoTriggerFilter).toBe(true);
73
+ });
74
+
75
+ it('tracks fork lifecycles independently when submit actions share the same uid', () => {
76
+ const blockModel = { autoTriggerFilter: true };
77
+ const firstFork = {};
78
+ const secondFork = {};
79
+ const firstAction = createAction(blockModel, false, firstFork);
80
+ const secondAction = createAction(blockModel, false, secondFork);
81
+
82
+ firstAction.onMount();
83
+ secondAction.onMount();
84
+ firstAction.onUnmount();
85
+ expect(blockModel.autoTriggerFilter).toBe(false);
86
+
87
+ secondAction.onUnmount();
88
+ expect(blockModel.autoTriggerFilter).toBe(true);
89
+ });
90
+ });
@@ -17,7 +17,15 @@ import { FilterFormBlockModel } from '../FilterFormBlockModel';
17
17
  function resolveTemplateValue(raw: any, values: Record<string, any>): any {
18
18
  if (typeof raw === 'string') {
19
19
  const matched = raw.match(/^\{\{\s*ctx\.formValues\.([^}]+?)\s*\}\}$/);
20
- return matched ? values[matched[1]] : raw;
20
+ if (matched) {
21
+ return values[matched[1]];
22
+ }
23
+
24
+ if (/^\{\{\s*ctx\.date\.relative\.past\.day\.n7\s*\}\}$/.test(raw)) {
25
+ return '2026-06-15';
26
+ }
27
+
28
+ return raw;
21
29
  }
22
30
  if (Array.isArray(raw)) {
23
31
  return raw.map((item) => resolveTemplateValue(item, values));
@@ -30,7 +38,7 @@ function resolveTemplateValue(raw: any, values: Record<string, any>): any {
30
38
 
31
39
  function createFilterFormDefaultValuesModel(rules: any[], initialValues: Record<string, any> = {}) {
32
40
  const values = { ...initialValues };
33
- const createItem = (fieldPath: string, uid: string) => ({
41
+ const createItem = (fieldPath: string, uid: string, operator?: string) => ({
34
42
  uid,
35
43
  fieldPath,
36
44
  props: { name: `${fieldPath}_${uid}` },
@@ -44,7 +52,7 @@ function createFilterFormDefaultValuesModel(rules: any[], initialValues: Record<
44
52
  return undefined;
45
53
  },
46
54
  subModels: {
47
- field: {},
55
+ field: operator ? { operator } : {},
48
56
  },
49
57
  });
50
58
  const model = {
@@ -76,7 +84,11 @@ function createFilterFormDefaultValuesModel(rules: any[], initialValues: Record<
76
84
  subModels: {
77
85
  grid: {
78
86
  subModels: {
79
- items: [createItem('nickname', 'nick'), createItem('username', 'user')],
87
+ items: [
88
+ createItem('nickname', 'nick'),
89
+ createItem('username', 'user'),
90
+ createItem('birthdate_tz', 'birthdate', '$dateOn'),
91
+ ],
80
92
  },
81
93
  },
82
94
  },
@@ -354,6 +366,22 @@ describe('filter-form defaultValues wiring', () => {
354
366
  expect(values.username_user).toBe('Bob');
355
367
  });
356
368
 
369
+ it('preserves relative date descriptors for date filter default values', async () => {
370
+ const { model, values } = createFilterFormDefaultValuesModel([
371
+ {
372
+ key: 'birthdate-default',
373
+ enable: true,
374
+ targetPath: 'birthdate_tz',
375
+ mode: 'assign',
376
+ value: '{{ ctx.date.relative.past.day.n7 }}',
377
+ },
378
+ ]);
379
+
380
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
381
+
382
+ expect(values.birthdate_tz_birthdate).toEqual({ type: 'past', unit: 'day', number: 7 });
383
+ });
384
+
357
385
  it('applies override values until the target filter field is changed by user', async () => {
358
386
  const { model, values } = createFilterFormDefaultValuesModel(
359
387
  [
@@ -351,7 +351,7 @@ export const FieldComponentProps: React.FC<{ fieldModel: string; source: string[
351
351
  <FormItem label={t('Options')}>
352
352
  <Space direction="vertical" size={8} style={{ width: '100%' }}>
353
353
  {options.map((option: any, index: number) => (
354
- <Space key={`${option.value}-${index}`} style={{ width: '100%' }} size={8} wrap align="start">
354
+ <Space key={index} style={{ width: '100%' }} size={8} wrap align="start">
355
355
  <Input
356
356
  style={{ flex: 1, minWidth: 120 }}
357
357
  placeholder={t('Option label')}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import React from 'react';
11
+ import { describe, expect, it } from 'vitest';
12
+ import { createForm } from '@formily/core';
13
+ import { Field, FormProvider } from '@formily/react';
14
+ import { render, screen } from '@nocobase/test/client';
15
+ import userEvent from '@testing-library/user-event';
16
+ import { FlowEngine, FlowEngineProvider, FlowModel, FlowModelProvider } from '@nocobase/flow-engine';
17
+ import { FieldComponentProps } from '../FieldComponentProps';
18
+
19
+ class HostModel extends FlowModel {
20
+ render() {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ describe.each(['RadioGroupFieldModel', 'CheckboxGroupFieldModel', 'SelectFieldModel'])('%s options', (fieldModel) => {
26
+ it('keeps focus while editing option values and preserves remaining rows after removal', async () => {
27
+ const form = createForm();
28
+ const engine = new FlowEngine();
29
+ engine.registerModels({ HostModel });
30
+ const model = engine.createModel<HostModel>({ use: 'HostModel' });
31
+ render(
32
+ <FlowEngineProvider engine={engine}>
33
+ <FlowModelProvider model={model}>
34
+ <FormProvider form={form}>
35
+ <Field name="props" component={[FieldComponentProps, { fieldModel, source: [] }]} />
36
+ </FormProvider>
37
+ </FlowModelProvider>
38
+ </FlowEngineProvider>,
39
+ );
40
+
41
+ await userEvent.click(screen.getByRole('button', { name: 'plus Add' }));
42
+ const valueInput = screen.getByPlaceholderText('Option value');
43
+ await userEvent.type(valueInput, 'abcdef');
44
+ expect(screen.getByPlaceholderText('Option value')).toHaveValue('abcdef');
45
+ expect(valueInput).toHaveFocus();
46
+
47
+ await userEvent.keyboard('{Backspace}{Backspace}xy');
48
+ expect(valueInput).toHaveValue('abcdxy');
49
+ expect(valueInput).toHaveFocus();
50
+ await userEvent.type(screen.getByPlaceholderText('Option label'), 'First');
51
+ await userEvent.click(screen.getByRole('button', { name: 'plus Add' }));
52
+ await userEvent.type(screen.getAllByPlaceholderText('Option label')[1], 'Second');
53
+ await userEvent.type(screen.getAllByPlaceholderText('Option value')[1], 'second');
54
+ await userEvent.click(screen.getAllByRole('button', { name: 'close' })[0]);
55
+ expect(screen.getByPlaceholderText('Option label')).toHaveValue('Second');
56
+ expect(screen.getByPlaceholderText('Option value')).toHaveValue('second');
57
+ await userEvent.type(screen.getByPlaceholderText('Option value'), '2');
58
+ expect(screen.getByPlaceholderText('Option value')).toHaveFocus();
59
+ expect(form.values.props.options).toEqual([{ label: 'Second', value: 'second2' }]);
60
+ });
61
+ });
@@ -569,6 +569,9 @@ export class FilterManager {
569
569
 
570
570
  const blockModel = targetModel as CollectionBlockModel;
571
571
  const resource = (targetModel as any).resource;
572
+ const filterResettableTarget = targetModel as CollectionBlockModel & {
573
+ resetAfterFilterChange?: () => void;
574
+ };
572
575
 
573
576
  // 4.4 检查数据加载模式
574
577
  const loadingMode = blockModel.getDataLoadingMode();
@@ -581,6 +584,7 @@ export class FilterManager {
581
584
  resource.setPage(1);
582
585
  }
583
586
  resource.loading = false;
587
+ filterResettableTarget.resetAfterFilterChange?.();
584
588
  return;
585
589
  }
586
590
 
@@ -590,6 +594,7 @@ export class FilterManager {
590
594
  resource.setPage(1); // 重置到第一页
591
595
  }
592
596
  await resource.refresh();
597
+ filterResettableTarget.resetAfterFilterChange?.();
593
598
  } catch (error) {
594
599
  console.error(`Failed to refresh target model "${targetId}": ${error.message}`);
595
600
  return;
@@ -615,6 +615,46 @@ describe('FilterManager', () => {
615
615
  expect(mockTargetModel2.resource.refresh).toHaveBeenCalledTimes(1);
616
616
  });
617
617
 
618
+ it('should reset target interaction state after refreshing filtered data', async () => {
619
+ (filterManager as any).filterConfigs = [
620
+ {
621
+ filterId: 'filter-1',
622
+ targetId: 'target-1',
623
+ filterPaths: ['name'],
624
+ operator: '$eq',
625
+ },
626
+ ];
627
+
628
+ const refresh = vi.fn().mockResolvedValue(undefined);
629
+ const resetAfterFilterChange = vi.fn(() => {
630
+ expect(refresh).toHaveBeenCalledTimes(1);
631
+ });
632
+ const mockTargetModel = {
633
+ resource: {
634
+ addFilterGroup: vi.fn(),
635
+ removeFilterGroup: vi.fn(),
636
+ refresh,
637
+ },
638
+ setFilterActive: vi.fn(),
639
+ getDataLoadingMode: vi.fn().mockReturnValue('auto'),
640
+ resetAfterFilterChange,
641
+ };
642
+ const mockFilterModel = {
643
+ getFilterValue: vi.fn().mockReturnValue('test-value'),
644
+ };
645
+
646
+ (mockFlowModel.flowEngine.getModel as any).mockImplementation((uid: string) => {
647
+ if (uid === 'target-1') return mockTargetModel;
648
+ if (uid === 'filter-1') return mockFilterModel;
649
+ return null;
650
+ });
651
+
652
+ await filterManager.refreshTargetsByFilter('filter-1');
653
+
654
+ expect(resetAfterFilterChange).toHaveBeenCalledTimes(1);
655
+ expect(refresh).toHaveBeenCalledTimes(1);
656
+ });
657
+
618
658
  it('should wait for target resource idle before refreshing', async () => {
619
659
  vi.useFakeTimers();
620
660
  (filterManager as any).filterConfigs = [
@@ -72,17 +72,64 @@ const flowKeepMobileHorizontalClassName = css`
72
72
  function isGridDelegatedStep(flowKey: string, stepKey: string): boolean {
73
73
  return !!GRID_DELEGATED_STEP_KEYS[flowKey]?.has(stepKey);
74
74
  }
75
+
76
+ interface FormModelWithAvailableData {
77
+ hasAvailableData(): boolean;
78
+ }
79
+
80
+ interface FormModelWithSubmit {
81
+ submit(): Promise<unknown>;
82
+ }
83
+
84
+ function hasAvailableDataCapability(model: object): model is FormModelWithAvailableData {
85
+ return 'hasAvailableData' in model && typeof model.hasAvailableData === 'function';
86
+ }
87
+
88
+ function hasSubmitCapability(model: object): model is FormModelWithSubmit {
89
+ return 'submit' in model && typeof model.submit === 'function';
90
+ }
91
+
75
92
  export class FormBlockModel<
76
93
  T extends DefaultCollectionBlockModelStructure = DefaultCollectionBlockModelStructure,
77
94
  > extends CollectionBlockModel<T> {
78
95
  formValueRuntime?: FormValueRuntime;
79
96
 
97
+ serialize() {
98
+ return { ...super.serialize(), variableContractType: { type: 'form', use: this.use } };
99
+ }
100
+
80
101
  private userModifiedTopLevelFields = new Set<string>();
81
102
 
82
103
  get form() {
83
104
  return this.context.form as FormInstance;
84
105
  }
85
106
 
107
+ submitFromRunJs() {
108
+ if (hasAvailableDataCapability(this) && !this.hasAvailableData()) {
109
+ return;
110
+ }
111
+
112
+ const submitAction = this.mapSubModels('actions', (action) => action).find((action) =>
113
+ action.getFlow('submitSettings'),
114
+ );
115
+ if (submitAction) {
116
+ return submitAction.onClick(undefined);
117
+ }
118
+
119
+ const isCoreForm = ['CreateFormModel', 'EditFormModel'].some((modelName) => {
120
+ const ModelClass = this.flowEngine.getModelClass(modelName);
121
+ return ModelClass && this instanceof ModelClass;
122
+ });
123
+ if (this.context.publicFormRuntime || !isCoreForm || !hasSubmitCapability(this)) {
124
+ return;
125
+ }
126
+
127
+ this.submit().catch((error: unknown) => {
128
+ this.context.message.error(this.context.t('Save failed'));
129
+ console.error('Form submission error:', error);
130
+ });
131
+ }
132
+
86
133
  _defaultCustomModelClasses = {
87
134
  FormActionGroupModel: 'FormActionGroupModel',
88
135
  FormItemModel: 'FormItemModel',
@@ -352,20 +399,41 @@ export class FormBlockModel<
352
399
  if (Array.isArray(topValue) && topValue.length === 0) return false;
353
400
 
354
401
  // 本地优先:支持对多关系的 dot 聚合路径(例如 assignees.name)。
355
- // lodash.get 对数组聚合路径会返回 undefined(如 _.get({ assignees:[{name:'A'}] }, 'assignees.name')),
356
- // 因而这里先用 getValuesByPath 做一次前端可解析性检查,命中则直接前端解析。
402
+ // 关联字段只有在本地值包含目标标题字段时才算完整;标量外键或仅含主键的轻量对象仍需服务端补全。
357
403
  const formValuesSnapshot = runtime.getFormValuesSnapshot();
404
+ let shouldResolveAssociationValueOnServer = false;
358
405
  if (formValuesSnapshot && typeof formValuesSnapshot === 'object') {
359
- const localResolved = getValuesByPath(formValuesSnapshot as Record<string, any>, subPath);
406
+ const localResolved = getValuesByPath(formValuesSnapshot as Record<string, unknown>, subPath);
360
407
  if (typeof localResolved !== 'undefined') {
361
- return false;
408
+ const fieldPath = subPath
409
+ .replace(/\[\d+\]/g, '')
410
+ .split('.')
411
+ .filter((segment) => !/^\d+$/.test(segment))
412
+ .join('.');
413
+ const resolvedField = this.collection?.getFieldByPath?.(fieldPath);
414
+ const titleFieldName = resolvedField?.targetCollectionTitleFieldName;
415
+ const isLoadedAssociationRecord = (value: unknown) => {
416
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
417
+ if (!titleFieldName) return true;
418
+ return typeof (value as Record<string, unknown>)[titleFieldName] !== 'undefined';
419
+ };
420
+ const isAssociationValueLoaded =
421
+ localResolved === null ||
422
+ (Array.isArray(localResolved)
423
+ ? localResolved.length === 0 ||
424
+ localResolved.every((value) => value === null || isLoadedAssociationRecord(value))
425
+ : isLoadedAssociationRecord(localResolved));
426
+ if (!resolvedField?.isAssociationField?.() || isAssociationValueLoaded) {
427
+ return false;
428
+ }
429
+ shouldResolveAssociationValueOnServer = true;
362
430
  }
363
431
  }
364
432
 
365
433
  // 已配置字段:仅关联字段的子路径按需服务端补全(保持现有语义)
366
434
  const assocResolver = createAssociationSubpathResolver(
367
435
  () => this.collection,
368
- () => runtime.getFormValuesSnapshot(),
436
+ shouldResolveAssociationValueOnServer ? undefined : () => runtime.getFormValuesSnapshot(),
369
437
  );
370
438
  return assocResolver(subPath);
371
439
  }
@@ -21,6 +21,10 @@ export type DefaultFormGridStructure = {
21
21
  };
22
22
 
23
23
  export class FormGridModel<T extends DefaultFormGridStructure = DefaultFormGridStructure> extends GridModel<T> {
24
+ serialize() {
25
+ return { ...super.serialize(), variableContractType: { type: 'formGrid', use: this.use } };
26
+ }
27
+
24
28
  itemFallback = (<Skeleton.Input block size="small" style={{ marginBottom: '0.5rem' }} />);
25
29
  itemSettingsMenuLevel = 2;
26
30
  itemFlowSettings = {