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}
>
<span className="text-sm font-semibold">Flow Details</span>
<FlowSettingsComponent close={() => setOpenSettings(false)} />
<FlowSettingsComponent
close={() => setOpenSettings(false)}
open={openSettings}
/>
</PopoverContent>
</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 { cn } from "../../../utils/utils";
import { Input } from "../../ui/input";
import { Label } from "../../ui/label";
import { Textarea } from "../../ui/textarea";
export const EditFlowSettings: React.FC<InputProps> = ({
export const EditFlowSettings: React.FC<
InputProps & { submitForm?: () => void }
> = ({
name,
invalidNameList = [],
description,
@ -14,13 +16,14 @@ export const EditFlowSettings: React.FC<InputProps> = ({
minLength = 1,
setName,
setDescription,
}: InputProps): JSX.Element => {
submitForm,
}: InputProps & { submitForm?: () => void }): JSX.Element => {
const [isMaxLength, setIsMaxLength] = useState(false);
const [isMaxDescriptionLength, setIsMaxDescriptionLength] = useState(false);
const [isMinLength, setIsMinLength] = useState(false);
const [isInvalidName, setIsInvalidName] = useState(false);
const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
if (value.length >= maxLength) {
setIsMaxLength(true);
@ -41,16 +44,15 @@ export const EditFlowSettings: React.FC<InputProps> = ({
invalid = false;
}
setIsInvalidName(invalid);
setName!(value);
if (value.length === 0) {
// For empty string, update state but keep isMinLength true
setIsMinLength(true);
}
};
const handleDescriptionChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
const handleDescriptionChange = (
event: React.ChangeEvent<HTMLTextAreaElement>,
) => {
const { value } = event.target;
if (value.length >= descriptionMaxLength) {
setIsMaxDescriptionLength(true);
@ -60,14 +62,25 @@ export const EditFlowSettings: React.FC<InputProps> = ({
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();
return (
<>
<Label>
<Form.Field name="name">
<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 && (
<span className="edit-flow-span">Character limit reached</span>
)}
@ -81,52 +94,63 @@ export const EditFlowSettings: React.FC<InputProps> = ({
)}
</div>
{setName ? (
<Input
className="nopan nodelete nodrag noflow mt-2 font-normal"
onChange={handleNameChange}
type="text"
name="name"
value={name ?? ""}
placeholder="Flow name"
id="name"
maxLength={maxLength}
minLength={minLength}
required={true}
onDoubleClickCapture={(event) => {
handleFocus(event);
}}
data-testid="input-flow-name"
/>
<Form.Control asChild>
<Input
className="nopan nodelete nodrag noflow mt-2 font-normal"
onChange={handleNameChange}
type="text"
name="name"
value={name ?? ""}
placeholder="Flow name"
id="name"
maxLength={maxLength}
minLength={minLength}
required={true}
onDoubleClickCapture={handleFocus}
data-testid="input-flow-name"
autoFocus
/>
</Form.Control>
) : (
<span className="font-normal text-muted-foreground word-break-break-word">
{name}
</span>
)}
</Label>
<Label>
<Form.Message match="valueMissing" className="field-invalid">
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">
<span className="text-mmd font-medium">
<Form.Label className="text-mmd font-medium">
Description{setDescription ? "" : ":"}
</span>
</Form.Label>
{isMaxDescriptionLength && (
<span className="edit-flow-span">Character limit reached</span>
)}
</div>
{setDescription ? (
<Textarea
name="description"
id="description"
onChange={handleDescriptionChange}
value={description!}
placeholder="Flow description"
data-testid="input-flow-description"
className="mt-2 max-h-[250px] resize-none font-normal"
rows={5}
maxLength={descriptionMaxLength}
onDoubleClickCapture={(event) => {
handleFocus(event);
}}
/>
<Form.Control asChild>
<Textarea
name="description"
id="description"
onChange={handleDescriptionChange}
value={description!}
placeholder="Flow description"
data-testid="input-flow-description"
className="mt-2 max-h-[250px] resize-none font-normal"
rows={5}
maxLength={descriptionMaxLength}
onDoubleClickCapture={handleFocus}
onKeyDown={handleDescriptionKeyDown}
/>
</Form.Control>
) : (
<div
className={cn(
@ -137,7 +161,10 @@ export const EditFlowSettings: React.FC<InputProps> = ({
{description === "" ? "No description" : description}
</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 useFlowStore from "@/stores/flowStore";
import { FlowType } from "@/types/flow";
import * as Form from "@radix-ui/react-form";
import { cloneDeep } from "lodash";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import EditFlowSettings from "../editFlowSettingsComponent";
export default function FlowSettingsComponent({
flowData,
close,
open,
}: {
flowData?: FlowType;
close: () => void;
open: boolean;
}): JSX.Element {
const saveFlow = useSaveFlow();
const currentFlow = useFlowStore((state) =>
@ -28,13 +31,15 @@ export default function FlowSettingsComponent({
const [isSaving, setIsSaving] = useState(false);
const [disableSave, setDisableSave] = useState(true);
const autoSaving = useFlowsManagerStore((state) => state.autoSaving);
const formRef = useRef<HTMLFormElement>(null);
useEffect(() => {
setName(flow?.name ?? "");
setDescription(flow?.description ?? "");
}, [flow?.name, flow?.description, flow?.endpoint_name, open]);
function handleClick(): void {
function handleSubmit(event?: React.FormEvent<HTMLFormElement>): void {
if (event) event.preventDefault();
setIsSaving(true);
if (!flow) return;
const newFlow = cloneDeep(flow);
@ -58,6 +63,10 @@ export default function FlowSettingsComponent({
}
}
const submitForm = () => {
formRef.current?.requestSubmit();
};
const [nameLists, setNameList] = useState<string[]>([]);
useEffect(() => {
@ -81,36 +90,41 @@ export default function FlowSettingsComponent({
}
}, [nameLists, flow, description, name]);
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<EditFlowSettings
invalidNameList={nameLists}
name={name}
description={description}
setName={setName}
setDescription={setDescription}
/>
<Form.Root onSubmit={handleSubmit} ref={formRef}>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<EditFlowSettings
invalidNameList={nameLists}
name={name}
description={description}
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 className="flex justify-end gap-2">
<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>
</Form.Root>
);
}

View file

@ -5,7 +5,7 @@ import { cn } from "../../utils/utils";
import ForwardedIconComponent from "../common/genericIconComponent";
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: {
variant: {
@ -105,7 +105,14 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
>
{loading ? (
<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">
<ForwardedIconComponent
name={"Loader2"}

View file

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

View file

@ -191,7 +191,7 @@
/* The same as primary-input but no-truncate */
.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 {