ERP-node/frontend/lib/registry/components/universal-form-modal/modals/SectionLayoutModal.tsx

941 lines
45 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Badge } from "@/components/ui/badge";
import { Plus, Trash2, GripVertical, ChevronUp, ChevronDown, Settings as SettingsIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { FormSectionConfig, FormFieldConfig, OptionalFieldGroupConfig, FIELD_TYPE_OPTIONS } from "../types";
import { defaultFieldConfig, generateFieldId, generateUniqueId } from "../config";
// 도움말 텍스트 컴포넌트
const HelpText = ({ children }: { children: React.ReactNode }) => (
<p className="text-[10px] text-muted-foreground mt-0.5">{children}</p>
);
interface SectionLayoutModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
section: FormSectionConfig;
onSave: (updates: Partial<FormSectionConfig>) => void;
onOpenFieldDetail: (field: FormFieldConfig) => void;
}
export function SectionLayoutModal({
open,
onOpenChange,
section,
onSave,
onOpenFieldDetail,
}: SectionLayoutModalProps) {
// 로컬 상태로 섹션 관리
const [localSection, setLocalSection] = useState<FormSectionConfig>(section);
// open이 변경될 때마다 데이터 동기화
useEffect(() => {
if (open) {
setLocalSection(section);
}
}, [open, section]);
// 섹션 업데이트 함수
const updateSection = (updates: Partial<FormSectionConfig>) => {
setLocalSection((prev) => ({ ...prev, ...updates }));
};
// 저장 함수
const handleSave = () => {
onSave(localSection);
onOpenChange(false);
};
// 필드 추가
const addField = () => {
const newField: FormFieldConfig = {
...defaultFieldConfig,
id: generateFieldId(),
label: `새 필드 ${localSection.fields.length + 1}`,
columnName: `field_${localSection.fields.length + 1}`,
};
updateSection({
fields: [...localSection.fields, newField],
});
};
// 필드 삭제
const removeField = (fieldId: string) => {
updateSection({
fields: localSection.fields.filter((f) => f.id !== fieldId),
});
};
// 필드 업데이트
const updateField = (fieldId: string, updates: Partial<FormFieldConfig>) => {
updateSection({
fields: localSection.fields.map((f) => (f.id === fieldId ? { ...f, ...updates } : f)),
});
};
// 필드 이동
const moveField = (fieldId: string, direction: "up" | "down") => {
const index = localSection.fields.findIndex((f) => f.id === fieldId);
if (index === -1) return;
if (direction === "up" && index === 0) return;
if (direction === "down" && index === localSection.fields.length - 1) return;
const newFields = [...localSection.fields];
const targetIndex = direction === "up" ? index - 1 : index + 1;
[newFields[index], newFields[targetIndex]] = [newFields[targetIndex], newFields[index]];
updateSection({ fields: newFields });
};
// 필드 타입별 색상
const getFieldTypeColor = (fieldType: FormFieldConfig["fieldType"]): string => {
switch (fieldType) {
case "text":
case "email":
case "password":
case "tel":
return "bg-blue-50 border-blue-200 text-blue-700";
case "number":
return "bg-cyan-50 border-cyan-200 text-cyan-700";
case "date":
case "datetime":
return "bg-purple-50 border-purple-200 text-purple-700";
case "select":
return "bg-green-50 border-green-200 text-green-700";
case "checkbox":
return "bg-pink-50 border-pink-200 text-pink-700";
case "textarea":
return "bg-orange-50 border-orange-200 text-orange-700";
default:
return "bg-gray-50 border-gray-200 text-gray-700";
}
};
// 필드 타입 라벨
const getFieldTypeLabel = (fieldType: FormFieldConfig["fieldType"]): string => {
const option = FIELD_TYPE_OPTIONS.find((opt) => opt.value === fieldType);
return option?.label || fieldType;
};
// 그리드 너비 라벨
const getGridSpanLabel = (span: number): string => {
switch (span) {
case 3:
return "1/4";
case 4:
return "1/3";
case 6:
return "1/2";
case 8:
return "2/3";
case 12:
return "전체";
default:
return `${span}/12`;
}
};
// 필드 상세 설정 열기
const handleOpenFieldDetail = (field: FormFieldConfig) => {
// 먼저 현재 변경사항 저장
onSave(localSection);
// 그 다음 필드 상세 모달 열기
onOpenFieldDetail(field);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[95vw] sm:max-w-[900px] max-h-[90vh] flex flex-col p-0">
<DialogHeader className="px-4 pt-4 pb-2 border-b shrink-0">
<DialogTitle className="text-base"> : {localSection.title}</DialogTitle>
<DialogDescription className="text-xs">
. .
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-hidden px-4">
<ScrollArea className="h-[calc(90vh-180px)]">
<div className="space-y-4 py-3 pr-3">
{/* 섹션 기본 정보 */}
<div className="space-y-3 border rounded-lg p-3 bg-card">
<h3 className="text-xs font-semibold"> </h3>
<div>
<Label className="text-[10px]"> </Label>
<Input
value={localSection.title}
onChange={(e) => updateSection({ title: e.target.value })}
className="h-7 text-xs mt-1"
/>
<HelpText> (: 기본 , )</HelpText>
</div>
<div>
<Label className="text-[10px]"> </Label>
<Textarea
value={localSection.description || ""}
onChange={(e) => updateSection({ description: e.target.value })}
className="text-xs mt-1 min-h-[50px]"
placeholder="섹션에 대한 설명 (선택사항)"
/>
<HelpText> </HelpText>
</div>
<div>
<Label className="text-[10px]"> ()</Label>
<Select
value={String(localSection.columns || 2)}
onValueChange={(value) => updateSection({ columns: parseInt(value) })}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1 ( )</SelectItem>
<SelectItem value="2">2 ()</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="4">4</SelectItem>
</SelectContent>
</Select>
<HelpText> </HelpText>
</div>
<Separator />
<div className="flex items-center justify-between">
<span className="text-[10px]"> </span>
<Switch
checked={localSection.collapsible || false}
onCheckedChange={(checked) => updateSection({ collapsible: checked })}
/>
</div>
<HelpText> </HelpText>
{localSection.collapsible && (
<>
<Separator className="my-2" />
<div className="flex items-center justify-between">
<span className="text-[10px]"> </span>
<Switch
checked={localSection.defaultCollapsed || false}
onCheckedChange={(checked) => updateSection({ defaultCollapsed: checked })}
/>
</div>
<HelpText> </HelpText>
</>
)}
</div>
{/* 반복 섹션 설정 */}
<div className="space-y-3 border rounded-lg p-3 bg-card">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold"> </span>
<Switch
checked={localSection.repeatable || false}
onCheckedChange={(checked) => updateSection({ repeatable: checked })}
/>
</div>
<HelpText>
<br />
: 경력사항, ,
</HelpText>
{localSection.repeatable && (
<div className="space-y-2 pt-2 border-t">
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-[10px]"> </Label>
<Input
type="number"
value={localSection.repeatConfig?.minItems || 0}
onChange={(e) =>
updateSection({
repeatConfig: {
...localSection.repeatConfig,
minItems: parseInt(e.target.value) || 0,
},
})
}
className="h-6 text-[10px] mt-1"
/>
</div>
<div>
<Label className="text-[10px]"> </Label>
<Input
type="number"
value={localSection.repeatConfig?.maxItems || 10}
onChange={(e) =>
updateSection({
repeatConfig: {
...localSection.repeatConfig,
maxItems: parseInt(e.target.value) || 10,
},
})
}
className="h-6 text-[10px] mt-1"
/>
</div>
</div>
<div>
<Label className="text-[10px]"> </Label>
<Input
value={localSection.repeatConfig?.addButtonText || "+ 추가"}
onChange={(e) =>
updateSection({
repeatConfig: {
...localSection.repeatConfig,
addButtonText: e.target.value,
},
})
}
className="h-6 text-[10px] mt-1"
/>
</div>
</div>
)}
</div>
{/* 필드 목록 */}
<div className="space-y-3 border rounded-lg p-3 bg-card">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="text-xs font-semibold"> </h3>
<Badge variant="secondary" className="text-[9px] px-1.5 py-0">
{localSection.fields.length}
</Badge>
</div>
<Button size="sm" variant="outline" onClick={addField} className="h-7 text-[10px] px-2">
<Plus className="h-3 w-3 mr-1" />
</Button>
</div>
<HelpText>
. "상세 설정" .
</HelpText>
{localSection.fields.length === 0 ? (
<div className="text-center py-8 border border-dashed rounded-lg">
<p className="text-sm text-muted-foreground mb-2"> </p>
<p className="text-xs text-muted-foreground"> "필드 추가" </p>
</div>
) : (
<div className="space-y-2">
{localSection.fields.map((field, index) => (
<div
key={field.id}
className={cn(
"border rounded-lg overflow-hidden",
getFieldTypeColor(field.fieldType)
)}
>
{/* 필드 헤더 */}
<div className="flex items-center justify-between p-2 bg-white/50">
<div className="flex items-center gap-2 flex-1">
{/* 순서 변경 버튼 */}
<div className="flex flex-col">
<Button
size="sm"
variant="ghost"
onClick={() => moveField(field.id, "up")}
disabled={index === 0}
className="h-3 w-5 p-0"
>
<ChevronUp className="h-2.5 w-2.5" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => moveField(field.id, "down")}
disabled={index === localSection.fields.length - 1}
className="h-3 w-5 p-0"
>
<ChevronDown className="h-2.5 w-2.5" />
</Button>
</div>
<GripVertical className="h-3.5 w-3.5 text-muted-foreground" />
{/* 필드 정보 */}
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="text-[11px] font-medium">{field.label}</span>
<Badge
variant="outline"
className={cn("text-[8px] px-1 py-0", getFieldTypeColor(field.fieldType))}
>
{getFieldTypeLabel(field.fieldType)}
</Badge>
{field.required && (
<Badge variant="destructive" className="text-[8px] px-1 py-0">
</Badge>
)}
{field.hidden && (
<Badge variant="secondary" className="text-[8px] px-1 py-0">
</Badge>
)}
</div>
<p className="text-[9px] text-muted-foreground mt-0.5">
: {field.columnName} | : {getGridSpanLabel(field.gridSpan || 6)}
</p>
</div>
</div>
{/* 액션 버튼 */}
<div className="flex items-center gap-1">
<Button
size="sm"
variant="ghost"
onClick={() => handleOpenFieldDetail(field)}
className="h-6 px-2 text-[9px]"
>
<SettingsIcon className="h-3 w-3 mr-1" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => removeField(field.id)}
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
{/* 간단한 인라인 설정 */}
<div className="px-2 pb-2 space-y-2 bg-white/30">
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-[9px]"></Label>
<Input
value={field.label}
onChange={(e) => updateField(field.id, { label: e.target.value })}
className="h-6 text-[9px] mt-0.5"
/>
</div>
<div>
<Label className="text-[9px]"></Label>
<Input
value={field.columnName}
onChange={(e) => updateField(field.id, { columnName: e.target.value })}
className="h-6 text-[9px] mt-0.5"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-[9px]"> </Label>
<Select
value={field.fieldType}
onValueChange={(value) =>
updateField(field.id, {
fieldType: value as FormFieldConfig["fieldType"],
})
}
>
<SelectTrigger className="h-6 text-[9px] mt-0.5">
<SelectValue />
</SelectTrigger>
<SelectContent>
{FIELD_TYPE_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-[9px]"></Label>
<Select
value={String(field.gridSpan || 6)}
onValueChange={(value) => updateField(field.id, { gridSpan: parseInt(value) })}
>
<SelectTrigger className="h-6 text-[9px] mt-0.5">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="3">1/4</SelectItem>
<SelectItem value="4">1/3</SelectItem>
<SelectItem value="6">1/2</SelectItem>
<SelectItem value="8">2/3</SelectItem>
<SelectItem value="12"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-center justify-between pt-1">
<span className="text-[9px]"></span>
<Switch
checked={field.required || false}
onCheckedChange={(checked) => updateField(field.id, { required: checked })}
className="scale-75"
/>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* 옵셔널 필드 그룹 */}
<div className="space-y-3 border rounded-lg p-3 bg-card">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="text-xs font-semibold"> </h3>
<Badge variant="secondary" className="text-[9px] px-1.5 py-0">
{localSection.optionalFieldGroups?.length || 0}
</Badge>
</div>
<Button
size="sm"
variant="outline"
onClick={() => {
const newGroup: OptionalFieldGroupConfig = {
id: generateUniqueId("optgroup"),
title: `옵셔널 그룹 ${(localSection.optionalFieldGroups?.length || 0) + 1}`,
fields: [],
};
updateSection({
optionalFieldGroups: [...(localSection.optionalFieldGroups || []), newGroup],
});
}}
className="h-7 text-[10px] px-2"
>
<Plus className="h-3 w-3 mr-1" />
</Button>
</div>
<HelpText>
"추가" .
<br />
: 해외 (, , )
</HelpText>
{(!localSection.optionalFieldGroups || localSection.optionalFieldGroups.length === 0) ? (
<div className="text-center py-6 border border-dashed rounded-lg">
<p className="text-xs text-muted-foreground"> </p>
</div>
) : (
<div className="space-y-3">
{localSection.optionalFieldGroups.map((group, groupIndex) => (
<div key={group.id} className="border rounded-lg p-3 bg-muted/30">
{/* 그룹 헤더 */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-[9px]"> {groupIndex + 1}</Badge>
<span className="text-xs font-medium">{group.title}</span>
<Badge variant="secondary" className="text-[8px] px-1">
{group.fields.length}
</Badge>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => {
updateSection({
optionalFieldGroups: localSection.optionalFieldGroups?.filter((g) => g.id !== group.id),
});
}}
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
{/* 그룹 기본 설정 */}
<div className="space-y-2">
{/* 제목 및 설명 */}
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-[9px]"> </Label>
<Input
value={group.title}
onChange={(e) => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id ? { ...g, title: e.target.value } : g
);
updateSection({ optionalFieldGroups: newGroups });
}}
className="mt-0.5 h-6 text-[9px]"
placeholder="예: 해외 판매 정보"
/>
</div>
<div>
<Label className="text-[9px]"> </Label>
<Input
value={group.description || ""}
onChange={(e) => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id ? { ...g, description: e.target.value } : g
);
updateSection({ optionalFieldGroups: newGroups });
}}
className="mt-0.5 h-6 text-[9px]"
placeholder="해외 판매 시 추가 정보 입력"
/>
</div>
</div>
{/* 레이아웃 및 옵션 */}
<div className="grid grid-cols-4 gap-2">
<div>
<Label className="text-[8px]"> </Label>
<Select
value={String(group.columns || "inherit")}
onValueChange={(value) => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id
? { ...g, columns: value === "inherit" ? undefined : parseInt(value) }
: g
);
updateSection({ optionalFieldGroups: newGroups });
}}
>
<SelectTrigger className="mt-0.5 h-5 text-[8px]">
<SelectValue placeholder="상속" />
</SelectTrigger>
<SelectContent>
<SelectItem value="inherit"></SelectItem>
<SelectItem value="1">1</SelectItem>
<SelectItem value="2">2</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="4">4</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-end gap-1 pb-0.5">
<Switch
checked={group.collapsible || false}
onCheckedChange={(checked) => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id ? { ...g, collapsible: checked } : g
);
updateSection({ optionalFieldGroups: newGroups });
}}
className="scale-50"
/>
<span className="text-[8px]"> </span>
</div>
<div className="flex items-end gap-1 pb-0.5">
<Switch
checked={group.defaultCollapsed || false}
onCheckedChange={(checked) => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id ? { ...g, defaultCollapsed: checked } : g
);
updateSection({ optionalFieldGroups: newGroups });
}}
disabled={!group.collapsible}
className="scale-50"
/>
<span className="text-[8px]"> </span>
</div>
<div className="flex items-end gap-1 pb-0.5">
<Switch
checked={group.confirmRemove || false}
onCheckedChange={(checked) => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id ? { ...g, confirmRemove: checked } : g
);
updateSection({ optionalFieldGroups: newGroups });
}}
className="scale-50"
/>
<span className="text-[8px]"> </span>
</div>
</div>
<Separator className="my-2" />
{/* 버튼 텍스트 설정 */}
<div>
<Label className="text-[9px] font-medium"> </Label>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-[8px]"> </Label>
<Input
value={group.addButtonText || ""}
onChange={(e) => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id ? { ...g, addButtonText: e.target.value } : g
);
updateSection({ optionalFieldGroups: newGroups });
}}
className="mt-0.5 h-5 text-[8px]"
placeholder="+ 해외 판매 설정 추가"
/>
</div>
<div>
<Label className="text-[8px]"> </Label>
<Input
value={group.removeButtonText || ""}
onChange={(e) => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id ? { ...g, removeButtonText: e.target.value } : g
);
updateSection({ optionalFieldGroups: newGroups });
}}
className="mt-0.5 h-5 text-[8px]"
placeholder="제거"
/>
</div>
</div>
<Separator className="my-2" />
{/* 연동 필드 설정 */}
<div>
<Label className="text-[9px] font-medium"> ()</Label>
<HelpText>/ </HelpText>
</div>
<div className="grid grid-cols-3 gap-2">
<div>
<Label className="text-[8px]"> ()</Label>
<Input
value={group.triggerField || ""}
onChange={(e) => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id ? { ...g, triggerField: e.target.value } : g
);
updateSection({ optionalFieldGroups: newGroups });
}}
className="mt-0.5 h-5 text-[8px]"
placeholder="sales_type"
/>
</div>
<div>
<Label className="text-[8px]"> </Label>
<Input
value={group.triggerValueOnAdd || ""}
onChange={(e) => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id ? { ...g, triggerValueOnAdd: e.target.value } : g
);
updateSection({ optionalFieldGroups: newGroups });
}}
className="mt-0.5 h-5 text-[8px]"
placeholder="해외"
/>
</div>
<div>
<Label className="text-[8px]"> </Label>
<Input
value={group.triggerValueOnRemove || ""}
onChange={(e) => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id ? { ...g, triggerValueOnRemove: e.target.value } : g
);
updateSection({ optionalFieldGroups: newGroups });
}}
className="mt-0.5 h-5 text-[8px]"
placeholder="국내"
/>
</div>
</div>
<Separator className="my-2" />
{/* 그룹 내 필드 목록 */}
<div className="flex items-center justify-between">
<Label className="text-[9px] font-medium"> </Label>
<Button
size="sm"
variant="outline"
onClick={() => {
const newField: FormFieldConfig = {
...defaultFieldConfig,
id: generateFieldId(),
label: `필드 ${group.fields.length + 1}`,
columnName: `field_${group.fields.length + 1}`,
};
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id ? { ...g, fields: [...g.fields, newField] } : g
);
updateSection({ optionalFieldGroups: newGroups });
}}
className="h-5 text-[8px] px-1.5"
>
<Plus className="h-2.5 w-2.5 mr-0.5" />
</Button>
</div>
{group.fields.length === 0 ? (
<div className="text-center py-3 border border-dashed rounded">
<p className="text-[9px] text-muted-foreground"> </p>
</div>
) : (
<div className="space-y-1">
{group.fields.map((field, fieldIndex) => (
<div key={field.id} className="flex items-center gap-2 bg-white/50 rounded p-1.5">
<div className="flex-1 grid grid-cols-4 gap-1">
<Input
value={field.label}
onChange={(e) => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id
? {
...g,
fields: g.fields.map((f) =>
f.id === field.id ? { ...f, label: e.target.value } : f
),
}
: g
);
updateSection({ optionalFieldGroups: newGroups });
}}
className="h-5 text-[8px]"
placeholder="라벨"
/>
<Input
value={field.columnName}
onChange={(e) => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id
? {
...g,
fields: g.fields.map((f) =>
f.id === field.id ? { ...f, columnName: e.target.value } : f
),
}
: g
);
updateSection({ optionalFieldGroups: newGroups });
}}
className="h-5 text-[8px]"
placeholder="컬럼명"
/>
<Select
value={field.fieldType}
onValueChange={(value) => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id
? {
...g,
fields: g.fields.map((f) =>
f.id === field.id
? { ...f, fieldType: value as FormFieldConfig["fieldType"] }
: f
),
}
: g
);
updateSection({ optionalFieldGroups: newGroups });
}}
>
<SelectTrigger className="h-5 text-[8px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{FIELD_TYPE_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={String(field.gridSpan || 3)}
onValueChange={(value) => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id
? {
...g,
fields: g.fields.map((f) =>
f.id === field.id ? { ...f, gridSpan: parseInt(value) } : f
),
}
: g
);
updateSection({ optionalFieldGroups: newGroups });
}}
>
<SelectTrigger className="h-5 text-[8px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="3">1/4</SelectItem>
<SelectItem value="4">1/3</SelectItem>
<SelectItem value="6">1/2</SelectItem>
<SelectItem value="12"></SelectItem>
</SelectContent>
</Select>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => handleOpenFieldDetail(field)}
className="h-5 px-1.5 text-[8px]"
>
<SettingsIcon className="h-2.5 w-2.5 mr-0.5" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => {
const newGroups = localSection.optionalFieldGroups?.map((g) =>
g.id === group.id
? { ...g, fields: g.fields.filter((f) => f.id !== field.id) }
: g
);
updateSection({ optionalFieldGroups: newGroups });
}}
className="h-5 w-5 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-2.5 w-2.5" />
</Button>
</div>
))}
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
</ScrollArea>
</div>
<DialogFooter className="px-4 py-3 border-t shrink-0">
<Button variant="outline" onClick={() => onOpenChange(false)} className="h-9 text-sm">
</Button>
<Button onClick={handleSave} className="h-9 text-sm">
({localSection.fields.length} )
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}