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:
Deon Sanchez 2025-07-01 07:07:48 -06:00 committed by GitHub
commit 466a18c744
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 204 additions and 202 deletions

View file

@ -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
)}

View file

@ -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>
);

View file

@ -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>
)
);
}

View file

@ -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();
}

View file

@ -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 });
},
);