feat: display attachments on public boards
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user