@nocobase/client-v2 2.2.0-alpha.4 → 2.2.0-alpha.6

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 (52) hide show
  1. package/es/BaseApplication.d.ts +2 -0
  2. package/es/RouterManager.d.ts +15 -0
  3. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  4. package/es/entry-actions/index.d.ts +9 -0
  5. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  6. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  7. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  8. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  9. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  10. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  11. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  12. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  13. package/es/index.d.ts +4 -0
  14. package/es/index.mjs +178 -124
  15. package/es/utils/markdownSanitize.d.ts +11 -0
  16. package/lib/index.js +164 -110
  17. package/package.json +7 -7
  18. package/src/BaseApplication.tsx +2 -0
  19. package/src/RouterManager.tsx +142 -1
  20. package/src/__tests__/RouterManager.test.ts +125 -0
  21. package/src/entry-actions/EntryActionManager.ts +76 -0
  22. package/src/entry-actions/index.ts +10 -0
  23. package/src/flow/FlowPage.tsx +29 -2
  24. package/src/flow/__tests__/FlowPage.test.tsx +152 -25
  25. package/src/flow/__tests__/FlowRoute.test.tsx +547 -2
  26. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
  27. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
  28. package/src/flow/actions/dataScopeFilter.ts +10 -1
  29. package/src/flow/actions/openView.tsx +21 -1
  30. package/src/flow/admin-shell/BaseLayoutModel.tsx +111 -0
  31. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
  32. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
  33. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -4
  34. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  35. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  36. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +359 -2
  37. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
  38. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  39. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  40. package/src/flow/common/Markdown/Display.tsx +14 -3
  41. package/src/flow/common/Markdown/Edit.tsx +19 -7
  42. package/src/flow/components/FlowRoute.tsx +128 -16
  43. package/src/flow/internal/components/Markdown/util.ts +2 -1
  44. package/src/flow/models/base/ActionModel.tsx +10 -8
  45. package/src/flow/models/base/ActionModelCore.tsx +5 -0
  46. package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
  47. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
  48. package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
  49. package/src/flow/routeTransientInputArgs.ts +27 -0
  50. package/src/index.ts +4 -0
  51. package/src/nocobase-buildin-plugin/index.tsx +7 -1
  52. package/src/utils/markdownSanitize.ts +88 -0
@@ -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
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));
@@ -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>();
@@ -392,43 +525,52 @@ export class BaseLayoutRouteCoordinator {
392
525
  : openViewParams?.associationName;
393
526
  const openViewRouteState = viewItem.params.openViewRouteState;
394
527
  const openerUids = viewList.slice(0, viewItem.index).map((item) => item.params.viewUid);
528
+ const transientInputArgs = getRouteTransientInputArgs(routeState, viewItem.params.viewUid);
395
529
  const navigation = new ViewNavigation(
396
530
  this.flowEngine.context,
397
531
  viewList.slice(0, viewItem.index + 1).map((item) => item.params),
398
532
  { basePath: this.basePathname },
399
533
  );
400
534
 
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
- });
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
+ }
424
565
 
425
- runtime.viewState[getKey(viewItem)] = {
566
+ runtime.viewState[viewKey] = {
426
567
  destroy: (_force?: boolean) => destroyRef.current?.(),
427
568
  update: (value: any) => updateRef.current?.(value),
428
569
  activate: (forceRefresh?: boolean) => activateRef.current?.(forceRefresh),
429
570
  deactivate: () => deactivateRef.current?.(),
430
571
  navigation,
431
572
  };
573
+ runtime.pendingOpenViewKeys.delete(viewKey);
432
574
  }
433
575
 
434
576
  private ensureRouteModelContext(runtime: RoutePageRuntime) {