Bulk import frontend utility
This commit is contained in:
@@ -24,8 +24,6 @@ export const Config = Schema.Struct({
|
||||
),
|
||||
OAUTH_AUTHORIZATION_ENDPOINT: Schema.String,
|
||||
OAUTH_TOKEN_ENDPOINT: Schema.String,
|
||||
POCKET_ID_API_ORIGIN: Schema.String,
|
||||
POCKET_ID_API_KEY: Schema.String,
|
||||
});
|
||||
|
||||
export type Config = typeof Config.Type;
|
||||
|
||||
@@ -6,11 +6,15 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { useLoading } from "@/hooks/useLoading";
|
||||
import { authors, created, DEBOUNCE, modified } from "@/snippets";
|
||||
import { Updater } from "@/hooks/useStore";
|
||||
import { authors, created, DEBOUNCE, modified, SAVE_DELAY } from "@/snippets";
|
||||
import clsx from "clsx";
|
||||
import { PieceId } from "common";
|
||||
import { Cause, Effect, Match, Option, Scope } from "effect";
|
||||
import { Loader2, Plus } from "lucide-react";
|
||||
import { FormEventHandler, useId, useRef, useState } from "react";
|
||||
import * as Body from "common/Body";
|
||||
import { getMediaTypeForFilename } from "common/MediaType";
|
||||
import { Array, Cause, Effect, Fiber, Iterable, Match, Option, Order, pipe, Scope, SortedMap } from "effect";
|
||||
import { Import, Loader2, Plus, UploadCloud } from "lucide-react";
|
||||
import { DragEventHandler, FormEventHandler, useId, useRef, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
export function Pieces() {
|
||||
@@ -18,6 +22,8 @@ export function Pieces() {
|
||||
const [name, setName] = useState("");
|
||||
const [author, setAuthor] = useState("");
|
||||
|
||||
const [importDialogOpen, setImportDialogOpen] = useState(false);
|
||||
|
||||
const debounce = useRef(Effect.void);
|
||||
|
||||
const { isLoading, error, data: pieceIds } = useLoading(Effect.gen(function* () {
|
||||
@@ -42,6 +48,14 @@ export function Pieces() {
|
||||
</DialogTrigger>
|
||||
<AddPieceDialogContent />
|
||||
</Dialog>
|
||||
<Dialog open={importDialogOpen} onOpenChange={setImportDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Import />Importuj utwory
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<ImportPiecesDialogContent setDialogOpen={setImportDialogOpen} />
|
||||
</Dialog>
|
||||
<Input
|
||||
className="w-[32ch]"
|
||||
type="text"
|
||||
@@ -237,3 +251,157 @@ function AddPieceDialogContent() {
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
namespace ImportPiecesDialogContent {
|
||||
export interface Props {
|
||||
readonly setDialogOpen: Updater<boolean>;
|
||||
}
|
||||
}
|
||||
|
||||
function ImportPiecesDialogContent(props: ImportPiecesDialogContent.Props) {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [attachments, setAttachments] = useState<SortedMap.SortedMap<string, readonly File[]>>(() => SortedMap.empty(Order.string));
|
||||
|
||||
const onDragOver: DragEventHandler<HTMLElement> = (e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
};
|
||||
|
||||
const onDrop: DragEventHandler<HTMLElement> = (e) => Effect.gen(function* () {
|
||||
e.preventDefault();
|
||||
|
||||
setAttachments((value) => {
|
||||
for (const file of e.dataTransfer.files) {
|
||||
const mediaType = getMediaTypeForFilename(file.name);
|
||||
if (mediaType === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = file.name.substring(0, file.name.lastIndexOf("."));
|
||||
|
||||
const entry = pipe(
|
||||
value,
|
||||
SortedMap.get(name),
|
||||
Option.getOrElse(Array.empty),
|
||||
Array.append(file),
|
||||
);
|
||||
|
||||
value = SortedMap.set(value, name, entry);
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
}).pipe(Effect.runPromise);
|
||||
|
||||
const onSubmit: FormEventHandler<HTMLFormElement> = (e) => Effect.gen(function* () {
|
||||
e.preventDefault();
|
||||
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delay = yield* Effect.fork(SAVE_DELAY);
|
||||
|
||||
yield* Effect.scopedWith((scope) => Effect.gen(function* () {
|
||||
|
||||
yield* Scope.addFinalizer(scope, Effect.gen(function* () {
|
||||
yield* Fiber.join(delay);
|
||||
setIsLoading(false);
|
||||
}));
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
yield* pipe(
|
||||
attachments,
|
||||
Iterable.map(([name, attachments]) => Effect.gen(function* () {
|
||||
|
||||
const { pieceId } = yield* client.createPiece({
|
||||
name,
|
||||
composer: Option.none(),
|
||||
arranger: Option.none(),
|
||||
lyricist: Option.none(),
|
||||
});
|
||||
|
||||
yield* pipe(
|
||||
attachments,
|
||||
Array.map((file) => Effect.gen(function* () {
|
||||
const mediaType = getMediaTypeForFilename(file.name);
|
||||
if (mediaType === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE Apparently, file.bytes is not a thing in this context
|
||||
const data = new Uint8Array(yield* Body.arrayBuffer(file));
|
||||
|
||||
yield* client.createAttachment({
|
||||
pieceId,
|
||||
data,
|
||||
filename: file.name,
|
||||
mediaType,
|
||||
});
|
||||
})),
|
||||
Effect.allWith({ concurrency: "unbounded" }),
|
||||
);
|
||||
})),
|
||||
Effect.allWith({ concurrency: "unbounded" }),
|
||||
);
|
||||
|
||||
props.setDialogOpen(false);
|
||||
}));
|
||||
}).pipe(Effect.runPromise);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<form onSubmit={onSubmit}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Importuj utwory</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 py-4">
|
||||
{SortedMap.size(attachments) > 0 && (
|
||||
<div className="max-h-[50vh] overflow-y-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Tytuł</TableHead>
|
||||
<TableHead className="text-center">Liczba plików</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{pipe(
|
||||
attachments,
|
||||
SortedMap.entries,
|
||||
Iterable.map(([name, files]) => (
|
||||
<TableRow key={name}>
|
||||
<TableCell>{name}</TableCell>
|
||||
<TableCell className="text-center">{files.length}</TableCell>
|
||||
</TableRow>
|
||||
)),
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
"flex flex-col border rounded px-4 py-8 items-center select-none bg-stone-100",
|
||||
isLoading && "text-stone-400",
|
||||
)}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
<Import />
|
||||
<div>Przeciągnij i upuść tutaj załączniki</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading && <Loader2 className="animate-spin" />}
|
||||
Importuj
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user