컴포넌트 리뉴얼 1.0
This commit is contained in:
676
frontend/components/unified/UnifiedSelect.tsx
Normal file
676
frontend/components/unified/UnifiedSelect.tsx
Normal file
@@ -0,0 +1,676 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* UnifiedSelect
|
||||
*
|
||||
* 통합 선택 컴포넌트
|
||||
* - dropdown: 드롭다운 선택
|
||||
* - radio: 라디오 버튼 그룹
|
||||
* - check: 체크박스 그룹
|
||||
* - tag: 태그 선택
|
||||
* - toggle: 토글 스위치
|
||||
* - swap: 스왑 선택 (좌우 이동)
|
||||
*/
|
||||
|
||||
import React, { forwardRef, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { UnifiedSelectProps, SelectOption } from "@/types/unified-components";
|
||||
import { Check, ChevronsUpDown, X, ArrowLeftRight } from "lucide-react";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
|
||||
/**
|
||||
* 드롭다운 선택 컴포넌트
|
||||
*/
|
||||
const DropdownSelect = forwardRef<HTMLButtonElement, {
|
||||
options: SelectOption[];
|
||||
value?: string | string[];
|
||||
onChange?: (value: string | string[]) => void;
|
||||
placeholder?: string;
|
||||
searchable?: boolean;
|
||||
multiple?: boolean;
|
||||
maxSelect?: number;
|
||||
allowClear?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}>(({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "선택",
|
||||
searchable,
|
||||
multiple,
|
||||
maxSelect,
|
||||
allowClear = true,
|
||||
disabled,
|
||||
className
|
||||
}, ref) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// 단일 선택 + 검색 불가능 → 기본 Select 사용
|
||||
if (!searchable && !multiple) {
|
||||
return (
|
||||
<Select
|
||||
value={typeof value === "string" ? value : value?.[0] ?? ""}
|
||||
onValueChange={(v) => onChange?.(v)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger ref={ref} className={cn("h-10", className)}>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
// 검색 가능 또는 다중 선택 → Combobox 사용
|
||||
const selectedValues = useMemo(() => {
|
||||
if (!value) return [];
|
||||
return Array.isArray(value) ? value : [value];
|
||||
}, [value]);
|
||||
|
||||
const selectedLabels = useMemo(() => {
|
||||
return selectedValues
|
||||
.map((v) => options.find((o) => o.value === v)?.label)
|
||||
.filter(Boolean) as string[];
|
||||
}, [selectedValues, options]);
|
||||
|
||||
const handleSelect = useCallback((selectedValue: string) => {
|
||||
if (multiple) {
|
||||
const newValues = selectedValues.includes(selectedValue)
|
||||
? selectedValues.filter((v) => v !== selectedValue)
|
||||
: maxSelect && selectedValues.length >= maxSelect
|
||||
? selectedValues
|
||||
: [...selectedValues, selectedValue];
|
||||
onChange?.(newValues);
|
||||
} else {
|
||||
onChange?.(selectedValue);
|
||||
setOpen(false);
|
||||
}
|
||||
}, [multiple, selectedValues, maxSelect, onChange]);
|
||||
|
||||
const handleClear = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onChange?.(multiple ? [] : "");
|
||||
}, [multiple, onChange]);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
disabled={disabled}
|
||||
className={cn("h-10 w-full justify-between font-normal", className)}
|
||||
>
|
||||
<span className="truncate flex-1 text-left">
|
||||
{selectedLabels.length > 0
|
||||
? multiple
|
||||
? `${selectedLabels.length}개 선택됨`
|
||||
: selectedLabels[0]
|
||||
: placeholder}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 ml-2">
|
||||
{allowClear && selectedValues.length > 0 && (
|
||||
<X
|
||||
className="h-4 w-4 opacity-50 hover:opacity-100"
|
||||
onClick={handleClear}
|
||||
/>
|
||||
)}
|
||||
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
||||
<Command
|
||||
filter={(value, search) => {
|
||||
// value는 CommandItem의 value (라벨)
|
||||
// search는 검색어
|
||||
if (!search) return 1;
|
||||
const normalizedValue = value.toLowerCase();
|
||||
const normalizedSearch = search.toLowerCase();
|
||||
if (normalizedValue.includes(normalizedSearch)) return 1;
|
||||
return 0;
|
||||
}}
|
||||
>
|
||||
{searchable && <CommandInput placeholder="검색..." className="h-9" />}
|
||||
<CommandList>
|
||||
<CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option) => {
|
||||
const displayLabel = option.label || option.value || "(빈 값)";
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={displayLabel}
|
||||
onSelect={() => handleSelect(option.value)}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
selectedValues.includes(option.value) ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{displayLabel}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
DropdownSelect.displayName = "DropdownSelect";
|
||||
|
||||
/**
|
||||
* 라디오 선택 컴포넌트
|
||||
*/
|
||||
const RadioSelect = forwardRef<HTMLDivElement, {
|
||||
options: SelectOption[];
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}>(({ options, value, onChange, disabled, className }, ref) => {
|
||||
return (
|
||||
<RadioGroup
|
||||
ref={ref}
|
||||
value={value ?? ""}
|
||||
onValueChange={onChange}
|
||||
disabled={disabled}
|
||||
className={cn("flex flex-wrap gap-4", className)}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<div key={option.value} className="flex items-center space-x-2">
|
||||
<RadioGroupItem value={option.value} id={`radio-${option.value}`} />
|
||||
<Label htmlFor={`radio-${option.value}`} className="text-sm cursor-pointer">
|
||||
{option.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
);
|
||||
});
|
||||
RadioSelect.displayName = "RadioSelect";
|
||||
|
||||
/**
|
||||
* 체크박스 선택 컴포넌트
|
||||
*/
|
||||
const CheckSelect = forwardRef<HTMLDivElement, {
|
||||
options: SelectOption[];
|
||||
value?: string[];
|
||||
onChange?: (value: string[]) => void;
|
||||
maxSelect?: number;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}>(({ options, value = [], onChange, maxSelect, disabled, className }, ref) => {
|
||||
const handleChange = useCallback((optionValue: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
if (maxSelect && value.length >= maxSelect) return;
|
||||
onChange?.([...value, optionValue]);
|
||||
} else {
|
||||
onChange?.(value.filter((v) => v !== optionValue));
|
||||
}
|
||||
}, [value, maxSelect, onChange]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn("flex flex-wrap gap-4", className)}>
|
||||
{options.map((option) => (
|
||||
<div key={option.value} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`check-${option.value}`}
|
||||
checked={value.includes(option.value)}
|
||||
onCheckedChange={(checked) => handleChange(option.value, checked as boolean)}
|
||||
disabled={disabled || (maxSelect && value.length >= maxSelect && !value.includes(option.value))}
|
||||
/>
|
||||
<Label htmlFor={`check-${option.value}`} className="text-sm cursor-pointer">
|
||||
{option.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CheckSelect.displayName = "CheckSelect";
|
||||
|
||||
/**
|
||||
* 태그 선택 컴포넌트
|
||||
*/
|
||||
const TagSelect = forwardRef<HTMLDivElement, {
|
||||
options: SelectOption[];
|
||||
value?: string[];
|
||||
onChange?: (value: string[]) => void;
|
||||
maxSelect?: number;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}>(({ options, value = [], onChange, maxSelect, disabled, className }, ref) => {
|
||||
const handleToggle = useCallback((optionValue: string) => {
|
||||
const isSelected = value.includes(optionValue);
|
||||
if (isSelected) {
|
||||
onChange?.(value.filter((v) => v !== optionValue));
|
||||
} else {
|
||||
if (maxSelect && value.length >= maxSelect) return;
|
||||
onChange?.([...value, optionValue]);
|
||||
}
|
||||
}, [value, maxSelect, onChange]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn("flex flex-wrap gap-2", className)}>
|
||||
{options.map((option) => {
|
||||
const isSelected = value.includes(option.value);
|
||||
return (
|
||||
<Badge
|
||||
key={option.value}
|
||||
variant={isSelected ? "default" : "outline"}
|
||||
className={cn(
|
||||
"cursor-pointer transition-colors",
|
||||
disabled && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
onClick={() => !disabled && handleToggle(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
{isSelected && <X className="ml-1 h-3 w-3" />}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
TagSelect.displayName = "TagSelect";
|
||||
|
||||
/**
|
||||
* 토글 선택 컴포넌트 (Boolean용)
|
||||
*/
|
||||
const ToggleSelect = forwardRef<HTMLDivElement, {
|
||||
options: SelectOption[];
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}>(({ options, value, onChange, disabled, className }, ref) => {
|
||||
// 토글은 2개 옵션만 지원
|
||||
const [offOption, onOption] = options.length >= 2
|
||||
? [options[0], options[1]]
|
||||
: [{ value: "false", label: "아니오" }, { value: "true", label: "예" }];
|
||||
|
||||
const isOn = value === onOption.value;
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn("flex items-center gap-3", className)}>
|
||||
<span className={cn("text-sm", !isOn && "font-medium")}>{offOption.label}</span>
|
||||
<Switch
|
||||
checked={isOn}
|
||||
onCheckedChange={(checked) => onChange?.(checked ? onOption.value : offOption.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<span className={cn("text-sm", isOn && "font-medium")}>{onOption.label}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
ToggleSelect.displayName = "ToggleSelect";
|
||||
|
||||
/**
|
||||
* 스왑 선택 컴포넌트 (좌우 이동 방식)
|
||||
*/
|
||||
const SwapSelect = forwardRef<HTMLDivElement, {
|
||||
options: SelectOption[];
|
||||
value?: string[];
|
||||
onChange?: (value: string[]) => void;
|
||||
maxSelect?: number;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}>(({ options, value = [], onChange, disabled, className }, ref) => {
|
||||
const available = useMemo(() =>
|
||||
options.filter((o) => !value.includes(o.value)),
|
||||
[options, value]
|
||||
);
|
||||
|
||||
const selected = useMemo(() =>
|
||||
options.filter((o) => value.includes(o.value)),
|
||||
[options, value]
|
||||
);
|
||||
|
||||
const handleMoveRight = useCallback((optionValue: string) => {
|
||||
onChange?.([...value, optionValue]);
|
||||
}, [value, onChange]);
|
||||
|
||||
const handleMoveLeft = useCallback((optionValue: string) => {
|
||||
onChange?.(value.filter((v) => v !== optionValue));
|
||||
}, [value, onChange]);
|
||||
|
||||
const handleMoveAllRight = useCallback(() => {
|
||||
onChange?.(options.map((o) => o.value));
|
||||
}, [options, onChange]);
|
||||
|
||||
const handleMoveAllLeft = useCallback(() => {
|
||||
onChange?.([]);
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn("flex gap-2 items-stretch", className)}>
|
||||
{/* 왼쪽: 선택 가능 */}
|
||||
<div className="flex-1 border rounded-md">
|
||||
<div className="p-2 bg-muted text-xs font-medium border-b">선택 가능</div>
|
||||
<div className="p-2 space-y-1 max-h-40 overflow-y-auto">
|
||||
{available.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={cn(
|
||||
"p-2 text-sm rounded cursor-pointer hover:bg-accent",
|
||||
disabled && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
onClick={() => !disabled && handleMoveRight(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
{available.length === 0 && (
|
||||
<div className="text-xs text-muted-foreground p-2">항목 없음</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 중앙: 이동 버튼 */}
|
||||
<div className="flex flex-col gap-1 justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={handleMoveAllRight}
|
||||
disabled={disabled || available.length === 0}
|
||||
>
|
||||
<ArrowLeftRight className="h-4 w-4 rotate-180" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={handleMoveAllLeft}
|
||||
disabled={disabled || selected.length === 0}
|
||||
>
|
||||
<ArrowLeftRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 오른쪽: 선택됨 */}
|
||||
<div className="flex-1 border rounded-md">
|
||||
<div className="p-2 bg-primary/10 text-xs font-medium border-b">선택됨</div>
|
||||
<div className="p-2 space-y-1 max-h-40 overflow-y-auto">
|
||||
{selected.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={cn(
|
||||
"p-2 text-sm rounded cursor-pointer hover:bg-accent flex justify-between items-center",
|
||||
disabled && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
onClick={() => !disabled && handleMoveLeft(option.value)}
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
<X className="h-3 w-3 opacity-50" />
|
||||
</div>
|
||||
))}
|
||||
{selected.length === 0 && (
|
||||
<div className="text-xs text-muted-foreground p-2">선택 없음</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
SwapSelect.displayName = "SwapSelect";
|
||||
|
||||
/**
|
||||
* 메인 UnifiedSelect 컴포넌트
|
||||
*/
|
||||
export const UnifiedSelect = forwardRef<HTMLDivElement, UnifiedSelectProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
id,
|
||||
label,
|
||||
required,
|
||||
readonly,
|
||||
disabled,
|
||||
style,
|
||||
size,
|
||||
config: configProp,
|
||||
value,
|
||||
onChange,
|
||||
} = props;
|
||||
|
||||
// config가 없으면 기본값 사용
|
||||
const config = configProp || { mode: "dropdown" as const, source: "static" as const, options: [] };
|
||||
|
||||
const [options, setOptions] = useState<SelectOption[]>(config.options || []);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 데이터 소스에 따른 옵션 로딩
|
||||
useEffect(() => {
|
||||
const loadOptions = async () => {
|
||||
console.log("🎯 UnifiedSelect 전체 props:", props);
|
||||
console.log("🎯 UnifiedSelect config:", config);
|
||||
console.log("🎯 UnifiedSelect loadOptions 호출:", {
|
||||
source: config.source,
|
||||
entityTable: config.entityTable,
|
||||
entityValueColumn: config.entityValueColumn,
|
||||
entityLabelColumn: config.entityLabelColumn,
|
||||
codeGroup: config.codeGroup,
|
||||
table: config.table,
|
||||
config,
|
||||
});
|
||||
|
||||
if (config.source === "static") {
|
||||
setOptions(config.options || []);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
let fetchedOptions: SelectOption[] = [];
|
||||
|
||||
if (config.source === "code" && config.codeGroup) {
|
||||
// 공통코드에서 로드
|
||||
const response = await apiClient.get(`/common-codes/${config.codeGroup}/items`);
|
||||
const data = response.data;
|
||||
if (data.success && data.data) {
|
||||
fetchedOptions = data.data.map((item: { code: string; codeName: string }) => ({
|
||||
value: item.code,
|
||||
label: item.codeName,
|
||||
}));
|
||||
}
|
||||
} else if (config.source === "db" && config.table) {
|
||||
// DB 테이블에서 로드
|
||||
const response = await apiClient.get(`/entity/${config.table}/options`, {
|
||||
params: {
|
||||
value: config.valueColumn || "id",
|
||||
label: config.labelColumn || "name",
|
||||
},
|
||||
});
|
||||
const data = response.data;
|
||||
if (data.success && data.data) {
|
||||
fetchedOptions = data.data;
|
||||
}
|
||||
} else if (config.source === "entity" && config.entityTable) {
|
||||
// 엔티티(참조 테이블)에서 로드
|
||||
const valueCol = config.entityValueColumn || config.entityValueField || "id";
|
||||
const labelCol = config.entityLabelColumn || config.entityLabelField || "name";
|
||||
console.log("🔍 Entity 옵션 API 호출:", `/entity/${config.entityTable}/options`, { value: valueCol, label: labelCol });
|
||||
const response = await apiClient.get(`/entity/${config.entityTable}/options`, {
|
||||
params: {
|
||||
value: valueCol,
|
||||
label: labelCol,
|
||||
},
|
||||
});
|
||||
const data = response.data;
|
||||
console.log("🔍 Entity 옵션 API 응답:", data);
|
||||
if (data.success && data.data) {
|
||||
fetchedOptions = data.data;
|
||||
}
|
||||
} else if (config.source === "api" && config.apiEndpoint) {
|
||||
// 외부 API에서 로드
|
||||
const response = await apiClient.get(config.apiEndpoint);
|
||||
const data = response.data;
|
||||
if (Array.isArray(data)) {
|
||||
fetchedOptions = data;
|
||||
}
|
||||
}
|
||||
|
||||
setOptions(fetchedOptions);
|
||||
} catch (error) {
|
||||
console.error("옵션 로딩 실패:", error);
|
||||
setOptions([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadOptions();
|
||||
}, [config]);
|
||||
|
||||
// 모드별 컴포넌트 렌더링
|
||||
const renderSelect = () => {
|
||||
if (loading) {
|
||||
return <div className="h-10 flex items-center text-sm text-muted-foreground">로딩 중...</div>;
|
||||
}
|
||||
|
||||
const isDisabled = disabled || readonly;
|
||||
|
||||
switch (config.mode) {
|
||||
case "dropdown":
|
||||
return (
|
||||
<DropdownSelect
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="선택"
|
||||
searchable={config.searchable}
|
||||
multiple={config.multiple}
|
||||
maxSelect={config.maxSelect}
|
||||
allowClear={config.allowClear}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
|
||||
case "radio":
|
||||
return (
|
||||
<RadioSelect
|
||||
options={options}
|
||||
value={typeof value === "string" ? value : value?.[0]}
|
||||
onChange={(v) => onChange?.(v)}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
|
||||
case "check":
|
||||
return (
|
||||
<CheckSelect
|
||||
options={options}
|
||||
value={Array.isArray(value) ? value : value ? [value] : []}
|
||||
onChange={onChange}
|
||||
maxSelect={config.maxSelect}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
|
||||
case "tag":
|
||||
return (
|
||||
<TagSelect
|
||||
options={options}
|
||||
value={Array.isArray(value) ? value : value ? [value] : []}
|
||||
onChange={onChange}
|
||||
maxSelect={config.maxSelect}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
|
||||
case "toggle":
|
||||
return (
|
||||
<ToggleSelect
|
||||
options={options}
|
||||
value={typeof value === "string" ? value : value?.[0]}
|
||||
onChange={(v) => onChange?.(v)}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
|
||||
case "swap":
|
||||
return (
|
||||
<SwapSelect
|
||||
options={options}
|
||||
value={Array.isArray(value) ? value : value ? [value] : []}
|
||||
onChange={onChange}
|
||||
maxSelect={config.maxSelect}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<DropdownSelect
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const showLabel = label && style?.labelDisplay !== false;
|
||||
const componentWidth = size?.width || style?.width;
|
||||
const componentHeight = size?.height || style?.height;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
id={id}
|
||||
className="flex flex-col"
|
||||
style={{
|
||||
width: componentWidth,
|
||||
height: componentHeight,
|
||||
}}
|
||||
>
|
||||
{showLabel && (
|
||||
<Label
|
||||
htmlFor={id}
|
||||
style={{
|
||||
fontSize: style?.labelFontSize,
|
||||
color: style?.labelColor,
|
||||
fontWeight: style?.labelFontWeight,
|
||||
marginBottom: style?.labelMarginBottom,
|
||||
}}
|
||||
className="text-sm font-medium flex-shrink-0"
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-orange-500 ml-0.5">*</span>}
|
||||
</Label>
|
||||
)}
|
||||
<div className="flex-1 min-h-0">
|
||||
{renderSelect()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
UnifiedSelect.displayName = "UnifiedSelect";
|
||||
|
||||
export default UnifiedSelect;
|
||||
|
||||
Reference in New Issue
Block a user