Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 2m37s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / updater-image (push) Successful in 38s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 8s
121 lines
3.7 KiB
TypeScript
121 lines
3.7 KiB
TypeScript
"use client"
|
|
|
|
import { useCallback, useState } from "react"
|
|
import { UploadIcon } from "lucide-react"
|
|
import { toast } from "sonner"
|
|
import { useFileUpload } from "@/hooks/use-file-upload"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
interface FileImportDialogProps {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
title: string
|
|
description?: string
|
|
accept?: string
|
|
multiple?: boolean
|
|
onImport: (files: File[]) => void | Promise<void>
|
|
}
|
|
|
|
function FileImportDialog({
|
|
open,
|
|
onOpenChange,
|
|
title,
|
|
description,
|
|
accept = "*",
|
|
multiple = false,
|
|
onImport,
|
|
}: FileImportDialogProps) {
|
|
const [importing, setImporting] = useState(false)
|
|
|
|
const [{ files, isDragging, errors }, actions] = useFileUpload({
|
|
accept,
|
|
multiple,
|
|
maxFiles: multiple ? 10 : 1,
|
|
onError: (errs) => {
|
|
errs.forEach((e) => toast.error(e))
|
|
},
|
|
})
|
|
|
|
const handleImport = useCallback(async () => {
|
|
const rawFiles = files
|
|
.map((f) => (f.file instanceof File ? f.file : null))
|
|
.filter((f): f is File => f != null)
|
|
if (rawFiles.length === 0) {
|
|
toast.error("Выберите файл для импорта")
|
|
return
|
|
}
|
|
setImporting(true)
|
|
try {
|
|
await onImport(rawFiles)
|
|
actions.clearFiles()
|
|
onOpenChange(false)
|
|
toast.success("Импорт выполнен")
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : "Ошибка импорта")
|
|
} finally {
|
|
setImporting(false)
|
|
}
|
|
}, [actions, files, onImport, onOpenChange])
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>{title}</DialogTitle>
|
|
{description && <DialogDescription>{description}</DialogDescription>}
|
|
</DialogHeader>
|
|
<div
|
|
className={cn(
|
|
"flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed p-8 text-center transition-colors",
|
|
isDragging ? "border-primary bg-primary/5" : "border-border",
|
|
)}
|
|
onDragEnter={actions.handleDragEnter}
|
|
onDragLeave={actions.handleDragLeave}
|
|
onDragOver={actions.handleDragOver}
|
|
onDrop={actions.handleDrop}
|
|
>
|
|
<UploadIcon className="size-8 text-muted-foreground/40" />
|
|
<p className="text-sm text-muted-foreground">
|
|
Перетащите файл сюда или выберите на диске
|
|
</p>
|
|
<Button type="button" variant="outline" size="sm" onClick={actions.openFileDialog}>
|
|
Выбрать файл
|
|
</Button>
|
|
<input {...actions.getInputProps()} className="sr-only" />
|
|
{files.length > 0 && (
|
|
<ul className="w-full text-left text-sm">
|
|
{files.map((f) => (
|
|
<li key={f.id} className="truncate font-mono text-xs">
|
|
{f.file instanceof File ? f.file.name : f.file.name}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
{errors.map((e) => (
|
|
<p key={e} className="text-xs text-destructive">{e}</p>
|
|
))}
|
|
</div>
|
|
<DialogFooter>
|
|
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
|
Отмена
|
|
</Button>
|
|
<Button type="button" onClick={handleImport} disabled={importing || files.length === 0}>
|
|
{importing ? "Импорт…" : "Импортировать"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
export { FileImportDialog, type FileImportDialogProps }
|