@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.
- package/es/BaseApplication.d.ts +1 -0
- package/es/flow/admin-shell/BaseLayoutModel.d.ts +2 -0
- package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +1 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +0 -1
- package/es/flow/routeTransientInputArgs.d.ts +14 -0
- package/es/index.d.ts +1 -0
- package/es/index.mjs +93 -93
- package/es/utils/markdownSanitize.d.ts +11 -0
- package/lib/index.js +114 -114
- package/package.json +7 -7
- package/src/BaseApplication.tsx +4 -0
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
- package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
- package/src/flow/actions/dataScopeFilter.ts +10 -1
- package/src/flow/actions/openView.tsx +21 -1
- package/src/flow/admin-shell/BaseLayoutModel.tsx +5 -0
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +28 -12
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +120 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +48 -0
- package/src/flow/common/Markdown/Display.tsx +14 -3
- package/src/flow/common/Markdown/Edit.tsx +19 -7
- package/src/flow/internal/components/Markdown/util.ts +2 -1
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +13 -18
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +30 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +1 -0
- package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
- package/src/flow/routeTransientInputArgs.ts +27 -0
- package/src/index.ts +1 -0
- 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.
|
|
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.
|
|
31
|
-
"@nocobase/flow-engine": "2.1.
|
|
32
|
-
"@nocobase/sdk": "2.1.
|
|
33
|
-
"@nocobase/shared": "2.1.
|
|
34
|
-
"@nocobase/utils": "2.1.
|
|
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": "
|
|
49
|
+
"gitHead": "ca646ba28bb11da083e3436866552932ed780b6f"
|
|
50
50
|
}
|
package/src/BaseApplication.tsx
CHANGED
|
@@ -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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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 });
|