@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.
- package/es/flow/admin-shell/BaseLayoutModel.d.ts +4 -0
- package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +6 -0
- package/es/index.mjs +91 -91
- package/lib/index.js +118 -118
- package/package.json +7 -7
- package/src/flow/FlowPage.tsx +29 -2
- package/src/flow/__tests__/FlowPage.test.tsx +152 -25
- package/src/flow/admin-shell/BaseLayoutModel.tsx +106 -0
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +160 -34
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +896 -119
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +29 -2
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +311 -2
|
@@ -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 (
|
|
279
|
-
|
|
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 &&
|
|
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 (
|
|
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
|
|
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(
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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[
|
|
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) {
|