@nocobase/client-v2 2.1.0-alpha.32 → 2.1.0-alpha.34

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.
Files changed (32) hide show
  1. package/es/flow/actions/index.d.ts +1 -1
  2. package/es/flow/actions/linkageRules.d.ts +2 -0
  3. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +4 -0
  4. package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -0
  5. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +5 -0
  6. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +3 -0
  7. package/es/index.mjs +78 -73
  8. package/lib/index.js +59 -54
  9. package/package.json +5 -5
  10. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +67 -46
  11. package/src/__tests__/settings-center.test.tsx +30 -0
  12. package/src/flow/__tests__/FlowRoute.test.tsx +4 -5
  13. package/src/flow/actions/__tests__/actionLinkageRules.race.repro.test.ts +199 -0
  14. package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +6 -1
  15. package/src/flow/actions/__tests__/linkageRules.menu.test.ts +90 -0
  16. package/src/flow/actions/index.ts +2 -0
  17. package/src/flow/actions/linkageRules.tsx +77 -23
  18. package/src/flow/actions/linkageRulesFormValueRefresh.ts +2 -8
  19. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +8 -1
  20. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +70 -12
  21. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +26 -87
  22. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +11 -0
  23. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +5 -1
  24. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +292 -31
  25. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +50 -12
  26. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +77 -56
  27. package/src/flow/components/AdminLayout.tsx +2 -2
  28. package/src/flow/components/FlowRoute.tsx +17 -4
  29. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -0
  30. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +7 -0
  31. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +4 -1
  32. package/src/flow/system-settings/useSystemSettings.tsx +36 -1
@@ -479,6 +479,41 @@ export const linkageSetActionProps = defineAction({
479
479
  },
480
480
  });
481
481
 
