@nocobase/client-v2 2.2.0-beta.8 → 2.2.0

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 (271) hide show
  1. package/es/APIClient.d.ts +13 -3
  2. package/es/BaseApplication.d.ts +3 -0
  3. package/es/RouteRepository.d.ts +8 -0
  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/category-tabs/SortableCategoryTabs.d.ts +55 -0
  9. package/es/components/category-tabs/index.d.ts +9 -0
  10. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  11. package/es/components/form/TypedVariableInput.d.ts +31 -5
  12. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  13. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  14. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  15. package/es/components/form/filter/index.d.ts +2 -0
  16. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  17. package/es/components/index.d.ts +1 -0
  18. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  19. package/es/entry-actions/index.d.ts +9 -0
  20. package/es/flow/actions/index.d.ts +1 -1
  21. package/es/flow/actions/linkageRules.d.ts +27 -0
  22. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  23. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  24. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  25. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  26. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  27. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  28. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  29. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  30. package/es/flow/components/FieldAssignValueInput.d.ts +7 -24
  31. package/es/flow/components/FlowRoute.d.ts +2 -2
  32. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  33. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  34. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  35. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  36. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  37. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  38. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  39. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  40. package/es/flow/index.d.ts +1 -0
  41. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  42. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  43. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  44. package/es/flow/models/base/ActionModelCore.d.ts +6 -2
  45. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  46. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  47. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  48. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  49. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  50. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  51. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  52. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  53. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  54. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  55. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  56. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  57. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  58. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  59. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  60. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  61. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  62. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  63. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  64. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  65. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  66. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  67. package/es/flow-compat/passwordUtils.d.ts +1 -1
  68. package/es/flow-compat/routeTypes.d.ts +1 -0
  69. package/es/index.d.ts +7 -0
  70. package/es/index.mjs +301 -177
  71. package/es/ui-operation/index.d.ts +15 -0
  72. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  73. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  74. package/es/utils/index.d.ts +1 -0
  75. package/es/utils/markdownSanitize.d.ts +11 -0
  76. package/lib/index.js +313 -189
  77. package/lib/locale/languageCodes.js +3 -1
  78. package/package.json +9 -7
  79. package/src/APIClient.ts +90 -7
  80. package/src/Application.tsx +7 -1
  81. package/src/BaseApplication.tsx +13 -6
  82. package/src/RouteRepository.ts +25 -0
  83. package/src/RouterManager.tsx +142 -1
  84. package/src/__tests__/APIClient.test.tsx +58 -0
  85. package/src/__tests__/RouteRepository.test.ts +23 -0
  86. package/src/__tests__/RouterManager.test.ts +125 -0
  87. package/src/__tests__/app.test.tsx +73 -0
  88. package/src/__tests__/authRedirect.test.ts +17 -0
  89. package/src/__tests__/browserChecker.test.ts +61 -0
  90. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  91. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +35 -2
  92. package/src/__tests__/settings-center.test.tsx +270 -6
  93. package/src/acl/ACLProvider.tsx +2 -0
  94. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  95. package/src/authRedirect.ts +38 -0
  96. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  97. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  98. package/src/collection-manager/field-validation.ts +1 -1
  99. package/src/components/README.md +7 -1
  100. package/src/components/README.zh-CN.md +6 -1
  101. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  102. package/src/components/category-tabs/index.ts +10 -0
  103. package/src/components/form/JsonTextArea.tsx +4 -0
  104. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  105. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  106. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  107. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  108. package/src/components/form/TypedVariableInput.tsx +452 -101
  109. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  110. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  111. package/src/components/form/filter/CollectionFilterItem.tsx +36 -9
  112. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  113. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +55 -0
  114. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  115. package/src/components/form/filter/index.ts +2 -0
  116. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  117. package/src/components/form/table/Table.tsx +81 -6
  118. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  119. package/src/components/index.ts +1 -0
  120. package/src/entry-actions/EntryActionManager.ts +76 -0
  121. package/src/entry-actions/index.ts +10 -0
  122. package/src/flow/FlowPage.tsx +38 -3
  123. package/src/flow/__tests__/FlowPage.test.tsx +201 -27
  124. package/src/flow/__tests__/FlowRoute.test.tsx +675 -12
  125. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  126. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  127. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
  128. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  129. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  130. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  131. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  132. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  133. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
  134. package/src/flow/actions/dataScopeFilter.ts +10 -1
  135. package/src/flow/actions/dateTimeFormat.tsx +2 -2
  136. package/src/flow/actions/index.ts +2 -0
  137. package/src/flow/actions/linkageRules.tsx +144 -26
  138. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  139. package/src/flow/actions/openView.tsx +21 -1
  140. package/src/flow/admin-shell/BaseLayoutModel.tsx +124 -0
  141. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
  142. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
  143. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -5
  144. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +177 -1
  145. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +20 -0
  146. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  147. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  148. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  149. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  150. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  151. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +436 -2
  152. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
  153. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  154. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  155. package/src/flow/common/Markdown/Display.tsx +14 -3
  156. package/src/flow/common/Markdown/Edit.tsx +19 -7
  157. package/src/flow/common/Markdown/style.ts +4 -0
  158. package/src/flow/components/BlockItemCard.tsx +2 -2
  159. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  160. package/src/flow/components/FieldAssignValueInput.tsx +72 -584
  161. package/src/flow/components/FlowRoute.tsx +204 -46
  162. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  163. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  164. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  165. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  166. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  167. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  168. package/src/flow/components/field-value-variable/index.ts +12 -0
  169. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  170. package/src/flow/components/filter/VariableFilterItem.tsx +170 -13
  171. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  172. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  173. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +195 -1
  174. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  175. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  176. package/src/flow/index.ts +3 -0
  177. package/src/flow/internal/components/Markdown/util.ts +2 -1
  178. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  179. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  180. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  181. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  182. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  183. package/src/flow/models/base/ActionModel.tsx +10 -8
  184. package/src/flow/models/base/ActionModelCore.tsx +16 -2
  185. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  186. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  187. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  188. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  189. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  190. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  191. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  192. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  193. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  194. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  195. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  196. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  197. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  198. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  199. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  200. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  201. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  202. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  203. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +155 -25
  204. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  205. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  206. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +395 -1
  207. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  208. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  209. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  210. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  211. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  212. package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
  213. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  214. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  215. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
  216. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  217. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  218. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  219. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  220. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  221. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  222. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  223. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  224. package/src/flow/models/blocks/table/TableBlockModel.tsx +29 -3
  225. package/src/flow/models/blocks/table/TableColumnModel.tsx +54 -11
  226. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  227. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  228. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  229. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +84 -0
  230. package/src/flow/models/blocks/table/utils.ts +7 -0
  231. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  232. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +10 -1
  233. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  234. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  235. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
  236. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +54 -149
  237. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
  238. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  239. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  240. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  241. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  242. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  243. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  244. package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
  245. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  246. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  247. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  248. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  249. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
  250. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  251. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +26 -10
  252. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +51 -14
  253. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  254. package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
  255. package/src/flow/routeTransientInputArgs.ts +27 -0
  256. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
  257. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  258. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  259. package/src/flow-compat/routeTypes.ts +1 -0
  260. package/src/index.ts +7 -0
  261. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  262. package/src/layout-manager/LayoutRoute.tsx +2 -1
  263. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  264. package/src/locale/languageCodes.ts +3 -1
  265. package/src/nocobase-buildin-plugin/index.tsx +19 -1
  266. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  267. package/src/ui-operation/index.ts +33 -0
  268. package/src/ui-operation/ui-operation-codec.ts +54 -0
  269. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  270. package/src/utils/index.tsx +1 -0
  271. package/src/utils/markdownSanitize.ts +88 -0
