@nocobase/client-v2 2.1.0-alpha.31 → 2.1.0-alpha.33
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/components/form/JsonTextArea.d.ts +18 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/actions/dateRangeLimit.d.ts +9 -0
- package/es/flow/actions/index.d.ts +2 -1
- package/es/flow/actions/linkageRules.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +4 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +5 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +4 -0
- package/es/flow/models/base/PageModel/RootPageModel.d.ts +9 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +7 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +5 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +20 -0
- package/es/flow/models/fields/JSEditableFieldModel.d.ts +4 -0
- package/es/index.mjs +79 -67
- package/lib/index.js +80 -68
- package/package.json +6 -5
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +67 -46
- package/src/__tests__/settings-center.test.tsx +30 -0
- package/src/components/form/JsonTextArea.tsx +129 -0
- package/src/components/index.ts +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +4 -5
- package/src/flow/actions/__tests__/actionLinkageRules.race.repro.test.ts +199 -0
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +478 -0
- package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +6 -1
- package/src/flow/actions/__tests__/linkageRules.menu.test.ts +90 -0
- package/src/flow/actions/__tests__/pattern.test.ts +190 -0
- package/src/flow/actions/dateRangeLimit.tsx +66 -0
- package/src/flow/actions/index.ts +3 -0
- package/src/flow/actions/linkageRules.tsx +194 -42
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +2 -8
- package/src/flow/actions/openView.tsx +2 -1
- package/src/flow/actions/pattern.tsx +25 -2
- package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +7 -1
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +117 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +8 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +70 -12
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +26 -87
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +11 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +5 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +292 -31
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +50 -12
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +77 -56
- package/src/flow/components/AdminLayout.tsx +2 -2
- package/src/flow/components/FlowRoute.tsx +17 -4
- package/src/flow/models/base/PageModel/PageModel.tsx +15 -3
- package/src/flow/models/base/PageModel/RootPageModel.tsx +37 -2
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +73 -0
- package/src/flow/models/base/PageModel/__tests__/RootPageModel.test.ts +116 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +167 -1
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +103 -11
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +34 -3
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +47 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +42 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.refresh.test.tsx +122 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -1
- package/src/flow/models/fields/ClickableFieldModel.tsx +21 -9
- package/src/flow/models/fields/DateTimeFieldModel/DateOnlyFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeFieldModel.tsx +4 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeNoTzFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeTzFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +242 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +152 -0
- package/src/flow/models/fields/JSEditableFieldModel.tsx +110 -14
- package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +87 -0
- package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +210 -0
- package/src/flow/system-settings/useSystemSettings.tsx +36 -1
|
@@ -15,11 +15,7 @@ import {
|
|
|
15
15
|
isRunJSValue,
|
|
16
16
|
} from '@nocobase/flow-engine';
|
|
17
17
|
import _ from 'lodash';
|
|
18
|
-
import {
|
|
19
|
-
namePathToPathKey,
|
|
20
|
-
parsePathString,
|
|
21
|
-
pathKeyToNamePath,
|
|
22
|
-
} from '../models/blocks/form/value-runtime/path';
|
|
18
|
+
import { namePathToPathKey, parsePathString, pathKeyToNamePath } from '../models/blocks/form/value-runtime/path';
|
|
23
19
|
import {
|
|
24
20
|
collectStaticDepsFromRunJSValue,
|
|
25
21
|
collectStaticDepsFromTemplateValue,
|
|
@@ -243,9 +239,7 @@ function collectLinkageRefreshDeps(ctx: FlowContext, params: any): LinkageRefres
|
|
|
243
239
|
|
|
244
240
|
if (depKey === 'ctx:item' || depKey.startsWith('ctx:item:')) {
|
|
245
241
|
const subPath = depKey === 'ctx:item' ? '' : depKey.slice('ctx:item:'.length);
|
|
246
|
-
const depPath = subPath
|
|
247
|
-
? (parsePathString(subPath).filter((seg) => typeof seg !== 'object') as NamePath)
|
|
248
|
-
: [];
|
|
242
|
+
const depPath = subPath ? (parsePathString(subPath).filter((seg) => typeof seg !== 'object') as NamePath) : [];
|
|
249
243
|
const resolved = resolveItemDependencyPath(ctx, depPath);
|
|
250
244
|
wildcard ||= resolved.wildcard;
|
|
251
245
|
valuePaths.push(...resolved.valuePaths);
|
|
@@ -345,7 +345,8 @@ export const openView = defineAction({
|
|
|
345
345
|
target: ctx.inputArgs.target || ctx.layoutContentElement,
|
|
346
346
|
dataSourceKey: runtimeDataSourceKey ?? actionDefaults.dataSourceKey,
|
|
347
347
|
collectionName: runtimeCollectionName ?? actionDefaults.collectionName,
|
|
348
|
-
associationName:
|
|
348
|
+
associationName:
|
|
349
|
+
typeof runtimeAssociationName !== 'undefined' ? runtimeAssociationName : actionDefaults.associationName,
|
|
349
350
|
filterByTk: mergedFilterByTk,
|
|
350
351
|
sourceId: mergedSourceId,
|
|
351
352
|
tabUid: mergedTabUid,
|
|
@@ -7,9 +7,25 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { defineAction, tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import { DetailsItemModel } from '../models/blocks/details/DetailsItemModel';
|
|
12
|
-
import { rebuildFieldSubModel } from '../internal/utils/rebuildFieldSubModel';
|
|
12
|
+
import { getFieldBindingUse, rebuildFieldSubModel } from '../internal/utils/rebuildFieldSubModel';
|
|
13
|
+
|
|
14
|
+
type PatternAwareFieldModelMeta = {
|
|
15
|
+
preserveOnPatternChange?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type PatternAwareFieldModel = {
|
|
19
|
+
scheduleApplyJsSettings?: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function shouldPreserveFieldModelOnPatternChange(ctx: any) {
|
|
23
|
+
const fieldModel = ctx.model.subModels.field;
|
|
24
|
+
const fieldUse = getFieldBindingUse(fieldModel) ?? fieldModel?.use;
|
|
25
|
+
const ModelClass = typeof fieldUse === 'string' ? ctx.engine.getModelClass(fieldUse) : fieldUse;
|
|
26
|
+
|
|
27
|
+
return ((ModelClass?.meta as PatternAwareFieldModelMeta | undefined)?.preserveOnPatternChange ?? false) === true;
|
|
28
|
+
}
|
|
13
29
|
|
|
14
30
|
export const pattern = defineAction({
|
|
15
31
|
name: 'pattern',
|
|
@@ -56,6 +72,13 @@ export const pattern = defineAction({
|
|
|
56
72
|
};
|
|
57
73
|
},
|
|
58
74
|
afterParamsSave: async (ctx: any, params, previousParams) => {
|
|
75
|
+
if (shouldPreserveFieldModelOnPatternChange(ctx)) {
|
|
76
|
+
if (params.pattern !== previousParams.pattern) {
|
|
77
|
+
(ctx.model.subModels.field as PatternAwareFieldModel | undefined)?.scheduleApplyJsSettings?.();
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
59
82
|
const targetCollection = ctx.collectionField.targetCollection;
|
|
60
83
|
const targetCollectionTitleField = targetCollection?.getField(
|
|
61
84
|
ctx.model.subModels.field.props?.fieldNames?.label || ctx.model.props.titleField,
|
|
@@ -47,6 +47,8 @@ interface RouteLike {
|
|
|
47
47
|
pathname?: string;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
const hasUsableSourceId = (sourceId: unknown) => sourceId !== undefined && sourceId !== null && String(sourceId) !== '';
|
|
51
|
+
|
|
50
52
|
/**
|
|
51
53
|
* 管理 admin 场景下每个 page 的 v2 视图栈编排。
|
|
52
54
|
* 该协调器只负责状态机和开关视图,不直接绑定 React 生命周期。
|
|
@@ -264,6 +266,10 @@ export class AdminLayoutRouteCoordinator {
|
|
|
264
266
|
const destroyRef = React.createRef<(result?: any, force?: boolean) => void>();
|
|
265
267
|
const updateRef = React.createRef<(value: any) => void>();
|
|
266
268
|
const openViewParams = getOpenViewStepParams(viewItem.model);
|
|
269
|
+
const associationName =
|
|
270
|
+
openViewParams?.associationName && !hasUsableSourceId(viewItem.params.sourceId)
|
|
271
|
+
? null
|
|
272
|
+
: openViewParams?.associationName;
|
|
267
273
|
const openerUids = viewList.slice(0, viewItem.index).map((item) => item.params.viewUid);
|
|
268
274
|
const navigation = new ViewNavigation(
|
|
269
275
|
this.flowEngine.context,
|
|
@@ -273,7 +279,7 @@ export class AdminLayoutRouteCoordinator {
|
|
|
273
279
|
viewItem.model.dispatchEvent('click', {
|
|
274
280
|
target: runtime.meta.layoutContentElement || this.layoutContentElement,
|
|
275
281
|
collectionName: openViewParams?.collectionName,
|
|
276
|
-
associationName
|
|
282
|
+
associationName,
|
|
277
283
|
dataSourceKey: openViewParams?.dataSourceKey,
|
|
278
284
|
destroyRef,
|
|
279
285
|
updateRef,
|
|
@@ -0,0 +1,117 @@
|
|
|
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 } from '@nocobase/flow-engine';
|
|
11
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
12
|
+
import { getViewDiffAndUpdateHidden } from '../../getViewDiffAndUpdateHidden';
|
|
13
|
+
import { getOpenViewStepParams } from '../../flows/openViewFlow';
|
|
14
|
+
import { resolveViewParamsToViewList } from '../../resolveViewParamsToViewList';
|
|
15
|
+
import { AdminLayoutRouteCoordinator } from '../AdminLayoutRouteCoordinator';
|
|
16
|
+
import { RouteModel } from '../../models/base/RouteModel';
|
|
17
|
+
|
|
18
|
+
vi.mock('../../resolveViewParamsToViewList', () => ({
|
|
19
|
+
resolveViewParamsToViewList: vi.fn(),
|
|
20
|
+
updateViewListHidden: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock('../../getViewDiffAndUpdateHidden', () => ({
|
|
24
|
+
getViewDiffAndUpdateHidden: vi.fn(),
|
|
25
|
+
getKey: vi.fn((viewItem) => viewItem.params.viewUid),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock('../../flows/openViewFlow', async (importOriginal) => {
|
|
29
|
+
const actual = await importOriginal();
|
|
30
|
+
return {
|
|
31
|
+
...(actual as any),
|
|
32
|
+
getOpenViewStepParams: vi.fn(),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const mockResolveViewParamsToViewList = vi.mocked(resolveViewParamsToViewList);
|
|
37
|
+
const mockGetViewDiffAndUpdateHidden = vi.mocked(getViewDiffAndUpdateHidden);
|
|
38
|
+
const mockGetOpenViewStepParams = vi.mocked(getOpenViewStepParams);
|
|
39
|
+
|
|
40
|
+
function setupRouteReplay(viewParams: Record<string, any>) {
|
|
41
|
+
const engine = new FlowEngine();
|
|
42
|
+
engine.registerModels({ RouteModel });
|
|
43
|
+
engine.context.defineProperty('route', {
|
|
44
|
+
value: {
|
|
45
|
+
params: { name: 'test-route' },
|
|
46
|
+
pathname: '/admin/popup/filterbytk/member',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
engine.context.defineProperty('routeRepository', {
|
|
50
|
+
value: {
|
|
51
|
+
getRouteBySchemaUid: vi.fn(() => ({})),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const dispatchEvent = vi.fn(() => Promise.resolve());
|
|
56
|
+
const viewItem = {
|
|
57
|
+
params: {
|
|
58
|
+
viewUid: 'popup',
|
|
59
|
+
filterByTk: 'member',
|
|
60
|
+
...viewParams,
|
|
61
|
+
},
|
|
62
|
+
modelUid: 'popup',
|
|
63
|
+
model: { uid: 'popup', dispatchEvent } as any,
|
|
64
|
+
hidden: { value: false },
|
|
65
|
+
index: 0,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
mockResolveViewParamsToViewList.mockReturnValue([viewItem]);
|
|
69
|
+
mockGetViewDiffAndUpdateHidden.mockReturnValue({
|
|
70
|
+
viewsToClose: [],
|
|
71
|
+
viewsToOpen: [viewItem],
|
|
72
|
+
});
|
|
73
|
+
mockGetOpenViewStepParams.mockReturnValue({
|
|
74
|
+
collectionName: 'roles',
|
|
75
|
+
associationName: 'users.roles',
|
|
76
|
+
dataSourceKey: 'main',
|
|
77
|
+
} as any);
|
|
78
|
+
|
|
79
|
+
const coordinator = new AdminLayoutRouteCoordinator(engine);
|
|
80
|
+
coordinator.registerPage('test-route', {
|
|
81
|
+
active: true,
|
|
82
|
+
layoutContentElement: document.createElement('div'),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return { dispatchEvent };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
describe('AdminLayoutRouteCoordinator', () => {
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
vi.clearAllMocks();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('drops configured associationName during route replay when sourceId is absent', () => {
|
|
94
|
+
const { dispatchEvent } = setupRouteReplay({});
|
|
95
|
+
|
|
96
|
+
expect(dispatchEvent.mock.calls[0][1]).toMatchObject({
|
|
97
|
+
collectionName: 'roles',
|
|
98
|
+
associationName: null,
|
|
99
|
+
dataSourceKey: 'main',
|
|
100
|
+
filterByTk: 'member',
|
|
101
|
+
triggerByRouter: true,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('keeps configured associationName during route replay when sourceId is present', () => {
|
|
106
|
+
const { dispatchEvent } = setupRouteReplay({ sourceId: '1' });
|
|
107
|
+
|
|
108
|
+
expect(dispatchEvent.mock.calls[0][1]).toMatchObject({
|
|
109
|
+
collectionName: 'roles',
|
|
110
|
+
associationName: 'users.roles',
|
|
111
|
+
dataSourceKey: 'main',
|
|
112
|
+
filterByTk: 'member',
|
|
113
|
+
sourceId: '1',
|
|
114
|
+
triggerByRouter: true,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -116,6 +116,11 @@ const resetStyle = css`
|
|
|
116
116
|
.ant-pro-base-menu-vertical-collapsed .ant-pro-base-menu-vertical-menu-item {
|
|
117
117
|
height: auto;
|
|
118
118
|
}
|
|
119
|
+
|
|
120
|
+
.ant-menu-item:has([data-nb-hidden-menu-item='true']),
|
|
121
|
+
.ant-menu-submenu:has([data-nb-hidden-menu-item='true']) {
|
|
122
|
+
display: none !important;
|
|
123
|
+
}
|
|
119
124
|
`;
|
|
120
125
|
|
|
121
126
|
const contentStyle = {
|
|
@@ -373,6 +378,7 @@ export const AdminLayoutComponent = observer((props: any) => {
|
|
|
373
378
|
const { token } = antdTheme.useToken();
|
|
374
379
|
const customToken = token as CustomToken;
|
|
375
380
|
const isMobileLayout = !!adminLayoutModel?.isMobileLayout;
|
|
381
|
+
const menuRouteRefreshVersion = adminLayoutModel?.menuRouteRefreshVersion || 0;
|
|
376
382
|
const isMobileSider = isMobileLayout || isMobileViewport;
|
|
377
383
|
const [collapsed, setCollapsed] = useState(isMobileSider);
|
|
378
384
|
const [preferredFlowSettingsEnabled, setPreferredFlowSettingsEnabled] = useState(() => readFlowSettingsPreference());
|
|
@@ -505,7 +511,7 @@ export const AdminLayoutComponent = observer((props: any) => {
|
|
|
505
511
|
children: [],
|
|
506
512
|
};
|
|
507
513
|
setRoute(nextRoute);
|
|
508
|
-
}, [adminLayoutModel, allAccessRoutes, designable, isMobileSider, t]);
|
|
514
|
+
}, [adminLayoutModel, allAccessRoutes, designable, isMobileSider, menuRouteRefreshVersion, t]);
|
|
509
515
|
|
|
510
516
|
useEffect(() => {
|
|
511
517
|
const syncId = ++flowSettingsSyncRef.current;
|
|
@@ -627,6 +633,7 @@ export const AdminLayoutComponent = observer((props: any) => {
|
|
|
627
633
|
<MobileMenuControlContext.Provider value={{ closeMobileMenu }}>
|
|
628
634
|
<DndProvider collisionDetection={menuCollisionDetection} onDragEnd={handleMenuDragEnd}>
|
|
629
635
|
<ProLayout
|
|
636
|
+
key={`admin-layout-menu-${menuRouteRefreshVersion}`}
|
|
630
637
|
{...props}
|
|
631
638
|
contentStyle={contentStyle}
|
|
632
639
|
siderWidth={customToken.siderWidth || 200}
|
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
reconcileAdminLayoutMenuItems,
|
|
29
29
|
shouldRenderIconInTitle,
|
|
30
30
|
} from './AdminLayoutMenuUtils';
|
|
31
|
-
import { findFirstPageRoute } from './AdminLayoutCompat';
|
|
32
31
|
import {
|
|
33
32
|
buildMenuBasicSchema,
|
|
34
33
|
buildLinkSettingSchema,
|
|
@@ -41,7 +40,14 @@ import {
|
|
|
41
40
|
matchesRoutePath,
|
|
42
41
|
toTreeSelectItems,
|
|
43
42
|
} from './AdminLayoutMenuFlowUtils';
|
|
44
|
-
import {
|
|
43
|
+
import { ADMIN_LAYOUT_MODEL_UID } from './constants';
|
|
44
|
+
import {
|
|
45
|
+
findFirstAccessiblePageRoute,
|
|
46
|
+
findFirstV2LandingRoute,
|
|
47
|
+
isV2AdminRuntime,
|
|
48
|
+
resolveAdminRouteRuntimeTarget,
|
|
49
|
+
toRouterNavigationPath,
|
|
50
|
+
} from './resolveAdminRouteRuntimeTarget';
|
|
45
51
|
|
|
46
52
|
export * from './AdminLayoutMenuUtils';
|
|
47
53
|
const insertPositionToMethod = {
|
|
@@ -122,6 +128,15 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
122
128
|
return this.flowRegistry.getFlows().size;
|
|
123
129
|
}
|
|
124
130
|
|
|
131
|
+
hasPersistableMenuLinkageRules() {
|
|
132
|
+
const params = this.getStepParams('menuSettings', 'linkageRules') as { value?: any[] } | undefined;
|
|
133
|
+
return Array.isArray(params?.value) && params.value.length > 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
hasCurrentPersistedMenuState() {
|
|
137
|
+
return this.getCurrentPersistedInstanceFlowCount() > 0 || this.hasPersistableMenuLinkageRules();
|
|
138
|
+
}
|
|
139
|
+
|
|
125
140
|
buildRouteOptionsWithPersistedFlowFlag(hasPersistedMenuInstanceFlow: boolean) {
|
|
126
141
|
const route = this.getRoute();
|
|
127
142
|
const nextOptions = {
|
|
@@ -147,7 +162,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
147
162
|
return;
|
|
148
163
|
}
|
|
149
164
|
|
|
150
|
-
const hasPersistedMenuInstanceFlow = this.
|
|
165
|
+
const hasPersistedMenuInstanceFlow = this.hasCurrentPersistedMenuState();
|
|
151
166
|
if (this.hasPersistedMenuInstanceFlowFlag(route) === hasPersistedMenuInstanceFlow) {
|
|
152
167
|
return;
|
|
153
168
|
}
|
|
@@ -188,6 +203,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
188
203
|
if (this.isCreationSession()) {
|
|
189
204
|
return;
|
|
190
205
|
}
|
|
206
|
+
let shouldRerenderAfterHydrate = false;
|
|
191
207
|
|
|
192
208
|
const repository = this.flowEngine.modelRepository;
|
|
193
209
|
if (!repository?.findOne) {
|
|
@@ -201,6 +217,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
201
217
|
|
|
202
218
|
if (data.stepParams && typeof data.stepParams === 'object') {
|
|
203
219
|
this.setStepParams(data.stepParams);
|
|
220
|
+
shouldRerenderAfterHydrate = this.hasPersistableMenuLinkageRules();
|
|
204
221
|
}
|
|
205
222
|
|
|
206
223
|
if (data.flowRegistry && typeof data.flowRegistry === 'object') {
|
|
@@ -219,9 +236,11 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
219
236
|
return typeof flow.on === 'string' ? flow.on === 'beforeRender' : flow.on?.eventName === 'beforeRender';
|
|
220
237
|
});
|
|
221
238
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
239
|
+
shouldRerenderAfterHydrate = shouldRerenderAfterHydrate || hasBeforeRenderFlow;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (shouldRerenderAfterHydrate) {
|
|
243
|
+
void this.rerender();
|
|
225
244
|
}
|
|
226
245
|
})().finally(() => {
|
|
227
246
|
this.persistedStateHydrated = true;
|
|
@@ -242,6 +261,27 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
242
261
|
return currentFlowCount > 0 || initialFlowCount > 0;
|
|
243
262
|
}
|
|
244
263
|
|
|
264
|
+
setHidden(value: boolean) {
|
|
265
|
+
const previous = this.hidden;
|
|
266
|
+
super.setHidden(value);
|
|
267
|
+
if (previous !== this.hidden) {
|
|
268
|
+
(this.flowEngine.getModel?.(ADMIN_LAYOUT_MODEL_UID) as any)?.refreshMenuRouteTree?.();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
protected renderHiddenInConfig(): React.ReactNode | undefined {
|
|
273
|
+
const { item, dom, options, renderType } = this.props;
|
|
274
|
+
if (!item || !renderType) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<div style={{ opacity: 0.3 }}>
|
|
280
|
+
<AdminLayoutMenuItemRenderer item={item} dom={dom} options={options} renderType={renderType} />
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
245
285
|
async createMenuRoute(
|
|
246
286
|
route: NocoBaseDesktopRoute,
|
|
247
287
|
options?: {
|
|
@@ -436,11 +476,11 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
436
476
|
return true;
|
|
437
477
|
}
|
|
438
478
|
|
|
439
|
-
const
|
|
479
|
+
const hasCurrentPersistedMenuState = this.hasCurrentPersistedMenuState();
|
|
440
480
|
|
|
441
481
|
// 菜单基础设置继续直接保存到 route repository;
|
|
442
482
|
// 只有实例事件流需要回退到 FlowModel 默认持久化链路。
|
|
443
|
-
if (
|
|
483
|
+
if (hasCurrentPersistedMenuState) {
|
|
444
484
|
await super.saveStepParams();
|
|
445
485
|
} else if (this.hasPersistedMenuInstanceFlowFlag()) {
|
|
446
486
|
await this.destroyPersistedState();
|
|
@@ -523,6 +563,10 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
523
563
|
return null;
|
|
524
564
|
}
|
|
525
565
|
|
|
566
|
+
if (!options.designable && this.hidden) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
|
|
526
570
|
const shouldShowIconInTitle = shouldRenderIconInTitle({ depth, isMobile: options.isMobile });
|
|
527
571
|
const { name, icon } = buildMenuTitleWithIcon(route, options.t, shouldShowIconInTitle);
|
|
528
572
|
|
|
@@ -546,6 +590,11 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
546
590
|
app: this.context.app,
|
|
547
591
|
route,
|
|
548
592
|
});
|
|
593
|
+
|
|
594
|
+
if (runtimeTarget.reason === 'unsupportedV2Runtime') {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
|
|
549
598
|
const path = route.schemaUid
|
|
550
599
|
? `/admin/${route.schemaUid}`
|
|
551
600
|
: getAdminLayoutMenuVirtualPath('link', `${this.uid}-invalid`);
|
|
@@ -579,13 +628,20 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
579
628
|
)
|
|
580
629
|
.filter(Boolean) || [];
|
|
581
630
|
|
|
631
|
+
if (isV2AdminRuntime(this.context.app) && children.length === 0) {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
|
|
582
635
|
if (options.designable && depth === 0) {
|
|
583
636
|
children.push(getAdminLayoutMenuInitializerButton('schema-initializer-Menu-side', this, route));
|
|
584
637
|
}
|
|
585
638
|
|
|
639
|
+
const landingRoute = isV2AdminRuntime(this.context.app)
|
|
640
|
+
? findFirstV2LandingRoute(itemChildren)
|
|
641
|
+
: findFirstAccessiblePageRoute(itemChildren);
|
|
586
642
|
const runtimeTarget = resolveAdminRouteRuntimeTarget({
|
|
587
643
|
app: this.context.app,
|
|
588
|
-
route,
|
|
644
|
+
route: isV2AdminRuntime(this.context.app) && landingRoute ? landingRoute : route,
|
|
589
645
|
});
|
|
590
646
|
|
|
591
647
|
const groupRoute: AdminLayoutMenuNode = {
|
|
@@ -593,9 +649,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
593
649
|
icon,
|
|
594
650
|
path: `/admin/${route.id}`,
|
|
595
651
|
redirect:
|
|
596
|
-
children[0]?.key === 'x-designer-button'
|
|
597
|
-
? undefined
|
|
598
|
-
: `/admin/${findFirstPageRoute(itemChildren)?.schemaUid || route.id}`,
|
|
652
|
+
children[0]?.key === 'x-designer-button' ? undefined : `/admin/${landingRoute?.schemaUid || route.id}`,
|
|
599
653
|
hideInMenu: route.hideInMenu,
|
|
600
654
|
_runtimePath: runtimeTarget.runtimePath,
|
|
601
655
|
_navigationMode: runtimeTarget.navigationMode,
|
|
@@ -707,6 +761,10 @@ AdminLayoutMenuItemModel.registerFlow({
|
|
|
707
761
|
});
|
|
708
762
|
},
|
|
709
763
|
},
|
|
764
|
+
linkageRules: {
|
|
765
|
+
use: 'menuLinkageRules',
|
|
766
|
+
hideInSettings: async (ctx: FlowSettingsContext<AdminLayoutMenuItemModel>) => ctx.model.isCreationSession(),
|
|
767
|
+
},
|
|
710
768
|
moveTo: {
|
|
711
769
|
title: 'Move to',
|
|
712
770
|
defaultParams: async () => ({
|
|
@@ -15,9 +15,9 @@ import {
|
|
|
15
15
|
FlowModel,
|
|
16
16
|
FlowModelRenderer,
|
|
17
17
|
FlowSettingsButton,
|
|
18
|
+
observer,
|
|
18
19
|
} from '@nocobase/flow-engine';
|
|
19
|
-
import {
|
|
20
|
-
import type { HookAPI } from 'antd/es/modal/useModal';
|
|
20
|
+
import { Badge, Tooltip } from 'antd';
|
|
21
21
|
import qs from 'qs';
|
|
22
22
|
import React, { FC, useCallback, useContext, useEffect } from 'react';
|
|
23
23
|
import { Link, useLocation, type NavigateFunction } from 'react-router-dom';
|
|
@@ -284,33 +284,6 @@ const translateByModel = (model: FlowModel, value: any) => {
|
|
|
284
284
|
return typeof model.context.t === 'function' ? model.context.t(value) : value;
|
|
285
285
|
};
|
|
286
286
|
|
|
287
|
-
const translateMenuNode = (item: AdminLayoutMenuNode, value: any) => {
|
|
288
|
-
return item._model ? translateByModel(item._model, value) : value;
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* 经典页面需要整页跳回 v1,先给用户一个明确确认,避免误离开当前 v2 上下文。
|
|
293
|
-
*/
|
|
294
|
-
const confirmLegacyPageNavigation = async (options: { item: AdminLayoutMenuNode; confirm?: HookAPI['confirm'] }) => {
|
|
295
|
-
const { item, confirm } = options;
|
|
296
|
-
|
|
297
|
-
if (!item._isLegacy || typeof confirm !== 'function') {
|
|
298
|
-
return true;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const confirmed = await confirm({
|
|
302
|
-
title: translateMenuNode(item, 'Open classic page access'),
|
|
303
|
-
content: translateMenuNode(
|
|
304
|
-
item,
|
|
305
|
-
'This page requires the classic version to open properly. Do you want to go there now?',
|
|
306
|
-
),
|
|
307
|
-
okText: translateMenuNode(item, 'Yes'),
|
|
308
|
-
cancelText: translateMenuNode(item, 'Cancel'),
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
return !!confirmed;
|
|
312
|
-
};
|
|
313
|
-
|
|
314
287
|
const MENU_TYPE_ITEMS: Array<{ key: string; label: string; menuType: AdminLayoutMenuCreationType }> = [
|
|
315
288
|
{ key: 'group', label: 'Group', menuType: 'group' },
|
|
316
289
|
{ key: 'flow-page', label: 'Page', menuType: 'flowPage' },
|
|
@@ -520,7 +493,6 @@ export function resolveAdminLayoutMenuDragMoveOptionsFromEvent(
|
|
|
520
493
|
const GroupItem: FC<{ item: AdminLayoutMenuNode; options?: AdminLayoutMenuRenderOptions }> = (props) => {
|
|
521
494
|
const { item } = props;
|
|
522
495
|
const badgeCount = useEvaluatedExpression(item._route.options?.badge?.count, item._model?.context);
|
|
523
|
-
const { modal } = App.useApp();
|
|
524
496
|
const navigate = useNavigateNoUpdate();
|
|
525
497
|
const routerBasename = useRouterBasename();
|
|
526
498
|
const { closeMobileMenu } = useContext(MobileMenuControlContext);
|
|
@@ -556,23 +528,13 @@ const GroupItem: FC<{ item: AdminLayoutMenuNode; options?: AdminLayoutMenuRender
|
|
|
556
528
|
|
|
557
529
|
event.preventDefault();
|
|
558
530
|
event.stopPropagation();
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
runAfterMobileMenuClosed({
|
|
569
|
-
isMobile: !!props.options?.isMobile,
|
|
570
|
-
closeMobileMenu,
|
|
571
|
-
callback: () => {
|
|
572
|
-
window.location.assign(runtimePath);
|
|
573
|
-
},
|
|
574
|
-
});
|
|
575
|
-
})();
|
|
531
|
+
runAfterMobileMenuClosed({
|
|
532
|
+
isMobile: !!props.options?.isMobile,
|
|
533
|
+
closeMobileMenu,
|
|
534
|
+
callback: () => {
|
|
535
|
+
window.location.assign(runtimePath);
|
|
536
|
+
},
|
|
537
|
+
});
|
|
576
538
|
return;
|
|
577
539
|
}
|
|
578
540
|
|
|
@@ -589,16 +551,7 @@ const GroupItem: FC<{ item: AdminLayoutMenuNode; options?: AdminLayoutMenuRender
|
|
|
589
551
|
},
|
|
590
552
|
});
|
|
591
553
|
},
|
|
592
|
-
[
|
|
593
|
-
closeMobileMenu,
|
|
594
|
-
item,
|
|
595
|
-
item._navigationMode,
|
|
596
|
-
modal,
|
|
597
|
-
navigate,
|
|
598
|
-
props.options?.isMobile,
|
|
599
|
-
runtimePath,
|
|
600
|
-
spaRuntimePath,
|
|
601
|
-
],
|
|
554
|
+
[closeMobileMenu, item, item._navigationMode, navigate, props.options?.isMobile, runtimePath, spaRuntimePath],
|
|
602
555
|
);
|
|
603
556
|
|
|
604
557
|
const landingEntryAriaLabel = ariaLabel ? `${ariaLabel}-landing-entry` : 'group-landing-entry';
|
|
@@ -656,7 +609,6 @@ const MenuItem: FC<{ item: AdminLayoutMenuNode; options?: AdminLayoutMenuRenderO
|
|
|
656
609
|
const { item } = props;
|
|
657
610
|
const location = useLocation();
|
|
658
611
|
const badgeCount = useEvaluatedExpression(item._route.options?.badge?.count, item._model?.context);
|
|
659
|
-
const { modal } = App.useApp();
|
|
660
612
|
const navigate = useNavigateNoUpdate();
|
|
661
613
|
const basenameOfCurrentRouter = useRouterBasename();
|
|
662
614
|
const { closeMobileMenu } = useContext(MobileMenuControlContext);
|
|
@@ -717,23 +669,13 @@ const MenuItem: FC<{ item: AdminLayoutMenuNode; options?: AdminLayoutMenuRenderO
|
|
|
717
669
|
|
|
718
670
|
event.preventDefault();
|
|
719
671
|
event.stopPropagation();
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
runAfterMobileMenuClosed({
|
|
730
|
-
isMobile: !!props.options?.isMobile,
|
|
731
|
-
closeMobileMenu,
|
|
732
|
-
callback: () => {
|
|
733
|
-
window.location.assign(runtimePath);
|
|
734
|
-
},
|
|
735
|
-
});
|
|
736
|
-
})();
|
|
672
|
+
runAfterMobileMenuClosed({
|
|
673
|
+
isMobile: !!props.options?.isMobile,
|
|
674
|
+
closeMobileMenu,
|
|
675
|
+
callback: () => {
|
|
676
|
+
window.location.assign(runtimePath);
|
|
677
|
+
},
|
|
678
|
+
});
|
|
737
679
|
return;
|
|
738
680
|
}
|
|
739
681
|
|
|
@@ -757,16 +699,7 @@ const MenuItem: FC<{ item: AdminLayoutMenuNode; options?: AdminLayoutMenuRenderO
|
|
|
757
699
|
},
|
|
758
700
|
});
|
|
759
701
|
},
|
|
760
|
-
[
|
|
761
|
-
props.options?.isMobile,
|
|
762
|
-
closeMobileMenu,
|
|
763
|
-
isDocumentNavigation,
|
|
764
|
-
item,
|
|
765
|
-
modal,
|
|
766
|
-
navigate,
|
|
767
|
-
runtimePath,
|
|
768
|
-
spaRuntimePath,
|
|
769
|
-
],
|
|
702
|
+
[props.options?.isMobile, closeMobileMenu, isDocumentNavigation, item, navigate, runtimePath, spaRuntimePath],
|
|
770
703
|
);
|
|
771
704
|
|
|
772
705
|
if (item._route?.type === NocoBaseDesktopRouteType.link) {
|
|
@@ -889,13 +822,15 @@ export const shouldRenderIconInTitle = ({ depth, isMobile }: { depth: number; is
|
|
|
889
822
|
return depth > 1 || (isMobile && depth > 0);
|
|
890
823
|
};
|
|
891
824
|
|
|
825
|
+
const HiddenMenuItemPlaceholder = () => <span data-nb-hidden-menu-item="true" />;
|
|
826
|
+
|
|
892
827
|
export const AdminLayoutMenuModelRenderer: FC<{
|
|
893
828
|
model: FlowModel;
|
|
894
829
|
item: AdminLayoutMenuNode;
|
|
895
830
|
dom: React.ReactNode;
|
|
896
831
|
renderType: AdminLayoutMenuRenderType;
|
|
897
832
|
options?: AdminLayoutMenuRenderOptions;
|
|
898
|
-
}> = ({ model, item, dom, renderType, options }) => {
|
|
833
|
+
}> = observer(({ model, item, dom, renderType, options }) => {
|
|
899
834
|
const token = model.context.themeToken;
|
|
900
835
|
|
|
901
836
|
useEffect(() => {
|
|
@@ -907,6 +842,10 @@ export const AdminLayoutMenuModelRenderer: FC<{
|
|
|
907
842
|
});
|
|
908
843
|
}, [dom, item, model, options, renderType]);
|
|
909
844
|
|
|
845
|
+
if (!model.context.flowSettingsEnabled && model.hidden) {
|
|
846
|
+
return <HiddenMenuItemPlaceholder />;
|
|
847
|
+
}
|
|
848
|
+
|
|
910
849
|
return (
|
|
911
850
|
<ResetThemeTokenAndKeepAlgorithm>
|
|
912
851
|
<Droppable model={model}>
|
|
@@ -931,7 +870,7 @@ export const AdminLayoutMenuModelRenderer: FC<{
|
|
|
931
870
|
</Droppable>
|
|
932
871
|
</ResetThemeTokenAndKeepAlgorithm>
|
|
933
872
|
);
|
|
934
|
-
};
|
|
873
|
+
});
|
|
935
874
|
|
|
936
875
|
export function getAdminLayoutMenuInitializerButton(
|
|
937
876
|
testId: string,
|
|
@@ -50,6 +50,7 @@ type GetAdminLayoutModelOptions<TModel extends FlowModel = AdminLayoutModel> = {
|
|
|
50
50
|
*/
|
|
51
51
|
export class AdminLayoutModel extends FlowModel<AdminLayoutStructure> {
|
|
52
52
|
isMobileLayout = false;
|
|
53
|
+
menuRouteRefreshVersion = 0;
|
|
53
54
|
private routeCoordinator?: AdminLayoutRouteCoordinator;
|
|
54
55
|
private routeDisposer?: () => void;
|
|
55
56
|
private activePageUid = '';
|
|
@@ -60,9 +61,19 @@ export class AdminLayoutModel extends FlowModel<AdminLayoutStructure> {
|
|
|
60
61
|
super(options);
|
|
61
62
|
define(this, {
|
|
62
63
|
isMobileLayout: observable.ref,
|
|
64
|
+
menuRouteRefreshVersion: observable.ref,
|
|
63
65
|
});
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
/**
|
|
69
|
+
* 通知 Layout 重新生成 ProLayout 菜单路由。
|
|
70
|
+
*
|
|
71
|
+
* @returns {void}
|
|
72
|
+
*/
|
|
73
|
+
refreshMenuRouteTree() {
|
|
74
|
+
this.menuRouteRefreshVersion += 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
66
77
|
/**
|
|
67
78
|
* 注册页面运行时信息。
|
|
68
79
|
*
|