mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-03 00:50:22 -06:00
feat: add option to overwrite color and position for single surveys (#645)
* feat: styles ui * feat: handling brand color and position * schema update * survey type update * feat: local styles preview * prisma migration * feat: overriding global settings * fix: update wording * fix: do not show positioning in link mode * update wording * feat: new component for placement ui * fix: destructuring localsurvey * fix: overwritePosition optional * fix: brandcolor zod validation * feat: overwrite highlight border setting * update wording * merge conflit resolved * fix: minor review suggestions * fix: renamed localsurvey to survey * fix: db schema * fix comment in .env.example * fix build errors * fix: placement * fix getFirstEnvironmentByUserId function * fix build errors --------- Co-authored-by: Johannes <johannes@formbricks.com> Co-authored-by: Matthias Nannt <mail@matthiasnannt.com> Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
@@ -79,6 +79,9 @@ export async function duplicateSurveyAction(environmentId: string, surveyId: str
|
||||
singleUse: existingSurvey.singleUse
|
||||
? JSON.parse(JSON.stringify(existingSurvey.singleUse))
|
||||
: prismaClient.JsonNull,
|
||||
productOverwrites: existingSurvey.productOverwrites
|
||||
? JSON.parse(JSON.stringify(existingSurvey.productOverwrites))
|
||||
: prismaClient.JsonNull,
|
||||
verifyEmail: existingSurvey.verifyEmail
|
||||
? JSON.parse(JSON.stringify(existingSurvey.verifyEmail))
|
||||
: prismaClient.JsonNull,
|
||||
@@ -228,6 +231,7 @@ export async function copyToOtherEnvironmentAction(
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
|
||||
singleUse: existingSurvey.singleUse ?? prismaClient.JsonNull,
|
||||
productOverwrites: existingSurvey.productOverwrites ?? prismaClient.JsonNull,
|
||||
verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { Button, Label, RadioGroup, RadioGroupItem } from "@formbricks/ui";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getPlacementStyle } from "@/lib/preview";
|
||||
import { PlacementType } from "@formbricks/types/js";
|
||||
import { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { updateProductAction } from "./actions";
|
||||
|
||||
const placements = [
|
||||
@@ -35,7 +35,9 @@ export function EditPlacement({ product }: EditPlacementProps) {
|
||||
darkOverlay: overlay === "darkOverlay",
|
||||
clickOutsideClose: clickOutside === "allow",
|
||||
};
|
||||
|
||||
await updateProductAction(product.id, inputProduct);
|
||||
|
||||
toast.success("Placement updated successfully.");
|
||||
} catch (error) {
|
||||
toast.error(`Error: ${error.message}`);
|
||||
|
||||
@@ -15,6 +15,7 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsTitle title="Look & Feel" />
|
||||
|
||||
@@ -11,20 +11,23 @@ import { ArrowPathRoundedSquareIcon } from "@heroicons/react/24/outline";
|
||||
import { ComputerDesktopIcon, DevicePhoneMobileIcon } from "@heroicons/react/24/solid";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
type TPreviewType = "modal" | "fullwidth" | "email";
|
||||
|
||||
interface PreviewSurveyProps {
|
||||
survey: TSurvey | Survey;
|
||||
setActiveQuestionId: (id: string | null) => void;
|
||||
activeQuestionId?: string | null;
|
||||
previewType?: "modal" | "fullwidth" | "email";
|
||||
previewType?: TPreviewType;
|
||||
product: TProduct;
|
||||
environment: TEnvironment;
|
||||
}
|
||||
|
||||
let surveyNameTemp;
|
||||
|
||||
export default function PreviewSurvey({
|
||||
survey,
|
||||
setActiveQuestionId,
|
||||
activeQuestionId,
|
||||
survey,
|
||||
previewType,
|
||||
product,
|
||||
environment,
|
||||
@@ -34,6 +37,18 @@ export default function PreviewSurvey({
|
||||
const [previewMode, setPreviewMode] = useState("desktop");
|
||||
const ContentRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { productOverwrites } = survey || {};
|
||||
|
||||
const {
|
||||
brandColor: surveyBrandColor,
|
||||
highlightBorderColor: surveyHighlightBorderColor,
|
||||
placement: surveyPlacement,
|
||||
} = productOverwrites || {};
|
||||
|
||||
const brandColor = surveyBrandColor || product.brandColor;
|
||||
const placement = surveyPlacement || product.placement;
|
||||
const highlightBorderColor = surveyHighlightBorderColor || product.highlightBorderColor;
|
||||
|
||||
useEffect(() => {
|
||||
// close modal if there are no questions left
|
||||
if (survey.type === "web" && !survey.thankYouCard.enabled) {
|
||||
@@ -52,6 +67,8 @@ export default function PreviewSurvey({
|
||||
resetQuestionProgress();
|
||||
surveyNameTemp = survey.name;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [survey]);
|
||||
|
||||
function resetQuestionProgress() {
|
||||
@@ -94,12 +111,12 @@ export default function PreviewSurvey({
|
||||
{previewType === "modal" ? (
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
placement={product.placement}
|
||||
highlightBorderColor={product.highlightBorderColor}
|
||||
placement={placement}
|
||||
highlightBorderColor={highlightBorderColor}
|
||||
previewMode="mobile">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={product.brandColor}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
formbricksSignature={product.formbricksSignature}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
@@ -114,7 +131,7 @@ export default function PreviewSurvey({
|
||||
<div className="w-full max-w-md px-4">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={product.brandColor}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
formbricksSignature={product.formbricksSignature}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
@@ -143,12 +160,12 @@ export default function PreviewSurvey({
|
||||
{previewType === "modal" ? (
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
placement={product.placement}
|
||||
highlightBorderColor={product.highlightBorderColor}
|
||||
placement={placement}
|
||||
highlightBorderColor={highlightBorderColor}
|
||||
previewMode="desktop">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={product.brandColor}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
formbricksSignature={product.formbricksSignature}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
@@ -161,7 +178,7 @@ export default function PreviewSurvey({
|
||||
<div className="w-full max-w-md">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={product.brandColor}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
formbricksSignature={product.formbricksSignature}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { Label, RadioGroup, RadioGroupItem } from "@formbricks/ui";
|
||||
import { getPlacementStyle } from "@/lib/preview";
|
||||
import { PlacementType } from "@formbricks/types/js";
|
||||
const placements = [
|
||||
{ name: "Bottom Right", value: "bottomRight", disabled: false },
|
||||
{ name: "Top Right", value: "topRight", disabled: false },
|
||||
{ name: "Top Left", value: "topLeft", disabled: false },
|
||||
{ name: "Bottom Left", value: "bottomLeft", disabled: false },
|
||||
{ name: "Centered Modal", value: "center", disabled: false },
|
||||
];
|
||||
|
||||
type TPlacementProps = {
|
||||
currentPlacement: PlacementType;
|
||||
setCurrentPlacement: (placement: PlacementType) => void;
|
||||
setOverlay: (overlay: string) => void;
|
||||
overlay: string;
|
||||
setClickOutside: (clickOutside: boolean) => void;
|
||||
clickOutside: boolean;
|
||||
};
|
||||
|
||||
export default function Placement({
|
||||
setCurrentPlacement,
|
||||
currentPlacement,
|
||||
setOverlay,
|
||||
overlay,
|
||||
setClickOutside,
|
||||
clickOutside,
|
||||
}: TPlacementProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex">
|
||||
<RadioGroup onValueChange={(e) => setCurrentPlacement(e as PlacementType)} value={currentPlacement}>
|
||||
{placements.map((placement) => (
|
||||
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id={placement.value} value={placement.value} disabled={placement.disabled} />
|
||||
<Label
|
||||
htmlFor={placement.value}
|
||||
className={cn(placement.disabled ? "cursor-not-allowed text-slate-500" : "text-slate-900")}>
|
||||
{placement.name}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
<div className="relative ml-8 h-40 w-full rounded bg-slate-200">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute h-16 w-16 rounded bg-slate-700",
|
||||
getPlacementStyle(currentPlacement)
|
||||
)}></div>
|
||||
</div>
|
||||
</div>
|
||||
{currentPlacement === "center" && (
|
||||
<>
|
||||
<div className="mt-6 space-y-2">
|
||||
<Label className="font-semibold">Centered modal overlay color</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(overlay) => setOverlay(overlay)}
|
||||
value={overlay}
|
||||
className="flex space-x-4">
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="lightOverlay" value="light" />
|
||||
<Label htmlFor="lightOverlay" className="text-slate-900">
|
||||
Light Overlay
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="darkOverlay" value="dark" />
|
||||
<Label htmlFor="darkOverlay" className="text-slate-900">
|
||||
Dark Overlay
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className="mt-6 space-y-2">
|
||||
<Label className="font-semibold">Allow users to exit by clicking outside the study</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(value) => setClickOutside(value === "allow")}
|
||||
value={clickOutside ? "allow" : "disallow"}
|
||||
className="flex space-x-4">
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="disallow" value="disallow" />
|
||||
<Label htmlFor="disallow" className="text-slate-900">
|
||||
Don't Allow
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="allow" value="allow" />
|
||||
<Label htmlFor="allow" className="text-slate-900">
|
||||
Allow
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import RecontactOptionsCard from "./RecontactOptionsCard";
|
||||
import ResponseOptionsCard from "./ResponseOptionsCard";
|
||||
import WhenToSendCard from "./WhenToSendCard";
|
||||
import WhoToSendCard from "./WhoToSendCard";
|
||||
import StylingCard from "./StylingCard";
|
||||
import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TActionClass } from "@formbricks/types/v1/actionClasses";
|
||||
@@ -54,6 +55,8 @@ export default function SettingsView({
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
environmentId={environment.id}
|
||||
/>
|
||||
|
||||
<StylingCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
"use client";
|
||||
|
||||
import Placement from "./Placement";
|
||||
import { PlacementType } from "@formbricks/types/js";
|
||||
import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
|
||||
import { ColorPicker, Label, Switch } from "@formbricks/ui";
|
||||
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { useState } from "react";
|
||||
|
||||
interface StylingCardProps {
|
||||
localSurvey: TSurveyWithAnalytics;
|
||||
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurveyWithAnalytics>>;
|
||||
}
|
||||
|
||||
export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCardProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { type, productOverwrites } = localSurvey;
|
||||
const { brandColor, clickOutside, darkOverlay, placement, highlightBorderColor } = productOverwrites ?? {};
|
||||
|
||||
const togglePlacement = () => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
placement: !!placement ? null : "bottomRight",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const toggleBrandColor = () => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
brandColor: !!brandColor ? null : "#64748b",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const toggleHighlightBorderColor = () => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
highlightBorderColor: !!highlightBorderColor ? null : "#64748b",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleColorChange = (color: string) => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
brandColor: color,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBorderColorChange = (color: string) => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
highlightBorderColor: color,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handlePlacementChange = (placement: PlacementType) => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
placement,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleOverlay = (overlayType: string) => {
|
||||
const darkOverlay = overlayType === "dark";
|
||||
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
darkOverlay,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickOutside = (clickOutside: boolean) => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
clickOutside,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
className="w-full rounded-lg border border-slate-300 bg-white">
|
||||
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
|
||||
<div className="inline-flex px-4 py-4">
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
<CheckCircleIcon className="h-8 w-8 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-800">Styling</p>
|
||||
<p className="mt-1 truncate text-sm text-slate-500">Overwrite global styling settings</p>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
<div className="p-3">
|
||||
{/* Brand Color */}
|
||||
<div className="p-3">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch id="autoComplete" checked={!!brandColor} onCheckedChange={toggleBrandColor} />
|
||||
<Label htmlFor="autoComplete" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Overwrite Brand Color</h3>
|
||||
<p className="text-xs font-normal text-slate-500">Change the main color for this survey.</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{brandColor && (
|
||||
<div className="ml-2 mt-4 rounded-lg border bg-slate-50 p-4">
|
||||
<div className="w-full max-w-xs">
|
||||
<Label htmlFor="brandcolor">Color (HEX)</Label>
|
||||
<ColorPicker color={brandColor} onChange={handleColorChange} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* positioning */}
|
||||
{type !== "link" && (
|
||||
<div className="p-3 ">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch id="surveyDeadline" checked={!!placement} onCheckedChange={togglePlacement} />
|
||||
<Label htmlFor="surveyDeadline" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Overwrite Placement</h3>
|
||||
<p className="text-xs font-normal text-slate-500">Change the placement of this survey.</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{placement && (
|
||||
<div className="ml-2 mt-4 flex items-center space-x-1 pb-4">
|
||||
<div className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
<div className="w-full items-center">
|
||||
<Placement
|
||||
currentPlacement={placement}
|
||||
setCurrentPlacement={handlePlacementChange}
|
||||
setOverlay={handleOverlay}
|
||||
overlay={darkOverlay ? "dark" : "light"}
|
||||
setClickOutside={handleClickOutside}
|
||||
clickOutside={!!clickOutside}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Highlight border */}
|
||||
{type !== "link" && (
|
||||
<div className="p-3 ">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch
|
||||
id="autoComplete"
|
||||
checked={!!highlightBorderColor}
|
||||
onCheckedChange={toggleHighlightBorderColor}
|
||||
/>
|
||||
<Label htmlFor="autoComplete" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Overwrite border highlight</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Change the border highlight for this survey.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{!!highlightBorderColor && (
|
||||
<div className="ml-2 mt-4 rounded-lg border bg-slate-50 p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="highlightBorder"
|
||||
checked={!!highlightBorderColor}
|
||||
onCheckedChange={toggleHighlightBorderColor}
|
||||
/>
|
||||
<h2 className="text-sm font-medium text-slate-800">Show highlight border</h2>
|
||||
</div>
|
||||
{!!highlightBorderColor && (
|
||||
<div className="mt-6 w-full max-w-xs">
|
||||
<Label htmlFor="brandcolor">Color (HEX)</Label>
|
||||
<ColorPicker color={highlightBorderColor || ""} onChange={handleBorderColorChange} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
||||
@@ -51,6 +51,8 @@ export default function SurveyEditor({
|
||||
if (localSurvey?.questions?.length && localSurvey.questions.length > 0) {
|
||||
setActiveQuestionId(localSurvey.questions[0].id);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSurvey?.type]);
|
||||
|
||||
if (!localSurvey) {
|
||||
|
||||
@@ -2062,5 +2062,6 @@ export const minimalSurvey: TSurvey = {
|
||||
surveyClosedMessage: {
|
||||
enabled: false,
|
||||
},
|
||||
productOverwrites: null,
|
||||
singleUse: null,
|
||||
};
|
||||
|
||||
@@ -13,8 +13,14 @@ export default async function OnboardingPage() {
|
||||
if (!session) {
|
||||
throw new Error("No session found");
|
||||
}
|
||||
const environment = await getFirstEnvironmentByUserId(session?.user.id);
|
||||
const profile = await getProfile(session?.user.id!);
|
||||
const userId = session?.user.id;
|
||||
const environment = await getFirstEnvironmentByUserId(userId);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("No environment found for user");
|
||||
}
|
||||
|
||||
const profile = await getProfile(userId);
|
||||
const product = await getProductByEnvironmentId(environment?.id!);
|
||||
|
||||
if (!environment || !profile || !product) {
|
||||
|
||||
@@ -46,6 +46,8 @@ export default function LinkSurvey({
|
||||
? getPrefillResponseData(survey.questions[0], survey, prefillAnswer)
|
||||
: undefined;
|
||||
|
||||
const brandColor = survey.productOverwrites?.brandColor || product.brandColor;
|
||||
|
||||
const responseQueue = useMemo(
|
||||
() =>
|
||||
new ResponseQueue(
|
||||
@@ -110,7 +112,7 @@ export default function LinkSurvey({
|
||||
)}
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={product.brandColor}
|
||||
brandColor={brandColor}
|
||||
formbricksSignature={product.formbricksSignature}
|
||||
onDisplay={async () => {
|
||||
if (!isPreview) {
|
||||
|
||||
@@ -147,6 +147,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
|
||||
singleUse: existingSurvey.singleUse ?? prismaClient.JsonNull,
|
||||
productOverwrites: existingSurvey.productOverwrites ?? prismaClient.JsonNull,
|
||||
verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -67,6 +67,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
|
||||
singleUse: existingSurvey.singleUse ?? prismaClient.JsonNull,
|
||||
productOverwrites: existingSurvey.productOverwrites ?? prismaClient.JsonNull,
|
||||
verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull,
|
||||
},
|
||||
});
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
BIN
apps/web/uploads/cln2o8ist000g19nh5zqpze7e/public/cpTree.png
Normal file
BIN
apps/web/uploads/cln2o8ist000g19nh5zqpze7e/public/cpTree.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
@@ -3,6 +3,7 @@ import { TIntegrationConfig } from "@formbricks/types/v1/integrations";
|
||||
import { TResponsePersonAttributes, TResponseData, TResponseMeta } from "@formbricks/types/v1/responses";
|
||||
import {
|
||||
TSurveyClosedMessage,
|
||||
TSurveyProductOverwrites,
|
||||
TSurveyQuestions,
|
||||
TSurveySingleUse,
|
||||
TSurveyThankYouCard,
|
||||
@@ -20,6 +21,7 @@ declare global {
|
||||
export type ResponsePersonAttributes = TResponsePersonAttributes;
|
||||
export type SurveyQuestions = TSurveyQuestions;
|
||||
export type SurveyThankYouCard = TSurveyThankYouCard;
|
||||
export type SurveyProductOverwrites = TSurveyProductOverwrites;
|
||||
export type SurveyClosedMessage = TSurveyClosedMessage;
|
||||
export type SurveySingleUse = TSurveySingleUse;
|
||||
export type SurveyVerifyEmail = TSurveyVerifyEmail;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Survey" ADD COLUMN "productOverwrites" JSONB;
|
||||
@@ -251,10 +251,16 @@ model Survey {
|
||||
surveyClosedMessage Json?
|
||||
/// @zod.custom(imports.ZSurveySingleUse)
|
||||
/// [SurveySingleUse]
|
||||
singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}")
|
||||
|
||||
/// @zod.custom(imports.ZSurveyProductOverwrites)
|
||||
/// [SurveyProductOverwrites]
|
||||
productOverwrites Json?
|
||||
/// @zod.custom(imports.ZSurveySingleUse)
|
||||
/// [SurveySingleUse]
|
||||
singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}")
|
||||
/// @zod.custom(imports.ZSurveyVerifyEmail)
|
||||
/// [SurveyVerifyEmail]
|
||||
verifyEmail Json?
|
||||
verifyEmail Json?
|
||||
}
|
||||
|
||||
model Event {
|
||||
|
||||
@@ -10,6 +10,7 @@ export {
|
||||
ZSurveyQuestions,
|
||||
ZSurveyThankYouCard,
|
||||
ZSurveyClosedMessage,
|
||||
ZSurveyProductOverwrites,
|
||||
ZSurveyVerifyEmail,
|
||||
ZSurveySingleUse,
|
||||
} from "@formbricks/types/v1/surveys";
|
||||
|
||||
@@ -42,15 +42,22 @@ export const renderWidget = (survey: TSurvey) => {
|
||||
surveyState
|
||||
);
|
||||
|
||||
const productOverwrites = survey.productOverwrites ?? {};
|
||||
const brandColor = productOverwrites.brandColor ?? product.brandColor;
|
||||
const highlightBorderColor = productOverwrites.highlightBorderColor ?? product.highlightBorderColor;
|
||||
const clickOutside = productOverwrites.clickOutside ?? product.clickOutsideClose;
|
||||
const darkOverlay = productOverwrites.darkOverlay ?? product.darkOverlay;
|
||||
const placement = productOverwrites.placement ?? product.placement;
|
||||
|
||||
setTimeout(() => {
|
||||
renderSurveyModal({
|
||||
survey: survey,
|
||||
brandColor: product.brandColor,
|
||||
brandColor,
|
||||
formbricksSignature: product.formbricksSignature,
|
||||
clickOutside: product.clickOutsideClose,
|
||||
darkOverlay: product.darkOverlay,
|
||||
highlightBorderColor: product.highlightBorderColor,
|
||||
placement: product.placement,
|
||||
clickOutside,
|
||||
darkOverlay,
|
||||
highlightBorderColor,
|
||||
placement,
|
||||
onDisplay: async () => {
|
||||
const { id } = await createDisplay(
|
||||
{
|
||||
|
||||
@@ -136,9 +136,8 @@ export const updateEnvironment = async (
|
||||
|
||||
export const getFirstEnvironmentByUserId = async (userId: string): Promise<TEnvironment | null> => {
|
||||
validateInputs([userId, ZId]);
|
||||
let environmentPrisma;
|
||||
try {
|
||||
environmentPrisma = await prisma.environment.findFirst({
|
||||
return await prisma.environment.findFirst({
|
||||
where: {
|
||||
type: "production",
|
||||
product: {
|
||||
@@ -159,16 +158,6 @@ export const getFirstEnvironmentByUserId = async (userId: string): Promise<TEnvi
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
const environment = ZEnvironment.parse(environmentPrisma);
|
||||
return environment;
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error(JSON.stringify(error.errors, null, 2));
|
||||
}
|
||||
throw new ValidationError("Data validation of environment failed");
|
||||
}
|
||||
};
|
||||
|
||||
export const createEnvironment = async (
|
||||
|
||||
@@ -42,6 +42,7 @@ export const selectSurvey = {
|
||||
autoComplete: true,
|
||||
verifyEmail: true,
|
||||
redirectUrl: true,
|
||||
productOverwrites: true,
|
||||
surveyClosedMessage: true,
|
||||
singleUse: true,
|
||||
triggers: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PlacementType } from "./js";
|
||||
import { Question } from "./questions";
|
||||
|
||||
export interface ThankYouCard {
|
||||
@@ -22,6 +23,14 @@ export interface VerifyEmail {
|
||||
subheading?: string;
|
||||
}
|
||||
|
||||
export interface SurveyProductOverwrites {
|
||||
brandColor: string;
|
||||
highlightBorderColor: string | null;
|
||||
placement: PlacementType;
|
||||
clickOutside: boolean;
|
||||
darkOverlay: boolean;
|
||||
}
|
||||
|
||||
export interface Survey {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
@@ -47,6 +56,7 @@ export interface Survey {
|
||||
closeOnDate: Date | null;
|
||||
singleUse: SurveySingleUse | null;
|
||||
_count: { responses: number | null } | null;
|
||||
productOverwrites: SurveyProductOverwrites | null;
|
||||
}
|
||||
|
||||
export interface AttributeFilter {
|
||||
|
||||
4
packages/types/v1/common.ts
Normal file
4
packages/types/v1/common.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZColor = z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/);
|
||||
export const ZSurveyPlacement = z.enum(["bottomLeft", "bottomRight", "topLeft", "topRight", "center"]);
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { ZEnvironment } from "./environment";
|
||||
import { ZColor, ZSurveyPlacement } from "./common";
|
||||
|
||||
export const ZProduct = z.object({
|
||||
id: z.string().cuid2(),
|
||||
@@ -7,14 +8,11 @@ export const ZProduct = z.object({
|
||||
updatedAt: z.date(),
|
||||
name: z.string(),
|
||||
teamId: z.string(),
|
||||
brandColor: z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/),
|
||||
highlightBorderColor: z
|
||||
.string()
|
||||
.regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/)
|
||||
.nullable(),
|
||||
brandColor: ZColor,
|
||||
highlightBorderColor: ZColor.nullable(),
|
||||
recontactDays: z.number().int(),
|
||||
formbricksSignature: z.boolean(),
|
||||
placement: z.enum(["bottomLeft", "bottomRight", "topLeft", "topRight", "center"]),
|
||||
placement: ZSurveyPlacement,
|
||||
clickOutsideClose: z.boolean(),
|
||||
darkOverlay: z.boolean(),
|
||||
environments: z.array(ZEnvironment),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { ZActionClass } from "./actionClasses";
|
||||
import { QuestionType } from "../questions";
|
||||
import { ZColor, ZSurveyPlacement } from "./common";
|
||||
|
||||
export const ZSurveyThankYouCard = z.object({
|
||||
enabled: z.boolean(),
|
||||
@@ -8,6 +9,16 @@ export const ZSurveyThankYouCard = z.object({
|
||||
subheader: z.optional(z.string()),
|
||||
});
|
||||
|
||||
export const ZSurveyProductOverwrites = z.object({
|
||||
brandColor: ZColor.nullish(),
|
||||
highlightBorderColor: ZColor.nullish(),
|
||||
placement: ZSurveyPlacement.nullish(),
|
||||
clickOutside: z.boolean().nullish(),
|
||||
darkOverlay: z.boolean().nullish(),
|
||||
});
|
||||
|
||||
export type TSurveyProductOverwrites = z.infer<typeof ZSurveyProductOverwrites>;
|
||||
|
||||
export const ZSurveyClosedMessage = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
@@ -269,6 +280,7 @@ export const ZSurvey = z.object({
|
||||
delay: z.number(),
|
||||
autoComplete: z.number().nullable(),
|
||||
closeOnDate: z.date().nullable(),
|
||||
productOverwrites: ZSurveyProductOverwrites.nullable(),
|
||||
surveyClosedMessage: ZSurveyClosedMessage.nullable(),
|
||||
singleUse: ZSurveySingleUse.nullable(),
|
||||
verifyEmail: ZSurveyVerifyEmail.nullable(),
|
||||
|
||||
Reference in New Issue
Block a user