Junyi 86e672e9bb
feat(plugin-workflow): add form trigger type (#2347)
* feat(plugin-workflow): add form trigger

* test(plugin-workflow): add test cases

* fix(client): fix component warning

* fix(plugin-workflow): fix context data for create and update

* fix(plugin-workflow): allow to select any form type workflow in configuration

* fix(client): fix tree component value

* fix(client): fix value render in component

* fix(plugin-workflow): fix context load

* fix(client): fix type

* fix(client): fix type

* fix(plugin-workflow): fix params

* fix(plugin-workflow): fix required

* fix(plugin): fix context not matching collection error

* fix(plugin-workflow): fix test cases

* refactor(plugin-workflow): change trigger workflow action config to cascaded

* fix(plugin-workflow): remove useless locale

* fix(client): adjust locale

* fix(client): remove useless locale
2023-08-09 12:12:57 +08:00

328 lines
10 KiB
TypeScript

import { InfoOutlined } from '@ant-design/icons';
import { createForm } from '@formily/core';
import { ISchema, useForm } from '@formily/react';
import {
ActionContextProvider,
SchemaComponent,
SchemaInitializerItemOptions,
css,
cx,
useAPIClient,
useActionContext,
useCompile,
useResourceActionContext,
} from '@nocobase/client';
import { Registry } from '@nocobase/utils/client';
import { Alert, Button, Input, Tag, message } from 'antd';
import React, { useEffect, useMemo, useState } from 'react';
import { useFlowContext } from '../FlowContext';
import { DrawerDescription } from '../components/DrawerDescription';
import { NAMESPACE, lang } from '../locale';
import useStyles from '../style';
import { VariableOptions } from '../variable';
import collection from './collection';
import formTrigger from './form';
import schedule from './schedule/';
function useUpdateConfigAction() {
const form = useForm();
const api = useAPIClient();
const { workflow } = useFlowContext() ?? {};
const ctx = useActionContext();
const { refresh } = useResourceActionContext();
return {
async run() {
if (workflow.executed) {
message.error(lang('Trigger in executed workflow cannot be modified'));
return;
}
await form.submit();
await api.resource('workflows').update?.({
filterByTk: workflow.id,
values: {
config: form.values,
},
});
ctx.setVisible(false);
refresh();
},
};
}
export interface Trigger {
title: string;
type: string;
description?: string;
// group: string;
useVariables?(config: any, options?): VariableOptions;
fieldset: { [key: string]: ISchema };
view?: ISchema;
scope?: { [key: string]: any };
components?: { [key: string]: any };
useInitializers?(config): SchemaInitializerItemOptions | null;
initializers?: any;
}
export const triggers = new Registry<Trigger>();
triggers.register(formTrigger.type, formTrigger);
triggers.register(collection.type, collection);
triggers.register(schedule.type, schedule);
function TriggerExecution() {
const compile = useCompile();
const { workflow, execution } = useFlowContext();
const { styles } = useStyles();
if (!execution) {
return null;
}
const trigger = triggers.get(workflow.type);
return (
<SchemaComponent
schema={{
type: 'void',
name: 'execution',
'x-component': 'Action',
'x-component-props': {
title: <InfoOutlined />,
shape: 'circle',
size: 'small',
className: styles.nodeJobButtonClass,
type: 'primary',
},
properties: {
[execution.id]: {
type: 'void',
'x-decorator': 'Form',
'x-decorator-props': {
initialValue: execution,
},
'x-component': 'Action.Modal',
title: (
<div className={cx(styles.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", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'DatePicker',
'x-component-props': {
showTime: true,
},
'x-read-pretty': true,
},
context: {
type: 'object',
title: `{{t("Trigger variables", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'Input.JSON',
'x-component-props': {
className: css`
padding: 1em;
background-color: #eee;
`,
},
'x-read-pretty': true,
},
},
},
},
}}
/>
);
}
export const TriggerConfig = () => {
const api = useAPIClient();
const compile = useCompile();
const { workflow, refresh } = useFlowContext();
const [editingTitle, setEditingTitle] = useState<string>('');
const [editingConfig, setEditingConfig] = useState(false);
const { styles } = useStyles();
let typeTitle = '';
useEffect(() => {
if (workflow) {
setEditingTitle(workflow.title ?? typeTitle);
}
}, [workflow]);
const form = useMemo(
() =>
createForm({
initialValues: workflow?.config,
values: workflow?.config,
disabled: workflow?.executed,
}),
[workflow],
);
if (!workflow || !workflow.type) {
return null;
}
const { title, type, executed } = workflow;
const trigger = triggers.get(type);
const { fieldset, scope, components } = trigger;
typeTitle = trigger.title;
const detailText = executed ? '{{t("View")}}' : '{{t("Configure")}}';
const titleText = lang('Trigger');
async function onChangeTitle(next) {
const t = next || typeTitle;
setEditingTitle(t);
if (t === title) {
return;
}
await api.resource('workflows').update?.({
filterByTk: workflow.id,
values: {
title: t,
},
});
refresh();
}
function onOpenDrawer(ev) {
if (ev.target === ev.currentTarget) {
setEditingConfig(true);
return;
}
const whiteSet = new Set(['workflow-node-meta', 'workflow-node-config-button', 'ant-input-disabled']);
for (let el = ev.target; el && el !== ev.currentTarget; el = el.parentNode) {
if ((Array.from(el.classList ?? []) as string[]).some((name: string) => whiteSet.has(name))) {
setEditingConfig(true);
ev.stopPropagation();
return;
}
}
}
return (
<div className={cx(styles.nodeCardClass)} onClick={onOpenDrawer}>
<div className={cx(styles.nodeMetaClass, 'workflow-node-meta')}>
<Tag color="gold">{titleText}</Tag>
</div>
<div>
<Input.TextArea
value={editingTitle}
onChange={(ev) => setEditingTitle(ev.target.value)}
onBlur={(ev) => onChangeTitle(ev.target.value)}
autoSize
/>
</div>
<TriggerExecution />
<ActionContextProvider value={{ visible: editingConfig, setVisible: setEditingConfig }}>
<SchemaComponent
schema={{
name: `workflow-trigger-${workflow.id}`,
type: 'void',
properties: {
config: {
type: 'void',
'x-content': detailText,
'x-component': Button,
'x-component-props': {
type: 'link',
className: 'workflow-node-config-button',
},
},
drawer: {
type: 'void',
title: titleText,
'x-component': 'Action.Drawer',
'x-decorator': 'FormV2',
'x-decorator-props': {
form,
},
properties: {
...(executed
? {
alert: {
'x-component': Alert,
'x-component-props': {
type: 'warning',
showIcon: true,
message: `{{t("Trigger in executed workflow cannot be modified", { ns: "${NAMESPACE}" })}}`,
className: css`
width: 100%;
font-size: 85%;
margin-bottom: 2em;
`,
},
},
}
: trigger.description
? {
description: {
type: 'void',
'x-component': DrawerDescription,
'x-component-props': {
label: lang('Trigger type'),
title: trigger.title,
description: trigger.description,
},
},
}
: {}),
fieldset: {
type: 'void',
'x-component': 'fieldset',
'x-component-props': {
className: css`
.ant-select.auto-width {
width: auto;
min-width: 6em;
}
`,
},
properties: fieldset,
},
actions: {
...(executed
? {}
: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
cancel: {
title: '{{t("Cancel")}}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ cm.useCancelAction }}',
},
},
submit: {
title: '{{t("Submit")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: useUpdateConfigAction,
},
},
},
}),
},
},
},
},
}}
scope={scope}
components={components}
/>
</ActionContextProvider>
</div>
);
};
export function useTrigger() {
const { workflow } = useFlowContext();
return triggers.get(workflow.type);
}