@nocobase/client-v2 2.1.17 → 2.1.19
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/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
- package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
- 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/flow/FlowPage.tsx +29 -2
- package/src/flow/__tests__/FlowPage.test.tsx +152 -25
- 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 +29 -2
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +359 -2
- 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/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.19",
|
|
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.19",
|
|
31
|
+
"@nocobase/flow-engine": "2.1.19",
|
|
32
|
+
"@nocobase/sdk": "2.1.19",
|
|
33
|
+
"@nocobase/shared": "2.1.19",
|
|
34
|
+
"@nocobase/utils": "2.1.19",
|
|
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": "c316683666cfb478cb355b971c8f067a4f925fcb"
|
|
50
50
|
}
|
package/src/flow/FlowPage.tsx
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
useFlowModelById,
|
|
15
15
|
useFlowViewContext,
|
|
16
16
|
} from '@nocobase/flow-engine';
|
|
17
|
-
import type { FlowModel, FlowModelRendererProps, ModelConstructor } from '@nocobase/flow-engine';
|
|
17
|
+
import type { FlowEngineContext, FlowModel, FlowModelRendererProps, ModelConstructor } from '@nocobase/flow-engine';
|
|
18
18
|
import { useRequest } from 'ahooks';
|
|
19
19
|
import React from 'react';
|
|
20
20
|
import FlowRoute from './components/FlowRoute';
|
|
@@ -57,10 +57,36 @@ type FlowPageProps = {
|
|
|
57
57
|
showFlowSettings?: FlowModelRendererProps['showFlowSettings'];
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
type FlowPageViewContext = FlowEngineContext & {
|
|
61
|
+
view?: {
|
|
62
|
+
inputArgs?: {
|
|
63
|
+
isMobileLayout?: unknown;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const bindViewLayoutState = (model: FlowModel, ctx?: FlowPageViewContext | null) => {
|
|
69
|
+
const hasViewMobileLayout = typeof ctx?.view?.inputArgs?.isMobileLayout === 'boolean';
|
|
70
|
+
const hasContextMobileLayout = typeof ctx?.isMobileLayout === 'boolean';
|
|
71
|
+
if (!hasViewMobileLayout && !hasContextMobileLayout) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
model.context.defineProperty('isMobileLayout', {
|
|
76
|
+
get: () => {
|
|
77
|
+
if (typeof ctx?.isMobileLayout === 'boolean') {
|
|
78
|
+
return ctx.isMobileLayout;
|
|
79
|
+
}
|
|
80
|
+
return !!ctx?.view?.inputArgs?.isMobileLayout;
|
|
81
|
+
},
|
|
82
|
+
cache: false,
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
60
86
|
export const FlowPage = React.memo((props: FlowPageProps & Record<string, unknown>) => {
|
|
61
87
|
const { pageModelClass = 'ChildPageModel', parentId, onModelLoaded, defaultTabTitle, ...rest } = props;
|
|
62
88
|
const flowEngine = useFlowEngine();
|
|
63
|
-
const ctx = useFlowViewContext();
|
|
89
|
+
const ctx = useFlowViewContext<FlowPageViewContext>();
|
|
64
90
|
const { loading, data, error } = useRequest(
|
|
65
91
|
async () => {
|
|
66
92
|
const ModelClass = await flowEngine.getModelClassAsync(pageModelClass);
|
|
@@ -105,6 +131,7 @@ export const FlowPage = React.memo((props: FlowPageProps & Record<string, unknow
|
|
|
105
131
|
const data = await flowEngine.loadOrCreateModel(options, { skipSave: !flowEngine.context.flowSettingsEnabled });
|
|
106
132
|
if (data?.uid && onModelLoaded) {
|
|
107
133
|
data.context.addDelegate(ctx);
|
|
134
|
+
bindViewLayoutState(data, ctx);
|
|
108
135
|
data.removeParentDelegate();
|
|
109
136
|
onModelLoaded(data.uid, data);
|
|
110
137
|
}
|
|
@@ -7,44 +7,105 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { render } from '@testing-library/react';
|
|
10
|
+
import { render, waitFor } from '@testing-library/react';
|
|
11
11
|
import React from 'react';
|
|
12
12
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
13
13
|
import { FlowPage } from '../FlowPage';
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
type TestContextProperty = {
|
|
16
|
+
value?: unknown;
|
|
17
|
+
get?: () => unknown;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type TestModelContext = {
|
|
21
|
+
isMobileLayout: boolean;
|
|
22
|
+
themeToken: {
|
|
23
|
+
marginBlock: number;
|
|
24
|
+
};
|
|
25
|
+
addDelegate: ReturnType<typeof vi.fn>;
|
|
26
|
+
defineProperty: ReturnType<typeof vi.fn>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type TestPageModel = {
|
|
30
|
+
uid: string;
|
|
31
|
+
props: {
|
|
32
|
+
showFlowSettings: boolean;
|
|
33
|
+
};
|
|
34
|
+
context: TestModelContext;
|
|
35
|
+
removeParentDelegate: ReturnType<typeof vi.fn>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type RendererProps = {
|
|
39
|
+
model?: TestPageModel;
|
|
40
|
+
showFlowSettings?: boolean;
|
|
41
|
+
} & Record<string, unknown>;
|
|
42
|
+
|
|
43
|
+
const defineContextProperty = (context: TestModelContext, key: string, property: TestContextProperty) => {
|
|
44
|
+
Object.defineProperty(context, key, {
|
|
45
|
+
configurable: true,
|
|
46
|
+
enumerable: true,
|
|
47
|
+
get: property.get || (() => property.value),
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const createPageModel = (): TestPageModel => {
|
|
52
|
+
const context = {
|
|
53
|
+
isMobileLayout: false,
|
|
54
|
+
themeToken: {
|
|
55
|
+
marginBlock: 16,
|
|
56
|
+
},
|
|
57
|
+
addDelegate: vi.fn(),
|
|
58
|
+
defineProperty: vi.fn((key: string, property: TestContextProperty) => {
|
|
59
|
+
defineContextProperty(context, key, property);
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return {
|
|
18
64
|
uid: 'public-form-page',
|
|
19
65
|
props: {
|
|
20
66
|
showFlowSettings: false,
|
|
21
67
|
},
|
|
22
|
-
context
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
68
|
+
context,
|
|
69
|
+
removeParentDelegate: vi.fn(),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const mocks = vi.hoisted(() => ({
|
|
74
|
+
rendererMobileStates: [] as boolean[],
|
|
75
|
+
rendererProps: undefined as RendererProps | undefined,
|
|
76
|
+
model: undefined as TestPageModel | undefined,
|
|
77
|
+
viewInputMobileLayout: true as boolean | undefined,
|
|
78
|
+
contextMobileLayout: undefined as boolean | undefined,
|
|
29
79
|
}));
|
|
30
80
|
|
|
31
81
|
vi.mock('@nocobase/flow-engine', async () => {
|
|
32
|
-
const actual = await vi.importActual<
|
|
82
|
+
const actual = await vi.importActual<typeof import('@nocobase/flow-engine')>('@nocobase/flow-engine');
|
|
33
83
|
const ReactModule = await import('react');
|
|
84
|
+
class TestPageModelClass {}
|
|
34
85
|
|
|
35
86
|
return {
|
|
36
87
|
...actual,
|
|
37
|
-
FlowModelRenderer: vi.fn((props:
|
|
88
|
+
FlowModelRenderer: vi.fn((props: RendererProps) => {
|
|
38
89
|
mocks.rendererProps = props;
|
|
90
|
+
if (typeof props.model?.context.isMobileLayout === 'boolean') {
|
|
91
|
+
mocks.rendererMobileStates.push(props.model.context.isMobileLayout);
|
|
92
|
+
}
|
|
39
93
|
return ReactModule.createElement('div', { 'data-testid': 'flow-model-renderer' });
|
|
40
94
|
}),
|
|
41
95
|
useFlowEngine: vi.fn(() => ({
|
|
42
|
-
getModelClassAsync: vi.fn(),
|
|
43
|
-
loadOrCreateModel: vi.fn(),
|
|
96
|
+
getModelClassAsync: vi.fn(async () => TestPageModelClass),
|
|
97
|
+
loadOrCreateModel: vi.fn(async () => mocks.model),
|
|
44
98
|
context: {},
|
|
99
|
+
translate: vi.fn((value: string) => value),
|
|
45
100
|
})),
|
|
46
101
|
useFlowModelById: vi.fn(() => mocks.model),
|
|
47
102
|
useFlowViewContext: vi.fn(() => ({
|
|
103
|
+
view: {
|
|
104
|
+
inputArgs: {
|
|
105
|
+
...(typeof mocks.viewInputMobileLayout === 'boolean' ? { isMobileLayout: mocks.viewInputMobileLayout } : {}),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
...(typeof mocks.contextMobileLayout === 'boolean' ? { isMobileLayout: mocks.contextMobileLayout } : {}),
|
|
48
109
|
themeToken: {
|
|
49
110
|
marginBlock: 16,
|
|
50
111
|
},
|
|
@@ -53,27 +114,93 @@ vi.mock('@nocobase/flow-engine', async () => {
|
|
|
53
114
|
});
|
|
54
115
|
|
|
55
116
|
vi.mock('ahooks', async () => {
|
|
56
|
-
const actual = await vi.importActual<
|
|
117
|
+
const actual = await vi.importActual<typeof import('ahooks')>('ahooks');
|
|
118
|
+
const ReactModule = await import('react');
|
|
57
119
|
return {
|
|
58
120
|
...actual,
|
|
59
|
-
useRequest: vi.fn(() =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
121
|
+
useRequest: vi.fn((service: () => Promise<unknown>) => {
|
|
122
|
+
const serviceRef = ReactModule.useRef(service);
|
|
123
|
+
const [state, setState] = ReactModule.useState<{
|
|
124
|
+
loading: boolean;
|
|
125
|
+
data?: unknown;
|
|
126
|
+
error?: unknown;
|
|
127
|
+
}>({
|
|
128
|
+
loading: true,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
ReactModule.useEffect(() => {
|
|
132
|
+
let active = true;
|
|
133
|
+
serviceRef
|
|
134
|
+
.current()
|
|
135
|
+
.then((data) => {
|
|
136
|
+
if (active) {
|
|
137
|
+
setState({ loading: false, data, error: null });
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
.catch((error) => {
|
|
141
|
+
if (active) {
|
|
142
|
+
setState({ loading: false, data: undefined, error });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return () => {
|
|
147
|
+
active = false;
|
|
148
|
+
};
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
return state;
|
|
152
|
+
}),
|
|
66
153
|
};
|
|
67
154
|
});
|
|
68
155
|
|
|
69
156
|
describe('FlowPage', () => {
|
|
70
157
|
afterEach(() => {
|
|
158
|
+
mocks.rendererMobileStates = [];
|
|
71
159
|
mocks.rendererProps = undefined;
|
|
160
|
+
mocks.model = undefined;
|
|
161
|
+
mocks.viewInputMobileLayout = true;
|
|
162
|
+
mocks.contextMobileLayout = undefined;
|
|
72
163
|
});
|
|
73
164
|
|
|
74
|
-
it('uses showFlowSettings from page model props', () => {
|
|
165
|
+
it('uses showFlowSettings from page model props', async () => {
|
|
166
|
+
mocks.model = createPageModel();
|
|
75
167
|
render(<FlowPage />);
|
|
76
168
|
|
|
77
|
-
|
|
169
|
+
await waitFor(() => {
|
|
170
|
+
expect((mocks.rendererProps as { showFlowSettings?: boolean } | undefined)?.showFlowSettings).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('exposes mobile layout state from route-managed view inputArgs before first render', async () => {
|
|
175
|
+
mocks.model = createPageModel();
|
|
176
|
+
render(<FlowPage onModelLoaded={vi.fn()} />);
|
|
177
|
+
|
|
178
|
+
await waitFor(() => {
|
|
179
|
+
expect(mocks.rendererMobileStates.length).toBeGreaterThan(0);
|
|
180
|
+
});
|
|
181
|
+
expect(mocks.rendererMobileStates[0]).toBe(true);
|
|
78
182
|
});
|
|
183
|
+
|
|
184
|
+
it.each([
|
|
185
|
+
{ viewInputMobileLayout: true, contextMobileLayout: false, expected: false },
|
|
186
|
+
{ viewInputMobileLayout: false, contextMobileLayout: true, expected: true },
|
|
187
|
+
{ viewInputMobileLayout: undefined, contextMobileLayout: true, expected: true },
|
|
188
|
+
{ viewInputMobileLayout: undefined, contextMobileLayout: false, expected: false },
|
|
189
|
+
{ viewInputMobileLayout: true, contextMobileLayout: undefined, expected: true },
|
|
190
|
+
{ viewInputMobileLayout: false, contextMobileLayout: undefined, expected: false },
|
|
191
|
+
])(
|
|
192
|
+
'resolves mobile layout state from view input args and layout context %#',
|
|
193
|
+
async ({ viewInputMobileLayout, contextMobileLayout, expected }) => {
|
|
194
|
+
mocks.viewInputMobileLayout = viewInputMobileLayout;
|
|
195
|
+
mocks.contextMobileLayout = contextMobileLayout;
|
|
196
|
+
mocks.model = createPageModel();
|
|
197
|
+
|
|
198
|
+
render(<FlowPage onModelLoaded={vi.fn()} />);
|
|
199
|
+
|
|
200
|
+
await waitFor(() => {
|
|
201
|
+
expect(mocks.rendererMobileStates.length).toBeGreaterThan(0);
|
|
202
|
+
});
|
|
203
|
+
expect(mocks.rendererMobileStates[0]).toBe(expected);
|
|
204
|
+
},
|
|
205
|
+
);
|
|
79
206
|
});
|
|
@@ -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
|
}
|