@nocobase/client-v2 2.2.0-beta.9 → 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 (224) hide show
  1. package/es/APIClient.d.ts +13 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  4. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  5. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  6. package/es/components/category-tabs/index.d.ts +9 -0
  7. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  8. package/es/components/form/TypedVariableInput.d.ts +31 -5
  9. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  10. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  11. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  12. package/es/components/form/filter/index.d.ts +2 -0
  13. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  14. package/es/components/index.d.ts +1 -0
  15. package/es/flow/actions/index.d.ts +1 -1
  16. package/es/flow/actions/linkageRules.d.ts +27 -0
  17. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  18. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  19. package/es/flow/components/FieldAssignValueInput.d.ts +7 -24
  20. package/es/flow/components/FlowRoute.d.ts +2 -2
  21. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  22. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  23. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  24. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  25. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  26. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  27. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  28. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  29. package/es/flow/index.d.ts +1 -0
  30. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  31. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  32. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  33. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  34. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  35. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  36. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  37. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  38. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  39. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  40. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  41. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  42. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  43. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  44. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  45. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  46. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  47. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  48. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  49. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  50. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  51. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  52. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  53. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  54. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  55. package/es/flow-compat/passwordUtils.d.ts +1 -1
  56. package/es/flow-compat/routeTypes.d.ts +1 -0
  57. package/es/index.d.ts +5 -0
  58. package/es/index.mjs +199 -161
  59. package/es/ui-operation/index.d.ts +15 -0
  60. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  61. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  62. package/es/utils/index.d.ts +1 -0
  63. package/lib/index.js +215 -177
  64. package/lib/locale/languageCodes.js +3 -1
  65. package/package.json +9 -7
  66. package/src/APIClient.ts +90 -7
  67. package/src/Application.tsx +7 -1
  68. package/src/BaseApplication.tsx +11 -6
  69. package/src/__tests__/APIClient.test.tsx +58 -0
  70. package/src/__tests__/app.test.tsx +73 -0
  71. package/src/__tests__/browserChecker.test.ts +61 -0
  72. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  73. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +35 -2
  74. package/src/__tests__/settings-center.test.tsx +270 -6
  75. package/src/acl/ACLProvider.tsx +2 -0
  76. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  77. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  78. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  79. package/src/collection-manager/field-validation.ts +1 -1
  80. package/src/components/README.md +7 -1
  81. package/src/components/README.zh-CN.md +6 -1
  82. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  83. package/src/components/category-tabs/index.ts +10 -0
  84. package/src/components/form/JsonTextArea.tsx +4 -0
  85. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  86. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  87. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  88. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  89. package/src/components/form/TypedVariableInput.tsx +452 -101
  90. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  91. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  92. package/src/components/form/filter/CollectionFilterItem.tsx +36 -9
  93. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  94. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +55 -0
  95. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  96. package/src/components/form/filter/index.ts +2 -0
  97. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  98. package/src/components/form/table/Table.tsx +81 -6
  99. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  100. package/src/components/index.ts +1 -0
  101. package/src/flow/FlowPage.tsx +9 -1
  102. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  103. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  104. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  105. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  106. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  107. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  108. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  109. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  110. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  111. package/src/flow/actions/index.ts +2 -0
  112. package/src/flow/actions/linkageRules.tsx +144 -26
  113. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  114. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
  115. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  116. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  117. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  118. package/src/flow/common/Markdown/style.ts +4 -0
  119. package/src/flow/components/BlockItemCard.tsx +2 -2
  120. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  121. package/src/flow/components/FieldAssignValueInput.tsx +72 -584
  122. package/src/flow/components/FlowRoute.tsx +78 -32
  123. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  124. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  125. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  126. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  127. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  128. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  129. package/src/flow/components/field-value-variable/index.ts +12 -0
  130. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  131. package/src/flow/components/filter/VariableFilterItem.tsx +170 -13
  132. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  133. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  134. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +195 -1
  135. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  136. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  137. package/src/flow/index.ts +3 -0
  138. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  139. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  140. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  141. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  142. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  143. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  144. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  145. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  146. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  147. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  148. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  149. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  152. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  153. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  154. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  155. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  156. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  157. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  158. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  159. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  160. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  161. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  162. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +155 -25
  163. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  164. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  165. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +395 -1
  166. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  167. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  168. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  169. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  170. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  171. package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
  172. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  173. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  174. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
  175. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  176. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  177. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  178. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  179. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  180. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  181. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  182. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  183. package/src/flow/models/blocks/table/TableBlockModel.tsx +29 -3
  184. package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
  185. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  186. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  187. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  188. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  189. package/src/flow/models/blocks/table/utils.ts +7 -0
  190. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  191. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +10 -1
  192. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  193. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  194. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
  195. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
  196. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
  197. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  198. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  199. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  200. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  201. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  202. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  203. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  204. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  205. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  206. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  207. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  208. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +26 -10
  209. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +51 -14
  210. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  211. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  212. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  213. package/src/flow-compat/routeTypes.ts +1 -0
  214. package/src/index.ts +5 -0
  215. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  216. package/src/layout-manager/LayoutRoute.tsx +2 -1
  217. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  218. package/src/locale/languageCodes.ts +3 -1
  219. package/src/nocobase-buildin-plugin/index.tsx +12 -0
  220. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  221. package/src/ui-operation/index.ts +33 -0
  222. package/src/ui-operation/ui-operation-codec.ts +54 -0
  223. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  224. package/src/utils/index.tsx +1 -0
