@nocobase/client-v2 2.1.0-alpha.35 → 2.1.0-alpha.37
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/components/code-editor/index.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +4 -0
- package/es/flow/models/fields/ClickableFieldModel.d.ts +3 -0
- package/es/flow/models/fields/DisplayTitleFieldModel.d.ts +2 -2
- package/es/index.mjs +49 -49
- package/lib/index.js +37 -37
- package/package.json +6 -5
- package/src/__tests__/globalDeps.test.ts +1 -0
- package/src/flow/actions/__tests__/pattern.test.ts +134 -0
- package/src/flow/actions/__tests__/titleField.test.ts +45 -0
- package/src/flow/actions/pattern.tsx +41 -6
- package/src/flow/actions/titleField.tsx +3 -1
- package/src/flow/components/DynamicFlowsIcon.tsx +87 -13
- package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +195 -8
- package/src/flow/components/code-editor/index.tsx +12 -8
- package/src/flow/models/blocks/form/FormActionModel.tsx +2 -8
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +144 -3
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +170 -1
- package/src/flow/models/fields/ClickableFieldModel.tsx +46 -2
- package/src/flow/models/fields/DisplayTitleFieldModel.tsx +40 -15
- package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +180 -2
- package/src/utils/globalDeps.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.1.0-alpha.
|
|
3
|
+
"version": "2.1.0-alpha.37",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
"@formily/antd-v5": "1.2.3",
|
|
25
25
|
"@formily/react": "^2.2.27",
|
|
26
26
|
"@formily/shared": "^2.2.27",
|
|
27
|
-
"@nocobase/
|
|
28
|
-
"@nocobase/
|
|
29
|
-
"@nocobase/
|
|
27
|
+
"@nocobase/evaluators": "2.1.0-alpha.37",
|
|
28
|
+
"@nocobase/flow-engine": "2.1.0-alpha.37",
|
|
29
|
+
"@nocobase/sdk": "2.1.0-alpha.37",
|
|
30
|
+
"@nocobase/shared": "2.1.0-alpha.37",
|
|
30
31
|
"ahooks": "^3.7.2",
|
|
31
32
|
"antd": "5.24.2",
|
|
32
33
|
"antd-style": "3.7.1",
|
|
@@ -40,5 +41,5 @@
|
|
|
40
41
|
"react-i18next": "^11.15.1",
|
|
41
42
|
"react-router-dom": "^6.30.1"
|
|
42
43
|
},
|
|
43
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "8b45f4586ea5b386b376188cfc1012ec12e9bc8b"
|
|
44
45
|
}
|
|
@@ -25,6 +25,7 @@ describe('client-v2 defineGlobalDeps', () => {
|
|
|
25
25
|
expect(define).toHaveBeenCalledWith('@nocobase/utils/client', expect.any(Function));
|
|
26
26
|
expect(define).toHaveBeenCalledWith('@nocobase/client-v2', expect.any(Function));
|
|
27
27
|
expect(define).toHaveBeenCalledWith('@nocobase/flow-engine', expect.any(Function));
|
|
28
|
+
expect(define).toHaveBeenCalledWith('@nocobase/evaluators/client', expect.any(Function));
|
|
28
29
|
expect(define).toHaveBeenCalledWith('ahooks', expect.any(Function));
|
|
29
30
|
expect(define).toHaveBeenCalledWith('dayjs', expect.any(Function));
|
|
30
31
|
expect(define).toHaveBeenCalledWith('lodash', expect.any(Function));
|
|
@@ -187,4 +187,138 @@ describe('pattern action', () => {
|
|
|
187
187
|
|
|
188
188
|
getDisplayBindingSpy.mockRestore();
|
|
189
189
|
});
|
|
190
|
+
|
|
191
|
+
it('falls back to the target collection title field for association display only mode', async () => {
|
|
192
|
+
const engine = new FlowEngine();
|
|
193
|
+
engine.registerModels({
|
|
194
|
+
DummyFormItemModel,
|
|
195
|
+
FieldModel,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const parent = engine.createModel<DummyFormItemModel>({
|
|
199
|
+
use: DummyFormItemModel,
|
|
200
|
+
uid: 'form-item-association',
|
|
201
|
+
subModels: {
|
|
202
|
+
field: {
|
|
203
|
+
use: FieldModel,
|
|
204
|
+
uid: 'field-association',
|
|
205
|
+
props: {},
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
const collectionField = {
|
|
210
|
+
targetCollectionTitleFieldName: 'name',
|
|
211
|
+
isAssociationField: () => true,
|
|
212
|
+
};
|
|
213
|
+
parent.collectionField = collectionField;
|
|
214
|
+
|
|
215
|
+
await pattern.beforeParamsSave?.(
|
|
216
|
+
{
|
|
217
|
+
model: parent,
|
|
218
|
+
collectionField,
|
|
219
|
+
} as any,
|
|
220
|
+
{ pattern: 'readPretty' },
|
|
221
|
+
{ pattern: 'editable' },
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
expect(parent.props).toMatchObject({
|
|
225
|
+
pattern: 'readPretty',
|
|
226
|
+
disabled: false,
|
|
227
|
+
titleField: 'name',
|
|
228
|
+
});
|
|
229
|
+
expect(parent.getStepParams('editItemSettings', 'titleField')).toEqual({
|
|
230
|
+
titleField: 'name',
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('passes the association title field to the rebuilt display field model', async () => {
|
|
235
|
+
const engine = new FlowEngine();
|
|
236
|
+
engine.registerModels({
|
|
237
|
+
DummyFormItemModel,
|
|
238
|
+
FieldModel,
|
|
239
|
+
DummyDisplayFieldModel,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const parent = engine.createModel<DummyFormItemModel>({
|
|
243
|
+
use: DummyFormItemModel,
|
|
244
|
+
uid: 'form-item-association-rebuild',
|
|
245
|
+
subModels: {
|
|
246
|
+
field: {
|
|
247
|
+
use: FieldModel,
|
|
248
|
+
uid: 'field-association-rebuild',
|
|
249
|
+
props: {},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
const targetTitleField = { name: 'name' };
|
|
254
|
+
const collectionField = {
|
|
255
|
+
targetCollectionTitleFieldName: 'name',
|
|
256
|
+
targetCollection: {
|
|
257
|
+
getField: vi.fn(() => targetTitleField),
|
|
258
|
+
},
|
|
259
|
+
isAssociationField: () => true,
|
|
260
|
+
};
|
|
261
|
+
parent.collectionField = collectionField;
|
|
262
|
+
const getDisplayBindingSpy = vi.spyOn(DetailsItemModel, 'getDefaultBindingByField').mockReturnValue({
|
|
263
|
+
modelName: 'DummyDisplayFieldModel',
|
|
264
|
+
defaultProps: { display: true },
|
|
265
|
+
} as any);
|
|
266
|
+
|
|
267
|
+
await pattern.afterParamsSave?.(
|
|
268
|
+
{
|
|
269
|
+
model: parent,
|
|
270
|
+
collectionField,
|
|
271
|
+
engine,
|
|
272
|
+
} as any,
|
|
273
|
+
{ pattern: 'readPretty' },
|
|
274
|
+
{ pattern: 'editable' },
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
expect(parent.subModels.field).toBeInstanceOf(DummyDisplayFieldModel);
|
|
278
|
+
expect(parent.subModels.field?.props).toMatchObject({
|
|
279
|
+
display: true,
|
|
280
|
+
titleField: 'name',
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
getDisplayBindingSpy.mockRestore();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('refreshes the parent model after leaving display only mode', async () => {
|
|
287
|
+
const engine = new FlowEngine();
|
|
288
|
+
engine.registerModels({
|
|
289
|
+
DummyFormItemModel,
|
|
290
|
+
FieldModel,
|
|
291
|
+
DummyDisplayFieldModel,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const host = engine.createModel<FlowModel>({
|
|
295
|
+
use: FlowModel,
|
|
296
|
+
uid: 'sub-table-host',
|
|
297
|
+
});
|
|
298
|
+
const parent = engine.createModel<DummyFormItemModel>({
|
|
299
|
+
use: DummyFormItemModel,
|
|
300
|
+
uid: 'sub-table-column',
|
|
301
|
+
subModels: {
|
|
302
|
+
field: {
|
|
303
|
+
use: FieldModel,
|
|
304
|
+
uid: 'sub-table-column-field',
|
|
305
|
+
stepParams: {
|
|
306
|
+
fieldBinding: {
|
|
307
|
+
use: 'DummyDisplayFieldModel',
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
parent.setParent(host);
|
|
314
|
+
const hostSetPropsSpy = vi.spyOn(host, 'setProps');
|
|
315
|
+
|
|
316
|
+
await pattern.afterParamsSave?.(makeCtx(parent), { pattern: 'editable' }, { pattern: 'readPretty' });
|
|
317
|
+
|
|
318
|
+
expect(parent.subModels.field).toBeInstanceOf(FieldModel);
|
|
319
|
+
expect(parent.subModels.field?.uid).toBe('sub-table-column-field');
|
|
320
|
+
expect(hostSetPropsSpy).toHaveBeenCalledWith({
|
|
321
|
+
__patternRefreshKey: expect.any(String),
|
|
322
|
+
});
|
|
323
|
+
});
|
|
190
324
|
});
|
|
@@ -0,0 +1,45 @@
|
|
|
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 { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { titleField } from '../titleField';
|
|
12
|
+
|
|
13
|
+
describe('titleField action', () => {
|
|
14
|
+
it('builds options from target field interface metadata', () => {
|
|
15
|
+
const titleableField = {
|
|
16
|
+
name: 'nickname',
|
|
17
|
+
title: 'Nickname',
|
|
18
|
+
getInterfaceOptions: vi.fn(() => ({ titleUsable: true })),
|
|
19
|
+
};
|
|
20
|
+
const nonTitleableField = {
|
|
21
|
+
name: 'profile',
|
|
22
|
+
title: 'Profile',
|
|
23
|
+
getInterfaceOptions: vi.fn(() => ({ titleUsable: false })),
|
|
24
|
+
};
|
|
25
|
+
const missingContextManager = {
|
|
26
|
+
collectionFieldInterfaceManager: {
|
|
27
|
+
getFieldInterface: vi.fn(() => undefined),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const uiMode = (titleField as any).uiMode({
|
|
32
|
+
dataSourceManager: missingContextManager,
|
|
33
|
+
collectionField: {
|
|
34
|
+
targetCollection: {
|
|
35
|
+
getFields: () => [titleableField, nonTitleableField],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(uiMode.props.options).toEqual([{ value: 'nickname', label: 'Nickname' }]);
|
|
41
|
+
expect(titleableField.getInterfaceOptions).toHaveBeenCalled();
|
|
42
|
+
expect(nonTitleableField.getInterfaceOptions).toHaveBeenCalled();
|
|
43
|
+
expect(missingContextManager.collectionFieldInterfaceManager.getFieldInterface).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { uid } from '@formily/shared';
|
|
10
11
|
import { defineAction, tExpr } from '@nocobase/flow-engine';
|
|
11
12
|
import { DetailsItemModel } from '../models/blocks/details/DetailsItemModel';
|
|
12
13
|
import { getFieldBindingUse, rebuildFieldSubModel } from '../internal/utils/rebuildFieldSubModel';
|
|
@@ -19,6 +20,14 @@ type PatternAwareFieldModel = {
|
|
|
19
20
|
scheduleApplyJsSettings?: () => void;
|
|
20
21
|
};
|
|
21
22
|
|
|
23
|
+
function resolveAssociationTitleField(ctx: any) {
|
|
24
|
+
return (
|
|
25
|
+
ctx.model.subModels?.field?.props?.fieldNames?.label ||
|
|
26
|
+
ctx.model.props.titleField ||
|
|
27
|
+
ctx.collectionField?.targetCollectionTitleFieldName
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
function shouldPreserveFieldModelOnPatternChange(ctx: any) {
|
|
23
32
|
const fieldModel = ctx.model.subModels.field;
|
|
24
33
|
const fieldUse = getFieldBindingUse(fieldModel) ?? fieldModel?.use;
|
|
@@ -27,6 +36,22 @@ function shouldPreserveFieldModelOnPatternChange(ctx: any) {
|
|
|
27
36
|
return ((ModelClass?.meta as PatternAwareFieldModelMeta | undefined)?.preserveOnPatternChange ?? false) === true;
|
|
28
37
|
}
|
|
29
38
|
|
|
39
|
+
async function refreshPatternRuntime(ctx: any) {
|
|
40
|
+
ctx.model.invalidateFlowCache?.('beforeRender', true);
|
|
41
|
+
await ctx.model.rerender?.();
|
|
42
|
+
|
|
43
|
+
const parent = ctx.model.parent;
|
|
44
|
+
if (!parent) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
parent.invalidateFlowCache?.('beforeRender', true);
|
|
49
|
+
parent.setProps?.({
|
|
50
|
+
__patternRefreshKey: uid(),
|
|
51
|
+
});
|
|
52
|
+
await parent.rerender?.();
|
|
53
|
+
}
|
|
54
|
+
|
|
30
55
|
export const pattern = defineAction({
|
|
31
56
|
name: 'pattern',
|
|
32
57
|
title: tExpr('Display mode'),
|
|
@@ -76,17 +101,25 @@ export const pattern = defineAction({
|
|
|
76
101
|
if (params.pattern !== previousParams.pattern) {
|
|
77
102
|
(ctx.model.subModels.field as PatternAwareFieldModel | undefined)?.scheduleApplyJsSettings?.();
|
|
78
103
|
}
|
|
104
|
+
await refreshPatternRuntime(ctx);
|
|
79
105
|
return;
|
|
80
106
|
}
|
|
81
107
|
|
|
82
108
|
const targetCollection = ctx.collectionField.targetCollection;
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
);
|
|
109
|
+
const associationTitleField = resolveAssociationTitleField(ctx);
|
|
110
|
+
const targetCollectionTitleField = targetCollection?.getField(associationTitleField);
|
|
86
111
|
const { model } = ctx;
|
|
87
112
|
const resolveDefaultProps = (binding, field = ctx.collectionField) => {
|
|
88
113
|
if (!binding) return undefined;
|
|
89
|
-
|
|
114
|
+
const defaultProps =
|
|
115
|
+
typeof binding.defaultProps === 'function' ? binding.defaultProps(ctx, field) : binding.defaultProps;
|
|
116
|
+
if (!ctx.collectionField?.isAssociationField?.() || !associationTitleField) {
|
|
117
|
+
return defaultProps;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
...(defaultProps || {}),
|
|
121
|
+
titleField: associationTitleField,
|
|
122
|
+
};
|
|
90
123
|
};
|
|
91
124
|
if (params.pattern === 'readPretty') {
|
|
92
125
|
const binding = DetailsItemModel.getDefaultBindingByField(ctx, ctx.collectionField, {
|
|
@@ -111,17 +144,19 @@ export const pattern = defineAction({
|
|
|
111
144
|
});
|
|
112
145
|
}
|
|
113
146
|
}
|
|
147
|
+
await refreshPatternRuntime(ctx);
|
|
114
148
|
},
|
|
115
149
|
async beforeParamsSave(ctx, params, previousParams) {
|
|
116
150
|
if (params.pattern === 'readPretty') {
|
|
151
|
+
const titleField = resolveAssociationTitleField(ctx);
|
|
117
152
|
ctx.model.setProps({
|
|
118
153
|
pattern: 'readPretty',
|
|
119
154
|
disabled: false,
|
|
120
|
-
titleField
|
|
155
|
+
titleField,
|
|
121
156
|
});
|
|
122
157
|
if (ctx.collectionField.isAssociationField())
|
|
123
158
|
await ctx.model.setStepParams('editItemSettings', 'titleField', {
|
|
124
|
-
titleField
|
|
159
|
+
titleField,
|
|
125
160
|
});
|
|
126
161
|
} else {
|
|
127
162
|
ctx.model.setProps({
|
|
@@ -43,7 +43,9 @@ export const titleField = defineAction({
|
|
|
43
43
|
const options = targetFields
|
|
44
44
|
.filter((field) =>
|
|
45
45
|
isTitleFieldInterface(
|
|
46
|
-
|
|
46
|
+
typeof field.getInterfaceOptions === 'function'
|
|
47
|
+
? field.getInterfaceOptions()
|
|
48
|
+
: getFlowFieldInterfaceOptions(field.options?.interface || field.interface, dataSourceManager),
|
|
47
49
|
),
|
|
48
50
|
)
|
|
49
51
|
.map((field) => ({
|
|
@@ -28,6 +28,9 @@ import {
|
|
|
28
28
|
GLOBAL_EMBED_CONTAINER_ID,
|
|
29
29
|
EMBED_REPLACING_DATA_KEY,
|
|
30
30
|
shouldHideEventInSettings,
|
|
31
|
+
DetachedFlowRegistry,
|
|
32
|
+
replaceFlowRegistry,
|
|
33
|
+
serializeFlowRegistry,
|
|
31
34
|
} from '@nocobase/flow-engine';
|
|
32
35
|
import { Collapse, Input, Button, Space, Tooltip, Empty, Dropdown, Select } from 'antd';
|
|
33
36
|
import { uid } from '@formily/shared';
|
|
@@ -35,6 +38,9 @@ import { useUpdate } from 'ahooks';
|
|
|
35
38
|
import _ from 'lodash';
|
|
36
39
|
|
|
37
40
|
type FlowOnObject = Exclude<FlowDefinition['on'], string | undefined>;
|
|
41
|
+
type FlowRegistryAvailability = {
|
|
42
|
+
hasFlow(flowKey: string): boolean;
|
|
43
|
+
};
|
|
38
44
|
|
|
39
45
|
function isFlowOnObject(on: FlowDefinition['on']): on is FlowOnObject {
|
|
40
46
|
return !!on && typeof on === 'object';
|
|
@@ -93,7 +99,7 @@ function validateFlowOnPhase(onObj: FlowOnObject): 'flowKey' | 'stepKey' | undef
|
|
|
93
99
|
|
|
94
100
|
export const DynamicFlowsIcon: React.FC<{ model: FlowModel }> = (props) => {
|
|
95
101
|
const { model } = props;
|
|
96
|
-
const t = model.translate.bind(model);
|
|
102
|
+
const t = React.useMemo(() => model.translate.bind(model), [model]);
|
|
97
103
|
|
|
98
104
|
const handleClick = () => {
|
|
99
105
|
const target = document.querySelector<HTMLDivElement>(`#${GLOBAL_EMBED_CONTAINER_ID}`);
|
|
@@ -190,7 +196,17 @@ const FieldLabel = ({
|
|
|
190
196
|
|
|
191
197
|
// 事件配置组件 - 独立的 observer 组件确保响应式更新
|
|
192
198
|
const EventConfigSection = observer(
|
|
193
|
-
({
|
|
199
|
+
({
|
|
200
|
+
flow,
|
|
201
|
+
model,
|
|
202
|
+
flowEngine,
|
|
203
|
+
flowRegistry,
|
|
204
|
+
}: {
|
|
205
|
+
flow: FlowDefinition;
|
|
206
|
+
model: FlowModel;
|
|
207
|
+
flowEngine: any;
|
|
208
|
+
flowRegistry: FlowRegistryAvailability;
|
|
209
|
+
}) => {
|
|
194
210
|
const ctx = useFlowContext<FlowEngineContext>();
|
|
195
211
|
const t = model.translate.bind(model);
|
|
196
212
|
const refresh = useUpdate();
|
|
@@ -270,9 +286,9 @@ const EventConfigSection = observer(
|
|
|
270
286
|
return staticFlows.map((f) => ({
|
|
271
287
|
value: f.key,
|
|
272
288
|
label: formatKeyWithTitle(String(f.key), f.title),
|
|
273
|
-
disabled:
|
|
289
|
+
disabled: flowRegistry.hasFlow(f.key),
|
|
274
290
|
}));
|
|
275
|
-
}, [
|
|
291
|
+
}, [flowRegistry, formatKeyWithTitle, staticFlows]);
|
|
276
292
|
|
|
277
293
|
const stepOptions = React.useMemo(() => {
|
|
278
294
|
if (!flowKeyValue) return [];
|
|
@@ -446,12 +462,58 @@ const DynamicFlowsEditor = observer((props: { model: FlowModel }) => {
|
|
|
446
462
|
const { model } = props;
|
|
447
463
|
const ctx = useFlowContext<FlowEngineContext>();
|
|
448
464
|
const flowEngine = model.flowEngine;
|
|
465
|
+
const latestFlows = serializeFlowRegistry(model.flowRegistry);
|
|
466
|
+
const initialFlowsRef = React.useRef(latestFlows);
|
|
467
|
+
const [draftFlowRegistry] = React.useState(() => new DetachedFlowRegistry(latestFlows));
|
|
449
468
|
const [submitLoading, setSubmitLoading] = React.useState(false);
|
|
450
|
-
const t = model.translate.bind(model);
|
|
469
|
+
const t = React.useMemo(() => model.translate.bind(model), [model]);
|
|
470
|
+
const hasUnsavedChanges = React.useCallback(() => {
|
|
471
|
+
return !_.isEqual(initialFlowsRef.current, serializeFlowRegistry(draftFlowRegistry));
|
|
472
|
+
}, [draftFlowRegistry]);
|
|
473
|
+
|
|
474
|
+
React.useEffect(() => {
|
|
475
|
+
if (_.isEqual(initialFlowsRef.current, latestFlows) || hasUnsavedChanges()) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
replaceFlowRegistry(draftFlowRegistry, latestFlows);
|
|
480
|
+
initialFlowsRef.current = latestFlows;
|
|
481
|
+
}, [draftFlowRegistry, hasUnsavedChanges, latestFlows]);
|
|
482
|
+
|
|
483
|
+
React.useEffect(() => {
|
|
484
|
+
const view = ctx.view;
|
|
485
|
+
const previousBeforeClose = view.beforeClose;
|
|
486
|
+
const beforeClose = async (payload) => {
|
|
487
|
+
if (hasUnsavedChanges()) {
|
|
488
|
+
const confirmed =
|
|
489
|
+
(await ctx.modal?.confirm?.({
|
|
490
|
+
title: t('Unsaved changes'),
|
|
491
|
+
content: t("Are you sure you don't want to save?"),
|
|
492
|
+
okText: t('Confirm'),
|
|
493
|
+
cancelText: t('Cancel'),
|
|
494
|
+
})) ?? true;
|
|
495
|
+
|
|
496
|
+
if (!confirmed) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const result = await previousBeforeClose?.(payload);
|
|
502
|
+
return result !== false;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
view.beforeClose = beforeClose;
|
|
506
|
+
|
|
507
|
+
return () => {
|
|
508
|
+
if (view.beforeClose === beforeClose) {
|
|
509
|
+
view.beforeClose = previousBeforeClose;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}, [ctx, hasUnsavedChanges, t]);
|
|
451
513
|
|
|
452
514
|
// 添加新流
|
|
453
515
|
const handleAddFlow = () => {
|
|
454
|
-
|
|
516
|
+
draftFlowRegistry.addFlow(uid(), {
|
|
455
517
|
title: t('Event flow'),
|
|
456
518
|
steps: {},
|
|
457
519
|
});
|
|
@@ -516,7 +578,7 @@ const DynamicFlowsEditor = observer((props: { model: FlowModel }) => {
|
|
|
516
578
|
);
|
|
517
579
|
|
|
518
580
|
// 生成折叠面板项
|
|
519
|
-
const collapseItems =
|
|
581
|
+
const collapseItems = draftFlowRegistry.mapFlows((flow) => {
|
|
520
582
|
return {
|
|
521
583
|
key: flow.key,
|
|
522
584
|
label: renderPanelHeader(flow),
|
|
@@ -529,7 +591,7 @@ const DynamicFlowsEditor = observer((props: { model: FlowModel }) => {
|
|
|
529
591
|
children: (
|
|
530
592
|
<div>
|
|
531
593
|
{/* 事件部分 */}
|
|
532
|
-
<EventConfigSection flow={flow} model={model} flowEngine={flowEngine} />
|
|
594
|
+
<EventConfigSection flow={flow} model={model} flowEngine={flowEngine} flowRegistry={draftFlowRegistry} />
|
|
533
595
|
|
|
534
596
|
{/* 步骤部分 */}
|
|
535
597
|
<div>
|
|
@@ -670,13 +732,13 @@ const DynamicFlowsEditor = observer((props: { model: FlowModel }) => {
|
|
|
670
732
|
flexShrink: 0,
|
|
671
733
|
}}
|
|
672
734
|
>
|
|
673
|
-
<Button onClick={() => ctx.view.
|
|
735
|
+
<Button onClick={() => ctx.view.close()}>{t('Cancel')}</Button>
|
|
674
736
|
<Button
|
|
675
737
|
type="primary"
|
|
676
738
|
loading={submitLoading}
|
|
677
739
|
onClick={async () => {
|
|
678
740
|
setSubmitLoading(true);
|
|
679
|
-
const invalid =
|
|
741
|
+
const invalid = draftFlowRegistry
|
|
680
742
|
.mapFlows((flow) => {
|
|
681
743
|
if (!isFlowOnObject(flow.on)) return;
|
|
682
744
|
normalizeFlowOnPhase(flow.on);
|
|
@@ -695,10 +757,11 @@ const DynamicFlowsEditor = observer((props: { model: FlowModel }) => {
|
|
|
695
757
|
setSubmitLoading(false);
|
|
696
758
|
return;
|
|
697
759
|
}
|
|
698
|
-
|
|
699
|
-
|
|
760
|
+
const previousFlows = serializeFlowRegistry(model.flowRegistry);
|
|
761
|
+
const afterSaves: Array<() => Promise<void>> = [];
|
|
700
762
|
|
|
701
|
-
|
|
763
|
+
try {
|
|
764
|
+
for (const flow of draftFlowRegistry.mapFlows((it) => it)) {
|
|
702
765
|
for (const step of flow.mapSteps((it) => it)) {
|
|
703
766
|
const serialized = step.serialize();
|
|
704
767
|
const actionDef = step.use ? model.getAction(step.use) : undefined;
|
|
@@ -731,8 +794,19 @@ const DynamicFlowsEditor = observer((props: { model: FlowModel }) => {
|
|
|
731
794
|
}
|
|
732
795
|
}
|
|
733
796
|
|
|
797
|
+
replaceFlowRegistry(model.flowRegistry, serializeFlowRegistry(draftFlowRegistry));
|
|
734
798
|
await model.flowRegistry.save();
|
|
799
|
+
} catch (error) {
|
|
800
|
+
replaceFlowRegistry(model.flowRegistry, previousFlows);
|
|
801
|
+
setSubmitLoading(false);
|
|
802
|
+
model.context?.message?.error?.('Steps post-save hooks failed to run');
|
|
803
|
+
console.error(error);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
735
806
|
|
|
807
|
+
initialFlowsRef.current = serializeFlowRegistry(model.flowRegistry);
|
|
808
|
+
|
|
809
|
+
try {
|
|
736
810
|
for (const runAfterSave of afterSaves) {
|
|
737
811
|
await runAfterSave();
|
|
738
812
|
}
|