@nocobase/client-v2 2.2.0-beta.3 → 2.2.0-beta.6

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 (133) hide show
  1. package/es/RouteRepository.d.ts +21 -6
  2. package/es/components/form/JsonTextArea.d.ts +2 -1
  3. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  4. package/es/components/form/index.d.ts +1 -0
  5. package/es/components/index.d.ts +1 -0
  6. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  7. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  8. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  9. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  10. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +4 -0
  11. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  12. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  13. package/es/flow/models/base/GridModel.d.ts +1 -1
  14. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  15. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  16. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  17. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
  18. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  19. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  20. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +1 -0
  21. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  22. package/es/flow-compat/FieldValidation.d.ts +2 -1
  23. package/es/index.d.ts +2 -1
  24. package/es/index.mjs +167 -121
  25. package/lib/index.js +167 -121
  26. package/package.json +7 -7
  27. package/src/RouteRepository.ts +126 -24
  28. package/src/__tests__/RouteRepository.test.ts +216 -0
  29. package/src/__tests__/exports.test.ts +16 -0
  30. package/src/__tests__/settings-center.test.tsx +33 -0
  31. package/src/components/form/JsonTextArea.tsx +21 -11
  32. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  33. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  34. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  35. package/src/components/form/index.tsx +1 -0
  36. package/src/components/index.ts +1 -0
  37. package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
  38. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  39. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  40. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  41. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  42. package/src/flow/actions/customVariable.tsx +1 -2
  43. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  44. package/src/flow/actions/linkageRules.tsx +122 -34
  45. package/src/flow/actions/runjs.tsx +2 -14
  46. package/src/flow/actions/validation.tsx +62 -30
  47. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +33 -10
  48. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
  49. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
  50. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  51. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  52. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  53. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  54. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +21 -8
  55. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  56. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  57. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  58. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
  59. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  60. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  61. package/src/flow/components/DefaultValue.tsx +1 -2
  62. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  63. package/src/flow/components/FieldAssignRulesEditor.tsx +62 -12
  64. package/src/flow/components/FlowRoute.tsx +56 -11
  65. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  66. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
  67. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  68. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  69. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  70. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  71. package/src/flow/index.ts +1 -1
  72. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  73. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  74. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  75. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  76. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  77. package/src/flow/models/base/ActionModel.tsx +12 -1
  78. package/src/flow/models/base/GridModel.tsx +38 -7
  79. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  80. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  81. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  82. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  83. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  84. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
  85. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  86. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
  87. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  88. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  89. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  90. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  91. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  92. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
  93. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  94. package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
  95. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  96. package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
  97. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  98. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
  99. package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
  100. package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
  101. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
  102. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  103. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
  104. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  105. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
  106. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
  107. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +24 -1
  108. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  109. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +12 -0
  110. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  111. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  112. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  113. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  114. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  115. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  116. package/src/flow/models/fields/InputFieldModel.tsx +14 -22
  117. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  118. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  119. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  120. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  121. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  122. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +17 -0
  123. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  124. package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
  125. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
  126. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  127. package/src/flow-compat/FieldValidation.tsx +122 -60
  128. package/src/flow-compat/Popover.tsx +43 -4
  129. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  130. package/src/index.ts +8 -1
  131. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  132. package/src/settings-center/AdminSettingsLayout.tsx +3 -0
  133. package/src/settings-center/plugin-manager/index.tsx +3 -0
@@ -19,11 +19,16 @@ const capturedDroppableUids: string[] = [];
19
19
  const capturedRendererProps: any[] = [];
20
20
  let latestOnDragEnd: ((event: any) => void) | undefined;
21
21
  const capturedDndProviders: Array<{ persist: boolean }> = [];
22
+ const capturedAddSubModelButtonProps: any[] = [];
22
23
 
