Clone attachment to other piece
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"typescript.preferences.autoImportFileExcludePatterns": [
|
||||||
|
"@radix-ui",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -316,14 +316,26 @@ export const handle = implement(api, {
|
|||||||
const { userId } = yield* requireOneOf(WRITE_ACCESS);
|
const { userId } = yield* requireOneOf(WRITE_ACCESS);
|
||||||
const db = yield* Database.Database;
|
const db = yield* Database.Database;
|
||||||
|
|
||||||
const sha256 = Sha256.make(new Uint8Array(Bun.SHA256.byteLength));
|
let sha256: Sha256;
|
||||||
Bun.SHA256.hash(attachment.data, sha256);
|
if ("sha256" in attachment) {
|
||||||
|
sha256 = attachment.sha256;
|
||||||
|
|
||||||
yield* db
|
yield* db
|
||||||
.insertInto("File")
|
.selectFrom("File")
|
||||||
.values({ sha256, data: attachment.data })
|
.select("sha256")
|
||||||
.onConflict((cb) => cb.column("sha256").doNothing())
|
.where("sha256", "=", sha256)
|
||||||
.$call(Database.execute);
|
.$call(Database.executeTakeFirst)
|
||||||
|
.pipe(Effect.mapError(() => NotFound.make()));
|
||||||
|
} else {
|
||||||
|
sha256 = Sha256.make(new Uint8Array(Bun.SHA256.byteLength));
|
||||||
|
Bun.SHA256.hash(attachment.data, sha256);
|
||||||
|
|
||||||
|
yield* db
|
||||||
|
.insertInto("File")
|
||||||
|
.values({ sha256, data: attachment.data })
|
||||||
|
.onConflict((cb) => cb.column("sha256").doNothing())
|
||||||
|
.$call(Database.execute);
|
||||||
|
}
|
||||||
|
|
||||||
const res = yield* db
|
const res = yield* db
|
||||||
.insertInto("Attachment")
|
.insertInto("Attachment")
|
||||||
|
|||||||
@@ -169,12 +169,17 @@ export default Api.bundle({
|
|||||||
// --- Attachment CRUD ---
|
// --- Attachment CRUD ---
|
||||||
|
|
||||||
createAttachment: Api.make(
|
createAttachment: Api.make(
|
||||||
Schema.Struct({
|
pipe(
|
||||||
pieceId: PieceId,
|
Schema.Union(
|
||||||
filename: Schema.NonEmptyString,
|
Schema.Struct({ data: Uint8ArrayArrayBufferFromSelf }),
|
||||||
mediaType: Schema.NonEmptyString,
|
Schema.Struct({ sha256: Sha256 }),
|
||||||
data: Uint8ArrayArrayBufferFromSelf,
|
),
|
||||||
}),
|
Schema.extend(Schema.Struct({
|
||||||
|
pieceId: PieceId,
|
||||||
|
filename: Schema.NonEmptyString,
|
||||||
|
mediaType: Schema.NonEmptyString,
|
||||||
|
})),
|
||||||
|
),
|
||||||
Attachment,
|
Attachment,
|
||||||
Schema.Union(Unauthenticated, Unauthorized, NotFound),
|
Schema.Union(Unauthenticated, Unauthorized, NotFound),
|
||||||
),
|
),
|
||||||
|
|||||||
153
packages/frontend/src/components/PieceDialog.tsx
Normal file
153
packages/frontend/src/components/PieceDialog.tsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { pieceCache } from "@/cache";
|
||||||
|
import { client } from "@/client";
|
||||||
|
import { DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
|
||||||
|
import { useCache } from "@/hooks/useCache";
|
||||||
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
|
import { setPieceQueryAuthor, setPieceQueryName, useStore } from "@/hooks/useStore";
|
||||||
|
import { authors, DEBOUNCE } from "@/snippets";
|
||||||
|
import { PieceId } from "common";
|
||||||
|
import { Cause, Effect, Match, Option } from "effect";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { ReactNode, useCallback, useRef } from "react";
|
||||||
|
|
||||||
|
const ADD_ENTRY_DIALOG_LIMIT = 10;
|
||||||
|
|
||||||
|
export namespace PieceDialogContent {
|
||||||
|
export interface Props {
|
||||||
|
readonly title: ReactNode;
|
||||||
|
readonly onPiece: (pieceId: PieceId) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PieceDialogContent(props: PieceDialogContent.Props) {
|
||||||
|
|
||||||
|
const name = useStore(state => state.pieceQueryName);
|
||||||
|
const author = useStore(state => state.pieceQueryAuthor);
|
||||||
|
|
||||||
|
const debounce = useRef(Effect.void);
|
||||||
|
|
||||||
|
const { isLoading, error, data: pieceIds } = useLoading(Effect.gen(function* () {
|
||||||
|
yield* debounce.current;
|
||||||
|
const data = yield* client.queryPieces({
|
||||||
|
name: name !== "" ? Option.some(name) : Option.none(),
|
||||||
|
author: author !== "" ? Option.some(author) : Option.none(),
|
||||||
|
offset: 0,
|
||||||
|
limit: ADD_ENTRY_DIALOG_LIMIT,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}), [name, author]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{props.title}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-row gap-4">
|
||||||
|
<Input
|
||||||
|
className="grow"
|
||||||
|
type="text"
|
||||||
|
placeholder="Tytuł"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPieceQueryName(e.target.value);
|
||||||
|
debounce.current = DEBOUNCE;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className="grow"
|
||||||
|
type="text"
|
||||||
|
placeholder="Twórcy"
|
||||||
|
value={author}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPieceQueryAuthor(e.target.value);
|
||||||
|
debounce.current = DEBOUNCE;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Table className="overflow-y-hidden">
|
||||||
|
<TableBody>
|
||||||
|
{isLoading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} >
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
Ładowanie…
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : error !== null ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-center">
|
||||||
|
{Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${JSON.stringify(error)}`}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
pieceIds.map((pieceId) => (
|
||||||
|
<EntryDialogPieceRow
|
||||||
|
key={pieceId}
|
||||||
|
pieceId={pieceId}
|
||||||
|
onPiece={props.onPiece}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace EntryDialogPieceRow {
|
||||||
|
export interface Props {
|
||||||
|
readonly pieceId: PieceId;
|
||||||
|
readonly onPiece: (pieceId: PieceId) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function EntryDialogPieceRow(props: EntryDialogPieceRow.Props) {
|
||||||
|
|
||||||
|
const { isLoading, error, data: piece } = useCache(pieceCache, props.pieceId);
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
props.onPiece(props.pieceId);
|
||||||
|
}, [props.pieceId, props.onPiece]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={2}>Ładowanie…</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error !== null) {
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={2}>
|
||||||
|
Wystąpił błąd: {Match.value(error).pipe(
|
||||||
|
Match.tag("FetchError", () => "Nie można połączyć się z serwerem"),
|
||||||
|
Match.tag("NotFound", () => "Repertuar nie istnieje"),
|
||||||
|
Match.tag("Unauthenticated", () => "Zaloguj się, aby kontynuować"),
|
||||||
|
Match.tag("Unauthorized", () => "Nie posiadasz uprawnień"),
|
||||||
|
Match.exhaustive,
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow className="select-none cursor-pointer" onClick={onClick}>
|
||||||
|
<TableCell>
|
||||||
|
{piece.name}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{authors(piece)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,16 +9,28 @@ export const mapProp = <const K extends string, T>(prop: K, action: Update<T>) =
|
|||||||
|
|
||||||
export interface Store {
|
export interface Store {
|
||||||
readonly user: User | null;
|
readonly user: User | null;
|
||||||
|
readonly pieceQueryName: string;
|
||||||
|
readonly pieceQueryAuthor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let store: Store = Object.freeze<Store>({
|
let store: Store = Object.freeze<Store>({
|
||||||
user: null,
|
user: null,
|
||||||
|
pieceQueryName: "",
|
||||||
|
pieceQueryAuthor: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
export function setUser(action: Update<User | null>) {
|
export function setUser(action: Update<User | null>) {
|
||||||
set(mapProp("user", action));
|
set(mapProp("user", action));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setPieceQueryName(action: Update<string>) {
|
||||||
|
set(mapProp("pieceQueryName", action));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setPieceQueryAuthor(action: Update<string>) {
|
||||||
|
set(mapProp("pieceQueryAuthor", action));
|
||||||
|
}
|
||||||
|
|
||||||
// --- STORE IMPLEMENTATION ----------------------------------------------------
|
// --- STORE IMPLEMENTATION ----------------------------------------------------
|
||||||
|
|
||||||
class Listener<T> {
|
class Listener<T> {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { Attachment, denormalizePiece, denormalizeSystemInformation, pieceCache, type Piece } from "@/cache";
|
import { Attachment, denormalizePiece, denormalizeSystemInformation, pieceCache, type Piece } from "@/cache";
|
||||||
import { API_URL_PREFIX, client } from "@/client";
|
import { client } from "@/client";
|
||||||
|
import { PieceDialogContent } from "@/components/PieceDialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
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 { useCache } from "@/hooks/useCache";
|
import { useCache } from "@/hooks/useCache";
|
||||||
@@ -13,7 +16,7 @@ import * as Body from "common/Body";
|
|||||||
import { getMediaTypeForFilename } from "common/MediaType";
|
import { getMediaTypeForFilename } from "common/MediaType";
|
||||||
import { Array, Cause, DateTime, Effect, Exit, Fiber, Option, pipe, Scope } from "effect";
|
import { Array, Cause, DateTime, Effect, Exit, Fiber, Option, pipe, Scope } from "effect";
|
||||||
import { constant } from "effect/Function";
|
import { constant } from "effect/Function";
|
||||||
import { Download, Loader2, Trash, UploadCloud } from "lucide-react";
|
import { Download, EllipsisVertical, Loader2, Trash, UploadCloud } from "lucide-react";
|
||||||
import { DragEventHandler, FormEventHandler, MouseEvent, useCallback, useId, useState } from "react";
|
import { DragEventHandler, FormEventHandler, MouseEvent, useCallback, useId, useState } from "react";
|
||||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
@@ -208,6 +211,8 @@ namespace AttachmentRow {
|
|||||||
|
|
||||||
function AttachmentRow(props: AttachmentRow.Props) {
|
function AttachmentRow(props: AttachmentRow.Props) {
|
||||||
|
|
||||||
|
const [cloneDialogOpen, setCloneDialogOpen] = useState(false);
|
||||||
|
|
||||||
const download = () => Effect.gen(function* () {
|
const download = () => Effect.gen(function* () {
|
||||||
const { data, mediaType, filename } = yield* client.getAttachment(props.attachment.attachmentId);
|
const { data, mediaType, filename } = yield* client.getAttachment(props.attachment.attachmentId);
|
||||||
|
|
||||||
@@ -248,7 +253,28 @@ function AttachmentRow(props: AttachmentRow.Props) {
|
|||||||
yield* pieceCache.update(props.attachment.pieceId, mapProp("attachments", Array.filter<Attachment>((a) => a.attachmentId !== props.attachment.attachmentId)));
|
yield* pieceCache.update(props.attachment.pieceId, mapProp("attachments", Array.filter<Attachment>((a) => a.attachmentId !== props.attachment.attachmentId)));
|
||||||
}).pipe(Effect.runPromise);
|
}).pipe(Effect.runPromise);
|
||||||
|
|
||||||
return (
|
const onCloneToPiece = (pieceId: PieceId) => Effect.gen(function* () {
|
||||||
|
|
||||||
|
const attachment = yield* pipe(
|
||||||
|
client.createAttachment({
|
||||||
|
pieceId,
|
||||||
|
filename: props.attachment.filename,
|
||||||
|
mediaType: props.attachment.mediaType,
|
||||||
|
sha256: props.attachment.sha256,
|
||||||
|
}),
|
||||||
|
Effect.flatMap(denormalizeSystemInformation)
|
||||||
|
);
|
||||||
|
|
||||||
|
yield* pieceCache.update(pieceId, mapProp("attachments", (attachments: readonly Attachment[]) => {
|
||||||
|
const next = [...attachments, attachment];
|
||||||
|
next.sort((a, b) => a.filename.localeCompare(b.filename));
|
||||||
|
return next;
|
||||||
|
}));
|
||||||
|
|
||||||
|
setCloneDialogOpen(false);
|
||||||
|
}).pipe(Effect.runPromise);
|
||||||
|
|
||||||
|
return (<>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{props.attachment.mediaType === "application/vnd.recordare.musicxml"
|
{props.attachment.mediaType === "application/vnd.recordare.musicxml"
|
||||||
@@ -277,9 +303,27 @@ function AttachmentRow(props: AttachmentRow.Props) {
|
|||||||
<Button type="button" variant="ghost" size="icon" title="Usuń" onClick={doDelete}>
|
<Button type="button" variant="ghost" size="icon" title="Usuń" onClick={doDelete}>
|
||||||
<Trash />
|
<Trash />
|
||||||
</Button>
|
</Button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button type="button" variant="ghost" size="icon" title="Więcej">
|
||||||
|
<EllipsisVertical />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => setCloneDialogOpen(true)}>
|
||||||
|
Kopiuj do…
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
<Dialog open={cloneDialogOpen} onOpenChange={setCloneDialogOpen}>
|
||||||
|
<PieceDialogContent
|
||||||
|
title="Kopiuj do utworu"
|
||||||
|
onPiece={onCloneToPiece}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
</>);
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace AttachmentForm {
|
namespace AttachmentForm {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { denormalizeRepertoire, Piece, pieceCache, type Repertoire, repertoireCache } from "@/cache";
|
import { denormalizeRepertoire, Piece, type Repertoire, repertoireCache } from "@/cache";
|
||||||
import { client } from "@/client";
|
import { client } from "@/client";
|
||||||
|
import { PieceDialogContent } from "@/components/PieceDialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { useCache } from "@/hooks/useCache";
|
import { useCache } from "@/hooks/useCache";
|
||||||
import { useLoading } from "@/hooks/useLoading";
|
import { authors, SAVE_DELAY } from "@/snippets";
|
||||||
import { authors, DEBOUNCE, SAVE_DELAY } from "@/snippets";
|
import { PieceId, RepertoireId } from "common";
|
||||||
import { PieceId, RepertoireId, Updater } from "common";
|
import { Array, Cause, Effect, Fiber, Option, pipe, Scope } from "effect";
|
||||||
import { Array, Cause, Effect, Fiber, Match, Option, pipe, Scope } from "effect";
|
|
||||||
import { ChevronDown, ChevronUp, CircleMinus, Loader2, Plus } from "lucide-react";
|
import { ChevronDown, ChevronUp, CircleMinus, Loader2, Plus } from "lucide-react";
|
||||||
import { FormEventHandler, useId, useRef, useState } from "react";
|
import { FormEventHandler, useId, useState } from "react";
|
||||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
export function Repertoire() {
|
export function Repertoire() {
|
||||||
@@ -135,6 +135,24 @@ function Entries(props: Entries.Props) {
|
|||||||
|
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const onPiece = (pieceId: PieceId) => Effect.gen(function* () {
|
||||||
|
|
||||||
|
const action = Array.append(pieceId);
|
||||||
|
const mapToId = Array.map<readonly Piece[], PieceId>(({ pieceId }) => pieceId);
|
||||||
|
|
||||||
|
yield* pipe(
|
||||||
|
client.updateRepertoire({
|
||||||
|
repertoireId: props.repertoire.repertoireId,
|
||||||
|
name: props.repertoire.name,
|
||||||
|
entries: pipe(props.repertoire.entries, mapToId, action),
|
||||||
|
}),
|
||||||
|
Effect.flatMap(denormalizeRepertoire),
|
||||||
|
Effect.tap((repertoire) => repertoireCache.update(repertoire.repertoireId, repertoire)),
|
||||||
|
);
|
||||||
|
|
||||||
|
setDialogOpen(false);
|
||||||
|
}).pipe(Effect.runPromise);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 items-start grow">
|
<div className="flex flex-col gap-4 items-start grow">
|
||||||
<Table>
|
<Table>
|
||||||
@@ -163,7 +181,10 @@ function Entries(props: Entries.Props) {
|
|||||||
<Plus />Dodaj utwór
|
<Plus />Dodaj utwór
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<AddEntryDialogContent repertoire={props.repertoire} setDialogOpen={setDialogOpen} />
|
<PieceDialogContent
|
||||||
|
title="Dodaj utwór"
|
||||||
|
onPiece={onPiece}
|
||||||
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -248,159 +269,3 @@ function EntryRow({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADD_ENTRY_DIALOG_LIMIT = 10;
|
|
||||||
|
|
||||||
namespace AddEntryDialogContent {
|
|
||||||
export interface Props {
|
|
||||||
readonly repertoire: Repertoire;
|
|
||||||
readonly setDialogOpen: Updater<boolean>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function AddEntryDialogContent(props: AddEntryDialogContent.Props) {
|
|
||||||
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [author, setAuthor] = useState("");
|
|
||||||
|
|
||||||
const debounce = useRef(Effect.void);
|
|
||||||
|
|
||||||
const { isLoading, error, data: pieceIds } = useLoading(Effect.gen(function* () {
|
|
||||||
yield* debounce.current;
|
|
||||||
const data = yield* client.queryPieces({
|
|
||||||
name: name !== "" ? Option.some(name) : Option.none(),
|
|
||||||
author: author !== "" ? Option.some(author) : Option.none(),
|
|
||||||
offset: 0,
|
|
||||||
limit: ADD_ENTRY_DIALOG_LIMIT,
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}), [name, author]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Dodaj utwór</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex flex-row gap-4">
|
|
||||||
<Input
|
|
||||||
className="grow"
|
|
||||||
type="text"
|
|
||||||
placeholder="Tytuł"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => {
|
|
||||||
setName(e.target.value);
|
|
||||||
debounce.current = DEBOUNCE;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
className="grow"
|
|
||||||
type="text"
|
|
||||||
placeholder="Twórcy"
|
|
||||||
value={author}
|
|
||||||
onChange={(e) => {
|
|
||||||
setAuthor(e.target.value);
|
|
||||||
debounce.current = DEBOUNCE;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Table className="overflow-y-hidden">
|
|
||||||
<TableBody>
|
|
||||||
{isLoading ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={4} >
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
<Loader2 className="animate-spin" />
|
|
||||||
Ładowanie…
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : error !== null ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={4} className="text-center">
|
|
||||||
{Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${JSON.stringify(error)}`}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
|
||||||
pieceIds.map((pieceId) => (
|
|
||||||
<EntryDialogPieceRow
|
|
||||||
key={pieceId}
|
|
||||||
repertoire={props.repertoire}
|
|
||||||
pieceId={pieceId}
|
|
||||||
setDialogOpen={props.setDialogOpen}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace EntryDialogPieceRow {
|
|
||||||
export interface Props {
|
|
||||||
readonly repertoire: Repertoire;
|
|
||||||
readonly pieceId: PieceId;
|
|
||||||
readonly setDialogOpen: Updater<boolean>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function EntryDialogPieceRow(props: EntryDialogPieceRow.Props) {
|
|
||||||
|
|
||||||
const { isLoading, error, data: piece } = useCache(pieceCache, props.pieceId);
|
|
||||||
|
|
||||||
const onClick = () => Effect.gen(function* () {
|
|
||||||
|
|
||||||
const action = Array.append(piece!);
|
|
||||||
const mapToId = Array.map<readonly Piece[], PieceId>(({ pieceId }) => pieceId);
|
|
||||||
|
|
||||||
yield* pipe(
|
|
||||||
client.updateRepertoire({
|
|
||||||
repertoireId: props.repertoire.repertoireId,
|
|
||||||
name: props.repertoire.name,
|
|
||||||
entries: pipe(props.repertoire.entries, action, mapToId),
|
|
||||||
}),
|
|
||||||
Effect.flatMap(denormalizeRepertoire),
|
|
||||||
Effect.tap((repertoire) => repertoireCache.update(repertoire.repertoireId, repertoire)),
|
|
||||||
);
|
|
||||||
|
|
||||||
props.setDialogOpen(false);
|
|
||||||
}).pipe(Effect.runPromise);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={2}>Ładowanie…</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error !== null) {
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={2}>
|
|
||||||
Wystąpił błąd: {Match.value(error).pipe(
|
|
||||||
Match.tag("FetchError", () => "Nie można połączyć się z serwerem"),
|
|
||||||
Match.tag("NotFound", () => "Repertuar nie istnieje"),
|
|
||||||
Match.tag("Unauthenticated", () => "Zaloguj się, aby kontynuować"),
|
|
||||||
Match.tag("Unauthorized", () => "Nie posiadasz uprawnień"),
|
|
||||||
Match.exhaustive,
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow className="select-none cursor-pointer" onClick={onClick}>
|
|
||||||
<TableCell>
|
|
||||||
{piece.name}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{authors(piece)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user