@nocobase/client-v2 2.1.0-beta.29 → 2.1.0-beta.32

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 (100) hide show
  1. package/es/BaseApplication.d.ts +1 -0
  2. package/es/PluginManager.d.ts +1 -0
  3. package/es/components/form/DrawerFormLayout.d.ts +49 -0
  4. package/es/components/form/EnvVariableInput.d.ts +42 -0
  5. package/es/components/form/FileSizeInput.d.ts +27 -0
  6. package/es/components/form/createFormRegistry.d.ts +33 -0
  7. package/es/components/form/index.d.ts +13 -0
  8. package/es/components/index.d.ts +1 -1
  9. package/es/flow/actions/index.d.ts +1 -1
  10. package/es/flow/actions/linkageRules.d.ts +2 -0
  11. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +4 -0
  12. package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -0
  13. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +5 -0
  14. package/es/flow/components/FieldAssignRulesEditor.d.ts +1 -0
  15. package/es/flow/internal/utils/enumOptionsUtils.d.ts +5 -0
  16. package/es/flow/models/actions/AssociationActionUtils.d.ts +5 -0
  17. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +4 -0
  18. package/es/flow/models/blocks/filter-manager/FilterManager.d.ts +5 -1
  19. package/es/flow/models/blocks/table/TableSelectModel.d.ts +8 -0
  20. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +3 -0
  21. package/es/flow/models/fields/DisplayTitleFieldModel.d.ts +1 -1
  22. package/es/flow/models/utils/displayValueUtils.d.ts +12 -0
  23. package/es/flow-compat/passwordUtils.d.ts +1 -1
  24. package/es/index.mjs +122 -106
  25. package/es/utils/remotePlugins.d.ts +0 -4
  26. package/lib/index.js +121 -105
  27. package/package.json +6 -5
  28. package/src/BaseApplication.tsx +14 -8
  29. package/src/PluginManager.ts +1 -0
  30. package/src/__tests__/app.test.tsx +28 -1
  31. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +67 -46
  32. package/src/__tests__/remotePlugins.test.ts +29 -18
  33. package/src/__tests__/settings-center.test.tsx +30 -0
  34. package/src/components/form/DrawerFormLayout.tsx +103 -0
  35. package/src/components/form/EnvVariableInput.tsx +126 -0
  36. package/src/components/form/FileSizeInput.tsx +105 -0
  37. package/src/components/form/createFormRegistry.ts +60 -0
  38. package/src/components/form/index.tsx +14 -0
  39. package/src/components/index.ts +1 -1
  40. package/src/flow/__tests__/FlowRoute.test.tsx +4 -5
  41. package/src/flow/actions/__tests__/actionLinkageRules.race.repro.test.ts +199 -0
  42. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +92 -13
  43. package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +6 -1
  44. package/src/flow/actions/__tests__/linkageRules.menu.test.ts +90 -0
  45. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +476 -1
  46. package/src/flow/actions/index.ts +2 -0
  47. package/src/flow/actions/linkageRules.tsx +316 -280
  48. package/src/flow/actions/linkageRulesFormValueRefresh.ts +2 -8
  49. package/src/flow/actions/setTargetDataScope.tsx +32 -3
  50. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +8 -1
  51. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +70 -12
  52. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +26 -87
  53. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +11 -0
  54. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +5 -1
  55. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +292 -31
  56. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +50 -12
  57. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +77 -56
  58. package/src/flow/components/AdminLayout.tsx +2 -2
  59. package/src/flow/components/FieldAssignRulesEditor.tsx +2 -0
  60. package/src/flow/components/FlowRoute.tsx +17 -4
  61. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
  62. package/src/flow/components/filter/LinkageFilterItem.tsx +9 -2
  63. package/src/flow/components/filter/VariableFilterItem.tsx +2 -6
  64. package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +71 -0
  65. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -0
  66. package/src/flow/internal/utils/__tests__/enumOptionsUtils.test.ts +10 -1
  67. package/src/flow/internal/utils/enumOptionsUtils.ts +29 -0
  68. package/src/flow/models/actions/AssociateActionModel.tsx +2 -2
  69. package/src/flow/models/actions/AssociationActionUtils.ts +14 -0
  70. package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +63 -0
  71. package/src/flow/models/base/CollectionBlockModel.tsx +7 -0
  72. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +33 -9
  73. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +53 -13
  74. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +63 -3
  75. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +33 -1
  76. package/src/flow/models/blocks/filter-manager/FilterManager.ts +66 -2
  77. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +270 -0
  78. package/src/flow/models/blocks/form/FormBlockModel.tsx +8 -5
  79. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +30 -0
  80. package/src/flow/models/blocks/form/value-runtime/rules.ts +6 -1
  81. package/src/flow/models/blocks/form/value-runtime/runtime.ts +6 -1
  82. package/src/flow/models/blocks/table/TableBlockModel.tsx +11 -6
  83. package/src/flow/models/blocks/table/TableColumnModel.tsx +3 -0
  84. package/src/flow/models/blocks/table/TableSelectModel.tsx +36 -26
  85. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowClick.test.ts +69 -0
  86. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +96 -1
  87. package/src/flow/models/blocks/table/__tests__/TableSelectModel.test.ts +41 -0
  88. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -0
  89. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +7 -0
  90. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +4 -1
  91. package/src/flow/models/fields/ClickableFieldModel.tsx +9 -4
  92. package/src/flow/models/fields/DisplayTitleFieldModel.tsx +12 -4
  93. package/src/flow/models/fields/SelectFieldModel.tsx +31 -1
  94. package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +23 -0
  95. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +2 -1
  96. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +7 -0
  97. package/src/flow/models/utils/displayValueUtils.ts +57 -0
  98. package/src/flow/system-settings/useSystemSettings.tsx +36 -1
  99. package/src/utils/globalDeps.ts +2 -0
  100. package/src/utils/remotePlugins.ts +7 -27
