feat: add checklist activity

This commit is contained in:
Henry
2025-08-13 15:42:53 +01:00
parent 4f278b0567
commit 8c38c7c09a
6 changed files with 2615 additions and 7 deletions

View File

@@ -5,9 +5,11 @@ import { de, enGB, es, fr, it, nl } from "date-fns/locale";
import {
HiOutlineArrowLeft,
HiOutlineArrowRight,
HiOutlineCheckCircle,
HiOutlinePencil,
HiOutlinePlus,
HiOutlineTag,
HiOutlineTrash,
HiOutlineUserMinus,
HiOutlineUserPlus,
} from "react-icons/hi2";
@@ -31,6 +33,11 @@ const dateLocaleMap = {
nl: nl,
} as const;
const truncate = (value: string | null, maxLength = 50) => {
if (!value) return value;
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}` : value;
};
const getActivityText = ({
type,
toTitle,
@@ -39,6 +46,7 @@ const getActivityText = ({
memberName,
isSelf,
label,
fromTitle,
}: {
type: ActivityType;
toTitle: string | null;
@@ -47,6 +55,7 @@ const getActivityText = ({
memberName: string | null;
isSelf: boolean;
label: string | null;
fromTitle?: string | null;
}) => {
const ACTIVITY_TYPE_MAP = {
"card.created": t`created the card`,
@@ -57,6 +66,14 @@ const getActivityText = ({
"card.updated.label.removed": t`removed a label from the card`,
"card.updated.member.added": t`added a member to the card`,
"card.updated.member.removed": t`removed a member from the card`,
"card.updated.checklist.added": t`added a checklist`,
"card.updated.checklist.renamed": t`renamed a checklist`,
"card.updated.checklist.deleted": t`deleted a checklist`,
"card.updated.checklist.item.added": t`added a checklist item`,
"card.updated.checklist.item.updated": t`updated a checklist item`,
"card.updated.checklist.item.completed": t`completed a checklist item`,
"card.updated.checklist.item.uncompleted": t`marked a checklist item as incomplete`,
"card.updated.checklist.item.deleted": t`deleted a checklist item`,
} as const;
if (!(type in ACTIVITY_TYPE_MAP)) return null;
@@ -71,7 +88,7 @@ const getActivityText = ({
if (type === "card.updated.title" && toTitle) {
return (
<Trans>
updated the title to <TextHighlight>{toTitle}</TextHighlight>
updated the title to <TextHighlight>{truncate(toTitle)}</TextHighlight>
</Trans>
);
}
@@ -79,8 +96,9 @@ const getActivityText = ({
if (type === "card.updated.list" && fromList && toList) {
return (
<Trans>
moved the card from <TextHighlight>{fromList}</TextHighlight> to
<TextHighlight>{toList}</TextHighlight>
moved the card from <TextHighlight>{truncate(fromList)}</TextHighlight>{" "}
to
<TextHighlight>{truncate(toList)}</TextHighlight>
</Trans>
);
}
@@ -90,7 +108,8 @@ const getActivityText = ({
return (
<Trans>
assigned <TextHighlight>{memberName}</TextHighlight> to the card
assigned <TextHighlight>{truncate(memberName)}</TextHighlight> to the
card
</Trans>
);
}
@@ -100,7 +119,8 @@ const getActivityText = ({
return (
<Trans>
unassigned <TextHighlight>{memberName}</TextHighlight> from the card
unassigned <TextHighlight>{truncate(memberName)}</TextHighlight> from
the card
</Trans>
);
}
@@ -108,7 +128,7 @@ const getActivityText = ({
if (type === "card.updated.label.added" && label) {
return (
<Trans>
added label <TextHighlight>{label}</TextHighlight>
added label <TextHighlight>{truncate(label)}</TextHighlight>
</Trans>
);
}
@@ -116,7 +136,75 @@ const getActivityText = ({
if (type === "card.updated.label.removed" && label) {
return (
<Trans>
removed label <TextHighlight>{label}</TextHighlight>
removed label <TextHighlight>{truncate(label)}</TextHighlight>
</Trans>
);
}
if (type === "card.updated.checklist.added" && toTitle) {
return (
<Trans>
added checklist <TextHighlight>{truncate(toTitle)}</TextHighlight>
</Trans>
);
}
if (type === "card.updated.checklist.renamed" && toTitle) {
return (
<Trans>
renamed checklist <TextHighlight>{truncate(toTitle)}</TextHighlight>
</Trans>
);
}
if (type === "card.updated.checklist.deleted" && fromTitle) {
return (
<Trans>
deleted checklist <TextHighlight>{truncate(fromTitle)}</TextHighlight>
</Trans>
);
}
if (type === "card.updated.checklist.item.added" && toTitle) {
return (
<Trans>
added checklist item <TextHighlight>{truncate(toTitle)}</TextHighlight>
</Trans>
);
}
if (type === "card.updated.checklist.item.updated" && toTitle) {
return (
<Trans>
renamed checklist item to{" "}
<TextHighlight>{truncate(toTitle)}</TextHighlight>
</Trans>
);
}
if (type === "card.updated.checklist.item.completed" && toTitle) {
return (
<Trans>
completed checklist item{" "}
<TextHighlight>{truncate(toTitle)}</TextHighlight>
</Trans>
);
}
if (type === "card.updated.checklist.item.uncompleted" && toTitle) {
return (
<Trans>
marked checklist item <TextHighlight>{truncate(toTitle)}</TextHighlight>{" "}
as incomplete
</Trans>
);
}
if (type === "card.updated.checklist.item.deleted" && fromTitle) {
return (
<Trans>
deleted checklist item{" "}
<TextHighlight>{truncate(fromTitle)}</TextHighlight>
</Trans>
);
}
@@ -133,6 +221,14 @@ const ACTIVITY_ICON_MAP: Partial<Record<ActivityType, React.ReactNode | null>> =
"card.updated.label.removed": <HiOutlineTag />,
"card.updated.member.added": <HiOutlineUserPlus />,
"card.updated.member.removed": <HiOutlineUserMinus />,
"card.updated.checklist.added": <HiOutlinePlus />,
"card.updated.checklist.renamed": <HiOutlinePencil />,
"card.updated.checklist.deleted": <HiOutlineTrash />,
"card.updated.checklist.item.added": <HiOutlinePlus />,
"card.updated.checklist.item.updated": <HiOutlinePencil />,
"card.updated.checklist.item.completed": <HiOutlineCheckCircle />,
"card.updated.checklist.item.uncompleted": <HiOutlineCheckCircle />,
"card.updated.checklist.item.deleted": <HiOutlineTrash />,
} as const;
const getActivityIcon = (
@@ -179,6 +275,7 @@ const ActivityList = ({
memberName: activity.member?.user?.name ?? null,
isSelf: activity.member?.user?.id === data?.user.id,
label: activity.label?.name ?? null,
fromTitle: activity.fromTitle ?? null,
});
if (activity.type === "card.updated.comment.added")

View File

@@ -2,6 +2,7 @@ import { TRPCError } from "@trpc/server";
import { z } from "zod";
import * as cardRepo from "@kan/db/repository/card.repo";
import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo";
import * as checklistRepo from "@kan/db/repository/checklist.repo";
import { createTRPCRouter, protectedProcedure } from "../trpc";
@@ -71,6 +72,13 @@ export const checklistRouter = createTRPCRouter({
code: "INTERNAL_SERVER_ERROR",
});
await cardActivityRepo.create(ctx.db, {
type: "card.updated.checklist.added",
cardId: card.id,
toTitle: newChecklist.name,
createdBy: userId,
});
return newChecklist;
}),
update: protectedProcedure
@@ -105,6 +113,8 @@ export const checklistRouter = createTRPCRouter({
checklist.card.list.board.workspace.id,
);
const previousName = checklist.name;
const updated = await checklistRepo.updateChecklistById(ctx.db, {
id: checklist.id,
name: input.name,
@@ -116,6 +126,14 @@ export const checklistRouter = createTRPCRouter({
code: "INTERNAL_SERVER_ERROR",
});
await cardActivityRepo.create(ctx.db, {
type: "card.updated.checklist.renamed",
cardId: checklist.cardId,
fromTitle: previousName,
toTitle: updated.name,
createdBy: userId,
});
return updated;
}),
delete: protectedProcedure
@@ -173,6 +191,13 @@ export const checklistRouter = createTRPCRouter({
code: "INTERNAL_SERVER_ERROR",
});
await cardActivityRepo.create(ctx.db, {
type: "card.updated.checklist.deleted",
cardId: checklist.cardId,
fromTitle: checklist.name,
createdBy: userId,
});
return { success: true };
}),
createItem: protectedProcedure
@@ -231,6 +256,13 @@ export const checklistRouter = createTRPCRouter({
code: "INTERNAL_SERVER_ERROR",
});
await cardActivityRepo.create(ctx.db, {
type: "card.updated.checklist.item.added",
cardId: checklist.cardId,
toTitle: newChecklistItem.title,
createdBy: userId,
});
return newChecklistItem;
}),
updateItem: protectedProcedure
@@ -278,6 +310,8 @@ export const checklistRouter = createTRPCRouter({
item.checklist.card.list.board.workspace.id,
);
const previousTitle = item.title;
const updated = await checklistRepo.updateItemById(ctx.db, {
id: item.id,
title: input.title,
@@ -290,6 +324,29 @@ export const checklistRouter = createTRPCRouter({
code: "INTERNAL_SERVER_ERROR",
});
// Log completion toggle
if (input.completed !== undefined) {
await cardActivityRepo.create(ctx.db, {
type: input.completed
? "card.updated.checklist.item.completed"
: "card.updated.checklist.item.uncompleted",
cardId: item.checklist.cardId,
toTitle: updated.title,
createdBy: userId,
});
}
// Log title change
if (input.title !== undefined && input.title !== previousTitle) {
await cardActivityRepo.create(ctx.db, {
type: "card.updated.checklist.item.updated",
cardId: item.checklist.cardId,
fromTitle: previousTitle,
toTitle: updated.title,
createdBy: userId,
});
}
return updated;
}),
deleteItem: protectedProcedure
@@ -341,6 +398,13 @@ export const checklistRouter = createTRPCRouter({
code: "INTERNAL_SERVER_ERROR",
});
await cardActivityRepo.create(ctx.db, {
type: "card.updated.checklist.item.deleted",
cardId: item.checklist.cardId,
fromTitle: item.title,
createdBy: userId,
});
return { success: true };
}),
});

View File

@@ -0,0 +1,8 @@
ALTER TYPE "public"."card_activity_type" ADD VALUE 'card.updated.checklist.added' BEFORE 'card.archived';--> statement-breakpoint
ALTER TYPE "public"."card_activity_type" ADD VALUE 'card.updated.checklist.renamed' BEFORE 'card.archived';--> statement-breakpoint
ALTER TYPE "public"."card_activity_type" ADD VALUE 'card.updated.checklist.deleted' BEFORE 'card.archived';--> statement-breakpoint
ALTER TYPE "public"."card_activity_type" ADD VALUE 'card.updated.checklist.item.added' BEFORE 'card.archived';--> statement-breakpoint
ALTER TYPE "public"."card_activity_type" ADD VALUE 'card.updated.checklist.item.updated' BEFORE 'card.archived';--> statement-breakpoint
ALTER TYPE "public"."card_activity_type" ADD VALUE 'card.updated.checklist.item.completed' BEFORE 'card.archived';--> statement-breakpoint
ALTER TYPE "public"."card_activity_type" ADD VALUE 'card.updated.checklist.item.uncompleted' BEFORE 'card.archived';--> statement-breakpoint
ALTER TYPE "public"."card_activity_type" ADD VALUE 'card.updated.checklist.item.deleted' BEFORE 'card.archived';

File diff suppressed because it is too large Load Diff

View File

@@ -57,6 +57,13 @@
"when": 1754511666265,
"tag": "20250806202106_glossy_luminals",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1755094668761,
"tag": "20250813141748_AddChecklistActivityTypes",
"breakpoints": true
}
]
}

View File

@@ -32,6 +32,15 @@ export const activityTypes = [
"card.updated.comment.added",
"card.updated.comment.updated",
"card.updated.comment.deleted",
// Checklist activities
"card.updated.checklist.added",
"card.updated.checklist.renamed",
"card.updated.checklist.deleted",
"card.updated.checklist.item.added",
"card.updated.checklist.item.updated",
"card.updated.checklist.item.completed",
"card.updated.checklist.item.uncompleted",
"card.updated.checklist.item.deleted",
"card.archived",
] as const;