@nocobase/client-v2 2.2.0-beta.5 → 2.2.0-beta.7

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 (121) hide show
  1. package/es/PluginSettingsManager.d.ts +33 -0
  2. package/es/RouteRepository.d.ts +21 -6
  3. package/es/components/form/JsonTextArea.d.ts +2 -1
  4. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  5. package/es/components/form/index.d.ts +1 -0
  6. package/es/components/index.d.ts +1 -0
  7. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  8. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  9. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  10. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  11. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +4 -0
  12. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  13. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  14. package/es/flow/models/base/GridModel.d.ts +1 -1
  15. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  16. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  17. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  18. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
  19. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  20. package/es/flow-compat/FieldValidation.d.ts +2 -1
  21. package/es/index.d.ts +2 -1
  22. package/es/index.mjs +145 -119
  23. package/lib/index.js +148 -122
  24. package/package.json +7 -7
  25. package/src/PluginSettingsManager.ts +53 -0
  26. package/src/RouteRepository.ts +126 -24
  27. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  28. package/src/__tests__/RouteRepository.test.ts +216 -0
  29. package/src/__tests__/exports.test.ts +16 -0
  30. package/src/__tests__/plugin-manager.test.tsx +44 -2
  31. package/src/__tests__/settings-center.test.tsx +40 -1
  32. package/src/components/form/JsonTextArea.tsx +21 -11
  33. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  34. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  35. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  36. package/src/components/form/index.tsx +1 -0
  37. package/src/components/index.ts +1 -0
  38. package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
  39. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
  40. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  41. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  42. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  43. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  44. package/src/flow/actions/customVariable.tsx +1 -2
  45. package/src/flow/actions/dataScopeFilter.ts +10 -0
  46. package/src/flow/actions/linkageRules.tsx +122 -34
  47. package/src/flow/actions/runjs.tsx +2 -14
  48. package/src/flow/actions/validation.tsx +62 -30
  49. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +33 -10
  50. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
  51. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
  52. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  53. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  54. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  55. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  56. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +21 -8
  57. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  58. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  59. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
  60. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  61. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  62. package/src/flow/components/DefaultValue.tsx +1 -2
  63. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  64. package/src/flow/components/FlowRoute.tsx +56 -11
  65. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  66. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  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/components/fieldAssignOptions.ts +155 -27
  71. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  72. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  73. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  74. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  75. package/src/flow/models/base/ActionModel.tsx +12 -1
  76. package/src/flow/models/base/GridModel.tsx +38 -7
  77. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  78. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  79. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  80. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  81. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  82. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
  83. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  84. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
  85. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  86. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  87. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  88. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  89. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
  90. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  91. package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
  92. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  93. package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
  94. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  95. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
  96. package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
  97. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
  98. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  99. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  100. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
  101. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  102. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  103. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  104. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  105. package/src/flow/models/fields/InputFieldModel.tsx +14 -22
  106. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  107. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  108. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  109. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +17 -0
  110. package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
  111. package/src/flow-compat/FieldValidation.tsx +122 -60
  112. package/src/flow-compat/Popover.tsx +43 -4
  113. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  114. package/src/index.ts +8 -1
  115. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  116. package/src/nocobase-buildin-plugin/index.tsx +0 -2
  117. package/src/settings-center/AdminSettingsLayout.tsx +3 -0
  118. package/src/settings-center/SystemSettingsPage.tsx +0 -1
  119. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  120. package/src/settings-center/plugin-manager/index.tsx +3 -0
  121. package/src/settings-center/utils.tsx +0 -6
@@ -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
+ });
@@ -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
  }
