Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map
This commit is contained in:
@@ -424,7 +424,7 @@ export default function CopyScreenModal({
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[700px] max-h-[90vh] overflow-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Copy className="h-5 w-5" />
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
import { useEffect, useMemo, useState, useRef } from "react";
|
||||
import {
|
||||
ResizableDialog,
|
||||
ResizableDialogContent,
|
||||
ResizableDialogHeader,
|
||||
ResizableDialogTitle,
|
||||
ResizableDialogFooter,
|
||||
} from "@/components/ui/resizable-dialog";
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -271,21 +271,11 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
||||
};
|
||||
|
||||
return (
|
||||
<ResizableDialog open={open} onOpenChange={onOpenChange}>
|
||||
<ResizableDialogContent
|
||||
className="sm:max-w-lg"
|
||||
defaultWidth={600}
|
||||
defaultHeight={700}
|
||||
minWidth={500}
|
||||
minHeight={600}
|
||||
maxWidth={900}
|
||||
maxHeight={900}
|
||||
modalId="create-screen"
|
||||
userId={user?.userId}
|
||||
>
|
||||
<ResizableDialogHeader>
|
||||
<ResizableDialogTitle>새 화면 생성</ResizableDialogTitle>
|
||||
</ResizableDialogHeader>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>새 화면 생성</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
@@ -603,15 +593,15 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ResizableDialogFooter className="mt-4">
|
||||
<DialogFooter className="mt-4">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={submitting}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={!isValid || submitting} variant="default">
|
||||
생성
|
||||
</Button>
|
||||
</ResizableDialogFooter>
|
||||
</ResizableDialogContent>
|
||||
</ResizableDialog>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
ResizableDialog,
|
||||
ResizableDialogContent,
|
||||
ResizableDialogHeader,
|
||||
ResizableDialogTitle,
|
||||
ResizableDialogDescription,
|
||||
ResizableDialogFooter,
|
||||
} from "@/components/ui/resizable-dialog";
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
|
||||
import { screenApi } from "@/lib/api/screen";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
@@ -678,14 +678,17 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||
}
|
||||
|
||||
// 화면관리에서 설정한 크기 = 컨텐츠 영역 크기
|
||||
// 실제 모달 크기 = 컨텐츠 + 헤더
|
||||
const headerHeight = 60; // DialogHeader
|
||||
const totalHeight = screenDimensions.height + headerHeight;
|
||||
// 실제 모달 크기 = 컨텐츠 + 헤더 + gap + padding
|
||||
const headerHeight = 52; // DialogHeader (타이틀 + border-b + py-3)
|
||||
const dialogGap = 16; // DialogContent gap-4
|
||||
const extraPadding = 24; // 추가 여백 (안전 마진)
|
||||
|
||||
const totalHeight = screenDimensions.height + headerHeight + dialogGap + extraPadding;
|
||||
|
||||
return {
|
||||
className: "overflow-hidden p-0",
|
||||
style: {
|
||||
width: `${Math.min(screenDimensions.width, window.innerWidth * 0.98)}px`,
|
||||
width: `${Math.min(screenDimensions.width + 48, window.innerWidth * 0.98)}px`, // 좌우 패딩 추가
|
||||
height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`,
|
||||
maxWidth: "98vw",
|
||||
maxHeight: "95vh",
|
||||
@@ -696,32 +699,24 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||
const modalStyle = getModalStyle();
|
||||
|
||||
return (
|
||||
<ResizableDialog open={modalState.isOpen} onOpenChange={handleClose}>
|
||||
<ResizableDialogContent
|
||||
className={`${modalStyle.className} ${className || ""}`}
|
||||
<Dialog open={modalState.isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent
|
||||
className={`${modalStyle.className} ${className || ""} max-w-none`}
|
||||
style={modalStyle.style}
|
||||
defaultWidth={800}
|
||||
defaultHeight={600}
|
||||
minWidth={600}
|
||||
minHeight={400}
|
||||
maxWidth={1400}
|
||||
maxHeight={1000}
|
||||
modalId={modalState.screenId ? `edit-modal-${modalState.screenId}` : undefined}
|
||||
userId={user?.userId}
|
||||
>
|
||||
<ResizableDialogHeader className="shrink-0 border-b px-4 py-3">
|
||||
<DialogHeader className="shrink-0 border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ResizableDialogTitle className="text-base">{modalState.title || "데이터 수정"}</ResizableDialogTitle>
|
||||
<DialogTitle className="text-base">{modalState.title || "데이터 수정"}</DialogTitle>
|
||||
{modalState.description && !loading && (
|
||||
<ResizableDialogDescription className="text-muted-foreground text-xs">{modalState.description}</ResizableDialogDescription>
|
||||
<DialogDescription className="text-muted-foreground text-xs">{modalState.description}</DialogDescription>
|
||||
)}
|
||||
{loading && (
|
||||
<ResizableDialogDescription className="text-xs">{loading ? "화면을 불러오는 중입니다..." : ""}</ResizableDialogDescription>
|
||||
<DialogDescription className="text-xs">{loading ? "화면을 불러오는 중입니다..." : ""}</DialogDescription>
|
||||
)}
|
||||
</div>
|
||||
</ResizableDialogHeader>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-1 items-center justify-center overflow-auto">
|
||||
<div className="flex flex-1 items-center justify-center overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-transparent">
|
||||
{loading ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
@@ -812,8 +807,8 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ResizableDialogContent>
|
||||
</ResizableDialog>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogTitle } from "@/components/ui/resizable-dialog";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -352,9 +352,9 @@ export const FileAttachmentDetailModal: React.FC<FileAttachmentDetailModalProps>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<ResizableDialogTitle className="text-xl font-semibold">
|
||||
<DialogTitle className="text-xl font-semibold">
|
||||
파일 첨부 관리 - {component.label || component.id}
|
||||
</ResizableDialogTitle>
|
||||
</DialogTitle>
|
||||
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
@@ -2471,7 +2471,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
|
||||
{/* 기존 데이터 추가 모달 (제거 예정 - SaveModal로 대체됨) */}
|
||||
<Dialog open={false} onOpenChange={() => {}}>
|
||||
<DialogContent className={`max-h-[80vh] overflow-y-auto ${getModalSizeClass()}`}>
|
||||
<DialogContent className={`max-h-[80vh] overflow-hidden ${getModalSizeClass()}`}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{component.addModalConfig?.title || "새 데이터 추가"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -2517,7 +2517,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
|
||||
{/* 기존 데이터 수정 모달 (제거 예정 - SaveModal로 대체됨) */}
|
||||
<Dialog open={false} onOpenChange={() => {}}>
|
||||
<DialogContent className={`max-h-[80vh] overflow-y-auto ${getModalSizeClass()}`}>
|
||||
<DialogContent className={`max-h-[80vh] overflow-hidden ${getModalSizeClass()}`}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>데이터 수정</DialogTitle>
|
||||
<DialogDescription>선택된 데이터를 수정합니다.</DialogDescription>
|
||||
@@ -2773,7 +2773,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
|
||||
{/* 파일 관리 모달 */}
|
||||
<Dialog open={showFileManagementModal} onOpenChange={setShowFileManagementModal}>
|
||||
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
|
||||
<DialogContent className="max-h-[80vh] max-w-4xl overflow-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Folder className="h-5 w-5" />
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, ResizableDialog, ResizableDialogContent, ResizableDialogHeader } from "@/components/ui/dialog";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { CalendarIcon, File, Upload, X } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { ko } from "date-fns/locale";
|
||||
@@ -441,6 +441,39 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
);
|
||||
}
|
||||
|
||||
// 🆕 렉 구조 컴포넌트 처리
|
||||
if (comp.type === "component" && componentType === "rack-structure") {
|
||||
const { RackStructureComponent } = require("@/lib/registry/components/rack-structure/RackStructureComponent");
|
||||
const componentConfig = (comp as any).componentConfig || {};
|
||||
// config가 중첩되어 있을 수 있음: componentConfig.config 또는 componentConfig 직접
|
||||
const rackConfig = componentConfig.config || componentConfig;
|
||||
|
||||
console.log("🏗️ 렉 구조 컴포넌트 렌더링:", {
|
||||
componentType,
|
||||
componentConfig,
|
||||
rackConfig,
|
||||
fieldMapping: rackConfig.fieldMapping,
|
||||
formData,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-auto">
|
||||
<RackStructureComponent
|
||||
config={rackConfig}
|
||||
formData={formData}
|
||||
tableName={tableName}
|
||||
onChange={(locations: any[]) => {
|
||||
console.log("📦 렉 구조 위치 데이터 변경:", locations.length, "개");
|
||||
// 컴포넌트의 columnName을 키로 사용
|
||||
const fieldKey = (comp as any).columnName || "_rackStructureLocations";
|
||||
updateFormData(fieldKey, locations);
|
||||
}}
|
||||
isPreview={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { widgetType, label, placeholder, required, readonly, columnName } = comp;
|
||||
const fieldName = columnName || comp.id;
|
||||
const currentValue = formData[fieldName] || "";
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader } from "@/components/ui/resizable-dialog";
|
||||
import { DialogTitle, DialogHeader } from "@/components/ui/dialog";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { uploadFilesAndCreateData } from "@/lib/api/file";
|
||||
import { toast } from "sonner";
|
||||
@@ -119,17 +118,19 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
const [popupFormData, setPopupFormData] = useState<Record<string, any>>({});
|
||||
|
||||
// 🆕 분할 패널에서 매핑된 부모 데이터 가져오기
|
||||
// disableAutoDataTransfer가 true이면 자동 전달 비활성화 (버튼 클릭으로만 전달)
|
||||
const splitPanelMappedData = React.useMemo(() => {
|
||||
if (splitPanelContext) {
|
||||
if (splitPanelContext && !splitPanelContext.disableAutoDataTransfer) {
|
||||
return splitPanelContext.getMappedParentData();
|
||||
}
|
||||
return {};
|
||||
}, [splitPanelContext, splitPanelContext?.selectedLeftData]);
|
||||
}, [splitPanelContext, splitPanelContext?.selectedLeftData, splitPanelContext?.disableAutoDataTransfer]);
|
||||
|
||||
// formData 결정 (외부에서 전달받은 것이 있으면 우선 사용, 분할 패널 데이터도 병합)
|
||||
const formData = React.useMemo(() => {
|
||||
const baseData = externalFormData || localFormData;
|
||||
// 분할 패널 매핑 데이터가 있으면 병합 (기존 값이 없는 경우에만)
|
||||
// disableAutoDataTransfer가 true이면 자동 병합 안함
|
||||
if (Object.keys(splitPanelMappedData).length > 0) {
|
||||
const merged = { ...baseData };
|
||||
for (const [key, value] of Object.entries(splitPanelMappedData)) {
|
||||
@@ -776,17 +777,15 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
|
||||
{/* 팝업 화면 렌더링 */}
|
||||
{popupScreen && (
|
||||
<ResizableDialog open={!!popupScreen} onOpenChange={() => setPopupScreen(null)}>
|
||||
<ResizableDialogContent
|
||||
className="overflow-hidden p-0"
|
||||
defaultWidth={popupScreen.size === "small" ? 600 : popupScreen.size === "large" ? 1400 : 1000}
|
||||
defaultHeight={800}
|
||||
minWidth={500}
|
||||
minHeight={400}
|
||||
maxWidth={1600}
|
||||
maxHeight={1200}
|
||||
modalId={`popup-screen-${popupScreen.screenId}`}
|
||||
userId={user?.userId || "guest"}
|
||||
<Dialog open={!!popupScreen} onOpenChange={() => setPopupScreen(null)}>
|
||||
<DialogContent
|
||||
className="overflow-hidden p-0 max-w-none"
|
||||
style={{
|
||||
width: popupScreen.size === "small" ? "600px" : popupScreen.size === "large" ? "1400px" : "1000px",
|
||||
height: "800px",
|
||||
maxWidth: "95vw",
|
||||
maxHeight: "90vh",
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{popupScreen.title}</DialogTitle>
|
||||
@@ -820,8 +819,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ResizableDialogContent>
|
||||
</ResizableDialog>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
ResizableDialog,
|
||||
ResizableDialogContent,
|
||||
ResizableDialogHeader,
|
||||
ResizableDialogTitle,
|
||||
ResizableDialogDescription,
|
||||
ResizableDialogFooter,
|
||||
} from "@/components/ui/resizable-dialog";
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
@@ -345,26 +345,26 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizableDialog open={isOpen} onOpenChange={onClose}>
|
||||
<ResizableDialogContent className="max-w-2xl">
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
{assignmentSuccess ? (
|
||||
// 성공 화면
|
||||
<>
|
||||
<ResizableDialogHeader>
|
||||
<ResizableDialogTitle className="flex items-center gap-2">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100">
|
||||
<svg className="h-5 w-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
{assignmentMessage.includes("나중에") ? "화면 저장 완료" : "화면 할당 완료"}
|
||||
</ResizableDialogTitle>
|
||||
<ResizableDialogDescription>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{assignmentMessage.includes("나중에")
|
||||
? "화면이 성공적으로 저장되었습니다. 나중에 메뉴에 할당할 수 있습니다."
|
||||
: "화면이 성공적으로 메뉴에 할당되었습니다."}
|
||||
</ResizableDialogDescription>
|
||||
</ResizableDialogHeader>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border bg-green-50 p-4">
|
||||
@@ -386,7 +386,7 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ResizableDialogFooter>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
// 타이머 정리
|
||||
@@ -407,19 +407,19 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||
<Monitor className="mr-2 h-4 w-4" />
|
||||
화면 목록으로 이동
|
||||
</Button>
|
||||
</ResizableDialogFooter>
|
||||
</DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
// 기본 할당 화면
|
||||
<>
|
||||
<ResizableDialogHeader>
|
||||
<ResizableDialogTitle className="flex items-center gap-2">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Settings className="h-5 w-5" />
|
||||
메뉴에 화면 할당
|
||||
</ResizableDialogTitle>
|
||||
<ResizableDialogDescription>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
저장된 화면을 메뉴에 할당하여 사용자가 접근할 수 있도록 설정합니다.
|
||||
</ResizableDialogDescription>
|
||||
</DialogDescription>
|
||||
{screenInfo && (
|
||||
<div className="bg-accent mt-2 rounded-lg border p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -432,7 +432,7 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||
{screenInfo.description && <p className="mt-1 text-sm text-blue-700">{screenInfo.description}</p>}
|
||||
</div>
|
||||
)}
|
||||
</ResizableDialogHeader>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 메뉴 선택 (검색 기능 포함) */}
|
||||
@@ -550,7 +550,7 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ResizableDialogFooter className="flex gap-2">
|
||||
<DialogFooter className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleAssignLater} disabled={assigning}>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
나중에 할당
|
||||
@@ -572,22 +572,22 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</ResizableDialogFooter>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)}
|
||||
</ResizableDialogContent>
|
||||
</ResizableDialog>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 화면 교체 확인 대화상자 */}
|
||||
<ResizableDialog open={showReplaceDialog} onOpenChange={setShowReplaceDialog}>
|
||||
<ResizableDialogContent className="max-w-md">
|
||||
<ResizableDialogHeader>
|
||||
<ResizableDialogTitle className="flex items-center gap-2">
|
||||
<Dialog open={showReplaceDialog} onOpenChange={setShowReplaceDialog}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Monitor className="h-5 w-5 text-orange-600" />
|
||||
화면 교체 확인
|
||||
</ResizableDialogTitle>
|
||||
<ResizableDialogDescription>선택한 메뉴에 이미 할당된 화면이 있습니다.</ResizableDialogDescription>
|
||||
</ResizableDialogHeader>
|
||||
</DialogTitle>
|
||||
<DialogDescription>선택한 메뉴에 이미 할당된 화면이 있습니다.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 기존 화면 목록 */}
|
||||
@@ -628,7 +628,7 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ResizableDialogFooter className="flex gap-2">
|
||||
<DialogFooter className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setShowReplaceDialog(false)} disabled={assigning}>
|
||||
취소
|
||||
</Button>
|
||||
@@ -652,9 +652,9 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</ResizableDialogFooter>
|
||||
</ResizableDialogContent>
|
||||
</ResizableDialog>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, createContext, useContext } from "react";
|
||||
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogTitle } from "@/components/ui/resizable-dialog";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Monitor, Tablet, Smartphone } from "lucide-react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
@@ -76,7 +76,7 @@ export const ResponsivePreviewModal: React.FC<ResponsivePreviewModalProps> = ({
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-h-[95vh] max-w-[95vw] p-0">
|
||||
<DialogHeader className="border-b px-6 pt-6 pb-4">
|
||||
<ResizableDialogTitle>반응형 미리보기</ResizableDialogTitle>
|
||||
<DialogTitle>반응형 미리보기</DialogTitle>
|
||||
|
||||
{/* 디바이스 선택 버튼들 */}
|
||||
<div className="mt-4 flex gap-2">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, ResizableDialogTitle } from "@/components/ui/resizable-dialog";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { X, Save, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
@@ -232,22 +232,19 @@ export const SaveModal: React.FC<SaveModalProps> = ({
|
||||
const dynamicSize = calculateDynamicSize();
|
||||
|
||||
return (
|
||||
<ResizableDialog open={isOpen} onOpenChange={(open) => !isSaving && !open && onClose()}>
|
||||
<ResizableDialogContent
|
||||
modalId={`save-modal-${screenId}`}
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !isSaving && !open && onClose()}>
|
||||
<DialogContent
|
||||
style={{
|
||||
width: `${dynamicSize.width}px`,
|
||||
height: `${dynamicSize.height}px`, // 화면관리 설정 크기 그대로 사용
|
||||
minWidth: "400px",
|
||||
minHeight: "300px",
|
||||
}}
|
||||
defaultWidth={600} // 폴백용 기본값
|
||||
defaultHeight={400} // 폴백용 기본값
|
||||
minWidth={400}
|
||||
minHeight={300}
|
||||
className="gap-0 p-0"
|
||||
className="gap-0 p-0 max-w-none"
|
||||
>
|
||||
<ResizableDialogHeader className="border-b px-6 py-4 flex-shrink-0">
|
||||
<DialogHeader className="border-b px-6 py-4 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<ResizableDialogTitle className="text-lg font-semibold">{initialData ? "데이터 수정" : "데이터 등록"}</ResizableDialogTitle>
|
||||
<DialogTitle className="text-lg font-semibold">{initialData ? "데이터 수정" : "데이터 등록"}</DialogTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={handleSave} disabled={isSaving} size="sm" className="gap-2">
|
||||
{isSaving ? (
|
||||
@@ -267,7 +264,7 @@ export const SaveModal: React.FC<SaveModalProps> = ({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ResizableDialogHeader>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="overflow-auto p-6 flex-1">
|
||||
{loading ? (
|
||||
@@ -376,7 +373,7 @@ export const SaveModal: React.FC<SaveModalProps> = ({
|
||||
<div className="text-muted-foreground py-12 text-center">화면에 컴포넌트가 없습니다.</div>
|
||||
)}
|
||||
</div>
|
||||
</ResizableDialogContent>
|
||||
</ResizableDialog>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2239,10 +2239,27 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
calculatedWidth: `${Math.round(widthPercent * 100) / 100}%`,
|
||||
});
|
||||
|
||||
// 🆕 라벨을 기반으로 기본 columnName 생성 (한글 → 스네이크 케이스)
|
||||
// 예: "창고코드" → "warehouse_code" 또는 그대로 유지
|
||||
const generateDefaultColumnName = (label: string): string => {
|
||||
// 한글 라벨의 경우 그대로 사용 (나중에 사용자가 수정 가능)
|
||||
// 영문의 경우 스네이크 케이스로 변환
|
||||
if (/[가-힣]/.test(label)) {
|
||||
// 한글이 포함된 경우: 공백을 언더스코어로, 소문자로 변환
|
||||
return label.replace(/\s+/g, "_").toLowerCase();
|
||||
}
|
||||
// 영문의 경우: 카멜케이스/파스칼케이스를 스네이크 케이스로 변환
|
||||
return label
|
||||
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
||||
.replace(/\s+/g, "_")
|
||||
.toLowerCase();
|
||||
};
|
||||
|
||||
const newComponent: ComponentData = {
|
||||
id: generateComponentId(),
|
||||
type: "component", // ✅ 새 컴포넌트 시스템 사용
|
||||
label: component.name,
|
||||
columnName: generateDefaultColumnName(component.name), // 🆕 기본 columnName 자동 생성
|
||||
widgetType: component.webType,
|
||||
componentType: component.id, // 새 컴포넌트 시스템의 ID (DynamicComponentRenderer용)
|
||||
position: snappedPosition,
|
||||
|
||||
@@ -91,6 +91,14 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
const [mappingSourceSearch, setMappingSourceSearch] = useState<Record<number, string>>({});
|
||||
const [mappingTargetSearch, setMappingTargetSearch] = useState<Record<number, string>>({});
|
||||
|
||||
// 🆕 openModalWithData 전용 필드 매핑 상태
|
||||
const [modalSourceColumns, setModalSourceColumns] = useState<Array<{ name: string; label: string }>>([]);
|
||||
const [modalTargetColumns, setModalTargetColumns] = useState<Array<{ name: string; label: string }>>([]);
|
||||
const [modalSourcePopoverOpen, setModalSourcePopoverOpen] = useState<Record<number, boolean>>({});
|
||||
const [modalTargetPopoverOpen, setModalTargetPopoverOpen] = useState<Record<number, boolean>>({});
|
||||
const [modalSourceSearch, setModalSourceSearch] = useState<Record<number, string>>({});
|
||||
const [modalTargetSearch, setModalTargetSearch] = useState<Record<number, string>>({});
|
||||
|
||||
// 🎯 플로우 위젯이 화면에 있는지 확인
|
||||
const hasFlowWidget = useMemo(() => {
|
||||
const found = allComponents.some((comp: any) => {
|
||||
@@ -318,6 +326,88 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
loadColumns();
|
||||
}, [config.action?.dataTransfer?.sourceTable, config.action?.dataTransfer?.targetTable]);
|
||||
|
||||
// 🆕 openModalWithData 소스/타겟 테이블 컬럼 로드
|
||||
useEffect(() => {
|
||||
const actionType = config.action?.type;
|
||||
if (actionType !== "openModalWithData") return;
|
||||
|
||||
const loadModalMappingColumns = async () => {
|
||||
// 소스 테이블: 현재 화면의 분할 패널 또는 테이블에서 감지
|
||||
// allComponents에서 split-panel-layout 또는 table-list 찾기
|
||||
let sourceTableName: string | null = null;
|
||||
|
||||
for (const comp of allComponents) {
|
||||
const compType = comp.componentType || (comp as any).componentConfig?.type;
|
||||
if (compType === "split-panel-layout" || compType === "screen-split-panel") {
|
||||
// 분할 패널의 좌측 테이블명
|
||||
sourceTableName = (comp as any).componentConfig?.leftPanel?.tableName ||
|
||||
(comp as any).componentConfig?.leftTableName;
|
||||
break;
|
||||
}
|
||||
if (compType === "table-list") {
|
||||
sourceTableName = (comp as any).componentConfig?.tableName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 소스 테이블 컬럼 로드
|
||||
if (sourceTableName) {
|
||||
try {
|
||||
const response = await apiClient.get(`/table-management/tables/${sourceTableName}/columns`);
|
||||
if (response.data.success) {
|
||||
let columnData = response.data.data;
|
||||
if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns;
|
||||
if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data;
|
||||
|
||||
if (Array.isArray(columnData)) {
|
||||
const columns = columnData.map((col: any) => ({
|
||||
name: col.name || col.columnName,
|
||||
label: col.displayName || col.label || col.columnLabel || col.name || col.columnName,
|
||||
}));
|
||||
setModalSourceColumns(columns);
|
||||
console.log(`✅ [openModalWithData] 소스 테이블(${sourceTableName}) 컬럼 로드:`, columns.length);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("소스 테이블 컬럼 로드 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 타겟 화면의 테이블 컬럼 로드
|
||||
const targetScreenId = config.action?.targetScreenId;
|
||||
if (targetScreenId) {
|
||||
try {
|
||||
// 타겟 화면 정보 가져오기
|
||||
const screenResponse = await apiClient.get(`/screen-management/screens/${targetScreenId}`);
|
||||
if (screenResponse.data.success && screenResponse.data.data) {
|
||||
const targetTableName = screenResponse.data.data.tableName;
|
||||
if (targetTableName) {
|
||||
const columnResponse = await apiClient.get(`/table-management/tables/${targetTableName}/columns`);
|
||||
if (columnResponse.data.success) {
|
||||
let columnData = columnResponse.data.data;
|
||||
if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns;
|
||||
if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data;
|
||||
|
||||
if (Array.isArray(columnData)) {
|
||||
const columns = columnData.map((col: any) => ({
|
||||
name: col.name || col.columnName,
|
||||
label: col.displayName || col.label || col.columnLabel || col.name || col.columnName,
|
||||
}));
|
||||
setModalTargetColumns(columns);
|
||||
console.log(`✅ [openModalWithData] 타겟 테이블(${targetTableName}) 컬럼 로드:`, columns.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("타겟 화면 테이블 컬럼 로드 실패:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadModalMappingColumns();
|
||||
}, [config.action?.type, config.action?.targetScreenId, allComponents]);
|
||||
|
||||
// 화면 목록 가져오기 (현재 편집 중인 화면의 회사 코드 기준)
|
||||
useEffect(() => {
|
||||
const fetchScreens = async () => {
|
||||
@@ -1024,6 +1114,194 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
SelectedItemsDetailInput 컴포넌트가 있는 화면을 선택하세요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 🆕 필드 매핑 설정 (소스 컬럼 → 타겟 컬럼) */}
|
||||
<div className="space-y-2 border-t pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-medium">필드 매핑 (선택사항)</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-[10px]"
|
||||
onClick={() => {
|
||||
const currentMappings = config.action?.fieldMappings || [];
|
||||
const newMapping = { sourceField: "", targetField: "" };
|
||||
onUpdateProperty("componentConfig.action.fieldMappings", [...currentMappings, newMapping]);
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
매핑 추가
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
소스 테이블의 컬럼명이 타겟 화면의 입력 필드 컬럼명과 다를 때 매핑을 설정하세요.
|
||||
<br />
|
||||
예: warehouse_code → warehouse_id (분할 패널의 창고코드를 모달의 창고ID에 매핑)
|
||||
</p>
|
||||
|
||||
{/* 컬럼 로드 상태 표시 */}
|
||||
{modalSourceColumns.length > 0 || modalTargetColumns.length > 0 ? (
|
||||
<div className="text-[10px] text-muted-foreground bg-muted/50 p-2 rounded">
|
||||
소스 컬럼: {modalSourceColumns.length}개 / 타겟 컬럼: {modalTargetColumns.length}개
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[10px] text-amber-600 bg-amber-50 p-2 rounded dark:bg-amber-950/20">
|
||||
분할 패널 또는 테이블 컴포넌트와 대상 화면을 설정하면 컬럼 목록이 로드됩니다.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(config.action?.fieldMappings || []).length === 0 ? (
|
||||
<div className="rounded-md border border-dashed p-3 text-center">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
매핑이 없으면 같은 이름의 컬럼끼리 자동으로 매핑됩니다.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{(config.action?.fieldMappings || []).map((mapping: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2 rounded-md border bg-background p-2">
|
||||
{/* 소스 필드 선택 (Combobox) */}
|
||||
<div className="flex-1">
|
||||
<Popover
|
||||
open={modalSourcePopoverOpen[index] || false}
|
||||
onOpenChange={(open) => setModalSourcePopoverOpen((prev) => ({ ...prev, [index]: open }))}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="h-7 w-full justify-between text-xs"
|
||||
>
|
||||
{mapping.sourceField
|
||||
? modalSourceColumns.find((c) => c.name === mapping.sourceField)?.label || mapping.sourceField
|
||||
: "소스 컬럼 선택"}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="컬럼 검색..."
|
||||
className="h-8 text-xs"
|
||||
value={modalSourceSearch[index] || ""}
|
||||
onValueChange={(value) => setModalSourceSearch((prev) => ({ ...prev, [index]: value }))}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty className="py-2 text-center text-xs">컬럼을 찾을 수 없습니다</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{modalSourceColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={`${col.label} ${col.name}`}
|
||||
onSelect={() => {
|
||||
const mappings = [...(config.action?.fieldMappings || [])];
|
||||
mappings[index] = { ...mappings[index], sourceField: col.name };
|
||||
onUpdateProperty("componentConfig.action.fieldMappings", mappings);
|
||||
setModalSourcePopoverOpen((prev) => ({ ...prev, [index]: false }));
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
mapping.sourceField === col.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<span>{col.label}</span>
|
||||
{col.label !== col.name && (
|
||||
<span className="ml-1 text-muted-foreground">({col.name})</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<span className="text-xs text-muted-foreground">→</span>
|
||||
|
||||
{/* 타겟 필드 선택 (Combobox) */}
|
||||
<div className="flex-1">
|
||||
<Popover
|
||||
open={modalTargetPopoverOpen[index] || false}
|
||||
onOpenChange={(open) => setModalTargetPopoverOpen((prev) => ({ ...prev, [index]: open }))}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="h-7 w-full justify-between text-xs"
|
||||
>
|
||||
{mapping.targetField
|
||||
? modalTargetColumns.find((c) => c.name === mapping.targetField)?.label || mapping.targetField
|
||||
: "타겟 컬럼 선택"}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="컬럼 검색..."
|
||||
className="h-8 text-xs"
|
||||
value={modalTargetSearch[index] || ""}
|
||||
onValueChange={(value) => setModalTargetSearch((prev) => ({ ...prev, [index]: value }))}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty className="py-2 text-center text-xs">컬럼을 찾을 수 없습니다</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{modalTargetColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={`${col.label} ${col.name}`}
|
||||
onSelect={() => {
|
||||
const mappings = [...(config.action?.fieldMappings || [])];
|
||||
mappings[index] = { ...mappings[index], targetField: col.name };
|
||||
onUpdateProperty("componentConfig.action.fieldMappings", mappings);
|
||||
setModalTargetPopoverOpen((prev) => ({ ...prev, [index]: false }));
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
mapping.targetField === col.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<span>{col.label}</span>
|
||||
{col.label !== col.name && (
|
||||
<span className="ml-1 text-muted-foreground">({col.name})</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* 삭제 버튼 */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive hover:bg-destructive/10"
|
||||
onClick={() => {
|
||||
const mappings = [...(config.action?.fieldMappings || [])];
|
||||
mappings.splice(index, 1);
|
||||
onUpdateProperty("componentConfig.action.fieldMappings", mappings);
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -584,20 +584,23 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{selectedComponent.type === "widget" && (
|
||||
{(selectedComponent.type === "widget" || selectedComponent.type === "component") && (
|
||||
<>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="columnName" className="text-xs font-medium">
|
||||
컬럼명 (읽기 전용)
|
||||
컬럼명 (필드명)
|
||||
</Label>
|
||||
<Input
|
||||
id="columnName"
|
||||
value={selectedComponent.columnName || ""}
|
||||
readOnly
|
||||
placeholder="데이터베이스 컬럼명"
|
||||
className="bg-muted/50 text-muted-foreground h-8"
|
||||
title="컬럼명은 변경할 수 없습니다"
|
||||
onChange={(e) => onUpdateProperty("columnName", e.target.value)}
|
||||
placeholder="formData에서 사용할 필드명"
|
||||
className="h-8"
|
||||
title="분할 패널에서 데이터를 전달받을 때 사용되는 필드명입니다"
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
분할 패널에서 데이터를 전달받을 때 매핑되는 필드명
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { ResizableDialog, ResizableDialogContent, ResizableDialogDescription, DialogFooter, ResizableDialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { toast } from "sonner";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user