Files
ragflow/web/src/pages/agent/form/components/select-with-secondary-menu.tsx
balibabu 410c0a829d Feat: The query variable of a loop operator can be a nested array variable. #10866 (#10921)
### What problem does this PR solve?

Feat: The query variable of a loop operator can be a nested array
variable. #10866

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-11-03 09:40:47 +08:00

231 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Button } from '@/components/ui/button';
import {
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/hover-card';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Separator } from '@/components/ui/separator';
import { cn } from '@/lib/utils';
import { get } from 'lodash';
import { ChevronDownIcon, XIcon } from 'lucide-react';
import * as React from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { VariableType } from '../../constant';
import {
useFilterStructuredOutputByValue,
useFindAgentStructuredOutputLabel,
useShowSecondaryMenu,
} from '../../hooks/use-build-structured-output';
import { StructuredOutputSecondaryMenu } from './structured-output-secondary-menu';
type Item = {
label: string;
value: string;
};
type Option = {
label: string;
value: string;
parentLabel?: string;
children?: Item[];
};
type Group = {
label: string | React.ReactNode;
options: Option[];
};
interface GroupedSelectWithSecondaryMenuProps {
options: Group[];
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
type?: VariableType;
}
export function GroupedSelectWithSecondaryMenu({
options,
value,
onChange,
placeholder,
type,
}: GroupedSelectWithSecondaryMenuProps) {
const { t } = useTranslation();
const [open, setOpen] = React.useState(false);
const showSecondaryMenu = useShowSecondaryMenu();
const filterStructuredOutput = useFilterStructuredOutputByValue();
const findAgentStructuredOutputLabel = useFindAgentStructuredOutputLabel();
// Find the label of the selected item
const flattenedOptions = options.flatMap((g) => g.options);
let selectedItem = flattenedOptions
.flatMap((o) => [o, ...(o.children || [])])
.find((o) => o.value === value);
if (!selectedItem && value) {
selectedItem = findAgentStructuredOutputLabel(value, flattenedOptions);
}
// Handle clear click
const handleClear = (e: React.MouseEvent) => {
e.stopPropagation();
onChange?.('');
setOpen(false);
};
const handleSecondaryMenuClick = useCallback(
(record: Item) => {
onChange?.(record.value);
setOpen(false);
},
[onChange],
);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn(
'!bg-bg-input hover:bg-background border-input w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px] [&_svg]:pointer-events-auto',
!value && 'text-muted-foreground',
)}
>
{value ? (
<div className="truncate flex items-center gap-1">
<span>{get(selectedItem, 'parentLabel')}</span>
<span className="text-text-disabled">/</span>
<span className="text-accent-primary">{selectedItem?.label}</span>
</div>
) : (
<span className="text-muted-foreground">
{placeholder || t('common.selectPlaceholder')}
</span>
)}
<div className="flex items-center justify-between">
{value && (
<>
<XIcon
className="h-4 mx-2 cursor-pointer text-muted-foreground"
onClick={handleClear}
/>
<Separator
orientation="vertical"
className="flex min-h-6 h-full"
/>
</>
)}
<ChevronDownIcon
size={16}
className="text-muted-foreground/80 shrink-0 ml-2"
aria-hidden="true"
/>
</div>
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command value={value}>
<CommandInput placeholder="Search..." />
<CommandList className="overflow-auto">
{options.map((group, idx) => (
<CommandGroup key={idx} heading={group.label}>
{group.options.map((option) => {
const shouldShowSecondary = showSecondaryMenu(
option.value,
option.label,
);
if (shouldShowSecondary) {
const filteredStructuredOutput = filterStructuredOutput(
option.value,
);
return (
<StructuredOutputSecondaryMenu
key={option.value}
data={option}
click={handleSecondaryMenuClick}
filteredStructuredOutput={filteredStructuredOutput}
type={type}
></StructuredOutputSecondaryMenu>
);
}
return option.children ? (
<HoverCard
key={option.value}
openDelay={100}
closeDelay={150}
>
<HoverCardTrigger asChild>
<CommandItem
onSelect={() => {}}
className="flex items-center justify-between cursor-default"
>
{option.label}
<span className="ml-auto text-muted-foreground">
</span>
</CommandItem>
</HoverCardTrigger>
<HoverCardContent
side="right"
align="start"
className="w-[180px] p-1"
>
{option.children.map((child) => (
<div
key={child.value}
className={cn(
'cursor-pointer rounded-sm px-2 py-1.5 text-sm hover:bg-bg-card hover:text-accent-foreground',
value === child.value &&
'bg-accent text-accent-foreground',
)}
onClick={() => {
onChange?.(child.value);
setOpen(false);
}}
>
{child.label}
</div>
))}
</HoverCardContent>
</HoverCard>
) : (
<CommandItem
key={option.value}
onSelect={() => {
onChange?.(option.value);
setOpen(false);
}}
>
{option.label}
</CommandItem>
);
})}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}