mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-07 22:33:57 -05:00
feat(frontend): add expandable text input modal for better editing experience (#11510)
Text inputs in the form builder can be difficult to edit when dealing with longer content. Users need a way to expand text inputs into a larger, more comfortable editing interface, especially for multi-line text, passwords, and longer string values. https://github.com/user-attachments/assets/443bf4eb-c77c-4bf6-b34c-77091e005c6d ### Changes 🏗️ - **Added `InputExpanderModal` component**: A new modal component that provides a larger textarea (300px min-height) for editing text inputs with the following features: - Copy-to-clipboard functionality with visual feedback (checkmark icon) - Toast notification on successful copy - Auto-focus on open for better UX - Proper state management to reset values when modal opens/closes - **Enhanced `TextInputWidget`**: - Added expand button (ArrowsOutIcon) with tooltip for text, password, and textarea input types - Button appears inline next to the input field - Integrated the new `InputExpanderModal` component - Improved layout with flexbox to accommodate the expand button - Added padding-right to input when expand button is visible to prevent text overlap - **Refactored file structure**: - Moved `TextInputWidget.tsx` into `TextInputWidget/` directory - Updated import path in `widgets/index.ts` - **UX improvements**: - Expand button only shows for applicable input types (text, password, textarea) - Number and integer inputs don't show expand button (not needed) - Modal preserves schema title, description, and placeholder for context ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Test expand button appears for text input fields - [x] Test expand button appears for password input fields - [x] Test expand button appears for textarea fields - [x] Test expand button does NOT appear for number/integer inputs - [x] Test clicking expand button opens modal with current value - [x] Test editing text in modal and saving updates the input field - [x] Test cancel button closes modal without saving changes - [x] Test copy-to-clipboard button copies text and shows success state - [x] Test toast notification appears on successful copy - [x] Test modal resets to original value when reopened - [x] Test modal auto-focuses textarea on open - [x] Test expand button tooltip displays correctly - [x] Test input field layout with expand button (no text overlap)
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { CheckIcon, CopyIcon } from "@phosphor-icons/react";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
|
||||
interface InputExpanderModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (value: string) => void;
|
||||
title?: string;
|
||||
defaultValue: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const InputExpanderModal: FC<InputExpanderModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
title,
|
||||
defaultValue,
|
||||
description,
|
||||
placeholder,
|
||||
}) => {
|
||||
const [tempValue, setTempValue] = useState(defaultValue);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setTempValue(defaultValue);
|
||||
setIsCopied(false);
|
||||
}
|
||||
}, [isOpen, defaultValue]);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(tempValue);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const copyValue = () => {
|
||||
navigator.clipboard.writeText(tempValue).then(() => {
|
||||
setIsCopied(true);
|
||||
toast({
|
||||
title: "Copied to clipboard!",
|
||||
duration: 2000,
|
||||
});
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
controlled={{
|
||||
isOpen,
|
||||
set: async (open) => {
|
||||
if (!open) onClose();
|
||||
},
|
||||
}}
|
||||
onClose={onClose}
|
||||
styling={{ maxWidth: "600px", minWidth: "600px" }}
|
||||
>
|
||||
<Dialog.Content>
|
||||
<div className="flex flex-col gap-4 px-1">
|
||||
<Text variant="h4" className="text-slate-900">
|
||||
{title || "Edit Text"}
|
||||
</Text>
|
||||
<Text variant="body">{description}</Text>
|
||||
<Input
|
||||
type="textarea"
|
||||
label=""
|
||||
hideLabel
|
||||
id="input-expander-modal"
|
||||
value={tempValue}
|
||||
className="!min-h-[300px] rounded-2xlarge"
|
||||
onChange={(e) => setTempValue(e.target.value)}
|
||||
placeholder={placeholder || "Enter text..."}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={copyValue}
|
||||
className={cn(
|
||||
"h-fit min-w-0 gap-1.5 border border-zinc-200 p-2 text-black hover:text-slate-900",
|
||||
isCopied &&
|
||||
"border-green-400 bg-green-100 hover:border-green-400 hover:bg-green-200",
|
||||
)}
|
||||
>
|
||||
{isCopied ? (
|
||||
<CheckIcon size={16} className="text-green-600" />
|
||||
) : (
|
||||
<CopyIcon size={16} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="secondary" size="small" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="small" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
import {
|
||||
InputType,
|
||||
mapJsonSchemaTypeToInputType,
|
||||
} from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { BlockUIType } from "@/lib/autogpt-server-api/types";
|
||||
import { InputExpanderModal } from "./InputExpanderModal";
|
||||
import { ArrowsOutIcon } from "@phosphor-icons/react";
|
||||
|
||||
export const TextInputWidget = (props: WidgetProps) => {
|
||||
const { schema, formContext } = props;
|
||||
@@ -13,6 +24,8 @@ export const TextInputWidget = (props: WidgetProps) => {
|
||||
size?: string;
|
||||
};
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const mapped = mapJsonSchemaTypeToInputType(schema);
|
||||
|
||||
type InputConfig = {
|
||||
@@ -59,9 +72,25 @@ export const TextInputWidget = (props: WidgetProps) => {
|
||||
return props.onChange(config.handleChange(v));
|
||||
};
|
||||
|
||||
const handleModalSave = (value: string) => {
|
||||
props.onChange(config.handleChange(value));
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleModalOpen = () => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
// Determine input size based on context
|
||||
const inputSize = size === "large" ? "medium" : "small";
|
||||
|
||||
// Check if this input type should show the expand button
|
||||
// Show for text and password types, not for number/integer
|
||||
const showExpandButton =
|
||||
config.htmlType === "text" ||
|
||||
config.htmlType === "password" ||
|
||||
config.htmlType === "textarea";
|
||||
|
||||
if (uiType === BlockUIType.NOTE) {
|
||||
return (
|
||||
<Input
|
||||
@@ -82,18 +111,49 @@ export const TextInputWidget = (props: WidgetProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
id={props.id}
|
||||
hideLabel={true}
|
||||
type={config.htmlType as any}
|
||||
label={""}
|
||||
size={inputSize as any}
|
||||
wrapperClassName="mb-0"
|
||||
value={props.value ?? ""}
|
||||
onChange={handleChange}
|
||||
placeholder={schema.placeholder || config.placeholder}
|
||||
required={props.required}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
<>
|
||||
<div className="nodrag relative flex items-center gap-2">
|
||||
<Input
|
||||
id={props.id}
|
||||
hideLabel={true}
|
||||
type={config.htmlType as any}
|
||||
label={""}
|
||||
size={inputSize as any}
|
||||
wrapperClassName="mb-0 flex-1"
|
||||
value={props.value ?? ""}
|
||||
onChange={handleChange}
|
||||
placeholder={schema.placeholder || config.placeholder}
|
||||
required={props.required}
|
||||
disabled={props.disabled}
|
||||
className={showExpandButton ? "pr-8" : ""}
|
||||
/>
|
||||
{showExpandButton && (
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleModalOpen}
|
||||
type="button"
|
||||
className="p-1"
|
||||
>
|
||||
<ArrowsOutIcon className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Expand input</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<InputExpanderModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
onSave={handleModalSave}
|
||||
title={schema.title || "Edit value"}
|
||||
description={schema.description || ""}
|
||||
defaultValue={props.value ?? ""}
|
||||
placeholder={schema.placeholder || config.placeholder}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RegistryWidgetsType } from "@rjsf/utils";
|
||||
import { SelectWidget } from "./SelectWidget";
|
||||
import { TextInputWidget } from "./TextInputWidget";
|
||||
import { TextInputWidget } from "./TextInputWidget/TextInputWidget";
|
||||
import { SwitchWidget } from "./SwitchWidget";
|
||||
import { FileWidget } from "./FileWidget";
|
||||
import { DateInputWidget } from "./DateInputWidget";
|
||||
|
||||
Reference in New Issue
Block a user