feat: adds functionality limit based on channel (#2772)

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
Piyush Gupta
2024-06-20 12:46:59 +05:30
committed by GitHub
parent 22e44b47e6
commit deef604325
39 changed files with 397 additions and 179 deletions

View File

@@ -1,11 +1,12 @@
"use client";
import * as Collapsible from "@radix-ui/react-collapsible";
import { AlertCircleIcon, CheckIcon, EarthIcon, LinkIcon, MonitorIcon, SmartphoneIcon } from "lucide-react";
import { AlertCircleIcon, BlocksIcon, CheckIcon, EarthIcon, LinkIcon, MonitorIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { TEnvironment } from "@formbricks/types/environment";
import { TProduct } from "@formbricks/types/product";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyType } from "@formbricks/types/surveys";
import { Badge } from "@formbricks/ui/Badge";
@@ -16,9 +17,10 @@ interface HowToSendCardProps {
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey | ((TSurvey: TSurvey) => TSurvey)) => void;
environment: TEnvironment;
product: TProduct;
}
export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowToSendCardProps) => {
export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment, product }: HowToSendCardProps) => {
const [open, setOpen] = useState(false);
const [appSetupCompleted, setAppSetupCompleted] = useState(false);
const [websiteSetupCompleted, setWebsiteSetupCompleted] = useState(false);
@@ -77,6 +79,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
description: "Run targeted surveys on public websites.",
comingSoon: false,
alert: !websiteSetupCompleted,
hide: product.config.channel !== "website",
},
{
id: "app",
@@ -85,6 +88,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
description: "Embed a survey in your web app to collect responses with user identification.",
comingSoon: false,
alert: !appSetupCompleted,
hide: product.config.channel !== "app",
},
{
id: "link",
@@ -93,17 +97,28 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
description: "Share a link to a survey page or embed it in a web page or email.",
comingSoon: false,
alert: false,
hide: false,
},
{
id: "mobile",
name: "Mobile App Survey",
icon: SmartphoneIcon,
description: "Survey users inside a mobile app (iOS & Android).",
id: "headless",
name: "Headless Survey",
icon: BlocksIcon,
description: "Use Formbricks API only and create your own frontend experience.",
comingSoon: true,
alert: false,
hide: false,
},
];
const promotedFeaturesString =
product.config.channel === "website"
? "app"
: product.config.channel === "app"
? "website"
: product.config.channel === "link"
? "app or website"
: "";
return (
<Collapsible.Root
open={open}
@@ -125,7 +140,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
</div>
<div>
<p className="font-semibold text-slate-800">Survey Type</p>
<p className="mt-1 text-sm text-slate-500">Choose between website, in-app or link survey.</p>
<p className="mt-1 text-sm text-slate-500">Choose where to run the survey.</p>
</div>
</div>
</Collapsible.CollapsibleTrigger>
@@ -137,66 +152,79 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
value={localSurvey.type}
onValueChange={setSurveyType}
className="flex flex-col space-y-3">
{options.map((option) => (
<Label
key={option.id}
htmlFor={option.id}
className={cn(
"flex w-full items-center rounded-lg border bg-slate-50 p-4",
option.comingSoon
? "border-slate-200 bg-slate-50/50"
: option.id === localSurvey.type
? "border-brand-dark cursor-pointer bg-slate-50"
: "cursor-pointer bg-slate-50"
)}
id={`howToSendCardOption-${option.id}`}>
<RadioGroupItem
value={option.id}
id={option.id}
className="aria-checked:border-brand-dark mx-5 disabled:border-slate-400 aria-checked:border-2"
disabled={option.comingSoon}
/>
<div className=" inline-flex items-center">
<option.icon className="mr-4 h-8 w-8 text-slate-500" />
<div>
<div className="inline-flex items-center">
<p
className={cn(
"font-semibold",
option.comingSoon ? "text-slate-500" : "text-slate-800"
)}>
{option.name}
</p>
{option.comingSoon && (
<Badge text="coming soon" size="normal" type="success" className="ml-2" />
{options
.filter((option) => !Boolean(option.hide))
.map((option) => (
<Label
key={option.id}
htmlFor={option.id}
className={cn(
"flex w-full items-center rounded-lg border bg-slate-50 p-4",
option.comingSoon
? "border-slate-200 bg-slate-50/50"
: option.id === localSurvey.type
? "border-brand-dark cursor-pointer bg-slate-50"
: "cursor-pointer bg-slate-50"
)}
id={`howToSendCardOption-${option.id}`}>
<RadioGroupItem
value={option.id}
id={option.id}
className="aria-checked:border-brand-dark mx-5 disabled:border-slate-400 aria-checked:border-2"
disabled={option.comingSoon}
/>
<div className=" inline-flex items-center">
<option.icon className="mr-4 h-8 w-8 text-slate-500" />
<div>
<div className="inline-flex items-center">
<p
className={cn(
"font-semibold",
option.comingSoon ? "text-slate-500" : "text-slate-800"
)}>
{option.name}
</p>
{option.comingSoon && (
<Badge text="coming soon" size="normal" type="success" className="ml-2" />
)}
</div>
<p className="mt-2 text-xs font-normal text-slate-600">{option.description}</p>
{option.alert && (
<div className="mt-2 flex items-center space-x-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-2">
<AlertCircleIcon className="h-5 w-5 text-amber-500" />
<div className=" text-amber-800">
<p className="text-xs font-semibold">
Your {option.id} is not yet connected to Formbricks.
</p>
<p className="text-xs font-normal">
<Link
href={`/environments/${environment.id}/product/${option.id}-connection`}
className="underline hover:text-amber-900"
target="_blank">
Connect Formbricks
</Link>{" "}
and launch surveys in your {option.id}.
</p>
</div>
</div>
)}
</div>
<p className="mt-2 text-xs font-normal text-slate-600">{option.description}</p>
{option.alert && (
<div className="mt-2 flex items-center space-x-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-2">
<AlertCircleIcon className="h-5 w-5 text-amber-500" />
<div className=" text-amber-800">
<p className="text-xs font-semibold">
Your {option.id} is not yet connected to Formbricks.
</p>
<p className="text-xs font-normal">
<Link
href={`/environments/${environment.id}/product/${option.id}-connection`}
className="underline hover:text-amber-900"
target="_blank">
Connect Formbricks
</Link>{" "}
and launch surveys in your {option.id}.
</p>
</div>
</div>
)}
</div>
</div>
</Label>
))}
</Label>
))}
</RadioGroup>
</div>
{promotedFeaturesString && (
<div className="mt-2 flex items-center space-x-3 rounded-b-lg border border-slate-200 bg-slate-50/50 px-4 py-2">
<AlertCircleIcon className="h-5 w-5 text-slate-500" />
<div className=" text-slate-500">
<p className="text-xs">
You can also use Formbricks to run {promotedFeaturesString} surveys. Create a new product for
your {promotedFeaturesString} to use this feature.
</p>
</div>
</div>
)}
</Collapsible.CollapsibleContent>
</Collapsible.Root>
);

View File

@@ -104,7 +104,8 @@ export const RecontactOptionsCard = ({
className="w-full rounded-lg border border-slate-300 bg-white">
<Collapsible.CollapsibleTrigger
asChild
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50"
id="recontactOptionsCardTrigger">
<div className="inline-flex px-4 py-4">
<div className="flex items-center pl-2 pr-5">
<CheckIcon
@@ -146,7 +147,7 @@ export const RecontactOptionsCard = ({
<RadioGroupItem
value={option.id}
id={option.name}
className="aria-checked:border-brand-dark mx-5 disabled:border-slate-400 aria-checked:border-2"
className="aria-checked:border-brand-dark mx-5 disabled:border-slate-400 aria-checked:border-2"
/>
<div>
<p className="font-semibold text-slate-700">{option.name}</p>

View File

@@ -3,6 +3,7 @@ import { TActionClass } from "@formbricks/types/actionClasses";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TEnvironment } from "@formbricks/types/environment";
import { TMembershipRole } from "@formbricks/types/memberships";
import { TProduct } from "@formbricks/types/product";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey } from "@formbricks/types/surveys";
import { HowToSendCard } from "./HowToSendCard";
@@ -23,6 +24,7 @@ interface SettingsViewProps {
membershipRole?: TMembershipRole;
isUserTargetingAllowed?: boolean;
isFormbricksCloud: boolean;
product: TProduct;
}
export const SettingsView = ({
@@ -36,12 +38,18 @@ export const SettingsView = ({
membershipRole,
isUserTargetingAllowed = false,
isFormbricksCloud,
product,
}: SettingsViewProps) => {
const isWebSurvey = localSurvey.type === "website" || localSurvey.type === "app";
return (
<div className="mt-12 space-y-3 p-5">
<HowToSendCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} environment={environment} />
<HowToSendCard
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
environment={environment}
product={product}
/>
{localSurvey.type === "app" ? (
!isUserTargetingAllowed ? (

View File

@@ -192,6 +192,7 @@ export const SurveyEditor = ({
membershipRole={membershipRole}
isUserTargetingAllowed={isUserTargetingAllowed}
isFormbricksCloud={isFormbricksCloud}
product={localProduct}
/>
)}
</main>

View File

@@ -1,7 +1,9 @@
import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation";
import { CircleHelpIcon } from "lucide-react";
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { Button } from "@formbricks/ui/Button";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
@@ -14,6 +16,18 @@ export const metadata: Metadata = {
const Page = async ({ params }) => {
let attributeClasses = await getAttributeClasses(params.environmentId);
const product = await getProductByEnvironmentId(params.environmentId);
if (!product) {
throw new Error("Product not found");
}
const currentProductChannel = product.config.channel ?? null;
if (currentProductChannel && currentProductChannel !== "app") {
return notFound();
}
const HowToAddAttributesButton = (
<Button
size="sm"

View File

@@ -1,3 +1,5 @@
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { TProductConfigChannel } from "@formbricks/types/product";
import { SecondaryNavigation } from "@formbricks/ui/SecondaryNavigation";
interface PeopleSegmentsTabsProps {
@@ -6,7 +8,23 @@ interface PeopleSegmentsTabsProps {
loading?: boolean;
}
export const PeopleSecondaryNavigation = ({ activeId, environmentId, loading }: PeopleSegmentsTabsProps) => {
export const PeopleSecondaryNavigation = async ({
activeId,
environmentId,
loading,
}: PeopleSegmentsTabsProps) => {
let currentProductChannel: TProductConfigChannel = null;
if (!loading && environmentId) {
const product = await getProductByEnvironmentId(environmentId);
if (!product) {
throw new Error("Product not found");
}
currentProductChannel = product.config.channel ?? null;
}
const navigation = [
{
id: "people",
@@ -22,6 +40,8 @@ export const PeopleSecondaryNavigation = ({ activeId, environmentId, loading }:
id: "attributes",
label: "Attributes",
href: `/environments/${environmentId}/attributes`,
// hide attributes tab if it's being used in the loading state or if the product's channel is website or link
hidden: !!(!loading && currentProductChannel && currentProductChannel !== "app"),
},
];

View File

@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import { convertDateTimeStringShort } from "@formbricks/lib/time";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TActionClass } from "@formbricks/types/actionClasses";
import { TProductConfigChannel } from "@formbricks/types/product";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { Label } from "@formbricks/ui/Label";
import { LoadingSpinner } from "@formbricks/ui/LoadingSpinner";
@@ -19,12 +20,14 @@ interface ActivityTabProps {
actionClass: TActionClass;
environmentId: string;
isUserTargetingEnabled: boolean;
currentProductChannel: TProductConfigChannel;
}
export const EventActivityTab = ({
actionClass,
environmentId,
isUserTargetingEnabled,
currentProductChannel,
}: ActivityTabProps) => {
// const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, actionClass.id);
@@ -36,6 +39,8 @@ export const EventActivityTab = ({
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const shouldShowActivity = isUserTargetingEnabled && currentProductChannel !== "website";
useEffect(() => {
setLoading(true);
@@ -48,9 +53,9 @@ export const EventActivityTab = ({
numEventsLast7DaysData,
activeInactiveSurveys,
] = await Promise.all([
isUserTargetingEnabled ? getActionCountInLastHourAction(actionClass.id, environmentId) : 0,
isUserTargetingEnabled ? getActionCountInLast24HoursAction(actionClass.id, environmentId) : 0,
isUserTargetingEnabled ? getActionCountInLast7DaysAction(actionClass.id, environmentId) : 0,
shouldShowActivity ? getActionCountInLastHourAction(actionClass.id, environmentId) : 0,
shouldShowActivity ? getActionCountInLast24HoursAction(actionClass.id, environmentId) : 0,
shouldShowActivity ? getActionCountInLast7DaysAction(actionClass.id, environmentId) : 0,
getActiveInactiveSurveysAction(actionClass.id, environmentId),
]);
setNumEventsLastHour(numEventsLastHourData);
@@ -66,7 +71,7 @@ export const EventActivityTab = ({
};
updateState();
}, [actionClass.id, environmentId, isUserTargetingEnabled]);
}, [actionClass.id, environmentId, shouldShowActivity]);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorComponent />;
@@ -74,9 +79,9 @@ export const EventActivityTab = ({
return (
<div className="grid grid-cols-3 pb-2">
<div className="col-span-2 space-y-4 pr-6">
{isUserTargetingEnabled && (
{shouldShowActivity && (
<div>
<Label className="text-slate-500">Ocurrances</Label>
<Label className="text-slate-500">Occurrences</Label>
<div className="mt-1 grid w-fit grid-cols-3 rounded-lg border-slate-100 bg-slate-50">
<div className="border-r border-slate-200 px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLastHour}</p>

View File

@@ -3,6 +3,7 @@
import { useState } from "react";
import { useMembershipRole } from "@formbricks/lib/membership/hooks/useMembershipRole";
import { TActionClass } from "@formbricks/types/actionClasses";
import { TProductConfigChannel } from "@formbricks/types/product";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { ActionDetailModal } from "./ActionDetailModal";
@@ -11,6 +12,7 @@ interface ActionClassesTableProps {
actionClasses: TActionClass[];
children: [JSX.Element, JSX.Element[]];
isUserTargetingEnabled: boolean;
currentProductChannel: TProductConfigChannel;
}
export const ActionClassesTable = ({
@@ -18,6 +20,7 @@ export const ActionClassesTable = ({
actionClasses,
children: [TableHeading, actionRows],
isUserTargetingEnabled,
currentProductChannel,
}: ActionClassesTableProps) => {
const [isActionDetailModalOpen, setActionDetailModalOpen] = useState(false);
const { membershipRole, error } = useMembershipRole(environmentId);
@@ -60,6 +63,7 @@ export const ActionClassesTable = ({
actionClass={activeActionClass}
membershipRole={membershipRole}
isUserTargetingEnabled={isUserTargetingEnabled}
currentProductChannel={currentProductChannel}
/>
)}
</>

View File

@@ -1,6 +1,7 @@
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
import { TActionClass } from "@formbricks/types/actionClasses";
import { TMembershipRole } from "@formbricks/types/memberships";
import { TProductConfigChannel } from "@formbricks/types/product";
import { ModalWithTabs } from "@formbricks/ui/ModalWithTabs";
import { EventActivityTab } from "./ActionActivityTab";
import { ActionSettingsTab } from "./ActionSettingsTab";
@@ -13,6 +14,7 @@ interface ActionDetailModalProps {
actionClasses: TActionClass[];
membershipRole?: TMembershipRole;
isUserTargetingEnabled: boolean;
currentProductChannel: TProductConfigChannel;
}
export const ActionDetailModal = ({
@@ -23,6 +25,7 @@ export const ActionDetailModal = ({
actionClasses,
membershipRole,
isUserTargetingEnabled,
currentProductChannel,
}: ActionDetailModalProps) => {
const tabs = [
{
@@ -32,6 +35,7 @@ export const ActionDetailModal = ({
actionClass={actionClass}
environmentId={environmentId}
isUserTargetingEnabled={isUserTargetingEnabled}
currentProductChannel={currentProductChannel}
/>
),
},

View File

@@ -3,10 +3,12 @@ import { ActionClassDataRow } from "@/app/(app)/environments/[environmentId]/act
import { ActionTableHeading } from "@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading";
import { AddActionModal } from "@/app/(app)/environments/[environmentId]/actions/components/AddActionModal";
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { getAdvancedTargetingPermission } from "@formbricks/ee/lib/service";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
@@ -15,8 +17,9 @@ export const metadata: Metadata = {
};
const Page = async ({ params }) => {
const [actionClasses, organization] = await Promise.all([
const [actionClasses, product, organization] = await Promise.all([
getActionClasses(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getOrganizationByEnvironmentId(params.environmentId),
]);
@@ -24,6 +27,11 @@ const Page = async ({ params }) => {
throw new Error("Organization not found");
}
const currentProductChannel = product?.config.channel ?? null;
if (currentProductChannel === "link") {
return notFound();
}
// On Formbricks Cloud only render the timeline if the user targeting feature is booked
const isUserTargetingEnabled = IS_FORMBRICKS_CLOUD
? await getAdvancedTargetingPermission(organization)
@@ -39,7 +47,8 @@ const Page = async ({ params }) => {
<ActionClassesTable
environmentId={params.environmentId}
actionClasses={actionClasses}
isUserTargetingEnabled={isUserTargetingEnabled}>
isUserTargetingEnabled={isUserTargetingEnabled}
currentProductChannel={currentProductChannel}>
<ActionTableHeading />
{actionClasses.map((actionClass) => (
<ActionClassDataRow key={actionClass.id} actionClass={actionClass} />

View File

@@ -45,6 +45,9 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
const currentProductChannel =
products.find((product) => product.id === environment.productId)?.config.channel ?? null;
const [peopleCount, responseCount] = await Promise.all([
getMonthlyActiveOrganizationPeopleCount(organization.id),
getMonthlyOrganizationResponseCount(organization.id),
@@ -74,7 +77,11 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
isMultiOrgEnabled={isMultiOrgEnabled}
/>
<div id="mainContent" className="flex-1 overflow-y-auto bg-slate-50">
<TopControlBar environment={environment} environments={environments} />
<TopControlBar
environment={environment}
environments={environments}
currentProductChannel={currentProductChannel}
/>
<div className="mt-14">{children}</div>
</div>
</div>

View File

@@ -152,7 +152,7 @@ export const MainNavigation = ({
href: `/environments/${environment.id}/actions`,
icon: MousePointerClick,
isActive: pathname?.includes("/actions") || pathname?.includes("/actions"),
isHidden: false,
isHidden: product?.config.channel === "link",
},
{
name: "Integrations",

View File

@@ -2,19 +2,28 @@ import { TopControlButtons } from "@/app/(app)/environments/[environmentId]/comp
import { WidgetStatusIndicator } from "@/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { TEnvironment } from "@formbricks/types/environment";
import { TProductConfigChannel } from "@formbricks/types/product";
interface SideBarProps {
environment: TEnvironment;
environments: TEnvironment[];
currentProductChannel: TProductConfigChannel;
}
export const TopControlBar = ({ environment, environments }: SideBarProps) => {
export const TopControlBar = ({ environment, environments, currentProductChannel }: SideBarProps) => {
return (
<div className="fixed inset-0 top-0 z-30 flex h-14 w-full items-center justify-end bg-slate-50 px-6">
<div className="shadow-xs z-10">
<div className="flex w-fit space-x-2 py-2">
<WidgetStatusIndicator environment={environment} size="mini" type="website" />
<WidgetStatusIndicator environment={environment} size="mini" type="app" />
<div className="flex w-fit items-center space-x-2 py-2">
{currentProductChannel && currentProductChannel !== "link" && (
<WidgetStatusIndicator environment={environment} size="mini" type={currentProductChannel} />
)}
{!currentProductChannel && (
<>
<WidgetStatusIndicator environment={environment} size="mini" type="website" />
<WidgetStatusIndicator environment={environment} size="mini" type="app" />
</>
)}
<TopControlButtons
environment={environment}
environments={environments}

View File

@@ -92,12 +92,14 @@ const Loading = () => {
label: "Website Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/website-connection"),
hidden: true,
},
{
id: "app-connection",
label: "App Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/app-connection"),
hidden: true,
},
];

View File

@@ -2,18 +2,21 @@ import { WidgetStatusIndicator } from "@/app/(app)/environments/[environmentId]/
import { EnvironmentIdField } from "@/app/(app)/environments/[environmentId]/product/(setup)/components/EnvironmentIdField";
import { SetupInstructions } from "@/app/(app)/environments/[environmentId]/product/(setup)/components/SetupInstructions";
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
import { notFound } from "next/navigation";
import { getMultiLanguagePermission } from "@formbricks/ee/lib/service";
import { IS_FORMBRICKS_CLOUD, WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { EnvironmentNotice } from "@formbricks/ui/EnvironmentNotice";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
import { SettingsCard } from "../../../settings/components/SettingsCard";
const Page = async ({ params }) => {
const [environment, organization] = await Promise.all([
const [environment, product, organization] = await Promise.all([
getEnvironment(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getOrganizationByEnvironmentId(params.environmentId),
]);
@@ -26,6 +29,11 @@ const Page = async ({ params }) => {
}
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
const currentProductChannel = product?.config.channel ?? null;
if (currentProductChannel && currentProductChannel !== "app") {
return notFound();
}
return (
<PageContentWrapper>
@@ -34,6 +42,7 @@ const Page = async ({ params }) => {
environmentId={params.environmentId}
activeId="app-connection"
isMultiLanguageAllowed={isMultiLanguageAllowed}
productChannel={currentProductChannel}
/>
</PageHeader>
<div className="space-y-4">

View File

@@ -92,12 +92,14 @@ const Loading = () => {
label: "Website Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/website-connection"),
hidden: true,
},
{
id: "app-connection",
label: "App Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/app-connection"),
hidden: true,
},
];

View File

@@ -2,18 +2,21 @@ import { WidgetStatusIndicator } from "@/app/(app)/environments/[environmentId]/
import { EnvironmentIdField } from "@/app/(app)/environments/[environmentId]/product/(setup)/components/EnvironmentIdField";
import { SetupInstructions } from "@/app/(app)/environments/[environmentId]/product/(setup)/components/SetupInstructions";
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
import { notFound } from "next/navigation";
import { getMultiLanguagePermission } from "@formbricks/ee/lib/service";
import { IS_FORMBRICKS_CLOUD, WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { EnvironmentNotice } from "@formbricks/ui/EnvironmentNotice";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
import { SettingsCard } from "../../../settings/components/SettingsCard";
const Page = async ({ params }) => {
const [environment, organization] = await Promise.all([
const [environment, product, organization] = await Promise.all([
getEnvironment(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getOrganizationByEnvironmentId(params.environmentId),
]);
@@ -26,6 +29,11 @@ const Page = async ({ params }) => {
}
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
const currentProductChannel = product?.config.channel ?? null;
if (currentProductChannel && currentProductChannel !== "website") {
return notFound();
}
return (
<PageContentWrapper>
@@ -34,6 +42,7 @@ const Page = async ({ params }) => {
environmentId={params.environmentId}
activeId="website-connection"
isMultiLanguageAllowed={isMultiLanguageAllowed}
productChannel={currentProductChannel}
/>
</PageHeader>
<div className="space-y-4">

View File

@@ -76,12 +76,14 @@ const Loading = () => {
label: "Website Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/website-connection"),
hidden: true,
},
{
id: "app-connection",
label: "App Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/app-connection"),
hidden: true,
},
];

View File

@@ -6,6 +6,7 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { EnvironmentNotice } from "@formbricks/ui/EnvironmentNotice";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
@@ -14,9 +15,12 @@ import { SettingsCard } from "../../settings/components/SettingsCard";
import { ApiKeyList } from "./components/ApiKeyList";
const Page = async ({ params }) => {
const environment = await getEnvironment(params.environmentId);
const organization = await getOrganizationByEnvironmentId(params.environmentId);
const session = await getServerSession(authOptions);
const [session, environment, product, organization] = await Promise.all([
getServerSession(authOptions),
getEnvironment(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getOrganizationByEnvironmentId(params.environmentId),
]);
if (!environment) {
throw new Error("Environment not found");
@@ -31,6 +35,7 @@ const Page = async ({ params }) => {
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isViewer } = getAccessFlags(currentUserMembership?.role);
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
const currentProductChannel = product?.config.channel ?? null;
return !isViewer ? (
<PageContentWrapper>
@@ -39,6 +44,7 @@ const Page = async ({ params }) => {
environmentId={params.environmentId}
activeId="api-keys"
isMultiLanguageAllowed={isMultiLanguageAllowed}
productChannel={currentProductChannel}
/>
</PageHeader>
<EnvironmentNotice environmentId={environment.id} subPageUrl="/product/api-keys" />

View File

@@ -2,18 +2,21 @@
import { BrushIcon, KeyIcon, LanguagesIcon, ListChecksIcon, TagIcon, UsersIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import { TProductConfigChannel } from "@formbricks/types/product";
import { SecondaryNavigation } from "@formbricks/ui/SecondaryNavigation";
interface ProductConfigNavigationProps {
environmentId: string;
activeId: string;
isMultiLanguageAllowed: boolean;
productChannel: TProductConfigChannel;
}
export const ProductConfigNavigation = ({
environmentId,
activeId,
isMultiLanguageAllowed,
productChannel,
}: ProductConfigNavigationProps) => {
const pathname = usePathname();
let navigation = [
@@ -59,6 +62,7 @@ export const ProductConfigNavigation = ({
icon: <ListChecksIcon className="h-5 w-5" />,
href: `/environments/${environmentId}/product/website-connection`,
current: pathname?.includes("/website-connection"),
hidden: !!(productChannel && productChannel !== "website"),
},
{
id: "app-connection",
@@ -66,6 +70,7 @@ export const ProductConfigNavigation = ({
icon: <ListChecksIcon className="h-5 w-5" />,
href: `/environments/${environmentId}/product/app-connection`,
current: pathname?.includes("/app-connection"),
hidden: !!(productChannel && productChannel !== "app"),
},
];

View File

@@ -85,12 +85,14 @@ const Loading = () => {
label: "Website Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/website-connection"),
hidden: true,
},
{
id: "app-connection",
label: "App Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/app-connection"),
hidden: true,
},
];

View File

@@ -41,6 +41,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
}
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
const currentProductChannel = product?.config.channel ?? null;
return (
<PageContentWrapper>
@@ -49,6 +50,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
environmentId={params.environmentId}
activeId="general"
isMultiLanguageAllowed={isMultiLanguageAllowed}
productChannel={currentProductChannel}
/>
</PageHeader>
@@ -59,11 +61,13 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
isProductNameEditDisabled={isProductNameEditDisabled}
/>
</SettingsCard>
<SettingsCard
title="Recontact Waiting Time"
description="Control how frequently users can be surveyed across all surveys.">
<EditWaitingTimeForm environmentId={params.environmentId} product={product} />
</SettingsCard>
{currentProductChannel !== "link" && (
<SettingsCard
title="Recontact Waiting Time"
description="Control how frequently users can be surveyed across all surveys.">
<EditWaitingTimeForm environmentId={params.environmentId} product={product} />
</SettingsCard>
)}
<SettingsCard
title="Delete Product"
description="Delete product with all surveys, responses, people, actions and attributes. This cannot be undone.">

View File

@@ -26,6 +26,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
if (!isMultiLanguageAllowed) {
notFound();
}
const currentProductChannel = product?.config.channel ?? null;
return (
<PageContentWrapper>
@@ -34,6 +35,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
environmentId={params.environmentId}
activeId="languages"
isMultiLanguageAllowed={isMultiLanguageAllowed}
productChannel={currentProductChannel}
/>
</PageHeader>
<SettingsCard

View File

@@ -119,6 +119,8 @@ export const ThemeStylingPreviewSurvey = ({
}
};
const currentProductChannel = product?.config.channel ?? null;
return (
<div className="flex h-full w-full flex-col items-center justify-items-center overflow-hidden">
<motion.div
@@ -210,11 +212,13 @@ export const ThemeStylingPreviewSurvey = ({
Link survey
</div>
<div
className={`${isAppSurvey ? "rounded-full bg-slate-200" : ""} cursor-pointer px-3 py-1 text-sm`}
onClick={() => setPreviewType("app")}>
App / Website survey
</div>
{currentProductChannel !== "link" && (
<div
className={`${isAppSurvey ? "rounded-full bg-slate-200" : ""} cursor-pointer px-3 py-1 text-sm`}
onClick={() => setPreviewType("app")}>
App / Website survey
</div>
)}
</div>
</div>
);

View File

@@ -60,12 +60,14 @@ const Loading = () => {
label: "Website Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/website-connection"),
hidden: true,
},
{
id: "app-connection",
label: "App Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/app-connection"),
hidden: true,
},
];

View File

@@ -48,6 +48,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
}
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
const currentProductChannel = product?.config.channel ?? null;
return (
<PageContentWrapper>
@@ -56,6 +57,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
environmentId={params.environmentId}
activeId="look"
isMultiLanguageAllowed={isMultiLanguageAllowed}
productChannel={currentProductChannel}
/>
</PageHeader>
<SettingsCard
@@ -72,27 +74,31 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
<SettingsCard title="Logo" description="Upload your company logo to brand surveys and link previews.">
<EditLogo product={product} environmentId={params.environmentId} isViewer={isViewer} />
</SettingsCard>
<SettingsCard
title="In-app Survey Placement"
description="Change where surveys will be shown in your web app.">
<EditPlacementForm product={product} environmentId={params.environmentId} />
</SettingsCard>
<SettingsCard
title="Formbricks Branding"
description="We love your support but understand if you toggle it off.">
<EditFormbricksBranding
type="linkSurvey"
product={product}
canRemoveBranding={canRemoveLinkBranding}
environmentId={params.environmentId}
/>
<EditFormbricksBranding
type="inAppSurvey"
product={product}
canRemoveBranding={canRemoveInAppBranding}
environmentId={params.environmentId}
/>
</SettingsCard>
{currentProductChannel !== "link" && (
<SettingsCard
title="In-app Survey Placement"
description="Change where surveys will be shown in your web app.">
<EditPlacementForm product={product} environmentId={params.environmentId} />
</SettingsCard>
)}
{currentProductChannel !== "link" && (
<SettingsCard
title="Formbricks Branding"
description="We love your support but understand if you toggle it off.">
<EditFormbricksBranding
type="linkSurvey"
product={product}
canRemoveBranding={canRemoveLinkBranding}
environmentId={params.environmentId}
/>
<EditFormbricksBranding
type="inAppSurvey"
product={product}
canRemoveBranding={canRemoveInAppBranding}
environmentId={params.environmentId}
/>
</SettingsCard>
)}
</PageContentWrapper>
);
};

View File

@@ -7,6 +7,7 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
import { getTagsOnResponsesCount } from "@formbricks/lib/tagOnResponse/service";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
@@ -19,10 +20,14 @@ const Page = async ({ params }) => {
if (!environment) {
throw new Error("Environment not found");
}
const tags = await getTagsByEnvironmentId(params.environmentId);
const environmentTagsCount = await getTagsOnResponsesCount(params.environmentId);
const organization = await getOrganizationByEnvironmentId(params.environmentId);
const session = await getServerSession(authOptions);
const [tags, environmentTagsCount, product, organization, session] = await Promise.all([
getTagsByEnvironmentId(params.environmentId),
getTagsOnResponsesCount(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getOrganizationByEnvironmentId(params.environmentId),
getServerSession(authOptions),
]);
if (!environment) {
throw new Error("Environment not found");
@@ -40,6 +45,7 @@ const Page = async ({ params }) => {
const isTagSettingDisabled = isViewer;
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
const currentProductChannel = product?.config.channel ?? null;
return !isTagSettingDisabled ? (
<PageContentWrapper>
@@ -48,6 +54,7 @@ const Page = async ({ params }) => {
environmentId={params.environmentId}
activeId="tags"
isMultiLanguageAllowed={isMultiLanguageAllowed}
productChannel={currentProductChannel}
/>
</PageHeader>
<SettingsCard title="Manage Tags" description="Add, merge and remove response tags.">

View File

@@ -61,6 +61,8 @@ const Page = async ({ params, searchParams }: SurveyTemplateProps) => {
const environments = await getEnvironments(product.id);
const otherEnvironment = environments.find((e) => e.type !== environment.type)!;
const currentProductChannel = product.config.channel ?? null;
const CreateSurveyButton = (
<Button
size="sm"
@@ -83,6 +85,7 @@ const Page = async ({ params, searchParams }: SurveyTemplateProps) => {
WEBAPP_URL={WEBAPP_URL}
userId={session.user.id}
surveysPerPage={SURVEYS_PER_PAGE}
currentProductChannel={currentProductChannel}
/>
</>
) : (

View File

@@ -21,7 +21,6 @@ import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { getSyncSurveys, transformToLegacySurvey } from "@formbricks/lib/survey/service";
import { isVersionGreaterThanOrEqualTo } from "@formbricks/lib/utils/version";
import { TLegacySurvey } from "@formbricks/types/LegacySurvey";
import { TEnvironment } from "@formbricks/types/environment";
import { TJsAppLegacyStateSync, TJsAppStateSync, ZJsPeopleUserIdInput } from "@formbricks/types/js";
import { TProductLegacy } from "@formbricks/types/product";
import { TSurvey } from "@formbricks/types/surveys";
@@ -62,24 +61,20 @@ export const GET = async (
const { environmentId, userId } = inputValidation.data;
let environment: TEnvironment | null;
// check if environment exists
environment = await getEnvironment(environmentId);
const environment = await getEnvironment(environmentId);
if (!environment) {
throw new Error("Environment does not exist");
}
// temporary remove the example survey creation to avoid caching issue with multiple example surveys
/* if (!environment.appSetupCompleted) {
const exampleTrigger = await getActionClassByEnvironmentIdAndName(environmentId, "New Session");
if (!exampleTrigger) {
throw new Error("Example trigger not found");
}
const firstSurvey = getExampleAppSurveyTemplate(WEBAPP_URL, exampleTrigger);
await createSurvey(environmentId, firstSurvey);
await updateEnvironment(environment.id, { appSetupCompleted: true });
} */
const product = await getProductByEnvironmentId(environmentId);
if (!product) {
throw new Error("Product not found");
}
if (product.config.channel && product.config.channel !== "app") {
return responses.forbiddenResponse("Product channel is not app", true);
}
if (!environment.appSetupCompleted) {
await updateEnvironment(environment.id, { appSetupCompleted: true });
@@ -169,12 +164,11 @@ export const GET = async (
}
}
const [surveys, actionClasses, product] = await Promise.all([
const [surveys, actionClasses] = await Promise.all([
getSyncSurveys(environmentId, person.id, device.type === "mobile" ? "phone" : "desktop", {
version: version ?? undefined,
}),
getActionClasses(environmentId),
getProductByEnvironmentId(environmentId),
]);
if (!product) {

View File

@@ -46,8 +46,11 @@ export const GET = async (
const { environmentId } = syncInputValidation.data;
const environment = await getEnvironment(environmentId);
const organization = await getOrganizationByEnvironmentId(environmentId);
const [environment, organization, product] = await Promise.all([
getEnvironment(environmentId),
getOrganizationByEnvironmentId(environmentId),
getProductByEnvironmentId(environmentId),
]);
if (!organization) {
throw new Error("Organization does not exist");
@@ -57,6 +60,14 @@ export const GET = async (
throw new Error("Environment does not exist");
}
if (!product) {
throw new Error("Product not found");
}
if (product.config.channel && product.config.channel !== "website") {
return responses.forbiddenResponse("Product channel is not website", true);
}
// check if response limit is reached
let isWebsiteSurveyResponseLimitReached = false;
if (IS_FORMBRICKS_CLOUD) {
@@ -93,16 +104,11 @@ export const GET = async (
await updateEnvironment(environment.id, { websiteSetupCompleted: true });
}
const [surveys, actionClasses, product] = await Promise.all([
const [surveys, actionClasses] = await Promise.all([
getSurveys(environmentId),
getActionClasses(environmentId),
getProductByEnvironmentId(environmentId),
]);
if (!product) {
throw new Error("Product not found");
}
// Common filter condition for selecting surveys that are in progress, are of type 'website' and have no active segment filtering.
const filteredSurveys = surveys.filter(
(survey) => survey.status === "inProgress" && survey.type === "website"

View File

@@ -9,7 +9,7 @@ test.describe("JS Package Test", async () => {
test("Admin creates an In-App Survey", async ({ page }) => {
await signUpAndLogin(page, name, email, password);
await finishOnboarding(page);
await finishOnboarding(page, "app");
await page.getByRole("heading", { name: "Product Market Fit (Superhuman)" }).isVisible();
await page.getByRole("heading", { name: "Product Market Fit (Superhuman)" }).click();
@@ -22,12 +22,17 @@ test.describe("JS Package Test", async () => {
await expect(page.locator("#howToSendCardTrigger")).toBeVisible();
await page.locator("#howToSendCardTrigger").click();
await expect(page.locator("#howToSendCardOption-website")).toBeVisible();
await page.locator("#howToSendCardOption-website").click();
await page.locator("#howToSendCardOption-website").click();
await page.locator("#whenToSendCardTrigger").click();
await expect(page.locator("#howToSendCardOption-app")).toBeVisible();
await page.locator("#howToSendCardOption-app").click();
await page.getByRole("button", { name: "Add action" }).click();
await page.getByText("New SessionGets fired when a").click();
await page.locator("#recontactOptionsCardTrigger").click();
await page.locator("label").filter({ hasText: "Keep showing while conditions" }).click();
await page.locator("#recontactDays").check();
await page.getByRole("button", { name: "Publish" }).click();
environmentId =
@@ -52,7 +57,7 @@ test.describe("JS Package Test", async () => {
await page.goto(htmlFile);
// Formbricks In App Sync has happened
const syncApi = await page.waitForResponse((response) => response.url().includes("/website/sync"));
const syncApi = await page.waitForResponse((response) => response.url().includes("/app/sync"));
expect(syncApi.status()).toBe(200);
// Formbricks Modal exists in the DOM
@@ -79,7 +84,7 @@ test.describe("JS Package Test", async () => {
await page.goto(htmlFile);
// Formbricks In App Sync has happened
const syncApi = await page.waitForResponse((response) => response.url().includes("/website/sync"));
const syncApi = await page.waitForResponse((response) => response.url().includes("/app/sync"));
expect(syncApi.status()).toBe(200);
// Formbricks Modal exists in the DOM

View File

@@ -186,7 +186,7 @@ test.describe("Multi Language Survey Create", async () => {
const { name, email, password } = users.survey[3];
test("Create Survey", async ({ page }) => {
await signUpAndLogin(page, name, email, password);
await finishOnboarding(page);
await finishOnboarding(page, "app");
//add a new language
await page.getByRole("link", { name: "Configuration" }).click();
@@ -418,6 +418,8 @@ test.describe("Multi Language Survey Create", async () => {
.getByPlaceholder("Your description here. Recall")
.fill(surveys.germanCreate.thankYouCard.description);
await page.locator("#showButton").check();
await page.getByPlaceholder("Create your own Survey").click();
await page.getByPlaceholder("Create your own Survey").fill(surveys.germanCreate.thankYouCard.buttonLabel);

View File

@@ -2,6 +2,7 @@ import { CreateSurveyParams } from "@/playwright/utils/mock";
import { expect } from "@playwright/test";
import { readFileSync, writeFileSync } from "fs";
import { Page } from "playwright";
import { TProductConfigChannel } from "@formbricks/types/product";
export const signUpAndLogin = async (
page: Page,
@@ -54,15 +55,31 @@ export const login = async (page: Page, email: string, password: string): Promis
await page.getByRole("button", { name: "Login with Email" }).click();
};
export const finishOnboarding = async (page: Page): Promise<void> => {
export const finishOnboarding = async (
page: Page,
ProductChannel: TProductConfigChannel = "website"
): Promise<void> => {
await page.waitForURL(/\/organizations\/[^/]+\/products\/new\/channel/);
await page.getByRole("button", { name: "100% custom branding Anywhere" }).click();
if (ProductChannel === "website") {
await page.getByRole("button", { name: "Built for scale Public website" }).click();
} else if (ProductChannel === "app") {
await page.getByRole("button", { name: "Enrich user profiles App with" }).click();
} else {
await page.getByRole("button", { name: "100% custom branding Anywhere" }).click();
}
await page.getByRole("button", { name: "Proven methods SaaS" }).click();
await page.getByPlaceholder("Formbricks Merch Store").click();
await page.getByPlaceholder("Formbricks Merch Store").fill("My Product");
await page.locator("form").filter({ hasText: "Brand colorChange the brand" }).getByRole("button").click();
if (ProductChannel !== "link") {
await page.getByRole("button", { name: "Skip" }).click();
await page.waitForTimeout(500);
await page.getByRole("button", { name: "Skip" }).click();
}
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
await expect(page.getByText("My Product")).toBeVisible();
};

View File

@@ -234,6 +234,10 @@ export const getBiggerUploadFileSizePermission = async (organization: TOrganizat
};
export const getMultiLanguagePermission = async (organization: TOrganization): Promise<boolean> => {
if (E2E_TESTING) {
const previousResult = await fetchLicenseForE2ETesting();
return previousResult && previousResult.active !== null ? previousResult.active : false;
}
if (IS_FORMBRICKS_CLOUD)
return (
organization.billing.plan === PRODUCT_FEATURE_KEYS.SCALE ||

View File

@@ -2,12 +2,13 @@
<script type="text/javascript">
!(function () {
var t = document.createElement("script");
(t.type = "text/javascript"), (t.async = !0), (t.src = "http://localhost:3000/api/packages/website");
(t.type = "text/javascript"), (t.async = !0), (t.src = "http://localhost:3000/api/packages/app");
var e = document.getElementsByTagName("script")[0];
e.parentNode.insertBefore(t, e),
setTimeout(function () {
formbricks.init({
environmentId: "clxlr7qq2004nh7aeoh90m13l",
environmentId: "clxmx0pbw00hj9ed7xxpxlos1",
userId: "RANDOM_USER_ID",
apiHost: "http://localhost:3000",
});
}, 500);

View File

@@ -16,7 +16,7 @@ export const AdditionalIntegrationSettings = ({
}: AdditionalIntegrationSettingsProps) => {
return (
<div className="mt-4">
<Label htmlFor="Surveys">Additional Setings</Label>
<Label htmlFor="Surveys">Additional Settings</Label>
<div className="text-sm">
<div className="my-1 flex items-center space-x-2">
<label htmlFor={"includeHiddenFields"} className="flex cursor-pointer items-center">

View File

@@ -1,6 +1,7 @@
import { ChevronDownIcon, Equal, Grid2X2, Search, X } from "lucide-react";
import { useState } from "react";
import { useDebounce } from "react-use";
import { TProductConfigChannel } from "@formbricks/types/product";
import { TFilterOption, TSortOption, TSurveyFilters } from "@formbricks/types/surveys";
import { initialFilters } from "..";
import { Button } from "../../Button";
@@ -14,6 +15,7 @@ interface SurveyFilterProps {
setOrientation: (orientation: string) => void;
surveyFilters: TSurveyFilters;
setSurveyFilters: React.Dispatch<React.SetStateAction<TSurveyFilters>>;
currentProductChannel: TProductConfigChannel;
}
const creatorOptions: TFilterOption[] = [
@@ -28,11 +30,6 @@ const statusOptions: TFilterOption[] = [
{ label: "Completed", value: "completed" },
{ label: "Draft", value: "draft" },
];
const typeOptions: TFilterOption[] = [
{ label: "Link", value: "link" },
{ label: "App", value: "app" },
{ label: "Website", value: "website" },
];
const sortOptions: TSortOption[] = [
{
@@ -59,6 +56,7 @@ export const SurveyFilters = ({
setOrientation,
surveyFilters,
setSurveyFilters,
currentProductChannel,
}: SurveyFilterProps) => {
const { createdBy, sortBy, status, type } = surveyFilters;
const [name, setName] = useState("");
@@ -67,6 +65,20 @@ export const SurveyFilters = ({
const [dropdownOpenStates, setDropdownOpenStates] = useState(new Map());
const typeOptions: TFilterOption[] = [
{ label: "Link", value: "link" },
{ label: "App", value: "app" },
{ label: "Website", value: "website" },
].filter((option) => {
if (currentProductChannel === "website") {
return option.value !== "app";
} else if (currentProductChannel === "app") {
return option.value !== "website";
} else {
return option;
}
});
const toggleDropdown = (id: string) => {
setDropdownOpenStates(new Map(dropdownOpenStates).set(id, !dropdownOpenStates.get(id)));
};
@@ -152,17 +164,19 @@ export const SurveyFilters = ({
toggleDropdown={toggleDropdown}
/>
</div>
<div>
<SurveyFilterDropdown
title="Type"
id="type"
options={typeOptions}
selectedOptions={type}
setSelectedOptions={handleTypeChange}
isOpen={dropdownOpenStates.get("type")}
toggleDropdown={toggleDropdown}
/>
</div>
{currentProductChannel !== "link" && (
<div>
<SurveyFilterDropdown
title="Type"
id="type"
options={typeOptions}
selectedOptions={type}
setSelectedOptions={handleTypeChange}
isOpen={dropdownOpenStates.get("type")}
toggleDropdown={toggleDropdown}
/>
</div>
)}
{(createdBy.length > 0 || status.length > 0 || type.length > 0) && (
<Button

View File

@@ -2,6 +2,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TProductConfigChannel } from "@formbricks/types/product";
import { TSurvey, TSurveyFilters } from "@formbricks/types/surveys";
import { Button } from "../v2/Button";
import { getSurveysAction } from "./actions";
@@ -16,6 +17,7 @@ interface SurveysListProps {
WEBAPP_URL: string;
userId: string;
surveysPerPage: number;
currentProductChannel: TProductConfigChannel;
}
export const initialFilters: TSurveyFilters = {
@@ -33,6 +35,7 @@ export const SurveysList = ({
WEBAPP_URL,
userId,
surveysPerPage: surveysLimit,
currentProductChannel,
}: SurveysListProps) => {
const [surveys, setSurveys] = useState<TSurvey[]>([]);
const [isFetching, setIsFetching] = useState(true);
@@ -100,6 +103,7 @@ export const SurveysList = ({
setOrientation={setOrientation}
surveyFilters={surveyFilters}
setSurveyFilters={setSurveyFilters}
currentProductChannel={currentProductChannel}
/>
{surveys.length > 0 ? (
<div>

View File

@@ -37,11 +37,7 @@ export const TemplateList = ({
const createSurvey = async (activeTemplate: TTemplate) => {
setLoading(true);
const surveyType = environment?.appSetupCompleted
? "app"
: environment?.websiteSetupCompleted
? "website"
: "link";
const surveyType = product.config.channel ?? "link";
const augmentedTemplate: TSurveyInput = {
...activeTemplate.preset,
type: surveyType,