Add snapshot of user attributes to a response (#403)

* feat: added current person attributes to the user response

* feat: added tooltip showing user attributes in response view

* fix: switched to using the service layer and added annotations for json field

* rename PersonAttributesData to ResponsePersonAttributes to fit current naming scheme

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Pradumn Kumar
2023-06-21 16:32:55 +05:30
committed by GitHub
parent 08717cd396
commit 6922b3ed3f
9 changed files with 66 additions and 35 deletions

View File

@@ -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 && (
<TooltipContent>
{Object.keys(data.personAttributes).map((key) => {
return (
<p>
{key}: <span className="font-bold">{data.personAttributes[key]}</span>
</p>
);
})}
</TooltipContent>
);
return (
<div className={clsx("group relative", isOpen && "min-h-[300px]")}>
<div
@@ -87,7 +102,14 @@ export default function SingleResponse({ data, environmentId, surveyId }: OpenTe
<Link
className="group flex items-center"
href={`/environments/${environmentId}/people/${data.personId}`}>
<PersonAvatar personId={data.personId} />
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<PersonAvatar personId={data.personId} />
</TooltipTrigger>
{tooltipContent}
</Tooltip>
</TooltipProvider>
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600 hover:underline">
{displayIdentifier}
</h3>

View File

@@ -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;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Response" ADD COLUMN "personAttributes" JSONB;

View File

@@ -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 {

View File

@@ -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";

View File

@@ -13,7 +13,7 @@ type TransformPersonInput = {
}[];
};
type TransformPersonOutput = {
export type TransformPersonOutput = {
id: string;
attributes: Record<string, string | number>;
};

View File

@@ -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<TResponse> => {
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<TRe
id: responseInput.personId,
},
},
personAttributes: person?.attributes,
}),
},
select: {
@@ -30,27 +37,12 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
surveyId: true,
finished: true,
data: true,
person: {
select: {
id: true,
attributes: {
select: {
value: true,
attributeClass: {
select: {
name: true,
},
},
},
},
},
},
},
});
const response: TResponse = {
...responsePrisma,
person: transformPrismaPerson(responsePrisma.person),
person,
};
return response;
@@ -76,6 +68,7 @@ export const getResponse = async (responseId: string): Promise<TResponse | null>
surveyId: true,
finished: true,
data: true,
personAttributes: true,
person: {
select: {
id: true,
@@ -100,6 +93,7 @@ export const getResponse = async (responseId: string): Promise<TResponse | null>
const response: TResponse = {
...responsePrisma,
personAttributes: responsePrisma.personAttributes as Record<string, string | number>,
person: transformPrismaPerson(responsePrisma.person),
};

View File

@@ -1,8 +1,11 @@
import z from "zod";
export const ZPersonAttributes = z.record(z.union([z.string(), z.number()]));
export type TPersonAttributes = z.infer<typeof ZPersonAttributes>;
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<typeof ZPerson>;

View File

@@ -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<typeof ZResponseData>;
export const ZResponsePersonAttributes = ZPersonAttributes.optional();
export type TResponsePersonAttributes = z.infer<typeof ZResponsePersonAttributes>;
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,
});