ERP-node/frontend/components/v2/config-panels/V2TableGroupedConfigPanel.tsx

774 lines
32 KiB
TypeScript
Raw Normal View History

"use client";
/**
* V2TableGrouped
* UX: 데이터 -> -> -> () -> ()
* TableGroupedConfigPanel의 UI로
*/
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Separator } from "@/components/ui/separator";
import {
Table2,
Database,
Layers,
Columns3,
Check,
ChevronsUpDown,
Settings,
ChevronDown,
Loader2,
Link2,
Plus,
Trash2,
FoldVertical,
ArrowUpDown,
CheckSquare,
LayoutGrid,
Type,
Hash,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { tableTypeApi } from "@/lib/api/screen";
import type { TableGroupedConfig, LinkedFilterConfig } from "@/lib/registry/components/v2-table-grouped/types";
import type { ColumnConfig } from "@/lib/registry/components/v2-table-list/types";
import {
groupHeaderStyleOptions,
checkboxModeOptions,
sortDirectionOptions,
} from "@/lib/registry/components/v2-table-grouped/config";
// ─── 섹션 헤더 컴포넌트 ───
function SectionHeader({ icon: Icon, title, description }: {
icon: React.ComponentType<{ className?: string }>;
title: string;
description?: string;
}) {
return (
<div className="space-y-1">
<div className="flex items-center gap-2">
<Icon className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">{title}</h3>
</div>
{description && <p className="text-muted-foreground text-[10px]">{description}</p>}
</div>
);
}
// ─── 수평 Switch Row (토스 패턴) ───
function SwitchRow({ label, description, checked, onCheckedChange }: {
label: string;
description?: string;
checked: boolean;
onCheckedChange: (checked: boolean) => void;
}) {
return (
<div className="flex items-center justify-between py-1">
<div className="space-y-0.5">
<p className="text-sm">{label}</p>
{description && <p className="text-[11px] text-muted-foreground">{description}</p>}
</div>
<Switch checked={checked} onCheckedChange={onCheckedChange} />
</div>
);
}
// ─── 수평 라벨 + 컨트롤 Row ───
function LabeledRow({ label, description, children }: {
label: string;
description?: string;
children: React.ReactNode;
}) {
return (
<div className="flex items-center justify-between py-1">
<div className="space-y-0.5">
<p className="text-xs text-muted-foreground">{label}</p>
{description && <p className="text-[10px] text-muted-foreground">{description}</p>}
</div>
{children}
</div>
);
}
// ─── 그룹 헤더 스타일 카드 ───
const HEADER_STYLE_CARDS = [
{ value: "default", icon: LayoutGrid, title: "기본", description: "표준 그룹 헤더" },
{ value: "compact", icon: FoldVertical, title: "컴팩트", description: "간결한 헤더" },
{ value: "card", icon: Layers, title: "카드", description: "카드 스타일 헤더" },
] as const;
interface V2TableGroupedConfigPanelProps {
config: TableGroupedConfig;
onChange: (newConfig: Partial<TableGroupedConfig>) => void;
}
export const V2TableGroupedConfigPanel: React.FC<V2TableGroupedConfigPanelProps> = ({
config,
onChange,
}) => {
// componentConfigChanged 이벤트 발행 래퍼
const handleChange = useCallback((newConfig: Partial<TableGroupedConfig>) => {
onChange(newConfig);
if (typeof window !== "undefined") {
window.dispatchEvent(
new CustomEvent("componentConfigChanged", {
detail: { config: { ...config, ...newConfig } },
})
);
}
}, [onChange, config]);
const updateConfig = useCallback((updates: Partial<TableGroupedConfig>) => {
handleChange({ ...config, ...updates });
}, [handleChange, config]);
const updateGroupConfig = useCallback((updates: Partial<TableGroupedConfig["groupConfig"]>) => {
handleChange({
...config,
groupConfig: { ...config.groupConfig, ...updates },
});
}, [handleChange, config]);
// ─── 상태 ───
const [tables, setTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
const [tableColumns, setTableColumns] = useState<ColumnConfig[]>([]);
const [loadingTables, setLoadingTables] = useState(false);
const [loadingColumns, setLoadingColumns] = useState(false);
const [tableComboboxOpen, setTableComboboxOpen] = useState(false);
// Collapsible 상태
const [displayOpen, setDisplayOpen] = useState(false);
const [linkedOpen, setLinkedOpen] = useState(false);
// ─── 실제 사용할 테이블 이름 ───
const targetTableName = useMemo(() => {
if (config.useCustomTable && config.customTableName) {
return config.customTableName;
}
return config.selectedTable;
}, [config.useCustomTable, config.customTableName, config.selectedTable]);
// ─── 테이블 목록 로드 ───
useEffect(() => {
const loadTables = async () => {
setLoadingTables(true);
try {
const tableList = await tableTypeApi.getTables();
if (tableList && Array.isArray(tableList)) {
setTables(
tableList.map((t: any) => ({
tableName: t.tableName || t.table_name,
displayName: t.displayName || t.display_name || t.tableName || t.table_name,
}))
);
}
} catch (err) {
console.error("테이블 목록 로드 실패:", err);
} finally {
setLoadingTables(false);
}
};
loadTables();
}, []);
// ─── 선택된 테이블의 컬럼 로드 ───
useEffect(() => {
if (!targetTableName) {
setTableColumns([]);
return;
}
const loadColumns = async () => {
setLoadingColumns(true);
try {
const columns = await tableTypeApi.getColumns(targetTableName);
if (columns && Array.isArray(columns)) {
const cols: ColumnConfig[] = columns.map((col: any, idx: number) => ({
columnName: col.column_name || col.columnName,
displayName: col.display_name || col.displayName || col.column_name || col.columnName,
visible: true,
sortable: true,
searchable: false,
align: "left" as const,
order: idx,
}));
setTableColumns(cols);
if (!config.columns || config.columns.length === 0) {
updateConfig({ columns: cols });
}
}
} catch (err) {
console.error("컬럼 로드 실패:", err);
} finally {
setLoadingColumns(false);
}
};
loadColumns();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [targetTableName]);
// ─── 테이블 변경 핸들러 ───
const handleTableChange = useCallback((newTableName: string) => {
if (newTableName === config.selectedTable) return;
updateConfig({ selectedTable: newTableName, columns: [] });
setTableComboboxOpen(false);
}, [config.selectedTable, updateConfig]);
// ─── 컬럼 가시성 토글 ───
const toggleColumnVisibility = useCallback((columnName: string) => {
const updatedColumns = (config.columns || []).map((col) =>
col.columnName === columnName ? { ...col, visible: !col.visible } : col
);
updateConfig({ columns: updatedColumns });
}, [config.columns, updateConfig]);
// ─── 합계 컬럼 토글 ───
const toggleSumColumn = useCallback((columnName: string) => {
const currentSumCols = config.groupConfig?.summary?.sumColumns || [];
const newSumCols = currentSumCols.includes(columnName)
? currentSumCols.filter((c) => c !== columnName)
: [...currentSumCols, columnName];
updateGroupConfig({
summary: {
...config.groupConfig?.summary,
sumColumns: newSumCols,
},
});
}, [config.groupConfig?.summary, updateGroupConfig]);
// ─── 연결 필터 관리 ───
const addLinkedFilter = useCallback(() => {
const newFilter: LinkedFilterConfig = {
sourceComponentId: "",
sourceField: "value",
targetColumn: "",
enabled: true,
};
updateConfig({
linkedFilters: [...(config.linkedFilters || []), newFilter],
});
}, [config.linkedFilters, updateConfig]);
const removeLinkedFilter = useCallback((index: number) => {
const filters = [...(config.linkedFilters || [])];
filters.splice(index, 1);
updateConfig({ linkedFilters: filters });
}, [config.linkedFilters, updateConfig]);
const updateLinkedFilter = useCallback((index: number, updates: Partial<LinkedFilterConfig>) => {
const filters = [...(config.linkedFilters || [])];
filters[index] = { ...filters[index], ...updates };
updateConfig({ linkedFilters: filters });
}, [config.linkedFilters, updateConfig]);
// ─── 렌더링 ───
return (
<div className="space-y-4">
{/* ═══════════════════════════════════════ */}
{/* 1단계: 데이터 소스 (테이블 선택) */}
{/* ═══════════════════════════════════════ */}
<div className="space-y-3">
<SectionHeader icon={Table2} title="데이터 소스" description="그룹화할 테이블을 선택하세요" />
<Separator />
<SwitchRow
label="커스텀 테이블 사용"
description="화면 메인 테이블 대신 다른 테이블을 사용합니다"
checked={config.useCustomTable ?? false}
onCheckedChange={(checked) => updateConfig({ useCustomTable: checked })}
/>
{config.useCustomTable ? (
<Input
value={config.customTableName || ""}
onChange={(e) => updateConfig({ customTableName: e.target.value })}
placeholder="테이블명을 직접 입력하세요"
className="h-8 text-xs"
/>
) : (
<Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={tableComboboxOpen}
className="h-8 w-full justify-between text-xs"
disabled={loadingTables}
>
<div className="flex items-center gap-2 truncate">
<Table2 className="h-3 w-3 shrink-0" />
<span className="truncate">
{loadingTables
? "테이블 로딩 중..."
: config.selectedTable
? tables.find((t) => t.tableName === config.selectedTable)?.displayName || config.selectedTable
: "테이블 선택"}
</span>
</div>
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
<Command
filter={(value, search) => {
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
return 0;
}}
>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="text-xs"> .</CommandEmpty>
<CommandGroup>
{tables.map((table) => (
<CommandItem
key={table.tableName}
value={`${table.displayName} ${table.tableName}`}
onSelect={() => handleTableChange(table.tableName)}
className="text-xs"
>
<Check
className={cn("mr-2 h-3 w-3", config.selectedTable === table.tableName ? "opacity-100" : "opacity-0")}
/>
<div className="flex flex-col">
<span>{table.displayName}</span>
{table.displayName !== table.tableName && (
<span className="text-[10px] text-muted-foreground/70">{table.tableName}</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)}
</div>
{/* ═══════════════════════════════════════ */}
{/* 2단계: 그룹화 설정 */}
{/* ═══════════════════════════════════════ */}
{targetTableName && (
<div className="space-y-3">
<SectionHeader icon={Layers} title="그룹화 설정" description="데이터를 어떤 컬럼 기준으로 그룹화할지 설정합니다" />
<Separator />
{/* 그룹화 기준 컬럼 */}
<LabeledRow label="그룹화 기준 컬럼 *">
<Select
value={config.groupConfig?.groupByColumn || ""}
onValueChange={(value) => updateGroupConfig({ groupByColumn: value })}
>
<SelectTrigger className="h-7 w-[160px] text-xs">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{(loadingColumns ? [] : tableColumns).map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.displayName || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</LabeledRow>
{/* 그룹 라벨 형식 */}
<div className="space-y-1">
<div className="flex items-center gap-2">
<Type className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-medium"> </span>
</div>
<Input
value={config.groupConfig?.groupLabelFormat || "{value}"}
onChange={(e) => updateGroupConfig({ groupLabelFormat: e.target.value })}
placeholder="{value} ({컬럼명})"
className="h-7 text-xs"
/>
<p className="text-[10px] text-muted-foreground">
{"{value}"} = , {"{컬럼명}"} =
</p>
</div>
<SwitchRow
label="기본 펼침 상태"
description="그룹이 기본으로 펼쳐진 상태로 표시됩니다"
checked={config.groupConfig?.defaultExpanded ?? true}
onCheckedChange={(checked) => updateGroupConfig({ defaultExpanded: checked })}
/>
{/* 그룹 정렬 */}
<LabeledRow label="그룹 정렬">
<Select
value={config.groupConfig?.sortDirection || "asc"}
onValueChange={(value: string) => updateGroupConfig({ sortDirection: value as "asc" | "desc" })}
>
<SelectTrigger className="h-7 w-[120px] text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{sortDirectionOptions.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</LabeledRow>
<SwitchRow
label="개수 표시"
description="그룹 헤더에 항목 수를 표시합니다"
checked={config.groupConfig?.summary?.showCount ?? true}
onCheckedChange={(checked) =>
updateGroupConfig({
summary: { ...config.groupConfig?.summary, showCount: checked },
})
}
/>
{/* 합계 컬럼 */}
{tableColumns.length > 0 && (
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<Hash className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-medium"> </span>
</div>
<p className="text-[10px] text-muted-foreground"> </p>
<div className="max-h-28 space-y-0.5 overflow-y-auto rounded-md border p-2">
{tableColumns.map((col) => {
const isChecked = config.groupConfig?.summary?.sumColumns?.includes(col.columnName) ?? false;
return (
<div
key={col.columnName}
className={cn(
"flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-muted/50",
isChecked && "bg-primary/10",
)}
onClick={() => toggleSumColumn(col.columnName)}
>
<Checkbox
checked={isChecked}
onCheckedChange={() => toggleSumColumn(col.columnName)}
className="pointer-events-none h-3.5 w-3.5"
/>
<span className="truncate text-xs">{col.displayName || col.columnName}</span>
</div>
);
})}
</div>
</div>
)}
</div>
)}
{/* 테이블 미선택 안내 */}
{!targetTableName && (
<div className="rounded-lg border-2 border-dashed p-6 text-center">
<Table2 className="mx-auto mb-2 h-8 w-8 text-muted-foreground opacity-30" />
<p className="text-sm text-muted-foreground"> </p>
<p className="text-xs text-muted-foreground"> </p>
</div>
)}
{/* ═══════════════════════════════════════ */}
{/* 3단계: 컬럼 선택 */}
{/* ═══════════════════════════════════════ */}
{targetTableName && (config.columns || tableColumns).length > 0 && (
<div className="space-y-3">
<SectionHeader
icon={Columns3}
title={`컬럼 선택 (${(config.columns || tableColumns).filter((c) => c.visible !== false).length}개 표시)`}
description="표시할 컬럼을 선택하세요"
/>
<Separator />
<div className="max-h-48 space-y-0.5 overflow-y-auto rounded-md border p-2">
{(config.columns || tableColumns).map((col) => {
const isVisible = col.visible !== false;
return (
<div
key={col.columnName}
className={cn(
"flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-muted/50",
isVisible && "bg-primary/10",
)}
onClick={() => toggleColumnVisibility(col.columnName)}
>
<Checkbox
checked={isVisible}
onCheckedChange={() => toggleColumnVisibility(col.columnName)}
className="pointer-events-none h-3.5 w-3.5"
/>
<Database className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
<span className="truncate text-xs">{col.displayName || col.columnName}</span>
</div>
);
})}
</div>
</div>
)}
{/* ═══════════════════════════════════════ */}
{/* 4단계: 그룹 헤더 스타일 (카드 선택) */}
{/* ═══════════════════════════════════════ */}
{targetTableName && (
<div className="space-y-3">
<SectionHeader icon={LayoutGrid} title="그룹 헤더 스타일" description="그룹 헤더의 디자인을 선택하세요" />
<Separator />
<div className="grid grid-cols-3 gap-2">
{HEADER_STYLE_CARDS.map((card) => {
const Icon = card.icon;
const isSelected = (config.groupHeaderStyle || "default") === card.value;
return (
<button
key={card.value}
type="button"
onClick={() => updateConfig({ groupHeaderStyle: card.value as "default" | "compact" | "card" })}
className={cn(
"flex min-h-[70px] flex-col items-center justify-center rounded-lg border p-2.5 text-center transition-all",
isSelected
? "border-primary bg-primary/5 ring-1 ring-primary/20"
: "border-border hover:border-primary/50 hover:bg-muted/50"
)}
>
<Icon className="mb-1 h-4 w-4 text-primary" />
<span className="text-xs font-medium leading-tight">{card.title}</span>
<span className="mt-0.5 text-[10px] leading-tight text-muted-foreground">{card.description}</span>
</button>
);
})}
</div>
</div>
)}
{/* ═══════════════════════════════════════ */}
{/* 5단계: 표시 설정 (기본 접힘) */}
{/* ═══════════════════════════════════════ */}
<Collapsible open={displayOpen} onOpenChange={setDisplayOpen}>
<CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
>
<div className="flex items-center gap-2">
<Settings className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span>
</div>
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", displayOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
{/* 체크박스 */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<CheckSquare className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-medium"></span>
</div>
<SwitchRow
label="체크박스 표시"
description="행 선택용 체크박스를 표시합니다"
checked={config.showCheckbox ?? false}
onCheckedChange={(checked) => updateConfig({ showCheckbox: checked })}
/>
{config.showCheckbox && (
<div className="ml-4 border-l-2 border-primary/20 pl-3">
<LabeledRow label="선택 모드">
<Select
value={config.checkboxMode || "multi"}
onValueChange={(value: string) => updateConfig({ checkboxMode: value as "single" | "multi" })}
>
<SelectTrigger className="h-7 w-[120px] text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{checkboxModeOptions.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</LabeledRow>
</div>
)}
</div>
<Separator />
{/* UI 옵션 */}
<SwitchRow
label="펼치기/접기 버튼 표시"
description="전체 펼치기/접기 버튼을 상단에 표시합니다"
checked={config.showExpandAllButton ?? true}
onCheckedChange={(checked) => updateConfig({ showExpandAllButton: checked })}
/>
<SwitchRow
label="행 클릭 가능"
description="행 클릭 시 이벤트를 발생시킵니다"
checked={config.rowClickable ?? true}
onCheckedChange={(checked) => updateConfig({ rowClickable: checked })}
/>
<Separator />
{/* 높이 및 메시지 */}
<LabeledRow label="최대 높이 (px)">
<Input
type="number"
value={typeof config.maxHeight === "number" ? config.maxHeight : 600}
onChange={(e) => updateConfig({ maxHeight: parseInt(e.target.value) || 600 })}
min={200}
max={2000}
className="h-7 w-[100px] text-xs"
/>
</LabeledRow>
<div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span>
<Input
value={config.emptyMessage || ""}
onChange={(e) => updateConfig({ emptyMessage: e.target.value })}
placeholder="데이터가 없습니다."
className="h-7 text-xs"
/>
</div>
</div>
</CollapsibleContent>
</Collapsible>
{/* ═══════════════════════════════════════ */}
{/* 6단계: 연동 설정 (기본 접힘) */}
{/* ═══════════════════════════════════════ */}
<Collapsible open={linkedOpen} onOpenChange={setLinkedOpen}>
<CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
>
<div className="flex items-center gap-2">
<Link2 className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span>
{(config.linkedFilters?.length ?? 0) > 0 && (
<span className="rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary">
{config.linkedFilters!.length}
</span>
)}
</div>
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", linkedOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
<div className="flex items-center justify-between">
<p className="text-[10px] text-muted-foreground">
( )
</p>
<Button
type="button"
variant="outline"
size="sm"
onClick={addLinkedFilter}
className="h-6 shrink-0 px-2 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{(config.linkedFilters || []).length === 0 ? (
<div className="rounded-lg border-2 border-dashed py-4 text-center">
<Link2 className="mx-auto mb-1 h-6 w-6 text-muted-foreground opacity-30" />
<p className="text-xs text-muted-foreground"> </p>
</div>
) : (
<div className="space-y-2">
{(config.linkedFilters || []).map((filter, idx) => (
<div key={idx} className="space-y-2 rounded-md border p-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium"> #{idx + 1}</span>
<div className="flex items-center gap-2">
<Switch
checked={filter.enabled !== false}
onCheckedChange={(checked) => updateLinkedFilter(idx, { enabled: checked })}
/>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeLinkedFilter(idx)}
className="h-5 w-5 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
<div className="space-y-1">
<span className="text-[10px] text-muted-foreground"> ID</span>
<Input
value={filter.sourceComponentId}
onChange={(e) => updateLinkedFilter(idx, { sourceComponentId: e.target.value })}
placeholder="예: search-filter-1"
className="h-6 text-xs"
/>
</div>
<div className="space-y-1">
<span className="text-[10px] text-muted-foreground"> </span>
<Input
value={filter.sourceField || "value"}
onChange={(e) => updateLinkedFilter(idx, { sourceField: e.target.value })}
placeholder="value"
className="h-6 text-xs"
/>
</div>
<div className="space-y-1">
<span className="text-[10px] text-muted-foreground"> </span>
<Select
value={filter.targetColumn}
onValueChange={(value) => updateLinkedFilter(idx, { targetColumn: value })}
>
<SelectTrigger className="h-6 text-xs">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{tableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.displayName || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
))}
</div>
)}
</div>
</CollapsibleContent>
</Collapsible>
</div>
);
};
V2TableGroupedConfigPanel.displayName = "V2TableGroupedConfigPanel";
export default V2TableGroupedConfigPanel;