feat: card comments
This commit is contained in:
@@ -11,7 +11,7 @@ const Avatar = ({
|
||||
size?: "sm" | "md" | "lg";
|
||||
name: string;
|
||||
email: string;
|
||||
icon: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
const initials = name
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
const LoadingSpinner = () => {
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
const LoadingSpinner = ({ size = "md" }: { size?: "sm" | "md" | "lg" }) => {
|
||||
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
|
||||
fill="none"
|
||||
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 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 listRepo from "~/server/db/repository/list.repo";
|
||||
import * as workspaceRepo from "~/server/db/repository/workspace.repo";
|
||||
@@ -168,6 +169,63 @@ export const cardRouter = createTRPCRouter({
|
||||
|
||||
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
|
||||
.meta({
|
||||
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,
|
||||
"tag": "0006_neat_korg",
|
||||
"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,
|
||||
name,
|
||||
email
|
||||
),
|
||||
comment:card_comments!card_activity_commentId_card_comments_id_fk (
|
||||
publicId,
|
||||
comment,
|
||||
createdBy
|
||||
)
|
||||
)
|
||||
`,
|
||||
|
||||
@@ -18,6 +18,9 @@ export const create = async (
|
||||
fromDescription?: string;
|
||||
toDescription?: string;
|
||||
createdBy: string;
|
||||
commentId?: number;
|
||||
fromComment?: string;
|
||||
toComment?: string;
|
||||
},
|
||||
) => {
|
||||
const { data } = await db
|
||||
@@ -37,6 +40,9 @@ export const create = async (
|
||||
fromDescription: activityInput.fromDescription,
|
||||
toDescription: activityInput.toDescription,
|
||||
createdBy: activityInput.createdBy,
|
||||
commentId: activityInput.commentId,
|
||||
fromComment: activityInput.fromComment,
|
||||
toComment: activityInput.toComment,
|
||||
})
|
||||
.select(`id`)
|
||||
.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.member.added",
|
||||
"card.updated.member.removed",
|
||||
"card.updated.comment.added",
|
||||
"card.updated.comment.updated",
|
||||
"card.updated.comment.deleted",
|
||||
"card.archived",
|
||||
]);
|
||||
|
||||
@@ -260,6 +263,7 @@ export const cardsRelations = relations(cards, ({ one, many }) => ({
|
||||
fields: [cards.importId],
|
||||
references: [imports.id],
|
||||
}),
|
||||
comments: many(comments),
|
||||
}));
|
||||
|
||||
export const users = pgTable("user", {
|
||||
@@ -362,6 +366,11 @@ export const cardActivities = pgTable("card_activity", {
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
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 }) => ({
|
||||
@@ -390,3 +399,34 @@ export const cardActivitiesRelations = relations(cardActivities, ({ one }) => ({
|
||||
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: {
|
||||
Row: {
|
||||
cardId: number;
|
||||
commentId: number | null;
|
||||
createdAt: string;
|
||||
createdBy: string;
|
||||
fromComment: string | null;
|
||||
fromDescription: string | null;
|
||||
fromIndex: number | null;
|
||||
fromListId: number | null;
|
||||
@@ -225,6 +227,7 @@ export type Database = {
|
||||
id: number;
|
||||
labelId: number | null;
|
||||
publicId: string;
|
||||
toComment: string | null;
|
||||
toDescription: string | null;
|
||||
toIndex: number | null;
|
||||
toListId: number | null;
|
||||
@@ -234,8 +237,10 @@ export type Database = {
|
||||
};
|
||||
Insert: {
|
||||
cardId: number;
|
||||
commentId?: number | null;
|
||||
createdAt?: string;
|
||||
createdBy: string;
|
||||
fromComment?: string | null;
|
||||
fromDescription?: string | null;
|
||||
fromIndex?: number | null;
|
||||
fromListId?: number | null;
|
||||
@@ -243,6 +248,7 @@ export type Database = {
|
||||
id?: number;
|
||||
labelId?: number | null;
|
||||
publicId: string;
|
||||
toComment?: string | null;
|
||||
toDescription?: string | null;
|
||||
toIndex?: number | null;
|
||||
toListId?: number | null;
|
||||
@@ -252,8 +258,10 @@ export type Database = {
|
||||
};
|
||||
Update: {
|
||||
cardId?: number;
|
||||
commentId?: number | null;
|
||||
createdAt?: string;
|
||||
createdBy?: string;
|
||||
fromComment?: string | null;
|
||||
fromDescription?: string | null;
|
||||
fromIndex?: number | null;
|
||||
fromListId?: number | null;
|
||||
@@ -261,6 +269,7 @@ export type Database = {
|
||||
id?: number;
|
||||
labelId?: number | null;
|
||||
publicId?: string;
|
||||
toComment?: string | null;
|
||||
toDescription?: string | null;
|
||||
toIndex?: number | null;
|
||||
toListId?: number | null;
|
||||
@@ -276,6 +285,13 @@ export type Database = {
|
||||
referencedRelation: "card";
|
||||
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";
|
||||
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: {
|
||||
Row: {
|
||||
createdAt: string;
|
||||
@@ -680,7 +754,10 @@ export type Database = {
|
||||
| "card.updated.label.removed"
|
||||
| "card.updated.member.added"
|
||||
| "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";
|
||||
role: "admin" | "member" | "guest";
|
||||
source: "trello";
|
||||
|
||||
@@ -144,6 +144,35 @@ const ActivityList = ({
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -159,9 +188,11 @@ const ActivityList = ({
|
||||
icon={getActivityIcon(activity.type)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
{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" />
|
||||
)}
|
||||
{index !== activities.length - 1 &&
|
||||
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>
|
||||
<p className="text-sm">
|
||||
<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 { LabelForm } from "./components/LabelForm";
|
||||
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||
import NewCommentForm from "./components/NewCommentForm";
|
||||
|
||||
import Modal from "~/components/modal";
|
||||
import { useModal } from "~/providers/modal";
|
||||
@@ -179,6 +180,9 @@ export default function CardPage() {
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<NewCommentForm cardPublicId={cardId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user