@nocobase/client-v2 2.1.11 → 2.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/es/flow/actions/afterSuccess.d.ts +8 -1
  2. package/es/flow/actions/index.d.ts +1 -1
  3. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  4. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  5. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  6. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  7. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  8. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  9. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  10. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  11. package/es/index.mjs +137 -89
  12. package/lib/index.js +163 -115
  13. package/package.json +7 -7
  14. package/src/collection-manager/field-configure.ts +1 -1
  15. package/src/collection-manager/interfaces/id.ts +1 -1
  16. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  17. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  18. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  19. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  20. package/src/collection-manager/interfaces/obo.tsx +2 -2
  21. package/src/collection-manager/interfaces/oho.tsx +2 -2
  22. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  23. package/src/collection-manager/interfaces/uuid.ts +1 -1
  24. package/src/flow/__tests__/getKey.test.ts +7 -0
  25. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  26. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  27. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +104 -0
  28. package/src/flow/actions/afterSuccess.tsx +142 -3
  29. package/src/flow/actions/dateTimeFormat.tsx +2 -2
  30. package/src/flow/actions/index.ts +1 -1
  31. package/src/flow/actions/openView.tsx +38 -4
  32. package/src/flow/admin-shell/BaseLayoutModel.tsx +25 -3
  33. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +5 -2
  34. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +13 -0
  35. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +82 -0
  36. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  37. package/src/flow/models/base/ActionModelCore.tsx +6 -4
  38. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  39. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  40. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  41. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  42. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  43. package/src/flow/models/blocks/js-block/JSBlock.tsx +223 -2
  44. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  45. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +35 -12
  46. package/src/flow/models/blocks/table/TableBlockModel.tsx +25 -14
  47. package/src/flow/models/blocks/table/TableColumnModel.tsx +6 -2
  48. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +57 -1
  49. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  50. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  51. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +51 -0
  52. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  53. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  54. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  55. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +45 -13
  56. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  57. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
  58. package/src/settings-center/SystemSettingsPage.tsx +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.1.11",
3
+ "version": "2.1.14",
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.11",
31
- "@nocobase/flow-engine": "2.1.11",
32
- "@nocobase/sdk": "2.1.11",
33
- "@nocobase/shared": "2.1.11",
34
- "@nocobase/utils": "2.1.11",
30
+ "@nocobase/evaluators": "2.1.14",
31
+ "@nocobase/flow-engine": "2.1.14",
32
+ "@nocobase/sdk": "2.1.14",
33
+ "@nocobase/shared": "2.1.14",
34
+ "@nocobase/utils": "2.1.14",
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": "1ccf891d837e21089f65f84b892407b34a0a0cb9"
49
+ "gitHead": "2a8c3e0e9362aeb49b39c1d02358639b29769f32"
50
50
  }
@@ -530,7 +530,7 @@ export function reverseFieldConfigureItems(): FieldConfigureItem[] {
530
530
  component: 'Input',
531
531
  required: true,
532
532
  description:
533
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
533
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
534
534
  hidden: ({ context, values }) => !context.showReverseFieldConfig && !get(values, 'autoCreateReverseField'),
535
535
  disabled: ({ context }) => !context.showReverseFieldConfig,
536
536
  },
@@ -48,7 +48,7 @@ export class IdFieldInterface extends CollectionFieldInterface {
48
48
  'x-decorator': 'FormItem',
49
49
  'x-component': 'Input',
50
50
  description:
51
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
51
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
52
52
  },
53
53
  };
