|
|
|
@ -3,16 +3,17 @@ import { includes } from "lodash-es";
|
|
|
|
import { PaperclipIcon, SearchIcon } from "lucide-react";
|
|
|
|
import { PaperclipIcon, SearchIcon } from "lucide-react";
|
|
|
|
import { observer } from "mobx-react-lite";
|
|
|
|
import { observer } from "mobx-react-lite";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
|
|
|
import { toast } from "react-hot-toast";
|
|
|
|
import AttachmentIcon from "@/components/AttachmentIcon";
|
|
|
|
import AttachmentIcon from "@/components/AttachmentIcon";
|
|
|
|
import Empty from "@/components/Empty";
|
|
|
|
import Empty from "@/components/Empty";
|
|
|
|
import MobileHeader from "@/components/MobileHeader";
|
|
|
|
import MobileHeader from "@/components/MobileHeader";
|
|
|
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
import { Separator } from "@/components/ui/separator";
|
|
|
|
import { Separator } from "@/components/ui/separator";
|
|
|
|
import { attachmentServiceClient } from "@/grpcweb";
|
|
|
|
import { attachmentServiceClient } from "@/grpcweb";
|
|
|
|
import useLoading from "@/hooks/useLoading";
|
|
|
|
import useLoading from "@/hooks/useLoading";
|
|
|
|
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
|
|
|
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
|
|
|
import i18n from "@/i18n";
|
|
|
|
import i18n from "@/i18n";
|
|
|
|
import { memoStore } from "@/store";
|
|
|
|
|
|
|
|
import { Attachment } from "@/types/proto/api/v1/attachment_service";
|
|
|
|
import { Attachment } from "@/types/proto/api/v1/attachment_service";
|
|
|
|
import { useTranslate } from "@/utils/i18n";
|
|
|
|
import { useTranslate } from "@/utils/i18n";
|
|
|
|
|
|
|
|
|
|
|
|
@ -42,18 +43,51 @@ const Attachments = observer(() => {
|
|
|
|
searchQuery: "",
|
|
|
|
searchQuery: "",
|
|
|
|
});
|
|
|
|
});
|
|
|
|
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
|
|
|
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
|
|
|
|
|
|
|
const [nextPageToken, setNextPageToken] = useState("");
|
|
|
|
|
|
|
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
|
|
const filteredAttachments = attachments.filter((attachment) => includes(attachment.filename, state.searchQuery));
|
|
|
|
const filteredAttachments = attachments.filter((attachment) => includes(attachment.filename, state.searchQuery));
|
|
|
|
const groupedAttachments = groupAttachmentsByDate(filteredAttachments.filter((attachment) => attachment.memo));
|
|
|
|
const groupedAttachments = groupAttachmentsByDate(filteredAttachments.filter((attachment) => attachment.memo));
|
|
|
|
const unusedAttachments = filteredAttachments.filter((attachment) => !attachment.memo);
|
|
|
|
const unusedAttachments = filteredAttachments.filter((attachment) => !attachment.memo);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
attachmentServiceClient.listAttachments({}).then(({ attachments }) => {
|
|
|
|
const fetchInitialAttachments = async () => {
|
|
|
|
setAttachments(attachments);
|
|
|
|
try {
|
|
|
|
loadingState.setFinish();
|
|
|
|
const { attachments: fetchedAttachments, nextPageToken } = await attachmentServiceClient.listAttachments({
|
|
|
|
Promise.all(attachments.map((attachment) => (attachment.memo ? memoStore.getOrFetchMemoByName(attachment.memo) : null)));
|
|
|
|
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 (
|
|
|
|
return (
|
|
|
|
<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">
|
|
|
|
<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">
|
|
|
|
{!md && <MobileHeader />}
|
|
|
|
{!md && <MobileHeader />}
|
|
|
|
@ -89,61 +123,70 @@ const Attachments = observer(() => {
|
|
|
|
<p className="mt-4 text-muted-foreground">{t("message.no-data")}</p>
|
|
|
|
<p className="mt-4 text-muted-foreground">{t("message.no-data")}</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
) : (
|
|
|
|
) : (
|
|
|
|
<div className={"w-full h-auto px-2 flex flex-col justify-start items-start gap-y-8"}>
|
|
|
|
<>
|
|
|
|
{Array.from(groupedAttachments.entries()).map(([monthStr, attachments]) => {
|
|
|
|
<div className={"w-full h-auto px-2 flex flex-col justify-start items-start gap-y-8"}>
|
|
|
|
return (
|
|
|
|
{Array.from(groupedAttachments.entries()).map(([monthStr, attachments]) => {
|
|
|
|
<div key={monthStr} className="w-full flex flex-row justify-start items-start">
|
|
|
|
return (
|
|
|
|
<div className="w-16 sm:w-24 pt-4 sm:pl-4 flex flex-col justify-start items-start">
|
|
|
|
<div key={monthStr} className="w-full flex flex-row justify-start items-start">
|
|
|
|
<span className="text-sm opacity-60">{dayjs(monthStr).year()}</span>
|
|
|
|
<div className="w-16 sm:w-24 pt-4 sm:pl-4 flex flex-col justify-start items-start">
|
|
|
|
<span className="font-medium text-xl">
|
|
|
|
<span className="text-sm opacity-60">{dayjs(monthStr).year()}</span>
|
|
|
|
{dayjs(monthStr).toDate().toLocaleString(i18n.language, { month: "short" })}
|
|
|
|
<span className="font-medium text-xl">
|
|
|
|
</span>
|
|
|
|
{dayjs(monthStr).toDate().toLocaleString(i18n.language, { month: "short" })}
|
|
|
|
</div>
|
|
|
|
</span>
|
|
|
|
<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">
|
|
|
|
</div>
|
|
|
|
{attachments.map((attachment) => {
|
|
|
|
<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">
|
|
|
|
return (
|
|
|
|
{attachments.map((attachment) => {
|
|
|
|
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
|
|
|
|
return (
|
|
|
|
<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">
|
|
|
|
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
|
|
|
|
<AttachmentIcon attachment={attachment} strokeWidth={0.5} />
|
|
|
|
<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">
|
|
|
|
</div>
|
|
|
|
<AttachmentIcon attachment={attachment} strokeWidth={0.5} />
|
|
|
|
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
|
|
|
|
</div>
|
|
|
|
<p className="text-xs shrink text-muted-foreground truncate">{attachment.filename}</p>
|
|
|
|
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
|
|
|
|
|
|
|
|
<p className="text-xs shrink text-muted-foreground truncate">{attachment.filename}</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{unusedAttachments.length > 0 && (
|
|
|
|
{unusedAttachments.length > 0 && (
|
|
|
|
<>
|
|
|
|
<>
|
|
|
|
<Separator />
|
|
|
|
<Separator />
|
|
|
|
<div className="w-full flex flex-row justify-start items-start">
|
|
|
|
<div className="w-full flex flex-row justify-start items-start">
|
|
|
|
<div className="w-16 sm:w-24 sm:pl-4 flex flex-col justify-start items-start"></div>
|
|
|
|
<div className="w-16 sm:w-24 sm:pl-4 flex flex-col justify-start items-start"></div>
|
|
|
|
<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">
|
|
|
|
<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">
|
|
|
|
<div className="w-full flex flex-row justify-start items-center gap-2">
|
|
|
|
<div className="w-full flex flex-row justify-start items-center gap-2">
|
|
|
|
<span className="text-muted-foreground">{t("resource.unused-resources")}</span>
|
|
|
|
<span className="text-muted-foreground">{t("resource.unused-resources")}</span>
|
|
|
|
<span className="text-muted-foreground opacity-80">({unusedAttachments.length})</span>
|
|
|
|
<span className="text-muted-foreground opacity-80">({unusedAttachments.length})</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{unusedAttachments.map((attachment) => {
|
|
|
|
{unusedAttachments.map((attachment) => {
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
|
|
|
|
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
|
|
|
|
<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">
|
|
|
|
<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">
|
|
|
|
<AttachmentIcon attachment={attachment} strokeWidth={0.5} />
|
|
|
|
<AttachmentIcon attachment={attachment} strokeWidth={0.5} />
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
|
|
|
|
|
|
|
|
<p className="text-xs shrink text-muted-foreground truncate">{attachment.filename}</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
|
|
|
|
);
|
|
|
|
<p className="text-xs shrink text-muted-foreground truncate">{attachment.filename}</p>
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{nextPageToken && (
|
|
|
|
|
|
|
|
<div className="w-full flex flex-row justify-center items-center mt-4">
|
|
|
|
|
|
|
|
<Button variant="outline" size="sm" onClick={handleLoadMore} disabled={isLoadingMore}>
|
|
|
|
|
|
|
|
{isLoadingMore ? t("resource.fetching-data") : t("memo.load-more")}
|
|
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
|