mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
feat(hitl-input): add readonly prop to HITL input components for enhanced user interaction control
This commit is contained in:
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user