@nocobase/client-v2 2.1.11 → 2.2.0-alpha.2

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 (137) hide show
  1. package/es/PluginSettingsManager.d.ts +33 -0
  2. package/es/RouteRepository.d.ts +21 -6
  3. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  4. package/es/components/category-tabs/index.d.ts +9 -0
  5. package/es/components/form/TypedVariableInput.d.ts +22 -4
  6. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  7. package/es/components/form/filter/index.d.ts +2 -0
  8. package/es/components/index.d.ts +2 -0
  9. package/es/flow/actions/afterSuccess.d.ts +8 -1
  10. package/es/flow/actions/index.d.ts +1 -1
  11. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  12. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  13. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  14. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  15. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  16. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  17. package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
  18. package/es/flow/index.d.ts +1 -0
  19. package/es/flow/models/base/GridModel.d.ts +1 -1
  20. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  21. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  22. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  23. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  24. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  25. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  26. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  27. package/es/index.d.ts +2 -1
  28. package/es/index.mjs +181 -131
  29. package/lib/index.js +186 -136
  30. package/package.json +7 -7
  31. package/src/PluginSettingsManager.ts +53 -0
  32. package/src/RouteRepository.ts +126 -24
  33. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  34. package/src/__tests__/RouteRepository.test.ts +216 -0
  35. package/src/__tests__/browserChecker.test.ts +61 -0
  36. package/src/__tests__/exports.test.ts +16 -0
  37. package/src/__tests__/plugin-manager.test.tsx +44 -2
  38. package/src/__tests__/settings-center.test.tsx +40 -1
  39. package/src/collection-manager/field-configure.ts +1 -1
  40. package/src/collection-manager/interfaces/id.ts +1 -1
  41. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  42. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  43. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  44. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  45. package/src/collection-manager/interfaces/obo.tsx +2 -2
  46. package/src/collection-manager/interfaces/oho.tsx +2 -2
  47. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  48. package/src/collection-manager/interfaces/uuid.ts +1 -1
  49. package/src/components/README.md +7 -1
  50. package/src/components/README.zh-CN.md +6 -1
  51. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  52. package/src/components/category-tabs/index.ts +10 -0
  53. package/src/components/form/TypedVariableInput.tsx +416 -93
  54. package/src/components/form/__tests__/TypedVariableInput.test.tsx +266 -9
  55. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  56. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  57. package/src/components/form/filter/index.ts +2 -0
  58. package/src/components/index.ts +2 -0
  59. package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
  60. package/src/flow/__tests__/getKey.test.ts +7 -0
  61. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  62. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  63. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +104 -0
  64. package/src/flow/actions/afterSuccess.tsx +142 -3
  65. package/src/flow/actions/customVariable.tsx +1 -2
  66. package/src/flow/actions/index.ts +1 -1
  67. package/src/flow/actions/linkageRules.tsx +2 -7
  68. package/src/flow/actions/openView.tsx +38 -4
  69. package/src/flow/actions/runjs.tsx +2 -14
  70. package/src/flow/admin-shell/BaseLayoutModel.tsx +25 -3
  71. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +5 -2
  72. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +13 -0
  73. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +34 -10
  74. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
  75. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
  76. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  77. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  78. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  79. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  80. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  81. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  82. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  83. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +82 -0
  84. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
  85. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  86. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  87. package/src/flow/components/DefaultValue.tsx +1 -2
  88. package/src/flow/components/FieldAssignValueInput.tsx +10 -5
  89. package/src/flow/components/FlowRoute.tsx +56 -11
  90. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  91. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  92. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  93. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  94. package/src/flow/index.ts +1 -0
  95. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  96. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  97. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  98. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  99. package/src/flow/models/base/ActionModel.tsx +12 -1
  100. package/src/flow/models/base/GridModel.tsx +38 -7
  101. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +0 -4
  102. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  103. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  104. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  105. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -2
  106. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  107. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  108. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  109. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  110. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  111. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  112. package/src/flow/models/blocks/form/value-runtime/rules.ts +2 -2
  113. package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
  114. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  115. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +36 -13
  116. package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
  117. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  118. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  119. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  120. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  121. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  122. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  123. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  124. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  125. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  126. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  127. package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
  128. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  129. package/src/flow-compat/Popover.tsx +43 -4
  130. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  131. package/src/index.ts +8 -1
  132. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  133. package/src/nocobase-buildin-plugin/index.tsx +0 -2
  134. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  135. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  136. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  137. package/src/settings-center/utils.tsx +0 -6
