- {data.finished && (
-
- Completed
-
+
+
+
+
+ {data.personId ? (
+
+
+
+ {displayIdentifier}
+
+
+ ) : (
+
)}
-
-
+
+
+ {data.finished && (
+
+ Completed
+
+ )}
+
+
+
-
-
- {data.responses.map((response, idx) => (
-
-
{response.question}
- {typeof response.answer !== "object" ? (
- response.type === "rating" ? (
-
-
-
+
+ {data.responses.map((response, idx) => (
+
+
{response.question}
+ {typeof response.answer !== "object" ? (
+ response.type === "rating" ? (
+
+
+
+ ) : (
+
{response.answer}
+ )
) : (
-
{response.answer}
- )
- ) : (
-
{response.answer.join(", ")}
- )}
-
- ))}
+
{response.answer.join(", ")}
+ )}
+
+ ))}
+
+
-
+
);
}
diff --git a/apps/web/lib/responseNote/responsesNote.ts b/apps/web/lib/responseNote/responsesNote.ts
new file mode 100644
index 0000000000..90b9c0af96
--- /dev/null
+++ b/apps/web/lib/responseNote/responsesNote.ts
@@ -0,0 +1,21 @@
+export const addResponseNote = async (
+ environmentId: string,
+ surveyId: string,
+ responseId: string,
+ text: string
+) => {
+ try {
+ const res = await fetch(
+ `/api/v1/environments/${environmentId}/surveys/${surveyId}/responses/${responseId}/responsesNote`,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(text),
+ }
+ );
+ return await res.json();
+ } catch (error) {
+ console.error(error);
+ throw Error(`createResponseNote: unable to create responseNote: ${error.message}`);
+ }
+};
diff --git a/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/[submissionId]/responsesNote/index.ts b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/[submissionId]/responsesNote/index.ts
new file mode 100644
index 0000000000..979650388b
--- /dev/null
+++ b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/[submissionId]/responsesNote/index.ts
@@ -0,0 +1,86 @@
+import { captureTelemetry } from "@/../../packages/lib/telemetry";
+import { hasEnvironmentAccess, getSessionUser } from "@/lib/api/apiHelper";
+import { prisma } from "@formbricks/database";
+import type { NextApiRequest, NextApiResponse } from "next";
+import { responses } from "@/lib/api/response";
+
+export default async function handle(req: NextApiRequest, res: NextApiResponse) {
+ const environmentId = req.query.environmentId?.toString();
+ const responseId = req.query.submissionId?.toString();
+ const surveyId = req.query.surveyId?.toString();
+
+ // Check Authentication
+ const currentUser: any = await getSessionUser(req, res);
+ if (!currentUser) {
+ return res.status(401).json({ message: "Not authenticated" });
+ }
+
+ // Check environmentId
+ if (environmentId === undefined) {
+ return res.status(400).json({ message: "Missing environmentId" });
+ }
+
+ // Check responseId
+ if (responseId === undefined) {
+ return res.status(400).json({ message: "Missing responseId" });
+ }
+
+ // Check surveyId
+ if (surveyId === undefined) {
+ return res.status(400).json({ message: "Missing surveyId" });
+ }
+
+ // Check whether user has access to the environment
+ const hasAccess = await hasEnvironmentAccess(req, res, environmentId);
+ if (!hasAccess) {
+ return res.status(403).json({ message: "Not authorized" });
+ }
+
+ // GET /api/environments[environmentId]/survey[surveyId]/responses/[responseId]/responsesNote
+ // Create a note to a response
+ if (req.method === "POST") {
+ const currentResponse = await prisma.response.findUnique({
+ where: {
+ id: responseId,
+ },
+ select: {
+ data: true,
+ survey: {
+ select: {
+ environmentId: true,
+ },
+ },
+ },
+ });
+
+ if (!currentResponse) {
+ return responses.notFoundResponse("Response", responseId, true);
+ }
+ const responseNote = {
+ data: {
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ response: {
+ connect: {
+ id: responseId,
+ },
+ },
+ user: {
+ connect: {
+ id: currentUser.id,
+ },
+ },
+ text: req.body,
+ },
+ };
+
+ const newResponseNote = await prisma.responseNote.create(responseNote);
+ captureTelemetry("responseNote created");
+ return res.json(newResponseNote);
+ }
+
+ // Unknown HTTP Method
+ else {
+ throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
+ }
+}
diff --git a/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/index.ts b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/index.ts
index f7e748dc5b..e667525b9f 100644
--- a/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/index.ts
+++ b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/index.ts
@@ -49,6 +49,12 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
},
},
},
+ responseNote: {
+ include: {
+ response: true,
+ user: true,
+ },
+ },
},
});
diff --git a/packages/database/prisma/migrations/20230605074319_add_model_response_note/migration.sql b/packages/database/prisma/migrations/20230605074319_add_model_response_note/migration.sql
new file mode 100644
index 0000000000..5a5873c163
--- /dev/null
+++ b/packages/database/prisma/migrations/20230605074319_add_model_response_note/migration.sql
@@ -0,0 +1,17 @@
+-- CreateTable
+CREATE TABLE "ResponseNote" (
+ "id" TEXT NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL,
+ "responseId" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "text" TEXT NOT NULL,
+
+ CONSTRAINT "ResponseNote_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "ResponseNote" ADD CONSTRAINT "ResponseNote_responseId_fkey" FOREIGN KEY ("responseId") REFERENCES "Response"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ResponseNote" ADD CONSTRAINT "ResponseNote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma
index b925a6ba27..39285c61d2 100644
--- a/packages/database/prisma/schema.prisma
+++ b/packages/database/prisma/schema.prisma
@@ -85,20 +85,32 @@ model Person {
}
model Response {
- id String @id @default(cuid())
- createdAt DateTime @default(now()) @map(name: "created_at")
- updatedAt DateTime @updatedAt @map(name: "updated_at")
- finished Boolean @default(false)
- survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade)
- surveyId String
- person Person? @relation(fields: [personId], references: [id], onDelete: Cascade)
- personId String?
+ id String @id @default(cuid())
+ createdAt DateTime @default(now()) @map(name: "created_at")
+ updatedAt DateTime @updatedAt @map(name: "updated_at")
+ finished Boolean @default(false)
+ survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade)
+ surveyId String
+ person Person? @relation(fields: [personId], references: [id], onDelete: Cascade)
+ personId String?
+ responseNote ResponseNote[]
/// [ResponseData]
data Json @default("{}")
/// [ResponseMeta]
meta Json @default("{}")
}
+model ResponseNote {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now()) @map(name: "created_at")
+ updatedAt DateTime @updatedAt @map(name: "updated_at")
+ response Response @relation(fields: [responseId], references: [id], onDelete: Cascade)
+ responseId String
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ userId String
+ text String
+}
+
enum SurveyStatus {
draft
inProgress
@@ -394,6 +406,7 @@ model User {
identityProviderAccountId String?
memberships Membership[]
accounts Account[]
+ responseNote ResponseNote[]
groupId String?
invitesCreated Invite[] @relation("inviteCreatedBy")
invitesAccepted Invite[] @relation("inviteAcceptedBy")