@nocobase/client-v2 2.2.0-beta.7 → 2.2.0-beta.9
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.
- package/es/BaseApplication.d.ts +2 -0
- package/es/RouteRepository.d.ts +8 -0
- package/es/RouterManager.d.ts +15 -0
- package/es/authRedirect.d.ts +1 -0
- package/es/entry-actions/EntryActionManager.d.ts +24 -0
- package/es/entry-actions/index.d.ts +9 -0
- package/es/flow/actions/afterSuccess.d.ts +8 -1
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
- package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
- package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
- package/es/flow/models/base/ActionModelCore.d.ts +2 -0
- package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +9 -0
- package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
- package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
- package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
- package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
- package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
- package/es/flow/routeTransientInputArgs.d.ts +14 -0
- package/es/index.d.ts +2 -0
- package/es/index.mjs +243 -141
- package/es/utils/markdownSanitize.d.ts +11 -0
- package/lib/index.js +232 -130
- package/package.json +7 -7
- package/src/BaseApplication.tsx +2 -0
- package/src/RouteRepository.ts +25 -0
- package/src/RouterManager.tsx +142 -1
- package/src/__tests__/RouteRepository.test.ts +23 -0
- package/src/__tests__/RouterManager.test.ts +125 -0
- package/src/__tests__/authRedirect.test.ts +17 -0
- package/src/authRedirect.ts +38 -0
- package/src/collection-manager/field-configure.ts +1 -1
- package/src/collection-manager/interfaces/id.ts +1 -1
- package/src/collection-manager/interfaces/m2m.tsx +2 -2
- package/src/collection-manager/interfaces/m2o.tsx +2 -2
- package/src/collection-manager/interfaces/nanoid.ts +1 -1
- package/src/collection-manager/interfaces/o2m.tsx +2 -2
- package/src/collection-manager/interfaces/obo.tsx +2 -2
- package/src/collection-manager/interfaces/oho.tsx +2 -2
- package/src/collection-manager/interfaces/properties/index.ts +2 -2
- package/src/collection-manager/interfaces/uuid.ts +1 -1
- package/src/entry-actions/EntryActionManager.ts +76 -0
- package/src/entry-actions/index.ts +10 -0
- package/src/flow/FlowPage.tsx +29 -2
- package/src/flow/__tests__/FlowPage.test.tsx +152 -25
- package/src/flow/__tests__/FlowRoute.test.tsx +547 -2
- package/src/flow/__tests__/getKey.test.ts +7 -0
- package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
- package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
- package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +164 -0
- package/src/flow/actions/afterSuccess.tsx +142 -3
- package/src/flow/actions/dataScopeFilter.ts +10 -1
- package/src/flow/actions/dateTimeFormat.tsx +2 -2
- package/src/flow/actions/index.ts +1 -1
- package/src/flow/actions/openView.tsx +59 -5
- package/src/flow/admin-shell/BaseLayoutModel.tsx +149 -3
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +187 -42
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1020 -110
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +42 -4
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +177 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +20 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +5 -4
- package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
- package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +518 -2
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
- package/src/flow/admin-shell/admin-layout/index.ts +1 -0
- package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
- package/src/flow/common/Markdown/Display.tsx +14 -3
- package/src/flow/common/Markdown/Edit.tsx +19 -7
- package/src/flow/components/FlowRoute.tsx +128 -16
- package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
- package/src/flow/internal/components/Markdown/util.ts +2 -1
- package/src/flow/models/base/ActionModel.tsx +10 -8
- package/src/flow/models/base/ActionModelCore.tsx +11 -4
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
- package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
- package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
- package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
- package/src/flow/models/blocks/form/submitHandler.ts +17 -3
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +87 -0
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +91 -0
- package/src/flow/models/blocks/js-block/JSBlock.tsx +223 -2
- package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +35 -12
- package/src/flow/models/blocks/table/TableBlockModel.tsx +25 -14
- package/src/flow/models/blocks/table/TableColumnModel.tsx +6 -2
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +57 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +51 -0
- package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
- package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
- package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +45 -13
- package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
- package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
- package/src/flow/resolveViewParamsToViewList.tsx +11 -3
- package/src/flow/routeTransientInputArgs.ts +27 -0
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
- package/src/index.ts +2 -0
- package/src/nocobase-buildin-plugin/index.tsx +7 -1
- package/src/settings-center/SystemSettingsPage.tsx +1 -1
- package/src/utils/markdownSanitize.ts +88 -0
|
@@ -11,10 +11,12 @@ import React from 'react';
|
|
|
11
11
|
import { observable } from '@formily/reactive';
|
|
12
12
|
import { act, render, waitFor } from '@testing-library/react';
|
|
13
13
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
14
15
|
|
|
15
|
-
const { flowModelRendererSpy } = vi.hoisted(() => {
|
|
16
|
+
const { flowModelRendererSpy, adminLayoutContentEffectSpy } = vi.hoisted(() => {
|
|
16
17
|
return {
|
|
17
18
|
flowModelRendererSpy: vi.fn(),
|
|
19
|
+
adminLayoutContentEffectSpy: vi.fn(),
|
|
18
20
|
};
|
|
19
21
|
});
|
|
20
22
|
|
|
@@ -35,14 +37,62 @@ vi.mock('@nocobase/flow-engine', async (importOriginal) => {
|
|
|
35
37
|
};
|
|
36
38
|
});
|
|
37
39
|
|
|
40
|
+
vi.mock('@ant-design/pro-layout', async () => {
|
|
41
|
+
const ReactActual = await import('react');
|
|
42
|
+
const RouteContext = ReactActual.createContext({ isMobile: true });
|
|
43
|
+
const ProLayout = (props: { children?: React.ReactNode }) => {
|
|
44
|
+
return ReactActual.createElement(RouteContext.Provider, { value: { isMobile: true } }, props.children);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
default: ProLayout,
|
|
49
|
+
RouteContext,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
vi.mock('../AdminLayoutSlotModels', async (importOriginal) => {
|
|
54
|
+
const actual = await importOriginal<typeof import('../AdminLayoutSlotModels')>();
|
|
55
|
+
const ReactActual = await import('react');
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
...actual,
|
|
59
|
+
AdminLayoutContent: (props: { onContentElementChange?: (element: HTMLDivElement | null) => void }) => {
|
|
60
|
+
const bindContentRef = ReactActual.useCallback(
|
|
61
|
+
(element: HTMLDivElement | null) => {
|
|
62
|
+
props.onContentElementChange?.(element);
|
|
63
|
+
if (element) {
|
|
64
|
+
adminLayoutContentEffectSpy();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
[props.onContentElementChange],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return ReactActual.createElement('div', { ref: bindContentRef, 'data-testid': 'admin-layout-content' });
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
vi.mock('../useApplications', () => ({
|
|
76
|
+
useApplications: () => ({
|
|
77
|
+
Component: null,
|
|
78
|
+
appList: [],
|
|
79
|
+
}),
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
vi.mock('../AppListRender', () => ({
|
|
83
|
+
useAppListRender: () => undefined,
|
|
84
|
+
}));
|
|
85
|
+
|
|
38
86
|
import {
|
|
39
87
|
FlowEngine,
|
|
40
88
|
FlowEngineProvider,
|
|
41
89
|
FlowModelRenderer,
|
|
90
|
+
encodeOpenViewRouteState,
|
|
42
91
|
useFlowEngine,
|
|
43
92
|
type FlowModel,
|
|
44
93
|
} from '@nocobase/flow-engine';
|
|
45
|
-
import { AdminLayoutModel, getAdminLayoutModel } from '..';
|
|
94
|
+
import { AdminLayoutComponent, AdminLayoutModel, getAdminLayoutModel } from '..';
|
|
95
|
+
import { NocoBaseDesktopRouteType } from '../../../../flow-compat';
|
|
46
96
|
import { getLayoutPageRouteName, getLayoutPageViewRouteName } from '../../../../layout-manager/utils';
|
|
47
97
|
import { TopbarActionModel } from '../../../models/topbar/TopbarActionModel';
|
|
48
98
|
import { UserCenterTopbarActionModel } from '../../../models/topbar/UserCenterTopbarActionModel';
|
|
@@ -82,6 +132,7 @@ const TestAdminLayoutHost = (props) => {
|
|
|
82
132
|
describe('AdminLayoutModel runtime', () => {
|
|
83
133
|
beforeEach(() => {
|
|
84
134
|
flowModelRendererSpy.mockClear();
|
|
135
|
+
adminLayoutContentEffectSpy.mockReset();
|
|
85
136
|
});
|
|
86
137
|
|
|
87
138
|
it('should create model via getAdminLayoutModel and update props on rerender', async () => {
|
|
@@ -141,6 +192,46 @@ describe('AdminLayoutModel runtime', () => {
|
|
|
141
192
|
expect(model.context.layoutContentElement).toBeNull();
|
|
142
193
|
});
|
|
143
194
|
|
|
195
|
+
it('should expose mobile layout state before initial layout content effects run', async () => {
|
|
196
|
+
const engine = new FlowEngine();
|
|
197
|
+
engine.context.defineProperty('routeRepository', {
|
|
198
|
+
value: {
|
|
199
|
+
listAccessible: vi.fn(() => []),
|
|
200
|
+
subscribe: vi.fn(),
|
|
201
|
+
unsubscribe: vi.fn(),
|
|
202
|
+
ensureAccessibleLoaded: vi.fn(() => Promise.resolve()),
|
|
203
|
+
moveRoute: vi.fn(() => Promise.resolve()),
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
const model = getAdminLayoutModel<AdminLayoutModel>(engine, { create: true });
|
|
207
|
+
if (!model) {
|
|
208
|
+
throw new Error('[NocoBase] Failed to create admin-layout-model.');
|
|
209
|
+
}
|
|
210
|
+
const modelLifecycle = model as unknown as { onMount: () => void; onUnmount: () => void };
|
|
211
|
+
modelLifecycle.onMount();
|
|
212
|
+
const observedMobileStates: boolean[] = [];
|
|
213
|
+
adminLayoutContentEffectSpy.mockImplementation(() => {
|
|
214
|
+
observedMobileStates.push(model.context.isMobileLayout);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const { unmount } = render(
|
|
218
|
+
<FlowEngineProvider engine={engine}>
|
|
219
|
+
<MemoryRouter initialEntries={['/admin/lrmg36pcahi']}>
|
|
220
|
+
<AdminLayoutComponent model={model} />
|
|
221
|
+
</MemoryRouter>
|
|
222
|
+
</FlowEngineProvider>,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
await waitFor(() => {
|
|
226
|
+
expect(adminLayoutContentEffectSpy).toHaveBeenCalledTimes(1);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(observedMobileStates).toEqual([true]);
|
|
230
|
+
|
|
231
|
+
unmount();
|
|
232
|
+
modelLifecycle.onUnmount();
|
|
233
|
+
});
|
|
234
|
+
|
|
144
235
|
it('should expose layout definition only while mounted', async () => {
|
|
145
236
|
const engine = new FlowEngine();
|
|
146
237
|
const { unmount } = render(
|
|
@@ -213,6 +304,211 @@ describe('AdminLayoutModel runtime', () => {
|
|
|
213
304
|
});
|
|
214
305
|
});
|
|
215
306
|
|
|
307
|
+
it('should keep route state when route page registers after route sync', async () => {
|
|
308
|
+
const engine = new FlowEngine();
|
|
309
|
+
|
|
310
|
+
render(
|
|
311
|
+
<FlowEngineProvider engine={engine}>
|
|
312
|
+
<TestAdminLayoutHost />
|
|
313
|
+
</FlowEngineProvider>,
|
|
314
|
+
);
|
|
315
|
+
const model = engine.getModel<TestAdminLayoutModel>('admin-layout-model');
|
|
316
|
+
const routeState = {
|
|
317
|
+
__nocobaseOpenViewInputArgs: {
|
|
318
|
+
popup: {
|
|
319
|
+
formData: {
|
|
320
|
+
start: '2026-06-24',
|
|
321
|
+
end: '2026-06-25',
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
act(() => {
|
|
328
|
+
model.syncLayoutRoute({
|
|
329
|
+
name: getLayoutPageViewRouteName('admin'),
|
|
330
|
+
pathname: '/admin/page-1/view/popup',
|
|
331
|
+
layoutBasePathname: '/admin',
|
|
332
|
+
state: routeState,
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const coordinator = (model as any).getCoordinator();
|
|
337
|
+
const syncRoute = vi.spyOn(coordinator, 'syncRoute').mockImplementation(() => undefined);
|
|
338
|
+
|
|
339
|
+
act(() => {
|
|
340
|
+
model.registerRoutePage('page-1', {
|
|
341
|
+
active: true,
|
|
342
|
+
layoutContentElement: document.createElement('div'),
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(syncRoute).toHaveBeenCalledWith(
|
|
347
|
+
expect.objectContaining({
|
|
348
|
+
pageUid: 'page-1',
|
|
349
|
+
pathname: '/admin/page-1/view/popup',
|
|
350
|
+
state: routeState,
|
|
351
|
+
}),
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should parse RunJS openView route params into layout route view stack state', async () => {
|
|
356
|
+
const token = encodeOpenViewRouteState('popup', { mode: 'dialog', size: 'large' });
|
|
357
|
+
if (!token) {
|
|
358
|
+
throw new Error('Expected openView route state token.');
|
|
359
|
+
}
|
|
360
|
+
const engine = new FlowEngine();
|
|
361
|
+
engine.context.defineProperty('routeRepository', {
|
|
362
|
+
value: {
|
|
363
|
+
getRouteBySchemaUid: (pageUid: string) => ({ title: pageUid }),
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
render(
|
|
368
|
+
<FlowEngineProvider engine={engine}>
|
|
369
|
+
<TestAdminLayoutHost />
|
|
370
|
+
</FlowEngineProvider>,
|
|
371
|
+
);
|
|
372
|
+
const model = engine.getModel<TestAdminLayoutModel>('admin-layout-model');
|
|
373
|
+
expect(model).toBeTruthy();
|
|
374
|
+
|
|
375
|
+
act(() => {
|
|
376
|
+
model.syncLayoutRoute({
|
|
377
|
+
name: getLayoutPageViewRouteName('admin'),
|
|
378
|
+
pathname: `/admin/page-1/view/popup/opts/${token}/filterbytk/1`,
|
|
379
|
+
layoutBasePathname: '/admin',
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await waitFor(() => {
|
|
384
|
+
expect(model.context.layoutRoute).toMatchObject({
|
|
385
|
+
type: 'page',
|
|
386
|
+
pageUid: 'page-1',
|
|
387
|
+
viewStack: [
|
|
388
|
+
{ viewUid: 'page-1' },
|
|
389
|
+
{
|
|
390
|
+
viewUid: 'popup',
|
|
391
|
+
openViewRouteState: { mode: 'dialog', size: 'large' },
|
|
392
|
+
filterByTk: '1',
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should reject malformed RunJS openView route params', async () => {
|
|
400
|
+
const wrongViewToken = encodeOpenViewRouteState('other-popup', { mode: 'dialog', size: 'large' });
|
|
401
|
+
if (!wrongViewToken) {
|
|
402
|
+
throw new Error('Expected openView route state token.');
|
|
403
|
+
}
|
|
404
|
+
const engine = new FlowEngine();
|
|
405
|
+
|
|
406
|
+
render(
|
|
407
|
+
<FlowEngineProvider engine={engine}>
|
|
408
|
+
<TestAdminLayoutHost />
|
|
409
|
+
</FlowEngineProvider>,
|
|
410
|
+
);
|
|
411
|
+
const model = engine.getModel<TestAdminLayoutModel>('admin-layout-model');
|
|
412
|
+
expect(model).toBeTruthy();
|
|
413
|
+
|
|
414
|
+
[
|
|
415
|
+
'/admin/page-1/sourceid',
|
|
416
|
+
'/admin/page-1/AbCdEfGh/filterbytk/1',
|
|
417
|
+
'/admin/page-1/view/popup/AbCdEfGh/filterbytk/1',
|
|
418
|
+
'/admin/page-1/view/popup/opts/AbCdEfGh/filterbytk/1',
|
|
419
|
+
'/admin/page-1/view/popup/openviewmode/dialog',
|
|
420
|
+
'/admin/page-1/view/popup/openviewsize/large',
|
|
421
|
+
`/admin/page-1/view/popup/opts/${wrongViewToken}/filterbytk/1`,
|
|
422
|
+
].forEach((pathname) => {
|
|
423
|
+
expect(
|
|
424
|
+
model.resolveLayoutRoute({
|
|
425
|
+
name: getLayoutPageViewRouteName('admin'),
|
|
426
|
+
pathname,
|
|
427
|
+
layoutBasePathname: '/admin',
|
|
428
|
+
}),
|
|
429
|
+
).toMatchObject({
|
|
430
|
+
type: 'notFound',
|
|
431
|
+
pathname,
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('should resolve group id paths as blank root routes', async () => {
|
|
437
|
+
const engine = new FlowEngine();
|
|
438
|
+
engine.context.defineProperty('routeRepository', {
|
|
439
|
+
value: {
|
|
440
|
+
getRouteById: (routeId: string) =>
|
|
441
|
+
routeId === '1'
|
|
442
|
+
? {
|
|
443
|
+
id: 1,
|
|
444
|
+
type: NocoBaseDesktopRouteType.group,
|
|
445
|
+
}
|
|
446
|
+
: undefined,
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
render(
|
|
451
|
+
<FlowEngineProvider engine={engine}>
|
|
452
|
+
<TestAdminLayoutHost />
|
|
453
|
+
</FlowEngineProvider>,
|
|
454
|
+
);
|
|
455
|
+
const model = engine.getModel<TestAdminLayoutModel>('admin-layout-model');
|
|
456
|
+
expect(model).toBeTruthy();
|
|
457
|
+
|
|
458
|
+
expect(
|
|
459
|
+
model.resolveLayoutRoute({
|
|
460
|
+
name: getLayoutPageRouteName('admin'),
|
|
461
|
+
pathname: '/admin/1',
|
|
462
|
+
layoutBasePathname: '/admin',
|
|
463
|
+
}),
|
|
464
|
+
).toMatchObject({
|
|
465
|
+
type: 'root',
|
|
466
|
+
pathname: '/admin/1',
|
|
467
|
+
relativePath: '1',
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('should prefer schema uid routes before treating numeric paths as group ids', async () => {
|
|
472
|
+
const engine = new FlowEngine();
|
|
473
|
+
engine.context.defineProperty('routeRepository', {
|
|
474
|
+
value: {
|
|
475
|
+
getRouteBySchemaUid: (pageUid: string) =>
|
|
476
|
+
pageUid === '1'
|
|
477
|
+
? {
|
|
478
|
+
schemaUid: '1',
|
|
479
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
480
|
+
}
|
|
481
|
+
: undefined,
|
|
482
|
+
getRouteById: (routeId: string) =>
|
|
483
|
+
routeId === '1'
|
|
484
|
+
? {
|
|
485
|
+
id: 1,
|
|
486
|
+
type: NocoBaseDesktopRouteType.group,
|
|
487
|
+
}
|
|
488
|
+
: undefined,
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
render(
|
|
493
|
+
<FlowEngineProvider engine={engine}>
|
|
494
|
+
<TestAdminLayoutHost />
|
|
495
|
+
</FlowEngineProvider>,
|
|
496
|
+
);
|
|
497
|
+
const model = engine.getModel<TestAdminLayoutModel>('admin-layout-model');
|
|
498
|
+
expect(model).toBeTruthy();
|
|
499
|
+
|
|
500
|
+
expect(
|
|
501
|
+
model.resolveLayoutRoute({
|
|
502
|
+
name: getLayoutPageRouteName('admin'),
|
|
503
|
+
pathname: '/admin/1',
|
|
504
|
+
layoutBasePathname: '/admin',
|
|
505
|
+
}),
|
|
506
|
+
).toMatchObject({
|
|
507
|
+
type: 'page',
|
|
508
|
+
pageUid: '1',
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
216
512
|
it('should not consume global routes that belong to nested layouts', async () => {
|
|
217
513
|
const engine = new FlowEngine();
|
|
218
514
|
const routeRef = observable.ref({
|
|
@@ -313,6 +609,226 @@ describe('AdminLayoutModel runtime', () => {
|
|
|
313
609
|
});
|
|
314
610
|
});
|
|
315
611
|
|
|
612
|
+
it('should restore layout route from router context when a route page registers after stale cleanup', async () => {
|
|
613
|
+
const engine = new FlowEngine();
|
|
614
|
+
engine.context.defineProperty('route', {
|
|
615
|
+
value: {
|
|
616
|
+
name: 'admin.page',
|
|
617
|
+
pathname: '/admin/page-1',
|
|
618
|
+
params: { name: 'page-1' },
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
engine.context.defineProperty('routeRepository', {
|
|
622
|
+
value: {
|
|
623
|
+
getRouteBySchemaUid: (pageUid: string) => ({ title: pageUid }),
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
render(
|
|
628
|
+
<FlowEngineProvider engine={engine}>
|
|
629
|
+
<TestAdminLayoutHost />
|
|
630
|
+
</FlowEngineProvider>,
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
const model = engine.getModel<TestAdminLayoutModel>('admin-layout-model');
|
|
634
|
+
expect(model).toBeTruthy();
|
|
635
|
+
const routeLike = {
|
|
636
|
+
name: 'admin.page',
|
|
637
|
+
pathname: '/admin/page-1',
|
|
638
|
+
layoutRouteName: 'admin',
|
|
639
|
+
layoutBasePathname: '/admin',
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
act(() => {
|
|
643
|
+
model.syncLayoutRoute(routeLike);
|
|
644
|
+
model.clearLayoutRoute(routeLike);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
expect(model.context.layoutRoute).toBeNull();
|
|
648
|
+
const syncRouteSpy = vi.spyOn(model.getCoordinator(), 'syncRoute');
|
|
649
|
+
syncRouteSpy.mockClear();
|
|
650
|
+
|
|
651
|
+
act(() => {
|
|
652
|
+
model.registerRoutePage('page-1', {
|
|
653
|
+
active: true,
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
await waitFor(() => {
|
|
658
|
+
expect(model.context.layoutRoute).toMatchObject({
|
|
659
|
+
type: 'page',
|
|
660
|
+
pageUid: 'page-1',
|
|
661
|
+
pathname: '/admin/page-1',
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
expect(model.context.currentRoute.title).toBe('page-1');
|
|
665
|
+
expect(syncRouteSpy).toHaveBeenLastCalledWith(
|
|
666
|
+
expect.objectContaining({
|
|
667
|
+
layoutRouteName: 'admin',
|
|
668
|
+
pageUid: 'page-1',
|
|
669
|
+
pathname: '/admin/page-1',
|
|
670
|
+
layoutBasePathname: '/admin',
|
|
671
|
+
}),
|
|
672
|
+
);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('should restore layout route from a legacy dotted page view route name', async () => {
|
|
676
|
+
const engine = new FlowEngine();
|
|
677
|
+
engine.context.defineProperty('route', {
|
|
678
|
+
value: {
|
|
679
|
+
name: 'admin.page.view',
|
|
680
|
+
pathname: '/admin/page-1/view/popup',
|
|
681
|
+
params: { name: 'page-1' },
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
engine.context.defineProperty('routeRepository', {
|
|
685
|
+
value: {
|
|
686
|
+
getRouteBySchemaUid: (pageUid: string) => ({ title: pageUid }),
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
render(
|
|
691
|
+
<FlowEngineProvider engine={engine}>
|
|
692
|
+
<TestAdminLayoutHost />
|
|
693
|
+
</FlowEngineProvider>,
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
const model = engine.getModel<TestAdminLayoutModel>('admin-layout-model');
|
|
697
|
+
expect(model).toBeTruthy();
|
|
698
|
+
|
|
699
|
+
act(() => {
|
|
700
|
+
model.registerRoutePage('page-1', {
|
|
701
|
+
active: true,
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
expect(model.context.layoutRoute).toMatchObject({
|
|
706
|
+
type: 'page',
|
|
707
|
+
pageUid: 'page-1',
|
|
708
|
+
pathname: '/admin/page-1/view/popup',
|
|
709
|
+
viewStack: [{ viewUid: 'page-1' }, { viewUid: 'popup' }],
|
|
710
|
+
});
|
|
711
|
+
expect(model.context.currentRoute.title).toBe('page-1');
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('should not restore layout route from another layout when a route page registers', async () => {
|
|
715
|
+
const engine = new FlowEngine();
|
|
716
|
+
engine.context.defineProperty('route', {
|
|
717
|
+
value: {
|
|
718
|
+
name: 'admin.settings.publicForms.page',
|
|
719
|
+
pathname: '/admin/settings/public-forms/form-1',
|
|
720
|
+
params: { name: 'form-1' },
|
|
721
|
+
layoutRouteName: 'admin.settings.publicForms',
|
|
722
|
+
layoutBasePathname: '/admin/settings/public-forms',
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
engine.context.defineProperty('routeRepository', {
|
|
726
|
+
value: {
|
|
727
|
+
getRouteBySchemaUid: (pageUid: string) => ({ title: pageUid }),
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
render(
|
|
732
|
+
<FlowEngineProvider engine={engine}>
|
|
733
|
+
<TestAdminLayoutHost />
|
|
734
|
+
</FlowEngineProvider>,
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
const model = engine.getModel<TestAdminLayoutModel>('admin-layout-model');
|
|
738
|
+
expect(model).toBeTruthy();
|
|
739
|
+
|
|
740
|
+
act(() => {
|
|
741
|
+
model.registerRoutePage('form-1', {
|
|
742
|
+
active: true,
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
expect(model.context.layoutRoute).toBeNull();
|
|
747
|
+
expect(model.context.currentRoute).toEqual({});
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('should not restore layout route from a nested layout route without layoutRouteName', async () => {
|
|
751
|
+
const engine = new FlowEngine();
|
|
752
|
+
engine.context.defineProperty('route', {
|
|
753
|
+
value: {
|
|
754
|
+
name: 'admin.settings.publicForms.page',
|
|
755
|
+
pathname: '/admin/settings/public-forms/form-1',
|
|
756
|
+
params: { name: 'form-1' },
|
|
757
|
+
layoutBasePathname: '/admin/settings/public-forms',
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
engine.context.defineProperty('routeRepository', {
|
|
761
|
+
value: {
|
|
762
|
+
getRouteBySchemaUid: (pageUid: string) => ({ title: pageUid }),
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
render(
|
|
767
|
+
<FlowEngineProvider engine={engine}>
|
|
768
|
+
<TestAdminLayoutHost />
|
|
769
|
+
</FlowEngineProvider>,
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
const model = engine.getModel<TestAdminLayoutModel>('admin-layout-model');
|
|
773
|
+
expect(model).toBeTruthy();
|
|
774
|
+
|
|
775
|
+
act(() => {
|
|
776
|
+
model.registerRoutePage('form-1', {
|
|
777
|
+
active: true,
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
expect(model.context.layoutRoute).toBeNull();
|
|
782
|
+
expect(model.context.currentRoute).toEqual({});
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('should ignore stale layout route cleanup after a route page has registered on the same path', async () => {
|
|
786
|
+
const engine = new FlowEngine();
|
|
787
|
+
engine.context.defineProperty('route', {
|
|
788
|
+
value: {
|
|
789
|
+
name: 'admin.page',
|
|
790
|
+
pathname: '/admin/page-1',
|
|
791
|
+
params: { name: 'page-1' },
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
engine.context.defineProperty('routeRepository', {
|
|
795
|
+
value: {
|
|
796
|
+
getRouteBySchemaUid: (pageUid: string) => ({ title: pageUid }),
|
|
797
|
+
},
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
render(
|
|
801
|
+
<FlowEngineProvider engine={engine}>
|
|
802
|
+
<TestAdminLayoutHost />
|
|
803
|
+
</FlowEngineProvider>,
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
const model = engine.getModel<TestAdminLayoutModel>('admin-layout-model');
|
|
807
|
+
expect(model).toBeTruthy();
|
|
808
|
+
const staleRouteLike = {
|
|
809
|
+
name: 'admin.page',
|
|
810
|
+
pathname: '/admin/page-1',
|
|
811
|
+
layoutRouteName: 'admin',
|
|
812
|
+
layoutBasePathname: '/admin',
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
act(() => {
|
|
816
|
+
model.syncLayoutRoute(staleRouteLike);
|
|
817
|
+
model.clearLayoutRoute(staleRouteLike);
|
|
818
|
+
model.registerRoutePage('page-1', {
|
|
819
|
+
active: true,
|
|
820
|
+
});
|
|
821
|
+
model.clearLayoutRoute(staleRouteLike);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
expect(model.context.layoutRoute).toMatchObject({
|
|
825
|
+
type: 'page',
|
|
826
|
+
pageUid: 'page-1',
|
|
827
|
+
pathname: '/admin/page-1',
|
|
828
|
+
});
|
|
829
|
+
expect(model.context.currentRoute.title).toBe('page-1');
|
|
830
|
+
});
|
|
831
|
+
|
|
316
832
|
it('should keep pageActive in sync after non-active route page updates', async () => {
|
|
317
833
|
const engine = new FlowEngine();
|
|
318
834
|
engine.context.defineProperty('routeRepository', {
|
|
@@ -222,6 +222,31 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
222
222
|
expect(link).toHaveAttribute('rel', expect.stringContaining('noreferrer'));
|
|
223
223
|
});
|
|
224
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
|
+
|
|
225
250
|
it('should keep regular admin settings as SPA links inside admin runtime', () => {
|
|
226
251
|
const items = getTopbarPluginSettingsItems({
|
|
227
252
|
canManagePlugins: false,
|
|
@@ -245,6 +270,29 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
245
270
|
expect(link).not.toHaveAttribute('target', '_blank');
|
|
246
271
|
});
|
|
247
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
|
+
|
|
248
296
|
it('should not treat admin-like paths as admin runtime', () => {
|
|
249
297
|
const items = getTopbarPluginSettingsItems({
|
|
250
298
|
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';
|