@@ -8,22 +8,30 @@
8
8
  */
9
9
 
10
10
  import { type FlowEngine, useFlowContext, useFlowEngine } from '@nocobase/flow-engine';
11
+ import { Button, Result } from 'antd';
11
12
  import React, { useEffect, useMemo, useRef, useState } from 'react';
12
- import { deviceType } from 'react-device-detect';
13
- import { useParams } from 'react-router-dom';
13
+ import { useTranslation } from 'react-i18next';
14
+ import { useLocation, useNavigate, useParams } from 'react-router-dom';
15
+ import { getModernClientPrefix, stripModernClientPrefix } from '../../authRedirect';
14
16
  import { useApp } from '../../hooks/useApp';
15
- import { NocoBaseDesktopRouteType } from '../../flow-compat';
16
- import { resolveAdminRouteRuntimeTarget } from '../admin-shell/admin-layout/resolveAdminRouteRuntimeTarget';
17
+ import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../flow-compat';
18
+ import {
19
+ resolveAdminRouteRuntimeTarget,
20
+ toRouterNavigationPath,
21
+ } from '../admin-shell/admin-layout/resolveAdminRouteRuntimeTarget';
17
22
  import { getAdminLayoutModel, type AdminLayoutModel } from '../admin-shell/admin-layout/AdminLayoutModel';