54
54
  filterable = {
@@ -158,7 +158,7 @@ export class M2MFieldInterface extends CollectionFieldInterface {
158
158
  required: true,
159
159
  default: '{{ useNewId("f_") }}',
160
160
  description:
161
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
161
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
162
162
  'x-decorator': 'FormItem',
163
163
  'x-component': 'ForeignKey',
164
164
  'x-validator': 'uid',
@@ -192,7 +192,7 @@ export class M2MFieldInterface extends CollectionFieldInterface {
192
192
  required: true,
193
193
  default: '{{ useNewId("f_") }}',
194
194
  description:
195
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
195
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
196
196
  'x-decorator': 'FormItem',
197
197
  'x-component': 'ForeignKey',
198
198
  'x-validator': 'uid',
@@ -69,7 +69,7 @@ export class M2OFieldInterface extends CollectionFieldInterface {
69
69
  'x-decorator': 'FormItem',
70
70
  'x-component': 'Input',
71
71
  description:
72
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
72
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
73
73
  },
74
74
  type: relationshipType,
75
75
  grid: {
@@ -123,7 +123,7 @@ export class M2OFieldInterface extends CollectionFieldInterface {
123
123
  required: true,
124
124
  default: '{{ useNewId("f_") }}',
125
125
  description:
126
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
126
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
127
127
  'x-decorator': 'FormItem',
128
128
  'x-component': 'ForeignKey',
129
129
  'x-validator': 'uid',
@@ -42,7 +42,7 @@ export class NanoidFieldInterface extends CollectionFieldInterface {
42
42
  'x-component': 'Input',
43
43
  'x-disabled': '{{ !createOnly }}',
44
44
  description:
45
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
45
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
46
46
  },
47
47
  customAlphabet: {
48
48
  type: 'string',
@@ -68,7 +68,7 @@ export class O2MFieldInterface extends CollectionFieldInterface {
68
68
  'x-decorator': 'FormItem',
69
69
  'x-component': 'Input',
70
70
  description:
71
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
71
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
72
72
  },
73
73
  type: relationshipType,
74
74
  grid: {
@@ -135,7 +135,7 @@ export class O2MFieldInterface extends CollectionFieldInterface {
135
135
  required: true,
136
136
  default: '{{ useNewId("f_") }}',
137
137
  description:
138
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
138
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
139
139
  'x-decorator': 'FormItem',
140
140
  'x-component': 'ForeignKey',
141
141
  'x-validator': 'uid',
@@ -55,7 +55,7 @@ export class OBOFieldInterface extends CollectionFieldInterface {
55
55
  'x-decorator': 'FormItem',
56
56
  'x-component': 'Input',
57
57
  description:
58
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
58
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
59
59
  },
60
60
  type: relationshipType,
61
61
  grid: {
@@ -109,7 +109,7 @@ export class OBOFieldInterface extends CollectionFieldInterface {
109
109
  required: true,
110
110
  default: '{{ useNewId("f_") }}',
111
111
  description:
112
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
112
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
113
113
  'x-decorator': 'FormItem',
114
114
  'x-component': 'ForeignKey',
115
115
  'x-validator': 'uid',
@@ -55,7 +55,7 @@ export class OHOFieldInterface extends CollectionFieldInterface {
55
55
  'x-decorator': 'FormItem',
56
56
  'x-component': 'Input',
57
57
  description:
58
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
58
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
59
59
  },
60
60
  type: relationshipType,
61
61
  grid: {
@@ -123,7 +123,7 @@ export class OHOFieldInterface extends CollectionFieldInterface {
123
123
  required: true,
124
124
  default: '{{ useNewId("f_") }}',
125
125
  description:
126
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
126
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
127
127
  'x-decorator': 'FormItem',
128
128
  'x-component': 'ForeignKey',
129
129
  'x-validator': 'uid',
@@ -141,7 +141,7 @@ export const reverseFieldProperties: Record<string, ISchema> = {
141
141
  'x-validator': 'uid',
142
142
  'x-disabled': '{{ !showReverseFieldConfig }}',
143
143
  description:
144
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
144
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
145
145
  },
146
146
  },
147
147
  },
@@ -399,7 +399,7 @@ export const defaultProps = {
399
399
  'x-component': 'Input',
400
400
  'x-validator': 'uid',
401
401
  description:
402
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
402
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
403
403
  },
404
404
  };
405
405
 
@@ -46,7 +46,7 @@ export class UUIDFieldInterface extends CollectionFieldInterface {
46
46
  'x-component': 'Input',
47
47
  'x-disabled': '{{ !createOnly }}',
48
48
  description:
49
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
49
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
50
50
  },
51
51
  autoFill,
52
52
  layout: {
@@ -58,4 +58,11 @@ describe('getKey', () => {
58
58
 
59
59
  expect(getKey(a)).not.toBe(getKey(b));
60
60
  });
61
+
62
+ it('distinguishes RunJS openView route state overrides', () => {
63
+ const a = createViewItem({ viewUid: 'v', openViewRouteState: { mode: 'drawer' } }, 0);
64
+ const b = createViewItem({ viewUid: 'v', openViewRouteState: { mode: 'dialog' } }, 0);
65
+
66
+ expect(getKey(a)).not.toBe(getKey(b));
67
+ });
61
68
  });
@@ -250,6 +250,40 @@ describe('resolveViewParamsToViewList', () => {
250
250
  expect(result[0].hidden.value).toBe(false); // default drawer type
251
251
  expect(result[1].hidden.value).toBe(false); // default drawer type
252
252
  });
253
+
254
+ it('should let RunJS openView route state override persisted mode for hidden calculation', () => {
255
+ const model2 = createMockModel('view2', 'drawer');
256
+ mockFlowEngine.getModel = vi.fn().mockReturnValue(model2);
257
+
258
+ const viewParams: ViewParam[] = [
259
+ { viewUid: 'view1' },
260
+ { viewUid: 'view2', openViewRouteState: { mode: 'embed' } },
261
+ ];
262
+
263
+ const result = resolveViewParamsToViewList(mockFlowEngine, viewParams, mockRouteModel);
264
+ updateViewListHidden(result);
265
+
266
+ expect(result[0].hidden.value).toBe(true);
267
+ expect(result[1].hidden.value).toBe(false);
268
+ expect(model2.getStepParams).not.toHaveBeenCalled();
269
+ });
270
+
271
+ it('should treat child views as embed in mobile hidden calculation', () => {
272
+ const model2 = createMockModel('view2', 'drawer');
273
+ mockFlowEngine.getModel = vi.fn().mockReturnValue(model2);
274
+
275
+ const viewParams: ViewParam[] = [
276
+ { viewUid: 'view1' },
277
+ { viewUid: 'view2', openViewRouteState: { mode: 'dialog' } },
278
+ ];
279
+
280
+ const result = resolveViewParamsToViewList(mockFlowEngine, viewParams, mockRouteModel);
281
+ updateViewListHidden(result, true);
282
+
283
+ expect(result[0].hidden.value).toBe(true);
284
+ expect(result[1].hidden.value).toBe(false);
285
+ expect(model2.getStepParams).not.toHaveBeenCalled();
286
+ });
253
287
  });
254
288
 
255
289
  describe('edge cases', () => {
@@ -0,0 +1,73 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import {
11
+ FlowEngine,
12
+ FlowModel,
13
+ FlowRuntimeContext,
14
+ setupRuntimeContextSteps,
15
+ type Collection,
16
+ } from '@nocobase/flow-engine';
17
+ import { describe, expect, it } from 'vitest';
18
+ import { afterSuccess, getAfterSuccessResponseRecord, getMetaTreeWithResponseRecord } from '../afterSuccess';
19
+
20
+ describe('afterSuccess response record variable', () => {
21
+ it('reads the submit response record from the saveResource step', () => {
22
+ const record = { id: 1, title: 'Saved' };
23
+
24
+ expect(
25
+ getAfterSuccessResponseRecord({
26
+ steps: {
27
+ confirm: { params: {}, result: undefined },
28
+ saveResource: { params: {}, result: record },
29
+ afterSuccess: { params: {} },
30
+ },
31
+ }),
32
+ ).toBe(record);
33
+ });
34
+
35
+ it('injects responseRecord into the afterSuccess runtime context', async () => {
36
+ const record = { id: 1, title: 'Saved' };
37
+ const sourceCtx = {
38
+ steps: {
39
+ saveResource: { params: {}, result: record },
40
+ },
41
+ t: (value: string) => value,
42
+ };
43
+ const properties = await afterSuccess.defineProperties(sourceCtx as FlowRuntimeContext);
44
+
45
+ expect(properties.responseRecord.get()).toBe(record);
46
+ });
47
+
48
+ it('shows responseRecord in the variable picker for settings context', () => {
49
+ const engine = new FlowEngine();
50
+ class SubmitActionModel extends FlowModel {}
51
+ engine.registerModels({ SubmitActionModel });
52
+ const model = engine.createModel<SubmitActionModel>({ use: 'SubmitActionModel' });
53
+ const collection = {
54
+ name: 'users',
55
+ dataSourceKey: 'main',
56
+ getFilterByTK: (record: { id?: number }) => record.id,
57
+ } as unknown as Collection;
58
+ model.context.defineProperty('collection', { value: collection });
59
+ const flow = model.registerFlow({
60
+ key: 'submitSettings',
61
+ steps: {
62
+ saveResource: { title: 'Save record', handler: () => undefined },
63
+ afterSuccess: { use: 'afterSuccess' },
64
+ },
65
+ });
66
+ const ctx = new FlowRuntimeContext(model, 'submitSettings', 'settings');
67
+ setupRuntimeContextSteps(ctx, flow.steps, model, 'submitSettings');
68
+
69
+ const metaTree = getMetaTreeWithResponseRecord(ctx);
70
+
71
+ expect(metaTree.find((node) => node.name === 'responseRecord')?.title).toBe('Response record');
72
+ });
73
+ });
@@ -9,6 +9,7 @@
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';
14
15
 
@@ -50,6 +51,27 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
50
51
  return { ctx, engine };
51
52
  };
52
53
 
54
+ const createFirstStageCtx = (inputArgs: Record<PropertyKey, unknown>) => {
55
+ const navigateTo = vi.fn();
56
+ const ctx: any = {
57
+ inputArgs,
58
+ engine: { context: { themeToken: { colorBgLayout: '#fff' } } },
59
+ model: {
60
+ uid: 'popup-uid',
61
+ context: {
62
+ inputArgs: {},
63
+ defineProperty: vi.fn(),
64
+ },
65
+ flowEngine: { context: { themeToken: { colorBgLayout: '#fff' } } },
66
+ },
67
+ view: {
68
+ navigation: { navigateTo },
69
+ },
70
+ };
71
+
72
+ return { ctx, navigateTo };
73
+ };
74
+
53
75
  it('injects defineProperties/defineMethods on onModelLoaded even when not present in current inputArgs', async () => {
54
76
  const { ctx } = createRouteManagedCtx();
55
77
 
@@ -149,4 +171,86 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
149
171
  const defineMethodCalls = pageModelContext.defineMethod.mock.calls.map((c: any[]) => c[0]);
150
172
  expect(defineMethodCalls).toContain('pong');
151
173
  });
174
+
175
+ it('adds route state to first-stage navigation only for RunJS ctx.openView calls', async () => {
176
+ const { ctx, navigateTo } = createFirstStageCtx({
177
+ mode: 'dialog',
178
+ size: 'large',
179
+ [RUNJS_OPEN_VIEW_ROUTE_STATE]: { mode: 'dialog', size: 'large' },
180
+ });
181
+
182
+ await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
183
+
184
+ expect(navigateTo).toHaveBeenCalledWith({
185
+ viewUid: 'popup-uid',
186
+ filterByTk: undefined,
187
+ sourceId: undefined,
188
+ tabUid: undefined,
189
+ openViewRouteState: { mode: 'dialog', size: 'large' },
190
+ });
191
+ });
192
+
193
+ it('normalizes first-stage RunJS route state mode on mobile layout', async () => {
194
+ const { ctx, navigateTo } = createFirstStageCtx({
195
+ isMobileLayout: true,
196
+ mode: 'dialog',
197
+ size: 'large',
198
+ [RUNJS_OPEN_VIEW_ROUTE_STATE]: { mode: 'dialog', size: 'large' },
199
+ });
200
+
201
+ await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
202
+
203
+ expect(navigateTo).toHaveBeenCalledWith({
204
+ viewUid: 'popup-uid',
205
+ filterByTk: undefined,
206
+ sourceId: undefined,
207
+ tabUid: undefined,
208
+ openViewRouteState: { mode: 'embed', size: 'large' },
209
+ });
210
+ });
211
+
212
+ it('uses route replay mode and size before persisted openView defaults', async () => {
213
+ const { ctx } = createRouteManagedCtx();
214
+ ctx.inputArgs.mode = 'dialog';
215
+ ctx.inputArgs.size = 'large';
216
+
217
+ await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
218
+
219
+ expect(ctx.viewer.open).toHaveBeenCalledWith(
220
+ expect.objectContaining({
221
+ type: 'dialog',
222
+ width: '80%',
223
+ }),
224
+ );
225
+ });
226
+
227
+ it('uses decoded openView route state during route replay', async () => {
228
+ const { ctx } = createRouteManagedCtx();
229
+ ctx.inputArgs.openViewRouteState = { mode: 'dialog', size: 'large' };
230
+
231
+ await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
232
+
233
+ expect(ctx.viewer.open).toHaveBeenCalledWith(
234
+ expect.objectContaining({
235
+ type: 'dialog',
236
+ width: '80%',
237
+ }),
238
+ );
239
+ });
240
+
241
+ it('keeps first-stage navigation unchanged for non-RunJS openView calls', async () => {
242
+ const { ctx, navigateTo } = createFirstStageCtx({
243
+ mode: 'dialog',
244
+ size: 'large',
245
+ });
246
+
247
+ await openView.handler(ctx, { mode: 'drawer', size: 'medium', navigation: true });
248
+
249
+ expect(navigateTo).toHaveBeenCalledWith({
250
+ viewUid: 'popup-uid',
251
+ filterByTk: undefined,
252
+ sourceId: undefined,
253
+ tabUid: undefined,
254
+ });
255
+ });
152
256
  });
@@ -7,13 +7,152 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { defineAction, tExpr } from '@nocobase/flow-engine';
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 { TextAreaWithContextSelector } from '../components/TextAreaWithContextSelector';
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': TextAreaWithContextSelector,
188
+ 'x-component': AfterSuccessRedirectTextArea,
50
189
  'x-reactions': {
51
190
  dependencies: ['actionAfterSuccess'],
52
191
  fulfill: {
@@ -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) || !model?.parent?.collectionField?.isAssociationField?.()) {
31
+ if (!isTableColumnFieldSubModel(model)) {
32
32
  return;
33
33
  }
34
34
 
35
- model.parent.setProps(props);
35
+ model.parent?.setProps?.(props);
36
36
  };
37
37
 
38
38
  export const dateTimeFormat = defineAction({
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- export * from './afterSuccess';
10
+ export { afterSuccess } from './afterSuccess';
11
11
  export * from './confirm';
12
12
  export * from './dataScope';
13
13
  export * from './openView';