diff --git a/packages/core/client/src/collection-manager/interfaces/index.ts b/packages/core/client/src/collection-manager/interfaces/index.ts index 32388480d..f2aa1e806 100644 --- a/packages/core/client/src/collection-manager/interfaces/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/index.ts @@ -12,6 +12,7 @@ export * from './icon'; export * from './id'; export * from './input'; export * from './integer'; +export * from './json'; export * from './linkTo'; export * from './m2m'; export * from './m2o'; diff --git a/packages/core/client/src/collection-manager/interfaces/json.ts b/packages/core/client/src/collection-manager/interfaces/json.ts new file mode 100644 index 000000000..9b3ccbce5 --- /dev/null +++ b/packages/core/client/src/collection-manager/interfaces/json.ts @@ -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: { + } +}; diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index 99a052af8..f9c545957 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -416,7 +416,7 @@ export default { "Tencent COS": "Tencent COS", "Amazon S3": "Amazon S3", "Workflow": "Workflow", - "Execution History": "Execution History", + "Execution history": "Execution history", "Trigger type": "Trigger type", "Description": "Description", "Status": "Status", diff --git a/packages/core/client/src/locale/ja_JP.ts b/packages/core/client/src/locale/ja_JP.ts index 465f25bd0..4209484c1 100644 --- a/packages/core/client/src/locale/ja_JP.ts +++ b/packages/core/client/src/locale/ja_JP.ts @@ -416,7 +416,7 @@ export default { "Tencent COS": "Tencent COS", "Amazon S3": "Amazon S3", "Workflow": "ワークフロー", - "Execution History": "実行履歴", + "Execution history": "実行履歴", "Trigger type": "トリガータイプ", "Description": "説明", "Status": "状態", diff --git a/packages/core/client/src/locale/ru_RU.ts b/packages/core/client/src/locale/ru_RU.ts index 9c33e3779..4deeecc90 100644 --- a/packages/core/client/src/locale/ru_RU.ts +++ b/packages/core/client/src/locale/ru_RU.ts @@ -416,7 +416,7 @@ export default { "Tencent COS": "Tencent COS", "Amazon S3": "Amazon S3", "Workflow": "Workflow", - "Execution History": "История запусков", + "Execution history": "История запусков", "Trigger type": "Тип триггера", "Description": "Описание", "Status": "Статус", diff --git a/packages/core/client/src/locale/tr_TR.ts b/packages/core/client/src/locale/tr_TR.ts index 6f2ce1492..4d0f1d52a 100644 --- a/packages/core/client/src/locale/tr_TR.ts +++ b/packages/core/client/src/locale/tr_TR.ts @@ -415,7 +415,7 @@ export default { "Aliyun OSS": "Aliyun OSS", "Amazon S3": "Amazon S3", "Workflow": "İş Akışı", - "Execution History": "Yürütme Geçmişi", + "Execution history": "Yürütme Geçmişi", "Trigger type": "Tetikleme türü", "Description": "Açıklama", "Status": "Durum", diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 91a9ecdda..b7bf910a1 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -510,7 +510,7 @@ export default { // plugins/workflow 'Workflow': '工作流', - 'Execution History': '执行历史', + 'Execution history': '执行历史', 'Trigger type': '触发方式', 'Description': '描述', 'Status': '状态', @@ -519,9 +519,11 @@ export default { 'Version': '版本', 'Copy to new version': '复制到新版本', + 'Loading': '加载中', 'Load failed': '加载失败', 'Trigger': '触发器', + 'Triggered at': '触发时间', 'Collection event': '数据表事件', 'Trigger on': '触发时机', 'After record added': '新增数据后', @@ -578,9 +580,13 @@ export default { 'Arithmetic calculation': '算术运算', 'String operation': '字符串', + 'Executed at': '执行于', + + 'Queueing': '队列中', 'On going': '进行中', 'Succeeded': '成功', 'Failed': '失败', + 'Pending': '等待处理', 'Canceled': '已取消', 'This node contains branches, deleting will also be preformed to them, are you sure?': '节点包含分支,将同时删除其所有分支下的子节点,确定继续?', diff --git a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx index 18f53a8f5..2d810eb70 100644 --- a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx +++ b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx @@ -177,6 +177,7 @@ const InternalAdminLayout = (props: any) => { div { diff --git a/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx b/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx index a7a4a290a..364eef5a2 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx @@ -8,7 +8,7 @@ import { useActionContext } from '.'; import { ComposedActionDrawer } from './types'; 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 schema = useFieldSchema(); const field = useField(); @@ -27,7 +27,7 @@ export const ActionModal: ComposedActionDrawer = observer((props) => { }} > { if (!isValid(props.value)) { - return
; + return null; } return props.value ? : null; }), @@ -42,7 +42,7 @@ Checkbox.Group = connect( }), mapReadPretty((props) => { if (!isValid(props.value)) { - return
; + return null; } const { options = [] } = props; const field = useField(); @@ -53,7 +53,7 @@ Checkbox.Group = connect( {dataSource .filter((option) => value.includes(option.value)) .map((option, key) => ( - + {option.label} ))} diff --git a/packages/core/client/src/schema-component/antd/input/Input.tsx b/packages/core/client/src/schema-component/antd/input/Input.tsx index 0fe86c39d..cbfa847b4 100644 --- a/packages/core/client/src/schema-component/antd/input/Input.tsx +++ b/packages/core/client/src/schema-component/antd/input/Input.tsx @@ -4,10 +4,12 @@ import { Input as AntdInput } from 'antd'; import { InputProps, TextAreaProps } from 'antd/lib/input'; import React from 'react'; import { ReadPretty } from './ReadPretty'; +import { Json } from './Json'; type ComposedInput = React.FC & { TextArea?: React.FC; URL?: React.FC; + JSON?: React.FC; }; export const Input: ComposedInput = connect( @@ -34,6 +36,9 @@ Input.TextArea = connect( }), mapReadPretty(ReadPretty.TextArea), ); + Input.URL = connect(AntdInput, mapReadPretty(ReadPretty.URL)); +Input.JSON = connect(Json, mapReadPretty(ReadPretty.JSON)); + export default Input; diff --git a/packages/core/client/src/schema-component/antd/input/Json.tsx b/packages/core/client/src/schema-component/antd/input/Json.tsx new file mode 100644 index 000000000..fb5e51175 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/input/Json.tsx @@ -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(); + return ( + { + 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], + }); + } + }} + /> + ); +} diff --git a/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx index 103759b3a..25c0fb7ed 100644 --- a/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx +++ b/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx @@ -5,6 +5,7 @@ import React from 'react'; import { useCompile } from '../..'; import { EllipsisWithTooltip } from './EllipsisWithTooltip'; import { HTMLEncode } from './shared'; +import { cx, css } from '@emotion/css'; type Composed = { Input: React.FC; @@ -13,6 +14,7 @@ type Composed = { TextAreaProps & { ellipsis?: any; text?: any; addonBefore?: any; suffix?: any; addonAfter?: any; autop?: boolean } >; Html?: any; + JSON?: React.FC; }; export const ReadPretty: Composed = () => null; @@ -114,3 +116,17 @@ ReadPretty.URL = (props) => { ); }; + +ReadPretty.JSON = (props) => { + const prefixCls = usePrefixCls('json', props); + return ( +
+      {props.value != null ? JSON.stringify(props.value, null, props.space ?? 2) : ''}
+    
+ ); +}; diff --git a/packages/core/client/src/schema-component/antd/input/demos/demo4.tsx b/packages/core/client/src/schema-component/antd/input/demos/demo4.tsx new file mode 100644 index 000000000..5ad45413c --- /dev/null +++ b/packages/core/client/src/schema-component/antd/input/demos/demo4.tsx @@ -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 ( + + + + ); + }; diff --git a/packages/core/client/src/schema-component/antd/input/index.md b/packages/core/client/src/schema-component/antd/input/index.md index affbf75b2..d4654360a 100644 --- a/packages/core/client/src/schema-component/antd/input/index.md +++ b/packages/core/client/src/schema-component/antd/input/index.md @@ -26,3 +26,7 @@ group: ### URL + +### JSON + + diff --git a/packages/core/client/src/schema-component/antd/input/index.ts b/packages/core/client/src/schema-component/antd/input/index.ts index de2898f61..522be2339 100644 --- a/packages/core/client/src/schema-component/antd/input/index.ts +++ b/packages/core/client/src/schema-component/antd/input/index.ts @@ -1,2 +1,3 @@ export * from './Input'; export * from './ReadPretty'; +export * from './Json'; diff --git a/packages/core/client/src/schema-component/antd/radio/Radio.tsx b/packages/core/client/src/schema-component/antd/radio/Radio.tsx index ed95502f6..d0123a5a5 100644 --- a/packages/core/client/src/schema-component/antd/radio/Radio.tsx +++ b/packages/core/client/src/schema-component/antd/radio/Radio.tsx @@ -35,7 +35,7 @@ Radio.Group = connect( {dataSource .filter((option) => option.value === value) .map((option, key) => ( - + {option.label} ))} diff --git a/packages/core/client/src/schema-component/antd/select/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/select/ReadPretty.tsx index 4d0e487a0..95fb7ac51 100644 --- a/packages/core/client/src/schema-component/antd/select/ReadPretty.tsx +++ b/packages/core/client/src/schema-component/antd/select/ReadPretty.tsx @@ -24,7 +24,7 @@ export const ReadPretty = observer((props: any) => { return (
{options.map((option, key) => ( - + {option[fieldNames.label]} ))} diff --git a/packages/plugins/workflow/package.json b/packages/plugins/workflow/package.json index 2290e88cb..9fbe53b0a 100644 --- a/packages/plugins/workflow/package.json +++ b/packages/plugins/workflow/package.json @@ -15,6 +15,7 @@ "@nocobase/database": "0.8.0-alpha.1", "@nocobase/server": "0.8.0-alpha.1", "@nocobase/utils": "0.8.0-alpha.1", + "classnames": "^2.3.1", "cron-parser": "4.4.0", "json-templates": "^4.2.0", "react-js-cron": "^1.4.0" diff --git a/packages/plugins/workflow/src/client/AddButton.tsx b/packages/plugins/workflow/src/client/AddButton.tsx new file mode 100644 index 000000000..ba51ba5e5 --- /dev/null +++ b/packages/plugins/workflow/src/client/AddButton.tsx @@ -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 ( +
+ onCreate(ev)}> + {groups.map(group => ( + + {instructionList.filter(item => item.group === group.value).map(item => item.options + ? ( + + {item.options.map(option => ( + {compile(option.label)} + ))} + + ) + : ( + {compile(item.title)} + ))} + + ))} + + } + disabled={workflow.executed} + > +
+ ); +}; diff --git a/packages/plugins/workflow/src/client/Branch.tsx b/packages/plugins/workflow/src/client/Branch.tsx new file mode 100644 index 000000000..8a26026db --- /dev/null +++ b/packages/plugins/workflow/src/client/Branch.tsx @@ -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 ( +
+
+ {controller} + +
+ {list.map(item => )} +
+
+ ); +} diff --git a/packages/plugins/workflow/src/client/ExecutionCanvas.tsx b/packages/plugins/workflow/src/client/ExecutionCanvas.tsx new file mode 100644 index 000000000..0fb1a6c54 --- /dev/null +++ b/packages/plugins/workflow/src/client/ExecutionCanvas.tsx @@ -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
{t('Loading')}
+ } else { + return
{t('Load failed')}
; + } + } + + 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 ( + +
+
+ + + {t('Workflow')} + + + + + {workflow.title} + + + {`#${execution.id}`} +
+ +
+
+ +
+ +
+
+
+ {t('End')} +
+
+
+
+ ); +} diff --git a/packages/plugins/workflow/src/client/ExecutionLink.tsx b/packages/plugins/workflow/src/client/ExecutionLink.tsx new file mode 100644 index 000000000..bbbd93a5e --- /dev/null +++ b/packages/plugins/workflow/src/client/ExecutionLink.tsx @@ -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 ( + setVisible(false)}>{t('View')} + ); +} diff --git a/packages/plugins/workflow/src/client/ExecutionPage.tsx b/packages/plugins/workflow/src/client/ExecutionPage.tsx new file mode 100644 index 000000000..46529848d --- /dev/null +++ b/packages/plugins/workflow/src/client/ExecutionPage.tsx @@ -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(); + + return ( +
+ +
+ ); +}; diff --git a/packages/plugins/workflow/src/client/FlowContext.ts b/packages/plugins/workflow/src/client/FlowContext.ts new file mode 100644 index 000000000..ce3e7825c --- /dev/null +++ b/packages/plugins/workflow/src/client/FlowContext.ts @@ -0,0 +1,8 @@ +import React, { useContext } from "react"; + + +export const FlowContext = React.createContext(null); + +export function useFlowContext() { + return useContext(FlowContext); +} diff --git a/packages/plugins/workflow/src/client/WorkflowCanvas.tsx b/packages/plugins/workflow/src/client/WorkflowCanvas.tsx index 6c39c4afb..f3990d572 100644 --- a/packages/plugins/workflow/src/client/WorkflowCanvas.tsx +++ b/packages/plugins/workflow/src/client/WorkflowCanvas.tsx @@ -1,21 +1,21 @@ -import React, { useContext, useEffect } from 'react'; -import { useHistory } from 'react-router-dom'; +import React, { useEffect } from 'react'; +import { Link, useHistory } from 'react-router-dom'; 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 { useTranslation } from 'react-i18next'; +import classnames from 'classnames'; import { - useAPIClient, - useCompile, useDocumentTitle, useResourceActionContext, useResourceContext } from '@nocobase/client'; -import { Instruction, instructions, Node } from './nodes'; -import { addButtonClass, branchBlockClass, branchClass, nodeCardClass, nodeMetaClass, workflowVersionDropdownClass } from './style'; +import { FlowContext } from './FlowContext'; +import { branchBlockClass, nodeCardClass, nodeMetaClass, workflowVersionDropdownClass } from './style'; 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() { const { t } = useTranslation(); const history = useHistory(); @@ -48,7 +42,7 @@ export function WorkflowCanvas() { const { setTitle } = useDocumentTitle(); useEffect(() => { const { title } = data?.data ?? {}; - setTitle(`${title ? `${title} - ` : ''}${t('Workflow')}`); + setTitle(`${t('Workflow')}${title ? `: ${title}` : ''}`); }, [data?.data]); if (!data?.data && !loading) { @@ -72,7 +66,7 @@ export function WorkflowCanvas() { filterByTk: workflow[targetKey], values: { enabled: value, - // NOTE: keep `key` field to adapter for backend + // NOTE: keep `key` field to adapt for backend key: workflow.key } }); @@ -97,6 +91,11 @@ export function WorkflowCanvas() { }}>
+ + + {t('Workflow')} + + {workflow.title}
- +
@@ -154,98 +157,3 @@ export function WorkflowCanvas() { ); } - -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 ( -
-
- {controller} - -
- {list.map(item => )} -
-
- ); -} - -// 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 ( -
- onCreate(ev)}> - {groups.map(group => ( - - {instructionList.filter(item => item.group === group.value).map(item => item.options - ? ( - - {item.options.map(option => ( - {compile(option.label)} - ))} - - ) - : ( - {compile(item.title)} - ))} - - ))} - - } - disabled={workflow.executed} - > -
- ); -}; diff --git a/packages/plugins/workflow/src/client/WorkflowLink.tsx b/packages/plugins/workflow/src/client/WorkflowLink.tsx index 34c6ce953..2584b9593 100644 --- a/packages/plugins/workflow/src/client/WorkflowLink.tsx +++ b/packages/plugins/workflow/src/client/WorkflowLink.tsx @@ -10,6 +10,6 @@ export const WorkflowLink = () => { const { id } = useRecord(); const { setVisible } = useActionContext(); return ( - setVisible(false)}>{t('View')} + setVisible(false)}>{t('View')} ); } diff --git a/packages/plugins/workflow/src/client/WorkflowPage.tsx b/packages/plugins/workflow/src/client/WorkflowPage.tsx index 1bc6ff7e4..50f2ba90e 100644 --- a/packages/plugins/workflow/src/client/WorkflowPage.tsx +++ b/packages/plugins/workflow/src/client/WorkflowPage.tsx @@ -30,7 +30,7 @@ export const WorkflowPage = () => { action: 'get', 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'], }, }, }, diff --git a/packages/plugins/workflow/src/client/WorkflowProvider.tsx b/packages/plugins/workflow/src/client/WorkflowProvider.tsx index 9597fddfa..16da46904 100644 --- a/packages/plugins/workflow/src/client/WorkflowProvider.tsx +++ b/packages/plugins/workflow/src/client/WorkflowProvider.tsx @@ -2,14 +2,19 @@ import { PluginManagerContext, RouteSwitchContext, SettingsCenterProvider } from import React, { useContext } from 'react'; import { WorkflowPage } from './WorkflowPage'; import { WorkflowPane, WorkflowShortcut } from './WorkflowShortcut'; +import { ExecutionPage } from './ExecutionPage'; export const WorkflowProvider = (props) => { const ctx = useContext(PluginManagerContext); const { routes, components, ...others } = useContext(RouteSwitchContext); routes[1].routes.unshift({ type: 'route', - path: '/admin/plugins/workflows/:id', + path: '/admin/settings/workflow/workflows/:id', component: 'WorkflowPage', + }, { + type: 'route', + path: '/admin/settings/workflow/executions/:id', + component: 'ExecutionPage', }); return ( { }, }} > - + {props.children} diff --git a/packages/plugins/workflow/src/client/WorkflowShortcut.tsx b/packages/plugins/workflow/src/client/WorkflowShortcut.tsx index c6e7b12a6..5f8d8c4cb 100644 --- a/packages/plugins/workflow/src/client/WorkflowShortcut.tsx +++ b/packages/plugins/workflow/src/client/WorkflowShortcut.tsx @@ -1,46 +1,27 @@ -import { PartitionOutlined } from '@ant-design/icons'; -import { ISchema } from '@formily/react'; -import { uid } from '@formily/shared'; -import { ActionContext, PluginManager, SchemaComponent } from '@nocobase/client'; +import React from 'react'; import { Card } from 'antd'; -import React, { useState } from 'react'; +import { PartitionOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; -import { ExecutionResourceProvider } from './ExecutionResourceProvider'; + +import { PluginManager, SchemaComponent } from '@nocobase/client'; + import { workflowSchema } from './schemas/workflows'; 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 = () => { - const { t } = useTranslation(); - const [visible, setVisible] = useState(false); return ( @@ -60,26 +41,3 @@ export const WorkflowShortcut = () => { /> ); }; - -export const WorkflowShortcut2 = () => { - const { t } = useTranslation(); - const [visible, setVisible] = useState(false); - return ( - - } - title={t('Workflow')} - onClick={() => { - setVisible(true); - }} - /> - - - ); -}; diff --git a/packages/plugins/workflow/src/client/calculators.tsx b/packages/plugins/workflow/src/client/calculators.tsx index 6825dec8c..4dc5d5549 100644 --- a/packages/plugins/workflow/src/client/calculators.tsx +++ b/packages/plugins/workflow/src/client/calculators.tsx @@ -1,13 +1,11 @@ import React from "react"; -import { observer, useForm } from "@formily/react"; -import { Button, Cascader, Dropdown, Input, InputNumber, Menu, Select, Form } from "antd"; +import { Cascader, Input, InputNumber, Select } from "antd"; 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 { useFlowContext } from "./WorkflowCanvas"; +import { useFlowContext } from "./FlowContext"; import { triggers } from "./triggers"; import { useTranslation } from "react-i18next"; import { Registry } from "@nocobase/utils/client"; @@ -451,109 +449,3 @@ export function VariableComponent({ value, onChange, renderSchemaComponent }) { ); } - -// 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 ( -
.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 to replace this map - return ( - - - { - 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' - ? - : null - } - - - - ) - : null - } - - ) - :

{t('Please select collection first')}

- } -
- ); -}); diff --git a/packages/plugins/workflow/src/client/constants.tsx b/packages/plugins/workflow/src/client/constants.tsx new file mode 100644 index 000000000..30775e45e --- /dev/null +++ b/packages/plugins/workflow/src/client/constants.tsx @@ -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: }, + { value: JOB_STATUS.RESOLVED, label: '{{t("Succeeded")}}', color: '#67c068', icon: }, + { value: JOB_STATUS.REJECTED, label: '{{t("Failed")}}', color: '#f40', icon: }, + { value: JOB_STATUS.CANCELED, label: '{{t("Canceled")}}', color: '#f40', icon: } +]; + +export const JobStatusOptionsMap = JobStatusOptions.reduce((map, option) => Object.assign(map, { [option.value]: option }), {}); diff --git a/packages/plugins/workflow/src/client/nodes/condition.tsx b/packages/plugins/workflow/src/client/nodes/condition.tsx index 0cd16e9d9..0a05556a9 100644 --- a/packages/plugins/workflow/src/client/nodes/condition.tsx +++ b/packages/plugins/workflow/src/client/nodes/condition.tsx @@ -7,7 +7,8 @@ import { Trans, useTranslation } from "react-i18next"; import { i18n } from "@nocobase/client"; import { NodeDefaultView } from "."; -import { Branch, useFlowContext } from "../WorkflowCanvas"; +import { Branch } from "../Branch"; +import { useFlowContext } from '../FlowContext'; import { branchBlockClass, nodeSubtreeClass } from "../style"; import { Calculation } from "../calculators"; diff --git a/packages/plugins/workflow/src/client/nodes/create.tsx b/packages/plugins/workflow/src/client/nodes/create.tsx index 80b14bd47..c662403fa 100644 --- a/packages/plugins/workflow/src/client/nodes/create.tsx +++ b/packages/plugins/workflow/src/client/nodes/create.tsx @@ -1,11 +1,10 @@ -import { Select } from 'antd'; import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client'; -import { BaseTypeSet, CollectionFieldset } from '../calculators'; +import { useCollectionDataSource } from '@nocobase/client'; + import { collection, values } from '../schemas/collection'; -import { useFlowContext } from '../WorkflowCanvas'; +import { useFlowContext } from '../FlowContext'; import CollectionFieldSelect from '../components/CollectionFieldSelect'; +import CollectionFieldset from '../components/CollectionFieldset'; diff --git a/packages/plugins/workflow/src/client/nodes/index.tsx b/packages/plugins/workflow/src/client/nodes/index.tsx index ef9093b06..37ab8b77a 100644 --- a/packages/plugins/workflow/src/client/nodes/index.tsx +++ b/packages/plugins/workflow/src/client/nodes/index.tsx @@ -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 { ISchema, useForm } from '@formily/react'; -import { Registry } from '@nocobase/utils/client'; import { Button, message, Modal, Tag } from 'antd'; -import React, { useContext } from 'react'; import { useTranslation } from 'react-i18next'; +import { Registry } from '@nocobase/utils/client'; import { SchemaComponent, useActionContext, useAPIClient, useCompile, useRequest, useResourceActionContext } from '@nocobase/client'; 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 condition from './condition'; @@ -19,6 +23,7 @@ import query from './query'; import create from './create'; import update from './update'; import destroy from './destroy'; +import { JobStatusOptions, JobStatusOptionsMap } from '../constants'; export interface Instruction { title: string; @@ -125,10 +130,12 @@ export function Node({ data }) { export function RemoveButton() { const { t } = useTranslation(); const api = useAPIClient(); - const { workflow } = useFlowContext(); - const resource = api.resource('workflows.nodes', workflow.id); + const { workflow, nodes, onNodeRemoved } = useFlowContext() ?? {}; const current = useNodeContext(); - const { nodes, onNodeRemoved } = useFlowContext(); + if (!workflow) { + return null; + } + const resource = api.resource('workflows.nodes', workflow.id); async function onRemove() { 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 ( + + ); + } + + const instruction = instructions.get(type); + const { value, icon, color } = JobStatusOptionsMap[job.status]; + + return ( + + {compile(instruction.title)} + {title} + #{id} +
+ ), + 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) { const compile = useCompile(); - const { workflow } = useFlowContext(); + const { workflow } = useFlowContext() ?? {}; + if (!workflow) { + return null; + } + const { data, children } = props; const instruction = instructions.get(data.type); const detailText = workflow.executed ? '{{t("View")}}' : '{{t("Configure")}}'; @@ -182,6 +291,7 @@ export function NodeDefaultView(props) { #{data.id} +
setVisible(false)}>{`#${value}`}; + } } as ISchema, }, { @@ -36,12 +42,7 @@ const collection = { type: 'string', 'x-component': 'Select', 'x-decorator': 'FormItem', - enum: [ - { value: 0, label: '{{t("On going")}}' }, - { value: 1, label: '{{t("Succeeded")}}' }, - { value: -1, label: '{{t("Failed")}}' }, - { value: -2, label: '{{t("Canceled")}}' }, - ], + enum: ExecutionStatusOptions, } as ISchema, } ], @@ -58,6 +59,7 @@ export const executionSchema = { resource: 'executions', action: 'list', params: { + appends: ['workflow.id', 'workflow.title'], pageSize: 50, sort: ['-createdAt'], }, @@ -130,27 +132,27 @@ export const executionSchema = { }, } }, - // actions: { - // type: 'void', - // title: '{{ t("Actions") }}', - // 'x-component': 'Table.Column', - // properties: { - // actions: { - // type: 'void', - // 'x-component': 'Space', - // 'x-component-props': { - // split: '|', - // }, - // properties: { - // config: { - // type: 'void', - // title: '查看', - // 'x-component': 'ExecutionLink' - // }, - // } - // } - // } - // } + actions: { + type: 'void', + title: '{{ t("Actions") }}', + 'x-component': 'Table.Column', + properties: { + actions: { + type: 'void', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + properties: { + config: { + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'ExecutionLink' + }, + } + } + } + } } } } diff --git a/packages/plugins/workflow/src/client/schemas/workflows.ts b/packages/plugins/workflow/src/client/schemas/workflows.ts index 1bb88f40a..413fc08a3 100644 --- a/packages/plugins/workflow/src/client/schemas/workflows.ts +++ b/packages/plugins/workflow/src/client/schemas/workflows.ts @@ -30,6 +30,7 @@ const collection = { enum: Array.from(triggers.getEntities()).map(([value, { title }]) => ({ value, label: title, + color: 'gold' })), required: true, } as ISchema, @@ -52,7 +53,7 @@ const collection = { title: '{{t("Status")}}', type: 'string', enum: [ - { label: '{{t("On")}}', value: true }, + { label: '{{t("On")}}', value: true, color: '#52c41a' }, { label: '{{t("Off")}}', value: false }, ], 'x-component': 'Radio.Group', @@ -64,7 +65,7 @@ const collection = { }; export const workflowSchema: ISchema = { - type: 'object', + type: 'void', properties: { provider: { type: 'void', @@ -235,7 +236,7 @@ export const workflowSchema: ISchema = { }, executions: { type: 'void', - title: '{{t("Execution History")}}', + title: '{{t("Execution history")}}', 'x-component': 'Action.Link', 'x-component-props': { type: 'primary', @@ -243,7 +244,7 @@ export const workflowSchema: ISchema = { properties: { drawer: { type: 'void', - title: '{{t("Execution History")}}', + title: '{{t("Execution history")}}', 'x-component': 'Action.Drawer', properties: executionSchema } diff --git a/packages/plugins/workflow/src/client/style.tsx b/packages/plugins/workflow/src/client/style.tsx index 9b95a3a36..17ce8209c 100644 --- a/packages/plugins/workflow/src/client/style.tsx +++ b/packages/plugins/workflow/src/client/style.tsx @@ -15,7 +15,13 @@ export const workflowPageClass = css` header{ display: flex; align-items: center; - gap: .5em; + + > *:not(:last-child) { + &:after{ + content: ">"; + margin: 0 .5em; + } + } } aside{ @@ -38,8 +44,20 @@ export const workflowPageClass = css` export const workflowVersionDropdownClass = css` .ant-dropdown-menu-item{ + strong{ + font-weight: normal; + } + + &.enabled{ + strong{ + font-weight: bold; + } + } + &.unexecuted{ - font-style: italic; + strong{ + font-style: italic; + } } .ant-dropdown-menu-title-content{ @@ -148,10 +166,14 @@ export const nodeCardClass = css` padding: 1em; box-shadow: 0 .25em .5em rgba(0, 0, 0, .1); - .workflow-node-remove-button{ + .workflow-node-remove-button, + .workflow-node-job-button{ position: absolute; right: -.5em; top: -.5em; + } + + .workflow-node-remove-button{ color: #999; opacity: 0; 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{ .workflow-node-remove-button{ opacity: 1; @@ -181,6 +220,8 @@ export const nodeMetaClass = css` `; export const nodeTitleClass = css` + display: flex; + align-items: center; font-weight: normal; .workflow-node-id{ diff --git a/packages/plugins/workflow/src/client/triggers/collection.tsx b/packages/plugins/workflow/src/client/triggers/collection.tsx index a5a084ab0..742720226 100644 --- a/packages/plugins/workflow/src/client/triggers/collection.tsx +++ b/packages/plugins/workflow/src/client/triggers/collection.tsx @@ -4,7 +4,7 @@ import { observer, useForm, useFormEffects } from '@formily/react'; import { useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client'; -import { useFlowContext } from '../WorkflowCanvas'; +import { useFlowContext } from '../FlowContext'; import { collection, filter } from '../schemas/collection'; import { css } from '@emotion/css'; import { onFieldValueChange } from '@formily/core'; diff --git a/packages/plugins/workflow/src/client/triggers/index.tsx b/packages/plugins/workflow/src/client/triggers/index.tsx index cd3818674..28985c4e9 100644 --- a/packages/plugins/workflow/src/client/triggers/index.tsx +++ b/packages/plugins/workflow/src/client/triggers/index.tsx @@ -4,11 +4,12 @@ import { Registry } from "@nocobase/utils/client"; import { message, Tag } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; +import { InfoOutlined } from '@ant-design/icons'; import { SchemaComponent, useActionContext, useAPIClient, useCompile, useResourceActionContext } from '@nocobase/client'; -import { nodeCardClass, nodeMetaClass } from "../style"; -import { useFlowContext } from "../WorkflowCanvas"; +import { nodeCardClass, nodeHeaderClass, nodeMetaClass, nodeTitleClass } from "../style"; +import { useFlowContext } from "../FlowContext"; import collection from './collection'; import schedule from "./schedule/"; @@ -17,7 +18,7 @@ function useUpdateConfigAction() { const { t } = useTranslation(); const form = useForm(); const api = useAPIClient(); - const { workflow } = useFlowContext(); + const { workflow } = useFlowContext() ?? {}; const ctx = useActionContext(); const { refresh } = useResourceActionContext(); return { @@ -55,24 +56,98 @@ export const triggers = new Registry(); triggers.register(collection.type, collection); triggers.register(schedule.type, schedule); -export const TriggerConfig = () => { - const { t } = useTranslation(); +function TriggerExecution() { const compile = useCompile(); - const { data } = useResourceActionContext(); - if (!data) { + const { workflow, execution } = useFlowContext(); + if (!execution) { return null; } - const { type, config, executed } = data.data; + + const trigger = triggers.get(workflow.type); + + return ( + , + 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: ( +
+ {compile(trigger.title)} + {workflow.title} + #{execution.id} +
+ ), + 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 detailText = executed ? '{{t("View")}}' : '{{t("Configure")}}'; const titleText = `${t('Trigger')}: ${compile(title)}`; return (
-
- {t('Trigger')} +
+
+ {t('Trigger')} +
+

{compile(title)}

+
-

{compile(title)}