@nocobase/client-v2 2.2.0-beta.5 → 2.2.0-beta.6
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/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 +143 -117
- package/lib/index.js +148 -122
- package/package.json +7 -7
- package/src/RouteRepository.ts +126 -24
- package/src/__tests__/RouteRepository.test.ts +216 -0
- package/src/__tests__/exports.test.ts +16 -0
- package/src/__tests__/settings-center.test.tsx +33 -0
- 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__/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/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 +62 -12
- package/src/flow/components/FlowRoute.tsx +56 -11
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
- 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/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/settings-center/AdminSettingsLayout.tsx +3 -0
- package/src/settings-center/plugin-manager/index.tsx +3 -0
|
@@ -42,9 +42,11 @@ import {
|
|
|
42
42
|
} from './AdminLayoutMenuFlowUtils';
|
|
43
43
|
import { ADMIN_LAYOUT_MODEL_UID } from './constants';
|
|
44
44
|
import {
|
|
45
|
+
type AdminLayoutRoutePathLike,
|
|
45
46
|
findFirstAccessiblePageRoute,
|
|
46
47
|
findFirstV2LandingRoute,
|
|
47
48
|
isV2AdminRuntime,
|
|
49
|
+
joinAdminLayoutRoutePath,
|
|
48
50
|
resolveAdminRouteRuntimeTarget,
|
|
49
51
|
toRouterNavigationPath,
|
|
50
52
|
} from './resolveAdminRouteRuntimeTarget';
|
|
@@ -64,6 +66,26 @@ const omitMenuRuntimeProps = (props: Record<string, unknown> = {}) => {
|
|
|
64
66
|
return persistableProps;
|
|
65
67
|
};
|
|
66
68
|
|
|
69
|
+
type MenuRouteTreeRefreshableModel = FlowModel & {
|
|
70
|
+
refreshMenuRouteTree?: () => void;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function isMenuRouteTreeRefreshableModel(model: FlowModel | undefined): model is MenuRouteTreeRefreshableModel {
|
|
74
|
+
return typeof (model as MenuRouteTreeRefreshableModel | undefined)?.refreshMenuRouteTree === 'function';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getOwnerMenuRouteTreeRefreshModel(model: FlowModel) {
|
|
78
|
+
let current = model.parent as FlowModel | undefined;
|
|
79
|
+
while (current) {
|
|
80
|
+
if (isMenuRouteTreeRefreshableModel(current)) {
|
|
81
|
+
return current;
|
|
82
|
+
}
|
|
83
|
+
current = current.parent as FlowModel | undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
67
89
|
export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStructure> {
|
|
68
90
|
private creationPersisted = false;
|
|
69
91
|
private persistedStateHydrated = false;
|
|
@@ -129,6 +151,24 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
129
151
|
return this.context.routeRepository;
|
|
130
152
|
}
|
|
131
153
|
|
|
154
|
+
getLayout(): AdminLayoutRoutePathLike | undefined {
|
|
155
|
+
const contextLayout = this.context.layout as AdminLayoutRoutePathLike | undefined;
|
|
156
|
+
if (contextLayout?.routePath) {
|
|
157
|
+
return contextLayout;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let current: FlowModel | undefined = this;
|
|
161
|
+
while (current) {
|
|
162
|
+
const propsLayout = current.props?.layout as AdminLayoutRoutePathLike | undefined;
|
|
163
|
+
if (propsLayout?.routePath) {
|
|
164
|
+
return propsLayout;
|
|
165
|
+
}
|
|
166
|
+
current = current.parent as FlowModel | undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
132
172
|
hasPersistedMenuInstanceFlowFlag(route = this.getRoute()) {
|
|
133
173
|
return route?.options?.hasPersistedMenuInstanceFlow === true;
|
|
134
174
|
}
|
|
@@ -280,7 +320,12 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
280
320
|
const previous = this.hidden;
|
|
281
321
|
super.setHidden(value);
|
|
282
322
|
if (previous !== this.hidden) {
|
|
283
|
-
|
|
323
|
+
const defaultLayoutModel = this.flowEngine.getModel?.(ADMIN_LAYOUT_MODEL_UID);
|
|
324
|
+
const refreshableModel =
|
|
325
|
+
getOwnerMenuRouteTreeRefreshModel(this) ||
|
|
326
|
+
(isMenuRouteTreeRefreshableModel(defaultLayoutModel) ? defaultLayoutModel : undefined);
|
|
327
|
+
|
|
328
|
+
refreshableModel?.refreshMenuRouteTree?.();
|
|
284
329
|
}
|
|
285
330
|
}
|
|
286
331
|
|
|
@@ -533,9 +578,10 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
533
578
|
return false;
|
|
534
579
|
}
|
|
535
580
|
|
|
581
|
+
const layout = this.getLayout();
|
|
536
582
|
const allAccessRoutes = this.getRouteRepository().listAccessible();
|
|
537
583
|
const pathname = this.context.location?.pathname || window.location.pathname;
|
|
538
|
-
const shouldNavigate = matchesRoutePath(route, pathname, this.context.router?.basename);
|
|
584
|
+
const shouldNavigate = matchesRoutePath(route, pathname, this.context.router?.basename, layout);
|
|
539
585
|
const prevSibling = findPrevSiblingRoute(allAccessRoutes, route);
|
|
540
586
|
const nextSibling = findNextSiblingRoute(allAccessRoutes, route);
|
|
541
587
|
|
|
@@ -554,6 +600,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
554
600
|
const target = resolveAdminRouteRuntimeTarget({
|
|
555
601
|
app: this.context.app,
|
|
556
602
|
route: sibling,
|
|
603
|
+
layout,
|
|
557
604
|
});
|
|
558
605
|
|
|
559
606
|
if (!target.runtimePath) {
|
|
@@ -584,6 +631,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
584
631
|
|
|
585
632
|
const shouldShowIconInTitle = shouldRenderIconInTitle({ depth, isMobile: options.isMobile });
|
|
586
633
|
const { name, icon } = buildMenuTitleWithIcon(route, options.t, shouldShowIconInTitle);
|
|
634
|
+
const layout = this.getLayout();
|
|
587
635
|
|
|
588
636
|
if (route.type === NocoBaseDesktopRouteType.link) {
|
|
589
637
|
const identity = route.id ?? route.schemaUid ?? route.menuSchemaUid ?? route.title ?? this.uid;
|
|
@@ -591,7 +639,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
591
639
|
return {
|
|
592
640
|
name,
|
|
593
641
|
icon,
|
|
594
|
-
path: getAdminLayoutMenuVirtualPath('link', identity),
|
|
642
|
+
path: getAdminLayoutMenuVirtualPath('link', identity, layout),
|
|
595
643
|
hideInMenu: route.hideInMenu,
|
|
596
644
|
_route: route,
|
|
597
645
|
_parentRoute: parentRoute,
|
|
@@ -604,6 +652,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
604
652
|
const runtimeTarget = resolveAdminRouteRuntimeTarget({
|
|
605
653
|
app: this.context.app,
|
|
606
654
|
route,
|
|
655
|
+
layout,
|
|
607
656
|
});
|
|
608
657
|
|
|
609
658
|
if (runtimeTarget.reason === 'unsupportedV2Runtime') {
|
|
@@ -611,14 +660,14 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
611
660
|
}
|
|
612
661
|
|
|
613
662
|
const path = route.schemaUid
|
|
614
|
-
?
|
|
615
|
-
: getAdminLayoutMenuVirtualPath('link', `${this.uid}-invalid
|
|
663
|
+
? joinAdminLayoutRoutePath(layout, route.schemaUid)
|
|
664
|
+
: getAdminLayoutMenuVirtualPath('link', `${this.uid}-invalid`, layout);
|
|
616
665
|
|
|
617
666
|
return {
|
|
618
667
|
name,
|
|
619
668
|
icon,
|
|
620
669
|
path,
|
|
621
|
-
redirect: route.schemaUid ?
|
|
670
|
+
redirect: route.schemaUid ? joinAdminLayoutRoutePath(layout, route.schemaUid) : undefined,
|
|
622
671
|
hideInMenu: route.hideInMenu,
|
|
623
672
|
disabled: !runtimeTarget.runtimePath,
|
|
624
673
|
_runtimePath: runtimeTarget.runtimePath,
|
|
@@ -644,7 +693,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
644
693
|
.filter(Boolean) || [];
|
|
645
694
|
|
|
646
695
|
if (options.designable && depth === 0) {
|
|
647
|
-
children.push(getAdminLayoutMenuInitializerButton('schema-initializer-Menu-side', this, route));
|
|
696
|
+
children.push(getAdminLayoutMenuInitializerButton('schema-initializer-Menu-side', this, route, layout));
|
|
648
697
|
}
|
|
649
698
|
|
|
650
699
|
const landingRoute = isV2AdminRuntime(this.context.app)
|
|
@@ -653,14 +702,17 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
653
702
|
const runtimeTarget = resolveAdminRouteRuntimeTarget({
|
|
654
703
|
app: this.context.app,
|
|
655
704
|
route: isV2AdminRuntime(this.context.app) && landingRoute ? landingRoute : route,
|
|
705
|
+
layout,
|
|
656
706
|
});
|
|
657
707
|
|
|
658
708
|
const groupRoute: AdminLayoutMenuNode = {
|
|
659
709
|
name,
|
|
660
710
|
icon,
|
|
661
|
-
path:
|
|
711
|
+
path: joinAdminLayoutRoutePath(layout, route.id),
|
|
662
712
|
redirect:
|
|
663
|
-
children[0]?.key === 'x-designer-button'
|
|
713
|
+
children[0]?.key === 'x-designer-button'
|
|
714
|
+
? undefined
|
|
715
|
+
: joinAdminLayoutRoutePath(layout, landingRoute?.schemaUid || route.id),
|
|
664
716
|
hideInMenu: route.hideInMenu,
|
|
665
717
|
_runtimePath: runtimeTarget.runtimePath,
|
|
666
718
|
_navigationMode: runtimeTarget.navigationMode,
|
|
@@ -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
|
+
});
|