@nocobase/client-v2 2.2.0-alpha.1 → 2.2.0-alpha.3

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 (220) hide show
  1. package/es/PluginSettingsManager.d.ts +33 -0
  2. package/es/RouteRepository.d.ts +21 -6
  3. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  4. package/es/components/category-tabs/index.d.ts +9 -0
  5. package/es/components/form/JsonTextArea.d.ts +2 -1
  6. package/es/components/form/TypedVariableInput.d.ts +22 -4
  7. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  8. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  9. package/es/components/form/filter/index.d.ts +2 -0
  10. package/es/components/form/index.d.ts +1 -0
  11. package/es/components/index.d.ts +2 -0
  12. package/es/flow/actions/afterSuccess.d.ts +8 -1
  13. package/es/flow/actions/index.d.ts +1 -1
  14. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  15. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  16. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  17. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  18. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  19. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  20. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  21. package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
  22. package/es/flow/index.d.ts +1 -0
  23. package/es/flow/models/base/GridModel.d.ts +1 -1
  24. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  25. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  26. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  27. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  28. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
  29. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  30. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  31. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  32. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  33. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  34. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  35. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  36. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  37. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  38. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  39. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  40. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  41. package/es/flow/utils/formValueDeps.d.ts +32 -0
  42. package/es/flow-compat/FieldValidation.d.ts +2 -1
  43. package/es/index.d.ts +2 -1
  44. package/es/index.mjs +233 -137
  45. package/lib/index.js +234 -138
  46. package/package.json +7 -7
  47. package/src/PluginSettingsManager.ts +53 -0
  48. package/src/RouteRepository.ts +126 -24
  49. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  50. package/src/__tests__/RouteRepository.test.ts +216 -0
  51. package/src/__tests__/browserChecker.test.ts +61 -0
  52. package/src/__tests__/exports.test.ts +16 -0
  53. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
  54. package/src/__tests__/plugin-manager.test.tsx +44 -2
  55. package/src/__tests__/settings-center.test.tsx +40 -1
  56. package/src/collection-manager/field-configure.ts +1 -1
  57. package/src/collection-manager/interfaces/id.ts +1 -1
  58. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  59. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  60. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  61. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  62. package/src/collection-manager/interfaces/obo.tsx +2 -2
  63. package/src/collection-manager/interfaces/oho.tsx +2 -2
  64. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  65. package/src/collection-manager/interfaces/uuid.ts +1 -1
  66. package/src/components/AppComponents.tsx +19 -3
  67. package/src/components/README.md +7 -1
  68. package/src/components/README.zh-CN.md +6 -1
  69. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  70. package/src/components/category-tabs/index.ts +10 -0
  71. package/src/components/form/JsonTextArea.tsx +21 -11
  72. package/src/components/form/TypedVariableInput.tsx +416 -93
  73. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  74. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  75. package/src/components/form/__tests__/TypedVariableInput.test.tsx +268 -9
  76. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  77. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  78. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  79. package/src/components/form/filter/index.ts +2 -0
  80. package/src/components/form/index.tsx +1 -0
  81. package/src/components/index.ts +2 -0
  82. package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
  83. package/src/flow/__tests__/getKey.test.ts +7 -0
  84. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  85. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  86. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
  87. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
  88. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  89. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  90. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  91. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  92. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +104 -0
  93. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  94. package/src/flow/actions/afterSuccess.tsx +142 -3
  95. package/src/flow/actions/customVariable.tsx +1 -2
  96. package/src/flow/actions/dataScopeFilter.ts +10 -0
  97. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  98. package/src/flow/actions/index.ts +1 -1
  99. package/src/flow/actions/linkageRules.tsx +123 -35
  100. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  101. package/src/flow/actions/openView.tsx +38 -4
  102. package/src/flow/actions/runjs.tsx +2 -14
  103. package/src/flow/actions/validation.tsx +62 -30
  104. package/src/flow/admin-shell/BaseLayoutModel.tsx +25 -3
  105. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +5 -2
  106. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +13 -0
  107. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +34 -10
  108. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
  109. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
  110. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  111. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  112. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  113. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  114. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  115. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  116. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  117. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  118. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +82 -0
  119. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
  120. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  121. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  122. package/src/flow/components/DefaultValue.tsx +1 -2
  123. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  124. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  125. package/src/flow/components/FieldAssignValueInput.tsx +10 -5
  126. package/src/flow/components/FlowRoute.tsx +56 -11
  127. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  128. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  129. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  130. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  131. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  132. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  133. package/src/flow/components/fieldAssignOptions.ts +155 -27
  134. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  135. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  136. package/src/flow/index.ts +2 -1
  137. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  138. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  139. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  140. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  141. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  142. package/src/flow/models/base/ActionModel.tsx +12 -1
  143. package/src/flow/models/base/ActionModelCore.tsx +6 -4
  144. package/src/flow/models/base/GridModel.tsx +38 -7
  145. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  146. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  147. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  148. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  149. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  150. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  151. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
  152. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  153. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
  154. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  155. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  156. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  157. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  158. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  159. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  160. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  161. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  162. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  163. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
  164. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  165. package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
  166. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  167. package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
  168. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  169. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  170. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +36 -13
  171. package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
  172. package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
  173. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  174. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  175. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  176. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  177. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
  178. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  179. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  180. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  181. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  182. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
  183. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
  184. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  185. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  186. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  187. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  188. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  189. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  190. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  191. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  192. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  193. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  194. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  195. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  196. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  197. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  198. package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
  199. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  200. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  201. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  202. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
  203. package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
  204. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  205. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
  206. package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
  207. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  208. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  209. package/src/flow/utils/formValueDeps.ts +170 -0
  210. package/src/flow-compat/FieldValidation.tsx +122 -60
  211. package/src/flow-compat/Popover.tsx +43 -4
  212. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  213. package/src/index.ts +8 -1
  214. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  215. package/src/nocobase-buildin-plugin/index.tsx +44 -14
  216. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  217. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  218. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  219. package/src/settings-center/plugin-manager/index.tsx +3 -0
  220. package/src/settings-center/utils.tsx +0 -6
