fix: enhance dropdown component with refresh button and clean up parameter render logic (#8493)
* fix: enhance dropdown component with refresh button and clean up parameter render logic - Added a refresh button to the dropdown component, improving user interaction. - Refactored parameter render component to remove unnecessary wrapping around the render function. - Updated package-lock.json to remove extraneous dependencies. * [autofix.ci] apply automated fixes * refactor(OutputComponent): replace DropdownMenu with Popover and Command components - Updated OutputComponent to use Popover and Command components for improved UI interaction. - Refactored dropdown logic to enhance accessibility and user experience. - Added a reference for the button to manage focus visibility. * refactor: update Memory Chatbot configuration and remove unused RefreshParameterComponent - Changed display names and output methods in Memory Chatbot JSON configuration for clarity and consistency. - Introduced a new output method for retrieving messages as text. - Removed the RefreshParameterComponent and its references from the parameter render component to streamline the codebase. * refactor: update dropdown component layout for improved styling - Changed the layout classes in the dropdown component to enhance responsiveness and visual consistency. - Adjusted flex properties to ensure proper alignment and spacing based on the presence of filtered metadata. * refactor: streamline dropdown component structure and enhance button functionality - Removed redundant rendering functions for refresh and custom option dialogs, integrating them directly into the dropdown's main structure. - Improved layout and styling for better responsiveness and visual consistency. - Adjusted class names for better alignment and spacing, particularly in relation to filtered metadata. - Ensured the refresh button is consistently displayed based on dialog input conditions. * refactor: enhance dropdown component styling for better readability - Updated text size in dropdown options for improved visibility. - Increased padding in command items for better touch targets and visual consistency. * refactor: adjust dropdown component styling for improved usability - Reduced padding in the search input for a more compact design. - Updated text size in the search input for better readability. - Enhanced layout of filtered metadata display for clearer visibility. * refactor: enhance dropdown component rendering and styling - Added console log for filtered metadata to assist in debugging. - Adjusted class names in dropdown options for better responsiveness based on filtered metadata length. * feat: add data-testid attributes for refresh buttons and simplify memoization in ParameterRenderComponent - Added data-testid attributes to refresh buttons in Dropdown component for improved testability. - Removed unnecessary useMemo in ParameterRenderComponent to streamline rendering logic. - Introduced a new test for the refresh dropdown list functionality to ensure proper behavior. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
5734735e2d
commit
466a18c744
5 changed files with 204 additions and 202 deletions
|
|
@ -1,12 +1,19 @@
|
|||
import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
Command,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
|
||||
import {
|
||||
Popover,
|
||||
PopoverContentWithoutPortal,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import useFlowStore from "@/stores/flowStore";
|
||||
import { useRef } from "react";
|
||||
import ShadTooltip from "../../../../components/common/shadTooltipComponent";
|
||||
import { outputComponentType } from "../../../../types/components";
|
||||
import { cn } from "../../../../utils/utils";
|
||||
|
|
@ -56,6 +63,7 @@ export default function OutputComponent({
|
|||
const hasGroupOutputs = outputs?.some?.((output) => output.group_outputs);
|
||||
const isConditionalRouter = nodeType === "ConditionalRouter";
|
||||
const hasOutputs = outputs.length > 1;
|
||||
const refButton = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const shouldShowDropdown =
|
||||
hasOutputs && !hasLoopOutput && !hasGroupOutputs && !isConditionalRouter;
|
||||
|
|
@ -63,11 +71,13 @@ export default function OutputComponent({
|
|||
return (
|
||||
<div>
|
||||
{shouldShowDropdown ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
unstyled
|
||||
className="group flex items-center gap-2"
|
||||
role="combobox"
|
||||
ref={refButton}
|
||||
className="no-focus-visible group flex items-center gap-2"
|
||||
data-testid={`dropdown-output-${outputName?.toLowerCase()}`}
|
||||
>
|
||||
<div className="flex items-center gap-1 truncate rounded-md px-2 py-1 text-[13px] font-medium group-hover:bg-primary/10">
|
||||
|
|
@ -78,27 +88,38 @@ export default function OutputComponent({
|
|||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-[200px] max-w-[250px]">
|
||||
{outputs.map((output) => (
|
||||
<DropdownMenuItem
|
||||
key={output.name}
|
||||
data-testid={`dropdown-item-output-${outputName?.toLowerCase()}-${output.display_name?.toLowerCase()}`}
|
||||
className="cursor-pointer justify-between px-3 py-2"
|
||||
onClick={() => {
|
||||
handleSelectOutput && handleSelectOutput(output);
|
||||
}}
|
||||
>
|
||||
<span className="truncate text-[13px]">
|
||||
{output.display_name ?? output.name}
|
||||
</span>
|
||||
<span className="ml-4 text-[13px] text-muted-foreground">
|
||||
{output.types.join(", ")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</PopoverTrigger>
|
||||
<PopoverContentWithoutPortal
|
||||
side="bottom"
|
||||
align="end"
|
||||
className="noflow nowheel nopan nodelete nodrag w-full min-w-[200px] max-w-[250px] p-0"
|
||||
>
|
||||
<Command>
|
||||
<CommandList>
|
||||
<CommandGroup defaultChecked={false} className="p-0">
|
||||
{outputs.map((output) => (
|
||||
<CommandItem
|
||||
key={output.name}
|
||||
data-testid={`dropdown-item-output-${outputName?.toLowerCase()}-${output.display_name?.toLowerCase()}`}
|
||||
className="cursor-pointer justify-between rounded-none px-3 py-2"
|
||||
onSelect={() => {
|
||||
handleSelectOutput && handleSelectOutput(output);
|
||||
}}
|
||||
value={output.name}
|
||||
>
|
||||
<span className="truncate text-[13px]">
|
||||
{output.display_name ?? output.name}
|
||||
</span>
|
||||
<span className="ml-4 text-[13px] text-muted-foreground">
|
||||
{output.types.join(", ")}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContentWithoutPortal>
|
||||
</Popover>
|
||||
) : (
|
||||
singleOutput
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export default function Dropdown({
|
|||
const fuse = new Fuse(validOptions, { keys: ["name", "value"] });
|
||||
const PopoverContentDropdown =
|
||||
children || editNode ? PopoverContent : PopoverContentWithoutPortal;
|
||||
const { helperText } = baseInputProps;
|
||||
const { helperText, hasRefreshButton } = baseInputProps;
|
||||
|
||||
// API and store hooks
|
||||
const postTemplateValue = usePostTemplateValue({
|
||||
|
|
@ -357,7 +357,7 @@ export default function Dropdown({
|
|||
);
|
||||
|
||||
const renderSearchInput = () => (
|
||||
<div className="flex items-center border-b px-3">
|
||||
<div className="flex items-center border-b px-2.5">
|
||||
<ForwardedIconComponent
|
||||
name="search"
|
||||
className="mr-2 h-4 w-4 shrink-0 opacity-50"
|
||||
|
|
@ -366,66 +366,16 @@ export default function Dropdown({
|
|||
onChange={searchRoleByTerm}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
placeholder="Search options..."
|
||||
className="flex h-9 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="flex h-9 w-full rounded-md bg-transparent py-3 text-[13px] outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
autoComplete="off"
|
||||
data-testid="dropdown_search_input"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderCustomOptionDialog = () => (
|
||||
<CommandGroup className="flex flex-col">
|
||||
<CommandItem className="flex cursor-pointer items-center justify-start gap-2 truncate py-3 text-xs font-semibold text-muted-foreground">
|
||||
<Button
|
||||
className="w-full"
|
||||
unstyled
|
||||
onClick={() => {
|
||||
setOpenDialog(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 pl-1">
|
||||
<ForwardedIconComponent
|
||||
name="Plus"
|
||||
className="h-3 w-3 text-primary"
|
||||
/>
|
||||
{`New ${firstWord}`}
|
||||
</div>
|
||||
</Button>
|
||||
</CommandItem>
|
||||
<CommandItem className="flex cursor-pointer items-center justify-start gap-2 truncate py-3 text-xs font-semibold text-muted-foreground">
|
||||
<Button
|
||||
className="w-full"
|
||||
unstyled
|
||||
onClick={() => {
|
||||
handleRefreshButtonPress();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 pl-1">
|
||||
<ForwardedIconComponent
|
||||
name="RefreshCcw"
|
||||
className={cn("refresh-icon h-3 w-3 text-primary")}
|
||||
/>
|
||||
Refresh list
|
||||
</div>
|
||||
</Button>
|
||||
</CommandItem>
|
||||
<NodeDialog
|
||||
open={openDialog}
|
||||
dialogInputs={dialogInputs}
|
||||
onClose={() => {
|
||||
setOpenDialog(false);
|
||||
setOpen(false);
|
||||
}}
|
||||
nodeId={nodeId!}
|
||||
name={name!}
|
||||
nodeClass={nodeClass!}
|
||||
/>
|
||||
</CommandGroup>
|
||||
);
|
||||
|
||||
const renderOptionsList = () => (
|
||||
<CommandList>
|
||||
<CommandGroup defaultChecked={false}>
|
||||
<CommandList className="max-h-[300px] overflow-y-auto">
|
||||
<CommandGroup defaultChecked={false} className="p-0">
|
||||
{filteredOptions?.length > 0 ? (
|
||||
filteredOptions?.map((option, index) => (
|
||||
<ShadTooltip
|
||||
|
|
@ -441,7 +391,7 @@ export default function Dropdown({
|
|||
onSelect(currentValue);
|
||||
setOpen(false);
|
||||
}}
|
||||
className="items-center"
|
||||
className="w-full items-center rounded-none"
|
||||
data-testid={`${option}-${index}-option`}
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
|
|
@ -452,14 +402,18 @@ export default function Dropdown({
|
|||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn("flex truncate", {
|
||||
"flex-col":
|
||||
filteredMetadata && filteredMetadata?.length > 0,
|
||||
"w-full pl-2": !filteredMetadata?.[index]?.icon,
|
||||
className={cn("flex w-full", {
|
||||
"pl-2": !filteredMetadata?.[index]?.icon,
|
||||
})}
|
||||
>
|
||||
<div className="flex truncate">
|
||||
{option}{" "}
|
||||
<div
|
||||
className={cn("truncate text-[13px]", {
|
||||
"w-1/2": filteredMetadata?.length !== 0,
|
||||
})}
|
||||
>
|
||||
{option}
|
||||
</div>
|
||||
{filteredMetadata?.[index]?.status && (
|
||||
<span
|
||||
className={`flex items-center pl-2 text-xs ${getStatusColor(
|
||||
filteredMetadata?.[index]?.status,
|
||||
|
|
@ -471,9 +425,10 @@ export default function Dropdown({
|
|||
]?.status?.toLowerCase()}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{filteredMetadata && filteredMetadata?.length > 0 ? (
|
||||
<div className="flex w-full items-center overflow-hidden text-muted-foreground">
|
||||
)}
|
||||
|
||||
{filteredMetadata && filteredMetadata?.length > 0 && (
|
||||
<div className="ml-auto flex items-center overflow-hidden pl-2 text-muted-foreground">
|
||||
{Object.entries(
|
||||
filterMetadataKeys(filteredMetadata?.[index] || {}),
|
||||
)
|
||||
|
|
@ -495,25 +450,27 @@ export default function Dropdown({
|
|||
className="mx-1 h-1 w-1 flex-shrink-0 overflow-visible fill-muted-foreground"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn("text-xs", {
|
||||
"w-full truncate": i === arr.length - 1,
|
||||
})}
|
||||
>{`${String(value)} ${key}`}</div>
|
||||
<div className="truncate text-xs">
|
||||
{`${String(value)} ${key}`}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="ml-auto flex">
|
||||
<ForwardedIconComponent
|
||||
name="Check"
|
||||
className={cn(
|
||||
"h-4 w-4 shrink-0 text-primary",
|
||||
value === option ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn("pl-2", {
|
||||
"ml-auto":
|
||||
!filteredMetadata || filteredMetadata.length === 0,
|
||||
})}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Check"
|
||||
className={cn(
|
||||
"h-4 w-4 shrink-0 text-primary",
|
||||
value === option ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
|
|
@ -527,7 +484,56 @@ export default function Dropdown({
|
|||
)}
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
{dialogInputs && dialogInputs?.fields && renderCustomOptionDialog()}
|
||||
{dialogInputs && dialogInputs?.fields && (
|
||||
<CommandGroup className="p-0">
|
||||
<CommandItem className="flex cursor-pointer items-center justify-start gap-2 truncate rounded-none py-2.5 text-xs font-semibold text-muted-foreground">
|
||||
<Button
|
||||
className="w-full"
|
||||
unstyled
|
||||
onClick={() => {
|
||||
setOpenDialog(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 pl-1">
|
||||
<ForwardedIconComponent
|
||||
name="Plus"
|
||||
className="h-3 w-3 text-primary"
|
||||
/>
|
||||
{`New ${firstWord}`}
|
||||
</div>
|
||||
</Button>
|
||||
</CommandItem>
|
||||
<CommandItem className="flex cursor-pointer items-center justify-start gap-2 truncate rounded-none py-2.5 text-xs font-semibold text-muted-foreground">
|
||||
<Button
|
||||
className="w-full"
|
||||
unstyled
|
||||
data-testid={`refresh-dropdown-list-${name}`}
|
||||
onClick={() => {
|
||||
handleRefreshButtonPress();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 pl-1">
|
||||
<ForwardedIconComponent
|
||||
name="RefreshCcw"
|
||||
className={cn("refresh-icon h-3 w-3 text-primary")}
|
||||
/>
|
||||
Refresh list
|
||||
</div>
|
||||
</Button>
|
||||
</CommandItem>
|
||||
<NodeDialog
|
||||
open={openDialog}
|
||||
dialogInputs={dialogInputs}
|
||||
onClose={() => {
|
||||
setOpenDialog(false);
|
||||
setOpen(false);
|
||||
}}
|
||||
nodeId={nodeId!}
|
||||
name={name!}
|
||||
nodeClass={nodeClass!}
|
||||
/>
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
);
|
||||
|
||||
|
|
@ -540,9 +546,31 @@ export default function Dropdown({
|
|||
children ? {} : { minWidth: refButton?.current?.clientWidth ?? "200px" }
|
||||
}
|
||||
>
|
||||
<Command>
|
||||
<Command className="flex flex-col">
|
||||
{options?.length > 0 && renderSearchInput()}
|
||||
{renderOptionsList()}
|
||||
{!dialogInputs?.fields && hasRefreshButton && (
|
||||
<div className="sticky bottom-0 border-t bg-background">
|
||||
<CommandItem className="flex cursor-pointer items-center justify-start gap-2 truncate rounded-b-md py-3 text-xs font-semibold text-muted-foreground">
|
||||
<Button
|
||||
className="w-full"
|
||||
unstyled
|
||||
data-testid={`refresh-dropdown-list-${name}`}
|
||||
onClick={() => {
|
||||
handleRefreshButtonPress();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 pl-1">
|
||||
<ForwardedIconComponent
|
||||
name="RefreshCcw"
|
||||
className={cn("refresh-icon h-3 w-3 text-primary")}
|
||||
/>
|
||||
Refresh list
|
||||
</div>
|
||||
</Button>
|
||||
</CommandItem>
|
||||
</div>
|
||||
)}
|
||||
</Command>
|
||||
</PopoverContentDropdown>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
import { RefreshButton } from "@/components/ui/refreshButton";
|
||||
import { FLEX_VIEW_TYPES } from "@/constants/constants";
|
||||
import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value";
|
||||
import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import { APIClassType, InputFieldType } from "@/types/api";
|
||||
import { cn } from "@/utils/utils";
|
||||
import { InputProps } from "../../types";
|
||||
|
||||
export function RefreshParameterComponent({
|
||||
children,
|
||||
templateData,
|
||||
disabled,
|
||||
nodeClass,
|
||||
editNode,
|
||||
handleNodeClass,
|
||||
nodeId,
|
||||
name,
|
||||
}: {
|
||||
children: React.ReactElement<InputProps>;
|
||||
templateData: Partial<InputFieldType>;
|
||||
disabled: boolean;
|
||||
nodeClass: APIClassType;
|
||||
editNode: boolean;
|
||||
handleNodeClass: (value: any, code?: string, type?: string) => void;
|
||||
nodeId: string;
|
||||
name: string;
|
||||
}) {
|
||||
const postTemplateValue = usePostTemplateValue({
|
||||
parameterId: name,
|
||||
nodeId: nodeId,
|
||||
node: nodeClass,
|
||||
});
|
||||
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const handleRefreshButtonPress = () =>
|
||||
mutateTemplate(
|
||||
templateData.value,
|
||||
nodeId,
|
||||
nodeClass,
|
||||
handleNodeClass,
|
||||
postTemplateValue,
|
||||
setErrorData,
|
||||
);
|
||||
|
||||
const isFlexView = FLEX_VIEW_TYPES.includes(templateData.type ?? "");
|
||||
|
||||
return (
|
||||
(children ||
|
||||
(templateData.refresh_button && !templateData.dialog_inputs)) && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-center gap-3",
|
||||
isFlexView ? "justify-end" : "justify-center",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
{templateData.refresh_button &&
|
||||
!templateData.dialog_inputs?.fields?.data?.node?.template && (
|
||||
<div className="shrink-0 flex-col">
|
||||
<RefreshButton
|
||||
isLoading={postTemplateValue.isPending}
|
||||
disabled={disabled}
|
||||
editNode={editNode}
|
||||
button_text={templateData.refresh_button_text}
|
||||
handleUpdateValues={handleRefreshButtonPress}
|
||||
id={"refresh-button-" + name}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ import McpComponent from "./components/mcpComponent";
|
|||
import MultiselectComponent from "./components/multiselectComponent";
|
||||
import PromptAreaComponent from "./components/promptComponent";
|
||||
import QueryComponent from "./components/queryComponent";
|
||||
import { RefreshParameterComponent } from "./components/refreshParameterComponent";
|
||||
import SortableListComponent from "./components/sortableListComponent";
|
||||
import { StrRenderComponent } from "./components/strRenderComponent";
|
||||
import ToggleShadComponent from "./components/toggleShadComponent";
|
||||
|
|
@ -295,20 +294,5 @@ export function ParameterRenderComponent({
|
|||
}
|
||||
};
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<RefreshParameterComponent
|
||||
templateData={templateData}
|
||||
disabled={disabled}
|
||||
nodeId={nodeId}
|
||||
editNode={editNode}
|
||||
nodeClass={nodeClass}
|
||||
handleNodeClass={handleNodeClass}
|
||||
name={name}
|
||||
>
|
||||
{renderComponent()}
|
||||
</RefreshParameterComponent>
|
||||
),
|
||||
[templateData, disabled, nodeId, editNode, nodeClass, name, templateValue],
|
||||
);
|
||||
return renderComponent();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import { test } from "@playwright/test";
|
||||
import * as dotenv from "dotenv";
|
||||
import path from "path";
|
||||
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
|
||||
import { initialGPTsetup } from "../../utils/initialGPTsetup";
|
||||
|
||||
test(
|
||||
"refresh dropdown list",
|
||||
{ tag: ["@release", "@components"] },
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
!process?.env?.ANTHROPIC_API_KEY,
|
||||
"ANTHROPIC_API_KEY required to run this test",
|
||||
);
|
||||
|
||||
if (!process.env.CI) {
|
||||
dotenv.config({ path: path.resolve(__dirname, "../../.env") });
|
||||
}
|
||||
|
||||
await page.goto("/");
|
||||
await awaitBootstrapTest(page);
|
||||
|
||||
await page.getByTestId("side_nav_options_all-templates").click();
|
||||
await page
|
||||
.getByRole("heading", { name: "Portfolio Website Code Generator" })
|
||||
.click();
|
||||
|
||||
await page.waitForSelector('[data-testid="fit_view"]', {
|
||||
timeout: 100000,
|
||||
});
|
||||
|
||||
await initialGPTsetup(page, {
|
||||
skipAdjustScreenView: true,
|
||||
skipSelectGptModel: true,
|
||||
});
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
await page.getByTestId("dropdown_str_model_name").first().click();
|
||||
await page.getByTestId("refresh-dropdown-list-model_name").first().click();
|
||||
await page.getByText("Loading Options").isVisible({ timeout: 5000 });
|
||||
},
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue