@nocobase/client-v2 2.1.0-beta.37 → 2.1.0-beta.38

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 (123) hide show
  1. package/es/Application.d.ts +1 -0
  2. package/es/BaseApplication.d.ts +3 -0
  3. package/es/RouterManager.d.ts +1 -0
  4. package/es/components/KeepAlive.d.ts +22 -0
  5. package/es/components/RouterBridge.d.ts +9 -0
  6. package/es/data-source/ExtendCollectionsProvider.d.ts +28 -2
  7. package/es/flow/FlowPage.d.ts +2 -1
  8. package/es/flow/admin-shell/AdminLayoutRouteCoordinator.d.ts +8 -40
  9. package/es/flow/admin-shell/BaseLayoutModel.d.ts +89 -0
  10. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +74 -0
  11. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +12 -0
  12. package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -92
  13. package/es/flow/admin-shell/admin-layout/index.d.ts +2 -0
  14. package/es/flow/admin-shell/useAdminLayoutRoutePage.d.ts +2 -2
  15. package/es/flow/admin-shell/useLayoutRoutePage.d.ts +23 -0
  16. package/es/flow/components/FlowRoute.d.ts +10 -1
  17. package/es/flow/index.d.ts +4 -0
  18. package/es/flow/models/base/PageModel/PageModel.d.ts +3 -1
  19. package/es/flow/models/blocks/form/FormActionGroupModel.d.ts +1 -0
  20. package/es/flow/models/blocks/table/TableBlockModel.d.ts +10 -0
  21. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.d.ts +1 -1
  22. package/es/index.d.ts +1 -0
  23. package/es/index.mjs +484 -437
  24. package/es/layout-manager/LayoutContentRoute.d.ts +14 -0
  25. package/es/layout-manager/LayoutManager.d.ts +22 -0
  26. package/es/layout-manager/LayoutRoute.d.ts +14 -0
  27. package/es/layout-manager/index.d.ts +13 -0
  28. package/es/layout-manager/types.d.ts +20 -0
  29. package/es/layout-manager/utils.d.ts +14 -0
  30. package/es/settings-center/index.d.ts +1 -1
  31. package/es/settings-center/plugin-manager/BulkEnableButton.d.ts +15 -0
  32. package/es/settings-center/plugin-manager/PluginCard.d.ts +15 -0
  33. package/es/settings-center/plugin-manager/PluginDetail.d.ts +16 -0
  34. package/es/settings-center/{PluginManagerPage.d.ts → plugin-manager/index.d.ts} +1 -7
  35. package/es/settings-center/plugin-manager/types.d.ts +34 -0
  36. package/lib/index.js +484 -437
  37. package/package.json +8 -7
  38. package/src/Application.tsx +27 -12
  39. package/src/BaseApplication.tsx +6 -0
  40. package/src/PluginSettingsManager.ts +1 -1
  41. package/src/RouterManager.tsx +17 -1
  42. package/src/__tests__/PluginSettingsManager.test.ts +41 -2
  43. package/src/__tests__/app.test.tsx +8 -1
  44. package/src/__tests__/globalDeps.test.ts +1 -0
  45. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +45 -2
  46. package/src/__tests__/plugin-manager.test.tsx +177 -0
  47. package/src/__tests__/settings-center.test.tsx +24 -2
  48. package/src/components/KeepAlive.tsx +131 -0
  49. package/src/components/RouterBridge.tsx +28 -4
  50. package/src/components/__tests__/KeepAlive.test.tsx +63 -0
  51. package/src/components/__tests__/RouterBridge.test.tsx +27 -0
  52. package/src/data-source/ExtendCollectionsProvider.tsx +94 -20
  53. package/src/data-source/__tests__/ExtendCollectionsProvider.test.tsx +264 -0
  54. package/src/flow/FlowPage.tsx +35 -7
  55. package/src/flow/__tests__/FlowPage.test.tsx +79 -0
  56. package/src/flow/__tests__/FlowRoute.test.tsx +529 -2
  57. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +191 -0
  58. package/src/flow/actions/__tests__/openView.subModelKey.test.tsx +33 -0
  59. package/src/flow/actions/aclCheck.tsx +4 -0
  60. package/src/flow/actions/aclCheckRefresh.tsx +4 -0
  61. package/src/flow/actions/dateTimeFormat.tsx +12 -8
  62. package/src/flow/actions/linkageRules.tsx +122 -0
  63. package/src/flow/actions/openView.tsx +28 -4
  64. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +11 -329
  65. package/src/flow/admin-shell/BaseLayoutModel.tsx +455 -0
  66. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +502 -0
  67. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +547 -3
  68. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +4 -4
  69. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +160 -0
  70. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -12
  71. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +28 -201
  72. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +11 -2
  73. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +1 -26
  74. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +149 -27
  75. package/src/flow/admin-shell/admin-layout/index.ts +2 -0
  76. package/src/flow/admin-shell/useAdminLayoutRoutePage.ts +10 -26
  77. package/src/flow/admin-shell/useLayoutRoutePage.ts +61 -0
  78. package/src/flow/components/AdminLayout.tsx +4 -154
  79. package/src/flow/components/FlowRoute.tsx +105 -15
  80. package/src/flow/index.ts +4 -0
  81. package/src/flow/models/base/ActionModel.tsx +8 -1
  82. package/src/flow/models/base/PageModel/PageModel.tsx +51 -18
  83. package/src/flow/models/base/PageModel/RootPageModel.tsx +6 -13
  84. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +102 -1
  85. package/src/flow/models/base/RouteModel.tsx +1 -1
  86. package/src/flow/models/blocks/form/FormActionGroupModel.tsx +14 -0
  87. package/src/flow/models/blocks/form/FormItemModel.tsx +8 -1
  88. package/src/flow/models/blocks/form/__tests__/FormActionGroupModel.test.ts +46 -0
  89. package/src/flow/models/blocks/form/submitValues.ts +4 -1
  90. package/src/flow/models/blocks/table/TableBlockModel.tsx +118 -16
  91. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +114 -0
  92. package/src/flow/models/fields/AssociationFieldModel/SubFormFieldModel.tsx +7 -1
  93. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -1
  94. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -5
  95. package/src/flow/models/fields/ClickableFieldModel.tsx +9 -1
  96. package/src/flow/models/fields/DisplayTimeFieldModel.tsx +1 -1
  97. package/src/flow/models/fields/TimeFieldModel.tsx +1 -1
  98. package/src/flow/models/fields/__tests__/TimeFieldModel.test.tsx +61 -0
  99. package/src/flow/models/fields/mobile-components/MobileDatePicker.tsx +19 -3
  100. package/src/flow/models/fields/mobile-components/__tests__/MobileDatePicker.test.tsx +94 -0
  101. package/src/flow/models/topbar/TopbarActionModel.tsx +1 -1
  102. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +91 -0
  103. package/src/index.ts +1 -0
  104. package/src/layout-manager/LayoutContentRoute.tsx +90 -0
  105. package/src/layout-manager/LayoutManager.tsx +185 -0
  106. package/src/layout-manager/LayoutRoute.tsx +138 -0
  107. package/src/layout-manager/__tests__/LayoutManager.test.tsx +335 -0
  108. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +473 -0
  109. package/src/layout-manager/index.ts +14 -0
  110. package/src/layout-manager/types.ts +22 -0
  111. package/src/layout-manager/utils.ts +37 -0
  112. package/src/nocobase-buildin-plugin/index.tsx +56 -48
  113. package/src/settings-center/index.ts +1 -1
  114. package/src/settings-center/plugin-manager/BulkEnableButton.tsx +111 -0
  115. package/src/settings-center/plugin-manager/PluginCard.tsx +270 -0
  116. package/src/settings-center/plugin-manager/PluginDetail.tsx +195 -0
  117. package/src/settings-center/plugin-manager/index.tsx +254 -0
  118. package/src/settings-center/plugin-manager/types.ts +35 -0
  119. package/src/settings-center/utils.tsx +8 -1
  120. package/src/theme/__tests__/globalStyles.test.ts +24 -0
  121. package/src/theme/globalStyles.ts +10 -0
  122. package/src/utils/globalDeps.ts +2 -0
  123. package/src/settings-center/PluginManagerPage.tsx +0 -162
