feat: display attachments on public boards

This commit is contained in:
Henry
2025-11-19 20:42:22 +00:00
parent b7974750e0
commit b41815ba62
3 changed files with 71 additions and 43 deletions

View File

@@ -26,9 +26,11 @@ interface Attachment {
export function AttachmentThumbnails({ export function AttachmentThumbnails({
attachments, attachments,
cardPublicId, cardPublicId,
isReadOnly = false,
}: { }: {
attachments?: Attachment[]; attachments?: Attachment[];
cardPublicId: string; cardPublicId: string;
isReadOnly?: boolean;
}) { }) {
const { showPopup } = usePopup(); const { showPopup } = usePopup();
const utils = api.useUtils(); const utils = api.useUtils();
@@ -48,6 +50,8 @@ export function AttachmentThumbnails({
const deleteAttachment = api.attachment.delete.useMutation({ const deleteAttachment = api.attachment.delete.useMutation({
onMutate: async (args) => { onMutate: async (args) => {
if (isReadOnly) return;
await utils.card.byId.cancel({ cardPublicId }); await utils.card.byId.cancel({ cardPublicId });
const currentState = utils.card.byId.getData({ cardPublicId }); const currentState = utils.card.byId.getData({ cardPublicId });
@@ -62,6 +66,7 @@ export function AttachmentThumbnails({
return { previousState: currentState }; return { previousState: currentState };
}, },
onError: (_error, _args, context) => { onError: (_error, _args, context) => {
if (isReadOnly) return;
utils.card.byId.setData({ cardPublicId }, context?.previousState); utils.card.byId.setData({ cardPublicId }, context?.previousState);
showPopup({ showPopup({
header: t`Unable to delete attachment`, header: t`Unable to delete attachment`,
@@ -70,10 +75,12 @@ export function AttachmentThumbnails({
}); });
}, },
onSuccess: () => { onSuccess: () => {
if (isReadOnly) return;
// Close viewer if the deleted image was being viewed // Close viewer if the deleted image was being viewed
setSelectedIndex(null); setSelectedIndex(null);
}, },
onSettled: async () => { onSettled: async () => {
if (isReadOnly) return;
await utils.card.byId.invalidate({ cardPublicId }); await utils.card.byId.invalidate({ cardPublicId });
}, },
}); });
@@ -152,7 +159,7 @@ export function AttachmentThumbnails({
return ( return (
<> <>
<div className="mb-3 flex flex-wrap gap-2"> <div className="mb-3 flex flex-wrap gap-2 pt-1">
{imageAttachments.map((attachment, index) => { {imageAttachments.map((attachment, index) => {
if (!attachment.url) return null; if (!attachment.url) return null;
return ( return (
@@ -180,11 +187,15 @@ export function AttachmentThumbnails({
key={attachment.publicId} key={attachment.publicId}
attachment={attachment} attachment={attachment}
onDownload={() => handleDownload(attachment)} onDownload={() => handleDownload(attachment)}
onDelete={() => { onDelete={
deleteAttachment.mutate({ isReadOnly
attachmentPublicId: attachment.publicId, ? undefined
}); : () => {
}} deleteAttachment.mutate({
attachmentPublicId: attachment.publicId,
});
}
}
/> />
); );
})} })}
@@ -221,13 +232,13 @@ export function AttachmentThumbnails({
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto"> <div className="fixed inset-0 z-10 overflow-y-auto">
<div {selectedIndex !== null && selectedAttachment && (
className="fixed left-2 top-2 z-20 flex gap-1" <div
onMouseDown={(e) => e.stopPropagation()} className="fixed left-2 top-2 z-20 flex gap-1"
onClick={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}
> onClick={(e) => e.stopPropagation()}
{selectedIndex !== null && selectedAttachment && ( >
<> {!isReadOnly && (
<button <button
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
@@ -246,24 +257,24 @@ export function AttachmentThumbnails({
> >
<HiOutlineTrash className="h-4 w-4" /> <HiOutlineTrash className="h-4 w-4" />
</button> </button>
<button )}
onMouseDown={(e) => { <button
e.preventDefault(); onMouseDown={(e) => {
e.stopPropagation(); e.preventDefault();
}} e.stopPropagation();
onClick={(e) => { }}
e.preventDefault(); onClick={(e) => {
e.stopPropagation(); e.preventDefault();
handleDownload(selectedAttachment); e.stopPropagation();
}} handleDownload(selectedAttachment);
className="rounded-full bg-light-50 p-1.5 text-light-1000 transition-colors hover:bg-light-100 focus:outline-none dark:bg-dark-50 dark:text-dark-1000 dark:hover:bg-dark-100" }}
aria-label="Download image" className="rounded-full bg-light-50 p-1.5 text-light-1000 transition-colors hover:bg-light-100 focus:outline-none dark:bg-dark-50 dark:text-dark-1000 dark:hover:bg-dark-100"
> aria-label="Download image"
<HiArrowDownTray className="h-4 w-4" /> >
</button> <HiArrowDownTray className="h-4 w-4" />
</> </button>
)} </div>
</div> )}
<div className="fixed right-2 top-2 z-20 flex gap-1"> <div className="fixed right-2 top-2 z-20 flex gap-1">
{imageAttachments.length > 1 && selectedIndex !== null && ( {imageAttachments.length > 1 && selectedIndex !== null && (
@@ -405,7 +416,7 @@ function FileListItem({
}: { }: {
attachment: Attachment; attachment: Attachment;
onDownload: () => void; onDownload: () => void;
onDelete: () => void; onDelete?: () => void;
}) { }) {
return ( return (
<div className="group flex w-full items-center gap-3 rounded-lg border border-light-300 bg-light-50 px-3 py-2 dark:border-dark-200 dark:bg-dark-100"> <div className="group flex w-full items-center gap-3 rounded-lg border border-light-300 bg-light-50 px-3 py-2 dark:border-dark-200 dark:bg-dark-100">
@@ -432,16 +443,18 @@ function FileListItem({
> >
<HiArrowDownTray className="h-4 w-4" /> <HiArrowDownTray className="h-4 w-4" />
</button> </button>
<button {onDelete && (
onClick={(e) => { <button
e.stopPropagation(); onClick={(e) => {
onDelete(); e.stopPropagation();
}} onDelete();
className="flex-shrink-0 rounded-full bg-light-100 p-1.5 text-light-1000 transition-colors hover:bg-light-200 focus:outline-none dark:bg-dark-100 dark:text-dark-950 dark:hover:bg-dark-300" }}
aria-label={`Delete ${attachment.originalFilename}`} className="flex-shrink-0 rounded-full bg-light-100 p-1.5 text-light-1000 transition-colors hover:bg-light-200 focus:outline-none dark:bg-dark-100 dark:text-dark-950 dark:hover:bg-dark-300"
> aria-label={`Delete ${attachment.originalFilename}`}
<HiXMark className="h-4 w-4" /> >
</button> <HiXMark className="h-4 w-4" />
</button>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -9,6 +9,7 @@ import LabelIcon from "~/components/LabelIcon";
import { useModal } from "~/providers/modal"; import { useModal } from "~/providers/modal";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import ActivityList from "~/views/card/components/ActivityList"; import ActivityList from "~/views/card/components/ActivityList";
import { AttachmentThumbnails } from "~/views/card/components/AttachmentThumbnails";
import Checklists from "~/views/card/components/Checklists"; import Checklists from "~/views/card/components/Checklists";
export function CardModal({ export function CardModal({
@@ -126,11 +127,24 @@ export function CardModal({
<Editor <Editor
content={data.description} content={data.description}
readOnly readOnly
workspaceMembers={data?.list.board.workspace.members ?? []} workspaceMembers={
data?.list.board.workspace.members ?? []
}
/> />
</div> </div>
</div> </div>
)} )}
{data?.attachments &&
data.attachments.length > 0 &&
cardPublicId && (
<div className="mb-10 max-w-2xl">
<AttachmentThumbnails
attachments={data.attachments}
cardPublicId={cardPublicId}
isReadOnly
/>
</div>
)}
{data?.checklists && data.checklists.length > 0 && ( {data?.checklists && data.checklists.length > 0 && (
<Checklists <Checklists
checklists={data.checklists} checklists={data.checklists}

View File

@@ -189,6 +189,7 @@ export default function PublicBoardView() {
members={[]} members={[]}
description={card.description} description={card.description}
comments={card.comments ?? []} comments={card.comments ?? []}
attachments={card.attachments}
/> />
</Link> </Link>
))} ))}