diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/SingleResponse.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/SingleResponse.tsx index 7455e4138e..a70e20f4c8 100644 --- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/SingleResponse.tsx +++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/SingleResponse.tsx @@ -2,7 +2,7 @@ import DeleteDialog from "@/components/shared/DeleteDialog"; import { timeSince } from "@formbricks/lib/time"; -import { PersonAvatar } from "@formbricks/ui"; +import { PersonAvatar, TooltipContent, TooltipProvider, TooltipTrigger, Tooltip } from "@formbricks/ui"; import { CheckCircleIcon } from "@heroicons/react/24/solid"; import { TrashIcon } from "@heroicons/react/24/outline"; import Link from "next/link"; @@ -26,6 +26,9 @@ export interface OpenTextSummaryProps { environmentId: string; attributes: []; }; + personAttributes: { + [key: string]: string; + }; responseNotes: { updatedAt: string; createdAt: string; @@ -74,6 +77,18 @@ export default function SingleResponse({ data, environmentId, surveyId }: OpenTe setIsDeleting(false); }; + const tooltipContent = data.personAttributes && Object.keys(data.personAttributes).length > 0 && ( + + {Object.keys(data.personAttributes).map((key) => { + return ( +

+ {key}: {data.personAttributes[key]} +

+ ); + })} +
+ ); + return (
- + + + + + + {tooltipContent} + +

{displayIdentifier}

diff --git a/packages/database/jsonTypes.ts b/packages/database/jsonTypes.ts index c74ac0b041..1ec9b86da4 100644 --- a/packages/database/jsonTypes.ts +++ b/packages/database/jsonTypes.ts @@ -1,5 +1,5 @@ import { TEventClassNoCodeConfig } from "@formbricks/types/v1/eventClasses"; -import { TResponseData } from "@formbricks/types/v1/responses"; +import { TResponsePersonAttributes, TResponseData } from "@formbricks/types/v1/responses"; import { TSurveyQuestions, TSurveyThankYouCard } from "@formbricks/types/v1/surveys"; import { TUserNotificationSettings } from "@formbricks/types/v1/users"; @@ -9,6 +9,7 @@ declare global { export type EventClassNoCodeConfig = TEventClassNoCodeConfig; export type ResponseData = TResponseData; export type ResponseMeta = { [key: string]: string }; + export type ResponsePersonAttributes = TResponsePersonAttributes; export type SurveyQuestions = TSurveyQuestions; export type SurveyThankYouCard = TSurveyThankYouCard; export type UserNotificationSettings = TUserNotificationSettings; diff --git a/packages/database/migrations/20230618112915_person_attributes/migration.sql b/packages/database/migrations/20230618112915_person_attributes/migration.sql new file mode 100644 index 0000000000..c2c3c39fd1 --- /dev/null +++ b/packages/database/migrations/20230618112915_person_attributes/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Response" ADD COLUMN "personAttributes" JSONB; diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index 6b01b30de3..d443073b06 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -87,21 +87,24 @@ 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? - responseNotes ResponseNote[] - /// @zod.custom(imports.ZResponseData) + 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? + responseNotes ResponseNote[] + /// @zod.custom(imports.ZResponsePersonAttributes) + /// [ResponsePersonAttributes] + personAttributes Json? + /// @zod.custom(imports.ZResponseData) /// [ResponseData] - data Json @default("{}") + data Json @default("{}") /// @zod.custom(imports.ZResponseMeta) /// [ResponseMeta] - meta Json @default("{}") + meta Json @default("{}") } model ResponseNote { diff --git a/packages/database/zod-utils.ts b/packages/database/zod-utils.ts index 1d9f8cecfd..1e17d99712 100644 --- a/packages/database/zod-utils.ts +++ b/packages/database/zod-utils.ts @@ -3,7 +3,7 @@ import z from "zod"; export const ZEventProperties = z.record(z.string()); export { ZEventClassNoCodeConfig } from "@formbricks/types/v1/eventClasses"; -export { ZResponseData } from "@formbricks/types/v1/responses"; +export { ZResponseData, ZResponsePersonAttributes } from "@formbricks/types/v1/responses"; export const ZResponseMeta = z.record(z.union([z.string(), z.number()])); export { ZSurveyQuestions, ZSurveyThankYouCard } from "@formbricks/types/v1/surveys"; diff --git a/packages/lib/services/person.ts b/packages/lib/services/person.ts index d910523bd6..96d6335975 100644 --- a/packages/lib/services/person.ts +++ b/packages/lib/services/person.ts @@ -13,7 +13,7 @@ type TransformPersonInput = { }[]; }; -type TransformPersonOutput = { +export type TransformPersonOutput = { id: string; attributes: Record; }; diff --git a/packages/lib/services/response.ts b/packages/lib/services/response.ts index ce00d50a5a..4d3cc62ab5 100644 --- a/packages/lib/services/response.ts +++ b/packages/lib/services/response.ts @@ -2,10 +2,16 @@ import { prisma } from "@formbricks/database"; import { TResponse, TResponseInput, TResponseUpdateInput } from "@formbricks/types/v1/responses"; import { Prisma } from "@prisma/client"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/errors"; -import { transformPrismaPerson } from "./person"; +import { getPerson, TransformPersonOutput, transformPrismaPerson } from "./person"; export const createResponse = async (responseInput: TResponseInput): Promise => { try { + let person: TransformPersonOutput | null = null; + + if (responseInput.personId) { + person = await getPerson(responseInput.personId); + } + const responsePrisma = await prisma.response.create({ data: { survey: { @@ -21,6 +27,7 @@ export const createResponse = async (responseInput: TResponseInput): Promise surveyId: true, finished: true, data: true, + personAttributes: true, person: { select: { id: true, @@ -100,6 +93,7 @@ export const getResponse = async (responseId: string): Promise const response: TResponse = { ...responsePrisma, + personAttributes: responsePrisma.personAttributes as Record, person: transformPrismaPerson(responsePrisma.person), }; diff --git a/packages/types/v1/people.ts b/packages/types/v1/people.ts index e5cb6baa3c..b0a19febfd 100644 --- a/packages/types/v1/people.ts +++ b/packages/types/v1/people.ts @@ -1,8 +1,11 @@ import z from "zod"; +export const ZPersonAttributes = z.record(z.union([z.string(), z.number()])); +export type TPersonAttributes = z.infer; + export const ZPerson = z.object({ id: z.string().cuid2(), - attributes: z.record(z.union([z.string(), z.number()])), + attributes: ZPersonAttributes, }); export type TPerson = z.infer; diff --git a/packages/types/v1/responses.ts b/packages/types/v1/responses.ts index 3304cb642c..04b179a9cd 100644 --- a/packages/types/v1/responses.ts +++ b/packages/types/v1/responses.ts @@ -1,9 +1,14 @@ import { z } from "zod"; +import { ZPersonAttributes } from "./people"; export const ZResponseData = z.record(z.union([z.string(), z.number(), z.array(z.string())])); export type TResponseData = z.infer; +export const ZResponsePersonAttributes = ZPersonAttributes.optional(); + +export type TResponsePersonAttributes = z.infer; + const ZResponse = z.object({ id: z.string().cuid2(), createdAt: z.date(), @@ -15,6 +20,7 @@ const ZResponse = z.object({ attributes: z.record(z.union([z.string(), z.number()])), }) .nullable(), + personAttributes: ZResponsePersonAttributes, finished: z.boolean(), data: ZResponseData, });