fix: made button disabled state more congruent, made edit flow details submit on enter (#8339)

* Changed textarea classes

* Changed flowsettingscomponent to use form

* changed edit flow settings to use form and to submit on enter

* Reset form data on close

* Updated disabled state to have lower opacity instead of to have set background

* Fixed loading state of button
This commit is contained in:
Lucas Oliveira 2025-06-11 12:23:16 -03:00 • committed by GitHub
commit 97dd5b361c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 135 additions and 83 deletions

View file

@ -197,7 +197,10 @@ export const MenuBar = memo((): JSX.Element => {
sideOffset={15} sideOffset={15}
> >
<span className="text-sm font-semibold">Flow Details</span> <span className="text-sm font-semibold">Flow Details</span>
<FlowSettingsComponent close={() => setOpenSettings(false)} /> <FlowSettingsComponent
close={() => setOpenSettings(false)}
open={openSettings}
/>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
) : ( ) : (

View file

@ -1,11 +1,13 @@
import React, { ChangeEvent, useState } from "react"; import * as Form from "@radix-ui/react-form";
import React, { useState } from "react";
import { InputProps } from "../../../types/components"; import { InputProps } from "../../../types/components";
import { cn } from "../../../utils/utils"; import { cn } from "../../../utils/utils";
import { Input } from "../../ui/input"; import { Input } from "../../ui/input";
import { Label } from "../../ui/label";
import { Textarea } from "../../ui/textarea"; import { Textarea } from "../../ui/textarea";
export const EditFlowSettings: React.FC<InputProps> = ({ export const EditFlowSettings: React.FC<
InputProps & { submitForm?: () => void }
> = ({
name, name,
invalidNameList = [], invalidNameList = [],
description, description,
@ -14,13 +16,14 @@ export const EditFlowSettings: React.FC<InputProps> = ({
minLength = 1, minLength = 1,
setName, setName,
setDescription, setDescription,
}: InputProps): JSX.Element => { submitForm,
}: InputProps & { submitForm?: () => void }): JSX.Element => {
const [isMaxLength, setIsMaxLength] = useState(false); const [isMaxLength, setIsMaxLength] = useState(false);
const [isMaxDescriptionLength, setIsMaxDescriptionLength] = useState(false); const [isMaxDescriptionLength, setIsMaxDescriptionLength] = useState(false);
const [isMinLength, setIsMinLength] = useState(false); const [isMinLength, setIsMinLength] = useState(false);
const [isInvalidName, setIsInvalidName] = useState(false); const [isInvalidName, setIsInvalidName] = useState(false);
const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => { const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target; const { value } = event.target;
if (value.length >= maxLength) { if (value.length >= maxLength) {
setIsMaxLength(true); setIsMaxLength(true);
@ -41,16 +44,15 @@ export const EditFlowSettings: React.FC<InputProps> = ({
invalid = false; invalid = false;
} }
setIsInvalidName(invalid); setIsInvalidName(invalid);
setName!(value); setName!(value);
if (value.length === 0) { if (value.length === 0) {
// For empty string, update state but keep isMinLength true
setIsMinLength(true); setIsMinLength(true);
} }
}; };
const handleDescriptionChange = (event: ChangeEvent<HTMLTextAreaElement>) => { const handleDescriptionChange = (
event: React.ChangeEvent<HTMLTextAreaElement>,
) => {
const { value } = event.target; const { value } = event.target;
if (value.length >= descriptionMaxLength) { if (value.length >= descriptionMaxLength) {
setIsMaxDescriptionLength(true); setIsMaxDescriptionLength(true);
@ -60,14 +62,25 @@ export const EditFlowSettings: React.FC<InputProps> = ({
setDescription!(value); setDescription!(value);
}; };
//this function is necessary to select the text when double clicking, this was not working with the onFocus event const handleDescriptionKeyDown = (
event: React.KeyboardEvent<HTMLTextAreaElement>,
) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
if (submitForm) submitForm();
}
// else allow default (newline)
};
const handleFocus = (event) => event.target.select(); const handleFocus = (event) => event.target.select();
return ( return (
<> <>
<Label> <Form.Field name="name">
<div className="edit-flow-arrangement"> <div className="edit-flow-arrangement">
<span className="text-mmd font-medium">Name{setName ? "" : ":"}</span>{" "} <Form.Label className="text-mmd font-medium">
Name{setName ? "" : ":"}
</Form.Label>
{isMaxLength && ( {isMaxLength && (
<span className="edit-flow-span">Character limit reached</span> <span className="edit-flow-span">Character limit reached</span>
)} )}
@ -81,52 +94,63 @@ export const EditFlowSettings: React.FC<InputProps> = ({
)} )}
</div> </div>
{setName ? ( {setName ? (
<Input <Form.Control asChild>
className="nopan nodelete nodrag noflow mt-2 font-normal" <Input
onChange={handleNameChange} className="nopan nodelete nodrag noflow mt-2 font-normal"
type="text" onChange={handleNameChange}
name="name" type="text"
value={name ?? ""} name="name"
placeholder="Flow name" value={name ?? ""}
id="name" placeholder="Flow name"
maxLength={maxLength} id="name"
minLength={minLength} maxLength={maxLength}
required={true} minLength={minLength}
onDoubleClickCapture={(event) => { required={true}
handleFocus(event); onDoubleClickCapture={handleFocus}
}} data-testid="input-flow-name"
data-testid="input-flow-name" autoFocus
/> />
</Form.Control>
) : ( ) : (
<span className="font-normal text-muted-foreground word-break-break-word"> <span className="font-normal text-muted-foreground word-break-break-word">
{name} {name}
</span> </span>
)} )}
</Label> <Form.Message match="valueMissing" className="field-invalid">
<Label> Please enter a name
</Form.Message>
<Form.Message
match={(value) => !!(value && invalidNameList.includes(value))}
className="field-invalid"
>
Flow name already exists
</Form.Message>
</Form.Field>
<Form.Field name="description">
<div className="edit-flow-arrangement mt-3"> <div className="edit-flow-arrangement mt-3">
<span className="text-mmd font-medium"> <Form.Label className="text-mmd font-medium">
Description{setDescription ? "" : ":"} Description{setDescription ? "" : ":"}
</span> </Form.Label>
{isMaxDescriptionLength && ( {isMaxDescriptionLength && (
<span className="edit-flow-span">Character limit reached</span> <span className="edit-flow-span">Character limit reached</span>
)} )}
</div> </div>
{setDescription ? ( {setDescription ? (
<Textarea <Form.Control asChild>
name="description" <Textarea
id="description" name="description"
onChange={handleDescriptionChange} id="description"
value={description!} onChange={handleDescriptionChange}
placeholder="Flow description" value={description!}
data-testid="input-flow-description" placeholder="Flow description"
className="mt-2 max-h-[250px] resize-none font-normal" data-testid="input-flow-description"
rows={5} className="mt-2 max-h-[250px] resize-none font-normal"
maxLength={descriptionMaxLength} rows={5}
onDoubleClickCapture={(event) => { maxLength={descriptionMaxLength}
handleFocus(event); onDoubleClickCapture={handleFocus}
}} onKeyDown={handleDescriptionKeyDown}
/> />
</Form.Control>
) : ( ) : (
<div <div
className={cn( className={cn(
@ -137,7 +161,10 @@ export const EditFlowSettings: React.FC<InputProps> = ({
{description === "" ? "No description" : description} {description === "" ? "No description" : description}
</div> </div>
)} )}
</Label> <Form.Message match="valueMissing" className="field-invalid">
Please enter a description
</Form.Message>
</Form.Field>
</> </>
); );
}; };

View file

@ -4,16 +4,19 @@ import useAlertStore from "@/stores/alertStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore"; import useFlowsManagerStore from "@/stores/flowsManagerStore";
import useFlowStore from "@/stores/flowStore"; import useFlowStore from "@/stores/flowStore";
import { FlowType } from "@/types/flow"; import { FlowType } from "@/types/flow";
import * as Form from "@radix-ui/react-form";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import EditFlowSettings from "../editFlowSettingsComponent"; import EditFlowSettings from "../editFlowSettingsComponent";
export default function FlowSettingsComponent({ export default function FlowSettingsComponent({
flowData, flowData,
close, close,
open,
}: { }: {
flowData?: FlowType; flowData?: FlowType;
close: () => void; close: () => void;
open: boolean;
}): JSX.Element { }): JSX.Element {
const saveFlow = useSaveFlow(); const saveFlow = useSaveFlow();
const currentFlow = useFlowStore((state) => const currentFlow = useFlowStore((state) =>
@ -28,13 +31,15 @@ export default function FlowSettingsComponent({
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [disableSave, setDisableSave] = useState(true); const [disableSave, setDisableSave] = useState(true);
const autoSaving = useFlowsManagerStore((state) => state.autoSaving); const autoSaving = useFlowsManagerStore((state) => state.autoSaving);
const formRef = useRef<HTMLFormElement>(null);
useEffect(() => { useEffect(() => {
setName(flow?.name ?? ""); setName(flow?.name ?? "");
setDescription(flow?.description ?? ""); setDescription(flow?.description ?? "");
}, [flow?.name, flow?.description, flow?.endpoint_name, open]); }, [flow?.name, flow?.description, flow?.endpoint_name, open]);
function handleClick(): void { function handleSubmit(event?: React.FormEvent<HTMLFormElement>): void {
if (event) event.preventDefault();
setIsSaving(true); setIsSaving(true);
if (!flow) return; if (!flow) return;
const newFlow = cloneDeep(flow); const newFlow = cloneDeep(flow);
@ -58,6 +63,10 @@ export default function FlowSettingsComponent({
} }
} }
const submitForm = () => {
formRef.current?.requestSubmit();
};
const [nameLists, setNameList] = useState<string[]>([]); const [nameLists, setNameList] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
@ -81,36 +90,41 @@ export default function FlowSettingsComponent({
} }
}, [nameLists, flow, description, name]); }, [nameLists, flow, description, name]);
return ( return (
<div className="flex flex-col gap-4"> <Form.Root onSubmit={handleSubmit} ref={formRef}>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-4">
<EditFlowSettings <div className="flex flex-col gap-2">
invalidNameList={nameLists} <EditFlowSettings
name={name} invalidNameList={nameLists}
description={description} name={name}
setName={setName} description={description}
setDescription={setDescription} setName={setName}
/> setDescription={setDescription}
submitForm={submitForm}
/>
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
data-testid="cancel-flow-settings"
type="button"
onClick={() => close()}
>
Cancel
</Button>
<Form.Submit asChild>
<Button
variant="default"
size="sm"
data-testid="save-flow-settings"
loading={isSaving}
disabled={disableSave}
>
Save
</Button>
</Form.Submit>
</div>
</div> </div>
<div className="flex justify-end gap-2"> </Form.Root>
<Button
variant="outline"
size="sm"
data-testid="cancel-flow-settings"
onClick={() => close()}
>
Cancel
</Button>
<Button
variant="default"
size="sm"
data-testid="save-flow-settings"
onClick={handleClick}
loading={isSaving}
disabled={disableSave}
>
Save
</Button>
</div>
</div>
); );
} }

View file

@ -5,7 +5,7 @@ import { cn } from "../../utils/utils";
import ForwardedIconComponent from "../common/genericIconComponent"; import ForwardedIconComponent from "../common/genericIconComponent";
const buttonVariants = cva( const buttonVariants = cva(
"noflow nopan nodelete nodrag inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-100 disabled:disabled-state [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "noflow nopan nodelete nodrag inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-70 disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{ {
variants: { variants: {
variant: { variant: {
@ -105,7 +105,14 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
> >
{loading ? ( {loading ? (
<span className="relative flex items-center justify-center"> <span className="relative flex items-center justify-center">
<span className="invisible">{newChildren}</span> <span
className={cn(
className,
"invisible flex items-center justify-center gap-2 !p-0",
)}
>
{newChildren}
</span>
<span className="absolute inset-0 flex items-center justify-center"> <span className="absolute inset-0 flex items-center justify-center">
<ForwardedIconComponent <ForwardedIconComponent
name={"Loader2"} name={"Loader2"}

View file

@ -22,6 +22,7 @@ export default function FlowSettingsModal({
<FlowSettingsComponent <FlowSettingsComponent
flowData={flowData} flowData={flowData}
close={() => setOpen(false)} close={() => setOpen(false)}
open={open}
/> />
</BaseModal.Content> </BaseModal.Content>
</BaseModal> </BaseModal>

View file

@ -191,7 +191,7 @@
/* The same as primary-input but no-truncate */ /* The same as primary-input but no-truncate */
.textarea-primary { .textarea-primary {
@apply form-input block w-full rounded-md border border-border bg-background px-3 text-left shadow-sm placeholder:text-placeholder-foreground hover:border-muted focus:border-muted focus:placeholder-transparent focus:ring-[0.75px] focus:ring-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-secondary disabled:text-muted disabled:opacity-100 placeholder:disabled:text-muted-foreground sm:text-sm; @apply form-input block w-full rounded-md border border-border bg-background px-3 text-left text-sm placeholder:text-muted-foreground hover:border-muted-foreground focus:border-foreground focus:placeholder-transparent focus:ring-0 focus:ring-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground disabled:opacity-100 placeholder:disabled:text-muted-foreground;
} }
.input-edit-node { .input-edit-node {