@@ -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}
@@ -8,15 +8,61 @@
8
8
  */
9
9
 
10
10
  import { Input } from 'antd';
11
- import React from 'react';
12
- import _ from 'lodash';
11
+ import type { TextAreaProps } from 'antd/es/input';
12
+ import type { TextAreaRef } from 'antd/es/input/TextArea';
13
+ import React, { useEffect, useRef } from 'react';
13
14
  import { largeField, EditableItemModel } from '@nocobase/flow-engine';
14
15
  import { FieldModel } from '../base/FieldModel';
15
16
 
17
+ function IMESafeTextArea(props: TextAreaProps) {
18
+ const { value, onChange, onCompositionStart, onCompositionEnd, ...rest } = props;
19
+ const textAreaRef = useRef<TextAreaRef>(null);
20
+ const previousValueRef = useRef(value);
21
+ const defaultValue = typeof value === 'bigint' ? String(value) : value;
22
+
23
+ useEffect(() => {
24
+ if (Object.is(previousValueRef.current, value)) {
25
+ return;
26
+ }
27
+ previousValueRef.current = value;
28
+
29
+ const textArea = textAreaRef.current?.resizableTextArea?.textArea;
30
+ if (textArea) {
31
+ const nextValue = value == null ? '' : String(value);
32
+ if (textArea.value !== nextValue) {
33
+ textArea.value = nextValue;
34
+ }
35
+ }
36
+ }, [value]);
37
+
38
+ const getEventValue = (event: React.ChangeEvent<HTMLTextAreaElement> | React.CompositionEvent<HTMLTextAreaElement>) =>
39
+ event.currentTarget.value;
40
+
41
+ return (
42
+ <Input.TextArea
43
+ {...rest}
44
+ ref={textAreaRef}
45
+ defaultValue={defaultValue}
46
+ onChange={(event) => {
47
+ previousValueRef.current = getEventValue(event);
48
+ onChange?.(event);
49
+ }}
50
+ onCompositionStart={(event) => {
51
+ previousValueRef.current = getEventValue(event);
52
+ onCompositionStart?.(event);
53
+ }}
54
+ onCompositionEnd={(event) => {
55
+ previousValueRef.current = getEventValue(event);
56
+ onCompositionEnd?.(event);
57
+ }}
58
+ />
59
+ );
60
+ }
61
+
16
62
  @largeField()
