@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
@@ -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
+ });
@@ -28,6 +28,7 @@ import {
28
28
  normalizeRecordPickerValue,
29
29
  shouldClearRecordPickerValueOnMultipleChange,
30
30
  } from '../RecordPickerFieldModel';
31
+ import { getAssociationHydrationNamePath, getAssociationHydrationSetterContext } from '../RecordSelectFieldModel';
31
32
 
32
33
  function createMockCollection() {
33
34
  return {
@@ -93,6 +94,28 @@ describe('RecordPickerFieldModel item context', () => {
93
94
  expect(shouldClearRecordPickerValueOnMultipleChange({ type: 'belongsTo' }, true, false)).toBe(false);
94
95
  });
95
96
 
97
+ it('uses the current field context for association hydration writes', () => {
98
+ const setFormValue = vi.fn();
99
+ const blockSetFormValue = vi.fn();
100
+ const model = {
101
+ props: {
102
+ name: 'orders',
103
+ },
104
+ context: {
105
+ fieldPathArray: ['orders', 0, 'lines', 1, 'product'],
106
+ setFormValue,
107
+ blockModel: {
108
+ context: {
109
+ setFormValue: blockSetFormValue,
110
+ },
111
+ },
112
+ },
113
+ };
114
+
115
+ expect(getAssociationHydrationNamePath(model)).toEqual(['orders', 0, 'lines', 1, 'product']);
116
+ expect(getAssociationHydrationSetterContext(model)).toBe(model.context);
117
+ });
118
+
96
119
  it('itemChain helpers: createParentItemAccessorsFromInputArgs works', () => {
97
120
  const inputArgs = {
98
121
  parentItem: { value: { id: 1 } },
@@ -197,6 +197,11 @@ export class ClickableFieldModel extends FieldModel {
197
197
  titleField,
198
198
  overflowMode,
199
199
  disabled,
200
+ dateOnly,
201
+ dateFormat,
202
+ format,
203
+ picker,
204
+ showTime,
200
205
  timeFormat,
201
206
  ...restProps
202
207
  } = this.props;
@@ -8,14 +8,42 @@
8
8
  */
9
9
 
10
10
  import { DisplayItemModel, tExpr } from '@nocobase/flow-engine';
11
+ import { getDateTimeFormat, getPickerFormat } from '@nocobase/utils/client';
11
12
  import dayjs from 'dayjs';
12
13
  import React from 'react';
13
14
  import { ClickableFieldModel } from './ClickableFieldModel';
14
15
 
16
+ const stripTimeFromFormat = (format?: string) =>
17
+ format ? format.replace(/\s*[Hh]{1,2}:mm(?::ss)?(?:\.SSS)?(?:\s*[aA])?/g, '').trim() : format;
18
+
19
+ interface DisplayDateTimeFormatProps {
20
+ dateOnly?: boolean;
21
+ picker?: string;
22
+ format?: string;
23
+ dateFormat?: string;
24
+ showTime?: boolean;
25
+ timeFormat?: string;
26
+ }
27
+
28
+ const resolveDisplayDateTimeFormat = (props: DisplayDateTimeFormatProps) => {
29
+ const { dateOnly, picker = 'date', format, dateFormat, showTime, timeFormat } = props;
30
+ const normalizedFormat = stripTimeFromFormat(format);
31
+ if (picker !== 'date') {
32
+ return dateFormat || normalizedFormat || getPickerFormat(picker);
33
+ }
34
+
35
+ if (!dateOnly && !dateFormat && typeof showTime === 'undefined' && !timeFormat && normalizedFormat) {
36
+ return format;
37
+ }
38
+
39
+ const normalizedDateFormat = dateFormat || normalizedFormat || getPickerFormat(picker);
40
+ return getDateTimeFormat(picker, normalizedDateFormat, showTime, timeFormat);
41
+ };
42
+
15
43
  export class DisplayDateTimeFieldModel extends ClickableFieldModel {
16
44
  public renderComponent(value) {
17
45
  const { className, style } = this.props;
18
- const finalFormat = this.props.format;
46
+ const finalFormat = resolveDisplayDateTimeFormat(this.props);
19
47
  let formattedValue = '';
20
48
  if (value) {
21
49
  const day = dayjs(value);
@@ -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
  },
@@ -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, createSafeWindow, createSafeDocument, createSafeNavigator } from '@nocobase/flow-engine';
10
+ import { ElementProxy, tExpr } from '@nocobase/flow-engine';
11
11
  import React, { useEffect } from 'react';
12
12
  import { FieldModel } from '../base/FieldModel';
13
13
  import { resolveRunJsParams } from '../utils/resolveRunJsParams';
@@ -211,12 +211,7 @@ JSFieldModel.registerFlow({
211
211
  get: () => ctx.model.props?.value,
212
212
  cache: false,
213
213
  });
214
- const navigator = createSafeNavigator();
215
- await ctx.runjs(
216
- code,
217
- { window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
218
- { version },
219
- );
214
+ await ctx.runjs(code, undefined, { version });
220
215
  });
221
216
  },
222
217
  },
@@ -7,14 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import {
11
- ElementProxy,
12
- FormItem,
13
- createSafeDocument,
14
- createSafeWindow,
15
- createSafeNavigator,
16
- tExpr,
17
- } from '@nocobase/flow-engine';
10
+ import { ElementProxy, FormItem, tExpr } from '@nocobase/flow-engine';
18
11
  import React from 'react';
19
12
  import { CodeEditor } from '../../components/code-editor';
20
13
  import { CommonItemModel } from '../base/CommonItemModel';
@@ -150,12 +143,7 @@ ctx.render(<JsItem />);
150
143
  ctx.defineProperty('element', {
151
144
  get: () => new ElementProxy(element),
152
145
  });
153
- const navigator = createSafeNavigator();
154
- await ctx.runjs(
155
- code,
156
- { window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
157
- { version },
158
- );
146
+ await ctx.runjs(code, undefined, { version });
159
147
  });
160
148
  },
161
149
  },
