feat: card comments
This commit is contained in:
@@ -11,7 +11,7 @@ const Avatar = ({
|
|||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
icon: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const initials = name
|
const initials = name
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
const LoadingSpinner = () => {
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
const LoadingSpinner = ({ size = "md" }: { size?: "sm" | "md" | "lg" }) => {
|
||||||
return (
|
return (
|
||||||
<svg className="h-6 w-6 animate-spin" viewBox="0 0 100 100">
|
<svg
|
||||||
|
className={twMerge(
|
||||||
|
"animate-spin",
|
||||||
|
size === "sm" && "h-4 w-4",
|
||||||
|
size === "md" && "h-5 w-5",
|
||||||
|
size === "lg" && "h-6 w-6",
|
||||||
|
)}
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
>
|
||||||
<circle
|
<circle
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke-width="10"
|
stroke-width="10"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
|||||||
|
|
||||||
import * as cardRepo from "~/server/db/repository/card.repo";
|
import * as cardRepo from "~/server/db/repository/card.repo";
|
||||||
import * as cardActivityRepo from "~/server/db/repository/cardActivity.repo";
|
import * as cardActivityRepo from "~/server/db/repository/cardActivity.repo";
|
||||||
|
import * as cardCommentRepo from "~/server/db/repository/cardComment.repo";
|
||||||
import * as labelRepo from "~/server/db/repository/label.repo";
|
import * as labelRepo from "~/server/db/repository/label.repo";
|
||||||
import * as listRepo from "~/server/db/repository/list.repo";
|
import * as listRepo from "~/server/db/repository/list.repo";
|
||||||
import * as workspaceRepo from "~/server/db/repository/workspace.repo";
|
import * as workspaceRepo from "~/server/db/repository/workspace.repo";
|
||||||
@@ -168,6 +169,63 @@ export const cardRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return newCard;
|
return newCard;
|
||||||
}),
|
}),
|
||||||
|
addComment: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Add a comment to a card",
|
||||||
|
method: "POST",
|
||||||
|
path: "/cards/{cardPublicId}/comments",
|
||||||
|
description: "Adds a comment to a card",
|
||||||
|
tags: ["Cards"],
|
||||||
|
protect: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
cardPublicId: z.string().min(12),
|
||||||
|
comment: z.string().min(1),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.output(z.custom<Awaited<ReturnType<typeof cardCommentRepo.create>>>())
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
|
if (!userId)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `User not authenticated`,
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
|
||||||
|
const card = await cardRepo.getByPublicId(ctx.db, input.cardPublicId);
|
||||||
|
|
||||||
|
if (!card)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Card with public ID ${input.cardPublicId} not found`,
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
|
||||||
|
const newComment = await cardCommentRepo.create(ctx.db, {
|
||||||
|
comment: input.comment,
|
||||||
|
createdBy: userId,
|
||||||
|
cardId: card.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!newComment?.id)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Failed to create comment`,
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
});
|
||||||
|
|
||||||
|
await cardActivityRepo.create(ctx.db, {
|
||||||
|
type: "card.updated.comment.added" as const,
|
||||||
|
cardId: card.id,
|
||||||
|
commentId: newComment.id,
|
||||||
|
toComment: newComment.comment,
|
||||||
|
createdBy: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return newComment;
|
||||||
|
}),
|
||||||
addOrRemoveLabel: protectedProcedure
|
addOrRemoveLabel: protectedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
|
|||||||
30
src/server/db/migrations/0007_adorable_crystal.sql
Normal file
30
src/server/db/migrations/0007_adorable_crystal.sql
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "card_comments" (
|
||||||
|
"id" bigserial PRIMARY KEY NOT NULL,
|
||||||
|
"publicId" varchar(12) NOT NULL,
|
||||||
|
"comment" text NOT NULL,
|
||||||
|
"cardId" bigint NOT NULL,
|
||||||
|
"createdBy" uuid NOT NULL,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp,
|
||||||
|
"deletedAt" timestamp,
|
||||||
|
"deletedBy" uuid,
|
||||||
|
CONSTRAINT "card_comments_publicId_unique" UNIQUE("publicId")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "card_comments" ADD CONSTRAINT "card_comments_cardId_card_id_fk" FOREIGN KEY ("cardId") REFERENCES "card"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "card_comments" ADD CONSTRAINT "card_comments_createdBy_user_id_fk" FOREIGN KEY ("createdBy") REFERENCES "user"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "card_comments" ADD CONSTRAINT "card_comments_deletedBy_user_id_fk" FOREIGN KEY ("deletedBy") REFERENCES "user"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
11
src/server/db/migrations/0008_nasty_bloodstorm.sql
Normal file
11
src/server/db/migrations/0008_nasty_bloodstorm.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
ALTER TYPE "card_activity_type" ADD VALUE 'card.updated.comment.added';--> statement-breakpoint
|
||||||
|
ALTER TYPE "card_activity_type" ADD VALUE 'card.updated.comment.updated';--> statement-breakpoint
|
||||||
|
ALTER TYPE "card_activity_type" ADD VALUE 'card.updated.comment.deleted';--> statement-breakpoint
|
||||||
|
ALTER TABLE "card_activity" ADD COLUMN "commentId" bigint;--> statement-breakpoint
|
||||||
|
ALTER TABLE "card_activity" ADD COLUMN "fromComment" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "card_activity" ADD COLUMN "toComment" text;--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "card_activity" ADD CONSTRAINT "card_activity_commentId_card_comments_id_fk" FOREIGN KEY ("commentId") REFERENCES "card_comments"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
1359
src/server/db/migrations/meta/0007_snapshot.json
Normal file
1359
src/server/db/migrations/meta/0007_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1393
src/server/db/migrations/meta/0008_snapshot.json
Normal file
1393
src/server/db/migrations/meta/0008_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,20 @@
|
|||||||
"when": 1730967400524,
|
"when": 1730967400524,
|
||||||
"tag": "0006_neat_korg",
|
"tag": "0006_neat_korg",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 7,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1731934769875,
|
||||||
|
"tag": "0007_adorable_crystal",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1731958265600,
|
||||||
|
"tag": "0008_nasty_bloodstorm",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -271,6 +271,11 @@ export const getWithListAndMembersByPublicId = async (
|
|||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
email
|
email
|
||||||
|
),
|
||||||
|
comment:card_comments!card_activity_commentId_card_comments_id_fk (
|
||||||
|
publicId,
|
||||||
|
comment,
|
||||||
|
createdBy
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ export const create = async (
|
|||||||
fromDescription?: string;
|
fromDescription?: string;
|
||||||
toDescription?: string;
|
toDescription?: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
|
commentId?: number;
|
||||||
|
fromComment?: string;
|
||||||
|
toComment?: string;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const { data } = await db
|
const { data } = await db
|
||||||
@@ -37,6 +40,9 @@ export const create = async (
|
|||||||
fromDescription: activityInput.fromDescription,
|
fromDescription: activityInput.fromDescription,
|
||||||
toDescription: activityInput.toDescription,
|
toDescription: activityInput.toDescription,
|
||||||
createdBy: activityInput.createdBy,
|
createdBy: activityInput.createdBy,
|
||||||
|
commentId: activityInput.commentId,
|
||||||
|
fromComment: activityInput.fromComment,
|
||||||
|
toComment: activityInput.toComment,
|
||||||
})
|
})
|
||||||
.select(`id`)
|
.select(`id`)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|||||||
26
src/server/db/repository/cardComment.repo.ts
Normal file
26
src/server/db/repository/cardComment.repo.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { generateUID } from "~/utils/generateUID";
|
||||||
|
import { type Database } from "~/types/database.types";
|
||||||
|
import { type SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
export const create = async (
|
||||||
|
db: SupabaseClient<Database>,
|
||||||
|
commentInput: {
|
||||||
|
cardId: number;
|
||||||
|
comment: string;
|
||||||
|
createdBy: string;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const { data } = await db
|
||||||
|
.from("card_comments")
|
||||||
|
.insert({
|
||||||
|
publicId: generateUID(),
|
||||||
|
comment: commentInput.comment,
|
||||||
|
createdBy: commentInput.createdBy,
|
||||||
|
cardId: commentInput.cardId,
|
||||||
|
})
|
||||||
|
.select(`id, publicId, comment`)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -34,6 +34,9 @@ export const activityTypeEnum = pgEnum("card_activity_type", [
|
|||||||
"card.updated.label.removed",
|
"card.updated.label.removed",
|
||||||
"card.updated.member.added",
|
"card.updated.member.added",
|
||||||
"card.updated.member.removed",
|
"card.updated.member.removed",
|
||||||
|
"card.updated.comment.added",
|
||||||
|
"card.updated.comment.updated",
|
||||||
|
"card.updated.comment.deleted",
|
||||||
"card.archived",
|
"card.archived",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -260,6 +263,7 @@ export const cardsRelations = relations(cards, ({ one, many }) => ({
|
|||||||
fields: [cards.importId],
|
fields: [cards.importId],
|
||||||
references: [imports.id],
|
references: [imports.id],
|
||||||
}),
|
}),
|
||||||
|
comments: many(comments),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const users = pgTable("user", {
|
export const users = pgTable("user", {
|
||||||
@@ -362,6 +366,11 @@ export const cardActivities = pgTable("card_activity", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id),
|
.references(() => users.id),
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
|
commentId: bigint("commentId", { mode: "number" }).references(
|
||||||
|
() => comments.id,
|
||||||
|
),
|
||||||
|
fromComment: text("fromComment"),
|
||||||
|
toComment: text("toComment"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const cardActivitiesRelations = relations(cardActivities, ({ one }) => ({
|
export const cardActivitiesRelations = relations(cardActivities, ({ one }) => ({
|
||||||
@@ -390,3 +399,34 @@ export const cardActivitiesRelations = relations(cardActivities, ({ one }) => ({
|
|||||||
references: [users.id],
|
references: [users.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const comments = pgTable("card_comments", {
|
||||||
|
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||||
|
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
|
||||||
|
comment: text("comment").notNull(),
|
||||||
|
cardId: bigint("cardId", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => cards.id, { onDelete: "cascade" }),
|
||||||
|
createdBy: uuid("createdBy")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id),
|
||||||
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp("updatedAt"),
|
||||||
|
deletedAt: timestamp("deletedAt"),
|
||||||
|
deletedBy: uuid("deletedBy").references(() => users.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const commentsRelations = relations(comments, ({ one }) => ({
|
||||||
|
card: one(cards, {
|
||||||
|
fields: [comments.cardId],
|
||||||
|
references: [cards.id],
|
||||||
|
}),
|
||||||
|
createdBy: one(users, {
|
||||||
|
fields: [comments.createdBy],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
deletedBy: one(users, {
|
||||||
|
fields: [comments.deletedBy],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|||||||
@@ -216,8 +216,10 @@ export type Database = {
|
|||||||
card_activity: {
|
card_activity: {
|
||||||
Row: {
|
Row: {
|
||||||
cardId: number;
|
cardId: number;
|
||||||
|
commentId: number | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
|
fromComment: string | null;
|
||||||
fromDescription: string | null;
|
fromDescription: string | null;
|
||||||
fromIndex: number | null;
|
fromIndex: number | null;
|
||||||
fromListId: number | null;
|
fromListId: number | null;
|
||||||
@@ -225,6 +227,7 @@ export type Database = {
|
|||||||
id: number;
|
id: number;
|
||||||
labelId: number | null;
|
labelId: number | null;
|
||||||
publicId: string;
|
publicId: string;
|
||||||
|
toComment: string | null;
|
||||||
toDescription: string | null;
|
toDescription: string | null;
|
||||||
toIndex: number | null;
|
toIndex: number | null;
|
||||||
toListId: number | null;
|
toListId: number | null;
|
||||||
@@ -234,8 +237,10 @@ export type Database = {
|
|||||||
};
|
};
|
||||||
Insert: {
|
Insert: {
|
||||||
cardId: number;
|
cardId: number;
|
||||||
|
commentId?: number | null;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
|
fromComment?: string | null;
|
||||||
fromDescription?: string | null;
|
fromDescription?: string | null;
|
||||||
fromIndex?: number | null;
|
fromIndex?: number | null;
|
||||||
fromListId?: number | null;
|
fromListId?: number | null;
|
||||||
@@ -243,6 +248,7 @@ export type Database = {
|
|||||||
id?: number;
|
id?: number;
|
||||||
labelId?: number | null;
|
labelId?: number | null;
|
||||||
publicId: string;
|
publicId: string;
|
||||||
|
toComment?: string | null;
|
||||||
toDescription?: string | null;
|
toDescription?: string | null;
|
||||||
toIndex?: number | null;
|
toIndex?: number | null;
|
||||||
toListId?: number | null;
|
toListId?: number | null;
|
||||||
@@ -252,8 +258,10 @@ export type Database = {
|
|||||||
};
|
};
|
||||||
Update: {
|
Update: {
|
||||||
cardId?: number;
|
cardId?: number;
|
||||||
|
commentId?: number | null;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
|
fromComment?: string | null;
|
||||||
fromDescription?: string | null;
|
fromDescription?: string | null;
|
||||||
fromIndex?: number | null;
|
fromIndex?: number | null;
|
||||||
fromListId?: number | null;
|
fromListId?: number | null;
|
||||||
@@ -261,6 +269,7 @@ export type Database = {
|
|||||||
id?: number;
|
id?: number;
|
||||||
labelId?: number | null;
|
labelId?: number | null;
|
||||||
publicId?: string;
|
publicId?: string;
|
||||||
|
toComment?: string | null;
|
||||||
toDescription?: string | null;
|
toDescription?: string | null;
|
||||||
toIndex?: number | null;
|
toIndex?: number | null;
|
||||||
toListId?: number | null;
|
toListId?: number | null;
|
||||||
@@ -276,6 +285,13 @@ export type Database = {
|
|||||||
referencedRelation: "card";
|
referencedRelation: "card";
|
||||||
referencedColumns: ["id"];
|
referencedColumns: ["id"];
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "card_activity_commentId_card_comments_id_fk";
|
||||||
|
columns: ["commentId"];
|
||||||
|
isOneToOne: false;
|
||||||
|
referencedRelation: "card_comments";
|
||||||
|
referencedColumns: ["id"];
|
||||||
|
},
|
||||||
{
|
{
|
||||||
foreignKeyName: "card_activity_createdBy_user_id_fk";
|
foreignKeyName: "card_activity_createdBy_user_id_fk";
|
||||||
columns: ["createdBy"];
|
columns: ["createdBy"];
|
||||||
@@ -313,6 +329,64 @@ export type Database = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
card_comments: {
|
||||||
|
Row: {
|
||||||
|
cardId: number;
|
||||||
|
comment: string;
|
||||||
|
createdAt: string;
|
||||||
|
createdBy: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
deletedBy: string | null;
|
||||||
|
id: number;
|
||||||
|
publicId: string;
|
||||||
|
updatedAt: string | null;
|
||||||
|
};
|
||||||
|
Insert: {
|
||||||
|
cardId: number;
|
||||||
|
comment: string;
|
||||||
|
createdAt?: string;
|
||||||
|
createdBy: string;
|
||||||
|
deletedAt?: string | null;
|
||||||
|
deletedBy?: string | null;
|
||||||
|
id?: number;
|
||||||
|
publicId: string;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
};
|
||||||
|
Update: {
|
||||||
|
cardId?: number;
|
||||||
|
comment?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
createdBy?: string;
|
||||||
|
deletedAt?: string | null;
|
||||||
|
deletedBy?: string | null;
|
||||||
|
id?: number;
|
||||||
|
publicId?: string;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
};
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "card_comments_cardId_card_id_fk";
|
||||||
|
columns: ["cardId"];
|
||||||
|
isOneToOne: false;
|
||||||
|
referencedRelation: "card";
|
||||||
|
referencedColumns: ["id"];
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "card_comments_createdBy_user_id_fk";
|
||||||
|
columns: ["createdBy"];
|
||||||
|
isOneToOne: false;
|
||||||
|
referencedRelation: "user";
|
||||||
|
referencedColumns: ["id"];
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "card_comments_deletedBy_user_id_fk";
|
||||||
|
columns: ["deletedBy"];
|
||||||
|
isOneToOne: false;
|
||||||
|
referencedRelation: "user";
|
||||||
|
referencedColumns: ["id"];
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
import: {
|
import: {
|
||||||
Row: {
|
Row: {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -680,7 +754,10 @@ export type Database = {
|
|||||||
| "card.updated.label.removed"
|
| "card.updated.label.removed"
|
||||||
| "card.updated.member.added"
|
| "card.updated.member.added"
|
||||||
| "card.updated.member.removed"
|
| "card.updated.member.removed"
|
||||||
| "card.archived";
|
| "card.archived"
|
||||||
|
| "card.updated.comment.added"
|
||||||
|
| "card.updated.comment.updated"
|
||||||
|
| "card.updated.comment.deleted";
|
||||||
member_status: "invited" | "active" | "removed";
|
member_status: "invited" | "active" | "removed";
|
||||||
role: "admin" | "member" | "guest";
|
role: "admin" | "member" | "guest";
|
||||||
source: "trello";
|
source: "trello";
|
||||||
|
|||||||
@@ -144,6 +144,35 @@ const ActivityList = ({
|
|||||||
label: activity.label?.name ?? null,
|
label: activity.label?.name ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (activity.type === "card.updated.comment.added")
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={activity.publicId}
|
||||||
|
className="relative flex flex w-full flex-col rounded-xl border border-light-600 bg-light-200 p-5 text-light-900 focus-visible:outline-none dark:border-dark-600 dark:bg-dark-100 dark:text-dark-1000 sm:text-sm sm:leading-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Avatar
|
||||||
|
size="sm"
|
||||||
|
name={activity.user?.name ?? ""}
|
||||||
|
email={activity.user?.email ?? ""}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
<p className="text-sm">
|
||||||
|
<span className="font-medium dark:text-dark-1000">{`${activity.user?.name} `}</span>
|
||||||
|
<span className="mx-1 text-light-900 dark:text-dark-800">
|
||||||
|
·
|
||||||
|
</span>
|
||||||
|
<span className="space-x-1 text-light-900 dark:text-dark-800">
|
||||||
|
{formatDistanceToNow(new Date(activity.createdAt), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-sm">{activity.comment?.comment}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
if (!activityText) return null;
|
if (!activityText) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -159,9 +188,11 @@ const ActivityList = ({
|
|||||||
icon={getActivityIcon(activity.type)}
|
icon={getActivityIcon(activity.type)}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
{index !== activities.length - 1 && (
|
{index !== activities.length - 1 &&
|
||||||
<div className="absolute bottom-[-14px] left-1/2 top-[30px] w-0.5 -translate-x-1/2 bg-light-600 dark:bg-dark-600" />
|
activities[index + 1]?.type !==
|
||||||
)}
|
"card.updated.comment.added" && (
|
||||||
|
<div className="absolute bottom-[-14px] left-1/2 top-[30px] w-0.5 -translate-x-1/2 bg-light-600 dark:bg-dark-600" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
<span className="font-medium dark:text-dark-1000">{`${activity.user?.name} `}</span>
|
<span className="font-medium dark:text-dark-1000">{`${activity.user?.name} `}</span>
|
||||||
|
|||||||
72
src/views/card/components/NewCommentForm.tsx
Normal file
72
src/views/card/components/NewCommentForm.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import ContentEditable from "react-contenteditable";
|
||||||
|
import { HiOutlineArrowUp } from "react-icons/hi2";
|
||||||
|
|
||||||
|
import LoadingSpinner from "~/components/LoadingSpinner";
|
||||||
|
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { usePopup } from "~/providers/popup";
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
comment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { showPopup } = usePopup();
|
||||||
|
const { handleSubmit, setValue, watch, reset } = useForm<FormValues>({
|
||||||
|
values: {
|
||||||
|
comment: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const addCommentMutation = api.card.addComment.useMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
await utils.card.byId.refetch();
|
||||||
|
reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
showPopup({
|
||||||
|
header: "Unable to add comment",
|
||||||
|
message: "Please try again later, or contact customer support.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data: FormValues) => {
|
||||||
|
addCommentMutation.mutate({
|
||||||
|
cardPublicId,
|
||||||
|
comment: data.comment,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="flex w-full flex-col rounded-xl border border-light-600 bg-light-200 p-5 text-light-900 focus-visible:outline-none dark:border-dark-600 dark:bg-dark-100 dark:text-dark-1000 sm:text-sm sm:leading-6"
|
||||||
|
>
|
||||||
|
<ContentEditable
|
||||||
|
placeholder="Add a comment..."
|
||||||
|
html={watch("comment")}
|
||||||
|
disabled={false}
|
||||||
|
onChange={(e) => setValue("comment", e.target.value)}
|
||||||
|
className="block w-full border-0 bg-transparent py-1.5 text-light-900 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={addCommentMutation.isPending}
|
||||||
|
className="flex h-8 w-8 items-center justify-center rounded-full border border-light-600 bg-light-300 hover:bg-light-400 disabled:opacity-50 dark:border-dark-600 dark:bg-dark-300 dark:hover:bg-dark-400"
|
||||||
|
>
|
||||||
|
{addCommentMutation.isPending ? (
|
||||||
|
<LoadingSpinner size="sm" />
|
||||||
|
) : (
|
||||||
|
<HiOutlineArrowUp />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewCommentForm;
|
||||||
@@ -13,6 +13,7 @@ import ListSelector from "./components/ListSelector";
|
|||||||
import MemberSelector from "./components/MemberSelector";
|
import MemberSelector from "./components/MemberSelector";
|
||||||
import { LabelForm } from "./components/LabelForm";
|
import { LabelForm } from "./components/LabelForm";
|
||||||
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||||
|
import NewCommentForm from "./components/NewCommentForm";
|
||||||
|
|
||||||
import Modal from "~/components/modal";
|
import Modal from "~/components/modal";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
@@ -179,6 +180,9 @@ export default function CardPage() {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-6">
|
||||||
|
<NewCommentForm cardPublicId={cardId} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user