From a6ad34c7215f2302f5381d3f737579b27664972b Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Thu, 16 Oct 2025 01:32:48 +0200 Subject: [PATCH] Infinite piece query --- packages/frontend/src/routes/Pieces.tsx | 127 ++++++++++++++++++------ 1 file changed, 95 insertions(+), 32 deletions(-) diff --git a/packages/frontend/src/routes/Pieces.tsx b/packages/frontend/src/routes/Pieces.tsx index e81e2e5..bfc4661 100644 --- a/packages/frontend/src/routes/Pieces.tsx +++ b/packages/frontend/src/routes/Pieces.tsx @@ -14,32 +14,27 @@ import clsx from "clsx"; import { PieceId, Updater } from "common"; import * as Body from "common/Body"; import { getMediaTypeForFilename } from "common/MediaType"; -import { Array, Cause, Effect, Fiber, Iterable, Match, Option, Order, pipe, Predicate, Scope, SortedMap } from "effect"; -import { Import, Loader2, Plus } from "lucide-react"; -import { DragEventHandler, FormEventHandler, useId, useRef, useState } from "react"; +import { Array, Effect, Fiber, Iterable, Match, Option, Order, pipe, Predicate, Scope, SortedMap } from "effect"; +import { Import, Loader2, Plus, RefreshCwIcon } from "lucide-react"; +import { DragEventHandler, FormEventHandler, RefObject, useCallback, useId, useImperativeHandle, useRef, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; +const LIMIT = 100; + export function Pieces() { const name = useStore(state => state.pieceQueryName); const author = useStore(state => state.pieceQueryAuthor); const [importDialogOpen, setImportDialogOpen] = useState(false); + const [refresh, setRefresh] = useState(Effect.void); const debounce = useRef(Effect.void); const breakpoint = useBreakpoint(); - const columns = breakpoint ? 4 : 1; - const { isLoading, error, data: pieceIds, refresh } = 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: 100, - }); - return data; - }), [name, author]); + const queryRef = useCallback((ref: Query.Ref | null) => { + setRefresh(ref !== null ? ref.refresh : Effect.void); + }, []); return (
@@ -93,30 +88,98 @@ export function Pieces() { )} - {isLoading ? ( - - -
- - Ładowanie… -
-
-
- ) : error !== null ? ( - - - {Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${JSON.stringify(error)}`} - - - ) : ( - pieceIds.map((pieceId) => ) - )} +
); } +namespace Query { + export interface Props { + readonly offset: number; + readonly debounce?: RefObject> | undefined; + readonly ref?: React.Ref | undefined; + } + + export interface Ref { + readonly refresh: Effect.Effect; + } +} + +function Query({ + offset, + debounce, + ref, +}: Query.Props) { + + const name = useStore(state => state.pieceQueryName); + const author = useStore(state => state.pieceQueryAuthor); + + const breakpoint = useBreakpoint(); + const columns = breakpoint ? 4 : 1; + + const [continuationOpen, setContinuationOpen] = useState(false); + + const { isLoading, error, data: pieceIds, refresh } = useLoading(Effect.gen(function* () { + setContinuationOpen(false); + if (debounce !== undefined) { + yield* debounce.current; + } + + const data = yield* client.queryPieces({ + name: name !== "" ? Option.some(name) : Option.none(), + author: author !== "" ? Option.some(author) : Option.none(), + offset, + limit: LIMIT, + }); + return data; + }), [name, author, offset]); + + useImperativeHandle(ref, () => Object.freeze({ + refresh, + }), [refresh]); + + return isLoading ? ( + + +
+ + Ładowanie… +
+
+
+ ) : error !== null ? ( + + + Wystąpił błąd: {Match.value(error).pipe( + Match.tag("FetchError", () => "Nie można połączyć się z serwerem"), + Match.tag("Unauthenticated", () => "Zaloguj się, aby kontynuować"), + Match.tag("Unauthorized", () => "Nie posiadasz uprawnień"), + Match.exhaustive, + )} + + + ) : (<> + {pieceIds.map((pieceId) => )} + {continuationOpen ? ( + + ) : pieceIds.length >= LIMIT ? ( + + + + + + ) : null} + ); +} + namespace PieceRow { export interface Props { readonly pieceId: PieceId;