Drag and drop attachments, implement attachment delete, update attachment list
This commit is contained in:
@@ -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();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = timeout(250);
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const { error } = await client.piece({ pieceId: props.pieceId }).attachment.post({
|
for (const file of e.dataTransfer.files) {
|
||||||
filename,
|
const mediaType = getMediaTypeForFilename(file.name);
|
||||||
mediaType,
|
if (mediaType === undefined) {
|
||||||
data: file!,
|
continue;
|
||||||
});
|
}
|
||||||
|
|
||||||
if (error) {
|
const { data, error } = await client.piece({ pieceId: props.pieceId }).attachment.post({
|
||||||
console.error(error.value);
|
filename: file.name,
|
||||||
return;
|
mediaType,
|
||||||
}
|
data: file,
|
||||||
|
});
|
||||||
|
|
||||||
reduce(FileReducer.reset);
|
if (error !== null) {
|
||||||
if (fileInputRef.current !== null) {
|
console.error(error.value);
|
||||||
fileInputRef.current.files = null;
|
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"
|
</div>
|
||||||
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 className="flex flex-col-reverse items-end">
|
|
||||||
<Button type="submit" disabled={isLoading}>
|
|
||||||
{isLoading && <Loader2 className="animate-spin" />}
|
|
||||||
Dodaj
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user