23
24
  vi.mock('@nocobase/flow-engine', async () => {
24
25
  const actual = await vi.importActual<any>('@nocobase/flow-engine');
25
26
  return {
26
27
  ...actual,
28
+ AddSubModelButton: (props) => {
29
+ capturedAddSubModelButtonProps.push(props);
30
+ return <button data-testid="add-sub-model-button">{props.children}</button>;
31
+ },
27
32
  DndProvider: ({ children, onDragEnd, persist = true, ...restProps }) => {
28
33
  const engine = actual.useFlowEngine();
29
34
  const handleDragEnd = (event: any) => {
@@ -50,6 +55,17 @@ vi.mock('@nocobase/flow-engine', async () => {
50
55
  const ActualFlowModelRenderer = actual.FlowModelRenderer;
51
56
  return <ActualFlowModelRenderer {...props} />;
52
57
  },
58
+ FlowsFloatContextMenu: ({ children, extraToolbarItems = [], model }) => {
59
+ return (
60
+ <div data-test-flow-context-menu>
61
+ {children}
62
+ {extraToolbarItems.map((item) => {
63
+ const Component = item.component;
64
+ return <Component key={item.key} model={model} />;
65
+ })}
66
+ </div>
67
+ );
68
+ },
53
69
  };
54
70
  });
55
71
 
@@ -165,6 +181,7 @@ describe('TableActionsColumnModel: drag integration', () => {
165
181
  capturedDroppableUids.length = 0;
166
182
  capturedRendererProps.length = 0;
167
183
  capturedDndProviders.length = 0;
184
+ capturedAddSubModelButtonProps.length = 0;
168
185
  latestOnDragEnd = undefined;
169
186
  });
170
187
 
@@ -177,6 +194,7 @@ describe('TableActionsColumnModel: drag integration', () => {
177
194
  props: { width: 200, title: 'Actions' },
178
195
  subModels: { actions: [{ use: 'TestViewActionModel' }, { use: 'TestViewActionModel' }] },
179
196
  });
197
+ actionsCol.context.defineMethod('getModelClassName', (className: string) => className);
180
198
 
181
199
  const colProps = actionsCol.getColumnProps();
182
200
  const record = { id: 1, phone: '000000' } as any;
@@ -279,4 +297,40 @@ describe('TableActionsColumnModel: drag integration', () => {
279
297
 
280
298
  expect(moveModelSpy).toHaveBeenCalledWith(firstUid, secondUid, { persist: true });
281
299
  });
300
+
301
+ it('disables icon-only mode when adding row actions without icons', async () => {
302
+ const engine = new FlowEngine();
303
+ engine.registerModels({ TableActionsColumnModel, TestViewActionModel });
304
+
305
+ const actionsCol = engine.createModel<TableActionsColumnModel>({
306
+ use: 'TableActionsColumnModel',
307
+ props: { width: 200, title: 'Actions' },
308
+ });
309
+ actionsCol.context.defineMethod('getModelClassName', (className: string) => className);
310
+
311
+ render(
312
+ <FlowEngineProvider engine={engine}>
313
+ <ConfigProvider>
314
+ <App>{actionsCol.getColumnProps().title as React.ReactNode}</App>
315
+ </ConfigProvider>
316
+ </FlowEngineProvider>,
317
+ );
318
+
319
+ const addButtonProps = capturedAddSubModelButtonProps.find(
320
+ (props) => props.subModelKey === 'actions' && props.subModelBaseClass === 'RecordActionGroupModel',
321
+ );
322
+ expect(addButtonProps).toBeDefined();
323
+ expect(addButtonProps.afterSubModelInit).toBeTypeOf('function');
324
+
325
+ const actionModel = {
326
+ setStepParams: vi.fn(),
327
+ };
328
+ await addButtonProps.afterSubModelInit(actionModel);
329
+
330
+ expect(actionModel.setStepParams).toHaveBeenCalledWith('buttonSettings', 'general', {
331
+ type: 'link',
332
+ icon: null,
333
+ iconOnly: false,
334
+ });
335
+ });
282
336
  });
@@ -0,0 +1,78 @@
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 { FlowEngine } from '@nocobase/flow-engine';
11
+ import '@nocobase/client';
12
+ import React from 'react';
13
+ import { describe, expect, it } from 'vitest';
14
+ import { TableBlockModel } from '../TableBlockModel';
15
+
16
+ function createTableModel(options: { isMobileLayout?: boolean; flowSettingsEnabled?: boolean } = {}) {
17
+ const engine = new FlowEngine();
18
+ engine.registerModels({ TableBlockModel });
19
+ engine.context.defineProperty('isMobileLayout', { value: !!options.isMobileLayout });
20
+ engine.context.defineProperty('flowSettingsEnabled', { value: !!options.flowSettingsEnabled });
21
+
22
+ const ds = engine.dataSourceManager.getDataSource('main');
23
+ ds.addCollection({
24
+ name: 'posts',
25
+ filterTargetKey: 'id',
26
+ fields: [
27
+ { name: 'id', type: 'integer', interface: 'number' },
28
+ { name: 'title', type: 'string', interface: 'input' },
29
+ ],
30
+ });
31
+
32
+ return engine.createModel<TableBlockModel>({
33
+ uid: `posts-table-${options.isMobileLayout ? 'mobile' : 'desktop'}`,
34
+ use: 'TableBlockModel',
35
+ stepParams: {
36
+ resourceSettings: {
37
+ init: {
38
+ dataSourceKey: 'main',
39
+ collectionName: 'posts',
40
+ },
41
+ },
42
+ },
43
+ });
44
+ }
45
+
46
+ function getSettingsButton(element: React.ReactElement) {
47
+ return element.props.children as React.ReactElement;
48
+ }
49
+
50
+ describe('TableBlockModel mobile settings buttons', () => {
51
+ it('renders table actions and fields settings as icon-only buttons in mobile layouts', () => {
52
+ const model = createTableModel({ isMobileLayout: true, flowSettingsEnabled: true });
53
+ const actionButton = getSettingsButton(model.renderConfigureActions() as React.ReactElement);
54
+ const addColumn = model.getColumns().find((column) => column.key === 'addColumn');
55
+ const fieldsButton = getSettingsButton(
56
+ (addColumn?.title as React.ReactElement).type({ model }) as React.ReactElement,
57
+ );
58
+
59
+ expect(actionButton.props.children).toBeNull();
60
+ expect(actionButton.props['aria-label']).toBe('Actions');
61
+ expect(fieldsButton.props.children).toBeNull();
62
+ expect(fieldsButton.props['aria-label']).toBe('Fields');
63
+ expect(addColumn?.width).toBeUndefined();
64
+ });
65
+
66
+ it('keeps table actions and fields settings labels outside mobile layouts', () => {
67
+ const model = createTableModel({ flowSettingsEnabled: true });
68
+ const actionButton = getSettingsButton(model.renderConfigureActions() as React.ReactElement);
69
+ const addColumn = model.getColumns().find((column) => column.key === 'addColumn');
70
+ const fieldsButton = getSettingsButton(
71
+ (addColumn?.title as React.ReactElement).type({ model }) as React.ReactElement,
72
+ );
73
+
74
+ expect(actionButton.props.children).toBe('Actions');
75
+ expect(fieldsButton.props.children).toBe('Fields');
76
+ expect(addColumn?.width).toBe(100);
77
+ });
78
+ });
@@ -130,9 +130,14 @@ describe('TableColumnModel sorter settings', () => {
130
130
  const titleFieldStep = model.getFlow('tableColumnSettings')?.steps?.fieldNames as any;
131
131
  const setStepParams = vi.fn();
132
132
  const setProps = vi.fn();
133
+ const setFieldProps = vi.fn();
133
134
  const dispatchEvent = vi.fn();
135
+ const saveFieldModel = vi.fn();
134
136
  const targetCollectionField = {
135
- getComponentProps: () => ({}),
137
+ getComponentProps: () => ({
138
+ format: 'hh:mm:ss a',
139
+ timeFormat: 'hh:mm:ss a',
140
+ }),
136
141
  };
137
142
 
138
143
  await titleFieldStep.beforeParamsSave(
@@ -156,8 +161,10 @@ describe('TableColumnModel sorter settings', () => {
156
161
  subModels: {
157
162
  field: {
158
163
  use: 'DisplayTextFieldModel',
164
+ setProps: setFieldProps,
159
165
  setStepParams,
160
166
  dispatchEvent,
167
+ save: saveFieldModel,
161
168
  },
162
169
  },
163
170
  setStepParams,
@@ -169,6 +176,155 @@ describe('TableColumnModel sorter settings', () => {
169
176
  );
170
177
 
171
178
  expect(setStepParams).toHaveBeenCalledWith('tableColumnSettings', 'model', { use: 'DisplayTextFieldModel' });
179
+ expect(setFieldProps).toHaveBeenCalledWith(
180
+ expect.objectContaining({
181
+ format: 'hh:mm:ss a',
182
+ timeFormat: 'hh:mm:ss a',
183
+ titleField: 'code',
184
+ }),
185
+ );
186
+ expect(setProps).toHaveBeenCalledWith(
187
+ expect.objectContaining({
188
+ format: 'hh:mm:ss a',
189
+ timeFormat: 'hh:mm:ss a',
190
+ titleField: 'code',
191
+ }),
192
+ );
193
+ expect(saveFieldModel).toHaveBeenCalled();
194
+ });
195
+
196
+ it('clears stale datetime display props when association title field changes to date only', async () => {
197
+ const engine = new FlowEngine();
198
+ const model = new TableColumnModel({ uid: 'table-column-title-field-date-only', flowEngine: engine } as any);
199
+ const titleFieldStep = model.getFlow('tableColumnSettings')?.steps?.fieldNames as any;
200
+ const setStepParams = vi.fn();
201
+ const setProps = vi.fn();
202
+ const setFieldProps = vi.fn();
203
+ const targetCollectionField = {
204
+ getComponentProps: () => ({
205
+ dateOnly: true,
206
+ showTime: false,
207
+ }),
208
+ };
209
+
210
+ await titleFieldStep.beforeParamsSave(
211
+ {
212
+ collectionField: {
213
+ isAssociationField: () => true,
214
+ targetCollection: {
215
+ name: 'departments',
216
+ getField: () => targetCollectionField,
217
+ },
218
+ },
219
+ model: {
220
+ collectionField: {
221
+ dataSourceKey: 'main',
222
+ },
223
+ constructor: {
224
+ getDefaultBindingByField: () => ({
225
+ modelName: 'DisplayDateTimeFieldModel',
226
+ }),
227
+ },
228
+ subModels: {
229
+ field: {
230
+ use: 'DisplayDateTimeFieldModel',
231
+ setProps: setFieldProps,
232
+ setStepParams,
233
+ dispatchEvent: vi.fn(),
234
+ save: vi.fn(),
235
+ },
236
+ },
237
+ setStepParams,
238
+ setProps,
239
+ },
240
+ },
241
+ { label: 'dateOnly' },
242
+ { label: 'time' },
243
+ );
244
+
245
+ expect(setFieldProps).toHaveBeenCalledWith(
246
+ expect.objectContaining({
247
+ dateOnly: true,
248
+ format: undefined,
249
+ showTime: false,
250
+ timeFormat: undefined,
251
+ titleField: 'dateOnly',
252
+ }),
253
+ );
254
+ expect(setProps).toHaveBeenCalledWith(
255
+ expect.objectContaining({
256
+ dateOnly: true,
257
+ format: undefined,
258
+ showTime: false,
259
+ timeFormat: undefined,
260
+ titleField: 'dateOnly',
261
+ }),
262
+ );
263
+ });
264
+
265
+ it('keeps saved association title datetime format when table column initializes again', async () => {
266
+ const engine = new FlowEngine();
267
+ const model = new TableColumnModel({
268
+ uid: 'table-column-title-field-saved-datetime-format',
269
+ flowEngine: engine,
270
+ } as any);
271
+ const initStep = model.getFlow('tableColumnSettings')?.steps?.init as any;
272
+ const setProps = vi.fn();
273
+ const targetCollectionField = {
274
+ getComponentProps: () => ({
275
+ dateFormat: 'YYYY-MM-DD',
276
+ format: 'YYYY-MM-DD HH:mm:ss',
277
+ showTime: true,
278
+ timeFormat: 'HH:mm:ss',
279
+ }),
280
+ };
281
+
282
+ await initStep.handler({
283
+ model: {
284
+ context: {
285
+ collectionField: {
286
+ title: 'Shipments',
287
+ name: 'shipments',
288
+ isAssociationField: () => true,
289
+ getComponentProps: () => ({
290
+ fieldNames: {
291
+ label: 'shipmentsDatetime',
292
+ },
293
+ }),
294
+ targetCollection: {
295
+ getField: () => targetCollectionField,
296
+ },
297
+ },
298
+ },
299
+ props: {
300
+ titleField: 'shipmentsDatetime',
301
+ },
302
+ subModels: {
303
+ field: {
304
+ getStepParams: (flowKey, stepKey) =>
305
+ flowKey === 'datetimeSettings' && stepKey === 'dateFormat'
306
+ ? {
307
+ picker: 'date',
308
+ dateFormat: 'YYYY-MM-DD',
309
+ showTime: true,
310
+ timeFormat: 'h:mm a',
311
+ }
312
+ : undefined,
313
+ },
314
+ },
315
+ applySubModelsBeforeRenderFlows: vi.fn(),
316
+ setProps,
317
+ },
318
+ });
319
+
320
+ expect(setProps).toHaveBeenCalledWith(
321
+ expect.objectContaining({
322
+ dateFormat: 'YYYY-MM-DD',
323
+ format: 'YYYY-MM-DD h:mm a',
324
+ showTime: true,
325
+ timeFormat: 'h:mm a',
326
+ }),
327
+ );
172
328
  });
173
329
 
174
330
  it('does not update field component setting when title field refresh fails', async () => {
@@ -203,8 +359,10 @@ describe('TableColumnModel sorter settings', () => {
203
359
  subModels: {
204
360
  field: {
205
361
  use: 'DisplayTextFieldModel',
362
+ setProps: vi.fn(),
206
363
  setStepParams: vi.fn(),
207
364
  dispatchEvent: vi.fn().mockRejectedValue(new Error('beforeRender failed')),
365
+ save: vi.fn(),
208
366
  },
209
367
  },
210
368
  setStepParams,
@@ -11,7 +11,7 @@ import { FormItemModel } from '../../blocks/form/FormItemModel';
11
11
  import { FieldModel } from '../../base/FieldModel';
12
12
  export class AssociationFieldModel extends FieldModel {
13
13
  operator = '$eq';
14
- updateAssociation = true;
14
+ updateAssociation = false;
15
15
  }
16
16
 
17
17
  AssociationFieldModel.registerFlow({
@@ -203,6 +203,8 @@ const DisplayTable = (props) => {
203
203
  onSelectExitRecordClick,
204
204
  resetPage,
205
205
  allowCreate,
206
+ formValuesChangeEmitter,
207
+ onResetFieldValue,
206
208
  } = props;
207
209
  const [currentPage, setCurrentPage] = useState(1);
208
210
  const [currentPageSize, setCurrentPageSize] = useState(pageSize);
@@ -223,6 +225,18 @@ const DisplayTable = (props) => {
223
225
  resetPage && setCurrentPage(1);
224
226
  }, [resetPage]);
225
227
 
228
+ useEffect(() => {
229
+ if (!formValuesChangeEmitter?.on || !formValuesChangeEmitter?.off || !onResetFieldValue) return;
230
+ const listener = () => {
231
+ onResetFieldValue();
232
+ setTableData([]);
233
+ };
234
+ formValuesChangeEmitter.on('onFieldReset', listener);
235
+ return () => {
236
+ formValuesChangeEmitter.off('onFieldReset', listener);
237
+ };
238
+ }, [formValuesChangeEmitter, onResetFieldValue]);
239
+
226
240
  const pagination = useMemo(() => {
227
241
  return {
228
242
  current: currentPage, // 当前页码
@@ -237,7 +251,7 @@ const DisplayTable = (props) => {
237
251
  return t('Total {{count}} items', { count: total });
238
252
  },
239
253
  } as any;
240
- }, [currentPage, currentPageSize, tableData]);
254
+ }, [currentPage, currentPageSize, tableData, t]);
241
255
 
242
256
  const columns = useMemo(() => {
243
257
  const cols = adjustColumnOrder(
@@ -421,10 +435,6 @@ export class PopupSubTableFieldModel extends AssociationFieldModel {
421
435
  currentPageSize,
422
436
  });
423
437
  };
424
- // 监听表单reset
425
- this.context.blockModel.emitter.on('onFieldReset', () => {
426
- this.props?.onChange([]);
427
- });
428
438
  }
429
439
  }
430
440
 
@@ -457,8 +467,21 @@ export class PopupSubTableFieldModel extends AssociationFieldModel {
457
467
  }
458
468
 
459
469
  public render() {
470
+ const fieldPathArray = this.context.fieldPathArray ?? this.parent?.context?.fieldPathArray;
471
+ const onResetFieldValue = () => {
472
+ const value = [];
473
+ this.setProps({ value });
474
+ this.context.blockModel?.setFieldValue?.(fieldPathArray, value);
475
+ };
460
476
  return (
461
- <DisplayTable {...this.props} collection={this.collection} baseColumns={this.getBaseColumns(this)} model={this} />
477
+ <DisplayTable
478
+ {...this.props}
479
+ collection={this.collection}
480
+ baseColumns={this.getBaseColumns(this)}
481
+ model={this}
482
+ formValuesChangeEmitter={this.context.blockModel?.emitter}
483
+ onResetFieldValue={onResetFieldValue}
484
+ />
462
485
  );
463
486
  }
464
487
  }
@@ -99,6 +99,18 @@ export function collectAssociationHydrationCandidates(options: {
99
99
  return candidates;
100
100
  }
101
101
 
102
+ export function getAssociationHydrationNamePath(model: any) {
103
+ return model?.context?.fieldPathArray ?? model?.context?.fieldPath ?? model?.props?.name;
104
+ }
105
+
106
+ export function getAssociationHydrationSetterContext(model: any) {
107
+ if (typeof model?.context?.setFormValue === 'function') {
108
+ return model.context;
109
+ }
110
+
111
+ return model?.context?.blockModel?.context;
112
+ }
113
+
102
114
  function markAssociationHydrationDone(statusMap: Map<string, HydrateStatus>, tkKey: string | null | undefined) {
103
115
  if (!tkKey) return;
104
116
  statusMap.set(tkKey, 'done');
@@ -245,8 +257,8 @@ const LazySelect = (props: Readonly<LazySelectProps>) => {
245
257
  });
246
258
  if (!candidates.length) return;
247
259
 
248
- const namePath = model?.props?.name ?? model?.context?.fieldPathArray;
249
- const blockCtx: any = model?.context?.blockModel?.context;
260
+ const namePath = getAssociationHydrationNamePath(model);
261
+ const setterCtx: any = getAssociationHydrationSetterContext(model);
250
262
 
251
263
  candidates.forEach(({ item, tk, tkKey }) => {
252
264
  void (async () => {
@@ -268,8 +280,8 @@ const LazySelect = (props: Readonly<LazySelectProps>) => {
268
280
  })
269
281
  : merge;
270
282
 
271
- if (blockCtx && typeof blockCtx.setFormValue === 'function' && namePath != null) {
272
- await blockCtx.setFormValue(namePath, nextValue, {
283
+ if (setterCtx && typeof setterCtx.setFormValue === 'function' && namePath != null) {
284
+ await setterCtx.setFormValue(namePath, nextValue, {
273
285
  source: 'default',
274
286
  markExplicit: false,
275
287
  triggerEvent: false,
@@ -231,6 +231,20 @@ export function getLatestSubTableRowRecord(form: any, fieldIndex: unknown, fallb
231
231
  return typeof latestRecord === 'undefined' ? fallbackRecord : latestRecord;
232
232
  }
233
233
 
234
+ export function buildSubTableColumnNamePath(
235
+ fieldPath: Array<string | number>,
236
+ rowIdx: number,
237
+ namePath: string | number,
238
+ rowFieldIndex: unknown,
239
+ ): Array<string | number> {
240
+ const currentRowPath = buildRowPathFromFieldIndex(rowFieldIndex);
241
+ if (currentRowPath?.length) {
242
+ return [...currentRowPath, namePath];
243
+ }
244
+
245
+ return [...fieldPath, rowIdx, namePath];
246
+ }
247
+
234
248
  function shouldCommitImmediately(value: any) {
235
249
  if (Array.isArray(value)) {
236
250
  return true;
@@ -372,9 +386,18 @@ const MemoCell: React.FC<CellProps> = React.memo(
372
386
  {parent.mapSubModels('field', (action: FieldModel) => {
373
387
  const fieldPath = action.context.fieldPath.split('.');
374
388
  const namePath = fieldPath.pop();
389
+ if (typeof namePath === 'undefined') {
390
+ return null;
391
+ }
392
+ const fieldNamePath = buildSubTableColumnNamePath(fieldPath, rowIdx, namePath, rowFork?.context?.fieldIndex);
393
+ const formItemName = buildDynamicNamePath([...fieldPath, rowIdx, namePath], parentFieldIndex);
375
394
 
376
395
  const fork: any = action.createFork({}, `${id}`);
377
396
  fork.context.defineProperty('currentObject', { get: () => record });
397
+ fork.context.defineProperty('fieldPathArray', {
398
+ get: () => fieldNamePath,
399
+ cache: false,
400
+ });
378
401
  if (rowFork) {
379
402
  const itemOptions = rowFork.context.getPropertyOptions?.('item');
380
403
  const { value: _value, ...itemOptionsWithoutValue } = (itemOptions || {}) as any;
@@ -415,7 +438,7 @@ const MemoCell: React.FC<CellProps> = React.memo(
415
438
  <FormItem
416
439
  {...parent.props}
417
440
  key={id}
418
- name={buildDynamicNamePath([...fieldPath, rowIdx, namePath], parentFieldIndex)}
441
+ name={formItemName}
419
442
  style={{ marginBottom: 0 }}
420
443
  showLabel={false}
421
444
  disabled={
@@ -70,6 +70,7 @@ export function SubTableField(props) {
70
70
  getCurrentValue,
71
71
  fieldPathArray,
72
72
  formValuesChangeEmitter,
73
+ onResetFieldValue,
73
74
  } = props;
74
75
  const [currentPage, setCurrentPage] = useState(1);
75
76
  const [currentPageSize, setCurrentPageSize] = useState(pageSize);
@@ -97,6 +98,17 @@ export function SubTableField(props) {
97
98
  formValuesChangeEmitter.off('formValuesChange', listener);
98
99
  };
99
100
  }, [fieldPathArray, formValuesChangeEmitter]);
101
+ useEffect(() => {
102
+ if (!formValuesChangeEmitter?.on || !formValuesChangeEmitter?.off || !onResetFieldValue) return;
103
+ const listener = () => {
104
+ onResetFieldValue();
105
+ forceRefresh((v) => v + 1);
106
+ };
107
+ formValuesChangeEmitter.on('onFieldReset', listener);
108
+ return () => {
109
+ formValuesChangeEmitter.off('onFieldReset', listener);
110
+ };
111
+ }, [formValuesChangeEmitter, onResetFieldValue]);
100
112
  const applyValue = React.useCallback((nextValue: any) => onChange?.(normalizeSubTableRows(nextValue)), [onChange]);
101
113
  const getLatestValue = React.useCallback(() => normalizeSubTableRows(getCurrentValue()), [getCurrentValue]);
102
114
  useEffect(() => {
@@ -125,7 +137,7 @@ export function SubTableField(props) {
125
137
  return t('Total {{count}} items', { count: total });
126
138
  },
127
139
  } as any;
128
- }, [currentPage, currentPageSize, currentValue.length]);
140
+ }, [currentPage, currentPageSize, currentValue.length, t]);
129
141
 
130
142
  // 新增一行
131
143
  const handleAdd = () => {
@@ -15,6 +15,7 @@ import {
15
15
  SubTableColumnModel,
16
16
  getLatestSubTableRowRecord,
17
17
  buildRowPathFromFieldIndex,
18
+ buildSubTableColumnNamePath,
18
19
  isSubTableColumnConfiguredReadPretty,
19
20
  getSubTableColumnTitleField,
20
21
  getSubTableColumnReadPrettyFieldProps,
@@ -47,6 +48,17 @@ describe('SubTableColumnModel row record helpers', () => {
47
48
  expect(buildRowPathFromFieldIndex(['users:1', 'roles:2'])).toEqual(['users', 1, 'roles', 2]);
48
49
  });
49
50
 
51
+ it('builds absolute form item paths for nested sub-table columns', () => {
52
+ expect(buildSubTableColumnNamePath(['roles'], 0, 'name', ['roles:0'])).toEqual(['roles', 0, 'name']);
53
+ expect(buildSubTableColumnNamePath(['orders', 'lines'], 1, 'product', ['orders:0', 'lines:1'])).toEqual([
54
+ 'orders',
55
+ 0,
56
+ 'lines',
57
+ 1,
58
+ 'product',
59
+ ]);
60
+ });
61
+
50
62
  it('prefers the latest row value from form over the fallback record', () => {
51
63
  const form = {
52
64
  getFieldValue: vi.fn((path: any) => {