@@ -15,17 +15,20 @@ import { MobileSelect } from './mobile-components/MobileSelect';
15
15
  import { enumToOptions, getSelectedEnumLabels, translateOptionLabel } from '../../internal/utils/enumOptionsUtils';
16
16
 
17
17
  const getOriginalEnumOptions = (model: SelectFieldModel) => {
18
- const fromEnum = enumToOptions(model.context.collectionField?.uiSchema?.enum, (text) => text) || [];
18
+ const fromEnum = enumToOptions(model.context.collectionField?.uiSchema?.enum, model.translate) || [];
19
19
  if (fromEnum.length > 0) {
20
20
  return fromEnum.map((option) => ({ ...option }));
21
21
  }
22
22
  const current = Array.isArray(model.props.options) ? model.props.options : [];
23
- return current.map((option) => ({ ...option }));
23
+ return current.map((option) => ({
24
+ ...option,
25
+ label: translateOptionLabel(option.label, model.translate),
26
+ }));
24
27
  };
25
-
26
28
  export class SelectFieldModel extends FieldModel {
27
29
  render() {
28
30
  const fallbackOptions = getOriginalEnumOptions(this);
31
+
29
32
  const options = this.props.options?.map((v) => {
30
33
  return {
31
34
  ...v,
@@ -46,7 +49,6 @@ export class SelectFieldModel extends FieldModel {
46
49
  if (this.context.isMobileLayout) {
47
50
  return <MobileSelect {...this.props} options={options} displayValue={value} />;
48
51
  }
49
-
50
52
  return (
51
53
  <Select
52
54
  {...this.props}
@@ -0,0 +1,96 @@
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 { render, screen } from '@testing-library/react';
12
+ import { describe, expect, it } from 'vitest';
13
+ import { DisplayDateTimeFieldModel } from '../DisplayDateTimeFieldModel';
14
+
15
+ describe('DisplayDateTimeFieldModel', () => {
16
+ it('uses dateFormat, showTime, and timeFormat when rendering read pretty datetime text', () => {
17
+ const engine = new FlowEngine();
18
+ engine.registerModels({ DisplayDateTimeFieldModel });
19
+
20
+ const model = engine.createModel<DisplayDateTimeFieldModel>({
21
+ use: DisplayDateTimeFieldModel,
22
+ uid: 'display-datetime-field-format',
23
+ props: {
24
+ value: '2026-06-15 13:05:06',
25
+ dateFormat: 'YYYY-MM-DD',
26
+ showTime: true,
27
+ timeFormat: 'HH:mm:ss',
28
+ },
29
+ });
30
+
31
+ render(model.render());
32
+
33
+ expect(screen.getByText('2026-06-15 13:05:06')).toBeInTheDocument();
34
+ });
35
+
36
+ it('does not reuse a time-only format when rendering datetime text', () => {
37
+ const engine = new FlowEngine();
38
+ engine.registerModels({ DisplayDateTimeFieldModel });
39
+
40
+ const model = engine.createModel<DisplayDateTimeFieldModel>({
41
+ use: DisplayDateTimeFieldModel,
42
+ uid: 'display-datetime-field-stale-time-format',
43
+ props: {
44
+ value: '2026-06-15 13:05:06',
45
+ format: 'HH:mm:ss',
46
+ showTime: true,
47
+ timeFormat: 'HH:mm:ss',
48
+ },
49
+ });
50
+
51
+ render(model.render());
52
+
53
+ expect(screen.getByText('2026-06-15 13:05:06')).toBeInTheDocument();
54
+ expect(screen.queryByText('13:05:06')).not.toBeInTheDocument();
55
+ });
56
+
57
+ it('keeps an existing complete datetime format when no split format props are configured', () => {
58
+ const engine = new FlowEngine();
59
+ engine.registerModels({ DisplayDateTimeFieldModel });
60
+
61
+ const model = engine.createModel<DisplayDateTimeFieldModel>({
62
+ use: DisplayDateTimeFieldModel,
63
+ uid: 'display-datetime-field-complete-format',
64
+ props: {
65
+ value: '2026-06-15 13:05:06',
66
+ format: 'YYYY/MM/DD HH:mm:ss',
67
+ },
68
+ });
69
+
70
+ render(model.render());
71
+
72
+ expect(screen.getByText('2026/06/15 13:05:06')).toBeInTheDocument();
73
+ });
74
+
75
+ it('renders only the date for date-only fields even when a datetime format remains', () => {
76
+ const engine = new FlowEngine();
77
+ engine.registerModels({ DisplayDateTimeFieldModel });
78
+
79
+ const model = engine.createModel<DisplayDateTimeFieldModel>({
80
+ use: DisplayDateTimeFieldModel,
81
+ uid: 'display-datetime-field-date-only',
82
+ props: {
83
+ value: '2026-06-15 13:05:06',
84
+ dateOnly: true,
85
+ format: 'YYYY-MM-DD HH:mm:ss',
86
+ showTime: false,
87
+ timeFormat: 'HH:mm:ss',
88
+ },
89
+ });
90
+
91
+ render(model.render());
92
+
93
+ expect(screen.getByText('2026-06-15')).toBeInTheDocument();
94
+ expect(screen.queryByText('2026-06-15 13:05:06')).not.toBeInTheDocument();
95
+ });
96
+ });
@@ -142,6 +142,23 @@ describe('InputFieldModel', () => {
142
142
  expect(input.value).toBe('tét');
143
143
  });
144
144
 
145
+ it('does not overwrite local replacement edit when parent advances to an older value', () => {
146
+ const onChange = vi.fn();
147
+ const createModel = (value: string) =>
148
+ createInputFieldModel({
149
+ value,
150
+ onChange,
151
+ });
152
+
153
+ const { rerender } = render(<>{createModel('').render()}</>);
154
+ const input = screen.getByRole('textbox') as HTMLInputElement;
155
+
156
+ fireEvent.change(input, { target: { value: 'té' } });
157
+ rerender(<>{createModel('t').render()}</>);
158
+
159
+ expect(input.value).toBe('té');
160
+ });
161
+
145
162
  it('renders ScanInput when scan input is enabled', () => {
146
163
  const model = createInputFieldModel({ enableScan: true });
147
164