@nocobase/client-v2 2.2.0-beta.8 → 2.3.0-alpha.1

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 (164) hide show
  1. package/es/APIClient.d.ts +1 -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 +2 -1
  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 +2 -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 +9 -0
  31. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  32. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  33. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  34. package/es/flow/index.d.ts +1 -0
  35. package/es/flow/models/base/ActionModelCore.d.ts +2 -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/form/QuickEditFormModel.d.ts +17 -2
  40. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  41. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  42. package/es/flow-compat/routeTypes.d.ts +1 -0
  43. package/es/index.d.ts +6 -0
  44. package/es/index.mjs +279 -158
  45. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  46. package/es/utils/index.d.ts +1 -0
  47. package/es/utils/markdownSanitize.d.ts +11 -0
  48. package/lib/index.js +280 -159
  49. package/lib/locale/languageCodes.js +2 -1
  50. package/package.json +8 -7
  51. package/src/APIClient.ts +1 -10
  52. package/src/Application.tsx +4 -0
  53. package/src/BaseApplication.tsx +13 -6
  54. package/src/RouteRepository.ts +25 -0
  55. package/src/RouterManager.tsx +142 -1
  56. package/src/__tests__/RouteRepository.test.ts +23 -0
  57. package/src/__tests__/RouterManager.test.ts +125 -0
  58. package/src/__tests__/app.test.tsx +73 -0
  59. package/src/__tests__/authRedirect.test.ts +17 -0
  60. package/src/__tests__/browserChecker.test.ts +61 -0
  61. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  62. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +28 -0
  63. package/src/authRedirect.ts +38 -0
  64. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  65. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  66. package/src/collection-manager/field-validation.ts +1 -1
  67. package/src/components/README.md +7 -1
  68. package/src/components/README.zh-CN.md +6 -1
  69. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  70. package/src/components/category-tabs/index.ts +10 -0
  71. package/src/components/form/JsonTextArea.tsx +4 -0
  72. package/src/components/form/ScanInput/CodeScanner.tsx +23 -6
  73. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +180 -3
  74. package/src/components/form/ScanInput/useCodeScanner.ts +170 -5
  75. package/src/components/form/TypedVariableInput.tsx +452 -101
  76. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  77. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  78. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  79. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  80. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  81. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  82. package/src/components/form/filter/index.ts +2 -0
  83. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  84. package/src/components/index.ts +1 -0
  85. package/src/entry-actions/EntryActionManager.ts +76 -0
  86. package/src/entry-actions/index.ts +10 -0
  87. package/src/flow/FlowPage.tsx +38 -3
  88. package/src/flow/__tests__/FlowPage.test.tsx +201 -27
  89. package/src/flow/__tests__/FlowRoute.test.tsx +549 -4
  90. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
  91. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  92. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  93. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
  94. package/src/flow/actions/dataScopeFilter.ts +10 -1
  95. package/src/flow/actions/dateTimeFormat.tsx +2 -2
  96. package/src/flow/actions/index.ts +2 -0
  97. package/src/flow/actions/linkageRules.tsx +86 -11
  98. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  99. package/src/flow/actions/openView.tsx +21 -1
  100. package/src/flow/admin-shell/BaseLayoutModel.tsx +124 -0
  101. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
  102. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
  103. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -5
  104. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +177 -1
  105. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +20 -0
  106. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  107. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  108. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  109. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +436 -2
  110. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
  111. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  112. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  113. package/src/flow/common/Markdown/Display.tsx +14 -3
  114. package/src/flow/common/Markdown/Edit.tsx +19 -7
  115. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  116. package/src/flow/components/FieldAssignValueInput.tsx +91 -20
  117. package/src/flow/components/FlowRoute.tsx +134 -18
  118. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  119. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  120. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  121. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  122. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  123. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  124. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  125. package/src/flow/index.ts +1 -0
  126. package/src/flow/internal/components/Markdown/util.ts +2 -1
  127. package/src/flow/models/base/ActionModel.tsx +10 -8
  128. package/src/flow/models/base/ActionModelCore.tsx +5 -0
  129. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  130. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  131. package/src/flow/models/base/PageModel/PageTabModel.tsx +184 -3
  132. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  133. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  134. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  135. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +572 -1
  136. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -0
  137. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  138. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  139. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +34 -0
  140. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  141. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  142. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  143. package/src/flow/models/blocks/table/TableBlockModel.tsx +1 -1
  144. package/src/flow/models/blocks/table/TableColumnModel.tsx +6 -2
  145. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +51 -0
  146. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
  147. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +45 -13
  148. package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
  149. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  150. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
  151. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  152. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  153. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  154. package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
  155. package/src/flow/routeTransientInputArgs.ts +27 -0
  156. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
  157. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  158. package/src/flow-compat/routeTypes.ts +1 -0
  159. package/src/index.ts +6 -0
  160. package/src/locale/languageCodes.ts +2 -1
  161. package/src/nocobase-buildin-plugin/index.tsx +19 -1
  162. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  163. package/src/utils/index.tsx +1 -0
  164. package/src/utils/markdownSanitize.ts +88 -0
