@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
@@ -0,0 +1,473 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { FlowEngine, FlowEngineProvider, observer } from '@nocobase/flow-engine';
11
+ import { render, screen, waitFor } from '@testing-library/react';
12
+ import React from 'react';
13
+ import { createMemoryRouter, Outlet, RouterProvider, useOutlet } from 'react-router-dom';
14
+ import { describe, expect, it } from 'vitest';
15
+ import { BaseLayoutModel } from '../../flow/admin-shell/BaseLayoutModel';
16
+ import { LayoutContentRoute } from '../LayoutContentRoute';
17
+ import { LayoutRoute } from '../LayoutRoute';
18
+ import type { LayoutDefinition } from '../types';
19
+ import {
20
+ getLayoutPageRouteName,
21
+ getLayoutPageTabRouteName,
22
+ getLayoutPageTabViewRouteName,
23
+ getLayoutPageViewRouteName,
24
+ } from '../utils';
25
+
26
+ class TestLayoutModel extends BaseLayoutModel {
27
+ render() {
28
+ return <div data-testid="layout-route">{this.layout.routeName}</div>;
29
+ }
30
+ }
31
+
32
+ const GatedLayoutComponent = observer((props: { model: BaseLayoutModel }) => {
33
+ const { model } = props;
34
+ const outlet = useOutlet();
35
+ const pageUid = model.getPageUidFromLayoutRoute(model.currentLayoutRoute);
36
+
37
+ if (!pageUid) {
38
+ return <div data-testid="layout-page-uid">missing</div>;
39
+ }
40
+
41
+ return (
42
+ <div>
43
+ <div data-testid="layout-page-uid">{pageUid}</div>
44
+ {outlet}
45
+ </div>
46
+ );
47
+ });
48
+
49
+ class GatedLayoutModel extends BaseLayoutModel {
50
+ render() {
51
+ return <GatedLayoutComponent model={this} />;
52
+ }
53
+ }
54
+
55
+ const layout: LayoutDefinition = {
56
+ routeName: 'test',
57
+ routePath: '/test',
58
+ rootRouteName: 'test',
59
+ uid: 'test-layout-model',
60
+ layoutModelClass: 'TestLayoutModel',
61
+ rootPageModelClass: 'TestRootPageModel',
62
+ childPageModelClass: 'TestChildPageModel',
63
+ authCheck: true,
64
+ };
65
+
66
+ describe('LayoutRoute', () => {
67
+ it('creates layout model from registered string class and injects layout definition', async () => {
68
+ const engine = new FlowEngine();
69
+ engine.registerModels({ TestLayoutModel });
70
+ engine.context.defineProperty('app', {
71
+ value: {
72
+ layoutManager: {
73
+ getLayout: () => layout,
74
+ },
75
+ },
76
+ });
77
+
78
+ const router = createMemoryRouter(
79
+ [
80
+ {
81
+ id: layout.routeName,
82
+ path: layout.routePath,
83
+ element: <LayoutRoute layoutRouteName="test" />,
84
+ },
85
+ ],
86
+ {
87
+ initialEntries: ['/test'],
88
+ },
89
+ );
90
+
91
+ render(
92
+ <FlowEngineProvider engine={engine}>
93
+ <RouterProvider router={router} />
94
+ </FlowEngineProvider>,
95
+ );
96
+
97
+ expect(await screen.findByTestId('layout-route')).toHaveTextContent('test');
98
+ const model = engine.getModel<TestLayoutModel>('test-layout-model');
99
+ expect(model).toBeInstanceOf(TestLayoutModel);
100
+ expect(model.layout).toMatchObject({
101
+ routeName: 'test',
102
+ routePath: '/test',
103
+ rootRouteName: 'test',
104
+ rootPageModelClass: 'TestRootPageModel',
105
+ childPageModelClass: 'TestChildPageModel',
106
+ });
107
+ });
108
+
109
+ it('syncs nested page route before the layout renders its outlet', async () => {
110
+ const nestedLayout: LayoutDefinition = {
111
+ ...layout,
112
+ routeName: 'admin.settings.publicForms.layout',
113
+ routePath: '',
114
+ rootRouteName: 'admin',
115
+ uid: 'gated-layout-model',
116
+ layoutModelClass: 'GatedLayoutModel',
117
+ };
118
+ const engine = new FlowEngine();
119
+ engine.registerModels({ GatedLayoutModel });
120
+ engine.context.defineProperty('app', {
121
+ value: {
122
+ layoutManager: {
123
+ getLayout: () => nestedLayout,
124
+ },
125
+ },
126
+ });
127
+ const router = createMemoryRouter(
128
+ [
129
+ {
130
+ id: 'admin.settings',
131
+ path: '/admin/settings',
132
+ element: <Outlet />,
133
+ children: [
134
+ {
135
+ id: 'admin.settings.publicForms',
136
+ path: 'public-forms',
137
+ element: <Outlet />,
138
+ children: [
139
+ {
140
+ id: nestedLayout.routeName,
141
+ path: '',
142
+ element: <LayoutRoute layoutRouteName={nestedLayout.routeName} />,
143
+ children: [
144
+ {
145
+ id: getLayoutPageViewRouteName(nestedLayout.routeName),
146
+ path: ':name/view/*',
147
+ element: <div data-testid="layout-child-outlet">child page</div>,
148
+ },
149
+ ],
150
+ },
151
+ ],
152
+ },
153
+ ],
154
+ },
155
+ ],
156
+ {
157
+ initialEntries: ['/admin/settings/public-forms/form-1/view/popup'],
158
+ },
159
+ );
160
+
161
+ render(
162
+ <FlowEngineProvider engine={engine}>
163
+ <RouterProvider router={router} />
164
+ </FlowEngineProvider>,
165
+ );
166
+
167
+ expect(await screen.findByTestId('layout-page-uid')).toHaveTextContent('form-1');
168
+ expect(await screen.findByTestId('layout-child-outlet')).toHaveTextContent('child page');
169
+ });
170
+
171
+ it('keeps local layout route while parent layout route switches within the same page view stack', async () => {
172
+ const clearEvents: Array<{ routePathname?: string; before: string | null; after: string | null }> = [];
173
+
174
+ class TrackingLayoutModel extends TestLayoutModel {
175
+ clearLayoutRoute(routeLike?: Parameters<BaseLayoutModel['clearLayoutRoute']>[0]) {
176
+ const event: { routePathname?: string; before: string | null; after: string | null } = {
177
+ routePathname: routeLike?.pathname,
178
+ before: this.currentLayoutRoute?.pathname || null,
179
+ after: null,
180
+ };
181
+ super.clearLayoutRoute(routeLike);
182
+ event.after = this.currentLayoutRoute?.pathname || null;
183
+ clearEvents.push(event);
184
+ }
185
+ }
186
+
187
+ const engine = new FlowEngine();
188
+ engine.registerModels({ TrackingLayoutModel });
189
+ const trackingLayout: LayoutDefinition = {
190
+ ...layout,
191
+ layoutModelClass: 'TrackingLayoutModel',
192
+ };
193
+ engine.context.defineProperty('app', {
194
+ value: {
195
+ layoutManager: {
196
+ getLayout: () => trackingLayout,
197
+ },
198
+ },
199
+ });
200
+ const router = createMemoryRouter(
201
+ [
202
+ {
203
+ id: layout.routeName,
204
+ path: layout.routePath,
205
+ element: <LayoutRoute layoutRouteName={layout.routeName} />,
206
+ children: [
207
+ {
208
+ id: getLayoutPageRouteName(layout.routeName),
209
+ path: ':name',
210
+ element: <div data-testid="page-route">page</div>,
211
+ },
212
+ {
213
+ id: getLayoutPageViewRouteName(layout.routeName),
214
+ path: ':name/view/*',
215
+ element: <div data-testid="page-view-route">page view</div>,
216
+ },
217
+ ],
218
+ },
219
+ ],
220
+ {
221
+ initialEntries: ['/test/page-1'],
222
+ },
223
+ );
224
+
225
+ render(
226
+ <FlowEngineProvider engine={engine}>
227
+ <RouterProvider router={router} />
228
+ </FlowEngineProvider>,
229
+ );
230
+
231
+ const model = await waitFor(() => {
232
+ const layoutModel = engine.getModel<TrackingLayoutModel>(layout.uid);
233
+ expect(layoutModel?.currentLayoutRoute).toMatchObject({
234
+ type: 'page',
235
+ pageUid: 'page-1',
236
+ pathname: '/test/page-1',
237
+ });
238
+ return layoutModel as TrackingLayoutModel;
239
+ });
240
+
241
+ await router.navigate('/test/page-1/view/popup');
242
+
243
+ await waitFor(() => {
244
+ expect(model.currentLayoutRoute).toMatchObject({
245
+ type: 'page',
246
+ pageUid: 'page-1',
247
+ pathname: '/test/page-1/view/popup',
248
+ });
249
+ });
250
+
251
+ expect(clearEvents).not.toContainEqual({
252
+ routePathname: '/test/page-1',
253
+ before: '/test/page-1',
254
+ after: null,
255
+ });
256
+ });
257
+ });
258
+
259
+ describe('LayoutContentRoute', () => {
260
+ function setup(
261
+ initialEntry: string,
262
+ currentLayout: LayoutDefinition = layout,
263
+ ModelClass: typeof TestLayoutModel = TestLayoutModel,
264
+ ) {
265
+ const engine = new FlowEngine();
266
+ engine.registerModels({ TestLayoutModel, [ModelClass.name]: ModelClass });
267
+ engine.context.defineProperty('routeRepository', {
268
+ value: {
269
+ refreshAccessible: () => Promise.resolve(),
270
+ isAccessibleLoaded: () => true,
271
+ ensureAccessibleLoaded: () => Promise.resolve(),
272
+ getRouteBySchemaUid: () => ({ type: 'flowPage', schemaUid: 'page-1' }),
273
+ },
274
+ });
275
+ engine.context.defineProperty('app', {
276
+ value: {
277
+ getPublicPath: () => '/',
278
+ layoutManager: {
279
+ getLayout: () => currentLayout,
280
+ },
281
+ router: {
282
+ getBasename: () => '',
283
+ },
284
+ },
285
+ });
286
+ const model = engine.createModel<TestLayoutModel>({
287
+ uid: layout.uid,
288
+ use: ModelClass,
289
+ props: {
290
+ layout: currentLayout,
291
+ },
292
+ });
293
+ const layoutRoute = {
294
+ id: currentLayout.routeName,
295
+ path: currentLayout.routePath,
296
+ element: <Outlet />,
297
+ children: [
298
+ {
299
+ id: getLayoutPageRouteName(currentLayout.routeName),
300
+ path: ':name',
301
+ element: <LayoutContentRoute layoutRouteName={currentLayout.routeName} />,
302
+ },
303
+ {
304
+ id: getLayoutPageTabRouteName(currentLayout.routeName),
305
+ path: ':name/tab/:tabUid',
306
+ element: <LayoutContentRoute layoutRouteName={currentLayout.routeName} />,
307
+ },
308
+ {
309
+ id: getLayoutPageViewRouteName(currentLayout.routeName),
310
+ path: ':name/view/*',
311
+ element: <LayoutContentRoute layoutRouteName={currentLayout.routeName} />,
312
+ },
313
+ {
314
+ id: getLayoutPageTabViewRouteName(currentLayout.routeName),
315
+ path: ':name/tab/:tabUid/view/*',
316
+ element: <LayoutContentRoute layoutRouteName={currentLayout.routeName} />,
317
+ },
318
+ ],
319
+ };
320
+ let routes;
321
+ if (currentLayout.routeName === 'admin.settings.publicForms.layout') {
322
+ routes = [
323
+ {
324
+ id: 'admin.settings',
325
+ path: '/admin/settings',
326
+ element: <Outlet />,
327
+ children: [
328
+ {
329
+ id: 'admin.settings.publicForms',
330
+ path: '/admin/settings/public-forms',
331
+ element: <Outlet />,
332
+ children: [layoutRoute],
333
+ },
334
+ ],
335
+ },
336
+ ];
337
+ } else if (currentLayout.routeName.includes('.')) {
338
+ routes = [
339
+ {
340
+ id: 'admin.settings',
341
+ path: '/admin/settings',
342
+ element: <Outlet />,
343
+ children: [layoutRoute],
344
+ },
345
+ ];
346
+ } else {
347
+ routes = [layoutRoute];
348
+ }
349
+ const router = createMemoryRouter(routes, {
350
+ initialEntries: [initialEntry],
351
+ });
352
+
353
+ render(
354
+ <FlowEngineProvider engine={engine}>
355
+ <RouterProvider router={router} />
356
+ </FlowEngineProvider>,
357
+ );
358
+
359
+ return { model, router };
360
+ }
361
+
362
+ it('parses page route from standard layout route', async () => {
363
+ const { model } = setup('/test/page-1/tab/tab-1/view/popup');
364
+
365
+ await waitFor(() => {
366
+ expect(model.currentLayoutRoute).toMatchObject({
367
+ type: 'page',
368
+ basePathname: '/test',
369
+ pageUid: 'page-1',
370
+ tabUid: 'tab-1',
371
+ viewStack: [{ viewUid: 'page-1', tabUid: 'tab-1' }, { viewUid: 'popup' }],
372
+ });
373
+ });
374
+ });
375
+
376
+ it('parses nested layout route by matched layout pathname', async () => {
377
+ const nestedLayout: LayoutDefinition = {
378
+ ...layout,
379
+ routeName: 'admin.settings.publicForms',
380
+ routePath: 'public-forms',
381
+ rootRouteName: 'admin',
382
+ };
383
+ const { model } = setup('/admin/settings/public-forms/form-1/view/popup', nestedLayout);
384
+
385
+ await waitFor(() => {
386
+ expect(model.currentLayoutRoute).toMatchObject({
387
+ type: 'page',
388
+ basePathname: '/admin/settings/public-forms',
389
+ pageUid: 'form-1',
390
+ viewStack: [{ viewUid: 'form-1' }, { viewUid: 'popup' }],
391
+ });
392
+ });
393
+ });
394
+
395
+ it('parses empty nested layout route by matched layout pathname', async () => {
396
+ const nestedLayout: LayoutDefinition = {
397
+ ...layout,
398
+ routeName: 'admin.settings.publicForms.layout',
399
+ routePath: '',
400
+ rootRouteName: 'admin',
401
+ };
402
+ const { model } = setup('/admin/settings/public-forms/form-1/view/popup', nestedLayout);
403
+
404
+ await waitFor(() => {
405
+ expect(model.currentLayoutRoute).toMatchObject({
406
+ type: 'page',
407
+ basePathname: '/admin/settings/public-forms',
408
+ pageUid: 'form-1',
409
+ viewStack: [{ viewUid: 'form-1' }, { viewUid: 'popup' }],
410
+ });
411
+ });
412
+ });
413
+
414
+ it('clears local layout route when the content route unmounts', async () => {
415
+ const { model, router } = setup('/test/page-1/view/popup');
416
+
417
+ await waitFor(() => {
418
+ expect(model.currentLayoutRoute).toMatchObject({
419
+ type: 'page',
420
+ pageUid: 'page-1',
421
+ });
422
+ });
423
+
424
+ await router.navigate('/test');
425
+
426
+ await waitFor(() => {
427
+ expect(model.currentLayoutRoute).toBeNull();
428
+ });
429
+ });
430
+
431
+ it('does not clear local layout route while switching within the same page view stack', async () => {
432
+ const clearEvents: Array<{ routePathname?: string; before: string | null; after: string | null }> = [];
433
+
434
+ class TrackingLayoutModel extends TestLayoutModel {
435
+ clearLayoutRoute(routeLike?: Parameters<BaseLayoutModel['clearLayoutRoute']>[0]) {
436
+ const event: { routePathname?: string; before: string | null; after: string | null } = {
437
+ routePathname: routeLike?.pathname,
438
+ before: this.currentLayoutRoute?.pathname || null,
439
+ after: null,
440
+ };
441
+ super.clearLayoutRoute(routeLike);
442
+ event.after = this.currentLayoutRoute?.pathname || null;
443
+ clearEvents.push(event);
444
+ }
445
+ }
446
+
447
+ const { model, router } = setup('/test/page-1', layout, TrackingLayoutModel);
448
+
449
+ await waitFor(() => {
450
+ expect(model.currentLayoutRoute).toMatchObject({
451
+ type: 'page',
452
+ pageUid: 'page-1',
453
+ pathname: '/test/page-1',
454
+ });
455
+ });
456
+
457
+ await router.navigate('/test/page-1/view/popup');
458
+
459
+ await waitFor(() => {
460
+ expect(model.currentLayoutRoute).toMatchObject({
461
+ type: 'page',
462
+ pageUid: 'page-1',
463
+ pathname: '/test/page-1/view/popup',
464
+ });
465
+ });
466
+
467
+ expect(clearEvents).not.toContainEqual({
468
+ routePathname: '/test/page-1',
469
+ before: '/test/page-1',
470
+ after: null,
471
+ });
472
+ });
473
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ export * from './LayoutManager';
11
+ export * from './LayoutContentRoute';
12
+ export * from './LayoutRoute';
13
+ export * from './types';
14
+ export * from './utils';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ export interface LayoutRegisterOptions {
11
+ routeName: string;
12
+ routePath: string;
13
+ uid: string;
14
+ layoutModelClass: string;
15
+ rootPageModelClass?: string;
16
+ childPageModelClass?: string;
17
+ authCheck?: boolean;
18
+ }
19
+
20
+ export interface LayoutDefinition extends Required<LayoutRegisterOptions> {
21
+ rootRouteName: string;
22
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ export function getLayoutPageRouteName(routeName: string) {
11
+ return `${routeName}.__page`;
12
+ }
13
+
14
+ export function getLayoutPageTabRouteName(routeName: string) {
15
+ return `${routeName}.__pageTab`;
16
+ }
17
+
18
+ export function getLayoutPageViewRouteName(routeName: string) {
19
+ return `${routeName}.__pageView`;
20
+ }
21
+
22
+ export function getLayoutPageTabViewRouteName(routeName: string) {
23
+ return `${routeName}.__pageTabView`;
24
+ }
25
+
26
+ export function getLayoutContentRouteNames(routeName: string) {
27
+ return [
28
+ getLayoutPageRouteName(routeName),
29
+ getLayoutPageTabRouteName(routeName),
30
+ getLayoutPageViewRouteName(routeName),
31
+ getLayoutPageTabViewRouteName(routeName),
32
+ ];
33
+ }
34
+
35
+ export function isLayoutContentRouteName(routeName: string, targetRouteName?: string) {
36
+ return !!targetRouteName && getLayoutContentRouteNames(routeName).includes(targetRouteName);
37
+ }