@nocobase/client-v2 2.1.31 → 2.2.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +2 -0
  3. package/es/PluginSettingsManager.d.ts +33 -0
  4. package/es/RouteRepository.d.ts +29 -6
  5. package/es/RouterManager.d.ts +15 -0
  6. package/es/authRedirect.d.ts +1 -0
  7. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  8. package/es/components/category-tabs/index.d.ts +9 -0
  9. package/es/components/form/TypedVariableInput.d.ts +31 -5
  10. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  11. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  12. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  13. package/es/components/form/filter/index.d.ts +2 -0
  14. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  15. package/es/components/index.d.ts +2 -0
  16. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  17. package/es/entry-actions/index.d.ts +9 -0
  18. package/es/flow/actions/index.d.ts +1 -1
  19. package/es/flow/actions/linkageRules.d.ts +2 -0
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  21. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  22. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +3 -0
  23. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  24. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -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/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  29. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  30. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  31. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  32. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  33. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  34. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  35. package/es/flow/index.d.ts +1 -0
  36. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  37. package/es/flow/models/base/GridModel.d.ts +1 -1
  38. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  39. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  40. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  41. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +9 -0
  42. package/es/flow-compat/routeTypes.d.ts +1 -0
  43. package/es/index.d.ts +7 -1
  44. package/es/index.mjs +223 -135
  45. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  46. package/es/utils/index.d.ts +1 -0
  47. package/lib/index.js +220 -132
  48. package/package.json +7 -7
  49. package/src/APIClient.ts +1 -10
  50. package/src/Application.tsx +4 -0
  51. package/src/BaseApplication.tsx +2 -0
  52. package/src/PluginSettingsManager.ts +53 -0
  53. package/src/RouteRepository.ts +151 -24
  54. package/src/RouterManager.tsx +142 -1
  55. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  56. package/src/__tests__/RouteRepository.test.ts +239 -0
  57. package/src/__tests__/RouterManager.test.ts +125 -0
  58. package/src/__tests__/app.test.tsx +18 -0
  59. package/src/__tests__/authRedirect.test.ts +17 -0
  60. package/src/__tests__/browserChecker.test.ts +61 -0
  61. package/src/__tests__/exports.test.ts +16 -0
  62. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  63. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +28 -0
  64. package/src/__tests__/plugin-manager.test.tsx +44 -2
  65. package/src/__tests__/settings-center.test.tsx +40 -1
  66. package/src/authRedirect.ts +38 -0
  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/TypedVariableInput.tsx +452 -101
  73. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  74. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  75. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  76. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  77. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  78. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  79. package/src/components/form/filter/index.ts +2 -0
  80. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  81. package/src/components/index.ts +2 -0
  82. package/src/entry-actions/EntryActionManager.ts +76 -0
  83. package/src/entry-actions/index.ts +10 -0
  84. package/src/flow/__tests__/FlowRoute.test.tsx +1084 -145
  85. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  86. package/src/flow/actions/customVariable.tsx +1 -2
  87. package/src/flow/actions/index.ts +2 -0
  88. package/src/flow/actions/linkageRules.tsx +65 -8
  89. package/src/flow/actions/runjs.tsx +2 -14
  90. package/src/flow/admin-shell/BaseLayoutModel.tsx +13 -0
  91. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +46 -13
  92. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +486 -0
  93. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +64 -8
  94. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  95. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  96. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  97. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  98. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  99. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  100. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  101. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +435 -0
  102. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  103. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +77 -0
  104. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +214 -2
  105. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  106. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  107. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  108. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  109. package/src/flow/components/DefaultValue.tsx +1 -2
  110. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  111. package/src/flow/components/FieldAssignValueInput.tsx +76 -9
  112. package/src/flow/components/FlowRoute.tsx +185 -28
  113. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  114. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +84 -2
  115. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  116. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  117. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  118. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  119. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  120. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  121. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  122. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  123. package/src/flow/index.ts +1 -0
  124. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  125. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  126. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  127. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  128. package/src/flow/models/base/ActionModel.tsx +21 -8
  129. package/src/flow/models/base/ActionModelCore.tsx +5 -0
  130. package/src/flow/models/base/GridModel.tsx +38 -7
  131. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  132. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  133. package/src/flow/models/base/PageModel/PageTabModel.tsx +186 -3
  134. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -14
  135. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  136. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  137. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +586 -1
  138. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  139. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  140. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  141. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -2
  142. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  143. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  144. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +87 -0
  145. package/src/flow/models/blocks/form/value-runtime/rules.ts +2 -2
  146. package/src/flow/models/blocks/form/value-runtime/runtime.ts +91 -0
  147. package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
  148. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  149. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
  150. package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
  151. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
  152. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  153. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  154. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  155. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  156. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  157. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  158. package/src/flow/models/topbar/TopbarActionModel.tsx +168 -10
  159. package/src/flow-compat/Popover.tsx +43 -4
  160. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  161. package/src/flow-compat/routeTypes.ts +1 -0
  162. package/src/index.ts +13 -1
  163. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  164. package/src/nocobase-buildin-plugin/index.tsx +19 -3
  165. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  166. package/src/settings-center/SystemSettingsPage.tsx +0 -1
  167. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  168. package/src/settings-center/utils.tsx +0 -6
  169. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  170. package/src/utils/index.tsx +1 -0