@@ -8,12 +8,20 @@
8
8
  */
9
9
 
10
10
  import React from 'react';
11
- import { render, screen } from '@testing-library/react';
11
+ import { fireEvent, render, screen } from '@testing-library/react';
12
+ import { MemoryRouter } from 'react-router-dom';
12
13
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
13
14
 
14
- const { allowMock, flowModelRendererSpy } = vi.hoisted(() => {
15
+ const { allowMock, appMock, flowModelRendererSpy } = vi.hoisted(() => {
15
16
  return {
16
17
  allowMock: vi.fn(),
18
+ appMock: {
19
+ current: {
20
+ router: {
21
+ getBasename: () => '/nocobase/v',
22
+ },
23
+ },
24
+ },
17
25
  flowModelRendererSpy: vi.fn(),
18
26
  };
19
27
  });
@@ -32,6 +40,12 @@ vi.mock('../HelpLite', () => {
32
40
  };
33
41
  });
34
42
 
43
+ vi.mock('../../../../hooks/useApp', () => {
44
+ return {
45
+ useApp: () => appMock.current,
46
+ };
47
+ });
48
+
35
49
  vi.mock('@nocobase/flow-engine', async (importOriginal) => {
36
50
  const actual = await importOriginal<typeof import('@nocobase/flow-engine')>();
37
51
  return {
@@ -63,10 +77,19 @@ const createAction = (options: Record<string, any>) => {
63
77
  } as any;
64
78
  };
65
79
 
80
+ const renderSettingsLabel = (label: React.ReactNode, pathname: string) => {
81
+ return render(<MemoryRouter initialEntries={[pathname]}>{label}</MemoryRouter>);
82
+ };
83
+
66
84
  describe('TopbarActionsBar helpers', () => {
67
85
  beforeEach(() => {
68
86
  allowMock.mockReset();
69
87
  flowModelRendererSpy.mockClear();
88
+ appMock.current = {
89
+ router: {
90
+ getBasename: () => '/nocobase/v',
91
+ },
92
+ };
70
93
  });
71
94
 
72
95
  afterEach(() => {
@@ -149,6 +172,147 @@ describe('TopbarActionsBar helpers', () => {
149
172
  });
150
173
  });
151
174
 
175
+ it('should open regular admin settings in a new tab outside admin runtime', () => {
176
+ const items = getTopbarPluginSettingsItems({
177
+ canManagePlugins: false,
178
+ t: (key) => key,
179
+ settings: [
180
+ {
181
+ key: 'system-settings',
182
+ name: 'system-settings',
183
+ title: 'System settings',
184
+ path: '/admin/settings/system-settings',
185
+ icon: null,
186
+ componentLoader: async () => null,
187
+ },
188
+ ] as any,
189
+ });
190
+
191
+ renderSettingsLabel((items as any[])[0].label, '/sales/p1');
192
+
193
+ const link = screen.getByRole('link', { name: 'System settings' });
194
+ expect(link).toHaveAttribute('href', '/nocobase/v/admin/settings/system-settings');
195
+ expect(link).toHaveAttribute('target', '_blank');
196
+ expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
197
+ expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
198
+ });
199
+
200
+ it('should open plugin manager in a new tab outside admin runtime', () => {
201
+ const items = getTopbarPluginSettingsItems({
202
+ canManagePlugins: true,
203
+ t: (key) => key,
204
+ settings: [
205
+ {
206
+ key: 'plugin-manager',
207
+ name: 'plugin-manager',
208
+ title: 'Plugin manager',
209
+ path: '/admin/settings/plugin-manager',
210
+ icon: null,
211
+ componentLoader: async () => null,
212
+ },
213
+ ] as any,
214
+ });
215
+
216
+ renderSettingsLabel((items as any[])[0].label, '/sales/p1');
217
+
218
+ const link = screen.getByRole('link', { name: 'Plugin manager' });
219
+ expect(link).toHaveAttribute('href', '/nocobase/v/admin/settings/plugin-manager');
220
+ expect(link).toHaveAttribute('target', '_blank');
221
+ expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
222
+ expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
223
+ });
224
+
225
+ it('should keep regular admin settings as SPA links inside admin runtime', () => {
226
+ const items = getTopbarPluginSettingsItems({
227
+ canManagePlugins: false,
228
+ t: (key) => key,
229
+ settings: [
230
+ {
231
+ key: 'routes',
232
+ name: 'routes',
233
+ title: 'Routes',
234
+ path: '/admin/settings/routes',
235
+ icon: null,
236
+ componentLoader: async () => null,
237
+ },
238
+ ] as any,
239
+ });
240
+
241
+ renderSettingsLabel((items as any[])[0].label, '/admin/settings/routes');
242
+
243
+ const link = screen.getByRole('link', { name: 'Routes' });
244
+ expect(link).toHaveAttribute('href', '/admin/settings/routes');
245
+ expect(link).not.toHaveAttribute('target', '_blank');
246
+ });
247
+
248
+ it('should not treat admin-like paths as admin runtime', () => {
249
+ const items = getTopbarPluginSettingsItems({
250
+ canManagePlugins: false,
251
+ t: (key) => key,
252
+ settings: [
253
+ {
254
+ key: 'system-settings',
255
+ name: 'system-settings',
256
+ title: 'System settings',
257
+ path: '/admin/settings/system-settings',
258
+ icon: null,
259
+ componentLoader: async () => null,
260
+ },
261
+ ] as any,
262
+ });
263
+
264
+ renderSettingsLabel((items as any[])[0].label, '/admin2/foo');
265
+
266
+ const link = screen.getByRole('link', { name: 'System settings' });
267
+ expect(link).toHaveAttribute('href', '/nocobase/v/admin/settings/system-settings');
268
+ expect(link).toHaveAttribute('target', '_blank');
269
+ });
270
+
271
+ it('should not duplicate the basename when building new-tab settings hrefs', () => {
272
+ const items = getTopbarPluginSettingsItems({
273
+ canManagePlugins: false,
274
+ t: (key) => key,
275
+ settings: [
276
+ {
277
+ key: 'system-settings',
278
+ name: 'system-settings',
279
+ title: 'System settings',
280
+ path: '/nocobase/v/admin/settings/system-settings',
281
+ icon: null,
282
+ componentLoader: async () => null,
283
+ },
284
+ ] as any,
285
+ });
286
+
287
+ renderSettingsLabel((items as any[])[0].label, '/sales/p1');
288
+
289
+ const link = screen.getByRole('link', { name: 'System settings' });
290
+ expect(link).toHaveAttribute('href', '/nocobase/v/admin/settings/system-settings');
291
+ });
292
+
293
+ it('should keep external settings opening in a new tab', () => {
294
+ const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
295
+ const items = getTopbarPluginSettingsItems({
296
+ canManagePlugins: false,
297
+ t: (key) => key,
298
+ settings: [
299
+ {
300
+ key: 'docs',
301
+ name: 'docs',
302
+ title: 'Docs',
303
+ link: 'https://www.nocobase.com/docs',
304
+ icon: null,
305
+ componentLoader: async () => null,
306
+ },
307
+ ] as any,
308
+ });
309
+
310
+ renderSettingsLabel((items as any[])[0].label, '/sales/p1');
311
+ fireEvent.click(screen.getByText('Docs'));
312
+
313
+ expect(openSpy).toHaveBeenCalledWith('https://www.nocobase.com/docs', '_blank', 'noopener,noreferrer');
314
+ });
315
+
152
316
  it('should return empty dropdown items when plugin manager and settings are both unavailable', () => {
153
317
  const items = getTopbarPluginSettingsItems({
154
318
  canManagePlugins: false,
@@ -52,6 +52,26 @@ describe('resolveAdminRouteRuntimeTarget', () => {
52
52
  });
53
53
  });
54
54
 
55
+ it('should resolve flowPage under a custom admin layout route path', () => {
56
+ expect(
57
+ resolveAdminRouteRuntimeTarget({
58
+ app,
59
+ layout: {
60
+ routePath: '/admin2',
61
+ },
62
+ route: {
63
+ type: NocoBaseDesktopRouteType.flowPage,
64
+ schemaUid: 'flow-page-1',
65
+ },
66
+ }),
67
+ ).toEqual({
68
+ runtimePath: '/nocobase/v2/admin2/flow-page-1',
69
+ navigationMode: 'spa',
70
+ isLegacy: false,
71
+ reason: 'ok',
72
+ });
73
+ });
74
+
55
75
  it('should mark page unsupported in v2 admin runtime', () => {
56
76
  expect(
57
77
  resolveAdminRouteRuntimeTarget({
@@ -10,6 +10,7 @@
10
10
  import { getModernClientPrefix, getV2EffectiveBasePath } from '../../../authRedirect';
11
11
  import type { BaseApplication } from '../../../BaseApplication';
12
12
  import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../../flow-compat';
13
+ import type { LayoutDefinition } from '../../../layout-manager/types';
13
14
 
14
15
  export type AdminRouteNavigationMode = 'spa' | 'document';
15
16
  export type AdminRouteRuntimeTargetReason =
@@ -32,6 +33,8 @@ type LocationLike = {
32
33
  hash?: string;
33
34
  };
34
35
 
36
+ export type AdminLayoutRoutePathLike = Pick<LayoutDefinition, 'routePath'>;
37
+
35
38
  type ResolveAdminRouteRuntimeTargetOptions = {
36
39
  app: Pick<BaseApplication<any>, 'getPublicPath'> & {
37
40
  router?: {
@@ -39,12 +42,14 @@ type ResolveAdminRouteRuntimeTargetOptions = {
39
42
  };
40
43
  };
41
44
  route?: NocoBaseDesktopRoute;
45
+ layout?: AdminLayoutRoutePathLike;
42
46
  location?: LocationLike;
43
47
  preserveLocationState?: boolean;
44
48
  log?: (message?: any, ...optionalParams: any[]) => void;
45
49
  };
46
50
 
47
51
  const LOG_PREFIX = '[NocoBase] Admin route runtime target:';
52
+ export const DEFAULT_ADMIN_LAYOUT_ROUTE_PATH = '/admin';
48
53
 
49
54
  const EMPTY_TARGET: AdminRouteRuntimeTarget = {
50
55
  runtimePath: null,
@@ -61,6 +66,25 @@ function normalizeRootRelativePath(pathname: string) {
61
66
  return normalized;
62
67
  }
63
68
 
69
+ export function getAdminLayoutRoutePath(layout?: AdminLayoutRoutePathLike | null) {
70
+ const routePath = typeof layout?.routePath === 'string' ? layout.routePath.trim() : '';
71
+ return routePath.startsWith('/') ? normalizeRootRelativePath(routePath) : DEFAULT_ADMIN_LAYOUT_ROUTE_PATH;
72
+ }
73
+
74
+ export function joinAdminLayoutRoutePath(
75
+ layout: AdminLayoutRoutePathLike | null | undefined,
76
+ pathname?: string | number | null,
77
+ ) {
78
+ const layoutRoutePath = getAdminLayoutRoutePath(layout);
79
+ const routePath = pathname == null ? '' : String(pathname).replace(/^\/+/, '');
80
+
81
+ if (!routePath) {
82
+ return layoutRoutePath;
83
+ }
84
+
85
+ return normalizeRootRelativePath(`${layoutRoutePath}/${routePath}`);
86
+ }
87
+
64
88
  function normalizePublicPath(value = '/') {
65
89
  const normalized = normalizeRootRelativePath(value || '/');
66
90
  return normalized.endsWith('/') ? normalized : `${normalized}/`;
@@ -104,8 +128,12 @@ function joinRootRelativePath(basePath: string, pathname: string) {
104
128
  return normalizeRootRelativePath(`${normalizedBasePath}${normalizedPathname.slice(1)}`);
105
129
  }
106
130
 
107
- function getV2AdminPath(app: ResolveAdminRouteRuntimeTargetOptions['app'], schemaUid: string) {
108
- return joinRootRelativePath(getV2EffectiveBasePath(app), `/admin/${schemaUid}`);
131
+ function getV2AdminPath(
132
+ app: ResolveAdminRouteRuntimeTargetOptions['app'],
133
+ schemaUid: string,
134
+ layout?: AdminLayoutRoutePathLike,
135
+ ) {
136
+ return joinRootRelativePath(getV2EffectiveBasePath(app), joinAdminLayoutRoutePath(layout, schemaUid));
109
137
  }
110
138
 
111
139
  function appendLocationState(pathname: string, location?: LocationLike) {
@@ -212,7 +240,7 @@ function resolvePageRuntimeTarget(
212
240
 
213
241
  if (route.type === NocoBaseDesktopRouteType.flowPage) {
214
242
  return {
215
- runtimePath: getV2AdminPath(app, route.schemaUid),
243
+ runtimePath: getV2AdminPath(app, route.schemaUid, options.layout),
216
244
  navigationMode: 'spa' as const,
217
245
  isLegacy: false,
218
246
  reason: 'ok' as const,
@@ -229,8 +257,8 @@ function resolvePageRuntimeTarget(
229
257
  return {
230
258
  runtimePath:
231
259
  preserveLocationState && location
232
- ? appendLocationState(getV2AdminPath(app, route.schemaUid), location)
233
- : getV2AdminPath(app, route.schemaUid),
260
+ ? appendLocationState(getV2AdminPath(app, route.schemaUid, options.layout), location)
261
+ : getV2AdminPath(app, route.schemaUid, options.layout),
234
262
  navigationMode: 'spa' as const,
235
263
  isLegacy: false,
236
264
  reason: 'ok' as const,
@@ -18,7 +18,6 @@ import {
18
18
  parseValueToPath,
19
19
  isRunJSValue,
20
20
  normalizeRunJSValue,
21
- runjsWithSafeGlobals,
22
21
  useFlowContext,
23
22
  extractPropertyPath,
24
23
  FlowModel,
@@ -240,7 +239,7 @@ export const DefaultValue = connect((props: Props) => {
240
239
  if (isRunJSValue(out)) {
241
240
  try {
242
241
  const { code, version } = normalizeRunJSValue(out);
243
- const ret = await runjsWithSafeGlobals(model?.context, code, { version });
242
+ const ret = await model?.context?.runjs(code, undefined, { version });
244
243
  out = ret?.success ? ret.value : undefined;
245
244
  } catch {
246
245
  out = undefined;
@@ -341,6 +341,8 @@ interface Props {
341
341
  * 默认 false,保持历史行为。
342
342
  */
343
343
  enableDateVariableAsConstant?: boolean;
344
+ /** 是否允许在变量选择器中使用 RunJS。默认 true,保持历史行为。 */
345
+ allowRunJS?: boolean;
344
346
  maxAssociationFieldDepth?: number;
345
347
  }
346
348
 
@@ -712,6 +714,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
712
714
  preferFormItemFieldModel,
713
715
  associationFieldNamesOverride,
714
716
  enableDateVariableAsConstant = false,
717
+ allowRunJS = true,
715
718
  maxAssociationFieldDepth = 2,
716
719
  }) => {
717
720
  const flowCtx = useFlowContext<FlowModelContext>();
@@ -1417,11 +1420,13 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1417
1420
  render: ConstantEditor,
1418
1421
  },
1419
1422
  { title: tExpr('Null'), name: 'null', type: 'object', paths: ['null'], render: NullComponent },
1420
- { title: tExpr('RunJS'), name: 'runjs', type: 'object', paths: ['runjs'], render: RunJSComponent },
1423
+ ...(allowRunJS
1424
+ ? [{ title: tExpr('RunJS'), name: 'runjs', type: 'object', paths: ['runjs'], render: RunJSComponent }]
1425
+ : []),
1421
1426
  ...limitedBase,
1422
1427
  ];
1423
1428
  };
1424
- }, [flowCtx, ConstantEditor, NullComponent, RunJSComponent, maxAssociationFieldDepth]);
1429
+ }, [flowCtx, ConstantEditor, NullComponent, RunJSComponent, allowRunJS, maxAssociationFieldDepth]);
1425
1430
 
1426
1431
  const displayValue = React.useMemo(() => {
1427
1432
  if (!useDateVariableConstant) {
@@ -1465,7 +1470,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1465
1470
  const firstPath = meta?.paths?.[0];
1466
1471
  if (firstPath === 'constant') return ConstantEditor;
1467
1472
  if (firstPath === 'null') return NullComponent;
1468
- if (firstPath === 'runjs') return RunJSComponent;
1473
+ if (allowRunJS && firstPath === 'runjs') return RunJSComponent;
1469
1474
  return null;
1470
1475
  },
1471
1476
  resolveValueFromPath: (item) => {
@@ -1474,12 +1479,12 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1474
1479
  return useDateVariableConstant ? { type: 'today' } : '';
1475
1480
  }
1476
1481
  if (firstPath === 'null') return null;
1477
- if (firstPath === 'runjs') return { code: '', version: 'v2' };
1482
+ if (allowRunJS && firstPath === 'runjs') return { code: '', version: 'v2' };
1478
1483
  return undefined;
1479
1484
  },
1480
1485
  resolvePathFromValue: (currentValue) => {
1481
1486
  if (currentValue === null) return ['null'];
1482
- if (isRunJSValue(currentValue)) return ['runjs'];
1487
+ if (allowRunJS && isRunJSValue(currentValue)) return ['runjs'];
1483
1488
  if (useDateVariableConstant && isCtxDateExpression(currentValue)) {
1484
1489
  return ['constant'];
1485
1490
  }
@@ -28,6 +28,13 @@ type FlowRouteGuardState = {
28
28
 
29
29
  export type LegacyPageBehavior = 'redirect' | 'notFound' | 'bridge';
30
30
 
31
+ type FlowRouteLayoutContext = {
32
+ authCheck?: boolean;
33
+ routePath?: string;
34
+ routeName?: string;
35
+ uid?: string;
36
+ };
37
+
31
38
  export type FlowRouteProps = {
32
39
  pageUid?: string;
33
40
  active?: boolean;
@@ -38,7 +45,7 @@ export type FlowRouteProps = {
38
45
  const getDefaultAdminLayoutModel = (flowEngine: FlowEngine) =>
39
46
  getAdminLayoutModel<AdminLayoutModel>(flowEngine, { required: true });
40
47
 
41
- const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?: any) => {
48
+ const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?: FlowRouteLayoutContext) => {
42
49
  const layout = contextLayout || flowEngine.context.layout;
43
50
 
44
51
  if (layout?.uid) {
@@ -48,7 +55,10 @@ const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?: any) => {
48
55
  return getDefaultAdminLayoutModel(flowEngine);
49
56
  };
50
57
 
51
- const getDefaultLegacyPageBehavior = (flowEngine: FlowEngine, contextLayout?: any): LegacyPageBehavior => {
58
+ const getDefaultLegacyPageBehavior = (
59
+ flowEngine: FlowEngine,
60
+ contextLayout?: FlowRouteLayoutContext,
61
+ ): LegacyPageBehavior => {
52
62
  const layout = contextLayout || flowEngine.context.layout;
53
63
 
54
64
  if (layout?.routeName && layout.routeName !== 'admin') {
@@ -58,6 +68,8 @@ const getDefaultLegacyPageBehavior = (flowEngine: FlowEngine, contextLayout?: an
58
68
  return 'redirect';
59
69
  };
60
70
 
71
+ const shouldRequireAccessibleRoute = (contextLayout?: FlowRouteLayoutContext) => contextLayout?.authCheck !== false;
72
+
61
73
  const hasFlowModel = async (flowEngine: FlowEngine, pageUid: string) => {
62
74
  if (flowEngine.getModel(pageUid)) {
63
75
  return true;
@@ -148,18 +160,46 @@ const BridgeFlowRoute = ({
148
160
  * @throws {Error} 当缺少 `route.params.name` 时抛出异常
149
161
  */
150
162
  const FlowRoute = (props: FlowRouteProps = {}) => {
163
+ const {
164
+ active,
165
+ getLayoutModel: getLayoutModelProp,
166
+ legacyPageBehavior: legacyPageBehaviorProp,
167
+ pageUid: pageUidProp,
168
+ } = props;
151
169
  const flowEngine = useFlowEngine();
152
- const flowContext = useFlowContext<any>();
170
+ const flowContext = useFlowContext<{ layout?: FlowRouteLayoutContext }>();
153
171
  const contextLayout = flowContext?.layout;
172
+ const propsLayoutModel = useMemo(() => getLayoutModelProp?.(flowEngine), [flowEngine, getLayoutModelProp]);
173
+ const rawRouteLayout = contextLayout || propsLayoutModel?.layout;
174
+ const routeLayoutUid = rawRouteLayout?.uid;
175
+ const routeLayoutRouteName = rawRouteLayout?.routeName;
176
+ const routeLayoutRoutePath = rawRouteLayout?.routePath;
177
+ const routeLayoutAuthCheck = rawRouteLayout?.authCheck;
178
+ const routeLayout = useMemo(() => {
179
+ if (
180
+ !routeLayoutUid &&
181
+ !routeLayoutRouteName &&
182
+ !routeLayoutRoutePath &&
183
+ typeof routeLayoutAuthCheck === 'undefined'
184
+ ) {
185
+ return undefined;
186
+ }
187
+ return {
188
+ authCheck: routeLayoutAuthCheck,
189
+ routeName: routeLayoutRouteName,
190
+ routePath: routeLayoutRoutePath,
191
+ uid: routeLayoutUid,
192
+ };
193
+ }, [routeLayoutAuthCheck, routeLayoutRouteName, routeLayoutRoutePath, routeLayoutUid]);
154
194
  const getLayoutModel = useMemo(
155
- () => props.getLayoutModel || ((engine: FlowEngine) => getDefaultLayoutModel(engine, contextLayout)),
156
- [contextLayout, props.getLayoutModel],
195
+ () => getLayoutModelProp || ((engine: FlowEngine) => getDefaultLayoutModel(engine, routeLayout)),
196
+ [getLayoutModelProp, routeLayout],
157
197
  );
158
- const legacyPageBehavior = props.legacyPageBehavior || getDefaultLegacyPageBehavior(flowEngine, contextLayout);
198
+ const legacyPageBehavior = legacyPageBehaviorProp || getDefaultLegacyPageBehavior(flowEngine, routeLayout);
159
199
  const app = useApp();
160
200
  const routeRepository = flowEngine.context.routeRepository;
161
201
  const params = useParams();
162
- const pageUid = props.pageUid || params?.name;
202
+ const pageUid = pageUidProp || params?.name;
163
203
  const skipRouteRepositoryCheck = !routeRepository;
164
204
  const [guardState, setGuardState] = useState<FlowRouteGuardState>({
165
205
  pending: true,
@@ -196,6 +236,11 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
196
236
  }
197
237
 
198
238
  const route = skipRouteRepositoryCheck ? undefined : routeRepository?.getRouteBySchemaUid?.(pageUid);
239
+ if (!route && !skipRouteRepositoryCheck && shouldRequireAccessibleRoute(routeLayout)) {
240
+ setGuardState({ pending: false, allowBridge: false, notFound: true });
241
+ return;
242
+ }
243
+
199
244
  if (!route && legacyPageBehavior === 'notFound') {
200
245
  const flowModelExists = await hasFlowModel(flowEngine, pageUid);
201
246
  if (active && requestId === requestIdRef.current) {
@@ -245,12 +290,12 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
245
290
  }
246
291
  };
247
292
 
248
- void run();
293
+ run();
249
294
 
250
295
  return () => {
251
296
  active = false;
252
297
  };
253
- }, [app, flowEngine, legacyPageBehavior, pageUid, routeRepository, skipRouteRepositoryCheck]);
298
+ }, [app, flowEngine, legacyPageBehavior, pageUid, routeLayout, routeRepository, skipRouteRepositoryCheck]);
254
299
 
255
300
  const content = useMemo(() => {
256
301
  if (guardState.pending) {
@@ -265,8 +310,8 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
265
310
  return null;
266
311
  }
267
312
 
268
- return <BridgeFlowRoute pageUid={pageUid} active={props.active} getLayoutModel={getLayoutModel} />;
269
- }, [getLayoutModel, guardState.allowBridge, guardState.notFound, guardState.pending, pageUid, props.active]);
313
+ return <BridgeFlowRoute pageUid={pageUid} active={active} getLayoutModel={getLayoutModel} />;
314
+ }, [active, getLayoutModel, guardState.allowBridge, guardState.notFound, guardState.pending, pageUid]);
270
315
 
271
316
  return content;
272
317
  };
@@ -20,8 +20,6 @@ import {
20
20
  FlowEngineProvider,
21
21
  FlowModelRenderer,
22
22
  ElementProxy,
23
- createSafeWindow,
24
- createSafeDocument,
25
23
  createViewScopedEngine,
26
24
  } from '@nocobase/flow-engine';
27
25
  import { JSEditableFieldModel } from '../../../models/fields/JSEditableFieldModel';
@@ -167,11 +165,7 @@ describe('useCodeRunner (beforeRender)', () => {
167
165
  const code = ctx?.inputArgs?.preview?.code || '';
168
166
  ctx.onRefReady(ctx.ref, async (el) => {
169
167
  ctx.defineProperty('element', { get: () => new ElementProxy(el as any) });
170
- await ctx.runjs(
171
- code,
172
- { window: createSafeWindow(), document: createSafeDocument() },
173
- { preprocessTemplates: true },
174
- );
168
+ await ctx.runjs(code, undefined, { preprocessTemplates: true });
175
169
  });
176
170
  },
177
171
  },
@@ -286,16 +280,7 @@ return currentUsername;
286
280
  const code = ctx?.inputArgs?.preview?.code || '';
287
281
  ctx.onRefReady(ctx.ref, async (el) => {
288
282
  ctx.defineProperty('element', { get: () => new ElementProxy(el as any) });
289
- const navigator = { userAgent: 'test' } as any;
290
- await ctx.runjs(
291
- code,
292
- {
293
- window: createSafeWindow({ navigator }),
294
- document: createSafeDocument(),
295
- navigator,
296
- },
297
- { preprocessTemplates: true },
298
- );
283
+ await ctx.runjs(code, undefined, { preprocessTemplates: true });
299
284
  });
300
285
  },
301
286
  },
@@ -9,14 +9,7 @@
9
9
 
10
10
  import { useCallback, useRef, useState } from 'react';
11
11
  import { parseErrorLineColumn, WRAPPER_PRELUDE_LINES } from '../errorHelpers';
12
- import {
13
- FlowModelContext,
14
- JSRunner,
15
- createSafeWindow,
16
- createSafeDocument,
17
- createSafeNavigator,
18
- prepareRunJsCode,
19
- } from '@nocobase/flow-engine';
12
+ import { FlowModelContext, JSRunner, prepareRunJsCode } from '@nocobase/flow-engine';
20
13
 
21
14
  export type RunLog = { level: 'log' | 'info' | 'warn' | 'error'; msg: string; line?: number; column?: number };
22
15
  type RunResult = Awaited<ReturnType<JSRunner['run']>>;
@@ -286,12 +279,7 @@ export function useCodeRunner(hostCtx: FlowModelContext, version = 'v1') {
286
279
  const eventName = typeof onDef === 'string' ? onDef : onDef?.eventName;
287
280
  if (!flow) {
288
281
  // 无可用流程(典型场景:联动规则里的 RunJS 预览),直接在当前上下文执行代码
289
- const navigator = createSafeNavigator();
290
- await directRunCtx.runjs(
291
- code,
292
- { window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
293
- { version },
294
- );
282
+ await directRunCtx.runjs(code, undefined, { version });
295
283
  } else if (typeof eventName === 'string') {
296
284
  await m.dispatchEvent(eventName, { preview: { code } }, { sequential: true, useCache: false });
297
285
  } else if (isManual) {
@@ -20,10 +20,8 @@ import * as acornWalk from 'acorn-walk';
20
20
  import {
21
21
  JSRunner,
22
22
  FlowContext,
23
- createSafeDocument,
24
- createSafeNavigator,
25
- createSafeWindow,
26
23
  prepareRunJsCode,
24
+ RUNJS_ALLOWED_BARE_GLOBAL_NAMES,
27
25
  shouldPreprocessRunJSTemplates,
28
26
  } from '@nocobase/flow-engine';
29
27
 
@@ -604,41 +602,7 @@ function collectHeuristicIssues(code: string): RunJSIssue[] {
604
602
  return issues;
605
603
  }
606
604
 
607
- const declared = new Set<string>([
608
- // Common globals / allowed runtime context roots
609
- 'ctx',
610
- 'console',
611
- 'window',
612
- 'document',
613
- 'navigator',
614
- 'Math',
615
- 'Date',
616
- 'Array',
617
- 'Object',
618
- 'Number',
619
- 'String',
620
- 'Boolean',
621
- 'Promise',
622
- 'RegExp',
623
- 'Set',
624
- 'Map',
625
- 'WeakSet',
626
- 'WeakMap',
627
- 'JSON',
628
- 'Intl',
629
- 'URL',
630
- 'Error',
631
- 'TypeError',
632
- 'encodeURIComponent',
633
- 'decodeURIComponent',
634
- 'parseInt',
635
- 'parseFloat',
636
- 'isNaN',
637
- 'isFinite',
638
- 'undefined',
639
- 'NaN',
640
- 'Infinity',
641
- ]);
605
+ const declared = new Set<string>(RUNJS_ALLOWED_BARE_GLOBAL_NAMES);
642
606
 
643
607
  const addId = (id: any) => {
644
608
  if (id && typeof id.name === 'string') declared.add(id.name);
@@ -1036,15 +1000,14 @@ export async function diagnoseRunJS(
1036
1000
  const { consoleCapture, loggerCapture } = createLogCollectors(logs);
1037
1001
 
1038
1002
  const baseGlobals: Record<string, any> = { console: consoleCapture };
1039
- try {
1040
- if (typeof window !== 'undefined') {
1041
- const navigator = createSafeNavigator();
1003
+ if (typeof window !== 'undefined') {
1004
+ baseGlobals.window = window;
1005
+ if (typeof navigator !== 'undefined') {
1042
1006
  baseGlobals.navigator = navigator;
1043
- baseGlobals.window = createSafeWindow({ navigator });
1044
- baseGlobals.document = createSafeDocument();
1045
1007
  }
1046
- } catch (_) {
1047
- // ignore safe globals failures
1008
+ }
1009
+ if (typeof document !== 'undefined') {
1010
+ baseGlobals.document = document;
1048
1011
  }
1049
1012
 
1050
1013
  let prepared = src;
@@ -33,6 +33,7 @@ export const getKey = (viewItem: ViewItem) => {
33
33
  stableStringify(params.viewUid),
34
34
  stableStringify(params.sourceId),
35
35
  stableStringify(params.filterByTk),
36
+ stableStringify(params.openViewRouteState),
36
37
  String(index),
37
38
  ];
38
39