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 25c0fb7ed..1a28b261d 100644 --- a/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx +++ b/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx @@ -123,6 +123,8 @@ ReadPretty.JSON = (props) => {
diff --git a/packages/plugins/workflow/src/client/AddButton.tsx b/packages/plugins/workflow/src/client/AddButton.tsx
index a15ce3531..6fa75ce58 100644
--- a/packages/plugins/workflow/src/client/AddButton.tsx
+++ b/packages/plugins/workflow/src/client/AddButton.tsx
@@ -20,7 +20,7 @@ interface AddButtonProps {
 export function AddButton({ upstream, branchIndex = null }: AddButtonProps) {
   const compile = useCompile();
   const api = useAPIClient();
-  const { workflow, onNodeAdded } = useFlowContext() ?? {};
+  const { workflow, refresh } = useFlowContext() ?? {};
   if (!workflow) {
     return null;
   }
@@ -30,8 +30,9 @@ export function AddButton({ upstream, branchIndex = null }: AddButtonProps) {
     const type = keyPath.pop();
     const config = {};
     const [optionKey] = keyPath;
+    const instruction = instructions.get(type);
     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);
     }
 
@@ -40,47 +41,46 @@ export function AddButton({ upstream, branchIndex = null }: AddButtonProps) {
         type,
         upstreamId: upstream?.id ?? null,
         branchIndex,
+        title: compile(instruction.title),
         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 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 (
     
onCreate(ev)}> - {groups.map(group => { - const groupInstructions = instructionList.filter(item => item.group === group.value); - return groupInstructions.length ? ( - - {groupInstructions.map(item => item.options - ? ( - - {item.options.map(option => ( - {compile(option.label)} - ))} - - ) - : ( - {compile(item.title)} - ))} - - ) : null; - })} - - } + overlay={ onCreate(ev)} items={compile(groups)} />} disabled={workflow.executed} >
diff --git a/packages/plugins/workflow/src/client/nodes/query.tsx b/packages/plugins/workflow/src/client/nodes/query.tsx index 2a021754d..6902cc065 100644 --- a/packages/plugins/workflow/src/client/nodes/query.tsx +++ b/packages/plugins/workflow/src/client/nodes/query.tsx @@ -14,7 +14,7 @@ export default { type: 'query', group: 'collection', fieldset: { - 'config.collection': collection, + collection, // 'config.multiple': { // type: 'boolean', // title: `{{t("Multiple records", { ns: "${NAMESPACE}" })}}`, @@ -25,9 +25,8 @@ export default { // disabled: true // } // }, - 'config.params': { + params: { type: 'object', - name: 'config.params', title: '', 'x-decorator': 'FormItem', properties: { diff --git a/packages/plugins/workflow/src/client/nodes/request.tsx b/packages/plugins/workflow/src/client/nodes/request.tsx index e9d9b79b1..1a570e82e 100644 --- a/packages/plugins/workflow/src/client/nodes/request.tsx +++ b/packages/plugins/workflow/src/client/nodes/request.tsx @@ -11,9 +11,8 @@ export default { type: 'request', group: 'extended', fieldset: { - 'config.method': { + method: { type: 'string', - name: 'config.method', required: true, title: `{{t("HTTP method", { ns: "${NAMESPACE}" })}}`, 'x-decorator': 'FormItem', @@ -31,9 +30,8 @@ export default { ], default: 'POST' }, - 'config.url': { + url: { type: 'string', - name: 'config.url', required: true, title: `{{t("URL", { ns: "${NAMESPACE}" })}}`, 'x-decorator': 'FormItem', @@ -52,9 +50,8 @@ export default { placeholder: 'https://www.nocobase.com', }, }, - 'config.headers': { + headers: { type: 'array', - name: 'config.headers', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', title: `{{t("Headers", { ns: "${NAMESPACE}" })}}`, @@ -99,9 +96,8 @@ export default { }, }, }, - 'config.params': { + params: { type: 'array', - name: 'config.params', 'x-component': 'ArrayItems', 'x-decorator': 'FormItem', title: `{{t("Parameters", { ns: "${NAMESPACE}" })}}`, @@ -145,9 +141,8 @@ export default { }, }, }, - 'config.data': { + data: { type: 'string', - name: 'config.data', title: `{{t("Body", { ns: "${NAMESPACE}" })}}`, 'x-decorator': 'FormItem', 'x-decorator-props': {}, @@ -159,15 +154,14 @@ export default { }, placeholder: `{{t("Input request data", { ns: "${NAMESPACE}" })}}`, className: css` - font-size: 85%; + font-size: 90%; font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; ` }, description: `{{t("Only support standard JSON data", { ns: "${NAMESPACE}" })}}`, }, - 'config.timeout': { + timeout: { type: 'number', - name: 'config.timeout', title: `{{t("Timeout config", { ns: "${NAMESPACE}" })}}`, 'x-decorator': 'FormItem', 'x-decorator-props': {}, @@ -179,9 +173,8 @@ export default { defaultValue: 5000, }, }, - 'config.ignoreFail': { + ignoreFail: { type: 'boolean', - name: 'config.ignoreFail', title: `{{t("Ignore fail request and continue workflow", { ns: "${NAMESPACE}" })}}`, 'x-decorator': 'FormItem', 'x-component': 'Checkbox', diff --git a/packages/plugins/workflow/src/client/nodes/update.tsx b/packages/plugins/workflow/src/client/nodes/update.tsx index f8a729e15..38a202be1 100644 --- a/packages/plugins/workflow/src/client/nodes/update.tsx +++ b/packages/plugins/workflow/src/client/nodes/update.tsx @@ -12,12 +12,19 @@ export default { type: 'update', group: 'collection', fieldset: { - 'config.collection': collection, - 'config.params.filter': { - ...filter, - title: `{{t("Only update records matching conditions", { ns: "${NAMESPACE}" })}}`, - }, - 'config.params.values': values + collection, + params: { + type: 'object', + title: '', + 'x-decorator': 'FormItem', + properties: { + filter: { + ...filter, + title: `{{t("Only update records matching conditions", { ns: "${NAMESPACE}" })}}`, + }, + values + } + } }, view: { diff --git a/packages/plugins/workflow/src/client/schemas/collection.ts b/packages/plugins/workflow/src/client/schemas/collection.ts index b617e78f5..8728bbd65 100644 --- a/packages/plugins/workflow/src/client/schemas/collection.ts +++ b/packages/plugins/workflow/src/client/schemas/collection.ts @@ -6,7 +6,6 @@ import { NAMESPACE } from "../locale"; export const collection = { type: 'string', title: '{{t("Collection")}}', - name: 'config.collection', required: true, 'x-reactions': ['{{useCollectionDataSource()}}'], 'x-decorator': 'FormItem', @@ -19,7 +18,6 @@ export const collection = { export const values = { type: 'object', title: '{{t("Fields values")}}', - name: 'config.params.values', 'x-decorator': 'FormItem', 'x-decorator-props': { labelAlign: 'left', @@ -34,7 +32,6 @@ export const values = { export const filter = { type: 'object', title: '{{t("Filter")}}', - name: 'config.params.filter', 'x-decorator': 'FormItem', 'x-decorator-props': { labelAlign: 'left', @@ -46,7 +43,7 @@ export const filter = { 'x-component-props': { useProps() { const { values } = useForm(); - const options = useCollectionFilterOptions(values.config?.collection); + const options = useCollectionFilterOptions(values?.collection); return { options, className: css` diff --git a/packages/plugins/workflow/src/client/style.tsx b/packages/plugins/workflow/src/client/style.tsx index 56612872c..8f8d0158c 100644 --- a/packages/plugins/workflow/src/client/style.tsx +++ b/packages/plugins/workflow/src/client/style.tsx @@ -157,19 +157,27 @@ export const nodeClass = css` `; export const nodeCardClass = css` + position: relative; width: 20em; background: #fff; padding: 1em; 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-job-button{ position: absolute; - right: -.5em; - top: -.5em; } .workflow-node-remove-button{ + right: .5em; + top: .5em; color: #999; opacity: 0; transition: opacity .3s ease; @@ -185,8 +193,8 @@ export const nodeCardClass = css` .workflow-node-job-button{ display: flex; - top: 0; - right: 0; + top: 1em; + right: 1em; width: 1.25rem; height: 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{ + box-shadow: 0 .25em .5em rgba(0, 0, 0, .25); + .workflow-node-remove-button{ opacity: 1; } @@ -213,13 +246,20 @@ export const nodeHeaderClass = css` export const nodeMetaClass = css` margin-bottom: .5em; + + .workflow-node-id{ + color: #999; + + &:before{ + content: "#" + } + } `; export const nodeTitleClass = css` display: flex; align-items: center; font-weight: normal; - .workflow-node-id{ color: #999; } @@ -234,4 +274,10 @@ export const nodeSubtreeClass = css` export const addButtonClass = css` flex-shrink: 0; padding: 2em 0; + + > .ant-btn{ + &:disabled{ + visibility: hidden; + } + } `; diff --git a/packages/plugins/workflow/src/client/triggers/collection.tsx b/packages/plugins/workflow/src/client/triggers/collection.tsx index 8ee3fbb40..c76b743ba 100644 --- a/packages/plugins/workflow/src/client/triggers/collection.tsx +++ b/packages/plugins/workflow/src/client/triggers/collection.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Select } from 'antd'; -import { onFieldValueChange } from '@formily/core'; -import { observer, useForm, useFormEffects } from '@formily/react'; +import { observer, useForm } from '@formily/react'; import { SchemaInitializerItemOptions, @@ -16,26 +15,17 @@ import { CollectionBlockInitializer } from '../components/CollectionBlockInitial import { CollectionFieldInitializers } from '../components/CollectionFieldInitializers'; import { NAMESPACE, useWorkflowTranslation } from '../locale'; -const FieldsSelect = observer((props) => { +const FieldsSelect = observer((props: any) => { + const { filter = () => true, ...others } = props; const compile = useCompile(); const { getCollectionFields } = useCollectionManager(); - const { values, clearFormGraph, setValuesIn } = useForm(); - const fields = getCollectionFields(values?.config?.collection); - useFormEffects(() => { - onFieldValueChange('config.collection', (field) => { - clearFormGraph('config.changed'); - setValuesIn('config.condition', null); - }); - }); + const { values } = useForm(); + const fields = getCollectionFields(values?.collection); return ( - {fields - .filter(field => ( - !field.hidden - && (field.uiSchema ? !field.uiSchema['x-read-pretty'] : true) - && !['linkTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type) - )) + .filter(filter) .map(field => ( {compile(field.uiSchema?.title)} ))} @@ -61,40 +51,33 @@ export default { title: `{{t("Collection event", { ns: "${NAMESPACE}" })}}`, type: 'collection', fieldset: { - 'config.collection': { + collection: { ...collection, ['x-reactions']: [ ...collection['x-reactions'], { - target: 'config.mode', + target: 'changed', + effects: ['onFieldValueChange'], fulfill: { state: { - visible: '{{!!$self.value}}', - }, + value: [] + } } }, { - target: 'config.changed', + target: 'condition', + effects: ['onFieldValueChange'], fulfill: { state: { - visible: '{{!!$self.value}}', - }, - } - }, - { - target: 'config.condition', - fulfill: { - state: { - visible: '{{!!$self.value}}', - }, + value: null + } } } ] }, - 'config.mode': { + mode: { type: 'number', title: `{{t("Trigger on", { ns: "${NAMESPACE}" })}}`, - name: 'config.mode', 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { @@ -104,7 +87,15 @@ export default { required: true, 'x-reactions': [ { - target: 'config.changed', + dependencies: ['collection'], + fulfill: { + state: { + visible: '{{!!$deps[0]}}', + }, + } + }, + { + target: 'changed', fulfill: { state: { disabled: `{{!($self.value & ${COLLECTION_TRIGGER_MODE.UPDATED})}}`, @@ -113,23 +104,69 @@ export default { }, ] }, - 'config.changed': { + changed: { type: 'array', - name: 'changed', 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}" })}}`, 'x-decorator': 'FormItem', 'x-component': 'FieldsSelect', 'x-component-props': { 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) + ); + } + }, + 'x-reactions': [ + { + dependencies: ['collection'], + fulfill: { + state: { + visible: '{{!!$deps[0]}}', + }, + } + }, + ] }, - 'config.condition': { + condition: { ...filter, - name: 'config.condition', - title: `{{t("Only triggers when match conditions", { ns: "${NAMESPACE}" })}}` - } + 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: { useCollectionDataSource diff --git a/packages/plugins/workflow/src/client/triggers/index.tsx b/packages/plugins/workflow/src/client/triggers/index.tsx index 4d71482bb..85b0b5d7b 100644 --- a/packages/plugins/workflow/src/client/triggers/index.tsx +++ b/packages/plugins/workflow/src/client/triggers/index.tsx @@ -1,14 +1,14 @@ +import React, { useState } from "react"; import { css, cx } from "@emotion/css"; import { ISchema, useForm } from "@formily/react"; import { Registry } from "@nocobase/utils/client"; -import { message, Tag, Alert } from "antd"; -import React from "react"; +import { message, Tag, Alert, Button, Input } from "antd"; import { useTranslation } from "react-i18next"; 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 collection from './collection'; import schedule from "./schedule/"; @@ -16,7 +16,6 @@ import { lang, NAMESPACE } from "../locale"; function useUpdateConfigAction() { - const { t } = useTranslation(); const form = useForm(); const api = useAPIClient(); const { workflow } = useFlowContext() ?? {}; @@ -25,13 +24,15 @@ function useUpdateConfigAction() { return { async run() { if (workflow.executed) { - message.error(t('Trigger in executed workflow cannot be modified')); + message.error(lang('Trigger in executed workflow cannot be modified')); return; } await form.submit(); await api.resource('workflows').update?.({ filterByTk: workflow.id, - values: form.values + values: { + config: form.values + } }); ctx.setVisible(false); refresh(); @@ -127,108 +128,151 @@ function TriggerExecution() { } export const TriggerConfig = () => { - const { t } = useTranslation(); + const api = useAPIClient(); const compile = useCompile(); - const { workflow } = useFlowContext(); + const { workflow, refresh } = useFlowContext(); if (!workflow || !workflow.type) { return null; } - const { type, config, executed } = workflow; - const { title, fieldset, scope, components } = triggers.get(type); + const { title, type, config, executed } = workflow; + const { title: typeTitle, fieldset, scope, components } = triggers.get(type); const detailText = executed ? '{{t("View")}}' : '{{t("Configure")}}'; - const titleText = `${lang('Trigger')}: ${compile(title)}`; + const titleText = `${lang('Trigger')}: ${compile(typeTitle)}`; + + const [editingTitle, setEditingTitle] = useState(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 ( -
-
-
- {lang('Trigger')} -
-

{compile(title)}

- +
+
+ {titleText}
- Promise.resolve({ - data: { config }, - }), options); +
+ setEditingTitle(ev.target.value)} + onBlur={(ev) => onChangeTitle(ev.target.value)} + autoSize + /> +
+ + + Promise.resolve({ + data: config, + }), options); }, - 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; - ` - }, - } - } : {}), - ...fieldset - } }, - actions: executed - ? null - : { - type: 'void', - 'x-component': 'Action.Drawer.Footer', - properties: { - cancel: { - title: '{{t("Cancel")}}', - 'x-component': 'Action', + properties: { + ...(executed ? { + alert: { + 'x-component': Alert, 'x-component-props': { - useAction: '{{ cm.useCancelAction }}', + 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; + ` }, + } + } : {}), + fieldset: { + type: 'void', + 'x-component': 'fieldset', + 'x-component-props': { + className: css` + .ant-select{ + width: auto; + min-width: 6em; + } + ` }, - submit: { - title: '{{t("Submit")}}', - 'x-component': 'Action', - 'x-component-props': { - type: 'primary', - useAction: useUpdateConfigAction + properties: fieldset + }, + actions: executed + ? null + : { + 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} - /> + }} + scope={scope} + components={components} + /> +
); } diff --git a/packages/plugins/workflow/src/client/triggers/schedule/DateFieldsSelect.tsx b/packages/plugins/workflow/src/client/triggers/schedule/DateFieldsSelect.tsx index 65ec314d3..7e9df7efc 100644 --- a/packages/plugins/workflow/src/client/triggers/schedule/DateFieldsSelect.tsx +++ b/packages/plugins/workflow/src/client/triggers/schedule/DateFieldsSelect.tsx @@ -10,7 +10,7 @@ export const DateFieldsSelect: React.FC = observer((props) => { const compile = useCompile(); const { getCollectionFields } = useCollectionManager(); const { values } = useForm(); - const fields = getCollectionFields(values?.config?.collection); + const fields = getCollectionFields(values?.collection); return (