@nocobase/client-v2 2.2.0-beta.7 → 2.2.0-beta.9
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/RouteRepository.d.ts +8 -0
- package/es/RouterManager.d.ts +15 -0
- package/es/authRedirect.d.ts +1 -0
- package/es/entry-actions/EntryActionManager.d.ts +24 -0
- package/es/entry-actions/index.d.ts +9 -0
- package/es/flow/actions/afterSuccess.d.ts +8 -1
- package/es/flow/actions/index.d.ts +1 -1
- 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/AdminLayoutSlotModels.d.ts +1 -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/models/blocks/form/submitHandler.d.ts +1 -1
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +9 -0
- package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
- package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
- package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
- package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
- package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
- package/es/flow/routeTransientInputArgs.d.ts +14 -0
- package/es/index.d.ts +2 -0
- package/es/index.mjs +243 -141
- package/es/utils/markdownSanitize.d.ts +11 -0
- package/lib/index.js +232 -130
- package/package.json +7 -7
- package/src/BaseApplication.tsx +2 -0
- package/src/RouteRepository.ts +25 -0
- package/src/RouterManager.tsx +142 -1
- package/src/__tests__/RouteRepository.test.ts +23 -0
- package/src/__tests__/RouterManager.test.ts +125 -0
- package/src/__tests__/authRedirect.test.ts +17 -0
- package/src/authRedirect.ts +38 -0
- package/src/collection-manager/field-configure.ts +1 -1
- package/src/collection-manager/interfaces/id.ts +1 -1
- package/src/collection-manager/interfaces/m2m.tsx +2 -2
- package/src/collection-manager/interfaces/m2o.tsx +2 -2
- package/src/collection-manager/interfaces/nanoid.ts +1 -1
- package/src/collection-manager/interfaces/o2m.tsx +2 -2
- package/src/collection-manager/interfaces/obo.tsx +2 -2
- package/src/collection-manager/interfaces/oho.tsx +2 -2
- package/src/collection-manager/interfaces/properties/index.ts +2 -2
- package/src/collection-manager/interfaces/uuid.ts +1 -1
- 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/__tests__/getKey.test.ts +7 -0
- package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
- package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
- package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +164 -0
- package/src/flow/actions/afterSuccess.tsx +142 -3
- package/src/flow/actions/dataScopeFilter.ts +10 -1
- package/src/flow/actions/dateTimeFormat.tsx +2 -2
- package/src/flow/actions/index.ts +1 -1
- package/src/flow/actions/openView.tsx +59 -5
- package/src/flow/admin-shell/BaseLayoutModel.tsx +149 -3
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +187 -42
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1020 -110
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +42 -4
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +177 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +20 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +5 -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 +518 -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/getViewDiffAndUpdateHidden.tsx +1 -0
- 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 +11 -4
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
- package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
- package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
- package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
- package/src/flow/models/blocks/form/submitHandler.ts +17 -3
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +87 -0
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +91 -0
- package/src/flow/models/blocks/js-block/JSBlock.tsx +223 -2
- package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +35 -12
- package/src/flow/models/blocks/table/TableBlockModel.tsx +25 -14
- package/src/flow/models/blocks/table/TableColumnModel.tsx +6 -2
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +57 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +51 -0
- package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
- package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
- package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +45 -13
- 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/resolveViewParamsToViewList.tsx +11 -3
- package/src/flow/routeTransientInputArgs.ts +27 -0
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
- package/src/index.ts +2 -0
- package/src/nocobase-buildin-plugin/index.tsx +7 -1
- package/src/settings-center/SystemSettingsPage.tsx +1 -1
- package/src/utils/markdownSanitize.ts +88 -0
|
@@ -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 = {
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { describe, it, expect, vi } from 'vitest';
|
|
12
|
+
import { RUNJS_OPEN_VIEW_ROUTE_STATE } from '@nocobase/flow-engine';
|
|
12
13
|
import { openView } from '../openView';
|
|
13
14
|
import { FlowPage } from '../../FlowPage';
|
|
15
|
+
import { ROUTE_TRANSIENT_INPUT_ARGS_KEY } from '../../routeTransientInputArgs';
|
|
14
16
|
|
|
15
17
|
describe('openView action - route mode defineProperties/defineMethods', () => {
|
|
16
18
|
const createRouteManagedCtx = () => {
|
|
@@ -50,6 +52,27 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
|
|
|
50
52
|
return { ctx, engine };
|
|
51
53
|
};
|
|
52
54
|
|
|
55
|
+
const createFirstStageCtx = (inputArgs: Record<PropertyKey, unknown>) => {
|
|
56
|
+
const navigateTo = vi.fn();
|
|
57
|
+
const ctx: any = {
|
|
58
|
+
inputArgs,
|
|
59
|
+
engine: { context: { themeToken: { colorBgLayout: '#fff' } } },
|
|
60
|
+
model: {
|
|
61
|
+
uid: 'popup-uid',
|
|
62
|
+
context: {
|
|
63
|
+
inputArgs: {},
|
|
64
|
+
defineProperty: vi.fn(),
|
|
65
|
+
},
|
|
66
|
+
flowEngine: { context: { themeToken: { colorBgLayout: '#fff' } } },
|
|
67
|
+
},
|
|
68
|
+
view: {
|
|
69
|
+
navigation: { navigateTo },
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return { ctx, navigateTo };
|
|
74
|
+
};
|
|
75
|
+
|
|
53
76
|
it('injects defineProperties/defineMethods on onModelLoaded even when not present in current inputArgs', async () => {
|
|
54
77
|
const { ctx } = createRouteManagedCtx();
|
|
55
78
|
|
|
@@ -149,4 +172,145 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
|
|
|
149
172
|
const defineMethodCalls = pageModelContext.defineMethod.mock.calls.map((c: any[]) => c[0]);
|
|
150
173
|
expect(defineMethodCalls).toContain('pong');
|
|
151
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
|
+
|
|
226
|
+
it('adds route state to first-stage navigation only for RunJS ctx.openView calls', async () => {
|
|
227
|
+
const { ctx, navigateTo } = createFirstStageCtx({
|
|
228
|
+
mode: 'dialog',
|
|
229
|
+
size: 'large',
|
|
230
|
+
[RUNJS_OPEN_VIEW_ROUTE_STATE]: { mode: 'dialog', size: 'large' },
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
|
|
234
|
+
|
|
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
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('normalizes first-stage RunJS route state mode on mobile layout', async () => {
|
|
248
|
+
const { ctx, navigateTo } = createFirstStageCtx({
|
|
249
|
+
isMobileLayout: true,
|
|
250
|
+
mode: 'dialog',
|
|
251
|
+
size: 'large',
|
|
252
|
+
[RUNJS_OPEN_VIEW_ROUTE_STATE]: { mode: 'dialog', size: 'large' },
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
|
|
256
|
+
|
|
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
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('uses route replay mode and size before persisted openView defaults', async () => {
|
|
270
|
+
const { ctx } = createRouteManagedCtx();
|
|
271
|
+
ctx.inputArgs.mode = 'dialog';
|
|
272
|
+
ctx.inputArgs.size = 'large';
|
|
273
|
+
|
|
274
|
+
await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
|
|
275
|
+
|
|
276
|
+
expect(ctx.viewer.open).toHaveBeenCalledWith(
|
|
277
|
+
expect.objectContaining({
|
|
278
|
+
type: 'dialog',
|
|
279
|
+
width: '80%',
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('uses decoded openView route state during route replay', async () => {
|
|
285
|
+
const { ctx } = createRouteManagedCtx();
|
|
286
|
+
ctx.inputArgs.openViewRouteState = { mode: 'dialog', size: 'large' };
|
|
287
|
+
|
|
288
|
+
await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
|
|
289
|
+
|
|
290
|
+
expect(ctx.viewer.open).toHaveBeenCalledWith(
|
|
291
|
+
expect.objectContaining({
|
|
292
|
+
type: 'dialog',
|
|
293
|
+
width: '80%',
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('keeps first-stage navigation unchanged for non-RunJS openView calls', async () => {
|
|
299
|
+
const { ctx, navigateTo } = createFirstStageCtx({
|
|
300
|
+
mode: 'dialog',
|
|
301
|
+
size: 'large',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
|
|
305
|
+
|
|
306
|
+
expect(navigateTo).toHaveBeenCalledWith(
|
|
307
|
+
{
|
|
308
|
+
viewUid: 'popup-uid',
|
|
309
|
+
filterByTk: undefined,
|
|
310
|
+
sourceId: undefined,
|
|
311
|
+
tabUid: undefined,
|
|
312
|
+
},
|
|
313
|
+
undefined,
|
|
314
|
+
);
|
|
315
|
+
});
|
|
152
316
|
});
|
|
@@ -7,13 +7,152 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
FlowContext,
|
|
12
|
+
createRecordMetaFactory,
|
|
13
|
+
createRecordResolveOnServerWithLocal,
|
|
14
|
+
defineAction,
|
|
15
|
+
tExpr,
|
|
16
|
+
useFlowSettingsContext,
|
|
17
|
+
type Collection,
|
|
18
|
+
type FlowRuntimeContext,
|
|
19
|
+
type MetaTreeNode,
|
|
20
|
+
type PropertyMetaFactory,
|
|
21
|
+
} from '@nocobase/flow-engine';
|
|
11
22
|
import { isURL } from '@nocobase/utils/client';
|
|
12
|
-
import {
|
|
23
|
+
import React, { useMemo } from 'react';
|
|
24
|
+
import {
|
|
25
|
+
TextAreaWithContextSelector,
|
|
26
|
+
type TextAreaWithContextSelectorProps,
|
|
27
|
+
} from '../components/TextAreaWithContextSelector';
|
|
28
|
+
|
|
29
|
+
type ResponseRecordPlainStepContext = {
|
|
30
|
+
steps?: FlowRuntimeContext['steps'];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ResponseRecordFlowDefinitionContext = {
|
|
34
|
+
flowKey?: string;
|
|
35
|
+
model?: {
|
|
36
|
+
getFlow?: (flowKey: string) => { steps?: Record<string, unknown> } | undefined;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type ResponseRecordContext = FlowContext &
|
|
41
|
+
ResponseRecordPlainStepContext & {
|
|
42
|
+
blockModel?: { collection?: Collection };
|
|
43
|
+
collection?: Collection;
|
|
44
|
+
model?: FlowRuntimeContext['model'] & { collection?: Collection };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function getResponseRecordSteps(ctx: FlowContext | ResponseRecordPlainStepContext): FlowRuntimeContext['steps'] {
|
|
48
|
+
return 'steps' in ctx ? ctx.steps || {} : {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getAfterSuccessResponseRecord(ctx: FlowContext | ResponseRecordPlainStepContext) {
|
|
52
|
+
const steps = getResponseRecordSteps(ctx);
|
|
53
|
+
const preferredStepKeys = ['saveResource', 'submit', 'request', 'apply', 'save'];
|
|
54
|
+
|
|
55
|
+
for (const stepKey of preferredStepKeys) {
|
|
56
|
+
if (Object.prototype.hasOwnProperty.call(steps, stepKey) && steps[stepKey]?.result != null) {
|
|
57
|
+
return steps[stepKey].result;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const results = Object.entries(steps)
|
|
62
|
+
.filter(([stepKey, step]) => stepKey !== 'afterSuccess' && step?.result != null)
|
|
63
|
+
.map(([, step]) => step.result);
|
|
64
|
+
return results[results.length - 1];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getResponseRecordMeta(ctx: ResponseRecordContext): PropertyMetaFactory | undefined {
|
|
68
|
+
if (!hasResponseRecordSource(ctx)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const collectionAccessor = getResponseRecordCollectionAccessor(ctx);
|
|
72
|
+
if (!collectionAccessor()) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
return createRecordMetaFactory(collectionAccessor, ctx.t('Response record'), () => {
|
|
76
|
+
const collection = collectionAccessor();
|
|
77
|
+
const record = getAfterSuccessResponseRecord(ctx);
|
|
78
|
+
if (!collection || !record) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const filterByTk = collection.getFilterByTK(record);
|
|
83
|
+
if (filterByTk == null) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
collection: collection.name,
|
|
88
|
+
dataSourceKey: collection.dataSourceKey || 'main',
|
|
89
|
+
filterByTk,
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function hasResponseRecordSource(ctx: FlowContext | ResponseRecordPlainStepContext) {
|
|
98
|
+
if (Object.prototype.hasOwnProperty.call(getResponseRecordSteps(ctx), 'saveResource')) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const { model, flowKey } = ctx as ResponseRecordFlowDefinitionContext;
|
|
103
|
+
if (!model || typeof model.getFlow !== 'function' || !flowKey) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return Object.prototype.hasOwnProperty.call(model.getFlow(flowKey)?.steps || {}, 'saveResource');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getResponseRecordCollectionAccessor(ctx: ResponseRecordContext) {
|
|
111
|
+
return () => ctx.blockModel?.collection || ctx.collection || ctx.model?.collection || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getMetaTreeWithResponseRecord(ctx: FlowRuntimeContext): MetaTreeNode[] {
|
|
115
|
+
const responseRecordMeta = getResponseRecordMeta(ctx);
|
|
116
|
+
if (!responseRecordMeta) {
|
|
117
|
+
return ctx.getPropertyMetaTree?.() || [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const scoped = new FlowContext();
|
|
121
|
+
scoped.addDelegate(ctx);
|
|
122
|
+
scoped.defineProperty('responseRecord', {
|
|
123
|
+
get: () => getAfterSuccessResponseRecord(ctx),
|
|
124
|
+
cache: false,
|
|
125
|
+
resolveOnServer: createRecordResolveOnServerWithLocal(getResponseRecordCollectionAccessor(ctx), () =>
|
|
126
|
+
getAfterSuccessResponseRecord(ctx),
|
|
127
|
+
),
|
|
128
|
+
meta: responseRecordMeta,
|
|
129
|
+
});
|
|
130
|
+
return scoped.getPropertyMetaTree();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function AfterSuccessRedirectTextArea(props: TextAreaWithContextSelectorProps) {
|
|
134
|
+
const flowCtx = useFlowSettingsContext();
|
|
135
|
+
const metaTree = useMemo(() => () => getMetaTreeWithResponseRecord(flowCtx), [flowCtx]);
|
|
136
|
+
|
|
137
|
+
return <TextAreaWithContextSelector {...props} metaTree={metaTree} />;
|
|
138
|
+
}
|
|
13
139
|
|
|
14
140
|
export const afterSuccess = defineAction({
|
|
15
141
|
name: 'afterSuccess',
|
|
16
142
|
title: tExpr('After successful submission'),
|
|
143
|
+
defineProperties(ctx) {
|
|
144
|
+
const responseRecordMeta = getResponseRecordMeta(ctx);
|
|
145
|
+
return {
|
|
146
|
+
responseRecord: {
|
|
147
|
+
get: () => getAfterSuccessResponseRecord(ctx),
|
|
148
|
+
cache: false,
|
|
149
|
+
resolveOnServer: createRecordResolveOnServerWithLocal(getResponseRecordCollectionAccessor(ctx), () =>
|
|
150
|
+
getAfterSuccessResponseRecord(ctx),
|
|
151
|
+
),
|
|
152
|
+
...(responseRecordMeta ? { meta: responseRecordMeta } : {}),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
},
|
|
17
156
|
uiSchema: {
|
|
18
157
|
successMessage: {
|
|
19
158
|
type: 'string',
|
|
@@ -46,7 +185,7 @@ export const afterSuccess = defineAction({
|
|
|
46
185
|
type: 'string',
|
|
47
186
|
title: tExpr('Link'),
|
|
48
187
|
'x-decorator': 'FormItem',
|
|
49
|
-
'x-component':
|
|
188
|
+
'x-component': AfterSuccessRedirectTextArea,
|
|
50
189
|
'x-reactions': {
|
|
51
190
|
dependencies: ['actionAfterSuccess'],
|
|
52
191
|
fulfill: {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { isVariableExpression, pruneFilter } from '@nocobase/flow-engine';
|
|
10
|
+
import { extractPropertyPath, isVariableExpression, pruneFilter } from '@nocobase/flow-engine';
|
|
11
11
|
import { transformFilter } from '@nocobase/utils/client';
|
|
12
12
|
import _ from 'lodash';
|
|
13
13
|
|
|
@@ -45,6 +45,14 @@ function restorePreservedNull(value: any): any {
|
|
|
45
45
|
return value;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function isUrlSearchParamsExpression(value: any) {
|
|
49
|
+
if (!isVariableExpression(value)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return extractPropertyPath(value)?.[0] === 'urlSearchParams';
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
function markEmptyVariableValues(rawNode: any, resolvedNode: any) {
|
|
49
57
|
if (!rawNode || !resolvedNode || typeof rawNode !== 'object' || typeof resolvedNode !== 'object') {
|
|
50
58
|
return;
|
|
@@ -57,6 +65,7 @@ function markEmptyVariableValues(rawNode: any, resolvedNode: any) {
|
|
|
57
65
|
}
|
|
58
66
|
if (
|
|
59
67
|
isVariableExpression(rawNode.value) &&
|
|
68
|
+
!isUrlSearchParamsExpression(rawNode.value) &&
|
|
60
69
|
(resolvedNode.value === undefined || resolvedNode.value === null || resolvedNode.value === '')
|
|
61
70
|
) {
|
|
62
71
|
resolvedNode.value = PRESERVE_NULL;
|
|
@@ -28,11 +28,11 @@ const isTableColumnFieldSubModel = (model) => {
|
|
|
28
28
|
|
|
29
29
|
const syncTableColumnDateTimeFormatProps = (ctx, props) => {
|
|
30
30
|
const model = ctx.model;
|
|
31
|
-
if (!isTableColumnFieldSubModel(model)
|
|
31
|
+
if (!isTableColumnFieldSubModel(model)) {
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
model.parent
|
|
35
|
+
model.parent?.setProps?.(props);
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
export const dateTimeFormat = defineAction({
|
|
@@ -7,11 +7,20 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
defineAction,
|
|
12
|
+
tExpr,
|
|
13
|
+
FlowModelContext,
|
|
14
|
+
FlowModel,
|
|
15
|
+
FlowExitAllException,
|
|
16
|
+
createOpenViewRouteState,
|
|
17
|
+
RUNJS_OPEN_VIEW_ROUTE_STATE,
|
|
18
|
+
} from '@nocobase/flow-engine';
|
|
11
19
|
import React from 'react';
|
|
12
20
|
import { FlowPage } from '../FlowPage';
|
|
13
21
|
import { PageModel, RootPageModel } from '../models';
|
|
14
22
|
import _ from 'lodash';
|
|
23
|
+
import { ROUTE_TRANSIENT_INPUT_ARGS_KEY } from '../routeTransientInputArgs';
|
|
15
24
|
|
|
16
25
|
type DirtyAwareFlowModel = FlowModel & {
|
|
17
26
|
getUserModifiedFields?: () => Set<string> | undefined;
|
|
@@ -23,6 +32,15 @@ type BeforeCloseDirtyState = {
|
|
|
23
32
|
formModelUids: string[];
|
|
24
33
|
};
|
|
25
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
|
+
|
|
26
44
|
function collectDirtyFormModelUids(model?: FlowModel | null, ignoredDirtyFormModelUids: string[] = []): string[] {
|
|
27
45
|
if (!model) {
|
|
28
46
|
return [];
|
|
@@ -291,8 +309,32 @@ export const openView = defineAction({
|
|
|
291
309
|
? (inputArgs as any).associationName
|
|
292
310
|
: (params as any)?.associationName;
|
|
293
311
|
const mergedTabUid = typeof inputArgs.tabUid !== 'undefined' ? inputArgs.tabUid : params.tabUid;
|
|
294
|
-
|
|
295
|
-
const
|
|
312
|
+
const hasRunJSOpenViewRouteState = Object.prototype.hasOwnProperty.call(inputArgs, RUNJS_OPEN_VIEW_ROUTE_STATE);
|
|
313
|
+
const runJSOpenViewRouteStateInput = (inputArgs as Record<PropertyKey, unknown>)[RUNJS_OPEN_VIEW_ROUTE_STATE];
|
|
314
|
+
const runJSOpenViewRouteState = hasRunJSOpenViewRouteState
|
|
315
|
+
? createOpenViewRouteState(
|
|
316
|
+
runJSOpenViewRouteStateInput && typeof runJSOpenViewRouteStateInput === 'object'
|
|
317
|
+
? (runJSOpenViewRouteStateInput as { mode?: unknown; size?: unknown })
|
|
318
|
+
: {
|
|
319
|
+
mode: inputArgs.mode,
|
|
320
|
+
size: inputArgs.size,
|
|
321
|
+
},
|
|
322
|
+
)
|
|
323
|
+
: undefined;
|
|
324
|
+
const replayOpenViewRouteStateInput = (inputArgs as { openViewRouteState?: unknown }).openViewRouteState;
|
|
325
|
+
const replayOpenViewRouteState =
|
|
326
|
+
replayOpenViewRouteStateInput && typeof replayOpenViewRouteStateInput === 'object'
|
|
327
|
+
? createOpenViewRouteState(replayOpenViewRouteStateInput as { mode?: unknown; size?: unknown })
|
|
328
|
+
: undefined;
|
|
329
|
+
const runtimeOpenViewRouteState = runJSOpenViewRouteState || replayOpenViewRouteState;
|
|
330
|
+
// 移动端中只需要显示子页面。本次 dispatch 参数优先于持久化默认值。
|
|
331
|
+
const openMode = ctx.inputArgs?.isMobileLayout
|
|
332
|
+
? 'embed'
|
|
333
|
+
: runtimeOpenViewRouteState?.mode || ctx.inputArgs?.mode || params.mode || 'drawer';
|
|
334
|
+
const effectiveRunJSOpenViewRouteState =
|
|
335
|
+
runJSOpenViewRouteState && ctx.inputArgs?.isMobileLayout
|
|
336
|
+
? createOpenViewRouteState({ ...runJSOpenViewRouteState, mode: openMode })
|
|
337
|
+
: runJSOpenViewRouteState;
|
|
296
338
|
let navigation = typeof inputArgs.navigation !== 'undefined' ? inputArgs.navigation : params.navigation;
|
|
297
339
|
|
|
298
340
|
// 传递了上下文就必须禁用路由,否则下次路由打开会缺少上下文
|
|
@@ -313,6 +355,7 @@ export const openView = defineAction({
|
|
|
313
355
|
sourceId: mergedSourceId,
|
|
314
356
|
tabUid: mergedTabUid,
|
|
315
357
|
viewUid: ctx.model.context?.inputArgs?.viewUid || ctx.model.uid,
|
|
358
|
+
...(effectiveRunJSOpenViewRouteState ? { openViewRouteState: effectiveRunJSOpenViewRouteState } : {}),
|
|
316
359
|
} as Record<string, unknown>;
|
|
317
360
|
const pendingView = {
|
|
318
361
|
type: pendingType,
|
|
@@ -328,8 +371,19 @@ export const openView = defineAction({
|
|
|
328
371
|
filterByTk: mergedFilterByTk,
|
|
329
372
|
sourceId: mergedSourceId,
|
|
330
373
|
tabUid: mergedTabUid,
|
|
374
|
+
...(effectiveRunJSOpenViewRouteState ? { openViewRouteState: effectiveRunJSOpenViewRouteState } : {}),
|
|
331
375
|
};
|
|
332
|
-
|
|
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);
|
|
333
387
|
return;
|
|
334
388
|
}
|
|
335
389
|
}
|
|
@@ -373,7 +427,7 @@ export const openView = defineAction({
|
|
|
373
427
|
|
|
374
428
|
const pageModelClass =
|
|
375
429
|
ctx.inputArgs.pageModelClass || params.pageModelClass || ctx.layout?.childPageModelClass || 'ChildPageModel';
|
|
376
|
-
const size = ctx.inputArgs.size || params.size || 'medium';
|
|
430
|
+
const size = runtimeOpenViewRouteState?.size || ctx.inputArgs.size || params.size || 'medium';
|
|
377
431
|
let pageModelUid: string | null = null;
|
|
378
432
|
let pageModelRef: FlowModel | null = null;
|
|
379
433
|
|