@nocobase/client-v2 2.2.0-alpha.3 → 2.2.0-alpha.5

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 (81) 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/components/form/TypedVariableInput.d.ts +9 -1
  6. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  7. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  8. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  9. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  10. package/es/entry-actions/index.d.ts +9 -0
  11. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  12. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  13. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  14. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  15. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  16. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  17. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  18. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  19. package/es/index.d.ts +6 -0
  20. package/es/index.mjs +229 -143
  21. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  22. package/es/utils/index.d.ts +1 -0
  23. package/es/utils/markdownSanitize.d.ts +11 -0
  24. package/lib/index.js +219 -133
  25. package/package.json +7 -7
  26. package/src/BaseApplication.tsx +2 -0
  27. package/src/RouteRepository.ts +25 -0
  28. package/src/RouterManager.tsx +142 -1
  29. package/src/__tests__/RouteRepository.test.ts +23 -0
  30. package/src/__tests__/RouterManager.test.ts +125 -0
  31. package/src/__tests__/authRedirect.test.ts +17 -0
  32. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  33. package/src/authRedirect.ts +38 -0
  34. package/src/components/form/JsonTextArea.tsx +4 -0
  35. package/src/components/form/TypedVariableInput.tsx +58 -34
  36. package/src/components/form/__tests__/TypedVariableInput.test.tsx +52 -0
  37. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  38. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  39. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  40. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  41. package/src/entry-actions/EntryActionManager.ts +76 -0
  42. package/src/entry-actions/index.ts +10 -0
  43. package/src/flow/FlowPage.tsx +29 -2
  44. package/src/flow/__tests__/FlowPage.test.tsx +152 -25
  45. package/src/flow/__tests__/FlowRoute.test.tsx +547 -2
  46. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
  47. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
  48. package/src/flow/actions/dataScopeFilter.ts +10 -1
  49. package/src/flow/actions/dateTimeFormat.tsx +2 -2
  50. package/src/flow/actions/openView.tsx +21 -1
  51. package/src/flow/admin-shell/BaseLayoutModel.tsx +124 -0
  52. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
  53. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
  54. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -4
  55. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +177 -1
  56. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +20 -0
  57. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  58. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  59. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +436 -2
  60. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
  61. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  62. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  63. package/src/flow/common/Markdown/Display.tsx +14 -3
  64. package/src/flow/common/Markdown/Edit.tsx +19 -7
  65. package/src/flow/components/FlowRoute.tsx +128 -16
  66. package/src/flow/internal/components/Markdown/util.ts +2 -1
  67. package/src/flow/models/base/ActionModel.tsx +10 -8
  68. package/src/flow/models/base/ActionModelCore.tsx +5 -0
  69. package/src/flow/models/blocks/table/TableColumnModel.tsx +6 -2
  70. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +51 -0
  71. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +45 -13
  72. package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
  73. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
  74. package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
  75. package/src/flow/routeTransientInputArgs.ts +27 -0
  76. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
  77. package/src/index.ts +6 -0
  78. package/src/nocobase-buildin-plugin/index.tsx +7 -1
  79. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  80. package/src/utils/index.tsx +1 -0
  81. 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;
