feat: Revamp GlobalVariableModal (#5512)
* 📝 (GlobalVariableModal.tsx): Refactor GlobalVariableModal component to use Tabs component instead of Select for type selection and improve layout and styling of input fields and labels 🔧 (index.tsx): Add popoverWidth prop to InputComponent to allow setting the width of the popover in CustomInputPopover component 🔧 (index.tsx): Add popoverWidth prop to InputComponent in InputGlobalComponent to set the width of the popover to 315px ✨ (tabs-button.tsx): introduce new TabsButton component to handle tab functionality in UI components 📝 (tabs-button.tsx): add documentation for Tabs, TabsContent, TabsList, TabsTrigger components 📝 (components/index.ts): add popoverWidth property to InputComponentType to control the width of the popover in UI components * ✨ (GlobalVariableModal.tsx): Add ForwardedIconComponent to display an icon next to the modal header text for better visual representation 📝 (GlobalVariableModal.tsx): Update TabsTrigger components to include data-testid attribute for testing purposes ✨ (index.tsx): Introduce new components OptionBadge, CommandItemContent, and SelectionIndicator to improve code organization and reusability ♻️ (index.tsx): Refactor handleRemoveOption function to have a more descriptive parameter signature and improve readability 📝 (index.tsx): Add comments to clarify the purpose of handleOptionSelect function and improve code documentation 📝 (index.tsx): Add comments to describe the purpose of getInputClassName and getAnchorClassName functions for better code understanding ✨ (globalVariables.spec.ts): improve placeholder text for variable name and value fields for better user understanding and experience * 🐛 (GlobalVariableModal.tsx): Fix disabled state logic for TabsTrigger components to correctly reflect initialData type 📝 (GlobalVariableModal.tsx): Update label for submit button to dynamically change based on the presence of initialData * 🐛 (index.tsx): Fix issue where options were not being correctly memoized as a Set to prevent unnecessary re-renders. Update memoizedOptions to correctly memoize options as a Set. * 📝 (userSettings.spec.ts): Update placeholder text for variable name and value fields to improve clarity and user experience.
This commit is contained in:
parent
a142b45160
commit
af4fb3774e
8 changed files with 327 additions and 194 deletions
|
|
@ -7,17 +7,10 @@ import getUnavailableFields from "@/stores/globalVariablesStore/utils/get-unavai
|
||||||
import { GlobalVariable } from "@/types/global_variables";
|
import { GlobalVariable } from "@/types/global_variables";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import {
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs-button";
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import BaseModal from "@/modals/baseModal";
|
import BaseModal from "@/modals/baseModal";
|
||||||
import useAlertStore from "@/stores/alertStore";
|
import useAlertStore from "@/stores/alertStore";
|
||||||
import { useTypesStore } from "@/stores/typesStore";
|
import { useTypesStore } from "@/stores/typesStore";
|
||||||
|
|
@ -134,88 +127,89 @@ export default function GlobalVariableModal({
|
||||||
onSubmit={submitForm}
|
onSubmit={submitForm}
|
||||||
disable={disabled}
|
disable={disabled}
|
||||||
>
|
>
|
||||||
<BaseModal.Header
|
<BaseModal.Header description="This variable will be available for use across your flows.">
|
||||||
description={
|
|
||||||
"This variable will be available for you to use in any of your projects."
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="pr-2">
|
|
||||||
{" "}
|
|
||||||
{initialData ? "Update" : "Create"} Variable{" "}
|
|
||||||
</span>
|
|
||||||
<ForwardedIconComponent
|
<ForwardedIconComponent
|
||||||
name="Globe"
|
name="Globe"
|
||||||
className="h-6 w-6 pl-1 text-primary"
|
className="h-6 w-6 pr-1 text-primary"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
{initialData ? "Update Variable" : "Create Variable"}
|
||||||
</BaseModal.Header>
|
</BaseModal.Header>
|
||||||
<BaseModal.Trigger disable={disabled} asChild={asChild}>
|
<BaseModal.Trigger disable={disabled} asChild={asChild}>
|
||||||
{children}
|
{children}
|
||||||
</BaseModal.Trigger>
|
</BaseModal.Trigger>
|
||||||
<BaseModal.Content>
|
<BaseModal.Content>
|
||||||
<div className="flex h-full w-full flex-col gap-4 align-middle">
|
<div className="flex h-full w-full flex-col gap-4">
|
||||||
<Label>Variable Name</Label>
|
<div className="space-y-2">
|
||||||
|
<Label>Type*</Label>
|
||||||
|
<Tabs
|
||||||
|
defaultValue={type}
|
||||||
|
onValueChange={setType}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger
|
||||||
|
disabled={!!initialData?.type}
|
||||||
|
data-testid="credential-tab"
|
||||||
|
value="Credential"
|
||||||
|
>
|
||||||
|
Credential
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
disabled={!!initialData?.type}
|
||||||
|
data-testid="generic-tab"
|
||||||
|
value="Generic"
|
||||||
|
>
|
||||||
|
Generic
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Name*</Label>
|
||||||
<Input
|
<Input
|
||||||
value={key}
|
value={key}
|
||||||
onChange={(e) => {
|
onChange={(e) => setKey(e.target.value)}
|
||||||
setKey(e.target.value);
|
placeholder="Enter a name for the variable..."
|
||||||
}}
|
/>
|
||||||
placeholder="Insert a name for the variable..."
|
</div>
|
||||||
></Input>
|
|
||||||
<Label>Type (optional)</Label>
|
|
||||||
|
|
||||||
<Select
|
<div className="space-y-2">
|
||||||
disabled={disabled}
|
<Label>Value*</Label>
|
||||||
onValueChange={setType}
|
|
||||||
value={type}
|
|
||||||
defaultValue={type}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
className="h-full w-full"
|
|
||||||
data-testid="select-type-global-variables"
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Choose a type for the variable..." />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent id="type-global-variables">
|
|
||||||
{["Generic", "Credential"].map((option) => (
|
|
||||||
<SelectItem key={option} value={option}>
|
|
||||||
{option}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Label>Value</Label>
|
|
||||||
{type === "Credential" ? (
|
{type === "Credential" ? (
|
||||||
<InputComponent
|
<InputComponent
|
||||||
password
|
password
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={(e) => setValue(e)}
|
||||||
setValue(e);
|
placeholder="Enter a value for the variable..."
|
||||||
}}
|
|
||||||
placeholder="Insert a value for the variable..."
|
|
||||||
nodeStyle
|
nodeStyle
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Textarea
|
<Input
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={(e) => setValue(e.target.value)}
|
||||||
setValue(e.target.value);
|
placeholder="Enter a value for the variable..."
|
||||||
}}
|
|
||||||
placeholder="Insert a value for the variable..."
|
|
||||||
className="w-full resize-none custom-scroll"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Label>Apply To Fields (optional)</Label>
|
<div className="space-y-2">
|
||||||
|
<Label>Apply to fields</Label>
|
||||||
<InputComponent
|
<InputComponent
|
||||||
setSelectedOptions={(value) => setFields(value)}
|
setSelectedOptions={(value) => setFields(value)}
|
||||||
selectedOptions={fields}
|
selectedOptions={fields}
|
||||||
options={availableFields}
|
options={availableFields}
|
||||||
password={false}
|
password={false}
|
||||||
placeholder="Choose a field for the variable..."
|
placeholder="Choose a field for the variable..."
|
||||||
id={"apply-to-fields"}
|
id="apply-to-fields"
|
||||||
></InputComponent>
|
popoverWidth="520px"
|
||||||
|
optionsPlaceholder="Fields"
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Selected fields will auto-apply the variable as a default value.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseModal.Content>
|
</BaseModal.Content>
|
||||||
<BaseModal.Footer
|
<BaseModal.Footer
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,129 @@ import {
|
||||||
import { cn } from "@/utils/utils";
|
import { cn } from "@/utils/utils";
|
||||||
import { PopoverAnchor } from "@radix-ui/react-popover";
|
import { PopoverAnchor } from "@radix-ui/react-popover";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { ReactNode, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
const OptionBadge = ({
|
||||||
|
option,
|
||||||
|
onRemove,
|
||||||
|
variant = "emerald",
|
||||||
|
className = "",
|
||||||
|
}: {
|
||||||
|
option: string;
|
||||||
|
variant?:
|
||||||
|
| "default"
|
||||||
|
| "emerald"
|
||||||
|
| "gray"
|
||||||
|
| "secondary"
|
||||||
|
| "destructive"
|
||||||
|
| "outline"
|
||||||
|
| "secondaryStatic"
|
||||||
|
| "pinkStatic"
|
||||||
|
| "successStatic"
|
||||||
|
| "errorStatic";
|
||||||
|
className?: string;
|
||||||
|
onRemove: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
}) => (
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
variant as
|
||||||
|
| "default"
|
||||||
|
| "emerald"
|
||||||
|
| "gray"
|
||||||
|
| "secondary"
|
||||||
|
| "destructive"
|
||||||
|
| "outline"
|
||||||
|
| "secondaryStatic"
|
||||||
|
| "pinkStatic"
|
||||||
|
| "successStatic"
|
||||||
|
| "errorStatic"
|
||||||
|
}
|
||||||
|
className={cn("flex items-center gap-1 truncate", className)}
|
||||||
|
>
|
||||||
|
<div className="truncate">{option}</div>
|
||||||
|
<X
|
||||||
|
className="h-3 w-3 cursor-pointer bg-transparent hover:text-destructive"
|
||||||
|
onClick={(e) =>
|
||||||
|
onRemove(e as unknown as React.MouseEvent<HTMLButtonElement>)
|
||||||
|
}
|
||||||
|
data-testid="remove-icon-badge"
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CommandItemContent = ({
|
||||||
|
option,
|
||||||
|
isSelected,
|
||||||
|
optionButton,
|
||||||
|
}: {
|
||||||
|
option: string;
|
||||||
|
isSelected: boolean;
|
||||||
|
optionButton: (option: string) => ReactNode;
|
||||||
|
}) => (
|
||||||
|
<div className="group flex w-full items-center justify-between">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<SelectionIndicator isSelected={isSelected} />
|
||||||
|
<span className="max-w-52 truncate pr-2">{option}</span>
|
||||||
|
</div>
|
||||||
|
{optionButton && optionButton(option)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SelectionIndicator = ({ isSelected }: { isSelected: boolean }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative mr-2 h-4 w-4",
|
||||||
|
isSelected ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="absolute opacity-100 transition-all group-hover:opacity-0">
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name="Check"
|
||||||
|
className="mr-2 h-4 w-4 text-primary"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute opacity-0 transition-all group-hover:opacity-100">
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name="X"
|
||||||
|
className="mr-2 h-4 w-4 text-status-red"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const getInputClassName = (
|
||||||
|
editNode: boolean,
|
||||||
|
disabled: boolean,
|
||||||
|
password: boolean,
|
||||||
|
selectedOptions: string[],
|
||||||
|
) => {
|
||||||
|
return cn(
|
||||||
|
"popover-input nodrag w-full truncate px-1 pr-4",
|
||||||
|
editNode && "px-2",
|
||||||
|
editNode && disabled && "h-fit w-fit",
|
||||||
|
disabled &&
|
||||||
|
"disabled:text-muted disabled:opacity-100 placeholder:disabled:text-muted-foreground",
|
||||||
|
password && "text-clip pr-14",
|
||||||
|
selectedOptions?.length >= 0 && "cursor-default",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAnchorClassName = (
|
||||||
|
editNode: boolean,
|
||||||
|
disabled: boolean,
|
||||||
|
isFocused: boolean,
|
||||||
|
) => {
|
||||||
|
return cn(
|
||||||
|
"primary-input noflow nopan nodelete nodrag border-1 flex h-full min-h-[2.375rem] cursor-default flex-wrap items-center px-2",
|
||||||
|
editNode && "min-h-7 p-0 px-1",
|
||||||
|
editNode && disabled && "min-h-5 border-muted",
|
||||||
|
disabled && "bg-muted text-muted",
|
||||||
|
isFocused &&
|
||||||
|
"border-foreground ring-1 ring-foreground hover:border-foreground",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const CustomInputPopover = ({
|
const CustomInputPopover = ({
|
||||||
id,
|
id,
|
||||||
|
|
@ -43,16 +165,20 @@ const CustomInputPopover = ({
|
||||||
nodeStyle,
|
nodeStyle,
|
||||||
optionButton,
|
optionButton,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
className,
|
popoverWidth,
|
||||||
}) => {
|
}) => {
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const memoizedOptions = useMemo(() => new Set<string>(options), [options]);
|
||||||
|
|
||||||
const PopoverContentInput = editNode
|
const PopoverContentInput = editNode
|
||||||
? PopoverContent
|
? PopoverContent
|
||||||
: PopoverContentWithoutPortal;
|
: PopoverContentWithoutPortal;
|
||||||
|
|
||||||
const handleRemoveOption = (optionToRemove, e) => {
|
const handleRemoveOption = (
|
||||||
e.stopPropagation(); // Prevent the popover from opening when removing badges
|
optionToRemove: string,
|
||||||
|
e: React.MouseEvent<HTMLButtonElement>,
|
||||||
|
) => {
|
||||||
|
e.stopPropagation();
|
||||||
if (setSelectedOptions) {
|
if (setSelectedOptions) {
|
||||||
setSelectedOptions(
|
setSelectedOptions(
|
||||||
selectedOptions.filter((option) => option !== optionToRemove),
|
selectedOptions.filter((option) => option !== optionToRemove),
|
||||||
|
|
@ -62,58 +188,52 @@ const CustomInputPopover = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOptionSelect = (currentValue: string) => {
|
||||||
|
if (setSelectedOption) {
|
||||||
|
setSelectedOption(currentValue === selectedOption ? "" : currentValue);
|
||||||
|
}
|
||||||
|
if (setSelectedOptions) {
|
||||||
|
setSelectedOptions(
|
||||||
|
selectedOptions?.includes(currentValue)
|
||||||
|
? selectedOptions.filter((item) => item !== currentValue)
|
||||||
|
: [...(selectedOptions || []), currentValue],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
!setSelectedOptions && setShowOptions(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover modal open={showOptions} onOpenChange={setShowOptions}>
|
<Popover modal open={showOptions} onOpenChange={setShowOptions}>
|
||||||
<PopoverAnchor>
|
<PopoverAnchor>
|
||||||
<div
|
<div
|
||||||
data-testid={`anchor-${id}`}
|
data-testid={`anchor-${id}`}
|
||||||
className={cn(
|
className={getAnchorClassName(editNode, disabled, isFocused)}
|
||||||
"primary-input noflow nopan nodelete nodrag border-1 flex h-full min-h-[2.375rem] cursor-default flex-wrap items-center px-2",
|
onClick={() => !nodeStyle && !disabled && setShowOptions(true)}
|
||||||
editNode && "min-h-7 p-0 px-1",
|
|
||||||
editNode && disabled && "min-h-5 border-muted",
|
|
||||||
disabled && "bg-muted text-muted",
|
|
||||||
isFocused &&
|
|
||||||
"border-foreground ring-1 ring-foreground hover:border-foreground",
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
if (!nodeStyle && !disabled) {
|
|
||||||
setShowOptions(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{selectedOptions?.length > 0 ? (
|
{selectedOptions?.length > 0 ? (
|
||||||
selectedOptions.map((option) => (
|
<div className="mr-5 flex flex-wrap gap-2">
|
||||||
<Badge
|
{selectedOptions.map((option) => (
|
||||||
|
<OptionBadge
|
||||||
key={option}
|
key={option}
|
||||||
variant="secondary"
|
option={option}
|
||||||
className="m-[1px] flex items-center gap-1 truncate px-1"
|
onRemove={(e) => handleRemoveOption(option, e)}
|
||||||
>
|
className="rounded-[3px] p-1 font-mono"
|
||||||
<div className="truncate">{option}</div>
|
|
||||||
<X
|
|
||||||
className="h-3 w-3 cursor-pointer bg-transparent hover:text-destructive"
|
|
||||||
onClick={(e) => handleRemoveOption(option, e)}
|
|
||||||
/>
|
/>
|
||||||
</Badge>
|
))}
|
||||||
))
|
</div>
|
||||||
) : selectedOption?.length > 0 ? (
|
) : selectedOption?.length > 0 ? (
|
||||||
<Badge
|
<OptionBadge
|
||||||
|
option={selectedOption}
|
||||||
|
onRemove={(e) => handleRemoveOption(selectedOption, e)}
|
||||||
variant={nodeStyle ? "emerald" : "secondary"}
|
variant={nodeStyle ? "emerald" : "secondary"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1 truncate",
|
|
||||||
editNode && "text-xs",
|
editNode && "text-xs",
|
||||||
nodeStyle ? "font-jetbrains rounded-[3px] px-1" : "bg-muted",
|
nodeStyle ? "rounded-[3px] px-1 font-mono" : "bg-muted",
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
<div className="max-w-36 truncate">{selectedOption}</div>
|
|
||||||
<X
|
|
||||||
className="h-3 w-3 cursor-pointer bg-transparent hover:text-destructive"
|
|
||||||
onClick={(e) => handleRemoveOption(selectedOption, e)}
|
|
||||||
data-testid={"remove-icon-badge"}
|
|
||||||
/>
|
/>
|
||||||
</Badge>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!selectedOption && (
|
{!selectedOption?.length && !selectedOptions?.length && (
|
||||||
<input
|
<input
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onFocus={() => setIsFocused(true)}
|
onFocus={() => setIsFocused(true)}
|
||||||
|
|
@ -128,13 +248,11 @@ const CustomInputPopover = ({
|
||||||
value={value || ""}
|
value={value || ""}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
required={required}
|
required={required}
|
||||||
className={cn(
|
className={getInputClassName(
|
||||||
"popover-input nodrag w-full truncate px-1 pr-4",
|
editNode,
|
||||||
editNode && "px-2",
|
disabled,
|
||||||
editNode && disabled && "h-fit w-fit",
|
password,
|
||||||
disabled &&
|
selectedOptions,
|
||||||
"disabled:text-muted disabled:opacity-100 placeholder:disabled:text-muted-foreground",
|
|
||||||
password && "text-clip pr-14",
|
|
||||||
)}
|
)}
|
||||||
placeholder={
|
placeholder={
|
||||||
selectedOptions?.length > 0 || selectedOption ? "" : placeholder
|
selectedOptions?.length > 0 || selectedOption ? "" : placeholder
|
||||||
|
|
@ -149,9 +267,13 @@ const CustomInputPopover = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PopoverAnchor>
|
</PopoverAnchor>
|
||||||
|
|
||||||
<PopoverContentInput
|
<PopoverContentInput
|
||||||
className="noflow nowheel nopan nodelete nodrag p-0"
|
className="noflow nowheel nopan nodelete nodrag p-0"
|
||||||
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
|
style={{
|
||||||
|
minWidth: refInput?.current?.clientWidth ?? "200px",
|
||||||
|
width: popoverWidth ?? null,
|
||||||
|
}}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
>
|
>
|
||||||
|
|
@ -168,59 +290,21 @@ const CustomInputPopover = ({
|
||||||
<CommandInput placeholder={optionsPlaceholder} />
|
<CommandInput placeholder={optionsPlaceholder} />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{options.map((option, id) => (
|
{Array.from(memoizedOptions).map((option, id) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={option + id}
|
key={option + id}
|
||||||
value={option}
|
value={option}
|
||||||
onSelect={(currentValue) => {
|
onSelect={handleOptionSelect}
|
||||||
if (setSelectedOption) {
|
|
||||||
setSelectedOption(
|
|
||||||
currentValue === selectedOption ? "" : currentValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (setSelectedOptions) {
|
|
||||||
setSelectedOptions(
|
|
||||||
selectedOptions?.includes(currentValue)
|
|
||||||
? selectedOptions.filter(
|
|
||||||
(item) => item !== currentValue,
|
|
||||||
)
|
|
||||||
: [...(selectedOptions || []), currentValue],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
!setSelectedOptions && setShowOptions(false);
|
|
||||||
}}
|
|
||||||
className="group"
|
className="group"
|
||||||
>
|
>
|
||||||
<div className="group flex w-full items-center justify-between">
|
<CommandItemContent
|
||||||
<div className="flex items-center justify-between">
|
option={option}
|
||||||
<div
|
isSelected={
|
||||||
className={cn(
|
|
||||||
"relative mr-2 h-4 w-4",
|
|
||||||
selectedOption === option ||
|
selectedOption === option ||
|
||||||
selectedOptions?.includes(option)
|
selectedOptions?.includes(option)
|
||||||
? "opacity-100"
|
}
|
||||||
: "opacity-0",
|
optionButton={optionButton}
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="absolute opacity-100 transition-all group-hover:opacity-0">
|
|
||||||
<ForwardedIconComponent
|
|
||||||
name="Check"
|
|
||||||
className="mr-2 h-4 w-4 text-primary"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="absolute opacity-0 transition-all group-hover:opacity-100">
|
|
||||||
<ForwardedIconComponent
|
|
||||||
name="X"
|
|
||||||
className="mr-2 h-4 w-4 text-status-red"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="max-w-52 truncate pr-2">{option}</span>
|
|
||||||
</div>
|
|
||||||
{optionButton && optionButton(option)}
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
{optionsButton}
|
{optionsButton}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ export default function InputComponent({
|
||||||
onChangeFolderName,
|
onChangeFolderName,
|
||||||
nodeStyle,
|
nodeStyle,
|
||||||
isToolMode,
|
isToolMode,
|
||||||
|
popoverWidth,
|
||||||
}: InputComponentType): JSX.Element {
|
}: InputComponentType): JSX.Element {
|
||||||
const [pwdVisible, setPwdVisible] = useState(false);
|
const [pwdVisible, setPwdVisible] = useState(false);
|
||||||
const refInput = useRef<HTMLInputElement>(null);
|
const refInput = useRef<HTMLInputElement>(null);
|
||||||
|
|
@ -148,8 +149,8 @@ export default function InputComponent({
|
||||||
blurOnEnter={blurOnEnter}
|
blurOnEnter={blurOnEnter}
|
||||||
options={options}
|
options={options}
|
||||||
optionsPlaceholder={optionsPlaceholder}
|
optionsPlaceholder={optionsPlaceholder}
|
||||||
className={className}
|
|
||||||
nodeStyle={nodeStyle}
|
nodeStyle={nodeStyle}
|
||||||
|
popoverWidth={popoverWidth}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ export default function InputGlobalComponent({
|
||||||
return (
|
return (
|
||||||
<InputComponent
|
<InputComponent
|
||||||
nodeStyle
|
nodeStyle
|
||||||
|
popoverWidth="315px"
|
||||||
placeholder={getPlaceholder(disabled, placeholder)}
|
placeholder={getPlaceholder(disabled, placeholder)}
|
||||||
id={id}
|
id={id}
|
||||||
editNode={editNode}
|
editNode={editNode}
|
||||||
|
|
|
||||||
54
src/frontend/src/components/ui/tabs-button.tsx
Normal file
54
src/frontend/src/components/ui/tabs-button.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/utils/utils";
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
||||||
|
|
@ -46,6 +46,7 @@ export type InputComponentType = {
|
||||||
onChangeFolderName?: (e: any) => void;
|
onChangeFolderName?: (e: any) => void;
|
||||||
nodeStyle?: boolean;
|
nodeStyle?: boolean;
|
||||||
isToolMode?: boolean;
|
isToolMode?: boolean;
|
||||||
|
popoverWidth?: string;
|
||||||
};
|
};
|
||||||
export type DropDownComponent = {
|
export type DropDownComponent = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,11 @@ test(
|
||||||
await page.getByTestId("icon-Globe").nth(0).click();
|
await page.getByTestId("icon-Globe").nth(0).click();
|
||||||
await page.getByText("Add New Variable", { exact: true }).click();
|
await page.getByText("Add New Variable", { exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder("Insert a name for the variable...")
|
.getByPlaceholder("Enter a name for the variable...")
|
||||||
.fill(genericName);
|
.fill(genericName);
|
||||||
await page.getByText("Generic", { exact: true }).first().isVisible();
|
await page.getByText("Generic", { exact: true }).first().isVisible();
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder("Insert a value for the variable...")
|
.getByPlaceholder("Enter a value for the variable...")
|
||||||
.fill("This is a test of generic variable value");
|
.fill("This is a test of generic variable value");
|
||||||
await page.getByText("Save Variable", { exact: true }).click();
|
await page.getByText("Save Variable", { exact: true }).click();
|
||||||
expect(page.getByText(genericName, { exact: true })).not.toBeNull();
|
expect(page.getByText(genericName, { exact: true })).not.toBeNull();
|
||||||
|
|
@ -45,12 +45,11 @@ test(
|
||||||
|
|
||||||
await page.getByText("Add New Variable", { exact: true }).click();
|
await page.getByText("Add New Variable", { exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder("Insert a name for the variable...")
|
.getByPlaceholder("Enter a name for the variable...")
|
||||||
.fill(credentialName);
|
.fill(credentialName);
|
||||||
await page.getByTestId("select-type-global-variables").first().click();
|
await page.getByTestId("credential-tab").click();
|
||||||
await page.getByText("Credential", { exact: true }).last().click();
|
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder("Insert a value for the variable...")
|
.getByPlaceholder("Enter a value for the variable...")
|
||||||
.fill("This is a test of credential variable value");
|
.fill("This is a test of credential variable value");
|
||||||
await page.getByText("Save Variable", { exact: true }).click();
|
await page.getByText("Save Variable", { exact: true }).click();
|
||||||
expect(page.getByText(credentialName, { exact: true })).not.toBeNull();
|
expect(page.getByText(credentialName, { exact: true })).not.toBeNull();
|
||||||
|
|
|
||||||
|
|
@ -44,40 +44,39 @@ test(
|
||||||
.isVisible();
|
.isVisible();
|
||||||
await page.getByText("Add New").click();
|
await page.getByText("Add New").click();
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder("Insert a name for the variable...")
|
.getByPlaceholder("Enter a name for the variable...")
|
||||||
.fill(randomName);
|
.fill(randomName);
|
||||||
await page.getByTestId("select-type-global-variables").first().click();
|
|
||||||
await page.getByText("Generic", { exact: true }).last().isVisible();
|
await page.getByText("Generic", { exact: true }).last().isVisible();
|
||||||
await page.getByText("Generic", { exact: true }).last().click();
|
await page.getByText("Generic", { exact: true }).last().click();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder("Insert a value for the variable...")
|
.getByPlaceholder("Enter a value for the variable...")
|
||||||
.fill("testtesttesttesttesttesttesttest");
|
.fill("testtesttesttesttesttesttesttest");
|
||||||
await page.getByTestId("popover-anchor-apply-to-fields").click();
|
await page.getByTestId("popover-anchor-apply-to-fields").click();
|
||||||
|
|
||||||
await page.getByPlaceholder("Search options...").waitFor({
|
await page.getByPlaceholder("Fields").waitFor({
|
||||||
state: "visible",
|
state: "visible",
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByPlaceholder("Search options...").fill("System");
|
await page.getByPlaceholder("Fields").fill("System");
|
||||||
|
|
||||||
await page.waitForSelector("text=System", { timeout: 30000 });
|
await page.waitForSelector("text=System", { timeout: 30000 });
|
||||||
|
|
||||||
await page.getByText("System").last().click();
|
await page.getByText("System").last().click();
|
||||||
|
|
||||||
await page.getByPlaceholder("Search options...").fill("openAI");
|
await page.getByPlaceholder("Fields").fill("openAI");
|
||||||
|
|
||||||
await page.waitForSelector("text=openai", { timeout: 30000 });
|
await page.waitForSelector("text=openai", { timeout: 30000 });
|
||||||
|
|
||||||
await page.getByText("openai").last().click();
|
await page.getByText("openai").last().click();
|
||||||
|
|
||||||
await page.getByPlaceholder("Search options...").waitFor({
|
await page.getByPlaceholder("Fields").waitFor({
|
||||||
state: "visible",
|
state: "visible",
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByPlaceholder("Search options...").fill("ollama");
|
await page.getByPlaceholder("Fields").fill("ollama");
|
||||||
|
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
await page.getByText("Save Variable", { exact: true }).click();
|
await page.getByText("Save Variable", { exact: true }).click();
|
||||||
|
|
@ -87,13 +86,13 @@ test(
|
||||||
await page.getByText(randomName).last().click();
|
await page.getByText(randomName).last().click();
|
||||||
await page.getByText(randomName).last().click();
|
await page.getByText(randomName).last().click();
|
||||||
|
|
||||||
await page.getByPlaceholder("Insert a name for the variable...").waitFor({
|
await page.getByPlaceholder("Enter a name for the variable...").waitFor({
|
||||||
state: "visible",
|
state: "visible",
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder("Insert a name for the variable...")
|
.getByPlaceholder("Enter a name for the variable...")
|
||||||
.fill(randomName2);
|
.fill(randomName2);
|
||||||
|
|
||||||
await page.getByText("Update Variable", { exact: true }).last().click();
|
await page.getByText("Update Variable", { exact: true }).last().click();
|
||||||
|
|
@ -102,13 +101,13 @@ test(
|
||||||
|
|
||||||
await page.getByText(randomName2).last().click();
|
await page.getByText(randomName2).last().click();
|
||||||
|
|
||||||
await page.getByPlaceholder("Insert a name for the variable...").waitFor({
|
await page.getByPlaceholder("Enter a name for the variable...").waitFor({
|
||||||
state: "visible",
|
state: "visible",
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder("Insert a name for the variable...")
|
.getByPlaceholder("Enter a name for the variable...")
|
||||||
.fill(randomName3);
|
.fill(randomName3);
|
||||||
|
|
||||||
await page.getByText("Update Variable", { exact: true }).last().click();
|
await page.getByText("Update Variable", { exact: true }).last().click();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue