feat: create card activity from snapshot creation

This commit is contained in:
Henry
2025-10-10 13:45:01 +01:00
parent 8ba991a0d3
commit a41fe6ea3d
9 changed files with 2947 additions and 4 deletions

View File

@@ -1,3 +1,4 @@
import { useRouter } from "next/navigation";
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/core/macro";
import { useEffect, useState } from "react";
@@ -10,6 +11,7 @@ import Button from "~/components/Button";
import Input from "~/components/Input";
import Toggle from "~/components/Toggle";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { useWorkspace } from "~/providers/workspace";
import { api } from "~/utils/api";
import TemplateBoards from "./TemplateBoards";
@@ -32,6 +34,8 @@ interface NewBoardInputWithTemplate {
export function NewBoardForm({ isTemplate }: { isTemplate?: boolean }) {
const utils = api.useUtils();
const { closeModal } = useModal();
const router = useRouter();
const { showPopup } = usePopup();
const { workspace } = useWorkspace();
const [showTemplates, setShowTemplates] = useState(false);
const { data: templates } = api.board.all.useQuery(
@@ -41,6 +45,7 @@ export function NewBoardForm({ isTemplate }: { isTemplate?: boolean }) {
const formattedTemplates = templates?.map((template) => ({
id: template.publicId,
sourceBoardPublicId: template.publicId,
name: template.name,
lists: template.lists.map((list) => list.name),
labels: template.labels.map((label) => label.name),
@@ -66,16 +71,36 @@ export function NewBoardForm({ isTemplate }: { isTemplate?: boolean }) {
const refetchBoards = () => utils.board.all.refetch();
const createBoard = api.board.create.useMutation({
onSuccess: async () => {
onSuccess: async (board) => {
if (!board) {
showPopup({
header: t`Error`,
message: t`Failed to create board`,
icon: "error",
});
} else {
router.push(
`${isTemplate ? "/templates" : "/boards"}/${board.publicId}`,
);
}
closeModal();
await refetchBoards();
},
onError: () => {
showPopup({
header: t`Error`,
message: t`Failed to create board`,
icon: "error",
});
},
});
const onSubmit = (data: NewBoardInputWithTemplate) => {
createBoard.mutate({
name: data.name,
workspacePublicId: data.workspacePublicId,
sourceBoardPublicId: data.template?.sourceBoardPublicId ?? undefined,
lists: data.template?.lists ?? [],
labels: data.template?.labels ?? [],
type: isTemplate ? "template" : "regular",

View File

@@ -4,6 +4,7 @@ import { HiCheckCircle } from "react-icons/hi2";
export interface Template {
id: string;
sourceBoardPublicId?: string;
name: string;
lists: string[];
labels: string[];

View File

@@ -215,13 +215,26 @@ export const boardRouter = createTRPCRouter({
// If sourceBoardPublicId is provided, clone the source board
if (input.sourceBoardPublicId) {
// First get the source board info (ID and type)
const sourceBoardInfo = await boardRepo.getIdByPublicId(
ctx.db,
input.sourceBoardPublicId,
);
if (!sourceBoardInfo)
throw new TRPCError({
message: `Source board with public ID ${input.sourceBoardPublicId} not found`,
code: "NOT_FOUND",
});
// Get the full board data with the correct type
const sourceBoard = await boardRepo.getByPublicId(
ctx.db,
input.sourceBoardPublicId,
{
members: [],
labels: [],
type: undefined,
type: sourceBoardInfo.type,
},
);
@@ -260,6 +273,7 @@ export const boardRouter = createTRPCRouter({
slug,
name: input.name,
type: input.type ?? "regular",
sourceBoardId: sourceBoardInfo.id,
});
if (!result)

View File

@@ -0,0 +1,6 @@
ALTER TABLE "card_activity" ADD COLUMN "sourceBoardId" bigint;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "card_activity" ADD CONSTRAINT "card_activity_sourceBoardId_board_id_fk" FOREIGN KEY ("sourceBoardId") REFERENCES "public"."board"("id") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

File diff suppressed because it is too large Load Diff

View File

@@ -127,6 +127,13 @@
"when": 1759869689304,
"tag": "20251007204129_AddBoardTypeAndSourceIdColumns",
"breakpoints": true
},
{
"idx": 18,
"version": "7",
"when": 1760044396109,
"tag": "20251009211316_AddBoardSourceIdToCardActivity",
"breakpoints": true
}
]
}

View File

@@ -4,6 +4,7 @@ import type { dbClient } from "@kan/db/client";
import type { BoardVisibilityStatus } from "@kan/db/schema";
import {
boards,
cardActivities,
cards,
cardsToLabels,
cardToWorkspaceMembers,
@@ -54,6 +55,7 @@ export const getIdByPublicId = async (db: dbClient, boardPublicId: string) => {
const board = await db.query.boards.findFirst({
columns: {
id: true,
type: true,
},
where: eq(boards.publicId, boardPublicId),
});
@@ -707,6 +709,15 @@ export const createFromSnapshot = async (
if (!createdCard) throw new Error("Failed to create card");
// Create card.created activity
await tx.insert(cardActivities).values({
publicId: generateUID(),
type: "card.created",
cardId: createdCard.id,
createdBy: args.createdBy,
sourceBoardId: args.sourceBoardId,
});
if (card.labels.length) {
const cardLabels: { cardId: number; labelId: number }[] = [];
for (const label of card.labels) {
@@ -714,8 +725,20 @@ export const createFromSnapshot = async (
if (newLabelId)
cardLabels.push({ cardId: createdCard.id, labelId: newLabelId });
}
if (cardLabels.length)
if (cardLabels.length) {
await tx.insert(cardsToLabels).values(cardLabels);
// Create card.updated.label.added activities for each label
const labelActivities = cardLabels.map((cardLabel) => ({
publicId: generateUID(),
type: "card.updated.label.added" as const,
cardId: cardLabel.cardId,
labelId: cardLabel.labelId,
createdBy: args.createdBy,
sourceBoardId: args.sourceBoardId,
}));
await tx.insert(cardActivities).values(labelActivities);
}
}
if (card.checklists?.length) {
@@ -736,6 +759,16 @@ export const createFromSnapshot = async (
if (!createdChecklist) continue;
// Create card.updated.checklist.added activity
await tx.insert(cardActivities).values({
publicId: generateUID(),
type: "card.updated.checklist.added",
cardId: createdCard.id,
toTitle: checklist.name,
createdBy: args.createdBy,
sourceBoardId: args.sourceBoardId,
});
if (checklist.items.length) {
const itemValues = [...checklist.items]
.sort((a, b) => a.index - b.index)
@@ -748,8 +781,20 @@ export const createFromSnapshot = async (
completed: !!checklistItem.completed,
}));
if (itemValues.length)
if (itemValues.length) {
await tx.insert(checklistItems).values(itemValues);
// Create card.updated.checklist.item.added activities for each item
const itemActivities = itemValues.map((item) => ({
publicId: generateUID(),
type: "card.updated.checklist.item.added" as const,
cardId: createdCard.id,
toTitle: item.title,
createdBy: args.createdBy,
sourceBoardId: args.sourceBoardId,
}));
await tx.insert(cardActivities).values(itemActivities);
}
}
}
}

View File

@@ -22,6 +22,7 @@ export const create = async (
commentId?: number;
fromComment?: string;
toComment?: string;
sourceBoardId?: number;
},
) => {
const [result] = await db
@@ -44,6 +45,7 @@ export const create = async (
commentId: activityInput.commentId,
fromComment: activityInput.fromComment,
toComment: activityInput.toComment,
sourceBoardId: activityInput.sourceBoardId,
})
.returning({ id: cardActivities.id });
@@ -66,6 +68,7 @@ export const bulkCreate = async (
fromDescription?: string;
toDescription?: string;
createdBy: string;
sourceBoardId?: number;
}[],
) => {
const activitiesWithPublicIds = activityInputs.map((activity) => ({

View File

@@ -12,6 +12,7 @@ import {
varchar,
} from "drizzle-orm/pg-core";
import { boards } from "./boards";
import { checklists } from "./checklists";
import { imports } from "./imports";
import { labels } from "./labels";
@@ -133,6 +134,10 @@ export const cardActivities = pgTable("card_activity", {
),
fromComment: text("fromComment"),
toComment: text("toComment"),
sourceBoardId: bigint("sourceBoardId", { mode: "number" }).references(
() => boards.id,
{ onDelete: "set null" },
),
}).enableRLS();
export const cardActivitiesRelations = relations(cardActivities, ({ one }) => ({