@@ -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 = () => {
@@ -0,0 +1,180 @@
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 { act, render } from '@nocobase/test/client';
12
+ import { FlowEngine, type FlowModel } from '@nocobase/flow-engine';
13
+ import { describe, expect, it, vi } from 'vitest';
14
+ import { PopupSubTableFieldModel } from '../../PopupSubTableFieldModel';
15
+ import { SubTableFieldModel } from '..';
16
+
17
+ vi.mock('react-i18next', async (importOriginal) => ({
18
+ ...(await importOriginal<any>()),
19
+ useTranslation: () => ({
20
+ t: (value: string) => value,
21
+ }),
22
+ }));
23
+
24
+ vi.mock('antd', async () => {
25
+ const actual = await vi.importActual<any>('antd');
26
+ return {
27
+ ...actual,
28
+ Table: ({ dataSource = [], columns = [] }: any) =>
29
+ React.createElement(
30
+ 'div',
31
+ { 'data-testid': 'subtable' },
32
+ dataSource.map((record: any, rowIdx: number) =>
33
+ React.createElement(
34
+ 'div',
35
+ { 'data-testid': `row-${rowIdx}`, key: record.__index__ || rowIdx },
36
+ columns.map((column: any) =>
37
+ React.createElement(
38
+ 'div',
39
+ { 'data-testid': `cell-${rowIdx}-${String(column.dataIndex || column.key)}`, key: column.key },
40
+ column.render?.(record[column.dataIndex], record, rowIdx),
41
+ ),
42
+ ),
43
+ ),
44
+ ),
45
+ ),
46
+ };
47
+ });
48
+
49
+ function createSubTableFieldModel(props: Record<string, unknown> = {}) {
50
+ const engine = new FlowEngine();
51
+ engine.registerModels({ SubTableFieldModel });
52
+ const setFieldValue = vi.fn();
53
+
54
+ const blockModel = engine.createModel<FlowModel>({
55
+ use: 'FlowModel',
56
+ uid: 'form-block',
57
+ });
58
+ (blockModel as FlowModel & { setFieldValue: typeof setFieldValue }).setFieldValue = setFieldValue;
59
+ const parent = engine.createModel<FlowModel>({
60
+ use: 'FlowModel',
61
+ uid: 'form-item',
62
+ });
63
+
64
+ parent.context.defineProperty('blockModel', { value: blockModel });
65
+ parent.context.defineProperty('collectionField', {
66
+ value: {
67
+ target: 'roles',
68
+ targetCollection: {
69
+ filterTargetKey: 'id',
70
+ },
71
+ },
72
+ });
73
+ parent.context.defineProperty('fieldPathArray', { value: ['roles'] });
74
+
75
+ const model = engine.createModel<SubTableFieldModel>({
76
+ use: 'SubTableFieldModel',
77
+ uid: 'roles-subtable',
78
+ parentId: parent.uid,
79
+ props,
80
+ });
81
+
82
+ return { blockModel, model, setFieldValue };
83
+ }
84
+
85
+ async function createPopupSubTableFieldModel(props: Record<string, unknown> = {}) {
86
+ const engine = new FlowEngine();
87
+ engine.registerModels({ PopupSubTableFieldModel });
88
+ const setFieldValue = vi.fn();
89
+
90
+ const blockModel = engine.createModel<FlowModel>({
91
+ use: 'FlowModel',
92
+ uid: 'popup-form-block',
93
+ });
94
+ (blockModel as FlowModel & { setFieldValue: typeof setFieldValue }).setFieldValue = setFieldValue;
95
+ const parent = engine.createModel<FlowModel>({
96
+ use: 'FlowModel',
97
+ uid: 'popup-form-item',
98
+ });
99
+
100
+ parent.context.defineProperty('blockModel', { value: blockModel });
101
+ parent.context.defineProperty('collectionField', {
102
+ value: {
103
+ target: 'roles',
104
+ targetCollection: {
105
+ filterTargetKey: 'id',
106
+ },
107
+ },
108
+ });
109
+ parent.context.defineProperty('fieldPathArray', { value: ['roles'] });
110
+
111
+ const model = engine.createModel<PopupSubTableFieldModel>({
112
+ use: 'PopupSubTableFieldModel',
113
+ uid: 'roles-popup-subtable',
114
+ parentId: parent.uid,
115
+ props,
116
+ });
117
+ (model.subModels as Record<string, unknown>).subTableColumns = [];
118
+ await model.onDispatchEventStart('beforeRender');
119
+
120
+ return { blockModel, model, setFieldValue };
121
+ }
122
+
123
+ describe('SubTable field reset', () => {
124
+ it('resets the rendered fork through its own form field path', () => {
125
+ const onChange = vi.fn();
126
+ const { blockModel, model, setFieldValue } = createSubTableFieldModel({
127
+ onChange,
128
+ value: [{ id: 1 }],
129
+ });
130
+ const fork = model.createFork({ value: [{ id: 1 }] }, 'orders.0.lines');
131
+ fork.context.defineProperty('fieldPathArray', { value: ['orders', 0, 'lines'] });
132
+
133
+ render(React.createElement(React.Fragment, null, fork.render()));
134
+
135
+ act(() => {
136
+ blockModel.emitter.emit('onFieldReset');
137
+ });
138
+
139
+ expect(fork.props.value).toEqual([]);
140
+ expect(setFieldValue).toHaveBeenCalledWith(['orders', 0, 'lines'], []);
141
+ expect(onChange).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it('resets the non-forked model through the parent form field path', () => {
145
+ const onChange = vi.fn();
146
+ const { blockModel, model, setFieldValue } = createSubTableFieldModel({
147
+ onChange,
148
+ value: [{ id: 1 }],
149
+ });
150
+
151
+ render(React.createElement(React.Fragment, null, model.render()));
152
+
153
+ act(() => {
154
+ blockModel.emitter.emit('onFieldReset');
155
+ });
156
+
157
+ expect(model.props.value).toEqual([]);
158
+ expect(setFieldValue).toHaveBeenCalledWith(['roles'], []);
159
+ expect(onChange).not.toHaveBeenCalled();
160
+ });
161
+
162
+ it('resets popup sub-table form values without relying on onChange', async () => {
163
+ const onChange = vi.fn();
164
+ const { blockModel, model, setFieldValue } = await createPopupSubTableFieldModel({
165
+ onChange,
166
+ pageSize: 10,
167
+ value: [{ id: 1 }],
168
+ });
169
+
170
+ render(React.createElement(React.Fragment, null, model.render()));
171
+
172
+ act(() => {
173
+ blockModel.emitter.emit('onFieldReset');
174
+ });
175
+
176
+ expect(model.props.value).toEqual([]);
177
+ expect(setFieldValue).toHaveBeenCalledWith(['roles'], []);
178
+ expect(onChange).not.toHaveBeenCalled();
179
+ });
180
+ });
@@ -112,6 +112,11 @@ export class SubTableFieldModel extends AssociationFieldModel {
112
112
  };
113
113
  const isConfigMode = !!this.context.flowSettingsEnabled;
114
114
  const fieldPathArray = this.context.fieldPathArray ?? this.parent?.context?.fieldPathArray;
115
+ const onResetFieldValue = () => {
116
+ const value = [];
117
+ this.setProps({ value });
118
+ this.context.blockModel?.setFieldValue?.(fieldPathArray, value);
119
+ };
115
120
  return (
116
121
  <SubTableField
117
122
  {...this.props}
@@ -124,6 +129,7 @@ export class SubTableFieldModel extends AssociationFieldModel {
124
129
  formValuesChangeEmitter={this.context.blockModel?.emitter}
125
130
  fieldPathArray={fieldPathArray}
126
131
  getCurrentValue={() => this.getCurrentValue()}
132
+ onResetFieldValue={onResetFieldValue}
127
133
  />
128
134
  );
129
135
  }
@@ -136,10 +142,6 @@ export class SubTableFieldModel extends AssociationFieldModel {
136
142
  this.context.defineProperty('collection', {
137
143
  get: () => this.context.collectionField.targetCollection,
138
144
  });
139
- // 监听表单reset
140
- this.context.blockModel.emitter.on('onFieldReset', () => {
141
- this.props.onChange([]);
142
- });
143
145
  this.onSelectExitRecordClick = (setCurrentPage, currentPageSize) => {
144
146
  this.setCurrentPage = setCurrentPage;
145
147
  this.currentPageSize = currentPageSize;
@@ -0,0 +1,72 @@
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, FlowModel } from '@nocobase/flow-engine';
11
+ import { describe, expect, it, vi } from 'vitest';
12
+ import { AssociationFieldModel } from '../AssociationFieldModel';
13
+
14
+ class DefaultAssociationFieldModel extends AssociationFieldModel {}
15
+
16
+ class UpdateAssociationFieldModel extends AssociationFieldModel {
17
+ updateAssociation = true;
18
+ }
19
+
20
+ async function runAssociationFieldInit(FieldModelClass: typeof AssociationFieldModel) {
21
+ const engine = new FlowEngine();
22
+ engine.registerModels({
23
+ DefaultAssociationFieldModel,
24
+ UpdateAssociationFieldModel,
25
+ });
26
+
27
+ const addUpdateAssociationValues = vi.fn();
28
+ const blockModel = engine.createModel<FlowModel>({
29
+ use: 'FlowModel',
30
+ uid: 'form-block',
31
+ });
32
+ blockModel.context.defineProperty('resource', {
33
+ value: { addUpdateAssociationValues },
34
+ });
35
+
36
+ const formItem = engine.createModel<FlowModel>({
37
+ use: 'FlowModel',
38
+ uid: 'form-item',
39
+ });
40
+ (formItem as FlowModel & { fieldPath: string }).fieldPath = 'author';
41
+ formItem.context.defineProperty('blockModel', { value: blockModel });
42
+
43
+ const field = engine.createModel<AssociationFieldModel>({
44
+ use: FieldModelClass,
45
+ uid: 'association-field',
46
+ parentId: formItem.uid,
47
+ });
48
+ field.context.defineProperty('collectionField', {
49
+ value: {
50
+ dataSourceKey: 'main',
51
+ target: 'users',
52
+ },
53
+ });
54
+
55
+ await field.applyFlow('AssociationFieldInit');
56
+
57
+ return addUpdateAssociationValues;
58
+ }
59
+
60
+ describe('AssociationFieldModel updateAssociation', () => {
61
+ it('does not add updateAssociationValues by default', async () => {
62
+ const addUpdateAssociationValues = await runAssociationFieldInit(DefaultAssociationFieldModel);
63
+
64
+ expect(addUpdateAssociationValues).not.toHaveBeenCalled();
65
+ });
66
+
67
+ it('adds updateAssociationValues for opt-in association fields', async () => {
68
+ const addUpdateAssociationValues = await runAssociationFieldInit(UpdateAssociationFieldModel);
69
+
70
+ expect(addUpdateAssociationValues).toHaveBeenCalledWith('author');
71
+ });
72
+ });
@@ -9,31 +9,18 @@
9
9
 
10
10
  import { EditableItemModel, FilterableItemModel, tExpr } from '@nocobase/flow-engine';
11
11
  import { Input } from 'antd';
12
- import type { InputProps, InputRef } from 'antd';
13
- import React, { useEffect, useRef } from 'react';
12
+ import type { InputProps } from 'antd';
13
+ import React, { useEffect, useState } from 'react';
14
14
  import { customAlphabet as Alphabet } from 'nanoid';
15
15
  import { FieldModel } from '../base/FieldModel';
16
16
  import { ScanInput } from '../../../components/form/ScanInput';
17
17
 
18
18
  function IMESafeInput(props: InputProps) {
19
19
  const { value, onChange, onCompositionStart, onCompositionEnd, ...rest } = props;
20
- const inputRef = useRef<InputRef>(null);
21
- const previousValueRef = useRef(value);
22
- const defaultValue = typeof value === 'bigint' ? String(value) : value;
20
+ const [inputValue, setInputValue] = useState<InputProps['value']>(() => normalizeInputValue(value));
23
21
 
24
22
  useEffect(() => {
25
- if (Object.is(previousValueRef.current, value)) {
26
- return;
27
- }
28
- previousValueRef.current = value;
29
-
30
- const input = inputRef.current?.input;
31
- if (input) {
32
- const nextValue = value == null ? '' : String(value);
33
- if (input.value !== nextValue) {
34
- input.value = nextValue;
35
- }
36
- }
23
+ setInputValue(normalizeInputValue(value));
37
24
  }, [value]);
38
25
 
39
26
  const getEventValue = (event: React.ChangeEvent<HTMLInputElement> | React.CompositionEvent<HTMLInputElement>) =>
@@ -42,24 +29,29 @@ function IMESafeInput(props: InputProps) {
42
29
  return (
43
30
  <Input
44
31
  {...rest}
45
- ref={inputRef}
46
- defaultValue={defaultValue}
32
+ value={inputValue}
47
33
  onChange={(event) => {
48
- previousValueRef.current = getEventValue(event);
34
+ setInputValue(getEventValue(event));
49
35
  onChange?.(event);
50
36
  }}
51
37
  onCompositionStart={(event) => {
52
- previousValueRef.current = getEventValue(event);
38
+ setInputValue(getEventValue(event));
53
39
  onCompositionStart?.(event);
54
40
  }}
55
41
  onCompositionEnd={(event) => {
56
- previousValueRef.current = getEventValue(event);
42
+ setInputValue(getEventValue(event));
57
43
  onCompositionEnd?.(event);
58
44
  }}
59
45
  />
60
46
  );
61
47
  }
62
48
 
49
+ function normalizeInputValue(nextValue: unknown): InputProps['value'] {
50
+ if (typeof nextValue === 'bigint') return String(nextValue);
51
+ if (nextValue == null) return '';
52
+ return nextValue as InputProps['value'];
53
+ }
54
+
63
55
  export class InputFieldModel extends FieldModel {
64
56
  render() {
65
57
  if (this.props.enableScan) {
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { ElementProxy, tExpr, createSafeDocument, createSafeWindow, createSafeNavigator } from '@nocobase/flow-engine';
10
+ import { ElementProxy, tExpr } from '@nocobase/flow-engine';
11
11
  import React, { useEffect, useMemo, useRef } from 'react';
12
12
  import { Input } from 'antd';
13
13
  import { FieldModel } from '../base/FieldModel';
@@ -398,16 +398,7 @@ JSEditableFieldModel.registerFlow({
398
398
  get: () => isReadOnlyMode(ctx.model),
399
399
  cache: false,
400
400
  });
401
- const navigator = createSafeNavigator();
402
- await ctx.runjs(
403
- code,
404
- {
405
- window: createSafeWindow({ navigator }),
406
- document: createSafeDocument(),
407
- navigator,
408
- },
409
- { version },
410
- );
401
+ await ctx.runjs(code, undefined, { version });
411
402
  });
412
403
  },
413
404
  },