@nocobase/client-v2 2.2.0-beta.7 → 2.2.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/es/BaseApplication.d.ts +2 -0
  2. package/es/RouteRepository.d.ts +8 -0
  3. package/es/RouterManager.d.ts +15 -0
  4. package/es/authRedirect.d.ts +1 -0
  5. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  6. package/es/entry-actions/index.d.ts +9 -0
  7. package/es/flow/actions/afterSuccess.d.ts +8 -1
  8. package/es/flow/actions/index.d.ts +1 -1
  9. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  10. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  11. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +1 -0
  12. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  13. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  14. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  15. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  16. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  17. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  18. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +9 -0
  19. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  20. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  21. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  22. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  23. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  24. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  25. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  26. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  27. package/es/index.d.ts +2 -0
  28. package/es/index.mjs +243 -141
  29. package/es/utils/markdownSanitize.d.ts +11 -0
  30. package/lib/index.js +232 -130
  31. package/package.json +7 -7
  32. package/src/BaseApplication.tsx +2 -0
  33. package/src/RouteRepository.ts +25 -0
  34. package/src/RouterManager.tsx +142 -1
  35. package/src/__tests__/RouteRepository.test.ts +23 -0
  36. package/src/__tests__/RouterManager.test.ts +125 -0
  37. package/src/__tests__/authRedirect.test.ts +17 -0
  38. package/src/authRedirect.ts +38 -0
  39. package/src/collection-manager/field-configure.ts +1 -1
  40. package/src/collection-manager/interfaces/id.ts +1 -1
  41. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  42. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  43. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  44. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  45. package/src/collection-manager/interfaces/obo.tsx +2 -2
  46. package/src/collection-manager/interfaces/oho.tsx +2 -2
  47. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  48. package/src/collection-manager/interfaces/uuid.ts +1 -1
  49. package/src/entry-actions/EntryActionManager.ts +76 -0
  50. package/src/entry-actions/index.ts +10 -0
  51. package/src/flow/FlowPage.tsx +29 -2
  52. package/src/flow/__tests__/FlowPage.test.tsx +152 -25
  53. package/src/flow/__tests__/FlowRoute.test.tsx +547 -2
  54. package/src/flow/__tests__/getKey.test.ts +7 -0
  55. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  56. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  57. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
  58. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +164 -0
  59. package/src/flow/actions/afterSuccess.tsx +142 -3
  60. package/src/flow/actions/dataScopeFilter.ts +10 -1
  61. package/src/flow/actions/dateTimeFormat.tsx +2 -2
  62. package/src/flow/actions/index.ts +1 -1
  63. package/src/flow/actions/openView.tsx +59 -5
  64. package/src/flow/admin-shell/BaseLayoutModel.tsx +149 -3
  65. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +187 -42
  66. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1020 -110
  67. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +42 -4
  68. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +177 -1
  69. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +20 -0
  70. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +5 -4
  71. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  72. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  73. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +518 -2
  74. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
  75. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  76. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  77. package/src/flow/common/Markdown/Display.tsx +14 -3
  78. package/src/flow/common/Markdown/Edit.tsx +19 -7
  79. package/src/flow/components/FlowRoute.tsx +128 -16
  80. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  81. package/src/flow/internal/components/Markdown/util.ts +2 -1
  82. package/src/flow/models/base/ActionModel.tsx +10 -8
  83. package/src/flow/models/base/ActionModelCore.tsx +11 -4
  84. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  85. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  86. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  87. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  88. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  89. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +87 -0
  90. package/src/flow/models/blocks/form/value-runtime/runtime.ts +91 -0
  91. package/src/flow/models/blocks/js-block/JSBlock.tsx +223 -2
  92. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  93. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +35 -12
  94. package/src/flow/models/blocks/table/TableBlockModel.tsx +25 -14
  95. package/src/flow/models/blocks/table/TableColumnModel.tsx +6 -2
  96. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +57 -1
  97. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  98. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  99. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +51 -0
  100. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  101. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  102. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  103. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +45 -13
  104. package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
  105. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
  106. package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
  107. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  108. package/src/flow/routeTransientInputArgs.ts +27 -0
  109. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
  110. package/src/index.ts +2 -0
  111. package/src/nocobase-buildin-plugin/index.tsx +7 -1
  112. package/src/settings-center/SystemSettingsPage.tsx +1 -1
  113. package/src/utils/markdownSanitize.ts +88 -0
@@ -8,12 +8,19 @@
8
8
  */
9
9
 
