feat: plugin workflow visualization (#987)
* feat(plugin-workfow): adjust some ui * feat(plugin-workflow): add execution visualization * fix(plugin-workflow): fix changed component
This commit is contained in:
parent
ed6f9a0867
commit
7cb5ff554e
@ -12,6 +12,7 @@ export * from './icon';
|
|||||||
export * from './id';
|
export * from './id';
|
||||||
export * from './input';
|
export * from './input';
|
||||||
export * from './integer';
|
export * from './integer';
|
||||||
|
export * from './json';
|
||||||
export * from './linkTo';
|
export * from './linkTo';
|
||||||
export * from './m2m';
|
export * from './m2m';
|
||||||
export * from './m2o';
|
export * from './m2o';
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
import { defaultProps, operators, unique } from './properties';
|
||||||
|
import { IField } from './types';
|
||||||
|
import { registerValidateRules } from '@formily/core';
|
||||||
|
|
||||||
|
registerValidateRules({
|
||||||
|
json(value) {
|
||||||
|
try {
|
||||||
|
JSON.parse(value);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
type: 'error',
|
||||||
|
message: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const json: IField = {
|
||||||
|
name: 'json',
|
||||||
|
type: 'object',
|
||||||
|
group: 'advanced',
|
||||||
|
order: 3,
|
||||||
|
title: '{{t("JSON")}}',
|
||||||
|
sortable: true,
|
||||||
|
default: {
|
||||||
|
type: 'json',
|
||||||
|
// name,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'object',
|
||||||
|
// title,
|
||||||
|
'x-component': 'Input.JSON',
|
||||||
|
'x-component-props': {
|
||||||
|
autoSize: {
|
||||||
|
minRows: 5,
|
||||||
|
// maxRows: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hasDefaultValue: true,
|
||||||
|
properties: {
|
||||||
|
...defaultProps,
|
||||||
|
},
|
||||||
|
filterable: {
|
||||||
|
}
|
||||||
|
};
|
@ -416,7 +416,7 @@ export default {
|
|||||||
"Tencent COS": "Tencent COS",
|
"Tencent COS": "Tencent COS",
|
||||||
"Amazon S3": "Amazon S3",
|
"Amazon S3": "Amazon S3",
|
||||||
"Workflow": "Workflow",
|
"Workflow": "Workflow",
|
||||||
"Execution History": "Execution History",
|
"Execution history": "Execution history",
|
||||||
"Trigger type": "Trigger type",
|
"Trigger type": "Trigger type",
|
||||||
"Description": "Description",
|
"Description": "Description",
|
||||||
"Status": "Status",
|
"Status": "Status",
|
||||||
|
@ -416,7 +416,7 @@ export default {
|
|||||||
"Tencent COS": "Tencent COS",
|
"Tencent COS": "Tencent COS",
|
||||||
"Amazon S3": "Amazon S3",
|
"Amazon S3": "Amazon S3",
|
||||||
"Workflow": "ワークフロー",
|
"Workflow": "ワークフロー",
|
||||||
"Execution History": "実行履歴",
|
"Execution history": "実行履歴",
|
||||||
"Trigger type": "トリガータイプ",
|
"Trigger type": "トリガータイプ",
|
||||||
"Description": "説明",
|
"Description": "説明",
|
||||||
"Status": "状態",
|
"Status": "状態",
|
||||||
|
@ -416,7 +416,7 @@ export default {
|
|||||||
"Tencent COS": "Tencent COS",
|
"Tencent COS": "Tencent COS",
|
||||||
"Amazon S3": "Amazon S3",
|
"Amazon S3": "Amazon S3",
|
||||||
"Workflow": "Workflow",
|
"Workflow": "Workflow",
|
||||||
"Execution History": "История запусков",
|
"Execution history": "История запусков",
|
||||||
"Trigger type": "Тип триггера",
|
"Trigger type": "Тип триггера",
|
||||||
"Description": "Описание",
|
"Description": "Описание",
|
||||||
"Status": "Статус",
|
"Status": "Статус",
|
||||||
|
@ -415,7 +415,7 @@ export default {
|
|||||||
"Aliyun OSS": "Aliyun OSS",
|
"Aliyun OSS": "Aliyun OSS",
|
||||||
"Amazon S3": "Amazon S3",
|
"Amazon S3": "Amazon S3",
|
||||||
"Workflow": "İş Akışı",
|
"Workflow": "İş Akışı",
|
||||||
"Execution History": "Yürütme Geçmişi",
|
"Execution history": "Yürütme Geçmişi",
|
||||||
"Trigger type": "Tetikleme türü",
|
"Trigger type": "Tetikleme türü",
|
||||||
"Description": "Açıklama",
|
"Description": "Açıklama",
|
||||||
"Status": "Durum",
|
"Status": "Durum",
|
||||||
|
@ -510,7 +510,7 @@ export default {
|
|||||||
|
|
||||||
// plugins/workflow
|
// plugins/workflow
|
||||||
'Workflow': '工作流',
|
'Workflow': '工作流',
|
||||||
'Execution History': '执行历史',
|
'Execution history': '执行历史',
|
||||||
'Trigger type': '触发方式',
|
'Trigger type': '触发方式',
|
||||||
'Description': '描述',
|
'Description': '描述',
|
||||||
'Status': '状态',
|
'Status': '状态',
|
||||||
@ -519,9 +519,11 @@ export default {
|
|||||||
'Version': '版本',
|
'Version': '版本',
|
||||||
'Copy to new version': '复制到新版本',
|
'Copy to new version': '复制到新版本',
|
||||||
|
|
||||||
|
'Loading': '加载中',
|
||||||
'Load failed': '加载失败',
|
'Load failed': '加载失败',
|
||||||
|
|
||||||
'Trigger': '触发器',
|
'Trigger': '触发器',
|
||||||
|
'Triggered at': '触发时间',
|
||||||
'Collection event': '数据表事件',
|
'Collection event': '数据表事件',
|
||||||
'Trigger on': '触发时机',
|
'Trigger on': '触发时机',
|
||||||
'After record added': '新增数据后',
|
'After record added': '新增数据后',
|
||||||
@ -578,9 +580,13 @@ export default {
|
|||||||
'Arithmetic calculation': '算术运算',
|
'Arithmetic calculation': '算术运算',
|
||||||
'String operation': '字符串',
|
'String operation': '字符串',
|
||||||
|
|
||||||
|
'Executed at': '执行于',
|
||||||
|
|
||||||
|
'Queueing': '队列中',
|
||||||
'On going': '进行中',
|
'On going': '进行中',
|
||||||
'Succeeded': '成功',
|
'Succeeded': '成功',
|
||||||
'Failed': '失败',
|
'Failed': '失败',
|
||||||
|
'Pending': '等待处理',
|
||||||
'Canceled': '已取消',
|
'Canceled': '已取消',
|
||||||
|
|
||||||
'This node contains branches, deleting will also be preformed to them, are you sure?': '节点包含分支,将同时删除其所有分支下的子节点,确定继续?',
|
'This node contains branches, deleting will also be preformed to them, are you sure?': '节点包含分支,将同时删除其所有分支下的子节点,确定继续?',
|
||||||
|
@ -177,6 +177,7 @@ const InternalAdminLayout = (props: any) => {
|
|||||||
<Layout.Content
|
<Layout.Content
|
||||||
className={css`
|
className={css`
|
||||||
min-height: calc(100vh - 46px);
|
min-height: calc(100vh - 46px);
|
||||||
|
padding-bottom: 42px;
|
||||||
position: relative;
|
position: relative;
|
||||||
// padding-bottom: 70px;
|
// padding-bottom: 70px;
|
||||||
> div {
|
> div {
|
||||||
|
@ -8,7 +8,7 @@ import { useActionContext } from '.';
|
|||||||
import { ComposedActionDrawer } from './types';
|
import { ComposedActionDrawer } from './types';
|
||||||
|
|
||||||
export const ActionModal: ComposedActionDrawer = observer((props) => {
|
export const ActionModal: ComposedActionDrawer = observer((props) => {
|
||||||
const { footerNodeName = 'Action.Modal.Footer', ...others } = props;
|
const { footerNodeName = 'Action.Modal.Footer', width = '80%', ...others } = props;
|
||||||
const { visible, setVisible } = useActionContext();
|
const { visible, setVisible } = useActionContext();
|
||||||
const schema = useFieldSchema();
|
const schema = useFieldSchema();
|
||||||
const field = useField();
|
const field = useField();
|
||||||
@ -27,7 +27,7 @@ export const ActionModal: ComposedActionDrawer = observer((props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal
|
<Modal
|
||||||
width={'80%'}
|
width={width}
|
||||||
title={field.title}
|
title={field.title}
|
||||||
{...others}
|
{...others}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
|
@ -27,7 +27,7 @@ export const Checkbox: ComposedCheckbox = connect(
|
|||||||
),
|
),
|
||||||
mapReadPretty((props) => {
|
mapReadPretty((props) => {
|
||||||
if (!isValid(props.value)) {
|
if (!isValid(props.value)) {
|
||||||
return <div></div>;
|
return null;
|
||||||
}
|
}
|
||||||
return props.value ? <CheckOutlined style={{ color: '#52c41a' }} /> : null;
|
return props.value ? <CheckOutlined style={{ color: '#52c41a' }} /> : null;
|
||||||
}),
|
}),
|
||||||
@ -42,7 +42,7 @@ Checkbox.Group = connect(
|
|||||||
}),
|
}),
|
||||||
mapReadPretty((props) => {
|
mapReadPretty((props) => {
|
||||||
if (!isValid(props.value)) {
|
if (!isValid(props.value)) {
|
||||||
return <div></div>;
|
return null;
|
||||||
}
|
}
|
||||||
const { options = [] } = props;
|
const { options = [] } = props;
|
||||||
const field = useField<any>();
|
const field = useField<any>();
|
||||||
@ -53,7 +53,7 @@ Checkbox.Group = connect(
|
|||||||
{dataSource
|
{dataSource
|
||||||
.filter((option) => value.includes(option.value))
|
.filter((option) => value.includes(option.value))
|
||||||
.map((option, key) => (
|
.map((option, key) => (
|
||||||
<Tag key={key} color={option.color}>
|
<Tag key={key} color={option.color} icon={option.icon}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
|
@ -4,10 +4,12 @@ import { Input as AntdInput } from 'antd';
|
|||||||
import { InputProps, TextAreaProps } from 'antd/lib/input';
|
import { InputProps, TextAreaProps } from 'antd/lib/input';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ReadPretty } from './ReadPretty';
|
import { ReadPretty } from './ReadPretty';
|
||||||
|
import { Json } from './Json';
|
||||||
|
|
||||||
type ComposedInput = React.FC<InputProps> & {
|
type ComposedInput = React.FC<InputProps> & {
|
||||||
TextArea?: React.FC<TextAreaProps>;
|
TextArea?: React.FC<TextAreaProps>;
|
||||||
URL?: React.FC<InputProps>;
|
URL?: React.FC<InputProps>;
|
||||||
|
JSON?: React.FC<TextAreaProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Input: ComposedInput = connect(
|
export const Input: ComposedInput = connect(
|
||||||
@ -34,6 +36,9 @@ Input.TextArea = connect(
|
|||||||
}),
|
}),
|
||||||
mapReadPretty(ReadPretty.TextArea),
|
mapReadPretty(ReadPretty.TextArea),
|
||||||
);
|
);
|
||||||
|
|
||||||
Input.URL = connect(AntdInput, mapReadPretty(ReadPretty.URL));
|
Input.URL = connect(AntdInput, mapReadPretty(ReadPretty.URL));
|
||||||
|
|
||||||
|
Input.JSON = connect(Json, mapReadPretty(ReadPretty.JSON));
|
||||||
|
|
||||||
export default Input;
|
export default Input;
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Field } from '@formily/core';
|
||||||
|
import { useField } from '@formily/react';
|
||||||
|
import { Input } from 'antd';
|
||||||
|
import { TextAreaProps } from 'antd/lib/input';
|
||||||
|
|
||||||
|
export function Json({ value, onChange, space = 2, ...props }: TextAreaProps & { value: any, space: number }) {
|
||||||
|
const field = useField<Field>();
|
||||||
|
return (
|
||||||
|
<Input.TextArea
|
||||||
|
{...props}
|
||||||
|
defaultValue={value != null ? JSON.stringify(value, null, space) : ''}
|
||||||
|
onChange={(ev) => {
|
||||||
|
try {
|
||||||
|
const v = ev.target.value.trim() !== '' ? JSON.parse(ev.target.value) : null;
|
||||||
|
field.setFeedback({});
|
||||||
|
onChange(v);
|
||||||
|
} catch (err) {
|
||||||
|
field.setFeedback({
|
||||||
|
type: 'error',
|
||||||
|
code: 'JSONSyntaxError',
|
||||||
|
messages: [err.message],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -5,6 +5,7 @@ import React from 'react';
|
|||||||
import { useCompile } from '../..';
|
import { useCompile } from '../..';
|
||||||
import { EllipsisWithTooltip } from './EllipsisWithTooltip';
|
import { EllipsisWithTooltip } from './EllipsisWithTooltip';
|
||||||
import { HTMLEncode } from './shared';
|
import { HTMLEncode } from './shared';
|
||||||
|
import { cx, css } from '@emotion/css';
|
||||||
|
|
||||||
type Composed = {
|
type Composed = {
|
||||||
Input: React.FC<InputProps & { ellipsis?: any }>;
|
Input: React.FC<InputProps & { ellipsis?: any }>;
|
||||||
@ -13,6 +14,7 @@ type Composed = {
|
|||||||
TextAreaProps & { ellipsis?: any; text?: any; addonBefore?: any; suffix?: any; addonAfter?: any; autop?: boolean }
|
TextAreaProps & { ellipsis?: any; text?: any; addonBefore?: any; suffix?: any; addonAfter?: any; autop?: boolean }
|
||||||
>;
|
>;
|
||||||
Html?: any;
|
Html?: any;
|
||||||
|
JSON?: React.FC<TextAreaProps & { space: number }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReadPretty: Composed = () => null;
|
export const ReadPretty: Composed = () => null;
|
||||||
@ -114,3 +116,17 @@ ReadPretty.URL = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ReadPretty.JSON = (props) => {
|
||||||
|
const prefixCls = usePrefixCls('json', props);
|
||||||
|
return (
|
||||||
|
<pre
|
||||||
|
className={cx(prefixCls, props.className, css`
|
||||||
|
margin-bottom: 0;
|
||||||
|
`)}
|
||||||
|
style={props.style}
|
||||||
|
>
|
||||||
|
{props.value != null ? JSON.stringify(props.value, null, props.space ?? 2) : ''}
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* title: URL
|
||||||
|
*/
|
||||||
|
import { FormItem } from '@formily/antd';
|
||||||
|
import { Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'object',
|
||||||
|
title: `Editable`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input.JSON',
|
||||||
|
'x-reactions': {
|
||||||
|
target: 'read',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read: {
|
||||||
|
type: 'string',
|
||||||
|
title: `Read pretty`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input.JSON',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ Input, FormItem }}>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -26,3 +26,7 @@ group:
|
|||||||
### URL
|
### URL
|
||||||
|
|
||||||
<code src="./demos/demo3.tsx" />
|
<code src="./demos/demo3.tsx" />
|
||||||
|
|
||||||
|
### JSON
|
||||||
|
|
||||||
|
<code src="./demos/demo4.tsx" />
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from './Input';
|
export * from './Input';
|
||||||
export * from './ReadPretty';
|
export * from './ReadPretty';
|
||||||
|
export * from './Json';
|
||||||
|
@ -35,7 +35,7 @@ Radio.Group = connect(
|
|||||||
{dataSource
|
{dataSource
|
||||||
.filter((option) => option.value === value)
|
.filter((option) => option.value === value)
|
||||||
.map((option, key) => (
|
.map((option, key) => (
|
||||||
<Tag key={key} color={option.color}>
|
<Tag key={key} color={option.color} icon={option.icon}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
|
@ -24,7 +24,7 @@ export const ReadPretty = observer((props: any) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{options.map((option, key) => (
|
{options.map((option, key) => (
|
||||||
<Tag key={key} color={option[fieldNames.color]}>
|
<Tag key={key} color={option[fieldNames.color]} icon={option.icon}>
|
||||||
{option[fieldNames.label]}
|
{option[fieldNames.label]}
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"@nocobase/database": "0.8.0-alpha.1",
|
"@nocobase/database": "0.8.0-alpha.1",
|
||||||
"@nocobase/server": "0.8.0-alpha.1",
|
"@nocobase/server": "0.8.0-alpha.1",
|
||||||
"@nocobase/utils": "0.8.0-alpha.1",
|
"@nocobase/utils": "0.8.0-alpha.1",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
"cron-parser": "4.4.0",
|
"cron-parser": "4.4.0",
|
||||||
"json-templates": "^4.2.0",
|
"json-templates": "^4.2.0",
|
||||||
"react-js-cron": "^1.4.0"
|
"react-js-cron": "^1.4.0"
|
||||||
|
84
packages/plugins/workflow/src/client/AddButton.tsx
Normal file
84
packages/plugins/workflow/src/client/AddButton.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import { Dropdown, Menu, Button } from 'antd';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
useAPIClient,
|
||||||
|
useCompile
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { useFlowContext } from './FlowContext';
|
||||||
|
import { Instruction, instructions, Node } from './nodes';
|
||||||
|
import { addButtonClass } from './style';
|
||||||
|
|
||||||
|
|
||||||
|
interface AddButtonProps {
|
||||||
|
upstream;
|
||||||
|
branchIndex?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AddButton({ upstream, branchIndex = null }: AddButtonProps) {
|
||||||
|
const compile = useCompile();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const { workflow, onNodeAdded } = useFlowContext() ?? {};
|
||||||
|
if (!workflow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const resource = api.resource('workflows.nodes', workflow.id);
|
||||||
|
|
||||||
|
async function onCreate({ keyPath }) {
|
||||||
|
const type = keyPath.pop();
|
||||||
|
const config = {};
|
||||||
|
const [optionKey] = keyPath;
|
||||||
|
if (optionKey) {
|
||||||
|
const { value } = instructions.get(type).options.find(item => item.key === optionKey);
|
||||||
|
Object.assign(config, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: { data: node } } = await resource.create({
|
||||||
|
values: {
|
||||||
|
type,
|
||||||
|
upstreamId: upstream?.id ?? null,
|
||||||
|
branchIndex,
|
||||||
|
config
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onNodeAdded(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = [
|
||||||
|
{ value: 'control', name: '{{t("Control")}}' },
|
||||||
|
{ value: 'collection', name: '{{t("Collection operations")}}' },
|
||||||
|
];
|
||||||
|
const instructionList = (Array.from(instructions.getValues()) as Instruction[]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx(addButtonClass)}>
|
||||||
|
<Dropdown
|
||||||
|
trigger={['click']}
|
||||||
|
overlay={
|
||||||
|
<Menu onClick={ev => onCreate(ev)}>
|
||||||
|
{groups.map(group => (
|
||||||
|
<Menu.ItemGroup key={group.value} title={compile(group.name)}>
|
||||||
|
{instructionList.filter(item => item.group === group.value).map(item => item.options
|
||||||
|
? (
|
||||||
|
<Menu.SubMenu key={item.type} title={compile(item.title)}>
|
||||||
|
{item.options.map(option => (
|
||||||
|
<Menu.Item key={option.key}>{compile(option.label)}</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu.SubMenu>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<Menu.Item key={item.type}>{compile(item.title)}</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu.ItemGroup>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
disabled={workflow.executed}
|
||||||
|
>
|
||||||
|
<Button shape="circle" icon={<PlusOutlined />} />
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
28
packages/plugins/workflow/src/client/Branch.tsx
Normal file
28
packages/plugins/workflow/src/client/Branch.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import { branchClass } from "./style";
|
||||||
|
import { AddButton } from "./AddButton";
|
||||||
|
import { Node } from './nodes';
|
||||||
|
|
||||||
|
export function Branch({
|
||||||
|
from = null,
|
||||||
|
entry = null,
|
||||||
|
branchIndex = null,
|
||||||
|
controller = null
|
||||||
|
}) {
|
||||||
|
const list = [];
|
||||||
|
for (let node = entry; node; node = node.downstream) {
|
||||||
|
list.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx(branchClass)}>
|
||||||
|
<div className="workflow-branch-lines" />
|
||||||
|
{controller}
|
||||||
|
<AddButton upstream={from} branchIndex={branchIndex} />
|
||||||
|
<div className="workflow-node-list">
|
||||||
|
{list.map(item => <Node data={item} key={item.id} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
111
packages/plugins/workflow/src/client/ExecutionCanvas.tsx
Normal file
111
packages/plugins/workflow/src/client/ExecutionCanvas.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Tag } from 'antd';
|
||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useCompile,
|
||||||
|
useDocumentTitle,
|
||||||
|
useResourceActionContext,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { str2moment } from '@nocobase/utils/client';
|
||||||
|
|
||||||
|
import { FlowContext } from './FlowContext';
|
||||||
|
import { branchBlockClass, nodeCardClass, nodeMetaClass } from './style';
|
||||||
|
import { TriggerConfig } from './triggers';
|
||||||
|
import { Branch } from './Branch';
|
||||||
|
import { ExecutionStatusOptionsMap } from './constants';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function makeNodes(nodes, jobs = []): void {
|
||||||
|
const nodesMap = new Map();
|
||||||
|
nodes.forEach(item => nodesMap.set(item.id, item));
|
||||||
|
const jobsMap = new Map();
|
||||||
|
jobs.forEach(item => jobsMap.set(item.nodeId, item));
|
||||||
|
for (let node of nodesMap.values()) {
|
||||||
|
if (node.upstreamId) {
|
||||||
|
node.upstream = nodesMap.get(node.upstreamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.downstreamId) {
|
||||||
|
node.downstream = nodesMap.get(node.downstreamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobsMap.has(node.id)) {
|
||||||
|
node.job = jobsMap.get(node.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExecutionCanvas() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const compile = useCompile();
|
||||||
|
const { data, refresh, loading } = useResourceActionContext();
|
||||||
|
const { setTitle } = useDocumentTitle();
|
||||||
|
useEffect(() => {
|
||||||
|
const { workflow } = data?.data ?? {};
|
||||||
|
setTitle(`${workflow?.title ? `${workflow.title} - ` : ''}${t('Execution history')}`);
|
||||||
|
}, [data?.data]);
|
||||||
|
|
||||||
|
if (!data?.data) {
|
||||||
|
if (loading) {
|
||||||
|
return <div>{t('Loading')}</div>
|
||||||
|
} else {
|
||||||
|
return <div>{t('Load failed')}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
jobs = [],
|
||||||
|
workflow: { nodes = [], revisions = [], ...workflow } = {},
|
||||||
|
...execution
|
||||||
|
} = data?.data ?? {};
|
||||||
|
|
||||||
|
makeNodes(nodes, jobs);
|
||||||
|
|
||||||
|
const entry = nodes.find(item => !item.upstream);
|
||||||
|
|
||||||
|
const statusOption = ExecutionStatusOptionsMap[execution.status];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlowContext.Provider value={{
|
||||||
|
workflow: workflow.type ? workflow : null,
|
||||||
|
nodes,
|
||||||
|
execution
|
||||||
|
}}>
|
||||||
|
<div className="workflow-toolbar">
|
||||||
|
<header>
|
||||||
|
<span>
|
||||||
|
<Link to={`/admin/settings/workflow/workflows`}>
|
||||||
|
{t('Workflow')}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<Link to={`/admin/settings/workflow/workflows/${workflow.id}`}>
|
||||||
|
{workflow.title}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<strong>{`#${execution.id}`}</strong>
|
||||||
|
</header>
|
||||||
|
<aside>
|
||||||
|
<Tag color={statusOption.color}>{compile(statusOption.label)}</Tag>
|
||||||
|
<time>{str2moment(execution.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
<div className="workflow-canvas">
|
||||||
|
<TriggerConfig workflow={workflow} />
|
||||||
|
<div className={branchBlockClass}>
|
||||||
|
<Branch entry={entry} />
|
||||||
|
</div>
|
||||||
|
<div className={cx(nodeCardClass)}>
|
||||||
|
<div className={cx(nodeMetaClass)}>
|
||||||
|
<Tag color="#333">{t('End')}</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FlowContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
15
packages/plugins/workflow/src/client/ExecutionLink.tsx
Normal file
15
packages/plugins/workflow/src/client/ExecutionLink.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useActionContext, useRecord } from '@nocobase/client';
|
||||||
|
|
||||||
|
|
||||||
|
export const ExecutionLink = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { id } = useRecord();
|
||||||
|
const { setVisible } = useActionContext();
|
||||||
|
return (
|
||||||
|
<Link to={`/admin/settings/workflow/executions/${id}`} onClick={() => setVisible(false)}>{t('View')}</Link>
|
||||||
|
);
|
||||||
|
}
|
47
packages/plugins/workflow/src/client/ExecutionPage.tsx
Normal file
47
packages/plugins/workflow/src/client/ExecutionPage.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import { useRouteMatch } from 'react-router-dom';
|
||||||
|
import { SchemaComponent } from '@nocobase/client';
|
||||||
|
import { workflowPageClass } from './style';
|
||||||
|
import { ExecutionCanvas } from './ExecutionCanvas';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const ExecutionPage = () => {
|
||||||
|
const { params } = useRouteMatch<any>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx(workflowPageClass)}>
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
[`execution_${params.id}`]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'ResourceActionProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: {
|
||||||
|
name: 'executions',
|
||||||
|
fields: [],
|
||||||
|
},
|
||||||
|
resourceName: 'executions',
|
||||||
|
request: {
|
||||||
|
resource: 'executions',
|
||||||
|
action: 'get',
|
||||||
|
params: {
|
||||||
|
filter: params,
|
||||||
|
appends: ['jobs', 'workflow', 'workflow.nodes'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-component': 'ExecutionCanvas'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
ExecutionCanvas,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
8
packages/plugins/workflow/src/client/FlowContext.ts
Normal file
8
packages/plugins/workflow/src/client/FlowContext.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React, { useContext } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export const FlowContext = React.createContext(null);
|
||||||
|
|
||||||
|
export function useFlowContext() {
|
||||||
|
return useContext(FlowContext);
|
||||||
|
}
|
@ -1,21 +1,21 @@
|
|||||||
import React, { useContext, useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import { Dropdown, Menu, Button, Tag, Switch, message } from 'antd';
|
import { Dropdown, Menu, Button, Tag, Switch, message } from 'antd';
|
||||||
import { PlusOutlined, DownOutlined, RightOutlined } from '@ant-design/icons';
|
import { DownOutlined, RightOutlined } from '@ant-design/icons';
|
||||||
import { cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useAPIClient,
|
|
||||||
useCompile,
|
|
||||||
useDocumentTitle,
|
useDocumentTitle,
|
||||||
useResourceActionContext,
|
useResourceActionContext,
|
||||||
useResourceContext
|
useResourceContext
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
|
|
||||||
import { Instruction, instructions, Node } from './nodes';
|
import { FlowContext } from './FlowContext';
|
||||||
import { addButtonClass, branchBlockClass, branchClass, nodeCardClass, nodeMetaClass, workflowVersionDropdownClass } from './style';
|
import { branchBlockClass, nodeCardClass, nodeMetaClass, workflowVersionDropdownClass } from './style';
|
||||||
import { TriggerConfig } from './triggers';
|
import { TriggerConfig } from './triggers';
|
||||||
|
import { Branch } from './Branch';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -34,12 +34,6 @@ function makeNodes(nodes): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const FlowContext = React.createContext(null);
|
|
||||||
|
|
||||||
export function useFlowContext() {
|
|
||||||
return useContext(FlowContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WorkflowCanvas() {
|
export function WorkflowCanvas() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@ -48,7 +42,7 @@ export function WorkflowCanvas() {
|
|||||||
const { setTitle } = useDocumentTitle();
|
const { setTitle } = useDocumentTitle();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { title } = data?.data ?? {};
|
const { title } = data?.data ?? {};
|
||||||
setTitle(`${title ? `${title} - ` : ''}${t('Workflow')}`);
|
setTitle(`${t('Workflow')}${title ? `: ${title}` : ''}`);
|
||||||
}, [data?.data]);
|
}, [data?.data]);
|
||||||
|
|
||||||
if (!data?.data && !loading) {
|
if (!data?.data && !loading) {
|
||||||
@ -72,7 +66,7 @@ export function WorkflowCanvas() {
|
|||||||
filterByTk: workflow[targetKey],
|
filterByTk: workflow[targetKey],
|
||||||
values: {
|
values: {
|
||||||
enabled: value,
|
enabled: value,
|
||||||
// NOTE: keep `key` field to adapter for backend
|
// NOTE: keep `key` field to adapt for backend
|
||||||
key: workflow.key
|
key: workflow.key
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -97,6 +91,11 @@ export function WorkflowCanvas() {
|
|||||||
}}>
|
}}>
|
||||||
<div className="workflow-toolbar">
|
<div className="workflow-toolbar">
|
||||||
<header>
|
<header>
|
||||||
|
<span>
|
||||||
|
<Link to={`/admin/settings/workflow/workflows`}>
|
||||||
|
{t('Workflow')}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
<strong>{workflow.title}</strong>
|
<strong>{workflow.title}</strong>
|
||||||
</header>
|
</header>
|
||||||
<aside>
|
<aside>
|
||||||
@ -110,11 +109,15 @@ export function WorkflowCanvas() {
|
|||||||
defaultSelectedKeys={[workflow.id]}
|
defaultSelectedKeys={[workflow.id]}
|
||||||
className={cx(workflowVersionDropdownClass)}
|
className={cx(workflowVersionDropdownClass)}
|
||||||
>
|
>
|
||||||
{revisions.sort((a, b) => b.id - a.id).map(item => (
|
{revisions.sort((a, b) => b.id - a.id).map((item, index) => (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key={item.id}
|
key={item.id}
|
||||||
icon={item.current ? <RightOutlined /> : null}
|
icon={item.current ? <RightOutlined /> : null}
|
||||||
className={item.executed ? 'executed' : 'unexecuted'}
|
className={classnames({
|
||||||
|
executed: item.executed,
|
||||||
|
unexecuted: !item.executed,
|
||||||
|
enabled: item.enabled,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<strong>{`#${item.id}`}</strong>
|
<strong>{`#${item.id}`}</strong>
|
||||||
<time>{(new Date(item.createdAt)).toLocaleString()}</time>
|
<time>{(new Date(item.createdAt)).toLocaleString()}</time>
|
||||||
@ -141,7 +144,7 @@ export function WorkflowCanvas() {
|
|||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
<div className="workflow-canvas">
|
<div className="workflow-canvas">
|
||||||
<TriggerConfig />
|
<TriggerConfig workflow={workflow} />
|
||||||
<div className={branchBlockClass}>
|
<div className={branchBlockClass}>
|
||||||
<Branch entry={entry} />
|
<Branch entry={entry} />
|
||||||
</div>
|
</div>
|
||||||
@ -154,98 +157,3 @@ export function WorkflowCanvas() {
|
|||||||
</FlowContext.Provider>
|
</FlowContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Branch({
|
|
||||||
from = null,
|
|
||||||
entry = null,
|
|
||||||
branchIndex = null,
|
|
||||||
controller = null
|
|
||||||
}) {
|
|
||||||
const list = [];
|
|
||||||
for (let node = entry; node; node = node.downstream) {
|
|
||||||
list.push(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cx(branchClass)}>
|
|
||||||
<div className="workflow-branch-lines" />
|
|
||||||
{controller}
|
|
||||||
<AddButton upstream={from} branchIndex={branchIndex} />
|
|
||||||
<div className="workflow-node-list">
|
|
||||||
{list.map(item => <Node data={item} key={item.id} />)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(bug): useless observable
|
|
||||||
// const instructionsList = observable(Array.from(instructions.getValues()));
|
|
||||||
|
|
||||||
interface AddButtonProps {
|
|
||||||
upstream;
|
|
||||||
branchIndex?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AddButton({ upstream, branchIndex = null }: AddButtonProps) {
|
|
||||||
const compile = useCompile();
|
|
||||||
const api = useAPIClient();
|
|
||||||
const { workflow, onNodeAdded } = useFlowContext();
|
|
||||||
const resource = api.resource('workflows.nodes', workflow.id);
|
|
||||||
|
|
||||||
async function onCreate({ keyPath }) {
|
|
||||||
const type = keyPath.pop();
|
|
||||||
const config = {};
|
|
||||||
const [optionKey] = keyPath;
|
|
||||||
if (optionKey) {
|
|
||||||
const { value } = instructions.get(type).options.find(item => item.key === optionKey);
|
|
||||||
Object.assign(config, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: { data: node } } = await resource.create({
|
|
||||||
values: {
|
|
||||||
type,
|
|
||||||
upstreamId: upstream?.id ?? null,
|
|
||||||
branchIndex,
|
|
||||||
config
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onNodeAdded(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
const groups = [
|
|
||||||
{ value: 'control', name: '{{t("Control")}}' },
|
|
||||||
{ value: 'collection', name: '{{t("Collection operations")}}' },
|
|
||||||
];
|
|
||||||
const instructionList = (Array.from(instructions.getValues()) as Instruction[]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cx(addButtonClass)}>
|
|
||||||
<Dropdown
|
|
||||||
trigger={['click']}
|
|
||||||
overlay={
|
|
||||||
<Menu onClick={ev => onCreate(ev)}>
|
|
||||||
{groups.map(group => (
|
|
||||||
<Menu.ItemGroup key={group.value} title={compile(group.name)}>
|
|
||||||
{instructionList.filter(item => item.group === group.value).map(item => item.options
|
|
||||||
? (
|
|
||||||
<Menu.SubMenu key={item.type} title={compile(item.title)}>
|
|
||||||
{item.options.map(option => (
|
|
||||||
<Menu.Item key={option.key}>{compile(option.label)}</Menu.Item>
|
|
||||||
))}
|
|
||||||
</Menu.SubMenu>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<Menu.Item key={item.type}>{compile(item.title)}</Menu.Item>
|
|
||||||
))}
|
|
||||||
</Menu.ItemGroup>
|
|
||||||
))}
|
|
||||||
</Menu>
|
|
||||||
}
|
|
||||||
disabled={workflow.executed}
|
|
||||||
>
|
|
||||||
<Button shape="circle" icon={<PlusOutlined />} />
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -10,6 +10,6 @@ export const WorkflowLink = () => {
|
|||||||
const { id } = useRecord();
|
const { id } = useRecord();
|
||||||
const { setVisible } = useActionContext();
|
const { setVisible } = useActionContext();
|
||||||
return (
|
return (
|
||||||
<Link to={`/admin/plugins/workflows/${id}`} onClick={() => setVisible(false)}>{t('View')}</Link>
|
<Link to={`/admin/settings/workflow/workflows/${id}`} onClick={() => setVisible(false)}>{t('View')}</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ export const WorkflowPage = () => {
|
|||||||
action: 'get',
|
action: 'get',
|
||||||
params: {
|
params: {
|
||||||
filter: params,
|
filter: params,
|
||||||
appends: ['nodes', 'revisions.id', 'revisions.createdAt', 'revisions.current', 'revisions.executed'],
|
appends: ['nodes', 'revisions.id', 'revisions.createdAt', 'revisions.current', 'revisions.executed', 'revisions.enabled'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -2,14 +2,19 @@ import { PluginManagerContext, RouteSwitchContext, SettingsCenterProvider } from
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { WorkflowPage } from './WorkflowPage';
|
import { WorkflowPage } from './WorkflowPage';
|
||||||
import { WorkflowPane, WorkflowShortcut } from './WorkflowShortcut';
|
import { WorkflowPane, WorkflowShortcut } from './WorkflowShortcut';
|
||||||
|
import { ExecutionPage } from './ExecutionPage';
|
||||||
|
|
||||||
export const WorkflowProvider = (props) => {
|
export const WorkflowProvider = (props) => {
|
||||||
const ctx = useContext(PluginManagerContext);
|
const ctx = useContext(PluginManagerContext);
|
||||||
const { routes, components, ...others } = useContext(RouteSwitchContext);
|
const { routes, components, ...others } = useContext(RouteSwitchContext);
|
||||||
routes[1].routes.unshift({
|
routes[1].routes.unshift({
|
||||||
type: 'route',
|
type: 'route',
|
||||||
path: '/admin/plugins/workflows/:id',
|
path: '/admin/settings/workflow/workflows/:id',
|
||||||
component: 'WorkflowPage',
|
component: 'WorkflowPage',
|
||||||
|
}, {
|
||||||
|
type: 'route',
|
||||||
|
path: '/admin/settings/workflow/executions/:id',
|
||||||
|
component: 'ExecutionPage',
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<SettingsCenterProvider
|
<SettingsCenterProvider
|
||||||
@ -34,7 +39,7 @@ export const WorkflowProvider = (props) => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RouteSwitchContext.Provider value={{ components: { ...components, WorkflowPage }, ...others, routes }}>
|
<RouteSwitchContext.Provider value={{ components: { ...components, WorkflowPage, ExecutionPage }, ...others, routes }}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</RouteSwitchContext.Provider>
|
</RouteSwitchContext.Provider>
|
||||||
</PluginManagerContext.Provider>
|
</PluginManagerContext.Provider>
|
||||||
|
@ -1,46 +1,27 @@
|
|||||||
import { PartitionOutlined } from '@ant-design/icons';
|
import React from 'react';
|
||||||
import { ISchema } from '@formily/react';
|
|
||||||
import { uid } from '@formily/shared';
|
|
||||||
import { ActionContext, PluginManager, SchemaComponent } from '@nocobase/client';
|
|
||||||
import { Card } from 'antd';
|
import { Card } from 'antd';
|
||||||
import React, { useState } from 'react';
|
import { PartitionOutlined } from '@ant-design/icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { ExecutionResourceProvider } from './ExecutionResourceProvider';
|
|
||||||
|
import { PluginManager, SchemaComponent } from '@nocobase/client';
|
||||||
|
|
||||||
import { workflowSchema } from './schemas/workflows';
|
import { workflowSchema } from './schemas/workflows';
|
||||||
import { WorkflowLink } from './WorkflowLink';
|
import { WorkflowLink } from './WorkflowLink';
|
||||||
|
import { ExecutionResourceProvider } from './ExecutionResourceProvider';
|
||||||
|
import { ExecutionLink } from './ExecutionLink';
|
||||||
|
|
||||||
const schema: ISchema = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
[uid()]: {
|
|
||||||
'x-component': 'Action.Drawer',
|
|
||||||
type: 'void',
|
|
||||||
title: '{{t("Workflow")}}',
|
|
||||||
properties: {
|
|
||||||
table: workflowSchema,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const schema2: ISchema = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
[uid()]: workflowSchema,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WorkflowPane = () => {
|
export const WorkflowPane = () => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
return (
|
return (
|
||||||
<Card bordered={false}>
|
<Card bordered={false}>
|
||||||
<SchemaComponent
|
<SchemaComponent
|
||||||
schema={schema2}
|
schema={workflowSchema}
|
||||||
components={{
|
components={{
|
||||||
WorkflowLink,
|
WorkflowLink,
|
||||||
ExecutionResourceProvider,
|
ExecutionResourceProvider,
|
||||||
|
ExecutionLink
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
@ -60,26 +41,3 @@ export const WorkflowShortcut = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkflowShortcut2 = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
return (
|
|
||||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
|
||||||
<PluginManager.Toolbar.Item
|
|
||||||
icon={<PartitionOutlined />}
|
|
||||||
title={t('Workflow')}
|
|
||||||
onClick={() => {
|
|
||||||
setVisible(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SchemaComponent
|
|
||||||
schema={schema}
|
|
||||||
components={{
|
|
||||||
WorkflowLink,
|
|
||||||
ExecutionResourceProvider,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ActionContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer, useForm } from "@formily/react";
|
import { Cascader, Input, InputNumber, Select } from "antd";
|
||||||
import { Button, Cascader, Dropdown, Input, InputNumber, Menu, Select, Form } from "antd";
|
|
||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
|
||||||
|
|
||||||
import { SchemaComponent, useCollectionManager, useCompile } from "@nocobase/client";
|
import { useCompile } from "@nocobase/client";
|
||||||
|
|
||||||
import { instructions, useNodeContext } from "./nodes";
|
import { instructions, useNodeContext } from "./nodes";
|
||||||
import { useFlowContext } from "./WorkflowCanvas";
|
import { useFlowContext } from "./FlowContext";
|
||||||
import { triggers } from "./triggers";
|
import { triggers } from "./triggers";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Registry } from "@nocobase/utils/client";
|
import { Registry } from "@nocobase/utils/client";
|
||||||
@ -451,109 +449,3 @@ export function VariableComponent({ value, onChange, renderSchemaComponent }) {
|
|||||||
</VariableTypesContext.Provider>
|
</VariableTypesContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: observer for watching useProps
|
|
||||||
export const CollectionFieldset = observer(({ value, onChange }: any) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const compile = useCompile();
|
|
||||||
const { getCollectionFields } = useCollectionManager();
|
|
||||||
const { values: data } = useForm();
|
|
||||||
const fields = getCollectionFields(data?.config?.collection)
|
|
||||||
.filter(field => (
|
|
||||||
!field.hidden
|
|
||||||
&& (field.uiSchema ? !field.uiSchema['x-read-pretty'] : false)
|
|
||||||
));
|
|
||||||
|
|
||||||
const VTypes = {
|
|
||||||
...VariableTypes,
|
|
||||||
constant: {
|
|
||||||
title: '{{t("Constant")}}',
|
|
||||||
value: 'constant',
|
|
||||||
options: undefined
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<fieldset className={css`
|
|
||||||
margin-top: .5em;
|
|
||||||
|
|
||||||
> .ant-formily-item{
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
> .ant-formily-item-label{
|
|
||||||
line-height: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`}>
|
|
||||||
{fields.length
|
|
||||||
? (
|
|
||||||
<>
|
|
||||||
{fields
|
|
||||||
.filter(field => field.name in value)
|
|
||||||
.map(field => {
|
|
||||||
const operand = typeof value[field.name] === 'string'
|
|
||||||
? parseStringValue(value[field.name], VTypes)
|
|
||||||
: { type: 'constant', value: value[field.name] };
|
|
||||||
|
|
||||||
// TODO: try to use <ObjectField> to replace this map
|
|
||||||
return (
|
|
||||||
<Form.Item key={field.name} label={compile(field.uiSchema?.title ?? field.name)} labelAlign="left" className={css`
|
|
||||||
.ant-form-item-control-input-content{
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
`}>
|
|
||||||
<VariableTypesContext.Provider value={VTypes}>
|
|
||||||
<Operand
|
|
||||||
value={operand}
|
|
||||||
onChange={(next) => {
|
|
||||||
if (next.type !== operand.type && next.type === 'constant') {
|
|
||||||
onChange({ ...value, [field.name]: null });
|
|
||||||
} else {
|
|
||||||
const { stringify } = VTypes[next.type];
|
|
||||||
onChange({ ...value, [field.name]: stringify(next) });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{operand.type === 'constant'
|
|
||||||
? <SchemaComponent schema={{ ...field.uiSchema, name: field.name }} />
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
</Operand>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
icon={<CloseCircleOutlined />}
|
|
||||||
onClick={() => {
|
|
||||||
const { [field.name]: _, ...rest } = value;
|
|
||||||
onChange(rest);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</VariableTypesContext.Provider>
|
|
||||||
</Form.Item>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{Object.keys(value).length < fields.length
|
|
||||||
? (
|
|
||||||
<Dropdown overlay={
|
|
||||||
<Menu onClick={({ key }) => onChange({ ...value, [key]: null })} className={css`
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
`}>
|
|
||||||
{fields
|
|
||||||
.filter(field => !(field.name in value))
|
|
||||||
.map(field => (
|
|
||||||
<Menu.Item key={field.name}>{compile(field.uiSchema?.title ?? field.name)}</Menu.Item>
|
|
||||||
))}
|
|
||||||
</Menu>
|
|
||||||
}>
|
|
||||||
<Button icon={<PlusOutlined />}>{t('Add field')}</Button>
|
|
||||||
</Dropdown>
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
: <p>{t('Please select collection first')}</p>
|
|
||||||
}
|
|
||||||
</fieldset>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
41
packages/plugins/workflow/src/client/constants.tsx
Normal file
41
packages/plugins/workflow/src/client/constants.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
CloseOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
CheckOutlined,
|
||||||
|
ExclamationOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
|
export const EXECUTION_STATUS = {
|
||||||
|
QUEUEING: null,
|
||||||
|
STARTED: 0,
|
||||||
|
SUCCEEDED: 1,
|
||||||
|
FAILED: -1,
|
||||||
|
CANCELED: -2
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExecutionStatusOptions = [
|
||||||
|
{ value: EXECUTION_STATUS.QUEUEING, label: '{{t("Queueing")}}', color: 'blue' },
|
||||||
|
{ value: EXECUTION_STATUS.STARTED, label: '{{t("On going")}}', color: 'gold' },
|
||||||
|
{ value: EXECUTION_STATUS.SUCCEEDED, label: '{{t("Succeeded")}}', color: 'green' },
|
||||||
|
{ value: EXECUTION_STATUS.FAILED, label: '{{t("Failed")}}', color: 'red' },
|
||||||
|
{ value: EXECUTION_STATUS.CANCELED, label: '{{t("Canceled")}}' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ExecutionStatusOptionsMap = ExecutionStatusOptions.reduce((map, option) => Object.assign(map, { [option.value]: option }), {});
|
||||||
|
|
||||||
|
export const JOB_STATUS = {
|
||||||
|
PENDING: 0,
|
||||||
|
RESOLVED: 1,
|
||||||
|
REJECTED: -1,
|
||||||
|
CANCELED: -2
|
||||||
|
};
|
||||||
|
|
||||||
|
export const JobStatusOptions = [
|
||||||
|
{ value: JOB_STATUS.PENDING, label: '{{t("Pending")}}', color: '#d4c306', icon: <ClockCircleOutlined /> },
|
||||||
|
{ value: JOB_STATUS.RESOLVED, label: '{{t("Succeeded")}}', color: '#67c068', icon: <CheckOutlined /> },
|
||||||
|
{ value: JOB_STATUS.REJECTED, label: '{{t("Failed")}}', color: '#f40', icon: <ExclamationOutlined /> },
|
||||||
|
{ value: JOB_STATUS.CANCELED, label: '{{t("Canceled")}}', color: '#f40', icon: <CloseOutlined /> }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const JobStatusOptionsMap = JobStatusOptions.reduce((map, option) => Object.assign(map, { [option.value]: option }), {});
|
@ -7,7 +7,8 @@ import { Trans, useTranslation } from "react-i18next";
|
|||||||
import { i18n } from "@nocobase/client";
|
import { i18n } from "@nocobase/client";
|
||||||
|
|
||||||
import { NodeDefaultView } from ".";
|
import { NodeDefaultView } from ".";
|
||||||
import { Branch, useFlowContext } from "../WorkflowCanvas";
|
import { Branch } from "../Branch";
|
||||||
|
import { useFlowContext } from '../FlowContext';
|
||||||
import { branchBlockClass, nodeSubtreeClass } from "../style";
|
import { branchBlockClass, nodeSubtreeClass } from "../style";
|
||||||
import { Calculation } from "../calculators";
|
import { Calculation } from "../calculators";
|
||||||
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import { Select } from 'antd';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useCollectionDataSource } from '@nocobase/client';
|
||||||
import { useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client';
|
|
||||||
import { BaseTypeSet, CollectionFieldset } from '../calculators';
|
|
||||||
import { collection, values } from '../schemas/collection';
|
import { collection, values } from '../schemas/collection';
|
||||||
import { useFlowContext } from '../WorkflowCanvas';
|
import { useFlowContext } from '../FlowContext';
|
||||||
import CollectionFieldSelect from '../components/CollectionFieldSelect';
|
import CollectionFieldSelect from '../components/CollectionFieldSelect';
|
||||||
|
import CollectionFieldset from '../components/CollectionFieldset';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
import { CloseOutlined, DeleteOutlined } from '@ant-design/icons';
|
import React, { useContext } from 'react';
|
||||||
|
import {
|
||||||
|
CloseOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { ISchema, useForm } from '@formily/react';
|
import { ISchema, useForm } from '@formily/react';
|
||||||
import { Registry } from '@nocobase/utils/client';
|
|
||||||
import { Button, message, Modal, Tag } from 'antd';
|
import { Button, message, Modal, Tag } from 'antd';
|
||||||
import React, { useContext } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Registry } from '@nocobase/utils/client';
|
||||||
import { SchemaComponent, useActionContext, useAPIClient, useCompile, useRequest, useResourceActionContext } from '@nocobase/client';
|
import { SchemaComponent, useActionContext, useAPIClient, useCompile, useRequest, useResourceActionContext } from '@nocobase/client';
|
||||||
|
|
||||||
import { nodeBlockClass, nodeCardClass, nodeClass, nodeHeaderClass, nodeMetaClass, nodeTitleClass } from '../style';
|
import { nodeBlockClass, nodeCardClass, nodeClass, nodeHeaderClass, nodeMetaClass, nodeTitleClass } from '../style';
|
||||||
import { AddButton, useFlowContext } from '../WorkflowCanvas';
|
import { AddButton } from '../AddButton';
|
||||||
|
import { useFlowContext } from '../FlowContext';
|
||||||
|
|
||||||
import calculation from './calculation';
|
import calculation from './calculation';
|
||||||
import condition from './condition';
|
import condition from './condition';
|
||||||
@ -19,6 +23,7 @@ import query from './query';
|
|||||||
import create from './create';
|
import create from './create';
|
||||||
import update from './update';
|
import update from './update';
|
||||||
import destroy from './destroy';
|
import destroy from './destroy';
|
||||||
|
import { JobStatusOptions, JobStatusOptionsMap } from '../constants';
|
||||||
|
|
||||||
export interface Instruction {
|
export interface Instruction {
|
||||||
title: string;
|
title: string;
|
||||||
@ -125,10 +130,12 @@ export function Node({ data }) {
|
|||||||
export function RemoveButton() {
|
export function RemoveButton() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const { workflow } = useFlowContext();
|
const { workflow, nodes, onNodeRemoved } = useFlowContext() ?? {};
|
||||||
const resource = api.resource('workflows.nodes', workflow.id);
|
|
||||||
const current = useNodeContext();
|
const current = useNodeContext();
|
||||||
const { nodes, onNodeRemoved } = useFlowContext();
|
if (!workflow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const resource = api.resource('workflows.nodes', workflow.id);
|
||||||
|
|
||||||
async function onRemove() {
|
async function onRemove() {
|
||||||
async function onOk() {
|
async function onOk() {
|
||||||
@ -163,9 +170,111 @@ export function RemoveButton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function JobButton() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const compile = useCompile();
|
||||||
|
const { execution } = useFlowContext();
|
||||||
|
const { id, type, title, job } = useNodeContext() ?? {};
|
||||||
|
if (!execution) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!job) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cx('workflow-node-job-button', css`
|
||||||
|
border: 2px solid #d9d9d9;
|
||||||
|
border-radius: 50%;
|
||||||
|
`)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const instruction = instructions.get(type);
|
||||||
|
const { value, icon, color } = JobStatusOptionsMap[job.status];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
[job.id]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
title: icon,
|
||||||
|
shape: 'circle',
|
||||||
|
className: ['workflow-node-job-button', css`
|
||||||
|
background-color: ${color};
|
||||||
|
&:hover,&:focus{
|
||||||
|
background-color: ${color}
|
||||||
|
}
|
||||||
|
`]
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
[job.id]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Form',
|
||||||
|
'x-decorator-props': {
|
||||||
|
initialValue: job
|
||||||
|
},
|
||||||
|
'x-component': 'Action.Modal',
|
||||||
|
title: (
|
||||||
|
<div className={cx(nodeTitleClass)}>
|
||||||
|
<Tag>{compile(instruction.title)}</Tag>
|
||||||
|
<strong>{title}</strong>
|
||||||
|
<span className="workflow-node-id">#{id}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
properties: {
|
||||||
|
status: {
|
||||||
|
type: 'number',
|
||||||
|
title: '{{t("Status")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Select',
|
||||||
|
enum: JobStatusOptions,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Executed at")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
showTime: true
|
||||||
|
},
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
type: 'object',
|
||||||
|
title: '{{t("Node result")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input.JSON',
|
||||||
|
'x-component-props': {
|
||||||
|
className: css`
|
||||||
|
padding: 1em;
|
||||||
|
background-color: #eee;
|
||||||
|
`
|
||||||
|
},
|
||||||
|
'x-read-pretty': true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function NodeDefaultView(props) {
|
export function NodeDefaultView(props) {
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { workflow } = useFlowContext();
|
const { workflow } = useFlowContext() ?? {};
|
||||||
|
if (!workflow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const { data, children } = props;
|
const { data, children } = props;
|
||||||
const instruction = instructions.get(data.type);
|
const instruction = instructions.get(data.type);
|
||||||
const detailText = workflow.executed ? '{{t("View")}}' : '{{t("Configure")}}';
|
const detailText = workflow.executed ? '{{t("View")}}' : '{{t("Configure")}}';
|
||||||
@ -182,6 +291,7 @@ export function NodeDefaultView(props) {
|
|||||||
<span className="workflow-node-id">#{data.id}</span>
|
<span className="workflow-node-id">#{data.id}</span>
|
||||||
</h4>
|
</h4>
|
||||||
<RemoveButton />
|
<RemoveButton />
|
||||||
|
<JobButton />
|
||||||
</div>
|
</div>
|
||||||
<SchemaComponent
|
<SchemaComponent
|
||||||
scope={instruction.scope}
|
scope={instruction.scope}
|
||||||
|
@ -7,7 +7,8 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { i18n } from "@nocobase/client";
|
import { i18n } from "@nocobase/client";
|
||||||
|
|
||||||
import { NodeDefaultView } from ".";
|
import { NodeDefaultView } from ".";
|
||||||
import { Branch, useFlowContext } from "../WorkflowCanvas";
|
import { Branch } from "../Branch";
|
||||||
|
import { useFlowContext } from '../FlowContext';
|
||||||
import { branchBlockClass, nodeSubtreeClass } from "../style";
|
import { branchBlockClass, nodeSubtreeClass } from "../style";
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client';
|
import { useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client';
|
||||||
|
|
||||||
import { useFlowContext } from '../WorkflowCanvas';
|
import { useFlowContext } from '../FlowContext';
|
||||||
import { VariableComponent } from '../calculators';
|
import { VariableComponent } from '../calculators';
|
||||||
import { collection, filter } from '../schemas/collection';
|
import { collection, filter } from '../schemas/collection';
|
||||||
import CollectionFieldSelect from '../components/CollectionFieldSelect';
|
import CollectionFieldSelect from '../components/CollectionFieldSelect';
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useCollectionDataSource } from '@nocobase/client';
|
import { useCollectionDataSource } from '@nocobase/client';
|
||||||
|
|
||||||
import { CollectionFieldset, VariableComponent } from '../calculators';
|
import { VariableComponent } from '../calculators';
|
||||||
|
import CollectionFieldset from '../components/CollectionFieldset';
|
||||||
import { collection, filter, values } from '../schemas/collection';
|
import { collection, filter, values } from '../schemas/collection';
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useActionContext } from '@nocobase/client';
|
||||||
|
import { ExecutionStatusOptions } from '../constants';
|
||||||
|
|
||||||
const collection = {
|
const collection = {
|
||||||
name: 'executions',
|
name: 'executions',
|
||||||
@ -10,21 +14,23 @@ const collection = {
|
|||||||
name: 'createdAt',
|
name: 'createdAt',
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
title: '{{t("Created at")}}',
|
title: '{{t("Triggered at")}}',
|
||||||
'x-component': 'DatePicker',
|
'x-component': 'DatePicker',
|
||||||
'x-component-props': {},
|
'x-component-props': {},
|
||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
} as ISchema,
|
} as ISchema,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
interface: 'number',
|
interface: 'object',
|
||||||
type: 'number',
|
type: 'belongsTo',
|
||||||
name: 'workflowId',
|
name: 'workflowId',
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
title: '{{t("Version")}}',
|
title: '{{t("Version")}}',
|
||||||
'x-component': 'InputNumber',
|
['x-component']({ value }) {
|
||||||
'x-read-pretty': true,
|
const { setVisible } = useActionContext();
|
||||||
|
return <Link to={`/admin/settings/workflow/workflows/${value}`} onClick={() => setVisible(false)}>{`#${value}`}</Link>;
|
||||||
|
}
|
||||||
} as ISchema,
|
} as ISchema,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -36,12 +42,7 @@ const collection = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
'x-component': 'Select',
|
'x-component': 'Select',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
enum: [
|
enum: ExecutionStatusOptions,
|
||||||
{ value: 0, label: '{{t("On going")}}' },
|
|
||||||
{ value: 1, label: '{{t("Succeeded")}}' },
|
|
||||||
{ value: -1, label: '{{t("Failed")}}' },
|
|
||||||
{ value: -2, label: '{{t("Canceled")}}' },
|
|
||||||
],
|
|
||||||
} as ISchema,
|
} as ISchema,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -58,6 +59,7 @@ export const executionSchema = {
|
|||||||
resource: 'executions',
|
resource: 'executions',
|
||||||
action: 'list',
|
action: 'list',
|
||||||
params: {
|
params: {
|
||||||
|
appends: ['workflow.id', 'workflow.title'],
|
||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
sort: ['-createdAt'],
|
sort: ['-createdAt'],
|
||||||
},
|
},
|
||||||
@ -130,27 +132,27 @@ export const executionSchema = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// actions: {
|
actions: {
|
||||||
// type: 'void',
|
type: 'void',
|
||||||
// title: '{{ t("Actions") }}',
|
title: '{{ t("Actions") }}',
|
||||||
// 'x-component': 'Table.Column',
|
'x-component': 'Table.Column',
|
||||||
// properties: {
|
properties: {
|
||||||
// actions: {
|
actions: {
|
||||||
// type: 'void',
|
type: 'void',
|
||||||
// 'x-component': 'Space',
|
'x-component': 'Space',
|
||||||
// 'x-component-props': {
|
'x-component-props': {
|
||||||
// split: '|',
|
split: '|',
|
||||||
// },
|
},
|
||||||
// properties: {
|
properties: {
|
||||||
// config: {
|
config: {
|
||||||
// type: 'void',
|
type: 'void',
|
||||||
// title: '查看',
|
title: '{{t("Details")}}',
|
||||||
// 'x-component': 'ExecutionLink'
|
'x-component': 'ExecutionLink'
|
||||||
// },
|
},
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -30,6 +30,7 @@ const collection = {
|
|||||||
enum: Array.from(triggers.getEntities()).map(([value, { title }]) => ({
|
enum: Array.from(triggers.getEntities()).map(([value, { title }]) => ({
|
||||||
value,
|
value,
|
||||||
label: title,
|
label: title,
|
||||||
|
color: 'gold'
|
||||||
})),
|
})),
|
||||||
required: true,
|
required: true,
|
||||||
} as ISchema,
|
} as ISchema,
|
||||||
@ -52,7 +53,7 @@ const collection = {
|
|||||||
title: '{{t("Status")}}',
|
title: '{{t("Status")}}',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: [
|
enum: [
|
||||||
{ label: '{{t("On")}}', value: true },
|
{ label: '{{t("On")}}', value: true, color: '#52c41a' },
|
||||||
{ label: '{{t("Off")}}', value: false },
|
{ label: '{{t("Off")}}', value: false },
|
||||||
],
|
],
|
||||||
'x-component': 'Radio.Group',
|
'x-component': 'Radio.Group',
|
||||||
@ -64,7 +65,7 @@ const collection = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const workflowSchema: ISchema = {
|
export const workflowSchema: ISchema = {
|
||||||
type: 'object',
|
type: 'void',
|
||||||
properties: {
|
properties: {
|
||||||
provider: {
|
provider: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
@ -235,7 +236,7 @@ export const workflowSchema: ISchema = {
|
|||||||
},
|
},
|
||||||
executions: {
|
executions: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{t("Execution History")}}',
|
title: '{{t("Execution history")}}',
|
||||||
'x-component': 'Action.Link',
|
'x-component': 'Action.Link',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
@ -243,7 +244,7 @@ export const workflowSchema: ISchema = {
|
|||||||
properties: {
|
properties: {
|
||||||
drawer: {
|
drawer: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{t("Execution History")}}',
|
title: '{{t("Execution history")}}',
|
||||||
'x-component': 'Action.Drawer',
|
'x-component': 'Action.Drawer',
|
||||||
properties: executionSchema
|
properties: executionSchema
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,13 @@ export const workflowPageClass = css`
|
|||||||
header{
|
header{
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: .5em;
|
|
||||||
|
> *:not(:last-child) {
|
||||||
|
&:after{
|
||||||
|
content: ">";
|
||||||
|
margin: 0 .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
aside{
|
aside{
|
||||||
@ -38,8 +44,20 @@ export const workflowPageClass = css`
|
|||||||
export const workflowVersionDropdownClass = css`
|
export const workflowVersionDropdownClass = css`
|
||||||
.ant-dropdown-menu-item{
|
.ant-dropdown-menu-item{
|
||||||
|
|
||||||
|
strong{
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.enabled{
|
||||||
|
strong{
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.unexecuted{
|
&.unexecuted{
|
||||||
font-style: italic;
|
strong{
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-dropdown-menu-title-content{
|
.ant-dropdown-menu-title-content{
|
||||||
@ -148,10 +166,14 @@ export const nodeCardClass = css`
|
|||||||
padding: 1em;
|
padding: 1em;
|
||||||
box-shadow: 0 .25em .5em rgba(0, 0, 0, .1);
|
box-shadow: 0 .25em .5em rgba(0, 0, 0, .1);
|
||||||
|
|
||||||
.workflow-node-remove-button{
|
.workflow-node-remove-button,
|
||||||
|
.workflow-node-job-button{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -.5em;
|
right: -.5em;
|
||||||
top: -.5em;
|
top: -.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-node-remove-button{
|
||||||
color: #999;
|
color: #999;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity .3s ease;
|
transition: opacity .3s ease;
|
||||||
@ -165,6 +187,23 @@ export const nodeCardClass = css`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-node-job-button{
|
||||||
|
display: flex;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
min-width: 1.25rem;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&[type="button"]{
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover{
|
&:hover{
|
||||||
.workflow-node-remove-button{
|
.workflow-node-remove-button{
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@ -181,6 +220,8 @@ export const nodeMetaClass = css`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const nodeTitleClass = css`
|
export const nodeTitleClass = css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
|
||||||
.workflow-node-id{
|
.workflow-node-id{
|
||||||
|
@ -4,7 +4,7 @@ import { observer, useForm, useFormEffects } from '@formily/react';
|
|||||||
|
|
||||||
import { useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client';
|
import { useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client';
|
||||||
|
|
||||||
import { useFlowContext } from '../WorkflowCanvas';
|
import { useFlowContext } from '../FlowContext';
|
||||||
import { collection, filter } from '../schemas/collection';
|
import { collection, filter } from '../schemas/collection';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { onFieldValueChange } from '@formily/core';
|
import { onFieldValueChange } from '@formily/core';
|
||||||
|
@ -4,11 +4,12 @@ import { Registry } from "@nocobase/utils/client";
|
|||||||
import { message, Tag } from "antd";
|
import { message, Tag } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { InfoOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
import { SchemaComponent, useActionContext, useAPIClient, useCompile, useResourceActionContext } from '@nocobase/client';
|
import { SchemaComponent, useActionContext, useAPIClient, useCompile, useResourceActionContext } from '@nocobase/client';
|
||||||
|
|
||||||
import { nodeCardClass, nodeMetaClass } from "../style";
|
import { nodeCardClass, nodeHeaderClass, nodeMetaClass, nodeTitleClass } from "../style";
|
||||||
import { useFlowContext } from "../WorkflowCanvas";
|
import { useFlowContext } from "../FlowContext";
|
||||||
import collection from './collection';
|
import collection from './collection';
|
||||||
import schedule from "./schedule/";
|
import schedule from "./schedule/";
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ function useUpdateConfigAction() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const { workflow } = useFlowContext();
|
const { workflow } = useFlowContext() ?? {};
|
||||||
const ctx = useActionContext();
|
const ctx = useActionContext();
|
||||||
const { refresh } = useResourceActionContext();
|
const { refresh } = useResourceActionContext();
|
||||||
return {
|
return {
|
||||||
@ -55,24 +56,98 @@ export const triggers = new Registry<Trigger>();
|
|||||||
triggers.register(collection.type, collection);
|
triggers.register(collection.type, collection);
|
||||||
triggers.register(schedule.type, schedule);
|
triggers.register(schedule.type, schedule);
|
||||||
|
|
||||||
export const TriggerConfig = () => {
|
function TriggerExecution() {
|
||||||
const { t } = useTranslation();
|
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { data } = useResourceActionContext();
|
const { workflow, execution } = useFlowContext();
|
||||||
if (!data) {
|
if (!execution) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { type, config, executed } = data.data;
|
|
||||||
|
const trigger = triggers.get(workflow.type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
trigger: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
title: <InfoOutlined />,
|
||||||
|
shape: 'circle',
|
||||||
|
className: 'workflow-node-job-button',
|
||||||
|
type: 'primary'
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
[execution.id]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'Form',
|
||||||
|
'x-decorator-props': {
|
||||||
|
initialValue: execution
|
||||||
|
},
|
||||||
|
'x-component': 'Action.Modal',
|
||||||
|
title: (
|
||||||
|
<div className={cx(nodeTitleClass)}>
|
||||||
|
<Tag>{compile(trigger.title)}</Tag>
|
||||||
|
<strong>{workflow.title}</strong>
|
||||||
|
<span className="workflow-node-id">#{execution.id}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
properties: {
|
||||||
|
createdAt: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Triggered at")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
showTime: true
|
||||||
|
},
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
type: 'object',
|
||||||
|
title: '{{t("Trigger context")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input.JSON',
|
||||||
|
'x-component-props': {
|
||||||
|
className: css`
|
||||||
|
padding: 1em;
|
||||||
|
background-color: #eee;
|
||||||
|
`
|
||||||
|
},
|
||||||
|
'x-read-pretty': true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TriggerConfig = ({ workflow }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const compile = useCompile();
|
||||||
|
if (!workflow || !workflow.type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { type, config, executed } = workflow;
|
||||||
const { title, fieldset, scope, components } = triggers.get(type);
|
const { title, fieldset, scope, components } = triggers.get(type);
|
||||||
const detailText = executed ? '{{t("View")}}' : '{{t("Configure")}}';
|
const detailText = executed ? '{{t("View")}}' : '{{t("Configure")}}';
|
||||||
const titleText = `${t('Trigger')}: ${compile(title)}`;
|
const titleText = `${t('Trigger')}: ${compile(title)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(nodeCardClass)}>
|
<div className={cx(nodeCardClass)}>
|
||||||
<div className={cx(nodeMetaClass)}>
|
<div className={cx(nodeHeaderClass)}>
|
||||||
<Tag color="gold">{t('Trigger')}</Tag>
|
<div className={cx(nodeMetaClass)}>
|
||||||
|
<Tag color="gold">{t('Trigger')}</Tag>
|
||||||
|
</div>
|
||||||
|
<h4>{compile(title)}</h4>
|
||||||
|
<TriggerExecution />
|
||||||
</div>
|
</div>
|
||||||
<h4>{compile(title)}</h4>
|
|
||||||
<SchemaComponent
|
<SchemaComponent
|
||||||
schema={{
|
schema={{
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
@ -5,7 +5,7 @@ import { Select } from 'antd';
|
|||||||
import { useCompile, useCollectionDataSource, useCollectionManager } from '@nocobase/client';
|
import { useCompile, useCollectionDataSource, useCollectionManager } from '@nocobase/client';
|
||||||
|
|
||||||
import { ScheduleConfig } from './ScheduleConfig';
|
import { ScheduleConfig } from './ScheduleConfig';
|
||||||
import { useFlowContext } from '../../WorkflowCanvas';
|
import { useFlowContext } from '../../FlowContext';
|
||||||
import { BaseTypeSet } from '../../calculators';
|
import { BaseTypeSet } from '../../calculators';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
Loading…
Reference in New Issue
Block a user