@nocobase/client-v2 2.1.18 → 2.1.20

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 (30) hide show
  1. package/es/BaseApplication.d.ts +1 -0
  2. package/es/flow/admin-shell/BaseLayoutModel.d.ts +2 -0
  3. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +1 -0
  4. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +0 -1
  5. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  6. package/es/index.d.ts +1 -0
  7. package/es/index.mjs +93 -93
  8. package/es/utils/markdownSanitize.d.ts +11 -0
  9. package/lib/index.js +114 -114
  10. package/package.json +7 -7
  11. package/src/BaseApplication.tsx +4 -0
  12. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
  13. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
  14. package/src/flow/actions/dataScopeFilter.ts +10 -1
  15. package/src/flow/actions/openView.tsx +21 -1
  16. package/src/flow/admin-shell/BaseLayoutModel.tsx +5 -0
  17. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +28 -12
  18. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +120 -0
  19. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +48 -0
  20. package/src/flow/common/Markdown/Display.tsx +14 -3
  21. package/src/flow/common/Markdown/Edit.tsx +19 -7
  22. package/src/flow/internal/components/Markdown/util.ts +2 -1
  23. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +13 -18
  24. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +30 -1
  25. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +1 -0
  26. package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
  27. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
  28. package/src/flow/routeTransientInputArgs.ts +27 -0
  29. package/src/index.ts +1 -0
  30. package/src/utils/markdownSanitize.ts +88 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.1.18",
3
+ "version": "2.1.20",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -27,11 +27,11 @@
27
27
  "@formily/antd-v5": "1.2.3",
28
28
  "@formily/react": "^2.2.27",
29
29
  "@formily/shared": "^2.2.27",
30
- "@nocobase/evaluators": "2.1.18",
31
- "@nocobase/flow-engine": "2.1.18",
32
- "@nocobase/sdk": "2.1.18",
33
- "@nocobase/shared": "2.1.18",
34
- "@nocobase/utils": "2.1.18",
30
+ "@nocobase/evaluators": "2.1.20",
31
+ "@nocobase/flow-engine": "2.1.20",
32
+ "@nocobase/sdk": "2.1.20",
33
+ "@nocobase/shared": "2.1.20",
34
+ "@nocobase/utils": "2.1.20",
35
35
  "ahooks": "^3.7.2",
36
36
  "antd": "5.24.2",
37
37
  "antd-style": "3.7.1",
@@ -46,5 +46,5 @@
46
46
  "react-i18next": "^11.15.1",
47
47
  "react-router-dom": "^6.30.1"
48
48
  },
49
- "gitHead": "2234b42b21c96d22a4b65a1f16a65ae61f434779"
49
+ "gitHead": "ca646ba28bb11da083e3436866552932ed780b6f"
50
50
  }
@@ -431,6 +431,10 @@ export abstract class BaseApplication<
431
431
  return getSubAppName(this.getPublicPath()) || null;
432
432
  }
433
433
 
434
+ getCdnUrl() {
435
+ return window['__webpack_public_path__'] || this.getPublicPath();
436
+ }
437
+
434
438
  getPublicPath() {
435
439
  let publicPath = this.options.publicPath || '/';
436
440
  if (!publicPath.endsWith('/')) {
@@ -64,6 +64,40 @@ describe('normalizeDataScopeFilter', () => {
64
64
  });
65
65
  });
66
66
 