10
10
  import { define, observable } from '@formily/reactive';
11
- import { parsePathnameToViewParams, type FlowEngine, FlowModel, type ViewParam } from '@nocobase/flow-engine';
11
+ import {
12
+ decodeOpenViewRouteState,
13
+ parsePathnameToViewParams,
14
+ type FlowEngine,
15
+ FlowModel,
16
+ type ViewParam,
17
+ } from '@nocobase/flow-engine';
12
18
  import {
13
19
  BaseLayoutRouteCoordinator,
14
20
  type BaseLayoutRouteCoordinatorOptions,
15
21
  type RoutePageMeta,
16
22
  } from './BaseLayoutRouteCoordinator';
23
+ import { NocoBaseDesktopRouteType } from '../../flow-compat';
17
24
  import type { LayoutDefinition } from '../../layout-manager/types';
18
25
  import { isLayoutContentRouteName } from '../../layout-manager/utils';
19
26
 
@@ -66,6 +73,7 @@ export interface LayoutRouteLike {
66
73
  id?: string;
67
74
  name?: string;
68
75
  pathname?: string;
76
+ state?: unknown;
69
77
  params?: Record<string, string | undefined>;
70
78
  layoutRouteName?: string;
71
79
  layoutBasePathname?: string;
@@ -105,6 +113,15 @@ const getDefaultBasePathnameFromRoutePath = (routePath?: string) => {
105
113
 
106
114
  const isKnownViewParamName = (segment: string) => ['tab', 'filterbytk', 'sourceid'].includes(segment);
107
115
 
116
+ const isLegacyLayoutContentRouteName = (routeName: string, targetRouteName?: string) => {
117
+ return (
118
+ !!targetRouteName &&
119
+ ['page', 'page.tabs', 'page.tabs.popups', 'page.tab', 'page.view', 'page.tab.view'].some(
120
+ (legacyName) => targetRouteName === `${routeName}.${legacyName}`,
121
+ )
122
+ );
123
+ };
124
+
108
125
  const isStandardLayoutRelativePath = (relativePath: string) => {
109
126
  if (!relativePath) {
110
127
  return true;
@@ -116,20 +133,36 @@ const isStandardLayoutRelativePath = (relativePath: string) => {
116
133
  }
117
134
 
118
135
  let i = 1;
136
+ let currentViewUid = segments[0];
119
137
  while (i < segments.length) {
120
138
  const segment = segments[i];
139
+
121
140
  if (segment === 'view') {
122
141
  if (!segments[i + 1]) {
123
142
  return false;
124
143
  }
144
+ currentViewUid = segments[i + 1];
145
+ i += 2;
146
+ continue;
147
+ }
148
+
149
+ if (segment === 'opts') {
150
+ if (!segments[i + 1] || !decodeOpenViewRouteState(currentViewUid, segments[i + 1])) {
151
+ return false;
152
+ }
125
153
  i += 2;
126
154
  continue;
127
155
  }
128
156
 
129
- if (!isKnownViewParamName(segment) || !segments[i + 1]) {
157
+ if (isKnownViewParamName(segment) && segments[i + 1]) {
158
+ i += 2;
159
+ continue;
160
+ }
161
+
162
+ if (!segments[i + 1]) {
130
163
  return false;
131
164
  }
132
- i += 2;
165
+ return false;
133
166
  }
134
167
 
135
168
  return true;
@@ -148,6 +181,7 @@ export class BaseLayoutModel<
148
181
  currentLayoutRoute: LayoutRouteMatch | null = null;
149
182
  protected routeCoordinator?: BaseLayoutRouteCoordinator;
150
183
  private activePageUid = '';
184
+ private currentRouteState?: unknown;
151
185
  private layoutContentElement: HTMLElement | null = null;
152
186
  private readonly routePageMetaMap = new Map<string, RoutePageMeta>();
153
187
  private contextBindingsActive = false;
@@ -162,6 +196,7 @@ export class BaseLayoutModel<
162
196
 
163
197
  registerRoutePage(pageUid: string, meta: RoutePageMeta) {
164
198
  this.routePageMetaMap.set(pageUid, meta);
199
+ this.restoreCurrentLayoutRouteFromRouterContext(pageUid);
165
200
  const routeModel = this.getCoordinator().registerPage(pageUid, meta);
166
201
  this.getCoordinator().syncRoute(this.getCurrentCoordinatorRouteLike());
167
202
  return routeModel;
@@ -300,6 +335,18 @@ export class BaseLayoutModel<
300
335
  };
301
336
  }
302
337
 
338
+ const routeRepository = this.flowEngine.context.routeRepository;
339
+ const schemaRoute = routeRepository?.getRouteBySchemaUid?.(pageUid);
340
+ const route = schemaRoute ? undefined : routeRepository?.getRouteById?.(pageUid);
341
+ if (!schemaRoute && route?.type === NocoBaseDesktopRouteType.group) {
342
+ return {
343
+ type: 'root',
344
+ pathname,
345
+ basePathname,
346
+ relativePath,
347
+ };
348
+ }
349
+
303
350
  return {
304
351
  type: 'page',
305
352
  pathname,
@@ -323,6 +370,7 @@ export class BaseLayoutModel<
323
370
 
324
371
  const layoutRoute = this.resolveLayoutRoute(routeLike);
325
372
  this.currentLayoutRoute = layoutRoute;
373
+ this.currentRouteState = routeLike.state;
326
374
  this.activePageUid = this.getPageUidFromLayoutRoute(layoutRoute);
327
375
  this.getCoordinator().syncRoute({
328
376
  ...routeLike,
@@ -342,7 +390,12 @@ export class BaseLayoutModel<
342
390
  return;
343
391
  }
344
392
 
393
+ if (this.shouldIgnoreStaleLayoutRouteCleanup(routeLike)) {
394
+ return;
395
+ }
396
+
345
397
  this.currentLayoutRoute = null;
398
+ this.currentRouteState = undefined;
346
399
  this.activePageUid = '';
347
400
  this.routeCoordinator?.syncRoute({});
348
401
  }
@@ -412,12 +465,105 @@ export class BaseLayoutModel<
412
465
  layoutRouteName: this.layout.routeName,
413
466
  pageUid: this.currentLayoutRoute.pageUid,
414
467
  pathname: this.currentLayoutRoute.pathname,
468
+ state: this.currentRouteState,
415
469
  layoutBasePathname: this.currentLayoutRoute.basePathname,
416
470
  layoutRoute: this.currentLayoutRoute,
417
471
  };
418
472
  }
419
473
  return {};
420
474
  }
475
+
476
+ private restoreCurrentLayoutRouteFromRouterContext(pageUid: string) {
477
+ if (this.currentLayoutRoute?.type === 'page') {
478
+ return;
479
+ }
480
+
481
+ const routeLike = this.getRouterContextRouteLike();
482
+ if (!routeLike) {
483
+ return;
484
+ }
485
+
486
+ if (!this.isRouteLikeOwnedByCurrentLayout(routeLike)) {
487
+ return;
488
+ }
489
+
490
+ const layoutRoute = this.resolveLayoutRoute({
491
+ ...routeLike,
492
+ layoutRouteName: routeLike.layoutRouteName || this.layout.routeName,
493
+ });
494
+ if (layoutRoute.type !== 'page' || layoutRoute.pageUid !== pageUid) {
495
+ return;
496
+ }
497
+
498
+ this.currentLayoutRoute = layoutRoute;
499
+ this.activePageUid = layoutRoute.pageUid;
500
+ }
501
+
502
+ private shouldIgnoreStaleLayoutRouteCleanup(routeLike?: LayoutRouteLike) {
503
+ if (!routeLike?.pathname || this.currentLayoutRoute?.type !== 'page') {
504
+ return false;
505
+ }
506
+
507
+ const currentLayoutRoute = this.currentLayoutRoute;
508
+ if (!this.routePageMetaMap.has(currentLayoutRoute.pageUid)) {
509
+ return false;
510
+ }
511
+
512
+ const routeLikePathname = normalizePathname(routeLike.pathname);
513
+ if (routeLikePathname !== currentLayoutRoute.pathname) {
514
+ return false;
515
+ }
516
+
517
+ const routerRouteLike = this.getRouterContextRouteLike();
518
+ if (!routerRouteLike) {
519
+ return false;
520
+ }
521
+
522
+ if (!this.isRouteLikeOwnedByCurrentLayout(routerRouteLike)) {
523
+ return false;
524
+ }
525
+
526
+ const routerLayoutRoute = this.resolveLayoutRoute({
527
+ ...routerRouteLike,
528
+ layoutRouteName: routerRouteLike.layoutRouteName || this.layout.routeName,
529
+ });
530
+
531
+ return (
532
+ routerLayoutRoute.type === 'page' &&
533
+ routerLayoutRoute.pageUid === currentLayoutRoute.pageUid &&
534
+ routerLayoutRoute.pathname === currentLayoutRoute.pathname
535
+ );
536
+ }
537
+
538
+ private getRouterContextRouteLike(): LayoutRouteLike | null {
539
+ const route = this.flowEngine.context.route as LayoutRouteLike | undefined;
540
+ if (!route?.pathname) {
541
+ return null;
542
+ }
543
+
544
+ return {
545
+ id: route.id,
546
+ name: route.name || route.id,
547
+ pathname: route.pathname,
548
+ params: route.params,
549
+ layoutRouteName: route.layoutRouteName,
550
+ layoutBasePathname: route.layoutBasePathname,
551
+ };
552
+ }
553
+
554
+ private isRouteLikeOwnedByCurrentLayout(routeLike: LayoutRouteLike) {
555
+ if (routeLike.layoutRouteName) {
556
+ return routeLike.layoutRouteName === this.layout.routeName;
557
+ }
558
+
559
+ const layoutBasePathname = getDefaultBasePathnameFromRoutePath(this.layout.routePath);
560
+ if (routeLike.layoutBasePathname && layoutBasePathname) {
561
+ return normalizeBasePathname(routeLike.layoutBasePathname) === normalizeBasePathname(layoutBasePathname);
562
+ }
563
+
564
+ const routeName = routeLike.name || routeLike.id;
565
+ return this.isLayoutContentRoute(routeLike) || isLegacyLayoutContentRouteName(this.layout.routeName, routeName);
566
+ }
421
567
  }
422
568
 
423
569
  /**
@@ -22,6 +22,7 @@ import { getOpenViewStepParams } from '../flows/openViewFlow';
22
22
  import { RouteModel } from '../models/base/RouteModel';
23
23
  import { resolveViewParamsToViewList, updateViewListHidden, type ViewItem } from '../resolveViewParamsToViewList';
24
24
  import type { LayoutDefinition } from '../../layout-manager/types';
25
+ import { getRouteTransientInputArgs } from '../routeTransientInputArgs';
25
26
 
26
27
  export interface RoutePageMeta {
27
28
  active: boolean;
@@ -49,14 +50,19 @@ interface RoutePageRuntime {
49
50
  meta: RoutePageMeta;
50
51
  viewState: Record<string, ViewRuntimeState>;
51
52
  prevViewList: ViewItem[];
53
+ pendingOpenViewKeys: Set<string>;
52
54
  hasStepNavigated: boolean;
55
+ currentPathname?: string;
56
+ currentRouteState?: unknown;
53
57
  forceStop: boolean;
58
+ viewGeneration: number;
54
59
  }
55
60
 
56
61
  interface RouteLike {
57
62
  layoutRouteName?: string;
58
63
  params?: { name?: string };
59
64
  pathname?: string;
65
+ state?: unknown;
60
66
  pageUid?: string;
61
67
  layoutBasePathname?: string;
62
68
  layoutRoute?: {
@@ -66,6 +72,11 @@ interface RouteLike {
66
72
  } | null;
67
73
  }
68
74
 
75
+ interface SyncRuntimeOptions {
76
+ skipInitialStepNavigation?: boolean;
77
+ routeState?: unknown;
78
+ }
79
+
69
80
  const hasUsableSourceId = (sourceId: unknown) => sourceId !== undefined && sourceId !== null && String(sourceId) !== '';
70
81
 
71
82
  const normalizeBasePathname = (basePathname?: string) => {
@@ -92,6 +103,7 @@ export class BaseLayoutRouteCoordinator {
92
103
  private basePathname: string;
93
104
  private readonly runtimes = new Map<string, RoutePageRuntime>();
94
105
  private layoutContentElement: HTMLElement | null = null;
106
+ private lastNonNullLayoutContentElement: HTMLElement | null = null;
95
107
 
96
108
  constructor(flowEngine: FlowEngine, options: BaseLayoutRouteCoordinatorOptions = {}) {
97
109
  this.flowEngine = flowEngine;
@@ -104,7 +116,19 @@ export class BaseLayoutRouteCoordinator {
104
116
  }
105
117
 
106
118
  setLayoutContentElement(element: HTMLElement | null) {
119
+ const previousTarget = this.layoutContentElement || this.lastNonNullLayoutContentElement;
107
120
  this.layoutContentElement = element;
121
+
122
+ if (!element) {
123
+ return;
124
+ }
125
+
126
+ const shouldReplayActiveViews = !!previousTarget && previousTarget !== element;
127
+ this.lastNonNullLayoutContentElement = element;
128
+
129
+ if (shouldReplayActiveViews) {
130
+ this.replayActiveRuntimeViewsAfterLayoutContentElementChange();
131
+ }
108
132
  }
109
133
 
110
134
  registerPage(pageUid: string, meta: RoutePageMeta) {
@@ -119,8 +143,10 @@ export class BaseLayoutRouteCoordinator {
119
143
  },
120
144
  viewState: {},
121
145
  prevViewList: [],
146
+ pendingOpenViewKeys: new Set(),
122
147
  hasStepNavigated: false,
123
148
  forceStop: false,
149
+ viewGeneration: 0,
124
150
  };
125
151
 
126
152
  this.ensureRouteModelContext(runtime);
@@ -159,6 +185,7 @@ export class BaseLayoutRouteCoordinator {
159
185
  this.runtimes.forEach((runtime) => {
160
186
  this.setRuntimeActive(runtime, false);
161
187
  });
188
+ this.invalidatePendingRuntimeViewOpens();
162
189
  return;
163
190
  }
164
191
 
@@ -175,11 +202,16 @@ export class BaseLayoutRouteCoordinator {
175
202
  });
176
203
 
177
204
  if (!activePageUid || !pathname) {
205
+ this.invalidatePendingRuntimeViewOpens();
178
206
  return;
179
207
  }
180
208
 
181
209
  this.runtimes.forEach((runtime) => {
182
210
  if (runtime.pageUid !== activePageUid) {
211
+ if (!runtime.forceStop) {
212
+ runtime.viewGeneration += 1;
213
+ runtime.pendingOpenViewKeys.clear();
214
+ }
183
215
  runtime.forceStop = true;
184
216
  }
185
217
  });
@@ -190,7 +222,13 @@ export class BaseLayoutRouteCoordinator {
190
222
  }
191
223
 
192
224
  runtime.forceStop = false;
193
- this.syncRuntimeWithPathname(runtime, pathname);
225
+ if (runtime.currentPathname && runtime.currentPathname !== pathname) {
226
+ runtime.viewGeneration += 1;
227
+ runtime.pendingOpenViewKeys.clear();
228
+ }
229
+ runtime.currentPathname = pathname;
230
+ runtime.currentRouteState = routeLike.state;
231
+ this.syncRuntimeWithPathname(runtime, pathname, { routeState: routeLike.state });
194
232
  }
195
233
 
196
234
  cleanupPage(pageUid: string) {
@@ -200,6 +238,8 @@ export class BaseLayoutRouteCoordinator {
200
238
  }
201
239
 
202
240
  runtime.forceStop = true;
241
+ runtime.viewGeneration += 1;
242
+ runtime.pendingOpenViewKeys.clear();
203
243
  runtime.prevViewList.forEach((viewItem) => {
204
244
  this.flowEngine.removeModelWithSubModels(viewItem.params.viewUid);
205
245
  runtime.viewState[getKey(viewItem)]?.destroy?.();
@@ -207,6 +247,8 @@ export class BaseLayoutRouteCoordinator {
207
247
  });
208
248
  runtime.prevViewList = [];
209
249
  runtime.hasStepNavigated = false;
250
+ runtime.currentPathname = undefined;
251
+ runtime.currentRouteState = undefined;
210
252
  }
211
253
 
212
254
  destroy() {
@@ -249,7 +291,7 @@ export class BaseLayoutRouteCoordinator {
249
291
  viewState?.deactivate?.();
250
292
  }
251
293
 
252
- private syncRuntimeWithPathname(runtime: RoutePageRuntime, pathname: string) {
294
+ private syncRuntimeWithPathname(runtime: RoutePageRuntime, pathname: string, options: SyncRuntimeOptions = {}) {
253
295
  try {
254
296
  if (!this.basePathname) {
255
297
  return;
@@ -257,29 +299,42 @@ export class BaseLayoutRouteCoordinator {
257
299
 
258
300
  const viewStack = parsePathnameToViewParams(pathname, { basePath: this.basePathname });
259
301
  const viewList = resolveViewParamsToViewList(this.flowEngine, viewStack, runtime.routeModel);
302
+ const routeState = options.routeState;
260
303
 
261
- if (this.shouldStepNavigate(runtime, viewList)) {
262
- this.stepNavigate(viewList, 0);
304
+ if (!options.skipInitialStepNavigation && this.shouldStepNavigate(runtime, viewList)) {
305
+ this.stepNavigate(viewList, 0, routeState);
263
306
  runtime.hasStepNavigated = true;
264
- this.scheduleInitialDeepLinkReplay(runtime, pathname);
307
+ this.scheduleInitialDeepLinkReplay(runtime, pathname, routeState);
265
308
  return;
266
309
  }
267
310
 
268
311
  const { viewsToClose, viewsToOpen } = getViewDiffAndUpdateHidden(runtime.prevViewList, viewList);
312
+ const viewsToOpenKeys = new Set(viewsToOpen.map((viewItem) => getKey(viewItem)));
313
+ const nextViewsToOpen = viewList.filter((viewItem) => {
314
+ const key = getKey(viewItem);
315
+ if (runtime.pendingOpenViewKeys.has(key)) {
316
+ return false;
317
+ }
318
+ return viewsToOpenKeys.has(key) || !runtime.viewState[key];
319
+ });
269
320
 
270
321
  if (viewsToClose.length) {
271
322
  viewsToClose.forEach((viewItem) => {
323
+ runtime.pendingOpenViewKeys.delete(getKey(viewItem));
272
324
  runtime.viewState[getKey(viewItem)]?.destroy?.(true);
273
325
  delete runtime.viewState[getKey(viewItem)];
274
326
  });
275
- updateViewListHidden(viewList);
327
+ updateViewListHidden(viewList, !!this.layoutContext?.isMobileLayout);
276
328
  }
277
329
 
278
- if (viewsToOpen.length) {
279
- void this.handleOpenViews(runtime, viewList, viewsToOpen);
330
+ if (nextViewsToOpen.length) {
331
+ this.markPendingOpenViews(runtime, nextViewsToOpen);
332
+ this.handleOpenViews(runtime, viewList, nextViewsToOpen, runtime.viewGeneration, routeState).catch((error) => {
333
+ console.error(`[NocoBase] Failed to open route-managed views:`, error);
334
+ });
280
335
  }
281
336
 
282
- if (viewsToClose.length === 0 && viewsToOpen.length === 0) {
337
+ if (viewsToClose.length === 0 && nextViewsToOpen.length === 0) {
283
338
  const currentViewItem = viewList.at(-1);
284
339
  if (currentViewItem) {
285
340
  runtime.viewState[getKey(currentViewItem)]?.navigation.setViewStack(viewList.map((item) => item.params));
@@ -299,7 +354,7 @@ export class BaseLayoutRouteCoordinator {
299
354
  }
300
355
 
301
356
  if (runtime.meta.active) {
302
- updateViewListHidden(viewList);
357
+ updateViewListHidden(viewList, !!this.layoutContext?.isMobileLayout);
303
358
  return;
304
359
  }
305
360
 
@@ -312,30 +367,38 @@ export class BaseLayoutRouteCoordinator {
312
367
  return runtime.prevViewList.length === 0 && viewList.length > 1 && !runtime.hasStepNavigated;
313
368
  }
314
369
 
315
- private scheduleInitialDeepLinkReplay(runtime: RoutePageRuntime, pathname: string) {
370
+ private scheduleInitialDeepLinkReplay(runtime: RoutePageRuntime, pathname: string, routeState?: unknown) {
371
+ const viewGeneration = runtime.viewGeneration;
316
372
  Promise.resolve()
317
373
  .then(() => {
318
- if (runtime.forceStop || runtime.prevViewList.length > 0) {
374
+ if (
375
+ runtime.forceStop ||
376
+ runtime.viewGeneration !== viewGeneration ||
377
+ runtime.currentPathname !== pathname ||
378
+ runtime.prevViewList.length > 0
379
+ ) {
319
380
  return;
320
381
  }
321
382
 
322
- this.syncRuntimeWithPathname(runtime, pathname);
383
+ this.syncRuntimeWithPathname(runtime, pathname, { routeState });
323
384
  })
324
385
  .catch(() => {
325
386
  // ignore
326
387
  });
327
388
  }
328
389
 
329
- private stepNavigate(viewList: ViewItem[], index: number) {
390
+ private stepNavigate(viewList: ViewItem[], index: number, routeState?: unknown) {
330
391
  if (!viewList[index]) {
331
392
  return;
332
393
  }
333
394
 
395
+ const navigationOptions = routeState ? { state: routeState } : undefined;
334
396
  if (index === 0) {
335
397
  new ViewNavigation(this.flowEngine.context, [], { basePath: this.basePathname }).navigateTo(
336
398
  viewList[index].params,
337
399
  {
338
400
  replace: true,
401
+ ...navigationOptions,
339
402
  },
340
403
  );
341
404
  } else {
@@ -343,13 +406,70 @@ export class BaseLayoutRouteCoordinator {
343
406
  this.flowEngine.context,
344
407
  viewList.slice(0, index).map((item) => item.params),
345
408
  { basePath: this.basePathname },
346
- ).navigateTo(viewList[index].params);
409
+ ).navigateTo(viewList[index].params, navigationOptions);
347
410
  }
348
411
 
349
- this.stepNavigate(viewList, index + 1);
412
+ this.stepNavigate(viewList, index + 1, routeState);
413
+ }
414
+
415
+ private replayActiveRuntimeViewsAfterLayoutContentElementChange() {
416
+ this.runtimes.forEach((runtime) => {
417
+ if (!runtime.currentPathname || runtime.prevViewList.length === 0) {
418
+ return;
419
+ }
420
+
421
+ this.resetRuntimeViewStateForLayoutContentElementChange(runtime);
422
+ if (runtime.meta.active) {
423
+ this.syncRuntimeWithPathname(runtime, runtime.currentPathname, {
424
+ skipInitialStepNavigation: true,
425
+ routeState: runtime.currentRouteState,
426
+ });
427
+ }
428
+ });
429
+ }
430
+
431
+ private invalidatePendingRuntimeViewOpens() {
432
+ this.runtimes.forEach((runtime) => {
433
+ runtime.forceStop = true;
434
+ runtime.viewGeneration += 1;
435
+ runtime.pendingOpenViewKeys.clear();
436
+ });
437
+ }
438
+
439
+ private resetRuntimeViewStateForLayoutContentElementChange(runtime: RoutePageRuntime) {
440
+ runtime.viewGeneration += 1;
441
+ runtime.pendingOpenViewKeys.clear();
442
+ runtime.prevViewList.forEach((viewItem, index) => {
443
+ runtime.viewState[getKey(viewItem)]?.destroy?.(true);
444
+ delete runtime.viewState[getKey(viewItem)];
445
+
446
+ if (index > 0) {
447
+ this.flowEngine.removeModelWithSubModels(viewItem.params.viewUid);
448
+ }
449
+ });
450
+ runtime.prevViewList = [];
451
+ runtime.hasStepNavigated = false;
452
+ }
453
+
454
+ private markPendingOpenViews(runtime: RoutePageRuntime, viewItems: ViewItem[]) {
455
+ viewItems.forEach((viewItem) => {
456
+ runtime.pendingOpenViewKeys.add(getKey(viewItem));
457
+ });
458
+ }
459
+
460
+ private clearPendingOpenViews(runtime: RoutePageRuntime, viewItems: ViewItem[]) {
461
+ viewItems.forEach((viewItem) => {
462
+ runtime.pendingOpenViewKeys.delete(getKey(viewItem));
463
+ });
350
464
  }
351
465
 
352
- private async handleOpenViews(runtime: RoutePageRuntime, viewList: ViewItem[], viewsToOpen: ViewItem[]) {
466
+ private async handleOpenViews(
467
+ runtime: RoutePageRuntime,
468
+ viewList: ViewItem[],
469
+ viewsToOpen: ViewItem[],
470
+ viewGeneration: number,
471
+ routeState?: unknown,
472
+ ) {
353
473
  const missingModels = viewsToOpen.filter((v) => !v.model);
354
474
  if (missingModels.length > 0) {
355
475
  await Promise.all(
@@ -363,24 +483,37 @@ export class BaseLayoutRouteCoordinator {
363
483
  );
364
484
  }
365
485
 
366
- if (runtime.forceStop) {
486
+ if (runtime.forceStop || runtime.viewGeneration !== viewGeneration) {
367
487
  return;
368
488
  }
369
489
 
370
490
  this.syncViewListVisibility(runtime, viewList);
371
- this.openViews(runtime, viewList, viewsToOpen, 0);
491
+ this.openViews(runtime, viewList, viewsToOpen, 0, viewGeneration, routeState);
372
492
  }
373
493
 
374
- private openViews(runtime: RoutePageRuntime, viewList: ViewItem[], viewsToOpen: ViewItem[], index: number) {
494
+ private openViews(
495
+ runtime: RoutePageRuntime,
496
+ viewList: ViewItem[],
497
+ viewsToOpen: ViewItem[],
498
+ index: number,
499
+ viewGeneration: number,
500
+ routeState?: unknown,
501
+ ) {
502
+ if (runtime.forceStop || runtime.viewGeneration !== viewGeneration) {
503
+ return;
504
+ }
505
+
375
506
  if (!viewsToOpen[index]) {
376
507
  return;
377
508
  }
378
509
 
379
510
  const viewItem = viewsToOpen[index];
380
511
  if (!viewItem.model) {
512
+ this.clearPendingOpenViews(runtime, viewsToOpen.slice(index));
381
513
  return;
382
514
  }
383
515
 
516
+ const viewKey = getKey(viewItem);
384
517
  const destroyRef = React.createRef<(result?: any, force?: boolean) => void>();
385
518
  const updateRef = React.createRef<(value: any) => void>();
386
519
  const activateRef = React.createRef<(forceRefresh?: boolean) => void>();
@@ -390,42 +523,54 @@ export class BaseLayoutRouteCoordinator {
390
523
  openViewParams?.associationName && !hasUsableSourceId(viewItem.params.sourceId)
391
524
  ? null
392
525
  : openViewParams?.associationName;
526
+ const openViewRouteState = viewItem.params.openViewRouteState;
393
527
  const openerUids = viewList.slice(0, viewItem.index).map((item) => item.params.viewUid);
528
+ const transientInputArgs = getRouteTransientInputArgs(routeState, viewItem.params.viewUid);
394
529
  const navigation = new ViewNavigation(
395
530
  this.flowEngine.context,
396
531
  viewList.slice(0, viewItem.index + 1).map((item) => item.params),
397
532
  { basePath: this.basePathname },
398
533
  );
399
534
 
400
- viewItem.model.dispatchEvent('click', {
401
- target: this.layoutContentElement || runtime.meta.layoutContentElement,
402
- collectionName: openViewParams?.collectionName,
403
- associationName,
404
- dataSourceKey: openViewParams?.dataSourceKey,
405
- destroyRef,
406
- updateRef,
407
- activateRef,
408
- deactivateRef,
409
- openerUids,
410
- ...viewItem.params,
411
- pageActive: runtime.meta.active,
412
- activationControlledByLayout: true,
413
- navigation,
414
- onOpen: () => {
415
- this.openViews(runtime, viewList, viewsToOpen, index + 1);
416
- },
417
- hidden: viewItem.hidden,
418
- isMobileLayout: !!this.layoutContext?.isMobileLayout,
419
- triggerByRouter: true,
420
- });
535
+ try {
536
+ viewItem.model.dispatchEvent('click', {
537
+ target: this.layoutContentElement || runtime.meta.layoutContentElement,
538
+ collectionName: openViewParams?.collectionName,
539
+ associationName,
540
+ dataSourceKey: openViewParams?.dataSourceKey,
541
+ destroyRef,
542
+ updateRef,
543
+ activateRef,
544
+ deactivateRef,
545
+ openerUids,
546
+ ...viewItem.params,
547
+ ...transientInputArgs,
548
+ ...(openViewRouteState?.mode ? { mode: openViewRouteState.mode } : {}),
549
+ ...(openViewRouteState?.size ? { size: openViewRouteState.size } : {}),
550
+ pageActive: runtime.meta.active,
551
+ activationControlledByLayout: true,
552
+ navigation,
553
+ onOpen: () => {
554
+ this.openViews(runtime, viewList, viewsToOpen, index + 1, viewGeneration, routeState);
555
+ },
556
+ hidden: viewItem.hidden,
557
+ isMobileLayout: !!this.layoutContext?.isMobileLayout,
558
+ triggerByRouter: true,
559
+ });
560
+ } catch (error) {
561
+ this.clearPendingOpenViews(runtime, viewsToOpen.slice(index));
562
+ console.error(`[NocoBase] Failed to dispatch route-managed view open:`, error);
563
+ return;
564
+ }
421
565
 
422
- runtime.viewState[getKey(viewItem)] = {
566
+ runtime.viewState[viewKey] = {
423
567
  destroy: (_force?: boolean) => destroyRef.current?.(),
424
568
  update: (value: any) => updateRef.current?.(value),
425
569
  activate: (forceRefresh?: boolean) => activateRef.current?.(forceRefresh),
426
570
  deactivate: () => deactivateRef.current?.(),
427
571
  navigation,
428
572
  };
573
+ runtime.pendingOpenViewKeys.delete(viewKey);
429
574
  }
430
575
 
431
576
  private ensureRouteModelContext(runtime: RoutePageRuntime) {