@nocobase/client-v2 2.2.0-beta.7 → 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/actions/afterSuccess.d.ts +8 -1
- package/es/flow/actions/index.d.ts +1 -1
- 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/AdminLayoutSlotModels.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
- package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
- package/es/flow/models/base/ActionModelCore.d.ts +2 -0
- package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +9 -0
- package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
- package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
- package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
- package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
- package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
- package/es/flow/routeTransientInputArgs.d.ts +14 -0
- package/es/index.d.ts +2 -0
- package/es/index.mjs +243 -141
- package/es/utils/markdownSanitize.d.ts +11 -0
- package/lib/index.js +232 -130
- 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/collection-manager/field-configure.ts +1 -1
- package/src/collection-manager/interfaces/id.ts +1 -1
- package/src/collection-manager/interfaces/m2m.tsx +2 -2
- package/src/collection-manager/interfaces/m2o.tsx +2 -2
- package/src/collection-manager/interfaces/nanoid.ts +1 -1
- package/src/collection-manager/interfaces/o2m.tsx +2 -2
- package/src/collection-manager/interfaces/obo.tsx +2 -2
- package/src/collection-manager/interfaces/oho.tsx +2 -2
- package/src/collection-manager/interfaces/properties/index.ts +2 -2
- package/src/collection-manager/interfaces/uuid.ts +1 -1
- 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/__tests__/getKey.test.ts +7 -0
- package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
- package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
- package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +164 -0
- package/src/flow/actions/afterSuccess.tsx +142 -3
- package/src/flow/actions/dataScopeFilter.ts +10 -1
- package/src/flow/actions/dateTimeFormat.tsx +2 -2
- package/src/flow/actions/index.ts +1 -1
- package/src/flow/actions/openView.tsx +59 -5
- package/src/flow/admin-shell/BaseLayoutModel.tsx +149 -3
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +187 -42
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1020 -110
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +42 -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/AdminLayoutSlotModels.tsx +5 -4
- 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 +518 -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/getViewDiffAndUpdateHidden.tsx +1 -0
- package/src/flow/internal/components/Markdown/util.ts +2 -1
- package/src/flow/models/base/ActionModel.tsx +10 -8
- package/src/flow/models/base/ActionModelCore.tsx +11 -4
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
- package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
- package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
- package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
- package/src/flow/models/blocks/form/submitHandler.ts +17 -3
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +87 -0
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +91 -0
- package/src/flow/models/blocks/js-block/JSBlock.tsx +223 -2
- package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +35 -12
- package/src/flow/models/blocks/table/TableBlockModel.tsx +25 -14
- package/src/flow/models/blocks/table/TableColumnModel.tsx +6 -2
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +57 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +51 -0
- package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
- package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
- package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
- 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/resolveViewParamsToViewList.tsx +11 -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/settings-center/SystemSettingsPage.tsx +1 -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({
|
|
@@ -58,4 +58,11 @@ describe('getKey', () => {
|
|
|
58
58
|
|
|
59
59
|
expect(getKey(a)).not.toBe(getKey(b));
|
|
60
60
|
});
|
|
61
|
+
|
|
62
|
+
it('distinguishes RunJS openView route state overrides', () => {
|
|
63
|
+
const a = createViewItem({ viewUid: 'v', openViewRouteState: { mode: 'drawer' } }, 0);
|
|
64
|
+
const b = createViewItem({ viewUid: 'v', openViewRouteState: { mode: 'dialog' } }, 0);
|
|
65
|
+
|
|
66
|
+
expect(getKey(a)).not.toBe(getKey(b));
|
|
67
|
+
});
|
|
61
68
|
});
|
|
@@ -250,6 +250,40 @@ describe('resolveViewParamsToViewList', () => {
|
|
|
250
250
|
expect(result[0].hidden.value).toBe(false); // default drawer type
|
|
251
251
|
expect(result[1].hidden.value).toBe(false); // default drawer type
|
|
252
252
|
});
|
|
253
|
+
|
|
254
|
+
it('should let RunJS openView route state override persisted mode for hidden calculation', () => {
|
|
255
|
+
const model2 = createMockModel('view2', 'drawer');
|
|
256
|
+
mockFlowEngine.getModel = vi.fn().mockReturnValue(model2);
|
|
257
|
+
|
|
258
|
+
const viewParams: ViewParam[] = [
|
|
259
|
+
{ viewUid: 'view1' },
|
|
260
|
+
{ viewUid: 'view2', openViewRouteState: { mode: 'embed' } },
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
const result = resolveViewParamsToViewList(mockFlowEngine, viewParams, mockRouteModel);
|
|
264
|
+
updateViewListHidden(result);
|
|
265
|
+
|
|
266
|
+
expect(result[0].hidden.value).toBe(true);
|
|
267
|
+
expect(result[1].hidden.value).toBe(false);
|
|
268
|
+
expect(model2.getStepParams).not.toHaveBeenCalled();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should treat child views as embed in mobile hidden calculation', () => {
|
|
272
|
+
const model2 = createMockModel('view2', 'drawer');
|
|
273
|
+
mockFlowEngine.getModel = vi.fn().mockReturnValue(model2);
|
|
274
|
+
|
|
275
|
+
const viewParams: ViewParam[] = [
|
|
276
|
+
{ viewUid: 'view1' },
|
|
277
|
+
{ viewUid: 'view2', openViewRouteState: { mode: 'dialog' } },
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
const result = resolveViewParamsToViewList(mockFlowEngine, viewParams, mockRouteModel);
|
|
281
|
+
updateViewListHidden(result, true);
|
|
282
|
+
|
|
283
|
+
expect(result[0].hidden.value).toBe(true);
|
|
284
|
+
expect(result[1].hidden.value).toBe(false);
|
|
285
|
+
expect(model2.getStepParams).not.toHaveBeenCalled();
|
|
286
|
+
});
|
|
253
287
|
});
|
|
254
288
|
|
|
255
289
|
describe('edge cases', () => {
|
|
@@ -0,0 +1,73 @@
|
|
|
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 {
|
|
11
|
+
FlowEngine,
|
|
12
|
+
FlowModel,
|
|
13
|
+
FlowRuntimeContext,
|
|
14
|
+
setupRuntimeContextSteps,
|
|
15
|
+
type Collection,
|
|
16
|
+
} from '@nocobase/flow-engine';
|
|
17
|
+
import { describe, expect, it } from 'vitest';
|
|
18
|
+
import { afterSuccess, getAfterSuccessResponseRecord, getMetaTreeWithResponseRecord } from '../afterSuccess';
|
|
19
|
+
|
|
20
|
+
describe('afterSuccess response record variable', () => {
|
|
21
|
+
it('reads the submit response record from the saveResource step', () => {
|
|
22
|
+
const record = { id: 1, title: 'Saved' };
|
|
23
|
+
|
|
24
|
+
expect(
|
|
25
|
+
getAfterSuccessResponseRecord({
|
|
26
|
+
steps: {
|
|
27
|
+
confirm: { params: {}, result: undefined },
|
|
28
|
+
saveResource: { params: {}, result: record },
|
|
29
|
+
afterSuccess: { params: {} },
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
).toBe(record);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('injects responseRecord into the afterSuccess runtime context', async () => {
|
|
36
|
+
const record = { id: 1, title: 'Saved' };
|
|
37
|
+
const sourceCtx = {
|
|
38
|
+
steps: {
|
|
39
|
+
saveResource: { params: {}, result: record },
|
|
40
|
+
},
|
|
41
|
+
t: (value: string) => value,
|
|
42
|
+
};
|
|
43
|
+
const properties = await afterSuccess.defineProperties(sourceCtx as FlowRuntimeContext);
|
|
44
|
+
|
|
45
|
+
expect(properties.responseRecord.get()).toBe(record);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('shows responseRecord in the variable picker for settings context', () => {
|
|
49
|
+
const engine = new FlowEngine();
|
|
50
|
+
class SubmitActionModel extends FlowModel {}
|
|
51
|
+
engine.registerModels({ SubmitActionModel });
|
|
52
|
+
const model = engine.createModel<SubmitActionModel>({ use: 'SubmitActionModel' });
|
|
53
|
+
const collection = {
|
|
54
|
+
name: 'users',
|
|
55
|
+
dataSourceKey: 'main',
|
|
56
|
+
getFilterByTK: (record: { id?: number }) => record.id,
|
|
57
|
+
} as unknown as Collection;
|
|
58
|
+
model.context.defineProperty('collection', { value: collection });
|
|
59
|
+
const flow = model.registerFlow({
|
|
60
|
+
key: 'submitSettings',
|
|
61
|
+
steps: {
|
|
62
|
+
saveResource: { title: 'Save record', handler: () => undefined },
|
|
63
|
+
afterSuccess: { use: 'afterSuccess' },
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const ctx = new FlowRuntimeContext(model, 'submitSettings', 'settings');
|
|
67
|
+
setupRuntimeContextSteps(ctx, flow.steps, model, 'submitSettings');
|
|
68
|
+
|
|
69
|
+
const metaTree = getMetaTreeWithResponseRecord(ctx);
|
|
70
|
+
|
|
71
|
+
expect(metaTree.find((node) => node.name === 'responseRecord')?.title).toBe('Response record');
|
|
72
|
+
});
|
|
73
|
+
});
|