@@ -10,10 +10,13 @@
10
10
  import { type FlowEngine, useFlowContext, useFlowEngine } from '@nocobase/flow-engine';
11
11
  import React, { useEffect, useMemo, useRef, useState } from 'react';
12
12
  import { deviceType } from 'react-device-detect';
13
- import { useParams } from 'react-router-dom';
13
+ import { useLocation, useNavigate, useParams } from 'react-router-dom';
14
14
  import { useApp } from '../../hooks/useApp';
15
- import { NocoBaseDesktopRouteType } from '../../flow-compat';
16
- import { resolveAdminRouteRuntimeTarget } from '../admin-shell/admin-layout/resolveAdminRouteRuntimeTarget';
15
+ import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../flow-compat';
16
+ import {
17
+ resolveAdminRouteRuntimeTarget,
18
+ toRouterNavigationPath,
19
+ } from '../admin-shell/admin-layout/resolveAdminRouteRuntimeTarget';
17
20
  import { getAdminLayoutModel, type AdminLayoutModel } from '../admin-shell/admin-layout/AdminLayoutModel';
18
21
  import { getLayoutModel, type BaseLayoutModel } from '../admin-shell/BaseLayoutModel';
19
22
  import { useLayoutRoutePage } from '../admin-shell/useLayoutRoutePage';
@@ -21,6 +24,7 @@ import { AppNotFound } from '../../components';
21
24
  import { useKeepAlive } from '../../components/KeepAlive';
22
25
 
