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:
parent
4cbce28d26
commit
97dd5b361c
6 changed files with 135 additions and 83 deletions
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue