@nocobase/flow-engine 2.0.0-beta.9 → 2.0.0
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/lib/BlockScopedFlowEngine.js +0 -1
- package/lib/FlowDefinition.d.ts +2 -0
- package/lib/JSRunner.d.ts +6 -0
- package/lib/JSRunner.js +32 -2
- package/lib/ViewScopedFlowEngine.js +3 -0
- package/lib/acl/Acl.js +13 -3
- package/lib/components/FlowContextSelector.js +155 -10
- package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
- package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +5 -1
- package/lib/components/variables/VariableInput.js +9 -4
- package/lib/components/variables/VariableTag.js +46 -39
- package/lib/components/variables/utils.d.ts +7 -0
- package/lib/components/variables/utils.js +42 -2
- package/lib/data-source/index.d.ts +7 -27
- package/lib/data-source/index.js +81 -51
- package/lib/executor/FlowExecutor.d.ts +2 -1
- package/lib/executor/FlowExecutor.js +163 -22
- package/lib/flowContext.d.ts +230 -7
- package/lib/flowContext.js +2267 -148
- package/lib/flowEngine.d.ts +21 -0
- package/lib/flowEngine.js +56 -8
- package/lib/flowI18n.js +6 -4
- package/lib/flowSettings.js +17 -11
- package/lib/index.d.ts +7 -1
- package/lib/index.js +21 -0
- package/lib/locale/en-US.json +9 -2
- package/lib/locale/index.d.ts +14 -0
- package/lib/locale/zh-CN.json +8 -1
- package/lib/models/CollectionFieldModel.d.ts +1 -0
- package/lib/models/CollectionFieldModel.js +3 -2
- package/lib/models/flowModel.js +12 -1
- package/lib/provider.js +5 -5
- package/lib/resources/baseRecordResource.d.ts +5 -0
- package/lib/resources/baseRecordResource.js +24 -0
- package/lib/resources/multiRecordResource.d.ts +1 -0
- package/lib/resources/multiRecordResource.js +11 -4
- package/lib/resources/singleRecordResource.js +2 -0
- package/lib/resources/sqlResource.d.ts +4 -3
- package/lib/resources/sqlResource.js +8 -3
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/base.js +706 -41
- package/lib/runjs-context/contributions.d.ts +33 -0
- package/lib/runjs-context/contributions.js +88 -0
- package/lib/runjs-context/helpers.js +12 -1
- package/lib/runjs-context/setup.js +6 -0
- package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
- package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
- package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
- package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
- package/lib/runjs-context/snippets/index.d.ts +11 -1
- package/lib/runjs-context/snippets/index.js +61 -40
- package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
- package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
- package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
- package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
- package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
- package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
- package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
- package/lib/runjsLibs.d.ts +28 -0
- package/lib/runjsLibs.js +532 -0
- package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
- package/lib/scheduler/ModelOperationScheduler.js +25 -21
- package/lib/types.d.ts +27 -0
- package/lib/utils/associationObjectVariable.d.ts +2 -2
- package/lib/utils/createCollectionContextMeta.js +1 -0
- package/lib/utils/createEphemeralContext.js +2 -2
- package/lib/utils/dateVariable.d.ts +16 -0
- package/lib/utils/dateVariable.js +380 -0
- package/lib/utils/exceptions.d.ts +7 -0
- package/lib/utils/exceptions.js +10 -0
- package/lib/utils/index.d.ts +8 -3
- package/lib/utils/index.js +45 -0
- package/lib/utils/params-resolvers.js +16 -9
- package/lib/utils/resolveModuleUrl.d.ts +58 -0
- package/lib/utils/resolveModuleUrl.js +65 -0
- package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
- package/lib/utils/resolveRunJSObjectValues.js +61 -0
- package/lib/utils/runjsModuleLoader.d.ts +58 -0
- package/lib/utils/runjsModuleLoader.js +422 -0
- package/lib/utils/runjsTemplateCompat.d.ts +35 -0
- package/lib/utils/runjsTemplateCompat.js +743 -0
- package/lib/utils/runjsValue.d.ts +29 -0
- package/lib/utils/runjsValue.js +275 -0
- package/lib/utils/safeGlobals.d.ts +18 -8
- package/lib/utils/safeGlobals.js +164 -17
- package/lib/utils/schema-utils.d.ts +10 -0
- package/lib/utils/schema-utils.js +61 -0
- package/lib/views/createViewMeta.d.ts +0 -7
- package/lib/views/createViewMeta.js +19 -70
- package/lib/views/index.d.ts +1 -2
- package/lib/views/index.js +4 -3
- package/lib/views/useDialog.js +7 -2
- package/lib/views/useDrawer.js +7 -2
- package/lib/views/usePage.d.ts +4 -0
- package/lib/views/usePage.js +43 -6
- package/lib/views/usePopover.js +4 -1
- package/lib/views/viewEvents.d.ts +17 -0
- package/lib/views/viewEvents.js +90 -0
- package/package.json +4 -4
- package/src/BlockScopedFlowEngine.ts +2 -5
- package/src/JSRunner.ts +44 -2
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/JSRunner.test.ts +64 -0
- package/src/__tests__/createViewMeta.popup.test.ts +62 -1
- package/src/__tests__/flowContext.test.ts +693 -1
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
- package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
- package/src/__tests__/flowRuntimeContext.test.ts +2 -1
- package/src/__tests__/flowSettings.open.test.tsx +123 -19
- package/src/__tests__/runjsContext.test.ts +10 -7
- package/src/__tests__/runjsContextImplementations.test.ts +34 -3
- package/src/__tests__/runjsContextRuntime.test.ts +3 -3
- package/src/__tests__/runjsContributions.test.ts +89 -0
- package/src/__tests__/runjsExternalLibs.test.ts +242 -0
- package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
- package/src/__tests__/runjsLocales.test.ts +4 -1
- package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
- package/src/__tests__/runjsSnippets.test.ts +40 -3
- package/src/acl/Acl.tsx +3 -3
- package/src/components/FlowContextSelector.tsx +208 -12
- package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
- package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
- package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +13 -2
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
- package/src/components/variables/VariableInput.tsx +12 -4
- package/src/components/variables/VariableTag.tsx +54 -45
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
- package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
- package/src/components/variables/__tests__/utils.test.ts +81 -3
- package/src/components/variables/utils.ts +67 -6
- package/src/data-source/index.ts +85 -110
- package/src/executor/FlowExecutor.ts +200 -23
- package/src/executor/__tests__/flowExecutor.test.ts +66 -0
- package/src/flowContext.ts +2986 -211
- package/src/flowEngine.ts +59 -8
- package/src/flowI18n.ts +7 -5
- package/src/flowSettings.ts +18 -12
- package/src/index.ts +14 -1
- package/src/locale/en-US.json +9 -2
- package/src/locale/zh-CN.json +8 -1
- package/src/models/CollectionFieldModel.tsx +3 -1
- package/src/models/__tests__/dispatchEvent.when.test.ts +554 -0
- package/src/models/__tests__/flowModel.test.ts +20 -4
- package/src/models/flowModel.tsx +13 -1
- package/src/provider.tsx +7 -6
- package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
- package/src/resources/__tests__/sqlResource.test.ts +60 -0
- package/src/resources/baseRecordResource.ts +31 -0
- package/src/resources/multiRecordResource.ts +11 -4
- package/src/resources/singleRecordResource.ts +3 -0
- package/src/resources/sqlResource.ts +11 -6
- package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
- package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/base.ts +715 -44
- package/src/runjs-context/contributions.ts +88 -0
- package/src/runjs-context/helpers.ts +11 -1
- package/src/runjs-context/setup.ts +6 -0
- package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
- package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
- package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
- package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
- package/src/runjs-context/snippets/index.ts +75 -41
- package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
- package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
- package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
- package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
- package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
- package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
- package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
- package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
- package/src/runjsLibs.ts +622 -0
- package/src/scheduler/ModelOperationScheduler.ts +27 -21
- package/src/types.ts +38 -1
- package/src/utils/__tests__/dateVariable.test.ts +101 -0
- package/src/utils/__tests__/params-resolvers.test.ts +40 -0
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
- package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
- package/src/utils/__tests__/runjsValue.test.ts +44 -0
- package/src/utils/__tests__/safeGlobals.test.ts +57 -2
- package/src/utils/__tests__/utils.test.ts +95 -0
- package/src/utils/associationObjectVariable.ts +2 -2
- package/src/utils/createCollectionContextMeta.ts +1 -0
- package/src/utils/createEphemeralContext.ts +5 -4
- package/src/utils/dateVariable.ts +397 -0
- package/src/utils/exceptions.ts +11 -0
- package/src/utils/index.ts +37 -3
- package/src/utils/params-resolvers.ts +23 -9
- package/src/utils/resolveModuleUrl.ts +91 -0
- package/src/utils/resolveRunJSObjectValues.ts +46 -0
- package/src/utils/runjsModuleLoader.ts +553 -0
- package/src/utils/runjsTemplateCompat.ts +828 -0
- package/src/utils/runjsValue.ts +287 -0
- package/src/utils/safeGlobals.ts +188 -17
- package/src/utils/schema-utils.ts +79 -0
- package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
- package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
- package/src/views/createViewMeta.ts +22 -75
- package/src/views/index.tsx +1 -2
- package/src/views/useDialog.tsx +8 -1
- package/src/views/useDrawer.tsx +8 -1
- package/src/views/usePage.tsx +51 -5
- package/src/views/usePopover.tsx +4 -1
- package/src/views/viewEvents.ts +55 -0
|
@@ -7,14 +7,19 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { DeleteOutlined, ExclamationCircleOutlined, SettingOutlined } from '@ant-design/icons';
|
|
10
|
+
import { DeleteOutlined, ExclamationCircleOutlined, QuestionCircleOutlined, SettingOutlined } from '@ant-design/icons';
|
|
11
11
|
import type { MenuProps } from 'antd';
|
|
12
|
-
import { Alert, Dropdown, Modal } from 'antd';
|
|
12
|
+
import { Alert, Dropdown, Modal, Tooltip, theme } from 'antd';
|
|
13
13
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
14
14
|
import { FlowRuntimeContext } from '../../../../flowContext';
|
|
15
15
|
import { useFlowModelById } from '../../../../hooks';
|
|
16
16
|
import { FlowModel } from '../../../../models';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
getT,
|
|
19
|
+
resolveStepDisabledInSettings,
|
|
20
|
+
setupRuntimeContextSteps,
|
|
21
|
+
shouldHideStepInSettings,
|
|
22
|
+
} from '../../../../utils';
|
|
18
23
|
import { openStepSettingsDialog } from './StepSettingsDialog';
|
|
19
24
|
import { ActionDefinition } from '../../../../types';
|
|
20
25
|
import { observer } from '../../../../reactive';
|
|
@@ -74,6 +79,21 @@ const FlowsContextMenu: React.FC<FlowsContextMenuProps> = (props) => {
|
|
|
74
79
|
const FlowsContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
75
80
|
({ model, children, enabled = true, position = 'right', showDeleteButton = true }) => {
|
|
76
81
|
const t = getT(model);
|
|
82
|
+
const { token } = theme.useToken();
|
|
83
|
+
const disabledIconColor = token?.colorTextTertiary || token?.colorTextDescription || token?.colorTextSecondary;
|
|
84
|
+
const [configurableFlowsAndSteps, setConfigurableFlowsAndSteps] = useState<any[]>([]);
|
|
85
|
+
const isStepMenuItemDisabled = useCallback(
|
|
86
|
+
(key: string) => {
|
|
87
|
+
const [flowKey, stepKey] = key.split(':');
|
|
88
|
+
if (!flowKey || !stepKey) return false;
|
|
89
|
+
return configurableFlowsAndSteps.some(({ flow, steps }) => {
|
|
90
|
+
if (flow.key !== flowKey) return false;
|
|
91
|
+
return steps.some((stepInfo) => stepInfo.stepKey === stepKey && !!stepInfo.disabled);
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
[configurableFlowsAndSteps],
|
|
95
|
+
);
|
|
96
|
+
|
|
77
97
|
const handleMenuClick = useCallback(
|
|
78
98
|
({ key }: { key: string }) => {
|
|
79
99
|
if (key === 'delete') {
|
|
@@ -99,6 +119,9 @@ const FlowsContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
|
99
119
|
},
|
|
100
120
|
});
|
|
101
121
|
} else {
|
|
122
|
+
if (isStepMenuItemDisabled(key)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
102
125
|
// 处理step配置,key格式为 "flowKey:stepKey"
|
|
103
126
|
const [flowKey, stepKey] = key.split(':');
|
|
104
127
|
try {
|
|
@@ -127,7 +150,7 @@ const FlowsContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
|
127
150
|
}
|
|
128
151
|
}
|
|
129
152
|
},
|
|
130
|
-
[model],
|
|
153
|
+
[isStepMenuItemDisabled, model],
|
|
131
154
|
);
|
|
132
155
|
|
|
133
156
|
if (!model) {
|
|
@@ -156,6 +179,7 @@ const FlowsContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
|
156
179
|
if (await shouldHideStepInSettings(model as FlowModel, flow, actionStep)) {
|
|
157
180
|
return null;
|
|
158
181
|
}
|
|
182
|
+
const disabledState = await resolveStepDisabledInSettings(model as FlowModel, flow, actionStep);
|
|
159
183
|
|
|
160
184
|
// 从step获取uiSchema(如果存在)
|
|
161
185
|
const stepUiSchema: ActionDefinition['uiSchema'] = actionStep.uiSchema || {};
|
|
@@ -191,6 +215,8 @@ const FlowsContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
|
191
215
|
step: actionStep,
|
|
192
216
|
uiSchema: mergedUiSchema,
|
|
193
217
|
title: actionStep.title || stepKey,
|
|
218
|
+
disabled: disabledState.disabled,
|
|
219
|
+
disabledReason: disabledState.reason,
|
|
194
220
|
};
|
|
195
221
|
}),
|
|
196
222
|
).then((steps) => steps.filter(Boolean));
|
|
@@ -206,8 +232,6 @@ const FlowsContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
|
206
232
|
}
|
|
207
233
|
}, [model]);
|
|
208
234
|
|
|
209
|
-
const [configurableFlowsAndSteps, setConfigurableFlowsAndSteps] = useState<any[]>([]);
|
|
210
|
-
|
|
211
235
|
useEffect(() => {
|
|
212
236
|
let mounted = true;
|
|
213
237
|
(async () => {
|
|
@@ -243,7 +267,17 @@ const FlowsContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
|
243
267
|
menuItems.push({
|
|
244
268
|
key: `${flow.key}:${stepInfo.stepKey}`,
|
|
245
269
|
icon: <SettingOutlined />,
|
|
246
|
-
label: stepInfo.
|
|
270
|
+
label: stepInfo.disabled ? (
|
|
271
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
|
272
|
+
{stepInfo.title}
|
|
273
|
+
<Tooltip title={stepInfo.disabledReason} placement="right" destroyTooltipOnHide>
|
|
274
|
+
<QuestionCircleOutlined style={{ color: disabledIconColor }} />
|
|
275
|
+
</Tooltip>
|
|
276
|
+
</span>
|
|
277
|
+
) : (
|
|
278
|
+
stepInfo.title
|
|
279
|
+
),
|
|
280
|
+
disabled: !!stepInfo.disabled,
|
|
247
281
|
});
|
|
248
282
|
});
|
|
249
283
|
});
|
|
@@ -14,7 +14,15 @@ import { Button, Space } from 'antd';
|
|
|
14
14
|
import React, { useEffect } from 'react';
|
|
15
15
|
import { FlowSettingsContextProvider, useFlowSettingsContext } from '../../../../hooks/useFlowSettingsContext';
|
|
16
16
|
import { StepSettingsDialogProps } from '../../../../types';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
compileUiSchema,
|
|
19
|
+
FlowExitException,
|
|
20
|
+
getT,
|
|
21
|
+
resolveDefaultParams,
|
|
22
|
+
resolveStepUiSchema,
|
|
23
|
+
FlowCancelSaveException,
|
|
24
|
+
} from '../../../../utils';
|
|
25
|
+
import { FlowExitAllException } from '../../../../utils/exceptions';
|
|
18
26
|
import { observer } from '../../../../reactive';
|
|
19
27
|
|
|
20
28
|
const SchemaField = createSchemaField();
|
|
@@ -203,7 +211,10 @@ const openStepSettingsDialog = async ({
|
|
|
203
211
|
await afterParamsSave(flowRuntimeContext, currentValues, previousParams);
|
|
204
212
|
}
|
|
205
213
|
} catch (error) {
|
|
206
|
-
if (error instanceof
|
|
214
|
+
if (error instanceof FlowCancelSaveException) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (error instanceof FlowExitException || error instanceof FlowExitAllException) {
|
|
207
218
|
currentDialog.close();
|
|
208
219
|
return;
|
|
209
220
|
}
|
|
@@ -18,6 +18,7 @@ import { DefaultSettingsIcon } from '../DefaultSettingsIcon';
|
|
|
18
18
|
|
|
19
19
|
// ---- Mock antd to capture Dropdown menu props ----
|
|
20
20
|
const dropdownMenus: any[] = [];
|
|
21
|
+
const mockColorTextTertiary = '#8c8c8c';
|
|
21
22
|
vi.mock('antd', async (importOriginal) => {
|
|
22
23
|
const messageApi = {
|
|
23
24
|
success: vi.fn(),
|
|
@@ -35,6 +36,7 @@ vi.mock('antd', async (importOriginal) => {
|
|
|
35
36
|
const Dropdown = (props: any) => {
|
|
36
37
|
(globalThis as any).__lastDropdownMenu = props.menu;
|
|
37
38
|
(globalThis as any).__lastDropdownOnOpenChange = props.onOpenChange;
|
|
39
|
+
(globalThis as any).__lastDropdownOpen = props.open;
|
|
38
40
|
dropdownMenus.push(props.menu);
|
|
39
41
|
return React.createElement('span', { 'data-testid': 'dropdown' }, props.children);
|
|
40
42
|
};
|
|
@@ -71,6 +73,7 @@ vi.mock('antd', async (importOriginal) => {
|
|
|
71
73
|
const Alert = (props: any) => React.createElement('div', { role: 'alert' }, props.message ?? 'Alert');
|
|
72
74
|
const Button = (props: any) => React.createElement('button', props, props.children ?? 'Button');
|
|
73
75
|
const Result = (props: any) => React.createElement('div', null, props.children ?? 'Result');
|
|
76
|
+
const Tooltip = ({ children }: any) => React.createElement('span', null, children);
|
|
74
77
|
|
|
75
78
|
// Keep other components from original mock/default
|
|
76
79
|
return {
|
|
@@ -89,15 +92,46 @@ vi.mock('antd', async (importOriginal) => {
|
|
|
89
92
|
Alert,
|
|
90
93
|
Button,
|
|
91
94
|
Result,
|
|
92
|
-
|
|
95
|
+
Tooltip,
|
|
96
|
+
theme: { useToken: () => ({ token: { colorTextTertiary: mockColorTextTertiary } }) },
|
|
93
97
|
};
|
|
94
98
|
});
|
|
95
99
|
|
|
100
|
+
const findElement = (node: any, predicate: (element: React.ReactElement) => boolean): React.ReactElement | null => {
|
|
101
|
+
if (!node) return null;
|
|
102
|
+
|
|
103
|
+
if (React.isValidElement(node)) {
|
|
104
|
+
if (predicate(node)) {
|
|
105
|
+
return node;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const children = React.Children.toArray(node.props?.children);
|
|
109
|
+
for (const child of children) {
|
|
110
|
+
const matched = findElement(child, predicate);
|
|
111
|
+
if (matched) {
|
|
112
|
+
return matched;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (Array.isArray(node)) {
|
|
118
|
+
for (const child of node) {
|
|
119
|
+
const matched = findElement(child, predicate);
|
|
120
|
+
if (matched) {
|
|
121
|
+
return matched;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
};
|
|
128
|
+
|
|
96
129
|
describe('DefaultSettingsIcon - only static flows are shown', () => {
|
|
97
130
|
beforeEach(() => {
|
|
98
131
|
dropdownMenus.length = 0;
|
|
99
132
|
(globalThis as any).__lastDropdownMenu = undefined;
|
|
100
133
|
(globalThis as any).__lastDropdownOnOpenChange = undefined;
|
|
134
|
+
(globalThis as any).__lastDropdownOpen = undefined;
|
|
101
135
|
});
|
|
102
136
|
|
|
103
137
|
afterEach(() => {
|
|
@@ -239,6 +273,67 @@ describe('DefaultSettingsIcon - only static flows are shown', () => {
|
|
|
239
273
|
});
|
|
240
274
|
});
|
|
241
275
|
|
|
276
|
+
it('keeps disabled legacy step visible with tooltip and blocks click', async () => {
|
|
277
|
+
class TestFlowModel extends FlowModel {}
|
|
278
|
+
const engine = new FlowEngine();
|
|
279
|
+
const model = new TestFlowModel({ uid: 'm-disabled', flowEngine: engine });
|
|
280
|
+
const openSpy = vi.spyOn(model, 'openFlowSettings').mockResolvedValue(undefined as any);
|
|
281
|
+
const disabledReason = 'This setting has been moved to: Form block settings > Field values';
|
|
282
|
+
|
|
283
|
+
TestFlowModel.registerFlow({
|
|
284
|
+
key: 'flowDisabled',
|
|
285
|
+
title: 'Flow Disabled',
|
|
286
|
+
steps: {
|
|
287
|
+
legacyDefault: {
|
|
288
|
+
title: 'Default value',
|
|
289
|
+
disabledInSettings: true,
|
|
290
|
+
disabledReasonInSettings: disabledReason,
|
|
291
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
render(
|
|
297
|
+
React.createElement(
|
|
298
|
+
ConfigProvider as any,
|
|
299
|
+
null,
|
|
300
|
+
React.createElement(App as any, null, React.createElement(DefaultSettingsIcon as any, { model })),
|
|
301
|
+
),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
let disabledItem: any;
|
|
305
|
+
await waitFor(() => {
|
|
306
|
+
const menu = (globalThis as any).__lastDropdownMenu;
|
|
307
|
+
const items = (menu?.items || []) as any[];
|
|
308
|
+
disabledItem = items.find((it) => String(it.key || '') === 'flowDisabled:legacyDefault');
|
|
309
|
+
expect(disabledItem).toBeTruthy();
|
|
310
|
+
expect(disabledItem.disabled).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const resolvedLabel =
|
|
314
|
+
React.isValidElement(disabledItem.label) && typeof disabledItem.label.type === 'function'
|
|
315
|
+
? (disabledItem.label.type as any)(disabledItem.label.props)
|
|
316
|
+
: disabledItem.label;
|
|
317
|
+
|
|
318
|
+
const tooltipElement = findElement(
|
|
319
|
+
resolvedLabel,
|
|
320
|
+
(element) =>
|
|
321
|
+
Object.prototype.hasOwnProperty.call(element.props || {}, 'title') && element.props.title === disabledReason,
|
|
322
|
+
);
|
|
323
|
+
expect(tooltipElement).toBeTruthy();
|
|
324
|
+
|
|
325
|
+
const iconElement = React.isValidElement(tooltipElement) ? tooltipElement.props.children : null;
|
|
326
|
+
expect(React.isValidElement(iconElement)).toBe(true);
|
|
327
|
+
expect((iconElement as any).props?.style?.color).toBe(mockColorTextTertiary);
|
|
328
|
+
|
|
329
|
+
const menu = (globalThis as any).__lastDropdownMenu;
|
|
330
|
+
await act(async () => {
|
|
331
|
+
menu.onClick?.({ key: 'flowDisabled:legacyDefault' });
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
expect(openSpy).not.toHaveBeenCalled();
|
|
335
|
+
});
|
|
336
|
+
|
|
242
337
|
it('clicking a step item opens flow settings with correct args', async () => {
|
|
243
338
|
class TestFlowModel extends FlowModel {}
|
|
244
339
|
const engine = new FlowEngine();
|
|
@@ -265,10 +360,60 @@ describe('DefaultSettingsIcon - only static flows are shown', () => {
|
|
|
265
360
|
expect((globalThis as any).__lastDropdownMenu).toBeTruthy();
|
|
266
361
|
});
|
|
267
362
|
const menu = (globalThis as any).__lastDropdownMenu;
|
|
268
|
-
|
|
363
|
+
await act(async () => {
|
|
364
|
+
menu.onClick?.({ key: 'flowC:general' });
|
|
365
|
+
});
|
|
269
366
|
expect(openSpy).toHaveBeenCalledWith({ flowKey: 'flowC', stepKey: 'general' });
|
|
270
367
|
});
|
|
271
368
|
|
|
369
|
+
it('closes dropdown when opening flow settings modal', async () => {
|
|
370
|
+
class TestFlowModel extends FlowModel {}
|
|
371
|
+
const engine = new FlowEngine();
|
|
372
|
+
const model = new TestFlowModel({ uid: 'm-close', flowEngine: engine });
|
|
373
|
+
vi.spyOn(model, 'openFlowSettings').mockResolvedValue(undefined as any);
|
|
374
|
+
|
|
375
|
+
TestFlowModel.registerFlow({
|
|
376
|
+
key: 'flowClose',
|
|
377
|
+
title: 'Flow Close',
|
|
378
|
+
steps: {
|
|
379
|
+
general: { title: 'General', uiSchema: { f: { type: 'string', 'x-component': 'Input' } } },
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
render(
|
|
384
|
+
React.createElement(
|
|
385
|
+
ConfigProvider as any,
|
|
386
|
+
null,
|
|
387
|
+
React.createElement(App as any, null, React.createElement(DefaultSettingsIcon as any, { model })),
|
|
388
|
+
),
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
await waitFor(() => {
|
|
392
|
+
expect((globalThis as any).__lastDropdownMenu).toBeTruthy();
|
|
393
|
+
expect((globalThis as any).__lastDropdownOnOpenChange).toBeTruthy();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// open dropdown
|
|
397
|
+
await act(async () => {
|
|
398
|
+
(globalThis as any).__lastDropdownOnOpenChange?.(true, { source: 'trigger' });
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
await waitFor(() => {
|
|
402
|
+
expect((globalThis as any).__lastDropdownOpen).toBe(true);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const menu = (globalThis as any).__lastDropdownMenu;
|
|
406
|
+
|
|
407
|
+
// click config item to open modal
|
|
408
|
+
await act(async () => {
|
|
409
|
+
menu.onClick?.({ key: 'flowClose:general' });
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
await waitFor(() => {
|
|
413
|
+
expect((globalThis as any).__lastDropdownOpen).toBe(false);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
272
417
|
it('copy UID action writes model uid to clipboard', async () => {
|
|
273
418
|
class TestFlowModel extends FlowModel {}
|
|
274
419
|
const engine = new FlowEngine();
|
|
@@ -298,7 +443,9 @@ describe('DefaultSettingsIcon - only static flows are shown', () => {
|
|
|
298
443
|
expect((globalThis as any).__lastDropdownMenu).toBeTruthy();
|
|
299
444
|
});
|
|
300
445
|
const menu = (globalThis as any).__lastDropdownMenu;
|
|
301
|
-
|
|
446
|
+
await act(async () => {
|
|
447
|
+
menu.onClick?.({ key: 'copy-uid' });
|
|
448
|
+
});
|
|
302
449
|
expect((navigator as any).clipboard.writeText).toHaveBeenCalledWith('m-copy');
|
|
303
450
|
});
|
|
304
451
|
|
|
@@ -326,7 +473,9 @@ describe('DefaultSettingsIcon - only static flows are shown', () => {
|
|
|
326
473
|
expect((globalThis as any).__lastDropdownMenu).toBeTruthy();
|
|
327
474
|
});
|
|
328
475
|
const menu = (globalThis as any).__lastDropdownMenu;
|
|
329
|
-
|
|
476
|
+
await act(async () => {
|
|
477
|
+
menu.onClick?.({ key: 'delete' });
|
|
478
|
+
});
|
|
330
479
|
expect(destroySpy).toHaveBeenCalled();
|
|
331
480
|
});
|
|
332
481
|
|
|
@@ -556,8 +705,11 @@ describe('DefaultSettingsIcon - extra menu items', () => {
|
|
|
556
705
|
});
|
|
557
706
|
|
|
558
707
|
const menu = (globalThis as any).__lastDropdownMenu;
|
|
559
|
-
|
|
708
|
+
await act(async () => {
|
|
709
|
+
menu.onClick?.({ key: 'extra-action' });
|
|
710
|
+
});
|
|
560
711
|
expect(onClick).toHaveBeenCalled();
|
|
712
|
+
expect((globalThis as any).__lastDropdownOpen).toBe(false);
|
|
561
713
|
} finally {
|
|
562
714
|
dispose?.();
|
|
563
715
|
}
|
|
@@ -204,8 +204,10 @@ const VariableInputComponent: React.FC<VariableInputProps> = ({
|
|
|
204
204
|
useEffect(() => {
|
|
205
205
|
if (!resolvedMetaTreeNode) return;
|
|
206
206
|
if (!Array.isArray(resolvedMetaTree) || innerValue == null) return;
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
// During initial restoration, `innerValue` already represents the persisted value.
|
|
208
|
+
// Do NOT override it with `resolveValueFromPath`, otherwise truthy defaults (e.g. RunJSValue objects)
|
|
209
|
+
// may accidentally wipe persisted content when reopening.
|
|
210
|
+
emitChange(innerValue, resolvedMetaTreeNode);
|
|
209
211
|
setCurrentMetaTreeNode(resolvedMetaTreeNode);
|
|
210
212
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
211
213
|
}, [resolvedMetaTreeNode]);
|
|
@@ -241,12 +243,18 @@ const VariableInputComponent: React.FC<VariableInputProps> = ({
|
|
|
241
243
|
|
|
242
244
|
const handleVariableSelect = useCallback(
|
|
243
245
|
(variableValue: string, metaTreeNode?: MetaTreeNode) => {
|
|
246
|
+
if (!metaTreeNode && variableValue === '') {
|
|
247
|
+
const cleared = clearValue !== undefined ? clearValue : null;
|
|
248
|
+
setInnerValue(cleared);
|
|
249
|
+
emitChange(cleared as any);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
244
252
|
setCurrentMetaTreeNode(metaTreeNode);
|
|
245
253
|
const finalValue = resolveValueFromPath?.(metaTreeNode) || variableValue;
|
|
246
254
|
setInnerValue(finalValue);
|
|
247
255
|
emitChange(finalValue, metaTreeNode);
|
|
248
256
|
},
|
|
249
|
-
[emitChange, resolveValueFromPath],
|
|
257
|
+
[emitChange, resolveValueFromPath, clearValue],
|
|
250
258
|
);
|
|
251
259
|
|
|
252
260
|
const { disabled } = restProps;
|
|
@@ -286,7 +294,7 @@ const VariableInputComponent: React.FC<VariableInputProps> = ({
|
|
|
286
294
|
|
|
287
295
|
const inputProps = useMemo(() => {
|
|
288
296
|
const baseProps = {
|
|
289
|
-
value: innerValue ?? '',
|
|
297
|
+
value: ValueComponent === Input ? innerValue ?? '' : innerValue,
|
|
290
298
|
onChange: handleInputChange,
|
|
291
299
|
disabled,
|
|
292
300
|
};
|
|
@@ -30,65 +30,74 @@ const VariableTagComponent: React.FC<VariableTagProps> = ({
|
|
|
30
30
|
|
|
31
31
|
const { data: displayedValue } = useRequest(
|
|
32
32
|
async () => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
: ctx.t(metaTreeNode.title) || '';
|
|
38
|
-
}
|
|
33
|
+
const resolveLabelFromPath = async (rawPath?: (string | number)[]): Promise<string | null> => {
|
|
34
|
+
if (!rawPath) return null;
|
|
35
|
+
if (!Array.isArray(rawPath)) return null;
|
|
36
|
+
if (!Array.isArray(resolvedMetaTree)) return null;
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
// 兼容 metaTree 为子树:顶层不含首段时,裁剪首段
|
|
39
|
+
const topNames = new Set((resolvedMetaTree || []).map((n: any) => String(n?.name)));
|
|
40
|
+
const path = !topNames.has(String(rawPath[0])) ? rawPath.slice(1) : rawPath;
|
|
41
|
+
if (!path.length) return '';
|
|
42
|
+
|
|
43
|
+
let nodes: MetaTreeNode[] | undefined = resolvedMetaTree as MetaTreeNode[];
|
|
44
|
+
const titleChain: string[] = [];
|
|
45
|
+
let matchedCount = 0;
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < path.length; i++) {
|
|
48
|
+
if (!nodes) break;
|
|
49
|
+
const seg = String(path[i]);
|
|
50
|
+
const node = nodes.find((n) => String(n?.name) === seg) as MetaTreeNode | undefined;
|
|
51
|
+
if (!node) break; // 停在第一个无效段之前
|
|
52
|
+
|
|
53
|
+
titleChain.push(String(node.title ?? node.name ?? seg));
|
|
54
|
+
matchedCount = i + 1;
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (!node) break; // 停在第一个无效段之前
|
|
61
|
-
deepest = node;
|
|
62
|
-
matchedCount = i + 1;
|
|
63
|
-
if (i < path.length - 1) {
|
|
64
|
-
if (Array.isArray(node.children)) {
|
|
65
|
-
nodes = node.children as any;
|
|
66
|
-
} else if (typeof node.children === 'function') {
|
|
67
|
-
try {
|
|
68
|
-
const childNodes = await (node.children as any)();
|
|
69
|
-
(node as any).children = childNodes;
|
|
70
|
-
nodes = childNodes as any;
|
|
71
|
-
} catch {
|
|
56
|
+
if (i < path.length - 1) {
|
|
57
|
+
if (Array.isArray(node.children)) {
|
|
58
|
+
nodes = node.children as any;
|
|
59
|
+
} else if (typeof node.children === 'function') {
|
|
60
|
+
try {
|
|
61
|
+
const childNodes = await (node.children as any)();
|
|
62
|
+
(node as any).children = childNodes;
|
|
63
|
+
nodes = childNodes as any;
|
|
64
|
+
} catch {
|
|
65
|
+
nodes = undefined;
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
72
68
|
nodes = undefined;
|
|
73
69
|
}
|
|
74
|
-
} else {
|
|
75
|
-
nodes = undefined;
|
|
76
70
|
}
|
|
77
71
|
}
|
|
78
|
-
}
|
|
79
72
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let label =
|
|
73
|
+
if (matchedCount === 0) return null;
|
|
74
|
+
|
|
75
|
+
let label = titleChain.map(ctx.t).join('/');
|
|
83
76
|
if (matchedCount < path.length) {
|
|
84
77
|
const tail = path.slice(matchedCount).join('/');
|
|
85
78
|
label = tail ? `${label}/${tail}` : label;
|
|
86
79
|
}
|
|
87
80
|
return label;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// 1) 优先使用已解析到的节点(包含完整父标题链)
|
|
84
|
+
if (metaTreeNode?.parentTitles) {
|
|
85
|
+
return [...metaTreeNode.parentTitles, metaTreeNode.title].map(ctx.t).join('/');
|
|
88
86
|
}
|
|
89
87
|
|
|
90
|
-
//
|
|
91
|
-
|
|
88
|
+
// 2) metaTreeNode 存在但缺少 parentTitles:尝试根据 value/metaTreeNode.paths 从 metaTree 还原完整路径
|
|
89
|
+
if (metaTreeNode) {
|
|
90
|
+
const rawPath = parseValueToPath(value) || metaTreeNode.paths;
|
|
91
|
+
const label = await resolveLabelFromPath(rawPath as any);
|
|
92
|
+
return label ?? ctx.t(metaTreeNode.title) ?? '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 3) 无 metaTreeNode:从 value 还原路径并拼接标题链;若找不到任何前缀则回退原始路径字符串
|
|
96
|
+
if (!value) return String(value);
|
|
97
|
+
const rawPath = parseValueToPath(value);
|
|
98
|
+
const label = await resolveLabelFromPath(rawPath as any);
|
|
99
|
+
if (label != null) return label;
|
|
100
|
+
return Array.isArray(rawPath) ? rawPath.join('/') : String(value);
|
|
92
101
|
},
|
|
93
102
|
{ refreshDeps: [resolvedMetaTree, value, metaTreeNode] },
|
|
94
103
|
);
|