mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-05 16:19:55 -06:00
refactor: Unified single response card and improved UX (#750)
Co-authored-by: Johannes <johannes@formbricks.com> Co-authored-by: Dhruwang Jariwala <dhruwang@Dhruwangs-MacBook-Pro.local> Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
d84e06b909
commit
6a280913c3
@@ -11,7 +11,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@formbricks/ui";
|
||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
|
||||
import Modal from "../shared/Modal";
|
||||
import { Modal } from "@formbricks/ui";
|
||||
|
||||
interface EventDetailModalProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ResponsiveVideo } from "@formbricks/ui";
|
||||
import Modal from "../shared/Modal";
|
||||
import { Modal } from "@formbricks/ui";
|
||||
|
||||
interface VideoWalkThroughProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import { Fragment } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
type Modal = {
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
noPadding?: boolean;
|
||||
closeOnOutsideClick?: boolean;
|
||||
};
|
||||
|
||||
const Modal: React.FC<Modal> = ({
|
||||
open,
|
||||
setOpen,
|
||||
children,
|
||||
title,
|
||||
noPadding,
|
||||
closeOnOutsideClick = true,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Transition.Root show={open} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={() => closeOnOutsideClick && setOpen(false)}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0">
|
||||
<div className="fixed inset-0 bg-slate-500 bg-opacity-30 backdrop-blur-md transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||
<Dialog.Panel
|
||||
className={clsx(
|
||||
"relative transform rounded-lg bg-slate-100 text-left shadow-xl transition-all dark:bg-slate-800 sm:my-8 sm:w-full sm:max-w-xl ",
|
||||
`${noPadding ? "" : "px-4 pb-4 pt-5 sm:p-6"}`
|
||||
)}>
|
||||
<div className="absolute right-0 top-0 hidden pr-4 pt-4 sm:block">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md bg-white text-slate-400 hover:text-slate-500 focus:outline-none focus:ring-0 focus:ring-offset-2 dark:bg-slate-900"
|
||||
onClick={() => setOpen(false)}>
|
||||
<span className="sr-only">Close</span>
|
||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{title && <h3 className="mb-4 text-xl font-bold text-slate-500">{title}</h3>}
|
||||
|
||||
{children}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import { DeleteDialog } from "@formbricks/ui";
|
||||
import type { NoCodeConfig } from "@formbricks/types/events";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { Modal } from "@formbricks/ui";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { createActionClass, deleteActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
|
||||
import { canUserAccessActionClass } from "@formbricks/lib/actionClass/auth";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { Modal } from "@formbricks/ui";
|
||||
|
||||
interface UploadAttributesModalProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { SHORT_SURVEY_BASE_URL, SURVEY_BASE_URL } from "@formbricks/lib/constants";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { createProductAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { Modal } from "@formbricks/ui";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { PlusCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -304,7 +304,8 @@ export default function Navigation({
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="flex cursor-pointer flex-row items-center space-x-5">
|
||||
{session.user.image ? (
|
||||
<ProfileAvatar userId={session.user.id} />
|
||||
{/* {session.user.image ? (
|
||||
<Image
|
||||
src={session.user.image}
|
||||
width="100"
|
||||
@@ -314,7 +315,7 @@ export default function Navigation({
|
||||
/>
|
||||
) : (
|
||||
<ProfileAvatar userId={session.user.id} />
|
||||
)}
|
||||
)} */}
|
||||
|
||||
<div>
|
||||
<p className="ph-no-capture ph-no-capture -mb-0.5 text-sm font-bold text-slate-700">
|
||||
@@ -328,7 +329,7 @@ export default function Navigation({
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuLabel className="cursor-default break-all">
|
||||
<span className="ph-no-capture font-normal">Signed in as </span>
|
||||
{session?.user?.name.length > 30 ? (
|
||||
{session?.user?.name && session?.user?.name.length > 30 ? (
|
||||
<TooltipProvider delayDuration={50}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { Modal } from "@formbricks/ui";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { LinkIcon } from "@heroicons/react/24/outline";
|
||||
import clsx from "clsx";
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useState, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import Image from "next/image";
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { Modal } from "@formbricks/ui";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/solid";
|
||||
import { upsertIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/actions";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/actions";
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import { DeleteDialog } from "@formbricks/ui";
|
||||
import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
|
||||
@@ -2,7 +2,7 @@ import SurveyCheckboxGroup from "@/app/(app)/environments/[environmentId]/integr
|
||||
import TriggerCheckboxGroup from "@/app/(app)/environments/[environmentId]/integrations/webhooks/TriggerCheckboxGroup";
|
||||
import { triggers } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/HardcodedTriggers";
|
||||
import { testEndpoint } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/testEndpoint";
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { Modal } from "@formbricks/ui";
|
||||
import { createWebhookAction } from "./actions";
|
||||
import { TPipelineTrigger } from "@formbricks/types/v1/pipelines";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import { DeleteDialog } from "@formbricks/ui";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
import clsx from "clsx";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { createWebhook, deleteWebhook, updateWebhook } from "@formbricks/lib/webhook/service";
|
||||
import { TWebhook, TWebhookInput } from "@formbricks/types/v1/webhooks";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
@@ -2,7 +2,7 @@ import EnvironmentsNavbar from "@/app/(app)/environments/[environmentId]/compone
|
||||
import ToasterClient from "@/components/ToasterClient";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import FormbricksClient from "../../FormbricksClient";
|
||||
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
|
||||
@@ -1,31 +1,41 @@
|
||||
import ResponseTimeline from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseTimeline";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getResponsesByPersonId } from "@formbricks/lib/response/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TResponseWithSurvey } from "@formbricks/types/v1/responses";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
export default async function ResponseSection({
|
||||
environment,
|
||||
personId,
|
||||
environmentTags,
|
||||
}: {
|
||||
environment: TEnvironment;
|
||||
personId: string;
|
||||
environmentTags: TTag[];
|
||||
}) {
|
||||
const responses = await getResponsesByPersonId(personId);
|
||||
const surveyIds = responses?.map((response) => response.surveyId) || [];
|
||||
const surveys: TSurvey[] = surveyIds.length === 0 ? [] : (await getSurveys(environment.id)) ?? [];
|
||||
const responsesWithSurvey: TResponseWithSurvey[] =
|
||||
responses?.reduce((acc: TResponseWithSurvey[], response) => {
|
||||
const thisSurvey = surveys.find((survey) => survey?.id === response.surveyId);
|
||||
if (thisSurvey) {
|
||||
acc.push({
|
||||
...response,
|
||||
survey: thisSurvey,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []) || [];
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
return <ResponseTimeline environment={environment} responses={responsesWithSurvey} />;
|
||||
if (!session) {
|
||||
throw new Error("No session found");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{responses && (
|
||||
<ResponseTimeline
|
||||
profile={session.user}
|
||||
surveys={surveys}
|
||||
responses={responses}
|
||||
environment={environment}
|
||||
environmentTags={environmentTags}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import ResponseFeed from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponsesFeed";
|
||||
import { TResponse } from "@formbricks/types/v1/responses";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TResponseWithSurvey } from "@formbricks/types/v1/responses";
|
||||
import { ArrowsUpDownIcon } from "@heroicons/react/24/outline";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
|
||||
export default function ResponseTimeline({
|
||||
surveys,
|
||||
profile,
|
||||
environment,
|
||||
responses,
|
||||
environmentTags,
|
||||
}: {
|
||||
surveys: TSurvey[];
|
||||
profile: TProfile;
|
||||
responses: TResponse[];
|
||||
environment: TEnvironment;
|
||||
responses: TResponseWithSurvey[];
|
||||
environmentTags: TTag[];
|
||||
}) {
|
||||
const [responsesAscending, setResponsesAscending] = useState(true);
|
||||
const [responsesAscending, setResponsesAscending] = useState(false);
|
||||
const [sortedResponses, setSortedResponses] = useState(responses);
|
||||
const toggleSortResponses = () => {
|
||||
setResponsesAscending(!responsesAscending);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSortedResponses(responsesAscending ? [...responses].reverse() : responses);
|
||||
}, [responsesAscending]);
|
||||
|
||||
return (
|
||||
<div className="md:col-span-2">
|
||||
<div className="flex items-center justify-between pb-6">
|
||||
@@ -30,7 +44,13 @@ export default function ResponseTimeline({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ResponseFeed responses={responses} sortByDate={responsesAscending} environment={environment} />
|
||||
<ResponseFeed
|
||||
responses={sortedResponses}
|
||||
environment={environment}
|
||||
surveys={surveys}
|
||||
profile={profile}
|
||||
environmentTags={environmentTags}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,102 +1,48 @@
|
||||
import { formatDistance } from "date-fns";
|
||||
import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
|
||||
import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator";
|
||||
import { TResponseWithSurvey } from "@formbricks/types/v1/responses";
|
||||
import Link from "next/link";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
import { TResponse } from "@formbricks/types/v1/responses";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
import SingleResponseCard from "@formbricks/ui/SingleResponseCard";
|
||||
|
||||
export default function ResponseFeed({
|
||||
export default async function ResponseFeed({
|
||||
responses,
|
||||
sortByDate,
|
||||
environment,
|
||||
surveys,
|
||||
profile,
|
||||
environmentTags,
|
||||
}: {
|
||||
responses: TResponseWithSurvey[];
|
||||
sortByDate: boolean;
|
||||
responses: TResponse[];
|
||||
environment: TEnvironment;
|
||||
surveys: TSurvey[];
|
||||
profile: TProfile;
|
||||
environmentTags: TTag[];
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{responses.length === 0 ? (
|
||||
<EmptySpaceFiller type="response" environment={environment} />
|
||||
) : (
|
||||
<div>
|
||||
{responses
|
||||
.slice()
|
||||
.sort((a, b) =>
|
||||
sortByDate
|
||||
? new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
: new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||
)
|
||||
.map((response: TResponseWithSurvey, responseIdx) => (
|
||||
<li key={response.id} className="list-none">
|
||||
<div className="relative pb-8">
|
||||
{responseIdx !== responses.length - 1 ? (
|
||||
<span
|
||||
className="absolute left-4 top-4 -ml-px h-full w-0.5 bg-slate-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<div className="relative flex space-x-3">
|
||||
<div className="w-full overflow-hidden rounded-lg bg-white shadow">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="text-sm text-slate-400">
|
||||
<time
|
||||
className="text-slate-700"
|
||||
dateTime={formatDistance(response.createdAt, new Date(), {
|
||||
addSuffix: true,
|
||||
})}>
|
||||
{formatDistance(response.createdAt, new Date(), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
<div className="flex items-center justify-center space-x-2 rounded-full bg-slate-50 px-3 py-1 text-sm text-slate-600">
|
||||
<Link
|
||||
className="hover:underline"
|
||||
href={`/environments/${environment.id}/surveys/${response.survey.id}/summary`}>
|
||||
{response.survey.name}
|
||||
</Link>
|
||||
<SurveyStatusIndicator
|
||||
status={response.survey.status}
|
||||
environment={environment}
|
||||
type={response.survey.type}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 space-y-3">
|
||||
{response.survey.questions.map((question) => (
|
||||
<div key={question.id}>
|
||||
<p className="text-sm text-slate-500">{question.headline}</p>
|
||||
<p className="ph-no-capture my-1 text-lg font-semibold text-slate-700">
|
||||
{Array.isArray(response.data[question.id])
|
||||
? (response.data[question.id] as string[]).join(", ")
|
||||
: response.data[question.id]}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex w-full justify-end">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<p className="text-sm text-slate-500">{response.singleUseId}</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-sm text-slate-500">Single Use Id</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
responses.map((response, idx) => {
|
||||
const survey = surveys.find((survey) => {
|
||||
return survey.id === response.surveyId;
|
||||
});
|
||||
return (
|
||||
<div key={idx}>
|
||||
{survey && (
|
||||
<SingleResponseCard
|
||||
response={response}
|
||||
survey={survey}
|
||||
profile={profile}
|
||||
pageType="people"
|
||||
environmentTags={environmentTags}
|
||||
environment={environment}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { deletePersonAction } from "@/app/(app)/environments/[environmentId]/people/[personId]/actions";
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import { DeleteDialog } from "@formbricks/ui";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import GoBackButton from "@/components/shared/GoBackButton";
|
||||
import { DeletePersonButton } from "./DeletePersonButton";
|
||||
import { getPersonIdentifier } from "@formbricks/lib/people/helpers";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
|
||||
interface HeadingSectionProps {
|
||||
@@ -18,7 +19,7 @@ export default async function HeadingSection({ environmentId, personId }: Headin
|
||||
<GoBackButton />
|
||||
<div className="flex items-baseline justify-between border-b border-slate-200 pb-6 pt-4">
|
||||
<h1 className="ph-no-capture text-4xl font-bold tracking-tight text-slate-900">
|
||||
<span>{person.attributes.email || person.id}</span>
|
||||
<span>{getPersonIdentifier(person)}</span>
|
||||
</h1>
|
||||
<div className="flex items-center space-x-3">
|
||||
<DeletePersonButton environmentId={environmentId} personId={personId} />
|
||||
|
||||
@@ -5,10 +5,12 @@ import AttributesSection from "@/app/(app)/environments/[environmentId]/people/[
|
||||
import ResponseSection from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection";
|
||||
import HeadingSection from "@/app/(app)/environments/[environmentId]/people/[personId]/HeadingSection";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
|
||||
export default async function PersonPage({ params }) {
|
||||
const environment = await getEnvironment(params.environmentId);
|
||||
const environmentTags = await getTagsByEnvironmentId(params.environmentId);
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
@@ -20,7 +22,11 @@ export default async function PersonPage({ params }) {
|
||||
<section className="pb-24 pt-6">
|
||||
<div className="grid grid-cols-1 gap-x-8 md:grid-cols-4">
|
||||
<AttributesSection personId={params.personId} />
|
||||
<ResponseSection environment={environment} personId={params.personId} />
|
||||
<ResponseSection
|
||||
environment={environment}
|
||||
personId={params.personId}
|
||||
environmentTags={environmentTags}
|
||||
/>
|
||||
<ActivitySection environmentId={params.environmentId} personId={params.personId} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { Modal } from "@formbricks/ui";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import { DeleteDialog } from "@formbricks/ui";
|
||||
import { capitalizeFirstLetter } from "@/lib/utils";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { TApiKey } from "@formbricks/types/v1/apiKeys";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { deleteApiKey, createApiKey } from "@formbricks/lib/apiKey/service";
|
||||
import { canUserAccessApiKey } from "@formbricks/lib/apiKey/auth";
|
||||
|
||||
@@ -3,7 +3,7 @@ export const revalidate = REVALIDATION_INTERVAL;
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { Modal } from "@formbricks/ui";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { deleteTeamAction } from "@/app/(app)/environments/[environmentId]/settings/members/actions";
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import { DeleteDialog } from "@formbricks/ui";
|
||||
import { TTeam } from "@formbricks/types/v1/teams";
|
||||
import { Button, Input } from "@formbricks/ui";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
deleteMembershipAction,
|
||||
resendInviteAction,
|
||||
} from "@/app/(app)/environments/[environmentId]/settings/members/actions";
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import { DeleteDialog } from "@formbricks/ui";
|
||||
import { TInvite } from "@formbricks/types/v1/invites";
|
||||
import { TMember } from "@formbricks/types/v1/memberships";
|
||||
import { TTeam } from "@formbricks/types/v1/teams";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { Modal } from "@formbricks/ui";
|
||||
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { CheckIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { createInviteToken } from "@formbricks/lib/jwt";
|
||||
import { AuthenticationError, AuthorizationError, ValidationError } from "@formbricks/types/v1/errors";
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import TeamActions from "@/app/(app)/environments/[environmentId]/settings/members/EditMemberships/TeamActions";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getMembershipsByUserId, getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { Skeleton } from "@formbricks/ui";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import SettingsCard from "@/app/(app)/environments/[environmentId]/settings/SettingsCard";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { NotificationSettings } from "@formbricks/types/users";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import DeleteProductRender from "@/app/(app)/environments/[environmentId]/settings/product/DeleteProductRender";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
|
||||
type DeleteProductProps = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { deleteProductAction } from "@/app/(app)/environments/[environmentId]/settings/product/actions";
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import { DeleteDialog } from "@formbricks/ui";
|
||||
import { truncate } from "@/lib/utils";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { Button } from "@formbricks/ui";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import { DeleteDialog } from "@formbricks/ui";
|
||||
import AvatarPlaceholder from "@/images/avatar-placeholder.png";
|
||||
import { formbricksLogout } from "@/lib/formbricks";
|
||||
import { Button, Input, ProfileAvatar } from "@formbricks/ui";
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import AvatarPlaceholder from "@/images/avatar-placeholder.png";
|
||||
import { Button, ProfileAvatar } from "@formbricks/ui";
|
||||
import Image from "next/image";
|
||||
import { Session } from "next-auth";
|
||||
|
||||
export function EditAvatar({ session }: { session: Session | null }) {
|
||||
return (
|
||||
<div>
|
||||
{session?.user?.image ? (
|
||||
{/* {session?.user?.image ? (
|
||||
<Image
|
||||
src={AvatarPlaceholder}
|
||||
width="100"
|
||||
@@ -18,7 +16,8 @@ export function EditAvatar({ session }: { session: Session | null }) {
|
||||
/>
|
||||
) : (
|
||||
<ProfileAvatar userId={session!.user.id} />
|
||||
)}
|
||||
)} */}
|
||||
<ProfileAvatar userId={session!.user.id} />
|
||||
|
||||
<Button className="mt-4" variant="darkCTA" disabled={true}>
|
||||
Upload Image
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { updateProfile, deleteProfile } from "@formbricks/lib/profile/service";
|
||||
import { TProfileUpdateInput } from "@formbricks/types/v1/profile";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
@@ -2,7 +2,7 @@ export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import SettingsCard from "../SettingsCard";
|
||||
import SettingsTitle from "../SettingsTitle";
|
||||
import { DeleteAccount } from "./DeleteAccount";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { deleteTag, mergeTags, updateTagName } from "@formbricks/lib/tag/service";
|
||||
import { canUserAccessTag } from "@formbricks/lib/tag/auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
deleteSurveyAction,
|
||||
duplicateSurveyAction,
|
||||
} from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import { DeleteDialog } from "@formbricks/ui";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UsageAttributesUpdater } from "@/app/(app)/FormbricksClient";
|
||||
import SurveyDropDownMenu from "@/app/(app)/environments/[environmentId]/surveys/SurveyDropDownMenu";
|
||||
import SurveyStarter from "@/app/(app)/environments/[environmentId]/surveys/SurveyStarter";
|
||||
import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator";
|
||||
import { SurveyStatusIndicator } from "@formbricks/ui";
|
||||
import { SURVEY_BASE_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
@@ -81,12 +81,9 @@ export default async function SurveysList({ environmentId }: { environmentId: st
|
||||
<div className="flex items-center">
|
||||
{survey.status !== "draft" && (
|
||||
<>
|
||||
<SurveyStatusIndicator
|
||||
status={survey.status}
|
||||
tooltip
|
||||
environment={environment}
|
||||
type={survey.type}
|
||||
/>
|
||||
{(survey.type === "link" || environment.widgetSetupCompleted) && (
|
||||
<SurveyStatusIndicator status={survey.status} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{survey.status === "draft" && (
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
'use server'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export default async function revalidateSurveyIdPath(environmentId:string,surveyId:string) {
|
||||
revalidatePath(`/environments/${environmentId}/surveys/${surveyId}`)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { PresentationChartLineIcon, InboxStackIcon } from "@heroicons/react/24/solid";
|
||||
import Link from "next/link";
|
||||
import revalidateSurveyIdPath from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
||||
|
||||
interface SurveyResultsTabProps {
|
||||
activeId: string;
|
||||
@@ -31,6 +32,9 @@ export default function SurveyResultsTab({ activeId, environmentId, surveyId }:
|
||||
{tabs.map((tab) => (
|
||||
<Link
|
||||
key={tab.id}
|
||||
onClick={() => {
|
||||
revalidateSurveyIdPath(environmentId, surveyId);
|
||||
}}
|
||||
href={tab.href}
|
||||
className={cn(
|
||||
tab.id === activeId
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Metadata } from "next";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { getAnalysisData } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data";
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"use server";
|
||||
|
||||
import { deleteResponse } from "@formbricks/lib/response/service";
|
||||
import { updateResponseNote, resolveResponseNote } from "@formbricks/lib/responseNote/service";
|
||||
import { createTag } from "@formbricks/lib/tag/service";
|
||||
import { addTagToRespone, deleteTagOnResponse } from "@formbricks/lib/tagOnResponse/service";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
import { canUserAccessResponse } from "@formbricks/lib/response/auth";
|
||||
|
||||
@@ -72,6 +72,7 @@ const ResponsePage = ({
|
||||
surveyId={surveyId}
|
||||
responses={filterResponses}
|
||||
survey={survey}
|
||||
profile={profile}
|
||||
environmentTags={environmentTags}
|
||||
/>
|
||||
</ContentWrapper>
|
||||
|
||||
@@ -1,74 +1,29 @@
|
||||
"use client";
|
||||
import EmptyInAppSurveys from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
|
||||
import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
import { TResponse } from "@formbricks/types/v1/responses";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { useMemo } from "react";
|
||||
import SingleResponse from "./SingleResponse";
|
||||
import EmptyInAppSurveys from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
import SingleResponseCard from "@formbricks/ui/SingleResponseCard";
|
||||
|
||||
interface ResponseTimelineProps {
|
||||
environment: TEnvironment;
|
||||
surveyId: string;
|
||||
responses: TResponse[];
|
||||
survey: TSurvey;
|
||||
profile: TProfile;
|
||||
environmentTags: TTag[];
|
||||
}
|
||||
|
||||
export default function ResponseTimeline({
|
||||
environment,
|
||||
surveyId,
|
||||
responses,
|
||||
survey,
|
||||
profile,
|
||||
environmentTags,
|
||||
}: ResponseTimelineProps) {
|
||||
const matchQandA = useMemo(() => {
|
||||
if (survey && responses) {
|
||||
// Create a mapping of question IDs to their headlines
|
||||
const questionIdToHeadline = {};
|
||||
survey.questions.forEach((question) => {
|
||||
questionIdToHeadline[question.id] = question.headline;
|
||||
});
|
||||
|
||||
// Replace question IDs with question headlines in response data
|
||||
const updatedResponses = responses.map((response) => {
|
||||
const updatedResponse: Array<{
|
||||
id: string;
|
||||
question: string;
|
||||
answer: string;
|
||||
type: string;
|
||||
scale?: "number" | "star" | "smiley";
|
||||
range?: number;
|
||||
}> = []; // Specify the type of updatedData
|
||||
// iterate over survey questions and build the updated response
|
||||
for (const question of survey.questions) {
|
||||
const answer = response.data[question.id];
|
||||
if (answer !== null && answer !== undefined) {
|
||||
updatedResponse.push({
|
||||
id: createId(),
|
||||
question: question.headline,
|
||||
type: question.type,
|
||||
scale: question.scale,
|
||||
range: question.range,
|
||||
answer: answer as string,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { ...response, responses: updatedResponse };
|
||||
});
|
||||
|
||||
const updatedResponsesWithTags = updatedResponses.map((response) => ({
|
||||
...response,
|
||||
tags: response.tags?.map((tag) => tag),
|
||||
}));
|
||||
|
||||
return updatedResponsesWithTags;
|
||||
}
|
||||
return [];
|
||||
}, [survey, responses]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{survey.type === "web" && !environment.widgetSetupCompleted ? (
|
||||
@@ -81,15 +36,18 @@ export default function ResponseTimeline({
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
{matchQandA.map((updatedResponse) => {
|
||||
{responses.map((response) => {
|
||||
return (
|
||||
<SingleResponse
|
||||
key={updatedResponse.id}
|
||||
data={updatedResponse}
|
||||
surveyId={surveyId}
|
||||
environmentId={environment.id}
|
||||
environmentTags={environmentTags}
|
||||
/>
|
||||
<div key={response.id}>
|
||||
<SingleResponseCard
|
||||
survey={survey}
|
||||
response={response}
|
||||
profile={profile}
|
||||
environmentTags={environmentTags}
|
||||
pageType="response"
|
||||
environment={environment}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RatingResponse } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/RatingResponse";
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import { truncate } from "@/lib/utils";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { QuestionType } from "@formbricks/types/questions";
|
||||
import { TResponse } from "@formbricks/types/v1/responses";
|
||||
import { PersonAvatar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui";
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ReactNode, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import ResponseNote from "./ResponseNote";
|
||||
import ResponseTagsWrapper from "./ResponseTagsWrapper";
|
||||
import { deleteResponseAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
|
||||
export interface OpenTextSummaryProps {
|
||||
environmentId: string;
|
||||
surveyId: string;
|
||||
data: TResponse & {
|
||||
responses: {
|
||||
id: string;
|
||||
question: string;
|
||||
answer: string | any[];
|
||||
type: string;
|
||||
scale?: "number" | "star" | "smiley";
|
||||
range?: number;
|
||||
}[];
|
||||
};
|
||||
environmentTags: TTag[];
|
||||
}
|
||||
|
||||
function findEmail(person) {
|
||||
return person.attributes?.email || null;
|
||||
}
|
||||
|
||||
interface TooltipRendererProps {
|
||||
shouldRender: boolean;
|
||||
tooltipContent: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function TooltipRenderer(props: TooltipRendererProps) {
|
||||
const { children, shouldRender, tooltipContent } = props;
|
||||
if (shouldRender) {
|
||||
return (
|
||||
<TooltipProvider delayDuration={50}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>{children}</TooltipTrigger>
|
||||
<TooltipContent>{tooltipContent}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export default function SingleResponse({
|
||||
data,
|
||||
environmentId,
|
||||
surveyId,
|
||||
environmentTags,
|
||||
}: OpenTextSummaryProps) {
|
||||
const router = useRouter();
|
||||
const email = data.person && findEmail(data.person);
|
||||
const displayIdentifier = email || (data.person && truncate(data.person.id, 16)) || null;
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleDeleteSubmission = async () => {
|
||||
setIsDeleting(true);
|
||||
const deleteResponseStatus = await deleteResponseAction(data?.id);
|
||||
router.refresh();
|
||||
if (deleteResponseStatus) toast.success("Submission deleted successfully.");
|
||||
setDeleteDialogOpen(false);
|
||||
setIsDeleting(false);
|
||||
};
|
||||
|
||||
const renderTooltip = Boolean(
|
||||
(data.personAttributes && Object.keys(data.personAttributes).length > 0) ||
|
||||
(data.meta?.userAgent && Object.keys(data.meta.userAgent).length > 0)
|
||||
);
|
||||
|
||||
const tooltipContent = (
|
||||
<>
|
||||
{data.personAttributes && Object.keys(data.personAttributes).length > 0 && (
|
||||
<div>
|
||||
<p className="py-1 font-bold text-slate-700">Person attributes:</p>
|
||||
{Object.keys(data.personAttributes).map((key) => (
|
||||
<p key={key}>
|
||||
{key}: <span className="font-bold">{data.personAttributes && data.personAttributes[key]}</span>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.meta?.userAgent && Object.keys(data.meta.userAgent).length > 0 && (
|
||||
<div className="text-slate-600">
|
||||
{data.personAttributes && Object.keys(data.personAttributes).length > 0 && (
|
||||
<hr className="my-2 border-slate-200" />
|
||||
)}
|
||||
<p className="py-1 font-bold text-slate-700">Device info:</p>
|
||||
{data.meta?.userAgent?.browser && <p>Browser: {data.meta.userAgent.browser}</p>}
|
||||
{data.meta?.userAgent?.os && <p>OS: {data.meta.userAgent.os}</p>}
|
||||
{data.meta?.userAgent && (
|
||||
<p>Device: {data.meta.userAgent.device ? data.meta.userAgent.device : "PC / Generic device"}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={clsx("group relative", isOpen && "min-h-[300px]")}>
|
||||
<div
|
||||
className={clsx(
|
||||
"relative z-10 my-6 rounded-lg border border-slate-200 bg-slate-50 shadow-sm transition-all",
|
||||
isOpen ? "w-3/4" : data.notes.length ? "w-[96.5%]" : "w-full group-hover:w-[96.5%]"
|
||||
)}>
|
||||
<div className="space-y-2 px-6 pb-5 pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
{data.person?.id ? (
|
||||
<Link
|
||||
className="group flex items-center"
|
||||
href={`/environments/${environmentId}/people/${data.person.id}`}>
|
||||
<TooltipRenderer shouldRender={renderTooltip} tooltipContent={tooltipContent}>
|
||||
<PersonAvatar personId={data.person.id} />
|
||||
</TooltipRenderer>
|
||||
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600 hover:underline">
|
||||
{displayIdentifier}
|
||||
</h3>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<TooltipRenderer shouldRender={renderTooltip} tooltipContent={tooltipContent}>
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</TooltipRenderer>
|
||||
<h3 className="ml-4 pb-1 font-semibold text-slate-600">Anonymous</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex space-x-4 text-sm">
|
||||
{data.singleUseId && (
|
||||
<span className="flex items-center rounded-full bg-slate-100 px-3 text-slate-600">
|
||||
{data.singleUseId}
|
||||
</span>
|
||||
)}
|
||||
{data.finished && (
|
||||
<span className="flex items-center rounded-full bg-slate-100 px-3 text-slate-600">
|
||||
Completed <CheckCircleIcon className="ml-1 h-5 w-5 text-green-400" />
|
||||
</span>
|
||||
)}
|
||||
<time className="text-slate-500" dateTime={timeSince(data.updatedAt.toISOString())}>
|
||||
{timeSince(data.updatedAt.toISOString())}
|
||||
</time>
|
||||
<button
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
}}>
|
||||
<TrashIcon className="h-4 w-4 text-slate-500 hover:text-red-700" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6 rounded-b-lg bg-white p-6">
|
||||
{data.responses.map((response, idx) => (
|
||||
<div key={`${response.id}-${idx}`}>
|
||||
<p className="text-sm text-slate-500">{response.question}</p>
|
||||
{typeof response.answer !== "object" ? (
|
||||
response.type === QuestionType.Rating ? (
|
||||
<div className="h-8">
|
||||
<RatingResponse scale={response.scale} answer={response.answer} range={response.range} />
|
||||
</div>
|
||||
) : (
|
||||
<p className="ph-no-capture my-1 whitespace-pre-wrap font-semibold text-slate-700">
|
||||
{response.answer}
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
<p className="ph-no-capture my-1 font-semibold text-slate-700">
|
||||
{response.answer.join(", ")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ResponseTagsWrapper
|
||||
environmentId={environmentId}
|
||||
responseId={data.id}
|
||||
tags={data.tags.map((tag) => ({ tagId: tag.id, tagName: tag.name }))}
|
||||
key={data.tags.map((tag) => tag.id).join("-")}
|
||||
environmentTags={environmentTags}
|
||||
/>
|
||||
|
||||
<DeleteDialog
|
||||
open={deleteDialogOpen}
|
||||
setOpen={setDeleteDialogOpen}
|
||||
deleteWhat="response"
|
||||
onDelete={handleDeleteSubmission}
|
||||
isDeleting={isDeleting}
|
||||
/>
|
||||
</div>
|
||||
<ResponseNote
|
||||
responseId={data.id}
|
||||
notes={data.notes}
|
||||
environmentId={environmentId}
|
||||
surveyId={surveyId}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import ResponsePage from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
|
||||
import { getAnalysisData } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { sendEmbedSurveyPreviewEmail } from "@formbricks/lib/emails/emails";
|
||||
import { AuthenticationError } from "@formbricks/types/v1/errors";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PersonAvatar, ProgressBar } from "@formbricks/ui";
|
||||
import { InboxStackIcon } from "@heroicons/react/24/solid";
|
||||
import { useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { truncate } from "@/lib/utils";
|
||||
import { getPersonIdentifier } from "@formbricks/lib/people/helpers";
|
||||
import {
|
||||
TSurveyMultipleChoiceMultiQuestion,
|
||||
TSurveyMultipleChoiceSingleQuestion,
|
||||
@@ -57,20 +57,15 @@ export default function MultipleChoiceSummary({
|
||||
};
|
||||
}
|
||||
|
||||
function findEmail(person) {
|
||||
return person.attributes?.email || null;
|
||||
}
|
||||
|
||||
const addOtherChoice = (response, value) => {
|
||||
for (const key in resultsDict) {
|
||||
if (resultsDict[key].id === "other" && value !== "") {
|
||||
const email = response.person && findEmail(response.person);
|
||||
const displayIdentifier = email || truncate(response.personId, 16);
|
||||
const displayIdentifier = getPersonIdentifier(response.person);
|
||||
resultsDict[key].otherValues?.push({
|
||||
value,
|
||||
person: {
|
||||
id: response.personId,
|
||||
email: displayIdentifier,
|
||||
email: typeof displayIdentifier === "string" ? displayIdentifier : undefined,
|
||||
},
|
||||
});
|
||||
resultsDict[key].count += 1;
|
||||
@@ -196,7 +191,7 @@ export default function MultipleChoiceSummary({
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-1 flex items-center space-x-4 pl-6 font-medium text-slate-900">
|
||||
{otherValue.person.id && <PersonAvatar personId={otherValue.person.id} />}
|
||||
<span>{otherValue.person.email}</span>
|
||||
<span>{getPersonIdentifier(otherValue.person)}</span>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getPersonIdentifier } from "@formbricks/lib/people/helpers";
|
||||
import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/Headline";
|
||||
import { truncate } from "@/lib/utils";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import type { QuestionSummary } from "@formbricks/types/responses";
|
||||
import { TSurveyOpenTextQuestion } from "@formbricks/types/v1/surveys";
|
||||
@@ -13,10 +13,6 @@ interface OpenTextSummaryProps {
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
function findEmail(person) {
|
||||
return person.attributes?.email || null;
|
||||
}
|
||||
|
||||
export default function OpenTextSummary({ questionSummary, environmentId }: OpenTextSummaryProps) {
|
||||
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
|
||||
|
||||
@@ -43,8 +39,7 @@ export default function OpenTextSummary({ questionSummary, environmentId }: Open
|
||||
<div className="px-4 md:px-6">Time</div>
|
||||
</div>
|
||||
{questionSummary.responses.map((response) => {
|
||||
const email = response.person && findEmail(response.person);
|
||||
const displayIdentifier = email || (response.person && truncate(response.person.id, 16)) || null;
|
||||
const displayIdentifier = getPersonIdentifier(response.person!);
|
||||
return (
|
||||
<div
|
||||
key={response.id}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { InboxStackIcon } from "@heroicons/react/24/solid";
|
||||
import { useMemo } from "react";
|
||||
import { QuestionType } from "@formbricks/types/questions";
|
||||
import { TSurveyRatingQuestion } from "@formbricks/types/v1/surveys";
|
||||
import { RatingResponse } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/RatingResponse";
|
||||
import { RatingResponse } from "@formbricks/ui";
|
||||
import { questionTypes } from "@/lib/questions";
|
||||
import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/Headline";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ export const revalidate = REVALIDATION_INTERVAL;
|
||||
import ResponsesLimitReachedBanner from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/ResponsesLimitReachedBanner";
|
||||
import { getAnalysisData } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data";
|
||||
import SummaryPage from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { REVALIDATION_INTERVAL, SURVEY_BASE_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@formbricks/ui";
|
||||
import { PencilSquareIcon, EllipsisHorizontalIcon } from "@heroicons/react/24/solid";
|
||||
import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator";
|
||||
import { SurveyStatusIndicator } from "@formbricks/ui";
|
||||
import toast from "react-hot-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import SuccessMessage from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage";
|
||||
@@ -101,11 +101,9 @@ const SummaryHeader = ({
|
||||
disabled={isStatusChangeDisabled}
|
||||
style={isStatusChangeDisabled ? { pointerEvents: "none", opacity: 0.5 } : {}}>
|
||||
<div className="flex items-center">
|
||||
<SurveyStatusIndicator
|
||||
status={survey.status}
|
||||
environment={environment}
|
||||
type={survey.type}
|
||||
/>
|
||||
{(survey.type === "link" || environment.widgetSetupCompleted) && (
|
||||
<SurveyStatusIndicator status={survey.status} />
|
||||
)}
|
||||
<span className="ml-1 text-sm text-slate-700">
|
||||
{survey.status === "inProgress" && "In-progress"}
|
||||
{survey.status === "paused" && "Paused"}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import AlertDialog from "@/components/shared/AlertDialog";
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import { DeleteDialog } from "@formbricks/ui";
|
||||
import SurveyStatusDropdown from "@/components/shared/SurveyStatusDropdown";
|
||||
import { QuestionType } from "@formbricks/types/questions";
|
||||
import type { Survey } from "@formbricks/types/surveys";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { deleteSurvey, updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { createSurvey } from "@formbricks/lib/survey/service";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { createSurvey } from "@formbricks/lib/survey/service";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import FormbricksClient from "@/app/(app)/FormbricksClient";
|
||||
import { PHProvider, PostHogPageview } from "@/app/PostHogClient";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { updateProduct } from "@formbricks/lib/product/service";
|
||||
import { updateProfile } from "@formbricks/lib/profile/service";
|
||||
import { TProductUpdateInput } from "@formbricks/types/v1/product";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getFirstEnvironmentByUserId } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { sendInviteAcceptedEmail } from "@/lib/email";
|
||||
import { verifyInviteToken } from "@formbricks/lib/jwt";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasTeamAccess } from "@/lib/api/apiHelper";
|
||||
import { getEnvironments } from "@formbricks/lib/environment/service";
|
||||
import { getProduct } from "@formbricks/lib/product/service";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasTeamAccess } from "@/lib/api/apiHelper";
|
||||
import { getEnvironments } from "@formbricks/lib/environment/service";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "@formbricks/lib/constants";
|
||||
import { google } from "googleapis";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
const scopes = [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { AsyncParser } from "@json2csv/node";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { responses } from "@/lib/api/response";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { responses } from "@/lib/api/response";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ClientLogout from "@/app/ClientLogout";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getFirstEnvironmentByUserId } from "@formbricks/lib/environment/service";
|
||||
import type { Session } from "next-auth";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { Modal } from "@formbricks/ui";
|
||||
import { Button } from "@formbricks/ui";
|
||||
|
||||
interface AlertDialogProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { Modal } from "@formbricks/ui";
|
||||
import { Button } from "@formbricks/ui";
|
||||
|
||||
interface CustomDialogProps {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { Modal } from "@formbricks/ui";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface ModalWithTabsProps {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { updateSurveyAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||
import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator";
|
||||
import { SurveyStatusIndicator } from "@formbricks/ui";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import {
|
||||
@@ -35,7 +35,9 @@ export default function SurveyStatusDropdown({
|
||||
<>
|
||||
{survey.status === "draft" ? (
|
||||
<div className="flex items-center">
|
||||
<SurveyStatusIndicator status={survey.status} environment={environment} type={survey.type} />
|
||||
{(survey.type === "link" || environment.widgetSetupCompleted) && (
|
||||
<SurveyStatusIndicator status={survey.status} />
|
||||
)}
|
||||
{survey.status === "draft" && <p className="text-sm italic text-slate-600">Draft</p>}
|
||||
</div>
|
||||
) : (
|
||||
@@ -69,11 +71,9 @@ export default function SurveyStatusDropdown({
|
||||
<SelectTrigger className="w-[170px] bg-white py-6 md:w-[200px]">
|
||||
<SelectValue>
|
||||
<div className="flex items-center">
|
||||
<SurveyStatusIndicator
|
||||
status={survey.status}
|
||||
environment={environment}
|
||||
type={survey.type}
|
||||
/>
|
||||
{(survey.type === "link" || environment.widgetSetupCompleted) && (
|
||||
<SurveyStatusIndicator status={survey.status} />
|
||||
)}
|
||||
<span className="ml-2 text-sm text-slate-700">
|
||||
{survey.status === "inProgress" && "In-progress"}
|
||||
{survey.status === "paused" && "Paused"}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createTeamAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { Modal } from "@formbricks/ui";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { PlusCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { createHash } from "crypto";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
@@ -31,36 +31,6 @@ export const hasEnvironmentAccess = async (
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getPlan = async (req, res) => {
|
||||
if (req.headers["x-api-key"]) {
|
||||
const apiKey = req.headers["x-api-key"].toString();
|
||||
const apiKeyData = await prisma.apiKey.findUnique({
|
||||
where: {
|
||||
hashedKey: hashApiKey(apiKey),
|
||||
},
|
||||
select: {
|
||||
environment: {
|
||||
select: {
|
||||
product: {
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
plan: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return apiKeyData?.environment.product.team.plan || "free";
|
||||
} else {
|
||||
const user = await getSessionUser(req, res);
|
||||
return user && user.teams?.length > 0 ? user.teams[0].plan : "free";
|
||||
}
|
||||
};
|
||||
|
||||
export const hasApiEnvironmentAccess = async (apiKey, environmentId) => {
|
||||
// write function to check if the API Key has access to the environment
|
||||
const apiKeyData = await prisma.apiKey.findUnique({
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { getPlan, hasEnvironmentAccess } from "@/lib/api/apiHelper";
|
||||
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { RESPONSES_LIMIT_FREE } from "@formbricks/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
@@ -73,16 +71,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
});
|
||||
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
const plan = await getPlan(req, res);
|
||||
if (plan === "free" && responses.length > RESPONSES_LIMIT_FREE) {
|
||||
return res.json({
|
||||
count: responses.length,
|
||||
responses: responses.slice(responses.length - RESPONSES_LIMIT_FREE, responses.length), // get last 30 from array
|
||||
reachedLimit: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
return res.json({ count: responses.length, responses, reachedLimit: false });
|
||||
}
|
||||
|
||||
|
||||
366
packages/lib/authOptions.ts
Normal file
366
packages/lib/authOptions.ts
Normal file
@@ -0,0 +1,366 @@
|
||||
import { env } from "@/env.mjs";
|
||||
import { verifyPassword } from "@/lib/auth";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { EMAIL_VERIFICATION_DISABLED, INTERNAL_SECRET, WEBAPP_URL } from "./constants";
|
||||
import { verifyToken } from "./jwt";
|
||||
import { getProfileByEmail } from "./profile/service";
|
||||
import type { IdentityProvider } from "@prisma/client";
|
||||
import type { NextAuthOptions } from "next-auth";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import GitHubProvider from "next-auth/providers/github";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
id: "credentials",
|
||||
// The name to display on the sign in form (e.g. "Sign in with...")
|
||||
name: "Credentials",
|
||||
// The credentials is used to generate a suitable form on the sign in page.
|
||||
// You can specify whatever fields you are expecting to be submitted.
|
||||
// e.g. domain, username, password, 2FA token, etc.
|
||||
// You can pass any HTML attribute to the <input> tag through the object.
|
||||
credentials: {
|
||||
email: {
|
||||
label: "Email Address",
|
||||
type: "email",
|
||||
placeholder: "Your email address",
|
||||
},
|
||||
password: {
|
||||
label: "Password",
|
||||
type: "password",
|
||||
placeholder: "Your password",
|
||||
},
|
||||
},
|
||||
async authorize(credentials, _req) {
|
||||
let user;
|
||||
try {
|
||||
user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: credentials?.email,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw Error("Internal server error. Please try again later");
|
||||
}
|
||||
|
||||
if (!user || !credentials) {
|
||||
throw new Error("No user matches the provided credentials");
|
||||
}
|
||||
if (!user.password) {
|
||||
throw new Error("No user matches the provided credentials");
|
||||
}
|
||||
|
||||
const isValid = await verifyPassword(credentials.password, user.password);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error("No user matches the provided credentials");
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstname: user.firstname,
|
||||
lastname: user.firstname,
|
||||
emailVerified: user.emailVerified,
|
||||
};
|
||||
},
|
||||
}),
|
||||
CredentialsProvider({
|
||||
id: "token",
|
||||
// The name to display on the sign in form (e.g. "Sign in with...")
|
||||
name: "Token",
|
||||
// The credentials is used to generate a suitable form on the sign in page.
|
||||
// You can specify whatever fields you are expecting to be submitted.
|
||||
// e.g. domain, username, password, 2FA token, etc.
|
||||
// You can pass any HTML attribute to the <input> tag through the object.
|
||||
credentials: {
|
||||
token: {
|
||||
label: "Verification Token",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
async authorize(credentials, _req) {
|
||||
let user;
|
||||
try {
|
||||
if (!credentials?.token) {
|
||||
throw new Error("Token not found");
|
||||
}
|
||||
const { id } = await verifyToken(credentials?.token);
|
||||
user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw new Error("Either a user does not match the provided token or the token is invalid");
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new Error("Either a user does not match the provided token or the token is invalid");
|
||||
}
|
||||
|
||||
if (user.emailVerified) {
|
||||
throw new Error("Email already verified");
|
||||
}
|
||||
|
||||
user = await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: { emailVerified: new Date().toISOString() },
|
||||
});
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstname: user.firstname,
|
||||
lastname: user.firstname,
|
||||
emailVerified: user.emailVerified,
|
||||
};
|
||||
},
|
||||
}),
|
||||
GitHubProvider({
|
||||
clientId: env.GITHUB_ID || "",
|
||||
clientSecret: env.GITHUB_SECRET || "",
|
||||
}),
|
||||
GoogleProvider({
|
||||
clientId: env.GOOGLE_CLIENT_ID || "",
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET || "",
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token }) {
|
||||
const existingUser = await getProfileByEmail(token?.email!);
|
||||
|
||||
if (!existingUser) {
|
||||
return token;
|
||||
}
|
||||
|
||||
const additionalAttributs = {
|
||||
id: existingUser.id,
|
||||
createdAt: existingUser.createdAt,
|
||||
onboardingCompleted: existingUser.onboardingCompleted,
|
||||
name: existingUser.name,
|
||||
};
|
||||
|
||||
return {
|
||||
...token,
|
||||
...additionalAttributs,
|
||||
};
|
||||
},
|
||||
async session({ session, token }) {
|
||||
// @ts-ignore
|
||||
session.user.id = token?.id;
|
||||
// @ts-ignore
|
||||
session.user.createdAt = token?.createdAt ? new Date(token?.createdAt).toISOString() : undefined;
|
||||
// @ts-ignore
|
||||
session.user.onboardingCompleted = token?.onboardingCompleted;
|
||||
// @ts-ignore
|
||||
session.user.name = token.name || "";
|
||||
|
||||
return session;
|
||||
},
|
||||
async signIn({ user, account }: any) {
|
||||
if (account.provider === "credentials" || account.provider === "token") {
|
||||
if (!user.emailVerified && !EMAIL_VERIFICATION_DISABLED) {
|
||||
return `/auth/verification-requested?email=${encodeURIComponent(user.email)}`;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!user.email || !user.name || account.type !== "oauth") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (account.provider) {
|
||||
const provider = account.provider.toLowerCase() as IdentityProvider;
|
||||
// check if accounts for this provider / account Id already exists
|
||||
const existingUserWithAccount = await prisma.user.findFirst({
|
||||
include: {
|
||||
accounts: {
|
||||
where: {
|
||||
provider: account.provider,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
identityProvider: provider,
|
||||
identityProviderAccountId: account.providerAccountId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUserWithAccount) {
|
||||
// User with this provider found
|
||||
// check if email still the same
|
||||
if (existingUserWithAccount.email === user.email) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// user seemed to change his email within the provider
|
||||
// check if user with this email already exist
|
||||
// if not found just update user with new email address
|
||||
// if found throw an error (TODO find better solution)
|
||||
const otherUserWithEmail = await prisma.user.findFirst({
|
||||
where: { email: user.email },
|
||||
});
|
||||
|
||||
if (!otherUserWithEmail) {
|
||||
await prisma.user.update({
|
||||
where: { id: existingUserWithAccount.id },
|
||||
data: { email: user.email },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return "/auth/login?error=Looks%20like%20you%20updated%20your%20email%20somewhere%20else.%0AA%20user%20with%20this%20new%20email%20exists%20already.";
|
||||
}
|
||||
|
||||
// There is no existing account for this identity provider / account id
|
||||
// check if user account with this email already exists
|
||||
// if user already exists throw error and request password login
|
||||
const existingUserWithEmail = await prisma.user.findFirst({
|
||||
where: { email: user.email },
|
||||
});
|
||||
|
||||
if (existingUserWithEmail) {
|
||||
return "/auth/login?error=A%20user%20with%20this%20email%20exists%20already.";
|
||||
}
|
||||
|
||||
const createdUser = await prisma.user.create({
|
||||
data: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
emailVerified: new Date(Date.now()),
|
||||
onboardingCompleted: false,
|
||||
identityProvider: provider,
|
||||
identityProviderAccountId: user.id as string,
|
||||
accounts: {
|
||||
create: [{ ...account }],
|
||||
},
|
||||
memberships: {
|
||||
create: [
|
||||
{
|
||||
accepted: true,
|
||||
role: "owner",
|
||||
team: {
|
||||
create: {
|
||||
name: `${user.name}'s Team`,
|
||||
products: {
|
||||
create: [
|
||||
{
|
||||
name: "My Product",
|
||||
environments: {
|
||||
create: [
|
||||
{
|
||||
type: "production",
|
||||
eventClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "New Session",
|
||||
description: "Gets fired when a new session is created",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "Exit Intent (Desktop)",
|
||||
description: "A user on Desktop leaves the website with the cursor.",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "50% Scroll",
|
||||
description: "A user scrolled 50% of the current page",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
},
|
||||
attributeClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "userId",
|
||||
description: "The internal ID of the person",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
description: "The email of the person",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "development",
|
||||
eventClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "New Session",
|
||||
description: "Gets fired when a new session is created",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "Exit Intent (Desktop)",
|
||||
description: "A user on Desktop leaves the website with the cursor.",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "50% Scroll",
|
||||
description: "A user scrolled 50% of the current page",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
},
|
||||
attributeClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "userId",
|
||||
description: "The internal ID of the person",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
description: "The email of the person",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
memberships: true,
|
||||
},
|
||||
});
|
||||
|
||||
const teamId = createdUser.memberships?.[0]?.teamId;
|
||||
if (teamId) {
|
||||
fetch(`${WEBAPP_URL}/api/v1/teams/${teamId}/add_demo_product`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-api-key": INTERNAL_SECRET,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: "/auth/login",
|
||||
signOut: "/auth/logout",
|
||||
error: "/auth/login", // Error code passed in query string as ?error=
|
||||
},
|
||||
};
|
||||
@@ -90,9 +90,9 @@ export const sendPasswordResetNotifyEmail = async (user: TEmailUser) => {
|
||||
|
||||
export const sendInviteMemberEmail = async (
|
||||
inviteId: string,
|
||||
inviterName: string,
|
||||
inviteeName: string,
|
||||
email: string
|
||||
email: string,
|
||||
inviterName: string | null,
|
||||
inviteeName: string | null
|
||||
) => {
|
||||
const token = createInviteToken(inviteId, email, {
|
||||
expiresIn: "7d",
|
||||
|
||||
@@ -118,8 +118,8 @@ export const inviteUser = async ({
|
||||
teamId,
|
||||
}: {
|
||||
teamId: string;
|
||||
invitee: { name: string; email: string; role: TMembershipRole };
|
||||
currentUser: { id: string; name: string };
|
||||
invitee: { name: string | null; email: string; role: TMembershipRole };
|
||||
currentUser: { id: string; name: string | null };
|
||||
}) => {
|
||||
const { name, email, role } = invitee;
|
||||
const { id: currentUserId, name: currentUserName } = currentUser;
|
||||
@@ -157,7 +157,7 @@ export const inviteUser = async ({
|
||||
},
|
||||
});
|
||||
|
||||
await sendInviteMemberEmail(invite.id, currentUserName, name, email);
|
||||
await sendInviteMemberEmail(invite.id, email, currentUserName, name);
|
||||
|
||||
return invite;
|
||||
};
|
||||
|
||||
5
packages/lib/people/helpers.ts
Normal file
5
packages/lib/people/helpers.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { TPerson } from "@formbricks/types/v1/people";
|
||||
|
||||
export const getPersonIdentifier = (person: TPerson): string | number | null => {
|
||||
return person?.attributes?.userId || person?.attributes?.email || person?.id || null;
|
||||
};
|
||||
@@ -21,6 +21,30 @@ const select = {
|
||||
},
|
||||
};
|
||||
|
||||
export const createResponseNote = async (
|
||||
responseId: string,
|
||||
userId: string,
|
||||
text: string
|
||||
): Promise<TResponseNote> => {
|
||||
try {
|
||||
const responseNote = await prisma.responseNote.create({
|
||||
data: {
|
||||
responseId: responseId,
|
||||
userId: userId,
|
||||
text: text,
|
||||
},
|
||||
select,
|
||||
});
|
||||
return responseNote;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateResponseNote = async (responseNoteId: string, text: string): Promise<TResponseNote> => {
|
||||
try {
|
||||
const updatedResponseNote = await prisma.responseNote.update({
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["../../apps/web/*"],
|
||||
"@prisma/client/*": ["@formbricks/database/client/*"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ module.exports = {
|
||||
"./components/**/*.{js,ts,jsx,tsx}",
|
||||
"./lib/**/*.{js,ts,jsx,tsx}",
|
||||
// include packages if not transpiling
|
||||
"../../packages/ui/components/**/*.{js,ts,jsx,tsx}",
|
||||
"../../packages/ui/**/*.{js,ts,jsx,tsx}",
|
||||
"!../../packages/ui/node_modules/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
|
||||
15
packages/types/next-auth.d.ts
vendored
15
packages/types/next-auth.d.ts
vendored
@@ -1,22 +1,11 @@
|
||||
import NextAuth from "next-auth";
|
||||
import { TProfile } from "./v1/profile";
|
||||
|
||||
declare module "next-auth" {
|
||||
/**
|
||||
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
||||
*/
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
teams: {
|
||||
id: string;
|
||||
plan: string;
|
||||
role: string;
|
||||
}[];
|
||||
email: string;
|
||||
name: string;
|
||||
onboardingCompleted: boolean;
|
||||
image?: StaticImageData;
|
||||
};
|
||||
user: TProfile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { Modal } from "../Modal";
|
||||
import Button from "../components/Button";
|
||||
|
||||
interface DeleteDialogProps {
|
||||
open: boolean;
|
||||
@@ -16,7 +16,7 @@ interface DeleteDialogProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function DeleteDialog({
|
||||
export function DeleteDialog({
|
||||
open,
|
||||
setOpen,
|
||||
deleteWhat,
|
||||
@@ -16,7 +16,7 @@ type Modal = {
|
||||
closeOnOutsideClick?: boolean;
|
||||
};
|
||||
|
||||
const Modal: React.FC<Modal> = ({
|
||||
export const Modal: React.FC<Modal> = ({
|
||||
open,
|
||||
setOpen,
|
||||
children,
|
||||
@@ -81,5 +81,3 @@ const Modal: React.FC<Modal> = ({
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
41
packages/ui/SingleResponseCard/actions.ts
Normal file
41
packages/ui/SingleResponseCard/actions.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { deleteResponse } from "@formbricks/lib/response/service";
|
||||
import { canUserAccessResponse } from "@formbricks/lib/response/auth";
|
||||
import {
|
||||
updateResponseNote,
|
||||
resolveResponseNote,
|
||||
createResponseNote,
|
||||
} from "@formbricks/lib/responseNote/service";
|
||||
|
||||
export const deleteResponseAction = async (responseId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const isAuthorized = await canUserAccessResponse(session.user!.id, responseId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await deleteResponse(responseId);
|
||||
};
|
||||
|
||||
export const updateResponseNoteAction = async (responseNoteId: string, text: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
await updateResponseNote(responseNoteId, text);
|
||||
};
|
||||
|
||||
export const resolveResponseNoteAction = async (responseNoteId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
await resolveResponseNote(responseNoteId);
|
||||
};
|
||||
|
||||
export const createResponseNoteAction = async (responseId: string, userId: string, text: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const authotized = await canUserAccessResponse(session.user!.id, responseId);
|
||||
if (!authotized) throw new AuthorizationError("Not authorized");
|
||||
return await createResponseNote(responseId, userId, text);
|
||||
};
|
||||
78
packages/ui/SingleResponseCard/components/QuestionSkip.tsx
Normal file
78
packages/ui/SingleResponseCard/components/QuestionSkip.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../..";
|
||||
import { ChevronDoubleDownIcon, XCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { TSurveyQuestion } from "@formbricks/types/v1/surveys";
|
||||
|
||||
interface QuestionSkipProps {
|
||||
skippedQuestions: string[] | undefined;
|
||||
status: string;
|
||||
questions: TSurveyQuestion[];
|
||||
}
|
||||
|
||||
export default function QuestionSkip({ skippedQuestions, status, questions }: QuestionSkipProps) {
|
||||
return (
|
||||
<>
|
||||
{skippedQuestions && (
|
||||
<div className="flex w-full p-2 text-sm text-slate-400">
|
||||
{status === "skipped" && (
|
||||
<div className="flex">
|
||||
<div
|
||||
className="flex w-0.5 items-center justify-center"
|
||||
style={{
|
||||
background:
|
||||
"repeating-linear-gradient(to bottom, rgb(148 163 184), rgb(148 163 184) 8px, transparent 5px, transparent 15px)", // adjust the values to fit your design
|
||||
}}>
|
||||
{skippedQuestions.length > 1 && (
|
||||
<TooltipProvider delayDuration={50}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<ChevronDoubleDownIcon className="w-[1.25rem] min-w-[1.25rem] rounded-full bg-slate-400 p-0.5 text-white" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Respondent skipped these questions.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-6 flex flex-col">
|
||||
{skippedQuestions &&
|
||||
skippedQuestions.map((questionId) => {
|
||||
return (
|
||||
<p className="my-2" key={questionId}>
|
||||
{questions.find((question) => question.id === questionId)!.headline}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{status === "aborted" && (
|
||||
<div className="flex">
|
||||
<div
|
||||
className="flex w-0.5 flex-grow items-start justify-center"
|
||||
style={{
|
||||
background:
|
||||
"repeating-linear-gradient(to bottom, rgb(148 163 184), rgb(148 163 184) 2px, transparent 2px, transparent 10px)", // adjust the 2px to change dot size and 10px to change space between dots
|
||||
}}>
|
||||
<div className="flex">
|
||||
<XCircleIcon className="min-h-[1.5rem] min-w-[1.5rem] rounded-full bg-white text-slate-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2 ml-4 flex flex-col">
|
||||
<p className="mb-2 w-fit rounded-lg bg-slate-100 px-2 text-slate-700">Survey Closed</p>
|
||||
{skippedQuestions &&
|
||||
skippedQuestions.map((questionId) => {
|
||||
return (
|
||||
<p className="my-2" key={questionId}>
|
||||
{questions.find((question) => question.id === questionId)!.headline}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -9,14 +9,14 @@ import {
|
||||
SmilingFaceWithSmilingEyes,
|
||||
TiredFace,
|
||||
WearyFace,
|
||||
} from "@/components/Smileys";
|
||||
} from "./Smileys";
|
||||
|
||||
import { StarIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
interface RatingResponseProps {
|
||||
scale?: "number" | "star" | "smiley";
|
||||
range?: number;
|
||||
answer: string;
|
||||
answer: string | number | string[];
|
||||
}
|
||||
|
||||
export const RatingResponse: React.FC<RatingResponseProps> = ({ scale, range, answer }) => {
|
||||
@@ -27,7 +27,7 @@ export const RatingResponse: React.FC<RatingResponseProps> = ({ scale, range, an
|
||||
// show number of stars according to answer value
|
||||
const stars: any = [];
|
||||
for (let i = 0; i < range; i++) {
|
||||
if (i < parseInt(answer)) {
|
||||
if (i < parseInt(answer.toString())) {
|
||||
stars.push(<StarIcon key={i} className="h-7 text-yellow-400" />);
|
||||
} else {
|
||||
stars.push(<StarIcon key={i} className="h-7 text-gray-300" />);
|
||||
@@ -1,41 +1,28 @@
|
||||
"use client";
|
||||
|
||||
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 { TProfile } from "@formbricks/types/v1/profile";
|
||||
import { TResponseNote } from "@formbricks/types/v1/responses";
|
||||
import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui";
|
||||
import { CheckIcon, PencilIcon, PlusIcon } from "@heroicons/react/24/solid";
|
||||
import clsx from "clsx";
|
||||
import { Maximize2Icon, Minimize2Icon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../..";
|
||||
import { resolveResponseNoteAction, updateResponseNoteAction, createResponseNoteAction } from "../actions";
|
||||
|
||||
interface ResponseNotesProps {
|
||||
profile: TProfile;
|
||||
responseId: string;
|
||||
notes: TResponseNote[];
|
||||
environmentId: string;
|
||||
surveyId: string;
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export default function ResponseNotes({
|
||||
responseId,
|
||||
notes,
|
||||
environmentId,
|
||||
surveyId,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}: ResponseNotesProps) {
|
||||
export default function ResponseNotes({ profile, responseId, notes, isOpen, setIsOpen }: ResponseNotesProps) {
|
||||
const router = useRouter();
|
||||
const { profile } = useProfile();
|
||||
const [noteText, setNoteText] = useState("");
|
||||
const [isCreatingNote, setIsCreatingNote] = useState(false);
|
||||
const [isUpdatingNote, setIsUpdatingNote] = useState(false);
|
||||
@@ -47,7 +34,7 @@ export default function ResponseNotes({
|
||||
e.preventDefault();
|
||||
setIsCreatingNote(true);
|
||||
try {
|
||||
await addResponseNote(environmentId, surveyId, responseId, noteText);
|
||||
await createResponseNoteAction(responseId, profile.id, noteText);
|
||||
router.refresh();
|
||||
setIsCreatingNote(false);
|
||||
setNoteText("");
|
||||
@@ -3,10 +3,10 @@
|
||||
import TagsCombobox from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/TagsCombobox";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { Tag } from "./Tag";
|
||||
import { Tag } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/Tag";
|
||||
import { ExclamationCircleIcon, Cog6ToothIcon } from "@heroicons/react/24/solid";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { Button } from "../..";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
import {
|
||||
createTagToResponeAction,
|
||||
344
packages/ui/SingleResponseCard/index.tsx
Normal file
344
packages/ui/SingleResponseCard/index.tsx
Normal file
@@ -0,0 +1,344 @@
|
||||
"use client";
|
||||
|
||||
import { RatingResponse } from "./components/RatingResponse";
|
||||
import ResponseNotes from "./components/ResponseNote";
|
||||
import ResponseTagsWrapper from "./components/ResponseTagsWrapper";
|
||||
import { deleteResponseAction } from "./actions";
|
||||
import { DeleteDialog } from "../DeleteDialog";
|
||||
import QuestionSkip from "./components/QuestionSkip";
|
||||
import { SurveyStatusIndicator } from "../SurveyStatusIndicator";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { QuestionType } from "@formbricks/types/questions";
|
||||
import { TResponse } from "@formbricks/types/v1/responses";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { PersonAvatar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "..";
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ReactNode, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getPersonIdentifier } from "@formbricks/lib/people/helpers";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
|
||||
export interface SingleResponseCardProps {
|
||||
survey: TSurvey;
|
||||
response: TResponse;
|
||||
profile: TProfile;
|
||||
pageType: string;
|
||||
environmentTags: TTag[];
|
||||
environment: TEnvironment;
|
||||
}
|
||||
|
||||
interface TooltipRendererProps {
|
||||
shouldRender: boolean;
|
||||
tooltipContent: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function TooltipRenderer(props: TooltipRendererProps) {
|
||||
const { children, shouldRender, tooltipContent } = props;
|
||||
if (shouldRender) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>{children}</TooltipTrigger>
|
||||
<TooltipContent>{tooltipContent}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export default function SingleResponseCard({
|
||||
survey,
|
||||
response,
|
||||
profile,
|
||||
pageType,
|
||||
environmentTags,
|
||||
environment,
|
||||
}: SingleResponseCardProps) {
|
||||
const environmentId = survey.environmentId;
|
||||
const router = useRouter();
|
||||
const displayIdentifier = response.person ? getPersonIdentifier(response.person) : null;
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const isSubmissionFresh = isSubmissionTimeLessThan5Minutes(response.updatedAt);
|
||||
let skippedQuestions: string[][] = [];
|
||||
let temp: string[] = [];
|
||||
|
||||
function isValidValue(value: any) {
|
||||
return (
|
||||
(typeof value === "string" && value.trim() !== "") ||
|
||||
(Array.isArray(value) && value.length > 0) ||
|
||||
typeof value === "number"
|
||||
);
|
||||
}
|
||||
|
||||
if (response.finished) {
|
||||
survey.questions.forEach((question) => {
|
||||
if (!response.data[question.id]) {
|
||||
temp.push(question.id);
|
||||
} else {
|
||||
if (temp.length > 0) {
|
||||
skippedQuestions.push([...temp]);
|
||||
temp = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
for (let index = survey.questions.length - 1; index >= 0; index--) {
|
||||
const question = survey.questions[index];
|
||||
if (!response.data[question.id]) {
|
||||
if (skippedQuestions.length === 0) {
|
||||
temp.push(question.id);
|
||||
} else if (skippedQuestions.length > 0 && !isValidValue(response.data[question.id])) {
|
||||
temp.push(question.id);
|
||||
}
|
||||
} else {
|
||||
if (temp.length > 0) {
|
||||
temp.reverse();
|
||||
skippedQuestions.push([...temp]);
|
||||
temp = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle the case where the last entries are empty
|
||||
if (temp.length > 0) {
|
||||
skippedQuestions.push(temp);
|
||||
}
|
||||
|
||||
function handleArray(data: string | number | string[]): string {
|
||||
if (Array.isArray(data)) {
|
||||
return data.join(", ");
|
||||
} else {
|
||||
return String(data);
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteSubmission = async () => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await deleteResponseAction(response.id);
|
||||
router.refresh();
|
||||
toast.success("Submission deleted successfully.");
|
||||
setDeleteDialogOpen(false);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) toast.error(error.message);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderTooltip = Boolean(
|
||||
(response.personAttributes && Object.keys(response.personAttributes).length > 0) ||
|
||||
(response.meta?.userAgent && Object.keys(response.meta.userAgent).length > 0)
|
||||
);
|
||||
|
||||
function isSubmissionTimeLessThan5Minutes(submissionTimeISOString: Date) {
|
||||
const submissionTime: Date = new Date(submissionTimeISOString);
|
||||
const currentTime: Date = new Date();
|
||||
const timeDifference: number = (currentTime.getTime() - submissionTime.getTime()) / (1000 * 60); // Convert milliseconds to minutes
|
||||
return timeDifference < 5;
|
||||
}
|
||||
|
||||
const tooltipContent = (
|
||||
<>
|
||||
{response.singleUseId && (
|
||||
<div>
|
||||
<p className="py-1 font-bold text-slate-700">SingleUse ID:</p>
|
||||
<span>{response.singleUseId}</span>
|
||||
</div>
|
||||
)}
|
||||
{response.personAttributes && Object.keys(response.personAttributes).length > 0 && (
|
||||
<div>
|
||||
<p className="py-1 font-bold text-slate-700">Person attributes:</p>
|
||||
{Object.keys(response.personAttributes).map((key) => (
|
||||
<p key={key}>
|
||||
{key}:{" "}
|
||||
<span className="font-bold">{response.personAttributes && response.personAttributes[key]}</span>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{response.meta?.userAgent && Object.keys(response.meta.userAgent).length > 0 && (
|
||||
<div className="text-slate-600">
|
||||
{response.personAttributes && Object.keys(response.personAttributes).length > 0 && (
|
||||
<hr className="my-2 border-slate-200" />
|
||||
)}
|
||||
<p className="py-1 font-bold text-slate-700">Device info:</p>
|
||||
{response.meta?.userAgent?.browser && <p>Browser: {response.meta.userAgent.browser}</p>}
|
||||
{response.meta?.userAgent?.os && <p>OS: {response.meta.userAgent.os}</p>}
|
||||
{response.meta?.userAgent && (
|
||||
<p>
|
||||
Device:{" "}
|
||||
{response.meta.userAgent.device ? response.meta.userAgent.device : "PC / Generic device"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
const deleteSubmissionToolTip = <>This submission is in progress.</>;
|
||||
|
||||
return (
|
||||
<div className={clsx("group relative", isOpen && "min-h-[300px]")}>
|
||||
<div
|
||||
className={clsx(
|
||||
"relative z-10 my-6 rounded-lg border border-slate-200 bg-slate-50 shadow-sm transition-all",
|
||||
pageType === "response" &&
|
||||
(isOpen ? "w-3/4" : response.notes.length ? "w-[96.5%]" : "w-full group-hover:w-[96.5%]")
|
||||
)}>
|
||||
<div className="space-y-2 px-6 pb-5 pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
{pageType === "response" && (
|
||||
<div>
|
||||
{response.person?.id ? (
|
||||
<Link
|
||||
className="group flex items-center"
|
||||
href={`/environments/${environmentId}/people/${response.person.id}`}>
|
||||
<TooltipRenderer shouldRender={renderTooltip} tooltipContent={tooltipContent}>
|
||||
<PersonAvatar personId={response.person.id} />
|
||||
</TooltipRenderer>
|
||||
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600 hover:underline">
|
||||
{displayIdentifier}
|
||||
</h3>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<TooltipRenderer shouldRender={renderTooltip} tooltipContent={tooltipContent}>
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</TooltipRenderer>
|
||||
<h3 className="ml-4 pb-1 font-semibold text-slate-600">Anonymous</h3>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pageType === "people" && (
|
||||
<div className="flex items-center justify-center space-x-2 rounded-full bg-slate-100 p-1 px-2 text-sm text-slate-600">
|
||||
{(survey.type === "link" || environment.widgetSetupCompleted) && (
|
||||
<SurveyStatusIndicator status={survey.status} />
|
||||
)}
|
||||
<Link
|
||||
className="hover:underline"
|
||||
href={`/environments/${environmentId}/surveys/${survey.id}/summary`}>
|
||||
{survey.name}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex space-x-4 text-sm">
|
||||
<time className="text-slate-500" dateTime={timeSince(response.updatedAt.toISOString())}>
|
||||
{timeSince(response.updatedAt.toISOString())}
|
||||
</time>
|
||||
<TooltipRenderer shouldRender={isSubmissionFresh} tooltipContent={deleteSubmissionToolTip}>
|
||||
<TrashIcon
|
||||
onClick={() => {
|
||||
if (!isSubmissionFresh) {
|
||||
setDeleteDialogOpen(true);
|
||||
}
|
||||
}}
|
||||
className={`h-4 w-4 ${
|
||||
isSubmissionFresh
|
||||
? "cursor-not-allowed text-gray-400"
|
||||
: "text-slate-500 hover:text-red-700"
|
||||
} `}
|
||||
/>
|
||||
</TooltipRenderer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6 rounded-b-lg bg-white p-6">
|
||||
{survey.questions.map((question) => {
|
||||
const skipped = skippedQuestions.find((skippedQuestionElement) =>
|
||||
skippedQuestionElement.includes(question.id)
|
||||
);
|
||||
|
||||
// If found, remove it from the list
|
||||
if (skipped) {
|
||||
skippedQuestions = skippedQuestions.filter((item) => item !== skipped);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={`${question.id}`}>
|
||||
{isValidValue(response.data[question.id]) ? (
|
||||
<p className="text-sm text-slate-500">{question.headline}</p>
|
||||
) : (
|
||||
<QuestionSkip
|
||||
skippedQuestions={skipped}
|
||||
questions={survey.questions}
|
||||
status={
|
||||
response.finished ||
|
||||
(skippedQuestions.length > 0 &&
|
||||
!skippedQuestions[skippedQuestions.length - 1].includes(question.id))
|
||||
? "skipped"
|
||||
: "aborted"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{typeof response.data[question.id] !== "object" ? (
|
||||
question.type === QuestionType.Rating ? (
|
||||
<div>
|
||||
<RatingResponse
|
||||
scale={question.scale}
|
||||
answer={response.data[question.id]}
|
||||
range={question.range}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p className="ph-no-capture my-1 font-semibold text-slate-700">
|
||||
{response.data[question.id]}
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
<p className="ph-no-capture my-1 font-semibold text-slate-700">
|
||||
{handleArray(response.data[question.id])}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{response.finished && (
|
||||
<div className="flex">
|
||||
<CheckCircleIcon className="h-6 w-6 text-slate-400" />
|
||||
<p className="mx-2 rounded-lg bg-slate-100 px-2 text-slate-700">Completed</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ResponseTagsWrapper
|
||||
environmentId={environmentId}
|
||||
responseId={response.id}
|
||||
tags={response.tags.map((tag) => ({ tagId: tag.id, tagName: tag.name }))}
|
||||
environmentTags={environmentTags}
|
||||
/>
|
||||
|
||||
<DeleteDialog
|
||||
open={deleteDialogOpen}
|
||||
setOpen={setDeleteDialogOpen}
|
||||
deleteWhat="response"
|
||||
onDelete={handleDeleteSubmission}
|
||||
isDeleting={isDeleting}
|
||||
/>
|
||||
</div>
|
||||
{pageType === "response" && (
|
||||
<ResponseNotes
|
||||
profile={profile}
|
||||
responseId={response.id}
|
||||
notes={response.notes}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui";
|
||||
import { ArchiveBoxIcon, CheckIcon, PauseIcon, ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "..";
|
||||
import { ArchiveBoxIcon, CheckIcon, PauseIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
interface SurveyStatusIndicatorProps {
|
||||
status: string;
|
||||
tooltip?: boolean;
|
||||
environment: TEnvironment;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export default function SurveyStatusIndicator({
|
||||
status,
|
||||
tooltip,
|
||||
environment,
|
||||
type,
|
||||
}: SurveyStatusIndicatorProps) {
|
||||
if (!environment.widgetSetupCompleted) {
|
||||
if (type === "web") {
|
||||
return (
|
||||
<div>
|
||||
<ExclamationTriangleIcon className="h-4 w-4 text-amber-500" />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export function SurveyStatusIndicator({ status, tooltip }: SurveyStatusIndicatorProps) {
|
||||
if (tooltip) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
@@ -74,6 +74,10 @@ export { Switch } from "./components/Switch";
|
||||
export { TabBar } from "./components/TabBar";
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./components/Tooltip";
|
||||
export { AddVariablesDropdown, Editor } from "./components/editor";
|
||||
export { RatingResponse } from "./SingleResponseCard/components/RatingResponse";
|
||||
export { DeleteDialog } from "./DeleteDialog";
|
||||
export { Modal } from "./Modal";
|
||||
export { SurveyStatusIndicator } from "./SurveyStatusIndicator";
|
||||
export { QuestionTypeSelector } from "./components/QuestionTypeSelector";
|
||||
|
||||
/* Icons */
|
||||
|
||||
@@ -2,18 +2,14 @@
|
||||
"name": "@formbricks/ui",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"main": "./index.tsx",
|
||||
"exports": {
|
||||
".": "./index.tsx",
|
||||
"./components/icon": "./components/icon/index.ts"
|
||||
},
|
||||
"types": "./index.tsx",
|
||||
"scripts": {
|
||||
"clean": "rimraf .turbo node_modules dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"concurrently": "^8.2.1",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"postcss": "^8.4.31",
|
||||
@@ -49,6 +45,7 @@
|
||||
"react-confetti": "^6.1.0",
|
||||
"react-day-picker": "^8.8.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-radio-group": "^3.0.3",
|
||||
"react-use": "^17.4.0"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@formbricks/tsconfig/react-library.json",
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
"exclude": ["build", "node_modules"]
|
||||
}
|
||||
|
||||
82
pnpm-lock.yaml
generated
82
pnpm-lock.yaml
generated
@@ -28,7 +28,7 @@ importers:
|
||||
version: 3.13.0
|
||||
turbo:
|
||||
specifier: latest
|
||||
version: 1.10.12
|
||||
version: 1.10.13
|
||||
|
||||
apps/demo:
|
||||
dependencies:
|
||||
@@ -459,7 +459,7 @@ importers:
|
||||
version: 9.0.0(eslint@8.50.0)
|
||||
eslint-config-turbo:
|
||||
specifier: latest
|
||||
version: 1.10.12(eslint@8.50.0)
|
||||
version: 1.8.8(eslint@8.50.0)
|
||||
eslint-plugin-react:
|
||||
specifier: 7.33.2
|
||||
version: 7.33.2(eslint@8.50.0)
|
||||
@@ -792,6 +792,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
react-hot-toast:
|
||||
specifier: ^2.4.1
|
||||
version: 2.4.1(csstype@3.1.1)(react-dom@18.2.0)(react@18.2.0)
|
||||
react-radio-group:
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3(react-dom@18.2.0)(react@18.2.0)
|
||||
@@ -802,6 +805,9 @@ importers:
|
||||
'@formbricks/tsconfig':
|
||||
specifier: workspace:*
|
||||
version: link:../tsconfig
|
||||
'@formbricks/types':
|
||||
specifier: workspace:*
|
||||
version: link:../types
|
||||
concurrently:
|
||||
specifier: ^8.2.1
|
||||
version: 8.2.1
|
||||
@@ -10229,7 +10235,7 @@ packages:
|
||||
normalize-path: 3.0.0
|
||||
readdirp: 3.6.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
fsevents: 2.3.3
|
||||
|
||||
/chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
@@ -12512,13 +12518,13 @@ packages:
|
||||
resolution: {integrity: sha512-NB/L/1Y30qyJcG5xZxCJKW/+bqyj+llbcCwo9DEz8bESIP0SLTOQ8T1DWCCFc+wJ61AMEstj4511PSScqMMfCw==}
|
||||
dev: true
|
||||
|
||||
/eslint-config-turbo@1.10.12(eslint@8.50.0):
|
||||
resolution: {integrity: sha512-z3jfh+D7UGYlzMWGh+Kqz++hf8LOE96q3o5R8X4HTjmxaBWlLAWG+0Ounr38h+JLR2TJno0hU9zfzoPNkR9BdA==}
|
||||
/eslint-config-turbo@1.8.8(eslint@8.50.0):
|
||||
resolution: {integrity: sha512-+yT22sHOT5iC1sbBXfLIdXfbZuiv9bAyOXsxTxFCWelTeFFnANqmuKB3x274CFvf7WRuZ/vYP/VMjzU9xnFnxA==}
|
||||
peerDependencies:
|
||||
eslint: '>6.6.0'
|
||||
dependencies:
|
||||
eslint: 8.50.0
|
||||
eslint-plugin-turbo: 1.10.12(eslint@8.50.0)
|
||||
eslint-plugin-turbo: 1.8.8(eslint@8.50.0)
|
||||
dev: true
|
||||
|
||||
/eslint-import-resolver-node@0.3.9:
|
||||
@@ -12724,12 +12730,11 @@ packages:
|
||||
semver: 6.3.1
|
||||
string.prototype.matchall: 4.0.8
|
||||
|
||||
/eslint-plugin-turbo@1.10.12(eslint@8.50.0):
|
||||
resolution: {integrity: sha512-uNbdj+ohZaYo4tFJ6dStRXu2FZigwulR1b3URPXe0Q8YaE7thuekKNP+54CHtZPH9Zey9dmDx5btAQl9mfzGOw==}
|
||||
/eslint-plugin-turbo@1.8.8(eslint@8.50.0):
|
||||
resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==}
|
||||
peerDependencies:
|
||||
eslint: '>6.6.0'
|
||||
dependencies:
|
||||
dotenv: 16.0.3
|
||||
eslint: 8.50.0
|
||||
dev: true
|
||||
|
||||
@@ -13696,19 +13701,11 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
optional: true
|
||||
|
||||
/fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/fstream@1.0.12:
|
||||
@@ -15868,7 +15865,7 @@ packages:
|
||||
micromatch: 4.0.5
|
||||
walker: 1.0.8
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/jest-leak-detector@29.7.0:
|
||||
@@ -22204,7 +22201,7 @@ packages:
|
||||
engines: {node: '>=10.0.0'}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
fsevents: 2.3.3
|
||||
dev: false
|
||||
|
||||
/rollup@2.79.1:
|
||||
@@ -22212,7 +22209,7 @@ packages:
|
||||
engines: {node: '>=10.0.0'}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/rollup@3.29.4:
|
||||
@@ -22220,7 +22217,7 @@ packages:
|
||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/rtl-css-js@1.16.1:
|
||||
@@ -24216,65 +24213,64 @@ packages:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
/turbo-darwin-64@1.10.12:
|
||||
resolution: {integrity: sha512-vmDfGVPl5/aFenAbOj3eOx3ePNcWVUyZwYr7taRl0ZBbmv2TzjRiFotO4vrKCiTVnbqjQqAFQWY2ugbqCI1kOQ==}
|
||||
/turbo-darwin-64@1.10.13:
|
||||
resolution: {integrity: sha512-vmngGfa2dlYvX7UFVncsNDMuT4X2KPyPJ2Jj+xvf5nvQnZR/3IeDEGleGVuMi/hRzdinoxwXqgk9flEmAYp0Xw==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-darwin-arm64@1.10.12:
|
||||
resolution: {integrity: sha512-3JliEESLNX2s7g54SOBqqkqJ7UhcOGkS0ywMr5SNuvF6kWVTbuUq7uBU/sVbGq8RwvK1ONlhPvJne5MUqBCTCQ==}
|
||||
/turbo-darwin-arm64@1.10.13:
|
||||
resolution: {integrity: sha512-eMoJC+k7gIS4i2qL6rKmrIQGP6Wr9nN4odzzgHFngLTMimok2cGLK3qbJs5O5F/XAtEeRAmuxeRnzQwTl/iuAw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-linux-64@1.10.12:
|
||||
resolution: {integrity: sha512-siYhgeX0DidIfHSgCR95b8xPee9enKSOjCzx7EjTLmPqPaCiVebRYvbOIYdQWRqiaKh9yfhUtFmtMOMScUf1gg==}
|
||||
/turbo-linux-64@1.10.13:
|
||||
resolution: {integrity: sha512-0CyYmnKTs6kcx7+JRH3nPEqCnzWduM0hj8GP/aodhaIkLNSAGAa+RiYZz6C7IXN+xUVh5rrWTnU2f1SkIy7Gdg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-linux-arm64@1.10.12:
|
||||
resolution: {integrity: sha512-K/ZhvD9l4SslclaMkTiIrnfcACgos79YcAo4kwc8bnMQaKuUeRpM15sxLpZp3xDjDg8EY93vsKyjaOhdFG2UbA==}
|
||||
/turbo-linux-arm64@1.10.13:
|
||||
resolution: {integrity: sha512-0iBKviSGQQlh2OjZgBsGjkPXoxvRIxrrLLbLObwJo3sOjIH0loGmVIimGS5E323soMfi/o+sidjk2wU1kFfD7Q==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-windows-64@1.10.12:
|
||||
resolution: {integrity: sha512-7FSgSwvktWDNOqV65l9AbZwcoueAILeE4L7JvjauNASAjjbuzXGCEq5uN8AQU3U5BOFj4TdXrVmO2dX+lLu8Zg==}
|
||||
/turbo-windows-64@1.10.13:
|
||||
resolution: {integrity: sha512-S5XySRfW2AmnTeY1IT+Jdr6Goq7mxWganVFfrmqU+qqq3Om/nr0GkcUX+KTIo9mPrN0D3p5QViBRzulwB5iuUQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-windows-arm64@1.10.12:
|
||||
resolution: {integrity: sha512-gCNXF52dwom1HLY9ry/cneBPOKTBHhzpqhMylcyvJP0vp9zeMQQkt6yjYv+6QdnmELC92CtKNp2FsNZo+z0pyw==}
|
||||
/turbo-windows-arm64@1.10.13:
|
||||
resolution: {integrity: sha512-nKol6+CyiExJIuoIc3exUQPIBjP9nIq5SkMJgJuxsot2hkgGrafAg/izVDRDrRduQcXj2s8LdtxJHvvnbI8hEQ==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo@1.10.12:
|
||||
resolution: {integrity: sha512-WM3+jTfQWnB9W208pmP4oeehZcC6JQNlydb/ZHMRrhmQa+htGhWLCzd6Q9rLe0MwZLPpSPFV2/bN5egCLyoKjQ==}
|
||||
/turbo@1.10.13:
|
||||
resolution: {integrity: sha512-vOF5IPytgQPIsgGtT0n2uGZizR2N3kKuPIn4b5p5DdeLoI0BV7uNiydT7eSzdkPRpdXNnO8UwS658VaI4+YSzQ==}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
turbo-darwin-64: 1.10.12
|
||||
turbo-darwin-arm64: 1.10.12
|
||||
turbo-linux-64: 1.10.12
|
||||
turbo-linux-arm64: 1.10.12
|
||||
turbo-windows-64: 1.10.12
|
||||
turbo-windows-arm64: 1.10.12
|
||||
turbo-darwin-64: 1.10.13
|
||||
turbo-darwin-arm64: 1.10.13
|
||||
turbo-linux-64: 1.10.13
|
||||
turbo-linux-arm64: 1.10.13
|
||||
turbo-windows-64: 1.10.13
|
||||
turbo-windows-arm64: 1.10.13
|
||||
dev: true
|
||||
|
||||
/tw-to-css@0.0.11:
|
||||
@@ -25172,7 +25168,7 @@ packages:
|
||||
rollup: 3.29.4
|
||||
terser: 5.21.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/vm-browserify@1.1.2:
|
||||
|
||||
Reference in New Issue
Block a user