17
63
  export class TextareaFieldModel extends FieldModel {
18
64
  render() {
19
- return <Input.TextArea {...this.props} />;
65
+ return <IMESafeTextArea {...this.props} />;
20
66
  }
21
67
  }
22
68
 
@@ -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
+ });
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { FlowEngine } from '@nocobase/flow-engine';
11
- import { render, screen } from '@testing-library/react';
11
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
12
12
  import React from 'react';
13
13
  import { describe, expect, it, vi } from 'vitest';
14
14
  import { InputFieldModel } from '../InputFieldModel';
@@ -43,6 +43,122 @@ describe('InputFieldModel', () => {
43
43
  consoleError.mockRestore();
44
44
  });
45
45
 
46
+ it('keeps IME composition text visible before the parent value is committed', () => {
47
+ const onChange = vi.fn();
48
+ const onCompositionStart = vi.fn();
49
+ const onCompositionEnd = vi.fn();
50
+ const model = createInputFieldModel({
51
+ value: '',
52
+ onChange,
53
+ onCompositionStart,
54
+ onCompositionEnd,
55
+ });
56
+
57
+ render(<>{model.render()}</>);
58
+
59
+ const input = screen.getByRole('textbox') as HTMLInputElement;
60
+
61
+ fireEvent.compositionStart(input);
62
+ fireEvent.change(input, { target: { value: 'ni' }, nativeEvent: { isComposing: true } });
63
+
64
+ expect(input.value).toBe('ni');
65
+ expect(onChange).toHaveBeenCalledTimes(1);
66
+ expect(onCompositionStart).toHaveBeenCalledTimes(1);
67
+
68
+ fireEvent.compositionEnd(input, { target: { value: '你' } });
69
+
70
+ expect(input.value).toBe('你');
71
+ expect(onCompositionEnd).toHaveBeenCalledTimes(1);
72
+ });
73
+
74
+ it('returns to the committed parent value after IME composition is submitted', () => {
75
+ const onChange = vi.fn();
76
+ const onCompositionEnd = vi.fn();
77
+ const createModel = (value: string) =>
78
+ createInputFieldModel({
79
+ value,
80
+ onChange,
81
+ onCompositionEnd,
82
+ });
83
+
84
+ const { rerender } = render(<>{createModel('').render()}</>);
85
+ const input = screen.getByRole('textbox') as HTMLInputElement;
86
+
87
+ fireEvent.compositionStart(input);
88
+ fireEvent.change(input, { target: { value: 'ni' }, nativeEvent: { isComposing: true } });
89
+ fireEvent.compositionEnd(input, { target: { value: '你' } });
90
+
91
+ expect(input.value).toBe('你');
92
+
93
+ rerender(<>{createModel('你').render()}</>);
94
+
95
+ expect(input.value).toBe('你');
96
+ });
97
+
98
+ it('does not overwrite in-progress DOM input with a stale parent value', async () => {
99
+ const onChange = vi.fn();
100
+ const createModel = (value: string) =>
101
+ createInputFieldModel({
102
+ value,
103
+ onChange,
104
+ });
105
+
106
+ const { rerender } = render(<>{createModel('').render()}</>);
107
+ const input = screen.getByRole('textbox') as HTMLInputElement;
108
+
109
+ fireEvent.change(input, { target: { value: 'té' } });
110
+ rerender(<>{createModel('').render()}</>);
111
+
112
+ expect(input.value).toBe('té');
113
+
114
+ rerender(<>{createModel('reset').render()}</>);
115
+
116
+ await waitFor(() => {
117
+ expect((screen.getByRole('textbox') as HTMLInputElement).value).toBe('reset');
118
+ });
119
+ });
120
+
121
+ it('keeps replacement-style Vietnamese IME edits in the DOM', () => {
122
+ const onChange = vi.fn();
123
+ const createModel = (value: string) =>
124
+ createInputFieldModel({
125
+ value,
126
+ onChange,
127
+ });
128
+
129
+ const { rerender } = render(<>{createModel('').render()}</>);
130
+ const input = screen.getByRole('textbox') as HTMLInputElement;
131
+
132
+ fireEvent.change(input, { target: { value: 'te' } });
133
+ rerender(<>{createModel('te').render()}</>);
134
+ expect(input.value).toBe('te');
135
+
136
+ fireEvent.change(input, { target: { value: 'té' } });
137
+ rerender(<>{createModel('te').render()}</>);
138
+ expect(input.value).toBe('té');
139
+
140
+ fireEvent.change(input, { target: { value: 'tét' } });
141
+ rerender(<>{createModel('té').render()}</>);
142
+ expect(input.value).toBe('tét');
143
+ });
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
+
46
162
  it('renders ScanInput when scan input is enabled', () => {
47
163
  const model = createInputFieldModel({ enableScan: true });
48
164
 
@@ -0,0 +1,43 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import React from 'react';
11
+ import { describe, expect, it } from 'vitest';
12
+ import { SelectFieldModel } from '../SelectFieldModel';
13
+
14
+ function mockT(text: string) {
15
+ if (text === '{{t("Yes")}}') return '是';
16
+ if (text === '{{t("No")}}') return '否';
17
+ return text;
18
+ }
19
+
20
+ describe('SelectFieldModel', () => {
21
+ it('translates enum fallback labels for selected values', () => {
22
+ const model = {
23
+ props: {
24
+ value: true,
25
+ },
26
+ context: {
27
+ collectionField: {
28
+ uiSchema: {
29
+ enum: [
30
+ { label: '{{t("Yes")}}', value: true },
31
+ { label: '{{t("No")}}', value: false },
32
+ ],
33
+ },
34
+ },
35
+ },
36
+ translate: mockT,
37
+ } as unknown as SelectFieldModel;
38
+
39
+ const element = SelectFieldModel.prototype.render.call(model) as React.ReactElement;
40
+
41
+ expect(element.props.value).toEqual({ label: '是', value: true });
42
+ });
43
+ });
@@ -0,0 +1,100 @@
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 { fireEvent, render, screen, waitFor } from '@testing-library/react';
12
+ import React from 'react';
13
+ import { describe, expect, it, vi } from 'vitest';
14
+ import { TextareaFieldModel } from '../TextareaFieldModel';
15
+
16
+ function createTextareaFieldModel(props?: Record<string, unknown>) {
17
+ const engine = new FlowEngine();
18
+ return new TextareaFieldModel({
19
+ uid: 'textarea-field-model-test',
20
+ flowEngine: engine,
21
+ props,
22
+ });
23
+ }
24
+
25
+ describe('TextareaFieldModel', () => {
26
+ it('keeps IME composition text visible before the parent value is committed', () => {
27
+ const onChange = vi.fn();
28
+ const onCompositionStart = vi.fn();
29
+ const onCompositionEnd = vi.fn();
30
+ const model = createTextareaFieldModel({
31
+ value: '',
32
+ onChange,
33
+ onCompositionStart,
34
+ onCompositionEnd,
35
+ });
36
+
37
+ render(<>{model.render()}</>);
38
+
39
+ const textarea = screen.getByRole('textbox') as HTMLTextAreaElement;
40
+
41
+ fireEvent.compositionStart(textarea);
42
+ fireEvent.change(textarea, { target: { value: 'ni' }, nativeEvent: { isComposing: true } });
43
+
44
+ expect(textarea.value).toBe('ni');
45
+ expect(onChange).toHaveBeenCalledTimes(1);
46
+ expect(onCompositionStart).toHaveBeenCalledTimes(1);
47
+
48
+ fireEvent.compositionEnd(textarea, { target: { value: '你' } });
49
+
50
+ expect(textarea.value).toBe('你');
51
+ expect(onCompositionEnd).toHaveBeenCalledTimes(1);
52
+ });
53
+
54
+ it('does not overwrite in-progress DOM input with a stale parent value', async () => {
55
+ const onChange = vi.fn();
56
+ const createModel = (value: string) =>
57
+ createTextareaFieldModel({
58
+ value,
59
+ onChange,
60
+ });
61
+
62
+ const { rerender } = render(<>{createModel('').render()}</>);
63
+ const textarea = screen.getByRole('textbox') as HTMLTextAreaElement;
64
+
65
+ fireEvent.change(textarea, { target: { value: 'té' } });
66
+ rerender(<>{createModel('').render()}</>);
67
+
68
+ expect(textarea.value).toBe('té');
69
+
70
+ rerender(<>{createModel('reset').render()}</>);
71
+
72
+ await waitFor(() => {
73
+ expect((screen.getByRole('textbox') as HTMLTextAreaElement).value).toBe('reset');
74
+ });
75
+ });
76
+
77
+ it('keeps replacement-style Vietnamese IME edits in the DOM', () => {
78
+ const onChange = vi.fn();
79
+ const createModel = (value: string) =>
80
+ createTextareaFieldModel({
81
+ value,
82
+ onChange,
83
+ });
84
+
85
+ const { rerender } = render(<>{createModel('').render()}</>);
86
+ const textarea = screen.getByRole('textbox') as HTMLTextAreaElement;
87
+
88
+ fireEvent.change(textarea, { target: { value: 'te' } });
89
+ rerender(<>{createModel('te').render()}</>);
90
+ expect(textarea.value).toBe('te');
91
+
92
+ fireEvent.change(textarea, { target: { value: 'té' } });
93
+ rerender(<>{createModel('te').render()}</>);
94
+ expect(textarea.value).toBe('té');
95
+
96
+ fireEvent.change(textarea, { target: { value: 'tét' } });
97
+ rerender(<>{createModel('té').render()}</>);
98
+ expect(textarea.value).toBe('tét');
99
+ });
100
+ });
@@ -14,8 +14,9 @@ import { Button, Dropdown, theme, Tooltip, type ButtonProps, type MenuProps } fr
14
14
  import React, { useEffect, useMemo, useState } from 'react';
15
15
  import { useHotkeys } from 'react-hotkeys-hook';
16
16
  import { useTranslation } from 'react-i18next';
17
- import { Link } from 'react-router-dom';
17
+ import { Link, useLocation } from 'react-router-dom';
18
18
  import { useACLRoleContext } from '../../../acl';
19
+ import type { BaseApplication } from '../../../BaseApplication';
19
20
  import type { PluginSettingsPageType } from '../../../PluginSettingsManager';
20
21
  import { useApp } from '../../../hooks/useApp';
21
22
  import {
@@ -64,6 +65,89 @@ const topbarActionTriggerClassName = css`
64
65
  height: 100%;
65
66
  `;
66
67
 
68
+ type TopbarSettingsAppLike = Pick<BaseApplication<any>, 'router' | 'getPublicPath'>;
69
+
70
+ const normalizeTopbarPath = (pathname?: string) => {
71
+ const trimmed = pathname?.trim();
72
+ if (!trimmed || trimmed === '/') {
73
+ return '/';
74
+ }
75
+ return `/${trimmed.replace(/^\/+/, '')}`;
76
+ };
77
+
78
+ const normalizeTopbarBasePath = (pathname?: string) => {
79
+ const normalized = normalizeTopbarPath(pathname).replace(/\/+$/, '');
80
+ return normalized === '' || normalized === '/' ? '' : normalized;
81
+ };
82
+
83
+ const getTopbarRouterBasePath = (app: TopbarSettingsAppLike | undefined) => {
84
+ return app?.router?.getBasename?.() || app?.router?.basename || app?.getPublicPath?.() || '';
85
+ };
86
+
87
+ const stripTopbarRouterBasePath = (pathname: string, basename?: string) => {
88
+ const normalizedPath = normalizeTopbarPath(pathname);
89
+ const normalizedBase = normalizeTopbarBasePath(basename);
90
+
91
+ if (!normalizedBase) {
92
+ return normalizedPath;
93
+ }
94
+
95
+ if (normalizedPath === normalizedBase) {
96
+ return '/';
97
+ }
98
+
99
+ if (normalizedPath.startsWith(`${normalizedBase}/`)) {
100
+ return normalizeTopbarPath(normalizedPath.slice(normalizedBase.length));
101
+ }
102
+
103
+ return normalizedPath;
104
+ };
105
+
106
+ const isAdminRuntimePath = (pathname: string) => {
107
+ return pathname === '/admin' || pathname.startsWith('/admin/');
108
+ };
109
+
110
+ const buildTopbarDocumentHref = (targetPath: string, basename?: string) => {
111
+ const normalizedTarget = normalizeTopbarPath(targetPath);
112
+ const normalizedBase = normalizeTopbarBasePath(basename);
113
+
114
+ if (!normalizedBase || normalizedTarget === normalizedBase || normalizedTarget.startsWith(`${normalizedBase}/`)) {
115
+ return normalizedTarget;
116
+ }
117
+
118
+ return `${normalizedBase}${normalizedTarget}`;
119
+ };
120
+
121
+ function TopbarExternalSettingsLabel(props: { title: React.ReactNode; link: string }) {
122
+ return (
123
+ <div
124
+ onClick={() => {
125
+ window.open(props.link, '_blank', 'noopener,noreferrer');
126
+ }}
127
+ >
128
+ {props.title}
129
+ </div>
130
+ );
131
+ }
132
+
133
+ function TopbarInternalSettingsLabel(props: { title: React.ReactNode; path?: string }) {
134
+ const app = useApp();
135
+ const location = useLocation();
136
+ const targetPath = props.path || '/admin/settings';
137
+ const basename = getTopbarRouterBasePath(app);
138
+ const currentPath = stripTopbarRouterBasePath(location.pathname, basename);
139
+
140
+ if (isAdminRuntimePath(currentPath)) {
141
+ return <Link to={targetPath}>{props.title}</Link>;
142
+ }
143
+
144
+ return (
145
+ <a href={buildTopbarDocumentHref(targetPath, basename)} target="_blank" rel="noopener noreferrer">
146
+ {props.title}
147
+ </a>
148
+ );
149
+ }
150
+
67
151
  /**
68
152
  * 将 settings 顶部菜单转换成右上角 dropdown items。
69
153
  *
@@ -112,15 +196,9 @@ export function getTopbarPluginSettingsItems(options: {
112
196
  title: targetTitle,
113
197
  icon: item.icon,
114
198
  label: targetLink ? (
115
- <div
116
- onClick={() => {
117
- window.open(targetLink, '_blank', 'noopener,noreferrer');
118
- }}
119
- >
120
- {targetTitle}
121
- </div>
199
+ <TopbarExternalSettingsLabel title={targetTitle} link={targetLink} />
122
200
  ) : (
123
- <Link to={targetPath || '/admin/settings'}>{targetTitle}</Link>
201
+ <TopbarInternalSettingsLabel title={targetTitle} path={targetPath} />
124
202
  ),
125
203
  };
126
204
  });
@@ -131,7 +209,12 @@ export function getTopbarPluginSettingsItems(options: {
131
209
  items.push({
132
210
  key: pluginManagerSetting.key,
133
211
  icon: pluginManagerSetting.icon || <ApiOutlined />,
134
- label: <Link to={pluginManagerSetting.path}>{pluginManagerSetting.title || t('Plugin manager')}</Link>,
212
+ label: (
213
+ <TopbarInternalSettingsLabel
214
+ title={pluginManagerSetting.title || t('Plugin manager')}
215
+ path={pluginManagerSetting.path}
216
+ />
217
+ ),
135
218
  });
136
219
  }
137
220