@@ -8,9 +8,11 @@
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 { useTranslation } from 'react-i18next';
13
14
  import { useLocation, useNavigate, useParams } from 'react-router-dom';
15
+ import { getModernClientPrefix, stripModernClientPrefix } from '../../authRedirect';
14
16
  import { useApp } from '../../hooks/useApp';
15
17
  import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../flow-compat';
16
18
  import {
@@ -22,12 +24,14 @@ import { getLayoutModel, type BaseLayoutModel } from '../admin-shell/BaseLayoutM
22
24
  import { useLayoutRoutePage } from '../admin-shell/useLayoutRoutePage';
23
25
  import { AppNotFound } from '../../components';
24
26
  import { useKeepAlive } from '../../components/KeepAlive';
27
+ import { registerDeviceTypeContext } from '../internal/registerDeviceTypeContext';
25
28
 
26
29
  type FlowRouteGuardState = {
27
30
  pageUid?: string;
28
31
  pending: boolean;
29
32
  allowBridge: boolean;
30
33
  notFound: boolean;
34
+ legacyPageUnsupported?: boolean;
31
35
  };
32
36
 
33
37
  export type LegacyPageBehavior = 'redirect' | 'notFound' | 'bridge';
@@ -156,31 +160,9 @@ const BridgeFlowRoute = ({
156
160
  const layoutContentRef = useRef<HTMLDivElement>(null);
157
161
 
158
162
  useEffect(() => {
159
- flowEngine.context.defineProperty('deviceType', {
160
- get: () => (deviceType === 'browser' ? 'computer' : deviceType),
161
- cache: false,
162
- meta: {
163
- type: 'string',
164
- title: flowEngine.translate('Current device type'),
165
- interface: 'select',
166
- uiSchema: {
167
- enum: [
168
- { label: flowEngine.translate('Computer'), value: 'computer' },
169
- { label: flowEngine.translate('Mobile'), value: 'mobile' },
170
- { label: flowEngine.translate('Tablet'), value: 'tablet' },
171
- { label: flowEngine.translate('SmartTv'), value: 'smarttv' },
172
- { label: flowEngine.translate('Console'), value: 'console' },
173
- { label: flowEngine.translate('Wearable'), value: 'wearable' },
174
- { label: flowEngine.translate('Embedded'), value: 'embedded' },
175
- ],
176
- 'x-component': 'Select',
177
- },
178
- },
179
- info: {
180
- description: 'Current device type (computer/mobile/tablet/...).',
181
- detail: 'string',
182
- },
183
- });
163
+ if (!flowEngine.context.getPropertyOptions('deviceType')) {
164
+ registerDeviceTypeContext(flowEngine);
165
+ }
184
166
  }, [flowEngine]);
185
167
 
186
168
  useLayoutRoutePage({
@@ -195,11 +177,58 @@ const BridgeFlowRoute = ({
195
177
  return <div ref={layoutContentRef} />;
196
178
  };
197
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
+
198
227
  /**
199
228
  * 管理后台动态页面路由组件。
200
229
  *
201
- * 负责读取当前路由页面 UID,补充运行时设备变量,
202
- * 并把页面生命周期桥接到 AdminLayout host model。
230
+ * 负责读取当前路由页面 UID,并把页面生命周期桥接到 AdminLayout host model。
231
+ * 设备变量通常由 PluginFlowEngine 共享初始化提供;独立渲染时会在挂载后补充注册。
203
232
  *
204
233
  * @example
205
234
  * ```tsx
@@ -274,11 +303,12 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
274
303
  useEffect(() => {
275
304
  let active = true;
276
305
  const requestId = ++requestIdRef.current;
306
+ const requiresAccessibleRoute = shouldRequireAccessibleRoute(routeLayout);
277
307
 
278
308
  const run = async () => {
279
309
  setGuardState({ pageUid, pending: true, allowBridge: false, notFound: false });
280
310
 
281
- if (!skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
311
+ if (requiresAccessibleRoute && !skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
282
312
  try {
283
313
  await routeRepository?.ensureAccessibleLoaded?.();
284
314
  } catch (_error) {
@@ -293,8 +323,11 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
293
323
  return;
294
324
  }
295
325
 
296
- const route = skipRouteRepositoryCheck ? undefined : getAccessibleRouteByPageUid(routeRepository, pageUid);
297
- if (!route && !skipRouteRepositoryCheck && shouldRequireAccessibleRoute(routeLayout)) {
326
+ const route =
327
+ skipRouteRepositoryCheck || !requiresAccessibleRoute
328
+ ? undefined
329
+ : getAccessibleRouteByPageUid(routeRepository, pageUid);
330
+ if (!route && !skipRouteRepositoryCheck && requiresAccessibleRoute) {
298
331
  setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
299
332
  return;
300
333
  }
@@ -373,7 +406,13 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
373
406
 
374
407
  if (target.reason === 'unsupportedV2Runtime') {
375
408
  if (active && requestId === requestIdRef.current) {
376
- setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
409
+ setGuardState({
410
+ pageUid,
411
+ pending: false,
412
+ allowBridge: false,
413
+ notFound: false,
414
+ legacyPageUnsupported: true,
415
+ });
377
416
  }
378
417
  return;
379
418
  }
@@ -410,6 +449,10 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
410
449
  return <AppNotFound />;
411
450
  }
412
451
 
452
+ if (guardState.legacyPageUnsupported) {
453
+ return <LegacyPageUnsupported app={app} location={location} />;
454
+ }
455
+
413
456
  if (!guardState.allowBridge) {
414
457
  return null;
415
458
  }
@@ -417,11 +460,14 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
417
460
  return <BridgeFlowRoute pageUid={pageUid} active={active} getLayoutModel={getLayoutModel} />;
418
461
  }, [
419
462
  active,
463
+ app,
420
464
  getLayoutModel,
421
465
  guardState.allowBridge,
466
+ guardState.legacyPageUnsupported,
422
467
  guardState.notFound,
423
468
  guardState.pageUid,
424
469
  guardState.pending,
470
+ location,
425
471
  pageUid,
426
472
  ]);
427
473
 
@@ -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
+ });
@@ -0,0 +1,181 @@
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 { useFlowContext, type CtxDateExpressionConfig, type CtxDateRelativeUnit } from '@nocobase/flow-engine';
11
+ import { AutoComplete, Input, InputNumber, Select, Space } from 'antd';
12
+ import type { Dayjs } from 'dayjs';
13
+ import React from 'react';
14
+ import { FieldAssignExactDatePicker, type ExactDatePickerValue } from '../FieldAssignExactDatePicker';
15
+ import { toExactPickerDisplayValue, type DateVariableComponentProps } from './dateValue';
16
+
17
+ const DATE_FORMAT_OPTIONS = [
18
+ 'YYYY-MM-DD',
19
+ 'YYYY-MM-DD HH:mm:ss',
20
+ 'YYYY/MM/DD',
21
+ 'MM/DD/YYYY',
22
+ 'DD/MM/YYYY',
23
+ 'YYYYMMDD',
24
+ ].map((value) => ({ value }));
25
+
26
+ const PRESET_LABELS: Record<string, string> = {
27
+ now: 'Now',
28
+ today: 'Today',
29
+ yesterday: 'Yesterday',
30
+ tomorrow: 'Tomorrow',
31
+ thisWeek: 'This Week',
32
+ lastWeek: 'Last Week',
33
+ nextWeek: 'Next Week',
34
+ thisMonth: 'This Month',
35
+ lastMonth: 'Last Month',
36
+ nextMonth: 'Next Month',
37
+ thisQuarter: 'This Quarter',
38
+ lastQuarter: 'Last Quarter',
39
+ nextQuarter: 'Next Quarter',
40
+ thisYear: 'This Year',
41
+ lastYear: 'Last Year',
42
+ nextYear: 'Next Year',
43
+ };
44
+
45
+ export function getDefaultDateVariableFormat(config: CtxDateExpressionConfig): string {
46
+ return config.kind === 'preset' && config.preset === 'now' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
47
+ }
48
+
49
+ export function serializeExactDatePickerValue(
50
+ value: ExactDatePickerValue,
51
+ showTime: boolean,
52
+ ): string | [string, string] {
53
+ const serialize = (item: Dayjs) => (showTime ? item.toISOString() : item.format('YYYY-MM-DD'));
54
+ if (Array.isArray(value)) {
55
+ return [serialize(value[0]), serialize(value[1])];
56
+ }
57
+ return value ? serialize(value) : '';
58
+ }
59
+
60
+ type DateVariableEditorProps = {
61
+ value?: CtxDateExpressionConfig;
62
+ onChange?: (value: CtxDateExpressionConfig) => void;
63
+ isDateLikeField: boolean;
64
+ dateComponentProps: DateVariableComponentProps;
65
+ style?: React.CSSProperties;
66
+ disabled?: boolean;
67
+ };
68
+
69
+ const DateFormatEditor: React.FC<{
70
+ value: string;
71
+ placeholder: string;
72
+ onChange: (value: string) => void;
73
+ disabled?: boolean;
74
+ }> = ({ value, placeholder, onChange, disabled }) => {
75
+ const ctx = useFlowContext();
76
+ return (
77
+ <Space.Compact style={{ width: '100%' }}>
78
+ <Input
79
+ value={ctx.t('Format')}
80
+ readOnly
81
+ disabled={disabled}
82
+ tabIndex={-1}
83
+ aria-hidden
84
+ style={{ width: 88, pointerEvents: 'none' }}
85
+ />
86
+ <AutoComplete
87
+ value={value}
88
+ placeholder={placeholder}
89
+ options={DATE_FORMAT_OPTIONS}
90
+ onChange={(nextValue) => onChange(nextValue.slice(0, 128))}
91
+ disabled={disabled}
92
+ style={{ flex: 1, minWidth: 0 }}
93
+ aria-label={ctx.t('Format')}
94
+ />
95
+ </Space.Compact>
96
+ );
97
+ };
98
+
99
+ export const DateVariableEditor: React.FC<DateVariableEditorProps> = ({
100
+ value,
101
+ onChange,
102
+ isDateLikeField,
103
+ dateComponentProps,
104
+ style,
105
+ disabled = false,
106
+ }) => {
107
+ const ctx = useFlowContext();
108
+ if (!value) return null;
109
+
110
+ const updateFormat = (format: string) => onChange?.({ ...value, format });
111
+ const format = value.format || '';
112
+
113
+ const renderValueEditor = () => {
114
+ if (value.kind === 'exact') {
115
+ const isRange = Array.isArray(value.value);
116
+ return (
117
+ <FieldAssignExactDatePicker
118
+ picker={dateComponentProps.picker}
119
+ showTime={dateComponentProps.showTime}
120
+ timeFormat={dateComponentProps.timeFormat}
121
+ format={dateComponentProps.format}
122
+ isRange={isRange}
123
+ value={toExactPickerDisplayValue(value.value, {
124
+ format: dateComponentProps.format,
125
+ isRange,
126
+ })}
127
+ onChange={(nextValue) => {
128
+ onChange?.({ ...value, value: serializeExactDatePickerValue(nextValue, dateComponentProps.showTime) });
129
+ }}
130
+ disabled={disabled}
131
+ style={{ width: '100%' }}
132
+ />
133
+ );
134
+ }
135
+
136
+ if (value.kind === 'relative') {
137
+ return (
138
+ <Space.Compact style={{ width: '100%' }}>
139
+ <InputNumber
140
+ min={1}
141
+ precision={0}
142
+ value={value.amount}
143
+ disabled={disabled}
144
+ onChange={(amount) => onChange?.({ ...value, amount: typeof amount === 'number' ? amount : 1 })}
145
+ aria-label={ctx.t(value.direction === 'past' ? 'Past' : 'Next')}
146
+ style={{ flex: '1 1 auto', minWidth: 80 }}
147
+ />
148
+ <Select
149
+ value={value.unit}
150
+ disabled={disabled}
151
+ onChange={(unit: CtxDateRelativeUnit) => onChange?.({ ...value, unit })}
152
+ aria-label={ctx.t('Unit')}
153
+ style={{ flex: '0 0 130px', minWidth: 130 }}
154
+ options={[
155
+ { value: 'day', label: ctx.t('Day') },
156
+ { value: 'week', label: ctx.t('Calendar week') },
157
+ { value: 'month', label: ctx.t('Calendar Month') },
158
+ { value: 'year', label: ctx.t('Calendar Year') },
159
+ ]}
160
+ />
161
+ </Space.Compact>
162
+ );
163
+ }
164
+
165
+ return <Input value={ctx.t(PRESET_LABELS[value.preset] || value.preset)} readOnly disabled={disabled} />;
166
+ };
167
+
168
+ return (
169
+ <div style={{ display: 'flex', flex: 1, flexDirection: 'column', gap: 8, width: '100%', minWidth: 0, ...style }}>
170
+ {renderValueEditor()}
171
+ {!isDateLikeField && (
172
+ <DateFormatEditor
173
+ value={format}
174
+ placeholder={getDefaultDateVariableFormat(value)}
175
+ onChange={updateFormat}
176
+ disabled={disabled}
177
+ />
178
+ )}
179
+ </div>
180
+ );
181
+ };