@@ -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);
@@ -10,6 +10,7 @@
10
10
  import {
11
11
  ActionScene,
12
12
  defineAction,
13
+ extractUsedVariablePaths,
13
14
  FlowModel,
14
15
  MultiRecordResource,
15
16
  useFlowContext,
@@ -20,6 +21,34 @@ import React from 'react';
20
21
  import { FilterGroup, VariableFilterItem } from '../components/filter';
21
22
  import { normalizeDataScopeFilter } from './dataScopeFilter';
22
23
 
24
+ function dependsOnClickedRowRecord(filter: any) {
25
+ if (!filter) {
26
+ return false;
27
+ }
28
+ const used = extractUsedVariablePaths(filter) || {};
29
+ return Object.prototype.hasOwnProperty.call(used, 'clickedRowRecord');
30
+ }
31
+
32
+ function shouldClearClickedRowDataScope(ctx: any, params: any) {
33
+ return ctx.inputArgs?.selected === false && dependsOnClickedRowRecord(params.filter);
34
+ }
35
+
36
+ function resolveTargetDataScopeFilter(ctx: any, params: any, resolvedParams: any) {
37
+ if (shouldClearClickedRowDataScope(ctx, params)) {
38
+ return undefined;
39
+ }
40
+
41
+ return normalizeDataScopeFilter(params.filter, resolvedParams.filter);
42
+ }
43
+
44
+ function shouldRefreshTargetResource(resource: MultiRecordResource) {
45
+ if (resource.hasData()) {
46
+ return true;
47
+ }
48
+
49
+ return resource.getMeta?.('count') !== undefined || resource.getMeta?.('hasNext') !== undefined;
50
+ }
51
+
23
52
  export const setTargetDataScope = defineAction({
24
53
  name: 'setTargetDataScope',
25
54
  title: tExpr('Set data scope'),
@@ -69,20 +98,20 @@ export const setTargetDataScope = defineAction({
69
98
  return;
70
99
  }
71
100
  const model: FlowModel = ctx.model;
101
+ const filter = resolveTargetDataScopeFilter(ctx, params, resolvedParams);
102
+
72
103
  model.scheduleModelOperation(targetBlockUid, (targetModel) => {
73
104
  const resource = targetModel['resource'] as MultiRecordResource;
74
105
  if (!resource) {
75
106
  return;
76
107
  }
77
108
 
78
- const filter = normalizeDataScopeFilter(params.filter, resolvedParams.filter);
79
-
80
109
  if (isEmptyFilter(filter)) {
81
110
  resource.removeFilterGroup(`setTargetDataScope_${ctx.model.uid}`);
82
111
  } else {
83
112
  resource.addFilterGroup(`setTargetDataScope_${ctx.model.uid}`, filter);
84
113
  }
85
- if (resource.hasData()) {
114
+ if (shouldRefreshTargetResource(resource)) {
86
115
  resource.refresh();
87
116
  }
88
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 { 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 }} />}