@nocobase/client-v2 2.1.15 → 2.1.18

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.
@@ -49,8 +49,11 @@ interface RoutePageRuntime {
49
49
  meta: RoutePageMeta;
50
50
  viewState: Record<string, ViewRuntimeState>;
51
51
  prevViewList: ViewItem[];
52
+ pendingOpenViewKeys: Set<string>;
52
53
  hasStepNavigated: boolean;
54
+ currentPathname?: string;
53
55
  forceStop: boolean;
56
+ viewGeneration: number;
54
57
  }
55
58
 
56
59
  interface RouteLike {
@@ -66,6 +69,10 @@ interface RouteLike {
66
69
  } | null;
67
70
  }
68
71
 
72
+ interface SyncRuntimeOptions {
73
+ skipInitialStepNavigation?: boolean;
74
+ }
75
+
69
76
  const hasUsableSourceId = (sourceId: unknown) => sourceId !== undefined && sourceId !== null && String(sourceId) !== '';
70
77
 
71
78
  const normalizeBasePathname = (basePathname?: string) => {
@@ -92,6 +99,7 @@ export class BaseLayoutRouteCoordinator {
92
99
  private basePathname: string;
93
100
  private readonly runtimes = new Map<string, RoutePageRuntime>();
94
101
  private layoutContentElement: HTMLElement | null = null;
102
+ private lastNonNullLayoutContentElement: HTMLElement | null = null;
95
103
 
96
104
  constructor(flowEngine: FlowEngine, options: BaseLayoutRouteCoordinatorOptions = {}) {
97
105
  this.flowEngine = flowEngine;
@@ -104,7 +112,19 @@ export class BaseLayoutRouteCoordinator {
104
112
  }
105
113
 
106
114
  setLayoutContentElement(element: HTMLElement | null) {
115
+ const previousTarget = this.layoutContentElement || this.lastNonNullLayoutContentElement;
107
116
  this.layoutContentElement = element;
117
+
118
+ if (!element) {
119
+ return;
120
+ }
121
+
122
+ const shouldReplayActiveViews = !!previousTarget && previousTarget !== element;
123
+ this.lastNonNullLayoutContentElement = element;
124
+
125
+ if (shouldReplayActiveViews) {
126
+ this.replayActiveRuntimeViewsAfterLayoutContentElementChange();
127
+ }
108
128
  }
109
129
 
110
130
  registerPage(pageUid: string, meta: RoutePageMeta) {
@@ -119,8 +139,10 @@ export class BaseLayoutRouteCoordinator {
119
139
  },
120
140
  viewState: {},
121
141
  prevViewList: [],
142
+ pendingOpenViewKeys: new Set(),
122
143
  hasStepNavigated: false,
123
144
  forceStop: false,
145
+ viewGeneration: 0,
124
146
  };
125
147
 
126
148
  this.ensureRouteModelContext(runtime);
@@ -159,6 +181,7 @@ export class BaseLayoutRouteCoordinator {
159
181
  this.runtimes.forEach((runtime) => {
160
182
  this.setRuntimeActive(runtime, false);
161
183
  });
184
+ this.invalidatePendingRuntimeViewOpens();
162
185
  return;
163
186
  }
164
187
 
@@ -175,11 +198,16 @@ export class BaseLayoutRouteCoordinator {
175
198
  });
176
199
 
177
200
  if (!activePageUid || !pathname) {
201
+ this.invalidatePendingRuntimeViewOpens();
178
202
  return;
179
203
  }
180
204
 
181
205
  this.runtimes.forEach((runtime) => {
182
206
  if (runtime.pageUid !== activePageUid) {
207
+ if (!runtime.forceStop) {
208
+ runtime.viewGeneration += 1;
209
+ runtime.pendingOpenViewKeys.clear();
210
+ }
183
211
  runtime.forceStop = true;
184
212
  }
185
213
  });
@@ -190,6 +218,11 @@ export class BaseLayoutRouteCoordinator {
190
218
  }
191
219
 
192
220
  runtime.forceStop = false;
221
+ if (runtime.currentPathname && runtime.currentPathname !== pathname) {
222
+ runtime.viewGeneration += 1;
223
+ runtime.pendingOpenViewKeys.clear();
224
+ }
225
+ runtime.currentPathname = pathname;
193
226
  this.syncRuntimeWithPathname(runtime, pathname);
194
227
  }
195
228
 
@@ -200,6 +233,8 @@ export class BaseLayoutRouteCoordinator {
200
233
  }
201
234
 
202
235
  runtime.forceStop = true;
236
+ runtime.viewGeneration += 1;
237
+ runtime.pendingOpenViewKeys.clear();
203
238
  runtime.prevViewList.forEach((viewItem) => {
204
239
  this.flowEngine.removeModelWithSubModels(viewItem.params.viewUid);
205
240
  runtime.viewState[getKey(viewItem)]?.destroy?.();
@@ -207,6 +242,7 @@ export class BaseLayoutRouteCoordinator {
207
242
  });
208
243
  runtime.prevViewList = [];
209
244
  runtime.hasStepNavigated = false;
245
+ runtime.currentPathname = undefined;
210
246
  }
211
247
 
212
248
  destroy() {
@@ -249,7 +285,7 @@ export class BaseLayoutRouteCoordinator {
249
285
  viewState?.deactivate?.();
250
286
  }
251
287
 
252
- private syncRuntimeWithPathname(runtime: RoutePageRuntime, pathname: string) {
288
+ private syncRuntimeWithPathname(runtime: RoutePageRuntime, pathname: string, options: SyncRuntimeOptions = {}) {
253
289
  try {
254
290
  if (!this.basePathname) {
255
291
  return;
@@ -258,7 +294,7 @@ export class BaseLayoutRouteCoordinator {
258
294
  const viewStack = parsePathnameToViewParams(pathname, { basePath: this.basePathname });
259
295
  const viewList = resolveViewParamsToViewList(this.flowEngine, viewStack, runtime.routeModel);
260
296
 
261
- if (this.shouldStepNavigate(runtime, viewList)) {
297
+ if (!options.skipInitialStepNavigation && this.shouldStepNavigate(runtime, viewList)) {
262
298
  this.stepNavigate(viewList, 0);
263
299
  runtime.hasStepNavigated = true;
264
300
  this.scheduleInitialDeepLinkReplay(runtime, pathname);
@@ -266,20 +302,32 @@ export class BaseLayoutRouteCoordinator {
266
302
  }
267
303
 
268
304
  const { viewsToClose, viewsToOpen } = getViewDiffAndUpdateHidden(runtime.prevViewList, viewList);
305
+ const viewsToOpenKeys = new Set(viewsToOpen.map((viewItem) => getKey(viewItem)));
306
+ const nextViewsToOpen = viewList.filter((viewItem) => {
307
+ const key = getKey(viewItem);
308
+ if (runtime.pendingOpenViewKeys.has(key)) {
309
+ return false;
310
+ }
311
+ return viewsToOpenKeys.has(key) || !runtime.viewState[key];
312
+ });
269
313
 
270
314
  if (viewsToClose.length) {
271
315
  viewsToClose.forEach((viewItem) => {
316
+ runtime.pendingOpenViewKeys.delete(getKey(viewItem));
272
317
  runtime.viewState[getKey(viewItem)]?.destroy?.(true);
273
318
  delete runtime.viewState[getKey(viewItem)];
274
319
  });
275
320
  updateViewListHidden(viewList, !!this.layoutContext?.isMobileLayout);
276
321
  }
277
322
 
278
- if (viewsToOpen.length) {
279
- void this.handleOpenViews(runtime, viewList, viewsToOpen);
323
+ if (nextViewsToOpen.length) {
324
+ this.markPendingOpenViews(runtime, nextViewsToOpen);
325
+ this.handleOpenViews(runtime, viewList, nextViewsToOpen, runtime.viewGeneration).catch((error) => {
326
+ console.error(`[NocoBase] Failed to open route-managed views:`, error);
327
+ });
280
328
  }
281
329
 
282
- if (viewsToClose.length === 0 && viewsToOpen.length === 0) {
330
+ if (viewsToClose.length === 0 && nextViewsToOpen.length === 0) {
283
331
  const currentViewItem = viewList.at(-1);
284
332
  if (currentViewItem) {
285
333
  runtime.viewState[getKey(currentViewItem)]?.navigation.setViewStack(viewList.map((item) => item.params));
@@ -313,9 +361,15 @@ export class BaseLayoutRouteCoordinator {
313
361
  }
314
362
 
315
363
  private scheduleInitialDeepLinkReplay(runtime: RoutePageRuntime, pathname: string) {
364
+ const viewGeneration = runtime.viewGeneration;
316
365
  Promise.resolve()
317
366
  .then(() => {
318
- if (runtime.forceStop || runtime.prevViewList.length > 0) {
367
+ if (
368
+ runtime.forceStop ||
369
+ runtime.viewGeneration !== viewGeneration ||
370
+ runtime.currentPathname !== pathname ||
371
+ runtime.prevViewList.length > 0
372
+ ) {
319
373
  return;
320
374
  }
321
375
 
@@ -349,7 +403,60 @@ export class BaseLayoutRouteCoordinator {
349
403
  this.stepNavigate(viewList, index + 1);
350
404
  }
351
405
 
352
- private async handleOpenViews(runtime: RoutePageRuntime, viewList: ViewItem[], viewsToOpen: ViewItem[]) {
406
+ private replayActiveRuntimeViewsAfterLayoutContentElementChange() {
407
+ this.runtimes.forEach((runtime) => {
408
+ if (!runtime.currentPathname || runtime.prevViewList.length === 0) {
409
+ return;
410
+ }
411
+
412
+ this.resetRuntimeViewStateForLayoutContentElementChange(runtime);
413
+ if (runtime.meta.active) {
414
+ this.syncRuntimeWithPathname(runtime, runtime.currentPathname, { skipInitialStepNavigation: true });
415
+ }
416
+ });
417
+ }
418
+
419
+ private invalidatePendingRuntimeViewOpens() {
420
+ this.runtimes.forEach((runtime) => {
421
+ runtime.forceStop = true;
422
+ runtime.viewGeneration += 1;
423
+ runtime.pendingOpenViewKeys.clear();
424
+ });
425
+ }
426
+
427
+ private resetRuntimeViewStateForLayoutContentElementChange(runtime: RoutePageRuntime) {
428
+ runtime.viewGeneration += 1;
429
+ runtime.pendingOpenViewKeys.clear();
430
+ runtime.prevViewList.forEach((viewItem, index) => {
431
+ runtime.viewState[getKey(viewItem)]?.destroy?.(true);
432
+ delete runtime.viewState[getKey(viewItem)];
433
+
434
+ if (index > 0) {
435
+ this.flowEngine.removeModelWithSubModels(viewItem.params.viewUid);
436
+ }
437
+ });
438
+ runtime.prevViewList = [];
439
+ runtime.hasStepNavigated = false;
440
+ }
441
+
442
+ private markPendingOpenViews(runtime: RoutePageRuntime, viewItems: ViewItem[]) {
443
+ viewItems.forEach((viewItem) => {
444
+ runtime.pendingOpenViewKeys.add(getKey(viewItem));
445
+ });
446
+ }
447
+
448
+ private clearPendingOpenViews(runtime: RoutePageRuntime, viewItems: ViewItem[]) {
449
+ viewItems.forEach((viewItem) => {
450
+ runtime.pendingOpenViewKeys.delete(getKey(viewItem));
451
+ });
452
+ }
453
+
454
+ private async handleOpenViews(
455
+ runtime: RoutePageRuntime,
456
+ viewList: ViewItem[],
457
+ viewsToOpen: ViewItem[],
458
+ viewGeneration: number,
459
+ ) {
353
460
  const missingModels = viewsToOpen.filter((v) => !v.model);
354
461
  if (missingModels.length > 0) {
355
462
  await Promise.all(
@@ -363,24 +470,36 @@ export class BaseLayoutRouteCoordinator {
363
470
  );
364
471
  }
365
472
 
366
- if (runtime.forceStop) {
473
+ if (runtime.forceStop || runtime.viewGeneration !== viewGeneration) {
367
474
  return;
368
475
  }
369
476
 
370
477
  this.syncViewListVisibility(runtime, viewList);
371
- this.openViews(runtime, viewList, viewsToOpen, 0);
478
+ this.openViews(runtime, viewList, viewsToOpen, 0, viewGeneration);
372
479
  }
373
480
 
374
- private openViews(runtime: RoutePageRuntime, viewList: ViewItem[], viewsToOpen: ViewItem[], index: number) {
481
+ private openViews(
482
+ runtime: RoutePageRuntime,
483
+ viewList: ViewItem[],
484
+ viewsToOpen: ViewItem[],
485
+ index: number,
486
+ viewGeneration: number,
487
+ ) {
488
+ if (runtime.forceStop || runtime.viewGeneration !== viewGeneration) {
489
+ return;
490
+ }
491
+
375
492
  if (!viewsToOpen[index]) {
376
493
  return;
377
494
  }
378
495
 
379
496
  const viewItem = viewsToOpen[index];
380
497
  if (!viewItem.model) {
498
+ this.clearPendingOpenViews(runtime, viewsToOpen.slice(index));
381
499
  return;
382
500
  }
383
501
 
502
+ const viewKey = getKey(viewItem);
384
503
  const destroyRef = React.createRef<(result?: any, force?: boolean) => void>();
385
504
  const updateRef = React.createRef<(value: any) => void>();
386
505
  const activateRef = React.createRef<(forceRefresh?: boolean) => void>();
@@ -398,37 +517,44 @@ export class BaseLayoutRouteCoordinator {
398
517
  { basePath: this.basePathname },
399
518
  );
400
519
 
401
- viewItem.model.dispatchEvent('click', {
402
- target: this.layoutContentElement || runtime.meta.layoutContentElement,
403
- collectionName: openViewParams?.collectionName,
404
- associationName,
405
- dataSourceKey: openViewParams?.dataSourceKey,
406
- destroyRef,
407
- updateRef,
408
- activateRef,
409
- deactivateRef,
410
- openerUids,
411
- ...viewItem.params,
412
- ...(openViewRouteState?.mode ? { mode: openViewRouteState.mode } : {}),
413
- ...(openViewRouteState?.size ? { size: openViewRouteState.size } : {}),
414
- pageActive: runtime.meta.active,
415
- activationControlledByLayout: true,
416
- navigation,
417
- onOpen: () => {
418
- this.openViews(runtime, viewList, viewsToOpen, index + 1);
419
- },
420
- hidden: viewItem.hidden,
421
- isMobileLayout: !!this.layoutContext?.isMobileLayout,
422
- triggerByRouter: true,
423
- });
520
+ try {
521
+ viewItem.model.dispatchEvent('click', {
522
+ target: this.layoutContentElement || runtime.meta.layoutContentElement,
523
+ collectionName: openViewParams?.collectionName,
524
+ associationName,
525
+ dataSourceKey: openViewParams?.dataSourceKey,
526
+ destroyRef,
527
+ updateRef,
528
+ activateRef,
529
+ deactivateRef,
530
+ openerUids,
531
+ ...viewItem.params,
532
+ ...(openViewRouteState?.mode ? { mode: openViewRouteState.mode } : {}),
533
+ ...(openViewRouteState?.size ? { size: openViewRouteState.size } : {}),
534
+ pageActive: runtime.meta.active,
535
+ activationControlledByLayout: true,
536
+ navigation,
537
+ onOpen: () => {
538
+ this.openViews(runtime, viewList, viewsToOpen, index + 1, viewGeneration);
539
+ },
540
+ hidden: viewItem.hidden,
541
+ isMobileLayout: !!this.layoutContext?.isMobileLayout,
542
+ triggerByRouter: true,
543
+ });
544
+ } catch (error) {
545
+ this.clearPendingOpenViews(runtime, viewsToOpen.slice(index));
546
+ console.error(`[NocoBase] Failed to dispatch route-managed view open:`, error);
547
+ return;
548
+ }
424
549
 
425
- runtime.viewState[getKey(viewItem)] = {
550
+ runtime.viewState[viewKey] = {
426
551
  destroy: (_force?: boolean) => destroyRef.current?.(),
427
552
  update: (value: any) => updateRef.current?.(value),
428
553
  activate: (forceRefresh?: boolean) => activateRef.current?.(forceRefresh),
429
554
  deactivate: () => deactivateRef.current?.(),
430
555
  navigation,
431
556
  };
557
+ runtime.pendingOpenViewKeys.delete(viewKey);
432
558
  }
433
559
 
434
560
  private ensureRouteModelContext(runtime: RoutePageRuntime) {