mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-08 23:19:26 +08:00
413 lines
12 KiB
TypeScript
413 lines
12 KiB
TypeScript
/**
|
|
* This file is part of the NocoBase (R) project.
|
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
* Authors: NocoBase Team.
|
|
*
|
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
*/
|
|
|
|
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
|
import { createForm } from '@formily/core';
|
|
import { observer, useForm } from '@formily/react';
|
|
|
|
import {
|
|
ActionContextProvider,
|
|
css,
|
|
SchemaComponent,
|
|
useActionContext,
|
|
useAPIClient,
|
|
useCancelAction,
|
|
useCompile,
|
|
usePlugin,
|
|
} from '@nocobase/client';
|
|
|
|
import WorkflowPlugin, { Instruction, useStyles } from '.';
|
|
import { useFlowContext } from './FlowContext';
|
|
import { lang, NAMESPACE } from './locale';
|
|
import { RadioWithTooltip } from './components';
|
|
import { uid } from '@nocobase/utils/client';
|
|
import { Button, Dropdown } from 'antd';
|
|
import { PlusOutlined } from '@ant-design/icons';
|
|
|
|
interface AddButtonProps {
|
|
upstream;
|
|
branchIndex?: number | null;
|
|
[key: string]: any;
|
|
}
|
|
|
|
export function AddButton(props: AddButtonProps) {
|
|
const { upstream, branchIndex = null } = props;
|
|
const engine = usePlugin(WorkflowPlugin);
|
|
const compile = useCompile();
|
|
const { workflow } = useFlowContext() ?? {};
|
|
const instructionList = Array.from(engine.instructions.getValues()) as Instruction[];
|
|
const { styles } = useStyles();
|
|
const { onCreate, creating } = useAddNodeContext();
|
|
|
|
const groups = useMemo(() => {
|
|
const result = [
|
|
{ key: 'control', label: `{{t("Control", { ns: "${NAMESPACE}" })}}` },
|
|
{ key: 'calculation', label: `{{t("Calculation", { ns: "${NAMESPACE}" })}}` },
|
|
{ key: 'collection', label: `{{t("Collection operations", { ns: "${NAMESPACE}" })}}` },
|
|
{ key: 'manual', label: `{{t("Manual", { ns: "${NAMESPACE}" })}}` },
|
|
{ key: 'extended', label: `{{t("Extended types", { ns: "${NAMESPACE}" })}}` },
|
|
]
|
|
.map((group) => {
|
|
const groupInstructions = instructionList.filter(
|
|
(item) =>
|
|
item.group === group.key &&
|
|
(item.isAvailable ? item.isAvailable({ engine, workflow, upstream, branchIndex }) : true),
|
|
);
|
|
|
|
return {
|
|
...group,
|
|
type: 'group',
|
|
children: groupInstructions.map((item) => ({
|
|
role: 'button',
|
|
'aria-label': item.type,
|
|
key: item.type,
|
|
label: item.title,
|
|
})),
|
|
};
|
|
})
|
|
.filter((group) => group.children.length);
|
|
|
|
return compile(result);
|
|
}, [branchIndex, compile, engine, instructionList, upstream, workflow]);
|
|
|
|
const onClick = useCallback(
|
|
async ({ keyPath }) => {
|
|
const [type] = keyPath;
|
|
onCreate({ type, upstream, branchIndex });
|
|
},
|
|
[branchIndex, onCreate, upstream],
|
|
);
|
|
|
|
if (!workflow) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className={styles.addButtonClass}>
|
|
<Dropdown
|
|
menu={{
|
|
items: groups,
|
|
onClick,
|
|
}}
|
|
disabled={workflow.executed}
|
|
overlayClassName={css`
|
|
.ant-dropdown-menu-root {
|
|
max-height: 30em;
|
|
overflow-y: auto;
|
|
}
|
|
`}
|
|
>
|
|
<Button
|
|
aria-label={props['aria-label'] || 'add-button'}
|
|
shape="circle"
|
|
icon={<PlusOutlined />}
|
|
loading={creating?.upstreamId == upstream?.id && creating?.branchIndex === branchIndex}
|
|
size="small"
|
|
/>
|
|
</Dropdown>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function useAddNodeSubmitAction() {
|
|
const form = useForm();
|
|
const api = useAPIClient();
|
|
const { workflow, refresh } = useFlowContext();
|
|
const { presetting, setPresetting, setCreating } = useAddNodeContext();
|
|
const ctx = useActionContext();
|
|
return {
|
|
async run() {
|
|
if (!presetting) {
|
|
return;
|
|
}
|
|
await form.submit();
|
|
setCreating(presetting.data);
|
|
try {
|
|
const {
|
|
data: { data: newNode },
|
|
} = await api.resource('workflows.nodes', workflow.id).create({
|
|
values: {
|
|
...presetting.data,
|
|
config: {
|
|
...presetting.data.config,
|
|
...form.values.config,
|
|
},
|
|
},
|
|
});
|
|
if (typeof form.values.downstreamBranchIndex === 'number' && newNode.downstreamId) {
|
|
await api.resource('flow_nodes').update({
|
|
filterByTk: newNode.downstreamId,
|
|
values: {
|
|
branchIndex: form.values.downstreamBranchIndex,
|
|
upstream: {
|
|
id: newNode.id,
|
|
downstreamId: null,
|
|
},
|
|
},
|
|
updateAssociationValues: ['upstream'],
|
|
});
|
|
}
|
|
ctx.setVisible(false);
|
|
setPresetting(null);
|
|
refresh();
|
|
} catch (err) {
|
|
console.error(err);
|
|
} finally {
|
|
setCreating(null);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
const AddNodeContext = createContext(null);
|
|
|
|
export function useAddNodeContext() {
|
|
return useContext(AddNodeContext);
|
|
}
|
|
|
|
const defaultBranchingOptions = [
|
|
{
|
|
label: `{{t('After end of branches', { ns: "${NAMESPACE}" })}}`,
|
|
value: false,
|
|
},
|
|
{
|
|
label: `{{t('Inside of branch', { ns: "${NAMESPACE}" })}}`,
|
|
value: 0,
|
|
},
|
|
];
|
|
|
|
const DownstreamBranchIndex = observer((props) => {
|
|
const { presetting } = useAddNodeContext();
|
|
const { nodes } = useFlowContext();
|
|
const { values } = useForm();
|
|
const options = useMemo(() => {
|
|
if (!presetting?.instruction) {
|
|
return [];
|
|
}
|
|
const { instruction, data } = presetting;
|
|
const downstream = data.upstreamId
|
|
? nodes.find((item) => item.upstreamId === data.upstreamId && item.branchIndex === data.branchIndex)
|
|
: nodes.find((item) => item.upstreamId === null);
|
|
if (!downstream) {
|
|
return [];
|
|
}
|
|
const branching =
|
|
typeof instruction.branching === 'function' ? instruction.branching(values.config ?? {}) : instruction.branching;
|
|
if (!branching) {
|
|
return [];
|
|
}
|
|
return branching === true ? defaultBranchingOptions : branching;
|
|
}, [presetting, nodes, values.config]);
|
|
|
|
if (!options.length) {
|
|
return null;
|
|
}
|
|
|
|
const { data } = presetting;
|
|
|
|
return (
|
|
<SchemaComponent
|
|
components={{
|
|
RadioWithTooltip,
|
|
}}
|
|
schema={{
|
|
name: `${data.type ?? 'unknown'}-${data.upstreamId ?? 'root'}-${data.branchIndex}`,
|
|
type: 'void',
|
|
properties: {
|
|
downstreamBranchIndex: {
|
|
type: 'number',
|
|
title: lang('Move all downstream nodes to', { ns: NAMESPACE }),
|
|
'x-decorator': 'FormItem',
|
|
'x-component': 'RadioWithTooltip',
|
|
'x-component-props': {
|
|
options,
|
|
direction: 'vertical',
|
|
},
|
|
default: false,
|
|
required: true,
|
|
},
|
|
},
|
|
}}
|
|
/>
|
|
);
|
|
|
|
// return (
|
|
// <FormItem label={lang('Move all downstream nodes to', { ns: NAMESPACE })}>
|
|
// <RadioWithTooltip {...props} options={options} defaultValue={-1} direction="vertical" />
|
|
// </FormItem>
|
|
// );
|
|
});
|
|
|
|
function PresetFieldset() {
|
|
const { presetting } = useAddNodeContext();
|
|
if (!presetting?.instruction.presetFieldset) {
|
|
return null;
|
|
}
|
|
return (
|
|
<SchemaComponent
|
|
schema={{
|
|
type: 'void',
|
|
properties: {
|
|
config: {
|
|
type: 'object',
|
|
properties: presetting.instruction.presetFieldset,
|
|
},
|
|
},
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function AddNodeContextProvider(props) {
|
|
const api = useAPIClient();
|
|
const compile = useCompile();
|
|
const engine = usePlugin(WorkflowPlugin);
|
|
const [creating, setCreating] = useState(null);
|
|
const [presetting, setPresetting] = useState(null);
|
|
const [formValueChanged, setFormValueChanged] = useState(false);
|
|
const { workflow, nodes, refresh } = useFlowContext() ?? {};
|
|
|
|
const form = useMemo(() => createForm(), []);
|
|
|
|
const onModalCancel = useCallback(
|
|
(visible) => {
|
|
if (!visible) {
|
|
form.reset();
|
|
form.clearFormGraph('*');
|
|
setPresetting(null);
|
|
}
|
|
},
|
|
[form],
|
|
);
|
|
|
|
const create = useCallback(
|
|
async (data) => {
|
|
setCreating(data);
|
|
try {
|
|
await api.resource('workflows.nodes', workflow.id).create({
|
|
values: data,
|
|
});
|
|
refresh();
|
|
} catch (err) {
|
|
console.error(err);
|
|
} finally {
|
|
setCreating(null);
|
|
}
|
|
},
|
|
[api, refresh, workflow.id],
|
|
);
|
|
|
|
const onCreate = useCallback(
|
|
({ type, upstream, branchIndex }) => {
|
|
const instruction = engine.instructions.get(type);
|
|
if (!instruction) {
|
|
console.error(`Instruction "${type}" not found`);
|
|
return;
|
|
}
|
|
const data = {
|
|
key: uid(),
|
|
type,
|
|
upstreamId: upstream?.id ?? null,
|
|
branchIndex,
|
|
title: compile(instruction.title),
|
|
config: instruction.createDefaultConfig?.() ?? {},
|
|
};
|
|
const downstream = upstream?.id
|
|
? nodes.find((item) => item.upstreamId === data.upstreamId && item.branchIndex === data.branchIndex)
|
|
: nodes.find((item) => item.upstreamId === null);
|
|
if (
|
|
instruction.presetFieldset ||
|
|
((typeof instruction.branching === 'function' ? instruction.branching(data.config) : instruction.branching) &&
|
|
downstream)
|
|
) {
|
|
setPresetting({ data, instruction });
|
|
return;
|
|
}
|
|
|
|
create(data);
|
|
},
|
|
[compile, create, engine.instructions],
|
|
);
|
|
|
|
return (
|
|
<AddNodeContext.Provider value={{ presetting, setPresetting, onCreate, creating, setCreating }}>
|
|
{props.children}
|
|
<ActionContextProvider
|
|
value={{
|
|
visible: Boolean(presetting),
|
|
setVisible: onModalCancel,
|
|
formValueChanged,
|
|
setFormValueChanged,
|
|
openSize: 'small',
|
|
}}
|
|
>
|
|
<SchemaComponent
|
|
components={{
|
|
DownstreamBranchIndex,
|
|
PresetFieldset,
|
|
}}
|
|
scope={{
|
|
useCancelAction,
|
|
useAddNodeSubmitAction,
|
|
}}
|
|
schema={{
|
|
name: `modal`,
|
|
type: 'void',
|
|
'x-decorator': 'FormV2',
|
|
'x-decorator-props': {
|
|
form,
|
|
},
|
|
'x-component': 'Action.Modal',
|
|
title: `{{ t("Add node", { ns: "${NAMESPACE}" }) }}`,
|
|
properties: {
|
|
config: {
|
|
type: 'void',
|
|
'x-component': 'PresetFieldset',
|
|
},
|
|
downstreamBranchIndex: {
|
|
type: 'void',
|
|
'x-component': 'DownstreamBranchIndex',
|
|
},
|
|
footer: {
|
|
'x-component': 'Action.Modal.Footer',
|
|
properties: {
|
|
actions: {
|
|
type: 'void',
|
|
'x-component': 'ActionBar',
|
|
properties: {
|
|
cancel: {
|
|
type: 'void',
|
|
title: '{{ t("Cancel") }}',
|
|
'x-component': 'Action',
|
|
'x-component-props': {
|
|
useAction: '{{ useCancelAction }}',
|
|
},
|
|
},
|
|
submit: {
|
|
type: 'void',
|
|
title: `{{ t("Submit") }}`,
|
|
'x-component': 'Action',
|
|
'x-component-props': {
|
|
type: 'primary',
|
|
htmlType: 'submit',
|
|
useAction: '{{ useAddNodeSubmitAction }}',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}}
|
|
/>
|
|
</ActionContextProvider>
|
|
</AddNodeContext.Provider>
|
|
);
|
|
}
|