@nocobase/client-v2 2.2.0-beta.8 → 2.3.0-alpha.1
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/APIClient.d.ts +1 -3
- package/es/BaseApplication.d.ts +3 -0
- package/es/RouteRepository.d.ts +8 -0
- package/es/RouterManager.d.ts +15 -0
- package/es/authRedirect.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
- package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
- package/es/components/category-tabs/index.d.ts +9 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -1
- package/es/components/form/TypedVariableInput.d.ts +31 -5
- package/es/components/form/filter/CollectionFilter.d.ts +2 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
- package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
- package/es/components/index.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/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +2 -0
- 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/AdminLayoutMenuModels.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/components/FieldAssignExactDatePicker.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
- package/es/flow/components/RunJSValueEditor.d.ts +1 -0
- package/es/flow/components/filter/FilterGroup.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/base/ActionModelCore.d.ts +2 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
- package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
- package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
- package/es/flow/routeTransientInputArgs.d.ts +14 -0
- package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.d.ts +6 -0
- package/es/index.mjs +279 -158
- package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
- package/es/utils/index.d.ts +1 -0
- package/es/utils/markdownSanitize.d.ts +11 -0
- package/lib/index.js +280 -159
- package/lib/locale/languageCodes.js +2 -1
- package/package.json +8 -7
- package/src/APIClient.ts +1 -10
- package/src/Application.tsx +4 -0
- package/src/BaseApplication.tsx +13 -6
- 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__/app.test.tsx +73 -0
- package/src/__tests__/authRedirect.test.ts +17 -0
- package/src/__tests__/browserChecker.test.ts +61 -0
- package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +28 -0
- package/src/authRedirect.ts +38 -0
- package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
- package/src/collection-manager/field-validation.ts +1 -1
- package/src/components/README.md +7 -1
- package/src/components/README.zh-CN.md +6 -1
- package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
- package/src/components/category-tabs/index.ts +10 -0
- package/src/components/form/JsonTextArea.tsx +4 -0
- package/src/components/form/ScanInput/CodeScanner.tsx +23 -6
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +180 -3
- package/src/components/form/ScanInput/useCodeScanner.ts +170 -5
- package/src/components/form/TypedVariableInput.tsx +452 -101
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
- package/src/components/form/filter/CollectionFilter.tsx +4 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
- package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
- package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/form/filter/useFilterActionProps.ts +37 -6
- package/src/components/index.ts +1 -0
- package/src/entry-actions/EntryActionManager.ts +76 -0
- package/src/entry-actions/index.ts +10 -0
- package/src/flow/FlowPage.tsx +38 -3
- package/src/flow/__tests__/FlowPage.test.tsx +201 -27
- package/src/flow/__tests__/FlowRoute.test.tsx +549 -4
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
- package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
- package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
- package/src/flow/actions/dataScopeFilter.ts +10 -1
- package/src/flow/actions/dateTimeFormat.tsx +2 -2
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +86 -11
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
- package/src/flow/actions/openView.tsx +21 -1
- package/src/flow/admin-shell/BaseLayoutModel.tsx +124 -0
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -5
- 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/AppListRender.tsx +13 -2
- package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +436 -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/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +91 -20
- package/src/flow/components/FlowRoute.tsx +134 -18
- package/src/flow/components/RunJSValueEditor.tsx +9 -1
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
- package/src/flow/components/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
- package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
- package/src/flow/index.ts +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 +5 -0
- package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
- package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
- package/src/flow/models/base/PageModel/PageTabModel.tsx +184 -3
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +572 -1
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -0
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +34 -0
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +1 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +6 -2
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +51 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +45 -13
- package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
- package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
- package/src/flow/routeTransientInputArgs.ts +27 -0
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
- package/src/flow-compat/fieldValidationConstants.ts +1 -1
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/index.ts +6 -0
- package/src/locale/languageCodes.ts +2 -1
- package/src/nocobase-buildin-plugin/index.tsx +19 -1
- package/src/utils/getRouteRuntimeVersion.ts +185 -0
- package/src/utils/index.tsx +1 -0
- package/src/utils/markdownSanitize.ts +88 -0
|
@@ -83,7 +83,8 @@ const languageCodes = {
|
|
|
83
83
|
"tk-TK": { label: "Turkmen" },
|
|
84
84
|
"tr-TR": { label: "T\xFCrk\xE7e" },
|
|
85
85
|
"uk-UA": { label: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430" },
|
|
86
|
-
"ur-PK": { label: "
|
|
86
|
+
"ur-PK": { label: "\u0627\u0631\u062F\u0648" },
|
|
87
|
+
"uz-UZ": { label: "O\u02BBzbekcha" },
|
|
87
88
|
"vi-VN": { label: "Ti\u1EBFng Vi\u1EC7t" },
|
|
88
89
|
"zh-CN": { label: "\u7B80\u4F53\u4E2D\u6587" },
|
|
89
90
|
"zh-HK": { label: "\u7E41\u9AD4\u4E2D\u6587\uFF08\u9999\u6E2F\uFF09" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0-alpha.1",
|
|
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.3.0-alpha.1",
|
|
31
|
+
"@nocobase/flow-engine": "2.3.0-alpha.1",
|
|
32
|
+
"@nocobase/sdk": "2.3.0-alpha.1",
|
|
33
|
+
"@nocobase/shared": "2.3.0-alpha.1",
|
|
34
|
+
"@nocobase/utils": "2.3.0-alpha.1",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -42,9 +42,10 @@
|
|
|
42
42
|
"html5-qrcode": "^2.3.8",
|
|
43
43
|
"i18next": "^22.4.9",
|
|
44
44
|
"json5": "^2.2.3",
|
|
45
|
+
"jsqr": "^1.4.0",
|
|
45
46
|
"lodash": "4.17.21",
|
|
46
47
|
"react-i18next": "^11.15.1",
|
|
47
48
|
"react-router-dom": "^6.30.1"
|
|
48
49
|
},
|
|
49
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "2377df8ceb12549149017f7f14a61207bf6e49a2"
|
|
50
51
|
}
|
package/src/APIClient.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { APIClient as APIClientSDK,
|
|
10
|
+
import { APIClient as APIClientSDK, hasHeaderValue } from '@nocobase/sdk';
|
|
11
11
|
|
|
12
12
|
function offsetToTimeZone(offset: number) {
|
|
13
13
|
const hours = Math.floor(Math.abs(offset));
|
|
@@ -23,15 +23,6 @@ function getCurrentTimezone() {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export class APIClient extends APIClientSDK {
|
|
26
|
-
appName?: string;
|
|
27
|
-
|
|
28
|
-
constructor(options?: APIClientOptions) {
|
|
29
|
-
super(options);
|
|
30
|
-
if (options && typeof options !== 'function') {
|
|
31
|
-
this.appName = options.appName;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
26
|
getHostname() {
|
|
36
27
|
if (process.env.API_BASE_URL) {
|
|
37
28
|
try {
|
package/src/Application.tsx
CHANGED
|
@@ -46,6 +46,10 @@ export class Application extends BaseApplication<
|
|
|
46
46
|
|
|
47
47
|
protected createApiClient(options: ApplicationOptions) {
|
|
48
48
|
return new APIClient({
|
|
49
|
+
// Cross-origin API deployments rely on cookies for auth (e.g. permanent file
|
|
50
|
+
// URLs); trust is enforced server-side via the CORS origin whitelist and the
|
|
51
|
+
// CSRF middleware, not by omitting credentials.
|
|
52
|
+
withCredentials: true,
|
|
49
53
|
...options.apiClient,
|
|
50
54
|
appName: options.name || getSubAppName(options.publicPath),
|
|
51
55
|
});
|
package/src/BaseApplication.tsx
CHANGED
|
@@ -30,10 +30,12 @@ import AntdAppProvider from './theme/AntdAppProvider';
|
|
|
30
30
|
import { GlobalThemeProvider } from './theme';
|
|
31
31
|
import { AIManager } from './ai';
|
|
32
32
|
import { AppError, AppMaintaining, AppMaintainingDialog, AppNotFound, AppSpin, BlankComponent } from './components';
|
|
33
|
+
import { EntryActionManager } from './entry-actions';
|
|
33
34
|
import { SystemSettingsSource } from './flow/system-settings';
|
|
34
35
|
import { LayoutManager } from './layout-manager/LayoutManager';
|
|
35
36
|
import type { PluginClass, PluginManager, PluginType } from './PluginManager';
|
|
36
37
|
import { RouteRepository } from './RouteRepository';
|
|
38
|
+
import { stripModernClientPrefix } from './authRedirect';
|
|
37
39
|
import type {
|
|
38
40
|
ComponentTypeAndString,
|
|
39
41
|
RenderableComponentType,
|
|
@@ -80,6 +82,10 @@ const trimTrailingSlashes = (value: string) => {
|
|
|
80
82
|
return match ? value.slice(0, -match[0].length) : value;
|
|
81
83
|
};
|
|
82
84
|
|
|
85
|
+
const ensureTrailingSlash = (value: string) => {
|
|
86
|
+
return `${trimTrailingSlashes(value)}/`;
|
|
87
|
+
};
|
|
88
|
+
|
|
83
89
|
const isRenderableComponentType = (value: unknown): value is AnyComponent => isValidElementType(value);
|
|
84
90
|
|
|
85
91
|
export type DevDynamicImport = (packageName: string) => Promise<{ default: PluginClass }>;
|
|
@@ -126,6 +132,7 @@ export abstract class BaseApplication<
|
|
|
126
132
|
public pluginManager: TPluginManager;
|
|
127
133
|
public pluginSettingsManager: TPluginSettingsManager;
|
|
128
134
|
public layoutManager: LayoutManager<this>;
|
|
135
|
+
public entryActionManager = new EntryActionManager();
|
|
129
136
|
public aiManager!: AIManager;
|
|
130
137
|
public devDynamicImport?: DevDynamicImport;
|
|
131
138
|
public requirejs!: RequireJS;
|
|
@@ -396,7 +403,7 @@ export abstract class BaseApplication<
|
|
|
396
403
|
this.favicon = favicon || '';
|
|
397
404
|
}
|
|
398
405
|
|
|
399
|
-
const iconHref = this.favicon || '
|
|
406
|
+
const iconHref = this.favicon || this.getCdnUrl() + 'favicon/favicon.ico';
|
|
400
407
|
|
|
401
408
|
if (faviconLinkElement) {
|
|
402
409
|
faviconLinkElement.href = iconHref;
|
|
@@ -431,12 +438,12 @@ export abstract class BaseApplication<
|
|
|
431
438
|
return getSubAppName(this.getPublicPath()) || null;
|
|
432
439
|
}
|
|
433
440
|
|
|
441
|
+
getCdnUrl() {
|
|
442
|
+
return ensureTrailingSlash(window['__webpack_public_path__'] || stripModernClientPrefix(this.getPublicPath()));
|
|
443
|
+
}
|
|
444
|
+
|
|
434
445
|
getPublicPath() {
|
|
435
|
-
|
|
436
|
-
if (!publicPath.endsWith('/')) {
|
|
437
|
-
publicPath += '/';
|
|
438
|
-
}
|
|
439
|
-
return publicPath;
|
|
446
|
+
return ensureTrailingSlash(this.options.publicPath || '/');
|
|
440
447
|
}
|
|
441
448
|
|
|
442
449
|
getApiUrl(pathname = '') {
|
package/src/RouteRepository.ts
CHANGED
|
@@ -268,6 +268,16 @@ export class RouteRepository {
|
|
|
268
268
|
return this.findRoute(this.listAccessible(), schemaUid);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/**
|
|
272
|
+
* 通过路由 id 反查对应路由节点。
|
|
273
|
+
*
|
|
274
|
+
* @param routeId 桌面路由主键
|
|
275
|
+
* @returns 匹配到的路由节点
|
|
276
|
+
*/
|
|
277
|
+
getRouteById(routeId: string | number): NocoBaseDesktopRoute | undefined {
|
|
278
|
+
return this.findRouteById(this.listAccessible(), routeId);
|
|
279
|
+
}
|
|
280
|
+
|
|
271
281
|
protected getAPIClient(): APIClient {
|
|
272
282
|
if (!this.ctx?.api) {
|
|
273
283
|
throw new Error('[NocoBase] RouteRepository requires context.api.');
|
|
@@ -346,4 +356,19 @@ export class RouteRepository {
|
|
|
346
356
|
}
|
|
347
357
|
return undefined;
|
|
348
358
|
}
|
|
359
|
+
|
|
360
|
+
private findRouteById(routes: NocoBaseDesktopRoute[], routeId: string | number): NocoBaseDesktopRoute | undefined {
|
|
361
|
+
for (const route of routes) {
|
|
362
|
+
if (route.id != null && String(route.id) === String(routeId)) {
|
|
363
|
+
return route;
|
|
364
|
+
}
|
|
365
|
+
if (route.children) {
|
|
366
|
+
const found = this.findRouteById(route.children, routeId);
|
|
367
|
+
if (found) {
|
|
368
|
+
return found;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
349
374
|
}
|
package/src/RouterManager.tsx
CHANGED
|
@@ -26,6 +26,7 @@ import type { BaseApplication } from './BaseApplication';
|
|
|
26
26
|
import { BlankComponent, RouterContextCleaner } from './components';
|
|
27
27
|
import { RouterBridge } from './components/RouterBridge';
|
|
28
28
|
import { Router } from '@remix-run/router';
|
|
29
|
+
import { getV2EffectiveBasePath } from './authRedirect';
|
|
29
30
|
|
|
30
31
|
export interface BrowserRouterOptions extends Omit<BrowserRouterProps, 'children'> {
|
|
31
32
|
type?: 'browser';
|
|
@@ -55,6 +56,87 @@ export interface RouteType extends Omit<RouteObject, 'children' | 'Component'> {
|
|
|
55
56
|
export type RenderComponentType = (Component: ComponentTypeAndString, props?: any) => React.ReactNode;
|
|
56
57
|
export type RouterComponentType = React.FC<{ BaseLayout?: ComponentType }>;
|
|
57
58
|
|
|
59
|
+
const DEFAULT_ADMIN_ROUTE_PATH = '/admin';
|
|
60
|
+
|
|
61
|
+
type AdminRouteNavigationTarget = {
|
|
62
|
+
currentPathname?: string;
|
|
63
|
+
targetPathname: unknown;
|
|
64
|
+
basePath?: string;
|
|
65
|
+
adminRoutePath?: string;
|
|
66
|
+
replace?: boolean;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function trimPathSearchAndHash(pathname: string) {
|
|
70
|
+
return pathname.split(/[?#]/)[0];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function splitPathSearchAndHash(pathname: string) {
|
|
74
|
+
const match = pathname.match(/^([^?#]*)(.*)$/);
|
|
75
|
+
return {
|
|
76
|
+
pathname: match?.[1] || '',
|
|
77
|
+
suffix: match?.[2] || '',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeRootPath(pathname?: string) {
|
|
82
|
+
const trimmed = trimPathSearchAndHash(pathname || '').trim();
|
|
83
|
+
if (!trimmed || trimmed === '/') {
|
|
84
|
+
return '/';
|
|
85
|
+
}
|
|
86
|
+
return `/${trimmed.replace(/^\/+|\/+$/g, '')}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isRootRelativePath(pathname: string) {
|
|
90
|
+
return pathname.startsWith('/') && !pathname.startsWith('//') && !pathname.startsWith('/\\');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function removeBasePath(pathname: string, basePath?: string) {
|
|
94
|
+
const normalizedPathname = normalizeRootPath(pathname);
|
|
95
|
+
const normalizedBasePath = normalizeRootPath(basePath);
|
|
96
|
+
|
|
97
|
+
if (normalizedBasePath === '/') {
|
|
98
|
+
return normalizedPathname;
|
|
99
|
+
}
|
|
100
|
+
if (normalizedPathname === normalizedBasePath) {
|
|
101
|
+
return '/';
|
|
102
|
+
}
|
|
103
|
+
if (normalizedPathname.startsWith(`${normalizedBasePath}/`)) {
|
|
104
|
+
return normalizeRootPath(normalizedPathname.slice(normalizedBasePath.length));
|
|
105
|
+
}
|
|
106
|
+
return normalizedPathname;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function removeAppSegment(pathname: string) {
|
|
110
|
+
return normalizeRootPath(pathname).replace(/^\/(?:apps|_app)\/[^/]+(?=\/|$)/, '') || '/';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizePortalRoutePath(pathname: string, basePath?: string) {
|
|
114
|
+
return removeAppSegment(removeBasePath(pathname, basePath));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isSameOrChildPath(pathname: string, basePath: string) {
|
|
118
|
+
const normalizedPathname = normalizeRootPath(pathname);
|
|
119
|
+
const normalizedBasePath = normalizeRootPath(basePath);
|
|
120
|
+
return normalizedPathname === normalizedBasePath || normalizedPathname.startsWith(`${normalizedBasePath}/`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function shouldOpenAdminRouteInNewWindow(options: AdminRouteNavigationTarget) {
|
|
124
|
+
if (options.replace || typeof options.targetPathname !== 'string') {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const targetPathname = options.targetPathname.trim();
|
|
129
|
+
if (!isRootRelativePath(targetPathname)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const adminRoutePath = normalizeRootPath(options.adminRoutePath || DEFAULT_ADMIN_ROUTE_PATH);
|
|
134
|
+
const currentRoutePath = normalizePortalRoutePath(options.currentPathname || '/', options.basePath);
|
|
135
|
+
const targetRoutePath = normalizePortalRoutePath(targetPathname, options.basePath);
|
|
136
|
+
|
|
137
|
+
return !isSameOrChildPath(currentRoutePath, adminRoutePath) && isSameOrChildPath(targetRoutePath, adminRoutePath);
|
|
138
|
+
}
|
|
139
|
+
|
|
58
140
|
function removeBasename(pathname: string, basename?: string) {
|
|
59
141
|
if (!basename || basename === '/') {
|
|
60
142
|
return pathname;
|
|
@@ -72,6 +154,7 @@ function removeBasename(pathname: string, basename?: string) {
|
|
|
72
154
|
export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<any>> {
|
|
73
155
|
protected routes: Record<string, RouteType> = {};
|
|
74
156
|
protected options: RouterOptions;
|
|
157
|
+
private routerNavigate?: Router['navigate'];
|
|
75
158
|
public app: TApp;
|
|
76
159
|
public router!: Router;
|
|
77
160
|
get basename() {
|
|
@@ -81,7 +164,7 @@ export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<a
|
|
|
81
164
|
return this.router.state;
|
|
82
165
|
}
|
|
83
166
|
get navigate() {
|
|
84
|
-
return this.
|
|
167
|
+
return this.navigateWithPortalPolicy;
|
|
85
168
|
}
|
|
86
169
|
|
|
87
170
|
constructor(options: RouterOptions = {}, app: TApp) {
|
|
@@ -90,6 +173,62 @@ export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<a
|
|
|
90
173
|
this.routes = options.routes || {};
|
|
91
174
|
}
|
|
92
175
|
|
|
176
|
+
private navigateWithPortalPolicy: Router['navigate'] = ((to, opts) => {
|
|
177
|
+
if (this.shouldOpenAdminRouteInNewWindow(to, opts)) {
|
|
178
|
+
window.open(this.getAdminRouteNavigationHref(to as string), '_blank', 'noopener,noreferrer');
|
|
179
|
+
return Promise.resolve();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const navigate = this.routerNavigate || this.router.navigate.bind(this.router);
|
|
183
|
+
return navigate(to, opts);
|
|
184
|
+
}) as Router['navigate'];
|
|
185
|
+
|
|
186
|
+
private shouldOpenAdminRouteInNewWindow(
|
|
187
|
+
to: Parameters<Router['navigate']>[0],
|
|
188
|
+
opts?: Parameters<Router['navigate']>[1],
|
|
189
|
+
) {
|
|
190
|
+
if (this.options.type && this.options.type !== 'browser') {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return shouldOpenAdminRouteInNewWindow({
|
|
195
|
+
currentPathname:
|
|
196
|
+
typeof window !== 'undefined' ? window.location.pathname : this.router?.state?.location?.pathname,
|
|
197
|
+
targetPathname: to,
|
|
198
|
+
basePath: this.getRuntimeBasePath(),
|
|
199
|
+
adminRoutePath: this.getAdminRoutePath(),
|
|
200
|
+
replace: opts?.replace,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private getRuntimeBasePath() {
|
|
205
|
+
return getV2EffectiveBasePath(this.app);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private getAdminRoutePath() {
|
|
209
|
+
try {
|
|
210
|
+
return this.app.layoutManager?.getLayout?.('admin')?.routePath || DEFAULT_ADMIN_ROUTE_PATH;
|
|
211
|
+
} catch {
|
|
212
|
+
return DEFAULT_ADMIN_ROUTE_PATH;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private getAdminRouteNavigationHref(pathname: string) {
|
|
217
|
+
const { pathname: rawPathname, suffix } = splitPathSearchAndHash(pathname.trim());
|
|
218
|
+
const normalizedPathname = normalizeRootPath(rawPathname);
|
|
219
|
+
const runtimeBasePath = this.getRuntimeBasePath();
|
|
220
|
+
const normalizedBasePath = normalizeRootPath(runtimeBasePath);
|
|
221
|
+
|
|
222
|
+
if (
|
|
223
|
+
normalizedBasePath !== '/' &&
|
|
224
|
+
(normalizedPathname === normalizedBasePath || normalizedPathname.startsWith(`${normalizedBasePath}/`))
|
|
225
|
+
) {
|
|
226
|
+
return `${normalizedPathname}${suffix}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return `${this.app.getHref(normalizePortalRoutePath(normalizedPathname, runtimeBasePath))}${suffix}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
93
232
|
protected resolveLoadedComponent(moduleOrComponent: ComponentLoaderResult): ComponentTypeAndString | undefined {
|
|
94
233
|
if (!moduleOrComponent) {
|
|
95
234
|
return undefined;
|
|
@@ -257,6 +396,8 @@ export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<a
|
|
|
257
396
|
],
|
|
258
397
|
opts,
|
|
259
398
|
);
|
|
399
|
+
this.routerNavigate = this.router.navigate.bind(this.router) as Router['navigate'];
|
|
400
|
+
this.router.navigate = this.navigateWithPortalPolicy;
|
|
260
401
|
|
|
261
402
|
const RenderRouter: RouterComponentType = ({ BaseLayout = BlankComponent }) => {
|
|
262
403
|
return (
|
|
@@ -140,6 +140,29 @@ describe('RouteRepository', () => {
|
|
|
140
140
|
expect(repository.getRouteBySchemaUid('custom-page')).toBeUndefined();
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
+
it('should find nested routes by id', () => {
|
|
144
|
+
const { repository } = createRouteRepository();
|
|
145
|
+
|
|
146
|
+
repository.setRoutes([
|
|
147
|
+
{
|
|
148
|
+
id: 1,
|
|
149
|
+
schemaUid: 'group-1',
|
|
150
|
+
children: [
|
|
151
|
+
{
|
|
152
|
+
id: 11,
|
|
153
|
+
schemaUid: 'page-1',
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
] as NocoBaseDesktopRoute[]);
|
|
158
|
+
|
|
159
|
+
expect(repository.getRouteById('11')).toMatchObject({
|
|
160
|
+
id: 11,
|
|
161
|
+
schemaUid: 'page-1',
|
|
162
|
+
});
|
|
163
|
+
expect(repository.getRouteById('missing')).toBeUndefined();
|
|
164
|
+
});
|
|
165
|
+
|
|
143
166
|
it('should notify only subscribers for the changed layout cache', () => {
|
|
144
167
|
const { repository } = createRouteRepository();
|
|
145
168
|
const adminEvents: string[][] = [];
|
|
@@ -0,0 +1,125 @@
|
|
|
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 type { BaseApplication } from '../BaseApplication';
|
|
11
|
+
import { RouterManager, shouldOpenAdminRouteInNewWindow } from '../RouterManager';
|
|
12
|
+
|
|
13
|
+
describe('RouterManager', () => {
|
|
14
|
+
describe('shouldOpenAdminRouteInNewWindow', () => {
|
|
15
|
+
it('opens admin route in a new window from a non-admin portal', () => {
|
|
16
|
+
expect(
|
|
17
|
+
shouldOpenAdminRouteInNewWindow({
|
|
18
|
+
currentPathname: '/v/test',
|
|
19
|
+
targetPathname: '/admin/settings',
|
|
20
|
+
basePath: '/v',
|
|
21
|
+
adminRoutePath: '/admin',
|
|
22
|
+
}),
|
|
23
|
+
).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('supports target pathname with the v2 base path', () => {
|
|
27
|
+
expect(
|
|
28
|
+
shouldOpenAdminRouteInNewWindow({
|
|
29
|
+
currentPathname: '/nocobase/v/test',
|
|
30
|
+
targetPathname: '/nocobase/v/admin/settings',
|
|
31
|
+
basePath: '/nocobase/v',
|
|
32
|
+
adminRoutePath: '/admin',
|
|
33
|
+
}),
|
|
34
|
+
).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('supports custom portal routes in sub-apps', () => {
|
|
38
|
+
expect(
|
|
39
|
+
shouldOpenAdminRouteInNewWindow({
|
|
40
|
+
currentPathname: '/v/apps/a_9xlild35jir/crm-amd/ekeisumx1zu',
|
|
41
|
+
targetPathname: '/v/apps/a_9xlild35jir/admin/settings',
|
|
42
|
+
basePath: '/v',
|
|
43
|
+
adminRoutePath: '/admin',
|
|
44
|
+
}),
|
|
45
|
+
).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('keeps admin route navigation in the current window when already in sub-app admin', () => {
|
|
49
|
+
expect(
|
|
50
|
+
shouldOpenAdminRouteInNewWindow({
|
|
51
|
+
currentPathname: '/v/apps/a_9xlild35jir/admin',
|
|
52
|
+
targetPathname: '/v/apps/a_9xlild35jir/admin/settings',
|
|
53
|
+
basePath: '/v',
|
|
54
|
+
adminRoutePath: '/admin',
|
|
55
|
+
}),
|
|
56
|
+
).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('keeps admin route navigation in the current window when already in admin', () => {
|
|
60
|
+
expect(
|
|
61
|
+
shouldOpenAdminRouteInNewWindow({
|
|
62
|
+
currentPathname: '/v/admin',
|
|
63
|
+
targetPathname: '/admin/settings',
|
|
64
|
+
basePath: '/v',
|
|
65
|
+
adminRoutePath: '/admin',
|
|
66
|
+
}),
|
|
67
|
+
).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('keeps non-admin portal navigation in the current window', () => {
|
|
71
|
+
expect(
|
|
72
|
+
shouldOpenAdminRouteInNewWindow({
|
|
73
|
+
currentPathname: '/v/test',
|
|
74
|
+
targetPathname: '/test/page',
|
|
75
|
+
basePath: '/v',
|
|
76
|
+
adminRoutePath: '/admin',
|
|
77
|
+
}),
|
|
78
|
+
).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('does not intercept replace navigation or relative paths', () => {
|
|
82
|
+
expect(
|
|
83
|
+
shouldOpenAdminRouteInNewWindow({
|
|
84
|
+
currentPathname: '/v/test',
|
|
85
|
+
targetPathname: '/admin/settings',
|
|
86
|
+
basePath: '/v',
|
|
87
|
+
adminRoutePath: '/admin',
|
|
88
|
+
replace: true,
|
|
89
|
+
}),
|
|
90
|
+
).toBe(false);
|
|
91
|
+
expect(
|
|
92
|
+
shouldOpenAdminRouteInNewWindow({
|
|
93
|
+
currentPathname: '/v/test',
|
|
94
|
+
targetPathname: 'admin/settings',
|
|
95
|
+
basePath: '/v',
|
|
96
|
+
adminRoutePath: '/admin',
|
|
97
|
+
}),
|
|
98
|
+
).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('opens admin route in a new window through the underlying router navigate from a sub-app portal', async () => {
|
|
103
|
+
const app = {
|
|
104
|
+
name: 'a_9xlild35jir',
|
|
105
|
+
getPublicPath: () => '/v/',
|
|
106
|
+
getHref: (pathname: string) => `/v/apps/a_9xlild35jir/${pathname.replace(/^\/+/, '')}`,
|
|
107
|
+
layoutManager: {
|
|
108
|
+
getLayout: () => ({ routePath: '/admin' }),
|
|
109
|
+
},
|
|
110
|
+
renderComponent: () => null,
|
|
111
|
+
} as unknown as BaseApplication<unknown>;
|
|
112
|
+
const manager = new RouterManager({ type: 'browser' }, app);
|
|
113
|
+
const open = vi.spyOn(window, 'open').mockImplementation(() => null);
|
|
114
|
+
|
|
115
|
+
window.history.pushState({}, '', '/v/apps/a_9xlild35jir/crm-amd/ekeisumx1zu');
|
|
116
|
+
manager.getRouterComponent();
|
|
117
|
+
await manager.router.navigate('/admin/settings/version-control/list');
|
|
118
|
+
|
|
119
|
+
expect(open).toHaveBeenCalledWith(
|
|
120
|
+
'/v/apps/a_9xlild35jir/admin/settings/version-control/list',
|
|
121
|
+
'_blank',
|
|
122
|
+
'noopener,noreferrer',
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -34,6 +34,8 @@ describe('app', () => {
|
|
|
34
34
|
afterEach(() => {
|
|
35
35
|
document.querySelectorAll('link[rel="shortcut icon"]').forEach((node) => node.remove());
|
|
36
36
|
document.documentElement.removeAttribute('lang');
|
|
37
|
+
delete window['__webpack_public_path__'];
|
|
38
|
+
delete window['__nocobase_modern_client_prefix__'];
|
|
37
39
|
vi.restoreAllMocks();
|
|
38
40
|
});
|
|
39
41
|
|
|
@@ -50,6 +52,24 @@ describe('app', () => {
|
|
|
50
52
|
expect(app.getHref('/test')).toBe('/test');
|
|
51
53
|
});
|
|
52
54
|
|
|
55
|
+
it('should enable credentials for api clients by default', () => {
|
|
56
|
+
const sameOriginApp = new Application({
|
|
57
|
+
router,
|
|
58
|
+
apiClient: {
|
|
59
|
+
baseURL: '/api/',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const crossOriginApp = new Application({
|
|
63
|
+
router,
|
|
64
|
+
apiClient: {
|
|
65
|
+
baseURL: 'https://api.example.com/api/',
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(sameOriginApp.apiClient.axios.defaults.withCredentials).toBe(true);
|
|
70
|
+
expect(crossOriginApp.apiClient.axios.defaults.withCredentials).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
53
73
|
it('should initialize shared jsonLogic operators', () => {
|
|
54
74
|
const app = new Application({ router });
|
|
55
75
|
|
|
@@ -58,6 +78,59 @@ describe('app', () => {
|
|
|
58
78
|
expect(app.jsonLogic.apply({ $testAlwaysTrue: [] })).toBe(true);
|
|
59
79
|
});
|
|
60
80
|
|
|
81
|
+
it('should normalize publicPath and webpack public path with a single trailing slash', () => {
|
|
82
|
+
const app = new Application({
|
|
83
|
+
router,
|
|
84
|
+
publicPath: '/admin//',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(app.getPublicPath()).toBe('/admin/');
|
|
88
|
+
|
|
89
|
+
window['__webpack_public_path__'] = '/cdn/assets///';
|
|
90
|
+
expect(app.getCdnUrl()).toBe('/cdn/assets/');
|
|
91
|
+
|
|
92
|
+
delete window['__webpack_public_path__'];
|
|
93
|
+
expect(app.getCdnUrl()).toBe('/admin/');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should normalize webpack public path without a trailing slash', () => {
|
|
97
|
+
const app = new Application({
|
|
98
|
+
router,
|
|
99
|
+
publicPath: '/admin/',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
window['__webpack_public_path__'] = '/cdn/assets';
|
|
103
|
+
expect(app.getCdnUrl()).toBe('/cdn/assets/');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should remove the modern client prefix from the CDN fallback path', () => {
|
|
107
|
+
const app = new Application({
|
|
108
|
+
router,
|
|
109
|
+
publicPath: '/v/',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(app.getCdnUrl()).toBe('/');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should preserve APP_PUBLIC_PATH when removing the modern client prefix', () => {
|
|
116
|
+
const app = new Application({
|
|
117
|
+
router,
|
|
118
|
+
publicPath: '/nocobase/v/',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(app.getCdnUrl()).toBe('/nocobase/');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should support a custom modern client prefix', () => {
|
|
125
|
+
window['__nocobase_modern_client_prefix__'] = 'modern';
|
|
126
|
+
const app = new Application({
|
|
127
|
+
router,
|
|
128
|
+
publicPath: '/nocobase/modern/',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(app.getCdnUrl()).toBe('/nocobase/');
|
|
132
|
+
});
|
|
133
|
+
|
|
61
134
|
it('should apply the provided favicon immediately', () => {
|
|
62
135
|
const app = new Application({ router });
|
|
63
136
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import {
|
|
11
11
|
buildV2SigninHref,
|
|
12
12
|
getCurrentV2RedirectPath,
|
|
13
|
+
normalizeV2RedirectPath,
|
|
13
14
|
redirectToV2Signin,
|
|
14
15
|
resolveV2SigninRedirect,
|
|
15
16
|
} from '../authRedirect';
|
|
@@ -133,6 +134,22 @@ describe('auth redirect helpers', () => {
|
|
|
133
134
|
});
|
|
134
135
|
|
|
135
136
|
describe('v2 sub-app context (router basename contains /apps/<id>/)', () => {
|
|
137
|
+
it('should normalize signin redirect fallback under the current sub-app basename', () => {
|
|
138
|
+
const app = {
|
|
139
|
+
getPublicPath: () => '/v/',
|
|
140
|
+
router: {
|
|
141
|
+
getBasename: () => '/v/apps/test-app/',
|
|
142
|
+
},
|
|
143
|
+
} as any;
|
|
144
|
+
|
|
145
|
+
expect(normalizeV2RedirectPath(app, '')).toBe('/v/apps/test-app/admin/');
|
|
146
|
+
expect(normalizeV2RedirectPath(app, '/admin/?tab=overview#panel')).toBe(
|
|
147
|
+
'/v/apps/test-app/admin/?tab=overview#panel',
|
|
148
|
+
);
|
|
149
|
+
expect(normalizeV2RedirectPath(app, '/v/apps/test-app/admin/')).toBe('/v/apps/test-app/admin/');
|
|
150
|
+
expect(normalizeV2RedirectPath(app, '/v/admin/')).toBe('/v/apps/test-app/admin/');
|
|
151
|
+
});
|
|
152
|
+
|
|
136
153
|
it('should preserve sub-app segment when building current redirect path under simple public path', () => {
|
|
137
154
|
const app = {
|
|
138
155
|
getPublicPath: () => '/v2/',
|