diff --git a/web/app/components/workflow/header/run-mode.tsx b/web/app/components/workflow/header/run-mode.tsx index 1a101bc6d2..5c225156ac 100644 --- a/web/app/components/workflow/header/run-mode.tsx +++ b/web/app/components/workflow/header/run-mode.tsx @@ -32,7 +32,7 @@ const RunMode = ({ handleWorkflowRunAllTriggersInWorkflow, } = useWorkflowStartRun() const { handleStopRun } = useWorkflowRun() - const { validateBeforeRun, warningNodes } = useWorkflowRunValidation() + const { warningNodes } = useWorkflowRunValidation() const workflowRunningData = useStore(s => s.workflowRunningData) const isListening = useStore(s => s.isListening) @@ -98,14 +98,7 @@ const RunMode = ({ // Placeholder for trigger-specific execution logic for schedule, webhook, plugin types console.log('TODO: Handle trigger execution for type:', option.type, 'nodeId:', option.nodeId) } - }, [ - validateBeforeRun, - handleWorkflowStartRunInWorkflow, - handleWorkflowTriggerScheduleRunInWorkflow, - handleWorkflowTriggerWebhookRunInWorkflow, - handleWorkflowTriggerPluginRunInWorkflow, - handleWorkflowRunAllTriggersInWorkflow, - ]) + }, [warningNodes, notify, t, handleWorkflowStartRunInWorkflow, handleWorkflowTriggerScheduleRunInWorkflow, handleWorkflowTriggerWebhookRunInWorkflow, handleWorkflowTriggerPluginRunInWorkflow, handleWorkflowRunAllTriggersInWorkflow]) const { eventEmitter } = useEventEmitterContextContext() eventEmitter?.useSubscription((v: any) => { diff --git a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts index 7288397f7d..06843eacef 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts @@ -316,20 +316,7 @@ const useOneStepRun = ({ invalidateSysVarValues() invalidateConversationVarValues() // loop, iteration, variable assigner node can update the conversation variables, but to simple the logic(some nodes may also can update in the future), all nodes refresh. } - }, [ - isRunAfterSingleRun, - runningStatus, - flowId, - id, - store, - appendNodeInspectVars, - updateNodeInspectRunningState, - invalidLastRun, - isStartNode, - isTriggerNode, - invalidateSysVarValues, - invalidateConversationVarValues, - ]) + }, [isRunAfterSingleRun, runningStatus, flowType, flowId, id, store, appendNodeInspectVars, updateNodeInspectRunningState, invalidLastRun, isStartNode, isTriggerNode, invalidateSysVarValues, invalidateConversationVarValues]) const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate() const setNodeRunning = () => { diff --git a/web/app/components/workflow/nodes/human-input/components/user-action.tsx b/web/app/components/workflow/nodes/human-input/components/user-action.tsx index b109d6653e..9868ea4393 100644 --- a/web/app/components/workflow/nodes/human-input/components/user-action.tsx +++ b/web/app/components/workflow/nodes/human-input/components/user-action.tsx @@ -7,10 +7,12 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' -import { genActionId } from '../utils' +import Toast from '@/app/components/base/toast' import ButtonStyleDropdown from './button-style-dropdown' const i18nPrefix = 'nodes.humanInput' +const ACTION_ID_MAX_LENGTH = 20 +const BUTTON_TEXT_MAX_LENGTH = 40 type UserActionItemProps = { data: UserAction @@ -28,17 +30,42 @@ const UserActionItem: FC = ({ const { t } = useTranslation() const handleIDChange = (e: React.ChangeEvent) => { - if (!e.target.value.trim()) - onChange({ ...data, id: genActionId() }) - else - onChange({ ...data, id: e.target.value }) + const value = e.target.value + if (!value.trim()) { + onChange({ ...data, id: '' }) + return + } + // Convert spaces to underscores, then only allow characters matching /^[A-Za-z_][A-Za-z0-9_]*$/ + const withUnderscores = value.replace(/ /g, '_') + let sanitized = withUnderscores + .split('') + .filter((char, index) => { + if (index === 0) + return /^[a-z_]$/i.test(char) + return /^\w$/.test(char) + }) + .join('') + + if (sanitized !== withUnderscores) + Toast.notify({ type: 'error', message: t(`${i18nPrefix}.userActions.invalidActionIdFormat`, { ns: 'workflow' }) }) + + // Limit to 20 characters + if (sanitized.length > ACTION_ID_MAX_LENGTH) { + sanitized = sanitized.slice(0, ACTION_ID_MAX_LENGTH) + Toast.notify({ type: 'error', message: t(`${i18nPrefix}.userActions.actionIdTooLong`, { ns: 'workflow' }) }) + } + + if (sanitized) + onChange({ ...data, id: sanitized }) } const handleTextChange = (e: React.ChangeEvent) => { - if (!e.target.value.trim()) - onChange({ ...data, title: 'Button Text' }) - else - onChange({ ...data, title: e.target.value }) + let value = e.target.value + if (value.length > BUTTON_TEXT_MAX_LENGTH) { + value = value.slice(0, BUTTON_TEXT_MAX_LENGTH) + Toast.notify({ type: 'error', message: t(`${i18nPrefix}.userActions.buttonTextTooLong`, { ns: 'workflow' }) }) + } + onChange({ ...data, title: value }) } return ( diff --git a/web/app/components/workflow/nodes/human-input/default.ts b/web/app/components/workflow/nodes/human-input/default.ts index b9b95ae1cc..e3d5c7ae1e 100644 --- a/web/app/components/workflow/nodes/human-input/default.ts +++ b/web/app/components/workflow/nodes/human-input/default.ts @@ -42,6 +42,25 @@ const nodeDefault: NodeDefault = { if (!errorMessages && !payload.user_actions.length) errorMessages = t(`${i18nPrefix}.noUserActions`, { ns: 'workflow' }) + if (!errorMessages && payload.user_actions.length > 0) { + const actionIds = payload.user_actions.map(action => action.id) + const hasDuplicateIds = actionIds.length !== new Set(actionIds).size + if (hasDuplicateIds) + errorMessages = t(`${i18nPrefix}.duplicateActionId`, { ns: 'workflow' }) + } + + if (!errorMessages && payload.user_actions.length > 0) { + const hasEmptyId = payload.user_actions.some(action => !action.id?.trim()) + if (hasEmptyId) + errorMessages = t(`${i18nPrefix}.emptyActionId`, { ns: 'workflow' }) + } + + if (!errorMessages && payload.user_actions.length > 0) { + const hasEmptyTitle = payload.user_actions.some(action => !action.title?.trim()) + if (hasEmptyTitle) + errorMessages = t(`${i18nPrefix}.emptyActionTitle`, { ns: 'workflow' }) + } + return { isValid: !errorMessages, errorMessage: errorMessages, diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 80c7fa153b..421267a484 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -558,6 +558,9 @@ "nodes.humanInput.deliveryMethod.types.webapp.description": "Display to end-user in webapp", "nodes.humanInput.deliveryMethod.types.webapp.title": "Webapp", "nodes.humanInput.editor.previewTip": "In preview mode, action buttons are not functional.", + "nodes.humanInput.errorMsg.duplicateActionId": "Duplicate action ID found in user actions", + "nodes.humanInput.errorMsg.emptyActionId": "Action ID cannot be empty", + "nodes.humanInput.errorMsg.emptyActionTitle": "Action title cannot be empty", "nodes.humanInput.errorMsg.noDeliveryMethod": "Please select at least one delivery method", "nodes.humanInput.errorMsg.noDeliveryMethodEnabled": "Please enable at least one delivery method", "nodes.humanInput.errorMsg.noUserActions": "Please add at least one user action", @@ -586,10 +589,13 @@ "nodes.humanInput.timeout.days": "Days", "nodes.humanInput.timeout.hours": "Hours", "nodes.humanInput.timeout.title": "Timeout", + "nodes.humanInput.userActions.actionIdTooLong": "Action ID must be 20 characters or less", "nodes.humanInput.userActions.actionNamePlaceholder": "Action Name", "nodes.humanInput.userActions.buttonTextPlaceholder": "Button display Text", + "nodes.humanInput.userActions.buttonTextTooLong": "Button text must be 40 characters or less", "nodes.humanInput.userActions.chooseStyle": "Choose a button style", "nodes.humanInput.userActions.emptyTip": "Click the '+' button to add user actions", + "nodes.humanInput.userActions.invalidActionIdFormat": "Action ID must start with a letter or underscore, followed by letters, numbers, or underscores", "nodes.humanInput.userActions.title": "User Actions", "nodes.humanInput.userActions.tooltip": "Define buttons that users can click to respond to this form. Each button can trigger different workflow paths.", "nodes.humanInput.userActions.triggered": "{{actionName}} has been triggered", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index 11651a7478..c6069701e1 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -558,6 +558,9 @@ "nodes.humanInput.deliveryMethod.types.webapp.description": "在 Web 应用中显示给最终用户", "nodes.humanInput.deliveryMethod.types.webapp.title": "Webapp", "nodes.humanInput.editor.previewTip": "在预览模式下,操作按钮无法使用。", + "nodes.humanInput.errorMsg.duplicateActionId": "用户操作中存在重复的操作 ID", + "nodes.humanInput.errorMsg.emptyActionId": "操作 ID 不能为空", + "nodes.humanInput.errorMsg.emptyActionTitle": "操作标题不能为空", "nodes.humanInput.errorMsg.noDeliveryMethod": "请至少选择一种提交方式", "nodes.humanInput.errorMsg.noDeliveryMethodEnabled": "请至少启用一种提交方式", "nodes.humanInput.errorMsg.noUserActions": "请添加至少一个用户操作", @@ -586,10 +589,13 @@ "nodes.humanInput.timeout.days": "日", "nodes.humanInput.timeout.hours": "小时", "nodes.humanInput.timeout.title": "超时设置", + "nodes.humanInput.userActions.actionIdTooLong": "操作 ID 不能超过 20 个字符", "nodes.humanInput.userActions.actionNamePlaceholder": "操作名称", "nodes.humanInput.userActions.buttonTextPlaceholder": "按钮显示文本", + "nodes.humanInput.userActions.buttonTextTooLong": "按钮文本不能超过 40 个字符", "nodes.humanInput.userActions.chooseStyle": "选择按钮样式", "nodes.humanInput.userActions.emptyTip": "点击 '+' 按钮添加用户操作", + "nodes.humanInput.userActions.invalidActionIdFormat": "操作 ID 必须以字母或下划线开头,后跟字母、数字或下划线", "nodes.humanInput.userActions.title": "用户操作", "nodes.humanInput.userActions.tooltip": "定义用户可以点击以响应此表单的按钮。每个按钮都可以触发不同的工作流路径。", "nodes.humanInput.userActions.triggered": "已触发{{actionName}}",