feat: create card activity from snapshot creation
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -10,6 +11,7 @@ import Button from "~/components/Button";
|
|||||||
import Input from "~/components/Input";
|
import Input from "~/components/Input";
|
||||||
import Toggle from "~/components/Toggle";
|
import Toggle from "~/components/Toggle";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
|
import { usePopup } from "~/providers/popup";
|
||||||
import { useWorkspace } from "~/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import TemplateBoards from "./TemplateBoards";
|
import TemplateBoards from "./TemplateBoards";
|
||||||
@@ -32,6 +34,8 @@ interface NewBoardInputWithTemplate {
|
|||||||
export function NewBoardForm({ isTemplate }: { isTemplate?: boolean }) {
|
export function NewBoardForm({ isTemplate }: { isTemplate?: boolean }) {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { closeModal } = useModal();
|
const { closeModal } = useModal();
|
||||||
|
const router = useRouter();
|
||||||
|
const { showPopup } = usePopup();
|
||||||
const { workspace } = useWorkspace();
|
const { workspace } = useWorkspace();
|
||||||
const [showTemplates, setShowTemplates] = useState(false);
|
const [showTemplates, setShowTemplates] = useState(false);
|
||||||
const { data: templates } = api.board.all.useQuery(
|
const { data: templates } = api.board.all.useQuery(
|
||||||
@@ -41,6 +45,7 @@ export function NewBoardForm({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
|
|
||||||
const formattedTemplates = templates?.map((template) => ({
|
const formattedTemplates = templates?.map((template) => ({
|
||||||
id: template.publicId,
|
id: template.publicId,
|
||||||
|
sourceBoardPublicId: template.publicId,
|
||||||
name: template.name,
|
name: template.name,
|
||||||
lists: template.lists.map((list) => list.name),
|
lists: template.lists.map((list) => list.name),
|
||||||
labels: template.labels.map((label) => label.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 refetchBoards = () => utils.board.all.refetch();
|
||||||
|
|
||||||
const createBoard = api.board.create.useMutation({
|
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();
|
closeModal();
|
||||||
|
|
||||||
await refetchBoards();
|
await refetchBoards();
|
||||||
},
|
},
|
||||||
|
onError: () => {
|
||||||
|
showPopup({
|
||||||
|
header: t`Error`,
|
||||||
|
message: t`Failed to create board`,
|
||||||
|
icon: "error",
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (data: NewBoardInputWithTemplate) => {
|
const onSubmit = (data: NewBoardInputWithTemplate) => {
|
||||||
createBoard.mutate({
|
createBoard.mutate({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
workspacePublicId: data.workspacePublicId,
|
workspacePublicId: data.workspacePublicId,
|
||||||
|
sourceBoardPublicId: data.template?.sourceBoardPublicId ?? undefined,
|
||||||
lists: data.template?.lists ?? [],
|
lists: data.template?.lists ?? [],
|
||||||
labels: data.template?.labels ?? [],
|
labels: data.template?.labels ?? [],
|
||||||
type: isTemplate ? "template" : "regular",
|
type: isTemplate ? "template" : "regular",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { HiCheckCircle } from "react-icons/hi2";
|
|||||||
|
|
||||||
export interface Template {
|
export interface Template {
|
||||||
id: string;
|
id: string;
|
||||||
|
sourceBoardPublicId?: string;
|
||||||
name: string;
|
name: string;
|
||||||
lists: string[];
|
lists: string[];
|
||||||
labels: string[];
|
labels: string[];
|
||||||
|
|||||||
@@ -215,13 +215,26 @@ export const boardRouter = createTRPCRouter({
|
|||||||
|
|
||||||
// If sourceBoardPublicId is provided, clone the source board
|
// If sourceBoardPublicId is provided, clone the source board
|
||||||
if (input.sourceBoardPublicId) {
|
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(
|
const sourceBoard = await boardRepo.getByPublicId(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
input.sourceBoardPublicId,
|
input.sourceBoardPublicId,
|
||||||
{
|
{
|
||||||
members: [],
|
members: [],
|
||||||
labels: [],
|
labels: [],
|
||||||
type: undefined,
|
type: sourceBoardInfo.type,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -260,6 +273,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
slug,
|
slug,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
type: input.type ?? "regular",
|
type: input.type ?? "regular",
|
||||||
|
sourceBoardId: sourceBoardInfo.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result)
|
if (!result)
|
||||||
|
|||||||
@@ -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 $$;
|
||||||
2837
packages/db/migrations/meta/20251009211316_snapshot.json
Normal file
2837
packages/db/migrations/meta/20251009211316_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -127,6 +127,13 @@
|
|||||||
"when": 1759869689304,
|
"when": 1759869689304,
|
||||||
"tag": "20251007204129_AddBoardTypeAndSourceIdColumns",
|
"tag": "20251007204129_AddBoardTypeAndSourceIdColumns",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 18,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1760044396109,
|
||||||
|
"tag": "20251009211316_AddBoardSourceIdToCardActivity",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import type { dbClient } from "@kan/db/client";
|
|||||||
import type { BoardVisibilityStatus } from "@kan/db/schema";
|
import type { BoardVisibilityStatus } from "@kan/db/schema";
|
||||||
import {
|
import {
|
||||||
boards,
|
boards,
|
||||||
|
cardActivities,
|
||||||
cards,
|
cards,
|
||||||
cardsToLabels,
|
cardsToLabels,
|
||||||
cardToWorkspaceMembers,
|
cardToWorkspaceMembers,
|
||||||
@@ -54,6 +55,7 @@ export const getIdByPublicId = async (db: dbClient, boardPublicId: string) => {
|
|||||||
const board = await db.query.boards.findFirst({
|
const board = await db.query.boards.findFirst({
|
||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
|
type: true,
|
||||||
},
|
},
|
||||||
where: eq(boards.publicId, boardPublicId),
|
where: eq(boards.publicId, boardPublicId),
|
||||||
});
|
});
|
||||||
@@ -707,6 +709,15 @@ export const createFromSnapshot = async (
|
|||||||
|
|
||||||
if (!createdCard) throw new Error("Failed to create card");
|
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) {
|
if (card.labels.length) {
|
||||||
const cardLabels: { cardId: number; labelId: number }[] = [];
|
const cardLabels: { cardId: number; labelId: number }[] = [];
|
||||||
for (const label of card.labels) {
|
for (const label of card.labels) {
|
||||||
@@ -714,8 +725,20 @@ export const createFromSnapshot = async (
|
|||||||
if (newLabelId)
|
if (newLabelId)
|
||||||
cardLabels.push({ cardId: createdCard.id, labelId: newLabelId });
|
cardLabels.push({ cardId: createdCard.id, labelId: newLabelId });
|
||||||
}
|
}
|
||||||
if (cardLabels.length)
|
if (cardLabels.length) {
|
||||||
await tx.insert(cardsToLabels).values(cardLabels);
|
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) {
|
if (card.checklists?.length) {
|
||||||
@@ -736,6 +759,16 @@ export const createFromSnapshot = async (
|
|||||||
|
|
||||||
if (!createdChecklist) continue;
|
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) {
|
if (checklist.items.length) {
|
||||||
const itemValues = [...checklist.items]
|
const itemValues = [...checklist.items]
|
||||||
.sort((a, b) => a.index - b.index)
|
.sort((a, b) => a.index - b.index)
|
||||||
@@ -748,8 +781,20 @@ export const createFromSnapshot = async (
|
|||||||
completed: !!checklistItem.completed,
|
completed: !!checklistItem.completed,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (itemValues.length)
|
if (itemValues.length) {
|
||||||
await tx.insert(checklistItems).values(itemValues);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const create = async (
|
|||||||
commentId?: number;
|
commentId?: number;
|
||||||
fromComment?: string;
|
fromComment?: string;
|
||||||
toComment?: string;
|
toComment?: string;
|
||||||
|
sourceBoardId?: number;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
@@ -44,6 +45,7 @@ export const create = async (
|
|||||||
commentId: activityInput.commentId,
|
commentId: activityInput.commentId,
|
||||||
fromComment: activityInput.fromComment,
|
fromComment: activityInput.fromComment,
|
||||||
toComment: activityInput.toComment,
|
toComment: activityInput.toComment,
|
||||||
|
sourceBoardId: activityInput.sourceBoardId,
|
||||||
})
|
})
|
||||||
.returning({ id: cardActivities.id });
|
.returning({ id: cardActivities.id });
|
||||||
|
|
||||||
@@ -66,6 +68,7 @@ export const bulkCreate = async (
|
|||||||
fromDescription?: string;
|
fromDescription?: string;
|
||||||
toDescription?: string;
|
toDescription?: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
|
sourceBoardId?: number;
|
||||||
}[],
|
}[],
|
||||||
) => {
|
) => {
|
||||||
const activitiesWithPublicIds = activityInputs.map((activity) => ({
|
const activitiesWithPublicIds = activityInputs.map((activity) => ({
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
varchar,
|
varchar,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
import { boards } from "./boards";
|
||||||
import { checklists } from "./checklists";
|
import { checklists } from "./checklists";
|
||||||
import { imports } from "./imports";
|
import { imports } from "./imports";
|
||||||
import { labels } from "./labels";
|
import { labels } from "./labels";
|
||||||
@@ -133,6 +134,10 @@ export const cardActivities = pgTable("card_activity", {
|
|||||||
),
|
),
|
||||||
fromComment: text("fromComment"),
|
fromComment: text("fromComment"),
|
||||||
toComment: text("toComment"),
|
toComment: text("toComment"),
|
||||||
|
sourceBoardId: bigint("sourceBoardId", { mode: "number" }).references(
|
||||||
|
() => boards.id,
|
||||||
|
{ onDelete: "set null" },
|
||||||
|
),
|
||||||
}).enableRLS();
|
}).enableRLS();
|
||||||
|
|
||||||
export const cardActivitiesRelations = relations(cardActivities, ({ one }) => ({
|
export const cardActivitiesRelations = relations(cardActivities, ({ one }) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user