@nocobase/client-v2 2.2.0-beta.1 → 2.2.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/es/BaseApplication.d.ts +3 -0
  2. package/es/PluginSettingsManager.d.ts +33 -0
  3. package/es/RouteRepository.d.ts +29 -6
  4. package/es/RouterManager.d.ts +15 -0
  5. package/es/authRedirect.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  7. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  8. package/es/components/form/JsonTextArea.d.ts +2 -1
  9. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  10. package/es/components/form/index.d.ts +1 -0
  11. package/es/components/index.d.ts +1 -0
  12. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  13. package/es/entry-actions/index.d.ts +9 -0
  14. package/es/flow/actions/afterSuccess.d.ts +8 -1
  15. package/es/flow/actions/index.d.ts +1 -1
  16. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  17. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  18. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  19. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  21. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  22. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  23. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  24. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  25. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  26. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  27. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  28. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  29. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  30. package/es/flow/models/base/GridModel.d.ts +1 -1
  31. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  32. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  33. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  34. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  35. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  36. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
  37. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  38. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  39. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  40. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  41. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  42. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  43. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  44. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  45. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  46. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  47. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  48. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  49. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  50. package/es/flow/utils/formValueDeps.d.ts +32 -0
  51. package/es/flow-compat/FieldValidation.d.ts +2 -1
  52. package/es/index.d.ts +4 -1
  53. package/es/index.mjs +305 -157
  54. package/es/utils/markdownSanitize.d.ts +11 -0
  55. package/lib/index.js +305 -157
  56. package/package.json +7 -7
  57. package/src/BaseApplication.tsx +6 -0
  58. package/src/PluginSettingsManager.ts +53 -0
  59. package/src/RouteRepository.ts +151 -24
  60. package/src/RouterManager.tsx +142 -1
  61. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  62. package/src/__tests__/RouteRepository.test.ts +239 -0
  63. package/src/__tests__/RouterManager.test.ts +125 -0
  64. package/src/__tests__/authRedirect.test.ts +17 -0
  65. package/src/__tests__/exports.test.ts +16 -0
  66. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
  67. package/src/__tests__/plugin-manager.test.tsx +44 -2
  68. package/src/__tests__/settings-center.test.tsx +40 -1
  69. package/src/authRedirect.ts +38 -0
  70. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  71. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  72. package/src/collection-manager/field-configure.ts +1 -1
  73. package/src/collection-manager/interfaces/id.ts +1 -1
  74. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  75. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  76. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  77. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  78. package/src/collection-manager/interfaces/obo.tsx +2 -2
  79. package/src/collection-manager/interfaces/oho.tsx +2 -2
  80. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  81. package/src/collection-manager/interfaces/uuid.ts +1 -1
  82. package/src/components/AppComponents.tsx +19 -3
  83. package/src/components/form/JsonTextArea.tsx +21 -11
  84. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  85. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  86. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  87. package/src/components/form/index.tsx +1 -0
  88. package/src/components/index.ts +1 -0
  89. package/src/entry-actions/EntryActionManager.ts +76 -0
  90. package/src/entry-actions/index.ts +10 -0
  91. package/src/flow/FlowPage.tsx +38 -3
  92. package/src/flow/__tests__/FlowPage.test.tsx +201 -27
  93. package/src/flow/__tests__/FlowRoute.test.tsx +1088 -105
  94. package/src/flow/__tests__/getKey.test.ts +7 -0
  95. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  96. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  97. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +120 -0
  98. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
  99. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  100. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  101. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  102. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  103. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +164 -0
  104. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  105. package/src/flow/actions/afterSuccess.tsx +142 -3
  106. package/src/flow/actions/customVariable.tsx +1 -2
  107. package/src/flow/actions/dataScopeFilter.ts +20 -1
  108. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  109. package/src/flow/actions/index.ts +1 -1
  110. package/src/flow/actions/linkageRules.tsx +123 -35
  111. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  112. package/src/flow/actions/openView.tsx +59 -5
  113. package/src/flow/actions/runjs.tsx +2 -14
  114. package/src/flow/actions/validation.tsx +62 -30
  115. package/src/flow/admin-shell/BaseLayoutModel.tsx +149 -3
  116. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +187 -42
  117. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1020 -110
  118. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +74 -13
  119. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +486 -0
  120. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +64 -8
  121. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  122. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  123. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  124. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  125. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  126. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  127. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  128. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  129. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  130. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  131. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +518 -2
  132. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +214 -2
  133. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  134. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  135. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  136. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  137. package/src/flow/common/Markdown/Display.tsx +14 -3
  138. package/src/flow/common/Markdown/Edit.tsx +19 -7
  139. package/src/flow/components/DefaultValue.tsx +1 -2
  140. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  141. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  142. package/src/flow/components/FieldAssignValueInput.tsx +15 -11
  143. package/src/flow/components/FlowRoute.tsx +186 -25
  144. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  145. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  146. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +134 -0
  147. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  148. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  149. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  150. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  151. package/src/flow/components/fieldAssignOptions.ts +155 -27
  152. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  153. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  154. package/src/flow/index.ts +1 -1
  155. package/src/flow/internal/components/Markdown/util.ts +2 -1
  156. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  157. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  158. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  159. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  160. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  161. package/src/flow/models/base/ActionModel.tsx +21 -8
  162. package/src/flow/models/base/ActionModelCore.tsx +11 -4
  163. package/src/flow/models/base/GridModel.tsx +38 -7
  164. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  165. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  166. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  167. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  168. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  169. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  170. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +32 -3
  171. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  172. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  173. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  174. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +111 -0
  175. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  176. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  177. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  178. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  179. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  180. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  181. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  182. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  183. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  184. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  185. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  186. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
  187. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  188. package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
  189. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  190. package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
  191. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  192. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  193. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +36 -13
  194. package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
  195. package/src/flow/models/blocks/table/TableColumnModel.tsx +42 -5
  196. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  197. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  198. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  199. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  200. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +210 -1
  201. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  202. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  203. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  204. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  205. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +30 -6
  206. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +61 -17
  207. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  208. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  209. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  210. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  211. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  212. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  213. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  214. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  215. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  216. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  217. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  218. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  219. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  220. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  221. package/src/flow/models/fields/TextareaFieldModel.tsx +72 -3
  222. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  223. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  224. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  225. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +131 -0
  226. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  227. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  228. package/src/flow/models/topbar/TopbarActionModel.tsx +168 -10
  229. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  230. package/src/flow/routeTransientInputArgs.ts +27 -0
  231. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +300 -0
  232. package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
  233. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  234. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  235. package/src/flow/utils/formValueDeps.ts +170 -0
  236. package/src/flow-compat/FieldValidation.tsx +122 -60
  237. package/src/flow-compat/Popover.tsx +43 -4
  238. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  239. package/src/index.ts +10 -1
  240. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  241. package/src/nocobase-buildin-plugin/index.tsx +51 -15
  242. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  243. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  244. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  245. package/src/settings-center/plugin-manager/index.tsx +3 -0
  246. package/src/settings-center/utils.tsx +0 -6
  247. package/src/utils/markdownSanitize.ts +88 -0
