Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✍️ Add comment edit feature by appending #464

Merged
merged 14 commits into from
May 10, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import {
import { ErrorTooltip } from "@/components/ui/tooltip";
import { AddCommentIcon } from "@/components/icons/addCommentIcon";

import { addCommentSchema, type AddCommentSchema } from "./addCommentSchema";
import { AddReviewDialog } from "./addReviewDialog";
import {
addCommentAction,
addReviewAction,
type AddCommentActionPayload,
} from "./addCommentAction";
import { addCommentSchema, type AddCommentSchema } from "./addCommentSchema";
import { AddReviewDialog } from "./addReviewDialog";
} from "./commentActions";

interface AddCommentFormProps {
application: ApplicationWithComments;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import { urls } from "@/constants/urls";
import {
insertComment,
insertCommentAsReview,
updateComment,
} from "@/drizzle/queries/comments";

import { type ApplicationId } from "@/types/Application";
import { canAddComment, canAddReview } from "@/config/actionPermissions";
import { Comment } from "@/types/Comment";
import {
canAddComment,
canAddReview,
canEditComment,
} from "@/config/actionPermissions";

import { type AddCommentSchema } from "./addCommentSchema";

Expand Down Expand Up @@ -83,3 +89,26 @@ export async function addReplyAction({

revalidatePath(urls.applications.preview({ waveId, applicationId }));
}

export interface UpdateCommentActionPayload {
applicationId: ApplicationId;
commentId: Comment["id"];
newContent: Comment["content"];
}

export async function updateCommentAction({
applicationId,
commentId,
newContent,
}: UpdateCommentActionPayload) {
const { validationErrorMessage } = await canEditComment(applicationId);

if (typeof validationErrorMessage !== "undefined") {
throw new Error(validationErrorMessage);
}

await updateComment({
newContent,
commentId,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";

import { ApplicationWithComments } from "@/types/Application";
import { type Comment } from "@/types/Comment";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Editor } from "@/components/ui/editor/editor";
import {
Form,
FormControl,
FormField,
FormFooter,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { CheckIcon } from "@/components/icons/checkIcon";
import { InfoCircleIcon } from "@/components/icons/infoCircleIcon";

import { addCommentSchema, type AddCommentSchema } from "./addCommentSchema";
import { updateCommentAction } from "./commentActions";

interface CommentEditFormProps {
application: ApplicationWithComments;
comment: Comment;
onEdit: () => void;
}

export function CommentEditForm({
application,
comment,
onEdit,
}: CommentEditFormProps) {
const isReview = comment.review?.isReview;

const form = useForm<AddCommentSchema>({
resolver: zodResolver(addCommentSchema),
defaultValues: {
content: "",
},
});

const handleSubmit = form.handleSubmit(async ({ content }) => {
await updateCommentAction({
applicationId: application.id,
commentId: comment.id,
newContent: content,
});
form.reset();
onEdit();
});

function handleCancel() {
form.reset();
onEdit();
}

return (
<Form {...form}>
<form className="flex flex-col gap-4">
<Badge
variant="gray"
className="text-sm font-normal [&>svg]:h-4 [&>svg]:w-4"
>
<InfoCircleIcon className="h-4 w-4" />
You are only able to provide additional information to your{" "}
{isReview ? "review" : "comment"}. Original{" "}
{isReview ? "review" : "comment"} cannot be changed.
</Badge>
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormControl>
<Editor onChange={field.onChange} size="small" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormFooter className="mt-0 justify-start">
<Button
type="button"
variant="secondary"
disabled={form.formState.isSubmitting}
onClick={handleCancel}
>
Cancel
</Button>
<Button
variant="secondary"
disabled={form.formState.isSubmitting}
onClick={handleSubmit}
>
Submit
<CheckIcon />
</Button>
</FormFooter>
</form>
</Form>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {
} from "@/components/ui/form";
import { AddCommentIcon } from "@/components/icons/addCommentIcon";

import { addReplyAction, AddReplyActionPayload } from "./addCommentAction";
import { addCommentSchema, type AddCommentSchema } from "./addCommentSchema";
import { addReplyAction, AddReplyActionPayload } from "./commentActions";

interface CommentReplyFormProps
extends Pick<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import { MarkdownPreview } from "@/components/ui/markdownPreview";
import { Separator } from "@/components/ui/separator";
import { ErrorTooltip } from "@/components/ui/tooltip";
import { UserAvatar } from "@/components/ui/userAvatar";
import { EditSquareIcon } from "@/components/icons/editSquareIcon";
import { ReplyIcon } from "@/components/icons/replyIcon";

import { CommentEditForm } from "../addCommentForm/commentEditForm";
import { CommentReplyForm } from "../addCommentForm/commentReplyForm";
import { CommentValueForm } from "../commentValue/commentValueForm";
import { ReplyTarget } from "../replyTarget/replyTarget";
Expand All @@ -24,6 +26,7 @@ interface CommentPreviewProps {
comment: Comment;
userId: UserId | undefined;
addCommentValidationError: string | undefined;
editCommentValidationError: string | undefined;
rateCommentValidationError: string | undefined;
}

Expand All @@ -32,18 +35,24 @@ export const CommentPreview = ({
comment,
userId,
addCommentValidationError,
editCommentValidationError,
rateCommentValidationError,
}: CommentPreviewProps) => {
const { waveId, comments: allComments } = application;
const { review, user, replyTargetId, createdAt, id } = comment;

const isReview = review?.isReview;
const [isReply, setIsReply] = useState(false);
const [isEdit, setIsEdit] = useState(false);

function onReply() {
setIsReply(false);
}

function onEdit() {
setIsEdit(false);
}

return (
<div className="flex flex-col gap-4">
<div className="flex flex-col">
Expand All @@ -67,17 +76,34 @@ export const CommentPreview = ({
<Separator orientation="dot" className="opacity-60" />
<span className="opacity-60">{formatTime(createdAt)}</span>
</div>
<ErrorTooltip message={addCommentValidationError}>
<Button
disabled={!!addCommentValidationError}
variant="link"
className="h-6 p-2 py-0 opacity-60 transition-opacity before:opacity-0 hover:opacity-100 focus-visible:opacity-100"
onClick={() => setIsReply(true)}
>
<ReplyIcon />
Reply
</Button>
</ErrorTooltip>
<div className="flex items-center gap-1">
<ErrorTooltip message={editCommentValidationError}>
<Button
disabled={!!editCommentValidationError}
variant="link"
className="h-6 p-2 py-0 opacity-60 transition-opacity before:opacity-0 hover:opacity-100 focus-visible:opacity-100"
onClick={() => setIsEdit(true)}
>
<EditSquareIcon />
Edit
</Button>
</ErrorTooltip>
<Separator
orientation="vertical"
className="h-6 bg-primary opacity-10"
/>
<ErrorTooltip message={addCommentValidationError}>
<Button
disabled={!!addCommentValidationError}
variant="link"
className="h-6 p-2 py-0 opacity-60 transition-opacity before:opacity-0 hover:opacity-100 focus-visible:opacity-100"
onClick={() => setIsReply(true)}
>
<ReplyIcon />
Reply
</Button>
</ErrorTooltip>
</div>
</div>

<MarkdownPreview body={comment.markdownContent} />
Expand All @@ -100,6 +126,13 @@ export const CommentPreview = ({
onReply={onReply}
/>
)}
{isEdit && (
<CommentEditForm
application={application}
comment={comment}
onEdit={onEdit}
/>
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { type UserId } from "@/types/User";
import {
canAddComment,
canAddReview,
canEditComment,
canRateComment,
} from "@/config/actionPermissions";
import { Separator } from "@/components/ui/separator";
Expand All @@ -31,12 +32,15 @@ export async function Comments({ application, userId }: CommentsProps) {

const reviews = comments.filter((comment) => comment.review?.isReview);

const { validationErrorMessage: commentValidationError } =
const { validationErrorMessage: addCommentValidationError } =
await canAddComment(application.id);
const { validationErrorMessage: reviewValidationError } = await canAddReview(
application.id,
);

const { validationErrorMessage: editCommentValidationError } =
await canEditComment(application.id);

const { validationErrorMessage: rateCommentValidationError } =
await canRateComment({ waveId: application.waveId });

Expand All @@ -63,7 +67,8 @@ export async function Comments({ application, userId }: CommentsProps) {
application={application}
comments={comments}
userId={userId}
commentValidationError={commentValidationError}
addCommentValidationError={addCommentValidationError}
editCommentValidationError={editCommentValidationError}
rateCommentValidationError={rateCommentValidationError}
/>
</TabsContent>
Expand All @@ -76,7 +81,8 @@ export async function Comments({ application, userId }: CommentsProps) {
application={application}
comments={reviews}
userId={userId}
commentValidationError={commentValidationError}
addCommentValidationError={addCommentValidationError}
editCommentValidationError={editCommentValidationError}
rateCommentValidationError={rateCommentValidationError}
/>
</TabsContent>
Expand All @@ -85,7 +91,7 @@ export async function Comments({ application, userId }: CommentsProps) {

<AddCommentForm
application={application}
commentValidationError={commentValidationError}
commentValidationError={addCommentValidationError}
reviewValidationError={reviewValidationError}
/>
</div>
Expand All @@ -112,15 +118,17 @@ interface CommentsListProps {
application: ApplicationWithComments;
comments: Comment[];
userId: UserId | undefined;
commentValidationError: string | undefined;
addCommentValidationError: string | undefined;
editCommentValidationError: string | undefined;
rateCommentValidationError: string | undefined;
}

function CommentsList({
application,
comments,
userId,
commentValidationError,
addCommentValidationError,
editCommentValidationError,
rateCommentValidationError,
}: CommentsListProps) {
return comments.map((comment) => (
Expand All @@ -129,7 +137,8 @@ function CommentsList({
application={application}
comment={comment}
userId={userId}
addCommentValidationError={commentValidationError}
addCommentValidationError={addCommentValidationError}
editCommentValidationError={editCommentValidationError}
rateCommentValidationError={rateCommentValidationError}
/>
<Separator className="my-6 last:hidden" />
Expand Down
Loading
Loading