Skip to content

Commit a533ba0

Browse files
authored
fix: add load more button and pagination to attachments page (#5258)
1 parent edfbd6b commit a533ba0

File tree

1 file changed

+98
-55
lines changed

1 file changed

+98
-55
lines changed

web/src/pages/Attachments.tsx

Lines changed: 98 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ import { includes } from "lodash-es";
33
import { PaperclipIcon, SearchIcon } from "lucide-react";
44
import { observer } from "mobx-react-lite";
55
import { useEffect, useState } from "react";
6+
import { toast } from "react-hot-toast";
67
import AttachmentIcon from "@/components/AttachmentIcon";
78
import Empty from "@/components/Empty";
89
import MobileHeader from "@/components/MobileHeader";
10+
import { Button } from "@/components/ui/button";
911
import { Input } from "@/components/ui/input";
1012
import { Separator } from "@/components/ui/separator";
1113
import { attachmentServiceClient } from "@/grpcweb";
1214
import useLoading from "@/hooks/useLoading";
1315
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
1416
import i18n from "@/i18n";
15-
import { memoStore } from "@/store";
1617
import { Attachment } from "@/types/proto/api/v1/attachment_service";
1718
import { useTranslate } from "@/utils/i18n";
1819

@@ -42,18 +43,51 @@ const Attachments = observer(() => {
4243
searchQuery: "",
4344
});
4445
const [attachments, setAttachments] = useState<Attachment[]>([]);
46+
const [nextPageToken, setNextPageToken] = useState("");
47+
const [isLoadingMore, setIsLoadingMore] = useState(false);
4548
const filteredAttachments = attachments.filter((attachment) => includes(attachment.filename, state.searchQuery));
4649
const groupedAttachments = groupAttachmentsByDate(filteredAttachments.filter((attachment) => attachment.memo));
4750
const unusedAttachments = filteredAttachments.filter((attachment) => !attachment.memo);
4851

4952
useEffect(() => {
50-
attachmentServiceClient.listAttachments({}).then(({ attachments }) => {
51-
setAttachments(attachments);
52-
loadingState.setFinish();
53-
Promise.all(attachments.map((attachment) => (attachment.memo ? memoStore.getOrFetchMemoByName(attachment.memo) : null)));
54-
});
53+
const fetchInitialAttachments = async () => {
54+
try {
55+
const { attachments: fetchedAttachments, nextPageToken } = await attachmentServiceClient.listAttachments({
56+
pageSize: 50,
57+
});
58+
setAttachments(fetchedAttachments);
59+
setNextPageToken(nextPageToken ?? "");
60+
} catch (error) {
61+
console.error("Failed to fetch attachments:", error);
62+
toast.error("Failed to load attachments. Please try again.");
63+
} finally {
64+
loadingState.setFinish();
65+
}
66+
};
67+
68+
fetchInitialAttachments();
5569
}, []);
5670

71+
const handleLoadMore = async () => {
72+
if (!nextPageToken || isLoadingMore) {
73+
return;
74+
}
75+
setIsLoadingMore(true);
76+
try {
77+
const { attachments: fetchedAttachments, nextPageToken: newPageToken } = await attachmentServiceClient.listAttachments({
78+
pageSize: 50,
79+
pageToken: nextPageToken,
80+
});
81+
setAttachments((prevAttachments) => [...prevAttachments, ...fetchedAttachments]);
82+
setNextPageToken(newPageToken ?? "");
83+
} catch (error) {
84+
console.error("Failed to load more attachments:", error);
85+
toast.error("Failed to load more attachments. Please try again.");
86+
} finally {
87+
setIsLoadingMore(false);
88+
}
89+
};
90+
5791
return (
5892
<section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8">
5993
{!md && <MobileHeader />}
@@ -89,61 +123,70 @@ const Attachments = observer(() => {
89123
<p className="mt-4 text-muted-foreground">{t("message.no-data")}</p>
90124
</div>
91125
) : (
92-
<div className={"w-full h-auto px-2 flex flex-col justify-start items-start gap-y-8"}>
93-
{Array.from(groupedAttachments.entries()).map(([monthStr, attachments]) => {
94-
return (
95-
<div key={monthStr} className="w-full flex flex-row justify-start items-start">
96-
<div className="w-16 sm:w-24 pt-4 sm:pl-4 flex flex-col justify-start items-start">
97-
<span className="text-sm opacity-60">{dayjs(monthStr).year()}</span>
98-
<span className="font-medium text-xl">
99-
{dayjs(monthStr).toDate().toLocaleString(i18n.language, { month: "short" })}
100-
</span>
101-
</div>
102-
<div className="w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap">
103-
{attachments.map((attachment) => {
104-
return (
105-
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
106-
<div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-border overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80">
107-
<AttachmentIcon attachment={attachment} strokeWidth={0.5} />
108-
</div>
109-
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
110-
<p className="text-xs shrink text-muted-foreground truncate">{attachment.filename}</p>
126+
<>
127+
<div className={"w-full h-auto px-2 flex flex-col justify-start items-start gap-y-8"}>
128+
{Array.from(groupedAttachments.entries()).map(([monthStr, attachments]) => {
129+
return (
130+
<div key={monthStr} className="w-full flex flex-row justify-start items-start">
131+
<div className="w-16 sm:w-24 pt-4 sm:pl-4 flex flex-col justify-start items-start">
132+
<span className="text-sm opacity-60">{dayjs(monthStr).year()}</span>
133+
<span className="font-medium text-xl">
134+
{dayjs(monthStr).toDate().toLocaleString(i18n.language, { month: "short" })}
135+
</span>
136+
</div>
137+
<div className="w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap">
138+
{attachments.map((attachment) => {
139+
return (
140+
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
141+
<div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-border overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80">
142+
<AttachmentIcon attachment={attachment} strokeWidth={0.5} />
143+
</div>
144+
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
145+
<p className="text-xs shrink text-muted-foreground truncate">{attachment.filename}</p>
146+
</div>
111147
</div>
112-
</div>
113-
);
114-
})}
148+
);
149+
})}
150+
</div>
115151
</div>
116-
</div>
117-
);
118-
})}
152+
);
153+
})}
119154

120-
{unusedAttachments.length > 0 && (
121-
<>
122-
<Separator />
123-
<div className="w-full flex flex-row justify-start items-start">
124-
<div className="w-16 sm:w-24 sm:pl-4 flex flex-col justify-start items-start"></div>
125-
<div className="w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap">
126-
<div className="w-full flex flex-row justify-start items-center gap-2">
127-
<span className="text-muted-foreground">{t("resource.unused-resources")}</span>
128-
<span className="text-muted-foreground opacity-80">({unusedAttachments.length})</span>
129-
</div>
130-
{unusedAttachments.map((attachment) => {
131-
return (
132-
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
133-
<div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-border overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80">
134-
<AttachmentIcon attachment={attachment} strokeWidth={0.5} />
155+
{unusedAttachments.length > 0 && (
156+
<>
157+
<Separator />
158+
<div className="w-full flex flex-row justify-start items-start">
159+
<div className="w-16 sm:w-24 sm:pl-4 flex flex-col justify-start items-start"></div>
160+
<div className="w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap">
161+
<div className="w-full flex flex-row justify-start items-center gap-2">
162+
<span className="text-muted-foreground">{t("resource.unused-resources")}</span>
163+
<span className="text-muted-foreground opacity-80">({unusedAttachments.length})</span>
164+
</div>
165+
{unusedAttachments.map((attachment) => {
166+
return (
167+
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
168+
<div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-border overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80">
169+
<AttachmentIcon attachment={attachment} strokeWidth={0.5} />
170+
</div>
171+
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
172+
<p className="text-xs shrink text-muted-foreground truncate">{attachment.filename}</p>
173+
</div>
135174
</div>
136-
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
137-
<p className="text-xs shrink text-muted-foreground truncate">{attachment.filename}</p>
138-
</div>
139-
</div>
140-
);
141-
})}
175+
);
176+
})}
177+
</div>
142178
</div>
143-
</div>
144-
</>
179+
</>
180+
)}
181+
</div>
182+
{nextPageToken && (
183+
<div className="w-full flex flex-row justify-center items-center mt-4">
184+
<Button variant="outline" size="sm" onClick={handleLoadMore} disabled={isLoadingMore}>
185+
{isLoadingMore ? t("resource.fetching-data") : t("memo.load-more")}
186+
</Button>
187+
</div>
145188
)}
146-
</div>
189+
</>
147190
)}
148191
</>
149192
)}

0 commit comments

Comments
 (0)