@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.
- package/es/BaseApplication.d.ts +2 -0
- package/es/RouterManager.d.ts +15 -0
- package/es/entry-actions/EntryActionManager.d.ts +24 -0
- package/es/entry-actions/index.d.ts +9 -0
- package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
- package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
- package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
- package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
- package/es/flow/models/base/ActionModelCore.d.ts +2 -0
- package/es/flow/routeTransientInputArgs.d.ts +14 -0
- package/es/index.d.ts +4 -0
- package/es/index.mjs +178 -124
- package/es/utils/markdownSanitize.d.ts +11 -0
- package/lib/index.js +164 -110
- package/package.json +7 -7
- package/src/BaseApplication.tsx +2 -0
- package/src/RouterManager.tsx +142 -1
- package/src/__tests__/RouterManager.test.ts +125 -0
- package/src/entry-actions/EntryActionManager.ts +76 -0
- package/src/entry-actions/index.ts +10 -0
- package/src/flow/FlowPage.tsx +29 -2
- package/src/flow/__tests__/FlowPage.test.tsx +152 -25
- package/src/flow/__tests__/FlowRoute.test.tsx +547 -2
- 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 +111 -0
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -4
- package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
- package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +359 -2
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
- package/src/flow/admin-shell/admin-layout/index.ts +1 -0
- package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
- package/src/flow/common/Markdown/Display.tsx +14 -3
- package/src/flow/common/Markdown/Edit.tsx +19 -7
- package/src/flow/components/FlowRoute.tsx +128 -16
- package/src/flow/internal/components/Markdown/util.ts +2 -1
- package/src/flow/models/base/ActionModel.tsx +10 -8
- package/src/flow/models/base/ActionModelCore.tsx +5 -0
- package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
- package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
- package/src/flow/routeTransientInputArgs.ts +27 -0
- package/src/index.ts +4 -0
- package/src/nocobase-buildin-plugin/index.tsx +7 -1
- 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
|
-
|
|
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
|
}
|
|
@@ -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
|
/**
|