feat(hitl-input): add readonly prop to HITL input components for enhanced user interaction control

This commit is contained in:
twwu
2026-01-12 13:46:04 +08:00
parent a280df2c07
commit b6c6d52725
13 changed files with 194 additions and 121 deletions

View File

@ -20,12 +20,14 @@ type Props = {
text: string
data: UserActionButtonType
onChange: (state: UserActionButtonType) => void
readonly?: boolean
}
const ButtonStyleDropdown: FC<Props> = ({
text = 'Button Text',
data,
onChange,
readonly,
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
@ -44,7 +46,7 @@ const ButtonStyleDropdown: FC<Props> = ({
return (
<PortalToFollowElem
open={open}
open={open && !readonly}
onOpenChange={setOpen}
placement="bottom-end"
offset={{
@ -52,8 +54,8 @@ const ButtonStyleDropdown: FC<Props> = ({
crossAxis: 44,
}}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<div className={cn('flex cursor-pointer items-center justify-center rounded-lg bg-components-button-tertiary-bg p-1 hover:bg-components-button-tertiary-bg-hover', open && 'bg-components-button-tertiary-bg-hover')}>
<PortalToFollowElemTrigger onClick={() => !readonly && setOpen(v => !v)}>
<div className={cn('flex items-center justify-center rounded-lg bg-components-button-tertiary-bg p-1', !readonly && 'cursor-pointer hover:bg-components-button-tertiary-bg-hover', open && 'bg-components-button-tertiary-bg-hover')}>
<Button size="small" className="pointer-events-none px-1" variant={currentStyle}>
<RiFontSize className="h-4 w-4" />
</Button>

View File

@ -19,6 +19,7 @@ type Props = {
availableNodes?: Node[]
formContent?: string
onChange: (value: DeliveryMethod[]) => void
readonly?: boolean
}
const DeliveryMethodForm: React.FC<Props> = ({
@ -28,6 +29,7 @@ const DeliveryMethodForm: React.FC<Props> = ({
availableNodes,
formContent,
onChange,
readonly,
}) => {
const { t } = useTranslation()
@ -59,12 +61,14 @@ const DeliveryMethodForm: React.FC<Props> = ({
popupContent={t(`${i18nPrefix}.deliveryMethod.tooltip`, { ns: 'workflow' })}
/>
</div>
<div className="flex items-center px-1">
<MethodSelector
data={value}
onAdd={handleMethodAdd}
/>
</div>
{!readonly && (
<div className="flex items-center px-1">
<MethodSelector
data={value}
onAdd={handleMethodAdd}
/>
</div>
)}
</div>
{!value.length && (
<div className="system-xs-regular flex items-center justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.emptyTip`, { ns: 'workflow' })}</div>
@ -81,6 +85,7 @@ const DeliveryMethodForm: React.FC<Props> = ({
nodesOutputVars={nodesOutputVars}
availableNodes={availableNodes}
formContent={formContent}
readonly={readonly}
/>
))}
</div>

View File

@ -32,6 +32,7 @@ type Props = {
formContent?: string
onChange: (method: DeliveryMethod) => void
onDelete: (type: DeliveryMethodType) => void
readonly?: boolean
}
const DeliveryMethodItem: React.FC<Props> = ({
@ -42,6 +43,7 @@ const DeliveryMethodItem: React.FC<Props> = ({
formContent,
onChange,
onDelete,
readonly,
}) => {
const { t } = useTranslation()
const [isHovering, setIsHovering] = React.useState(false)
@ -82,33 +84,36 @@ const DeliveryMethodItem: React.FC<Props> = ({
{method.type === DeliveryMethodType.Email && (method.config as EmailConfig)?.debug_mode && <Badge size="s" className="!px-1 !py-0.5">DEBUG</Badge>}
</div>
<div className="flex items-center gap-1">
<div className="hidden items-end gap-1 group-hover:flex">
{method.type === DeliveryMethodType.Email && method.config && (
<>
<ActionButton onClick={() => setShowTestEmailModal(true)}>
<RiSendPlane2Line className="h-4 w-4" />
</ActionButton>
<ActionButton onClick={() => setShowEmailModal(true)}>
<RiEqualizer2Line className="h-4 w-4" />
</ActionButton>
</>
)}
<div
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<ActionButton
state={isHovering ? ActionButtonState.Destructive : ActionButtonState.Default}
onClick={() => onDelete(method.type)}
{!readonly && (
<div className="hidden items-end gap-1 group-hover:flex">
{method.type === DeliveryMethodType.Email && method.config && (
<>
<ActionButton onClick={() => setShowTestEmailModal(true)}>
<RiSendPlane2Line className="h-4 w-4" />
</ActionButton>
<ActionButton onClick={() => setShowEmailModal(true)}>
<RiEqualizer2Line className="h-4 w-4" />
</ActionButton>
</>
)}
<div
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<RiDeleteBinLine className="h-4 w-4" />
</ActionButton>
<ActionButton
state={isHovering ? ActionButtonState.Destructive : ActionButtonState.Default}
onClick={() => onDelete(method.type)}
>
<RiDeleteBinLine className="h-4 w-4" />
</ActionButton>
</div>
</div>
</div>
)}
{(method.config || method.type === DeliveryMethodType.WebApp) && (
<Switch
defaultValue={method.enabled}
onChange={handleEnableStatusChange}
disabled={readonly}
/>
)}
{method.type === DeliveryMethodType.Email && !method.config && (
@ -116,6 +121,7 @@ const DeliveryMethodItem: React.FC<Props> = ({
className="-mr-1"
size="small"
onClick={() => setShowEmailModal(true)}
disabled={readonly}
>
{t(`${i18nPrefix}.deliveryMethod.notConfigured`, { ns: 'workflow' })}
<Indicator color="orange" className="ml-1" />

View File

@ -27,6 +27,7 @@ type FormContentProps = {
isExpand: boolean
availableVars: NodeOutPutVar[]
availableNodes: Node[]
readonly?: boolean
}
const Key: FC<{ children: React.ReactNode, className?: string }> = ({ children, className }) => {
@ -49,6 +50,7 @@ const FormContent: FC<FormContentProps> = ({
isExpand,
availableVars,
availableNodes,
readonly,
}) => {
const { t } = useTranslation()
@ -122,6 +124,7 @@ const FormContent: FC<FormContentProps> = ({
variables: availableVars || [],
workflowNodesMap,
getVarType,
readonly,
}}
workflowVariableBlock={{
show: true,
@ -129,17 +132,19 @@ const FormContent: FC<FormContentProps> = ({
getVarType: getVarType as any,
workflowNodesMap,
}}
editable
shortcutPopups={[{
hotkey: ['mod', '/'],
Popup: ({ onClose, onInsert }) => (
<AddInputField
nodeId={nodeId}
onSave={handleInsertHITLNode(onInsert!)}
onCancel={onClose}
/>
),
}]}
editable={!readonly}
shortcutPopups={readonly
? []
: [{
hotkey: ['mod', '/'],
Popup: ({ onClose, onInsert }) => (
<AddInputField
nodeId={nodeId}
onSave={handleInsertHITLNode(onInsert!)}
onCancel={onClose}
/>
),
}]}
/>
{isFocus && (
<div className="system-xs-regular flex h-8 shrink-0 items-center px-3 text-components-input-text-placeholder">

View File

@ -10,12 +10,14 @@ type Props = {
timeout: number
unit: 'day' | 'hour'
onChange: (state: { timeout: number, unit: 'day' | 'hour' }) => void
readonly?: boolean
}
const TimeoutInput: FC<Props> = ({
timeout,
unit,
onChange,
readonly,
}) => {
const { t } = useTranslation()
@ -34,23 +36,28 @@ const TimeoutInput: FC<Props> = ({
value={timeout}
min={1}
onChange={handleValueChange}
disabled={readonly}
/>
<div className="flex items-center gap-0.5 rounded-[10px] bg-components-segmented-control-bg-normal p-0.5">
<div
className={cn(
'cursor-pointer rounded-lg px-2 py-1 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
unit === 'day' && 'bg-components-segmented-control-item-active-bg text-text-accent-light-mode-only shadow-sm hover:bg-components-segmented-control-item-active-bg hover:text-text-accent-light-mode-only',
'rounded-lg px-2 py-1 text-text-tertiary',
!readonly && 'cursor-pointer hover:bg-state-base-hover hover:text-text-secondary',
unit === 'day' && 'bg-components-segmented-control-item-active-bg text-text-accent-light-mode-only shadow-sm',
!readonly && unit === 'day' && 'hover:bg-components-segmented-control-item-active-bg hover:text-text-accent-light-mode-only',
)}
onClick={() => onChange({ timeout, unit: 'day' })}
onClick={() => !readonly && onChange({ timeout, unit: 'day' })}
>
<div className="system-sm-medium p-0.5">{t(`${i18nPrefix}.timeout.days`, { ns: 'workflow' })}</div>
</div>
<div
className={cn(
'cursor-pointer rounded-lg px-2 py-1 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
unit === 'hour' && 'bg-components-segmented-control-item-active-bg text-text-accent-light-mode-only shadow-sm hover:bg-components-segmented-control-item-active-bg hover:text-text-accent-light-mode-only',
'rounded-lg px-2 py-1 text-text-tertiary',
!readonly && 'cursor-pointer hover:bg-state-base-hover hover:text-text-secondary',
unit === 'hour' && 'bg-components-segmented-control-item-active-bg text-text-accent-light-mode-only shadow-sm',
!readonly && unit === 'hour' && 'hover:bg-components-segmented-control-item-active-bg hover:text-text-accent-light-mode-only',
)}
onClick={() => onChange({ timeout, unit: 'hour' })}
onClick={() => !readonly && onChange({ timeout, unit: 'hour' })}
>
<div className="system-sm-medium p-0.5">{t(`${i18nPrefix}.timeout.hours`, { ns: 'workflow' })}</div>
</div>

View File

@ -16,12 +16,14 @@ type UserActionItemProps = {
data: UserAction
onChange: (state: UserAction) => void
onDelete: (id: string) => void
readonly?: boolean
}
const UserActionItem: FC<UserActionItemProps> = ({
data,
onChange,
onDelete,
readonly,
}) => {
const { t } = useTranslation()
@ -47,6 +49,7 @@ const UserActionItem: FC<UserActionItemProps> = ({
value={data.id}
placeholder={t(`${i18nPrefix}.userActions.actionNamePlaceholder`, { ns: 'workflow' })}
onChange={handleIDChange}
disabled={readonly}
/>
</div>
<div className="grow">
@ -54,20 +57,24 @@ const UserActionItem: FC<UserActionItemProps> = ({
value={data.title}
placeholder={t(`${i18nPrefix}.userActions.buttonTextPlaceholder`, { ns: 'workflow' })}
onChange={handleTextChange}
disabled={readonly}
/>
</div>
<ButtonStyleDropdown
text={data.title}
data={data.button_style}
onChange={type => onChange({ ...data, button_style: type })}
readonly={readonly}
/>
<Button
className="px-2"
variant="tertiary"
onClick={() => onDelete(data.id)}
>
<RiDeleteBinLine className="h-4 w-4" />
</Button>
{!readonly && (
<Button
className="px-2"
variant="tertiary"
onClick={() => onDelete(data.id)}
>
<RiDeleteBinLine className="h-4 w-4" />
</Button>
)}
</div>
)
}

View File

@ -40,6 +40,7 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
}) => {
const { t } = useTranslation()
const {
readOnly,
inputs,
handleDeliveryMethodChange,
handleUserActionAdd,
@ -82,6 +83,7 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
onChange={handleDeliveryMethodChange}
readonly={readOnly}
/>
<div className="px-4 py-2">
<Divider className="!my-0 !h-px !bg-divider-subtle" />
@ -95,35 +97,37 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
popupContent={t(`${i18nPrefix}.formContent.tooltip`, { ns: 'workflow' })}
/>
</div>
<div className="flex items-center ">
<Button
variant="ghost"
size="small"
className={cn(
'flex items-center space-x-1 px-2',
isPreview && 'bg-state-accent-active text-text-accent',
)}
onClick={togglePreview}
>
<RiEyeLine className="size-3.5" />
<div className="system-xs-medium">{t(`${i18nPrefix}.formContent.preview`, { ns: 'workflow' })}</div>
</Button>
<div className="mx-2 h-3 w-px bg-divider-regular"></div>
<div className="flex items-center space-x-1">
<div
className="flex size-6 cursor-pointer items-center justify-center rounded-md hover:bg-components-button-ghost-bg-hover"
onClick={() => {
copy(inputs.form_content)
Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) })
}}
{!readOnly && (
<div className="flex items-center ">
<Button
variant="ghost"
size="small"
className={cn(
'flex items-center space-x-1 px-2',
isPreview && 'bg-state-accent-active text-text-accent',
)}
onClick={togglePreview}
>
<RiClipboardLine className="h-4 w-4 text-text-secondary" />
</div>
<div className={cn('flex size-6 cursor-pointer items-center justify-center rounded-md text-text-secondary hover:bg-components-button-ghost-bg-hover', isExpandFormContent && 'bg-state-accent-active text-text-accent')} onClick={toggleExpandFormContent}>
{isExpandFormContent ? <RiCollapseDiagonalLine className="h-4 w-4" /> : <RiExpandDiagonalLine className="h-4 w-4" />}
<RiEyeLine className="size-3.5" />
<div className="system-xs-medium">{t(`${i18nPrefix}.formContent.preview`, { ns: 'workflow' })}</div>
</Button>
<div className="mx-2 h-3 w-px bg-divider-regular"></div>
<div className="flex items-center space-x-1">
<div
className="flex size-6 cursor-pointer items-center justify-center rounded-md hover:bg-components-button-ghost-bg-hover"
onClick={() => {
copy(inputs.form_content)
Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) })
}}
>
<RiClipboardLine className="h-4 w-4 text-text-secondary" />
</div>
<div className={cn('flex size-6 cursor-pointer items-center justify-center rounded-md text-text-secondary hover:bg-components-button-ghost-bg-hover', isExpandFormContent && 'bg-state-accent-active text-text-accent')} onClick={toggleExpandFormContent}>
{isExpandFormContent ? <RiCollapseDiagonalLine className="h-4 w-4" /> : <RiExpandDiagonalLine className="h-4 w-4" />}
</div>
</div>
</div>
</div>
)}
</div>
<FormContent
editorKey={editorKey}
@ -137,6 +141,7 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
isExpand={isExpandFormContent}
availableVars={availableVars}
availableNodes={availableNodesWithParent}
readonly={readOnly}
/>
</div>
{/* user actions */}
@ -148,19 +153,21 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
popupContent={t(`${i18nPrefix}.userActions.tooltip`, { ns: 'workflow' })}
/>
</div>
<div className="flex items-center px-1">
<ActionButton
onClick={() => {
handleUserActionAdd({
id: genActionId(),
title: 'Button Text',
button_style: UserActionButtonType.Default,
})
}}
>
<RiAddLine className="h-4 w-4" />
</ActionButton>
</div>
{!readOnly && (
<div className="flex items-center px-1">
<ActionButton
onClick={() => {
handleUserActionAdd({
id: genActionId(),
title: 'Button Text',
button_style: UserActionButtonType.Default,
})
}}
>
<RiAddLine className="h-4 w-4" />
</ActionButton>
</div>
)}
</div>
{!inputs.user_actions.length && (
<div className="system-xs-regular flex items-center justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary">{t(`${i18nPrefix}.userActions.emptyTip`, { ns: 'workflow' })}</div>
@ -173,6 +180,7 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
data={action}
onChange={data => handleUserActionChange(index, data)}
onDelete={handleUserActionDelete}
readonly={readOnly}
/>
))}
</div>
@ -188,6 +196,7 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
timeout={inputs.timeout}
unit={inputs.timeout_unit}
onChange={handleTimeoutChange}
readonly={readOnly}
/>
</div>
{/* output vars */}