refactor(plugin-workflow): change canvas card and adjust styles (#1529)

* refactor(plugin-workflow): optimize workflow canvas

* feat(plugin-workflow): allow click on node card to open config and adjust styles

* fix(plugin-workflow): fix collection trigger linkages
This commit is contained in:
Junyi 2023-03-04 00:19:11 +08:00 committed by GitHub
parent 4d44e31b31
commit 37998d03ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 516 additions and 379 deletions

View File

@ -123,6 +123,8 @@ ReadPretty.JSON = (props) => {
<pre <pre
className={cx(prefixCls, props.className, css` className={cx(prefixCls, props.className, css`
margin-bottom: 0; margin-bottom: 0;
line-height: 1.5;
font-size: 90%;
`)} `)}
style={props.style} style={props.style}
> >

View File

@ -20,7 +20,7 @@ interface AddButtonProps {
export function AddButton({ upstream, branchIndex = null }: AddButtonProps) { export function AddButton({ upstream, branchIndex = null }: AddButtonProps) {
const compile = useCompile(); const compile = useCompile();
const api = useAPIClient(); const api = useAPIClient();
const { workflow, onNodeAdded } = useFlowContext() ?? {}; const { workflow, refresh } = useFlowContext() ?? {};
if (!workflow) { if (!workflow) {
return null; return null;
} }
@ -30,8 +30,9 @@ export function AddButton({ upstream, branchIndex = null }: AddButtonProps) {
const type = keyPath.pop(); const type = keyPath.pop();
const config = {}; const config = {};
const [optionKey] = keyPath; const [optionKey] = keyPath;
const instruction = instructions.get(type);
if (optionKey) { if (optionKey) {
const { value } = instructions.get(type)?.options?.find(item => item.key === optionKey) ?? {}; const { value } = instruction.options?.find(item => item.key === optionKey) ?? {};
Object.assign(config, value); Object.assign(config, value);
} }
@ -40,47 +41,46 @@ export function AddButton({ upstream, branchIndex = null }: AddButtonProps) {
type, type,
upstreamId: upstream?.id ?? null, upstreamId: upstream?.id ?? null,
branchIndex, branchIndex,
title: compile(instruction.title),
config config
} }
}); });
onNodeAdded(node); refresh();
} }
const groups = [
{ value: 'control', name: `{{t("Control", { ns: "${NAMESPACE}" })}}` },
{ value: 'collection', name: `{{t("Collection operations", { ns: "${NAMESPACE}" })}}` },
{ value: 'manual', name: `{{t("Manual", { ns: "${NAMESPACE}" })}}` },
{ value: 'extended', name: `{{t("Extended types", { ns: "${NAMESPACE}" })}}` },
];
const instructionList = (Array.from(instructions.getValues()) as Instruction[]); const instructionList = (Array.from(instructions.getValues()) as Instruction[]);
const groups = [
{ key: 'control', label: `{{t("Control", { 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}" })}}` },
]
.filter(group => instructionList.filter(item => item.group === group.key).length)
.map(group => {
const groupInstructions = instructionList.filter(item => item.group === group.key);
return {
...group,
type: 'group',
children: groupInstructions.map(item => ({
key: item.type,
label: item.title,
type: item.options ? 'subMenu' : null,
children: item.options ? item.options.map(option => ({
key: option.key,
label: option.label,
})) : null
}))
};
});
return ( return (
<div className={cx(addButtonClass)}> <div className={cx(addButtonClass)}>
<Dropdown <Dropdown
trigger={['click']} trigger={['click']}
overlay={ overlay={<Menu onClick={ev => onCreate(ev)} items={compile(groups)} />}
<Menu onClick={ev => onCreate(ev)}>
{groups.map(group => {
const groupInstructions = instructionList.filter(item => item.group === group.value);
return groupInstructions.length ? (
<Menu.ItemGroup key={group.value} title={compile(group.name)}>
{groupInstructions.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>
) : null;
})}
</Menu>
}
disabled={workflow.executed} disabled={workflow.executed}
> >
<Button shape="circle" icon={<PlusOutlined />} /> <Button shape="circle" icon={<PlusOutlined />} />

View File

@ -117,8 +117,7 @@ export function WorkflowCanvas() {
<FlowContext.Provider value={{ <FlowContext.Provider value={{
workflow, workflow,
nodes, nodes,
onNodeAdded: refresh, refresh,
onNodeRemoved: refresh
}}> }}>
<div className="workflow-toolbar"> <div className="workflow-toolbar">
<header> <header>

View File

@ -33,8 +33,8 @@ export default observer(({ value, disabled, onChange }: any) => {
const compile = useCompile(); const compile = useCompile();
const form = useForm(); const form = useForm();
const { getCollection, getCollectionFields } = useCollectionManager(); const { getCollection, getCollectionFields } = useCollectionManager();
const { values: data } = useForm(); const { values: config } = useForm();
const collectionName = data?.config?.collection; const collectionName = config?.collection;
const fields = getCollectionFields(collectionName) const fields = getCollectionFields(collectionName)
.filter(field => ( .filter(field => (
!field.hidden !field.hidden

View File

@ -17,10 +17,9 @@ export default {
type: 'calculation', type: 'calculation',
group: 'control', group: 'control',
fieldset: { fieldset: {
'config.engine': { engine: {
type: 'string', type: 'string',
title: `{{t("Calculation engine", { ns: "${NAMESPACE}" })}}`, title: `{{t("Calculation engine", { ns: "${NAMESPACE}" })}}`,
name: 'config.engine',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'RadioWithTooltip', 'x-component': 'RadioWithTooltip',
'x-component-props': { 'x-component-props': {
@ -29,10 +28,9 @@ export default {
required: true, required: true,
default: 'math.js' default: 'math.js'
}, },
'config.expression': { expression: {
type: 'string', type: 'string',
title: `{{t("Calculation expression", { ns: "${NAMESPACE}" })}}`, title: `{{t("Calculation expression", { ns: "${NAMESPACE}" })}}`,
name: 'config.expression',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Variable.TextArea', 'x-component': 'Variable.TextArea',
'x-component-props': { 'x-component-props': {
@ -40,7 +38,7 @@ export default {
}, },
['x-validator'](value, rules, { form }) { ['x-validator'](value, rules, { form }) {
const { values } = form; const { values } = form;
const { evaluate } = evaluators.get(values.config.engine) as Evaluator; const { evaluate } = evaluators.get(values.engine) as Evaluator;
const exp = value.trim().replace(/{{([^{}]+)}}/g, '1'); const exp = value.trim().replace(/{{([^{}]+)}}/g, '1');
try { try {
evaluate(exp); evaluate(exp);
@ -50,7 +48,7 @@ export default {
} }
}, },
'x-reactions': { 'x-reactions': {
dependencies: ['config.engine'], dependencies: ['engine'],
fulfill: { fulfill: {
schema: { schema: {
description: '{{renderReference($deps[0])}}', description: '{{renderReference($deps[0])}}',

View File

@ -304,9 +304,8 @@ export default {
type: 'condition', type: 'condition',
group: 'control', group: 'control',
fieldset: { fieldset: {
'config.rejectOnFalse': { rejectOnFalse: {
type: 'boolean', type: 'boolean',
name: 'config.rejectOnFalse',
title: `{{t("Mode", { ns: "${NAMESPACE}" })}}`, title: `{{t("Mode", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Radio.Group', 'x-component': 'Radio.Group',
@ -324,10 +323,9 @@ export default {
} }
], ],
}, },
'config.engine': { engine: {
type: 'string', type: 'string',
title: `{{t("Calculation engine", { ns: "${NAMESPACE}" })}}`, title: `{{t("Calculation engine", { ns: "${NAMESPACE}" })}}`,
name: 'config.engine',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'RadioWithTooltip', 'x-component': 'RadioWithTooltip',
'x-component-props': { 'x-component-props': {
@ -339,14 +337,13 @@ export default {
required: true, required: true,
default: 'basic', default: 'basic',
}, },
'config.calculation': { calculation: {
type: 'string', type: 'string',
name: 'config.calculation',
title: `{{t("Condition", { ns: "${NAMESPACE}" })}}`, title: `{{t("Condition", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'CalculationConfig', 'x-component': 'CalculationConfig',
'x-reactions': { 'x-reactions': {
dependencies: ['config.engine'], dependencies: ['engine'],
fulfill: { fulfill: {
state: { state: {
visible: '{{$deps[0] === "basic"}}' visible: '{{$deps[0] === "basic"}}'
@ -355,10 +352,9 @@ export default {
}, },
required: true required: true
}, },
'config.expression': { expression: {
type: 'string', type: 'string',
title: `{{t("Condition expression", { ns: "${NAMESPACE}" })}}`, title: `{{t("Condition expression", { ns: "${NAMESPACE}" })}}`,
name: 'config.expression',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Variable.TextArea', 'x-component': 'Variable.TextArea',
'x-component-props': { 'x-component-props': {
@ -366,7 +362,7 @@ export default {
}, },
['x-validator'](value, rules, { form }) { ['x-validator'](value, rules, { form }) {
const { values } = form; const { values } = form;
const { evaluate } = evaluators.get(values.config.engine); const { evaluate } = evaluators.get(values.engine);
const exp = value.trim().replace(/{{([^{}]+)}}/g, '1'); const exp = value.trim().replace(/{{([^{}]+)}}/g, '1');
try { try {
evaluate(exp); evaluate(exp);
@ -376,7 +372,7 @@ export default {
} }
}, },
'x-reactions': { 'x-reactions': {
dependencies: ['config.engine'], dependencies: ['engine'],
fulfill: { fulfill: {
state: { state: {
visible: '{{$deps[0] !== "basic"}}' visible: '{{$deps[0] !== "basic"}}'

View File

@ -14,10 +14,7 @@ export default {
type: 'create', type: 'create',
group: 'collection', group: 'collection',
fieldset: { fieldset: {
'config.collection': { collection,
...collection,
name: 'config.collection'
},
// multiple: { // multiple: {
// type: 'boolean', // type: 'boolean',
// title: '多条数据', // title: '多条数据',
@ -28,7 +25,7 @@ export default {
// disabled: true // disabled: true
// } // }
// }, // },
'config.params.values': values 'params.values': values
}, },
view: { view: {

View File

@ -7,18 +7,16 @@ export default {
type: 'delay', type: 'delay',
group: 'control', group: 'control',
fieldset: { fieldset: {
'config.duration': { duration: {
type: 'number', type: 'number',
name: 'config.duration',
title: `{{t("Duration", { ns: "${NAMESPACE}" })}}`, title: `{{t("Duration", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Duration', 'x-component': 'Duration',
default: 60000, default: 60000,
required: true required: true
}, },
'config.endStatus': { endStatus: {
type: 'number', type: 'number',
name: 'config.endStatus',
title: `{{t("End Status", { ns: "${NAMESPACE}" })}}`, title: `{{t("End Status", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Select', 'x-component': 'Select',

View File

@ -8,10 +8,9 @@ export default {
type: 'destroy', type: 'destroy',
group: 'collection', group: 'collection',
fieldset: { fieldset: {
'config.collection': collection, collection,
'config.params': { params: {
type: 'object', type: 'object',
name: 'config.params',
title: '', title: '',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
properties: { properties: {

View File

@ -1,18 +1,18 @@
import React, { useContext } from 'react'; import React, { useState, useContext } from 'react';
import { import {
CloseOutlined, CloseOutlined,
DeleteOutlined, DeleteOutlined,
} from '@ant-design/icons'; } 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 { Button, message, Modal, Tag, Alert } from 'antd'; import { Button, message, Modal, Tag, Alert, Input } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import parse from 'json-templates'; import parse from 'json-templates';
import { Registry } from '@nocobase/utils/client'; import { Registry } from '@nocobase/utils/client';
import { SchemaComponent, SchemaInitializerItemOptions, useActionContext, useAPIClient, useCompile, useRequest, useResourceActionContext } from '@nocobase/client'; import { ActionContext, SchemaComponent, SchemaInitializerItemOptions, useActionContext, useAPIClient, useCompile, useRequest, useResourceActionContext } from '@nocobase/client';
import { nodeBlockClass, nodeCardClass, nodeClass, nodeHeaderClass, nodeMetaClass, nodeTitleClass } from '../style'; import { nodeBlockClass, nodeCardClass, nodeClass, nodeMetaClass, nodeTitleClass } from '../style';
import { AddButton } from '../AddButton'; import { AddButton } from '../AddButton';
import { useFlowContext } from '../FlowContext'; import { useFlowContext } from '../FlowContext';
@ -76,17 +76,11 @@ function useUpdateAction() {
message.error(lang('Node in executed workflow cannot be modified')); message.error(lang('Node in executed workflow cannot be modified'));
return; return;
} }
// TODO: how to do validation separately for each field? especially disabled for dynamic fields?
try {
await form.submit(); await form.submit();
} catch (err) {
return;
}
await api.resource('flow_nodes', data.id).update?.({ await api.resource('flow_nodes', data.id).update?.({
filterByTk: data.id, filterByTk: data.id,
values: { values: {
title: form.values.title, config: form.values
config: form.values.config
} }
}); });
ctx.setVisible(false); ctx.setVisible(false);
@ -153,7 +147,7 @@ export function Node({ data }) {
export function RemoveButton() { export function RemoveButton() {
const { t } = useTranslation(); const { t } = useTranslation();
const api = useAPIClient(); const api = useAPIClient();
const { workflow, nodes, onNodeRemoved } = useFlowContext() ?? {}; const { workflow, nodes, refresh } = useFlowContext() ?? {};
const current = useNodeContext(); const current = useNodeContext();
if (!workflow) { if (!workflow) {
return null; return null;
@ -162,10 +156,10 @@ export function RemoveButton() {
async function onRemove() { async function onRemove() {
async function onOk() { async function onOk() {
const { data: { data: node } } = await resource.destroy?.({ await resource.destroy?.({
filterByTk: current.id filterByTk: current.id
}); });
onNodeRemoved(node); refresh();
} }
const usingNodes = nodes.filter(node => { const usingNodes = nodes.filter(node => {
@ -225,6 +219,7 @@ export function JobButton() {
className={cx('workflow-node-job-button', css` className={cx('workflow-node-job-button', css`
border: 2px solid #d9d9d9; border: 2px solid #d9d9d9;
border-radius: 50%; border-radius: 50%;
cursor: not-allowed;
`)} `)}
/> />
); );
@ -312,30 +307,67 @@ export function JobButton() {
} }
export function NodeDefaultView(props) { export function NodeDefaultView(props) {
const compile = useCompile();
const { workflow } = useFlowContext() ?? {};
if (!workflow) {
return null;
}
const { data, children } = props; const { data, children } = props;
const compile = useCompile();
const api = useAPIClient();
const { workflow, refresh } = useFlowContext() ?? {};
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")}}';
const typeTitle = compile(instruction.title);
const [editingTitle, setEditingTitle] = useState<string>(data.title ?? typeTitle);
const [editingConfig, setEditingConfig] = useState(false);
async function onChangeTitle(next) {
const title = next || typeTitle;
setEditingTitle(title);
if (title === data.title) {
return;
}
await api.resource('flow_nodes').update?.({
filterByTk: data.id,
values: {
title
}
});
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 ( return (
<div className={cx(nodeClass, `workflow-node-type-${data.type}`)}> <div className={cx(nodeClass, `workflow-node-type-${data.type}`)}>
<div className={cx(nodeCardClass)}> <div className={cx(nodeCardClass, { configuring: editingConfig })} onClick={onOpenDrawer}>
<div className={cx(nodeHeaderClass)}> <div className={cx(nodeMetaClass, 'workflow-node-meta')}>
<div className={cx(nodeMetaClass)}> <Tag>{typeTitle}</Tag>
<Tag>{compile(instruction.title)}</Tag> <span className="workflow-node-id">{data.id}</span>
</div>
<div>
<Input.TextArea
disabled={workflow.executed}
value={editingTitle}
onChange={(ev) => setEditingTitle(ev.target.value)}
onBlur={(ev) => onChangeTitle(ev.target.value)}
autoSize
/>
</div> </div>
<h4 className={cx(nodeTitleClass)}>
<strong>{data.title}</strong>
<span className="workflow-node-id">#{data.id}</span>
</h4>
<RemoveButton /> <RemoveButton />
<JobButton /> <JobButton />
</div> <ActionContext.Provider value={{ visible: editingConfig, setVisible: setEditingConfig }}>
<SchemaComponent <SchemaComponent
scope={instruction.scope} scope={instruction.scope}
components={instruction.components} components={instruction.components}
@ -345,12 +377,13 @@ export function NodeDefaultView(props) {
...(instruction.view ? { view: instruction.view } : {}), ...(instruction.view ? { view: instruction.view } : {}),
config: { config: {
type: 'void', type: 'void',
title: detailText, 'x-content': detailText,
'x-component': 'Action.Link', 'x-component': Button,
'x-component-props': { 'x-component-props': {
type: 'primary', type: 'link',
className: 'workflow-node-config-button'
},
}, },
properties: {
[`${instruction.type}_${data.id}`]: { [`${instruction.type}_${data.id}`]: {
type: 'void', type: 'void',
title: instruction.title, title: instruction.title,
@ -359,15 +392,16 @@ export function NodeDefaultView(props) {
'x-decorator-props': { 'x-decorator-props': {
disabled: workflow.executed, disabled: workflow.executed,
useValues(options) { useValues(options) {
const d = useNodeContext(); const { config } = useNodeContext();
return useRequest(() => { return useRequest(() => {
return Promise.resolve({ data: d }); return Promise.resolve({ data: config });
}, options); }, options);
} }
}, },
properties: { properties: {
...(workflow.executed ? { ...(workflow.executed ? {
alert: { alert: {
type: 'void',
'x-component': Alert, 'x-component': Alert,
'x-component-props': { 'x-component-props': {
type: 'warning', type: 'warning',
@ -381,16 +415,8 @@ export function NodeDefaultView(props) {
}, },
} }
} : {}), } : {}),
title: { fieldset: {
type: 'string',
name: 'title',
title: '{{t("Name")}}',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
config: {
type: 'void', type: 'void',
name: 'config',
'x-component': 'fieldset', 'x-component': 'fieldset',
'x-component-props': { 'x-component-props': {
className: css` className: css`
@ -433,10 +459,9 @@ export function NodeDefaultView(props) {
} }
} as ISchema } as ISchema
} }
}
}
}} }}
/> />
</ActionContext.Provider>
</div> </div>
{children} {children}
</div> </div>

View File

@ -34,9 +34,8 @@ export default {
type: 'manual', type: 'manual',
group: 'manual', group: 'manual',
fieldset: { fieldset: {
'config.assignees': { assignees: {
type: 'array', type: 'array',
name: 'config.assignees',
title: `{{t("Assignees", { ns: "${NAMESPACE}" })}}`, title: `{{t("Assignees", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'AssigneesSelect', 'x-component': 'AssigneesSelect',
@ -53,15 +52,14 @@ export default {
required: true, required: true,
default: [], default: [],
}, },
'config.mode': { mode: {
type: 'number', type: 'number',
name: 'config.mode',
title: `{{t("Mode", { ns: "${NAMESPACE}" })}}`, title: `{{t("Mode", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'ModeConfig', 'x-component': 'ModeConfig',
default: 1, default: 1,
'x-reactions': { 'x-reactions': {
dependencies: ['config.assignees'], dependencies: ['assignees'],
fulfill: { fulfill: {
state: { state: {
visible: '{{$deps[0].length > 1}}', visible: '{{$deps[0].length > 1}}',
@ -69,7 +67,7 @@ export default {
}, },
} }
}, },
'config.schema': { schema: {
type: 'void', type: 'void',
title: `{{t("User interface", { ns: "${NAMESPACE}" })}}`, title: `{{t("User interface", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',

View File

@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { css, cx } from "@emotion/css"; import { css, cx } from "@emotion/css";
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { Button, Tooltip } from "antd"; import { Button, Tooltip } from "antd";
import { NodeDefaultView } from "."; import { NodeDefaultView } from ".";
@ -17,9 +17,8 @@ export default {
type: 'parallel', type: 'parallel',
group: 'control', group: 'control',
fieldset: { fieldset: {
'config.mode': { mode: {
type: 'string', type: 'string',
name: 'config.mode',
title: `{{t("Mode", { ns: "${NAMESPACE}" })}}`, title: `{{t("Mode", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'RadioWithTooltip', 'x-component': 'RadioWithTooltip',
@ -50,7 +49,7 @@ export default {
}, },
render(data) { render(data) {
const { id, config: { mode } } = data; const { id, config: { mode } } = data;
const { nodes } = useFlowContext(); const { workflow, nodes } = useFlowContext();
const branches = nodes.reduce((result, node) => { const branches = nodes.reduce((result, node) => {
if (node.upstreamId === id && node.branchIndex != null) { if (node.upstreamId === id && node.branchIndex != null) {
return result.concat(node); return result.concat(node);
@ -90,6 +89,7 @@ export default {
shape="circle" shape="circle"
icon={<PlusOutlined />} icon={<PlusOutlined />}
onClick={() => setBranchCount(branchCount - 1)} onClick={() => setBranchCount(branchCount - 1)}
disabled={workflow.executed}
/> />
</div> </div>
) )
@ -104,7 +104,9 @@ export default {
height: 2em; height: 2em;
`} `}
> >
<Tooltip title={lang('Add branch')}> <Tooltip title={lang('Add branch')} className={css`
visibility: ${workflow.executed ? 'hidden' : 'visible'}
`}>
<Button <Button
icon={<PlusOutlined />} icon={<PlusOutlined />}
className={css` className={css`
@ -117,6 +119,7 @@ export default {
} }
`} `}
onClick={() => setBranchCount(branchCount + 1)} onClick={() => setBranchCount(branchCount + 1)}
disabled={workflow.executed}
/> />
</Tooltip> </Tooltip>
</div> </div>

View File

@ -14,7 +14,7 @@ export default {
type: 'query', type: 'query',
group: 'collection', group: 'collection',
fieldset: { fieldset: {
'config.collection': collection, collection,
// 'config.multiple': { // 'config.multiple': {
// type: 'boolean', // type: 'boolean',
// title: `{{t("Multiple records", { ns: "${NAMESPACE}" })}}`, // title: `{{t("Multiple records", { ns: "${NAMESPACE}" })}}`,
@ -25,9 +25,8 @@ export default {
// disabled: true // disabled: true
// } // }
// }, // },
'config.params': { params: {
type: 'object', type: 'object',
name: 'config.params',
title: '', title: '',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
properties: { properties: {

View File

@ -11,9 +11,8 @@ export default {
type: 'request', type: 'request',
group: 'extended', group: 'extended',
fieldset: { fieldset: {
'config.method': { method: {
type: 'string', type: 'string',
name: 'config.method',
required: true, required: true,
title: `{{t("HTTP method", { ns: "${NAMESPACE}" })}}`, title: `{{t("HTTP method", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
@ -31,9 +30,8 @@ export default {
], ],
default: 'POST' default: 'POST'
}, },
'config.url': { url: {
type: 'string', type: 'string',
name: 'config.url',
required: true, required: true,
title: `{{t("URL", { ns: "${NAMESPACE}" })}}`, title: `{{t("URL", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
@ -52,9 +50,8 @@ export default {
placeholder: 'https://www.nocobase.com', placeholder: 'https://www.nocobase.com',
}, },
}, },
'config.headers': { headers: {
type: 'array', type: 'array',
name: 'config.headers',
'x-component': 'ArrayItems', 'x-component': 'ArrayItems',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
title: `{{t("Headers", { ns: "${NAMESPACE}" })}}`, title: `{{t("Headers", { ns: "${NAMESPACE}" })}}`,
@ -99,9 +96,8 @@ export default {
}, },
}, },
}, },
'config.params': { params: {
type: 'array', type: 'array',
name: 'config.params',
'x-component': 'ArrayItems', 'x-component': 'ArrayItems',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
title: `{{t("Parameters", { ns: "${NAMESPACE}" })}}`, title: `{{t("Parameters", { ns: "${NAMESPACE}" })}}`,
@ -145,9 +141,8 @@ export default {
}, },
}, },
}, },
'config.data': { data: {
type: 'string', type: 'string',
name: 'config.data',
title: `{{t("Body", { ns: "${NAMESPACE}" })}}`, title: `{{t("Body", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-decorator-props': {}, 'x-decorator-props': {},
@ -159,15 +154,14 @@ export default {
}, },
placeholder: `{{t("Input request data", { ns: "${NAMESPACE}" })}}`, placeholder: `{{t("Input request data", { ns: "${NAMESPACE}" })}}`,
className: css` className: css`
font-size: 85%; font-size: 90%;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
` `
}, },
description: `{{t("Only support standard JSON data", { ns: "${NAMESPACE}" })}}`, description: `{{t("Only support standard JSON data", { ns: "${NAMESPACE}" })}}`,
}, },
'config.timeout': { timeout: {
type: 'number', type: 'number',
name: 'config.timeout',
title: `{{t("Timeout config", { ns: "${NAMESPACE}" })}}`, title: `{{t("Timeout config", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-decorator-props': {}, 'x-decorator-props': {},
@ -179,9 +173,8 @@ export default {
defaultValue: 5000, defaultValue: 5000,
}, },
}, },
'config.ignoreFail': { ignoreFail: {
type: 'boolean', type: 'boolean',
name: 'config.ignoreFail',
title: `{{t("Ignore fail request and continue workflow", { ns: "${NAMESPACE}" })}}`, title: `{{t("Ignore fail request and continue workflow", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Checkbox', 'x-component': 'Checkbox',

View File

@ -12,12 +12,19 @@ export default {
type: 'update', type: 'update',
group: 'collection', group: 'collection',
fieldset: { fieldset: {
'config.collection': collection, collection,
'config.params.filter': { params: {
type: 'object',
title: '',
'x-decorator': 'FormItem',
properties: {
filter: {
...filter, ...filter,
title: `{{t("Only update records matching conditions", { ns: "${NAMESPACE}" })}}`, title: `{{t("Only update records matching conditions", { ns: "${NAMESPACE}" })}}`,
}, },
'config.params.values': values values
}
}
}, },
view: { view: {

View File

@ -6,7 +6,6 @@ import { NAMESPACE } from "../locale";
export const collection = { export const collection = {
type: 'string', type: 'string',
title: '{{t("Collection")}}', title: '{{t("Collection")}}',
name: 'config.collection',
required: true, required: true,
'x-reactions': ['{{useCollectionDataSource()}}'], 'x-reactions': ['{{useCollectionDataSource()}}'],
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
@ -19,7 +18,6 @@ export const collection = {
export const values = { export const values = {
type: 'object', type: 'object',
title: '{{t("Fields values")}}', title: '{{t("Fields values")}}',
name: 'config.params.values',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-decorator-props': { 'x-decorator-props': {
labelAlign: 'left', labelAlign: 'left',
@ -34,7 +32,6 @@ export const values = {
export const filter = { export const filter = {
type: 'object', type: 'object',
title: '{{t("Filter")}}', title: '{{t("Filter")}}',
name: 'config.params.filter',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-decorator-props': { 'x-decorator-props': {
labelAlign: 'left', labelAlign: 'left',
@ -46,7 +43,7 @@ export const filter = {
'x-component-props': { 'x-component-props': {
useProps() { useProps() {
const { values } = useForm(); const { values } = useForm();
const options = useCollectionFilterOptions(values.config?.collection); const options = useCollectionFilterOptions(values?.collection);
return { return {
options, options,
className: css` className: css`

View File

@ -157,19 +157,27 @@ export const nodeClass = css`
`; `;
export const nodeCardClass = css` export const nodeCardClass = css`
position: relative;
width: 20em; width: 20em;
background: #fff; background: #fff;
padding: 1em; padding: 1em;
box-shadow: 0 .25em .5em rgba(0, 0, 0, .1); box-shadow: 0 .25em .5em rgba(0, 0, 0, .1);
border-radius: .5em;
cursor: pointer;
transition: box-shadow .3s ease;
&.configuring{
box-shadow: 0 .25em 1em rgba(0, 100, 200, .25);
}
.workflow-node-remove-button, .workflow-node-remove-button,
.workflow-node-job-button{ .workflow-node-job-button{
position: absolute; position: absolute;
right: -.5em;
top: -.5em;
} }
.workflow-node-remove-button{ .workflow-node-remove-button{
right: .5em;
top: .5em;
color: #999; color: #999;
opacity: 0; opacity: 0;
transition: opacity .3s ease; transition: opacity .3s ease;
@ -185,8 +193,8 @@ export const nodeCardClass = css`
.workflow-node-job-button{ .workflow-node-job-button{
display: flex; display: flex;
top: 0; top: 1em;
right: 0; right: 1em;
width: 1.25rem; width: 1.25rem;
height: 1.25rem; height: 1.25rem;
min-width: 1.25rem; min-width: 1.25rem;
@ -200,7 +208,32 @@ export const nodeCardClass = css`
} }
} }
.ant-input{
font-weight: bold;
&:not(:focus){
transition: background-color .3s ease, border-color .3s ease;
border-color: #f7f7f7;
background-color: #f7f7f7;
&:not(:disabled):hover{
border-color: #eee;
background-color: #eee;
}
&:disabled:hover{
border-color: #f7f7f7;
}
}
}
.workflow-node-config-button{
padding: 0;
}
&:hover{ &:hover{
box-shadow: 0 .25em .5em rgba(0, 0, 0, .25);
.workflow-node-remove-button{ .workflow-node-remove-button{
opacity: 1; opacity: 1;
} }
@ -213,13 +246,20 @@ export const nodeHeaderClass = css`
export const nodeMetaClass = css` export const nodeMetaClass = css`
margin-bottom: .5em; margin-bottom: .5em;
.workflow-node-id{
color: #999;
&:before{
content: "#"
}
}
`; `;
export const nodeTitleClass = css` export const nodeTitleClass = css`
display: flex; display: flex;
align-items: center; align-items: center;
font-weight: normal; font-weight: normal;
.workflow-node-id{ .workflow-node-id{
color: #999; color: #999;
} }
@ -234,4 +274,10 @@ export const nodeSubtreeClass = css`
export const addButtonClass = css` export const addButtonClass = css`
flex-shrink: 0; flex-shrink: 0;
padding: 2em 0; padding: 2em 0;
> .ant-btn{
&:disabled{
visibility: hidden;
}
}
`; `;

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { Select } from 'antd'; import { Select } from 'antd';
import { onFieldValueChange } from '@formily/core'; import { observer, useForm } from '@formily/react';
import { observer, useForm, useFormEffects } from '@formily/react';
import { import {
SchemaInitializerItemOptions, SchemaInitializerItemOptions,
@ -16,26 +15,17 @@ import { CollectionBlockInitializer } from '../components/CollectionBlockInitial
import { CollectionFieldInitializers } from '../components/CollectionFieldInitializers'; import { CollectionFieldInitializers } from '../components/CollectionFieldInitializers';
import { NAMESPACE, useWorkflowTranslation } from '../locale'; import { NAMESPACE, useWorkflowTranslation } from '../locale';
const FieldsSelect = observer((props) => { const FieldsSelect = observer((props: any) => {
const { filter = () => true, ...others } = props;
const compile = useCompile(); const compile = useCompile();
const { getCollectionFields } = useCollectionManager(); const { getCollectionFields } = useCollectionManager();
const { values, clearFormGraph, setValuesIn } = useForm(); const { values } = useForm();
const fields = getCollectionFields(values?.config?.collection); const fields = getCollectionFields(values?.collection);
useFormEffects(() => {
onFieldValueChange('config.collection', (field) => {
clearFormGraph('config.changed');
setValuesIn('config.condition', null);
});
});
return ( return (
<Select {...props}> <Select {...others}>
{fields {fields
.filter(field => ( .filter(filter)
!field.hidden
&& (field.uiSchema ? !field.uiSchema['x-read-pretty'] : true)
&& !['linkTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type)
))
.map(field => ( .map(field => (
<Select.Option key={field.name} value={field.name}>{compile(field.uiSchema?.title)}</Select.Option> <Select.Option key={field.name} value={field.name}>{compile(field.uiSchema?.title)}</Select.Option>
))} ))}
@ -61,40 +51,33 @@ export default {
title: `{{t("Collection event", { ns: "${NAMESPACE}" })}}`, title: `{{t("Collection event", { ns: "${NAMESPACE}" })}}`,
type: 'collection', type: 'collection',
fieldset: { fieldset: {
'config.collection': { collection: {
...collection, ...collection,
['x-reactions']: [ ['x-reactions']: [
...collection['x-reactions'], ...collection['x-reactions'],
{ {
target: 'config.mode', target: 'changed',
effects: ['onFieldValueChange'],
fulfill: { fulfill: {
state: { state: {
visible: '{{!!$self.value}}', value: []
}, }
} }
}, },
{ {
target: 'config.changed', target: 'condition',
effects: ['onFieldValueChange'],
fulfill: { fulfill: {
state: { state: {
visible: '{{!!$self.value}}', value: null
},
} }
},
{
target: 'config.condition',
fulfill: {
state: {
visible: '{{!!$self.value}}',
},
} }
} }
] ]
}, },
'config.mode': { mode: {
type: 'number', type: 'number',
title: `{{t("Trigger on", { ns: "${NAMESPACE}" })}}`, title: `{{t("Trigger on", { ns: "${NAMESPACE}" })}}`,
name: 'config.mode',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Select', 'x-component': 'Select',
'x-component-props': { 'x-component-props': {
@ -104,7 +87,15 @@ export default {
required: true, required: true,
'x-reactions': [ 'x-reactions': [
{ {
target: 'config.changed', dependencies: ['collection'],
fulfill: {
state: {
visible: '{{!!$deps[0]}}',
},
}
},
{
target: 'changed',
fulfill: { fulfill: {
state: { state: {
disabled: `{{!($self.value & ${COLLECTION_TRIGGER_MODE.UPDATED})}}`, disabled: `{{!($self.value & ${COLLECTION_TRIGGER_MODE.UPDATED})}}`,
@ -113,24 +104,70 @@ export default {
}, },
] ]
}, },
'config.changed': { changed: {
type: 'array', type: 'array',
name: 'changed',
title: `{{t("Changed fields", { ns: "${NAMESPACE}" })}}`, title: `{{t("Changed fields", { ns: "${NAMESPACE}" })}}`,
description: `{{t("Triggered only if one of the selected fields changes. If unselected, it means that it will be triggered when any field changes. When record is added or deleted, any field is considered to have been changed.", { ns: "${NAMESPACE}" })}}`, description: `{{t("Triggered only if one of the selected fields changes. If unselected, it means that it will be triggered when any field changes. When record is added or deleted, any field is considered to have been changed.", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'FieldsSelect', 'x-component': 'FieldsSelect',
'x-component-props': { 'x-component-props': {
mode: 'multiple', mode: 'multiple',
placeholder: '{{t("Select Field")}}' placeholder: '{{t("Select Field")}}',
filter(field) {
return (
!field.hidden
&& (field.uiSchema ? !field.uiSchema['x-read-pretty'] : true)
&& !['linkTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type)
);
} }
}, },
'config.condition': { 'x-reactions': [
...filter, {
name: 'config.condition', dependencies: ['collection'],
title: `{{t("Only triggers when match conditions", { ns: "${NAMESPACE}" })}}` fulfill: {
state: {
visible: '{{!!$deps[0]}}',
},
} }
}, },
]
},
condition: {
...filter,
title: `{{t("Only triggers when match conditions", { ns: "${NAMESPACE}" })}}`,
'x-reactions': [
{
dependencies: ['collection'],
fulfill: {
state: {
visible: '{{!!$deps[0]}}',
},
}
},
]
},
// appends: {
// type: 'array',
// title: `{{t("Prefetch fields", { ns: "${NAMESPACE}" })}}`,
// description: `{{t("Triggered only if one of the selected fields changes. If unselected, it means that it will be triggered when any field changes. When record is added or deleted, any field is considered to have been changed.", { ns: "${NAMESPACE}" })}}`,
// 'x-decorator': 'FormItem',
// 'x-component': 'FieldsSelect',
// 'x-component-props': {
// mode: 'multiple',
// placeholder: '{{t("Select Field")}}'
// },
// 'x-reactions': [
// {
// dependencies: ['collection'],
// fulfill: {
// state: {
// visible: '{{!!$deps[0]}}',
// },
// }
// },
// ]
// },
},
scope: { scope: {
useCollectionDataSource useCollectionDataSource
}, },

View File

@ -1,14 +1,14 @@
import React, { useState } from "react";
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 { Registry } from "@nocobase/utils/client";
import { message, Tag, Alert } from "antd"; import { message, Tag, Alert, Button, Input } from "antd";
import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { InfoOutlined } from '@ant-design/icons'; import { InfoOutlined } from '@ant-design/icons';
import { SchemaComponent, SchemaInitializerItemOptions, useActionContext, useAPIClient, useCompile, useRequest, useResourceActionContext } from '@nocobase/client'; import { ActionContext, SchemaComponent, SchemaInitializerItemOptions, useActionContext, useAPIClient, useCompile, useRequest, useResourceActionContext } from '@nocobase/client';
import { nodeCardClass, nodeHeaderClass, nodeMetaClass, nodeTitleClass } from "../style"; import { nodeCardClass, nodeMetaClass, nodeTitleClass } from "../style";
import { useFlowContext } from "../FlowContext"; import { useFlowContext } from "../FlowContext";
import collection from './collection'; import collection from './collection';
import schedule from "./schedule/"; import schedule from "./schedule/";
@ -16,7 +16,6 @@ import { lang, NAMESPACE } from "../locale";
function useUpdateConfigAction() { function useUpdateConfigAction() {
const { t } = useTranslation();
const form = useForm(); const form = useForm();
const api = useAPIClient(); const api = useAPIClient();
const { workflow } = useFlowContext() ?? {}; const { workflow } = useFlowContext() ?? {};
@ -25,13 +24,15 @@ function useUpdateConfigAction() {
return { return {
async run() { async run() {
if (workflow.executed) { if (workflow.executed) {
message.error(t('Trigger in executed workflow cannot be modified')); message.error(lang('Trigger in executed workflow cannot be modified'));
return; return;
} }
await form.submit(); await form.submit();
await api.resource('workflows').update?.({ await api.resource('workflows').update?.({
filterByTk: workflow.id, filterByTk: workflow.id,
values: form.values values: {
config: form.values
}
}); });
ctx.setVisible(false); ctx.setVisible(false);
refresh(); refresh();
@ -127,33 +128,78 @@ function TriggerExecution() {
} }
export const TriggerConfig = () => { export const TriggerConfig = () => {
const { t } = useTranslation(); const api = useAPIClient();
const compile = useCompile(); const compile = useCompile();
const { workflow } = useFlowContext(); const { workflow, refresh } = useFlowContext();
if (!workflow || !workflow.type) { if (!workflow || !workflow.type) {
return null; return null;
} }
const { type, config, executed } = workflow; const { title, type, config, executed } = workflow;
const { title, fieldset, scope, components } = triggers.get(type); const { title: typeTitle, fieldset, scope, components } = triggers.get(type);
const detailText = executed ? '{{t("View")}}' : '{{t("Configure")}}'; const detailText = executed ? '{{t("View")}}' : '{{t("Configure")}}';
const titleText = `${lang('Trigger')}: ${compile(title)}`; const titleText = `${lang('Trigger')}: ${compile(typeTitle)}`;
const [editingTitle, setEditingTitle] = useState<string>(title ?? typeTitle);
const [editingConfig, setEditingConfig] = useState(false);
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).some((name: string) => whiteSet.has(name))) {
setEditingConfig(true);
ev.stopPropagation();
return;
}
}
}
return ( return (
<div className={cx(nodeCardClass)}> <div className={cx(nodeCardClass)} onClick={onOpenDrawer}>
<div className={cx(nodeHeaderClass)}> <div className={cx(nodeMetaClass, 'workflow-node-meta')}>
<div className={cx(nodeMetaClass)}> <Tag color="gold">{titleText}</Tag>
<Tag color="gold">{lang('Trigger')}</Tag> </div>
<div>
<Input.TextArea
value={editingTitle}
onChange={(ev) => setEditingTitle(ev.target.value)}
onBlur={(ev) => onChangeTitle(ev.target.value)}
autoSize
/>
</div> </div>
<h4>{compile(title)}</h4>
<TriggerExecution /> <TriggerExecution />
</div> <ActionContext.Provider value={{ visible: editingConfig, setVisible: setEditingConfig }}>
<SchemaComponent <SchemaComponent
schema={{ schema={{
type: 'void', type: 'void',
title: detailText,
'x-component': 'Action.Link',
name: 'drawer',
properties: { properties: {
config: {
type: 'void',
'x-content': detailText,
'x-component': Button,
'x-component-props': {
type: 'link',
className: 'workflow-node-config-button'
},
},
drawer: { drawer: {
type: 'void', type: 'void',
title: titleText, title: titleText,
@ -163,23 +209,10 @@ export const TriggerConfig = () => {
disabled: workflow.executed, disabled: workflow.executed,
useValues(options) { useValues(options) {
return useRequest(() => Promise.resolve({ return useRequest(() => Promise.resolve({
data: { config }, data: config,
}), options); }), options);
}, },
}, },
properties: {
config: {
type: 'void',
name: 'config',
'x-component': 'fieldset',
'x-component-props': {
className: css`
.ant-select{
width: auto;
min-width: 6em;
}
`
},
properties: { properties: {
...(executed ? { ...(executed ? {
alert: { alert: {
@ -196,8 +229,18 @@ export const TriggerConfig = () => {
}, },
} }
} : {}), } : {}),
...fieldset fieldset: {
type: 'void',
'x-component': 'fieldset',
'x-component-props': {
className: css`
.ant-select{
width: auto;
min-width: 6em;
} }
`
},
properties: fieldset
}, },
actions: executed actions: executed
? null ? null
@ -229,6 +272,7 @@ export const TriggerConfig = () => {
scope={scope} scope={scope}
components={components} components={components}
/> />
</ActionContext.Provider>
</div> </div>
); );
} }

View File

@ -10,7 +10,7 @@ export const DateFieldsSelect: React.FC<any> = observer((props) => {
const compile = useCompile(); const compile = useCompile();
const { getCollectionFields } = useCollectionManager(); const { getCollectionFields } = useCollectionManager();
const { values } = useForm(); const { values } = useForm();
const fields = getCollectionFields(values?.config?.collection); const fields = getCollectionFields(values?.collection);
return ( return (
<Select <Select

View File

@ -33,7 +33,7 @@ const ModeFieldsets = {
'x-component': 'RepeatField', 'x-component': 'RepeatField',
'x-reactions': [ 'x-reactions': [
{ {
target: 'config.endsOn', target: 'endsOn',
fulfill: { fulfill: {
state: { state: {
visible: '{{!!$self.value}}', visible: '{{!!$self.value}}',
@ -41,7 +41,7 @@ const ModeFieldsets = {
} }
}, },
{ {
target: 'config.limit', target: 'limit',
fulfill: { fulfill: {
state: { state: {
visible: '{{!!$self.value}}', visible: '{{!!$self.value}}',
@ -79,7 +79,7 @@ const ModeFieldsets = {
...collection['x-reactions'], ...collection['x-reactions'],
{ {
// only full path works // only full path works
target: 'config.startsOn', target: 'startsOn',
fulfill: { fulfill: {
state: { state: {
visible: '{{!!$self.value}}', visible: '{{!!$self.value}}',
@ -95,7 +95,7 @@ const ModeFieldsets = {
'x-component': 'OnField', 'x-component': 'OnField',
'x-reactions': [ 'x-reactions': [
{ {
target: 'config.repeat', target: 'repeat',
fulfill: { fulfill: {
state: { state: {
visible: '{{!!$self.value}}', visible: '{{!!$self.value}}',
@ -113,7 +113,7 @@ const ModeFieldsets = {
'x-component': 'RepeatField', 'x-component': 'RepeatField',
'x-reactions': [ 'x-reactions': [
{ {
target: 'config.endsOn', target: 'endsOn',
fulfill: { fulfill: {
state: { state: {
visible: '{{!!$self.value}}', visible: '{{!!$self.value}}',
@ -121,7 +121,7 @@ const ModeFieldsets = {
} }
}, },
{ {
target: 'config.limit', target: 'limit',
fulfill: { fulfill: {
state: { state: {
visible: '{{!!$self.value}}', visible: '{{!!$self.value}}',
@ -157,16 +157,15 @@ const scheduleModeOptions = [
export const ScheduleConfig = () => { export const ScheduleConfig = () => {
const { values = {}, clearFormGraph } = useForm(); const { values = {}, clearFormGraph } = useForm();
const { config = {} } = values; const [mode, setMode] = useState(values.mode);
const [mode, setMode] = useState(config.mode);
useFormEffects(() => { useFormEffects(() => {
onFieldValueChange('config.mode', (field) => { onFieldValueChange('mode', (field) => {
setMode(field.value); setMode(field.value);
clearFormGraph('config.collection'); clearFormGraph('collection');
clearFormGraph('config.startsOn'); clearFormGraph('startsOn');
clearFormGraph('config.repeat'); clearFormGraph('repeat');
clearFormGraph('config.endsOn'); clearFormGraph('endsOn');
clearFormGraph('config.limit'); clearFormGraph('limit');
}); });
}); });
@ -182,7 +181,8 @@ export const ScheduleConfig = () => {
'x-component-props': { 'x-component-props': {
options: scheduleModeOptions options: scheduleModeOptions
}, },
required: true required: true,
default: SCHEDULE_MODE.STATIC
}} }}
/> />
<SchemaComponent <SchemaComponent

View File

@ -14,8 +14,7 @@ export default {
type: 'schedule', type: 'schedule',
fieldset: { fieldset: {
config: { config: {
type: 'object', type: 'void',
name: 'config',
'x-component': 'ScheduleConfig', 'x-component': 'ScheduleConfig',
'x-component-props': { 'x-component-props': {
} }