mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-05 02:58:36 -06:00
Merge branch 'main' of https://github.com/formbricks/formbricks into feat/form-styling
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
"dependencies": {
|
||||
"@formbricks/js": "workspace:*",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"next": "14.1.0",
|
||||
"next": "14.1.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
{
|
||||
name: "Argos",
|
||||
description: "Argos provides the developer tools to debug tests and detect visual regressions..",
|
||||
description: "Argos provides the developer tools to debug tests and detect visual regressions.",
|
||||
href: "https://argos-ci.com",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -19,25 +19,29 @@ export default function PosthogIdentify({
|
||||
userTargetingBillingStatus,
|
||||
}: {
|
||||
session: Session;
|
||||
environmentId: string;
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
inAppSurveyBillingStatus: TSubscriptionStatus;
|
||||
linkSurveyBillingStatus: TSubscriptionStatus;
|
||||
userTargetingBillingStatus: TSubscriptionStatus;
|
||||
environmentId?: string;
|
||||
teamId?: string;
|
||||
teamName?: string;
|
||||
inAppSurveyBillingStatus?: TSubscriptionStatus;
|
||||
linkSurveyBillingStatus?: TSubscriptionStatus;
|
||||
userTargetingBillingStatus?: TSubscriptionStatus;
|
||||
}) {
|
||||
const posthog = usePostHog();
|
||||
|
||||
useEffect(() => {
|
||||
if (posthogEnabled && session.user && posthog) {
|
||||
posthog.identify(session.user.id, { name: session.user.name, email: session.user.email });
|
||||
posthog.group("environment", environmentId, { name: environmentId });
|
||||
posthog.group("team", teamId, {
|
||||
name: teamName,
|
||||
inAppSurveyBillingStatus,
|
||||
linkSurveyBillingStatus,
|
||||
userTargetingBillingStatus,
|
||||
});
|
||||
if (environmentId) {
|
||||
posthog.group("environment", environmentId, { name: environmentId });
|
||||
}
|
||||
if (teamId) {
|
||||
posthog.group("team", teamId, {
|
||||
name: teamName,
|
||||
inAppSurveyBillingStatus,
|
||||
linkSurveyBillingStatus,
|
||||
userTargetingBillingStatus,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [
|
||||
posthog,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useEffect, useState } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationAirtable,
|
||||
@@ -333,7 +334,7 @@ export default function AddIntegrationModal(props: AddIntegrationModalProps) {
|
||||
<Label htmlFor="Surveys">Questions</Label>
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{selectedSurvey?.questions.map((question) => (
|
||||
{checkForRecallInHeadline(selectedSurvey)?.questions.map((question) => (
|
||||
<Controller
|
||||
key={question.id}
|
||||
control={control}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
@@ -273,7 +274,7 @@ export default function AddIntegrationModal({
|
||||
<Label htmlFor="Surveys">Questions</Label>
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{selectedSurvey?.questions.map((question) => (
|
||||
{checkForRecallInHeadline(selectedSurvey)?.questions.map((question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
|
||||
@@ -13,6 +13,7 @@ import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TIntegrationInput } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationNotion,
|
||||
@@ -105,12 +106,13 @@ export default function AddIntegrationModal({
|
||||
}, [selectedDatabase?.id]);
|
||||
|
||||
const questionItems = useMemo(() => {
|
||||
const questions =
|
||||
selectedSurvey?.questions.map((q) => ({
|
||||
id: q.id,
|
||||
name: q.headline,
|
||||
type: q.type,
|
||||
})) || [];
|
||||
const questions = selectedSurvey
|
||||
? checkForRecallInHeadline(selectedSurvey)?.questions.map((q) => ({
|
||||
id: q.id,
|
||||
name: q.headline,
|
||||
type: q.type,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const hiddenFields = selectedSurvey?.hiddenFields.enabled
|
||||
? selectedSurvey?.hiddenFields.fieldIds?.map((fId) => ({
|
||||
|
||||
@@ -53,7 +53,8 @@ export default function AddWebhookModal({ environmentId, surveys, open, setOpen
|
||||
return true;
|
||||
} catch (err) {
|
||||
setHittingEndpoint(false);
|
||||
toast.error("Oh no! We are unable to ping the webhook!");
|
||||
toast.error("Unable to ping the webhook! Please check browser console for logs");
|
||||
console.error("Webhook Test Failed due to: ", err.message);
|
||||
setEndpointAccessible(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,8 @@ export default function WebhookSettingsTab({
|
||||
return true;
|
||||
} catch (err) {
|
||||
setHittingEndpoint(false);
|
||||
toast.error("Oh no! We are unable to ping the webhook!");
|
||||
toast.error("Unable to ping the webhook! Please check browser console for logs");
|
||||
console.error("Webhook Test Failed due to: ", err.message);
|
||||
setEndpointAccessible(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function AddMemberModal({
|
||||
<div>
|
||||
<AddMemberRole control={control} canDoRoleManagement={canDoRoleManagement} />
|
||||
{!canDoRoleManagement &&
|
||||
(!isFormbricksCloud ? (
|
||||
(isFormbricksCloud ? (
|
||||
<UpgradePlanNotice
|
||||
message="To manage access roles,"
|
||||
url={`/environments/${environmentId}/settings/billing`}
|
||||
|
||||
@@ -111,7 +111,7 @@ export default function MemberActions({ team, member, invite, showDeleteButton }
|
||||
onClick={() => {
|
||||
handleShareInvite();
|
||||
}}
|
||||
id="shareInviteButton">
|
||||
className="shareInviteButton">
|
||||
<ShareIcon className="h-5 w-5 text-slate-700 hover:text-slate-500" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -94,7 +94,7 @@ export default function ShareEmbedSurvey({ survey, open, setOpen, webAppUrl, use
|
||||
<div className="flex max-w-full flex-col items-center justify-center space-x-2 lg:flex-row">
|
||||
<div
|
||||
ref={linkTextRef}
|
||||
className="mt-2 max-w-[70%] overflow-hidden rounded-lg border border-slate-300 bg-slate-50 px-3 py-2 text-slate-800"
|
||||
className="mt-2 max-w-[80%] overflow-hidden rounded-lg border border-slate-300 bg-slate-50 px-3 py-2 text-slate-800"
|
||||
style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}
|
||||
onClick={() => handleTextSelection()}>
|
||||
{surveyUrl}
|
||||
|
||||
@@ -314,12 +314,6 @@ export default function SurveyMenuBar({
|
||||
toast.success("Changes saved.");
|
||||
if (shouldNavigateBack) {
|
||||
router.back();
|
||||
} else {
|
||||
if (localSurvey.status !== "draft") {
|
||||
router.push(`/environments/${environment.id}/surveys/${localSurvey.id}/summary`);
|
||||
} else {
|
||||
router.push(`/environments/${environment.id}/surveys`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -216,7 +216,7 @@ export default function PreviewSurvey({
|
||||
/>
|
||||
</Modal>
|
||||
) : (
|
||||
<div className="w-full px-4">
|
||||
<div className="w-full px-3">
|
||||
<div className="no-scrollbar z-10 w-full max-w-md overflow-y-auto rounded-lg border border-transparent">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
|
||||
@@ -44,6 +44,7 @@ export default function PathwaySelect({
|
||||
/>
|
||||
<div className="flex space-x-8">
|
||||
<OptionCard
|
||||
cssId="onboarding-link-survey-card"
|
||||
size="lg"
|
||||
title="Link Surveys"
|
||||
description="Create a new survey and share a link."
|
||||
@@ -53,6 +54,7 @@ export default function PathwaySelect({
|
||||
<Image src={LinkMockup} alt="" height={350} />
|
||||
</OptionCard>
|
||||
<OptionCard
|
||||
cssId="onboarding-inapp-survey-card"
|
||||
size="lg"
|
||||
title="In-app Surveys"
|
||||
description="Run a survey on a website or in-app."
|
||||
|
||||
@@ -58,6 +58,7 @@ const ConnectedState = ({ goToProduct }) => {
|
||||
</div>
|
||||
<div className="mt-4 text-right">
|
||||
<Button
|
||||
id="onboarding-inapp-connect-connection-successful"
|
||||
variant="minimal"
|
||||
loading={isLoading}
|
||||
onClick={() => {
|
||||
@@ -91,6 +92,7 @@ const NotConnectedState = ({ environment, webAppUrl, jsPackageVersion, goToTeamI
|
||||
/>
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
id="onboarding-inapp-connect-not-sure-how-to-do-this"
|
||||
className="opacity-0 transition-all delay-[3000ms] duration-500 ease-in-out group-hover:opacity-100"
|
||||
variant="minimal"
|
||||
onClick={goToTeamInvitePage}>
|
||||
@@ -127,7 +129,7 @@ export function ConnectWithFormbricks({
|
||||
setLocalEnvironment(refetchedEnvironment);
|
||||
};
|
||||
fetchLatestEnvironmentOnFirstLoad();
|
||||
}, []);
|
||||
}, [environment.id]);
|
||||
|
||||
return localEnvironment.widgetSetupCompleted ? (
|
||||
<ConnectedState
|
||||
|
||||
@@ -105,16 +105,17 @@ export function InviteTeamMate({ team, environmentId, setCurrentStep }: InviteTe
|
||||
/>
|
||||
|
||||
<div className="flex w-full justify-between">
|
||||
<Button variant="minimal" onClick={() => goBackToConnectPage()}>
|
||||
<Button id="onboarding-inapp-invite-back" variant="minimal" onClick={() => goBackToConnectPage()}>
|
||||
Back
|
||||
</Button>
|
||||
<Button variant="darkCTA" onClick={handleInvite}>
|
||||
<Button id="onboarding-inapp-invite-send-invite" variant="darkCTA" onClick={handleInvite}>
|
||||
Invite
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-auto flex justify-center">
|
||||
<Button
|
||||
id="onboarding-inapp-invite-have-a-look-first"
|
||||
className="opacity-0 transition-all delay-[3000ms] duration-500 ease-in-out group-hover:opacity-100"
|
||||
variant="minimal"
|
||||
onClick={goToProduct}
|
||||
|
||||
@@ -73,6 +73,7 @@ if (typeof window !== "undefined") {
|
||||
});
|
||||
}`}</CodeBlock>
|
||||
<Button
|
||||
id="onboarding-inapp-connect-read-npm-docs"
|
||||
className="mt-3"
|
||||
variant="secondary"
|
||||
href="https://formbricks.com/docs/getting-started/framework-guides"
|
||||
@@ -90,6 +91,7 @@ if (typeof window !== "undefined") {
|
||||
</CodeBlock>
|
||||
<div className="mt-4 space-x-2">
|
||||
<Button
|
||||
id="onboarding-inapp-connect-copy-code"
|
||||
variant="darkCTA"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(htmlSnippet);
|
||||
@@ -98,6 +100,7 @@ if (typeof window !== "undefined") {
|
||||
Copy code
|
||||
</Button>
|
||||
<Button
|
||||
id="onboarding-inapp-connect-step-by-step-manual"
|
||||
variant="secondary"
|
||||
href="https://formbricks.com/docs/getting-started/framework-guides#html"
|
||||
target="_blank">
|
||||
|
||||
@@ -155,7 +155,7 @@ export const Objective: React.FC<ObjectiveProps> = ({ formbricksResponseId, user
|
||||
loading={isProfileUpdating}
|
||||
disabled={!selectedChoice}
|
||||
onClick={handleNextClick}
|
||||
id="objective-next">
|
||||
id="onboarding-inapp-objective-next">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -151,7 +151,7 @@ export const Role: React.FC<RoleProps> = ({ setFormbricksResponseId, session, se
|
||||
loading={isUpdating}
|
||||
disabled={!selectedChoice}
|
||||
onClick={handleNextClick}
|
||||
id="role-next">
|
||||
id="onboarding-inapp-role-next">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -59,8 +59,10 @@ export function CreateFirstSurvey({ environmentId }: CreateFirstSurveyProps) {
|
||||
<div className="grid w-11/12 max-w-6xl grid-cols-3 grid-rows-1 gap-6">
|
||||
{filteredTemplates.map((template) => {
|
||||
const TemplateImage = templateImages[template.name];
|
||||
const cssId = `onboarding-link-template-${template.name.toLowerCase().replace(/ /g, "-")}`;
|
||||
return (
|
||||
<OptionCard
|
||||
cssId={cssId} // Use the generated cssId here
|
||||
size="md"
|
||||
key={template.name}
|
||||
title={template.name}
|
||||
@@ -72,7 +74,9 @@ export function CreateFirstSurvey({ environmentId }: CreateFirstSurveyProps) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
id="onboarding-start-from-scratch"
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
loading={loadingTemplate === "Start from scratch"}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import jsPackageJson from "@/../../packages/js/package.json";
|
||||
import { finishOnboardingAction } from "@/app/(app)/onboarding/actions";
|
||||
import { ConnectWithFormbricks } from "@/app/(app)/onboarding/components/inapp/ConnectWithFormbricks";
|
||||
import { InviteTeamMate } from "@/app/(app)/onboarding/components/inapp/InviteTeamMate";
|
||||
import { Objective } from "@/app/(app)/onboarding/components/inapp/SurveyObjective";
|
||||
import { Role } from "@/app/(app)/onboarding/components/inapp/SurveyRole";
|
||||
import { CreateFirstSurvey } from "@/app/(app)/onboarding/components/link/CreateFirstSurvey";
|
||||
import { Session } from "next-auth";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
@@ -33,6 +35,7 @@ export function Onboarding({
|
||||
team,
|
||||
webAppUrl,
|
||||
}: OnboardingProps) {
|
||||
const router = useRouter();
|
||||
const [selectedPathway, setSelectedPathway] = useState<string | null>(null);
|
||||
const [progress, setProgress] = useState<number>(16);
|
||||
const [formbricksResponseId, setFormbricksResponseId] = useState<string | undefined>();
|
||||
@@ -142,6 +145,17 @@ export function Onboarding({
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center bg-slate-50">
|
||||
<div className="hidden">
|
||||
<button
|
||||
id="FB__INTERNAL__SKIP_ONBOARDING"
|
||||
onClick={async () => {
|
||||
await finishOnboardingAction();
|
||||
router.push(`/environments/${environment.id}/surveys`);
|
||||
}}>
|
||||
Skip onboarding
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<OnboardingHeader progress={progress} />
|
||||
<div className="mt-20 flex w-full justify-center bg-slate-50">
|
||||
{renderOnboardingStep()}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import PosthogIdentify from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
@@ -12,6 +13,7 @@ export default async function EnvironmentLayout({ children }) {
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-slate-50">
|
||||
<PosthogIdentify session={session} />
|
||||
<ToasterClient />
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -162,9 +162,13 @@ export default function LinkSurvey({
|
||||
getSetIsError={(f: (value: boolean) => void) => {
|
||||
setIsError = f;
|
||||
}}
|
||||
getSetIsResponseSendingFinished={(f: (value: boolean) => void) => {
|
||||
setIsResponseSendingFinished = f;
|
||||
}}
|
||||
getSetIsResponseSendingFinished={
|
||||
!isPreview
|
||||
? (f: (value: boolean) => void) => {
|
||||
setIsResponseSendingFinished = f;
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onRetry={() => {
|
||||
setIsError(false);
|
||||
responseQueue.processQueue();
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
import { sendLinkSurveyEmailAction } from "@/app/s/[surveyId]/actions";
|
||||
import { EnvelopeIcon } from "@heroicons/react/24/solid";
|
||||
import { useState } from "react";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Toaster, toast } from "react-hot-toast";
|
||||
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
@@ -19,6 +21,10 @@ export default function VerifyEmail({
|
||||
isErrorComponent?: boolean;
|
||||
singleUseId?: string;
|
||||
}) {
|
||||
survey = useMemo(() => {
|
||||
return checkForRecallInHeadline(survey);
|
||||
}, [survey]);
|
||||
|
||||
const [showPreviewQuestions, setShowPreviewQuestions] = useState(false);
|
||||
const [email, setEmail] = useState<string | null>(null);
|
||||
const [emailSent, setEmailSent] = useState<boolean>(false);
|
||||
@@ -106,7 +112,6 @@ export default function VerifyEmail({
|
||||
)}
|
||||
{!emailSent && showPreviewQuestions && (
|
||||
<div>
|
||||
{" "}
|
||||
<p className="text-4xl font-bold">Question Preview</p>
|
||||
<div className="mt-4 flex w-full flex-col justify-center rounded-lg border border-slate-200 bg-slate-50 bg-opacity-20 p-8 text-slate-700">
|
||||
{survey.questions.map((question, index) => (
|
||||
@@ -126,11 +131,8 @@ export default function VerifyEmail({
|
||||
We sent an email to <span className="font-semibold italic">{email}</span>. Please click the link
|
||||
in the email to take your survey.
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mt-6 cursor-pointer text-sm text-slate-400"
|
||||
onClick={handleGoBackClick}>
|
||||
Go Back
|
||||
<Button variant="secondary" className="mt-6" onClick={handleGoBackClick} StartIcon={ArrowLeft}>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -8,12 +8,10 @@ import { checkValidity } from "@/app/s/[surveyId]/lib/prefilling";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { IMPRINT_URL, IS_FORMBRICKS_CLOUD, PRIVACY_URL } from "@formbricks/lib/constants";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { IMPRINT_URL, IS_FORMBRICKS_CLOUD, PRIVACY_URL, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { createPerson, getPersonByUserId } from "@formbricks/lib/person/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getResponseBySingleUseId } from "@formbricks/lib/response/service";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getResponseBySingleUseId, getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
|
||||
@@ -23,11 +23,8 @@ export async function middleware(request: NextRequest) {
|
||||
const token = await getToken({ req: request });
|
||||
|
||||
if (isWebAppRoute(request.nextUrl.pathname) && !token) {
|
||||
const loginUrl = new URL(
|
||||
`/auth/login?callbackUrl=${encodeURIComponent(request.nextUrl.toString())}`,
|
||||
WEBAPP_URL
|
||||
);
|
||||
return NextResponse.redirect(loginUrl.href);
|
||||
const loginUrl = `${WEBAPP_URL}/auth/login?callbackUrl=${encodeURIComponent(WEBAPP_URL + request.nextUrl.pathname + request.nextUrl.search)}`;
|
||||
return NextResponse.redirect(loginUrl);
|
||||
}
|
||||
|
||||
const callbackUrl = request.nextUrl.searchParams.get("callbackUrl");
|
||||
|
||||
@@ -27,31 +27,31 @@
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@react-email/components": "^0.0.15",
|
||||
"@sentry/nextjs": "^7.102.1",
|
||||
"@sentry/nextjs": "^7.104.0",
|
||||
"@vercel/og": "^0.6.2",
|
||||
"@vercel/speed-insights": "^1.0.10",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"encoding": "^0.1.13",
|
||||
"framer-motion": "11.0.6",
|
||||
"framer-motion": "11.0.8",
|
||||
"googleapis": "^133.0.0",
|
||||
"jiti": "^1.21.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^10.2.0",
|
||||
"lucide-react": "^0.339.0",
|
||||
"lucide-react": "^0.344.0",
|
||||
"mime": "^4.0.1",
|
||||
"next": "14.1.0",
|
||||
"nodemailer": "^6.9.10",
|
||||
"next": "14.1.1",
|
||||
"nodemailer": "^6.9.11",
|
||||
"otplib": "^12.0.1",
|
||||
"posthog-js": "^1.108.3",
|
||||
"posthog-js": "^1.110.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-email": "^2.1.0",
|
||||
"react-hook-form": "^7.50.1",
|
||||
"react-hook-form": "^7.51.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^5.0.1",
|
||||
"sharp": "^0.33.2",
|
||||
|
||||
@@ -64,7 +64,8 @@ test.describe("JS Package Test", async () => {
|
||||
// Formbricks Modal is visible
|
||||
await expect(page.getByRole("link", { name: "Powered by Formbricks" })).toBeVisible();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(1500);
|
||||
});
|
||||
|
||||
test("Admin checks Display", async ({ page }) => {
|
||||
@@ -74,9 +75,11 @@ test.describe("JS Package Test", async () => {
|
||||
(await page.waitForSelector("text=Responses")).isVisible();
|
||||
|
||||
// Survey should have 1 Display
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(page.getByText("Displays1")).toBeVisible();
|
||||
|
||||
// Survey should have 0 Responses
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(page.getByRole("button", { name: "Responses0% -" })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -115,7 +118,7 @@ test.describe("JS Package Test", async () => {
|
||||
// Formbricks Modal is not visible
|
||||
await expect(page.getByText("Powered by Formbricks")).not.toBeVisible({ timeout: 10000 });
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(500);
|
||||
await page.waitForTimeout(1500);
|
||||
});
|
||||
|
||||
test("Admin validates Response", async ({ page }) => {
|
||||
@@ -125,8 +128,11 @@ test.describe("JS Package Test", async () => {
|
||||
(await page.waitForSelector("text=Responses")).isVisible();
|
||||
|
||||
// Survey should have 2 Displays
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(page.getByText("Displays2")).toBeVisible();
|
||||
|
||||
// Survey should have 1 Response
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(page.getByRole("button", { name: "Responses50%" })).toBeVisible();
|
||||
await expect(page.getByText("1 responses", { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByText("Clickthrough Rate (CTR)100%")).toBeVisible();
|
||||
|
||||
@@ -14,8 +14,10 @@ test.describe("Onboarding Flow Test", async () => {
|
||||
|
||||
await page.getByRole("button", { name: "Link Surveys Create a new" }).click();
|
||||
await page.getByRole("button", { name: "Collect Feedback Collect" }).click();
|
||||
await page.getByRole("button", { name: "Back", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
await expect(page.getByText(productName)).toBeVisible();
|
||||
});
|
||||
|
||||
test("In app survey", async ({ page }) => {
|
||||
@@ -24,6 +26,10 @@ test.describe("Onboarding Flow Test", async () => {
|
||||
await page.waitForURL("/onboarding");
|
||||
await expect(page).toHaveURL("/onboarding");
|
||||
await page.getByRole("button", { name: "In-app Surveys Run a survey" }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Skip" }).click();
|
||||
await page.getByRole("button", { name: "Skip" }).click();
|
||||
|
||||
await page.getByRole("button", { name: "I am not sure how to do this" }).click();
|
||||
await page.locator("input").click();
|
||||
await page.locator("input").fill("test@gmail.com");
|
||||
|
||||
@@ -58,7 +58,7 @@ test.describe("Invite, accept and remove team member", async () => {
|
||||
const pendingSpan = lastMemberInfo.locator("span").filter({ hasText: "Pending" });
|
||||
await expect(pendingSpan).toBeVisible();
|
||||
|
||||
const shareInviteButton = page.locator("#shareInviteButton");
|
||||
const shareInviteButton = page.locator(".shareInviteButton").last();
|
||||
await expect(shareInviteButton).toBeVisible();
|
||||
|
||||
await shareInviteButton.click();
|
||||
|
||||
@@ -12,11 +12,19 @@ export const signUpAndLogin = async (
|
||||
await page.goto("/auth/login");
|
||||
await page.getByRole("link", { name: "Create an account" }).click();
|
||||
await page.getByRole("button", { name: "Continue with Email" }).click();
|
||||
|
||||
await expect(page.getByPlaceholder("Full Name")).toBeVisible();
|
||||
await page.getByPlaceholder("Full Name").fill(name);
|
||||
await page.getByPlaceholder("Full Name").press("Tab");
|
||||
|
||||
await expect(page.getByPlaceholder("work@email.com")).toBeVisible();
|
||||
|
||||
await page.getByPlaceholder("work@email.com").click();
|
||||
await page.getByPlaceholder("work@email.com").fill(email);
|
||||
await page.getByPlaceholder("work@email.com").press("Tab");
|
||||
|
||||
await expect(page.getByPlaceholder("*******")).toBeVisible();
|
||||
|
||||
await page.getByPlaceholder("*******").click();
|
||||
await page.getByPlaceholder("*******").fill(password);
|
||||
await page.getByRole("button", { name: "Continue with Email" }).click();
|
||||
@@ -30,8 +38,17 @@ export const signUpAndLogin = async (
|
||||
|
||||
export const login = async (page: Page, email: string, password: string): Promise<void> => {
|
||||
await page.goto("/auth/login");
|
||||
|
||||
await expect(page.getByRole("button", { name: "Login with Email" })).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Login with Email" }).click();
|
||||
|
||||
await expect(page.getByPlaceholder("work@email.com")).toBeVisible();
|
||||
|
||||
await page.getByPlaceholder("work@email.com").fill(email);
|
||||
|
||||
await expect(page.getByPlaceholder("*******")).toBeVisible();
|
||||
|
||||
await page.getByPlaceholder("*******").click();
|
||||
await page.getByPlaceholder("*******").fill(password);
|
||||
await page.getByRole("button", { name: "Login with Email" }).click();
|
||||
@@ -40,11 +57,19 @@ export const login = async (page: Page, email: string, password: string): Promis
|
||||
export const finishOnboarding = async (page: Page): Promise<void> => {
|
||||
await page.waitForURL("/onboarding");
|
||||
await expect(page).toHaveURL("/onboarding");
|
||||
await page.getByRole("button", { name: "In-app Surveys Run a survey" }).click();
|
||||
await page.getByRole("button", { name: "I am not sure how to do this" }).click();
|
||||
await page.locator("input").click();
|
||||
await page.locator("input").fill("test@gmail.com");
|
||||
await page.getByRole("button", { name: "Invite" }).click();
|
||||
|
||||
const hiddenSkipButton = page.locator("#FB__INTERNAL__SKIP_ONBOARDING");
|
||||
hiddenSkipButton.evaluate((el: HTMLElement) => el.click());
|
||||
|
||||
// await page.getByRole("button", { name: "In-app Surveys Run a survey" }).click();
|
||||
|
||||
// await page.getByRole("button", { name: "Skip" }).click();
|
||||
// await page.getByRole("button", { name: "Skip" }).click();
|
||||
|
||||
// await page.getByRole("button", { name: "I am not sure how to do this" }).click();
|
||||
// await page.locator("input").click();
|
||||
// await page.locator("input").fill("test@gmail.com");
|
||||
// await page.getByRole("button", { name: "Invite" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
await expect(page.getByText("My Product")).toBeVisible();
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"maxDuration": 10,
|
||||
"memory": 300
|
||||
},
|
||||
"**/*.ts": {
|
||||
"app/**/*.ts": {
|
||||
"maxDuration": 10,
|
||||
"memory": 512
|
||||
}
|
||||
|
||||
12
package.json
12
package.json
@@ -32,13 +32,13 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.41.1",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"husky": "^9.0.5",
|
||||
"lint-staged": "^15.2.0",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^15.2.2",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsx": "^4.7.0",
|
||||
"turbo": "^1.11.3"
|
||||
"tsx": "^4.7.1",
|
||||
"turbo": "^1.12.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
"(apps|packages)/**/*.{js,ts,jsx,tsx}": [
|
||||
@@ -64,6 +64,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@changesets/cli": "^2.27.1",
|
||||
"playwright": "^1.41.1"
|
||||
"playwright": "^1.42.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[personId,attributeClassId]` on the table `Attribute` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- DropIndex
|
||||
DROP INDEX "Attribute_attributeClassId_personId_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "AttributeClass_environmentId_idx";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Attribute_personId_attributeClassId_key" ON "Attribute"("personId", "attributeClassId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AttributeClass_environmentId_created_at_idx" ON "AttributeClass"("environmentId", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AttributeClass_environmentId_archived_idx" ON "AttributeClass"("environmentId", "archived");
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.10.2",
|
||||
"@prisma/extension-accelerate": "^0.6.3",
|
||||
"@prisma/extension-accelerate": "^1.0.0",
|
||||
"@t3-oss/env-nextjs": "^0.9.2",
|
||||
"dotenv-cli": "^7.3.0"
|
||||
},
|
||||
|
||||
@@ -63,7 +63,7 @@ model Attribute {
|
||||
personId String
|
||||
value String
|
||||
|
||||
@@unique([attributeClassId, personId])
|
||||
@@unique([personId, attributeClassId])
|
||||
}
|
||||
|
||||
enum AttributeType {
|
||||
@@ -86,7 +86,8 @@ model AttributeClass {
|
||||
attributeFilters SurveyAttributeFilter[]
|
||||
|
||||
@@unique([name, environmentId])
|
||||
@@index([environmentId])
|
||||
@@index([environmentId, createdAt])
|
||||
@@index([environmentId, archived])
|
||||
}
|
||||
|
||||
model Person {
|
||||
|
||||
@@ -19,6 +19,6 @@
|
||||
"dependencies": {
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@t3-oss/env-nextjs": "^0.9.2",
|
||||
"stripe": "^14.18.0"
|
||||
"stripe": "^14.19.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
"eslint-config-next": "^14.1.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-turbo": "1.10.12",
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@formbricks/js",
|
||||
"license": "MIT",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"description": "Formbricks-js allows you to connect your app to Formbricks, display surveys and trigger events.",
|
||||
"homepage": "https://formbricks.com",
|
||||
"repository": {
|
||||
@@ -39,16 +39,16 @@
|
||||
},
|
||||
"author": "Formbricks <hola@formbricks.com>",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.9",
|
||||
"@babel/preset-env": "^7.23.9",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/preset-env": "^7.24.0",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@formbricks/api": "workspace:*",
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/surveys": "workspace:*",
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
||||
"@typescript-eslint/parser": "^7.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-turbo": "1.10.12",
|
||||
|
||||
@@ -46,7 +46,10 @@ export class CommandQueue {
|
||||
if (currentItem.checkInitialized) {
|
||||
const initResult = checkInitialized();
|
||||
|
||||
if (initResult && initResult.ok !== true) errorHandler.handle(initResult.error);
|
||||
if (initResult && initResult.ok !== true) {
|
||||
errorHandler.handle(initResult.error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const executeCommand = async () => {
|
||||
|
||||
@@ -25,11 +25,12 @@ export class Config {
|
||||
|
||||
public update(newConfig: TJsConfigUpdateInput): void {
|
||||
if (newConfig) {
|
||||
const expiresAt = new Date(new Date().getTime() + 2 * 60000); // 2 minutes from now
|
||||
const expiresAt = new Date(new Date().getTime() + 2 * 60000); // 2 minutes in the future
|
||||
|
||||
this.config = {
|
||||
...this.config,
|
||||
...newConfig,
|
||||
status: newConfig.status || "success",
|
||||
expiresAt,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { TJsConfig, TJsConfigInput } from "@formbricks/types/js";
|
||||
import { TPersonAttributes } from "@formbricks/types/people";
|
||||
|
||||
import { trackAction } from "./actions";
|
||||
import { Config } from "./config";
|
||||
import { Config, LOCAL_STORAGE_KEY } from "./config";
|
||||
import {
|
||||
ErrorHandler,
|
||||
MissingFieldError,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Result,
|
||||
err,
|
||||
okVoid,
|
||||
wrapThrows,
|
||||
} from "./errors";
|
||||
import { addCleanupEventListeners, addEventListeners, removeAllEventListeners } from "./eventListeners";
|
||||
import { Logger } from "./logger";
|
||||
@@ -19,23 +20,46 @@ import { checkPageUrl } from "./noCodeActions";
|
||||
import { updatePersonAttributes } from "./person";
|
||||
import { sync } from "./sync";
|
||||
import { getIsDebug } from "./utils";
|
||||
import { addWidgetContainer, closeSurvey } from "./widget";
|
||||
import { addWidgetContainer, removeWidgetContainer, setIsSurveyRunning } from "./widget";
|
||||
|
||||
const config = Config.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
let isInitialized = false;
|
||||
|
||||
export const setIsInitialized = (value: boolean) => {
|
||||
isInitialized = value;
|
||||
};
|
||||
|
||||
export const initialize = async (
|
||||
c: TJsConfigInput
|
||||
): Promise<Result<void, MissingFieldError | NetworkError | MissingPersonError>> => {
|
||||
if (getIsDebug()) {
|
||||
logger.configure({ logLevel: "debug" });
|
||||
}
|
||||
|
||||
if (isInitialized) {
|
||||
logger.debug("Already initialized, skipping initialization.");
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
if (getIsDebug()) {
|
||||
logger.configure({ logLevel: "debug" });
|
||||
let existingConfig: TJsConfig | undefined;
|
||||
try {
|
||||
existingConfig = config.get();
|
||||
logger.debug("Found existing configuration.");
|
||||
} catch (e) {
|
||||
logger.debug("No existing configuration found.");
|
||||
}
|
||||
|
||||
// formbricks is in error state, skip initialization
|
||||
if (existingConfig?.status === "error") {
|
||||
logger.debug("Formbricks was set to an error state.");
|
||||
if (existingConfig?.expiresAt && new Date(existingConfig.expiresAt) > new Date()) {
|
||||
logger.debug("Error state is not expired, skipping initialization");
|
||||
return okVoid();
|
||||
} else {
|
||||
logger.debug("Error state is expired. Continue with initialization.");
|
||||
}
|
||||
}
|
||||
|
||||
ErrorHandler.getInstance().printStatus();
|
||||
@@ -81,13 +105,6 @@ export const initialize = async (
|
||||
updatedAttributes = res.value;
|
||||
}
|
||||
|
||||
let existingConfig: TJsConfig | undefined;
|
||||
try {
|
||||
existingConfig = config.get();
|
||||
} catch (e) {
|
||||
logger.debug("No existing configuration found.");
|
||||
}
|
||||
|
||||
if (
|
||||
existingConfig &&
|
||||
existingConfig.state &&
|
||||
@@ -96,29 +113,39 @@ export const initialize = async (
|
||||
existingConfig.userId === c.userId &&
|
||||
existingConfig.expiresAt // only accept config when they follow new config version with expiresAt
|
||||
) {
|
||||
logger.debug("Found existing configuration.");
|
||||
logger.debug("Configuration fits init parameters.");
|
||||
if (existingConfig.expiresAt < new Date()) {
|
||||
logger.debug("Configuration expired.");
|
||||
|
||||
await sync({
|
||||
apiHost: c.apiHost,
|
||||
environmentId: c.environmentId,
|
||||
userId: c.userId,
|
||||
});
|
||||
try {
|
||||
await sync({
|
||||
apiHost: c.apiHost,
|
||||
environmentId: c.environmentId,
|
||||
userId: c.userId,
|
||||
});
|
||||
} catch (e) {
|
||||
putFormbricksInErrorState();
|
||||
}
|
||||
} else {
|
||||
logger.debug("Configuration not expired. Extending expiration.");
|
||||
config.update(existingConfig);
|
||||
}
|
||||
} else {
|
||||
logger.debug("No valid configuration found or it has been expired. Creating new config.");
|
||||
logger.debug(
|
||||
"No valid configuration found or it has been expired. Resetting config and creating new one."
|
||||
);
|
||||
config.resetConfig();
|
||||
logger.debug("Syncing.");
|
||||
|
||||
await sync({
|
||||
apiHost: c.apiHost,
|
||||
environmentId: c.environmentId,
|
||||
userId: c.userId,
|
||||
});
|
||||
|
||||
try {
|
||||
await sync({
|
||||
apiHost: c.apiHost,
|
||||
environmentId: c.environmentId,
|
||||
userId: c.userId,
|
||||
});
|
||||
} catch (e) {
|
||||
handleErrorOnFirstInit();
|
||||
}
|
||||
// and track the new session event
|
||||
await trackAction("New Session");
|
||||
}
|
||||
@@ -140,7 +167,7 @@ export const initialize = async (
|
||||
addEventListeners();
|
||||
addCleanupEventListeners();
|
||||
|
||||
isInitialized = true;
|
||||
setIsInitialized(true);
|
||||
logger.debug("Initialized");
|
||||
|
||||
// check page url if initialized after page load
|
||||
@@ -149,6 +176,17 @@ export const initialize = async (
|
||||
return okVoid();
|
||||
};
|
||||
|
||||
const handleErrorOnFirstInit = () => {
|
||||
// put formbricks in error state (by creating a new config) and throw error
|
||||
const initialErrorConfig: Partial<TJsConfig> = {
|
||||
status: "error",
|
||||
expiresAt: new Date(new Date().getTime() + 10 * 60000), // 10 minutes in the future
|
||||
};
|
||||
// can't use config.update here because the config is not yet initialized
|
||||
wrapThrows(() => localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(initialErrorConfig)))();
|
||||
throw new Error("Could not initialize formbricks");
|
||||
};
|
||||
|
||||
export const checkInitialized = (): Result<void, NotInitializedError> => {
|
||||
logger.debug("Check if initialized");
|
||||
if (!isInitialized || !ErrorHandler.initialized) {
|
||||
@@ -163,8 +201,19 @@ export const checkInitialized = (): Result<void, NotInitializedError> => {
|
||||
|
||||
export const deinitalize = (): void => {
|
||||
logger.debug("Deinitializing");
|
||||
closeSurvey();
|
||||
removeWidgetContainer();
|
||||
setIsSurveyRunning(false);
|
||||
removeAllEventListeners();
|
||||
config.resetConfig();
|
||||
isInitialized = false;
|
||||
setIsInitialized(false);
|
||||
};
|
||||
|
||||
export const putFormbricksInErrorState = (): void => {
|
||||
logger.debug("Putting formbricks in error state");
|
||||
// change formbricks status to error
|
||||
config.update({
|
||||
...config.get(),
|
||||
status: "error",
|
||||
expiresAt: new Date(new Date().getTime() + 10 * 60000), // 10 minutes in the future
|
||||
});
|
||||
deinitalize();
|
||||
};
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "./errors";
|
||||
import { deinitalize, initialize } from "./initialize";
|
||||
import { Logger } from "./logger";
|
||||
import { sync } from "./sync";
|
||||
import { closeSurvey } from "./widget";
|
||||
|
||||
const config = Config.getInstance();
|
||||
@@ -55,15 +54,7 @@ export const updatePersonAttribute = async (
|
||||
}
|
||||
|
||||
if (res.data.changed) {
|
||||
logger.debug("Attribute updated. Syncing...");
|
||||
await sync(
|
||||
{
|
||||
environmentId: environmentId,
|
||||
apiHost: apiHost,
|
||||
userId: userId,
|
||||
},
|
||||
true
|
||||
);
|
||||
logger.debug("Attribute updated in Formbricks");
|
||||
}
|
||||
|
||||
return okVoid();
|
||||
@@ -178,6 +169,7 @@ export const setPersonAttribute = async (
|
||||
|
||||
export const logoutPerson = async (): Promise<void> => {
|
||||
deinitalize();
|
||||
config.resetConfig();
|
||||
};
|
||||
|
||||
export const resetPerson = async (): Promise<Result<void, NetworkError>> => {
|
||||
|
||||
@@ -15,21 +15,42 @@ const syncWithBackend = async (
|
||||
{ apiHost, environmentId, userId }: TJsSyncParams,
|
||||
noCache: boolean
|
||||
): Promise<Result<TJsStateSync, NetworkError>> => {
|
||||
const baseUrl = `${apiHost}/api/v1/client/${environmentId}/in-app/sync`;
|
||||
const urlSuffix = `?version=${import.meta.env.VERSION}`;
|
||||
try {
|
||||
const baseUrl = `${apiHost}/api/v1/client/${environmentId}/in-app/sync`;
|
||||
const urlSuffix = `?version=${import.meta.env.VERSION}`;
|
||||
|
||||
let fetchOptions: RequestInit = {};
|
||||
let fetchOptions: RequestInit = {};
|
||||
|
||||
if (noCache || getIsDebug()) {
|
||||
fetchOptions.cache = "no-cache";
|
||||
logger.debug("No cache option set for sync");
|
||||
}
|
||||
if (noCache || getIsDebug()) {
|
||||
fetchOptions.cache = "no-cache";
|
||||
logger.debug("No cache option set for sync");
|
||||
}
|
||||
|
||||
// if user id is available
|
||||
// if user id is not available
|
||||
if (!userId) {
|
||||
const url = baseUrl + urlSuffix;
|
||||
// public survey
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const jsonRes = await response.json();
|
||||
|
||||
return err({
|
||||
code: "network_error",
|
||||
status: response.status,
|
||||
message: "Error syncing with backend",
|
||||
url,
|
||||
responseMessage: jsonRes.message,
|
||||
});
|
||||
}
|
||||
|
||||
return ok((await response.json()).data as TJsState);
|
||||
}
|
||||
|
||||
// userId is available, call the api with the `userId` param
|
||||
|
||||
const url = `${baseUrl}/${userId}${urlSuffix}`;
|
||||
|
||||
if (!userId) {
|
||||
const url = baseUrl + urlSuffix;
|
||||
// public survey
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -44,38 +65,20 @@ const syncWithBackend = async (
|
||||
});
|
||||
}
|
||||
|
||||
return ok((await response.json()).data as TJsState);
|
||||
const data = await response.json();
|
||||
const { data: state } = data;
|
||||
|
||||
return ok(state as TJsStateSync);
|
||||
} catch (e) {
|
||||
return err(e as NetworkError);
|
||||
}
|
||||
|
||||
// userId is available, call the api with the `userId` param
|
||||
|
||||
const url = `${baseUrl}/${userId}${urlSuffix}`;
|
||||
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const jsonRes = await response.json();
|
||||
|
||||
return err({
|
||||
code: "network_error",
|
||||
status: response.status,
|
||||
message: "Error syncing with backend",
|
||||
url,
|
||||
responseMessage: jsonRes.message,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const { data: state } = data;
|
||||
|
||||
return ok(state as TJsStateSync);
|
||||
};
|
||||
|
||||
export const sync = async (params: TJsSyncParams, noCache = false): Promise<void> => {
|
||||
try {
|
||||
const syncResult = await syncWithBackend(params, noCache);
|
||||
|
||||
if (syncResult?.ok !== true) {
|
||||
logger.error(`Sync failed: ${JSON.stringify(syncResult.error)}`);
|
||||
throw syncResult.error;
|
||||
}
|
||||
|
||||
@@ -115,8 +118,6 @@ export const sync = async (params: TJsSyncParams, noCache = false): Promise<void
|
||||
userId: params.userId,
|
||||
state,
|
||||
});
|
||||
|
||||
// before finding the surveys, check for public use
|
||||
} catch (error) {
|
||||
logger.error(`Error during sync: ${error}`);
|
||||
throw error;
|
||||
@@ -175,17 +176,23 @@ export const addExpiryCheckListener = (): void => {
|
||||
// add event listener to check sync with backend on regular interval
|
||||
if (typeof window !== "undefined" && syncIntervalId === null) {
|
||||
syncIntervalId = window.setInterval(async () => {
|
||||
// check if the config has not expired yet
|
||||
if (config.get().expiresAt && new Date(config.get().expiresAt) >= new Date()) {
|
||||
return;
|
||||
try {
|
||||
// check if the config has not expired yet
|
||||
if (config.get().expiresAt && new Date(config.get().expiresAt) >= new Date()) {
|
||||
return;
|
||||
}
|
||||
logger.debug("Config has expired. Starting sync.");
|
||||
await sync({
|
||||
apiHost: config.get().apiHost,
|
||||
environmentId: config.get().environmentId,
|
||||
userId: config.get().userId,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(`Error during expiry check: ${e}`);
|
||||
logger.debug("Extending config and try again later.");
|
||||
const existingConfig = config.get();
|
||||
config.update(existingConfig);
|
||||
}
|
||||
logger.debug("Config has expired. Starting sync.");
|
||||
await sync({
|
||||
apiHost: config.get().apiHost,
|
||||
environmentId: config.get().environmentId,
|
||||
userId: config.get().userId,
|
||||
// personId: config.get().state?.person?.id,
|
||||
});
|
||||
}, updateInterval);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,23 +7,29 @@ import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
import { Config } from "./config";
|
||||
import { ErrorHandler } from "./errors";
|
||||
import { putFormbricksInErrorState } from "./initialize";
|
||||
import { Logger } from "./logger";
|
||||
import { filterPublicSurveys, sync } from "./sync";
|
||||
|
||||
const containerId = "formbricks-web-container";
|
||||
|
||||
const config = Config.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
const errorHandler = ErrorHandler.getInstance();
|
||||
let surveyRunning = false;
|
||||
let isSurveyRunning = false;
|
||||
let setIsError = (_: boolean) => {};
|
||||
let setIsResponseSendingFinished = (_: boolean) => {};
|
||||
|
||||
export const setIsSurveyRunning = (value: boolean) => {
|
||||
isSurveyRunning = value;
|
||||
};
|
||||
|
||||
export const renderWidget = async (survey: TSurvey) => {
|
||||
if (surveyRunning) {
|
||||
if (isSurveyRunning) {
|
||||
logger.debug("A survey is already running. Skipping.");
|
||||
return;
|
||||
}
|
||||
surveyRunning = true;
|
||||
setIsSurveyRunning(false);
|
||||
|
||||
if (survey.delay) {
|
||||
logger.debug(`Delaying survey by ${survey.delay} seconds.`);
|
||||
@@ -163,7 +169,7 @@ export const renderWidget = async (survey: TSurvey) => {
|
||||
|
||||
export const closeSurvey = async (): Promise<void> => {
|
||||
// remove container element from DOM
|
||||
document.getElementById(containerId)?.remove();
|
||||
removeWidgetContainer();
|
||||
addWidgetContainer();
|
||||
|
||||
// if unidentified user, refilter the surveys
|
||||
@@ -174,7 +180,7 @@ export const closeSurvey = async (): Promise<void> => {
|
||||
...config.get(),
|
||||
state: updatedState,
|
||||
});
|
||||
surveyRunning = false;
|
||||
setIsSurveyRunning(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -188,9 +194,10 @@ export const closeSurvey = async (): Promise<void> => {
|
||||
},
|
||||
true
|
||||
);
|
||||
surveyRunning = false;
|
||||
} catch (e) {
|
||||
setIsSurveyRunning(false);
|
||||
} catch (e: any) {
|
||||
errorHandler.handle(e);
|
||||
putFormbricksInErrorState();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -200,6 +207,10 @@ export const addWidgetContainer = (): void => {
|
||||
document.body.appendChild(containerElement);
|
||||
};
|
||||
|
||||
export const removeWidgetContainer = (): void => {
|
||||
document.getElementById(containerId)?.remove();
|
||||
};
|
||||
|
||||
const loadFormbricksSurveysExternally = (): Promise<typeof window.formbricksSurveys> => {
|
||||
const formbricksSurveysScriptSrc = import.meta.env.FORMBRICKS_SURVEYS_SCRIPT_SRC;
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
"test": "dotenv -e ../../.env -- vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.521.0",
|
||||
"@aws-sdk/s3-presigned-post": "3.521.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.521.0",
|
||||
"@aws-sdk/client-s3": "3.525.0",
|
||||
"@aws-sdk/s3-presigned-post": "3.525.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.525.0",
|
||||
"@formbricks/api": "*",
|
||||
"@formbricks/database": "*",
|
||||
"@formbricks/types": "*",
|
||||
@@ -29,14 +29,14 @@
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "^5.0.6",
|
||||
"next-auth": "^4.24.6",
|
||||
"nodemailer": "^6.9.10",
|
||||
"nodemailer": "^6.9.11",
|
||||
"posthog-node": "^3.6.3",
|
||||
"server-only": "^0.0.1",
|
||||
"tailwind-merge": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/tsconfig": "*",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
|
||||
@@ -258,7 +258,7 @@ export const updatePerson = async (personId: string, personInput: TPersonUpdateI
|
||||
// Now perform the upsert for the attribute with the found or created attributeClassId
|
||||
await prisma.attribute.upsert({
|
||||
where: {
|
||||
attributeClassId_personId: {
|
||||
personId_attributeClassId: {
|
||||
attributeClassId: attributeClass!.id,
|
||||
personId,
|
||||
},
|
||||
@@ -391,7 +391,7 @@ export const updatePersonAttribute = async (
|
||||
|
||||
const attributes = await prisma.attribute.upsert({
|
||||
where: {
|
||||
attributeClassId_personId: {
|
||||
personId_attributeClassId: {
|
||||
attributeClassId,
|
||||
personId,
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@ import { productCache } from "../product/cache";
|
||||
import { getProductByEnvironmentId } from "../product/service";
|
||||
import { responseCache } from "../response/cache";
|
||||
import { segmentCache } from "../segment/cache";
|
||||
import { evaluateSegment, getSegment, updateSegment } from "../segment/service";
|
||||
import { createSegment, evaluateSegment, getSegment, updateSegment } from "../segment/service";
|
||||
import { transformSegmentFiltersToAttributeFilters } from "../segment/utils";
|
||||
import { subscribeTeamMembersToSurveyResponses } from "../team/service";
|
||||
import { diffInDays, formatDateFields } from "../utils/datetime";
|
||||
@@ -100,7 +100,10 @@ const getActionClassIdFromName = (actionClasses: TActionClass[], actionClassName
|
||||
return actionClasses.find((actionClass) => actionClass.name === actionClassName)!.id;
|
||||
};
|
||||
|
||||
const revalidateSurveyByActionClassId = (actionClasses: TActionClass[], actionClassNames: string[]): void => {
|
||||
const revalidateSurveyByActionClassName = (
|
||||
actionClasses: TActionClass[],
|
||||
actionClassNames: string[]
|
||||
): void => {
|
||||
for (const actionClassName of actionClassNames) {
|
||||
const actionClassId: string = getActionClassIdFromName(actionClasses, actionClassName);
|
||||
surveyCache.revalidate({
|
||||
@@ -128,7 +131,7 @@ const processTriggerUpdates = (
|
||||
// find removed triggers
|
||||
for (const trigger of currentSurveyTriggers) {
|
||||
if (!triggers.includes(trigger)) {
|
||||
removedTriggers.push(getActionClassIdFromName(actionClasses, trigger));
|
||||
removedTriggers.push(trigger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,10 +146,12 @@ const processTriggerUpdates = (
|
||||
|
||||
if (removedTriggers.length > 0) {
|
||||
triggersUpdate.deleteMany = {
|
||||
actionClassId: { in: removedTriggers },
|
||||
actionClassId: {
|
||||
in: removedTriggers.map((trigger) => getActionClassIdFromName(actionClasses, trigger)),
|
||||
},
|
||||
};
|
||||
}
|
||||
revalidateSurveyByActionClassId(actionClasses, [...newTriggers, ...removedTriggers]);
|
||||
revalidateSurveyByActionClassName(actionClasses, [...newTriggers, ...removedTriggers]);
|
||||
return triggersUpdate;
|
||||
};
|
||||
|
||||
@@ -438,7 +443,7 @@ export const createSurvey = async (environmentId: string, surveyBody: TSurveyInp
|
||||
|
||||
if (surveyBody.triggers) {
|
||||
const actionClasses = await getActionClasses(environmentId);
|
||||
revalidateSurveyByActionClassId(actionClasses, surveyBody.triggers);
|
||||
revalidateSurveyByActionClassName(actionClasses, surveyBody.triggers);
|
||||
}
|
||||
|
||||
const createdBy = surveyBody.createdBy;
|
||||
@@ -544,24 +549,67 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string, u
|
||||
verifyEmail: existingSurvey.verifyEmail
|
||||
? JSON.parse(JSON.stringify(existingSurvey.verifyEmail))
|
||||
: Prisma.JsonNull,
|
||||
segment: existingSurvey.segment ? { connect: { id: existingSurvey.segment.id } } : undefined,
|
||||
// we'll update the segment later
|
||||
segment: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// if the existing survey has an inline segment, we copy the filters and create a new inline segment and connect it to the new survey
|
||||
if (existingSurvey.segment) {
|
||||
if (existingSurvey.segment.isPrivate) {
|
||||
const newInlineSegment = await createSegment({
|
||||
environmentId,
|
||||
title: `${newSurvey.id}`,
|
||||
isPrivate: true,
|
||||
surveyId: newSurvey.id,
|
||||
filters: existingSurvey.segment.filters,
|
||||
});
|
||||
|
||||
await prisma.survey.update({
|
||||
where: {
|
||||
id: newSurvey.id,
|
||||
},
|
||||
data: {
|
||||
segment: {
|
||||
connect: {
|
||||
id: newInlineSegment.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
segmentCache.revalidate({
|
||||
id: newInlineSegment.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
} else {
|
||||
await prisma.survey.update({
|
||||
where: {
|
||||
id: newSurvey.id,
|
||||
},
|
||||
data: {
|
||||
segment: {
|
||||
connect: {
|
||||
id: existingSurvey.segment.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
segmentCache.revalidate({
|
||||
id: existingSurvey.segment.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
surveyCache.revalidate({
|
||||
id: newSurvey.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
|
||||
if (newSurvey.segmentId) {
|
||||
segmentCache.revalidate({
|
||||
id: newSurvey.segmentId,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
}
|
||||
|
||||
// Revalidate surveys by actionClassId
|
||||
revalidateSurveyByActionClassId(actionClasses, existingSurvey.triggers);
|
||||
revalidateSurveyByActionClassName(actionClasses, existingSurvey.triggers);
|
||||
|
||||
return newSurvey;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@formbricks/surveys",
|
||||
"license": "MIT",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"description": "Formbricks-surveys is a helper library to embed surveys into your application",
|
||||
"homepage": "https://formbricks.com",
|
||||
"repository": {
|
||||
@@ -42,7 +42,7 @@
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@preact/preset-vite": "^2.8.1",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"concurrently": "8.2.2",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-turbo": "1.10.12",
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function CalEmbed({ question, onSuccessfulBooking }: CalEmbedProp
|
||||
}, [cal, question.calUserName]);
|
||||
|
||||
return (
|
||||
<div className="relative mt-4 max-h-[30vh] overflow-auto">
|
||||
<div className="relative mt-4 max-h-[33vh] overflow-auto">
|
||||
<div id="fb-cal-embed" className={cn("rounded-lg border border-slate-200")} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -37,8 +37,10 @@ export function Survey({
|
||||
activeQuestionId || (survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id)
|
||||
);
|
||||
const [showError, setShowError] = useState(false);
|
||||
// flag state to store whether response processing has been completed or not
|
||||
const [isResponseSendingFinished, setIsResponseSendingFinished] = useState(false);
|
||||
// flag state to store whether response processing has been completed or not, we ignore this check for survey editor preview and link survey preview where getSetIsResponseSendingFinished is undefined
|
||||
const [isResponseSendingFinished, setIsResponseSendingFinished] = useState(
|
||||
getSetIsResponseSendingFinished ? false : true
|
||||
);
|
||||
|
||||
const [loadingElement, setLoadingElement] = useState(false);
|
||||
const [history, setHistory] = useState<string[]>([]);
|
||||
|
||||
@@ -7,8 +7,7 @@ import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn, shuffleQuestions } from "@/lib/utils";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TResponseTtc } from "@formbricks/types/responses";
|
||||
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyMultipleChoiceMultiQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
interface MultipleChoiceMultiProps {
|
||||
@@ -127,7 +126,7 @@ export default function MultipleChoiceMultiQuestion({
|
||||
<div className="mt-4">
|
||||
<fieldset>
|
||||
<legend className="sr-only">Options</legend>
|
||||
<div className="bg-survey-bg relative max-h-[30vh] space-y-2 overflow-y-auto rounded-md py-0.5 pr-2">
|
||||
<div className="bg-survey-bg relative max-h-[33vh] space-y-2 overflow-y-auto rounded-md py-0.5 pr-2">
|
||||
{questionChoices.map((choice, idx) => (
|
||||
<label
|
||||
key={choice.id}
|
||||
|
||||
@@ -7,8 +7,7 @@ import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn, shuffleQuestions } from "@/lib/utils";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TResponseTtc } from "@formbricks/types/responses";
|
||||
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyMultipleChoiceSingleQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
interface MultipleChoiceSingleProps {
|
||||
@@ -86,7 +85,7 @@ export default function MultipleChoiceSingleQuestion({
|
||||
<legend className="sr-only">Options</legend>
|
||||
|
||||
<div
|
||||
className="bg-survey-bg relative max-h-[30vh] space-y-2 overflow-y-auto rounded-md py-0.5 pr-2"
|
||||
className="bg-survey-bg relative max-h-[33vh] space-y-2 overflow-y-auto rounded-md py-0.5 pr-2"
|
||||
role="radiogroup">
|
||||
{questionChoices.map((choice, idx) => (
|
||||
<label
|
||||
|
||||
@@ -86,6 +86,7 @@ export default function OpenTextQuestion({
|
||||
tabIndex={1}
|
||||
name={question.id}
|
||||
id={question.id}
|
||||
step={"any"}
|
||||
placeholder={question.placeholder}
|
||||
required={question.required}
|
||||
value={value ? (value as string) : ""}
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function PictureSelectionQuestion({
|
||||
<div className="mt-4">
|
||||
<fieldset>
|
||||
<legend className="sr-only">Options</legend>
|
||||
<div className="rounded-m bg-survey-bg relative grid max-h-[30vh] grid-cols-2 gap-x-5 gap-y-4 overflow-y-auto">
|
||||
<div className="rounded-m bg-survey-bg relative grid max-h-[33vh] grid-cols-2 gap-x-5 gap-y-4 overflow-y-auto">
|
||||
{questionChoices.map((choice, idx) => (
|
||||
<label
|
||||
key={choice.id}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1"
|
||||
},
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"clean": "rimraf node_modules dist turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.11.20",
|
||||
"@types/react": "18.2.58",
|
||||
"@types/node": "20.11.24",
|
||||
"@types/react": "18.2.61",
|
||||
"@types/react-dom": "18.2.19",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
|
||||
@@ -78,6 +78,7 @@ export const ZJsConfig = z.object({
|
||||
userId: z.string().optional(),
|
||||
state: ZJsState,
|
||||
expiresAt: z.date(),
|
||||
status: z.enum(["success", "error"]).optional(),
|
||||
});
|
||||
|
||||
export type TJsConfig = z.infer<typeof ZJsConfig>;
|
||||
@@ -87,6 +88,8 @@ export const ZJsConfigUpdateInput = z.object({
|
||||
apiHost: z.string(),
|
||||
userId: z.string().optional(),
|
||||
state: ZJsState,
|
||||
expiresAt: z.date().optional(),
|
||||
status: z.enum(["success", "error"]).optional(),
|
||||
});
|
||||
|
||||
export type TJsConfigUpdateInput = z.infer<typeof ZJsConfigUpdateInput>;
|
||||
|
||||
@@ -8,6 +8,7 @@ interface PathwayOptionProps {
|
||||
description: string;
|
||||
loading?: boolean;
|
||||
onSelect: () => void;
|
||||
cssId?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -24,9 +25,11 @@ export const OptionCard: React.FC<PathwayOptionProps> = ({
|
||||
children,
|
||||
onSelect,
|
||||
loading,
|
||||
cssId,
|
||||
}) => (
|
||||
<div className="relative">
|
||||
<div
|
||||
id={cssId}
|
||||
className={`flex cursor-pointer flex-col items-center justify-center bg-white p-4 hover:scale-105 hover:border-slate-300 ${sizeClasses[size]}`}
|
||||
onClick={onSelect}
|
||||
role="button"
|
||||
|
||||
@@ -384,7 +384,7 @@ export default function SingleResponseCard({
|
||||
{response.data[question.id]}
|
||||
</p>
|
||||
) : (
|
||||
<p className="ph-no-capture my-1 font-semibold text-slate-700">
|
||||
<p className="ph-no-capture my-1 whitespace-pre-line font-semibold text-slate-700">
|
||||
{response.data[question.id]}
|
||||
</p>
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"@formbricks/types": "workspace:*",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"postcss": "^8.4.33",
|
||||
"postcss": "^8.4.35",
|
||||
"react": "18.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -20,13 +20,13 @@
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/surveys": "workspace:*",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"@lexical/code": "^0.13.0",
|
||||
"@lexical/link": "^0.13.0",
|
||||
"@lexical/list": "^0.13.0",
|
||||
"@lexical/markdown": "^0.13.0",
|
||||
"@lexical/react": "^0.13.0",
|
||||
"@lexical/rich-text": "^0.13.0",
|
||||
"@lexical/table": "^0.13.0",
|
||||
"@lexical/code": "^0.13.1",
|
||||
"@lexical/link": "^0.13.1",
|
||||
"@lexical/list": "^0.13.1",
|
||||
"@lexical/markdown": "^0.13.1",
|
||||
"@lexical/react": "^0.13.1",
|
||||
"@lexical/rich-text": "^0.13.1",
|
||||
"@lexical/table": "^0.13.1",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
@@ -42,9 +42,9 @@
|
||||
"boring-avatars": "^1.10.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"lexical": "^0.13.0",
|
||||
"lucide-react": "^0.315.0",
|
||||
"cmdk": "^0.2.1",
|
||||
"lexical": "^0.13.1",
|
||||
"lucide-react": "^0.344.0",
|
||||
"mime": "^4.0.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-confetti": "^6.1.0",
|
||||
|
||||
2905
pnpm-lock.yaml
generated
2905
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user