mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-07 08:50:25 -06:00
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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Response" ADD COLUMN "personAttributes" JSONB;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -13,7 +13,7 @@ type TransformPersonInput = {
|
||||
}[];
|
||||
};
|
||||
|
||||
type TransformPersonOutput = {
|
||||
export type TransformPersonOutput = {
|
||||
id: string;
|
||||
attributes: Record<string, string | number>;
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user