@@ -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,195 @@ 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 open sub-app admin settings in a new tab outside sub-app admin runtime', () => {
226
+ const items = getTopbarPluginSettingsItems({
227
+ canManagePlugins: false,
228
+ t: (key) => key,
229
+ settings: [
230
+ {
231
+ key: 'system-settings',
232
+ name: 'system-settings',
233
+ title: 'System settings',
234
+ path: '/admin/settings/system-settings',
235
+ icon: null,
236
+ componentLoader: async () => null,
237
+ },
238
+ ] as any,
239
+ });
240
+
241
+ renderSettingsLabel((items as any[])[0].label, '/apps/a_9xlild35jir/crm-amd/ekeisumx1zu');
242
+
243
+ const link = screen.getByRole('link', { name: 'System settings' });
244
+ expect(link).toHaveAttribute('href', '/nocobase/v/apps/a_9xlild35jir/admin/settings/system-settings');
245
+ expect(link).toHaveAttribute('target', '_blank');
246
+ expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'));
247
+ expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
248
+ });
249
+
250
+ it('should keep regular admin settings as SPA links inside admin runtime', () => {
251
+ const items = getTopbarPluginSettingsItems({
252
+ canManagePlugins: false,
253
+ t: (key) => key,
254
+ settings: [
255
+ {
256
+ key: 'routes',
257
+ name: 'routes',
258
+ title: 'Routes',
259
+ path: '/admin/settings/routes',
260
+ icon: null,
261
+ componentLoader: async () => null,
262
+ },
263
+ ] as any,
264
+ });
265
+
266
+ renderSettingsLabel((items as any[])[0].label, '/admin/settings/routes');
267
+
268
+ const link = screen.getByRole('link', { name: 'Routes' });
269
+ expect(link).toHaveAttribute('href', '/admin/settings/routes');
270
+ expect(link).not.toHaveAttribute('target', '_blank');
271
+ });
272
+
273
+ it('should keep sub-app admin settings in the current window inside sub-app admin runtime', () => {
274
+ const items = getTopbarPluginSettingsItems({
275
+ canManagePlugins: false,
276
+ t: (key) => key,
277
+ settings: [
278
+ {
279
+ key: 'routes',
280
+ name: 'routes',
281
+ title: 'Routes',
282
+ path: '/admin/settings/routes',
283
+ icon: null,
284
+ componentLoader: async () => null,
285
+ },
286
+ ] as any,
287
+ });
288
+
289
+ renderSettingsLabel((items as any[])[0].label, '/apps/a_9xlild35jir/admin/settings/routes');
290
+
291
+ const link = screen.getByRole('link', { name: 'Routes' });
292
+ expect(link).toHaveAttribute('href', '/nocobase/v/apps/a_9xlild35jir/admin/settings/routes');
293
+ expect(link).not.toHaveAttribute('target', '_blank');
294
+ });
295
+
296
+ it('should not treat admin-like paths as admin runtime', () => {
297
+ const items = getTopbarPluginSettingsItems({
298
+ canManagePlugins: false,
299
+ t: (key) => key,
300
+ settings: [
301
+ {
302
+ key: 'system-settings',
303
+ name: 'system-settings',
304
+ title: 'System settings',
305
+ path: '/admin/settings/system-settings',
306
+ icon: null,
307
+ componentLoader: async () => null,
308
+ },
309
+ ] as any,
310
+ });
311
+
312
+ renderSettingsLabel((items as any[])[0].label, '/admin2/foo');
313
+
314
+ const link = screen.getByRole('link', { name: 'System settings' });
315
+ expect(link).toHaveAttribute('href', '/nocobase/v/admin/settings/system-settings');
316
+ expect(link).toHaveAttribute('target', '_blank');
317
+ });
318
+
319
+ it('should not duplicate the basename when building new-tab settings hrefs', () => {
320
+ const items = getTopbarPluginSettingsItems({
321
+ canManagePlugins: false,
322
+ t: (key) => key,
323
+ settings: [
324
+ {
325
+ key: 'system-settings',
326
+ name: 'system-settings',
327
+ title: 'System settings',
328
+ path: '/nocobase/v/admin/settings/system-settings',
329
+ icon: null,
330
+ componentLoader: async () => null,
331
+ },
332
+ ] as any,
333
+ });
334
+
335
+ renderSettingsLabel((items as any[])[0].label, '/sales/p1');
336
+
337
+ const link = screen.getByRole('link', { name: 'System settings' });
338
+ expect(link).toHaveAttribute('href', '/nocobase/v/admin/settings/system-settings');
339
+ });
340
+
341
+ it('should keep external settings opening in a new tab', () => {
342
+ const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
343
+ const items = getTopbarPluginSettingsItems({
344
+ canManagePlugins: false,
345
+ t: (key) => key,
346
+ settings: [
347
+ {
348
+ key: 'docs',
349
+ name: 'docs',
350
+ title: 'Docs',
351
+ link: 'https://www.nocobase.com/docs',
352
+ icon: null,
353
+ componentLoader: async () => null,
354
+ },
355
+ ] as any,
356
+ });
357
+
358
+ renderSettingsLabel((items as any[])[0].label, '/sales/p1');
359
+ fireEvent.click(screen.getByText('Docs'));
360
+
361
+ expect(openSpy).toHaveBeenCalledWith('https://www.nocobase.com/docs', '_blank', 'noopener,noreferrer');
362
+ });
363
+
152
364
  it('should return empty dropdown items when plugin manager and settings are both unavailable', () => {
153
365
  const items = getTopbarPluginSettingsItems({
154
366
  canManagePlugins: false,
@@ -9,6 +9,7 @@
9
9
 
10
10
  export * from './AdminLayoutComponent';
11
11
  export * from './AdminLayoutModel';
12
+ export * from './AppSwitcherActionPanelModel';
12
13
  export * from '../BaseLayoutModel';
13
14
  export * from '../BaseLayoutRouteCoordinator';
14
15
  export * from './AdminLayoutSlotModels';
@@ -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,
@@ -8,41 +8,110 @@
8
8
  */
9
9
 
10
10
  import { useApp } from '../../../flow-compat';
11
+ import type { AppListProps } from '@ant-design/pro-layout/es/components/AppsLogoComponents/types';
11
12
  import React from 'react';
13
+ import type { AdminLayoutModel } from './AdminLayoutModel';
14
+ import { ADMIN_LAYOUT_MODEL_UID } from './constants';
15
+ import type { AppSwitcherActionPanelModel } from './AppSwitcherActionPanelModel';
12
16
 
13
- export const useApplications = () => {
17
+ export const APP_SWITCHER_ACTION_PANEL_MODEL_UID = `${ADMIN_LAYOUT_MODEL_UID}-app-switcher-actions`;
18
+
19
+ function getErrorStatus(error: unknown) {
20
+ if (!error || typeof error !== 'object') {
21
+ return;
22
+ }
23
+ const errorLike = error as { response?: { status?: unknown }; status?: unknown };
24
+ const status = errorLike.response?.status ?? errorLike.status;
25
+ return typeof status === 'number' ? status : undefined;
26
+ }
27
+
28
+ export const useApplications = (adminLayoutModel?: AdminLayoutModel) => {
14
29
  const app = useApp();
15
30
  const loadAppList = app.apps.loadAppList;
16
- const [appList, setAppList] = React.useState([]);
31
+ const [legacyAppList, setLegacyAppList] = React.useState<AppListProps>([]);
32
+ const [appSwitcherModel, setAppSwitcherModel] = React.useState<AppSwitcherActionPanelModel>();
33
+
34
+ React.useEffect(() => {
35
+ let canceled = false;
36
+ let modelWithLayoutContext: AppSwitcherActionPanelModel | undefined;
37
+
38
+ if (!adminLayoutModel) {
39
+ setAppSwitcherModel(undefined);
40
+ return;
41
+ }
42
+
43
+ const load = async () => {
44
+ try {
45
+ const model = await adminLayoutModel.flowEngine.loadOrCreateModel<AppSwitcherActionPanelModel>({
46
+ uid: APP_SWITCHER_ACTION_PANEL_MODEL_UID,
47
+ use: 'AppSwitcherActionPanelModel',
48
+ parentId: adminLayoutModel.uid,
49
+ subKey: 'appSwitcher',
50
+ subType: 'object',
51
+ });
52
+ if (canceled || !model) {
53
+ return;
54
+ }
55
+ model.context.addDelegate(adminLayoutModel.context);
56
+ modelWithLayoutContext = model;
57
+ setAppSwitcherModel(model);
58
+ } catch (error) {
59
+ if (canceled) {
60
+ return;
61
+ }
62
+ if (getErrorStatus(error) !== 404) {
63
+ throw error;
64
+ }
65
+ setAppSwitcherModel(undefined);
66
+ }
67
+ };
68
+ load();
69
+
70
+ return () => {
71
+ canceled = true;
72
+ modelWithLayoutContext?.context.removeDelegate(adminLayoutModel.context);
73
+ };
74
+ }, [adminLayoutModel]);
17
75
 
18
76
  React.useEffect(() => {
19
77
  let canceled = false;
20
78
 
79
+ if (adminLayoutModel) {
80
+ setLegacyAppList([]);
81
+ return;
82
+ }
83
+
21
84
  if (!loadAppList) {
22
- setAppList([]);
85
+ setLegacyAppList([]);
23
86
  return;
24
87
  }
25
88
 
26
- void Promise.resolve(loadAppList(app))
27
- .then((list) => {
89
+ const load = async () => {
90
+ try {
91
+ const list = await Promise.resolve(loadAppList(app));
28
92
  if (!canceled) {
29
- setAppList(Array.isArray(list) ? list : []);
93
+ setLegacyAppList(Array.isArray(list) ? list : []);
30
94
  }
31
- })
32
- .catch((error) => {
95
+ } catch (error) {
33
96
  console.error('[NocoBase] Failed to load application switcher list.', error);
34
97
  if (!canceled) {
35
- setAppList([]);
98
+ setLegacyAppList([]);
36
99
  }
37
- });
100
+ }
101
+ };
102
+ load();
38
103
 
39
104
  return () => {
40
105
  canceled = true;
41
106
  };
42
- }, [app, loadAppList]);
107
+ }, [adminLayoutModel, app, loadAppList]);
108
+
109
+ const shouldRenderConfiguredSwitcher =
110
+ !!appSwitcherModel && (appSwitcherModel.hasActions() || app.flowEngine.context.flowSettingsEnabled);
43
111
 
44
112
  return {
45
113
  Component: app.apps.Component,
46
- appList,
114
+ appList: shouldRenderConfiguredSwitcher ? [{ title: '', url: '#' }] : legacyAppList,
115
+ appSwitcherModel,
47
116
  };
48
117
  };
@@ -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;
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import React, { useMemo, useState } from 'react';
10
+ import React, { useCallback, useMemo, useState } from 'react';
11
11
  import { DatePicker, Select, Space } from 'antd';
12
12
  import { inferPickerType } from '../../flow-compat';
13
13
  import { dayjs, getDateTimeFormat, getPickerFormat } from '@nocobase/utils/client';
@@ -29,6 +29,7 @@ export interface FieldAssignExactDatePickerProps {
29
29
  isRange?: boolean;
30
30
  onChange?: (value: ExactDatePickerValue) => void;
31
31
  style?: React.CSSProperties;
32
+ disabled?: boolean;
32
33
  }
33
34
 
34
35
  function getRawSingleValue(value: unknown, isRange: boolean): unknown {
@@ -114,7 +115,7 @@ function parseRangeValue(value: unknown, format: string): [dayjs.Dayjs, dayjs.Da
114
115
  }
115
116
 
116
117
  export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProps> = (props) => {
117
- const { picker = 'date', format, showTime, timeFormat, value, isRange = false, onChange, style } = props;
118
+ const { picker = 'date', format, showTime, timeFormat, value, isRange = false, onChange, style, disabled } = props;
118
119
  const flowCtx = useFlowContext();
119
120
  const t = flowCtx.model.translate.bind(flowCtx.model);
120
121
 
@@ -124,17 +125,17 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
124
125
  return inferPickerFromRawValue(rawSingleValue, picker);
125
126
  });
126
127
 
127
- const getResolvedFormat = (nextPicker: ExactDatePickerMode) => {
128
- const baseFormat = nextPicker === picker && format ? format : getPickerFormat(nextPicker);
129
- const dateFormat = nextPicker === 'date' ? stripTimeFromFormat(baseFormat) : baseFormat;
130
- return getDateTimeFormat(nextPicker, dateFormat, showTime, timeFormat);
131
- };
132
-
133
- const resolvedFormat = useMemo(
134
- () => getResolvedFormat(targetPicker),
135
- [targetPicker, format, picker, showTime, timeFormat],
128
+ const getResolvedFormat = useCallback(
129
+ (nextPicker: ExactDatePickerMode) => {
130
+ const baseFormat = nextPicker === picker && format ? format : getPickerFormat(nextPicker);
131
+ const dateFormat = nextPicker === 'date' ? stripTimeFromFormat(baseFormat) : baseFormat;
132
+ return getDateTimeFormat(nextPicker, dateFormat, showTime, timeFormat);
133
+ },
134
+ [format, picker, showTime, timeFormat],
136
135
  );
137
136
 
137
+ const resolvedFormat = useMemo(() => getResolvedFormat(targetPicker), [getResolvedFormat, targetPicker]);
138
+
138
139
  const singleValue = useMemo(() => {
139
140
  if (isRange) return null;
140
141
  return parseDateFromRawValue(value, resolvedFormat);
@@ -156,6 +157,10 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
156
157
  );
157
158
 
158
159
  const handlePickerTypeChange = (nextPicker: ExactDatePickerMode) => {
160
+ if (disabled) {
161
+ return;
162
+ }
163
+
159
164
  setTargetPicker(nextPicker);
160
165
 
161
166
  if (!onChange) {
@@ -182,7 +187,11 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
182
187
  inputReadOnly: flowCtx.isMobileLayout,
183
188
  showTime: showTime ? { defaultValue: dayjs('00:00:00', 'HH:mm:ss') } : false,
184
189
  value: singleValue,
190
+ disabled,
185
191
  onChange: (nextDate: dayjs.Dayjs | null) => {
192
+ if (disabled) {
193
+ return;
194
+ }
186
195
  if (nextDate && dayjs.isDayjs(nextDate)) {
187
196
  onChange?.(nextDate);
188
197
  return;
@@ -200,7 +209,11 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
200
209
  inputReadOnly: flowCtx.isMobileLayout,
201
210
  showTime: showTime ? { defaultValue: [dayjs('00:00:00', 'HH:mm:ss'), dayjs('23:59:59', 'HH:mm:ss')] } : false,
202
211
  value: rangeValue,
212
+ disabled,
203
213
  onChange: (nextDates: [dayjs.Dayjs | null, dayjs.Dayjs | null] | null) => {
214
+ if (disabled) {
215
+ return;
216
+ }
204
217
  if (Array.isArray(nextDates) && nextDates[0] && nextDates[1]) {
205
218
  onChange?.([nextDates[0], nextDates[1]]);
206
219
  return;
@@ -218,6 +231,7 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
218
231
  value={targetPicker}
219
232
  options={pickerOptions}
220
233
  onChange={handlePickerTypeChange}
234
+ disabled={disabled}
221
235
  />
222
236
  {isRange ? <DatePicker.RangePicker {...rangePickerProps} /> : <DatePicker {...singlePickerProps} />}
223
237
  </Space.Compact>