Merge remote-tracking branch 'upstream/main' into feature/survey-back-button

This commit is contained in:
tykerr
2023-07-20 20:56:17 +07:00
262 changed files with 16033 additions and 7821 deletions

View File

@@ -1,4 +1,4 @@
Copyright (c) 2023 Matthias Nannt, Johannes Dancker
Copyright (c) 2023 Formbricks GmbH
Portions of this software are licensed as follows:

View File

@@ -13,7 +13,7 @@
"dependencies": {
"@formbricks/js": "workspace:*",
"@heroicons/react": "^2.0.18",
"next": "13.4.9",
"next": "13.4.10",
"react": "18.2.0",
"react-dom": "18.2.0"
},

View File

@@ -5,7 +5,10 @@ import Link from "next/link";
export const GitHubSponsorship: React.FC = () => {
return (
<div className="xs:mx-auto xs:w-full relative mx-auto my-4 mb-12 mt-12 rounded-xl bg-gradient-to-br from-slate-100 to-slate-200 px-4 py-8 dark:from-slate-800 dark:via-slate-800 dark:to-slate-700 sm:px-6 sm:pb-12 sm:pt-8 md:max-w-none lg:mt-6 lg:px-8 lg:pt-8 ">
<div className="mx-4 my-4 mb-12 mt-12 rounded-xl bg-gradient-to-br from-slate-100 to-slate-200 px-4 py-8 dark:from-slate-800 dark:via-slate-800 dark:to-slate-700 sm:px-6 sm:pb-12 sm:pt-8 md:max-w-none lg:mt-6 lg:px-8 lg:pt-8">
<style jsx>{`
@media (min-width: 426px);
`}</style>
<div className="right-10 lg:absolute">
<Image
src={GitHubMarkDark}

View File

@@ -22,7 +22,7 @@ export const Hero: React.FC = ({}) => {
<a
href="https://github.com/formbricks/formbricks"
target="_blank"
className="border-brand-dark rounded-full border px-6 py-1.5 text-sm text-slate-500 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800">
className="border-brand-dark rounded-full border px-4 py-1.5 text-sm text-slate-500 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800">
We&apos;re Open-Source | Star us on GitHub{" "}
<ChevronRightIcon className="inline h-5 w-5 text-slate-300" />
</a>
@@ -43,7 +43,7 @@ export const Hero: React.FC = ({}) => {
<p className="hidden whitespace-nowrap pt-3 text-xs text-slate-400 dark:text-slate-500 md:block">
Trusted by
</p>
<div className="grid grid-cols-3 items-center gap-8 pt-2 md:grid-cols-4">
<div className="grid grid-cols-4 items-center gap-5 pt-2 md:gap-8">
<Image
src={CalLogoLight}
alt="Cal Logo"

View File

@@ -0,0 +1,36 @@
import Image from "next/image";
import AuthorJohannes from "@/images/blog/johannes-co-founder-formbricks-small.jpg";
interface AuthorBoxProps {
name: string;
title: string;
date: string;
duration: string;
}
export default function AuthorBox({ name, title, date, duration }: AuthorBoxProps) {
return (
<div className="mb-8 flex items-center space-x-4 rounded-lg border border-slate-200 bg-slate-100 px-6 py-3 dark:border-slate-700 dark:bg-slate-800">
<Image
className="m-0 rounded-full"
src={AuthorJohannes}
alt={name}
width={45}
height={45}
quality={100}
placeholder="blur"
style={{ objectFit: "contain" }}
/>
<div className="flex w-full items-end justify-between">
<div>
<p className="m-0 font-medium text-slate-600 dark:text-slate-300">{name}</p>
<p className="m-0 text-sm text-slate-400">{title}</p>
</div>
<div className="text-right">
<p className="m-0 font-medium text-slate-600 dark:text-slate-300">{duration} Minutes</p>
<p className="m-0 text-sm text-slate-400">{date}</p>
</div>
</div>
</div>
);
}

View File

@@ -1,5 +1,4 @@
import clsx from "clsx";
import { Icon } from "@/components/shared/Icon";
const styles = {

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -21,12 +21,12 @@
"@mapbox/rehype-prism": "^0.8.0",
"@mdx-js/loader": "^2.3.0",
"@mdx-js/react": "^2.3.0",
"@next/mdx": "^13.4.9",
"@next/mdx": "^13.4.10",
"@paralleldrive/cuid2": "^2.2.1",
"clsx": "^1.2.1",
"clsx": "^2.0.0",
"lottie-web": "^5.12.2",
"next": "13.4.9",
"next-plausible": "^3.9.1",
"next": "13.4.10",
"next-plausible": "^3.10.0",
"next-sitemap": "^4.1.8",
"prism-react-renderer": "^2.0.6",
"prismjs": "^1.29.0",
@@ -35,7 +35,7 @@
"react-icons": "^4.10.1",
"react-responsive-embed": "^2.1.0",
"remark-gfm": "^3.0.1",
"sharp": "^0.32.2"
"sharp": "^0.32.3"
},
"devDependencies": {
"@formbricks/tsconfig": "workspace:*",

View File

@@ -6,6 +6,7 @@ import LimeSurvey from "./free-survey-tool-limesurvey-open-source-software-opens
import OpnForm from "./opnform-free-open-source-form-survey-tools-builder-2023-self-hostign.jpg";
import HeaderImage from "./2023-title-best-open-source-survey-software-tools-and-alternatives.png";
import SurveyJS from "./surveyjs-free-opensource-form-survey-tool-software-to-make-surveys-2023.png";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "Best Open-source Form & Survey Tools (still maintained in 2023)",
@@ -14,6 +15,8 @@ export const meta = {
date: "2023-04-12",
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
_Most open-source projects get abandoned after a while. But these 5 open-source form and survey tools are still alive and kicking in 2023._
<Image

View File

@@ -5,6 +5,7 @@ import Demo from "./our-experience-github-acc-demo-screenshot.png";
import Mail from "./github-accelerator-selection-mail.png";
import Teams from "./github-accelerator-2022-teams.png";
import NewsletterSignup from "@/components/shared/NewsletterSignup";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "Our GitHub Accelerator Experience 👀",
@@ -13,6 +14,8 @@ export const meta = {
date: "2023-04-13",
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
_We were among the first 20 teams ever to run through the Open-Source Accelerator by Github. Read about our experience and if we would do it again:_
<Image

View File

@@ -2,6 +2,7 @@ import Image from "next/image";
import LayoutMdx from "@/components/shared/LayoutMdx";
import TitleImage from "./formbricks-sponsored-by-github-accelerator-2023.webp";
import NewsletterSignup from "@/components/shared/NewsletterSignup";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "Formbricks Joins GitHub Accelerator's Inaugural Cohort 💃",
@@ -10,6 +11,8 @@ export const meta = {
date: "2023-04-13",
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
_We're getting ready to take our open-source experience management platform to new heights, thanks to being part of the first-ever GitHub Accelerator program!_
<Image

View File

@@ -4,6 +4,7 @@ import RobinHoodMeme from "./robin-hood-meme.png";
import WhyWeDoIt from "./why-we-do-it.png";
import EverythinEverywhereAllAtOnce from "./everything_everywhere_all_at_once.png";
import ResponsiveEmbed from "react-responsive-embed";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "Open source forms will save the world.",
@@ -11,6 +12,8 @@ export const meta = {
date: "2022-08-26",
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
<Image src={RobinHoodMeme} alt="Robin Hood Meme" className="rounded-lg" />
_What motivates us to build open source tech in such a crowded space? What do we see what others might not? And how do we understand the relationship between free open source tech and a commercial complement?_

View File

@@ -9,6 +9,7 @@ import Wrestling from "./wrestling.jpg";
import TypeformValue from "./typeform-value-prop.png";
import ResponsiveEmbed from "react-responsive-embed";
import { Callout } from "@/components/shared/Callout";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "Why Qualtrics beats Typeform, especially Open-Source",
@@ -17,6 +18,8 @@ export const meta = {
date: "2023-03-24",
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
<Image src={Wrestling} alt="Why we do it" className="rounded-lg" />
_In September, we kicked it off with a Typeform open-source alternative. As we build and learn, our focus is shifting. We talk about how we look at form and survey tools today, why experience management not only matters for enterprise and why the endgame looks a lot more like open-source Qualtrics. Qualtrics? What happened to Typeform?_

View File

@@ -2,6 +2,7 @@ import Image from "next/image";
import LayoutMdx from "@/components/shared/LayoutMdx";
import HeaderImage from "./formbricks-logo-header-open-source-form-infrastructure.svg";
import HeroAnimation from "../../../components/shared/HeroAnimation.tsx";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "snoopForms → Formbricks 🎉",
@@ -9,6 +10,8 @@ export const meta = {
date: "2022-11-07",
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
<Image src={HeaderImage} alt="Formbricks - Open Source Forms and Surveys" className="rounded-lg" />
_It has been quiet in the past weeks, but we didn't spend our days sitting around. Find out what we were up to and where we are taking Formbricks from here._

View File

@@ -1,6 +1,7 @@
import Image from "next/image";
import LayoutMdx from "@/components/shared/LayoutMdx";
import { Callout } from "@/components/shared/Callout";
import AuthorBox from "@/components/shared/AuthorBox";
import TweetPeer from "./peer-tweet-typeform-open-source.png";
import SnoopForms from "./snoopforms-open-source-typeform-alternative.png";
@@ -17,6 +18,8 @@ export const meta = {
date: "2023-07-14",
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
_A lot has happened since Matti and I had a chat about open-source surveys in May last year. The release of Formbricks v1.0 is a perfect opportunity to look back on how it all started._
Funnily enough, it started with a tweet:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 976 KiB

View File

@@ -1,47 +0,0 @@
import Image from "next/image";
import LayoutMdx from "@/components/shared/LayoutMdx";
import FormbricksSneak from "./formbricks-sneak.png";
import ResponsiveEmbed from "react-responsive-embed";
export const meta = {
title: "Video: Walk-through of the new Formbricks",
description: "The new, powerful Formbricks is almost ready!",
date: "2023-03-30",
};
_The new, powerful Formbricks is almost ready!_
<Image src={FormbricksSneak} alt="Sneakpeek into what the new Formbricks can do" className="rounded-lg" />
We've been working hard on getting a revamped Formbricks ready - we're almost there!
What you can do with it:
1. Design **any survey** you want
2. Trigger at any point in your app both **No Code** (page view, element click) and **Code** (hook `formbricks.track` into your event)
3. Pass custom user attributes to Formbricks to **segment your user base**
## Have a look:
<ResponsiveEmbed
src="https://www.tella.tv/video/clfrymq2f00sk0fjqd9r6btf1/embed?b=0&title=0&a=1&loop=0&t=0&muted=0"
allowFullScreen
className="rounded-lg"
/>
Formbricks is a lot more powerful than ever before! :mechanical_arm:
You can create:
- Onboarding surveys,
- PMF surveys,
- Churn surveys,
- Feature chaser,
- Feedback box,
- Identify customer goals,
- Measure task completion,
- etc, etc.
## Stay tuned, Formbricks Cloud goes live soon!
export default ({ children }) => <LayoutMdx meta={meta}>{children}</LayoutMdx>;

View File

@@ -6,6 +6,7 @@ import TitleImage from "./title-image.png";
import HeaderImage from "./formbricks-logo.svg";
import ProprietaryDependence from "./propietary-dependence.jpeg";
import ResponsiveEmbed from "react-responsive-embed";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "Why Open-Source + No-Code is the Future of Enterprise & Goverment Software",
@@ -14,6 +15,8 @@ export const meta = {
date: "2022-06-03",
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
<Image src={TitleImage} alt="Title Image" className="rounded-lg" />
_Open source software (OSS) beats out proprietary software in every regard - except for value capturing. No-Code tools shorten the feedback loop between builders and consumers, kicking productivity through the roof. Here is why a no-code interface is cheatcode for OSS and why particularly large corporations and governments are to benefit the most._

View File

@@ -3,12 +3,13 @@ import Layout from "@/components/shared/Layout";
import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
import { CheckBadgeIcon } from "@heroicons/react/24/solid";
import { Button } from "@formbricks/ui";
const XMOffer = [
{
step: "1",
header: "Kick-off call",
description: "You share with our seasoned PMs which areas of your customer experience need improvement.",
header: "Kick-off call (FREE)",
description: "Share with our seasoned PMs which areas of customer experience need improvement.",
},
{
step: "2",
@@ -69,20 +70,29 @@ const ConciergePage = () => {
</div>
))}
<div className="border-b border-t border-slate-300 p-6 text-4xl font-semibold text-slate-800">
<p className="mr-2 font-light">$2.290</p>
<p className="mr-2 font-light">$1.290</p>
</div>
<div className="p-6 text-sm text-slate-800">
<p>
<CheckBadgeIcon className="mr-1 inline h-5 w-5 text-slate-800" />
100% Risk-free: Pay after the kick-off call.
100% Risk-free: Pay after the kick-off call, if you liked it.
</p>
<p>
<CheckBadgeIcon className="mr-1 inline h-5 w-5 text-slate-800" />
Money-back: If you&apos;re not happy, get a full refund.
</p>
</div>
<div className="px-6">
<Button
variant="darkCTA"
className="w-full justify-center"
href="https://cal.com/johannes/kick-off"
target="_blank">
Book free Kick-Off call
</Button>
</div>
</div>
<div className="rounded-xl">
<div className="!mt-0 rounded-xl">
<Cal
calLink="johannes/kick-off"
style={{

View File

@@ -39,7 +39,7 @@ export const meta = {
},
]}
example={`{
"personId: "clfqjny0v000ayzgsycx54a2c",
"personId": "clfqjny0v000ayzgsycx54a2c",
"surveyId": "clfqz1esd0000yzah51trddn8",
"finished": true,
"data": {

View File

@@ -45,18 +45,18 @@ import formbricks from "@formbricks/js";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";
if (typeof window !== "undefined") {
formbricks.init({
environmentId: "clj66eqzu00m5qu0g8leglrns",
apiHost: "https://app.formbricks.com", // e.g. https://app.formbricks.com
debug: true, // remove when in production
});
}
export default function FormbricksProvider() {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
formbricks.init({
environmentId: "clj66eqzu00m5qu0g8leglrns",
apiHost: "https://app.formbricks.com", // e.g. https://app.formbricks.com
debug: true, // remove when in production
});
}, []);
useEffect(() => {
formbricks?.registerRouteChange();
}, [pathname, searchParams]);

View File

@@ -42,7 +42,7 @@ Add the following script to the `<head>` tag of your HTML file:
var t = document.createElement("script");
(t.type = "text/javascript"),
(t.async = !0),
(t.src = "https://unpkg.com/@formbricks/js@^0.1.17/dist/index.umd.js");
(t.src = "https://unpkg.com/@formbricks/js@^1.0.0/dist/index.umd.js");
var e = document.getElementsByTagName("script")[0];
e.parentNode.insertBefore(t, e),
setTimeout(function () {

View File

@@ -6,9 +6,8 @@ export const meta = {
## Information according to § 5 TMG
Johannes Dancker & Matthias Nannt\
Formbricks GmbH\
Kuhnkestr. 6\
c/o Starterkitchen\
24118 Kiel\
Germany

View File

@@ -114,9 +114,8 @@ Please use the following contact information for privacy inquiries:
privacy@formbricks.com
Johannes Dancker & Matthias Nannt<br/>
Formbricks GmbH<br/>
Kuhnkestr. 6<br/>
c/o Starterkitchen<br/>
24118 Kiel<br/>
Germany

View File

@@ -5,14 +5,6 @@ import { formbricksEnabled } from "@/lib/formbricks";
import formbricks from "@formbricks/js";
import { useEffect } from "react";
/* if (typeof window !== "undefined" && formbricksEnabled) {
formbricks.init({
environmentId: env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID || "",
apiHost: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "",
debug: true,
});
} */
export default function FormbricksClient({ session }) {
useEffect(() => {
if (formbricksEnabled && session.user && formbricks) {

View File

@@ -17,8 +17,7 @@ export default function ConfirmationPage() {
<div className="my-6 sm:flex-auto">
<h1 className="text-center text-xl font-semibold text-slate-900">Upgrade successful</h1>
<p className="mt-2 text-center text-sm text-slate-700">
Thanks a lot for upgrading your formbricks subscription. You can now access all features and
improve your user research.
Thanks a lot for upgrading your Formbricks subscription. You have now unlimited access.
</p>
</div>
<Button variant="darkCTA" className="w-full justify-center" href="/">

View File

@@ -1,18 +1,18 @@
import SecondNavbar from "../environments/SecondNavBar";
import SecondNavbar from "@/components/environments/SecondNavBar";
import { CursorArrowRaysIcon, TagIcon } from "@heroicons/react/24/solid";
interface EventsAttributesTabsProps {
interface ActionsAttributesTabsProps {
activeId: string;
environmentId: string;
}
export default function EventsAttributesTabs({ activeId, environmentId }: EventsAttributesTabsProps) {
export default function ActionsAttributesTabs({ activeId, environmentId }: ActionsAttributesTabsProps) {
const tabs = [
{
id: "events",
id: "actions",
label: "Actions",
icon: <CursorArrowRaysIcon />,
href: `/environments/${environmentId}/events`,
href: `/environments/${environmentId}/actions`,
},
{
id: "attributes",

View File

@@ -8,11 +8,11 @@ import { CodeBracketIcon, CursorArrowRaysIcon, SparklesIcon } from "@heroicons/r
interface ActivityTabProps {
environmentId: string;
eventClassId: string;
actionClassId: string;
}
export default function EventActivityTab({ environmentId, eventClassId }: ActivityTabProps) {
const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, eventClassId);
export default function EventActivityTab({ environmentId, actionClassId }: ActivityTabProps) {
const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, actionClassId);
if (isLoadingEventClass) return <LoadingSpinner />;
if (isErrorEventClass) return <ErrorComponent />;

View File

@@ -0,0 +1,79 @@
"use client";
import { Button } from "@formbricks/ui";
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
import { useState } from "react";
import AddNoCodeActionModal from "./AddNoCodeActionModal";
import ActionDetailModal from "./ActionDetailModal";
import { TActionClass } from "@formbricks/types/v1/actionClasses";
export default function ActionClassesTable({
environmentId,
actionClasses,
children: [TableHeading, actionRows],
}: {
environmentId: string;
actionClasses: TActionClass[];
children: [JSX.Element, JSX.Element[]];
}) {
const [isActionDetailModalOpen, setActionDetailModalOpen] = useState(false);
const [isAddActionModalOpen, setAddActionModalOpen] = useState(false);
const [activeActionClass, setActiveActionClass] = useState<TActionClass>({
environmentId,
id: "",
name: "",
type: "noCode",
description: "",
noCodeConfig: null,
createdAt: new Date(),
updatedAt: new Date(),
});
const handleOpenActionDetailModalClick = (e, actionClass: TActionClass) => {
e.preventDefault();
setActiveActionClass(actionClass);
setActionDetailModalOpen(true);
};
return (
<>
<div className="mb-6 text-right">
<Button
variant="darkCTA"
onClick={() => {
setAddActionModalOpen(true);
}}>
<CursorArrowRaysIcon className="mr-2 h-5 w-5 text-white" />
Add Action
</Button>
</div>
<div className="rounded-lg border border-slate-200">
{TableHeading}
<div className="grid-cols-7">
{actionClasses.map((actionClass, index) => (
<button
onClick={(e) => {
handleOpenActionDetailModalClick(e, actionClass);
}}
className="w-full"
key={actionClass.id}>
{actionRows[index]}
</button>
))}
</div>
</div>
<ActionDetailModal
environmentId={environmentId}
open={isActionDetailModalOpen}
setOpen={setActionDetailModalOpen}
actionClass={activeActionClass}
/>
<AddNoCodeActionModal
environmentId={environmentId}
open={isAddActionModalOpen}
setOpen={setAddActionModalOpen}
/>
</>
);
}

View File

@@ -1,31 +1,31 @@
import ModalWithTabs from "@/components/shared/ModalWithTabs";
import { CodeBracketIcon, CursorArrowRaysIcon, SparklesIcon } from "@heroicons/react/24/solid";
import type { EventClass } from "@prisma/client";
import EventActivityTab from "./EventActivityTab";
import EventSettingsTab from "./EventSettingsTab";
import EventActivityTab from "./ActionActivityTab";
import ActionSettingsTab from "./ActionSettingsTab";
import { TActionClass } from "@formbricks/types/v1/actionClasses";
interface EventDetailModalProps {
interface ActionDetailModalProps {
environmentId: string;
open: boolean;
setOpen: (v: boolean) => void;
eventClass: EventClass;
actionClass: TActionClass;
}
export default function EventDetailModal({
export default function ActionDetailModal({
environmentId,
open,
setOpen,
eventClass,
}: EventDetailModalProps) {
actionClass,
}: ActionDetailModalProps) {
const tabs = [
{
title: "Activity",
children: <EventActivityTab environmentId={environmentId} eventClassId={eventClass.id} />,
children: <EventActivityTab environmentId={environmentId} actionClassId={actionClass.id} />,
},
{
title: "Settings",
children: (
<EventSettingsTab environmentId={environmentId} eventClassId={eventClass.id} setOpen={setOpen} />
<ActionSettingsTab environmentId={environmentId} actionClass={actionClass} setOpen={setOpen} />
),
},
];
@@ -37,16 +37,16 @@ export default function EventDetailModal({
setOpen={setOpen}
tabs={tabs}
icon={
eventClass.type === "code" ? (
actionClass.type === "code" ? (
<CodeBracketIcon />
) : eventClass.type === "noCode" ? (
) : actionClass.type === "noCode" ? (
<CursorArrowRaysIcon />
) : eventClass.type === "automatic" ? (
) : actionClass.type === "automatic" ? (
<SparklesIcon />
) : null
}
label={eventClass.name}
description={eventClass.description || ""}
label={actionClass.name}
description={actionClass.description || ""}
/>
</>
);

View File

@@ -0,0 +1,31 @@
import { timeSinceConditionally } from "@formbricks/lib/time";
import { TActionClass } from "@formbricks/types/v1/actionClasses";
import { CodeBracketIcon, CursorArrowRaysIcon, SparklesIcon } from "@heroicons/react/24/solid";
export default function ActionClassDataRow({ actionClass }: { actionClass: TActionClass }) {
return (
<div className="m-2 grid h-16 grid-cols-6 content-center rounded-lg hover:bg-slate-100">
<div className="col-span-4 flex items-center pl-6 text-sm">
<div className="flex items-center">
<div className="h-5 w-5 flex-shrink-0 text-slate-500">
{actionClass.type === "code" ? (
<CodeBracketIcon />
) : actionClass.type === "noCode" ? (
<CursorArrowRaysIcon />
) : actionClass.type === "automatic" ? (
<SparklesIcon />
) : null}
</div>
<div className="ml-4 text-left">
<div className="font-medium text-slate-900">{actionClass.name}</div>
<div className="text-xs text-slate-400">{actionClass.description}</div>
</div>
</div>
</div>
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
{timeSinceConditionally(actionClass.createdAt.toString())}
</div>
<div className="text-center"></div>
</div>
);
}

View File

@@ -1,11 +1,9 @@
"use client";
import DeleteDialog from "@/components/shared/DeleteDialog";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import { deleteEventClass, useEventClass, useEventClasses } from "@/lib/eventClasses/eventClasses";
import { useEventClassMutation } from "@/lib/eventClasses/mutateEventClasses";
import type { Event, NoCodeConfig } from "@formbricks/types/events";
import type { NoCodeConfig } from "@formbricks/types/events";
import {
Button,
ErrorComponent,
Input,
Label,
RadioGroup,
@@ -18,46 +16,46 @@ import {
} from "@formbricks/ui";
import { TrashIcon } from "@heroicons/react/24/outline";
import clsx from "clsx";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { testURLmatch } from "./testURLmatch";
import { deleteActionClass, updateActionClass } from "@formbricks/lib/services/actionClass";
import { TActionClassInput } from "@formbricks/types/v1/actionClasses";
interface EventSettingsTabProps {
interface ActionSettingsTabProps {
environmentId: string;
eventClassId: string;
actionClass: any;
setOpen: (v: boolean) => void;
}
export default function EventSettingsTab({ environmentId, eventClassId, setOpen }: EventSettingsTabProps) {
const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, eventClassId);
export default function ActionSettingsTab({ environmentId, actionClass, setOpen }: ActionSettingsTabProps) {
const router = useRouter();
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
const { register, handleSubmit, control, watch } = useForm({
defaultValues: {
name: eventClass.name,
description: eventClass.description,
noCodeConfig: eventClass.noCodeConfig,
name: actionClass.name,
description: actionClass.description,
noCodeConfig: actionClass.noCodeConfig,
},
});
const { triggerEventClassMutate, isMutatingEventClass } = useEventClassMutation(
environmentId,
eventClass.id
);
const { mutateEventClasses } = useEventClasses(environmentId);
const [isUpdatingAction, setIsUpdatingAction] = useState(false);
const onSubmit = async (data) => {
const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as NoCodeConfig);
const updatedData: Event = {
const updatedData: TActionClassInput = {
...data,
noCodeConfig: filteredNoCodeConfig,
type: "noCode",
} as Event;
} as TActionClassInput;
await triggerEventClassMutate(updatedData);
mutateEventClasses();
setIsUpdatingAction(true);
await updateActionClass(environmentId, actionClass.id, updatedData);
router.refresh();
setIsUpdatingAction(false);
setOpen(false);
};
@@ -83,9 +81,6 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
if (match === "no") toast.error("Your survey would not be shown.");
};
if (isLoadingEventClass) return <LoadingSpinner />;
if (isErrorEventClass) return <ErrorComponent />;
return (
<div>
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
@@ -95,8 +90,8 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
type="text"
placeholder="e.g. Product Team Info"
{...register("name", {
value: eventClass.name,
disabled: eventClass.type === "automatic" || eventClass.type === "code" ? true : false,
value: actionClass.name,
disabled: actionClass.type === "automatic" || actionClass.type === "code" ? true : false,
})}
/>
</div>
@@ -106,18 +101,18 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
type="text"
placeholder="e.g. Triggers when user changed subscription"
{...register("description", {
value: eventClass.description,
disabled: eventClass.type === "automatic" ? true : false,
value: actionClass.description,
disabled: actionClass.type === "automatic" ? true : false,
})}
/>
</div>
<div className="">
<Label>Action Type</Label>
{eventClass.type === "code" ? (
{actionClass.type === "code" ? (
<p className="text-sm text-slate-600">
This is a code action. Please make changes in your code base.
</p>
) : eventClass.type === "noCode" ? (
) : actionClass.type === "noCode" ? (
<div className="flex justify-between rounded-lg">
<div className="w-full space-y-4">
<Controller
@@ -258,7 +253,7 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
)}
</div>
</div>
) : eventClass.type === "automatic" ? (
) : actionClass.type === "automatic" ? (
<p className="text-sm text-slate-600">
This action was created automatically. You cannot make changes to it.
</p>
@@ -266,7 +261,7 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
</div>
<div className="flex justify-between border-t border-slate-200 py-6">
<div>
{eventClass.type !== "automatic" && (
{actionClass.type !== "automatic" && (
<Button
type="button"
variant="warn"
@@ -281,9 +276,9 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
Read Docs
</Button>
</div>
{eventClass.type !== "automatic" && (
{actionClass.type !== "automatic" && (
<div className="flex space-x-2">
<Button type="submit" variant="darkCTA" loading={isMutatingEventClass}>
<Button type="submit" variant="darkCTA" loading={isUpdatingAction}>
Save changes
</Button>
</div>
@@ -298,8 +293,8 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
onDelete={async () => {
setOpen(false);
try {
await deleteEventClass(environmentId, eventClass.id);
mutateEventClasses();
await deleteActionClass(environmentId, actionClass.id);
router.refresh();
toast.success("Action deleted successfully");
} catch (error) {
toast.error("Something went wrong. Please try again.");

View File

@@ -0,0 +1,11 @@
export default function ActionTableHeading() {
return (
<>
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<span className="sr-only">Edit</span>
<div className="col-span-4 pl-6 ">User Actions</div>
<div className="col-span-2 text-center">Created</div>
</div>
</>
);
}

View File

@@ -1,8 +1,6 @@
"use client";
import Modal from "@/components/shared/Modal";
import { createEventClass } from "@/lib/eventClasses/eventClasses";
import type { Event, NoCodeConfig } from "@formbricks/types/events";
import {
Button,
Input,
@@ -21,24 +19,22 @@ import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { testURLmatch } from "./testURLmatch";
import { createActionClass } from "@formbricks/lib/services/actionClass";
import { TActionClassInput, TActionClassNoCodeConfig } from "@formbricks/types/v1/actionClasses";
import { useRouter } from "next/navigation";
interface EventDetailModalProps {
interface AddNoCodeActionModalProps {
environmentId: string;
open: boolean;
setOpen: (v: boolean) => void;
mutateEventClasses: (data?: any) => void;
}
export default function AddNoCodeEventModal({
environmentId,
open,
setOpen,
mutateEventClasses,
}: EventDetailModalProps) {
export default function AddNoCodeActionModal({ environmentId, open, setOpen }: AddNoCodeActionModalProps) {
const router = useRouter();
const { register, control, handleSubmit, watch, reset } = useForm();
// clean up noCodeConfig before submitting by removing unnecessary fields
const filterNoCodeConfig = (noCodeConfig: NoCodeConfig): NoCodeConfig => {
const filterNoCodeConfig = (noCodeConfig: TActionClassNoCodeConfig): TActionClassNoCodeConfig => {
const { type } = noCodeConfig;
return {
type,
@@ -46,18 +42,18 @@ export default function AddNoCodeEventModal({
};
};
const submitEventClass = async (data: Partial<Event>): Promise<void> => {
const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as NoCodeConfig);
const submitEventClass = async (data: Partial<TActionClassInput>): Promise<void> => {
const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as TActionClassNoCodeConfig);
const updatedData: Event = {
const updatedData: TActionClassInput = {
...data,
noCodeConfig: filteredNoCodeConfig,
type: "noCode",
} as Event;
} as TActionClassInput;
try {
await createEventClass(environmentId, updatedData);
mutateEventClasses();
await createActionClass(environmentId, updatedData);
router.refresh();
reset();
setOpen(false);
toast.success("Action added successfully.");

View File

@@ -0,0 +1,11 @@
import ActionsAttributesTabs from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs";
import ContentWrapper from "@/components/shared/ContentWrapper";
export default function ActionsAndAttributesLayout({ params, children }) {
return (
<>
<ActionsAttributesTabs activeId="actions" environmentId={params.environmentId} />
<ContentWrapper>{children}</ContentWrapper>
</>
);
}

View File

@@ -0,0 +1,47 @@
import { Button } from "@formbricks/ui";
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
export default function Loading() {
return (
<>
<div className="mb-6 text-right">
<Button
variant="darkCTA"
className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-gray-200">
<CursorArrowRaysIcon className="mr-2 h-5 w-5 text-white" />
Loading
</Button>
</div>
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<span className="sr-only">Edit</span>
<div className="col-span-4 pl-6 ">User Actions</div>
<div className="col-span-2 text-center">Created</div>
</div>
</div>
{[...Array(3)].map((_, index) => (
<div key={index} className="m-2 grid h-16 grid-cols-6 content-center rounded-lg hover:bg-slate-100">
<div className="col-span-4 flex items-center pl-6 text-sm">
<div className="flex items-center">
<div className="h-6 w-6 flex-shrink-0 animate-pulse rounded-full bg-gray-200 text-slate-500"></div>
<div className="ml-4 text-left">
<div className="font-medium text-slate-900">
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-gray-200"></div>
</div>
<div className="mt-1 text-xs text-slate-400">
<div className="h-2 w-24 animate-pulse rounded-full bg-gray-200"></div>
</div>
</div>
</div>
</div>
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="m-28 h-4 animate-pulse rounded-full bg-gray-200"></div>
</div>
<div className="text-center"></div>
</div>
))}
</>
);
}

View File

@@ -0,0 +1,26 @@
export const revalidate = REVALIDATION_INTERVAL;
import ActionClassesTable from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionClassesTable";
import ActionClassDataRow from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionRowData";
import ActionTableHeading from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionTableHeading";
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import { getActionClasses } from "@formbricks/lib/services/actionClass";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Actions",
};
export default async function ActionClassesComponent({ params }) {
let actionClasses = await getActionClasses(params.environmentId);
return (
<>
<ActionClassesTable environmentId={params.environmentId} actionClasses={actionClasses}>
<ActionTableHeading />
{actionClasses.map((actionClass) => (
<ActionClassDataRow key={actionClass.id} actionClass={actionClass} />
))}
</ActionClassesTable>
</>
);
}

View File

@@ -0,0 +1,81 @@
"use client";
import { Switch } from "@formbricks/ui";
import { useState } from "react";
import AttributeDetailModal from "./AttributeDetailModal";
import UploadAttributesModal from "./UploadAttributesModal";
import { useMemo } from "react";
import { TAttributeClass } from "@formbricks/types/v1/attributeClasses";
export default function AttributeClassesTable({
environmentId,
attributeClasses,
children: [TableHeading, howToAddAttributeButton, attributeRows],
}: {
environmentId: string;
attributeClasses: TAttributeClass[];
children: [JSX.Element, JSX.Element, JSX.Element[]];
}) {
const [isAttributeDetailModalOpen, setAttributeDetailModalOpen] = useState(false);
const [isUploadCSVModalOpen, setUploadCSVModalOpen] = useState(false);
const [activeAttributeClass, setActiveAttributeClass] = useState("" as any);
const [showArchived, setShowArchived] = useState(false);
const displayedAttributeClasses = useMemo(() => {
return attributeClasses
? showArchived
? attributeClasses
: attributeClasses.filter((ac) => !ac.archived)
: [];
}, [showArchived, attributeClasses]);
const hasArchived = useMemo(() => {
return attributeClasses ? attributeClasses.some((ac) => ac.archived) : false;
}, [attributeClasses]);
const handleOpenAttributeDetailModalClick = (e, attributeClass) => {
e.preventDefault();
setActiveAttributeClass(attributeClass);
setAttributeDetailModalOpen(true);
};
const toggleShowArchived = () => {
setShowArchived(!showArchived);
};
return (
<>
<div className="mb-6 flex items-center justify-end text-right">
{hasArchived && (
<div className="flex items-center text-sm font-medium">
Show archived
<Switch className="mx-3" checked={showArchived} onCheckedChange={toggleShowArchived} />
</div>
)}
{howToAddAttributeButton}
</div>
<div className="rounded-lg border border-slate-200">
{TableHeading}
<div className="grid-cols-7">
{displayedAttributeClasses.map((attributeClass, index) => (
<button
onClick={(e) => {
handleOpenAttributeDetailModalClick(e, attributeClass);
}}
className="w-full"
key={attributeClass.id}>
{attributeRows[index]}
</button>
))}
</div>
<AttributeDetailModal
environmentId={environmentId}
open={isAttributeDetailModalOpen}
setOpen={setAttributeDetailModalOpen}
attributeClass={activeAttributeClass}
/>
<UploadAttributesModal open={isUploadCSVModalOpen} setOpen={setUploadCSVModalOpen} />
</div>
</>
);
}

View File

@@ -27,7 +27,6 @@ export default function AttributeDetailModal({
children: (
<AttributeSettingsTab
attributeClass={attributeClass}
environmentId={environmentId}
setOpen={setOpen}
/>
),

View File

@@ -0,0 +1,33 @@
import { timeSinceConditionally } from "@formbricks/lib/time";
import { Badge } from "@formbricks/ui";
import { TagIcon } from "@heroicons/react/24/solid";
export default function AttributeClassDataRow({ attributeClass }) {
return (
<div className="m-2 grid h-16 grid-cols-5 content-center rounded-lg hover:bg-slate-100">
<div className="col-span-3 flex items-center pl-6 text-sm">
<div className="flex items-center">
<div className="h-10 w-10 flex-shrink-0">
<TagIcon className="h-8 w-8 flex-shrink-0 text-slate-500" />
</div>
<div className="ml-4 text-left">
<div className="font-medium text-slate-900">
{attributeClass.name}
<span className="ml-2">
{attributeClass.archived && <Badge text="Archived" type="gray" size="tiny" />}
</span>
</div>
<div className="text-xs text-slate-400">{attributeClass.description}</div>
</div>
</div>
</div>
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="text-slate-900">{timeSinceConditionally(attributeClass.createdAt.toString())}</div>
</div>
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="text-slate-900">{timeSinceConditionally(attributeClass.updatedAt.toString())}</div>
</div>
</div>
);
}

View File

@@ -1,41 +1,38 @@
import { useAttributeClasses } from "@/lib/attributeClasses/attributeClasses";
import { useAttributeClassMutation } from "@/lib/attributeClasses/mutateAttributeClasses";
"use client";
import { Button, Input, Label } from "@formbricks/ui";
import type { AttributeClass } from "@prisma/client";
import { useForm } from "react-hook-form";
import { ArchiveBoxArrowDownIcon, ArchiveBoxXMarkIcon } from "@heroicons/react/24/solid";
import { useRouter } from "next/navigation";
import { updatetAttributeClass } from "@formbricks/lib/services/attributeClass";
import { useState } from "react";
interface AttributeSettingsTabProps {
environmentId: string;
attributeClass: AttributeClass;
setOpen: (v: boolean) => void;
}
export default function AttributeSettingsTab({
environmentId,
attributeClass,
setOpen,
}: AttributeSettingsTabProps) {
export default function AttributeSettingsTab({ attributeClass, setOpen }: AttributeSettingsTabProps) {
const router = useRouter();
const { register, handleSubmit } = useForm({
defaultValues: { name: attributeClass.name, description: attributeClass.description },
});
const { triggerAttributeClassMutate, isMutatingAttributeClass } = useAttributeClassMutation(
environmentId,
attributeClass.id
);
const { mutateAttributeClasses } = useAttributeClasses(environmentId);
const [isAttributeBeingSubmitted, setisAttributeBeingSubmitted] = useState(false);
const onSubmit = async (data) => {
await triggerAttributeClassMutate(data);
mutateAttributeClasses();
setisAttributeBeingSubmitted(true);
setOpen(false);
await updatetAttributeClass(attributeClass.id, data);
router.refresh();
setisAttributeBeingSubmitted(false);
};
const handleArchiveToggle = async () => {
setisAttributeBeingSubmitted(true);
const data = { archived: !attributeClass.archived };
await triggerAttributeClassMutate(data);
mutateAttributeClasses();
await updatetAttributeClass(attributeClass.id, data);
setisAttributeBeingSubmitted(false);
};
return (
@@ -101,7 +98,7 @@ export default function AttributeSettingsTab({
</div>
{attributeClass.type !== "automatic" && (
<div className="flex space-x-2">
<Button type="submit" variant="darkCTA" loading={isMutatingAttributeClass}>
<Button type="submit" variant="darkCTA" loading={isAttributeBeingSubmitted}>
Save changes
</Button>
</div>

View File

@@ -0,0 +1,11 @@
export default function AttributeTableHeading() {
return (
<>
<div className="grid h-12 grid-cols-5 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6 ">Name</div>
<div className="text-center">Created</div>
<div className="text-center">Last Updated</div>
</div>
</>
);
}

View File

@@ -0,0 +1,14 @@
import { Button } from "@formbricks/ui";
import { QuestionMarkCircleIcon } from "@heroicons/react/24/solid";
export default function HowToAddAttributesButton() {
return (
<Button
variant="secondary"
href="http://formbricks.com/docs/attributes/custom-attributes"
target="_blank">
<QuestionMarkCircleIcon className="mr-2 h-4 w-4" />
How to add attributes
</Button>
);
}

View File

@@ -0,0 +1,11 @@
import ActionsAttributesTabs from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs";
import ContentWrapper from "@/components/shared/ContentWrapper";
export default function ActionsAndAttributesLayout({ params, children }) {
return (
<>
<ActionsAttributesTabs activeId="attributes" environmentId={params.environmentId} />
<ContentWrapper>{children}</ContentWrapper>
</>
);
}

View File

@@ -0,0 +1,55 @@
import { Button } from "@formbricks/ui";
import { TagIcon } from "@heroicons/react/24/solid";
import { QuestionMarkCircleIcon } from "@heroicons/react/24/solid";
export default function Loading() {
return (
<>
<div className="mb-6 text-right">
<div className="mb-6 flex items-center justify-end text-right">
<Button
variant="secondary"
className="pointer-events-none animate-pulse cursor-not-allowed select-none">
<QuestionMarkCircleIcon className="mr-2 h-4 w-4" />
Loading Attributes
</Button>
</div>
</div>
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-5 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6 ">Name</div>
<div className="text-center">Created</div>
<div className="text-center">Last Updated</div>
</div>
</div>
{[...Array(3)].map((_, index) => (
<div key={index} className="m-2 grid h-16 grid-cols-5 content-center rounded-lg hover:bg-slate-100">
<div className="col-span-3 flex items-center pl-6 text-sm">
<div className="flex items-center">
<div className="h-10 w-10 flex-shrink-0">
<TagIcon className="h-8 w-8 flex-shrink-0 animate-pulse text-slate-500" />
</div>
<div className="ml-4 text-left">
<div className="font-medium text-slate-900">
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-gray-200"></div>
</div>
<div className="mt-1 text-xs text-slate-400">
<div className="h-2 w-24 animate-pulse rounded-full bg-gray-200"></div>
</div>
</div>
</div>
</div>
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="m-4 h-4 animate-pulse rounded-full bg-gray-200"></div>
</div>
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="m-4 h-4 animate-pulse rounded-full bg-gray-200"></div>
</div>
</div>
))}
</>
);
}

View File

@@ -0,0 +1,29 @@
export const revalidate = REVALIDATION_INTERVAL;
import AttributeClassesTable from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeClassesTable";
import AttributeClassDataRow from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeRowData";
import AttributeTableHeading from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeTableHeading";
import HowToAddAttributesButton from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/HowToAddAttributesButton";
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import { getAttributeClasses } from "@formbricks/lib/services/attributeClass";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Attributes",
};
export default async function AttributesPage({ params }) {
let attributeClasses = await getAttributeClasses(params.environmentId);
return (
<>
<AttributeClassesTable environmentId={params.environmentId} attributeClasses={attributeClasses}>
<AttributeTableHeading />
<HowToAddAttributesButton />
{attributeClasses.map((attributeClass) => (
<AttributeClassDataRow key={attributeClass.id} attributeClass={attributeClass} />
))}
</AttributeClassesTable>
</>
);
}

View File

@@ -117,9 +117,9 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
},
{
name: "Actions & Attributes",
href: `/environments/${environmentId}/events`,
href: `/environments/${environmentId}/actions`,
icon: FilterIcon,
current: pathname?.includes("/events") || pathname?.includes("/attributes"),
current: pathname?.includes("/actions") || pathname?.includes("/attributes"),
},
{
name: "Integrations",
@@ -166,7 +166,7 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
icon: CreditCardIcon,
label: "Billing & Plan",
href: `/environments/${environmentId}/settings/billing`,
hidden: IS_FORMBRICKS_CLOUD,
hidden: !IS_FORMBRICKS_CLOUD,
},
],
},
@@ -221,7 +221,7 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
<nav className="top-0 z-10 w-full border-b border-slate-200 bg-white">
{environment?.type === "development" && (
<div className="h-6 w-full bg-[#A33700] p-0.5 text-center text-sm text-white">
You&apos;re in development mode. Use it to test surveys, events and attributes.
You&apos;re in development mode. Use it to test surveys, actions and attributes.
</div>
)}

View File

@@ -3,8 +3,8 @@
import {
QuestionOption,
QuestionOptions,
} from "@/app/environments/[environmentId]/surveys/[surveyId]/QuestionsComboBox";
import { QuestionFilterOptions } from "@/app/environments/[environmentId]/surveys/[surveyId]/ResponseFilter";
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/QuestionsComboBox";
import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/ResponseFilter";
import { getTodayDate } from "@/lib/surveys/surveys";
import { createContext, useContext, useState } from "react";

View File

@@ -1,9 +1,12 @@
"use server";
import { prisma } from "@formbricks/database";
import { Team } from "@prisma/client";
import { ResourceNotFoundError } from "@formbricks/errors";
import { deleteSurvey, getSurvey } from "@formbricks/lib/services/survey";
import { QuestionType } from "@formbricks/types/questions";
import { createId } from "@paralleldrive/cuid2";
import { Team } from "@prisma/client";
import { Prisma as prismaClient } from "@prisma/client/";
export async function createTeam(teamName: string, ownerUserId: string): Promise<Team> {
const newTeam = await prisma.team.create({
@@ -1372,3 +1375,176 @@ export async function addDemoData(teamId: string): Promise<void> {
InterviewPromptResults.displays
);
}
export async function duplicateSurveyAction(environmentId: string, surveyId: string) {
const existingSurvey = await getSurvey(surveyId);
if (!existingSurvey) {
throw new ResourceNotFoundError("Survey", surveyId);
}
// create new survey with the data of the existing survey
const newSurvey = await prisma.survey.create({
data: {
...existingSurvey,
id: undefined, // id is auto-generated
environmentId: undefined, // environmentId is set below
name: `${existingSurvey.name} (copy)`,
status: "draft",
questions: JSON.parse(JSON.stringify(existingSurvey.questions)),
thankYouCard: JSON.parse(JSON.stringify(existingSurvey.thankYouCard)),
triggers: {
create: existingSurvey.triggers.map((trigger) => ({
eventClassId: trigger.id,
})),
},
attributeFilters: {
create: existingSurvey.attributeFilters.map((attributeFilter) => ({
attributeClassId: attributeFilter.attributeClassId,
condition: attributeFilter.condition,
value: attributeFilter.value,
})),
},
environment: {
connect: {
id: environmentId,
},
},
surveyClosedMessage: existingSurvey.surveyClosedMessage
? JSON.parse(JSON.stringify(existingSurvey.surveyClosedMessage))
: prismaClient.JsonNull,
},
});
return newSurvey;
}
export async function copyToOtherEnvironmentAction(
environmentId: string,
surveyId: string,
targetEnvironmentId: string
) {
const existingSurvey = await prisma.survey.findFirst({
where: {
id: surveyId,
environmentId,
},
include: {
triggers: {
include: {
eventClass: true,
},
},
attributeFilters: {
include: {
attributeClass: true,
},
},
},
});
if (!existingSurvey) {
throw new ResourceNotFoundError("Survey", surveyId);
}
let targetEnvironmentTriggers: string[] = [];
// map the local triggers to the target environment
for (const trigger of existingSurvey.triggers) {
const targetEnvironmentTrigger = await prisma.eventClass.findFirst({
where: {
name: trigger.eventClass.name,
environment: {
id: targetEnvironmentId,
},
},
});
if (!targetEnvironmentTrigger) {
// if the trigger does not exist in the target environment, create it
const newTrigger = await prisma.eventClass.create({
data: {
name: trigger.eventClass.name,
environment: {
connect: {
id: targetEnvironmentId,
},
},
description: trigger.eventClass.description,
type: trigger.eventClass.type,
noCodeConfig: trigger.eventClass.noCodeConfig
? JSON.parse(JSON.stringify(trigger.eventClass.noCodeConfig))
: undefined,
},
});
targetEnvironmentTriggers.push(newTrigger.id);
} else {
targetEnvironmentTriggers.push(targetEnvironmentTrigger.id);
}
}
let targetEnvironmentAttributeFilters: string[] = [];
// map the local attributeFilters to the target env
for (const attributeFilter of existingSurvey.attributeFilters) {
// check if attributeClass exists in target env.
// if not, create it
const targetEnvironmentAttributeClass = await prisma.attributeClass.findFirst({
where: {
name: attributeFilter.attributeClass.name,
environment: {
id: targetEnvironmentId,
},
},
});
if (!targetEnvironmentAttributeClass) {
const newAttributeClass = await prisma.attributeClass.create({
data: {
name: attributeFilter.attributeClass.name,
description: attributeFilter.attributeClass.description,
type: attributeFilter.attributeClass.type,
environment: {
connect: {
id: targetEnvironmentId,
},
},
},
});
targetEnvironmentAttributeFilters.push(newAttributeClass.id);
} else {
targetEnvironmentAttributeFilters.push(targetEnvironmentAttributeClass.id);
}
}
// create new survey with the data of the existing survey
const newSurvey = await prisma.survey.create({
data: {
...existingSurvey,
id: undefined, // id is auto-generated
environmentId: undefined, // environmentId is set below
name: `${existingSurvey.name} (copy)`,
status: "draft",
questions: JSON.parse(JSON.stringify(existingSurvey.questions)),
thankYouCard: JSON.parse(JSON.stringify(existingSurvey.thankYouCard)),
triggers: {
create: targetEnvironmentTriggers.map((eventClassId) => ({
eventClassId: eventClassId,
})),
},
attributeFilters: {
create: existingSurvey.attributeFilters.map((attributeFilter, idx) => ({
attributeClassId: targetEnvironmentAttributeFilters[idx],
condition: attributeFilter.condition,
value: attributeFilter.value,
})),
},
environment: {
connect: {
id: targetEnvironmentId,
},
},
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
},
});
return newSurvey;
}
export const deleteSurveyAction = async (surveyId: string) => {
await deleteSurvey(surveyId);
};

View File

@@ -1,12 +1,10 @@
import EnvironmentsNavbar from "@/app/environments/[environmentId]/EnvironmentsNavbar";
import EnvironmentsNavbar from "@/app/(app)/environments/[environmentId]/EnvironmentsNavbar";
import ToasterClient from "@/components/ToasterClient";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import PosthogIdentify from "./PosthogIdentify";
import FormbricksClient from "../../FormbricksClient";
import { PosthogClientWrapper } from "../../PosthogClientWrapper";
import { ResponseFilterProvider } from "@/app/environments/[environmentId]/ResponseFilterContext";
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/ResponseFilterContext";
import { hasUserEnvironmentAccess } from "@/lib/api/apiHelper";
export default async function EnvironmentLayout({ children, params }) {
@@ -22,16 +20,13 @@ export default async function EnvironmentLayout({ children, params }) {
return (
<>
<ResponseFilterProvider>
<PosthogIdentify session={session} />
<FormbricksClient session={session} />
<ToasterClient />
<EnvironmentsNavbar environmentId={params.environmentId} session={session} />
<PosthogClientWrapper>
<main className="h-full flex-1 overflow-y-auto bg-slate-50">
{children}
<main />
</main>
</PosthogClientWrapper>
<main className="h-full flex-1 overflow-y-auto bg-slate-50">
{children}
<main />
</main>
</ResponseFilterProvider>
</>
);

View File

@@ -7,7 +7,6 @@ export default function SettingsCard({
children,
soon = false,
noPadding = false,
dangerZone,
beta,
}: {
title: string;
@@ -15,16 +14,13 @@ export default function SettingsCard({
children: any;
soon?: boolean;
noPadding?: boolean;
dangerZone?: boolean;
beta?: boolean;
}) {
return (
<div className="my-4 w-full bg-white shadow sm:rounded-lg">
<div className="rounded-t-lg border-b border-slate-200 bg-slate-100 px-6 py-5">
<div className="flex">
<h3 className={`${dangerZone ? "text-red-600" : "text-slate-900"} "text-lg font-medium leading-6 `}>
{title}
</h3>
<h3 className="text-lg font-medium leading-6 text-slate-900">{title}</h3>
<div className="ml-2">
{beta && <Badge text="Beta" size="normal" type="warning" />}
{soon && <Badge text="coming soon" size="normal" type="success" />}

View File

@@ -7,10 +7,11 @@ import type { Session } from "next-auth";
import { useRouter } from "next/navigation";
import { useState } from "react";
// upated on 20th of July 2023
const stripeURl =
process.env.NODE_ENV === "production"
? "https://buy.stripe.com/28o00R4GDf9qdfa5kp"
: "https://buy.stripe.com/test_9AQfZw5CL9hmcXSdQQ";
? "https://buy.stripe.com/5kA9ABal07ZjgEw3cc"
: "https://buy.stripe.com/test_8wMaHA3UWcACfuM3cc";
interface PricingTableProps {
environmentId: string;

View File

@@ -0,0 +1,14 @@
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { getServerSession } from "next-auth";
import SettingsTitle from "../SettingsTitle";
import PricingTable from "./PricingTable";
export default async function ProfileSettingsPage({ params }) {
const session = await getServerSession(authOptions);
return (
<div>
<SettingsTitle title="Billing & Plan" />
<PricingTable environmentId={params.environmentId} session={session} />
</div>
);
}

View File

@@ -1,6 +1,6 @@
"use client";
import ShareInviteModal from "@/app/environments/[environmentId]/settings/members/ShareInviteModal";
import ShareInviteModal from "@/app/(app)/environments/[environmentId]/settings/members/ShareInviteModal";
import DeleteDialog from "@/components/shared/DeleteDialog";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import CreateTeamModal from "@/components/team/CreateTeamModal";

View File

@@ -1,5 +1,5 @@
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import SettingsCard from "@/app/environments/[environmentId]/settings/SettingsCard";
import SettingsCard from "@/app/(app)/environments/[environmentId]/settings/SettingsCard";
import { prisma } from "@formbricks/database";
import { NotificationSettings } from "@formbricks/types/users";
import { getServerSession } from "next-auth";

View File

@@ -141,21 +141,24 @@ export function DeleteProduct({ environmentId }) {
return (
<div>
<p className="text-sm text-slate-900">
Here you can delete&nbsp;
<strong>{truncate(product?.name, 30)}</strong>
&nbsp;incl. all surveys, responses, people, actions and attributes.{" "}
<strong>This action cannot be undone.</strong>
</p>
<Button
disabled={isDeleteDisabled}
variant="warn"
className={`mt-4 ${isDeleteDisabled ? "ring-grey-500 ring-1 ring-offset-1" : ""}`}
onClick={() => setIsDeleteDialogOpen(true)}>
Delete
</Button>
{!isDeleteDisabled && (
<div>
<p className="text-sm text-slate-900">
Delete {truncate(product?.name, 30)}
&nbsp;incl. all surveys, responses, people, actions and attributes.{" "}
<strong>This action cannot be undone.</strong>
</p>
<Button
disabled={isDeleteDisabled}
variant="warn"
className={`mt-4 ${isDeleteDisabled ? "ring-grey-500 ring-1 ring-offset-1" : ""}`}
onClick={() => setIsDeleteDialogOpen(true)}>
Delete
</Button>
</div>
)}
{isDeleteDisabled && (
<p className="mt-2 text-xs text-red-700">
<p className="text-sm text-red-700">
{!isUserAdminOrOwner
? "Only Admin or Owners can delete products."
: "This is your only product, it cannot be deleted. Create a new product first."}

View File

@@ -15,8 +15,8 @@ export default function ProfileSettingsPage({ params }) {
<EditWaitingTime environmentId={params.environmentId} />
</SettingsCard>
<SettingsCard
title="Danger Zone"
description="You will delete all surveys, responses, people, actions and attributes along with the product.">
title="Delete Product"
description="Delete product with all surveys, responses, people, actions and attributes. This cannot be undone.">
<DeleteProduct environmentId={params.environmentId} />
</SettingsCard>
</div>

View File

@@ -114,23 +114,27 @@ function DeleteAccountModal({ setOpen, open, session }: DeleteAccounModaltProps)
isDeleting={deleting}
disabled={inputValue !== session.user.email}>
<div className="py-5">
<p>
Deleting your account will result in the permanent removal of all your personal information, saved
preferences, and access to team data. If you are the owner of a team with other admins, the
ownership of that team will be transferred to another admin.
</p>
<p className="py-5">
Please note, however, that if you are the only member of a team or there is no other admin present,
the team will be irreversibly deleted along with all associated data.
</p>
<ul className="list-disc pb-6 pl-6">
<li>Permanent removal of all of your personal information and data.</li>
<li>
If you are the owner of a team with other admins, the ownership of that team will be transferred
to another admin.
</li>
<li>
If you are the only member of a team or there is no other admin present, the team will be
irreversibly deleted along with all associated data.
</li>
<li>This action cannot be undone. If it&apos;s gone, it&apos;s gone.</li>
</ul>
<form>
<label htmlFor="deleteAccountConfirmation">
Please enter <span className="font-bold">{session.user.email}</span> in the following field to
confirm the definitive deletion of your account.
confirm the definitive deletion of your account:
</label>
<Input
value={inputValue}
onChange={handleInputChange}
placeholder={session.user.email}
className="mt-5"
type="text"
id="deleteAccountConfirmation"
@@ -152,6 +156,9 @@ export function DeleteAccount({ session }: { session: Session | null }) {
return (
<div>
<DeleteAccountModal open={isModalOpen} setOpen={setModalOpen} session={session} />
<p className="text-sm text-slate-700">
Delete your account with all personal data. <strong>This cannot be undone!</strong>
</p>
<Button className="mt-4" variant="warn" onClick={() => setModalOpen(!isModalOpen)}>
Delete my account
</Button>

View File

@@ -17,8 +17,7 @@ export default async function ProfileSettingsPage() {
</SettingsCard>
<SettingsCard
title="Delete account"
description="Delete your account, your personal information, your preferences and access to your data"
dangerZone>
description="Delete your account with all of your personal information and data.">
<DeleteAccount session={session} />
</SettingsCard>
</div>

View File

@@ -104,7 +104,7 @@ if (typeof window !== "undefined") {
</p>
<CodeBlock language="js">{`<!-- START Formbricks Surveys -->
<script type="text/javascript">
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://unpkg.com/@formbricks/js@^0.1.17/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks=window.js;window.formbricks.init({environmentId: "${environmentId}", apiHost: "${window.location.protocol}//${window.location.host}"})},500)}();
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://unpkg.com/@formbricks/js@^1.0.0/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks=window.js;window.formbricks.init({environmentId: "${environmentId}", apiHost: "${window.location.protocol}//${window.location.host}"})},500)}();
</script>
<!-- END Formbricks Surveys -->`}</CodeBlock>
<p className="text-lg font-semibold text-slate-800">You&apos;re done 🎉</p>

Some files were not shown because too many files have changed in this diff Show More