tachybase_todo/packages/client/src/workflow/WorkflowCanvas.tsx

150 lines
3.7 KiB
TypeScript
Raw Normal View History

import React, { useContext } from 'react';
import { Dropdown, Menu, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { cx } from '@emotion/css';
import { addButtonClass, branchBlockClass, branchClass, nodeBlockClass, nodeCardClass, nodeHeaderClass, nodeTitleClass } from './style';
import {
useCollection,
useResourceActionContext
} from '..';
import { Instruction, instructions, Node } from './nodes';
function makeNodes(nodes): void {
const nodesMap = new Map();
nodes.forEach(item => nodesMap.set(item.id, 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);
}
}
}
const FlowContext = React.createContext(null);
export function useFlowContext() {
return useContext(FlowContext);
}
export function WorkflowCanvas() {
const { data, refresh, loading } = useResourceActionContext();
if (!data?.data && !loading) {
return <div></div>;
}
const { nodes = [] } = data?.data ?? {};
makeNodes(nodes);
const entry = nodes.find(item => !item.upstream);
return (
<FlowContext.Provider value={{
nodes,
onNodeAdded: refresh,
onNodeRemoved: refresh
}}>
<div className={branchBlockClass}>
<Branch entry={entry} />
</div>
<div className={cx(nodeCardClass)}></div>
</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 => {
return (
<div key={item.id} className={cx(nodeBlockClass)}>
<Node data={item} />
<AddButton upstream={item} />
</div>
);
})}
</div>
</div>
);
}
// TODO(bug): useless observable
// const instructionsList = observable(Array.from(instructions.getValues()));
interface AddButtonProps {
upstream;
branchIndex?: number;
};
function AddButton({ upstream, branchIndex = null }: AddButtonProps) {
const { resource } = useCollection();
const { data } = useResourceActionContext();
const { onNodeAdded } = useFlowContext();
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,
workflowId: data.data.id,
upstreamId: upstream?.id ?? null,
branchIndex,
config
}
});
onNodeAdded(node);
}
return (
<div className={cx(addButtonClass)}>
<Dropdown trigger={['click']} overlay={
<Menu onClick={ev => onCreate(ev)}>
{(Array.from(instructions.getValues()) as Instruction[]).map(item => item.options
? (
<Menu.SubMenu key={item.type} title={item.title}>
{item.options.map(option => (
<Menu.Item key={option.key}>{option.label}</Menu.Item>
))}
</Menu.SubMenu>
)
: (
<Menu.Item key={item.type}>{item.title}</Menu.Item>
))}
</Menu>
}>
<Button shape="circle" icon={<PlusOutlined />} />
</Dropdown>
</div>
);
};