From a533ba02dc3f75c9bcfabf60bc5c760f241d61d0 Mon Sep 17 00:00:00 2001 From: gitkeniwo <34304497+gitkeniwo@users.noreply.github.com> Date: Fri, 21 Nov 2025 02:04:41 +0100 Subject: [PATCH] fix: add load more button and pagination to attachments page (#5258) --- web/src/pages/Attachments.tsx | 153 ++++++++++++++++++++++------------ 1 file changed, 98 insertions(+), 55 deletions(-) diff --git a/web/src/pages/Attachments.tsx b/web/src/pages/Attachments.tsx index 7e6d33e40..6384f5c8c 100644 --- a/web/src/pages/Attachments.tsx +++ b/web/src/pages/Attachments.tsx @@ -3,16 +3,17 @@ import { includes } from "lodash-es"; import { PaperclipIcon, SearchIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import AttachmentIcon from "@/components/AttachmentIcon"; import Empty from "@/components/Empty"; import MobileHeader from "@/components/MobileHeader"; +import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { attachmentServiceClient } from "@/grpcweb"; import useLoading from "@/hooks/useLoading"; import useResponsiveWidth from "@/hooks/useResponsiveWidth"; import i18n from "@/i18n"; -import { memoStore } from "@/store"; import { Attachment } from "@/types/proto/api/v1/attachment_service"; import { useTranslate } from "@/utils/i18n"; @@ -42,18 +43,51 @@ const Attachments = observer(() => { searchQuery: "", }); const [attachments, setAttachments] = useState([]); + const [nextPageToken, setNextPageToken] = useState(""); + const [isLoadingMore, setIsLoadingMore] = useState(false); const filteredAttachments = attachments.filter((attachment) => includes(attachment.filename, state.searchQuery)); const groupedAttachments = groupAttachmentsByDate(filteredAttachments.filter((attachment) => attachment.memo)); const unusedAttachments = filteredAttachments.filter((attachment) => !attachment.memo); useEffect(() => { - attachmentServiceClient.listAttachments({}).then(({ attachments }) => { - setAttachments(attachments); - loadingState.setFinish(); - Promise.all(attachments.map((attachment) => (attachment.memo ? memoStore.getOrFetchMemoByName(attachment.memo) : null))); - }); + const fetchInitialAttachments = async () => { + try { + const { attachments: fetchedAttachments, nextPageToken } = await attachmentServiceClient.listAttachments({ + pageSize: 50, + }); + setAttachments(fetchedAttachments); + setNextPageToken(nextPageToken ?? ""); + } catch (error) { + console.error("Failed to fetch attachments:", error); + toast.error("Failed to load attachments. Please try again."); + } finally { + loadingState.setFinish(); + } + }; + + fetchInitialAttachments(); }, []); + const handleLoadMore = async () => { + if (!nextPageToken || isLoadingMore) { + return; + } + setIsLoadingMore(true); + try { + const { attachments: fetchedAttachments, nextPageToken: newPageToken } = await attachmentServiceClient.listAttachments({ + pageSize: 50, + pageToken: nextPageToken, + }); + setAttachments((prevAttachments) => [...prevAttachments, ...fetchedAttachments]); + setNextPageToken(newPageToken ?? ""); + } catch (error) { + console.error("Failed to load more attachments:", error); + toast.error("Failed to load more attachments. Please try again."); + } finally { + setIsLoadingMore(false); + } + }; + return (
{!md && } @@ -89,61 +123,70 @@ const Attachments = observer(() => {

{t("message.no-data")}

) : ( -
- {Array.from(groupedAttachments.entries()).map(([monthStr, attachments]) => { - return ( -
-
- {dayjs(monthStr).year()} - - {dayjs(monthStr).toDate().toLocaleString(i18n.language, { month: "short" })} - -
-
- {attachments.map((attachment) => { - return ( -
-
- -
-
-

{attachment.filename}

+ <> +
+ {Array.from(groupedAttachments.entries()).map(([monthStr, attachments]) => { + return ( +
+
+ {dayjs(monthStr).year()} + + {dayjs(monthStr).toDate().toLocaleString(i18n.language, { month: "short" })} + +
+
+ {attachments.map((attachment) => { + return ( +
+
+ +
+
+

{attachment.filename}

+
-
- ); - })} + ); + })} +
-
- ); - })} + ); + })} - {unusedAttachments.length > 0 && ( - <> - -
-
-
-
- {t("resource.unused-resources")} - ({unusedAttachments.length}) -
- {unusedAttachments.map((attachment) => { - return ( -
-
- + {unusedAttachments.length > 0 && ( + <> + +
+
+
+
+ {t("resource.unused-resources")} + ({unusedAttachments.length}) +
+ {unusedAttachments.map((attachment) => { + return ( +
+
+ +
+
+

{attachment.filename}

+
-
-

{attachment.filename}

-
-
- ); - })} + ); + })} +
-
- + + )} +
+ {nextPageToken && ( +
+ +
)} -
+ )} )}