refactor(plugin-workflow): add log for node testing (#7129)

This commit is contained in:
Junyi 2025-06-30 11:45:39 +08:00 committed by GitHub
parent 6afcbffdec
commit 3c555e9fc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 177 additions and 93 deletions

View File

@ -100,4 +100,5 @@ export default class extends Instruction {
resultTitle: lang('Calculation result'), resultTitle: lang('Calculation result'),
}; };
} }
testable = true;
} }

View File

@ -194,4 +194,5 @@ export default class extends Instruction {
</NodeDefaultView> </NodeDefaultView>
); );
} }
testable = true;
} }

View File

@ -7,11 +7,11 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { CloseOutlined, DeleteOutlined } from '@ant-design/icons'; import { CaretRightOutlined, CloseOutlined, DeleteOutlined } from '@ant-design/icons';
import { createForm, Field } from '@formily/core'; import { createForm, Field } from '@formily/core';
import { toJS } from '@formily/reactive'; import { toJS } from '@formily/reactive';
import { ISchema, observer, useField, useForm } from '@formily/react'; import { ISchema, observer, useField, useForm } from '@formily/react';
import { Alert, App, Button, Dropdown, Empty, Input, Space, Tag, Tooltip, message } from 'antd'; import { Alert, App, Button, Collapse, Dropdown, Empty, Input, Space, Tag, Tooltip, message } from 'antd';
import { cloneDeep, get, set } from 'lodash'; import { cloneDeep, get, set } from 'lodash';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -26,7 +26,6 @@ import {
cx, cx,
useAPIClient, useAPIClient,
useActionContext, useActionContext,
useCancelAction,
useCompile, useCompile,
usePlugin, usePlugin,
useResourceActionContext, useResourceActionContext,
@ -330,6 +329,7 @@ const useRunAction = () => {
async run() { async run() {
const template = parse(node.config); const template = parse(node.config);
const config = template(toJS(values.config)); const config = template(toJS(values.config));
const logField = query('log').take() as Field;
const resultField = query('result').take() as Field; const resultField = query('result').take() as Field;
resultField.setValue(null); resultField.setValue(null);
resultField.setFeedback({}); resultField.setFeedback({});
@ -352,6 +352,7 @@ const useRunAction = () => {
messages: data.status > 0 ? [lang('Resolved')] : [lang('Failed')], messages: data.status > 0 ? [lang('Resolved')] : [lang('Failed')],
}); });
resultField.setValue(data.result); resultField.setValue(data.result);
logField.setValue(data.log || '');
} catch (err) { } catch (err) {
resultField.setFeedback({ resultField.setFeedback({
type: 'error', type: 'error',
@ -359,7 +360,6 @@ const useRunAction = () => {
}); });
} }
field.data.loading = false; field.data.loading = false;
ctx.setFormValueChanged(false);
}, },
}; };
}; };
@ -397,113 +397,163 @@ function TestFormFieldset({ value, onChange }) {
); );
} }
function LogCollapse({ value }) {
return value ? (
<Collapse
ghost
items={[
{
key: 'log',
label: lang('Log'),
children: (
<Input.TextArea
value={value}
autoSize={{ minRows: 5, maxRows: 20 }}
style={{ whiteSpace: 'pre', cursor: 'text', fontFamily: 'monospace', fontSize: '80%' }}
disabled
/>
),
},
]}
className={css`
.ant-collapse-item > .ant-collapse-header {
padding: 0;
}
.ant-collapse-content > .ant-collapse-content-box {
padding: 0;
}
`}
/>
) : null;
}
function useCancelAction() {
const form = useForm();
const ctx = useActionContext();
return {
async run() {
const resultField = form.query('result').take() as Field;
resultField.setFeedback();
form.setValues({ result: null, log: null });
form.clearFormGraph('*');
form.reset();
ctx.setVisible(false);
},
};
}
function TestButton() { function TestButton() {
const node = useNodeContext(); const node = useNodeContext();
const { values } = useForm(); const { values } = useForm();
const [visible, setVisible] = useState(false);
const template = parse(values); const template = parse(values);
const keys = template.parameters.map((item) => item.key); const keys = template.parameters.map((item) => item.key);
const form = useMemo(() => createForm(), []); const form = useMemo(() => createForm(), []);
const onOpen = useCallback(() => {
setVisible(true);
}, []);
const setModalVisible = useCallback(
(v: boolean) => {
if (v) {
setVisible(true);
return;
}
const resultField = form.query('result').take() as Field;
resultField?.setFeedback();
form.setValues({ result: null, log: null });
form.clearFormGraph('*');
form.reset();
setVisible(false);
},
[form],
);
return ( return (
<NodeContext.Provider value={{ ...node, config: values }}> <NodeContext.Provider value={{ ...node, config: values }}>
<VariableKeysContext.Provider value={keys}> <VariableKeysContext.Provider value={keys}>
<SchemaComponent <ActionContextProvider value={{ visible, setVisible: setModalVisible }}>
components={{ <Button icon={<CaretRightOutlined />} onClick={onOpen}>
Alert, {lang('Test run')}
TestFormFieldset, </Button>
}} <SchemaComponent
scope={{ components={{
useCancelAction, Alert,
useRunAction, TestFormFieldset,
}} LogCollapse,
schema={{ }}
type: 'void', scope={{
name: 'testButton', useCancelAction,
title: '{{t("Test run")}}', useRunAction,
'x-component': 'Action', }}
'x-component-props': { schema={{
icon: 'CaretRightOutlined', type: 'void',
// openSize: 'small', name: 'modal',
}, 'x-decorator': 'FormV2',
properties: { 'x-decorator-props': {
modal: { form,
type: 'void', },
'x-decorator': 'FormV2', 'x-component': 'Action.Modal',
'x-decorator-props': { title: `{{t("Test run", { ns: "workflow" })}}`,
form, properties: {
alert: {
type: 'void',
'x-component': 'Alert',
'x-component-props': {
message: `{{t("Test run will do the actual data manipulating or API calling, please use with caution.", { ns: "workflow" })}}`,
type: 'warning',
showIcon: true,
className: css`
margin-bottom: 1em;
`,
},
}, },
'x-component': 'Action.Modal', config: {
title: `{{t("Test run", { ns: "workflow" })}}`, type: 'object',
properties: { title: '{{t("Replace variables", { ns: "workflow" })}}',
alert: { 'x-decorator': 'FormItem',
type: 'void', 'x-component': 'TestFormFieldset',
'x-component': 'Alert', },
'x-component-props': { actions: {
message: `{{t("Test run will do the actual data manipulating or API calling, please use with caution.", { ns: "workflow" })}}`, type: 'void',
type: 'warning', 'x-component': 'ActionBar',
showIcon: true, properties: {
className: css` submit: {
margin-bottom: 1em; type: 'void',
`, title: '{{t("Run")}}',
}, 'x-component': 'Action',
}, 'x-component-props': {
config: { type: 'primary',
type: 'object', useAction: '{{ useRunAction }}',
title: '{{t("Replace variables", { ns: "workflow" })}}',
'x-decorator': 'FormItem',
'x-component': 'TestFormFieldset',
},
actions: {
type: 'void',
'x-component': 'ActionBar',
properties: {
submit: {
type: 'void',
title: '{{t("Run")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ useRunAction }}',
},
}, },
}, },
}, },
result: { },
type: 'string', result: {
title: `{{t("Result", { ns: "workflow" })}}`, type: 'string',
'x-decorator': 'FormItem', title: `{{t("Result", { ns: "workflow" })}}`,
'x-component': 'Input.JSON', 'x-decorator': 'FormItem',
'x-component-props': { 'x-component': 'Input.JSON',
autoSize: { 'x-component-props': {
minRows: 5, autoSize: {
maxRows: 20, minRows: 5,
}, maxRows: 20,
style: {
whiteSpace: 'pre',
cursor: 'text',
},
}, },
'x-pattern': 'disabled', style: {
}, whiteSpace: 'pre',
footer: { cursor: 'text',
type: 'void',
'x-component': 'Action.Modal.Footer',
properties: {
cancel: {
type: 'void',
title: '{{t("Close")}}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCancelAction }}',
},
},
}, },
}, },
'x-pattern': 'disabled',
},
log: {
type: 'string',
'x-component': 'LogCollapse',
}, },
}, },
}, }}
}} />
/> </ActionContextProvider>
</VariableKeysContext.Provider> </VariableKeysContext.Provider>
</NodeContext.Provider> </NodeContext.Provider>
); );

