@nocobase/client-v2 2.1.0-beta.37 → 2.1.0-beta.38
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/Application.d.ts +1 -0
- package/es/BaseApplication.d.ts +3 -0
- package/es/RouterManager.d.ts +1 -0
- package/es/components/KeepAlive.d.ts +22 -0
- package/es/components/RouterBridge.d.ts +9 -0
- package/es/data-source/ExtendCollectionsProvider.d.ts +28 -2
- package/es/flow/FlowPage.d.ts +2 -1
- package/es/flow/admin-shell/AdminLayoutRouteCoordinator.d.ts +8 -40
- package/es/flow/admin-shell/BaseLayoutModel.d.ts +89 -0
- package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +74 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +12 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -92
- package/es/flow/admin-shell/admin-layout/index.d.ts +2 -0
- package/es/flow/admin-shell/useAdminLayoutRoutePage.d.ts +2 -2
- package/es/flow/admin-shell/useLayoutRoutePage.d.ts +23 -0
- package/es/flow/components/FlowRoute.d.ts +10 -1
- package/es/flow/index.d.ts +4 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +3 -1
- package/es/flow/models/blocks/form/FormActionGroupModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +10 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +484 -437
- package/es/layout-manager/LayoutContentRoute.d.ts +14 -0
- package/es/layout-manager/LayoutManager.d.ts +22 -0
- package/es/layout-manager/LayoutRoute.d.ts +14 -0
- package/es/layout-manager/index.d.ts +13 -0
- package/es/layout-manager/types.d.ts +20 -0
- package/es/layout-manager/utils.d.ts +14 -0
- package/es/settings-center/index.d.ts +1 -1
- package/es/settings-center/plugin-manager/BulkEnableButton.d.ts +15 -0
- package/es/settings-center/plugin-manager/PluginCard.d.ts +15 -0
- package/es/settings-center/plugin-manager/PluginDetail.d.ts +16 -0
- package/es/settings-center/{PluginManagerPage.d.ts → plugin-manager/index.d.ts} +1 -7
- package/es/settings-center/plugin-manager/types.d.ts +34 -0
- package/lib/index.js +484 -437
- package/package.json +8 -7
- package/src/Application.tsx +27 -12
- package/src/BaseApplication.tsx +6 -0
- package/src/PluginSettingsManager.ts +1 -1
- package/src/RouterManager.tsx +17 -1
- package/src/__tests__/PluginSettingsManager.test.ts +41 -2
- package/src/__tests__/app.test.tsx +8 -1
- package/src/__tests__/globalDeps.test.ts +1 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +45 -2
- package/src/__tests__/plugin-manager.test.tsx +177 -0
- package/src/__tests__/settings-center.test.tsx +24 -2
- package/src/components/KeepAlive.tsx +131 -0
- package/src/components/RouterBridge.tsx +28 -4
- package/src/components/__tests__/KeepAlive.test.tsx +63 -0
- package/src/components/__tests__/RouterBridge.test.tsx +27 -0
- package/src/data-source/ExtendCollectionsProvider.tsx +94 -20
- package/src/data-source/__tests__/ExtendCollectionsProvider.test.tsx +264 -0
- package/src/flow/FlowPage.tsx +35 -7
- package/src/flow/__tests__/FlowPage.test.tsx +79 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +529 -2
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +191 -0
- package/src/flow/actions/__tests__/openView.subModelKey.test.tsx +33 -0
- package/src/flow/actions/aclCheck.tsx +4 -0
- package/src/flow/actions/aclCheckRefresh.tsx +4 -0
- package/src/flow/actions/dateTimeFormat.tsx +12 -8
- package/src/flow/actions/linkageRules.tsx +122 -0
- package/src/flow/actions/openView.tsx +28 -4
- package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +11 -329
- package/src/flow/admin-shell/BaseLayoutModel.tsx +455 -0
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +502 -0
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +547 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +4 -4
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +160 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -12
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +28 -201
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +11 -2
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +1 -26
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +149 -27
- package/src/flow/admin-shell/admin-layout/index.ts +2 -0
- package/src/flow/admin-shell/useAdminLayoutRoutePage.ts +10 -26
- package/src/flow/admin-shell/useLayoutRoutePage.ts +61 -0
- package/src/flow/components/AdminLayout.tsx +4 -154
- package/src/flow/components/FlowRoute.tsx +105 -15
- package/src/flow/index.ts +4 -0
- package/src/flow/models/base/ActionModel.tsx +8 -1
- package/src/flow/models/base/PageModel/PageModel.tsx +51 -18
- package/src/flow/models/base/PageModel/RootPageModel.tsx +6 -13
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +102 -1
- package/src/flow/models/base/RouteModel.tsx +1 -1
- package/src/flow/models/blocks/form/FormActionGroupModel.tsx +14 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +8 -1
- package/src/flow/models/blocks/form/__tests__/FormActionGroupModel.test.ts +46 -0
- package/src/flow/models/blocks/form/submitValues.ts +4 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +118 -16
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +114 -0
- package/src/flow/models/fields/AssociationFieldModel/SubFormFieldModel.tsx +7 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -5
- package/src/flow/models/fields/ClickableFieldModel.tsx +9 -1
- package/src/flow/models/fields/DisplayTimeFieldModel.tsx +1 -1
- package/src/flow/models/fields/TimeFieldModel.tsx +1 -1
- package/src/flow/models/fields/__tests__/TimeFieldModel.test.tsx +61 -0
- package/src/flow/models/fields/mobile-components/MobileDatePicker.tsx +19 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileDatePicker.test.tsx +94 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +1 -1
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +91 -0
- package/src/index.ts +1 -0
- package/src/layout-manager/LayoutContentRoute.tsx +90 -0
- package/src/layout-manager/LayoutManager.tsx +185 -0
- package/src/layout-manager/LayoutRoute.tsx +138 -0
- package/src/layout-manager/__tests__/LayoutManager.test.tsx +335 -0
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +473 -0
- package/src/layout-manager/index.ts +14 -0
- package/src/layout-manager/types.ts +22 -0
- package/src/layout-manager/utils.ts +37 -0
- package/src/nocobase-buildin-plugin/index.tsx +56 -48
- package/src/settings-center/index.ts +1 -1
- package/src/settings-center/plugin-manager/BulkEnableButton.tsx +111 -0
- package/src/settings-center/plugin-manager/PluginCard.tsx +270 -0
- package/src/settings-center/plugin-manager/PluginDetail.tsx +195 -0
- package/src/settings-center/plugin-manager/index.tsx +254 -0
- package/src/settings-center/plugin-manager/types.ts +35 -0
- package/src/settings-center/utils.tsx +8 -1
- package/src/theme/__tests__/globalStyles.test.ts +24 -0
- package/src/theme/globalStyles.ts +10 -0
- package/src/utils/globalDeps.ts +2 -0
- package/src/settings-center/PluginManagerPage.tsx +0 -162
|
@@ -0,0 +1,91 @@
|
|
|
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 { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { dateTimeFormat } from '../../actions/dateTimeFormat';
|
|
12
|
+
|
|
13
|
+
describe('dateTimeFormat', () => {
|
|
14
|
+
it('only shows the time format schema for interface time fields', () => {
|
|
15
|
+
const ctx = {
|
|
16
|
+
model: {
|
|
17
|
+
context: {
|
|
18
|
+
collectionField: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
interface: 'time',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
expect(Object.keys(dateTimeFormat.uiSchema(ctx))).toEqual(['timeFormat']);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('treats interface time fields as time fields', () => {
|
|
30
|
+
const setProps = vi.fn();
|
|
31
|
+
const ctx = {
|
|
32
|
+
model: {
|
|
33
|
+
context: {
|
|
34
|
+
collectionField: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
interface: 'time',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
setProps,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
dateTimeFormat.handler(ctx, { timeFormat: 'hh:mm:ss a' });
|
|
44
|
+
|
|
45
|
+
expect(setProps).toHaveBeenCalledWith({
|
|
46
|
+
timeFormat: 'hh:mm:ss a',
|
|
47
|
+
format: 'hh:mm:ss a',
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('uses format as the default time format when timeFormat is missing', () => {
|
|
52
|
+
const ctx = {
|
|
53
|
+
model: {
|
|
54
|
+
props: {},
|
|
55
|
+
context: {
|
|
56
|
+
collectionField: {
|
|
57
|
+
interface: 'time',
|
|
58
|
+
getComponentProps: () => ({
|
|
59
|
+
format: 'hh:mm:ss a',
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
expect(dateTimeFormat.defaultParams(ctx)).toMatchObject({
|
|
67
|
+
timeFormat: 'hh:mm:ss a',
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('does not use datetime format as the default time format for non-time fields', () => {
|
|
72
|
+
const ctx = {
|
|
73
|
+
model: {
|
|
74
|
+
props: {},
|
|
75
|
+
context: {
|
|
76
|
+
collectionField: {
|
|
77
|
+
type: 'datetime',
|
|
78
|
+
interface: 'datetime',
|
|
79
|
+
getComponentProps: () => ({
|
|
80
|
+
format: 'YYYY-MM-DD hh:mm:ss a',
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
expect(dateTimeFormat.defaultParams(ctx)).toMatchObject({
|
|
88
|
+
timeFormat: 'HH:mm:ss',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ export * from './WebSocketClient';
|
|
|
26
26
|
export * from './RouterManager';
|
|
27
27
|
export * from './PluginManager';
|
|
28
28
|
export * from './PluginSettingsManager';
|
|
29
|
+
export * from './layout-manager';
|
|
29
30
|
export * from './hooks';
|
|
30
31
|
export { default as languageCodes } from './locale/languageCodes';
|
|
31
32
|
export * from './nocobase-buildin-plugin';
|
|
@@ -0,0 +1,90 @@
|
|
|
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 { type FlowEngine, useFlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
12
|
+
import { useLocation, useMatches } from 'react-router-dom';
|
|
13
|
+
import { AppNotFound } from '../components';
|
|
14
|
+
import { getLayoutModel, type BaseLayoutModel, type LayoutRouteLike } from '../flow/admin-shell/BaseLayoutModel';
|
|
15
|
+
import FlowRoute from '../flow/components/FlowRoute';
|
|
16
|
+
import { useApp } from '../hooks/useApp';
|
|
17
|
+
|
|
18
|
+
export interface LayoutContentRouteProps {
|
|
19
|
+
layoutRouteName: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const getRefCurrent = <T,>(ref: React.MutableRefObject<T>) => ref.current;
|
|
23
|
+
|
|
24
|
+
export const LayoutContentRoute = (props: LayoutContentRouteProps) => {
|
|
25
|
+
const { layoutRouteName } = props;
|
|
26
|
+
const app = useApp();
|
|
27
|
+
const flowEngine = useFlowEngine();
|
|
28
|
+
const layout = app.layoutManager.getLayout(layoutRouteName);
|
|
29
|
+
const location = useLocation();
|
|
30
|
+
const matches = useMatches();
|
|
31
|
+
const model = getLayoutModel<BaseLayoutModel>(flowEngine, layout.uid, { required: true });
|
|
32
|
+
const legacyPageBehavior = layout.routeName === 'admin' ? 'redirect' : 'notFound';
|
|
33
|
+
const syncVersionRef = useRef(0);
|
|
34
|
+
const routeLike = useMemo<LayoutRouteLike>(() => {
|
|
35
|
+
const lastMatch = matches[matches.length - 1];
|
|
36
|
+
const layoutMatch = matches.find((match) => match.id === layout.routeName);
|
|
37
|
+
return {
|
|
38
|
+
id: lastMatch?.id,
|
|
39
|
+
name: lastMatch?.id,
|
|
40
|
+
pathname: location.pathname,
|
|
41
|
+
params: (lastMatch?.params || {}) as Record<string, string | undefined>,
|
|
42
|
+
layoutRouteName: layout.routeName,
|
|
43
|
+
layoutBasePathname: layoutMatch?.pathname,
|
|
44
|
+
};
|
|
45
|
+
}, [layout.routeName, location.pathname, matches]);
|
|
46
|
+
if (!model) {
|
|
47
|
+
throw new Error(`[NocoBase] Layout '${layout.routeName}' model '${layout.uid}' is not mounted.`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const layoutRoute = model.resolveLayoutRoute(routeLike);
|
|
51
|
+
const getCurrentLayoutModel = useMemo(
|
|
52
|
+
() => (flowEngine: FlowEngine) => getLayoutModel<BaseLayoutModel>(flowEngine, layout.uid, { required: true }),
|
|
53
|
+
[layout.uid],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const syncVersion = ++syncVersionRef.current;
|
|
58
|
+
model.syncLayoutRoute(routeLike);
|
|
59
|
+
return () => {
|
|
60
|
+
Promise.resolve()
|
|
61
|
+
.then(() => {
|
|
62
|
+
if (getRefCurrent(syncVersionRef) !== syncVersion) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
model.clearLayoutRoute(routeLike);
|
|
66
|
+
})
|
|
67
|
+
.catch(() => {
|
|
68
|
+
// ignore
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
}, [model, routeLike]);
|
|
72
|
+
|
|
73
|
+
if (layoutRoute.type === 'root') {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (layoutRoute.type === 'notFound') {
|
|
78
|
+
return <AppNotFound />;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<FlowRoute
|
|
83
|
+
pageUid={layoutRoute.pageUid}
|
|
84
|
+
getLayoutModel={getCurrentLayoutModel}
|
|
85
|
+
legacyPageBehavior={legacyPageBehavior}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export default LayoutContentRoute;
|
|
@@ -0,0 +1,185 @@
|
|
|
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 React from 'react';
|
|
11
|
+
import type { BaseApplication } from '../BaseApplication';
|
|
12
|
+
import { LayoutContentRoute } from './LayoutContentRoute';
|
|
13
|
+
import { LayoutRoute } from './LayoutRoute';
|
|
14
|
+
import type { LayoutDefinition, LayoutRegisterOptions } from './types';
|
|
15
|
+
import {
|
|
16
|
+
getLayoutPageRouteName,
|
|
17
|
+
getLayoutPageTabRouteName,
|
|
18
|
+
getLayoutPageTabViewRouteName,
|
|
19
|
+
getLayoutPageViewRouteName,
|
|
20
|
+
} from './utils';
|
|
21
|
+
|
|
22
|
+
const DEFAULT_ROOT_PAGE_MODEL_CLASS = 'RootPageModel';
|
|
23
|
+
const DEFAULT_CHILD_PAGE_MODEL_CLASS = 'ChildPageModel';
|
|
24
|
+
|
|
25
|
+
const assertNonEmptyString = (value: unknown, fieldName: string) => {
|
|
26
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
27
|
+
throw new Error(`[NocoBase] layoutManager.registerLayout() requires '${fieldName}' to be a non-empty string.`);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const assertString = (value: unknown, fieldName: string) => {
|
|
32
|
+
if (typeof value !== 'string') {
|
|
33
|
+
throw new Error(`[NocoBase] layoutManager.registerLayout() requires '${fieldName}' to be a string.`);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const assertOptionalNonEmptyString = (value: unknown, fieldName: string) => {
|
|
38
|
+
if (value !== undefined) {
|
|
39
|
+
assertNonEmptyString(value, fieldName);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const assertOptionalBoolean = (value: unknown, fieldName: string) => {
|
|
44
|
+
if (value !== undefined && typeof value !== 'boolean') {
|
|
45
|
+
throw new Error(`[NocoBase] layoutManager.registerLayout() requires '${fieldName}' to be a boolean.`);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function assertValidRouteName(routeName: string) {
|
|
50
|
+
if (routeName.includes('..') || routeName.startsWith('.') || routeName.endsWith('.')) {
|
|
51
|
+
throw new Error(`[NocoBase] layoutManager.registerLayout() received invalid routeName '${routeName}'.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function normalizeLayoutRoutePath(routeName: string, routePath: string) {
|
|
56
|
+
const trimmed = routePath.trim();
|
|
57
|
+
const isNestedRoute = routeName.includes('.');
|
|
58
|
+
if (isNestedRoute && trimmed.startsWith('/')) {
|
|
59
|
+
throw new Error(`[NocoBase] nested layout routePath '${routePath}' must be relative.`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const normalized = isNestedRoute
|
|
63
|
+
? trimmed.replace(/^\/+/, '').replace(/\/+$/, '')
|
|
64
|
+
: `/${trimmed.replace(/^\/+/, '').replace(/\/+$/, '')}`;
|
|
65
|
+
|
|
66
|
+
if (!isNestedRoute && (!normalized || normalized === '/')) {
|
|
67
|
+
throw new Error(`[NocoBase] layoutManager.registerLayout() does not support root routePath '/'.`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (normalized.includes('*')) {
|
|
71
|
+
throw new Error(`[NocoBase] layoutManager.registerLayout() does not support wildcard routePath '${routePath}'.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return normalized;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeLayoutDefinition(options: LayoutRegisterOptions): LayoutDefinition {
|
|
78
|
+
assertNonEmptyString(options.routeName, 'routeName');
|
|
79
|
+
assertString(options.routePath, 'routePath');
|
|
80
|
+
assertNonEmptyString(options.uid, 'uid');
|
|
81
|
+
assertNonEmptyString(options.layoutModelClass, 'layoutModelClass');
|
|
82
|
+
assertOptionalNonEmptyString(options.rootPageModelClass, 'rootPageModelClass');
|
|
83
|
+
assertOptionalNonEmptyString(options.childPageModelClass, 'childPageModelClass');
|
|
84
|
+
assertOptionalBoolean(options.authCheck, 'authCheck');
|
|
85
|
+
assertValidRouteName(options.routeName);
|
|
86
|
+
|
|
87
|
+
const routePath = normalizeLayoutRoutePath(options.routeName, options.routePath);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
routeName: options.routeName,
|
|
91
|
+
rootRouteName: options.routeName.split('.')[0],
|
|
92
|
+
routePath,
|
|
93
|
+
uid: options.uid,
|
|
94
|
+
layoutModelClass: options.layoutModelClass,
|
|
95
|
+
rootPageModelClass: options.rootPageModelClass || DEFAULT_ROOT_PAGE_MODEL_CLASS,
|
|
96
|
+
childPageModelClass: options.childPageModelClass || DEFAULT_CHILD_PAGE_MODEL_CLASS,
|
|
97
|
+
authCheck: options.authCheck ?? true,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class LayoutManager<TApp extends BaseApplication<any> = BaseApplication<any>> {
|
|
102
|
+
private readonly app: TApp;
|
|
103
|
+
private readonly layouts = new Map<string, LayoutDefinition>();
|
|
104
|
+
private readonly uidIndex = new Map<string, string>();
|
|
105
|
+
|
|
106
|
+
constructor(app: TApp) {
|
|
107
|
+
this.app = app;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
registerLayout(options: LayoutRegisterOptions) {
|
|
111
|
+
const layout = normalizeLayoutDefinition(options);
|
|
112
|
+
|
|
113
|
+
if (this.layouts.has(layout.routeName)) {
|
|
114
|
+
throw new Error(`[NocoBase] Layout '${layout.routeName}' has already been registered.`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const existingUidName = this.uidIndex.get(layout.uid);
|
|
118
|
+
if (existingUidName) {
|
|
119
|
+
throw new Error(`[NocoBase] Layout uid '${layout.uid}' has already been registered by '${existingUidName}'.`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.layouts.set(layout.routeName, layout);
|
|
123
|
+
this.uidIndex.set(layout.uid, layout.routeName);
|
|
124
|
+
this.addStandardRoutes(layout);
|
|
125
|
+
|
|
126
|
+
return layout;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getLayout(routeName: string) {
|
|
130
|
+
const layout = this.layouts.get(routeName);
|
|
131
|
+
if (!layout) {
|
|
132
|
+
throw new Error(`[NocoBase] Layout '${routeName}' is not registered.`);
|
|
133
|
+
}
|
|
134
|
+
return layout;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
hasLayout(routeName: string) {
|
|
138
|
+
return this.layouts.has(routeName);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
listLayouts() {
|
|
142
|
+
return Array.from(this.layouts.values());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private addStandardRoutes(layout: LayoutDefinition) {
|
|
146
|
+
const routeBaseName = layout.routeName;
|
|
147
|
+
const authCheck = layout.authCheck;
|
|
148
|
+
const skipAuthCheck = authCheck === false;
|
|
149
|
+
|
|
150
|
+
this.app.router.add(routeBaseName, {
|
|
151
|
+
path: layout.routePath,
|
|
152
|
+
authCheck,
|
|
153
|
+
skipAuthCheck,
|
|
154
|
+
element: <LayoutRoute layoutRouteName={layout.routeName} />,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
this.app.router.add(getLayoutPageRouteName(routeBaseName), {
|
|
158
|
+
path: ':name',
|
|
159
|
+
authCheck,
|
|
160
|
+
skipAuthCheck,
|
|
161
|
+
element: <LayoutContentRoute layoutRouteName={layout.routeName} />,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
this.app.router.add(getLayoutPageTabRouteName(routeBaseName), {
|
|
165
|
+
path: ':name/tab/:tabUid',
|
|
166
|
+
authCheck,
|
|
167
|
+
skipAuthCheck,
|
|
168
|
+
element: <LayoutContentRoute layoutRouteName={layout.routeName} />,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
this.app.router.add(getLayoutPageViewRouteName(routeBaseName), {
|
|
172
|
+
path: ':name/view/*',
|
|
173
|
+
authCheck,
|
|
174
|
+
skipAuthCheck,
|
|
175
|
+
element: <LayoutContentRoute layoutRouteName={layout.routeName} />,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
this.app.router.add(getLayoutPageTabViewRouteName(routeBaseName), {
|
|
179
|
+
path: ':name/tab/:tabUid/view/*',
|
|
180
|
+
authCheck,
|
|
181
|
+
skipAuthCheck,
|
|
182
|
+
element: <LayoutContentRoute layoutRouteName={layout.routeName} />,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
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, FlowModel, FlowModelRenderer, isInheritedFrom, useFlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import type { ModelConstructor } from '@nocobase/flow-engine';
|
|
12
|
+
import { useRequest } from 'ahooks';
|
|
13
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
14
|
+
import { useLocation, useMatches } from 'react-router-dom';
|
|
15
|
+
import { SkeletonFallback } from '../flow/components/SkeletonFallback';
|
|
16
|
+
import { useApp } from '../hooks/useApp';
|
|
17
|
+
import { BaseLayoutModel } from '../flow/admin-shell/BaseLayoutModel';
|
|
18
|
+
import type { LayoutDefinition } from './types';
|
|
19
|
+
|
|
20
|
+
export interface LayoutRouteProps {
|
|
21
|
+
layoutRouteName: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const getRefCurrent = <T,>(ref: React.MutableRefObject<T>) => ref.current;
|
|
25
|
+
|
|
26
|
+
function isBaseLayoutModelClass(ModelClass: ModelConstructor) {
|
|
27
|
+
return ModelClass === BaseLayoutModel || isInheritedFrom(ModelClass, BaseLayoutModel as ModelConstructor);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assertLayoutModelInstance(model: FlowModel, layout: LayoutDefinition) {
|
|
31
|
+
if (!(model instanceof BaseLayoutModel)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`[NocoBase] Layout '${layout.routeName}' requires model '${layout.uid}' to be an instance of BaseLayoutModel.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function assertLayoutModelClass(flowEngine: FlowEngine, layout: LayoutDefinition) {
|
|
39
|
+
const ModelClass = await flowEngine.getModelClassAsync(layout.layoutModelClass);
|
|
40
|
+
|
|
41
|
+
if (!ModelClass) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`[NocoBase] Layout '${layout.routeName}' model class '${layout.layoutModelClass}' is not registered.`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!isBaseLayoutModelClass(ModelClass)) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`[NocoBase] Layout '${layout.routeName}' model class '${layout.layoutModelClass}' must extend BaseLayoutModel.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const LayoutRoute = (props: LayoutRouteProps) => {
|
|
55
|
+
const { layoutRouteName } = props;
|
|
56
|
+
const app = useApp();
|
|
57
|
+
const flowEngine = useFlowEngine();
|
|
58
|
+
const layout = app.layoutManager.getLayout(layoutRouteName);
|
|
59
|
+
const location = useLocation();
|
|
60
|
+
const matches = useMatches();
|
|
61
|
+
const lastMatch = matches[matches.length - 1];
|
|
62
|
+
const layoutMatch = matches.find((match) => match.id === layout.routeName);
|
|
63
|
+
const lastMatchParamsSignature = JSON.stringify(lastMatch?.params || {});
|
|
64
|
+
const lastMatchParams = useMemo(
|
|
65
|
+
() => JSON.parse(lastMatchParamsSignature) as Record<string, string | undefined>,
|
|
66
|
+
[lastMatchParamsSignature],
|
|
67
|
+
);
|
|
68
|
+
const syncVersionRef = useRef(0);
|
|
69
|
+
const routeLike = useMemo(() => {
|
|
70
|
+
return {
|
|
71
|
+
id: lastMatch?.id,
|
|
72
|
+
name: lastMatch?.id,
|
|
73
|
+
pathname: location.pathname,
|
|
74
|
+
params: lastMatchParams,
|
|
75
|
+
layoutRouteName: layout.routeName,
|
|
76
|
+
layoutBasePathname: layoutMatch?.pathname,
|
|
77
|
+
};
|
|
78
|
+
}, [lastMatch?.id, lastMatchParams, layout.routeName, layoutMatch?.pathname, location.pathname]);
|
|
79
|
+
const { loading, data, error } = useRequest(
|
|
80
|
+
async () => {
|
|
81
|
+
const existingModel = flowEngine.getModel<BaseLayoutModel>(layout.uid);
|
|
82
|
+
|
|
83
|
+
if (existingModel) {
|
|
84
|
+
assertLayoutModelInstance(existingModel, layout);
|
|
85
|
+
existingModel.setProps({ layout });
|
|
86
|
+
return existingModel;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await assertLayoutModelClass(flowEngine, layout);
|
|
90
|
+
|
|
91
|
+
const model = await flowEngine.createModelAsync<BaseLayoutModel>({
|
|
92
|
+
uid: layout.uid,
|
|
93
|
+
use: layout.layoutModelClass,
|
|
94
|
+
props: {
|
|
95
|
+
layout,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
assertLayoutModelInstance(model, layout);
|
|
99
|
+
return model;
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
refreshDeps: [flowEngine, layout],
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!data) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const syncVersion = ++syncVersionRef.current;
|
|
112
|
+
data.syncLayoutRoute(routeLike);
|
|
113
|
+
return () => {
|
|
114
|
+
Promise.resolve()
|
|
115
|
+
.then(() => {
|
|
116
|
+
if (getRefCurrent(syncVersionRef) !== syncVersion) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
data.clearLayoutRoute(routeLike);
|
|
120
|
+
})
|
|
121
|
+
.catch(() => {
|
|
122
|
+
// ignore
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
}, [data, routeLike]);
|
|
126
|
+
|
|
127
|
+
if (error) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (loading || !data) {
|
|
132
|
+
return <SkeletonFallback style={{ margin: 16 }} />;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return <FlowModelRenderer model={data} />;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export default LayoutRoute;
|