@nocobase/client-v2 2.2.0-beta.9 → 2.2.0
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 +13 -3
- package/es/BaseApplication.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 +3 -2
- 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/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +27 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +7 -24
- package/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/components/RunJSValueEditor.d.ts +1 -0
- package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
- package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
- package/es/flow/components/field-value-variable/index.d.ts +11 -0
- package/es/flow/components/filter/FilterGroup.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
- package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
- package/es/flow/index.d.ts +1 -0
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
- package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
- package/es/flow/models/base/ActionModelCore.d.ts +4 -2
- package/es/flow/models/base/BlockGridModel.d.ts +3 -0
- package/es/flow/models/base/CollectionBlockModel.d.ts +3 -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/details/DetailsBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
- package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
- package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
- package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
- package/es/flow/models/blocks/table/utils.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
- package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.d.ts +5 -0
- package/es/index.mjs +199 -161
- package/es/ui-operation/index.d.ts +15 -0
- package/es/ui-operation/ui-operation-codec.d.ts +10 -0
- package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
- package/es/utils/index.d.ts +1 -0
- package/lib/index.js +215 -177
- package/lib/locale/languageCodes.js +3 -1
- package/package.json +9 -7
- package/src/APIClient.ts +90 -7
- package/src/Application.tsx +7 -1
- package/src/BaseApplication.tsx +11 -6
- package/src/__tests__/APIClient.test.tsx +58 -0
- package/src/__tests__/app.test.tsx +73 -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 +35 -2
- package/src/__tests__/settings-center.test.tsx +270 -6
- package/src/acl/ACLProvider.tsx +2 -0
- package/src/acl/__tests__/ACLProvider.test.tsx +92 -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 +79 -35
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
- package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
- 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 +36 -9
- package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +55 -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/form/table/Table.tsx +81 -6
- package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
- package/src/components/index.ts +1 -0
- package/src/flow/FlowPage.tsx +9 -1
- package/src/flow/__tests__/FlowPage.test.tsx +50 -3
- package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
- package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
- package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -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/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +144 -26
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
- package/src/flow/common/Markdown/style.ts +4 -0
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +72 -584
- package/src/flow/components/FlowRoute.tsx +78 -32
- package/src/flow/components/RunJSValueEditor.tsx +9 -1
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
- package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
- package/src/flow/components/field-value-variable/dateValue.ts +223 -0
- package/src/flow/components/field-value-variable/index.ts +12 -0
- package/src/flow/components/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +170 -13
- 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 +195 -1
- package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
- package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
- package/src/flow/index.ts +3 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
- package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
- package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
- package/src/flow/models/base/ActionModelCore.tsx +11 -2
- package/src/flow/models/base/BlockGridModel.tsx +26 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
- 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 +249 -18
- 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 +1075 -7
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
- package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
- package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +155 -25
- package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +395 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
- package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
- package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
- package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +29 -3
- package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
- package/src/flow/models/blocks/table/utils.ts +7 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +10 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
- package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
- package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
- package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
- package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
- package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +26 -10
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +51 -14
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
- package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
- package/src/flow-compat/fieldValidationConstants.ts +1 -1
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/index.ts +5 -0
- package/src/layout-manager/LayoutContentRoute.tsx +2 -1
- package/src/layout-manager/LayoutRoute.tsx +2 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
- package/src/locale/languageCodes.ts +3 -1
- package/src/nocobase-buildin-plugin/index.tsx +12 -0
- package/src/settings-center/AdminSettingsLayout.tsx +23 -2
- package/src/ui-operation/index.ts +33 -0
- package/src/ui-operation/ui-operation-codec.ts +54 -0
- package/src/utils/getRouteRuntimeVersion.ts +185 -0
- package/src/utils/index.tsx +1 -0
|
@@ -64,6 +64,7 @@ const languageCodes = {
|
|
|
64
64
|
"ml-IN": { label: "\u0D2E\u0D32\u0D2F\u0D3E\u0D33\u0D02" },
|
|
65
65
|
"mn-MN": { label: "\u041C\u043E\u043D\u0433\u043E\u043B \u0445\u044D\u043B" },
|
|
66
66
|
"ms-MY": { label: "\u0628\u0647\u0627\u0633 \u0645\u0644\u0627\u064A\u0648" },
|
|
67
|
+
"my-MM": { label: "\u1019\u103C\u1014\u103A\u1019\u102C\u1018\u102C\u101E\u102C" },
|
|
67
68
|
"nb-NO": { label: "Norsk bokm\xE5l" },
|
|
68
69
|
"ne-NP": { label: "\u0928\u0947\u092A\u093E\u0932\u0940" },
|
|
69
70
|
"nl-BE": { label: "Vlaams" },
|
|
@@ -83,7 +84,8 @@ const languageCodes = {
|
|
|
83
84
|
"tk-TK": { label: "Turkmen" },
|
|
84
85
|
"tr-TR": { label: "T\xFCrk\xE7e" },
|
|
85
86
|
"uk-UA": { label: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430" },
|
|
86
|
-
"ur-PK": { label: "
|
|
87
|
+
"ur-PK": { label: "\u0627\u0631\u062F\u0648" },
|
|
88
|
+
"uz-UZ": { label: "O\u02BBzbekcha" },
|
|
87
89
|
"vi-VN": { label: "Ti\u1EBFng Vi\u1EC7t" },
|
|
88
90
|
"zh-CN": { label: "\u7B80\u4F53\u4E2D\u6587" },
|
|
89
91
|
"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.2.0
|
|
3
|
+
"version": "2.2.0",
|
|
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.2.0
|
|
31
|
-
"@nocobase/flow-engine": "2.2.0
|
|
32
|
-
"@nocobase/sdk": "2.2.0
|
|
33
|
-
"@nocobase/shared": "2.2.0
|
|
34
|
-
"@nocobase/utils": "2.2.0
|
|
30
|
+
"@nocobase/evaluators": "2.2.0",
|
|
31
|
+
"@nocobase/flow-engine": "2.2.0",
|
|
32
|
+
"@nocobase/sdk": "2.2.0",
|
|
33
|
+
"@nocobase/shared": "2.2.0",
|
|
34
|
+
"@nocobase/utils": "2.2.0",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -42,9 +42,11 @@
|
|
|
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",
|
|
47
|
+
"react-device-detect": "2.2.3",
|
|
46
48
|
"react-i18next": "^11.15.1",
|
|
47
49
|
"react-router-dom": "^6.30.1"
|
|
48
50
|
},
|
|
49
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "d5052cae37177054c236f1bf7bba3aed6ef029d0"
|
|
50
52
|
}
|
package/src/APIClient.ts
CHANGED
|
@@ -7,7 +7,69 @@
|
|
|
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
|
+
import type { NotificationInstance } from 'antd/es/notification/interface';
|
|
12
|
+
import type { AxiosRequestConfig } from 'axios';
|
|
13
|
+
import React from 'react';
|
|
14
|
+
|
|
15
|
+
type ResponseMessage = string | { message?: unknown };
|
|
16
|
+
|
|
17
|
+
interface APIClientApplication {
|
|
18
|
+
getName?: () => string | undefined;
|
|
19
|
+
context?: {
|
|
20
|
+
notification?: NotificationInstance;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface NotificationError {
|
|
25
|
+
config?: AxiosRequestConfig & {
|
|
26
|
+
skipNotify?: boolean | ((error: unknown) => boolean);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const notificationCache = new Map<string, number>();
|
|
31
|
+
|
|
32
|
+
function getMessageText(item: ResponseMessage): string {
|
|
33
|
+
if (typeof item === 'string') {
|
|
34
|
+
return item;
|
|
35
|
+
}
|
|
36
|
+
return typeof item?.message === 'string' ? item.message : '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function deduplicateMessages(messages: ResponseMessage[]): ResponseMessage[] {
|
|
40
|
+
if (notificationCache.size > 10) {
|
|
41
|
+
notificationCache.clear();
|
|
42
|
+
}
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
return messages.filter((item) => {
|
|
45
|
+
const message = getMessageText(item);
|
|
46
|
+
if (!message) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const lastTime = notificationCache.get(message);
|
|
50
|
+
if (lastTime && now - lastTime < 500) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
notificationCache.set(message, now);
|
|
54
|
+
return true;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function notify(type: 'success' | 'error', messages: ResponseMessage[], instance?: NotificationInstance) {
|
|
59
|
+
if (!instance || messages.length === 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const filteredMessages = deduplicateMessages(messages);
|
|
63
|
+
if (filteredMessages.length === 0) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
instance[type]({
|
|
67
|
+
message: filteredMessages.map((item, index) => {
|
|
68
|
+
const message = getMessageText(item);
|
|
69
|
+
return React.createElement('div', { key: `${index}_${message}` }, message);
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
11
73
|
|
|
12
74
|
function offsetToTimeZone(offset: number) {
|
|
13
75
|
const hours = Math.floor(Math.abs(offset));
|
|
@@ -23,13 +85,10 @@ function getCurrentTimezone() {
|
|
|
23
85
|
}
|
|
24
86
|
|
|
25
87
|
export class APIClient extends APIClientSDK {
|
|
26
|
-
|
|
88
|
+
app?: APIClientApplication;
|
|
27
89
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (options && typeof options !== 'function') {
|
|
31
|
-
this.appName = options.appName;
|
|
32
|
-
}
|
|
90
|
+
get notification() {
|
|
91
|
+
return this.app?.context?.notification;
|
|
33
92
|
}
|
|
34
93
|
|
|
35
94
|
getHostname() {
|
|
@@ -64,5 +123,29 @@ export class APIClient extends APIClientSDK {
|
|
|
64
123
|
return config;
|
|
65
124
|
});
|
|
66
125
|
super.interceptors();
|
|
126
|
+
this.useNotificationMiddleware();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
handleNotificationError(error: unknown) {
|
|
130
|
+
const notificationError = error as NotificationError;
|
|
131
|
+
const skipNotify = notificationError.config?.skipNotify;
|
|
132
|
+
if (skipNotify && (skipNotify === true || (typeof skipNotify === 'function' && skipNotify(error)))) {
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
const messages = this.toErrMessages(error);
|
|
136
|
+
if (Array.isArray(messages)) {
|
|
137
|
+
notify('error', messages, this.notification);
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
useNotificationMiddleware() {
|
|
143
|
+
this.axios.interceptors.response.use((response) => {
|
|
144
|
+
const messages = response.data?.messages;
|
|
145
|
+
if (Array.isArray(messages)) {
|
|
146
|
+
notify('success', messages, this.notification);
|
|
147
|
+
}
|
|
148
|
+
return response;
|
|
149
|
+
}, this.handleNotificationError.bind(this));
|
|
67
150
|
}
|
|
68
151
|
}
|
package/src/Application.tsx
CHANGED
|
@@ -45,10 +45,16 @@ export class Application extends BaseApplication<
|
|
|
45
45
|
public hasLoadError = false;
|
|
46
46
|
|
|
47
47
|
protected createApiClient(options: ApplicationOptions) {
|
|
48
|
-
|
|
48
|
+
const apiClient = 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
|
});
|
|
56
|
+
apiClient.app = this;
|
|
57
|
+
return apiClient;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
protected configureRuntimeAdapters() {
|
package/src/BaseApplication.tsx
CHANGED
|
@@ -35,6 +35,7 @@ import { SystemSettingsSource } from './flow/system-settings';
|
|
|
35
35
|
import { LayoutManager } from './layout-manager/LayoutManager';
|
|
36
36
|
import type { PluginClass, PluginManager, PluginType } from './PluginManager';
|
|
37
37
|
import { RouteRepository } from './RouteRepository';
|
|
38
|
+
import { stripModernClientPrefix } from './authRedirect';
|
|
38
39
|
import type {
|
|
39
40
|
ComponentTypeAndString,
|
|
40
41
|
RenderableComponentType,
|
|
@@ -81,6 +82,10 @@ const trimTrailingSlashes = (value: string) => {
|
|
|
81
82
|
return match ? value.slice(0, -match[0].length) : value;
|
|
82
83
|
};
|
|
83
84
|
|
|
85
|
+
const ensureTrailingSlash = (value: string) => {
|
|
86
|
+
return `${trimTrailingSlashes(value)}/`;
|
|
87
|
+
};
|
|
88
|
+
|
|
84
89
|
const isRenderableComponentType = (value: unknown): value is AnyComponent => isValidElementType(value);
|
|
85
90
|
|
|
86
91
|
export type DevDynamicImport = (packageName: string) => Promise<{ default: PluginClass }>;
|
|
@@ -398,7 +403,7 @@ export abstract class BaseApplication<
|
|
|
398
403
|
this.favicon = favicon || '';
|
|
399
404
|
}
|
|
400
405
|
|
|
401
|
-
const iconHref = this.favicon || '
|
|
406
|
+
const iconHref = this.favicon || this.getCdnUrl() + 'favicon/favicon.ico';
|
|
402
407
|
|
|
403
408
|
if (faviconLinkElement) {
|
|
404
409
|
faviconLinkElement.href = iconHref;
|
|
@@ -433,12 +438,12 @@ export abstract class BaseApplication<
|
|
|
433
438
|
return getSubAppName(this.getPublicPath()) || null;
|
|
434
439
|
}
|
|
435
440
|
|
|
441
|
+
getCdnUrl() {
|
|
442
|
+
return ensureTrailingSlash(window['__webpack_public_path__'] || stripModernClientPrefix(this.getPublicPath()));
|
|
443
|
+
}
|
|
444
|
+
|
|
436
445
|
getPublicPath() {
|
|
437
|
-
|
|
438
|
-
if (!publicPath.endsWith('/')) {
|
|
439
|
-
publicPath += '/';
|
|
440
|
-
}
|
|
441
|
-
return publicPath;
|
|
446
|
+
return ensureTrailingSlash(this.options.publicPath || '/');
|
|
442
447
|
}
|
|
443
448
|
|
|
444
449
|
getApiUrl(pathname = '') {
|
|
@@ -0,0 +1,58 @@
|
|
|
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 { APIClient } from '@nocobase/client-v2';
|
|
11
|
+
import type { NotificationInstance } from 'antd/es/notification/interface';
|
|
12
|
+
import MockAdapter from 'axios-mock-adapter';
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
15
|
+
|
|
16
|
+
function createAPIClient() {
|
|
17
|
+
const notification = {
|
|
18
|
+
error: vi.fn(),
|
|
19
|
+
success: vi.fn(),
|
|
20
|
+
} as unknown as NotificationInstance;
|
|
21
|
+
const apiClient = new APIClient();
|
|
22
|
+
apiClient.app = { context: { notification } };
|
|
23
|
+
const apiMock = new MockAdapter(apiClient.axios);
|
|
24
|
+
return { apiClient, apiMock, notification };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('APIClient response notifications', () => {
|
|
28
|
+
it('shows error messages returned by failed requests', async () => {
|
|
29
|
+
const { apiClient, apiMock, notification } = createAPIClient();
|
|
30
|
+
apiMock.onPost('/posts:create').reply(400, { errors: [{ message: 'Request rejected by workflow' }] });
|
|
31
|
+
|
|
32
|
+
await expect(apiClient.request({ url: '/posts:create', method: 'post' })).rejects.toBeDefined();
|
|
33
|
+
|
|
34
|
+
expect(notification.error).toHaveBeenCalledOnce();
|
|
35
|
+
const [{ message }] = vi.mocked(notification.error).mock.calls[0];
|
|
36
|
+
expect(renderToStaticMarkup(<>{message}</>)).toContain('Request rejected by workflow');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('does not show errors when skipNotify is enabled', async () => {
|
|
40
|
+
const { apiClient, apiMock, notification } = createAPIClient();
|
|
41
|
+
apiMock.onGet('/health').reply(500, { errors: [{ message: 'Health check failed' }] });
|
|
42
|
+
|
|
43
|
+
await expect(apiClient.request({ url: '/health', skipNotify: true })).rejects.toBeDefined();
|
|
44
|
+
|
|
45
|
+
expect(notification.error).not.toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('shows messages returned by successful requests', async () => {
|
|
49
|
+
const { apiClient, apiMock, notification } = createAPIClient();
|
|
50
|
+
apiMock.onPost('/posts:create').reply(200, { messages: [{ message: 'Created successfully' }] });
|
|
51
|
+
|
|
52
|
+
await apiClient.request({ url: '/posts:create', method: 'post' });
|
|
53
|
+
|
|
54
|
+
expect(notification.success).toHaveBeenCalledOnce();
|
|
55
|
+
const [{ message }] = vi.mocked(notification.success).mock.calls[0];
|
|
56
|
+
expect(renderToStaticMarkup(<>{message}</>)).toContain('Created successfully');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -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
|
|
|
@@ -15,6 +15,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
15
15
|
type BrowserCheckerCase = {
|
|
16
16
|
pathname: string;
|
|
17
17
|
publicPath: string;
|
|
18
|
+
modernClientPrefix?: string;
|
|
19
|
+
appClientEntryMode?: string;
|
|
18
20
|
expectedRedirect?: string;
|
|
19
21
|
};
|
|
20
22
|
|
|
@@ -40,6 +42,8 @@ function executeBrowserChecker(scriptPath: string, input: BrowserCheckerCase) {
|
|
|
40
42
|
showLog: false,
|
|
41
43
|
window: {
|
|
42
44
|
__nocobase_public_path__: input.publicPath,
|
|
45
|
+
__nocobase_modern_client_prefix__: input.modernClientPrefix,
|
|
46
|
+
__nocobase_app_client_entry_mode__: input.appClientEntryMode,
|
|
43
47
|
location: {
|
|
44
48
|
origin: 'http://c.local.nocobase.com',
|
|
45
49
|
pathname: input.pathname,
|
|
@@ -109,4 +113,61 @@ describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
|
|
|
109
113
|
|
|
110
114
|
expect(replace).not.toHaveBeenCalled();
|
|
111
115
|
});
|
|
116
|
+
|
|
117
|
+
if (scriptPath.includes('/app/client/public/')) {
|
|
118
|
+
it('redirects app root to modern entry for modern-default', () => {
|
|
119
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
120
|
+
pathname: '/',
|
|
121
|
+
publicPath: '/',
|
|
122
|
+
modernClientPrefix: 'v',
|
|
123
|
+
appClientEntryMode: 'modern-default',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/v/');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('does not redirect legacy deep links for modern-default', () => {
|
|
130
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
131
|
+
pathname: '/admin',
|
|
132
|
+
publicPath: '/',
|
|
133
|
+
modernClientPrefix: 'v',
|
|
134
|
+
appClientEntryMode: 'modern-default',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(replace).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('rewrites legacy document paths for modern-only', () => {
|
|
141
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
142
|
+
pathname: '/admin/settings/workflow',
|
|
143
|
+
publicPath: '/',
|
|
144
|
+
modernClientPrefix: 'v',
|
|
145
|
+
appClientEntryMode: 'modern-only',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/v/admin/settings/workflow');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('redirects sub-path site root directly to final modern target', () => {
|
|
152
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
153
|
+
pathname: '/nocobase/',
|
|
154
|
+
publicPath: '/nocobase/',
|
|
155
|
+
modernClientPrefix: 'v',
|
|
156
|
+
appClientEntryMode: 'modern-default',
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/v/');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('rewrites sub-app legacy deep links for modern-only without collapsing the sub-app segment', () => {
|
|
163
|
+
const replace = executeBrowserChecker(scriptPath, {
|
|
164
|
+
pathname: '/nocobase/apps/a_31itq60q4kg/admin/',
|
|
165
|
+
publicPath: '/nocobase/',
|
|
166
|
+
modernClientPrefix: 'v',
|
|
167
|
+
appClientEntryMode: 'modern-only',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/v/apps/a_31itq60q4kg/admin');
|
|
171
|
+
});
|
|
172
|
+
}
|
|
112
173
|
});
|
|
@@ -0,0 +1,68 @@
|
|
|
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 { afterEach, describe, expect, it } from 'vitest';
|
|
11
|
+
import { getRouteRuntimeVersion } from '../utils/getRouteRuntimeVersion';
|
|
12
|
+
|
|
13
|
+
function replacePathname(pathname: string) {
|
|
14
|
+
Object.defineProperty(window, 'location', {
|
|
15
|
+
configurable: true,
|
|
16
|
+
value: {
|
|
17
|
+
...window.location,
|
|
18
|
+
pathname,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('getRouteRuntimeVersion', () => {
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
delete window.__nocobase_modern_client_prefix__;
|
|
26
|
+
delete window.__nocobase_public_path__;
|
|
27
|
+
replacePathname('/');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns modern when the current pathname is inside the modern client prefix', () => {
|
|
31
|
+
window.__nocobase_modern_client_prefix__ = 'v';
|
|
32
|
+
window.__nocobase_public_path__ = '/nocobase/v/';
|
|
33
|
+
replacePathname('/nocobase/v/admin/settings/workflow');
|
|
34
|
+
|
|
35
|
+
expect(getRouteRuntimeVersion()).toBe('modern');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns legacy when reused v2 components run under the v1 settings page', () => {
|
|
39
|
+
window.__nocobase_modern_client_prefix__ = 'v';
|
|
40
|
+
window.__nocobase_public_path__ = '/nocobase/';
|
|
41
|
+
replacePathname('/nocobase/admin/settings/workflow/workflows/123');
|
|
42
|
+
|
|
43
|
+
expect(getRouteRuntimeVersion()).toBe('legacy');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns legacy when root public path is "/"', () => {
|
|
47
|
+
window.__nocobase_modern_client_prefix__ = 'v';
|
|
48
|
+
window.__nocobase_public_path__ = '/';
|
|
49
|
+
replacePathname('/admin/settings/workflow/workflows/123');
|
|
50
|
+
|
|
51
|
+
expect(getRouteRuntimeVersion()).toBe('legacy');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns modern when root public path is "/v/"', () => {
|
|
55
|
+
window.__nocobase_modern_client_prefix__ = 'v';
|
|
56
|
+
window.__nocobase_public_path__ = '/v/';
|
|
57
|
+
replacePathname('/v/admin/workflow/workflows/123');
|
|
58
|
+
|
|
59
|
+
expect(getRouteRuntimeVersion()).toBe('modern');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('falls back to pathname matching when public path is unavailable', () => {
|
|
63
|
+
window.__nocobase_modern_client_prefix__ = 'v';
|
|
64
|
+
replacePathname('/v/admin/workflow/workflows/123');
|
|
65
|
+
|
|
66
|
+
expect(getRouteRuntimeVersion()).toBe('modern');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -55,6 +55,7 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
55
55
|
const originalLocation = globalThis.window.location;
|
|
56
56
|
|
|
57
57
|
beforeEach(() => {
|
|
58
|
+
globalThis.window.localStorage.clear();
|
|
58
59
|
// These fixtures mount the modern client under the `v2` segment; tell the
|
|
59
60
|
// runtime-prefix helper so v2-runtime detection matches (server injects it
|
|
60
61
|
// in production).
|
|
@@ -75,6 +76,7 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
75
76
|
});
|
|
76
77
|
|
|
77
78
|
afterEach(() => {
|
|
79
|
+
globalThis.window.localStorage.clear();
|
|
78
80
|
delete (globalThis.window as any).__nocobase_modern_client_prefix__;
|
|
79
81
|
Object.defineProperty(globalThis.window, 'location', {
|
|
80
82
|
configurable: true,
|
|
@@ -116,6 +118,7 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
116
118
|
plugins: [NocoBaseBuildInPlugin as any],
|
|
117
119
|
router: { type: 'memory', initialEntries: ['/v2/admin'] },
|
|
118
120
|
});
|
|
121
|
+
app.apiClient.auth.setToken('test-token');
|
|
119
122
|
app.apiMock.onGet('app:getLang').reply(200, {
|
|
120
123
|
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
121
124
|
});
|
|
@@ -130,6 +133,7 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
130
133
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
131
134
|
expect(app.router.router.state.location.pathname).toBe('/v2/admin');
|
|
132
135
|
expect(app.router.router.state.location.search).toBe('');
|
|
136
|
+
expect(app.apiMock.history.post.filter((request) => request.url === 'auth:syncCookies')).toHaveLength(0);
|
|
133
137
|
});
|
|
134
138
|
|
|
135
139
|
it('should redirect unauthenticated v2 root access to v2 signin via <Navigate />', async () => {
|
|
@@ -326,6 +330,7 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
326
330
|
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
327
331
|
});
|
|
328
332
|
app.apiMock.onGet('/auth:check').reply(200, { data: { id: 1 } });
|
|
333
|
+
app.apiMock.onPost('auth:syncCookies').reply(200, { data: { synced: true } });
|
|
329
334
|
const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
|
|
330
335
|
|
|
331
336
|
const Root = app.getRootComponent();
|
|
@@ -333,6 +338,29 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
333
338
|
|
|
334
339
|
expect(await screen.findByText('secure page')).toBeInTheDocument();
|
|
335
340
|
expect(ensureLoaded).toHaveBeenCalledTimes(1);
|
|
341
|
+
expect(app.apiMock.history.post.filter((request) => request.url === 'auth:syncCookies')).toHaveLength(1);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('should keep authenticated v2 startup working when cookie bootstrap fails', async () => {
|
|
345
|
+
const app = createMockClient({
|
|
346
|
+
publicPath: '/v2/',
|
|
347
|
+
plugins: [NocoBaseBuildInPlugin as any, AuthBootstrapRoutePlugin as any],
|
|
348
|
+
router: { type: 'memory', initialEntries: ['/v2/secure'] },
|
|
349
|
+
});
|
|
350
|
+
app.apiClient.auth.setToken('test-token');
|
|
351
|
+
app.apiMock.onGet('app:getLang').reply(200, {
|
|
352
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
353
|
+
});
|
|
354
|
+
app.apiMock.onGet('/auth:check').reply(200, { data: { id: 1 } });
|
|
355
|
+
app.apiMock.onPost('auth:syncCookies').reply(500, { errors: [{ message: 'cookie sync failed' }] });
|
|
356
|
+
const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
|
|
357
|
+
|
|
358
|
+
const Root = app.getRootComponent();
|
|
359
|
+
render(<Root />);
|
|
360
|
+
|
|
361
|
+
expect(await screen.findByText('secure page')).toBeInTheDocument();
|
|
362
|
+
expect(ensureLoaded).toHaveBeenCalledTimes(1);
|
|
363
|
+
expect(app.apiMock.history.post.filter((request) => request.url === 'auth:syncCookies')).toHaveLength(1);
|
|
336
364
|
});
|
|
337
365
|
|
|
338
366
|
it('should not auth-check or bootstrap authCheck false routes', async () => {
|
|
@@ -387,7 +415,7 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
387
415
|
});
|
|
388
416
|
|
|
389
417
|
it.each(['/v2/admin/legacy-page/tab/tab-1', '/v2/admin/legacy-page/view/detail'])(
|
|
390
|
-
'should
|
|
418
|
+
'should explain authenticated direct v1-style v2 page access: %s',
|
|
391
419
|
async (pathname) => {
|
|
392
420
|
const app = createMockClient({
|
|
393
421
|
publicPath: '/v2/',
|
|
@@ -413,7 +441,12 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
413
441
|
const Root = app.getRootComponent();
|
|
414
442
|
render(<Root />);
|
|
415
443
|
|
|
416
|
-
expect(await screen.findByText('
|
|
444
|
+
expect(await screen.findByText('This page is not supported in the /v2/ branch')).toBeInTheDocument();
|
|
445
|
+
expect(screen.queryByText('404')).not.toBeInTheDocument();
|
|
446
|
+
expect(screen.getByRole('link', { name: 'Open from the original entry' })).toHaveAttribute(
|
|
447
|
+
'href',
|
|
448
|
+
pathname.replace(/^\/v2/, ''),
|
|
449
|
+
);
|
|
417
450
|
expect(app.router.router.state.location.pathname).toBe(pathname);
|
|
418
451
|
},
|
|
419
452
|
);
|