482
+ export const linkageSetMenuItemProps = defineAction({
483
+ name: 'linkageSetMenuItemProps',
484
+ title: tExpr('Set menu item state'),
485
+ scene: ActionScene.MENU_LINKAGE_RULES,
486
+ sort: 100,
487
+ uiSchema: {
488
+ value: {
489
+ type: 'string',
490
+ 'x-component': (props) => {
491
+ const { value, onChange } = props;
492
+ // eslint-disable-next-line react-hooks/rules-of-hooks
493
+ const ctx = useFlowContext();
494
+ const t = ctx.model.translate.bind(ctx.model);
495
+
496
+ return (
497
+ <Select
498
+ value={value}
499
+ onChange={onChange}
500
+ placeholder={t('Please select state')}
501
+ style={{ width: '100%' }}
502
+ options={[
503
+ { label: t('Visible'), value: 'visible' },
504
+ { label: t('Hidden'), value: 'hidden' },
505
+ ]}
506
+ allowClear
507
+ />
508
+ );
509
+ },
510
+ },
511
+ },
512
+ handler(ctx, { value, setProps }) {
513
+ setProps(ctx.model, { hiddenModel: value === 'hidden' });
514
+ },
515
+ });
516
+
482
517
  export const linkageSetFieldProps = defineAction({
483
518
  name: 'linkageSetFieldProps',
484
519
  title: tExpr('Set field state'),
@@ -1288,6 +1323,7 @@ export const linkageRunjs = defineAction({
1288
1323
  ActionScene.BLOCK_LINKAGE_RULES,
1289
1324
  ActionScene.FIELD_LINKAGE_RULES,
1290
1325
  ActionScene.ACTION_LINKAGE_RULES,
1326
+ ActionScene.MENU_LINKAGE_RULES,
1291
1327
  ActionScene.DETAILS_FIELD_LINKAGE_RULES,
1292
1328
  ActionScene.SUB_FORM_FIELD_LINKAGE_RULES,
1293
1329
  ],
@@ -1755,6 +1791,8 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1755
1791
 
1756
1792
  const linkageRules: LinkageRule[] = params.value as LinkageRule[];
1757
1793
  const allModels: FlowModel[] = ctx.model.__allModels || (ctx.model.__allModels = []);
1794
+ const modelsToApply = new Set<FlowModel>(allModels);
1795
+ const patchPropsByModel = new Map<FlowModel, any>();
1758
1796
  const directValuePatches: Array<{ path: Array<string | number>; value: any; whenEmpty?: boolean }> = [];
1759
1797
  const rootCollection = getCollectionFromModel((ctx.model as any)?.context?.blockModel ?? ctx.model);
1760
1798
  const isSafeToWriteAssociationSubpath = (namePath: any): boolean => {
@@ -1906,11 +1944,6 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1906
1944
  return null;
1907
1945
  };
1908
1946
 
1909
- allModels.forEach((model: any) => {
1910
- // 重置临时属性
1911
- model.__props = {};
1912
- });
1913
-
1914
1947
  // 1. 运行所有的联动规则
1915
1948
  for (const rule of linkageRules.filter((r) => r.enable)) {
1916
1949
  const { condition: conditions, actions } = rule;
@@ -1919,10 +1952,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1919
1952
  if (!matched) continue;
1920
1953
 
1921
1954
  for (const action of actions) {
1922
- const setProps = (
1923
- model: FlowModel & { __originalProps?: any; __props?: any; __shouldReset?: boolean },
1924
- props: any,
1925
- ) => {
1955
+ const setProps = (model: FlowModel & { __originalProps?: any; __shouldReset?: boolean }, props: any) => {
1926
1956
  // 存储原始值,用于恢复
1927
1957
  if (!model.__originalProps) {
1928
1958
  model.__originalProps = {
@@ -1935,19 +1965,16 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1935
1965
  };
1936
1966
  }
1937
1967
 
1938
- if (!model.__props) {
1939
- model.__props = {};
1940
- }
1941
-
1942
1968
  // 临时存起来,遍历完所有规则后,再统一处理
1943
- model.__props = {
1944
- ...model.__props,
1969
+ patchPropsByModel.set(model, {
1970
+ ...(patchPropsByModel.get(model) || {}),
1945
1971
  ...props,
1946
- };
1972
+ });
1947
1973
 
1948
1974
  if (allModels.indexOf(model) === -1) {
1949
1975
  allModels.push(model);
1950
1976
  }
1977
+ modelsToApply.add(model);
1951
1978
  };
1952
1979
 
1953
1980
  // TODO: 需要改成 runAction 的写法。但 runAction 是异步的,用在这里会不符合预期。后面需要解决这个问题
@@ -1956,15 +1983,12 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1956
1983
  }
1957
1984
 
1958
1985
  // 2. 合并去重(按 uid)后再实际更改相关 model 的状态,避免重复项把“已设置的临时属性”覆盖掉
1959
- const mergedByUid = new Map<
1960
- string,
1961
- FlowModel & { __originalProps?: any; __props?: any; isFork?: boolean; forkId?: number }
1962
- >();
1986
+ const mergedByUid = new Map<string, FlowModel & { __originalProps?: any; isFork?: boolean; forkId?: number }>();
1963
1987
  const mergedPropsByUid = new Map<string, any>();
1964
1988
 
1965
- allModels.forEach((m: any) => {
1989
+ modelsToApply.forEach((m: any) => {
1966
1990
  const uid = m?.uid || String(m);
1967
- const curProps = m.__props || {};
1991
+ const curProps = patchPropsByModel.get(m) || {};
1968
1992
  if (!mergedByUid.has(uid)) {
1969
1993
  mergedByUid.set(uid, m);
1970
1994
  mergedPropsByUid.set(uid, { ...curProps });
@@ -1983,7 +2007,11 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1983
2007
  const newProps = { ...model.__originalProps, ...patchProps };
1984
2008
 
1985
2009
  model.setProps(_.omit(newProps, ['hiddenModel', 'value', 'hiddenText']));
1986
- model.hidden = !!newProps.hiddenModel;
2010
+ if (typeof model.setHidden === 'function') {
2011
+ model.setHidden(!!newProps.hiddenModel);
2012
+ } else {
2013
+ model.hidden = !!newProps.hiddenModel;
2014
+ }
1987
2015
 
1988
2016
  if (newProps.required === true) {
1989
2017
  const rules = (model.props.rules || []).filter((rule) => !rule.required);
@@ -2149,6 +2177,32 @@ export const actionLinkageRules = defineAction({
2149
2177
  },
2150
2178
  });
2151
2179
 
2180
+ export const menuLinkageRules = defineAction({
2181
+ name: 'menuLinkageRules',
2182
+ title: tExpr('Menu linkage rules'),
2183
+ uiMode: 'embed',
2184
+ uiSchema(ctx) {
2185
+ return {
2186
+ value: {
2187
+ type: 'array',
2188
+ 'x-component': LinkageRulesUI,
2189
+ 'x-component-props': {
2190
+ supportedActions: getSupportedActions(ctx, ActionScene.MENU_LINKAGE_RULES),
2191
+ title: tExpr('Menu linkage rules'),
2192
+ },
2193
+ },
2194
+ };
2195
+ },
2196
+ defaultParams: {
2197
+ value: [],
2198
+ },
2199
+ useRawParams: true,
2200
+ handler: async (ctx, params) => {
2201
+ const resolved = await resolveLinkageRulesParamsPreservingRunJsScripts(ctx, params);
2202
+ return commonLinkageRulesHandler(ctx, resolved);
2203
+ },
2204
+ });
2205
+
2152
2206
  export const fieldLinkageRules = defineAction({
2153
2207
  name: 'fieldLinkageRules',
2154
2208
  title: tExpr('Field linkage rules'),
@@ -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);
@@ -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 { resolveAdminRouteRuntimeTarget, toRouterNavigationPath } from './resolveAdminRouteRuntimeTarget';
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.getCurrentPersistedInstanceFlowCount() > 0;
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
- if (hasBeforeRenderFlow) {
223
- void this.rerender();
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 currentPersistedInstanceFlowCount = this.getCurrentPersistedInstanceFlowCount();
479
+ const hasCurrentPersistedMenuState = this.hasCurrentPersistedMenuState();
440
480
 
441
481
  // 菜单基础设置继续直接保存到 route repository;
442
482
  // 只有实例事件流需要回退到 FlowModel 默认持久化链路。
443
- if (currentPersistedInstanceFlowCount > 0) {
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 { App, Badge, Tooltip } from 'antd';
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
- void (async () => {
560
- const confirmed = await confirmLegacyPageNavigation({
561
- item,
562
- confirm: item._model?.context?.modal?.confirm || modal?.confirm,
563
- });
564
- if (!confirmed) {
565
- return;
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
- void (async () => {
721
- const confirmed = await confirmLegacyPageNavigation({
722
- item,
723
- confirm: item._model?.context?.modal?.confirm || modal?.confirm,
724
- });
725
- if (!confirmed) {
726
- return;
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
  *
@@ -14,6 +14,7 @@ import { Result, theme as antdTheme } from 'antd';
14
14
  import React, { FC, useCallback, useMemo } from 'react';
15
15
  import { useTranslation } from 'react-i18next';
16
16
  import { Outlet, useLocation } from 'react-router-dom';
17
+ import { isV2AdminRuntime, isV2MenuRoute } from './resolveAdminRouteRuntimeTarget';
17
18
 
18
19
  type AdminLayoutContentProps = {
19
20
  onContentElementChange?: (element: HTMLDivElement | null) => void;
@@ -67,9 +68,12 @@ const ShowTipWhenNoPages = observer(() => {
67
68
  const { t } = useTranslation();
68
69
  const location = useLocation();
69
70
  const allAccessRoutes = flowEngine.context.routeRepository?.listAccessible?.() || [];
71
+ const visibleRoutes = isV2AdminRuntime(flowEngine.context.app)
72
+ ? allAccessRoutes.filter((route) => isV2MenuRoute(route))
73
+ : allAccessRoutes;
70
74
  const designable = !!flowEngine.context.flowSettingsEnabled;
71
75
 
72
- if (allAccessRoutes.length === 0 && !designable && ['/admin', '/admin/'].includes(location.pathname)) {
76
+ if (visibleRoutes.length === 0 && !designable && ['/admin', '/admin/'].includes(location.pathname)) {
73
77
  return (
74
78
  <Result
75
79
  icon={<HighlightOutlined style={{ fontSize: '8em', color: token.colorText }} />}