@nocobase/client-v2 2.3.0-beta.6 → 2.4.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/acl/ACLProvider.d.ts +10 -0
- package/es/acl/aclCheckReadiness.d.ts +11 -0
- package/es/authRedirect.d.ts +11 -2
- package/es/components/AttachmentUpload.d.ts +51 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
- package/es/index.d.ts +4 -1
- package/es/index.mjs +129 -118
- package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
- package/es/nocobase-buildin-plugin/index.d.ts +10 -0
- package/es/settings-app/SettingsApplication.d.ts +18 -0
- package/es/settings-app/SettingsBrand.d.ts +16 -0
- package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
- package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
- package/es/settings-app/SettingsGroupNav.d.ts +18 -0
- package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
- package/es/settings-app/SettingsRouterManager.d.ts +15 -0
- package/es/settings-app/SettingsSearch.d.ts +17 -0
- package/es/settings-app/SettingsShell.d.ts +10 -0
- package/es/settings-app/isSettingsApp.d.ts +18 -0
- package/es/settings-app/runtimePaths.d.ts +18 -0
- package/es/settings-app/settingsDocumentPath.d.ts +14 -0
- package/es/settings-app/settingsTheme.d.ts +87 -0
- package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
- package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
- package/es/settings-center/groups.d.ts +66 -0
- package/es/settings-center/useSettingsGroups.d.ts +36 -0
- package/es/settings-center/useSettingsSearch.d.ts +35 -0
- package/es/settings-center/utils.d.ts +24 -0
- package/es/theme/index.d.ts +1 -0
- package/es/theme/itemActive.d.ts +24 -0
- package/es/theme/type.d.ts +9 -0
- package/lib/index.js +157 -146
- package/package.json +7 -7
- package/src/__tests__/app.test.tsx +122 -3
- package/src/__tests__/authRedirect.test.ts +154 -0
- package/src/__tests__/browserChecker.test.ts +172 -6
- package/src/__tests__/mockSettingsApplication.ts +29 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +81 -8
- package/src/__tests__/plugin-manager.test.tsx +7 -6
- package/src/__tests__/settings-application.test.ts +164 -0
- package/src/__tests__/settings-auth-routes.test.ts +71 -0
- package/src/__tests__/settings-center.test.tsx +168 -130
- package/src/__tests__/settings-layout-root.test.tsx +600 -0
- package/src/__tests__/settings-runtime-paths.test.ts +99 -0
- package/src/__tests__/settings-search-shortcut.test.tsx +321 -0
- package/src/__tests__/settings-shell.test.tsx +261 -0
- package/src/acl/ACLProvider.tsx +53 -3
- package/src/acl/aclCheckReadiness.ts +44 -0
- package/src/authRedirect.ts +85 -8
- package/src/components/AppComponents.tsx +16 -7
- package/src/components/AttachmentUpload.tsx +275 -0
- package/src/components/index.ts +1 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -56
- package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
- package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
- package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
- package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
- package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
- package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
- package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
- package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
- package/src/index.ts +11 -1
- package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
- package/src/nocobase-buildin-plugin/index.tsx +112 -71
- package/src/settings-app/SettingsApplication.ts +31 -0
- package/src/settings-app/SettingsBrand.tsx +108 -0
- package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
- package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
- package/src/settings-app/SettingsGroupNav.tsx +124 -0
- package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
- package/src/settings-app/SettingsRouterManager.ts +92 -0
- package/src/settings-app/SettingsSearch.tsx +343 -0
- package/src/settings-app/SettingsShell.tsx +184 -0
- package/src/settings-app/isSettingsApp.ts +21 -0
- package/src/settings-app/runtimePaths.ts +103 -0
- package/src/settings-app/settingsDocumentPath.ts +66 -0
- package/src/settings-app/settingsTheme.ts +369 -0
- package/src/settings-app/useSettingsThemeConfig.ts +100 -0
- package/src/settings-center/AdminSettingsLayout.tsx +154 -169
- package/src/settings-center/SystemSettingsPage.tsx +4 -170
- package/src/settings-center/__tests__/groups.test.ts +25 -0
- package/src/settings-center/groups.ts +108 -0
- package/src/settings-center/useSettingsGroups.ts +105 -0
- package/src/settings-center/useSettingsSearch.ts +173 -0
- package/src/settings-center/utils.tsx +50 -1
- package/src/theme/index.tsx +1 -0
- package/src/theme/itemActive.ts +31 -0
- package/src/theme/type.ts +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0-alpha.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"@formily/antd-v5": "1.2.3",
|
|
28
28
|
"@formily/react": "^2.2.27",
|
|
29
29
|
"@formily/shared": "^2.2.27",
|
|
30
|
-
"@nocobase/evaluators": "2.
|
|
31
|
-
"@nocobase/flow-engine": "2.
|
|
32
|
-
"@nocobase/sdk": "2.
|
|
33
|
-
"@nocobase/shared": "2.
|
|
34
|
-
"@nocobase/utils": "2.
|
|
30
|
+
"@nocobase/evaluators": "2.4.0-alpha.2",
|
|
31
|
+
"@nocobase/flow-engine": "2.4.0-alpha.2",
|
|
32
|
+
"@nocobase/sdk": "2.4.0-alpha.2",
|
|
33
|
+
"@nocobase/shared": "2.4.0-alpha.2",
|
|
34
|
+
"@nocobase/utils": "2.4.0-alpha.2",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -48,5 +48,5 @@
|
|
|
48
48
|
"react-i18next": "^11.15.1",
|
|
49
49
|
"react-router-dom": "^6.30.1"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "706bb81047a05f39f2ddce781ebcafeea4ec9cd5"
|
|
52
52
|
}
|
|
@@ -379,7 +379,11 @@ describe('app', () => {
|
|
|
379
379
|
expect(screen.getByText('maintaining dialog message')).toBeInTheDocument();
|
|
380
380
|
});
|
|
381
381
|
|
|
382
|
-
it(
|
|
382
|
+
it.each([
|
|
383
|
+
['pm.enable', 'Enabling plugin'],
|
|
384
|
+
['pm.disable', 'Disabling plugin'],
|
|
385
|
+
['future.command', 'future.command'],
|
|
386
|
+
])('should keep current content behind the default dialog while app commanding: %s', async (commandName, title) => {
|
|
383
387
|
const CurrentPage = () => {
|
|
384
388
|
const [count, setCount] = React.useState(0);
|
|
385
389
|
return <button onClick={() => setCount((value) => value + 1)}>Current page count: {count}</button>;
|
|
@@ -398,12 +402,127 @@ describe('app', () => {
|
|
|
398
402
|
act(() => {
|
|
399
403
|
app.maintained = true;
|
|
400
404
|
app.maintaining = true;
|
|
401
|
-
app.error = { code: 'APP_COMMANDING', command: { name:
|
|
405
|
+
app.error = { code: 'APP_COMMANDING', command: { name: commandName }, message: 'starting sub applications...' };
|
|
402
406
|
});
|
|
403
407
|
|
|
404
408
|
expect(screen.getByRole('button', { name: 'Current page count: 1' })).toBeInTheDocument();
|
|
405
409
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
406
|
-
expect(screen.getByText(
|
|
410
|
+
expect(screen.getByText(title)).toBeInTheDocument();
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('should keep current content without a dialog while app upgrading', async () => {
|
|
414
|
+
const CurrentPage = () => <div>Current page</div>;
|
|
415
|
+
|
|
416
|
+
class PluginHelloClient extends Plugin {
|
|
417
|
+
async load() {
|
|
418
|
+
this.router.add('root', { path: '/', Component: CurrentPage });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const app = createMockClient({ plugins: [PluginHelloClient] });
|
|
423
|
+
await renderApp(app);
|
|
424
|
+
|
|
425
|
+
act(() => {
|
|
426
|
+
app.maintained = true;
|
|
427
|
+
app.maintaining = true;
|
|
428
|
+
app.error = Object.assign(new Error('Loading data sources...'), {
|
|
429
|
+
code: 'APP_COMMANDING',
|
|
430
|
+
command: { name: 'upgrade' },
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
expect(screen.getByText('Current page')).toBeInTheDocument();
|
|
435
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it.each([
|
|
439
|
+
['upgrade', 'App upgrading'],
|
|
440
|
+
['pm.enable', 'Enabling plugin'],
|
|
441
|
+
['pm.disable', 'Disabling plugin'],
|
|
442
|
+
['future.command', 'future.command'],
|
|
443
|
+
])('should not show Try again for an app commanding error state: %s', async (commandName, title) => {
|
|
444
|
+
class PluginHelloClient extends Plugin {}
|
|
445
|
+
const app = createMockClient({ plugins: [PluginHelloClient] });
|
|
446
|
+
app.error = Object.assign(new Error('Loading data sources...'), {
|
|
447
|
+
code: 'APP_COMMANDING',
|
|
448
|
+
command: { name: commandName },
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
await renderApp(app);
|
|
452
|
+
|
|
453
|
+
expect(screen.getByText(title)).toBeInTheDocument();
|
|
454
|
+
expect(screen.queryByRole('button', { name: 'Try again' })).not.toBeInTheDocument();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('should clear upgrade feedback after an existing page error', async () => {
|
|
458
|
+
const BrokenPage = () => {
|
|
459
|
+
throw new Error('page error');
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
class PluginHelloClient extends Plugin {
|
|
463
|
+
async load() {
|
|
464
|
+
this.router.add('root', { path: '/', Component: BrokenPage });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const app = createMockClient({
|
|
469
|
+
plugins: [PluginHelloClient],
|
|
470
|
+
ws: { url: 'ws://localhost:3000/ws' },
|
|
471
|
+
});
|
|
472
|
+
app.maintained = true;
|
|
473
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
474
|
+
try {
|
|
475
|
+
await renderApp(app);
|
|
476
|
+
expect(screen.getByRole('button', { name: 'Try again' })).toBeInTheDocument();
|
|
477
|
+
|
|
478
|
+
act(() => {
|
|
479
|
+
app.ws.emit('message', {
|
|
480
|
+
type: 'maintaining',
|
|
481
|
+
payload: {
|
|
482
|
+
code: 'APP_COMMANDING',
|
|
483
|
+
command: { name: 'upgrade' },
|
|
484
|
+
message: 'Loading data sources...',
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
expect(screen.getByText('App upgrading')).toBeInTheDocument();
|
|
490
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
491
|
+
expect(screen.queryByRole('button', { name: 'Try again' })).not.toBeInTheDocument();
|
|
492
|
+
} finally {
|
|
493
|
+
errorSpy.mockRestore();
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('should keep a custom maintaining dialog while app upgrading', async () => {
|
|
498
|
+
class PluginHelloClient extends Plugin {}
|
|
499
|
+
const app = createMockClient({ plugins: [PluginHelloClient] });
|
|
500
|
+
app.addComponents({
|
|
501
|
+
CustomUpgradeStatus: () => <div>Custom upgrade status</div>,
|
|
502
|
+
});
|
|
503
|
+
app.maintained = true;
|
|
504
|
+
app.maintaining = true;
|
|
505
|
+
app.error = Object.assign(new Error('Loading data sources...'), {
|
|
506
|
+
code: 'APP_COMMANDING',
|
|
507
|
+
command: {
|
|
508
|
+
name: 'upgrade',
|
|
509
|
+
components: { maintainingDialog: 'CustomUpgradeStatus' },
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
await renderApp(app);
|
|
514
|
+
|
|
515
|
+
expect(screen.getByText('Custom upgrade status')).toBeInTheDocument();
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('should keep Try again for recoverable app errors', async () => {
|
|
519
|
+
class PluginHelloClient extends Plugin {}
|
|
520
|
+
const app = createMockClient({ plugins: [PluginHelloClient] });
|
|
521
|
+
app.error = Object.assign(new Error('load error'), { code: 'LOAD_ERROR' });
|
|
522
|
+
|
|
523
|
+
await renderApp(app);
|
|
524
|
+
|
|
525
|
+
expect(screen.getByRole('button', { name: 'Try again' })).toBeInTheDocument();
|
|
407
526
|
});
|
|
408
527
|
|
|
409
528
|
it('should handle long loading state gracefully', async () => {
|
|
@@ -17,12 +17,18 @@ import {
|
|
|
17
17
|
|
|
18
18
|
describe('auth redirect helpers', () => {
|
|
19
19
|
const originalLocation = globalThis.window.location;
|
|
20
|
+
const originalModernClientPrefix = window.__nocobase_modern_client_prefix__;
|
|
20
21
|
|
|
21
22
|
afterEach(() => {
|
|
22
23
|
Object.defineProperty(globalThis.window, 'location', {
|
|
23
24
|
configurable: true,
|
|
24
25
|
value: originalLocation,
|
|
25
26
|
});
|
|
27
|
+
if (originalModernClientPrefix === undefined) {
|
|
28
|
+
delete window.__nocobase_modern_client_prefix__;
|
|
29
|
+
} else {
|
|
30
|
+
window.__nocobase_modern_client_prefix__ = originalModernClientPrefix;
|
|
31
|
+
}
|
|
26
32
|
vi.restoreAllMocks();
|
|
27
33
|
});
|
|
28
34
|
|
|
@@ -84,6 +90,107 @@ describe('auth redirect helpers', () => {
|
|
|
84
90
|
expect(buildV2SigninHref(app, '/v2/admin/7vu4c2sdk6h')).toBe('/v2/signin?redirect=%2Fv2%2Fadmin%2F7vu4c2sdk6h');
|
|
85
91
|
});
|
|
86
92
|
|
|
93
|
+
it('should keep validated standalone settings redirects outside the v2 basename', () => {
|
|
94
|
+
const app = {
|
|
95
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
96
|
+
router: {
|
|
97
|
+
getBasename: () => '/nocobase/v2',
|
|
98
|
+
},
|
|
99
|
+
} as any;
|
|
100
|
+
|
|
101
|
+
expect(normalizeV2RedirectPath(app, '/nocobase/settings/workflow?tab=list#recent')).toBe(
|
|
102
|
+
'/nocobase/settings/workflow?tab=list#recent',
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should build the standalone settings signin URL for a standalone settings app', () => {
|
|
107
|
+
const app = {
|
|
108
|
+
name: 'main',
|
|
109
|
+
getPublicPath: () => '/nocobase/',
|
|
110
|
+
router: {
|
|
111
|
+
getBasename: () => '/nocobase',
|
|
112
|
+
},
|
|
113
|
+
pluginSettingsManager: {
|
|
114
|
+
getRouteName: () => 'settings.',
|
|
115
|
+
getRoutePath: () => '/settings/',
|
|
116
|
+
},
|
|
117
|
+
} satisfies Parameters<typeof buildV2SigninHref>[0];
|
|
118
|
+
expect(buildV2SigninHref(app, '/nocobase/settings/workflow?tab=list#recent')).toBe(
|
|
119
|
+
'/nocobase/settings/signin?redirect=%2Fnocobase%2Fsettings%2Fworkflow%3Ftab%3Dlist%23recent',
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should keep an apps segment inside the main public path out of the Settings application scope', () => {
|
|
124
|
+
const app = {
|
|
125
|
+
name: 'main',
|
|
126
|
+
getPublicPath: () => '/tenant/apps/root/',
|
|
127
|
+
router: {
|
|
128
|
+
getBasename: () => '/tenant/apps/root/',
|
|
129
|
+
},
|
|
130
|
+
pluginSettingsManager: {
|
|
131
|
+
getRouteName: () => 'settings.',
|
|
132
|
+
getRoutePath: () => '/settings/',
|
|
133
|
+
},
|
|
134
|
+
} satisfies Parameters<typeof buildV2SigninHref>[0];
|
|
135
|
+
|
|
136
|
+
expect(buildV2SigninHref(app, '/tenant/apps/root/settings/workflow')).toBe(
|
|
137
|
+
'/tenant/apps/root/settings/signin?redirect=%2Ftenant%2Fapps%2Froot%2Fsettings%2Fworkflow',
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it.each([
|
|
142
|
+
['/nocobase/settings/apps/test-app/', '/nocobase/settings/apps/test-app/signin'],
|
|
143
|
+
['/nocobase/settings/_app/test-app/', '/nocobase/settings/_app/test-app/signin'],
|
|
144
|
+
])('should keep the standalone settings signin in the current sub-app scope: %s', (basename, signinPath) => {
|
|
145
|
+
const app = {
|
|
146
|
+
name: 'test-app',
|
|
147
|
+
getPublicPath: () => '/nocobase/',
|
|
148
|
+
router: {
|
|
149
|
+
getBasename: () => basename,
|
|
150
|
+
},
|
|
151
|
+
pluginSettingsManager: {
|
|
152
|
+
getRouteName: () => 'settings.',
|
|
153
|
+
getRoutePath: () => '/',
|
|
154
|
+
},
|
|
155
|
+
} as any;
|
|
156
|
+
|
|
157
|
+
expect(buildV2SigninHref(app, `${basename}workflow`)).toBe(
|
|
158
|
+
`${signinPath}?redirect=${encodeURIComponent(`${basename}workflow`)}`,
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should accept the current standalone settings signin URL only', () => {
|
|
163
|
+
Object.defineProperty(globalThis.window, 'location', {
|
|
164
|
+
configurable: true,
|
|
165
|
+
value: {
|
|
166
|
+
...originalLocation,
|
|
167
|
+
origin: 'http://localhost:20000',
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
const app = {
|
|
171
|
+
name: 'test-app',
|
|
172
|
+
getPublicPath: () => '/nocobase/',
|
|
173
|
+
router: {
|
|
174
|
+
getBasename: () => '/nocobase/settings/apps/test-app/',
|
|
175
|
+
},
|
|
176
|
+
pluginSettingsManager: {
|
|
177
|
+
getRouteName: () => 'settings.',
|
|
178
|
+
getRoutePath: () => '/',
|
|
179
|
+
},
|
|
180
|
+
} as any;
|
|
181
|
+
|
|
182
|
+
expect(
|
|
183
|
+
resolveV2SigninRedirect(
|
|
184
|
+
'/nocobase/settings/apps/test-app/signin?redirect=%2Fnocobase%2Fsettings%2Fapps%2Ftest-app',
|
|
185
|
+
app,
|
|
186
|
+
),
|
|
187
|
+
).toBe(
|
|
188
|
+
'http://localhost:20000/nocobase/settings/apps/test-app/signin?redirect=%2Fnocobase%2Fsettings%2Fapps%2Ftest-app',
|
|
189
|
+
);
|
|
190
|
+
expect(resolveV2SigninRedirect('/nocobase/v/apps/test-app/signin', app)).toBeNull();
|
|
191
|
+
expect(resolveV2SigninRedirect('/nocobase/settings/apps/other-app/signin', app)).toBeNull();
|
|
192
|
+
});
|
|
193
|
+
|
|
87
194
|
it('should redirect with window.location.replace by default', () => {
|
|
88
195
|
const replace = vi.fn();
|
|
89
196
|
Object.defineProperty(globalThis.window, 'location', {
|
|
@@ -134,6 +241,36 @@ describe('auth redirect helpers', () => {
|
|
|
134
241
|
});
|
|
135
242
|
|
|
136
243
|
describe('v2 sub-app context (router basename contains /apps/<id>/)', () => {
|
|
244
|
+
it('should keep only the current sub-app standalone settings redirect', () => {
|
|
245
|
+
const app = {
|
|
246
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
247
|
+
router: {
|
|
248
|
+
getBasename: () => '/nocobase/v2/apps/test-app/',
|
|
249
|
+
},
|
|
250
|
+
} as any;
|
|
251
|
+
|
|
252
|
+
expect(normalizeV2RedirectPath(app, '/nocobase/settings/apps/test-app/workflow')).toBe(
|
|
253
|
+
'/nocobase/settings/apps/test-app/workflow',
|
|
254
|
+
);
|
|
255
|
+
expect(normalizeV2RedirectPath(app, '/nocobase/settings/apps/other-app/workflow')).toBe(
|
|
256
|
+
'/nocobase/v2/apps/test-app/admin/',
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it.each([
|
|
261
|
+
'/nocobase/settings/apps/test-app/../../other-app/settings',
|
|
262
|
+
'/nocobase/settings/apps/test-app/%2e%2e/%2E%2e/other-app/settings',
|
|
263
|
+
])('should reject a standalone settings redirect that resolves outside the current sub-app: %s', (target) => {
|
|
264
|
+
const app = {
|
|
265
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
266
|
+
router: {
|
|
267
|
+
getBasename: () => '/nocobase/v2/apps/test-app/',
|
|
268
|
+
},
|
|
269
|
+
} as any;
|
|
270
|
+
|
|
271
|
+
expect(normalizeV2RedirectPath(app, target)).toBe('/nocobase/v2/apps/test-app/admin/');
|
|
272
|
+
});
|
|
273
|
+
|
|
137
274
|
it('should normalize signin redirect fallback under the current sub-app basename', () => {
|
|
138
275
|
const app = {
|
|
139
276
|
getPublicPath: () => '/v/',
|
|
@@ -210,6 +347,23 @@ describe('auth redirect helpers', () => {
|
|
|
210
347
|
);
|
|
211
348
|
});
|
|
212
349
|
|
|
350
|
+
it('should derive a scoped Portal signin href under the current sub-app basename', () => {
|
|
351
|
+
const app = {
|
|
352
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
353
|
+
router: {
|
|
354
|
+
getBasename: () => '/nocobase/v2/apps/test-app/',
|
|
355
|
+
},
|
|
356
|
+
} satisfies Parameters<typeof buildV2SigninHref>[0];
|
|
357
|
+
|
|
358
|
+
expect(
|
|
359
|
+
buildV2SigninHref(app, '/nocobase/v2/apps/test-app/customer/orders', {
|
|
360
|
+
signInRoutePath: '/customer/signin',
|
|
361
|
+
}),
|
|
362
|
+
).toBe(
|
|
363
|
+
'/nocobase/v2/apps/test-app/customer/signin?redirect=%2Fnocobase%2Fv2%2Fapps%2Ftest-app%2Fcustomer%2Forders',
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
213
367
|
it('should redirect to sub-app signin with window.location.replace', () => {
|
|
214
368
|
const replace = vi.fn();
|
|
215
369
|
Object.defineProperty(globalThis.window, 'location', {
|
|
@@ -15,8 +15,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
15
15
|
type BrowserCheckerCase = {
|
|
16
16
|
pathname: string;
|
|
17
17
|
publicPath: string;
|
|
18
|
+
search?: string;
|
|
19
|
+
hash?: string;
|
|
18
20
|
modernClientPrefix?: string;
|
|
19
21
|
appClientEntryMode?: string;
|
|
22
|
+
scriptSrc?: string;
|
|
20
23
|
expectedRedirect?: string;
|
|
21
24
|
};
|
|
22
25
|
|
|
@@ -47,8 +50,8 @@ function executeBrowserChecker(scriptPath: string, input: BrowserCheckerCase) {
|
|
|
47
50
|
location: {
|
|
48
51
|
origin: 'http://c.local.nocobase.com',
|
|
49
52
|
pathname: input.pathname,
|
|
50
|
-
search: '',
|
|
51
|
-
hash: '',
|
|
53
|
+
search: input.search || '',
|
|
54
|
+
hash: input.hash || '',
|
|
52
55
|
replace,
|
|
53
56
|
},
|
|
54
57
|
console: consoleMock,
|
|
@@ -58,6 +61,13 @@ function executeBrowserChecker(scriptPath: string, input: BrowserCheckerCase) {
|
|
|
58
61
|
onresize: undefined,
|
|
59
62
|
},
|
|
60
63
|
document: {
|
|
64
|
+
currentScript: {
|
|
65
|
+
src:
|
|
66
|
+
input.scriptSrc ||
|
|
67
|
+
(scriptPath.includes('/app/client-v2/public/')
|
|
68
|
+
? 'http://assets.local.nocobase.com/v/browser-checker.js?v=1'
|
|
69
|
+
: 'http://assets.local.nocobase.com/browser-checker.js?v=1'),
|
|
70
|
+
},
|
|
61
71
|
documentElement: {
|
|
62
72
|
className: '',
|
|
63
73
|
clientWidth: 1280,
|
|
@@ -80,11 +90,11 @@ function executeBrowserChecker(scriptPath: string, input: BrowserCheckerCase) {
|
|
|
80
90
|
describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
|
|
81
91
|
it('normalizes a relative public path before redirecting to the trailing-slash entry', () => {
|
|
82
92
|
const replace = executeBrowserChecker(scriptPath, {
|
|
83
|
-
pathname: '/nocobase/
|
|
84
|
-
publicPath: 'nocobase/
|
|
93
|
+
pathname: '/nocobase/console',
|
|
94
|
+
publicPath: 'nocobase/console/',
|
|
85
95
|
});
|
|
86
96
|
|
|
87
|
-
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/
|
|
97
|
+
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/console/');
|
|
88
98
|
});
|
|
89
99
|
|
|
90
100
|
it('prefixes outside paths with a root-relative basename instead of duplicating a relative segment', () => {
|
|
@@ -98,7 +108,7 @@ describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
|
|
|
98
108
|
|
|
99
109
|
it('does not redirect when the current path is already under the normalized basename', () => {
|
|
100
110
|
const replace = executeBrowserChecker(scriptPath, {
|
|
101
|
-
pathname: '/nocobase/v/',
|
|
111
|
+
pathname: '/nocobase/v/admin',
|
|
102
112
|
publicPath: 'nocobase/v/',
|
|
103
113
|
});
|
|
104
114
|
|
|
@@ -126,6 +136,30 @@ describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
|
|
|
126
136
|
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/v/');
|
|
127
137
|
});
|
|
128
138
|
|
|
139
|
+
it('redirects app root to modern entry for modern-only', () => {
|
|
140
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
141
|
+
pathname: '/',
|
|
142
|
+
publicPath: '/',
|
|
143
|
+
modernClientPrefix: 'v',
|
|
144
|
+
appClientEntryMode: 'modern-only',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/v/');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it.each(['/', '/index.html'])('redirects app root entry %s to Settings for settings-default', (pathname) => {
|
|
151
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
152
|
+
pathname,
|
|
153
|
+
publicPath: '/',
|
|
154
|
+
modernClientPrefix: 'v',
|
|
155
|
+
appClientEntryMode: 'settings-default',
|
|
156
|
+
search: '?from=entry',
|
|
157
|
+
hash: '#panel',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/settings/?from=entry#panel');
|
|
161
|
+
});
|
|
162
|
+
|
|
129
163
|
it('does not redirect legacy deep links for modern-default', () => {
|
|
130
164
|
const replace = executeBrowserChecker(scriptPath, {
|
|
131
165
|
pathname: '/admin',
|
|
@@ -137,6 +171,17 @@ describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
|
|
|
137
171
|
expect(replace).not.toHaveBeenCalled();
|
|
138
172
|
});
|
|
139
173
|
|
|
174
|
+
it('does not redirect legacy deep links for settings-default', () => {
|
|
175
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
176
|
+
pathname: '/admin',
|
|
177
|
+
publicPath: '/',
|
|
178
|
+
modernClientPrefix: 'v',
|
|
179
|
+
appClientEntryMode: 'settings-default',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(replace).not.toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
|
|
140
185
|
it('rewrites legacy document paths for modern-only', () => {
|
|
141
186
|
const replace = executeBrowserChecker(scriptPath, {
|
|
142
187
|
pathname: '/admin/settings/workflow',
|
|
@@ -159,6 +204,19 @@ describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
|
|
|
159
204
|
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/v/');
|
|
160
205
|
});
|
|
161
206
|
|
|
207
|
+
it('redirects a sub-path app root directly to Settings for settings-default', () => {
|
|
208
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
209
|
+
pathname: '/nocobase/',
|
|
210
|
+
publicPath: '/nocobase/',
|
|
211
|
+
modernClientPrefix: 'v',
|
|
212
|
+
appClientEntryMode: 'settings-default',
|
|
213
|
+
search: '?from=entry',
|
|
214
|
+
hash: '#panel',
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/settings/?from=entry#panel');
|
|
218
|
+
});
|
|
219
|
+
|
|
162
220
|
it('rewrites sub-app legacy deep links for modern-only without collapsing the sub-app segment', () => {
|
|
163
221
|
const replace = executeBrowserChecker(scriptPath, {
|
|
164
222
|
pathname: '/nocobase/apps/a_31itq60q4kg/admin/',
|
|
@@ -170,4 +228,112 @@ describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
|
|
|
170
228
|
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/v/apps/a_31itq60q4kg/admin');
|
|
171
229
|
});
|
|
172
230
|
}
|
|
231
|
+
|
|
232
|
+
if (scriptPath.includes('/app/client-v2/public/')) {
|
|
233
|
+
it.each([
|
|
234
|
+
['/v', 'http://c.local.nocobase.com/v/'],
|
|
235
|
+
['/v/', undefined],
|
|
236
|
+
])('normalizes the modern client root %s without redirecting to Settings', (pathname, expectedRedirect) => {
|
|
237
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
238
|
+
pathname,
|
|
239
|
+
publicPath: '/v/',
|
|
240
|
+
modernClientPrefix: 'v',
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (expectedRedirect) {
|
|
244
|
+
expect(replace).toHaveBeenCalledOnce();
|
|
245
|
+
expect(replace).toHaveBeenCalledWith(expectedRedirect);
|
|
246
|
+
} else {
|
|
247
|
+
expect(replace).not.toHaveBeenCalled();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it.each(['/v/apps/demo', '/v/apps/demo/', '/v/_app/demo', '/v/_app/demo/'])(
|
|
252
|
+
'lets the scoped modern client handle its root %s',
|
|
253
|
+
(pathname) => {
|
|
254
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
255
|
+
pathname,
|
|
256
|
+
publicPath: '/v/',
|
|
257
|
+
modernClientPrefix: 'v',
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(replace).not.toHaveBeenCalled();
|
|
261
|
+
},
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
it.each([
|
|
265
|
+
['/settings/apps/demo', '/', '/settings/apps/demo/', 'v'],
|
|
266
|
+
['/settings/_app/demo', '/', '/settings/_app/demo/', 'v'],
|
|
267
|
+
['/nocobase/settings/apps/demo', '/nocobase/', '/nocobase/settings/apps/demo/', 'v'],
|
|
268
|
+
['/nocobase/settings/_app/demo', '/nocobase/', '/nocobase/settings/_app/demo/', 'v'],
|
|
269
|
+
['/tenant/apps/root/settings/apps/demo', '/tenant/apps/root/', '/tenant/apps/root/settings/apps/demo/', 'v'],
|
|
270
|
+
['/tenant/_app/root/settings/_app/demo', '/tenant/_app/root/', '/tenant/_app/root/settings/_app/demo/', 'v'],
|
|
271
|
+
['/tenant/v/settings/apps/demo', '/tenant/v/', '/tenant/v/settings/apps/demo/', 'v'],
|
|
272
|
+
['/tenant/modern/settings/_app/demo', '/tenant/modern/', '/tenant/modern/settings/_app/demo/', 'modern'],
|
|
273
|
+
])('normalizes the scoped Settings root %s to %s', (pathname, publicPath, expectedPathname, modernClientPrefix) => {
|
|
274
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
275
|
+
pathname,
|
|
276
|
+
publicPath,
|
|
277
|
+
modernClientPrefix,
|
|
278
|
+
scriptSrc: 'https://cdn.example.com/ui/settings/browser-checker.js?v=1',
|
|
279
|
+
search: '?tab=overview',
|
|
280
|
+
hash: '#panel',
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(replace).toHaveBeenCalledOnce();
|
|
284
|
+
expect(replace).toHaveBeenCalledWith(`http://c.local.nocobase.com${expectedPathname}?tab=overview#panel`);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it.each([
|
|
288
|
+
['/tenant/apps/root/modern/apps/demo/', '/tenant/apps/root/modern/'],
|
|
289
|
+
['/tenant/_app/root/modern/_app/demo/', '/tenant/_app/root/modern/'],
|
|
290
|
+
])('lets the modern client handle %s under a nested public path', (pathname, publicPath) => {
|
|
291
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
292
|
+
pathname,
|
|
293
|
+
publicPath,
|
|
294
|
+
modernClientPrefix: 'modern',
|
|
295
|
+
search: '?tab=overview',
|
|
296
|
+
hash: '#panel',
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
expect(replace).not.toHaveBeenCalled();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it.each([
|
|
303
|
+
['/v/admin', '/v/'],
|
|
304
|
+
['/v/apps/demo/admin', '/v/'],
|
|
305
|
+
['/v/_app/demo/admin', '/v/'],
|
|
306
|
+
['/v/_apps/demo', '/v/'],
|
|
307
|
+
['/v/settings/apps/demo', '/v/'],
|
|
308
|
+
['/nocobase/modern/settings/_app/demo', '/nocobase/modern/', 'modern'],
|
|
309
|
+
])('does not redirect a non-root modern path %s to Settings', (pathname, publicPath, modernClientPrefix = 'v') => {
|
|
310
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
311
|
+
pathname,
|
|
312
|
+
publicPath,
|
|
313
|
+
modernClientPrefix,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(replace).not.toHaveBeenCalled();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it.each([
|
|
320
|
+
['/settings', '/'],
|
|
321
|
+
['/settings/', '/'],
|
|
322
|
+
['/settings/apps/demo/', '/'],
|
|
323
|
+
['/settings/_app/demo/', '/'],
|
|
324
|
+
['/settings/apps/demo/multi-portal', '/'],
|
|
325
|
+
['/settings/_app/demo/multi-portal', '/'],
|
|
326
|
+
['/settings/_apps/demo', '/'],
|
|
327
|
+
['/nocobase/settings/apps/demo/multi-portal', '/nocobase/'],
|
|
328
|
+
])('does not redirect a non-target Settings path %s', (pathname, publicPath) => {
|
|
329
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
330
|
+
pathname,
|
|
331
|
+
publicPath,
|
|
332
|
+
modernClientPrefix: 'v',
|
|
333
|
+
scriptSrc: 'https://cdn.example.com/ui/settings/browser-checker.js?v=1',
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
expect(replace).not.toHaveBeenCalled();
|
|
337
|
+
});
|
|
338
|
+
}
|
|
173
339
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
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 MockAdapter from 'axios-mock-adapter';
|
|
11
|
+
import type { ApplicationOptions } from '../Application';
|
|
12
|
+
import { SettingsApplication } from '../settings-app/SettingsApplication';
|
|
13
|
+
|
|
14
|
+
class MockSettingsApplication extends SettingsApplication {
|
|
15
|
+
readonly apiMock: MockAdapter;
|
|
16
|
+
|
|
17
|
+
constructor(options: ApplicationOptions = {}) {
|
|
18
|
+
super({
|
|
19
|
+
router: { type: 'memory', initialEntries: ['/settings'] },
|
|
20
|
+
ws: false,
|
|
21
|
+
...options,
|
|
22
|
+
});
|
|
23
|
+
this.apiMock = new MockAdapter(this.apiClient.axios);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createMockSettingsClient(options?: ApplicationOptions) {
|
|
28
|
+
return new MockSettingsApplication(options);
|
|
29
|
+
}
|