mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 09:00:18 -06:00
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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -192,6 +192,7 @@ export const SurveyEditor = ({
|
||||
membershipRole={membershipRole}
|
||||
isUserTargetingAllowed={isUserTargetingAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
product={localProduct}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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.">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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.">
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user