@nocobase/client-v2 2.2.0-beta.8 → 2.2.0-beta.9
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/BaseApplication.d.ts +2 -0
- package/es/RouteRepository.d.ts +8 -0
- package/es/RouterManager.d.ts +15 -0
- package/es/authRedirect.d.ts +1 -0
- package/es/entry-actions/EntryActionManager.d.ts +24 -0
- package/es/entry-actions/index.d.ts +9 -0
- package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
- package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
- package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
- package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
- package/es/flow/models/base/ActionModelCore.d.ts +2 -0
- package/es/flow/routeTransientInputArgs.d.ts +14 -0
- package/es/index.d.ts +2 -0
- package/es/index.mjs +212 -126
- package/es/utils/markdownSanitize.d.ts +11 -0
- package/lib/index.js +198 -112
- package/package.json +7 -7
- package/src/BaseApplication.tsx +2 -0
- package/src/RouteRepository.ts +25 -0
- package/src/RouterManager.tsx +142 -1
- package/src/__tests__/RouteRepository.test.ts +23 -0
- package/src/__tests__/RouterManager.test.ts +125 -0
- package/src/__tests__/authRedirect.test.ts +17 -0
- package/src/authRedirect.ts +38 -0
- package/src/entry-actions/EntryActionManager.ts +76 -0
- package/src/entry-actions/index.ts +10 -0
- package/src/flow/FlowPage.tsx +29 -2
- package/src/flow/__tests__/FlowPage.test.tsx +152 -25
- package/src/flow/__tests__/FlowRoute.test.tsx +547 -2
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
- package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
- package/src/flow/actions/dataScopeFilter.ts +10 -1
- package/src/flow/actions/dateTimeFormat.tsx +2 -2
- package/src/flow/actions/openView.tsx +21 -1
- package/src/flow/admin-shell/BaseLayoutModel.tsx +124 -0
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -4
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +177 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +20 -0
- package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
- package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +436 -2
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
- package/src/flow/admin-shell/admin-layout/index.ts +1 -0
- package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
- package/src/flow/common/Markdown/Display.tsx +14 -3
- package/src/flow/common/Markdown/Edit.tsx +19 -7
- package/src/flow/components/FlowRoute.tsx +128 -16
- package/src/flow/internal/components/Markdown/util.ts +2 -1
- package/src/flow/models/base/ActionModel.tsx +10 -8
- package/src/flow/models/base/ActionModelCore.tsx +5 -0
- package/src/flow/models/blocks/table/TableColumnModel.tsx +6 -2
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +51 -0
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +45 -13
- package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
- package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
- package/src/flow/routeTransientInputArgs.ts +27 -0
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
- package/src/index.ts +2 -0
- package/src/nocobase-buildin-plugin/index.tsx +7 -1
- package/src/utils/markdownSanitize.ts +88 -0
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
12
|
-
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
|
13
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
12
|
+
import { MemoryRouter, Route, Router, Routes, useNavigate } from 'react-router-dom';
|
|
13
|
+
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
14
14
|
import { FlowContextProvider, FlowEngine, FlowEngineProvider, type FlowModel } from '@nocobase/flow-engine';
|
|
15
15
|
import FlowRoute from '../components/FlowRoute';
|
|
16
16
|
import { RouteRepository } from '../../RouteRepository';
|
|
17
|
+
import { NocoBaseDesktopRouteType } from '../../flow-compat';
|
|
17
18
|
|
|
18
19
|
type MockAdminLayoutModel = FlowModel & {
|
|
19
20
|
registerRoutePage: ReturnType<typeof vi.fn>;
|
|
@@ -21,6 +22,8 @@ type MockAdminLayoutModel = FlowModel & {
|
|
|
21
22
|
unregisterRoutePage: ReturnType<typeof vi.fn>;
|
|
22
23
|
};
|
|
23
24
|
|
|
25
|
+
type RouterNavigator = React.ComponentProps<typeof Router>['navigator'];
|
|
26
|
+
|
|
24
27
|
const { hookState } = vi.hoisted(() => {
|
|
25
28
|
return {
|
|
26
29
|
hookState: {
|
|
@@ -893,6 +896,548 @@ describe('FlowRoute', () => {
|
|
|
893
896
|
expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
|
|
894
897
|
});
|
|
895
898
|
|
|
899
|
+
it('should render blank content when current admin route matches an accessible empty group id', async () => {
|
|
900
|
+
const engine = new FlowEngine();
|
|
901
|
+
const listAccessible = vi.fn(() => [
|
|
902
|
+
{
|
|
903
|
+
id: 371750686228480,
|
|
904
|
+
schemaUid: 'group-schema',
|
|
905
|
+
title: '789',
|
|
906
|
+
type: NocoBaseDesktopRouteType.group,
|
|
907
|
+
},
|
|
908
|
+
]);
|
|
909
|
+
engine.context.defineProperty('routeRepository', {
|
|
910
|
+
value: {
|
|
911
|
+
refreshAccessible: hookState.refresh,
|
|
912
|
+
isAccessibleLoaded: () => true,
|
|
913
|
+
ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
|
|
914
|
+
getRouteBySchemaUid: vi.fn(() => undefined),
|
|
915
|
+
listAccessible,
|
|
916
|
+
},
|
|
917
|
+
});
|
|
918
|
+
engine.context.defineProperty('app', {
|
|
919
|
+
value: {
|
|
920
|
+
getPublicPath: () => '/v2/',
|
|
921
|
+
router: {
|
|
922
|
+
getBasename: () => '/v2',
|
|
923
|
+
},
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
const adminLayoutModel: MockAdminLayoutModel = Object.assign(
|
|
928
|
+
engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
|
|
929
|
+
{
|
|
930
|
+
registerRoutePage: vi.fn(),
|
|
931
|
+
updateRoutePage: vi.fn(),
|
|
932
|
+
unregisterRoutePage: vi.fn(),
|
|
933
|
+
},
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
const { container } = render(
|
|
937
|
+
<FlowEngineProvider engine={engine}>
|
|
938
|
+
<MemoryRouter initialEntries={['/admin/371750686228480']}>
|
|
939
|
+
<Routes>
|
|
940
|
+
<Route path="/admin/:name" element={<FlowRoute />} />
|
|
941
|
+
</Routes>
|
|
942
|
+
</MemoryRouter>
|
|
943
|
+
</FlowEngineProvider>,
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
await waitFor(() => {
|
|
947
|
+
expect(listAccessible).toHaveBeenCalled();
|
|
948
|
+
});
|
|
949
|
+
expect(container).toBeEmptyDOMElement();
|
|
950
|
+
expect(screen.queryByText('404')).not.toBeInTheDocument();
|
|
951
|
+
expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
it('should navigate a current admin group route to its first accessible flow page', async () => {
|
|
955
|
+
const engine = new FlowEngine();
|
|
956
|
+
const childRoute = {
|
|
957
|
+
schemaUid: 'child-flow-page',
|
|
958
|
+
title: 'Child flow page',
|
|
959
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
960
|
+
};
|
|
961
|
+
const groupRoute = {
|
|
962
|
+
id: 371750686228480,
|
|
963
|
+
schemaUid: 'group-schema',
|
|
964
|
+
title: 'Group with child',
|
|
965
|
+
type: NocoBaseDesktopRouteType.group,
|
|
966
|
+
children: [childRoute],
|
|
967
|
+
};
|
|
968
|
+
const getRouteBySchemaUid = vi.fn((schemaUid: string) =>
|
|
969
|
+
schemaUid === 'child-flow-page' ? childRoute : undefined,
|
|
970
|
+
);
|
|
971
|
+
const listAccessible = vi.fn(() => [groupRoute]);
|
|
972
|
+
engine.context.defineProperty('routeRepository', {
|
|
973
|
+
value: {
|
|
974
|
+
refreshAccessible: hookState.refresh,
|
|
975
|
+
isAccessibleLoaded: () => true,
|
|
976
|
+
ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
|
|
977
|
+
getRouteBySchemaUid,
|
|
978
|
+
listAccessible,
|
|
979
|
+
},
|
|
980
|
+
});
|
|
981
|
+
engine.context.defineProperty('app', {
|
|
982
|
+
value: {
|
|
983
|
+
getPublicPath: () => '/v2/',
|
|
984
|
+
router: {
|
|
985
|
+
getBasename: () => '/v2',
|
|
986
|
+
},
|
|
987
|
+
},
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
const adminLayoutModel: MockAdminLayoutModel = Object.assign(
|
|
991
|
+
engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
|
|
992
|
+
{
|
|
993
|
+
registerRoutePage: vi.fn(),
|
|
994
|
+
updateRoutePage: vi.fn(),
|
|
995
|
+
unregisterRoutePage: vi.fn(),
|
|
996
|
+
},
|
|
997
|
+
);
|
|
998
|
+
|
|
999
|
+
render(
|
|
1000
|
+
<FlowEngineProvider engine={engine}>
|
|
1001
|
+
<MemoryRouter initialEntries={['/admin/371750686228480']}>
|
|
1002
|
+
<Routes>
|
|
1003
|
+
<Route path="/admin/:name" element={<FlowRoute />} />
|
|
1004
|
+
</Routes>
|
|
1005
|
+
</MemoryRouter>
|
|
1006
|
+
</FlowEngineProvider>,
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
await waitFor(() => {
|
|
1010
|
+
expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith('child-flow-page', expect.any(Object));
|
|
1011
|
+
});
|
|
1012
|
+
expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('371750686228480', expect.any(Object));
|
|
1013
|
+
expect(screen.queryByText('404')).not.toBeInTheDocument();
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
it('should keep rendering 404 when a group id is opened with a nested admin route path', async () => {
|
|
1017
|
+
const engine = new FlowEngine();
|
|
1018
|
+
const childRoute = {
|
|
1019
|
+
schemaUid: 'child-flow-page',
|
|
1020
|
+
title: 'Child flow page',
|
|
1021
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
1022
|
+
};
|
|
1023
|
+
const groupRoute = {
|
|
1024
|
+
id: 371750686228480,
|
|
1025
|
+
schemaUid: 'group-schema',
|
|
1026
|
+
title: 'Group with child',
|
|
1027
|
+
type: NocoBaseDesktopRouteType.group,
|
|
1028
|
+
children: [childRoute],
|
|
1029
|
+
};
|
|
1030
|
+
engine.context.defineProperty('routeRepository', {
|
|
1031
|
+
value: {
|
|
1032
|
+
refreshAccessible: hookState.refresh,
|
|
1033
|
+
isAccessibleLoaded: () => true,
|
|
1034
|
+
ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
|
|
1035
|
+
getRouteBySchemaUid: vi.fn((schemaUid: string) => (schemaUid === 'child-flow-page' ? childRoute : undefined)),
|
|
1036
|
+
listAccessible: vi.fn(() => [groupRoute]),
|
|
1037
|
+
},
|
|
1038
|
+
});
|
|
1039
|
+
engine.context.defineProperty('app', {
|
|
1040
|
+
value: {
|
|
1041
|
+
getPublicPath: () => '/v2/',
|
|
1042
|
+
router: {
|
|
1043
|
+
getBasename: () => '/v2',
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
const adminLayoutModel: MockAdminLayoutModel = Object.assign(
|
|
1049
|
+
engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
|
|
1050
|
+
{
|
|
1051
|
+
registerRoutePage: vi.fn(),
|
|
1052
|
+
updateRoutePage: vi.fn(),
|
|
1053
|
+
unregisterRoutePage: vi.fn(),
|
|
1054
|
+
},
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
render(
|
|
1058
|
+
<FlowEngineProvider engine={engine}>
|
|
1059
|
+
<MemoryRouter initialEntries={['/admin/371750686228480/view/not-exists']}>
|
|
1060
|
+
<Routes>
|
|
1061
|
+
<Route path="/admin/:name/view/*" element={<FlowRoute />} />
|
|
1062
|
+
</Routes>
|
|
1063
|
+
</MemoryRouter>
|
|
1064
|
+
</FlowEngineProvider>,
|
|
1065
|
+
);
|
|
1066
|
+
|
|
1067
|
+
expect(await screen.findByText('404')).toBeInTheDocument();
|
|
1068
|
+
expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('child-flow-page', expect.any(Object));
|
|
1069
|
+
expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('371750686228480', expect.any(Object));
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
it('should keep rendering 404 when a group id is opened with a tab admin route path', async () => {
|
|
1073
|
+
const engine = new FlowEngine();
|
|
1074
|
+
const childRoute = {
|
|
1075
|
+
schemaUid: 'child-flow-page',
|
|
1076
|
+
title: 'Child flow page',
|
|
1077
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
1078
|
+
};
|
|
1079
|
+
const groupRoute = {
|
|
1080
|
+
id: 371750686228480,
|
|
1081
|
+
schemaUid: 'group-schema',
|
|
1082
|
+
title: 'Group with child',
|
|
1083
|
+
type: NocoBaseDesktopRouteType.group,
|
|
1084
|
+
children: [childRoute],
|
|
1085
|
+
};
|
|
1086
|
+
engine.context.defineProperty('routeRepository', {
|
|
1087
|
+
value: {
|
|
1088
|
+
refreshAccessible: hookState.refresh,
|
|
1089
|
+
isAccessibleLoaded: () => true,
|
|
1090
|
+
ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
|
|
1091
|
+
getRouteBySchemaUid: vi.fn((schemaUid: string) => (schemaUid === 'child-flow-page' ? childRoute : undefined)),
|
|
1092
|
+
listAccessible: vi.fn(() => [groupRoute]),
|
|
1093
|
+
},
|
|
1094
|
+
});
|
|
1095
|
+
engine.context.defineProperty('app', {
|
|
1096
|
+
value: {
|
|
1097
|
+
getPublicPath: () => '/v2/',
|
|
1098
|
+
router: {
|
|
1099
|
+
getBasename: () => '/v2',
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
const adminLayoutModel: MockAdminLayoutModel = Object.assign(
|
|
1105
|
+
engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
|
|
1106
|
+
{
|
|
1107
|
+
registerRoutePage: vi.fn(),
|
|
1108
|
+
updateRoutePage: vi.fn(),
|
|
1109
|
+
unregisterRoutePage: vi.fn(),
|
|
1110
|
+
},
|
|
1111
|
+
);
|
|
1112
|
+
|
|
1113
|
+
render(
|
|
1114
|
+
<FlowEngineProvider engine={engine}>
|
|
1115
|
+
<MemoryRouter initialEntries={['/admin/371750686228480/tab/not-exists']}>
|
|
1116
|
+
<Routes>
|
|
1117
|
+
<Route path="/admin/:name/tab/:tabUid" element={<FlowRoute />} />
|
|
1118
|
+
</Routes>
|
|
1119
|
+
</MemoryRouter>
|
|
1120
|
+
</FlowEngineProvider>,
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
expect(await screen.findByText('404')).toBeInTheDocument();
|
|
1124
|
+
expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('child-flow-page', expect.any(Object));
|
|
1125
|
+
expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('371750686228480', expect.any(Object));
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
it('should keep blank content while a group child navigation is already pending for the current path', async () => {
|
|
1129
|
+
const engine = new FlowEngine();
|
|
1130
|
+
const childRoute = {
|
|
1131
|
+
schemaUid: 'child-flow-page',
|
|
1132
|
+
title: 'Child flow page',
|
|
1133
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
1134
|
+
};
|
|
1135
|
+
const groupRoute = {
|
|
1136
|
+
id: 371750686228480,
|
|
1137
|
+
schemaUid: 'group-schema',
|
|
1138
|
+
title: 'Group with child',
|
|
1139
|
+
type: NocoBaseDesktopRouteType.group,
|
|
1140
|
+
children: [childRoute],
|
|
1141
|
+
};
|
|
1142
|
+
engine.context.defineProperty('routeRepository', {
|
|
1143
|
+
value: {
|
|
1144
|
+
refreshAccessible: hookState.refresh,
|
|
1145
|
+
isAccessibleLoaded: () => true,
|
|
1146
|
+
ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
|
|
1147
|
+
getRouteBySchemaUid: vi.fn((schemaUid: string) => (schemaUid === 'child-flow-page' ? childRoute : undefined)),
|
|
1148
|
+
listAccessible: vi.fn(() => [groupRoute]),
|
|
1149
|
+
},
|
|
1150
|
+
});
|
|
1151
|
+
engine.context.defineProperty('app', {
|
|
1152
|
+
value: {
|
|
1153
|
+
getPublicPath: () => '/v2/',
|
|
1154
|
+
router: {
|
|
1155
|
+
getBasename: () => '/v2',
|
|
1156
|
+
},
|
|
1157
|
+
},
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
const replace = vi.fn();
|
|
1161
|
+
const navigator: RouterNavigator = {
|
|
1162
|
+
createHref: (to: { pathname?: string; search?: string; hash?: string } | string) =>
|
|
1163
|
+
typeof to === 'string' ? to : `${to.pathname || ''}${to.search || ''}${to.hash || ''}`,
|
|
1164
|
+
go: vi.fn(),
|
|
1165
|
+
push: vi.fn(),
|
|
1166
|
+
replace,
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
const RerenderAfterReplace = () => {
|
|
1170
|
+
const [rerendered, setRerendered] = React.useState(false);
|
|
1171
|
+
React.useEffect(() => {
|
|
1172
|
+
if (!rerendered && replace.mock.calls.length > 0) {
|
|
1173
|
+
setRerendered(true);
|
|
1174
|
+
}
|
|
1175
|
+
}, [rerendered]);
|
|
1176
|
+
|
|
1177
|
+
return <FlowRoute legacyPageBehavior={rerendered ? 'bridge' : undefined} />;
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
render(
|
|
1181
|
+
<FlowEngineProvider engine={engine}>
|
|
1182
|
+
<Router location="/admin/371750686228480" navigator={navigator}>
|
|
1183
|
+
<Routes>
|
|
1184
|
+
<Route path="/admin/:name" element={<RerenderAfterReplace />} />
|
|
1185
|
+
</Routes>
|
|
1186
|
+
</Router>
|
|
1187
|
+
</FlowEngineProvider>,
|
|
1188
|
+
);
|
|
1189
|
+
|
|
1190
|
+
await waitFor(() => {
|
|
1191
|
+
expect(replace).toHaveBeenCalled();
|
|
1192
|
+
});
|
|
1193
|
+
await act(async () => {
|
|
1194
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1195
|
+
});
|
|
1196
|
+
expect(screen.queryByText('404')).not.toBeInTheDocument();
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
it('should reset group navigation guard when page uid changes in the same route instance', async () => {
|
|
1200
|
+
const engine = new FlowEngine();
|
|
1201
|
+
const childRoute1 = {
|
|
1202
|
+
schemaUid: 'child-flow-page-1',
|
|
1203
|
+
title: 'Child flow page 1',
|
|
1204
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
1205
|
+
};
|
|
1206
|
+
const childRoute2 = {
|
|
1207
|
+
schemaUid: 'child-flow-page-2',
|
|
1208
|
+
title: 'Child flow page 2',
|
|
1209
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
1210
|
+
};
|
|
1211
|
+
const groupRoute1 = {
|
|
1212
|
+
id: 371750686228480,
|
|
1213
|
+
schemaUid: 'group-schema-1',
|
|
1214
|
+
title: 'Group with child 1',
|
|
1215
|
+
type: NocoBaseDesktopRouteType.group,
|
|
1216
|
+
children: [childRoute1],
|
|
1217
|
+
};
|
|
1218
|
+
const groupRoute2 = {
|
|
1219
|
+
id: 371750755434496,
|
|
1220
|
+
schemaUid: 'group-schema-2',
|
|
1221
|
+
title: 'Group with child 2',
|
|
1222
|
+
type: NocoBaseDesktopRouteType.group,
|
|
1223
|
+
children: [childRoute2],
|
|
1224
|
+
};
|
|
1225
|
+
const routeMap: Record<string, typeof childRoute1 | typeof childRoute2> = {
|
|
1226
|
+
'child-flow-page-1': childRoute1,
|
|
1227
|
+
'child-flow-page-2': childRoute2,
|
|
1228
|
+
};
|
|
1229
|
+
const getRouteBySchemaUid = vi.fn((schemaUid: string) => routeMap[schemaUid]);
|
|
1230
|
+
const listAccessible = vi.fn(() => [groupRoute1, groupRoute2]);
|
|
1231
|
+
engine.context.defineProperty('routeRepository', {
|
|
1232
|
+
value: {
|
|
1233
|
+
refreshAccessible: hookState.refresh,
|
|
1234
|
+
isAccessibleLoaded: () => true,
|
|
1235
|
+
ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
|
|
1236
|
+
getRouteBySchemaUid,
|
|
1237
|
+
listAccessible,
|
|
1238
|
+
},
|
|
1239
|
+
});
|
|
1240
|
+
engine.context.defineProperty('app', {
|
|
1241
|
+
value: {
|
|
1242
|
+
getPublicPath: () => '/v2/',
|
|
1243
|
+
router: {
|
|
1244
|
+
getBasename: () => '/v2',
|
|
1245
|
+
},
|
|
1246
|
+
},
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
const adminLayoutModel: MockAdminLayoutModel = Object.assign(
|
|
1250
|
+
engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
|
|
1251
|
+
{
|
|
1252
|
+
registerRoutePage: vi.fn(),
|
|
1253
|
+
updateRoutePage: vi.fn(),
|
|
1254
|
+
unregisterRoutePage: vi.fn(),
|
|
1255
|
+
},
|
|
1256
|
+
);
|
|
1257
|
+
let navigateTo: (path: string) => void = () => {};
|
|
1258
|
+
const CaptureNavigate = () => {
|
|
1259
|
+
const navigate = useNavigate();
|
|
1260
|
+
React.useEffect(() => {
|
|
1261
|
+
navigateTo = navigate;
|
|
1262
|
+
}, [navigate]);
|
|
1263
|
+
return null;
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
render(
|
|
1267
|
+
<FlowEngineProvider engine={engine}>
|
|
1268
|
+
<MemoryRouter initialEntries={['/admin/371750686228480']}>
|
|
1269
|
+
<CaptureNavigate />
|
|
1270
|
+
<Routes>
|
|
1271
|
+
<Route path="/admin/:name" element={<FlowRoute />} />
|
|
1272
|
+
</Routes>
|
|
1273
|
+
</MemoryRouter>
|
|
1274
|
+
</FlowEngineProvider>,
|
|
1275
|
+
);
|
|
1276
|
+
|
|
1277
|
+
await waitFor(() => {
|
|
1278
|
+
expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith('child-flow-page-1', expect.any(Object));
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
act(() => {
|
|
1282
|
+
navigateTo('/admin/371750755434496');
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
await waitFor(() => {
|
|
1286
|
+
expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith('child-flow-page-2', expect.any(Object));
|
|
1287
|
+
});
|
|
1288
|
+
expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('371750755434496', expect.any(Object));
|
|
1289
|
+
expect(screen.queryByText('404')).not.toBeInTheDocument();
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
it('should keep rendering 404 when current admin route matches no accessible schema uid or id', async () => {
|
|
1293
|
+
const engine = new FlowEngine();
|
|
1294
|
+
engine.context.defineProperty('routeRepository', {
|
|
1295
|
+
value: {
|
|
1296
|
+
refreshAccessible: hookState.refresh,
|
|
1297
|
+
isAccessibleLoaded: () => true,
|
|
1298
|
+
ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
|
|
1299
|
+
getRouteBySchemaUid: vi.fn(() => undefined),
|
|
1300
|
+
listAccessible: vi.fn(() => [
|
|
1301
|
+
{
|
|
1302
|
+
id: 371750686228480,
|
|
1303
|
+
schemaUid: 'group-schema',
|
|
1304
|
+
title: '789',
|
|
1305
|
+
type: NocoBaseDesktopRouteType.group,
|
|
1306
|
+
},
|
|
1307
|
+
]),
|
|
1308
|
+
},
|
|
1309
|
+
});
|
|
1310
|
+
engine.context.defineProperty('app', {
|
|
1311
|
+
value: {
|
|
1312
|
+
getPublicPath: () => '/v2/',
|
|
1313
|
+
router: {
|
|
1314
|
+
getBasename: () => '/v2',
|
|
1315
|
+
},
|
|
1316
|
+
},
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
const adminLayoutModel: MockAdminLayoutModel = Object.assign(
|
|
1320
|
+
engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
|
|
1321
|
+
{
|
|
1322
|
+
registerRoutePage: vi.fn(),
|
|
1323
|
+
updateRoutePage: vi.fn(),
|
|
1324
|
+
unregisterRoutePage: vi.fn(),
|
|
1325
|
+
},
|
|
1326
|
+
);
|
|
1327
|
+
|
|
1328
|
+
render(
|
|
1329
|
+
<FlowEngineProvider engine={engine}>
|
|
1330
|
+
<MemoryRouter initialEntries={['/admin/not-exists-4822']}>
|
|
1331
|
+
<Routes>
|
|
1332
|
+
<Route path="/admin/:name" element={<FlowRoute />} />
|
|
1333
|
+
</Routes>
|
|
1334
|
+
</MemoryRouter>
|
|
1335
|
+
</FlowEngineProvider>,
|
|
1336
|
+
);
|
|
1337
|
+
|
|
1338
|
+
expect(await screen.findByText('404')).toBeInTheDocument();
|
|
1339
|
+
expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
it('should keep rendering 404 when current admin route matches only a non-group route id', async () => {
|
|
1343
|
+
const engine = new FlowEngine();
|
|
1344
|
+
engine.context.defineProperty('routeRepository', {
|
|
1345
|
+
value: {
|
|
1346
|
+
refreshAccessible: hookState.refresh,
|
|
1347
|
+
isAccessibleLoaded: () => true,
|
|
1348
|
+
ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
|
|
1349
|
+
getRouteBySchemaUid: vi.fn(() => undefined),
|
|
1350
|
+
listAccessible: vi.fn(() => [
|
|
1351
|
+
{
|
|
1352
|
+
id: 10001,
|
|
1353
|
+
schemaUid: 'flow-page-schema',
|
|
1354
|
+
title: 'Flow page',
|
|
1355
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
1356
|
+
},
|
|
1357
|
+
]),
|
|
1358
|
+
},
|
|
1359
|
+
});
|
|
1360
|
+
engine.context.defineProperty('app', {
|
|
1361
|
+
value: {
|
|
1362
|
+
getPublicPath: () => '/v2/',
|
|
1363
|
+
router: {
|
|
1364
|
+
getBasename: () => '/v2',
|
|
1365
|
+
},
|
|
1366
|
+
},
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
const adminLayoutModel: MockAdminLayoutModel = Object.assign(
|
|
1370
|
+
engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
|
|
1371
|
+
{
|
|
1372
|
+
registerRoutePage: vi.fn(),
|
|
1373
|
+
updateRoutePage: vi.fn(),
|
|
1374
|
+
unregisterRoutePage: vi.fn(),
|
|
1375
|
+
},
|
|
1376
|
+
);
|
|
1377
|
+
|
|
1378
|
+
render(
|
|
1379
|
+
<FlowEngineProvider engine={engine}>
|
|
1380
|
+
<MemoryRouter initialEntries={['/admin/10001']}>
|
|
1381
|
+
<Routes>
|
|
1382
|
+
<Route path="/admin/:name" element={<FlowRoute />} />
|
|
1383
|
+
</Routes>
|
|
1384
|
+
</MemoryRouter>
|
|
1385
|
+
</FlowEngineProvider>,
|
|
1386
|
+
);
|
|
1387
|
+
|
|
1388
|
+
expect(await screen.findByText('404')).toBeInTheDocument();
|
|
1389
|
+
expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
it('should keep rendering 404 when current admin route matches only a group schema uid', async () => {
|
|
1393
|
+
const engine = new FlowEngine();
|
|
1394
|
+
const groupRoute = {
|
|
1395
|
+
id: 371750686228480,
|
|
1396
|
+
schemaUid: 'group-schema',
|
|
1397
|
+
title: '789',
|
|
1398
|
+
type: NocoBaseDesktopRouteType.group,
|
|
1399
|
+
};
|
|
1400
|
+
engine.context.defineProperty('routeRepository', {
|
|
1401
|
+
value: {
|
|
1402
|
+
refreshAccessible: hookState.refresh,
|
|
1403
|
+
isAccessibleLoaded: () => true,
|
|
1404
|
+
ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
|
|
1405
|
+
getRouteBySchemaUid: vi.fn((schemaUid: string) => (schemaUid === 'group-schema' ? groupRoute : undefined)),
|
|
1406
|
+
listAccessible: vi.fn(() => [groupRoute]),
|
|
1407
|
+
},
|
|
1408
|
+
});
|
|
1409
|
+
engine.context.defineProperty('app', {
|
|
1410
|
+
value: {
|
|
1411
|
+
getPublicPath: () => '/v2/',
|
|
1412
|
+
router: {
|
|
1413
|
+
getBasename: () => '/v2',
|
|
1414
|
+
},
|
|
1415
|
+
},
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
const adminLayoutModel: MockAdminLayoutModel = Object.assign(
|
|
1419
|
+
engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
|
|
1420
|
+
{
|
|
1421
|
+
registerRoutePage: vi.fn(),
|
|
1422
|
+
updateRoutePage: vi.fn(),
|
|
1423
|
+
unregisterRoutePage: vi.fn(),
|
|
1424
|
+
},
|
|
1425
|
+
);
|
|
1426
|
+
|
|
1427
|
+
render(
|
|
1428
|
+
<FlowEngineProvider engine={engine}>
|
|
1429
|
+
<MemoryRouter initialEntries={['/admin/group-schema']}>
|
|
1430
|
+
<Routes>
|
|
1431
|
+
<Route path="/admin/:name" element={<FlowRoute />} />
|
|
1432
|
+
</Routes>
|
|
1433
|
+
</MemoryRouter>
|
|
1434
|
+
</FlowEngineProvider>,
|
|
1435
|
+
);
|
|
1436
|
+
|
|
1437
|
+
expect(await screen.findByText('404')).toBeInTheDocument();
|
|
1438
|
+
expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
|
|
1439
|
+
});
|
|
1440
|
+
|
|
896
1441
|
it('should not use flowModels:findOne to bridge a missing route for protected layouts', async () => {
|
|
897
1442
|
const engine = new FlowEngine();
|
|
898
1443
|
const findOne = vi.fn().mockResolvedValue({
|
|
@@ -64,6 +64,40 @@ describe('normalizeDataScopeFilter', () => {
|
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
it('prunes a missing URL search param variable', () => {
|
|
68
|
+
const rawFilter = {
|
|
69
|
+
logic: '$and',
|
|
70
|
+
items: [{ path: 'departmentId', operator: '$eq', value: '{{ ctx.urlSearchParams.departmentId }}' }],
|
|
71
|
+
};
|
|
72
|
+
const resolvedFilter = {
|
|
73
|
+
logic: '$and',
|
|
74
|
+
items: [{ path: 'departmentId', operator: '$eq', value: undefined }],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
expect(normalizeDataScopeFilter(rawFilter, resolvedFilter)).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('only prunes the missing URL search param condition from mixed filters', () => {
|
|
81
|
+
const rawFilter = {
|
|
82
|
+
logic: '$and',
|
|
83
|
+
items: [
|
|
84
|
+
{ path: 'status', operator: '$eq', value: 'active' },
|
|
85
|
+
{ path: 'departmentId', operator: '$eq', value: '{{ ctx.urlSearchParams.departmentId }}' },
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
const resolvedFilter = {
|
|
89
|
+
logic: '$and',
|
|
90
|
+
items: [
|
|
91
|
+
{ path: 'status', operator: '$eq', value: 'active' },
|
|
92
|
+
{ path: 'departmentId', operator: '$eq', value: undefined },
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
expect(normalizeDataScopeFilter(rawFilter, resolvedFilter)).toEqual({
|
|
97
|
+
$and: [{ status: { $eq: 'active' } }],
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
67
101
|
it('still prunes empty constant values', () => {
|
|
68
102
|
const filter = {
|
|
69
103
|
logic: '$and',
|
|
@@ -139,6 +173,34 @@ describe('normalizeDataScopeFilter', () => {
|
|
|
139
173
|
expect(resource.removeFilterGroup).not.toHaveBeenCalled();
|
|
140
174
|
});
|
|
141
175
|
|
|
176
|
+
it('dataScope handler removes data scope when a URL search param is missing', async () => {
|
|
177
|
+
const resource = {
|
|
178
|
+
addFilterGroup: vi.fn(),
|
|
179
|
+
removeFilterGroup: vi.fn(),
|
|
180
|
+
};
|
|
181
|
+
const ctx = {
|
|
182
|
+
model: {
|
|
183
|
+
uid: 'field-1',
|
|
184
|
+
resource,
|
|
185
|
+
},
|
|
186
|
+
resolveJsonTemplate: vi.fn(async (template) => ({
|
|
187
|
+
...template,
|
|
188
|
+
items: [{ ...template.items[0], value: undefined }],
|
|
189
|
+
})),
|
|
190
|
+
};
|
|
191
|
+
const params = {
|
|
192
|
+
filter: {
|
|
193
|
+
logic: '$and',
|
|
194
|
+
items: [{ path: 'departmentId', operator: '$eq', value: '{{ ctx.urlSearchParams.departmentId }}' }],
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
await (dataScope as any).handler(ctx, params);
|
|
199
|
+
|
|
200
|
+
expect(resource.removeFilterGroup).toHaveBeenCalledWith('field-1');
|
|
201
|
+
expect(resource.addFilterGroup).not.toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
142
204
|
it('dataScope handler preserves current role as server-side variable', async () => {
|
|
143
205
|
const engine = new FlowEngine();
|
|
144
206
|
const resource = {
|