[agent-pipeline] pipe-20260311052455-y968 round-3
This commit is contained in:
parent
d358de60d6
commit
834c52a2b2
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
|
|
@ -13,7 +13,18 @@ import {
|
|||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, X } from "lucide-react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { Plus, X, Check, ChevronsUpDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ConfigFieldDefinition, ConfigOption } from "./ConfigPanelTypes";
|
||||
|
||||
interface ConfigFieldProps<T = any> {
|
||||
|
|
@ -29,6 +40,8 @@ export function ConfigField<T>({
|
|||
onChange,
|
||||
tableColumns,
|
||||
}: ConfigFieldProps<T>) {
|
||||
const [comboboxOpen, setComboboxOpen] = useState(false);
|
||||
|
||||
const handleChange = (newValue: any) => {
|
||||
onChange(field.key, newValue);
|
||||
};
|
||||
|
|
@ -41,7 +54,7 @@ export function ConfigField<T>({
|
|||
value={value ?? ""}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
placeholder={field.placeholder}
|
||||
className="h-8 text-xs"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -59,7 +72,7 @@ export function ConfigField<T>({
|
|||
min={field.min}
|
||||
max={field.max}
|
||||
step={field.step}
|
||||
className="h-8 text-xs"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -77,7 +90,7 @@ export function ConfigField<T>({
|
|||
value={value ?? ""}
|
||||
onValueChange={handleChange}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder={field.placeholder || "선택"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -103,25 +116,25 @@ export function ConfigField<T>({
|
|||
|
||||
case "color":
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<input
|
||||
type="color"
|
||||
value={value ?? "#000000"}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
className="h-8 w-8 cursor-pointer rounded border"
|
||||
className="h-7 w-7 cursor-pointer rounded border"
|
||||
/>
|
||||
<Input
|
||||
value={value ?? ""}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
placeholder="#000000"
|
||||
className="h-8 flex-1 text-xs"
|
||||
className="h-7 flex-1 text-xs"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "slider":
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
value={value ?? field.min ?? 0}
|
||||
|
|
@ -129,17 +142,17 @@ export function ConfigField<T>({
|
|||
min={field.min}
|
||||
max={field.max}
|
||||
step={field.step}
|
||||
className="h-8 w-20 text-xs"
|
||||
className="h-7 w-16 text-xs"
|
||||
/>
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
{field.min ?? 0} ~ {field.max ?? 100}
|
||||
<span className="text-muted-foreground text-[9px]">
|
||||
{field.min ?? 0}~{field.max ?? 100}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "multi-select":
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-0.5">
|
||||
{(field.options || []).map((opt) => {
|
||||
const selected = Array.isArray(value) && value.includes(opt.value);
|
||||
return (
|
||||
|
|
@ -230,7 +243,7 @@ export function ConfigField<T>({
|
|||
value={value ?? ""}
|
||||
onValueChange={handleChange}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder={field.placeholder || "컬럼 선택"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -244,21 +257,123 @@ export function ConfigField<T>({
|
|||
);
|
||||
}
|
||||
|
||||
case "checkbox":
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`field-${field.key}`}
|
||||
checked={!!value}
|
||||
onCheckedChange={handleChange}
|
||||
/>
|
||||
{field.description && (
|
||||
<label
|
||||
htmlFor={`field-${field.key}`}
|
||||
className="cursor-pointer text-xs text-muted-foreground"
|
||||
>
|
||||
{field.description}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case "combobox": {
|
||||
const options = field.options || [];
|
||||
const selectedLabel = options.find((opt) => opt.value === value)?.label;
|
||||
return (
|
||||
<Popover open={comboboxOpen} onOpenChange={setComboboxOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={comboboxOpen}
|
||||
className="h-7 w-full justify-between text-xs font-normal"
|
||||
>
|
||||
<span className="truncate">
|
||||
{selectedLabel || field.placeholder || "선택"}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-0"
|
||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="검색..." className="text-xs" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="py-2 text-center text-xs">
|
||||
결과 없음
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((opt) => (
|
||||
<CommandItem
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
onSelect={(currentValue) => {
|
||||
handleChange(currentValue === value ? "" : currentValue);
|
||||
setComboboxOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-1.5 h-3 w-3",
|
||||
value === opt.value ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{opt.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-medium">{field.label}</Label>
|
||||
{field.type === "switch" && renderField()}
|
||||
// textarea, multi-select, key-value는 전체 폭 수직 레이아웃
|
||||
const isFullWidth = ["textarea", "multi-select", "key-value"].includes(field.type);
|
||||
// checkbox는 description을 인라인으로 표시하므로 별도 처리
|
||||
const isCheckbox = field.type === "checkbox";
|
||||
|
||||
if (isFullWidth) {
|
||||
return (
|
||||
<div className="py-1.5">
|
||||
<Label className="mb-1 block text-xs text-muted-foreground">{field.label}</Label>
|
||||
{field.description && !isCheckbox && (
|
||||
<p className="text-muted-foreground/60 mb-1 text-[9px]">{field.description}</p>
|
||||
)}
|
||||
{renderField()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// switch, checkbox: 라벨 왼쪽, 컨트롤 오른쪽 (고정폭 없이)
|
||||
if (field.type === "switch" || isCheckbox) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-1.5">
|
||||
<Label className="mr-3 truncate text-xs text-muted-foreground">{field.label}</Label>
|
||||
{renderField()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 기본: 수평 property row (라벨 왼쪽, 컨트롤 오른쪽 고정폭)
|
||||
return (
|
||||
<div className="flex items-center justify-between py-1.5">
|
||||
<Label className="mr-3 min-w-0 shrink truncate text-xs text-muted-foreground">
|
||||
{field.label}
|
||||
</Label>
|
||||
<div className="w-[140px] flex-shrink-0">
|
||||
{renderField()}
|
||||
</div>
|
||||
{field.description && (
|
||||
<p className="text-muted-foreground text-[10px]">{field.description}</p>
|
||||
)}
|
||||
{field.type !== "switch" && renderField()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ export type ConfigFieldType =
|
|||
| "slider"
|
||||
| "multi-select"
|
||||
| "key-value"
|
||||
| "column-picker";
|
||||
| "column-picker"
|
||||
| "checkbox"
|
||||
| "combobox";
|
||||
|
||||
export interface ConfigOption {
|
||||
label: string;
|
||||
|
|
@ -40,6 +42,7 @@ export interface ConfigSectionDefinition<T = any> {
|
|||
defaultOpen?: boolean;
|
||||
fields: ConfigFieldDefinition<T>[];
|
||||
condition?: (config: T) => boolean;
|
||||
group?: string;
|
||||
}
|
||||
|
||||
export interface ConfigPanelBuilderProps<T = any> {
|
||||
|
|
|
|||
|
|
@ -14,39 +14,45 @@ export function ConfigSection({ section, children }: ConfigSectionProps) {
|
|||
|
||||
if (section.collapsible) {
|
||||
return (
|
||||
<div className="border-b pb-3">
|
||||
<div className="border-b border-border/40 py-2.5">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex w-full items-center gap-1.5 py-1 text-left"
|
||||
className="flex w-full items-center justify-between py-0.5 text-left"
|
||||
>
|
||||
{isOpen ? (
|
||||
<ChevronDown className="h-3.5 w-3.5 shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5 shrink-0" />
|
||||
)}
|
||||
<span className="text-sm font-medium">{section.title}</span>
|
||||
{section.description && (
|
||||
<span className="text-muted-foreground ml-auto text-[10px]">
|
||||
{section.description}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{section.title}
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{section.description && (
|
||||
<span className="text-muted-foreground/60 text-[9px]">
|
||||
{section.description}
|
||||
</span>
|
||||
)}
|
||||
{isOpen ? (
|
||||
<ChevronDown className="h-3 w-3 shrink-0 text-muted-foreground/50" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3 shrink-0 text-muted-foreground/50" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
{isOpen && <div className="mt-2 space-y-3">{children}</div>}
|
||||
{isOpen && <div className="mt-1.5 space-y-1">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b pb-3">
|
||||
<div className="mb-2">
|
||||
<h4 className="text-sm font-medium">{section.title}</h4>
|
||||
<div className="border-b border-border/40 py-2.5">
|
||||
<div className="mb-1.5 flex items-center justify-between">
|
||||
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{section.title}
|
||||
</h4>
|
||||
{section.description && (
|
||||
<p className="text-muted-foreground text-[10px]">
|
||||
<span className="text-muted-foreground/60 text-[9px]">
|
||||
{section.description}
|
||||
</p>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3">{children}</div>
|
||||
<div className="space-y-1">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue