mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
Merge remote-tracking branch 'upstream/main' into feature/survey-back-button
This commit is contained in:
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2023 Matthias Nannt, Johannes Dancker
|
||||
Copyright (c) 2023 Formbricks GmbH
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'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"
|
||||
|
||||
36
apps/formbricks-com/components/shared/AuthorBox.tsx
Normal file
36
apps/formbricks-com/components/shared/AuthorBox.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 |
@@ -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:*",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?_
|
||||
|
||||
@@ -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?_
|
||||
|
||||
@@ -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._
|
||||
|
||||
@@ -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 |
@@ -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>;
|
||||
@@ -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._
|
||||
|
||||
@@ -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'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={{
|
||||
|
||||
@@ -39,7 +39,7 @@ export const meta = {
|
||||
},
|
||||
]}
|
||||
example={`{
|
||||
"personId: "clfqjny0v000ayzgsycx54a2c",
|
||||
"personId": "clfqjny0v000ayzgsycx54a2c",
|
||||
"surveyId": "clfqz1esd0000yzah51trddn8",
|
||||
"finished": true,
|
||||
"data": {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
@@ -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="/">
|
||||
@@ -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",
|
||||
@@ -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 />;
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 || ""}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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.");
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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.");
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -27,7 +27,6 @@ export default function AttributeDetailModal({
|
||||
children: (
|
||||
<AttributeSettingsTab
|
||||
attributeClass={attributeClass}
|
||||
environmentId={environmentId}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
),
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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're in development mode. Use it to test surveys, events and attributes.
|
||||
You're in development mode. Use it to test surveys, actions and attributes.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
@@ -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" />}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -141,21 +141,24 @@ export function DeleteProduct({ environmentId }) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="text-sm text-slate-900">
|
||||
Here you can delete
|
||||
<strong>{truncate(product?.name, 30)}</strong>
|
||||
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)}
|
||||
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."}
|
||||
@@ -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>
|
||||
@@ -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's gone, it'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>
|
||||
@@ -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>
|
||||
@@ -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're done 🎉</p>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user