From bf67af4dca3699a678ee6347d6fced03cc489e87 Mon Sep 17 00:00:00 2001 From: tyjkerr Date: Mon, 18 Sep 2023 10:34:16 +0700 Subject: [PATCH] feat: Make response notes resolvable (#801) * update notes ux * add capability to resolve notes * add migration * update text color * prevent updating unchanged note * add isEdited to ResponseNote * combine migrations into one * simplify services * fix UI issues --------- Co-authored-by: Matthias Nannt --- .../(analysis)/responses/ResponseNote.tsx | 123 ++++++++++++++---- .../(analysis)/responses/actions.ts | 10 +- .../responses/[responseId]/index.ts | 2 + .../[environmentId]/responses/index.ts | 2 + .../migration.sql | 3 + packages/database/schema.prisma | 2 + packages/lib/services/response.ts | 2 + packages/lib/services/responseNote.ts | 82 +++++++----- packages/types/v1/responses.ts | 2 + 9 files changed, 164 insertions(+), 64 deletions(-) create mode 100644 packages/database/migrations/20230918025340_add_is_resolved_and_is_edited_to_response_note/migration.sql diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponseNote.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponseNote.tsx index c5196167c9..cbdfcaf495 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponseNote.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponseNote.tsx @@ -1,16 +1,20 @@ "use client"; -import { updateResponseNoteAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions"; +import { + resolveResponseNoteAction, + updateResponseNoteAction, +} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions"; import { useProfile } from "@/lib/profile"; import { addResponseNote } from "@/lib/responseNotes/responsesNotes"; +import { cn } from "@formbricks/lib/cn"; import { timeSince } from "@formbricks/lib/time"; import { TResponseNote } from "@formbricks/types/v1/responses"; -import { Button } from "@formbricks/ui"; -import { MinusIcon, PencilIcon, PlusIcon } from "@heroicons/react/24/solid"; +import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui"; +import { CheckIcon, PencilIcon, PlusIcon } from "@heroicons/react/24/solid"; import clsx from "clsx"; -import { Maximize2Icon } from "lucide-react"; +import { Maximize2Icon, Minimize2Icon } from "lucide-react"; import { useRouter } from "next/navigation"; -import { FormEvent, useEffect, useRef, useState } from "react"; +import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; import toast from "react-hot-toast"; interface ResponseNotesProps { @@ -35,6 +39,7 @@ export default function ResponseNotes({ const [noteText, setNoteText] = useState(""); const [isCreatingNote, setIsCreatingNote] = useState(false); const [isUpdatingNote, setIsUpdatingNote] = useState(false); + const [isTextAreaOpen, setIsTextAreaOpen] = useState(true); const [noteId, setNoteId] = useState(""); const divRef = useRef(null); @@ -52,7 +57,22 @@ export default function ResponseNotes({ } }; + const handleResolveNote = (note: TResponseNote) => { + try { + resolveResponseNoteAction(note.id); + // when this was the last note, close the notes panel + if (unresolvedNotes.length === 1) { + setIsOpen(false); + } + router.refresh(); + } catch (e) { + toast.error("An error occurred resolving a note"); + setIsUpdatingNote(false); + } + }; + const handleEditPencil = (note: TResponseNote) => { + setIsTextAreaOpen(true); setNoteText(note.text); setIsUpdatingNote(true); setNoteId(note.id); @@ -62,7 +82,7 @@ export default function ResponseNotes({ e.preventDefault(); setIsUpdatingNote(true); try { - await updateResponseNoteAction(responseId, noteId, noteText); + await updateResponseNoteAction(noteId, noteText); router.refresh(); setIsUpdatingNote(false); setNoteText(""); @@ -78,15 +98,17 @@ export default function ResponseNotes({ } }, [notes]); + const unresolvedNotes = useMemo(() => notes.filter((note) => !note.isResolved), [notes]); + return (
- {!notes.length ? ( + {!unresolvedNotes.length ? (

Note

@@ -112,7 +134,7 @@ export default function ResponseNotes({
)}
- {!notes.length ? ( + {!unresolvedNotes.length ? (
@@ -125,20 +147,20 @@ export default function ResponseNotes({
-

Note

+

Note

- {notes.map((note) => ( -
+ {unresolvedNotes.map((note) => ( +
{note.user.name} + {note.isEdited && ( + {"(edited)"} + )} -
- {note.text} +
+ {note.text} {profile.id === note.user.id && ( - )} + + + + + + + Resolve + + +
))}
-
-
+
+
-
- + {isTextAreaOpen && ( + + )}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions.ts index ad090d816c..5a9de073fe 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions.ts @@ -1,7 +1,11 @@ "use server"; -import { updateResponseNote } from "@formbricks/lib/services/responseNote"; +import { updateResponseNote, resolveResponseNote } from "@formbricks/lib/services/responseNote"; -export const updateResponseNoteAction = async (responseId: string, noteId: string, text: string) => { - await updateResponseNote(responseId, noteId, text); +export const updateResponseNoteAction = async (responseNoteId: string, text: string) => { + await updateResponseNote(responseNoteId, text); +}; + +export const resolveResponseNoteAction = async (responseNoteId: string) => { + await resolveResponseNote(responseNoteId); }; diff --git a/apps/web/pages/api/v1/client/environments/[environmentId]/responses/[responseId]/index.ts b/apps/web/pages/api/v1/client/environments/[environmentId]/responses/[responseId]/index.ts index 23c304c251..c13dd3a976 100644 --- a/apps/web/pages/api/v1/client/environments/[environmentId]/responses/[responseId]/index.ts +++ b/apps/web/pages/api/v1/client/environments/[environmentId]/responses/[responseId]/index.ts @@ -93,6 +93,8 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) name: true, }, }, + isResolved: true, + isEdited: true, }, }, tags: { diff --git a/apps/web/pages/api/v1/client/environments/[environmentId]/responses/index.ts b/apps/web/pages/api/v1/client/environments/[environmentId]/responses/index.ts index 38bd300428..c0bb00a44c 100644 --- a/apps/web/pages/api/v1/client/environments/[environmentId]/responses/index.ts +++ b/apps/web/pages/api/v1/client/environments/[environmentId]/responses/index.ts @@ -138,6 +138,8 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) name: true, }, }, + isResolved: true, + isEdited: true, }, }, tags: { diff --git a/packages/database/migrations/20230918025340_add_is_resolved_and_is_edited_to_response_note/migration.sql b/packages/database/migrations/20230918025340_add_is_resolved_and_is_edited_to_response_note/migration.sql new file mode 100644 index 0000000000..54f7d7b1b0 --- /dev/null +++ b/packages/database/migrations/20230918025340_add_is_resolved_and_is_edited_to_response_note/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "ResponseNote" ADD COLUMN "isEdited" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "isResolved" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index 3b9e7fc834..45620e9a13 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -124,6 +124,8 @@ model ResponseNote { user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId String text String + isResolved Boolean @default(false) + isEdited Boolean @default(false) } model Tag { diff --git a/packages/lib/services/response.ts b/packages/lib/services/response.ts index e17884ce54..cc4e09efbe 100644 --- a/packages/lib/services/response.ts +++ b/packages/lib/services/response.ts @@ -55,6 +55,8 @@ const responseSelection = { name: true, }, }, + isResolved: true, + isEdited: true, }, }, tags: { diff --git a/packages/lib/services/responseNote.ts b/packages/lib/services/responseNote.ts index edbd4e479e..b600fa5428 100644 --- a/packages/lib/services/responseNote.ts +++ b/packages/lib/services/responseNote.ts @@ -2,49 +2,61 @@ import "server-only"; import { prisma } from "@formbricks/database"; -import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; +import { DatabaseError } from "@formbricks/types/v1/errors"; +import { TResponseNote } from "@formbricks/types/v1/responses"; import { Prisma } from "@prisma/client"; -export const updateResponseNote = async (responseId: string, noteId: string, text: string): Promise => { +const select = { + id: true, + createdAt: true, + updatedAt: true, + text: true, + isEdited: true, + isResolved: true, + user: { + select: { + id: true, + name: true, + }, + }, +}; + +export const updateResponseNote = async (responseNoteId: string, text: string): Promise => { try { - const currentResponse = await prisma.response.findUnique({ + const updatedResponseNote = await prisma.responseNote.update({ where: { - id: responseId, - }, - select: { - notes: true, - }, - }); - - if (!currentResponse) { - throw new ResourceNotFoundError("Response", "No Response Found"); - } - - const currentNote = currentResponse.notes.find((eachnote) => eachnote.id === noteId); - - if (!currentNote) { - throw new ResourceNotFoundError("Note", "No Note Found"); - } - - const updatedResponse = await prisma.response.update({ - where: { - id: responseId, + id: responseNoteId, }, data: { - notes: { - updateMany: { - where: { - id: noteId, - }, - data: { - text: text, - updatedAt: new Date(), - }, - }, - }, + text: text, + updatedAt: new Date(), + isEdited: true, }, + select, }); - return updatedResponse; + return updatedResponseNote; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + + throw error; + } +}; + +export const resolveResponseNote = async (responseNoteId: string): Promise => { + try { + const responseNote = await prisma.responseNote.update({ + where: { + id: responseNoteId, + }, + data: { + updatedAt: new Date(), + isResolved: true, + }, + select, + }); + return responseNote; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError("Database operation failed"); diff --git a/packages/types/v1/responses.ts b/packages/types/v1/responses.ts index 91a35ffa70..f5b988a034 100644 --- a/packages/types/v1/responses.ts +++ b/packages/types/v1/responses.ts @@ -24,6 +24,8 @@ const ZResponseNote = z.object({ id: z.string(), text: z.string(), user: ZResponseNoteUser, + isResolved: z.boolean(), + isEdited: z.boolean(), }); export type TResponseNote = z.infer;