feat: add checklist activity
This commit is contained in:
@@ -5,9 +5,11 @@ import { de, enGB, es, fr, it, nl } from "date-fns/locale";
|
|||||||
import {
|
import {
|
||||||
HiOutlineArrowLeft,
|
HiOutlineArrowLeft,
|
||||||
HiOutlineArrowRight,
|
HiOutlineArrowRight,
|
||||||
|
HiOutlineCheckCircle,
|
||||||
HiOutlinePencil,
|
HiOutlinePencil,
|
||||||
HiOutlinePlus,
|
HiOutlinePlus,
|
||||||
HiOutlineTag,
|
HiOutlineTag,
|
||||||
|
HiOutlineTrash,
|
||||||
HiOutlineUserMinus,
|
HiOutlineUserMinus,
|
||||||
HiOutlineUserPlus,
|
HiOutlineUserPlus,
|
||||||
} from "react-icons/hi2";
|
} from "react-icons/hi2";
|
||||||
@@ -31,6 +33,11 @@ const dateLocaleMap = {
|
|||||||
nl: nl,
|
nl: nl,
|
||||||
} as const;
|
} 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 = ({
|
const getActivityText = ({
|
||||||
type,
|
type,
|
||||||
toTitle,
|
toTitle,
|
||||||
@@ -39,6 +46,7 @@ const getActivityText = ({
|
|||||||
memberName,
|
memberName,
|
||||||
isSelf,
|
isSelf,
|
||||||
label,
|
label,
|
||||||
|
fromTitle,
|
||||||
}: {
|
}: {
|
||||||
type: ActivityType;
|
type: ActivityType;
|
||||||
toTitle: string | null;
|
toTitle: string | null;
|
||||||
@@ -47,6 +55,7 @@ const getActivityText = ({
|
|||||||
memberName: string | null;
|
memberName: string | null;
|
||||||
isSelf: boolean;
|
isSelf: boolean;
|
||||||
label: string | null;
|
label: string | null;
|
||||||
|
fromTitle?: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
const ACTIVITY_TYPE_MAP = {
|
const ACTIVITY_TYPE_MAP = {
|
||||||
"card.created": t`created the card`,
|
"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.label.removed": t`removed a label from the card`,
|
||||||
"card.updated.member.added": t`added a member to 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.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;
|
} as const;
|
||||||
|
|
||||||
if (!(type in ACTIVITY_TYPE_MAP)) return null;
|
if (!(type in ACTIVITY_TYPE_MAP)) return null;
|
||||||
@@ -71,7 +88,7 @@ const getActivityText = ({
|
|||||||
if (type === "card.updated.title" && toTitle) {
|
if (type === "card.updated.title" && toTitle) {
|
||||||
return (
|
return (
|
||||||
<Trans>
|
<Trans>
|
||||||
updated the title to <TextHighlight>{toTitle}</TextHighlight>
|
updated the title to <TextHighlight>{truncate(toTitle)}</TextHighlight>
|
||||||
</Trans>
|
</Trans>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -79,8 +96,9 @@ const getActivityText = ({
|
|||||||
if (type === "card.updated.list" && fromList && toList) {
|
if (type === "card.updated.list" && fromList && toList) {
|
||||||
return (
|
return (
|
||||||
<Trans>
|
<Trans>
|
||||||
moved the card from <TextHighlight>{fromList}</TextHighlight> to
|
moved the card from <TextHighlight>{truncate(fromList)}</TextHighlight>{" "}
|
||||||
<TextHighlight>{toList}</TextHighlight>
|
to
|
||||||
|
<TextHighlight>{truncate(toList)}</TextHighlight>
|
||||||
</Trans>
|
</Trans>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -90,7 +108,8 @@ const getActivityText = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Trans>
|
<Trans>
|
||||||
assigned <TextHighlight>{memberName}</TextHighlight> to the card
|
assigned <TextHighlight>{truncate(memberName)}</TextHighlight> to the
|
||||||
|
card
|
||||||
</Trans>
|
</Trans>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -100,7 +119,8 @@ const getActivityText = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Trans>
|
<Trans>
|
||||||
unassigned <TextHighlight>{memberName}</TextHighlight> from the card
|
unassigned <TextHighlight>{truncate(memberName)}</TextHighlight> from
|
||||||
|
the card
|
||||||
</Trans>
|
</Trans>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -108,7 +128,7 @@ const getActivityText = ({
|
|||||||
if (type === "card.updated.label.added" && label) {
|
if (type === "card.updated.label.added" && label) {
|
||||||
return (
|
return (
|
||||||
<Trans>
|
<Trans>
|
||||||
added label <TextHighlight>{label}</TextHighlight>
|
added label <TextHighlight>{truncate(label)}</TextHighlight>
|
||||||
</Trans>
|
</Trans>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -116,7 +136,75 @@ const getActivityText = ({
|
|||||||
if (type === "card.updated.label.removed" && label) {
|
if (type === "card.updated.label.removed" && label) {
|
||||||
return (
|
return (
|
||||||
<Trans>
|
<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>
|
</Trans>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -133,6 +221,14 @@ const ACTIVITY_ICON_MAP: Partial<Record<ActivityType, React.ReactNode | null>> =
|
|||||||
"card.updated.label.removed": <HiOutlineTag />,
|
"card.updated.label.removed": <HiOutlineTag />,
|
||||||
"card.updated.member.added": <HiOutlineUserPlus />,
|
"card.updated.member.added": <HiOutlineUserPlus />,
|
||||||
"card.updated.member.removed": <HiOutlineUserMinus />,
|
"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;
|
} as const;
|
||||||
|
|
||||||
const getActivityIcon = (
|
const getActivityIcon = (
|
||||||
@@ -179,6 +275,7 @@ const ActivityList = ({
|
|||||||
memberName: activity.member?.user?.name ?? null,
|
memberName: activity.member?.user?.name ?? null,
|
||||||
isSelf: activity.member?.user?.id === data?.user.id,
|
isSelf: activity.member?.user?.id === data?.user.id,
|
||||||
label: activity.label?.name ?? null,
|
label: activity.label?.name ?? null,
|
||||||
|
fromTitle: activity.fromTitle ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (activity.type === "card.updated.comment.added")
|
if (activity.type === "card.updated.comment.added")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { TRPCError } from "@trpc/server";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import * as cardRepo from "@kan/db/repository/card.repo";
|
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 * as checklistRepo from "@kan/db/repository/checklist.repo";
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
@@ -71,6 +72,13 @@ export const checklistRouter = createTRPCRouter({
|
|||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await cardActivityRepo.create(ctx.db, {
|
||||||
|
type: "card.updated.checklist.added",
|
||||||
|
cardId: card.id,
|
||||||
|
toTitle: newChecklist.name,
|
||||||
|
createdBy: userId,
|
||||||
|
});
|
||||||
|
|
||||||
return newChecklist;
|
return newChecklist;
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
@@ -105,6 +113,8 @@ export const checklistRouter = createTRPCRouter({
|
|||||||
checklist.card.list.board.workspace.id,
|
checklist.card.list.board.workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const previousName = checklist.name;
|
||||||
|
|
||||||
const updated = await checklistRepo.updateChecklistById(ctx.db, {
|
const updated = await checklistRepo.updateChecklistById(ctx.db, {
|
||||||
id: checklist.id,
|
id: checklist.id,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
@@ -116,6 +126,14 @@ export const checklistRouter = createTRPCRouter({
|
|||||||
code: "INTERNAL_SERVER_ERROR",
|
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;
|
return updated;
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
@@ -173,6 +191,13 @@ export const checklistRouter = createTRPCRouter({
|
|||||||
code: "INTERNAL_SERVER_ERROR",
|
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 };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
createItem: protectedProcedure
|
createItem: protectedProcedure
|
||||||
@@ -231,6 +256,13 @@ export const checklistRouter = createTRPCRouter({
|
|||||||
code: "INTERNAL_SERVER_ERROR",
|
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;
|
return newChecklistItem;
|
||||||
}),
|
}),
|
||||||
updateItem: protectedProcedure
|
updateItem: protectedProcedure
|
||||||
@@ -278,6 +310,8 @@ export const checklistRouter = createTRPCRouter({
|
|||||||
item.checklist.card.list.board.workspace.id,
|
item.checklist.card.list.board.workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const previousTitle = item.title;
|
||||||
|
|
||||||
const updated = await checklistRepo.updateItemById(ctx.db, {
|
const updated = await checklistRepo.updateItemById(ctx.db, {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
title: input.title,
|
title: input.title,
|
||||||
@@ -290,6 +324,29 @@ export const checklistRouter = createTRPCRouter({
|
|||||||
code: "INTERNAL_SERVER_ERROR",
|
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;
|
return updated;
|
||||||
}),
|
}),
|
||||||
deleteItem: protectedProcedure
|
deleteItem: protectedProcedure
|
||||||
@@ -341,6 +398,13 @@ export const checklistRouter = createTRPCRouter({
|
|||||||
code: "INTERNAL_SERVER_ERROR",
|
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 };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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';
|
||||||
2423
packages/db/migrations/meta/20250813141748_snapshot.json
Normal file
2423
packages/db/migrations/meta/20250813141748_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,13 @@
|
|||||||
"when": 1754511666265,
|
"when": 1754511666265,
|
||||||
"tag": "20250806202106_glossy_luminals",
|
"tag": "20250806202106_glossy_luminals",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1755094668761,
|
||||||
|
"tag": "20250813141748_AddChecklistActivityTypes",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,15 @@ export const activityTypes = [
|
|||||||
"card.updated.comment.added",
|
"card.updated.comment.added",
|
||||||
"card.updated.comment.updated",
|
"card.updated.comment.updated",
|
||||||
"card.updated.comment.deleted",
|
"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",
|
"card.archived",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user