67
+ it('prunes a missing URL search param variable', () => {
68
+ const rawFilter = {
69
+ logic: '$and',
70
+ items: [{ path: 'departmentId', operator: '$eq', value: '{{ ctx.urlSearchParams.departmentId }}' }],
71
+ };
72
+ const resolvedFilter = {
73
+ logic: '$and',
74
+ items: [{ path: 'departmentId', operator: '$eq', value: undefined }],
75
+ };
76
+
77
+ expect(normalizeDataScopeFilter(rawFilter, resolvedFilter)).toBeUndefined();
78
+ });
79
+
80
+ it('only prunes the missing URL search param condition from mixed filters', () => {
81
+ const rawFilter = {
82
+ logic: '$and',
83
+ items: [
84
+ { path: 'status', operator: '$eq', value: 'active' },
85
+ { path: 'departmentId', operator: '$eq', value: '{{ ctx.urlSearchParams.departmentId }}' },
86
+ ],
87
+ };
88
+ const resolvedFilter = {
89
+ logic: '$and',
90
+ items: [
91
+ { path: 'status', operator: '$eq', value: 'active' },
92
+ { path: 'departmentId', operator: '$eq', value: undefined },
93
+ ],
94
+ };
95
+
96
+ expect(normalizeDataScopeFilter(rawFilter, resolvedFilter)).toEqual({
97
+ $and: [{ status: { $eq: 'active' } }],
98
+ });
99
+ });
100
+
67
101
  it('still prunes empty constant values', () => {
68
102
  const filter = {
69
103
  logic: '$and',
@@ -139,6 +173,34 @@ describe('normalizeDataScopeFilter', () => {
139
173
  expect(resource.removeFilterGroup).not.toHaveBeenCalled();
140
174
  });
141
175
 
176
+ it('dataScope handler removes data scope when a URL search param is missing', async () => {
177
+ const resource = {
178
+ addFilterGroup: vi.fn(),
179
+ removeFilterGroup: vi.fn(),
180
+ };
181
+ const ctx = {
182
+ model: {
183
+ uid: 'field-1',
184
+ resource,
185
+ },
186
+ resolveJsonTemplate: vi.fn(async (template) => ({
187
+ ...template,
188
+ items: [{ ...template.items[0], value: undefined }],
189
+ })),
190
+ };
191
+ const params = {
192
+ filter: {
193
+ logic: '$and',
194
+ items: [{ path: 'departmentId', operator: '$eq', value: '{{ ctx.urlSearchParams.departmentId }}' }],
195
+ },
196
+ };
197
+
198
+ await (dataScope as any).handler(ctx, params);
199
+
200
+ expect(resource.removeFilterGroup).toHaveBeenCalledWith('field-1');
201
+ expect(resource.addFilterGroup).not.toHaveBeenCalled();
202
+ });
203
+
142
204
  it('dataScope handler preserves current role as server-side variable', async () => {
143
205
  const engine = new FlowEngine();
144
206
  const resource = {
@@ -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
  }
@@ -72,6 +72,7 @@ export interface LayoutRouteLike {
72
72
  id?: string;
73
73
  name?: string;
74
74
  pathname?: string;
75
+ state?: unknown;
75
76
  params?: Record<string, string | undefined>;
76
77
  layoutRouteName?: string;
77
78
  layoutBasePathname?: string;
@@ -179,6 +180,7 @@ export class BaseLayoutModel<
179
180
  currentLayoutRoute: LayoutRouteMatch | null = null;
180
181
  protected routeCoordinator?: BaseLayoutRouteCoordinator;
181
182
  private activePageUid = '';
183
+ private currentRouteState?: unknown;
182
184
  private layoutContentElement: HTMLElement | null = null;
183
185
  private readonly routePageMetaMap = new Map<string, RoutePageMeta>();
184
186
  private contextBindingsActive = false;
@@ -355,6 +357,7 @@ export class BaseLayoutModel<
355
357
 
356
358
  const layoutRoute = this.resolveLayoutRoute(routeLike);
357
359
  this.currentLayoutRoute = layoutRoute;
360
+ this.currentRouteState = routeLike.state;
358
361
  this.activePageUid = this.getPageUidFromLayoutRoute(layoutRoute);
359
362
  this.getCoordinator().syncRoute({
360
363
  ...routeLike,
@@ -379,6 +382,7 @@ export class BaseLayoutModel<
379
382
  }
380
383
 
381
384
  this.currentLayoutRoute = null;
385
+ this.currentRouteState = undefined;
382
386
  this.activePageUid = '';
383
387
  this.routeCoordinator?.syncRoute({});
384
388
  }
@@ -448,6 +452,7 @@ export class BaseLayoutModel<
448
452
  layoutRouteName: this.layout.routeName,
449
453
  pageUid: this.currentLayoutRoute.pageUid,
450
454
  pathname: this.currentLayoutRoute.pathname,
455
+ state: this.currentRouteState,
451
456
  layoutBasePathname: this.currentLayoutRoute.basePathname,
452
457
  layoutRoute: this.currentLayoutRoute,
453
458
  };
@@ -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;
@@ -52,6 +53,7 @@ interface RoutePageRuntime {
52
53
  pendingOpenViewKeys: Set<string>;
53
54
  hasStepNavigated: boolean;
54
55
  currentPathname?: string;
56
+ currentRouteState?: unknown;
55
57
  forceStop: boolean;
56
58
  viewGeneration: number;
57
59
  }
@@ -60,6 +62,7 @@ interface RouteLike {
60
62
  layoutRouteName?: string;
61
63
  params?: { name?: string };
62
64
  pathname?: string;
65
+ state?: unknown;
63
66
  pageUid?: string;
64
67
  layoutBasePathname?: string;
65
68
  layoutRoute?: {
@@ -71,6 +74,7 @@ interface RouteLike {
71
74
 
72
75
  interface SyncRuntimeOptions {
73
76
  skipInitialStepNavigation?: boolean;
77
+ routeState?: unknown;
74
78
  }
75
79
 
76
80
  const hasUsableSourceId = (sourceId: unknown) => sourceId !== undefined && sourceId !== null && String(sourceId) !== '';
@@ -223,7 +227,8 @@ export class BaseLayoutRouteCoordinator {
223
227
  runtime.pendingOpenViewKeys.clear();
224
228
  }
225
229
  runtime.currentPathname = pathname;
226
- this.syncRuntimeWithPathname(runtime, pathname);
230
+ runtime.currentRouteState = routeLike.state;
231
+ this.syncRuntimeWithPathname(runtime, pathname, { routeState: routeLike.state });
227
232
  }
228
233
 
229
234
  cleanupPage(pageUid: string) {
@@ -243,6 +248,7 @@ export class BaseLayoutRouteCoordinator {
243
248
  runtime.prevViewList = [];
244
249
  runtime.hasStepNavigated = false;
245
250
  runtime.currentPathname = undefined;
251
+ runtime.currentRouteState = undefined;
246
252
  }
247
253
 
248
254
  destroy() {
@@ -293,11 +299,12 @@ export class BaseLayoutRouteCoordinator {
293
299
 
294
300
  const viewStack = parsePathnameToViewParams(pathname, { basePath: this.basePathname });
295
301
  const viewList = resolveViewParamsToViewList(this.flowEngine, viewStack, runtime.routeModel);
302
+ const routeState = options.routeState;
296
303
 
297
304
  if (!options.skipInitialStepNavigation && this.shouldStepNavigate(runtime, viewList)) {
298
- this.stepNavigate(viewList, 0);
305
+ this.stepNavigate(viewList, 0, routeState);
299
306
  runtime.hasStepNavigated = true;
300
- this.scheduleInitialDeepLinkReplay(runtime, pathname);
307
+ this.scheduleInitialDeepLinkReplay(runtime, pathname, routeState);
301
308
  return;
302
309
  }
303
310
 
@@ -322,7 +329,7 @@ export class BaseLayoutRouteCoordinator {
322
329
 
323
330
  if (nextViewsToOpen.length) {
324
331
  this.markPendingOpenViews(runtime, nextViewsToOpen);
325
- this.handleOpenViews(runtime, viewList, nextViewsToOpen, runtime.viewGeneration).catch((error) => {
332
+ this.handleOpenViews(runtime, viewList, nextViewsToOpen, runtime.viewGeneration, routeState).catch((error) => {
326
333
  console.error(`[NocoBase] Failed to open route-managed views:`, error);
327
334
  });
328
335
  }
@@ -360,7 +367,7 @@ export class BaseLayoutRouteCoordinator {
360
367
  return runtime.prevViewList.length === 0 && viewList.length > 1 && !runtime.hasStepNavigated;
361
368
  }
362
369
 
363
- private scheduleInitialDeepLinkReplay(runtime: RoutePageRuntime, pathname: string) {
370
+ private scheduleInitialDeepLinkReplay(runtime: RoutePageRuntime, pathname: string, routeState?: unknown) {
364
371
  const viewGeneration = runtime.viewGeneration;
365
372
  Promise.resolve()
366
373
  .then(() => {
@@ -373,23 +380,25 @@ export class BaseLayoutRouteCoordinator {
373
380
  return;
374
381
  }
375
382
 
376
- this.syncRuntimeWithPathname(runtime, pathname);
383
+ this.syncRuntimeWithPathname(runtime, pathname, { routeState });
377
384
  })
378
385
  .catch(() => {
379
386
  // ignore
380
387
  });
381
388
  }
382
389
 
383
- private stepNavigate(viewList: ViewItem[], index: number) {
390
+ private stepNavigate(viewList: ViewItem[], index: number, routeState?: unknown) {
384
391
  if (!viewList[index]) {
385
392
  return;
386
393
  }
387
394
 
395
+ const navigationOptions = routeState ? { state: routeState } : undefined;
388
396
  if (index === 0) {
389
397
  new ViewNavigation(this.flowEngine.context, [], { basePath: this.basePathname }).navigateTo(
390
398
  viewList[index].params,
391
399
  {
392
400
  replace: true,
401
+ ...navigationOptions,
393
402
  },
394
403
  );
395
404
  } else {
@@ -397,10 +406,10 @@ export class BaseLayoutRouteCoordinator {
397
406
  this.flowEngine.context,
398
407
  viewList.slice(0, index).map((item) => item.params),
399
408
  { basePath: this.basePathname },
400
- ).navigateTo(viewList[index].params);
409
+ ).navigateTo(viewList[index].params, navigationOptions);
401
410
  }
402
411
 
403
- this.stepNavigate(viewList, index + 1);
412
+ this.stepNavigate(viewList, index + 1, routeState);
404
413
  }
405
414
 
406
415
  private replayActiveRuntimeViewsAfterLayoutContentElementChange() {
@@ -411,7 +420,10 @@ export class BaseLayoutRouteCoordinator {
411
420
 
412
421
  this.resetRuntimeViewStateForLayoutContentElementChange(runtime);
413
422
  if (runtime.meta.active) {
414
- this.syncRuntimeWithPathname(runtime, runtime.currentPathname, { skipInitialStepNavigation: true });
423
+ this.syncRuntimeWithPathname(runtime, runtime.currentPathname, {
424
+ skipInitialStepNavigation: true,
425
+ routeState: runtime.currentRouteState,
426
+ });
415
427
  }
416
428
  });
417
429
  }
@@ -456,6 +468,7 @@ export class BaseLayoutRouteCoordinator {
456
468
  viewList: ViewItem[],
457
469
  viewsToOpen: ViewItem[],
458
470
  viewGeneration: number,
471
+ routeState?: unknown,
459
472
  ) {
460
473
  const missingModels = viewsToOpen.filter((v) => !v.model);
461
474
  if (missingModels.length > 0) {
@@ -475,7 +488,7 @@ export class BaseLayoutRouteCoordinator {
475
488
  }
476
489
 
477
490
  this.syncViewListVisibility(runtime, viewList);
478
- this.openViews(runtime, viewList, viewsToOpen, 0, viewGeneration);
491
+ this.openViews(runtime, viewList, viewsToOpen, 0, viewGeneration, routeState);
479
492
  }
480
493
 
481
494
  private openViews(
@@ -484,6 +497,7 @@ export class BaseLayoutRouteCoordinator {
484
497
  viewsToOpen: ViewItem[],
485
498
  index: number,
486
499
  viewGeneration: number,
500
+ routeState?: unknown,
487
501
  ) {
488
502
  if (runtime.forceStop || runtime.viewGeneration !== viewGeneration) {
489
503
  return;
@@ -511,6 +525,7 @@ export class BaseLayoutRouteCoordinator {
511
525
  : openViewParams?.associationName;
512
526
  const openViewRouteState = viewItem.params.openViewRouteState;
513
527
  const openerUids = viewList.slice(0, viewItem.index).map((item) => item.params.viewUid);
528
+ const transientInputArgs = getRouteTransientInputArgs(routeState, viewItem.params.viewUid);
514
529
  const navigation = new ViewNavigation(
515
530
  this.flowEngine.context,
516
531
  viewList.slice(0, viewItem.index + 1).map((item) => item.params),
@@ -529,13 +544,14 @@ export class BaseLayoutRouteCoordinator {
529
544
  deactivateRef,
530
545
  openerUids,
531
546
  ...viewItem.params,
547
+ ...transientInputArgs,
532
548
  ...(openViewRouteState?.mode ? { mode: openViewRouteState.mode } : {}),
533
549
  ...(openViewRouteState?.size ? { size: openViewRouteState.size } : {}),
534
550
  pageActive: runtime.meta.active,
535
551
  activationControlledByLayout: true,
536
552
  navigation,
537
553
  onOpen: () => {
538
- this.openViews(runtime, viewList, viewsToOpen, index + 1, viewGeneration);
554
+ this.openViews(runtime, viewList, viewsToOpen, index + 1, viewGeneration, routeState);
539
555
  },
540
556
  hidden: viewItem.hidden,
541
557
  isMobileLayout: !!this.layoutContext?.isMobileLayout,
@@ -16,6 +16,7 @@ import { resolveViewParamsToViewList, updateViewListHidden, type ViewItem } from
16
16
  import { AdminLayoutRouteCoordinator } from '../AdminLayoutRouteCoordinator';
17
17
  import { BaseLayoutRouteCoordinator, toViewStack } from '../BaseLayoutRouteCoordinator';
18
18
  import { RouteModel } from '../../models/base/RouteModel';
19
+ import { ROUTE_TRANSIENT_INPUT_ARGS_KEY } from '../../routeTransientInputArgs';
19
20
 
20
21
  vi.mock('../../resolveViewParamsToViewList', () => ({
21
22
  resolveViewParamsToViewList: vi.fn(),
@@ -210,6 +211,125 @@ describe('AdminLayoutRouteCoordinator', () => {
210
211
  expect(dispatchEvent.mock.calls[0][1].target).toBe(layoutContentElement);
211
212
  });
212
213
 
214
+ it('passes transient route state input args to the opened view', () => {
215
+ const engine = new FlowEngine();
216
+ engine.registerModels({ RouteModel });
217
+ engine.context.defineProperty('routeRepository', {
218
+ value: {
219
+ getRouteBySchemaUid: vi.fn(() => ({})),
220
+ },
221
+ });
222
+
223
+ const dispatchEvent = createDispatchEventMock();
224
+ const viewItem = createViewItem('test-route', dispatchEvent);
225
+ mockResolveViewParamsToViewList.mockReturnValue([viewItem]);
226
+ mockGetViewDiffAndUpdateHidden.mockReturnValue({
227
+ viewsToClose: [],
228
+ viewsToOpen: [viewItem],
229
+ });
230
+ mockGetOpenViewStepParams.mockReturnValue({
231
+ collectionName: '',
232
+ dataSourceKey: '',
233
+ });
234
+
235
+ const coordinator = new BaseLayoutRouteCoordinator(engine, { basePathname: '/admin' });
236
+ coordinator.registerPage('test-route', {
237
+ active: true,
238
+ layoutContentElement: document.createElement('div'),
239
+ });
240
+ coordinator.syncRoute({
241
+ pageUid: 'test-route',
242
+ pathname: '/admin/test-route',
243
+ state: {
244
+ usr: {
245
+ [ROUTE_TRANSIENT_INPUT_ARGS_KEY]: {
246
+ 'test-route': {
247
+ formData: {
248
+ start: '2026-06-24 08:00:00',
249
+ end: '2026-06-24 09:00:00',
250
+ },
251
+ },
252
+ },
253
+ },
254
+ },
255
+ });
256
+
257
+ expect(dispatchEvent.mock.calls[0][1]).toMatchObject({
258
+ formData: {
259
+ start: '2026-06-24 08:00:00',
260
+ end: '2026-06-24 09:00:00',
261
+ },
262
+ });
263
+ });
264
+
265
+ it('keeps transient route state input args during initial deep-link replay', async () => {
266
+ const engine = new FlowEngine();
267
+ engine.registerModels({ RouteModel });
268
+ const navigate = vi.fn();
269
+ engine.context.defineProperty('router', {
270
+ value: {
271
+ navigate,
272
+ },
273
+ });
274
+ engine.context.defineProperty('routeRepository', {
275
+ value: {
276
+ getRouteBySchemaUid: vi.fn(() => ({})),
277
+ },
278
+ });
279
+
280
+ const rootDispatchEvent = createDispatchEventMock((_eventName, payload) => {
281
+ payload.onOpen?.();
282
+ return Promise.resolve();
283
+ });
284
+ const popupDispatchEvent = createDispatchEventMock();
285
+ const rootViewItem = createViewItem('test-route', rootDispatchEvent);
286
+ const popupViewItem = createViewItem('popup', popupDispatchEvent, 1);
287
+
288
+ mockResolveViewParamsToViewList.mockReturnValue([rootViewItem, popupViewItem]);
289
+ mockGetViewDiffAndUpdateHidden.mockReturnValue({
290
+ viewsToClose: [],
291
+ viewsToOpen: [rootViewItem, popupViewItem],
292
+ });
293
+ mockGetOpenViewStepParams.mockReturnValue({
294
+ collectionName: '',
295
+ dataSourceKey: '',
296
+ });
297
+
298
+ const routeState = {
299
+ [ROUTE_TRANSIENT_INPUT_ARGS_KEY]: {
300
+ popup: {
301
+ formData: {
302
+ start: '2026-06-24',
303
+ end: '2026-06-25',
304
+ },
305
+ },
306
+ },
307
+ };
308
+ const coordinator = new BaseLayoutRouteCoordinator(engine, { basePathname: '/admin' });
309
+ coordinator.registerPage('test-route', {
310
+ active: true,
311
+ layoutContentElement: document.createElement('div'),
312
+ });
313
+ coordinator.syncRoute({
314
+ pageUid: 'test-route',
315
+ pathname: '/admin/test-route/view/popup',
316
+ state: routeState,
317
+ });
318
+
319
+ expect(navigate).toHaveBeenCalledWith('/admin/test-route', { replace: true, state: routeState });
320
+ expect(navigate).toHaveBeenCalledWith('/admin/test-route/view/popup', { state: routeState });
321
+ expect(popupDispatchEvent).not.toHaveBeenCalled();
322
+
323
+ await Promise.resolve();
324
+
325
+ expect(popupDispatchEvent.mock.calls[0][1]).toMatchObject({
326
+ formData: {
327
+ start: '2026-06-24',
328
+ end: '2026-06-25',
329
+ },
330
+ });
331
+ });
332
+
213
333
  it('replays the active route view when the layout content element changes', () => {
214
334
  const engine = new FlowEngine();
215
335
  engine.registerModels({ RouteModel });