18
23
  import { getLayoutModel, type BaseLayoutModel } from '../admin-shell/BaseLayoutModel';
19
24
  import { useLayoutRoutePage } from '../admin-shell/useLayoutRoutePage';
20
25
  import { AppNotFound } from '../../components';
21
26
  import { useKeepAlive } from '../../components/KeepAlive';
27
+ import { registerDeviceTypeContext } from '../internal/registerDeviceTypeContext';
22
28
 
23
29
  type FlowRouteGuardState = {
30
+ pageUid?: string;
24
31
  pending: boolean;
25
32
  allowBridge: boolean;
26
33
  notFound: boolean;
34
+ legacyPageUnsupported?: boolean;
27
35
  };
28
36
 
29
37
  export type LegacyPageBehavior = 'redirect' | 'notFound' | 'bridge';
@@ -35,6 +43,14 @@ type FlowRouteLayoutContext = {
35
43
  uid?: string;
36
44
  };
37
45
 
46
+ type FlowRouteRepositoryLike = {
47
+ refreshAccessible?: () => Promise<unknown>;
48
+ isAccessibleLoaded?: () => boolean;
49
+ ensureAccessibleLoaded?: () => Promise<unknown>;
50
+ getRouteBySchemaUid?: (schemaUid: string) => NocoBaseDesktopRoute | undefined;
51
+ listAccessible?: () => NocoBaseDesktopRoute[];
52
+ };
53
+
38
54
  export type FlowRouteProps = {
39
55
  pageUid?: string;
40
56
  active?: boolean;
@@ -70,6 +86,44 @@ const getDefaultLegacyPageBehavior = (
70
86
 
71
87
  const shouldRequireAccessibleRoute = (contextLayout?: FlowRouteLayoutContext) => contextLayout?.authCheck !== false;
72
88
 
89
+ const findAccessibleRouteByIdentity = (
90
+ routes: NocoBaseDesktopRoute[] | undefined,
91
+ pageUid: string,
92
+ ): NocoBaseDesktopRoute | undefined => {
93
+ if (!Array.isArray(routes)) {
94
+ return undefined;
95
+ }
96
+
97
+ for (const route of routes) {
98
+ if (
99
+ (route.type !== NocoBaseDesktopRouteType.group && route.schemaUid === pageUid) ||
100
+ (route.type === NocoBaseDesktopRouteType.group && route.id != null && String(route.id) === pageUid)
101
+ ) {
102
+ return route;
103
+ }
104
+
105
+ const matched = findAccessibleRouteByIdentity(route.children, pageUid);
106
+ if (matched) {
107
+ return matched;
108
+ }
109
+ }
110
+
111
+ return undefined;
112
+ };
113
+
114
+ const getAccessibleRouteByPageUid = (
115
+ routeRepository: FlowRouteRepositoryLike | undefined,
116
+ pageUid: string,
117
+ ): NocoBaseDesktopRoute | undefined => {
118
+ const routeBySchemaUid = routeRepository?.getRouteBySchemaUid?.(pageUid);
119
+
120
+ if (routeBySchemaUid && routeBySchemaUid.type !== NocoBaseDesktopRouteType.group) {
121
+ return routeBySchemaUid;
122
+ }
123
+
124
+ return findAccessibleRouteByIdentity(routeRepository?.listAccessible?.(), pageUid);
125
+ };
126
+
73
127
  const hasFlowModel = async (flowEngine: FlowEngine, pageUid: string) => {
74
128
  if (flowEngine.getModel(pageUid)) {
75
129
  return true;
@@ -106,31 +160,9 @@ const BridgeFlowRoute = ({
106
160
  const layoutContentRef = useRef<HTMLDivElement>(null);
107
161
 
108
162
  useEffect(() => {
109
- flowEngine.context.defineProperty('deviceType', {
110
- get: () => (deviceType === 'browser' ? 'computer' : deviceType),
111
- cache: false,
112
- meta: {
113
- type: 'string',
114
- title: flowEngine.translate('Current device type'),
115
- interface: 'select',
116
- uiSchema: {
117
- enum: [
118
- { label: flowEngine.translate('Computer'), value: 'computer' },
119
- { label: flowEngine.translate('Mobile'), value: 'mobile' },
120
- { label: flowEngine.translate('Tablet'), value: 'tablet' },
121
- { label: flowEngine.translate('SmartTv'), value: 'smarttv' },
122
- { label: flowEngine.translate('Console'), value: 'console' },
123
- { label: flowEngine.translate('Wearable'), value: 'wearable' },
124
- { label: flowEngine.translate('Embedded'), value: 'embedded' },
125
- ],
126
- 'x-component': 'Select',
127
- },
128
- },
129
- info: {
130
- description: 'Current device type (computer/mobile/tablet/...).',
131
- detail: 'string',
132
- },
133
- });
163
+ if (!flowEngine.context.getPropertyOptions('deviceType')) {
164
+ registerDeviceTypeContext(flowEngine);
165
+ }
134
166
  }, [flowEngine]);
135
167
 
136
168
  useLayoutRoutePage({
@@ -145,11 +177,58 @@ const BridgeFlowRoute = ({
145
177
  return <div ref={layoutContentRef} />;
146
178
  };
147
179
 
180
+ type RouteLocation = {
181
+ pathname: string;
182
+ search: string;
183
+ hash: string;
184
+ };
185
+
186
+ const getLegacyPageHref = (app: { getPublicPath: () => string }, location: RouteLocation) => {
187
+ const modernPublicPath = app.getPublicPath();
188
+ const browserLocationMatchesPublicPath = window.location.pathname.startsWith(modernPublicPath);
189
+ const currentLocation = browserLocationMatchesPublicPath ? window.location : location;
190
+ const pathWithinModernClient = currentLocation.pathname.startsWith(modernPublicPath)
191
+ ? currentLocation.pathname.slice(modernPublicPath.length)
192
+ : currentLocation.pathname.replace(/^\/+/, '');
193
+ return `${stripModernClientPrefix(modernPublicPath)}${pathWithinModernClient}${currentLocation.search}${
194
+ currentLocation.hash
195
+ }`;
196
+ };
197
+
198
+ const LegacyPageUnsupported = ({
199
+ app,
200
+ location,
201
+ }: {
202
+ app: { getPublicPath: () => string };
203
+ location: RouteLocation;
204
+ }) => {
205
+ const { t } = useTranslation();
206
+ const modernClientPath = `/${getModernClientPrefix()}/`;
207
+ const withModernClientPath = (message: string) => message.replaceAll('{{modernClientPath}}', modernClientPath);
208
+
209
+ return (
210
+ <Result
211
+ status="warning"
212
+ title={withModernClientPath(t('This page is not supported in the {{modernClientPath}} branch'))}
213
+ subTitle={withModernClientPath(
214
+ t(
215
+ 'The {{modernClientPath}} branch only supports new pages. This page is a legacy page. Please open it from the original entry.',
216
+ ),
217
+ )}
218
+ extra={
219
+ <Button href={getLegacyPageHref(app, location)} type="primary">
220
+ {t('Open from the original entry')}
221
+ </Button>
222
+ }
223
+ />
224
+ );
225
+ };
226
+
148
227
  /**
149
228
  * 管理后台动态页面路由组件。
150
229
  *
151
- * 负责读取当前路由页面 UID,补充运行时设备变量,
152
- * 并把页面生命周期桥接到 AdminLayout host model。
230
+ * 负责读取当前路由页面 UID,并把页面生命周期桥接到 AdminLayout host model。
231
+ * 设备变量通常由 PluginFlowEngine 共享初始化提供;独立渲染时会在挂载后补充注册。
153
232
  *
154
233
  * @example
155
234
  * ```tsx
@@ -197,11 +276,15 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
197
276
  );
198
277
  const legacyPageBehavior = legacyPageBehaviorProp || getDefaultLegacyPageBehavior(flowEngine, routeLayout);
199
278
  const app = useApp();
200
- const routeRepository = flowEngine.context.routeRepository;
279
+ const location = useLocation();
280
+ const navigate = useNavigate();
281
+ const routeRepository = flowEngine.context.routeRepository as FlowRouteRepositoryLike | undefined;
201
282
  const params = useParams();
202
283
  const pageUid = pageUidProp || params?.name;
284
+ const hasNestedRoutePath = typeof params?.tabUid !== 'undefined' || typeof params?.['*'] !== 'undefined';
203
285
  const skipRouteRepositoryCheck = !routeRepository;
204
286
  const [guardState, setGuardState] = useState<FlowRouteGuardState>({
287
+ pageUid: undefined,
205
288
  pending: true,
206
289
  allowBridge: false,
207
290
  notFound: false,
@@ -213,19 +296,24 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
213
296
  throw new Error('[NocoBase] FlowRoute requires pageUid or route.params.name.');
214
297
  }
215
298
 
299
+ useEffect(() => {
300
+ replaceTriggeredRef.current = false;
301
+ }, [location.hash, location.pathname, location.search, pageUid]);
302
+
216
303
  useEffect(() => {
217
304
  let active = true;
218
305
  const requestId = ++requestIdRef.current;
306
+ const requiresAccessibleRoute = shouldRequireAccessibleRoute(routeLayout);
219
307
 
220
308
  const run = async () => {
221
- setGuardState({ pending: true, allowBridge: false, notFound: false });
309
+ setGuardState({ pageUid, pending: true, allowBridge: false, notFound: false });
222
310
 
223
- if (!skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
311
+ if (requiresAccessibleRoute && !skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
224
312
  try {
225
313
  await routeRepository?.ensureAccessibleLoaded?.();
226
314
  } catch (_error) {
227
315
  if (active && requestId === requestIdRef.current) {
228
- setGuardState({ pending: false, allowBridge: true, notFound: false });
316
+ setGuardState({ pageUid, pending: false, allowBridge: true, notFound: false });
229
317
  }
230
318
  return;
231
319
  }
@@ -235,28 +323,67 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
235
323
  return;
236
324
  }
237
325
 
238
- const route = skipRouteRepositoryCheck ? undefined : routeRepository?.getRouteBySchemaUid?.(pageUid);
239
- if (!route && !skipRouteRepositoryCheck && shouldRequireAccessibleRoute(routeLayout)) {
240
- setGuardState({ pending: false, allowBridge: false, notFound: true });
326
+ const route =
327
+ skipRouteRepositoryCheck || !requiresAccessibleRoute
328
+ ? undefined
329
+ : getAccessibleRouteByPageUid(routeRepository, pageUid);
330
+ if (!route && !skipRouteRepositoryCheck && requiresAccessibleRoute) {
331
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
241
332
  return;
242
333
  }
243
334
 
244
335
  if (!route && legacyPageBehavior === 'notFound') {
245
336
  const flowModelExists = await hasFlowModel(flowEngine, pageUid);
246
337
  if (active && requestId === requestIdRef.current) {
247
- setGuardState({ pending: false, allowBridge: flowModelExists, notFound: !flowModelExists });
338
+ setGuardState({ pageUid, pending: false, allowBridge: flowModelExists, notFound: !flowModelExists });
339
+ }
340
+ return;
341
+ }
342
+
343
+ if (route?.type === NocoBaseDesktopRouteType.group) {
344
+ if (hasNestedRoutePath) {
345
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
346
+ return;
347
+ }
348
+
349
+ const target = resolveAdminRouteRuntimeTarget({
350
+ app,
351
+ route,
352
+ layout: routeLayout,
353
+ });
354
+
355
+ if (target.reason === 'emptyGroup') {
356
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: false });
357
+ return;
248
358
  }
359
+
360
+ if (target.runtimePath) {
361
+ if (replaceTriggeredRef.current) {
362
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: false });
363
+ return;
364
+ }
365
+
366
+ replaceTriggeredRef.current = true;
367
+ if (target.navigationMode === 'document') {
368
+ window.location.replace(target.runtimePath);
369
+ return;
370
+ }
371
+ navigate(toRouterNavigationPath(target.runtimePath, app.router?.getBasename?.()), { replace: true });
372
+ return;
373
+ }
374
+
375
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
249
376
  return;
250
377
  }
251
378
 
252
379
  if (route?.type === NocoBaseDesktopRouteType.page) {
253
380
  if (legacyPageBehavior === 'notFound') {
254
- setGuardState({ pending: false, allowBridge: false, notFound: true });
381
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
255
382
  return;
256
383
  }
257
384
 
258
385
  if (legacyPageBehavior === 'bridge') {
259
- setGuardState({ pending: false, allowBridge: true, notFound: false });
386
+ setGuardState({ pageUid, pending: false, allowBridge: true, notFound: false });
260
387
  return;
261
388
  }
262
389
 
@@ -279,14 +406,20 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
279
406
 
280
407
  if (target.reason === 'unsupportedV2Runtime') {
281
408
  if (active && requestId === requestIdRef.current) {
282
- setGuardState({ pending: false, allowBridge: false, notFound: true });
409
+ setGuardState({
410
+ pageUid,
411
+ pending: false,
412
+ allowBridge: false,
413
+ notFound: false,
414
+ legacyPageUnsupported: true,
415
+ });
283
416
  }
284
417
  return;
285
418
  }
286
419
  }
287
420
 
288
421
  if (active && requestId === requestIdRef.current) {
289
- setGuardState({ pending: false, allowBridge: true, notFound: false });
422
+ setGuardState({ pageUid, pending: false, allowBridge: true, notFound: false });
290
423
  }
291
424
  };
292
425
 
@@ -295,10 +428,20 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
295
428
  return () => {
296
429
  active = false;
297
430
  };
298
- }, [app, flowEngine, legacyPageBehavior, pageUid, routeLayout, routeRepository, skipRouteRepositoryCheck]);
431
+ }, [
432
+ app,
433
+ flowEngine,
434
+ hasNestedRoutePath,
435
+ legacyPageBehavior,
436
+ navigate,
437
+ pageUid,
438
+ routeLayout,
439
+ routeRepository,
440
+ skipRouteRepositoryCheck,
441
+ ]);
299
442
 
300
443
  const content = useMemo(() => {
301
- if (guardState.pending) {
444
+ if (guardState.pageUid !== pageUid || guardState.pending) {
302
445
  return null;
303
446
  }
304
447
 
@@ -306,12 +449,27 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
306
449
  return <AppNotFound />;
307
450
  }
308
451
 
452
+ if (guardState.legacyPageUnsupported) {
453
+ return <LegacyPageUnsupported app={app} location={location} />;
454
+ }
455
+
309
456
  if (!guardState.allowBridge) {
310
457
  return null;
311
458
  }
312
459
 
313
460
  return <BridgeFlowRoute pageUid={pageUid} active={active} getLayoutModel={getLayoutModel} />;
314
- }, [active, getLayoutModel, guardState.allowBridge, guardState.notFound, guardState.pending, pageUid]);
461
+ }, [
462
+ active,
463
+ app,
464
+ getLayoutModel,
465
+ guardState.allowBridge,
466
+ guardState.legacyPageUnsupported,
467
+ guardState.notFound,
468
+ guardState.pageUid,
469
+ guardState.pending,
470
+ location,
471
+ pageUid,
472
+ ]);
315
473
 
316
474
  return content;
317
475
  };
@@ -15,6 +15,7 @@ export interface RunJSValueEditorProps {
15
15
  t?: (key: string) => string;
16
16
  value?: unknown;
17
17
  onChange?: (value: RunJSValue) => void;
18
+ disabled?: boolean;
18
19
  height?: string;
19
20
  scene?: string;
20
21
  containerStyle?: React.CSSProperties;
@@ -25,6 +26,7 @@ export const RunJSValueEditor: React.FC<RunJSValueEditorProps> = (props) => {
25
26
  t,
26
27
  value,
27
28
  onChange,
29
+ disabled,
28
30
  height = '200px',
29
31
  scene = 'formValue',
30
32
  containerStyle = { flex: 1, minWidth: 0 },
@@ -38,9 +40,15 @@ export const RunJSValueEditor: React.FC<RunJSValueEditorProps> = (props) => {
38
40
  <div style={containerStyle}>
39
41
  <CodeEditor
40
42
  value={current.code}
41
- onChange={(code) => onChange?.({ ...current, code })}
43
+ onChange={(code) => {
44
+ if (disabled) {
45
+ return;
46
+ }
47
+ onChange?.({ ...current, code });
48
+ }}
42
49
  version={current.version}
43
50
  height={height}
51
+ readonly={disabled}
44
52
  enableLinter
45
53
  placeholder={placeholderText}
46
54
  scene={scene}
@@ -0,0 +1,216 @@
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, vi } from 'vitest';
12
+ import { render, waitFor } from '@nocobase/test/client';
13
+ import { FieldAssignValueInput } from '../FieldAssignValueInput';
14
+
15
+ const { mockUseFlowContext, mockGetDefaultBindingByField, mockVariableInput } = vi.hoisted(() => ({
16
+ mockUseFlowContext: vi.fn(),
17
+ mockGetDefaultBindingByField: vi.fn(),
18
+ mockVariableInput: vi.fn(() => <div data-testid="variable-input" />),
19
+ }));
20
+
21
+ vi.mock('@nocobase/flow-engine', async () => {
22
+ const actual = await vi.importActual<typeof import('@nocobase/flow-engine')>('@nocobase/flow-engine');
23
+ class MockEditableItemModel extends actual.EditableItemModel {}
24
+ MockEditableItemModel.getDefaultBindingByField = mockGetDefaultBindingByField;
25
+
26
+ return {
27
+ ...actual,
28
+ useFlowContext: () => mockUseFlowContext(),
29
+ VariableInput: mockVariableInput,
30
+ FlowModelRenderer: () => <div data-testid="flow-model-renderer" />,
31
+ EditableItemModel: MockEditableItemModel,
32
+ };
33
+ });
34
+
35
+ describe('FieldAssignValueInput context', () => {
36
+ it('delegates temporary field model context to the source form context', async () => {
37
+ type MockContext = {
38
+ dataSourceManager: { getDataSource: ReturnType<typeof vi.fn> };
39
+ t: (key: string) => string;
40
+ engine?: { createModel: ReturnType<typeof vi.fn> };
41
+ collection?: MockCollection;
42
+ blockModel?: MockFormModel;
43
+ };
44
+ type MockFieldModel = {
45
+ props: Record<string, unknown>;
46
+ setProps: ReturnType<typeof vi.fn>;
47
+ dispatchEvent: ReturnType<typeof vi.fn>;
48
+ remove: ReturnType<typeof vi.fn>;
49
+ };
50
+ type MockCollectionField = {
51
+ name: string;
52
+ interface: string;
53
+ uiSchema: { enum: Array<{ label: string; value: string }> };
54
+ isAssociationField: () => boolean;
55
+ getComponentProps: () => Record<string, unknown>;
56
+ };
57
+ type MockCollection = {
58
+ dataSourceKey: string;
59
+ name: string;
60
+ getField: (name: string) => MockCollectionField | null;
61
+ getFields: () => MockCollectionField[];
62
+ };
63
+ type MockFormModel = {
64
+ context: MockContext;
65
+ collection: MockCollection;
66
+ subModels: Record<string, unknown>;
67
+ };
68
+
69
+ const sourceContext: MockContext = {
70
+ dataSourceManager: {
71
+ getDataSource: vi.fn(() => ({})),
72
+ },
73
+ t: (key: string) => key,
74
+ };
75
+ const fieldModel: MockFieldModel = {
76
+ props: {},
77
+ setProps: vi.fn((props: Record<string, unknown>) => {
78
+ fieldModel.props = { ...fieldModel.props, ...props };
79
+ }),
80
+ dispatchEvent: vi.fn(),
81
+ remove: vi.fn(),
82
+ };
83
+ const tempRoot = {
84
+ context: {
85
+ defineProperty: vi.fn(),
86
+ },
87
+ subModels: {
88
+ fields: [fieldModel],
89
+ },
90
+ setProps: vi.fn(),
91
+ remove: vi.fn(),
92
+ };
93
+ const engine = {
94
+ createModel: vi.fn(() => tempRoot),
95
+ };
96
+ sourceContext.engine = engine;
97
+
98
+ const collectionField: MockCollectionField = {
99
+ name: 'status',
100
+ interface: 'select',
101
+ uiSchema: {
102
+ enum: [{ label: 'Open', value: 'open' }],
103
+ },
104
+ isAssociationField: () => false,
105
+ getComponentProps: () => ({}),
106
+ };
107
+ const collection: MockCollection = {
108
+ dataSourceKey: 'main',
109
+ name: 'tasks',
110
+ getField: (name: string) => (name === 'status' ? collectionField : null),
111
+ getFields: () => [collectionField],
112
+ };
113
+ const formModel: MockFormModel = {
114
+ context: sourceContext,
115
+ collection,
116
+ subModels: {},
117
+ };
118
+ sourceContext.collection = collection;
119
+ sourceContext.blockModel = formModel;
120
+
121
+ mockGetDefaultBindingByField.mockReturnValue({ modelName: 'SelectFieldModel' });
122
+ mockUseFlowContext.mockReturnValue({
123
+ model: formModel,
124
+ t: (key: string) => key,
125
+ getPropertyMetaTree: vi.fn(async () => []),
126
+ });
127
+
128
+ render(<FieldAssignValueInput targetPath="status" value="" onChange={vi.fn()} />);
129
+
130
+ await waitFor(() => {
131
+ expect(engine.createModel).toHaveBeenCalled();
132
+ });
133
+ expect(engine.createModel).toHaveBeenCalledWith(expect.any(Object), { delegate: sourceContext });
134
+ });
135
+
136
+ it('lets callers override variable path parsing for domain-specific stored formats', async () => {
137
+ mockVariableInput.mockClear();
138
+ const sourceContext = {
139
+ dataSourceManager: {
140
+ getDataSource: vi.fn(() => ({})),
141
+ },
142
+ t: (key: string) => key,
143
+ };
144
+ const fieldModel = {
145
+ props: {},
146
+ setProps: vi.fn(),
147
+ dispatchEvent: vi.fn(),
148
+ remove: vi.fn(),
149
+ };
150
+ const tempRoot = {
151
+ context: {
152
+ defineProperty: vi.fn(),
153
+ },
154
+ subModels: {
155
+ fields: [fieldModel],
156
+ },
157
+ setProps: vi.fn(),
158
+ remove: vi.fn(),
159
+ };
160
+ const engine = {
161
+ createModel: vi.fn(() => tempRoot),
162
+ };
163
+ const collectionField = {
164
+ name: 'status',
165
+ interface: 'input',
166
+ uiSchema: { 'x-component': 'Input' },
167
+ isAssociationField: () => false,
168
+ getComponentProps: () => ({}),
169
+ };
170
+ const collection = {
171
+ dataSourceKey: 'main',
172
+ name: 'tasks',
173
+ getField: (name: string) => (name === 'status' ? collectionField : null),
174
+ getFields: () => [collectionField],
175
+ };
176
+ const formModel = {
177
+ context: { ...sourceContext, engine, collection, blockModel: null as any },
178
+ collection,
179
+ subModels: {},
180
+ };
181
+ formModel.context.blockModel = formModel;
182
+
183
+ mockGetDefaultBindingByField.mockReturnValue({ modelName: 'InputFieldModel' });
184
+ mockUseFlowContext.mockReturnValue({
185
+ model: formModel,
186
+ t: (key: string) => key,
187
+ getPropertyMetaTree: vi.fn(async () => []),
188
+ });
189
+
190
+ const variableConverters = {
191
+ resolvePathFromValue: vi.fn((value: string) =>
192
+ value === '{{$context.data.updatedAt}}' ? ['$context', 'data', 'updatedAt'] : undefined,
193
+ ),
194
+ resolveValueFromPath: vi.fn(() => undefined),
195
+ };
196
+
197
+ render(
198
+ <FieldAssignValueInput
199
+ targetPath="status"
200
+ value="{{$context.data.updatedAt}}"
201
+ onChange={vi.fn()}
202
+ variableConverters={variableConverters}
203
+ />,
204
+ );
205
+
206
+ await waitFor(() => {
207
+ expect(engine.createModel).toHaveBeenCalled();
208
+ });
209
+ const latestVariableInputCall = mockVariableInput.mock.calls.at(-1)?.[0];
210
+ expect(latestVariableInputCall.converters.resolvePathFromValue('{{$context.data.updatedAt}}')).toEqual([
211
+ '$context',
212
+ 'data',
213
+ 'updatedAt',
214
+ ]);
215
+ });
216
+ });