23
26
  type FlowRouteGuardState = {
27
+ pageUid?: string;
24
28
  pending: boolean;
25
29
  allowBridge: boolean;
26
30
  notFound: boolean;
@@ -35,6 +39,14 @@ type FlowRouteLayoutContext = {
35
39
  uid?: string;
36
40
  };
37
41
 
42
+ type FlowRouteRepositoryLike = {
43
+ refreshAccessible?: () => Promise<unknown>;
44
+ isAccessibleLoaded?: () => boolean;
45
+ ensureAccessibleLoaded?: () => Promise<unknown>;
46
+ getRouteBySchemaUid?: (schemaUid: string) => NocoBaseDesktopRoute | undefined;
47
+ listAccessible?: () => NocoBaseDesktopRoute[];
48
+ };
49
+
38
50
  export type FlowRouteProps = {
39
51
  pageUid?: string;
40
52
  active?: boolean;
@@ -70,6 +82,44 @@ const getDefaultLegacyPageBehavior = (
70
82
 
71
83
  const shouldRequireAccessibleRoute = (contextLayout?: FlowRouteLayoutContext) => contextLayout?.authCheck !== false;
72
84
 
85
+ const findAccessibleRouteByIdentity = (
86
+ routes: NocoBaseDesktopRoute[] | undefined,
87
+ pageUid: string,
88
+ ): NocoBaseDesktopRoute | undefined => {
89
+ if (!Array.isArray(routes)) {
90
+ return undefined;
91
+ }
92
+
93
+ for (const route of routes) {
94
+ if (
95
+ (route.type !== NocoBaseDesktopRouteType.group && route.schemaUid === pageUid) ||
96
+ (route.type === NocoBaseDesktopRouteType.group && route.id != null && String(route.id) === pageUid)
97
+ ) {
98
+ return route;
99
+ }
100
+
101
+ const matched = findAccessibleRouteByIdentity(route.children, pageUid);
102
+ if (matched) {
103
+ return matched;
104
+ }
105
+ }
106
+
107
+ return undefined;
108
+ };
109
+
110
+ const getAccessibleRouteByPageUid = (
111
+ routeRepository: FlowRouteRepositoryLike | undefined,
112
+ pageUid: string,
113
+ ): NocoBaseDesktopRoute | undefined => {
114
+ const routeBySchemaUid = routeRepository?.getRouteBySchemaUid?.(pageUid);
115
+
116
+ if (routeBySchemaUid && routeBySchemaUid.type !== NocoBaseDesktopRouteType.group) {
117
+ return routeBySchemaUid;
118
+ }
119
+
120
+ return findAccessibleRouteByIdentity(routeRepository?.listAccessible?.(), pageUid);
121
+ };
122
+
73
123
  const hasFlowModel = async (flowEngine: FlowEngine, pageUid: string) => {
74
124
  if (flowEngine.getModel(pageUid)) {
75
125
  return true;
@@ -197,11 +247,15 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
197
247
  );
198
248
  const legacyPageBehavior = legacyPageBehaviorProp || getDefaultLegacyPageBehavior(flowEngine, routeLayout);
199
249
  const app = useApp();
200
- const routeRepository = flowEngine.context.routeRepository;
250
+ const location = useLocation();
251
+ const navigate = useNavigate();
252
+ const routeRepository = flowEngine.context.routeRepository as FlowRouteRepositoryLike | undefined;
201
253
  const params = useParams();
202
254
  const pageUid = pageUidProp || params?.name;
255
+ const hasNestedRoutePath = typeof params?.tabUid !== 'undefined' || typeof params?.['*'] !== 'undefined';
203
256
  const skipRouteRepositoryCheck = !routeRepository;
204
257
  const [guardState, setGuardState] = useState<FlowRouteGuardState>({
258
+ pageUid: undefined,
205
259
  pending: true,
206
260
  allowBridge: false,
207
261
  notFound: false,
@@ -213,19 +267,24 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
213
267
  throw new Error('[NocoBase] FlowRoute requires pageUid or route.params.name.');
214
268
  }
215
269
 
270
+ useEffect(() => {
271
+ replaceTriggeredRef.current = false;
272
+ }, [location.hash, location.pathname, location.search, pageUid]);
273
+
216
274
  useEffect(() => {
217
275
  let active = true;
218
276
  const requestId = ++requestIdRef.current;
277
+ const requiresAccessibleRoute = shouldRequireAccessibleRoute(routeLayout);
219
278
 
220
279
  const run = async () => {
221
- setGuardState({ pending: true, allowBridge: false, notFound: false });
280
+ setGuardState({ pageUid, pending: true, allowBridge: false, notFound: false });
222
281
 
223
- if (!skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
282
+ if (requiresAccessibleRoute && !skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
224
283
  try {
225
284
  await routeRepository?.ensureAccessibleLoaded?.();
226
285
  } catch (_error) {
227
286
  if (active && requestId === requestIdRef.current) {
228
- setGuardState({ pending: false, allowBridge: true, notFound: false });
287
+ setGuardState({ pageUid, pending: false, allowBridge: true, notFound: false });
229
288
  }
230
289
  return;
231
290
  }
@@ -235,28 +294,67 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
235
294
  return;
236
295
  }
237
296
 
238
- const route = skipRouteRepositoryCheck ? undefined : routeRepository?.getRouteBySchemaUid?.(pageUid);
239
- if (!route && !skipRouteRepositoryCheck && shouldRequireAccessibleRoute(routeLayout)) {
240
- setGuardState({ pending: false, allowBridge: false, notFound: true });
297
+ const route =
298
+ skipRouteRepositoryCheck || !requiresAccessibleRoute
299
+ ? undefined
300
+ : getAccessibleRouteByPageUid(routeRepository, pageUid);
301
+ if (!route && !skipRouteRepositoryCheck && requiresAccessibleRoute) {
302
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
241
303
  return;
242
304
  }
243
305
 
244
306
  if (!route && legacyPageBehavior === 'notFound') {
245
307
  const flowModelExists = await hasFlowModel(flowEngine, pageUid);
246
308
  if (active && requestId === requestIdRef.current) {
247
- setGuardState({ pending: false, allowBridge: flowModelExists, notFound: !flowModelExists });
309
+ setGuardState({ pageUid, pending: false, allowBridge: flowModelExists, notFound: !flowModelExists });
310
+ }
311
+ return;
312
+ }
313
+
314
+ if (route?.type === NocoBaseDesktopRouteType.group) {
315
+ if (hasNestedRoutePath) {
316
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
317
+ return;
318
+ }
319
+
320
+ const target = resolveAdminRouteRuntimeTarget({
321
+ app,
322
+ route,
323
+ layout: routeLayout,
324
+ });
325
+
326
+ if (target.reason === 'emptyGroup') {
327
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: false });
328
+ return;
329
+ }
330
+
331
+ if (target.runtimePath) {
332
+ if (replaceTriggeredRef.current) {
333
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: false });
334
+ return;
335
+ }
336
+
337
+ replaceTriggeredRef.current = true;
338
+ if (target.navigationMode === 'document') {
339
+ window.location.replace(target.runtimePath);
340
+ return;
341
+ }
342
+ navigate(toRouterNavigationPath(target.runtimePath, app.router?.getBasename?.()), { replace: true });
343
+ return;
248
344
  }
345
+
346
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
249
347
  return;
250
348
  }
251
349
 
252
350
  if (route?.type === NocoBaseDesktopRouteType.page) {
253
351
  if (legacyPageBehavior === 'notFound') {
254
- setGuardState({ pending: false, allowBridge: false, notFound: true });
352
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
255
353
  return;
256
354
  }
257
355
 
258
356
  if (legacyPageBehavior === 'bridge') {
259
- setGuardState({ pending: false, allowBridge: true, notFound: false });
357
+ setGuardState({ pageUid, pending: false, allowBridge: true, notFound: false });
260
358
  return;
261
359
  }
262
360
 
@@ -279,14 +377,14 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
279
377
 
280
378
  if (target.reason === 'unsupportedV2Runtime') {
281
379
  if (active && requestId === requestIdRef.current) {
282
- setGuardState({ pending: false, allowBridge: false, notFound: true });
380
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
283
381
  }
284
382
  return;
285
383
  }
286
384
  }
287
385
 
288
386
  if (active && requestId === requestIdRef.current) {
289
- setGuardState({ pending: false, allowBridge: true, notFound: false });
387
+ setGuardState({ pageUid, pending: false, allowBridge: true, notFound: false });
290
388
  }
291
389
  };
292
390
 
@@ -295,10 +393,20 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
295
393
  return () => {
296
394
  active = false;
297
395
  };
298
- }, [app, flowEngine, legacyPageBehavior, pageUid, routeLayout, routeRepository, skipRouteRepositoryCheck]);
396
+ }, [
397
+ app,
398
+ flowEngine,
399
+ hasNestedRoutePath,
400
+ legacyPageBehavior,
401
+ navigate,
402
+ pageUid,
403
+ routeLayout,
404
+ routeRepository,
405
+ skipRouteRepositoryCheck,
406
+ ]);
299
407
 
300
408
  const content = useMemo(() => {
301
- if (guardState.pending) {
409
+ if (guardState.pageUid !== pageUid || guardState.pending) {
302
410
  return null;
303
411
  }
304
412
 
@@ -311,7 +419,15 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
311
419
  }
312
420
 
313
421
  return <BridgeFlowRoute pageUid={pageUid} active={active} getLayoutModel={getLayoutModel} />;
314
- }, [active, getLayoutModel, guardState.allowBridge, guardState.notFound, guardState.pending, pageUid]);
422
+ }, [
423
+ active,
424
+ getLayoutModel,
425
+ guardState.allowBridge,
426
+ guardState.notFound,
427
+ guardState.pageUid,
428
+ guardState.pending,
429
+ pageUid,
430
+ ]);
315
431
 
