Merge branch 'main' of https://github.com/formbricks/formbricks into feature/delete-team

This commit is contained in:
Piyush Gupta
2023-08-08 00:59:37 +05:30
35 changed files with 603 additions and 189 deletions
+4 -4
View File
@@ -1,4 +1,4 @@
name: Cron - weeklySummary
name: Cron - closeOnDate
on:
# "Scheduled workflows run on the latest commit on the default or base branch."
@@ -10,14 +10,14 @@ jobs:
cron-weeklySummary:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_SECRET }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
runs-on: ubuntu-latest
steps:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_SECRET }}
run: |
curl ${{ secrets.APP_URL }}/api/cron/close_surveys \
curl ${{ env.APP_URL }}/api/cron/close_surveys \
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_SECRET }}' \
-H 'x-api-key: ${{ env.CRON_SECRET }}' \
--fail
+1 -1
View File
@@ -10,7 +10,7 @@ COPY .env.docker /app/apps/web/.env
RUN pnpm install
# Build the project
RUN pnpm prebuild --filter=web...
RUN pnpm post-install --filter=web...
RUN pnpm turbo run build --filter=web...
FROM node:18-alpine AS runner
@@ -18,6 +18,7 @@ import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { getPlacementStyle } from "@/lib/preview";
import { PlacementType } from "@formbricks/types/js";
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
export function EditBrandColor({ environmentId }) {
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
@@ -180,6 +181,111 @@ export function EditPlacement({ environmentId }) {
);
}
export const EditHighlightBorder: React.FC<{ environmentId: string }> = ({ environmentId }) => {
const { product, isLoadingProduct, isErrorProduct, mutateProduct } = useProduct(environmentId);
const { triggerProductMutate, isMutatingProduct } = useProductMutation(environmentId);
const [showHighlightBorder, setShowHighlightBorder] = useState(false);
const [color, setColor] = useState<string | null>(DEFAULT_BRAND_COLOR);
// Sync product state with local state
// not a good pattern, we should find a better way to do this
useEffect(() => {
if (product) {
setShowHighlightBorder(product.highlightBorderColor ? true : false);
setColor(product.highlightBorderColor);
}
}, [product]);
const handleSave = () => {
triggerProductMutate(
{ highlightBorderColor: color },
{
onSuccess: () => {
toast.success("Settings updated successfully.");
// refetch product to update data
mutateProduct();
},
onError: () => {
toast.error("Something went wrong!");
},
}
);
};
const handleSwitch = (checked: boolean) => {
if (checked) {
if (!color) {
setColor(DEFAULT_BRAND_COLOR);
setShowHighlightBorder(true);
} else {
setShowHighlightBorder(true);
}
} else {
setShowHighlightBorder(false);
setColor(null);
}
};
if (isLoadingProduct) {
return <LoadingSpinner />;
}
if (isErrorProduct) {
return <div>Error</div>;
}
return (
<div className="flex min-h-full w-full">
<div className="flex w-1/2 flex-col px-6 py-5">
<div className="mb-6 flex items-center space-x-2">
<Switch id="highlightBorder" checked={showHighlightBorder} onCheckedChange={handleSwitch} />
<h2 className="text-sm font-medium text-slate-800">Show highlight border</h2>
</div>
{showHighlightBorder && color ? (
<>
<Label htmlFor="brandcolor">Color (HEX)</Label>
<ColorPicker color={color} onChange={setColor} />
</>
) : null}
<Button
type="submit"
variant="darkCTA"
className="mt-4 flex max-w-[80px] items-center justify-center"
loading={isMutatingProduct}
onClick={() => {
handleSave();
}}>
Save
</Button>
</div>
<div className="flex w-1/2 flex-col items-center justify-center gap-4 bg-slate-200 px-6 py-5">
<h3 className="text-slate-500">Preview</h3>
<div
className={cn("flex flex-col gap-4 rounded-lg border-2 bg-white p-5")}
{...(showHighlightBorder &&
color && {
style: {
borderColor: color,
},
})}>
<h3 className="text-sm font-semibold text-slate-800">How easy was it for you to do this?</h3>
<div className="flex rounded-2xl border border-slate-400">
{[1, 2, 3, 4, 5].map((num) => (
<div className="border-r border-slate-400 px-6 py-5 last:border-r-0">
<span className="text-sm font-medium">{num}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
};
export function EditFormbricksSignature({ environmentId }) {
const { isLoadingEnvironment, isErrorEnvironment } = useEnvironment(environmentId);
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
@@ -1,6 +1,11 @@
import SettingsCard from "../SettingsCard";
import SettingsTitle from "../SettingsTitle";
import { EditBrandColor, EditPlacement, EditFormbricksSignature } from "./editLookAndFeel";
import {
EditBrandColor,
EditPlacement,
EditFormbricksSignature,
EditHighlightBorder,
} from "./editLookAndFeel";
export default function ProfileSettingsPage({ params }: { params: { environmentId: string } }) {
return (
@@ -14,6 +19,12 @@ export default function ProfileSettingsPage({ params }: { params: { environmentI
description="Change where surveys will be shown in your web app.">
<EditPlacement environmentId={params.environmentId} />
</SettingsCard>
<SettingsCard
noPadding
title="Highlight Border"
description="Make sure your users notice the survey you display">
<EditHighlightBorder environmentId={params.environmentId} />
</SettingsCard>
<SettingsCard
title="Formbricks Signature"
description="We love your support but understand if you toggle it off.">
@@ -5,18 +5,27 @@ import { useTeamMutation } from "@/lib/teams/mutateTeams";
import { useTeam } from "@/lib/teams/teams";
import { Button, ErrorComponent, Input, Label } from "@formbricks/ui";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useForm, useWatch } from "react-hook-form";
import toast from "react-hot-toast";
export default function EditTeamName({ environmentId }) {
const { team, isLoadingTeam, isErrorTeam, mutateTeam } = useTeam(environmentId);
const { register, handleSubmit } = useForm();
const { register, control, handleSubmit, setValue } = useForm();
const [teamId, setTeamId] = useState("");
const teamName = useWatch({
control,
name: "name",
});
const isTeamNameInputEmpty = !teamName?.trim();
const currentTeamName = teamName?.trim().toLowerCase() ?? "";
const previousTeamName = team?.name?.trim().toLowerCase() ?? "";
useEffect(() => {
if (team && team.id !== "") {
setTeamId(team.id);
}
setValue("name", team?.name ?? "");
}, [team]);
const { isMutatingTeam, triggerTeamMutate } = useTeamMutation(teamId);
@@ -42,9 +51,20 @@ export default function EditTeamName({ environmentId }) {
});
})}>
<Label htmlFor="teamname">Team Name</Label>
<Input type="text" id="teamname" defaultValue={team.name} {...register("name")} />
<Input
type="text"
id="teamname"
defaultValue={team?.name ?? ""}
{...register("name")}
className={isTeamNameInputEmpty ? "border-red-300 focus:border-red-300" : ""}
/>
<Button type="submit" className="mt-4" variant="darkCTA" loading={isMutatingTeam}>
<Button
type="submit"
className="mt-4"
variant="darkCTA"
loading={isMutatingTeam}
disabled={isTeamNameInputEmpty || currentTeamName === previousTeamName}>
Update
</Button>
</form>
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useEffect, useState } from "react";
import { useForm, useWatch } from "react-hook-form";
import toast from "react-hot-toast";
import { useRouter } from "next/navigation";
@@ -22,7 +22,19 @@ export function EditProductName({ environmentId }) {
const { isMutatingProduct, triggerProductMutate } = useProductMutation(environmentId);
const { mutateEnvironment } = useEnvironment(environmentId);
const { register, handleSubmit } = useForm();
const { register, handleSubmit, control, setValue } = useForm();
const productName = useWatch({
control,
name: "name",
});
const isProductNameInputEmpty = !productName?.trim();
const currentProductName = productName?.trim().toLowerCase() ?? "";
const previousProductName = product?.name?.trim().toLowerCase() ?? "";
useEffect(() => {
setValue("name", product?.name ?? "");
}, [product?.name]);
if (isLoadingProduct) {
return <LoadingSpinner />;
@@ -45,9 +57,20 @@ export function EditProductName({ environmentId }) {
});
})}>
<Label htmlFor="fullname">What&apos;s your product called?</Label>
<Input type="text" id="fullname" defaultValue={product.name} {...register("name")} />
<Input
type="text"
id="fullname"
defaultValue={product.name}
{...register("name")}
className={isProductNameInputEmpty ? "border-red-300 focus:border-red-300" : ""}
/>
<Button type="submit" variant="darkCTA" className="mt-4" loading={isMutatingProduct}>
<Button
type="submit"
variant="darkCTA"
className="mt-4"
loading={isMutatingProduct}
disabled={isProductNameInputEmpty || currentProductName === previousProductName}>
Update
</Button>
</form>
@@ -11,16 +11,28 @@ import { Button, ErrorComponent, Input, Label, ProfileAvatar } from "@formbricks
import { Session } from "next-auth";
import { signOut } from "next-auth/react";
import Image from "next/image";
import { Dispatch, SetStateAction, useState } from "react";
import { useForm } from "react-hook-form";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { useForm, useWatch } from "react-hook-form";
import toast from "react-hot-toast";
export function EditName() {
const { register, handleSubmit } = useForm();
const { register, handleSubmit, control, setValue } = useForm();
const { profile, isLoadingProfile, isErrorProfile } = useProfile();
const { triggerProfileMutate, isMutatingProfile } = useProfileMutation();
const profileName = useWatch({
control,
name: "name",
});
const isProfileNameInputEmpty = !profileName?.trim();
const currentProfileName = profileName?.trim().toLowerCase() ?? "";
const previousProfileName = profile?.name?.trim().toLowerCase() ?? "";
useEffect(() => {
setValue("name", profile?.name ?? "");
}, [profile?.name]);
if (isLoadingProfile) {
return <LoadingSpinner />;
}
@@ -41,13 +53,24 @@ export function EditName() {
});
})}>
<Label htmlFor="fullname">Full Name</Label>
<Input type="text" id="fullname" defaultValue={profile.name} {...register("name")} />
<Input
type="text"
id="fullname"
defaultValue={profile.name}
{...register("name")}
className={isProfileNameInputEmpty ? "border-red-300 focus:border-red-300" : ""}
/>
<div className="mt-4">
<Label htmlFor="email">Email</Label>
<Input type="email" id="fullname" defaultValue={profile.email} disabled />
</div>
<Button type="submit" variant="darkCTA" className="mt-4" loading={isMutatingProfile}>
<Button
type="submit"
variant="darkCTA"
className="mt-4"
loading={isMutatingProfile}
disabled={isProfileNameInputEmpty || currentProfileName === previousProfileName}>
Update
</Button>
</form>
@@ -275,7 +275,10 @@ export default function PreviewSurvey({
</div>
{previewType === "modal" ? (
<Modal isOpen={isModalOpen} placement={product.placement}>
<Modal
isOpen={isModalOpen}
placement={product.placement}
highlightBorderColor={product.highlightBorderColor}>
{!countdownStop && autoClose !== null && autoClose > 0 && (
<Progress progress={countdownProgress} brandColor={brandColor} />
)}
@@ -5,6 +5,7 @@ import { DatePicker, Input, Label, Switch } from "@formbricks/ui";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
interface ResponseOptionsCardProps {
localSurvey: Survey;
@@ -117,13 +118,27 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
}
};
const handleInputResponse = (e: any) => {
let value = parseInt(e.target.value);
if (value < 1) value = 1;
const updatedSurvey: Survey = { ...localSurvey, autoComplete: value };
const handleInputResponse = (e) => {
const updatedSurvey: Survey = { ...localSurvey, autoComplete: parseInt(e.target.value) };
setLocalSurvey(updatedSurvey);
};
const handleInputResponseBlur = (e) => {
if (parseInt(e.target.value) === 0) {
toast.error("Response limit can't be set to 0");
return;
}
const inputResponses = localSurvey?._count?.responses || 0;
if (parseInt(e.target.value) <= inputResponses) {
toast.error(
`Response limit needs to exceed number of received responses (${localSurvey?._count?.responses}).`
);
return;
}
};
return (
<Collapsible.Root
open={open}
@@ -169,11 +184,16 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
<Input
autoFocus
type="number"
min="1"
min={
localSurvey?._count?.responses
? (localSurvey?._count?.responses + 1).toString()
: "1"
}
id="autoCompleteResponses"
value={localSurvey.autoComplete?.toString()}
onChange={(e) => handleInputResponse(e)}
className="ml-2 mr-2 inline w-16 bg-white text-center text-sm"
onChange={handleInputResponse}
onBlur={handleInputResponseBlur}
className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
/>
completed responses.
</p>
@@ -23,7 +23,7 @@ export default function SurveyEditor({ environmentId, surveyId }: SurveyEditorPr
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
const [localSurvey, setLocalSurvey] = useState<Survey | null>();
const [invalidQuestions, setInvalidQuestions] = useState<String[] | null>(null);
const { survey, isLoadingSurvey, isErrorSurvey } = useSurvey(environmentId, surveyId);
const { survey, isLoadingSurvey, isErrorSurvey } = useSurvey(environmentId, surveyId, true);
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
const { environment, isLoadingEnvironment, isErrorEnvironment } = useEnvironment(environmentId);
@@ -106,6 +106,17 @@ export default function SurveyMenuBar({
return false;
}
/*
Check whether the count for autocomplete responses is not less
than the current count of accepted response and also it is not set to 0
*/
if (
(survey.autoComplete && survey._count?.responses && survey._count.responses >= survey.autoComplete) ||
survey?.autoComplete === 0
) {
return false;
}
return true;
};
@@ -9,6 +9,7 @@ import PreviewSurvey from "../PreviewSurvey";
import TemplateList from "./TemplateList";
import type { TProduct } from "@formbricks/types/v1/product";
import type { TEnvironment } from "@formbricks/types/v1/environment";
import { SearchBox } from "@formbricks/ui";
type TemplateContainerWithPreviewProps = {
environmentId: string;
@@ -23,7 +24,7 @@ export default function TemplateContainerWithPreview({
}: TemplateContainerWithPreviewProps) {
const [activeTemplate, setActiveTemplate] = useState<Template | null>(null);
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
const [templateSearch, setTemplateSearch] = useState<string | null>(null);
useEffect(() => {
if (product && templates?.length) {
const newTemplate = replacePresetPlaceholders(templates[0], product);
@@ -36,11 +37,26 @@ export default function TemplateContainerWithPreview({
<div className="flex h-full flex-col ">
<div className="relative z-0 flex flex-1 overflow-hidden">
<div className="flex-1 flex-col overflow-auto bg-slate-50">
<h1 className="ml-6 mt-6 text-2xl font-bold text-slate-800">Create a new survey</h1>
<div className="flex flex-col items-center justify-between md:flex-row md:items-start">
<h1 className="ml-6 mt-6 text-2xl font-bold text-slate-800">Create a new survey</h1>
<div className="ml-6 mt-6 px-6">
<SearchBox
autoFocus
value={templateSearch ?? ""}
onChange={(e) => setTemplateSearch(e.target.value)}
placeholder={"Search..."}
className="block rounded-md border border-slate-100 bg-white shadow-sm focus:border-slate-500 focus:outline-none focus:ring-0 sm:text-sm md:w-auto "
type="search"
name="search"
/>
</div>
</div>
<TemplateList
environmentId={environmentId}
environment={environment}
product={product}
templateSearch={templateSearch ?? ""}
onTemplateClick={(template) => {
setActiveQuestionId(template.preset.questions[0].id);
setActiveTemplate(template);
@@ -28,11 +28,18 @@ type TemplateList = {
onTemplateClick: (template: Template) => void;
environment: TEnvironment;
product: TProduct;
templateSearch?: string;
};
const ALL_CATEGORY_NAME = "All";
const RECOMMENDED_CATEGORY_NAME = "For you";
export default function TemplateList({ environmentId, onTemplateClick, product, environment }: TemplateList) {
export default function TemplateList({
environmentId,
onTemplateClick,
product,
environment,
templateSearch,
}: TemplateList) {
const router = useRouter();
const [activeTemplate, setActiveTemplate] = useState<Template | null>(null);
const [loading, setLoading] = useState(false);
@@ -49,15 +56,18 @@ export default function TemplateList({ environmentId, onTemplateClick, product,
const fullCategories =
!!profile?.objective && profile.objective !== "other"
? [RECOMMENDED_CATEGORY_NAME, ...defaultCategories]
? [RECOMMENDED_CATEGORY_NAME, ALL_CATEGORY_NAME, ...defaultCategories]
: [ALL_CATEGORY_NAME, ...defaultCategories];
setCategories(fullCategories);
const activeFilter =
!!profile?.objective && profile.objective !== "other" ? RECOMMENDED_CATEGORY_NAME : ALL_CATEGORY_NAME;
const activeFilter = templateSearch
? ALL_CATEGORY_NAME
: !!profile?.objective && profile.objective !== "other"
? RECOMMENDED_CATEGORY_NAME
: ALL_CATEGORY_NAME;
setSelectedFilter(activeFilter);
}, [profile]);
}, [profile, templateSearch]);
const addSurvey = async (activeTemplate) => {
setLoading(true);
@@ -72,19 +82,38 @@ export default function TemplateList({ environmentId, onTemplateClick, product,
if (isLoadingProfile) return <LoadingSpinner />;
if (isErrorProfile) return <ErrorComponent />;
const filteredTemplates = templates.filter((template) => {
const matchesCategory =
selectedFilter === ALL_CATEGORY_NAME ||
template.category === selectedFilter ||
(selectedFilter === RECOMMENDED_CATEGORY_NAME && template.objectives?.includes(profile.objective));
const templateName = template.name?.toLowerCase();
const templateDescription = template.description?.toLowerCase();
const searchQuery = templateSearch?.toLowerCase() ?? "";
const searchWords = searchQuery.split(" ");
const matchesSearch = searchWords.every(
(word) => templateName?.includes(word) || templateDescription?.includes(word)
);
return matchesCategory && matchesSearch;
});
return (
<main className="relative z-0 flex-1 overflow-y-auto px-6 pb-6 pt-3 focus:outline-none">
<div className="mb-6 flex flex-wrap gap-2">
<div className="mb-6 flex flex-wrap gap-1">
{categories.map((category) => (
<button
key={category}
type="button"
onClick={() => setSelectedFilter(category)}
disabled={templateSearch && templateSearch.length > 0 ? true : false}
className={cn(
selectedFilter === category
? " bg-slate-800 font-semibold text-white"
: " bg-white text-slate-700 hover:bg-slate-100",
"mt-2 rounded border border-slate-800 px-2 py-1 text-sm transition-all duration-150 "
"mt-2 rounded border border-slate-800 px-2 py-1 text-xs transition-all duration-150 "
)}>
{category}
{category === RECOMMENDED_CATEGORY_NAME && <SparklesIcon className="ml-1 inline h-5 w-5" />}
@@ -121,72 +150,62 @@ export default function TemplateList({ environmentId, onTemplateClick, product,
</div>
)}
</button>
{templates
.filter(
(template) =>
selectedFilter === ALL_CATEGORY_NAME ||
template.category === selectedFilter ||
(selectedFilter === RECOMMENDED_CATEGORY_NAME &&
template.objectives?.includes(profile.objective))
)
.map((template: Template) => (
<div
onClick={() => {
const newTemplate = replacePresetPlaceholders(template, product);
onTemplateClick(newTemplate);
setActiveTemplate(newTemplate);
}}
key={template.name}
className={cn(
activeTemplate?.name === template.name && "ring-2 ring-slate-400",
"duration-120 group relative cursor-pointer rounded-lg bg-white p-6 shadow transition-all duration-150 hover:scale-105"
)}>
<div className="flex">
<div
className={`rounded border px-1.5 py-0.5 text-xs ${
template.category === "Product Experience"
? "border-blue-300 bg-blue-50 text-blue-500"
: template.category === "Exploration"
? "border-pink-300 bg-pink-50 text-pink-500"
: template.category === "Growth"
? "border-orange-300 bg-orange-50 text-orange-500"
: template.category === "Increase Revenue"
? "border-emerald-300 bg-emerald-50 text-emerald-500"
: template.category === "Customer Success"
? "border-violet-300 bg-violet-50 text-violet-500"
: "border-slate-300 bg-slate-50 text-slate-500" // default color
}`}>
{template.category}
</div>
{template.preset.questions.some(
(question) => question.logic && question.logic.length > 0
) && (
<TooltipProvider delayDuration={80}>
<Tooltip>
<TooltipTrigger>
<div>
<SplitIcon className="ml-1.5 h-5 w-5 rounded border border-slate-300 bg-slate-50 p-0.5 text-slate-400" />
</div>
</TooltipTrigger>
<TooltipContent>This survey uses branching logic.</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{filteredTemplates.map((template: Template) => (
<div
onClick={() => {
const newTemplate = replacePresetPlaceholders(template, product);
onTemplateClick(newTemplate);
setActiveTemplate(newTemplate);
}}
key={template.name}
className={cn(
activeTemplate?.name === template.name && "ring-2 ring-slate-400",
"duration-120 group relative cursor-pointer rounded-lg bg-white p-6 shadow transition-all duration-150 hover:scale-105"
)}>
<div className="flex">
<div
className={`rounded border px-1.5 py-0.5 text-xs ${
template.category === "Product Experience"
? "border-blue-300 bg-blue-50 text-blue-500"
: template.category === "Exploration"
? "border-pink-300 bg-pink-50 text-pink-500"
: template.category === "Growth"
? "border-orange-300 bg-orange-50 text-orange-500"
: template.category === "Increase Revenue"
? "border-emerald-300 bg-emerald-50 text-emerald-500"
: template.category === "Customer Success"
? "border-violet-300 bg-violet-50 text-violet-500"
: "border-slate-300 bg-slate-50 text-slate-500" // default color
}`}>
{template.category}
</div>
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700">{template.name}</h3>
<p className="text-left text-xs text-slate-600">{template.description}</p>
{activeTemplate?.name === template.name && (
<Button
variant="darkCTA"
className="mt-6 px-6 py-3"
disabled={activeTemplate === null}
loading={loading}
onClick={() => addSurvey(activeTemplate)}>
Use this template
</Button>
{template.preset.questions.some((question) => question.logic && question.logic.length > 0) && (
<TooltipProvider delayDuration={80}>
<Tooltip>
<TooltipTrigger>
<div>
<SplitIcon className="ml-1.5 h-5 w-5 rounded border border-slate-300 bg-slate-50 p-0.5 text-slate-400" />
</div>
</TooltipTrigger>
<TooltipContent>This survey uses branching logic.</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
))}
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700">{template.name}</h3>
<p className="text-left text-xs text-slate-600">{template.description}</p>
{activeTemplate?.name === template.name && (
<Button
variant="darkCTA"
className="mt-6 px-6 py-3"
disabled={activeTemplate === null}
loading={loading}
onClick={() => addSurvey(activeTemplate)}>
Use this template
</Button>
)}
</div>
))}
</div>
</main>
);
+13
View File
@@ -49,3 +49,16 @@ input:focus {
}
}
}
/* Hide the clear button for input type "search" */
input[type="search"]::-webkit-search-cancel-button {
display: none;
}
input[type="search"]::-ms-clear {
display: none;
}
input[type="search"]::-ms-reveal {
display: none;
}
+15 -3
View File
@@ -1,19 +1,30 @@
import { getPlacementStyle } from "@/lib/preview";
import { cn } from "@formbricks/lib/cn";
import { PlacementType } from "@formbricks/types/js";
import { ReactNode, useEffect, useState } from "react";
import { ReactNode, useEffect, useMemo, useState } from "react";
export default function Modal({
children,
isOpen,
placement,
highlightBorderColor,
}: {
children: ReactNode;
isOpen: boolean;
placement: PlacementType;
highlightBorderColor: string | null | undefined;
}) {
const [show, setShow] = useState(false);
const highlightBorderColorStyle = useMemo(() => {
if (!highlightBorderColor) return {};
return {
border: `2px solid ${highlightBorderColor}`,
overflow: "hidden",
};
}, [highlightBorderColor]);
useEffect(() => {
setShow(isOpen);
}, [isOpen]);
@@ -23,9 +34,10 @@ export default function Modal({
<div
className={cn(
show ? "translate-x-0 opacity-100" : "translate-x-32 opacity-0",
"pointer-events-auto absolute max-h-[90%] w-full max-w-sm overflow-hidden overflow-y-auto rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out",
"pointer-events-auto absolute h-fit max-h-[90%] w-full max-w-sm overflow-hidden overflow-y-auto rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out",
getPlacementStyle(placement)
)}>
)}
style={highlightBorderColorStyle}>
{children}
</div>
</div>
@@ -39,19 +39,21 @@ export default function MultipleChoiceMultiQuestion({
.map((choice) => choice.label);
useEffect(() => {
if(Array.isArray(storedResponseValue)){
if (Array.isArray(storedResponseValue)) {
const nonOtherSavedChoices = storedResponseValue?.filter((answer) =>
nonOtherChoiceLabels.includes(answer)
);
const savedOtherSpecified = storedResponseValue?.find((answer) => !nonOtherChoiceLabels.includes(answer));
nonOtherChoiceLabels.includes(answer)
);
const savedOtherSpecified = storedResponseValue?.find(
(answer) => !nonOtherChoiceLabels.includes(answer)
);
setSelectedChoices(nonOtherSavedChoices ?? []);
setSelectedChoices(nonOtherSavedChoices ?? []);
if (savedOtherSpecified) {
setOtherSpecified(savedOtherSpecified);
setShowOther(true);
if (savedOtherSpecified) {
setOtherSpecified(savedOtherSpecified);
setShowOther(true);
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [storedResponseValue, question.id]);
+1 -1
View File
@@ -25,7 +25,7 @@ export default function AlertDialog({
return (
<Modal open={open} setOpen={setOpen} title={`Confirm ${confirmWhat}`}>
<p>{text || "Are you sure? This action cannot be undone."}</p>
<div className="my-4 space-x-2 text-right">
<div className="space-x-2 text-right">
<Button variant="warn" onClick={onDiscard}>
Discard
</Button>
+1 -1
View File
@@ -32,7 +32,7 @@ export default function DeleteDialog({
<Modal open={open} setOpen={setOpen} title={`Delete ${deleteWhat}`}>
<p>{text || "Are you sure? This action cannot be undone."}</p>
<div>{children}</div>
<div className="my-4 space-x-2 text-right">
<div className="space-x-2 text-right">
<Button
variant="secondary"
onClick={() => {
+2 -2
View File
@@ -27,9 +27,9 @@ export const useSurveys = (environmentId: string) => {
};
};
export const useSurvey = (environmentId: string, id: string) => {
export const useSurvey = (environmentId: string, id: string, analytics?: boolean) => {
const { data, error, mutate, isLoading } = useSWR(
`/api/v1/environments/${environmentId}/surveys/${id}`,
`/api/v1/environments/${environmentId}/surveys/${id}${analytics ? "?analytics=true" : ""}`,
fetcher
);
@@ -9,6 +9,8 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
const surveyId = req.query.surveyId?.toString();
const analytics = req.query.analytics?.toString() === "true";
if (environmentId === undefined) {
return res.status(400).json({ message: "Missing environmentId" });
}
@@ -31,6 +33,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
include: {
triggers: true,
attributeFilters: true,
_count: analytics ? { select: { responses: { where: { finished: true } } } } : false,
},
});
@@ -83,6 +86,8 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
const body = { ...req.body };
delete body.updatedAt;
// preventing issue with unknowingly updating analytics
delete body._count;
// delete unused fields for link surveys
if (body.type === "link") {
+2 -6
View File
@@ -17,12 +17,8 @@ services:
- postgres
env_file:
- .env
labels:
- "traefik.enable=true"
- "traefik.http.routers.formbricks.rule=Host(`api.example.com`)" # TODO: Change with your own domain
- "traefik.http.routers.formbricks.tls.certresolver=default"
- "traefik.http.routers.formbricks.entrypoints=websecure"
- "traefik.http.services.formbricks.loadbalancer.server.port=3000"
ports:
- 3000:3000
volumes:
postgres:
+1 -1
View File
@@ -12,7 +12,7 @@
"scripts": {
"clean": "turbo run clean && rimraf node_modules",
"build": "turbo run build",
"prebuild": "turbo run prebuild",
"post-install": "turbo run post-install",
"db:migrate:dev": "turbo run db:migrate:dev",
"db:migrate:deploy": "turbo run db:migrate:deploy",
"db:migrate:vercel": "turbo run db:migrate:vercel",
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Product" ADD COLUMN "highlightBorderColor" TEXT;
+2 -2
View File
@@ -20,8 +20,8 @@
"format": "prisma format",
"generate": "prisma generate",
"lint": "eslint ./src --fix",
"prebuild": "npm run generate",
"predev": "npm run generate"
"post-install": "pnpm generate",
"predev": "pnpm generate"
},
"dependencies": {
"@prisma/client": "^5.0.0",
+14 -13
View File
@@ -319,19 +319,20 @@ enum WidgetPlacement {
}
model Product {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId String
environments Environment[]
brandColor String @default("#64748b")
recontactDays Int @default(7)
formbricksSignature Boolean @default(true)
placement WidgetPlacement @default(bottomRight)
clickOutsideClose Boolean @default(true)
darkOverlay Boolean @default(false)
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId String
environments Environment[]
brandColor String @default("#64748b")
highlightBorderColor String?
recontactDays Int @default(7)
formbricksSignature Boolean @default(true)
placement WidgetPlacement @default(bottomRight)
clickOutsideClose Boolean @default(true)
darkOverlay Boolean @default(false)
}
enum Plan {
+1
View File
@@ -32,6 +32,7 @@ export default function App({ config, survey, closeSurvey, errorHandler }: AppPr
close={close}
placement={config.state.product.placement}
darkOverlay={config.state.product.darkOverlay}
highlightBorderColor={config.state.product.highlightBorderColor}
clickOutside={config.state.product.clickOutsideClose}>
<SurveyView config={config} survey={survey} close={close} errorHandler={errorHandler} />
</Modal>
+15 -2
View File
@@ -1,6 +1,6 @@
import type { PlacementType } from "@formbricks/types/js";
import { h, VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { cn } from "../lib/utils";
export default function Modal({
@@ -9,6 +9,7 @@ export default function Modal({
placement,
clickOutside,
darkOverlay,
highlightBorderColor,
close,
}: {
children: VNode;
@@ -16,6 +17,7 @@ export default function Modal({
placement: PlacementType;
clickOutside: boolean;
darkOverlay: boolean;
highlightBorderColor: string | null;
close: () => void;
}) {
const [show, setShow] = useState(false);
@@ -57,6 +59,17 @@ export default function Modal({
}
};
const highlightBorderColorStyle = useMemo(() => {
if (!highlightBorderColor) return {};
return {
borderRadius: "8px",
border: "2px solid",
overflow: "hidden",
borderColor: highlightBorderColor,
};
}, [highlightBorderColor]);
return (
<div
aria-live="assertive"
@@ -97,7 +110,7 @@ export default function Modal({
</svg>
</button>
</div>
<div className="">{children}</div>
<div style={highlightBorderColorStyle}>{children}</div>
</div>
</div>
</div>
+1
View File
@@ -18,3 +18,4 @@ export const WEBAPP_URL =
// Other
export const INTERNAL_SECRET = process.env.INTERNAL_SECRET || "";
export const CRON_SECRET = process.env.CRON_SECRET;
export const DEFAULT_BRAND_COLOR = "#64748b";
+16
View File
@@ -8,6 +8,21 @@ import { ZProduct } from "@formbricks/types/v1/product";
import type { TProduct } from "@formbricks/types/v1/product";
import { cache } from "react";
const selectProduct = {
id: true,
createdAt: true,
updatedAt: true,
name: true,
teamId: true,
brandColor: true,
highlightBorderColor: true,
recontactDays: true,
formbricksSignature: true,
placement: true,
clickOutsideClose: true,
darkOverlay: true,
};
export const getProductByEnvironmentId = cache(async (environmentId: string): Promise<TProduct> => {
let productPrisma;
try {
@@ -19,6 +34,7 @@ export const getProductByEnvironmentId = cache(async (environmentId: string): Pr
},
},
},
select: selectProduct,
});
if (!productPrisma) {
+1
View File
@@ -33,6 +33,7 @@ export interface Survey {
autoComplete: number | null;
surveyClosedMessage: SurveyClosedMessage | null;
closeOnDate: Date | null;
_count: { responses: number | null } | null;
}
export interface AttributeFilter {
+4
View File
@@ -7,6 +7,10 @@ export const ZProduct = z.object({
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})$/)
.nullish(),
recontactDays: z.number().int(),
formbricksSignature: z.boolean(),
placement: z.enum(["bottomLeft", "bottomRight", "topLeft", "topRight", "center"]),
+31
View File
@@ -0,0 +1,31 @@
import * as React from "react";
import { cn } from "@formbricks/lib/cn";
import { Search } from "lucide-react";
export interface InputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "crossOrigin" | "dangerouslySetInnerHTML"> {
crossOrigin?: "" | "anonymous" | "use-credentials" | undefined;
dangerouslySetInnerHTML?: {
__html: string;
};
}
const SearchBox = React.forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
return (
<div className="relative">
<input
className={cn(
"focus:border-brand flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-5 pr-10 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:text-slate-300",
className
)}
ref={ref}
{...props}
/>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<Search className="h-5 w-5 text-gray-400" />
</div>
</div>
);
});
export { SearchBox };
+1
View File
@@ -59,6 +59,7 @@ export {
} from "./components/Command";
export { Calendar } from "./components/Calendar";
export { DatePicker } from "./components/DatePicker";
export { SearchBox } from "./components/SearchBox";
/* Icons */
export { AngryBirdRageIcon } from "./components/icons/AngryBirdRageIcon";
+100 -45
View File
@@ -19,7 +19,7 @@ importers:
version: 3.12.7
turbo:
specifier: latest
version: 1.10.3
version: 1.10.7
apps/demo:
dependencies:
@@ -352,7 +352,7 @@ importers:
version: 8.10.0(eslint@8.46.0)
eslint-config-turbo:
specifier: latest
version: 1.10.7(eslint@8.46.0)
version: 1.8.8(eslint@8.46.0)
eslint-plugin-react:
specifier: 7.33.1
version: 7.33.1(eslint@8.46.0)
@@ -1205,7 +1205,7 @@ packages:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.22.9
'@babel/helper-create-class-features-plugin': 7.22.6(@babel/core@7.22.9)
'@babel/helper-create-class-features-plugin': 7.20.5(@babel/core@7.22.9)
'@babel/helper-plugin-utils': 7.22.5
transitivePeerDependencies:
- supports-color
@@ -1962,7 +1962,7 @@ packages:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.22.9
'@babel/plugin-transform-react-jsx': 7.22.5(@babel/core@7.22.9)
'@babel/plugin-transform-react-jsx': 7.19.0(@babel/core@7.22.9)
dev: true
/@babel/plugin-transform-react-jsx@7.19.0(@babel/core@7.22.9):
@@ -2271,7 +2271,7 @@ packages:
'@babel/helper-plugin-utils': 7.22.5
'@babel/helper-validator-option': 7.22.5
'@babel/plugin-transform-react-display-name': 7.18.6(@babel/core@7.22.9)
'@babel/plugin-transform-react-jsx': 7.22.5(@babel/core@7.22.9)
'@babel/plugin-transform-react-jsx': 7.19.0(@babel/core@7.22.9)
'@babel/plugin-transform-react-jsx-development': 7.18.6(@babel/core@7.22.9)
'@babel/plugin-transform-react-pure-annotations': 7.18.6(@babel/core@7.22.9)
dev: true
@@ -3227,7 +3227,7 @@ packages:
resolution: {integrity: sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==}
dependencies:
'@jridgewell/gen-mapping': 0.3.2
'@jridgewell/trace-mapping': 0.3.18
'@jridgewell/trace-mapping': 0.3.17
/@jridgewell/sourcemap-codec@1.4.14:
resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
@@ -3237,7 +3237,6 @@ packages:
dependencies:
'@jridgewell/resolve-uri': 3.1.0
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@jridgewell/trace-mapping@0.3.18:
resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==}
@@ -3583,6 +3582,10 @@ packages:
resolution: {integrity: sha512-RmHanbV21saP/6OEPBJ7yJMuys68cIf8OBBWd7+uj40LdpmswVAwe1uzeuFyUsd6SfeITWT3XnQfn6wULeKwDQ==}
dev: false
/@next/env@13.4.8:
resolution: {integrity: sha512-twuSf1klb3k9wXI7IZhbZGtFCWvGD4wXTY2rmvzIgVhXhs7ISThrbNyutBx3jWIL8Y/Hk9+woytFz5QsgtcRKQ==}
dev: false
/@next/eslint-plugin-next@13.4.12:
resolution: {integrity: sha512-6rhK9CdxEgj/j1qvXIyLTWEaeFv7zOK8yJMulz3Owel0uek0U9MJCGzmKgYxM3aAUBo3gKeywCZKyQnJKto60A==}
dependencies:
@@ -6423,6 +6426,11 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
/acorn@8.8.1:
resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==}
engines: {node: '>=0.4.0'}
hasBin: true
/agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
@@ -6863,8 +6871,8 @@ packages:
peerDependencies:
postcss: ^8.1.0
dependencies:
browserslist: 4.21.9
caniuse-lite: 1.0.30001512
browserslist: 4.21.5
caniuse-lite: 1.0.30001466
fraction.js: 4.2.0
normalize-range: 0.1.2
picocolors: 1.0.0
@@ -6879,8 +6887,8 @@ packages:
peerDependencies:
postcss: ^8.1.0
dependencies:
browserslist: 4.21.9
caniuse-lite: 1.0.30001512
browserslist: 4.21.5
caniuse-lite: 1.0.30001466
fraction.js: 4.2.0
normalize-range: 0.1.2
picocolors: 1.0.0
@@ -7370,6 +7378,17 @@ packages:
pako: 1.0.11
dev: true
/browserslist@4.21.5:
resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001512
electron-to-chromium: 1.4.284
node-releases: 2.0.10
update-browserslist-db: 1.0.10(browserslist@4.21.5)
dev: true
/browserslist@4.21.9:
resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -7611,6 +7630,10 @@ packages:
lodash.uniq: 4.5.0
dev: true
/caniuse-lite@1.0.30001466:
resolution: {integrity: sha512-ewtFBSfWjEmxUgNBSZItFSmVtvk9zkwkl1OfRZlKA8slltRN+/C/tuGVrF9styXkN36Yu3+SeJ1qkXxDEyNZ5w==}
dev: true
/caniuse-lite@1.0.30001512:
resolution: {integrity: sha512-2S9nK0G/mE+jasCUsMPlARhRCts1ebcp2Ji8Y8PWi4NDE1iRdLCnEPHkEfeBrGC45L4isBx5ur3IQ6yTE2mRZw==}
@@ -8923,8 +8946,8 @@ packages:
mimic-response: 3.1.0
dev: false
/dedent@1.5.1:
resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==}
/dedent@1.5.0:
resolution: {integrity: sha512-3sSQTYoWKGcRHmHl6Y6opLpRJH55bxeGQ0Y1LCI5pZzUXvokVkj0FC4bi7uEwazxA9FQZ0Nv067Zt5kSUvXxEA==}
peerDependencies:
babel-plugin-macros: ^3.1.0
peerDependenciesMeta:
@@ -9293,6 +9316,10 @@ packages:
jake: 10.8.5
dev: true
/electron-to-chromium@1.4.284:
resolution: {integrity: sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==}
dev: true
/electron-to-chromium@1.4.450:
resolution: {integrity: sha512-BLG5HxSELlrMx7dJ2s+8SFlsCtJp37Zpk2VAxyC6CZtbc+9AJeZHfYHbrlSgdXp6saQ8StMqOTEDaBKgA7u1sw==}
@@ -9788,7 +9815,7 @@ packages:
'@babel/eslint-parser': 7.19.1(@babel/core@7.22.9)(eslint@8.46.0)
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.22.9)
'@babel/plugin-syntax-decorators': 7.19.0(@babel/core@7.22.9)
'@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.22.9)
'@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.22.9)
eslint: 8.46.0
eslint-plugin-compat: 4.1.2(eslint@8.46.0)
eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@6.2.1)(eslint@8.46.0)(jest@29.6.2)(typescript@5.1.6)
@@ -9810,13 +9837,13 @@ packages:
eslint: 8.46.0
dev: true
/eslint-config-turbo@1.10.7(eslint@8.46.0):
resolution: {integrity: sha512-0yHt5UlXVph8S4SOvP6gYehLvYjJj6XFKTYOG/WUQbjlcF0OU4pOT1a1juqmmBPWYlvJ0evt7v+RekY4tOopPQ==}
/eslint-config-turbo@1.8.8(eslint@8.46.0):
resolution: {integrity: sha512-+yT22sHOT5iC1sbBXfLIdXfbZuiv9bAyOXsxTxFCWelTeFFnANqmuKB3x274CFvf7WRuZ/vYP/VMjzU9xnFnxA==}
peerDependencies:
eslint: '>6.6.0'
dependencies:
eslint: 8.46.0
eslint-plugin-turbo: 1.10.7(eslint@8.46.0)
eslint-plugin-turbo: 1.8.8(eslint@8.46.0)
dev: true
/eslint-import-resolver-node@0.3.6:
@@ -10004,12 +10031,11 @@ packages:
semver: 6.3.1
string.prototype.matchall: 4.0.8
/eslint-plugin-turbo@1.10.7(eslint@8.46.0):
resolution: {integrity: sha512-YikBHc75DY9VV1vAFUIBekHLQlxqVT5zTNibK8zBQInCUhF7PvyPJc0xXw5FSz8EYtt4uOV3r0Km3CmFRclS4Q==}
/eslint-plugin-turbo@1.8.8(eslint@8.46.0):
resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==}
peerDependencies:
eslint: '>6.6.0'
dependencies:
dotenv: 16.0.3
eslint: 8.46.0
dev: true
@@ -12301,7 +12327,7 @@ packages:
'@types/node': 20.4.6
chalk: 4.1.2
co: 4.6.0
dedent: 1.5.1
dedent: 1.5.0
is-generator-fn: 2.1.0
jest-each: 29.6.2
jest-matcher-utils: 29.6.2
@@ -12395,7 +12421,7 @@ packages:
chalk: 4.1.2
diff-sequences: 29.4.3
jest-get-type: 29.4.3
pretty-format: 29.6.2
pretty-format: 29.6.1
dev: true
/jest-diff@29.6.2:
@@ -12764,12 +12790,26 @@ packages:
chalk: 5.3.0
jest: 29.6.2
jest-regex-util: 29.4.3
jest-watcher: 29.6.2
jest-watcher: 29.6.1
slash: 5.1.0
string-length: 5.0.1
strip-ansi: 7.0.1
dev: true
/jest-watcher@29.6.1:
resolution: {integrity: sha512-d4wpjWTS7HEZPaaj8m36QiaP856JthRZkrgcIY/7ISoUWPIillrXM23WPboZVLbiwZBt4/qn2Jke84Sla6JhFA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/test-result': 29.6.2
'@jest/types': 29.6.1
'@types/node': 20.4.6
ansi-escapes: 4.3.2
chalk: 4.1.2
emittery: 0.13.1
jest-util: 29.6.2
string-length: 4.0.2
dev: true
/jest-watcher@29.6.2:
resolution: {integrity: sha512-GZitlqkMkhkefjfN/p3SJjrDaxPflqxEAv3/ik10OirZqJGYH5rPiIsgVcfof0Tdqg3shQGdEIxDBx+B4tuLzA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -14616,7 +14656,7 @@ packages:
next: '*'
dependencies:
'@corex/deepmerge': 4.0.43
'@next/env': 13.4.12
'@next/env': 13.4.8
fast-glob: 3.2.12
minimist: 1.2.8
next: 13.4.12(react-dom@18.2.0)(react@18.2.0)
@@ -14785,6 +14825,10 @@ packages:
vm-browserify: 1.1.2
dev: true
/node-releases@2.0.10:
resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==}
dev: true
/node-releases@2.0.12:
resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==}
@@ -19425,7 +19469,7 @@ packages:
hasBin: true
dependencies:
'@jridgewell/source-map': 0.3.2
acorn: 8.10.0
acorn: 8.8.1
commander: 2.20.3
source-map-support: 0.5.21
@@ -19766,65 +19810,65 @@ packages:
dependencies:
safe-buffer: 5.2.1
/turbo-darwin-64@1.10.3:
resolution: {integrity: sha512-IIB9IomJGyD3EdpSscm7Ip1xVWtYb7D0x7oH3vad3gjFcjHJzDz9xZ/iw/qItFEW+wGFcLSRPd+1BNnuLM8AsA==}
/turbo-darwin-64@1.10.7:
resolution: {integrity: sha512-N2MNuhwrl6g7vGuz4y3fFG2aR1oCs0UZ5HKl8KSTn/VC2y2YIuLGedQ3OVbo0TfEvygAlF3QGAAKKtOCmGPNKA==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/turbo-darwin-arm64@1.10.3:
resolution: {integrity: sha512-SBNmOZU9YEB0eyNIxeeQ+Wi0Ufd+nprEVp41rgUSRXEIpXjsDjyBnKnF+sQQj3+FLb4yyi/yZQckB+55qXWEsw==}
/turbo-darwin-arm64@1.10.7:
resolution: {integrity: sha512-WbJkvjU+6qkngp7K4EsswOriO3xrNQag7YEGRtfLoDdMTk4O4QTeU6sfg2dKfDsBpTidTvEDwgIYJhYVGzrz9Q==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/turbo-linux-64@1.10.3:
resolution: {integrity: sha512-kvAisGKE7xHJdyMxZLvg53zvHxjqPK1UVj4757PQqtx9dnjYHSc8epmivE6niPgDHon5YqImzArCjVZJYpIGHQ==}
/turbo-linux-64@1.10.7:
resolution: {integrity: sha512-x1CF2CDP1pDz/J8/B2T0hnmmOQI2+y11JGIzNP0KtwxDM7rmeg3DDTtDM/9PwGqfPotN9iVGgMiMvBuMFbsLhg==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/turbo-linux-arm64@1.10.3:
resolution: {integrity: sha512-Qgaqln0IYRgyL0SowJOi+PNxejv1I2xhzXOI+D+z4YHbgSx87ox1IsALYBlK8VRVYY8VCXl+PN12r1ioV09j7A==}
/turbo-linux-arm64@1.10.7:
resolution: {integrity: sha512-JtnBmaBSYbs7peJPkXzXxsRGSGBmBEIb6/kC8RRmyvPAMyqF8wIex0pttsI+9plghREiGPtRWv/lfQEPRlXnNQ==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/turbo-windows-64@1.10.3:
resolution: {integrity: sha512-rbH9wManURNN8mBnN/ZdkpUuTvyVVEMiUwFUX4GVE5qmV15iHtZfDLUSGGCP2UFBazHcpNHG1OJzgc55GFFrUw==}
/turbo-windows-64@1.10.7:
resolution: {integrity: sha512-7A/4CByoHdolWS8dg3DPm99owfu1aY/W0V0+KxFd0o2JQMTQtoBgIMSvZesXaWM57z3OLsietFivDLQPuzE75w==}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/turbo-windows-arm64@1.10.3:
resolution: {integrity: sha512-ThlkqxhcGZX39CaTjsHqJnqVe+WImjX13pmjnpChz6q5HHbeRxaJSFzgrHIOt0sUUVx90W/WrNRyoIt/aafniw==}
/turbo-windows-arm64@1.10.7:
resolution: {integrity: sha512-D36K/3b6+hqm9IBAymnuVgyePktwQ+F0lSXr2B9JfAdFPBktSqGmp50JNC7pahxhnuCLj0Vdpe9RqfnJw5zATA==}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/turbo@1.10.3:
resolution: {integrity: sha512-U4gKCWcKgLcCjQd4Pl8KJdfEKumpyWbzRu75A6FCj6Ctea1PIm58W6Ltw1QXKqHrl2pF9e1raAskf/h6dlrPCA==}
/turbo@1.10.7:
resolution: {integrity: sha512-xm0MPM28TWx1e6TNC3wokfE5eaDqlfi0G24kmeHupDUZt5Wd0OzHFENEHMPqEaNKJ0I+AMObL6nbSZonZBV2HA==}
hasBin: true
requiresBuild: true
optionalDependencies:
turbo-darwin-64: 1.10.3
turbo-darwin-arm64: 1.10.3
turbo-linux-64: 1.10.3
turbo-linux-arm64: 1.10.3
turbo-windows-64: 1.10.3
turbo-windows-arm64: 1.10.3
turbo-darwin-64: 1.10.7
turbo-darwin-arm64: 1.10.7
turbo-linux-64: 1.10.7
turbo-linux-arm64: 1.10.7
turbo-windows-64: 1.10.7
turbo-windows-arm64: 1.10.7
dev: true
/tween-functions@1.2.0:
@@ -20158,6 +20202,17 @@ packages:
engines: {node: '>=4'}
dev: true
/update-browserslist-db@1.0.10(browserslist@4.21.5):
resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
dependencies:
browserslist: 4.21.5
escalade: 3.1.1
picocolors: 1.0.0
dev: true
/update-browserslist-db@1.0.11(browserslist@4.21.9):
resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
hasBin: true
+11 -3
View File
@@ -1,6 +1,10 @@
{
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"@formbricks/database#build": {
"cache": false,
"dependsOn": ["post-install"]
},
"@formbricks/web#go": {
"cache": false,
"persistent": true,
@@ -77,6 +81,13 @@
"VERCEL_URL"
]
},
"post-install": {
"cache": false,
"dependsOn": [],
"outputs": [],
"inputs": ["./schema.prisma"],
"env": ["PRISMA_GENERATE_DATAPROXY"]
},
"db:setup": {
"cache": false,
"outputs": []
@@ -85,9 +96,6 @@
"persistent": true,
"cache": false
},
"prebuild": {
"outputs": []
},
"db:migrate:dev": {
"outputs": []
},