@@ -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,131 @@
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 { FieldModelRenderer, FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
11
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
12
+ import { Form } from 'antd';
13
+ import React from 'react';
14
+ import { describe, expect, it, vi } from 'vitest';
15
+ import { TextareaFieldModel } from '../TextareaFieldModel';
16
+
17
+ function createTextareaFieldModel(props?: Record<string, unknown>) {
18
+ const engine = new FlowEngine();
19
+ return new TextareaFieldModel({
20
+ uid: 'textarea-field-model-test',
21
+ flowEngine: engine,
22
+ props,
23
+ });
24
+ }
25
+
26
+ describe('TextareaFieldModel', () => {
27
+ it('syncs multiline values assigned through the form renderer', async () => {
28
+ const flowEngine = new FlowEngine();
29
+ const model = createTextareaFieldModel();
30
+ model.dispatchEvent = vi.fn().mockResolvedValue([]);
31
+
32
+ function FormHost() {
33
+ const [form] = Form.useForm();
34
+
35
+ React.useEffect(() => {
36
+ form.setFieldsValue({ address: 'aaaaaa\nbbbbbb' });
37
+ }, [form]);
38
+
39
+ return (
40
+ <FlowEngineProvider engine={flowEngine}>
41
+ <Form form={form}>
42
+ <Form.Item name="address">
43
+ <FieldModelRenderer model={model} />
44
+ </Form.Item>
45
+ </Form>
46
+ </FlowEngineProvider>
47
+ );
48
+ }
49
+
50
+ render(<FormHost />);
51
+
52
+ await waitFor(() => {
53
+ expect((screen.getByRole('textbox') as HTMLTextAreaElement).value).toBe('aaaaaa\nbbbbbb');
54
+ });
55
+ });
56
+
57
+ it('keeps IME composition text visible before the parent value is committed', () => {
58
+ const onChange = vi.fn();
59
+ const onCompositionStart = vi.fn();
60
+ const onCompositionEnd = vi.fn();
61
+ const model = createTextareaFieldModel({
62
+ value: '',
63
+ onChange,
64
+ onCompositionStart,
65
+ onCompositionEnd,
66
+ });
67
+
68
+ render(<>{model.render()}</>);
69
+
70
+ const textarea = screen.getByRole('textbox') as HTMLTextAreaElement;
71
+
72
+ fireEvent.compositionStart(textarea);
73
+ fireEvent.change(textarea, { target: { value: 'ni' }, nativeEvent: { isComposing: true } });
74
+
75
+ expect(textarea.value).toBe('ni');
76
+ expect(onChange).toHaveBeenCalledTimes(1);
77
+ expect(onCompositionStart).toHaveBeenCalledTimes(1);
78
+
79
+ fireEvent.compositionEnd(textarea, { target: { value: '你' } });
80
+
81
+ expect(textarea.value).toBe('你');
82
+ expect(onCompositionEnd).toHaveBeenCalledTimes(1);
83
+ });
84
+
85
+ it('does not overwrite in-progress DOM input with a stale parent value', async () => {
86
+ const onChange = vi.fn();
87
+ const createModel = (value: string) =>
88
+ createTextareaFieldModel({
89
+ value,
90
+ onChange,
91
+ });
92
+
93
+ const { rerender } = render(<>{createModel('').render()}</>);
94
+ const textarea = screen.getByRole('textbox') as HTMLTextAreaElement;
95
+
96
+ fireEvent.change(textarea, { target: { value: 'té' } });
97
+ rerender(<>{createModel('').render()}</>);
98
+
99
+ expect(textarea.value).toBe('té');
100
+
101
+ rerender(<>{createModel('reset').render()}</>);
102
+
103
+ await waitFor(() => {
104
+ expect((screen.getByRole('textbox') as HTMLTextAreaElement).value).toBe('reset');
105
+ });
106
+ });
107
+
108
+ it('keeps replacement-style Vietnamese IME edits in the DOM', () => {
109
+ const onChange = vi.fn();
110
+ const createModel = (value: string) =>
111
+ createTextareaFieldModel({
112
+ value,
113
+ onChange,
114
+ });
115
+
116
+ const { rerender } = render(<>{createModel('').render()}</>);
117
+ const textarea = screen.getByRole('textbox') as HTMLTextAreaElement;
118
+
119
+ fireEvent.change(textarea, { target: { value: 'te' } });
120
+ rerender(<>{createModel('te').render()}</>);
121
+ expect(textarea.value).toBe('te');
122
+
123
+ fireEvent.change(textarea, { target: { value: 'té' } });
124
+ rerender(<>{createModel('te').render()}</>);
125
+ expect(textarea.value).toBe('té');
126
+
127
+ fireEvent.change(textarea, { target: { value: 'tét' } });
128
+ rerender(<>{createModel('té').render()}</>);
129
+ expect(textarea.value).toBe('tét');
130
+ });
131
+ });
@@ -23,6 +23,20 @@ import {
23
23
  } from '../AssociationFieldModel/recordSelectShared';
24
24
  import _ from 'lodash';
25
25
 
26
+ const mobileSelectSafeAreaPaddingBottom = 'calc(12px + env(safe-area-inset-bottom, 0px))';
27
+
28
+ const mobileSelectConfirmFooterStyle: CSSProperties = {
29
+ paddingBottom: mobileSelectSafeAreaPaddingBottom,
30
+ };
31
+
32
+ function getMobileSelectListStyle(hasConfirmFooter: boolean): CSSProperties {
33
+ return {
34
+ maxHeight: '60vh',
35
+ overflowY: 'auto',
36
+ paddingBottom: hasConfirmFooter ? undefined : mobileSelectSafeAreaPaddingBottom,
37
+ };
38
+ }
39
+
26
40
  const labelClassName = css`
27
41
  div {
28
42
  white-space: nowrap !important;
@@ -280,13 +294,7 @@ export function MobileLazySelect(props: Readonly<LazySelectProps>) {
280
294
  showCancelButton
281
295
  />
282
296
  </div>
283
- <div
284
- style={{
285
- maxHeight: '60vh',
286
- overflowY: 'auto',
287
- }}
288
- onScroll={handleScroll}
289
- >
297
+ <div style={getMobileSelectListStyle(isMultiple)} onScroll={handleScroll}>
290
298
  <CheckList multiple={isMultiple} value={selectedValueIds} onChange={handleListChange}>
291
299
  {realOptions.map((item) => {
292
300
  const optionValue = item?.[valueKey];
@@ -311,9 +319,11 @@ export function MobileLazySelect(props: Readonly<LazySelectProps>) {
311
319
  )}
312
320
  </div>
313
321
  {isMultiple && (
314
- <Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
315
- {t('Confirm')}
316
- </Button>
322
+ <div style={mobileSelectConfirmFooterStyle}>
323
+ <Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
324
+ {t('Confirm')}
325
+ </Button>
326
+ </div>
317
327
  )}
318
328
  </Popup>
319
329
  </>
@@ -12,8 +12,23 @@ import { Select } from 'antd';
12
12
  import { Button, CheckList, Popup, SearchBar } from 'antd-mobile';
13
13
  import React, { useEffect, useMemo, useState } from 'react';
14
14
 
15
+ const mobileSelectSafeAreaPaddingBottom = 'calc(12px + env(safe-area-inset-bottom, 0px))';
16
+
17
+ const mobileSelectConfirmFooterStyle: React.CSSProperties = {
18
+ paddingBottom: mobileSelectSafeAreaPaddingBottom,
19
+ };
20
+
21
+ function getMobileSelectListStyle(hasConfirmFooter: boolean): React.CSSProperties {
22
+ return {
23
+ maxHeight: '60vh',
24
+ overflowY: 'auto',
25
+ paddingBottom: hasConfirmFooter ? undefined : mobileSelectSafeAreaPaddingBottom,
26
+ };
27
+ }
28
+
15
29
  export function MobileSelect(props) {
16
30
  const { value, displayValue, onChange, onChangeComplete, disabled, options = [], mode } = props;
31
+ const isMultiple = ['multiple', 'tags'].includes(mode);
17
32
  const ctx = useFlowModelContext();
18
33
  const t = ctx.t;
19
34
  const [visible, setVisible] = useState(false);
@@ -64,17 +79,12 @@ export function MobileSelect(props) {
64
79
  <div style={{ margin: '10px' }}>
65
80
  <SearchBar placeholder={t('search')} value={searchText} onChange={(v) => setSearchText(v)} showCancelButton />
66
81
  </div>
67
- <div
68
- style={{
69
- maxHeight: '60vh',
70
- overflowY: 'auto',
71
- }}
72
- >
82
+ <div style={getMobileSelectListStyle(isMultiple)}>
73
83
  <CheckList
74
- multiple={['multiple', 'tags'].includes(mode)}
84
+ multiple={isMultiple}
75
85
  value={Array.isArray(selected) ? selected : [selected]}
76
86
  onChange={(val) => {
77
- if (['multiple', 'tags'].includes(mode)) {
87
+ if (isMultiple) {
78
88
  setSelected(val);
79
89
  } else {
80
90
  setSelected(val[0]);
@@ -91,10 +101,12 @@ export function MobileSelect(props) {
91
101
  ))}
92
102
  </CheckList>
93
103
  </div>
94
- {['multiple', 'tags'].includes(mode) && (
95
- <Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
96
- {t('Confirm')}
97
- </Button>
104
+ {isMultiple && (
105
+ <div style={mobileSelectConfirmFooterStyle}>
106
+ <Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
107
+ {t('Confirm')}
108
+ </Button>
109
+ </div>
98
110
  )}
99
111
  </Popup>
100
112
  </>
@@ -14,9 +14,11 @@ 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';
21
+ import { shouldOpenAdminRouteInNewWindow } from '../../../RouterManager';
20
22
  import { useApp } from '../../../hooks/useApp';
21
23
  import {
22
24
  filterRenderableSettings,
@@ -64,6 +66,163 @@ const topbarActionTriggerClassName = css`
64
66
  height: 100%;
65
67
  `;
66
68
 
69
+ type TopbarSettingsAppLike = Pick<
70
+ BaseApplication<any>,
71
+ 'name' | 'router' | 'getPublicPath' | 'getHref' | 'layoutManager'
72
+ >;
73
+
74
+ const normalizeTopbarPath = (pathname?: string) => {
75
+ const trimmed = pathname?.trim();
76
+ if (!trimmed || trimmed === '/') {
77
+ return '/';
78
+ }
79
+ return `/${trimmed.replace(/^\/+/, '')}`;
80
+ };
81
+
82
+ const normalizeTopbarBasePath = (pathname?: string) => {
83
+ const normalized = normalizeTopbarPath(pathname).replace(/\/+$/, '');
84
+ return normalized === '' || normalized === '/' ? '' : normalized;
85
+ };
86
+
87
+ const getTopbarRouterBasePath = (app: TopbarSettingsAppLike | undefined) => {
88
+ return app?.router?.getBasename?.() || app?.router?.basename || app?.getPublicPath?.() || '';
89
+ };
90
+
91
+ const stripTopbarRouterBasePath = (pathname: string, basename?: string) => {
92
+ const normalizedPath = normalizeTopbarPath(pathname);
93
+ const normalizedBase = normalizeTopbarBasePath(basename);
94
+
95
+ if (!normalizedBase) {
96
+ return normalizedPath;
97
+ }
98
+
99
+ if (normalizedPath === normalizedBase) {
100
+ return '/';
101
+ }
102
+
103
+ if (normalizedPath.startsWith(`${normalizedBase}/`)) {
104
+ return normalizeTopbarPath(normalizedPath.slice(normalizedBase.length));
105
+ }
106
+
107
+ return normalizedPath;
108
+ };
109
+
110
+ const getTopbarAppPath = (pathname: string) => {
111
+ const normalizedPath = normalizeTopbarPath(pathname);
112
+ const match = /^(.*?\/(?:apps|_app)\/[^/]+)(?=\/|$)/.exec(normalizedPath);
113
+ const appPath = match?.[1] || '';
114
+
115
+ return {
116
+ appPath,
117
+ routePath: appPath ? normalizeTopbarPath(normalizedPath.slice(appPath.length)) : normalizedPath,
118
+ };
119
+ };
120
+
121
+ const getTopbarContextAppPath = (app: TopbarSettingsAppLike | undefined, basename?: string) => {
122
+ const basenameAppPath = getTopbarAppPath(basename || '').appPath;
123
+ if (basenameAppPath) {
124
+ return basenameAppPath;
125
+ }
126
+
127
+ const publicPathAppPath = getTopbarAppPath(app?.getPublicPath?.() || '').appPath;
128
+ if (publicPathAppPath) {
129
+ return publicPathAppPath;
130
+ }
131
+
132
+ if (app?.name && app.name !== 'main' && app.getHref) {
133
+ return normalizeTopbarBasePath(app.getHref('/'));
134
+ }
135
+
136
+ return '';
137
+ };
138
+
139
+ const prependTopbarAppPath = (pathname: string, appPath: string) => {
140
+ const normalizedPath = normalizeTopbarPath(pathname);
141
+
142
+ if (!appPath || normalizedPath === appPath || normalizedPath.startsWith(`${appPath}/`)) {
143
+ return normalizedPath;
144
+ }
145
+
146
+ return `${appPath}${normalizedPath}`;
147
+ };
148
+
149
+ const isAdminRuntimePath = (pathname: string) => {
150
+ return pathname === '/admin' || pathname.startsWith('/admin/');
151
+ };
152
+
153
+ const getTopbarAdminRoutePath = (app: TopbarSettingsAppLike | undefined) => {
154
+ try {
155
+ return app?.layoutManager?.getLayout?.('admin')?.routePath;
156
+ } catch {
157
+ return undefined;
158
+ }
159
+ };
160
+
161
+ const buildTopbarDocumentHref = (targetPath: string, basename?: string) => {
162
+ const normalizedTarget = normalizeTopbarPath(targetPath);
163
+ const normalizedBase = normalizeTopbarBasePath(basename);
164
+
165
+ if (!normalizedBase || normalizedTarget === normalizedBase || normalizedTarget.startsWith(`${normalizedBase}/`)) {
166
+ return normalizedTarget;
167
+ }
168
+
169
+ return `${normalizedBase}${normalizedTarget}`;
170
+ };
171
+
172
+ function TopbarExternalSettingsLabel(props: { title: React.ReactNode; link: string }) {
173
+ return (
174
+ <div
175
+ onClick={() => {
176
+ window.open(props.link, '_blank', 'noopener,noreferrer');
177
+ }}
178
+ >
179
+ {props.title}
180
+ </div>
181
+ );
182
+ }
183
+
184
+ function TopbarInternalSettingsLabel(props: { title: React.ReactNode; path?: string }) {
185
+ const app = useApp();
186
+ const location = useLocation();
187
+ const targetPath = props.path || '/admin/settings';
188
+ const basename = getTopbarRouterBasePath(app);
189
+ const currentPath = stripTopbarRouterBasePath(location.pathname, basename);
190
+ const currentLocationAppPath = getTopbarAppPath(currentPath);
191
+ const currentAppPath = currentLocationAppPath.appPath || getTopbarContextAppPath(app, basename);
192
+ const currentRoutePath = currentLocationAppPath.appPath ? currentLocationAppPath.routePath : currentPath;
193
+ const targetPathInCurrentApp = prependTopbarAppPath(stripTopbarRouterBasePath(targetPath, basename), currentAppPath);
194
+
195
+ if (currentAppPath) {
196
+ const href = buildTopbarDocumentHref(targetPathInCurrentApp, basename);
197
+ const shouldOpenInNewWindow = shouldOpenAdminRouteInNewWindow({
198
+ currentPathname: currentPath,
199
+ targetPathname: targetPathInCurrentApp,
200
+ basePath: basename,
201
+ adminRoutePath: getTopbarAdminRoutePath(app),
202
+ });
203
+
204
+ return (
205
+ <a
206
+ href={href}
207
+ target={shouldOpenInNewWindow ? '_blank' : undefined}
208
+ rel={shouldOpenInNewWindow ? 'noopener noreferrer' : undefined}
209
+ >
210
+ {props.title}
211
+ </a>
212
+ );
213
+ }
214
+
215
+ if (isAdminRuntimePath(currentRoutePath)) {
216
+ return <Link to={targetPathInCurrentApp}>{props.title}</Link>;
217
+ }
218
+
219
+ return (
220
+ <a href={buildTopbarDocumentHref(targetPath, basename)} target="_blank" rel="noopener noreferrer">
221
+ {props.title}
222
+ </a>
223
+ );
224
+ }
225
+
67
226
  /**
68
227
  * 将 settings 顶部菜单转换成右上角 dropdown items。
69
228
  *
@@ -112,15 +271,9 @@ export function getTopbarPluginSettingsItems(options: {
112
271
  title: targetTitle,
113
272
  icon: item.icon,
114
273
  label: targetLink ? (
115
- <div
116
- onClick={() => {
117
- window.open(targetLink, '_blank', 'noopener,noreferrer');
118
- }}
119
- >
120
- {targetTitle}
121
- </div>
274
+ <TopbarExternalSettingsLabel title={targetTitle} link={targetLink} />
122
275
  ) : (
123
- <Link to={targetPath || '/admin/settings'}>{targetTitle}</Link>
276
+ <TopbarInternalSettingsLabel title={targetTitle} path={targetPath} />
124
277
  ),
125
278
  };
126
279
  });
@@ -131,7 +284,12 @@ export function getTopbarPluginSettingsItems(options: {
131
284
  items.push({
132
285
  key: pluginManagerSetting.key,
133
286
  icon: pluginManagerSetting.icon || <ApiOutlined />,
134
- label: <Link to={pluginManagerSetting.path}>{pluginManagerSetting.title || t('Plugin manager')}</Link>,
287
+ label: (
288
+ <TopbarInternalSettingsLabel
289
+ title={pluginManagerSetting.title || t('Plugin manager')}
290
+ path={pluginManagerSetting.path}
291
+ />
292
+ ),
135
293
  });
136
294
  }
137
295
 
@@ -49,12 +49,12 @@ export function resolveViewParamsToViewList(
49
49
  return viewItems;
50
50
  }
51
51
 
52
- export function updateViewListHidden(viewItems: ViewItem[]) {
52
+ export function updateViewListHidden(viewItems: ViewItem[], isMobileLayout = false) {
53
53
  // Calculate hidden values based on view types and positions
54
54
  let hasEmbedAfter = false;
55
55
  for (let i = viewItems.length - 1; i >= 0; i--) {
56
56
  const viewItem = viewItems[i];
57
- const viewType = getViewType(viewItem);
57
+ const viewType = getViewType(viewItem, isMobileLayout);
58
58
 
59
59
  if (viewType === 'embed' && !hasEmbedAfter) {
60
60
  hasEmbedAfter = true;
@@ -65,15 +65,23 @@ export function updateViewListHidden(viewItems: ViewItem[]) {
65
65
  }
66
66
  }
67
67
 
68
- function getViewType(viewItem: ViewItem): string {
68
+ function getViewType(viewItem: ViewItem, isMobileLayout = false): string {
69
69
  if (viewItem.model instanceof RouteModel) {
70
70
  return 'embed';
71
71
  }
72
72
 
73
+ if (isMobileLayout && viewItem.index > 0) {
74
+ return 'embed';
75
+ }
76
+
73
77
  if (!viewItem.model) {
74
78
  return 'drawer';
75
79
  }
76
80
 
81
+ if (viewItem.params.openViewRouteState?.mode) {
82
+ return viewItem.params.openViewRouteState.mode;
83
+ }
84
+
77
85
  const params = viewItem.model.getStepParams('popupSettings', 'openView');
78
86
  return params?.mode || 'drawer';
79
87
  }
@@ -0,0 +1,27 @@
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
+ export const ROUTE_TRANSIENT_INPUT_ARGS_KEY = '__nocobaseOpenViewInputArgs';
11
+
12
+ export type RouteTransientInputArgsState = {
13
+ [ROUTE_TRANSIENT_INPUT_ARGS_KEY]?: Record<string, Record<string, unknown>>;
14
+ usr?: RouteTransientInputArgsState;
15
+ };
16
+
17
+ export const getRouteTransientInputArgs = (state: unknown, viewUid?: string) => {
18
+ if (!viewUid || !state || typeof state !== 'object') {
19
+ return {};
20
+ }
21
+
22
+ const stateValue = state as RouteTransientInputArgsState;
23
+ const values = (stateValue[ROUTE_TRANSIENT_INPUT_ARGS_KEY] || stateValue.usr?.[ROUTE_TRANSIENT_INPUT_ARGS_KEY])?.[
24
+ viewUid
25
+ ];
26
+ return values && typeof values === 'object' ? values : {};
27
+ };