feat: duplicate card via dropdown
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
|||||||
HiHashtag,
|
HiHashtag,
|
||||||
HiLink,
|
HiLink,
|
||||||
HiOutlineCheckCircle,
|
HiOutlineCheckCircle,
|
||||||
|
HiOutlineDocumentDuplicate,
|
||||||
HiOutlineTrash,
|
HiOutlineTrash,
|
||||||
} from "react-icons/hi2";
|
} from "react-icons/hi2";
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ import Dropdown from "~/components/Dropdown";
|
|||||||
import { usePermissions } from "~/hooks/usePermissions";
|
import { usePermissions } from "~/hooks/usePermissions";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
export default function CardDropdown({
|
export default function CardDropdown({
|
||||||
cardPublicId,
|
cardPublicId,
|
||||||
@@ -20,19 +22,44 @@ export default function CardDropdown({
|
|||||||
boardPublicId,
|
boardPublicId,
|
||||||
cardCreatedBy,
|
cardCreatedBy,
|
||||||
ticketNumber,
|
ticketNumber,
|
||||||
|
listPublicId,
|
||||||
|
cardIndex,
|
||||||
}: {
|
}: {
|
||||||
cardPublicId: string;
|
cardPublicId: string;
|
||||||
isTemplate?: boolean;
|
isTemplate?: boolean;
|
||||||
boardPublicId?: string;
|
boardPublicId?: string;
|
||||||
cardCreatedBy?: string | null;
|
cardCreatedBy?: string | null;
|
||||||
ticketNumber?: string | null;
|
ticketNumber?: string | null;
|
||||||
|
listPublicId?: string;
|
||||||
|
cardIndex?: number;
|
||||||
}) {
|
}) {
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
const { canEditCard, canDeleteCard } = usePermissions();
|
const { canEditCard, canDeleteCard } = usePermissions();
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = authClient.useSession();
|
||||||
|
const utils = api.useUtils();
|
||||||
const isCreator = cardCreatedBy && session?.user.id === cardCreatedBy;
|
const isCreator = cardCreatedBy && session?.user.id === cardCreatedBy;
|
||||||
|
|
||||||
|
const duplicateCard = api.card.duplicate.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
showPopup({
|
||||||
|
header: t`Card duplicated`,
|
||||||
|
icon: "success",
|
||||||
|
message: t`Card duplicated successfully.`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
showPopup({
|
||||||
|
header: t`Unable to duplicate card`,
|
||||||
|
icon: "error",
|
||||||
|
message: t`Please try again.`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSettled: async () => {
|
||||||
|
await utils.board.byId.invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleCopyCardLink = async () => {
|
const handleCopyCardLink = async () => {
|
||||||
const path =
|
const path =
|
||||||
isTemplate && boardPublicId
|
isTemplate && boardPublicId
|
||||||
@@ -99,6 +126,24 @@ export default function CardDropdown({
|
|||||||
<HiOutlineCheckCircle className="h-[16px] w-[16px] text-dark-900" />
|
<HiOutlineCheckCircle className="h-[16px] w-[16px] text-dark-900" />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t`Duplicate card`,
|
||||||
|
action: () => {
|
||||||
|
if (!listPublicId || cardIndex === undefined) return;
|
||||||
|
duplicateCard.mutate({
|
||||||
|
cardPublicId,
|
||||||
|
listPublicId,
|
||||||
|
index: cardIndex + 1,
|
||||||
|
copyLabels: true,
|
||||||
|
copyMembers: true,
|
||||||
|
copyChecklists: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: (
|
||||||
|
<HiOutlineDocumentDuplicate className="h-[16px] w-[16px] text-dark-900" />
|
||||||
|
),
|
||||||
|
disabled: duplicateCard.isPending || !listPublicId,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(canDeleteCard || isCreator
|
...(canDeleteCard || isCreator
|
||||||
|
|||||||
@@ -185,7 +185,11 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
? router.query.cardId[0]
|
? router.query.cardId[0]
|
||||||
: router.query.cardId;
|
: router.query.cardId;
|
||||||
|
|
||||||
const { data: card, isLoading, error } = api.card.byId.useQuery(
|
const {
|
||||||
|
data: card,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
} = api.card.byId.useQuery(
|
||||||
{ cardPublicId: cardId ?? "" },
|
{ cardPublicId: cardId ?? "" },
|
||||||
{ enabled: !!cardId && cardId.length >= 12 },
|
{ enabled: !!cardId && cardId.length >= 12 },
|
||||||
);
|
);
|
||||||
@@ -340,14 +344,15 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
>
|
>
|
||||||
{board?.name}
|
{board?.name}
|
||||||
</Link>
|
</Link>
|
||||||
{card.cardNumber != null && card.list.board.workspace.cardPrefix && (
|
{card.cardNumber != null &&
|
||||||
<>
|
card.list.board.workspace.cardPrefix && (
|
||||||
<IoChevronForwardSharp className="h-[10px] w-[10px] text-light-900 dark:text-dark-900" />
|
<>
|
||||||
<span className="whitespace-nowrap text-sm font-bold leading-[1.5rem] text-light-700 dark:text-dark-800">
|
<IoChevronForwardSharp className="h-[10px] w-[10px] text-light-900 dark:text-dark-900" />
|
||||||
{card.list.board.workspace.cardPrefix}-{card.cardNumber}
|
<span className="whitespace-nowrap text-sm font-bold leading-[1.5rem] text-light-700 dark:text-dark-800">
|
||||||
</span>
|
{card.list.board.workspace.cardPrefix}-{card.cardNumber}
|
||||||
</>
|
</span>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -356,10 +361,13 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
boardPublicId={boardId}
|
boardPublicId={boardId}
|
||||||
cardCreatedBy={card?.createdBy}
|
cardCreatedBy={card?.createdBy}
|
||||||
ticketNumber={
|
ticketNumber={
|
||||||
card.cardNumber != null && card.list.board.workspace.cardPrefix
|
card.cardNumber != null &&
|
||||||
|
card.list.board.workspace.cardPrefix
|
||||||
? `${card.list.board.workspace.cardPrefix}-${card.cardNumber}`
|
? `${card.list.board.workspace.cardPrefix}-${card.cardNumber}`
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
listPublicId={card?.list.publicId}
|
||||||
|
cardIndex={card?.index}
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
href={`/${isTemplate ? "templates" : "boards"}/${boardId}`}
|
href={`/${isTemplate ? "templates" : "boards"}/${boardId}`}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export const cardDetailSchema = z.object({
|
|||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string().nullable(),
|
description: z.string().nullable(),
|
||||||
cardNumber: z.number().nullable(),
|
cardNumber: z.number().nullable(),
|
||||||
|
index: z.number(),
|
||||||
dueDate: z.date().nullable(),
|
dueDate: z.date().nullable(),
|
||||||
createdBy: z.string().nullable(),
|
createdBy: z.string().nullable(),
|
||||||
labels: z.array(labelSchema),
|
labels: z.array(labelSchema),
|
||||||
|
|||||||
@@ -107,7 +107,12 @@ export const create = async (
|
|||||||
cardNumber,
|
cardNumber,
|
||||||
dueDate: cardInput.dueDate ?? null,
|
dueDate: cardInput.dueDate ?? null,
|
||||||
})
|
})
|
||||||
.returning({ id: cards.id, listId: cards.listId, publicId: cards.publicId, cardNumber: cards.cardNumber });
|
.returning({
|
||||||
|
id: cards.id,
|
||||||
|
listId: cards.listId,
|
||||||
|
publicId: cards.publicId,
|
||||||
|
cardNumber: cards.cardNumber,
|
||||||
|
});
|
||||||
|
|
||||||
if (!result[0]) throw new Error("Unable to create card");
|
if (!result[0]) throw new Error("Unable to create card");
|
||||||
|
|
||||||
@@ -321,8 +326,7 @@ export const bulkCreate = async (
|
|||||||
.where(eq(workspaces.id, workspaceId))
|
.where(eq(workspaces.id, workspaceId))
|
||||||
.returning({ cardCounter: workspaces.cardCounter });
|
.returning({ cardCounter: workspaces.cardCounter });
|
||||||
|
|
||||||
if (!counterResult)
|
if (!counterResult) throw new Error(`Workspace ${workspaceId} not found`);
|
||||||
throw new Error(`Workspace ${workspaceId} not found`);
|
|
||||||
|
|
||||||
const last = counterResult.cardCounter;
|
const last = counterResult.cardCounter;
|
||||||
const start = last - count + 1;
|
const start = last - count + 1;
|
||||||
@@ -486,6 +490,7 @@ export const getWithListAndMembersByPublicId = async (
|
|||||||
dueDate: true,
|
dueDate: true,
|
||||||
createdBy: true,
|
createdBy: true,
|
||||||
cardNumber: true,
|
cardNumber: true,
|
||||||
|
index: true,
|
||||||
},
|
},
|
||||||
with: {
|
with: {
|
||||||
labels: {
|
labels: {
|
||||||
|
|||||||
Reference in New Issue
Block a user