View File

@ -39,6 +39,22 @@ export class CalculationInstruction extends Instruction {
}; };
} }
} }
async test({ engine = 'math.js', expression = '' }) {
const evaluator = <Evaluator | undefined>evaluators.get(engine);
try {
const result = evaluator && expression ? evaluator(expression) : null;
return {
result,
status: JOB_STATUS.RESOLVED,
};
} catch (e) {
return {
result: e.toString(),
status: JOB_STATUS.ERROR,
};
}
}
} }
export default CalculationInstruction; export default CalculationInstruction;

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { evaluators } from '@nocobase/evaluators'; import { Evaluator, evaluators } from '@nocobase/evaluators';
import { Instruction } from '.'; import { Instruction } from '.';
import type Processor from '../Processor'; import type Processor from '../Processor';
import { JOB_STATUS } from '../constants'; import { JOB_STATUS } from '../constants';
@ -80,6 +80,22 @@ export class ConditionInstruction extends Instruction {
// pass control to upper scope by ending current scope // pass control to upper scope by ending current scope
return processor.exit(branchJob.status); return processor.exit(branchJob.status);
} }
async test({ engine, calculation, expression = '' }) {
const evaluator = <Evaluator | undefined>evaluators.get(engine);
try {
const result = evaluator ? evaluator(expression) : logicCalculate(calculation);
return {
result,
status: JOB_STATUS.RESOLVED,
};
} catch (e) {
return {
result: e.toString(),
status: JOB_STATUS.ERROR,
};
}
}
} }
export default ConditionInstruction; export default ConditionInstruction;