@@ -28,11 +28,11 @@ const isTableColumnFieldSubModel = (model) => {
28
28
 
29
29
  const syncTableColumnDateTimeFormatProps = (ctx, props) => {
30
30
  const model = ctx.model;
31
- if (!isTableColumnFieldSubModel(model) || !model?.parent?.collectionField?.isAssociationField?.()) {
31
+ if (!isTableColumnFieldSubModel(model)) {
32
32
  return;
33
33
  }
34
34
 
35
- model.parent.setProps(props);
35
+ model.parent?.setProps?.(props);
36
36
  };
37
37
 
38
38
  export const dateTimeFormat = defineAction({
@@ -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
  }
@@ -20,6 +20,7 @@ import {
20
20
  type BaseLayoutRouteCoordinatorOptions,
21
21
  type RoutePageMeta,
22
22
  } from './BaseLayoutRouteCoordinator';
23
+ import { NocoBaseDesktopRouteType } from '../../flow-compat';
23
24
  import type { LayoutDefinition } from '../../layout-manager/types';
24
25
  import { isLayoutContentRouteName } from '../../layout-manager/utils';
25
26
 
@@ -72,6 +73,7 @@ export interface LayoutRouteLike {
72
73
  id?: string;
73
74
  name?: string;
74
75
  pathname?: string;
76
+ state?: unknown;
75
77
  params?: Record<string, string | undefined>;
76
78
  layoutRouteName?: string;
77
79
  layoutBasePathname?: string;
@@ -111,6 +113,15 @@ const getDefaultBasePathnameFromRoutePath = (routePath?: string) => {
111
113
 
112
114
  const isKnownViewParamName = (segment: string) => ['tab', 'filterbytk', 'sourceid'].includes(segment);
113
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
+
114
125
  const isStandardLayoutRelativePath = (relativePath: string) => {
115
126
  if (!relativePath) {
116
127
  return true;
@@ -170,6 +181,7 @@ export class BaseLayoutModel<
170
181
  currentLayoutRoute: LayoutRouteMatch | null = null;
171
182
  protected routeCoordinator?: BaseLayoutRouteCoordinator;
172
183
  private activePageUid = '';
184
+ private currentRouteState?: unknown;
173
185
  private layoutContentElement: HTMLElement | null = null;
174
186
  private readonly routePageMetaMap = new Map<string, RoutePageMeta>();
175
187
  private contextBindingsActive = false;
@@ -184,6 +196,7 @@ export class BaseLayoutModel<
184
196
 
185
197
  registerRoutePage(pageUid: string, meta: RoutePageMeta) {
186
198
  this.routePageMetaMap.set(pageUid, meta);
199
+ this.restoreCurrentLayoutRouteFromRouterContext(pageUid);
187
200
  const routeModel = this.getCoordinator().registerPage(pageUid, meta);
188
201
  this.getCoordinator().syncRoute(this.getCurrentCoordinatorRouteLike());
189
202
  return routeModel;
@@ -322,6 +335,18 @@ export class BaseLayoutModel<
322
335
  };
323
336
  }
324
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
+
325
350
  return {
326
351
  type: 'page',
327
352
  pathname,
@@ -345,6 +370,7 @@ export class BaseLayoutModel<
345
370
 
346
371
  const layoutRoute = this.resolveLayoutRoute(routeLike);
347
372
  this.currentLayoutRoute = layoutRoute;
373
+ this.currentRouteState = routeLike.state;
348
374
  this.activePageUid = this.getPageUidFromLayoutRoute(layoutRoute);
349
375
  this.getCoordinator().syncRoute({
350
376
  ...routeLike,
@@ -364,7 +390,12 @@ export class BaseLayoutModel<
364
390
  return;
365
391
  }
366
392
 
393
+ if (this.shouldIgnoreStaleLayoutRouteCleanup(routeLike)) {
394
+ return;
395
+ }
396
+
367
397
  this.currentLayoutRoute = null;
398
+ this.currentRouteState = undefined;
368
399
  this.activePageUid = '';
369
400
  this.routeCoordinator?.syncRoute({});
370
401
  }
@@ -434,12 +465,105 @@ export class BaseLayoutModel<
434
465
  layoutRouteName: this.layout.routeName,
435
466
  pageUid: this.currentLayoutRoute.pageUid,
436
467
  pathname: this.currentLayoutRoute.pathname,
468
+ state: this.currentRouteState,
437
469
  layoutBasePathname: this.currentLayoutRoute.basePathname,
438
470
  layoutRoute: this.currentLayoutRoute,
439
471
  };
440
472
  }
441
473
  return {};
442
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
+ }
443
567
  }
444
568
 
445
569
  /**