This commit is contained in:
Cyl3el2Cleal2 2026-01-28 20:53:55 +01:00 committed by GitHub
commit 3d1a4cd89c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 149 additions and 5 deletions

View File

@ -4,6 +4,7 @@ import {
BookmarkMinusIcon,
BookmarkPlusIcon,
CopyIcon,
DownloadIcon,
Edit3Icon,
FileTextIcon,
LinkIcon,
@ -49,6 +50,7 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
handleToggleMemoStatusClick,
handleCopyLink,
handleCopyContent,
handleDownloadContent,
handleDeleteMemoClick,
confirmDeleteMemo,
handleRemoveCompletedTaskListItemsClick,
@ -73,7 +75,11 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
<>
{!isComment && (
<DropdownMenuItem onClick={handleTogglePinMemoBtnClick}>
{memo.pinned ? <BookmarkMinusIcon className="w-4 h-auto" /> : <BookmarkPlusIcon className="w-4 h-auto" />}
{memo.pinned ? (
<BookmarkMinusIcon className="w-4 h-auto" />
) : (
<BookmarkPlusIcon className="w-4 h-auto" />
)}
{memo.pinned ? t("common.unpin") : t("common.pin")}
</DropdownMenuItem>
)}
@ -91,6 +97,10 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
<CopyIcon className="w-4 h-auto" />
{t("common.copy")}
</DropdownMenuSubTrigger>
<DropdownMenuItem onClick={handleDownloadContent}>
<DownloadIcon className="w-4 h-auto" />
{t("memo.download")}
</DropdownMenuItem>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={handleCopyLink}>
<LinkIcon className="w-4 h-auto" />
@ -109,7 +119,9 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
<>
{/* Remove completed tasks (non-archived, non-comment, has completed tasks) */}
{!isArchived && !isComment && hasCompletedTaskList && (
<DropdownMenuItem onClick={handleRemoveCompletedTaskListItemsClick}>
<DropdownMenuItem
onClick={handleRemoveCompletedTaskListItemsClick}
>
<SquareCheckIcon className="w-4 h-auto" />
{t("memo.remove-completed-task-list-items")}
</DropdownMenuItem>
@ -118,7 +130,11 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
{/* Archive/Restore (non-comment) */}
{!isComment && (
<DropdownMenuItem onClick={handleToggleMemoStatusClick}>
{isArchived ? <ArchiveRestoreIcon className="w-4 h-auto" /> : <ArchiveIcon className="w-4 h-auto" />}
{isArchived ? (
<ArchiveRestoreIcon className="w-4 h-auto" />
) : (
<ArchiveIcon className="w-4 h-auto" />
)}
{isArchived ? t("common.restore") : t("common.archive")}
</DropdownMenuItem>
)}

View File

@ -10,8 +10,10 @@ import { userKeys } from "@/hooks/useUserQueries";
import { handleError } from "@/lib/error";
import { State } from "@/types/proto/api/v1/common_pb";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
import { toAttachmentItems } from "@/components/memo-metadata";
import { useTranslate } from "@/utils/i18n";
import { removeCompletedTasks } from "@/utils/markdown-manipulation";
import { downloadMemoContentAndAttachments } from "@/utils/content";
interface UseMemoActionHandlersOptions {
memo: Memo;
@ -20,7 +22,12 @@ interface UseMemoActionHandlersOptions {
setRemoveTasksDialogOpen: (open: boolean) => void;
}
export const useMemoActionHandlers = ({ memo, onEdit, setDeleteDialogOpen, setRemoveTasksDialogOpen }: UseMemoActionHandlersOptions) => {
export const useMemoActionHandlers = ({
memo,
onEdit,
setDeleteDialogOpen,
setRemoveTasksDialogOpen,
}: UseMemoActionHandlersOptions) => {
const t = useTranslate();
const location = useLocation();
const navigateTo = useNavigateTo();
@ -56,7 +63,10 @@ export const useMemoActionHandlers = ({ memo, onEdit, setDeleteDialogOpen, setRe
const handleToggleMemoStatusClick = useCallback(async () => {
const isArchiving = memo.state !== State.ARCHIVED;
const state = memo.state === State.ARCHIVED ? State.NORMAL : State.ARCHIVED;
const message = memo.state === State.ARCHIVED ? t("message.restored-successfully") : t("message.archived-successfully");
const message =
memo.state === State.ARCHIVED
? t("message.restored-successfully")
: t("message.archived-successfully");
try {
await updateMemo({
@ -99,6 +109,11 @@ export const useMemoActionHandlers = ({ memo, onEdit, setDeleteDialogOpen, setRe
setDeleteDialogOpen(true);
}, [setDeleteDialogOpen]);
const handleDownloadContent = useCallback(() => {
const attachmentItems = toAttachmentItems(memo.attachments, []);
downloadMemoContentAndAttachments(memo, attachmentItems);
}, [memo, memo.attachments]);
const confirmDeleteMemo = useCallback(async () => {
await deleteMemo(memo.name);
toast.success(t("message.deleted-successfully"));
@ -132,6 +147,7 @@ export const useMemoActionHandlers = ({ memo, onEdit, setDeleteDialogOpen, setRe
handleCopyLink,
handleCopyContent,
handleDeleteMemoClick,
handleDownloadContent,
confirmDeleteMemo,
handleRemoveCompletedTaskListItemsClick,
confirmRemoveCompletedTaskListItems,

View File

@ -155,6 +155,7 @@
},
"copy-content": "Copy Content",
"copy-link": "Copy Link",
"download": "Download",
"count-memos-in-date": "{{count}} {{memos}} in {{date}}",
"delete-confirm": "Are you sure you want to delete this memo?",
"delete-confirm-description": "This action is irreversible. Attachments, links, and references will also be removed.",

111
web/src/utils/content.ts Normal file
View File

@ -0,0 +1,111 @@
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
import type { AttachmentItem } from "@/components/memo-metadata";
/**
* Downloads a file using a temporary anchor element
* @param url - The URL to download from
* @param filename - The filename to save as
* @param isLocal - Whether this is a local file that needs special handling
*/
const downloadFile = (
url: string,
filename: string,
isLocal: boolean = false,
): void => {
const downloadElement = document.createElement("a");
downloadElement.href = url;
downloadElement.download = filename;
// For local files, ensure proper download behavior
if (isLocal) {
downloadElement.setAttribute("download", filename);
downloadElement.target = "_blank";
}
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
};
/**
* Creates a date prefix in YYYY/MM/DD format
* @param date - Date object to format (defaults to current date)
* @returns Formatted date string
*/
const createDatePrefix = (date: Date = new Date()): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}/${month}/${day}`;
};
/**
* Extracts memo short name from full name path
* @param memoName - Full memo name (format: memos/{id})
* @returns Short memo name
*/
const extractMemoName = (memoName: string): string => {
return memoName.split("/").pop() || "memo";
};
/**
* Downloads memo content as a markdown file if content exists
* @param memo - The memo object
* @param namePrefix - The filename prefix to use
*/
const downloadMemoContent = (memo: Memo, namePrefix: string): void => {
if (!memo.content || !memo.content.trim()) {
return;
}
const contentBlob = new Blob([memo.content], {
type: "text/markdown;charset=utf-8",
});
const contentUrl = URL.createObjectURL(contentBlob);
downloadFile(contentUrl, `${namePrefix}.md`);
URL.revokeObjectURL(contentUrl);
};
/**
* Downloads all attachments from a memo
* @param attachmentItems - Array of attachment items
* @param namePrefix - The filename prefix to use
*/
const downloadAttachments = (
attachmentItems: AttachmentItem[],
namePrefix: string,
): void => {
attachmentItems.forEach((item) => {
downloadFile(
item.sourceUrl,
`${namePrefix} - ${item.filename}`,
item.isLocal,
);
});
};
/**
* Downloads all content and attachments from a memo
* @param memo - The memo object
* @param attachmentItems - Array of attachment items
*/
export const downloadMemoContentAndAttachments = (
memo: Memo,
attachmentItems: AttachmentItem[],
): void => {
const date = new Date();
const datePrefix = createDatePrefix(date);
const memoShortName = extractMemoName(memo.name);
const namePrefix = `${datePrefix} ${memoShortName}`;
// Download memo content first
downloadMemoContent(memo, namePrefix);
// Then download all attachments
downloadAttachments(attachmentItems, namePrefix);
};
// Legacy export for backward compatibility
export const download = downloadMemoContentAndAttachments;