Drag and drop attachments, implement attachment delete, update attachment list

This commit is contained in:
2024-12-01 22:16:53 +01:00
parent b625a5bcc1
commit d75ecf61b6

View File

@@ -2,16 +2,17 @@ import { client } from "@/client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { FileReducer } from "@/FileReducer";
import { useLoading } from "@/hooks/useLoading"; import { useLoading } from "@/hooks/useLoading";
import { Updater } from "@/hooks/useStore";
import { timeout } from "@/lib/utils"; import { timeout } from "@/lib/utils";
import { Label } from "@radix-ui/react-label"; import { Label } from "@radix-ui/react-label";
import type { Attachment, Piece } from "backend/database"; import type { Attachment, Piece } from "backend/database";
import clsx from "clsx";
import { PieceId } from "common"; import { PieceId } from "common";
import { ACCEPTED_EXTENSIONS } from "common/MediaType"; import { getMediaTypeForFilename } from "common/MediaType";
import { ELYSIA_FORM_DATA } from "elysia"; import { ELYSIA_FORM_DATA } from "elysia";
import { Download, Loader2, Trash } from "lucide-react"; import { Download, Loader2, Trash, UploadCloud } from "lucide-react";
import { FormEventHandler, MouseEvent, useCallback, useId, useReducer, useRef, useState } from "react"; import { DragEventHandler, FormEventHandler, MouseEvent, useCallback, useId, useState } from "react";
import { Link, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
export function Piece() { export function Piece() {
@@ -40,9 +41,9 @@ export function Piece() {
<h3 className="font-bold text-lg">Utwór</h3> <h3 className="font-bold text-lg">Utwór</h3>
<PieceForm piece={piece.data[0]} /> <PieceForm piece={piece.data[0]} />
<h3 className="font-bold text-lg">Załączniki</h3> <h3 className="font-bold text-lg">Załączniki</h3>
<AttachmentForm pieceId={piece.data[0].pieceId} /> <AttachmentForm pieceId={piece.data[0].pieceId} setAttachments={attachments.setData} />
</div> </div>
<Attachments pieceId={piece.data[0].pieceId} attachments={attachments.data} /> <Attachments pieceId={piece.data[0].pieceId} attachments={attachments.data} setAttachments={attachments.setData} />
</>)} </>)}
</div> </div>
); );
@@ -143,6 +144,7 @@ namespace Attachments {
export interface Props { export interface Props {
readonly pieceId: PieceId; readonly pieceId: PieceId;
readonly attachments: readonly Attachment[]; readonly attachments: readonly Attachment[];
readonly setAttachments: Updater<Attachment[]>;
} }
} }
@@ -160,7 +162,13 @@ function Attachments(props: Attachments.Props) {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{props.attachments.map((attachment) => <AttachmentRow key={attachment.attachmentId} attachment={attachment} />)} {props.attachments.map((attachment) => (
<AttachmentRow
key={attachment.attachmentId}
attachment={attachment}
setAttachments={props.setAttachments}
/>
))}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
@@ -170,6 +178,7 @@ function Attachments(props: Attachments.Props) {
namespace AttachmentRow { namespace AttachmentRow {
export interface Props { export interface Props {
readonly attachment: Attachment; readonly attachment: Attachment;
readonly setAttachments: Updater<Attachment[]>;
} }
} }
@@ -218,6 +227,22 @@ function AttachmentRow(props: AttachmentRow.Props) {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}, [props.attachment.mediaType, props.attachment.attachmentId, props.attachment.pieceId]); }, [props.attachment.mediaType, props.attachment.attachmentId, props.attachment.pieceId]);
const doDelete = useCallback(async () => {
const { error } = await client
.piece({ pieceId: props.attachment.pieceId })
.attachment({ attachmentId: props.attachment.attachmentId })
.delete();
if (error !== null) {
console.error(error.value);
return;
}
props.setAttachments((prev) => prev.filter((a) => a.attachmentId !== props.attachment.attachmentId));
}, [props.attachment.attachmentId, props.attachment.pieceId]);
return ( return (
<TableRow> <TableRow>
<TableCell> <TableCell>
@@ -246,7 +271,7 @@ function AttachmentRow(props: AttachmentRow.Props) {
<Button type="button" variant="ghost" size="icon" title="Pobierz" onClick={download}> <Button type="button" variant="ghost" size="icon" title="Pobierz" onClick={download}>
<Download /> <Download />
</Button> </Button>
<Button type="button" variant="ghost" size="icon" title="Usuń"> <Button type="button" variant="ghost" size="icon" title="Usuń" onClick={doDelete}>
<Trash /> <Trash />
</Button> </Button>
</TableCell> </TableCell>
@@ -257,86 +282,75 @@ function AttachmentRow(props: AttachmentRow.Props) {
namespace AttachmentForm { namespace AttachmentForm {
export interface Props { export interface Props {
readonly pieceId: PieceId; readonly pieceId: PieceId;
readonly setAttachments: Updater<Attachment[]>;
} }
} }
function AttachmentForm(props: AttachmentForm.Props) { function AttachmentForm(props: AttachmentForm.Props) {
const [{ filename, mediaType, file }, reduce] = useReducer(FileReducer, FileReducer.initial);
const filenameId = useId();
const mediaTypeId = useId();
const fileId = useId();
const fileInputRef = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const onSubmit: FormEventHandler<HTMLFormElement> = async (e) => { const onDragOver: DragEventHandler<HTMLElement> = async (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
};
const onDrop: DragEventHandler<HTMLElement> = async (e) => {
e.preventDefault(); e.preventDefault();
try { if (isLoading) {
setIsLoading(true);
const { error } = await client.piece({ pieceId: props.pieceId }).attachment.post({
filename,
mediaType,
data: file!,
});
if (error) {
console.error(error.value);
return; return;
} }
reduce(FileReducer.reset); const delay = timeout(250);
if (fileInputRef.current !== null) { try {
fileInputRef.current.files = null; setIsLoading(true);
for (const file of e.dataTransfer.files) {
const mediaType = getMediaTypeForFilename(file.name);
if (mediaType === undefined) {
continue;
}
const { data, error } = await client.piece({ pieceId: props.pieceId }).attachment.post({
filename: file.name,
mediaType,
data: file,
});
if (error !== null) {
console.error(error.value);
continue;
}
props.setAttachments((prev) => {
const next = [...prev, data];
next.sort((a, b) => a.filename.localeCompare(b.filename));
return next;
});
} }
} finally { } finally {
await delay;
setIsLoading(false); setIsLoading(false);
} }
} }
return ( return (
<form className="flex flex-col gap-4" onSubmit={onSubmit}> <div
<div className="grid items-baseline grid-cols-4 gap-4"> className={clsx(
<Label htmlFor={filenameId} className="text-right">Nazwa pliku</Label> "flex flex-col border rounded px-4 py-8 items-center select-none bg-stone-100",
<Input isLoading && "text-stone-400",
id={filenameId} )}
className="col-span-3" onDragOver={onDragOver}
type="text" onDrop={onDrop}
value={filename} >
required {isLoading ? (<>
onChange={(e) => reduce(FileReducer.setFilename(e.target.value))} <Loader2 className="animate-spin" />
/> <div>Wysyłanie załączników</div>
<Label htmlFor={mediaTypeId} className="text-right">Typ</Label> </>) : (<>
<Input <UploadCloud />
id={mediaTypeId} <div>Przeciągnij i upuść tutaj załączniki</div>
className="col-span-3" </>)}
type="text"
value={mediaType}
onChange={(e) => reduce(FileReducer.setMediaType(e.target.value))}
/>
<Label htmlFor={fileId} className="text-right">Plik</Label>
<Input
ref={fileInputRef}
id={fileId}
className="col-span-3"
type="file"
onChange={(e) => {
const file = e.target.files?.item(0) ?? null;
reduce(FileReducer.setFile(file));
}}
accept={ACCEPTED_EXTENSIONS}
/>
</div> </div>
<div className="flex flex-col-reverse items-end">
<Button type="submit" disabled={isLoading}>
{isLoading && <Loader2 className="animate-spin" />}
Dodaj
</Button>
</div>
</form>
); );
} }