@@ -11,7 +11,7 @@ import React from 'react';
11
11
  import { describe, it, expect, vi, beforeEach } from 'vitest';
12
12
  import { MemoryRouter, Route, Routes } from 'react-router-dom';
13
13
  import { render, screen, waitFor } from '@testing-library/react';
14
- import { FlowEngine, FlowEngineProvider, type FlowModel } from '@nocobase/flow-engine';
14
+ import { FlowContextProvider, FlowEngine, FlowEngineProvider, type FlowModel } from '@nocobase/flow-engine';
15
15
  import FlowRoute from '../components/FlowRoute';
16
16
 
17
17
  type MockAdminLayoutModel = FlowModel & {
@@ -86,7 +86,7 @@ describe('FlowRoute', () => {
86
86
  expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith(
87
87
  'test-page',
88
88
  expect.objectContaining({
89
- active: false,
89
+ active: true,
90
90
  refreshDesktopRoutes: expect.any(Function),
91
91
  layoutContentElement: expect.any(HTMLDivElement),
92
92
  }),
@@ -120,6 +120,200 @@ describe('FlowRoute', () => {
120
120
  expect(adminLayoutModel.unregisterRoutePage).toHaveBeenCalledWith('test-page');
121
121
  });
122
122
 
123
+ it('should sync explicit active state to layout route page', async () => {
124
+ const engine = new FlowEngine();
125
+ engine.context.defineProperty('routeRepository', {
126
+ value: {
127
+ refreshAccessible: hookState.refresh,
128
+ isAccessibleLoaded: () => true,
129
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
130
+ getRouteBySchemaUid: vi.fn(() => ({ type: 'flowPage', schemaUid: 'test-page' })),
131
+ },
132
+ });
133
+ engine.context.defineProperty('app', {
134
+ value: {
135
+ getPublicPath: () => '/v2/',
136
+ router: {
137
+ getBasename: () => '/v2',
138
+ },
139
+ },
140
+ });
141
+
142
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
143
+ engine.createModel({
144
+ uid: 'admin-layout-model',
145
+ use: 'FlowModel',
146
+ }),
147
+ {
148
+ registerRoutePage: vi.fn(),
149
+ updateRoutePage: vi.fn(),
150
+ unregisterRoutePage: vi.fn(),
151
+ },
152
+ );
153
+
154
+ const result = render(
155
+ <FlowEngineProvider engine={engine}>
156
+ <MemoryRouter initialEntries={['/flow/test-page']}>
157
+ <Routes>
158
+ <Route path="/flow/:name" element={<FlowRoute active={false} />} />
159
+ </Routes>
160
+ </MemoryRouter>
161
+ </FlowEngineProvider>,
162
+ );
163
+
164
+ await waitFor(() => {
165
+ expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith(
166
+ 'test-page',
167
+ expect.objectContaining({
168
+ active: false,
169
+ }),
170
+ );
171
+ });
172
+
173
+ result.rerender(
174
+ <FlowEngineProvider engine={engine}>
175
+ <MemoryRouter initialEntries={['/flow/test-page']}>
176
+ <Routes>
177
+ <Route path="/flow/:name" element={<FlowRoute active={true} />} />
178
+ </Routes>
179
+ </MemoryRouter>
180
+ </FlowEngineProvider>,
181
+ );
182
+
183
+ await waitFor(() => {
184
+ expect(adminLayoutModel.updateRoutePage).toHaveBeenCalledWith(
185
+ 'test-page',
186
+ expect.objectContaining({
187
+ active: true,
188
+ }),
189
+ );
190
+ });
191
+ });
192
+
193
+ it('should bridge page lifecycle with explicit pageUid', async () => {
194
+ const engine = new FlowEngine();
195
+ engine.context.defineProperty('routeRepository', {
196
+ value: {
197
+ refreshAccessible: hookState.refresh,
198
+ isAccessibleLoaded: () => true,
199
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
200
+ getRouteBySchemaUid: vi.fn(() => ({ type: 'flowPage', schemaUid: 'test-page' })),
201
+ },
202
+ });
203
+ engine.context.defineProperty('app', {
204
+ value: {
205
+ getPublicPath: () => '/v2/',
206
+ router: {
207
+ getBasename: () => '/v2',
208
+ },
209
+ },
210
+ });
211
+
212
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
213
+ engine.createModel({
214
+ uid: 'admin-layout-model',
215
+ use: 'FlowModel',
216
+ }),
217
+ {
218
+ registerRoutePage: vi.fn(),
219
+ updateRoutePage: vi.fn(),
220
+ unregisterRoutePage: vi.fn(),
221
+ },
222
+ );
223
+
224
+ render(
225
+ <FlowEngineProvider engine={engine}>
226
+ <MemoryRouter initialEntries={['/flow']}>
227
+ <Routes>
228
+ <Route path="/flow" element={<FlowRoute pageUid="test-page" />} />
229
+ </Routes>
230
+ </MemoryRouter>
231
+ </FlowEngineProvider>,
232
+ );
233
+
234
+ await waitFor(() => {
235
+ expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith('test-page', expect.any(Object));
236
+ });
237
+ });
238
+
239
+ it('should derive layout model from current layout context when rendered from schema', async () => {
240
+ const engine = new FlowEngine();
241
+ const routeRepository = {
242
+ refreshAccessible: hookState.refresh,
243
+ isAccessibleLoaded: () => true,
244
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
245
+ getRouteBySchemaUid: vi.fn(() => ({ type: 'flowPage', schemaUid: 'test-page' })),
246
+ };
247
+ engine.context.defineProperty('route', {
248
+ value: {
249
+ params: { name: 'test-page' },
250
+ pathname: '/embed/test-page',
251
+ },
252
+ });
253
+ engine.context.defineProperty('routeRepository', {
254
+ value: routeRepository,
255
+ });
256
+ const routeModel = engine.createModel({
257
+ uid: 'route-model',
258
+ use: 'FlowModel',
259
+ });
260
+ routeModel.context.defineProperty('layout', {
261
+ value: {
262
+ routeName: 'embed',
263
+ routePath: '/embed',
264
+ rootRouteName: 'embed',
265
+ uid: 'embed-layout-model',
266
+ layoutModelClass: 'EmbedLayoutModelV2',
267
+ rootPageModelClass: 'RootPageModel',
268
+ childPageModelClass: 'ChildPageModel',
269
+ authCheck: true,
270
+ },
271
+ });
272
+ engine.context.defineProperty('app', {
273
+ value: {
274
+ getPublicPath: () => '/v2/',
275
+ router: {
276
+ getBasename: () => '/v2',
277
+ },
278
+ },
279
+ });
280
+
281
+ const embedLayoutModel: MockAdminLayoutModel = Object.assign(
282
+ engine.createModel({
283
+ uid: 'embed-layout-model',
284
+ use: 'FlowModel',
285
+ }),
286
+ {
287
+ registerRoutePage: vi.fn(),
288
+ updateRoutePage: vi.fn(),
289
+ unregisterRoutePage: vi.fn(),
290
+ },
291
+ );
292
+
293
+ render(
294
+ <FlowEngineProvider engine={engine}>
295
+ <FlowContextProvider context={routeModel.context}>
296
+ <MemoryRouter initialEntries={['/embed/test-page']}>
297
+ <Routes>
298
+ <Route path="/embed/:name" element={<FlowRoute />} />
299
+ </Routes>
300
+ </MemoryRouter>
301
+ </FlowContextProvider>
302
+ </FlowEngineProvider>,
303
+ );
304
+
305
+ await waitFor(() => {
306
+ expect(embedLayoutModel.registerRoutePage).toHaveBeenCalledWith(
307
+ 'test-page',
308
+ expect.objectContaining({
309
+ active: true,
310
+ refreshDesktopRoutes: expect.any(Function),
311
+ layoutContentElement: expect.any(HTMLDivElement),
312
+ }),
313
+ );
314
+ });
315
+ });
316
+
123
317
  it('should fail fast when admin-layout-model is missing', () => {
124
318
  const engine = new FlowEngine();
125
319
  engine.context.defineProperty('route', {
@@ -283,6 +477,339 @@ describe('FlowRoute', () => {
283
477
  }
284
478
  });
285
479
 
480
+ it('should render not found for legacy page when behavior is notFound', async () => {
481
+ const originalLocation = window.location;
482
+ const replace = vi.fn();
483
+ Object.defineProperty(window, 'location', {
484
+ configurable: true,
485
+ value: {
486
+ ...originalLocation,
487
+ pathname: '/v2/embed/test-page',
488
+ replace,
489
+ },
490
+ });
491
+
492
+ try {
493
+ const engine = new FlowEngine();
494
+ engine.context.defineProperty('routeRepository', {
495
+ value: {
496
+ refreshAccessible: hookState.refresh,
497
+ isAccessibleLoaded: () => true,
498
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
499
+ getRouteBySchemaUid: vi.fn(() => ({ type: 'page', schemaUid: 'test-page' })),
500
+ },
501
+ });
502
+ engine.context.defineProperty('app', {
503
+ value: {
504
+ getPublicPath: () => '/v2/',
505
+ router: {
506
+ getBasename: () => '/v2',
507
+ },
508
+ },
509
+ });
510
+
511
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
512
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
513
+ {
514
+ registerRoutePage: vi.fn(),
515
+ updateRoutePage: vi.fn(),
516
+ unregisterRoutePage: vi.fn(),
517
+ },
518
+ );
519
+
520
+ const result = render(
521
+ <FlowEngineProvider engine={engine}>
522
+ <MemoryRouter initialEntries={['/embed/test-page']}>
523
+ <Routes>
524
+ <Route path="/embed/:name" element={<FlowRoute legacyPageBehavior="notFound" />} />
525
+ </Routes>
526
+ </MemoryRouter>
527
+ </FlowEngineProvider>,
528
+ );
529
+
530
+ await result.findByText('404');
531
+ expect(replace).not.toHaveBeenCalled();
532
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
533
+ } finally {
534
+ Object.defineProperty(window, 'location', {
535
+ configurable: true,
536
+ value: originalLocation,
537
+ });
538
+ }
539
+ });
540
+
541
+ it('should bridge existing FlowModel when behavior is notFound and routeRepository has no route', async () => {
542
+ const engine = new FlowEngine();
543
+ engine.setModelRepository({
544
+ findOne: vi.fn().mockResolvedValue({
545
+ uid: 'public-form-1',
546
+ use: 'FlowModel',
547
+ }),
548
+ save: vi.fn(),
549
+ destroy: vi.fn(),
550
+ } as any);
551
+ engine.context.defineProperty('routeRepository', {
552
+ value: {
553
+ refreshAccessible: hookState.refresh,
554
+ isAccessibleLoaded: () => true,
555
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
556
+ getRouteBySchemaUid: vi.fn(() => undefined),
557
+ },
558
+ });
559
+ engine.context.defineProperty('app', {
560
+ value: {
561
+ getPublicPath: () => '/v2/',
562
+ router: {
563
+ getBasename: () => '/v2',
564
+ },
565
+ },
566
+ });
567
+
568
+ const layoutModel: MockAdminLayoutModel = Object.assign(
569
+ engine.createModel({ uid: 'public-form-layout-model', use: 'FlowModel' }),
570
+ {
571
+ registerRoutePage: vi.fn(),
572
+ updateRoutePage: vi.fn(),
573
+ unregisterRoutePage: vi.fn(),
574
+ },
575
+ );
576
+
577
+ render(
578
+ <FlowEngineProvider engine={engine}>
579
+ <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
580
+ <Routes>
581
+ <Route
582
+ path="/public-forms/:name"
583
+ element={<FlowRoute legacyPageBehavior="notFound" getLayoutModel={() => layoutModel} />}
584
+ />
585
+ </Routes>
586
+ </MemoryRouter>
587
+ </FlowEngineProvider>,
588
+ );
589
+
590
+ await waitFor(() => {
591
+ expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('public-form-1', expect.any(Object));
592
+ });
593
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
594
+ });
595
+
596
+ it('should check model existence without occupying the route model uid', async () => {
597
+ const engine = new FlowEngine();
598
+ const findOne = vi.fn().mockResolvedValue({
599
+ uid: 'public-form-1',
600
+ });
601
+ const request = vi.fn().mockResolvedValue({
602
+ data: {
603
+ data: {
604
+ uid: 'public-form-1',
605
+ },
606
+ },
607
+ });
608
+ engine.setModelRepository({
609
+ findOne,
610
+ save: vi.fn(),
611
+ destroy: vi.fn(),
612
+ } as any);
613
+ engine.context.defineProperty('routeRepository', {
614
+ value: {
615
+ refreshAccessible: hookState.refresh,
616
+ isAccessibleLoaded: () => true,
617
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
618
+ getRouteBySchemaUid: vi.fn(() => undefined),
619
+ },
620
+ });
621
+ engine.context.defineProperty('app', {
622
+ value: {
623
+ apiClient: {
624
+ request,
625
+ },
626
+ getPublicPath: () => '/v2/',
627
+ router: {
628
+ getBasename: () => '/v2',
629
+ },
630
+ },
631
+ });
632
+ const routeModel = engine.createModel({
633
+ uid: 'public-form-route-model',
634
+ use: 'FlowModel',
635
+ });
636
+ routeModel.context.defineProperty('layout', {
637
+ value: {
638
+ routeName: 'public-forms',
639
+ routePath: '/public-forms',
640
+ rootRouteName: 'public-forms',
641
+ uid: 'public-form-layout-model',
642
+ layoutModelClass: 'PublicFormLayoutModel',
643
+ rootPageModelClass: 'PublicFormPageModel',
644
+ childPageModelClass: 'ChildPageModel',
645
+ authCheck: false,
646
+ },
647
+ });
648
+
649
+ const layoutModel: MockAdminLayoutModel = Object.assign(
650
+ engine.createModel({ uid: 'public-form-layout-model', use: 'FlowModel' }),
651
+ {
652
+ registerRoutePage: vi.fn(),
653
+ updateRoutePage: vi.fn(),
654
+ unregisterRoutePage: vi.fn(),
655
+ },
656
+ );
657
+
658
+ render(
659
+ <FlowEngineProvider engine={engine}>
660
+ <FlowContextProvider context={routeModel.context}>
661
+ <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
662
+ <Routes>
663
+ <Route path="/public-forms/:name" element={<FlowRoute legacyPageBehavior="notFound" />} />
664
+ </Routes>
665
+ </MemoryRouter>
666
+ </FlowContextProvider>
667
+ </FlowEngineProvider>,
668
+ );
669
+
670
+ await waitFor(() => {
671
+ expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('public-form-1', expect.any(Object));
672
+ });
673
+ expect(findOne).toHaveBeenCalledWith({ uid: 'public-form-1' });
674
+ expect(request).not.toHaveBeenCalled();
675
+ expect(engine.getModel('public-form-1')).toBeUndefined();
676
+ });
677
+
678
+ it('should not skip accessible route loading just because layout authCheck is false', async () => {
679
+ const engine = new FlowEngine();
680
+ const ensureAccessibleLoaded = vi.fn().mockRejectedValue(new Error('cannot load accessible routes'));
681
+ const getRouteBySchemaUid = vi.fn();
682
+ engine.setModelRepository({
683
+ findOne: vi.fn().mockResolvedValue({
684
+ uid: 'public-form-1',
685
+ use: 'FlowModel',
686
+ }),
687
+ save: vi.fn(),
688
+ destroy: vi.fn(),
689
+ } as any);
690
+ engine.context.defineProperty('routeRepository', {
691
+ value: {
692
+ refreshAccessible: hookState.refresh,
693
+ isAccessibleLoaded: () => false,
694
+ ensureAccessibleLoaded,
695
+ getRouteBySchemaUid,
696
+ },
697
+ });
698
+ engine.context.defineProperty('app', {
699
+ value: {
700
+ getPublicPath: () => '/v2/',
701
+ router: {
702
+ getBasename: () => '/v2',
703
+ },
704
+ },
705
+ });
706
+ const routeModel = engine.createModel({
707
+ uid: 'public-form-route-model',
708
+ use: 'FlowModel',
709
+ });
710
+ routeModel.context.defineProperty('layout', {
711
+ value: {
712
+ routeName: 'public-forms',
713
+ routePath: '/public-forms',
714
+ rootRouteName: 'public-forms',
715
+ uid: 'public-form-layout-model',
716
+ layoutModelClass: 'PublicFormLayoutModel',
717
+ rootPageModelClass: 'PublicFormPageModel',
718
+ childPageModelClass: 'ChildPageModel',
719
+ authCheck: false,
720
+ },
721
+ });
722
+
723
+ const layoutModel: MockAdminLayoutModel = Object.assign(
724
+ engine.createModel({ uid: 'public-form-layout-model', use: 'FlowModel' }),
725
+ {
726
+ registerRoutePage: vi.fn(),
727
+ updateRoutePage: vi.fn(),
728
+ unregisterRoutePage: vi.fn(),
729
+ },
730
+ );
731
+
732
+ render(
733
+ <FlowEngineProvider engine={engine}>
734
+ <FlowContextProvider context={routeModel.context}>
735
+ <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
736
+ <Routes>
737
+ <Route path="/public-forms/:name" element={<FlowRoute legacyPageBehavior="notFound" />} />
738
+ </Routes>
739
+ </MemoryRouter>
740
+ </FlowContextProvider>
741
+ </FlowEngineProvider>,
742
+ );
743
+
744
+ await waitFor(() => {
745
+ expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('public-form-1', expect.any(Object));
746
+ });
747
+ expect(ensureAccessibleLoaded).toHaveBeenCalledTimes(1);
748
+ expect(getRouteBySchemaUid).not.toHaveBeenCalled();
749
+ });
750
+
751
+ it('should bridge legacy page when behavior is bridge', async () => {
752
+ const originalLocation = window.location;
753
+ const replace = vi.fn();
754
+ Object.defineProperty(window, 'location', {
755
+ configurable: true,
756
+ value: {
757
+ ...originalLocation,
758
+ pathname: '/v2/admin/test-page',
759
+ replace,
760
+ },
761
+ });
762
+
763
+ try {
764
+ const engine = new FlowEngine();
765
+ engine.context.defineProperty('routeRepository', {
766
+ value: {
767
+ refreshAccessible: hookState.refresh,
768
+ isAccessibleLoaded: () => true,
769
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
770
+ getRouteBySchemaUid: vi.fn(() => ({ type: 'page', schemaUid: 'test-page' })),
771
+ },
772
+ });
773
+ engine.context.defineProperty('app', {
774
+ value: {
775
+ getPublicPath: () => '/v2/',
776
+ router: {
777
+ getBasename: () => '/v2',
778
+ },
779
+ },
780
+ });
781
+
782
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
783
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
784
+ {
785
+ registerRoutePage: vi.fn(),
786
+ updateRoutePage: vi.fn(),
787
+ unregisterRoutePage: vi.fn(),
788
+ },
789
+ );
790
+
791
+ render(
792
+ <FlowEngineProvider engine={engine}>
793
+ <MemoryRouter initialEntries={['/flow/test-page']}>
794
+ <Routes>
795
+ <Route path="/flow/:name" element={<FlowRoute legacyPageBehavior="bridge" />} />
796
+ </Routes>
797
+ </MemoryRouter>
798
+ </FlowEngineProvider>,
799
+ );
800
+
801
+ await waitFor(() => {
802
+ expect(adminLayoutModel.registerRoutePage).toHaveBeenCalled();
803
+ });
804
+ expect(replace).not.toHaveBeenCalled();
805
+ } finally {
806
+ Object.defineProperty(window, 'location', {
807
+ configurable: true,
808
+ value: originalLocation,
809
+ });
810
+ }
811
+ });
812
+
286
813
  it('should not redirect when route does not exist', async () => {
287
814
  const originalLocation = window.location;
288
815
  const replace = vi.fn();