diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-09 12:19:05 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-09 12:19:05 +0000 |
| commit | 6d654b1ba2c19e0bf1745b636908e3b00a0f02c7 (patch) | |
| tree | f6d48c0d3a65b428a828acea5db65db8e7bf0db8 /components/form-data/spreadJS-dialog.tsx | |
| parent | 44794a8628997c0d979adb5bd6711cd848b3e397 (diff) | |
(대표님) 20250709 변경사항 (약 18시 30분까지)
Diffstat (limited to 'components/form-data/spreadJS-dialog.tsx')
| -rw-r--r-- | components/form-data/spreadJS-dialog.tsx | 498 |
1 files changed, 200 insertions, 298 deletions
diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx index 5a51c2b5..8be9d175 100644 --- a/components/form-data/spreadJS-dialog.tsx +++ b/components/form-data/spreadJS-dialog.tsx @@ -2,7 +2,14 @@ import * as React from "react"; import dynamic from "next/dynamic"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { GenericData } from "./export-excel-form"; import * as GC from "@mescius/spread-sheets"; @@ -16,24 +23,24 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import '@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css'; +import "@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css"; -// SpreadSheets를 동적으로 import (SSR 비활성화) +// Dynamically load the SpreadSheets component (disable SSR) const SpreadSheets = dynamic( - () => import("@mescius/spread-sheets-react").then(mod => mod.SpreadSheets), - { + () => import("@mescius/spread-sheets-react").then((mod) => mod.SpreadSheets), + { ssr: false, loading: () => ( <div className="flex items-center justify-center h-full"> <Loader className="mr-2 h-4 w-4 animate-spin" /> Loading SpreadSheets... </div> - ) + ), } ); -// 라이센스 키 설정을 클라이언트에서만 실행 -if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_SPREAD_LICENSE) { +// Apply license key on the client only +if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_SPREAD_LICENSE) { GC.Spread.Sheets.LicenseKey = process.env.NEXT_PUBLIC_SPREAD_LICENSE; } @@ -81,7 +88,7 @@ interface TemplateViewDialogProps { selectedRow: GenericData; formCode: string; contractItemId: number; - editableFieldsMap?: Map<string, string[]>; // 편집 가능 필드 정보 + editableFieldsMap?: Map<string, string[]>; // editable field info per tag onUpdateSuccess?: (updatedValues: Record<string, any>) => void; } @@ -93,310 +100,232 @@ export function TemplateViewDialog({ formCode, contractItemId, editableFieldsMap = new Map(), - onUpdateSuccess + onUpdateSuccess, }: TemplateViewDialogProps) { - const [hostStyle, setHostStyle] = React.useState({ - width: '100%', - height: '100%' - }); - + /* ------------------------- local state ------------------------- */ + const [hostStyle] = React.useState({ width: "100%", height: "100%" }); const [isPending, setIsPending] = React.useState(false); const [hasChanges, setHasChanges] = React.useState(false); - const [currentSpread, setCurrentSpread] = React.useState<any>(null); + const [currentSpread, setCurrentSpread] = React.useState<GC.Spread.Sheets.Workbook | null>( + null + ); const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>(""); - const [cellMappings, setCellMappings] = React.useState<Array<{attId: string, cellAddress: string, isEditable: boolean}>>([]); + const [cellMappings, setCellMappings] = React.useState< + Array<{ attId: string; cellAddress: string; isEditable: boolean }> + >([]); const [isClient, setIsClient] = React.useState(false); - // 클라이언트 사이드에서만 렌더링되도록 보장 + // Render only on client side React.useEffect(() => { setIsClient(true); }, []); - // 템플릿 데이터를 배열로 정규화하고 CONTENT가 있는 것만 필터링 + /* ------------------------- helpers ------------------------- */ + // Normalize template list and keep only those with CONTENT const normalizedTemplates = React.useMemo((): TemplateItem[] => { if (!templateData) return []; - - let templates: TemplateItem[]; - if (Array.isArray(templateData)) { - templates = templateData as TemplateItem[]; - } else { - templates = [templateData as TemplateItem]; - } - - return templates.filter(template => { - const sprContent = template.SPR_LST_SETUP?.CONTENT; - const sprItmContent = template.SPR_ITM_LST_SETUP?.CONTENT; - return sprContent || sprItmContent; - }); + + const list = Array.isArray(templateData) + ? (templateData as TemplateItem[]) + : ([templateData] as TemplateItem[]); + + return list.filter( + (t) => t.SPR_LST_SETUP?.CONTENT || t.SPR_ITM_LST_SETUP?.CONTENT + ); }, [templateData]); - // 선택된 템플릿 가져오기 + // Choose currently selected template const selectedTemplate = React.useMemo(() => { if (!selectedTemplateId) return normalizedTemplates[0]; - return normalizedTemplates.find(t => t.TMPL_ID === selectedTemplateId) || normalizedTemplates[0]; + return ( + normalizedTemplates.find((t) => t.TMPL_ID === selectedTemplateId) || + normalizedTemplates[0] + ); }, [normalizedTemplates, selectedTemplateId]); - // 현재 TAG의 편집 가능한 필드 목록 가져오기 + // Editable fields for the current TAG_NO const editableFields = React.useMemo(() => { - if (!selectedRow?.TAG_NO || !editableFieldsMap.has(selectedRow.TAG_NO)) { - return []; - } + if (!selectedRow?.TAG_NO) return []; return editableFieldsMap.get(selectedRow.TAG_NO) || []; }, [selectedRow?.TAG_NO, editableFieldsMap]); - // 필드가 편집 가능한지 판별하는 함수 - const isFieldEditable = React.useCallback((attId: string) => { - // TAG_NO와 TAG_DESC는 기본적으로 편집 가능 - if (attId === "TAG_NO" || attId === "TAG_DESC") { - return true; - } - - // editableFieldsMap이 있으면 해당 리스트에 있는지 확인 - if (selectedRow?.TAG_NO && editableFieldsMap.has(selectedRow.TAG_NO)) { + const isFieldEditable = React.useCallback( + (attId: string) => { + // TAG_NO and TAG_DESC are always editable + if (attId === "TAG_NO" || attId === "TAG_DESC") return true; + if (!selectedRow?.TAG_NO) return false; return editableFields.includes(attId); - } - - return false; - }, [selectedRow?.TAG_NO, editableFieldsMap, editableFields]); - - // 셀 주소를 행과 열로 변환하는 함수 (예: "M1" -> {row: 0, col: 12}) - const parseCellAddress = (address: string): {row: number, col: number} | null => { - if (!address || address.trim() === "") return null; - - const match = address.match(/^([A-Z]+)(\d+)$/); + }, + [selectedRow?.TAG_NO, editableFields] + ); + + /** Convert a cell address like "M1" into {row:0,col:12}. */ + const parseCellAddress = (addr: string): { row: number; col: number } | null => { + if (!addr) return null; + const match = addr.match(/^([A-Z]+)(\d+)$/); if (!match) return null; - const [, colStr, rowStr] = match; - - // 열 문자를 숫자로 변환 (A=0, B=1, ..., Z=25, AA=26, ...) let col = 0; for (let i = 0; i < colStr.length; i++) { col = col * 26 + (colStr.charCodeAt(i) - 65 + 1); } - col -= 1; // 0-based index로 변환 - - const row = parseInt(rowStr) - 1; // 0-based index로 변환 - + col -= 1; + const row = parseInt(rowStr, 10) - 1; return { row, col }; }; - // 템플릿 변경 시 기본 선택 + // Auto‑select first template React.useEffect(() => { - if (normalizedTemplates.length > 0 && !selectedTemplateId) { + if (normalizedTemplates.length && !selectedTemplateId) { setSelectedTemplateId(normalizedTemplates[0].TMPL_ID); } }, [normalizedTemplates, selectedTemplateId]); - const initSpread = React.useCallback((spread: any) => { - if (!spread || !selectedTemplate || !selectedRow) return; + /* ------------------------- init spread ------------------------- */ + const initSpread = React.useCallback( + (spread: GC.Spread.Sheets.Workbook | undefined) => { + if (!spread || !selectedTemplate || !selectedRow) return; - try { setCurrentSpread(spread); setHasChanges(false); - // CONTENT 찾기 - let contentJson = null; - let dataSheets = null; - - if (selectedTemplate.SPR_LST_SETUP?.CONTENT) { - contentJson = selectedTemplate.SPR_LST_SETUP.CONTENT; - dataSheets = selectedTemplate.SPR_LST_SETUP.DATA_SHEETS; - console.log('Using SPR_LST_SETUP.CONTENT for template:', selectedTemplate.NAME); - } else if (selectedTemplate.SPR_ITM_LST_SETUP?.CONTENT) { - contentJson = selectedTemplate.SPR_ITM_LST_SETUP.CONTENT; - dataSheets = selectedTemplate.SPR_ITM_LST_SETUP.DATA_SHEETS; - console.log('Using SPR_ITM_LST_SETUP.CONTENT for template:', selectedTemplate.NAME); - } + // Pick content JSON and data‑sheet mapping + const contentJson = + selectedTemplate.SPR_LST_SETUP?.CONTENT ?? + selectedTemplate.SPR_ITM_LST_SETUP?.CONTENT; + const dataSheets = + selectedTemplate.SPR_LST_SETUP?.DATA_SHEETS ?? + selectedTemplate.SPR_ITM_LST_SETUP?.DATA_SHEETS; + if (!contentJson) return; - if (!contentJson) { - console.warn('No CONTENT found in template:', selectedTemplate.NAME); - return; - } + // Prepare shared styles once + const editableStyle = new GC.Spread.Sheets.Style(); + editableStyle.backColor = "#f0fdf4"; + editableStyle.locked = false; - console.log('Loading template content for:', selectedTemplate.NAME); - - const jsonData = typeof contentJson === 'string' - ? JSON.parse(contentJson) - : contentJson; + const readOnlyStyle = new GC.Spread.Sheets.Style(); + readOnlyStyle.backColor = "#f9fafb"; + readOnlyStyle.foreColor = "#6b7280"; + readOnlyStyle.locked = true; - // 렌더링 일시 중단 (성능 향상) - spread.suspendPaint(); + const jsonObj = typeof contentJson === "string" ? JSON.parse(contentJson) : contentJson; - try { - // fromJSON으로 템플릿 구조 로드 - spread.fromJSON(jsonData); - - // 활성 시트 가져오기 - const activeSheet = spread.getActiveSheet(); - - // 시트 보호 먼저 해제 - activeSheet.options.isProtected = false; - - // MAP_CELL_ATT 정보를 사용해서 셀에 데이터 매핑과 스타일을 한번에 처리 - if (dataSheets && dataSheets.length > 0) { - const mappings: Array<{attId: string, cellAddress: string, isEditable: boolean}> = []; - - dataSheets.forEach(dataSheet => { - if (dataSheet.MAP_CELL_ATT) { - dataSheet.MAP_CELL_ATT.forEach(mapping => { - const { ATT_ID, IN } = mapping; - - // 셀 주소가 비어있지 않은 경우만 처리 - if (IN && IN.trim() !== "") { - const cellPos = parseCellAddress(IN); - if (cellPos) { - const isEditable = isFieldEditable(ATT_ID); - mappings.push({ - attId: ATT_ID, - cellAddress: IN, - isEditable: isEditable - }); - - // 셀 객체 가져오기 - const cell = activeSheet.getCell(cellPos.row, cellPos.col); - - // selectedRow에서 해당 값 가져와서 셀에 설정 - const value = selectedRow[ATT_ID]; - if (value !== undefined && value !== null) { - cell.value(value); - } - - // 편집 권한 설정 - cell.locked(!isEditable); - - // 즉시 스타일 적용 (기존 스타일 보존하면서) - const existingStyle = activeSheet.getStyle(cellPos.row, cellPos.col); - if (existingStyle) { - // 기존 스타일 복사 - const newStyle = Object.assign(new GC.Spread.Sheets.Style(), existingStyle); - - // 편집 권한에 따라 배경색만 변경 - if (isEditable) { - newStyle.backColor = "#f0fdf4"; // 연한 녹색 - } else { - newStyle.backColor = "#f9fafb"; // 연한 회색 - newStyle.foreColor = "#6b7280"; // 회색 글자 - } - - // 스타일 적용 - activeSheet.setStyle(cellPos.row, cellPos.col, newStyle); - } else { - // 기존 스타일이 없는 경우 새로운 스타일 생성 - const newStyle = new GC.Spread.Sheets.Style(); - if (isEditable) { - newStyle.backColor = "#f0fdf4"; - } else { - newStyle.backColor = "#f9fafb"; - newStyle.foreColor = "#6b7280"; - } - activeSheet.setStyle(cellPos.row, cellPos.col, newStyle); - } - - console.log(`Mapped ${ATT_ID} (${value}) to cell ${IN} - ${isEditable ? 'Editable' : 'Read-only'}`); - } - } - }); - } - }); - - setCellMappings(mappings); - - // 시트 보호 설정 - activeSheet.options.isProtected = true; - activeSheet.options.protectionOptions = { - allowSelectLockedCells: true, - allowSelectUnlockedCells: true, - allowSort: false, - allowFilter: false, - allowEditObjects: false, - allowResizeRows: false, - allowResizeColumns: false - }; - - // 이벤트 리스너 추가 - activeSheet.bind(GC.Spread.Sheets.Events.CellChanged, (event: any, info: any) => { - console.log('Cell changed:', info); - setHasChanges(true); - }); - - activeSheet.bind(GC.Spread.Sheets.Events.ValueChanged, (event: any, info: any) => { - console.log('Value changed:', info); - setHasChanges(true); - }); + const sheet = spread.getActiveSheet(); - // 편집 시작 시 읽기 전용 셀 확인 - activeSheet.bind(GC.Spread.Sheets.Events.EditStarting, (event: any, info: any) => { - const mapping = mappings.find(m => { - const cellPos = parseCellAddress(m.cellAddress); - return cellPos && cellPos.row === info.row && cellPos.col === info.col; + /* -------- batch load + style -------- */ + sheet.suspendPaint(); + sheet.suspendCalcService(true); + try { + spread.fromJSON(jsonObj); + sheet.options.isProtected = false; + + const mappings: Array<{ attId: string; cellAddress: string; isEditable: boolean }> = []; + + if (dataSheets?.length) { + dataSheets.forEach((ds) => { + ds.MAP_CELL_ATT?.forEach(({ ATT_ID, IN }) => { + if (!IN) return; + const pos = parseCellAddress(IN); + if (!pos) return; + const editable = isFieldEditable(ATT_ID); + mappings.push({ attId: ATT_ID, cellAddress: IN, isEditable: editable }); }); - - if (mapping && !mapping.isEditable) { - toast.warning(`${mapping.attId} field is read-only`); - info.cancel = true; - } }); } - } finally { - // 렌더링 재개 (모든 변경사항이 한번에 화면에 표시됨) - spread.resumePaint(); - } - } catch (error) { - console.error('Error initializing spread:', error); - toast.error('Failed to load template'); - // 에러 발생 시에도 렌더링 재개 - if (spread && spread.resumePaint) { - spread.resumePaint(); + // Apply values + style in chunks for large templates + const CHUNK = 500; + let idx = 0; + const applyChunk = () => { + const end = Math.min(idx + CHUNK, mappings.length); + for (; idx < end; idx++) { + const { attId, cellAddress, isEditable } = mappings[idx]; + const pos = parseCellAddress(cellAddress)!; + if (selectedRow[attId] !== undefined && selectedRow[attId] !== null) { + sheet.setValue(pos.row, pos.col, selectedRow[attId]); + } + sheet.setStyle(pos.row, pos.col, isEditable ? editableStyle : readOnlyStyle); + } + if (idx < mappings.length) { + requestAnimationFrame(applyChunk); + } else { + // enable protection & events after styling done + sheet.options.isProtected = true; + sheet.options.protectionOptions = { + allowSelectLockedCells: true, + allowSelectUnlockedCells: true, + } as any; + + // Cell/value change events + sheet.bind(GC.Spread.Sheets.Events.ValueChanged, () => setHasChanges(true)); + sheet.bind(GC.Spread.Sheets.Events.CellChanged, () => setHasChanges(true)); + + // Prevent editing read‑only fields + sheet.bind( + GC.Spread.Sheets.Events.EditStarting, + (event: any, info: any) => { + const map = mappings.find((m) => { + const pos = parseCellAddress(m.cellAddress); + return pos && pos.row === info.row && pos.col === info.col; + }); + if (map && !map.isEditable) { + toast.warning(`${map.attId} field is read‑only`); + info.cancel = true; + } + } + ); + + setCellMappings(mappings); + sheet.resumeCalcService(false); + sheet.resumePaint(); + } + }; + applyChunk(); + } catch (err) { + console.error(err); + toast.error("Failed to load template"); + sheet.resumeCalcService(false); + sheet.resumePaint(); } - } - }, [selectedTemplate, selectedRow, isFieldEditable]); + }, + [selectedTemplate, selectedRow, isFieldEditable] + ); - // 템플릿 변경 핸들러 - const handleTemplateChange = (templateId: string) => { - setSelectedTemplateId(templateId); + /* ------------------------- handlers ------------------------- */ + const handleTemplateChange = (id: string) => { + setSelectedTemplateId(id); setHasChanges(false); - if (currentSpread) { - setTimeout(() => { - initSpread(currentSpread); - }, 100); + // re‑init after a short tick so component remounts SpreadSheets + setTimeout(() => initSpread(currentSpread), 50); } }; - // 변경사항 저장 함수 const handleSaveChanges = React.useCallback(async () => { if (!currentSpread || !hasChanges || !selectedRow) { toast.info("No changes to save"); return; } + setIsPending(true); + try { - setIsPending(true); - - const activeSheet = currentSpread.getActiveSheet(); - const dataToSave = { ...selectedRow }; - - // cellMappings를 사용해서 편집 가능한 셀의 값만 추출 - cellMappings.forEach(mapping => { - if (mapping.isEditable) { - const cellPos = parseCellAddress(mapping.cellAddress); - if (cellPos) { - const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); - dataToSave[mapping.attId] = cellValue; - } + const sheet = currentSpread.getActiveSheet(); + const payload: Record<string, any> = { ...selectedRow }; + + cellMappings.forEach((m) => { + if (m.isEditable) { + const pos = parseCellAddress(m.cellAddress); + if (pos) payload[m.attId] = sheet.getValue(pos.row, pos.col); } }); - // TAG_NO는 절대 변경되지 않도록 원본 값으로 강제 설정 - dataToSave.TAG_NO = selectedRow.TAG_NO; - - console.log('Data to save (TAG_NO preserved):', dataToSave); + payload.TAG_NO = selectedRow.TAG_NO; // never change TAG_NO const { success, message } = await updateFormDataInDB( formCode, contractItemId, - dataToSave + payload ); if (!success) { @@ -405,60 +334,47 @@ export function TemplateViewDialog({ } toast.success("Changes saved successfully!"); - - const updatedData = { - ...selectedRow, - ...dataToSave, - }; - - onUpdateSuccess?.(updatedData); + onUpdateSuccess?.({ ...selectedRow, ...payload }); setHasChanges(false); - - } catch (error) { - console.error("Error saving changes:", error); + } catch (err) { + console.error(err); toast.error("An unexpected error occurred while saving"); } finally { setIsPending(false); } - }, [currentSpread, hasChanges, formCode, contractItemId, selectedRow, onUpdateSuccess, cellMappings]); + }, [currentSpread, hasChanges, selectedRow, cellMappings, formCode, contractItemId, onUpdateSuccess]); + /* ------------------------- render ------------------------- */ if (!isOpen) return null; return ( <Dialog open={isOpen} onOpenChange={onClose}> - <DialogContent - className="w-[80%] max-w-none h-[80vh] flex flex-col" - style={{maxWidth:"80vw"}} - > + <DialogContent className="w-[80%] max-w-none h-[80vh] flex flex-col" style={{ maxWidth: "80vw" }}> <DialogHeader className="flex-shrink-0"> - <DialogTitle>SEDP Template - {formCode}</DialogTitle> + <DialogTitle>SEDP Template – {formCode}</DialogTitle> <DialogDescription> - {selectedRow && `Selected TAG_NO: ${selectedRow.TAG_NO || 'N/A'}`} - {hasChanges && ( - <span className="ml-2 text-orange-600 font-medium"> - • Unsaved changes - </span> - )} + {selectedRow && `Selected TAG_NO: ${selectedRow.TAG_NO || "N/A"}`} + {hasChanges && <span className="ml-2 text-orange-600 font-medium">• Unsaved changes</span>} <br /> <div className="flex items-center gap-4 mt-2"> <span className="text-xs text-muted-foreground"> - <span className="inline-block w-3 h-3 bg-green-100 border border-green-400 mr-1"></span> + <span className="inline-block w-3 h-3 bg-green-100 border border-green-400 mr-1" /> Editable fields </span> <span className="text-xs text-muted-foreground"> - <span className="inline-block w-3 h-3 bg-gray-100 border border-gray-300 mr-1"></span> - Read-only fields + <span className="inline-block w-3 h-3 bg-gray-100 border border-gray-300 mr-1" /> + Read‑only fields </span> - {cellMappings.length > 0 && ( + {!!cellMappings.length && ( <span className="text-xs text-blue-600"> - {cellMappings.filter(m => m.isEditable).length} of {cellMappings.length} fields editable + {cellMappings.filter((m) => m.isEditable).length} of {cellMappings.length} fields editable </span> )} </div> </DialogDescription> </DialogHeader> - {/* 템플릿 선택 UI */} + {/* Template selector */} {normalizedTemplates.length > 1 && ( <div className="flex-shrink-0 px-4 py-2 border-b"> <div className="flex items-center gap-2"> @@ -468,37 +384,30 @@ export function TemplateViewDialog({ <SelectValue placeholder="Select a template" /> </SelectTrigger> <SelectContent> - {normalizedTemplates.map((template) => ( - <SelectItem key={template.TMPL_ID} value={template.TMPL_ID}> + {normalizedTemplates.map((t) => ( + <SelectItem key={t.TMPL_ID} value={t.TMPL_ID}> <div className="flex flex-col"> - <span>{template.NAME || `Template ${template.TMPL_ID.slice(0, 8)}`}</span> - <span className="text-xs text-muted-foreground">{template.TMPL_TYPE}</span> + <span>{t.NAME || `Template ${t.TMPL_ID.slice(0, 8)}`}</span> + <span className="text-xs text-muted-foreground">{t.TMPL_TYPE}</span> </div> </SelectItem> ))} </SelectContent> </Select> - <span className="text-xs text-muted-foreground"> - ({normalizedTemplates.length} templates available) - </span> + <span className="text-xs text-muted-foreground">({normalizedTemplates.length} templates available)</span> </div> </div> )} - - {/* SpreadSheets 컴포넌트 영역 */} + + {/* Spreadsheet */} <div className="flex-1 overflow-hidden"> {selectedTemplate && isClient ? ( - <SpreadSheets - key={selectedTemplateId} - workbookInitialized={initSpread} - hostStyle={hostStyle} - /> + <SpreadSheets key={selectedTemplateId} workbookInitialized={initSpread} hostStyle={hostStyle} /> ) : ( <div className="flex items-center justify-center h-full text-muted-foreground"> {!isClient ? ( <> - <Loader className="mr-2 h-4 w-4 animate-spin" /> - Loading... + <Loader className="mr-2 h-4 w-4 animate-spin" /> Loading... </> ) : ( "No template available" @@ -507,33 +416,26 @@ export function TemplateViewDialog({ )} </div> + {/* footer */} <DialogFooter className="flex-shrink-0"> <Button variant="outline" onClick={onClose}> Close </Button> - {hasChanges && ( - <Button - variant="default" - onClick={handleSaveChanges} - disabled={isPending} - > + <Button variant="default" onClick={handleSaveChanges} disabled={isPending}> {isPending ? ( <> - <Loader className="mr-2 h-4 w-4 animate-spin" /> - Saving... + <Loader className="mr-2 h-4 w-4 animate-spin" /> Saving... </> ) : ( <> - <Save className="mr-2 h-4 w-4" /> - Save Changes + <Save className="mr-2 h-4 w-4" /> Save Changes </> )} </Button> )} - </DialogFooter> </DialogContent> </Dialog> ); -}
\ No newline at end of file +} |
