@nocobase/client-v2 2.2.0-beta.5 → 2.2.0-beta.7
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/PluginSettingsManager.d.ts +33 -0
- package/es/RouteRepository.d.ts +21 -6
- package/es/components/form/JsonTextArea.d.ts +2 -1
- package/es/components/form/VariableJsonTextArea.d.ts +19 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
- package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +4 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/models/base/GridModel.d.ts +1 -1
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
- package/es/flow-compat/FieldValidation.d.ts +2 -1
- package/es/index.d.ts +2 -1
- package/es/index.mjs +145 -119
- package/lib/index.js +148 -122
- package/package.json +7 -7
- package/src/PluginSettingsManager.ts +53 -0
- package/src/RouteRepository.ts +126 -24
- package/src/__tests__/PluginSettingsManager.test.ts +13 -0
- package/src/__tests__/RouteRepository.test.ts +216 -0
- package/src/__tests__/exports.test.ts +16 -0
- package/src/__tests__/plugin-manager.test.tsx +44 -2
- package/src/__tests__/settings-center.test.tsx +40 -1
- package/src/components/form/JsonTextArea.tsx +21 -11
- package/src/components/form/VariableJsonTextArea.tsx +175 -0
- package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
- package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/index.ts +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
- package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
- package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
- package/src/flow/actions/customVariable.tsx +1 -2
- package/src/flow/actions/dataScopeFilter.ts +10 -0
- package/src/flow/actions/linkageRules.tsx +122 -34
- package/src/flow/actions/runjs.tsx +2 -14
- package/src/flow/actions/validation.tsx +62 -30
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +33 -10
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +21 -8
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
- package/src/flow/components/DefaultValue.tsx +1 -2
- package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
- package/src/flow/components/FlowRoute.tsx +56 -11
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
- package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
- package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
- package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
- package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
- package/src/flow/components/fieldAssignOptions.ts +155 -27
- package/src/flow/models/actions/JSActionModel.tsx +2 -7
- package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
- package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
- package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
- package/src/flow/models/base/ActionModel.tsx +12 -1
- package/src/flow/models/base/GridModel.tsx +38 -7
- package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
- package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
- package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
- package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
- package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
- package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
- package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
- package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
- package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
- package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
- package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
- package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
- package/src/flow/models/fields/InputFieldModel.tsx +14 -22
- package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
- package/src/flow/models/fields/JSFieldModel.tsx +2 -7
- package/src/flow/models/fields/JSItemModel.tsx +2 -14
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +17 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
- package/src/flow-compat/FieldValidation.tsx +122 -60
- package/src/flow-compat/Popover.tsx +43 -4
- package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
- package/src/index.ts +8 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
- package/src/nocobase-buildin-plugin/index.tsx +0 -2
- package/src/settings-center/AdminSettingsLayout.tsx +3 -0
- package/src/settings-center/SystemSettingsPage.tsx +0 -1
- package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
- package/src/settings-center/plugin-manager/index.tsx +3 -0
- package/src/settings-center/utils.tsx +0 -6
|
@@ -38,7 +38,11 @@ import {
|
|
|
38
38
|
VariableScope,
|
|
39
39
|
withTooltipComponent,
|
|
40
40
|
} from './AdminLayoutCompat';
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
type AdminLayoutRoutePathLike,
|
|
43
|
+
joinAdminLayoutRoutePath,
|
|
44
|
+
toRouterNavigationPath,
|
|
45
|
+
} from './resolveAdminRouteRuntimeTarget';
|
|
42
46
|
|
|
43
47
|
export type AdminLayoutMenuRenderType = 'item' | 'group';
|
|
44
48
|
|
|
@@ -122,8 +126,30 @@ type AdminLayoutMenuItemsParent = FlowModel & {
|
|
|
122
126
|
* @param identity 节点身份
|
|
123
127
|
* @returns 唯一占位路径
|
|
124
128
|
*/
|
|
125
|
-
export const getAdminLayoutMenuVirtualPath = (
|
|
126
|
-
|
|
129
|
+
export const getAdminLayoutMenuVirtualPath = (
|
|
130
|
+
type: 'link' | 'designer',
|
|
131
|
+
identity: string | number,
|
|
132
|
+
layout?: AdminLayoutRoutePathLike | null,
|
|
133
|
+
) => {
|
|
134
|
+
return joinAdminLayoutRoutePath(layout, `__admin_layout__/${type}/${encodeURIComponent(String(identity))}`);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const getLayoutRoutePathFromModel = (model: FlowModel): AdminLayoutRoutePathLike | undefined => {
|
|
138
|
+
const contextLayout = model.context.layout as AdminLayoutRoutePathLike | undefined;
|
|
139
|
+
if (contextLayout?.routePath) {
|
|
140
|
+
return contextLayout;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let current: FlowModel | undefined = model;
|
|
144
|
+
while (current) {
|
|
145
|
+
const propsLayout = current.props?.layout as AdminLayoutRoutePathLike | undefined;
|
|
146
|
+
if (propsLayout?.routePath) {
|
|
147
|
+
return propsLayout;
|
|
148
|
+
}
|
|
149
|
+
current = current.parent as FlowModel | undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return undefined;
|
|
127
153
|
};
|
|
128
154
|
|
|
129
155
|
const menuItemStyle = { display: 'flex', alignItems: 'center', justifyContent: 'space-between' };
|
|
@@ -225,6 +251,41 @@ export const resolveAdminLayoutMenuLink = async (options: {
|
|
|
225
251
|
return appendQueryStringToUrl(String(resolvedHref ?? href ?? ''), qs.stringify(query));
|
|
226
252
|
};
|
|
227
253
|
|
|
254
|
+
function normalizeRouterBasename(basename?: string) {
|
|
255
|
+
if (!basename || basename === '/') {
|
|
256
|
+
return '';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return `/${basename.replace(/^\/+/, '').replace(/\/+$/, '')}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isStandaloneDocumentUrl(url: string) {
|
|
263
|
+
return /^[a-z][a-z\d+\-.]*:/i.test(url) || url.startsWith('//') || url.startsWith('#') || url.startsWith('?');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function toDocumentUrlWithRouterBasename(url: string, basename?: string) {
|
|
267
|
+
if (!url || isStandaloneDocumentUrl(url)) {
|
|
268
|
+
return url;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const normalizedBasename = normalizeRouterBasename(basename);
|
|
272
|
+
if (!normalizedBasename) {
|
|
273
|
+
return url;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const rootRelativeUrl = url.startsWith('/') ? url : `/${url}`;
|
|
277
|
+
if (
|
|
278
|
+
rootRelativeUrl === normalizedBasename ||
|
|
279
|
+
rootRelativeUrl.startsWith(`${normalizedBasename}/`) ||
|
|
280
|
+
rootRelativeUrl.startsWith(`${normalizedBasename}?`) ||
|
|
281
|
+
rootRelativeUrl.startsWith(`${normalizedBasename}#`)
|
|
282
|
+
) {
|
|
283
|
+
return rootRelativeUrl;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return `${normalizedBasename}${rootRelativeUrl}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
228
289
|
export const openAdminLayoutMenuLink = async (options: {
|
|
229
290
|
context: FlowModel['context'];
|
|
230
291
|
href: string;
|
|
@@ -249,7 +310,7 @@ export const openAdminLayoutMenuLink = async (options: {
|
|
|
249
310
|
if (isMobile) {
|
|
250
311
|
closeMobileMenu();
|
|
251
312
|
}
|
|
252
|
-
window.open(url, '_blank', 'noopener,noreferrer');
|
|
313
|
+
window.open(toDocumentUrlWithRouterBasename(url, basenameOfCurrentRouter), '_blank', 'noopener,noreferrer');
|
|
253
314
|
return;
|
|
254
315
|
}
|
|
255
316
|
|
|
@@ -265,7 +326,7 @@ export const openAdminLayoutMenuLink = async (options: {
|
|
|
265
326
|
if (isMobile) {
|
|
266
327
|
closeMobileMenu();
|
|
267
328
|
}
|
|
268
|
-
window.open(href, '_blank', 'noopener,noreferrer');
|
|
329
|
+
window.open(toDocumentUrlWithRouterBasename(href, basenameOfCurrentRouter), '_blank', 'noopener,noreferrer');
|
|
269
330
|
}
|
|
270
331
|
};
|
|
271
332
|
|
|
@@ -551,7 +612,7 @@ const GroupItem: FC<{ item: AdminLayoutMenuNode; options?: AdminLayoutMenuRender
|
|
|
551
612
|
},
|
|
552
613
|
});
|
|
553
614
|
},
|
|
554
|
-
[closeMobileMenu, item,
|
|
615
|
+
[closeMobileMenu, item, navigate, props.options?.isMobile, runtimePath, spaRuntimePath],
|
|
555
616
|
);
|
|
556
617
|
|
|
557
618
|
const landingEntryAriaLabel = ariaLabel ? `${ariaLabel}-landing-entry` : 'group-landing-entry';
|
|
@@ -699,7 +760,7 @@ const MenuItem: FC<{ item: AdminLayoutMenuNode; options?: AdminLayoutMenuRenderO
|
|
|
699
760
|
},
|
|
700
761
|
});
|
|
701
762
|
},
|
|
702
|
-
[props.options?.isMobile, closeMobileMenu, isDocumentNavigation,
|
|
763
|
+
[props.options?.isMobile, closeMobileMenu, isDocumentNavigation, navigate, runtimePath, spaRuntimePath],
|
|
703
764
|
);
|
|
704
765
|
|
|
705
766
|
if (item._route?.type === NocoBaseDesktopRouteType.link) {
|
|
@@ -876,14 +937,16 @@ export function getAdminLayoutMenuInitializerButton(
|
|
|
876
937
|
testId: string,
|
|
877
938
|
launcherModel: FlowModel,
|
|
878
939
|
parentRoute?: NocoBaseDesktopRoute,
|
|
940
|
+
layout?: AdminLayoutRoutePathLike | null,
|
|
879
941
|
): AdminLayoutMenuNode {
|
|
880
942
|
const identity =
|
|
881
943
|
parentRoute?.id ?? parentRoute?.schemaUid ?? parentRoute?.menuSchemaUid ?? parentRoute?.title ?? launcherModel.uid;
|
|
944
|
+
const menuLayout = layout || getLayoutRoutePathFromModel(launcherModel);
|
|
882
945
|
|
|
883
946
|
return {
|
|
884
947
|
key: 'x-designer-button',
|
|
885
948
|
name: <MenuDesignerButton testId={testId} launcherModel={launcherModel} parentRoute={parentRoute} />,
|
|
886
|
-
path: getAdminLayoutMenuVirtualPath('designer', identity),
|
|
949
|
+
path: getAdminLayoutMenuVirtualPath('designer', identity, menuLayout),
|
|
887
950
|
disabled: true,
|
|
888
951
|
_route: {},
|
|
889
952
|
_parentRoute: parentRoute,
|
|
@@ -114,7 +114,7 @@ export class AdminLayoutModel extends BaseLayoutModel<AdminLayoutStructure> {
|
|
|
114
114
|
|
|
115
115
|
render() {
|
|
116
116
|
return (
|
|
117
|
-
<AdminLayoutEntryGuard>
|
|
117
|
+
<AdminLayoutEntryGuard model={this}>
|
|
118
118
|
<AdminLayoutComponent {...this.props} model={this} />
|
|
119
119
|
</AdminLayoutEntryGuard>
|
|
120
120
|
);
|
|
@@ -16,10 +16,16 @@ import { useTranslation } from 'react-i18next';
|
|
|
16
16
|
import { Outlet, useLocation, useMatches, useParams } from 'react-router-dom';
|
|
17
17
|
import { KeepAlive } from '../../../components/KeepAlive';
|
|
18
18
|
import { getLayoutContentRouteNames } from '../../../layout-manager/utils';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
getAdminLayoutRoutePath,
|
|
21
|
+
isV2AdminRuntime,
|
|
22
|
+
isV2MenuRoute,
|
|
23
|
+
type AdminLayoutRoutePathLike,
|
|
24
|
+
} from './resolveAdminRouteRuntimeTarget';
|
|
20
25
|
|
|
21
26
|
type AdminLayoutContentProps = {
|
|
22
27
|
onContentElementChange?: (element: HTMLDivElement | null) => void;
|
|
28
|
+
layout?: (AdminLayoutRoutePathLike & { routeName?: string }) | null;
|
|
23
29
|
};
|
|
24
30
|
|
|
25
31
|
const layoutContentClass = css`
|
|
@@ -49,8 +55,6 @@ const mobileHeight = {
|
|
|
49
55
|
height: `calc(100dvh - var(--nb-header-height))`,
|
|
50
56
|
};
|
|
51
57
|
|
|
52
|
-
const adminLayoutContentRouteNames = getLayoutContentRouteNames('admin');
|
|
53
|
-
|
|
54
58
|
/**
|
|
55
59
|
* 检测当前浏览器是否支持 dvh,移动端支持时优先使用它计算可视区域高度。
|
|
56
60
|
*
|
|
@@ -66,7 +70,7 @@ function isDvhSupported() {
|
|
|
66
70
|
return testEl.style.height === '1dvh';
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
const ShowTipWhenNoPages = observer(() => {
|
|
73
|
+
const ShowTipWhenNoPages = observer((props: { layout?: AdminLayoutRoutePathLike | null }) => {
|
|
70
74
|
const flowEngine = useFlowEngine();
|
|
71
75
|
const { token } = antdTheme.useToken();
|
|
72
76
|
const { t } = useTranslation();
|
|
@@ -76,8 +80,13 @@ const ShowTipWhenNoPages = observer(() => {
|
|
|
76
80
|
? allAccessRoutes.filter((route) => isV2MenuRoute(route))
|
|
77
81
|
: allAccessRoutes;
|
|
78
82
|
const designable = !!flowEngine.context.flowSettingsEnabled;
|
|
83
|
+
const layoutRoutePath = getAdminLayoutRoutePath(props.layout);
|
|
79
84
|
|
|
80
|
-
if (
|
|
85
|
+
if (
|
|
86
|
+
visibleRoutes.length === 0 &&
|
|
87
|
+
!designable &&
|
|
88
|
+
(location.pathname === layoutRoutePath || location.pathname === `${layoutRoutePath}/`)
|
|
89
|
+
) {
|
|
81
90
|
return (
|
|
82
91
|
<Result
|
|
83
92
|
icon={<HighlightOutlined style={{ fontSize: '8em', color: token.colorText }} />}
|
|
@@ -95,13 +104,17 @@ const ShowTipWhenNoPages = observer(() => {
|
|
|
95
104
|
*
|
|
96
105
|
* 内容区不再依赖独立 FlowModel,而是通过回调把挂载目标同步给 root model。
|
|
97
106
|
*/
|
|
98
|
-
export const AdminLayoutContent: FC<AdminLayoutContentProps> = ({ onContentElementChange }) => {
|
|
107
|
+
export const AdminLayoutContent: FC<AdminLayoutContentProps> = ({ onContentElementChange, layout }) => {
|
|
99
108
|
const style = useMemo(() => (isDvhSupported() ? mobileHeight : undefined), []);
|
|
100
109
|
const params = useParams();
|
|
101
110
|
const matches = useMatches();
|
|
102
111
|
const pageUid = params.name;
|
|
103
112
|
const currentRouteId = matches.at(-1)?.id;
|
|
104
|
-
const
|
|
113
|
+
const layoutContentRouteNames = useMemo(
|
|
114
|
+
() => getLayoutContentRouteNames(layout?.routeName || 'admin'),
|
|
115
|
+
[layout?.routeName],
|
|
116
|
+
);
|
|
117
|
+
const shouldKeepAlive = !!pageUid && layoutContentRouteNames.includes(currentRouteId || '');
|
|
105
118
|
const bindLayoutContentRef = useCallback(
|
|
106
119
|
(node: HTMLDivElement | null) => {
|
|
107
120
|
// shell 直接渲染内容区时,仍需把挂载目标同步给 root model。
|
|
@@ -118,7 +131,7 @@ export const AdminLayoutContent: FC<AdminLayoutContentProps> = ({ onContentEleme
|
|
|
118
131
|
>
|
|
119
132
|
<div style={pageContentStyle}>
|
|
120
133
|
{shouldKeepAlive && pageUid ? <KeepAlive uid={pageUid}>{() => <Outlet />}</KeepAlive> : <Outlet />}
|
|
121
|
-
<ShowTipWhenNoPages />
|
|
134
|
+
<ShowTipWhenNoPages layout={layout} />
|
|
122
135
|
</div>
|
|
123
136
|
</div>
|
|
124
137
|
);
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
|
11
|
+
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
12
|
+
import React, { useEffect } from 'react';
|
|
13
|
+
import { createMemoryRouter, RouterProvider, useParams } from 'react-router-dom';
|
|
14
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
15
|
+
import type { LayoutDefinition } from '../../../../layout-manager/types';
|
|
16
|
+
import { AdminLayoutComponent } from '../AdminLayoutComponent';
|
|
17
|
+
import { AdminLayoutModel } from '../AdminLayoutModel';
|
|
18
|
+
import { AdminLayoutContent } from '../AdminLayoutSlotModels';
|
|
19
|
+
|
|
20
|
+
vi.mock('@ant-design/pro-layout', async () => {
|
|
21
|
+
const ReactModule = await import('react');
|
|
22
|
+
const RouteContext = ReactModule.createContext({ isMobile: false });
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
default: (props: { children?: React.ReactNode }) =>
|
|
26
|
+
ReactModule.createElement(
|
|
27
|
+
RouteContext.Provider,
|
|
28
|
+
{ value: { isMobile: false } },
|
|
29
|
+
ReactModule.createElement('div', { 'data-testid': 'pro-layout' }, props.children),
|
|
30
|
+
),
|
|
31
|
+
RouteContext,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
vi.mock('../../../system-settings', () => ({
|
|
36
|
+
useSystemSettings: () => ({
|
|
37
|
+
loading: false,
|
|
38
|
+
data: {
|
|
39
|
+
data: {
|
|
40
|
+
title: 'NocoBase',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
vi.mock('../useApplications', () => ({
|
|
47
|
+
useApplications: () => ({
|
|
48
|
+
appList: [],
|
|
49
|
+
}),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
vi.mock('../AppListRender', () => ({
|
|
53
|
+
useAppListRender: () => undefined,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
describe('AdminLayoutComponent', () => {
|
|
57
|
+
it('keeps custom admin layout pages alive by layout route name', async () => {
|
|
58
|
+
const layout: LayoutDefinition = {
|
|
59
|
+
routeName: 'admin2',
|
|
60
|
+
routePath: '/admin2',
|
|
61
|
+
rootRouteName: 'admin2',
|
|
62
|
+
uid: 'custom-admin-layout-model',
|
|
63
|
+
layoutModelClass: 'AdminLayoutModel',
|
|
64
|
+
rootPageModelClass: 'RootPageModel',
|
|
65
|
+
childPageModelClass: 'ChildPageModel',
|
|
66
|
+
authCheck: true,
|
|
67
|
+
};
|
|
68
|
+
const events: string[] = [];
|
|
69
|
+
const engine = new FlowEngine();
|
|
70
|
+
engine.context.defineProperty('routeRepository', {
|
|
71
|
+
value: {
|
|
72
|
+
listAccessible: () => [],
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const Page = () => {
|
|
77
|
+
const { name } = useParams();
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
events.push(`mount:${name}`);
|
|
80
|
+
return () => {
|
|
81
|
+
events.push(`unmount:${name}`);
|
|
82
|
+
};
|
|
83
|
+
}, [name]);
|
|
84
|
+
|
|
85
|
+
return <div data-testid={`admin-layout-page-${name}`}>page {name}</div>;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const router = createMemoryRouter(
|
|
89
|
+
[
|
|
90
|
+
{
|
|
91
|
+
id: layout.routeName,
|
|
92
|
+
path: layout.routePath,
|
|
93
|
+
element: <AdminLayoutContent layout={layout} />,
|
|
94
|
+
children: [
|
|
95
|
+
{
|
|
96
|
+
id: 'admin2.__page',
|
|
97
|
+
path: ':name',
|
|
98
|
+
element: <Page />,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
{
|
|
104
|
+
initialEntries: ['/admin2/page-a'],
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
render(
|
|
109
|
+
<FlowEngineProvider engine={engine}>
|
|
110
|
+
<RouterProvider router={router} />
|
|
111
|
+
</FlowEngineProvider>,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(await screen.findByTestId('admin-layout-page-page-a')).toBeInTheDocument();
|
|
115
|
+
|
|
116
|
+
await act(async () => {
|
|
117
|
+
await router.navigate('/admin2/page-b');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(await screen.findByTestId('admin-layout-page-page-b')).toBeInTheDocument();
|
|
121
|
+
expect(screen.getByTestId('admin-layout-page-page-a')).toBeInTheDocument();
|
|
122
|
+
expect(events).toEqual(['mount:page-a', 'mount:page-b']);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('activates the current layout before loading accessible routes', async () => {
|
|
126
|
+
const layout: LayoutDefinition = {
|
|
127
|
+
routeName: 'admin2',
|
|
128
|
+
routePath: '/admin2',
|
|
129
|
+
rootRouteName: 'admin2',
|
|
130
|
+
uid: 'custom-admin-layout-model',
|
|
131
|
+
layoutModelClass: 'AdminLayoutModel',
|
|
132
|
+
rootPageModelClass: 'RootPageModel',
|
|
133
|
+
childPageModelClass: 'ChildPageModel',
|
|
134
|
+
authCheck: true,
|
|
135
|
+
};
|
|
136
|
+
const deactivateLayout = vi.fn();
|
|
137
|
+
const activateLayout = vi.fn(() => deactivateLayout);
|
|
138
|
+
const ensureAccessibleLoaded = vi.fn(async () => []);
|
|
139
|
+
const engine = new FlowEngine();
|
|
140
|
+
engine.context.defineProperty('routeRepository', {
|
|
141
|
+
value: {
|
|
142
|
+
activateLayout,
|
|
143
|
+
ensureAccessibleLoaded,
|
|
144
|
+
listAccessible: () => [],
|
|
145
|
+
subscribe: vi.fn(),
|
|
146
|
+
unsubscribe: vi.fn(),
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
engine.context.defineProperty('t', {
|
|
150
|
+
value: (key: string) => key,
|
|
151
|
+
});
|
|
152
|
+
const model = engine.createModel<AdminLayoutModel>({
|
|
153
|
+
uid: layout.uid,
|
|
154
|
+
use: AdminLayoutModel,
|
|
155
|
+
props: {
|
|
156
|
+
layout,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const router = createMemoryRouter(
|
|
161
|
+
[
|
|
162
|
+
{
|
|
163
|
+
id: layout.routeName,
|
|
164
|
+
path: layout.routePath,
|
|
165
|
+
element: <AdminLayoutComponent model={model} />,
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
{
|
|
169
|
+
initialEntries: ['/admin2'],
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
const { unmount } = render(
|
|
173
|
+
<FlowEngineProvider engine={engine}>
|
|
174
|
+
<RouterProvider router={router} />
|
|
175
|
+
</FlowEngineProvider>,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
await waitFor(() => {
|
|
179
|
+
expect(ensureAccessibleLoaded).toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
expect(activateLayout).toHaveBeenCalledWith(layout);
|
|
182
|
+
expect(activateLayout.mock.invocationCallOrder[0]).toBeLessThan(ensureAccessibleLoaded.mock.invocationCallOrder[0]);
|
|
183
|
+
|
|
184
|
+
unmount();
|
|
185
|
+
|
|
186
|
+
expect(deactivateLayout).toHaveBeenCalledTimes(1);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -318,6 +318,50 @@ describe('AdminLayoutModel menu items', () => {
|
|
|
318
318
|
expect(route.children[1]._model).toBe(adminLayoutModel.subModels.menuItems?.[1]);
|
|
319
319
|
});
|
|
320
320
|
|
|
321
|
+
it('should generate ProLayout route tree under a custom admin layout route path', () => {
|
|
322
|
+
const adminLayoutModel = engine.createModel<AdminLayoutModel>({
|
|
323
|
+
uid: 'admin2-layout-model',
|
|
324
|
+
use: AdminLayoutModel,
|
|
325
|
+
props: {
|
|
326
|
+
layout: {
|
|
327
|
+
routeName: 'admin2',
|
|
328
|
+
routePath: '/admin2',
|
|
329
|
+
uid: 'admin2-layout-model',
|
|
330
|
+
layoutModelClass: 'AdminLayoutModel',
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
adminLayoutModel.syncMenuRoutes([
|
|
336
|
+
{
|
|
337
|
+
id: 1,
|
|
338
|
+
title: 'Group',
|
|
339
|
+
type: NocoBaseDesktopRouteType.group,
|
|
340
|
+
children: [
|
|
341
|
+
{
|
|
342
|
+
id: 11,
|
|
343
|
+
title: 'Page 1',
|
|
344
|
+
schemaUid: 'page-1',
|
|
345
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
},
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
const route = adminLayoutModel.toProLayoutRoute({
|
|
352
|
+
designable: false,
|
|
353
|
+
isMobile: false,
|
|
354
|
+
t: (title) => title,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
expect(route.children[0].path).toBe('/admin2/1');
|
|
358
|
+
expect(route.children[0].redirect).toBe('/admin2/page-1');
|
|
359
|
+
expect(route.children[0]._runtimePath).toBe('/apps/demo/v2/admin2/page-1');
|
|
360
|
+
expect(route.children[0].routes?.[0].path).toBe('/admin2/page-1');
|
|
361
|
+
expect(route.children[0].routes?.[0].redirect).toBe('/admin2/page-1');
|
|
362
|
+
expect(route.children[0].routes?.[0]._runtimePath).toBe('/apps/demo/v2/admin2/page-1');
|
|
363
|
+
});
|
|
364
|
+
|
|
321
365
|
it('should filter legacy page menu routes but keep empty groups in v2 admin layout', () => {
|
|
322
366
|
const adminLayoutModel = engine.createModel<AdminLayoutModel>({
|
|
323
367
|
uid: 'admin-layout-model',
|
|
@@ -1128,6 +1172,46 @@ describe('AdminLayoutModel menu items', () => {
|
|
|
1128
1172
|
expect(adminLayoutModel.menuRouteRefreshVersion).toBe(refreshBefore + 1);
|
|
1129
1173
|
});
|
|
1130
1174
|
|
|
1175
|
+
it('should refresh the owning admin layout when hiding custom layout menu routes dynamically', () => {
|
|
1176
|
+
const defaultAdminLayoutModel = engine.createModel<AdminLayoutModel>({
|
|
1177
|
+
uid: ADMIN_LAYOUT_MODEL_UID,
|
|
1178
|
+
use: AdminLayoutModel,
|
|
1179
|
+
});
|
|
1180
|
+
const customAdminLayoutModel = engine.createModel<AdminLayoutModel>({
|
|
1181
|
+
uid: 'custom-admin-layout-model',
|
|
1182
|
+
use: AdminLayoutModel,
|
|
1183
|
+
props: {
|
|
1184
|
+
layout: {
|
|
1185
|
+
routeName: 'admin2',
|
|
1186
|
+
routePath: '/admin2',
|
|
1187
|
+
rootRouteName: 'admin2',
|
|
1188
|
+
uid: 'custom-admin-layout-model',
|
|
1189
|
+
layoutModelClass: 'AdminLayoutModel',
|
|
1190
|
+
rootPageModelClass: 'RootPageModel',
|
|
1191
|
+
childPageModelClass: 'ChildPageModel',
|
|
1192
|
+
authCheck: true,
|
|
1193
|
+
},
|
|
1194
|
+
},
|
|
1195
|
+
});
|
|
1196
|
+
const model = engine.createModel<AdminLayoutMenuItemModel>({
|
|
1197
|
+
uid: 'menu-item-custom-layout-dynamic-hidden',
|
|
1198
|
+
use: AdminLayoutMenuItemModel,
|
|
1199
|
+
props: {
|
|
1200
|
+
route: createRoute(),
|
|
1201
|
+
},
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
model.setParent(customAdminLayoutModel);
|
|
1205
|
+
|
|
1206
|
+
const defaultRefreshBefore = defaultAdminLayoutModel.menuRouteRefreshVersion;
|
|
1207
|
+
const customRefreshBefore = customAdminLayoutModel.menuRouteRefreshVersion;
|
|
1208
|
+
|
|
1209
|
+
model.setHidden(true);
|
|
1210
|
+
|
|
1211
|
+
expect(customAdminLayoutModel.menuRouteRefreshVersion).toBe(customRefreshBefore + 1);
|
|
1212
|
+
expect(defaultAdminLayoutModel.menuRouteRefreshVersion).toBe(defaultRefreshBefore);
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1131
1215
|
it('should render hidden menu item with opacity and keep original title in config mode', () => {
|
|
1132
1216
|
const model = engine.createModel<AdminLayoutMenuItemModel>({
|
|
1133
1217
|
uid: 'menu-item-hidden-in-config',
|
|
@@ -2222,6 +2306,21 @@ describe('AdminLayoutModel menu items', () => {
|
|
|
2222
2306
|
);
|
|
2223
2307
|
});
|
|
2224
2308
|
|
|
2309
|
+
it('should include router basename when opening same-origin links in a new window', async () => {
|
|
2310
|
+
await openAdminLayoutMenuLink({
|
|
2311
|
+
context: engine.context as any,
|
|
2312
|
+
href: '/admin2/page',
|
|
2313
|
+
params: [{ name: 'from', value: 'admin' }],
|
|
2314
|
+
openInNewWindow: true,
|
|
2315
|
+
isMobile: false,
|
|
2316
|
+
closeMobileMenu: vi.fn(),
|
|
2317
|
+
navigate: navigateMock,
|
|
2318
|
+
basenameOfCurrentRouter: '/v',
|
|
2319
|
+
});
|
|
2320
|
+
|
|
2321
|
+
expect(window.open).toHaveBeenCalledWith('/v/admin2/page?from=admin', '_blank', 'noopener,noreferrer');
|
|
2322
|
+
});
|
|
2323
|
+
|
|
2225
2324
|
it('should resolve sibling move options for non-group drag target', () => {
|
|
2226
2325
|
const activeModel = engine.createModel<AdminLayoutMenuItemModel>({
|
|
2227
2326
|
uid: 'drag-source-page',
|