@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
@@ -12,6 +12,7 @@ import { describe, it, expect, vi } from 'vitest';
12
12
  import { RUNJS_OPEN_VIEW_ROUTE_STATE } from '@nocobase/flow-engine';
13
13
  import { openView } from '../openView';
14
14
  import { FlowPage } from '../../FlowPage';
15
+ import { ROUTE_TRANSIENT_INPUT_ARGS_KEY } from '../../routeTransientInputArgs';
15
16
 
16
17
  describe('openView action - route mode defineProperties/defineMethods', () => {
17
18
  const createRouteManagedCtx = () => {
@@ -172,6 +173,56 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
172
173
  expect(defineMethodCalls).toContain('pong');
173
174
  });
174
175
 
176
+ it('passes formData through route navigation state', async () => {
177
+ const navigateTo = vi.fn();
178
+ const ctx: any = {
179
+ inputArgs: {
180
+ formData: {
181
+ start: '2026-06-24 08:00:00',
182
+ end: '2026-06-24 09:00:00',
183
+ },
184
+ },
185
+ engine: {
186
+ context: {},
187
+ },
188
+ model: {
189
+ uid: 'popup-uid',
190
+ context: {
191
+ defineProperty: vi.fn(),
192
+ },
193
+ },
194
+ view: {
195
+ navigation: {
196
+ navigateTo,
197
+ },
198
+ },
199
+ isNavigationEnabled: true,
200
+ };
201
+
202
+ await openView.handler(ctx, { navigation: true });
203
+
204
+ expect(navigateTo).toHaveBeenCalledWith(
205
+ {
206
+ viewUid: 'popup-uid',
207
+ filterByTk: undefined,
208
+ sourceId: undefined,
209
+ tabUid: undefined,
210
+ },
211
+ {
212
+ state: {
213
+ [ROUTE_TRANSIENT_INPUT_ARGS_KEY]: {
214
+ 'popup-uid': {
215
+ formData: {
216
+ start: '2026-06-24 08:00:00',
217
+ end: '2026-06-24 09:00:00',
218
+ },
219
+ },
220
+ },
221
+ },
222
+ },
223
+ );
224
+ });
225
+
175
226
  it('adds route state to first-stage navigation only for RunJS ctx.openView calls', async () => {
176
227
  const { ctx, navigateTo } = createFirstStageCtx({
177
228
  mode: 'dialog',
@@ -181,13 +232,16 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
181
232
 
182
233
  await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
183
234
 
184
- expect(navigateTo).toHaveBeenCalledWith({
185
- viewUid: 'popup-uid',
186
- filterByTk: undefined,
187
- sourceId: undefined,
188
- tabUid: undefined,
189
- openViewRouteState: { mode: 'dialog', size: 'large' },
190
- });
235
+ expect(navigateTo).toHaveBeenCalledWith(
236
+ {
237
+ viewUid: 'popup-uid',
238
+ filterByTk: undefined,
239
+ sourceId: undefined,
240
+ tabUid: undefined,
241
+ openViewRouteState: { mode: 'dialog', size: 'large' },
242
+ },
243
+ undefined,
244
+ );
191
245
  });
192
246
 
193
247
  it('normalizes first-stage RunJS route state mode on mobile layout', async () => {
@@ -200,13 +254,16 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
200
254
 
201
255
  await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
202
256
 
203
- expect(navigateTo).toHaveBeenCalledWith({
204
- viewUid: 'popup-uid',
205
- filterByTk: undefined,
206
- sourceId: undefined,
207
- tabUid: undefined,
208
- openViewRouteState: { mode: 'embed', size: 'large' },
209
- });
257
+ expect(navigateTo).toHaveBeenCalledWith(
258
+ {
259
+ viewUid: 'popup-uid',
260
+ filterByTk: undefined,
261
+ sourceId: undefined,
262
+ tabUid: undefined,
263
+ openViewRouteState: { mode: 'embed', size: 'large' },
264
+ },
265
+ undefined,
266
+ );
210
267
  });
211
268
 
212
269
  it('uses route replay mode and size before persisted openView defaults', async () => {
@@ -246,11 +303,14 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
246
303
 
247
304
  await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
248
305
 
249
- expect(navigateTo).toHaveBeenCalledWith({
250
- viewUid: 'popup-uid',
251
- filterByTk: undefined,
252
- sourceId: undefined,
253
- tabUid: undefined,
254
- });
306
+ expect(navigateTo).toHaveBeenCalledWith(
307
+ {
308
+ viewUid: 'popup-uid',
309
+ filterByTk: undefined,
310
+ sourceId: undefined,
311
+ tabUid: undefined,
312
+ },
313
+ undefined,
314
+ );
255
315
  });
256
316
  });
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { isVariableExpression, pruneFilter } from '@nocobase/flow-engine';
10
+ import { extractPropertyPath, isVariableExpression, pruneFilter } from '@nocobase/flow-engine';
11
11
  import { transformFilter } from '@nocobase/utils/client';
12
12
  import _ from 'lodash';
13
13
 
@@ -45,6 +45,14 @@ function restorePreservedNull(value: any): any {
45
45
  return value;
46
46
  }
47
47
 
48
+ function isUrlSearchParamsExpression(value: any) {
49
+ if (!isVariableExpression(value)) {
50
+ return false;
51
+ }
52
+
53
+ return extractPropertyPath(value)?.[0] === 'urlSearchParams';
54
+ }
55
+
48
56
  function markEmptyVariableValues(rawNode: any, resolvedNode: any) {
49
57
  if (!rawNode || !resolvedNode || typeof rawNode !== 'object' || typeof resolvedNode !== 'object') {
50
58
  return;
@@ -57,6 +65,7 @@ function markEmptyVariableValues(rawNode: any, resolvedNode: any) {
57
65
  }
58
66
  if (
59
67
  isVariableExpression(rawNode.value) &&
68
+ !isUrlSearchParamsExpression(rawNode.value) &&
60
69
  (resolvedNode.value === undefined || resolvedNode.value === null || resolvedNode.value === '')
61
70
  ) {
62
71
  resolvedNode.value = PRESERVE_NULL;
@@ -20,6 +20,7 @@ import React from 'react';
20
20
  import { FlowPage } from '../FlowPage';
21
21
  import { PageModel, RootPageModel } from '../models';
22
22
  import _ from 'lodash';
23
+ import { ROUTE_TRANSIENT_INPUT_ARGS_KEY } from '../routeTransientInputArgs';
23
24
 
24
25
  type DirtyAwareFlowModel = FlowModel & {
25
26
  getUserModifiedFields?: () => Set<string> | undefined;
@@ -31,6 +32,15 @@ type BeforeCloseDirtyState = {
31
32
  formModelUids: string[];
32
33
  };
33
34
 
35
+ const pickRouteTransientInputArgs = (inputArgs: Record<string, unknown>) => {
36
+ return _.pickBy(
37
+ {
38
+ formData: inputArgs.formData,
39
+ },
40
+ (value) => typeof value !== 'undefined',
41
+ );
42
+ };
43
+
34
44
  function collectDirtyFormModelUids(model?: FlowModel | null, ignoredDirtyFormModelUids: string[] = []): string[] {
35
45
  if (!model) {
36
46
  return [];
@@ -363,7 +373,17 @@ export const openView = defineAction({
363
373
  tabUid: mergedTabUid,
364
374
  ...(effectiveRunJSOpenViewRouteState ? { openViewRouteState: effectiveRunJSOpenViewRouteState } : {}),
365
375
  };
366
- ctx.view.navigation.navigateTo(nextView);
376
+ const transientInputArgs = pickRouteTransientInputArgs(inputArgs);
377
+ const navigationOptions = Object.keys(transientInputArgs).length
378
+ ? {
379
+ state: {
380
+ [ROUTE_TRANSIENT_INPUT_ARGS_KEY]: {
381
+ [nextView.viewUid]: transientInputArgs,
382
+ },
383
+ },
384
+ }
385
+ : undefined;
386
+ ctx.view.navigation.navigateTo(nextView, navigationOptions);
367
387
  return;
368
388
  }
369
389
  }
@@ -73,6 +73,7 @@ export interface LayoutRouteLike {
73
73
  id?: string;
74
74
  name?: string;
75
75
  pathname?: string;
76
+ state?: unknown;
76
77
  params?: Record<string, string | undefined>;
77
78
  layoutRouteName?: string;
78
79
  layoutBasePathname?: string;
@@ -112,6 +113,15 @@ const getDefaultBasePathnameFromRoutePath = (routePath?: string) => {
112
113
 
113
114
  const isKnownViewParamName = (segment: string) => ['tab', 'filterbytk', 'sourceid'].includes(segment);
114
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
+
115
125
  const isStandardLayoutRelativePath = (relativePath: string) => {
116
126
  if (!relativePath) {
117
127
  return true;
@@ -171,6 +181,7 @@ export class BaseLayoutModel<
171
181
  currentLayoutRoute: LayoutRouteMatch | null = null;
172
182
  protected routeCoordinator?: BaseLayoutRouteCoordinator;
173
183
  private activePageUid = '';
184
+ private currentRouteState?: unknown;
174
185
  private layoutContentElement: HTMLElement | null = null;
175
186
  private readonly routePageMetaMap = new Map<string, RoutePageMeta>();
176
187
  private contextBindingsActive = false;
@@ -185,6 +196,7 @@ export class BaseLayoutModel<
185
196
 
186
197
  registerRoutePage(pageUid: string, meta: RoutePageMeta) {
187
198
  this.routePageMetaMap.set(pageUid, meta);
199
+ this.restoreCurrentLayoutRouteFromRouterContext(pageUid);
188
200
  const routeModel = this.getCoordinator().registerPage(pageUid, meta);
189
201
  this.getCoordinator().syncRoute(this.getCurrentCoordinatorRouteLike());
190
202
  return routeModel;
@@ -358,6 +370,7 @@ export class BaseLayoutModel<
358
370
 
359
371
  const layoutRoute = this.resolveLayoutRoute(routeLike);
360
372
  this.currentLayoutRoute = layoutRoute;
373
+ this.currentRouteState = routeLike.state;
361
374
  this.activePageUid = this.getPageUidFromLayoutRoute(layoutRoute);
362
375
  this.getCoordinator().syncRoute({
363
376
  ...routeLike,
@@ -377,7 +390,12 @@ export class BaseLayoutModel<
377
390
  return;
378
391
  }
379
392
 
393
+ if (this.shouldIgnoreStaleLayoutRouteCleanup(routeLike)) {
394
+ return;
395
+ }
396
+
380
397
  this.currentLayoutRoute = null;
398
+ this.currentRouteState = undefined;
381
399
  this.activePageUid = '';
382
400
  this.routeCoordinator?.syncRoute({});
383
401
  }
@@ -447,12 +465,105 @@ export class BaseLayoutModel<
447
465
  layoutRouteName: this.layout.routeName,
448
466
  pageUid: this.currentLayoutRoute.pageUid,
449
467
  pathname: this.currentLayoutRoute.pathname,
468
+ state: this.currentRouteState,
450
469
  layoutBasePathname: this.currentLayoutRoute.basePathname,
451
470
  layoutRoute: this.currentLayoutRoute,
452
471
  };
453
472
  }
454
473
  return {};
455
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
+ }
456
567
  }
457
568
 
458
569
  /**