316
432
  return content;
317
433
  };
@@ -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
+ });
@@ -45,6 +45,7 @@ interface FilterGroupProps {
45
45
  value: Record<string, any>;
46
46
  /** 自定义筛选项组件 */
47
47
  FilterItem?: React.FC<FilterItemProps>;
48
+ disabled?: boolean;
48
49
  closeIcon?: ReactNode;
49
50
  /** 是否显示边框 */
50
51
  showBorder?: boolean;
@@ -89,6 +90,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
89
90
  const {
90
91
  value = { logic: '$and', items: [] },
91
92
  FilterItem,
93
+ disabled = false,
92
94
  showBorder = false,
93
95
  onRemove,
94
96
  onChange,
@@ -120,11 +122,17 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
120
122
  };
121
123
 
122
124
  const handleLogicChange = (newLogic: '$and' | '$or') => {
125
+ if (disabled) {
126
+ return;
127
+ }
123
128
  value.logic = newLogic;
124
129
  onChange?.(value);
125
130
  };
126
131
 
127
132
  const handleAddCondition = () => {
133
+ if (disabled) {
134
+ return;
135
+ }
128
136
  items.push({
129
137
  path: '',
130
138
  operator: '',
@@ -134,6 +142,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
134
142
  };
135
143
 
136
144
  const handleAddConditionGroup = () => {
145
+ if (disabled) {
146
+ return;
147
+ }
137
148
  items.push({
138
149
  logic: '$and',
139
150
  items: [],
@@ -142,6 +153,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
142
153
  };
143
154
 
144
155
  const handleRemoveItem = (index: number) => {
156
+ if (disabled) {
157
+ return;
158
+ }
145
159
  items.splice(index, 1);
146
160
  onChange?.(value);
147
161
  };
@@ -160,11 +174,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
160
174
  <button
161
175
  type="button"
162
176
  aria-label="icon-close"
177
+ disabled={disabled}
163
178
  style={{
164
179
  position: 'absolute',
165
180
  right: 10,
166
181
  top: 10,
167
- cursor: 'pointer',
182
+ cursor: disabled ? 'not-allowed' : 'pointer',
168
183
  background: 'transparent',
169
184
  border: 0,
170
185
  padding: 0,
@@ -182,6 +197,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
182
197
  data-testid="filter-select-all-or-any"
183
198
  style={{ width: 'auto' }}
184
199
  value={logic}
200
+ disabled={disabled}
185
201
  onChange={handleLogicChange}
186
202
  >
187
203
  <Select.Option value="$and">{t('All')}</Select.Option>
@@ -200,6 +216,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
200
216
  key={getFilterItemKey(item)}
201
217
  value={item}
202
218
  FilterItem={FilterItem}
219
+ disabled={disabled}
203
220
  showBorder={true}
204
221
  onRemove={() => handleRemoveItem(index)}
205
222
  onChange={(v) => {
@@ -221,11 +238,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
221
238
  <button
222
239
  type="button"
223
240
  aria-label="icon-close"
241
+ disabled={disabled}
224
242
  style={{
225
243
  marginLeft: 8,
226
244
  marginRight: 8,
227
245
  flex: '0 0 auto',
228
- cursor: 'pointer',
246
+ cursor: disabled ? 'not-allowed' : 'pointer',
229
247
  background: 'transparent',
230
248
  border: 0,
231
249
  padding: 0,
@@ -251,11 +269,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
251
269
  <button
252
270
  type="button"
253
271
  aria-label="icon-close"
272
+ disabled={disabled}
254
273
  style={{
255
274
  marginLeft: 8,
256
275
  marginRight: 8,
257
276
  flex: '0 0 auto',
258
- cursor: 'pointer',
277
+ cursor: disabled ? 'not-allowed' : 'pointer',
259
278
  background: 'transparent',
260
279
  border: 0,
261
280
  padding: 0,
@@ -277,11 +296,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
277
296
  <button
278
297
  type="button"
279
298
  aria-label="icon-close"
299
+ disabled={disabled}
280
300
  style={{
281
301
  marginLeft: 8,
282
302
  marginRight: 8,
283
303
  flex: '0 0 auto',
284
- cursor: 'pointer',
304
+ cursor: disabled ? 'not-allowed' : 'pointer',
285
305
  background: 'transparent',
286
306
  border: 0,
287
307
  padding: 0,
@@ -298,7 +318,14 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
298
318
  </div>
299
319
 
300
320
  <Space size={16} style={{ marginTop: 8, marginBottom: 8 }}>
301
- <Button style={{ padding: 0 }} type="link" size="small" icon={<PlusOutlined />} onClick={handleAddCondition}>
321
+ <Button
322
+ style={{ padding: 0 }}
323
+ type="link"
324
+ size="small"
325
+ icon={<PlusOutlined />}
326
+ onClick={handleAddCondition}
327
+ disabled={disabled}
328
+ >
302
329
  {t('Add condition')}
303
330
  </Button>
304
331
  <Button
@@ -307,6 +334,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
307
334
  size="small"
308
335
  icon={<PlusOutlined />}
309
336
  onClick={handleAddConditionGroup}
337
+ disabled={disabled}
310
338
  >
311
339
  {t('Add condition group')}
312
340
  </Button>