feat: Product Model Revamp (#4353)

Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Piyush Gupta
2024-12-03 10:04:09 +05:30
committed by GitHub
parent 5dcd32050a
commit 35b2d12e18
315 changed files with 4344 additions and 3587 deletions
@@ -7,14 +7,14 @@ import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { cn } from "@formbricks/lib/cn";
import { TEnvironment } from "@formbricks/types/environment";
import { TProductConfigChannel } from "@formbricks/types/product";
import { TProjectConfigChannel } from "@formbricks/types/project";
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
interface ConnectWithFormbricksProps {
environment: TEnvironment;
webAppUrl: string;
widgetSetupCompleted: boolean;
channel: TProductConfigChannel;
channel: TProjectConfigChannel;
}
export const ConnectWithFormbricks = ({
@@ -8,7 +8,7 @@ import { useTranslations } from "next-intl";
import "prismjs/themes/prism.css";
import { useState } from "react";
import toast from "react-hot-toast";
import { TProductConfigChannel } from "@formbricks/types/product";
import { TProjectConfigChannel } from "@formbricks/types/project";
const tabs = [
{ id: "html", label: "HTML", icon: <Html5Icon /> },
@@ -18,7 +18,7 @@ const tabs = [
interface OnboardingSetupInstructionsProps {
environmentId: string;
webAppUrl: string;
channel: TProductConfigChannel;
channel: TProjectConfigChannel;
widgetSetupCompleted: boolean;
}
@@ -5,7 +5,7 @@ import { XIcon } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
interface ConnectPageProps {
params: Promise<{
@@ -22,12 +22,12 @@ const Page = async (props: ConnectPageProps) => {
throw new Error(t("common.environment_not_found"));
}
const product = await getProductByEnvironmentId(environment.id);
if (!product) {
throw new Error(t("common.product_not_found"));
const project = await getProjectByEnvironmentId(environment.id);
if (!project) {
throw new Error(t("common.project_not_found"));
}
const channel = product.config.channel || null;
const channel = project.config.channel || null;
return (
<div className="flex min-h-full flex-col items-center justify-center py-10">
@@ -10,18 +10,18 @@ import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
import { TProduct } from "@formbricks/types/product";
import { TProject } from "@formbricks/types/project";
import { TSurveyCreateInput } from "@formbricks/types/surveys/types";
import { TXMTemplate } from "@formbricks/types/templates";
import { TUser } from "@formbricks/types/user";
interface XMTemplateListProps {
product: TProduct;
project: TProject;
user: TUser;
environmentId: string;
}
export const XMTemplateList = ({ product, user, environmentId }: XMTemplateListProps) => {
export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListProps) => {
const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null);
const t = useTranslations();
const router = useRouter();
@@ -48,7 +48,7 @@ export const XMTemplateList = ({ product, user, environmentId }: XMTemplateListP
const handleTemplateClick = (templateIdx) => {
setActiveTemplateId(templateIdx);
const template = getXMTemplates(user.locale)[templateIdx];
const newTemplate = replacePresetPlaceholders(template, product);
const newTemplate = replacePresetPlaceholders(template, project);
createSurvey(newTemplate);
};
@@ -1,13 +1,13 @@
import { replaceQuestionPresetPlaceholders } from "@formbricks/lib/utils/templates";
import { TProduct } from "@formbricks/types/product";
import { TProject } from "@formbricks/types/project";
import { TXMTemplate } from "@formbricks/types/templates";
// replace all occurences of productName with the actual product name in the current template
export const replacePresetPlaceholders = (template: TXMTemplate, product: TProduct) => {
// replace all occurences of projectName with the actual project name in the current template
export const replacePresetPlaceholders = (template: TXMTemplate, project: TProject) => {
const survey = structuredClone(template);
survey.name = survey.name.replace("{{productName}}", product.name);
survey.name = survey.name.replace("{{projectName}}", project.name);
survey.questions = survey.questions.map((question) => {
return replaceQuestionPresetPlaceholders(question, product);
return replaceQuestionPresetPlaceholders(question, project);
});
return { ...template, ...survey };
};
@@ -7,7 +7,7 @@ import { XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProductByEnvironmentId, getUserProducts } from "@formbricks/lib/product/service";
import { getProjectByEnvironmentId, getUserProjects } from "@formbricks/lib/project/service";
import { getUser } from "@formbricks/lib/user/service";
interface XMTemplatePageProps {
@@ -35,18 +35,18 @@ const Page = async (props: XMTemplatePageProps) => {
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
const product = await getProductByEnvironmentId(environment.id);
if (!product) {
throw new Error(t("common.product_not_found"));
const project = await getProjectByEnvironmentId(environment.id);
if (!project) {
throw new Error(t("common.project_not_found"));
}
const products = await getUserProducts(session.user.id, organizationId);
const projects = await getUserProjects(session.user.id, organizationId);
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header title={t("environments.xm-templates.headline")} />
<XMTemplateList product={product} user={user} environmentId={environment.id} />
{products.length >= 2 && (
<XMTemplateList project={project} user={user} environmentId={environment.id} />
{projects.length >= 2 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="minimal"
@@ -26,12 +26,12 @@ export const getTeamsByOrganizationId = reactCache(
},
});
const productTeams = teams.map((team) => ({
const projectTeams = teams.map((team) => ({
id: team.id,
name: team.name,
}));
return productTeams;
return projectTeams;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
+5 -5
View File
@@ -1,12 +1,12 @@
import { TProductConfigChannel } from "@formbricks/types/product";
import { TProjectConfigChannel } from "@formbricks/types/project";
export const getCustomHeadline = (channel?: TProductConfigChannel) => {
export const getCustomHeadline = (channel?: TProjectConfigChannel) => {
switch (channel) {
case "website":
return "organizations.products.new.settings.website_channel_headline";
return "organizations.projects.new.settings.website_channel_headline";
case "app":
return "organizations.products.new.settings.app_channel_headline";
return "organizations.projects.new.settings.app_channel_headline";
default:
return "organizations.products.new.settings.link_channel_headline";
return "organizations.projects.new.settings.link_channel_headline";
}
};
@@ -3,7 +3,7 @@ import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation";
import { getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getUserProducts } from "@formbricks/lib/product/service";
import { getUserProjects } from "@formbricks/lib/project/service";
const LandingLayout = async (props) => {
const params = await props.params;
@@ -21,11 +21,11 @@ const LandingLayout = async (props) => {
return notFound();
}
const products = await getUserProducts(session.user.id, params.organizationId);
const projects = await getUserProjects(session.user.id, params.organizationId);
if (products.length !== 0) {
const firstProduct = products[0];
const environments = await getEnvironments(firstProduct.id);
if (projects.length !== 0) {
const firstProject = projects[0];
const environments = await getEnvironments(firstProject.id);
const prodEnvironment = environments.find((e) => e.type === "production");
if (prodEnvironment) {
@@ -39,8 +39,8 @@ const Page = async (props) => {
<div className="flex-1">
<div className="flex h-full flex-col items-center justify-center space-y-12">
<Header
title={t("organizations.landing.no_products_warning_title")}
subtitle={t("organizations.landing.no_products_warning_subtitle")}
title={t("organizations.landing.no_projects_warning_title")}
subtitle={t("organizations.landing.no_projects_warning_subtitle")}
/>
</div>
</div>
@@ -9,7 +9,7 @@ import { getOrganization } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
import { AuthorizationError } from "@formbricks/types/errors";
const ProductOnboardingLayout = async (props) => {
const ProjectOnboardingLayout = async (props) => {
const params = await props.params;
const { children } = props;
@@ -50,4 +50,4 @@ const ProductOnboardingLayout = async (props) => {
);
};
export default ProductOnboardingLayout;
export default ProjectOnboardingLayout;
@@ -6,7 +6,7 @@ import { GlobeIcon, GlobeLockIcon, LinkIcon, XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { getUserProducts } from "@formbricks/lib/product/service";
import { getUserProjects } from "@formbricks/lib/project/service";
interface ChannelPageProps {
params: Promise<{
@@ -24,39 +24,39 @@ const Page = async (props: ChannelPageProps) => {
const t = await getTranslations();
const channelOptions = [
{
title: t("organizations.products.new.channel.public_website"),
description: t("organizations.products.new.channel.public_website_description"),
title: t("organizations.projects.new.channel.public_website"),
description: t("organizations.projects.new.channel.public_website_description"),
icon: GlobeIcon,
iconText: t("organizations.products.new.channel.public_website_icon_text"),
href: `/organizations/${params.organizationId}/products/new/settings?channel=website`,
iconText: t("organizations.projects.new.channel.public_website_icon_text"),
href: `/organizations/${params.organizationId}/projects/new/settings?channel=website`,
},
{
title: t("organizations.products.new.channel.app_with_sign_up"),
description: t("organizations.products.new.channel.app_with_sign_up_description"),
title: t("organizations.projects.new.channel.app_with_sign_up"),
description: t("organizations.projects.new.channel.app_with_sign_up_description"),
icon: GlobeLockIcon,
iconText: t("organizations.products.new.channel.app_with_sign_up_icon_text"),
href: `/organizations/${params.organizationId}/products/new/settings?channel=app`,
iconText: t("organizations.projects.new.channel.app_with_sign_up_icon_text"),
href: `/organizations/${params.organizationId}/projects/new/settings?channel=app`,
},
{
channel: "link",
title: t("organizations.products.new.channel.link_and_email_surveys"),
description: t("organizations.products.new.channel.link_and_email_surveys_description"),
title: t("organizations.projects.new.channel.link_and_email_surveys"),
description: t("organizations.projects.new.channel.link_and_email_surveys_description"),
icon: LinkIcon,
iconText: t("organizations.products.new.channel.link_and_email_surveys_icon_text"),
href: `/organizations/${params.organizationId}/products/new/settings?channel=link`,
iconText: t("organizations.projects.new.channel.link_and_email_surveys_icon_text"),
href: `/organizations/${params.organizationId}/projects/new/settings?channel=link`,
},
];
const products = await getUserProducts(session.user.id, params.organizationId);
const projects = await getUserProjects(session.user.id, params.organizationId);
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header
title={t("organizations.products.new.channel.channel_select_title")}
subtitle={t("organizations.products.new.channel.channel_select_subtitle")}
title={t("organizations.projects.new.channel.channel_select_title")}
subtitle={t("organizations.projects.new.channel.channel_select_subtitle")}
/>
<OnboardingOptionsContainer options={channelOptions} />
{products.length >= 1 && (
{projects.length >= 1 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="minimal"
@@ -1,13 +1,18 @@
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { notFound, redirect } from "next/navigation";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getOrganizationProjectsCount } from "@formbricks/lib/project/service";
const OnboardingLayout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {
@@ -18,6 +23,18 @@ const OnboardingLayout = async (props) => {
const { isMember, isBilling } = getAccessFlags(membership?.role);
if (isMember || isBilling) return notFound();
const organization = await getOrganization(params.organizationId);
if (!organization) {
throw new Error(t("common.organization_not_found"));
}
const organizationProjectsLimit = await getOrganizationProjectsLimit(organization);
const organizationProjectsCount = await getOrganizationProjectsCount(organization.id);
if (organizationProjectsCount >= organizationProjectsLimit) {
return redirect(`/`);
}
return <>{children}</>;
};
@@ -6,7 +6,7 @@ import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { getUserProducts } from "@formbricks/lib/product/service";
import { getUserProjects } from "@formbricks/lib/project/service";
interface ModePageProps {
params: Promise<{
@@ -24,26 +24,26 @@ const Page = async (props: ModePageProps) => {
const t = await getTranslations();
const channelOptions = [
{
title: t("organizations.products.new.mode.formbricks_surveys"),
description: t("organizations.products.new.mode.formbricks_surveys_description"),
title: t("organizations.projects.new.mode.formbricks_surveys"),
description: t("organizations.projects.new.mode.formbricks_surveys_description"),
icon: ListTodoIcon,
href: `/organizations/${params.organizationId}/products/new/channel`,
href: `/organizations/${params.organizationId}/projects/new/channel`,
},
{
title: t("organizations.products.new.mode.formbricks_cx"),
description: t("organizations.products.new.mode.formbricks_cx_description"),
title: t("organizations.projects.new.mode.formbricks_cx"),
description: t("organizations.projects.new.mode.formbricks_cx_description"),
icon: HeartIcon,
href: `/organizations/${params.organizationId}/products/new/settings?mode=cx`,
href: `/organizations/${params.organizationId}/projects/new/settings?mode=cx`,
},
];
const products = await getUserProducts(session.user.id, params.organizationId);
const projects = await getUserProjects(session.user.id, params.organizationId);
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header title={t("organizations.products.new.mode.what_are_you_here_for")} />
<Header title={t("organizations.projects.new.mode.what_are_you_here_for")} />
<OnboardingOptionsContainer options={channelOptions} />
{products.length >= 1 && (
{projects.length >= 1 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="minimal"
@@ -1,8 +1,8 @@
"use client";
import { createProductAction } from "@/app/(app)/environments/[environmentId]/actions";
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { TOrganizationTeam } from "@/modules/ee/teams/product-teams/types/teams";
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/teams";
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
import { Button } from "@/modules/ui/components/button";
import { ColorPicker } from "@/modules/ui/components/color-picker";
@@ -28,41 +28,41 @@ import { toast } from "react-hot-toast";
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
import { getPreviewSurvey } from "@formbricks/lib/styling/constants";
import {
TProductConfigChannel,
TProductConfigIndustry,
TProductMode,
TProductUpdateInput,
ZProductUpdateInput,
} from "@formbricks/types/product";
TProjectConfigChannel,
TProjectConfigIndustry,
TProjectMode,
TProjectUpdateInput,
ZProjectUpdateInput,
} from "@formbricks/types/project";
interface ProductSettingsProps {
interface ProjectSettingsProps {
organizationId: string;
productMode: TProductMode;
channel: TProductConfigChannel;
industry: TProductConfigIndustry;
projectMode: TProjectMode;
channel: TProjectConfigChannel;
industry: TProjectConfigIndustry;
defaultBrandColor: string;
organizationTeams: TOrganizationTeam[];
canDoRoleManagement: boolean;
locale: string;
}
export const ProductSettings = ({
export const ProjectSettings = ({
organizationId,
productMode,
projectMode,
channel,
industry,
defaultBrandColor,
organizationTeams,
canDoRoleManagement = false,
locale,
}: ProductSettingsProps) => {
}: ProjectSettingsProps) => {
const [createTeamModalOpen, setCreateTeamModalOpen] = useState(false);
const router = useRouter();
const t = useTranslations();
const addProduct = async (data: TProductUpdateInput) => {
const addProject = async (data: TProjectUpdateInput) => {
try {
const createProductResponse = await createProductAction({
const createProjectResponse = await createProjectAction({
organizationId,
data: {
...data,
@@ -71,14 +71,14 @@ export const ProductSettings = ({
},
});
if (createProductResponse?.data) {
if (createProjectResponse?.data) {
// get production environment
const productionEnvironment = createProductResponse.data.environments.find(
const productionEnvironment = createProjectResponse.data.environments.find(
(environment) => environment.type === "production"
);
if (productionEnvironment) {
if (typeof window !== "undefined") {
// Rmove filters when creating a new product
// Rmove filters when creating a new project
localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
}
}
@@ -86,26 +86,26 @@ export const ProductSettings = ({
router.push(`/environments/${productionEnvironment?.id}/connect`);
} else if (channel === "link") {
router.push(`/environments/${productionEnvironment?.id}/surveys`);
} else if (productMode === "cx") {
} else if (projectMode === "cx") {
router.push(`/environments/${productionEnvironment?.id}/xm-templates`);
}
} else {
const errorMessage = getFormattedErrorMessage(createProductResponse);
const errorMessage = getFormattedErrorMessage(createProjectResponse);
toast.error(errorMessage);
}
} catch (error) {
toast.error("Product creation failed");
toast.error(t("organizations.projects.new.settings.project_creation_failed"));
console.error(error);
}
};
const form = useForm<TProductUpdateInput>({
const form = useForm<TProjectUpdateInput>({
defaultValues: {
name: "",
styling: { allowStyleOverwrite: true, brandColor: { light: defaultBrandColor } },
teamIds: [],
},
resolver: zodResolver(ZProductUpdateInput),
resolver: zodResolver(ZProjectUpdateInput),
});
const logoUrl = form.watch("logo.url");
const brandColor = form.watch("styling.brandColor.light") ?? defaultBrandColor;
@@ -120,16 +120,16 @@ export const ProductSettings = ({
<div className="mt-6 flex w-5/6 space-x-10 lg:w-2/3 2xl:w-1/2">
<div className="flex w-1/2 flex-col space-y-4">
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(addProduct)} className="w-full space-y-4">
<form onSubmit={form.handleSubmit(addProject)} className="w-full space-y-4">
<FormField
control={form.control}
name="styling.brandColor.light"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<div>
<FormLabel>{t("organizations.products.new.settings.brand_color")}</FormLabel>
<FormLabel>{t("organizations.projects.new.settings.brand_color")}</FormLabel>
<FormDescription>
{t("organizations.products.new.settings.brand_color_description")}
{t("organizations.projects.new.settings.brand_color_description")}
</FormDescription>
</div>
<FormControl>
@@ -151,9 +151,9 @@ export const ProductSettings = ({
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<div>
<FormLabel>{t("organizations.products.new.settings.product_name")}</FormLabel>
<FormLabel>{t("organizations.projects.new.settings.project_name")}</FormLabel>
<FormDescription>
{t("organizations.products.new.settings.product_name_description")}
{t("organizations.projects.new.settings.project_name_description")}
</FormDescription>
</div>
<FormControl>
@@ -181,14 +181,14 @@ export const ProductSettings = ({
<div className="flex items-center justify-between">
<div>
<FormLabel>Teams</FormLabel>
<FormDescription>Who all can access this product?</FormDescription>
<FormDescription>Who all can access this project?</FormDescription>
</div>
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setCreateTeamModalOpen(true)}>
{t("organizations.products.new.settings.create_new_team")}
{t("organizations.projects.new.settings.create_new_team")}
</Button>
</div>
<FormControl>
@@ -1,6 +1,6 @@
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils";
import { ProductSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/components/ProductSettings";
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
import { Button } from "@/modules/ui/components/button";
@@ -11,22 +11,22 @@ import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { DEFAULT_BRAND_COLOR, DEFAULT_LOCALE } from "@formbricks/lib/constants";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getUserProducts } from "@formbricks/lib/product/service";
import { getUserProjects } from "@formbricks/lib/project/service";
import { getUserLocale } from "@formbricks/lib/user/service";
import { TProductConfigChannel, TProductConfigIndustry, TProductMode } from "@formbricks/types/product";
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
interface ProductSettingsPageProps {
interface ProjectSettingsPageProps {
params: Promise<{
organizationId: string;
}>;
searchParams: Promise<{
channel?: TProductConfigChannel;
industry?: TProductConfigIndustry;
mode?: TProductMode;
channel?: TProjectConfigChannel;
industry?: TProjectConfigIndustry;
mode?: TProjectMode;
}>;
}
const Page = async (props: ProductSettingsPageProps) => {
const Page = async (props: ProjectSettingsPageProps) => {
const searchParams = await props.searchParams;
const params = await props.params;
const t = await getTranslations();
@@ -41,7 +41,7 @@ const Page = async (props: ProductSettingsPageProps) => {
const mode = searchParams.mode || "surveys";
const locale = session?.user.id ? await getUserLocale(session.user.id) : undefined;
const customHeadline = getCustomHeadline(channel);
const products = await getUserProducts(session.user.id, params.organizationId);
const projects = await getUserProjects(session.user.id, params.organizationId);
const organizationTeams = await getTeamsByOrganizationId(params.organizationId);
@@ -61,18 +61,18 @@ const Page = async (props: ProductSettingsPageProps) => {
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
{channel === "link" || mode === "cx" ? (
<Header
title={t("organizations.products.new.settings.channel_settings_title")}
subtitle={t("organizations.products.new.settings.channel_settings_subtitle")}
title={t("organizations.projects.new.settings.channel_settings_title")}
subtitle={t("organizations.projects.new.settings.channel_settings_subtitle")}
/>
) : (
<Header
title={t(customHeadline)}
subtitle={t("organizations.products.new.settings.channel_settings_description")}
subtitle={t("organizations.projects.new.settings.channel_settings_description")}
/>
)}
<ProductSettings
<ProjectSettings
organizationId={params.organizationId}
productMode={mode}
projectMode={mode}
channel={channel}
industry={industry}
defaultBrandColor={DEFAULT_BRAND_COLOR}
@@ -80,7 +80,7 @@ const Page = async (props: ProductSettingsPageProps) => {
canDoRoleManagement={canDoRoleManagement}
locale={locale ?? DEFAULT_LOCALE}
/>
{products.length >= 1 && (
{projects.length >= 1 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="minimal"
@@ -4,12 +4,12 @@ import { actionClient, authenticatedActionClient } from "@/lib/utils/action-clie
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import {
getOrganizationIdFromEnvironmentId,
getOrganizationIdFromProductId,
getOrganizationIdFromProjectId,
getOrganizationIdFromSegmentId,
getOrganizationIdFromSurveyId,
getProductIdFromEnvironmentId,
getProductIdFromSegmentId,
getProductIdFromSurveyId,
getProjectIdFromEnvironmentId,
getProjectIdFromSegmentId,
getProjectIdFromSurveyId,
} from "@/lib/utils/helper";
import { getSegment, getSurvey } from "@/lib/utils/services";
import { getSurveyFollowUpsPermission } from "@/modules/ee/license-check/lib/utils";
@@ -18,7 +18,7 @@ import { z } from "zod";
import { createActionClass } from "@formbricks/lib/actionClass/service";
import { UNSPLASH_ACCESS_KEY, UNSPLASH_ALLOWED_DOMAINS } from "@formbricks/lib/constants";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getProduct } from "@formbricks/lib/product/service";
import { getProject } from "@formbricks/lib/project/service";
import {
cloneSegment,
createSegment,
@@ -66,8 +66,8 @@ export const updateSurveyAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
productId: await getProductIdFromSurveyId(parsedInput.id),
type: "projectTeam",
projectId: await getProjectIdFromSurveyId(parsedInput.id),
minPermission: "readWrite",
},
],
@@ -84,30 +84,30 @@ export const updateSurveyAction = authenticatedActionClient
return await updateSurvey(parsedInput);
});
const ZRefetchProductAction = z.object({
productId: ZId,
const ZRefetchProjectAction = z.object({
projectId: ZId,
});
export const refetchProductAction = authenticatedActionClient
.schema(ZRefetchProductAction)
export const refetchProjectAction = authenticatedActionClient
.schema(ZRefetchProjectAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromProductId(parsedInput.productId),
organizationId: await getOrganizationIdFromProjectId(parsedInput.projectId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "readWrite",
productId: parsedInput.productId,
projectId: parsedInput.projectId,
},
],
});
return await getProduct(parsedInput.productId);
return await getProject(parsedInput.projectId);
});
const ZCreateBasicSegmentAction = z.object({
@@ -141,9 +141,9 @@ export const createBasicSegmentAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "readWrite",
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
},
],
});
@@ -188,9 +188,9 @@ export const updateBasicSegmentAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "readWrite",
productId: await getProductIdFromSegmentId(parsedInput.segmentId),
projectId: await getProjectIdFromSegmentId(parsedInput.segmentId),
},
],
});
@@ -242,9 +242,9 @@ export const loadNewBasicSegmentAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "readWrite",
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
},
],
});
@@ -285,9 +285,9 @@ export const cloneBasicSegmentAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "readWrite",
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
},
],
});
@@ -311,9 +311,9 @@ export const resetBasicSegmentFiltersAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "readWrite",
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
},
],
});
@@ -416,9 +416,9 @@ export const createActionClassAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "readWrite",
productId: await getProductIdFromEnvironmentId(parsedInput.action.environmentId),
projectId: await getProjectIdFromEnvironmentId(parsedInput.action.environmentId),
},
],
});
@@ -13,16 +13,16 @@ import {
getQuestionTypes,
universalQuestionPresets,
} from "@formbricks/lib/utils/questions";
import { TProduct } from "@formbricks/types/product";
import { TProject } from "@formbricks/types/project";
interface AddQuestionButtonProps {
addQuestion: (question: any) => void;
product: TProduct;
project: TProject;
isCxMode: boolean;
locale: string;
}
export const AddQuestionButton = ({ addQuestion, product, isCxMode, locale }: AddQuestionButtonProps) => {
export const AddQuestionButton = ({ addQuestion, project, isCxMode, locale }: AddQuestionButtonProps) => {
const t = useTranslations();
const [open, setOpen] = useState(false);
const [hoveredQuestionId, setHoveredQuestionId] = useState<string | null>(null);
@@ -60,7 +60,7 @@ export const AddQuestionButton = ({ addQuestion, product, isCxMode, locale }: Ad
onClick={() => {
addQuestion({
...universalQuestionPresets,
...getQuestionDefaults(questionType.id, product, locale),
...getQuestionDefaults(questionType.id, project, locale),
id: createId(),
type: questionType.id,
});
@@ -9,7 +9,7 @@ import { CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
import { TProductStyling } from "@formbricks/types/product";
import { TProjectStyling } from "@formbricks/types/project";
import { TSurveyStyling } from "@formbricks/types/surveys/types";
import { SurveyBgSelectorTab } from "./SurveyBgSelectorTab";
@@ -21,7 +21,7 @@ interface BackgroundStylingCardProps {
disabled?: boolean;
environmentId: string;
isUnsplashConfigured: boolean;
form: UseFormReturn<TProductStyling | TSurveyStyling>;
form: UseFormReturn<TProjectStyling | TSurveyStyling>;
}
export const BackgroundStylingCard = ({
@@ -14,7 +14,7 @@ import React from "react";
import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { TProduct, TProductStyling } from "@formbricks/types/product";
import { TProject, TProjectStyling } from "@formbricks/types/project";
import { TSurveyStyling, TSurveyType } from "@formbricks/types/surveys/types";
type CardStylingSettingsProps = {
@@ -23,8 +23,8 @@ type CardStylingSettingsProps = {
isSettingsPage?: boolean;
surveyType?: TSurveyType;
disabled?: boolean;
product: TProduct;
form: UseFormReturn<TProductStyling | TSurveyStyling>;
project: TProject;
form: UseFormReturn<TProjectStyling | TSurveyStyling>;
};
export const CardStylingSettings = ({
@@ -32,14 +32,14 @@ export const CardStylingSettings = ({
surveyType,
disabled,
open,
product,
project,
setOpen,
form,
}: CardStylingSettingsProps) => {
const t = useTranslations();
const isAppSurvey = surveyType === "app";
const surveyTypeDerived = isAppSurvey ? "App" : "Link";
const isLogoVisible = !!product.logo?.url;
const isLogoVisible = !!project.logo?.url;
const linkCardArrangement = form.watch("cardArrangement.linkSurveys") ?? "straight";
const appCardArrangement = form.watch("cardArrangement.appSurveys") ?? "straight";
@@ -21,7 +21,7 @@ import {
getQuestionDefaults,
getQuestionNameMap,
} from "@formbricks/lib/utils/questions";
import { TProduct } from "@formbricks/types/product";
import { TProject } from "@formbricks/types/project";
import {
TSurvey,
TSurveyEndScreenCard,
@@ -41,7 +41,7 @@ interface EditorCardMenuProps {
updateCard: (cardIdx: number, updatedAttributes: any) => void;
addCard: (question: any, index?: number) => void;
cardType: "question" | "ending";
product?: TProduct;
project?: TProject;
isCxMode?: boolean;
locale: string;
}
@@ -53,7 +53,7 @@ export const EditorCardMenu = ({
duplicateCard,
deleteCard,
moveCard,
product,
project,
card,
updateCard,
addCard,
@@ -83,7 +83,7 @@ export const EditorCardMenu = ({
const { headline, required, subheader, imageUrl, videoUrl, buttonLabel, backButtonLabel } =
card as TSurveyQuestion;
const questionDefaults = getQuestionDefaults(type, product, locale);
const questionDefaults = getQuestionDefaults(type, project, locale);
if (
(type === TSurveyQuestionTypeEnum.MultipleChoiceSingle &&
@@ -115,7 +115,7 @@ export const EditorCardMenu = ({
};
const addQuestionCardBelow = (type: TSurveyQuestionTypeEnum) => {
const questionDefaults = getQuestionDefaults(type, product, locale);
const questionDefaults = getQuestionDefaults(type, project, locale);
addCard(
{
@@ -14,13 +14,13 @@ import { toast } from "react-hot-toast";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/common";
import { TProduct } from "@formbricks/types/product";
import { TProject } from "@formbricks/types/project";
import { TSurvey, TSurveyFileUploadQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
interface FileUploadFormProps {
localSurvey: TSurvey;
product?: TProduct;
project?: TProject;
question: TSurveyFileUploadQuestion;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyFileUploadQuestion>) => void;
@@ -39,7 +39,7 @@ export const FileUploadQuestionForm = ({
questionIdx,
updateQuestion,
isInvalid,
product,
project,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
@@ -53,7 +53,7 @@ export const FileUploadQuestionForm = ({
billingInfo,
error: billingInfoError,
isLoading: billingInfoLoading,
} = useGetBillingInfo(product?.organizationId ?? "");
} = useGetBillingInfo(project?.organizationId ?? "");
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const handleInputChange = (event) => {
@@ -12,7 +12,7 @@ import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { mixColor } from "@formbricks/lib/utils/colors";
import { TProductStyling } from "@formbricks/types/product";
import { TProjectStyling } from "@formbricks/types/project";
import { TSurveyStyling } from "@formbricks/types/surveys/types";
type FormStylingSettingsProps = {
@@ -20,7 +20,7 @@ type FormStylingSettingsProps = {
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
isSettingsPage?: boolean;
disabled?: boolean;
form: UseFormReturn<TProductStyling | TSurveyStyling>;
form: UseFormReturn<TProjectStyling | TSurveyStyling>;
};
export const FormStylingSettings = ({
@@ -175,7 +175,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment, locale
</p>
<p className="text-xs font-normal">
<Link
href={`/environments/${environment.id}/product/${option.id}-connection`}
href={`/environments/${environment.id}/project/${option.id}-connection`}
className="underline hover:text-amber-900"
target="_blank">
{t("common.connect_formbricks")}
@@ -17,7 +17,7 @@ import { cn } from "@formbricks/lib/cn";
import { QUESTIONS_ICON_MAP, getTSurveyQuestionTypeEnumName } from "@formbricks/lib/utils/questions";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TProduct } from "@formbricks/types/product";
import { TProject } from "@formbricks/types/project";
import {
TI18nString,
TSurvey,
@@ -43,7 +43,7 @@ import { RatingQuestionForm } from "./RatingQuestionForm";
interface QuestionCardProps {
localSurvey: TSurvey;
product: TProduct;
project: TProject;
question: TSurveyQuestion;
questionIdx: number;
moveQuestion: (questionIndex: number, up: boolean) => void;
@@ -65,7 +65,7 @@ interface QuestionCardProps {
export const QuestionCard = ({
localSurvey,
product,
project,
question,
questionIdx,
moveQuestion,
@@ -228,7 +228,7 @@ export const QuestionCard = ({
deleteCard={deleteQuestion}
moveCard={moveQuestion}
card={question}
product={product}
project={project}
updateCard={updateQuestion}
addCard={addQuestion}
cardType="question"
@@ -358,7 +358,7 @@ export const QuestionCard = ({
) : question.type === TSurveyQuestionTypeEnum.FileUpload ? (
<FileUploadQuestionForm
localSurvey={localSurvey}
product={product}
project={project}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
@@ -1,14 +1,14 @@
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TProduct } from "@formbricks/types/product";
import { TProject } from "@formbricks/types/project";
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { QuestionCard } from "./QuestionCard";
interface QuestionsDraggableProps {
localSurvey: TSurvey;
product: TProduct;
project: TProject;
moveQuestion: (questionIndex: number, up: boolean) => void;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
deleteQuestion: (questionIdx: number) => void;
@@ -33,7 +33,7 @@ export const QuestionsDroppable = ({
invalidQuestions,
localSurvey,
moveQuestion,
product,
project,
selectedLanguageCode,
setActiveQuestionId,
setSelectedLanguageCode,
@@ -54,7 +54,7 @@ export const QuestionsDroppable = ({
<QuestionCard
key={internalQuestionIdMap[question.id]}
localSurvey={localSurvey}
product={product}
project={project}
question={question}
questionIdx={questionIdx}
moveQuestion={moveQuestion}
@@ -25,7 +25,7 @@ import { getDefaultEndingCard } from "@formbricks/lib/templates";
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
import { TProduct } from "@formbricks/types/product";
import { TProject } from "@formbricks/types/project";
import {
TConditionGroup,
TSingleCondition,
@@ -53,7 +53,7 @@ interface QuestionsViewProps {
setLocalSurvey: React.Dispatch<SetStateAction<TSurvey>>;
activeQuestionId: TSurveyQuestionId | null;
setActiveQuestionId: (questionId: TSurveyQuestionId | null) => void;
product: TProduct;
project: TProject;
invalidQuestions: string[] | null;
setInvalidQuestions: React.Dispatch<SetStateAction<string[] | null>>;
selectedLanguageCode: string;
@@ -71,7 +71,7 @@ export const QuestionsView = ({
setActiveQuestionId,
localSurvey,
setLocalSurvey,
product,
project,
invalidQuestions,
setInvalidQuestions,
setSelectedLanguageCode,
@@ -447,7 +447,7 @@ export const QuestionsView = ({
collisionDetection={closestCorners}>
<QuestionsDroppable
localSurvey={localSurvey}
product={product}
project={project}
moveQuestion={moveQuestion}
updateQuestion={updateQuestion}
duplicateQuestion={duplicateQuestion}
@@ -466,7 +466,7 @@ export const QuestionsView = ({
/>
</DndContext>
<AddQuestionButton addQuestion={addQuestion} product={product} isCxMode={isCxMode} locale={locale} />
<AddQuestionButton addQuestion={addQuestion} project={project} isCxMode={isCxMode} locale={locale} />
<div className="mt-5 flex flex-col gap-5" ref={parent}>
<hr className="border-t border-dashed" />
<DndContext
@@ -523,7 +523,7 @@ export const QuestionsView = ({
<MultiLanguageCard
localSurvey={localSurvey}
product={product}
project={project}
setLocalSurvey={setLocalSurvey}
setActiveQuestionId={setActiveQuestionId}
activeQuestionId={activeQuestionId}
@@ -195,7 +195,7 @@ export const RecontactOptionsCard = ({
{t("environments.surveys.edit.this_setting_overwrites_your")}{" "}
<Link
className="decoration-brand-dark underline"
href={`/environments/${environmentId}/product/general`}
href={`/environments/${environmentId}/project/general`}
target="_blank">
{t("environments.surveys.edit.waiting_period")}
</Link>
@@ -1,5 +1,5 @@
import { AdvancedTargetingCard } from "@/modules/ee/advanced-targeting/components/advanced-targeting-card";
import { TTeamPermission } from "@/modules/ee/teams/product-teams/types/teams";
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams";
import { TActionClass } from "@formbricks/types/action-classes";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TEnvironment } from "@formbricks/types/environment";
@@ -25,7 +25,7 @@ interface SettingsViewProps {
isUserTargetingAllowed?: boolean;
isFormbricksCloud: boolean;
locale: string;
productPermission: TTeamPermission | null;
projectPermission: TTeamPermission | null;
}
export const SettingsView = ({
@@ -40,7 +40,7 @@ export const SettingsView = ({
isUserTargetingAllowed = false,
isFormbricksCloud,
locale,
productPermission,
projectPermission,
}: SettingsViewProps) => {
const isAppSurvey = localSurvey.type === "app";
@@ -86,7 +86,7 @@ export const SettingsView = ({
environmentId={environment.id}
propActionClasses={actionClasses}
membershipRole={membershipRole}
productPermission={productPermission}
projectPermission={projectPermission}
/>
<ResponseOptionsCard
@@ -16,7 +16,7 @@ import React, { useEffect, useMemo, useState } from "react";
import { UseFormReturn, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { TEnvironment } from "@formbricks/types/environment";
import { TProduct, TProductStyling } from "@formbricks/types/product";
import { TProject, TProjectStyling } from "@formbricks/types/project";
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys/types";
import { BackgroundStylingCard } from "./BackgroundStylingCard";
import { CardStylingSettings } from "./CardStylingSettings";
@@ -24,7 +24,7 @@ import { FormStylingSettings } from "./FormStylingSettings";
interface StylingViewProps {
environment: TEnvironment;
product: TProduct;
project: TProject;
localSurvey: TSurvey;
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurvey>>;
colors: string[];
@@ -39,7 +39,7 @@ interface StylingViewProps {
export const StylingView = ({
colors,
environment,
product,
project,
localSurvey,
setLocalSurvey,
setStyling,
@@ -52,7 +52,7 @@ export const StylingView = ({
const t = useTranslations();
const form = useForm<TSurveyStyling>({
defaultValues: localSurvey.styling ?? product.styling,
defaultValues: localSurvey.styling ?? project.styling,
});
const overwriteThemeStyling = form.watch("overwriteThemeStyling");
@@ -64,8 +64,8 @@ export const StylingView = ({
const [confirmResetStylingModalOpen, setConfirmResetStylingModalOpen] = useState(false);
const onResetThemeStyling = () => {
const { styling: productStyling } = product;
const { allowStyleOverwrite, ...baseStyling } = productStyling ?? {};
const { styling: projectStyling } = project;
const { allowStyleOverwrite, ...baseStyling } = projectStyling ?? {};
setStyling({
...baseStyling,
@@ -101,12 +101,12 @@ export const StylingView = ({
});
}, [setLocalSurvey]);
const defaultProductStyling = useMemo(() => {
const { styling: productStyling } = product;
const { allowStyleOverwrite, ...baseStyling } = productStyling ?? {};
const defaultProjectStyling = useMemo(() => {
const { styling: projectStyling } = project;
const { allowStyleOverwrite, ...baseStyling } = projectStyling ?? {};
return baseStyling;
}, [product]);
}, [project]);
const handleOverwriteToggle = (value: boolean) => {
// survey styling from the server is surveyStyling, it could either be set or not
@@ -114,12 +114,12 @@ export const StylingView = ({
setOverwriteThemeStyling(value);
// if the toggle is turned on, we set the local styling to the product styling
// if the toggle is turned on, we set the local styling to the project styling
if (value) {
if (!styling) {
// copy the product styling to the survey styling
// copy the project styling to the survey styling
setStyling({
...defaultProductStyling,
...defaultProjectStyling,
overwriteThemeStyling: true,
});
return;
@@ -129,23 +129,23 @@ export const StylingView = ({
if (localStylingChanges) {
setStyling(localStylingChanges);
}
// if there are no local styling changes, we set the styling to the product styling
// if there are no local styling changes, we set the styling to the project styling
else {
setStyling({
...defaultProductStyling,
...defaultProjectStyling,
overwriteThemeStyling: true,
});
}
}
// if the toggle is turned off, we store the local styling changes and set the styling to the product styling
// if the toggle is turned off, we store the local styling changes and set the styling to the project styling
else {
// copy the styling to localStylingChanges
setLocalStylingChanges(styling);
// copy the product styling to the survey styling
// copy the project styling to the survey styling
setStyling({
...defaultProductStyling,
...defaultProjectStyling,
overwriteThemeStyling: false,
});
}
@@ -184,7 +184,7 @@ export const StylingView = ({
open={formStylingOpen}
setOpen={setFormStylingOpen}
disabled={!overwriteThemeStyling}
form={form as UseFormReturn<TProductStyling | TSurveyStyling>}
form={form as UseFormReturn<TProjectStyling | TSurveyStyling>}
/>
<CardStylingSettings
@@ -192,8 +192,8 @@ export const StylingView = ({
setOpen={setCardStylingOpen}
surveyType={localSurvey.type}
disabled={!overwriteThemeStyling}
product={product}
form={form as UseFormReturn<TProductStyling | TSurveyStyling>}
project={project}
form={form as UseFormReturn<TProjectStyling | TSurveyStyling>}
/>
{localSurvey.type === "link" && (
@@ -204,7 +204,7 @@ export const StylingView = ({
colors={colors}
disabled={!overwriteThemeStyling}
isUnsplashConfigured={isUnsplashConfigured}
form={form as UseFormReturn<TProductStyling | TSurveyStyling>}
form={form as UseFormReturn<TProjectStyling | TSurveyStyling>}
/>
)}
@@ -225,7 +225,7 @@ export const StylingView = ({
<p className="text-sm text-slate-500">
{t("environments.surveys.edit.adjust_the_theme_in_the")}{" "}
<Link
href={`/environments/${environment.id}/product/look`}
href={`/environments/${environment.id}/project/look`}
target="_blank"
className="font-semibold underline">
{t("common.look_and_feel")}
@@ -1,7 +1,7 @@
"use client";
import { FollowUpsView } from "@/modules/ee/survey-follow-ups/components/follow-ups-view";
import { TTeamPermission } from "@/modules/ee/teams/product-teams/types/teams";
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams";
import { PreviewSurvey } from "@/modules/ui/components/preview-survey";
import { useCallback, useEffect, useRef, useState } from "react";
import { extractLanguageCodes, getEnabledLanguages } from "@formbricks/lib/i18n/utils";
@@ -12,11 +12,11 @@ import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TEnvironment } from "@formbricks/types/environment";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
import { TProduct } from "@formbricks/types/product";
import { TProject } from "@formbricks/types/project";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyEditorTabs, TSurveyStyling } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { refetchProductAction } from "../actions";
import { refetchProjectAction } from "../actions";
import { LoadingSkeleton } from "./LoadingSkeleton";
import { QuestionsAudienceTabs } from "./QuestionsStylingSettingsTabs";
import { QuestionsView } from "./QuestionsView";
@@ -26,7 +26,7 @@ import { SurveyMenuBar } from "./SurveyMenuBar";
interface SurveyEditorProps {
survey: TSurvey;
product: TProduct;
project: TProject;
environment: TEnvironment;
actionClasses: TActionClass[];
attributeClasses: TAttributeClass[];
@@ -41,15 +41,15 @@ interface SurveyEditorProps {
plan: TOrganizationBillingPlan;
isCxMode: boolean;
locale: TUserLocale;
projectPermission: TTeamPermission | null;
mailFrom: string;
isSurveyFollowUpsAllowed: boolean;
productPermission: TTeamPermission | null;
userEmail: string;
}
export const SurveyEditor = ({
survey,
product,
project,
environment,
actionClasses,
attributeClasses,
@@ -64,9 +64,9 @@ export const SurveyEditor = ({
plan,
isCxMode = false,
locale,
projectPermission,
mailFrom,
isSurveyFollowUpsAllowed = false,
productPermission,
userEmail,
}: SurveyEditorProps) => {
const [activeView, setActiveView] = useState<TSurveyEditorTabs>("questions");
@@ -75,19 +75,19 @@ export const SurveyEditor = ({
const [invalidQuestions, setInvalidQuestions] = useState<string[] | null>(null);
const [selectedLanguageCode, setSelectedLanguageCode] = useState<string>("default");
const surveyEditorRef = useRef(null);
const [localProduct, setLocalProduct] = useState<TProduct>(product);
const [localProject, setLocalProject] = useState<TProject>(project);
const [styling, setStyling] = useState(localSurvey?.styling);
const [localStylingChanges, setLocalStylingChanges] = useState<TSurveyStyling | null>(null);
const fetchLatestProduct = useCallback(async () => {
const refetchProductResponse = await refetchProductAction({ productId: localProduct.id });
if (refetchProductResponse?.data) {
setLocalProduct(refetchProductResponse.data);
const fetchLatestProject = useCallback(async () => {
const refetchProjectResponse = await refetchProjectAction({ projectId: localProject.id });
if (refetchProjectResponse?.data) {
setLocalProject(refetchProjectResponse.data);
}
}, [localProduct.id]);
}, [localProject.id]);
useDocumentVisibility(fetchLatestProduct);
useDocumentVisibility(fetchLatestProject);
useEffect(() => {
if (survey) {
@@ -107,20 +107,20 @@ export const SurveyEditor = ({
useEffect(() => {
const listener = () => {
if (document.visibilityState === "visible") {
const fetchLatestProduct = async () => {
const refetchProductResponse = await refetchProductAction({ productId: localProduct.id });
if (refetchProductResponse?.data) {
setLocalProduct(refetchProductResponse.data);
const fetchLatestProject = async () => {
const refetchProjectResponse = await refetchProjectAction({ projectId: localProject.id });
if (refetchProjectResponse?.data) {
setLocalProject(refetchProjectResponse.data);
}
};
fetchLatestProduct();
fetchLatestProject();
}
};
document.addEventListener("visibilitychange", listener);
return () => {
document.removeEventListener("visibilitychange", listener);
};
}, [localProduct.id]);
}, [localProject.id]);
// when the survey type changes, we need to reset the active question id to the first question
useEffect(() => {
@@ -152,7 +152,7 @@ export const SurveyEditor = ({
activeId={activeView}
setActiveId={setActiveView}
setInvalidQuestions={setInvalidQuestions}
product={localProduct}
project={localProject}
responseCount={responseCount}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
@@ -167,7 +167,7 @@ export const SurveyEditor = ({
activeId={activeView}
setActiveId={setActiveView}
isCxMode={isCxMode}
isStylingTabVisible={!!product.styling.allowStyleOverwrite}
isStylingTabVisible={!!project.styling.allowStyleOverwrite}
isSurveyFollowUpsAllowed={isSurveyFollowUpsAllowed}
/>
@@ -177,7 +177,7 @@ export const SurveyEditor = ({
setLocalSurvey={setLocalSurvey}
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
product={localProduct}
project={localProject}
invalidQuestions={invalidQuestions}
setInvalidQuestions={setInvalidQuestions}
selectedLanguageCode={selectedLanguageCode ? selectedLanguageCode : "default"}
@@ -191,13 +191,13 @@ export const SurveyEditor = ({
/>
)}
{activeView === "styling" && product.styling.allowStyleOverwrite && (
{activeView === "styling" && project.styling.allowStyleOverwrite && (
<StylingView
colors={colors}
environment={environment}
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
product={localProduct}
project={localProject}
styling={styling ?? null}
setStyling={setStyling}
localStylingChanges={localStylingChanges}
@@ -220,7 +220,7 @@ export const SurveyEditor = ({
isUserTargetingAllowed={isUserTargetingAllowed}
isFormbricksCloud={isFormbricksCloud}
locale={locale}
productPermission={productPermission}
projectPermission={projectPermission}
/>
)}
@@ -241,7 +241,7 @@ export const SurveyEditor = ({
<PreviewSurvey
survey={localSurvey}
questionId={activeQuestionId}
product={localProduct}
project={localProject}
environment={environment}
previewType={localSurvey.type === "app" ? "modal" : "fullwidth"}
languageCode={selectedLanguageCode}
@@ -15,7 +15,7 @@ import { useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { getLanguageLabel } from "@formbricks/lib/i18n/utils";
import { TEnvironment } from "@formbricks/types/environment";
import { TProduct } from "@formbricks/types/product";
import { TProject } from "@formbricks/types/project";
import { TSegment } from "@formbricks/types/segment";
import {
TSurvey,
@@ -36,7 +36,7 @@ interface SurveyMenuBarProps {
activeId: TSurveyEditorTabs;
setActiveId: React.Dispatch<React.SetStateAction<TSurveyEditorTabs>>;
setInvalidQuestions: React.Dispatch<React.SetStateAction<string[]>>;
product: TProduct;
project: TProject;
responseCount: number;
selectedLanguageCode: string;
setSelectedLanguageCode: (selectedLanguage: string) => void;
@@ -52,7 +52,7 @@ export const SurveyMenuBar = ({
activeId,
setActiveId,
setInvalidQuestions,
product,
project,
responseCount,
selectedLanguageCode,
isCxMode,
@@ -326,7 +326,7 @@ export const SurveyMenuBar = ({
{t("common.back")}
</Button>
)}
<p className="hidden pl-4 font-semibold md:block">{product.name} / </p>
<p className="hidden pl-4 font-semibold md:block">{project.name} / </p>
<Input
defaultValue={localSurvey.name}
onChange={(e) => {
@@ -9,7 +9,7 @@ import { useTranslations } from "next-intl";
import Link from "next/link";
import { useState } from "react";
import { TPlacement } from "@formbricks/types/common";
import { TSurvey, TSurveyProductOverwrites } from "@formbricks/types/surveys/types";
import { TSurvey, TSurveyProjectOverwrites } from "@formbricks/types/surveys/types";
import { Placement } from "./Placement";
interface SurveyPlacementCardProps {
@@ -26,17 +26,17 @@ export const SurveyPlacementCard = ({
const t = useTranslations();
const [open, setOpen] = useState(false);
const { productOverwrites } = localSurvey ?? {};
const { placement, clickOutsideClose, darkOverlay } = productOverwrites ?? {};
const { projectOverwrites } = localSurvey ?? {};
const { placement, clickOutsideClose, darkOverlay } = projectOverwrites ?? {};
const setProductOverwrites = (productOverwrites: TSurveyProductOverwrites) => {
setLocalSurvey({ ...localSurvey, productOverwrites });
const setProjectOverwrites = (projectOverwrites: TSurveyProjectOverwrites) => {
setLocalSurvey({ ...localSurvey, projectOverwrites: projectOverwrites });
};
const togglePlacement = () => {
if (setProductOverwrites) {
setProductOverwrites({
...productOverwrites,
if (setProjectOverwrites) {
setProjectOverwrites({
...projectOverwrites,
placement: !!placement ? null : "bottomRight",
clickOutsideClose: false,
darkOverlay: false,
@@ -45,9 +45,9 @@ export const SurveyPlacementCard = ({
};
const handlePlacementChange = (placement: TPlacement) => {
if (setProductOverwrites) {
setProductOverwrites({
...productOverwrites,
if (setProjectOverwrites) {
setProjectOverwrites({
...projectOverwrites,
placement,
});
}
@@ -56,18 +56,18 @@ export const SurveyPlacementCard = ({
const handleOverlay = (overlayType: string) => {
const darkOverlay = overlayType === "dark";
if (setProductOverwrites) {
setProductOverwrites({
...productOverwrites,
if (setProjectOverwrites) {
setProjectOverwrites({
...projectOverwrites,
darkOverlay,
});
}
};
const handleClickOutsideClose = (clickOutsideClose: boolean) => {
if (setProductOverwrites) {
setProductOverwrites({
...productOverwrites,
if (setProjectOverwrites) {
setProjectOverwrites({
...projectOverwrites,
clickOutsideClose,
});
}
@@ -141,7 +141,7 @@ export const SurveyPlacementCard = ({
<div>
<p className="text-xs text-slate-500">
{t("environments.surveys.edit.to_keep_the_placement_over_all_surveys_consistent_you_can")}{" "}
<Link href={`/environments/${environmentId}/product/look`} target="_blank">
<Link href={`/environments/${environmentId}/project/look`} target="_blank">
<span className="underline">
{t("environments.surveys.edit.set_the_global_placement_in_the_look_feel_settings")}
</span>
@@ -1,6 +1,6 @@
"use client";
import { TTeamPermission } from "@/modules/ee/teams/product-teams/types/teams";
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
@@ -29,7 +29,7 @@ interface WhenToSendCardProps {
environmentId: string;
propActionClasses: TActionClass[];
membershipRole?: TOrganizationRole;
productPermission: TTeamPermission | null;
projectPermission: TTeamPermission | null;
}
export const WhenToSendCard = ({
@@ -38,7 +38,7 @@ export const WhenToSendCard = ({
setLocalSurvey,
propActionClasses,
membershipRole,
productPermission,
projectPermission,
}: WhenToSendCardProps) => {
const t = useTranslations();
const [open, setOpen] = useState(localSurvey.type === "app" ? true : false);
@@ -47,7 +47,7 @@ export const WhenToSendCard = ({
const [randomizerToggle, setRandomizerToggle] = useState(localSurvey.displayPercentage ? true : false);
const { isMember } = getAccessFlags(membershipRole);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
@@ -5,7 +5,7 @@ import {
getMultiLanguagePermission,
getSurveyFollowUpsPermission,
} from "@/modules/ee/license-check/lib/utils";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { ErrorComponent } from "@/modules/ui/components/error-component";
import { getServerSession } from "next-auth";
@@ -23,7 +23,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 { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
import { getSegments } from "@formbricks/lib/segment/service";
import { getSurvey } from "@formbricks/lib/survey/service";
@@ -44,7 +44,7 @@ const Page = async (props) => {
const t = await getTranslations();
const [
survey,
product,
project,
environment,
actionClasses,
attributeClasses,
@@ -54,7 +54,7 @@ const Page = async (props) => {
segments,
] = await Promise.all([
getSurvey(params.surveyId),
getProductByEnvironmentId(params.environmentId),
getProjectByEnvironmentId(params.environmentId),
getEnvironment(params.environmentId),
getActionClasses(params.environmentId),
getAttributeClasses(params.environmentId, undefined, { skipArchived: true }),
@@ -72,16 +72,16 @@ const Page = async (props) => {
throw new Error(t("common.organization_not_found"));
}
if (!product) {
throw new Error(t("common.product_not_found"));
if (!project) {
throw new Error(t("common.project_not_found"));
}
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const productPermission = await getProductPermissionByUserId(session.user.id, product.id);
const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isSurveyCreationDeletionDisabled = isMember && hasReadAccess;
const locale = session.user.id ? await getUserLocale(session.user.id) : undefined;
@@ -97,7 +97,7 @@ const Page = async (props) => {
!environment ||
!actionClasses ||
!attributeClasses ||
!product ||
!project ||
!userEmail ||
isSurveyCreationDeletionDisabled
) {
@@ -109,13 +109,13 @@ const Page = async (props) => {
return (
<SurveyEditor
survey={survey}
product={product}
project={project}
environment={environment}
actionClasses={actionClasses}
attributeClasses={attributeClasses}
responseCount={responseCount}
membershipRole={currentUserMembership?.role}
productPermission={productPermission}
projectPermission={projectPermission}
colors={SURVEY_BG_COLORS}
segments={segments}
isUserTargetingAllowed={isUserTargetingAllowed}
@@ -29,7 +29,7 @@ export const getMinimalSurvey = (locale: string): TSurvey => ({
surveyClosedMessage: {
enabled: false,
},
productOverwrites: null,
projectOverwrites: null,
singleUse: null,
styling: null,
resultShareKey: null,
@@ -2,7 +2,7 @@
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromEnvironmentId, getProductIdFromEnvironmentId } from "@/lib/utils/helper";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
import { createId } from "@paralleldrive/cuid2";
import { generateObject } from "ai";
@@ -32,8 +32,8 @@ export const createAISurveyAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
productId: await getProductIdFromEnvironmentId(parsedInput.environmentId),
type: "projectTeam",
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
minPermission: "readWrite",
},
],
@@ -10,22 +10,22 @@ import { useTranslations } from "next-intl";
import { useState } from "react";
import { getCustomSurveyTemplate } from "@formbricks/lib/templates";
import type { TEnvironment } from "@formbricks/types/environment";
import type { TProduct, TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
import type { TProject, TProjectConfigChannel, TProjectConfigIndustry } from "@formbricks/types/project";
import type { TTemplate, TTemplateRole } from "@formbricks/types/templates";
import { TUser } from "@formbricks/types/user";
import { getMinimalSurvey } from "../../lib/minimalSurvey";
type TemplateContainerWithPreviewProps = {
environmentId: string;
product: TProduct;
project: TProject;
environment: TEnvironment;
user: TUser;
prefilledFilters: (TProductConfigChannel | TProductConfigIndustry | TTemplateRole | null)[];
prefilledFilters: (TProjectConfigChannel | TProjectConfigIndustry | TTemplateRole | null)[];
isAIEnabled: boolean;
};
export const TemplateContainerWithPreview = ({
product,
project,
environment,
user,
prefilledFilters,
@@ -67,7 +67,7 @@ export const TemplateContainerWithPreview = ({
<TemplateList
environment={environment}
product={product}
project={project}
user={user}
templateSearch={templateSearch ?? ""}
onTemplateClick={(template) => {
@@ -82,7 +82,7 @@ export const TemplateContainerWithPreview = ({
<PreviewSurvey
survey={{ ...getMinimalSurvey(user.locale), ...activeTemplate.preset }}
questionId={activeQuestionId}
product={product}
project={project}
environment={environment}
languageCode={"default"}
onFileUpload={async (file) => file.name}
@@ -1,5 +1,5 @@
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
@@ -7,9 +7,9 @@ import { redirect } from "next/navigation";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getUser } from "@formbricks/lib/user/service";
import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
import { TProjectConfigChannel, TProjectConfigIndustry } from "@formbricks/types/project";
import { TTemplateRole } from "@formbricks/types/templates";
import { TemplateContainerWithPreview } from "./components/TemplateContainer";
@@ -18,8 +18,8 @@ interface SurveyTemplateProps {
environmentId: string;
}>;
searchParams: Promise<{
channel?: TProductConfigChannel;
industry?: TProductConfigIndustry;
channel?: TProjectConfigChannel;
industry?: TProjectConfigIndustry;
role?: TTemplateRole;
}>;
}
@@ -35,18 +35,18 @@ const Page = async (props: SurveyTemplateProps) => {
throw new Error(t("common.session_not_found"));
}
const [user, environment, product] = await Promise.all([
const [user, environment, project] = await Promise.all([
getUser(session.user.id),
getEnvironment(environmentId),
getProductByEnvironmentId(environmentId),
getProjectByEnvironmentId(environmentId),
]);
if (!user) {
throw new Error(t("common.user_not_found"));
}
if (!product) {
throw new Error(t("common.product_not_found"));
if (!project) {
throw new Error(t("common.project_not_found"));
}
if (!environment) {
@@ -54,26 +54,26 @@ const Page = async (props: SurveyTemplateProps) => {
}
const currentUserMembership = await getMembershipByUserIdOrganizationId(
session?.user.id,
product.organizationId
project.organizationId
);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const productPermission = await getProductPermissionByUserId(session.user.id, product.id);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
if (isReadOnly) {
return redirect(`/environments/${environment.id}/surveys`);
}
const prefilledFilters = [product.config.channel, product.config.industry, searchParams.role ?? null];
const prefilledFilters = [project.config.channel, project.config.industry, searchParams.role ?? null];
return (
<TemplateContainerWithPreview
environmentId={environmentId}
user={user}
environment={environment}
product={product}
project={project}
prefilledFilters={prefilledFilters}
// AI Survey Creation -- Need improvement
isAIEnabled={false}
@@ -2,7 +2,7 @@
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromEnvironmentId, getProductIdFromEnvironmentId } from "@/lib/utils/helper";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
import { z } from "zod";
import { getSegmentsByAttributeClassName } from "@formbricks/lib/segment/service";
import { ZAttributeClass } from "@formbricks/types/attribute-classes";
@@ -25,9 +25,9 @@ export const getSegmentsByAttributeClassAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "read",
productId: await getProductIdFromEnvironmentId(parsedInput.environmentId),
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
},
],
});
@@ -1,6 +1,6 @@
import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { Button } from "@/modules/ui/components/button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
@@ -13,7 +13,7 @@ import { getAttributeClasses } from "@formbricks/lib/attributeClass/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 { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { AttributeClassesTable } from "./components/AttributeClassesTable";
@@ -25,10 +25,10 @@ const Page = async (props) => {
const params = await props.params;
let attributeClasses = await getAttributeClasses(params.environmentId);
const t = await getTranslations();
const product = await getProductByEnvironmentId(params.environmentId);
const project = await getProjectByEnvironmentId(params.environmentId);
const locale = await findMatchingLocale();
if (!product) {
throw new Error(t("common.product_not_found"));
if (!project) {
throw new Error(t("common.project_not_found"));
}
const [organization, session] = await Promise.all([
@@ -47,9 +47,9 @@ const Page = async (props) => {
const currentUserMembership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const productPermission = await getProductPermissionByUserId(session.user.id, product.id);
const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
@@ -6,7 +6,7 @@ import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
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 { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { AuthorizationError } from "@formbricks/types/errors";
const ConfigLayout = async (props) => {
@@ -40,9 +40,9 @@ const ConfigLayout = async (props) => {
return redirect(`/environments/${params.environmentId}/settings/billing`);
}
const product = await getProductByEnvironmentId(params.environmentId);
if (!product) {
throw new Error(t("common.product_not_found"));
const project = await getProjectByEnvironmentId(params.environmentId);
if (!project) {
throw new Error(t("common.project_not_found"));
}
return children;
@@ -1,9 +1,9 @@
import { ResponseTimeline } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseTimeline";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getResponsesByPersonId } from "@formbricks/lib/response/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { getUser } from "@formbricks/lib/user/service";
@@ -33,26 +33,26 @@ export const ResponseSection = async ({
const t = await getTranslations();
if (!session) {
throw new Error(t("common.no_session_found"));
throw new Error(t("common.session_not_found"));
}
const user = await getUser(session.user.id);
if (!user) {
throw new Error(t("common.no_user_found"));
throw new Error(t("common.user_not_found"));
}
if (!responses) {
throw new Error(t("environments.people.no_responses_found"));
}
const product = await getProductByEnvironmentId(environment.id);
const project = await getProjectByEnvironmentId(environment.id);
if (!product) {
throw new Error(t("common.no_product_found"));
if (!project) {
throw new Error(t("common.project_not_found"));
}
const productPermission = await getProductPermissionByUserId(session.user.id, product.id);
const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
const locale = await findMatchingLocale();
@@ -65,7 +65,7 @@ export const ResponseSection = async ({
environmentTags={environmentTags}
attributeClasses={attributeClasses}
locale={locale}
productPermission={productPermission}
projectPermission={projectPermission}
/>
);
};
@@ -1,7 +1,7 @@
"use client";
import { ResponseFeed } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponsesFeed";
import { TTeamPermission } from "@/modules/ee/teams/product-teams/types/teams";
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams";
import { ArrowDownUpIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
@@ -20,7 +20,7 @@ interface ResponseTimelineProps {
environmentTags: TTag[];
attributeClasses: TAttributeClass[];
locale: TUserLocale;
productPermission: TTeamPermission | null;
projectPermission: TTeamPermission | null;
}
export const ResponseTimeline = ({
@@ -31,7 +31,7 @@ export const ResponseTimeline = ({
environmentTags,
attributeClasses,
locale,
productPermission,
projectPermission,
}: ResponseTimelineProps) => {
const t = useTranslations();
const [sortedResponses, setSortedResponses] = useState(responses);
@@ -64,7 +64,7 @@ export const ResponseTimeline = ({
environmentTags={environmentTags}
attributeClasses={attributeClasses}
locale={locale}
productPermission={productPermission}
projectPermission={projectPermission}
/>
</div>
);
@@ -1,7 +1,7 @@
"use client";
import { SingleResponseCard } from "@/modules/analysis/components/SingleResponseCard";
import { TTeamPermission } from "@/modules/ee/teams/product-teams/types/teams";
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
import { useEffect, useState } from "react";
@@ -23,7 +23,7 @@ interface ResponseTimelineProps {
environmentTags: TTag[];
attributeClasses: TAttributeClass[];
locale: TUserLocale;
productPermission: TTeamPermission | null;
projectPermission: TTeamPermission | null;
}
export const ResponseFeed = ({
@@ -34,7 +34,7 @@ export const ResponseFeed = ({
environmentTags,
attributeClasses,
locale,
productPermission,
projectPermission,
}: ResponseTimelineProps) => {
const [fetchedResponses, setFetchedResponses] = useState(responses);
@@ -69,7 +69,7 @@ export const ResponseFeed = ({
updateResponse={updateResponse}
attributeClasses={attributeClasses}
locale={locale}
productPermission={productPermission}
projectPermission={projectPermission}
/>
))
)}
@@ -87,7 +87,7 @@ const ResponseSurveyCard = ({
updateResponse,
attributeClasses,
locale,
productPermission,
projectPermission,
}: {
response: TResponse;
surveys: TSurvey[];
@@ -98,7 +98,7 @@ const ResponseSurveyCard = ({
updateResponse: (responseId: string, response: TResponse) => void;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
productPermission: TTeamPermission | null;
projectPermission: TTeamPermission | null;
}) => {
const survey = surveys.find((survey) => {
return survey.id === response.surveyId;
@@ -107,7 +107,7 @@ const ResponseSurveyCard = ({
const { membershipRole } = useMembershipRole(survey?.environmentId || "", user.id);
const { isMember } = getAccessFlags(membershipRole);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
@@ -2,7 +2,7 @@ import { AttributesSection } from "@/app/(app)/environments/[environmentId]/(peo
import { DeletePersonButton } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/DeletePersonButton";
import { ResponseSection } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseSection";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
@@ -16,17 +16,17 @@ import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getPerson } from "@formbricks/lib/person/service";
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const [environment, environmentTags, product, session, organization, person, attributes, attributeClasses] =
const [environment, environmentTags, project, session, organization, person, attributes, attributeClasses] =
await Promise.all([
getEnvironment(params.environmentId),
getTagsByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getProjectByEnvironmentId(params.environmentId),
getServerSession(authOptions),
getOrganizationByEnvironmentId(params.environmentId),
getPerson(params.personId),
@@ -34,8 +34,8 @@ const Page = async (props) => {
getAttributeClasses(params.environmentId),
]);
if (!product) {
throw new Error(t("common.product_not_found"));
if (!project) {
throw new Error(t("common.project_not_found"));
}
if (!environment) {
@@ -57,8 +57,8 @@ const Page = async (props) => {
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const productPermission = await getProductPermissionByUserId(session.user.id, product.id);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
@@ -5,8 +5,8 @@ import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"
import {
getOrganizationIdFromEnvironmentId,
getOrganizationIdFromPersonId,
getProductIdFromEnvironmentId,
getProductIdFromPersonId,
getProjectIdFromEnvironmentId,
getProjectIdFromPersonId,
} from "@/lib/utils/helper";
import { z } from "zod";
import { deletePerson, getPeople } from "@formbricks/lib/person/service";
@@ -30,9 +30,9 @@ export const getPersonsAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "read",
productId: await getProductIdFromEnvironmentId(parsedInput.environmentId),
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
},
],
});
@@ -56,9 +56,9 @@ export const deletePersonAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "readWrite",
productId: await getProductIdFromPersonId(parsedInput.personId),
projectId: await getProjectIdFromPersonId(parsedInput.personId),
},
],
});
@@ -1,7 +1,7 @@
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
import { getTranslations } from "next-intl/server";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { TProduct } from "@formbricks/types/product";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { TProject } from "@formbricks/types/project";
interface PersonSecondaryNavigationProps {
activeId: string;
@@ -14,13 +14,13 @@ export const PersonSecondaryNavigation = async ({
environmentId,
loading,
}: PersonSecondaryNavigationProps) => {
let product: TProduct | null = null;
let project: TProject | null = null;
const t = await getTranslations();
if (!loading && environmentId) {
product = await getProductByEnvironmentId(environmentId);
project = await getProjectByEnvironmentId(environmentId);
if (!product) {
throw new Error("Product not found");
if (!project) {
throw new Error(t("common.project_not_found"));
}
}
@@ -1,7 +1,7 @@
import { PersonDataView } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonDataView";
import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { Button } from "@/modules/ui/components/button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
@@ -14,7 +14,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 { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const params = await props.params;
@@ -35,17 +35,17 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
throw new Error(t("common.organization_not_found"));
}
const product = await getProductByEnvironmentId(params.environmentId);
if (!product) {
throw new Error(t("common.product_not_found"));
const project = await getProjectByEnvironmentId(params.environmentId);
if (!project) {
throw new Error(t("common.project_not_found"));
}
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const productPermission = await getProductPermissionByUserId(session?.user.id, product.id);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, project.id);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
@@ -2,7 +2,7 @@
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromSegmentId, getProductIdFromSegmentId } from "@/lib/utils/helper";
import { getOrganizationIdFromSegmentId, getProjectIdFromSegmentId } from "@/lib/utils/helper";
import { z } from "zod";
import { deleteSegment, updateSegment } from "@formbricks/lib/segment/service";
import { ZId } from "@formbricks/types/common";
@@ -24,9 +24,9 @@ export const deleteBasicSegmentAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "readWrite",
productId: await getProductIdFromSegmentId(parsedInput.segmentId),
projectId: await getProjectIdFromSegmentId(parsedInput.segmentId),
},
],
});
@@ -51,9 +51,9 @@ export const updateBasicSegmentAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "readWrite",
productId: await getProductIdFromSegmentId(parsedInput.segmentId),
projectId: await getProjectIdFromSegmentId(parsedInput.segmentId),
},
],
});
@@ -4,7 +4,7 @@ import { SegmentTable } from "@/app/(app)/environments/[environmentId]/(people)/
import { authOptions } from "@/modules/auth/lib/authOptions";
import { CreateSegmentModal } from "@/modules/ee/advanced-targeting/components/create-segment-modal";
import { getAdvancedTargetingPermission } from "@/modules/ee/license-check/lib/utils";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
@@ -16,18 +16,18 @@ 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 { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getSegments } from "@formbricks/lib/segment/service";
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const [environment, segments, attributeClasses, organization, product] = await Promise.all([
const [environment, segments, attributeClasses, organization, project] = await Promise.all([
getEnvironment(params.environmentId),
getSegments(params.environmentId),
getAttributeClasses(params.environmentId, undefined, { skipArchived: true }),
getOrganizationByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getProjectByEnvironmentId(params.environmentId),
]);
const session = await getServerSession(authOptions);
@@ -39,8 +39,8 @@ const Page = async (props) => {
throw new Error(t("common.environment_not_found"));
}
if (!product) {
throw new Error(t("common.product_not_found"));
if (!project) {
throw new Error(t("common.project_not_found"));
}
if (!organization) {
@@ -56,9 +56,9 @@ const Page = async (props) => {
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const productPermission = await getProductPermissionByUserId(session?.user.id, product.id);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, project.id);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
@@ -2,71 +2,26 @@
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getIsMultiOrgEnabled, getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
import {
getOrganizationProjectsLimit,
getRoleManagementPermission,
} from "@/modules/ee/license-check/lib/utils";
import { createProject } from "@/modules/projects/settings/lib/project";
import { z } from "zod";
import { createMembership } from "@formbricks/lib/membership/service";
import { createOrganization, getOrganization } from "@formbricks/lib/organization/service";
import { createProduct } from "@formbricks/lib/product/service";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getOrganizationProjectsCount } from "@formbricks/lib/project/service";
import { updateUser } from "@formbricks/lib/user/service";
import { ZId } from "@formbricks/types/common";
import { OperationNotAllowedError } from "@formbricks/types/errors";
import { ZProductUpdateInput } from "@formbricks/types/product";
import { TUserNotificationSettings } from "@formbricks/types/user";
import { ZProjectUpdateInput } from "@formbricks/types/project";
const ZCreateOrganizationAction = z.object({
organizationName: z.string(),
});
export const createOrganizationAction = authenticatedActionClient
.schema(ZCreateOrganizationAction)
.action(async ({ ctx, parsedInput }) => {
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
if (!isMultiOrgEnabled)
throw new OperationNotAllowedError(
"Creating Multiple organization is restricted on your instance of Formbricks"
);
const newOrganization = await createOrganization({
name: parsedInput.organizationName,
});
await createMembership(newOrganization.id, ctx.user.id, {
role: "owner",
accepted: true,
});
const product = await createProduct(newOrganization.id, {
name: "My Product",
});
const updatedNotificationSettings: TUserNotificationSettings = {
...ctx.user.notificationSettings,
alert: {
...ctx.user.notificationSettings?.alert,
},
weeklySummary: {
...ctx.user.notificationSettings?.weeklySummary,
[product.id]: true,
},
unsubscribedOrganizationIds: Array.from(
new Set([...(ctx.user.notificationSettings?.unsubscribedOrganizationIds || []), newOrganization.id])
),
};
await updateUser(ctx.user.id, {
notificationSettings: updatedNotificationSettings,
});
return newOrganization;
});
const ZCreateProductAction = z.object({
const ZCreateProjectAction = z.object({
organizationId: ZId,
data: ZProductUpdateInput,
data: ZProjectUpdateInput,
});
export const createProductAction = authenticatedActionClient
.schema(ZCreateProductAction)
export const createProjectAction = authenticatedActionClient
.schema(ZCreateProjectAction)
.action(async ({ parsedInput, ctx }) => {
const { user } = ctx;
@@ -78,20 +33,27 @@ export const createProductAction = authenticatedActionClient
access: [
{
data: parsedInput.data,
schema: ZProductUpdateInput,
schema: ZProjectUpdateInput,
type: "organization",
roles: ["owner", "manager"],
},
],
});
const organization = await getOrganization(organizationId);
if (!organization) {
throw new Error("Organization not found");
}
const organizationProjectsLimit = await getOrganizationProjectsLimit(organization);
const organizationProjectsCount = await getOrganizationProjectsCount(organization.id);
if (organizationProjectsCount >= organizationProjectsLimit) {
throw new OperationNotAllowedError("Organization project limit reached");
}
if (parsedInput.data.teamIds && parsedInput.data.teamIds.length > 0) {
const organization = await getOrganization(organizationId);
if (!organization) {
throw new Error("Organization not found");
}
const canDoRoleManagement = await getRoleManagementPermission(organization);
if (!canDoRoleManagement) {
@@ -99,7 +61,7 @@ export const createProductAction = authenticatedActionClient
}
}
const product = await createProduct(parsedInput.organizationId, parsedInput.data);
const project = await createProject(parsedInput.organizationId, parsedInput.data);
const updatedNotificationSettings = {
...user.notificationSettings,
alert: {
@@ -107,7 +69,7 @@ export const createProductAction = authenticatedActionClient
},
weeklySummary: {
...user.notificationSettings?.weeklySummary,
[product.id]: true,
[project.id]: true,
},
};
@@ -115,5 +77,5 @@ export const createProductAction = authenticatedActionClient
notificationSettings: updatedNotificationSettings,
});
return product;
return project;
});
@@ -2,7 +2,7 @@
import { actionClient, authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromActionClassId, getProductIdFromActionClassId } from "@/lib/utils/helper";
import { getOrganizationIdFromActionClassId, getProjectIdFromActionClassId } from "@/lib/utils/helper";
import { z } from "zod";
import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
import { cache } from "@formbricks/lib/cache";
@@ -27,9 +27,9 @@ export const deleteActionClassAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "readWrite",
productId: await getProductIdFromActionClassId(parsedInput.actionClassId),
projectId: await getProjectIdFromActionClassId(parsedInput.actionClassId),
},
],
});
@@ -59,9 +59,9 @@ export const updateActionClassAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "readWrite",
productId: await getProductIdFromActionClassId(parsedInput.actionClassId),
projectId: await getProjectIdFromActionClassId(parsedInput.actionClassId),
},
],
});
@@ -89,9 +89,9 @@ export const getActiveInactiveSurveysAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "read",
productId: await getProductIdFromActionClassId(parsedInput.actionClassId),
projectId: await getProjectIdFromActionClassId(parsedInput.actionClassId),
},
],
});
@@ -3,7 +3,7 @@ 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 { authOptions } from "@/modules/auth/lib/authOptions";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
@@ -16,7 +16,7 @@ import { getEnvironments } 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 { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
export const metadata: Metadata = {
@@ -27,10 +27,10 @@ const Page = async (props) => {
const params = await props.params;
const session = await getServerSession(authOptions);
const t = await getTranslations();
const [actionClasses, organization, product] = await Promise.all([
const [actionClasses, organization, project] = await Promise.all([
getActionClasses(params.environmentId),
getOrganizationByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getProjectByEnvironmentId(params.environmentId),
]);
const locale = await findMatchingLocale();
@@ -42,11 +42,11 @@ const Page = async (props) => {
throw new Error(t("common.organization_not_found"));
}
if (!product) {
throw new Error(t("common.product_not_found"));
if (!project) {
throw new Error(t("common.project_not_found"));
}
const environments = await getEnvironments(product.id);
const environments = await getEnvironments(project.id);
const currentEnvironment = environments.find((env) => env.id === params.environmentId);
if (!currentEnvironment) {
@@ -60,9 +60,9 @@ const Page = async (props) => {
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember, isBilling } = getAccessFlags(currentUserMembership?.role);
const productPermission = await getProductPermissionByUserId(session?.user.id, product.id);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, project.id);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
if (isBilling) {
return redirect(`/environments/${params.environmentId}/settings/billing`);
@@ -1,7 +1,7 @@
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getEnterpriseLicense, getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
import { LimitsReachedBanner } from "@/modules/ui/components/limits-reached-banner";
import { PendingDowngradeBanner } from "@/modules/ui/components/pending-downgrade-banner";
@@ -17,7 +17,7 @@ import {
getOrganizationByEnvironmentId,
getOrganizationsByUserId,
} from "@formbricks/lib/organization/service";
import { getUserProducts } from "@formbricks/lib/product/service";
import { getUserProjects } from "@formbricks/lib/project/service";
import { getUser } from "@formbricks/lib/user/service";
interface EnvironmentLayoutProps {
@@ -47,13 +47,13 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
throw new Error(t("common.environment_not_found"));
}
const [products, environments] = await Promise.all([
getUserProducts(user.id, organization.id),
getEnvironments(environment.productId),
const [projects, environments] = await Promise.all([
getUserProjects(user.id, organization.id),
getEnvironments(environment.projectId),
]);
if (!products || !environments || !organizations) {
throw new Error(t("environments.products_environments_organizations_not_found"));
if (!projects || !environments || !organizations) {
throw new Error(t("environments.projects_environments_organizations_not_found"));
}
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
@@ -62,10 +62,10 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
const { features, lastChecked, isPendingDowngrade, active } = await getEnterpriseLicense();
const productPermission = await getProductPermissionByUserId(session.user.id, environment.productId);
const projectPermission = await getProjectPermissionByUserId(session.user.id, environment.projectId);
if (isMember && !productPermission) {
throw new Error(t("common.product_permission_not_found"));
if (isMember && !projectPermission) {
throw new Error(t("common.project_permission_not_found"));
}
const isMultiOrgEnabled = features?.isMultiOrgEnabled ?? false;
@@ -80,6 +80,8 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
]);
}
const organizationProjectsLimit = await getOrganizationProjectsLimit(organization);
return (
<div className="flex h-screen min-h-screen flex-col overflow-hidden">
<DevEnvironmentBanner environment={environment} />
@@ -105,18 +107,20 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
environment={environment}
organization={organization}
organizations={organizations}
products={products}
projects={projects}
organizationProjectsLimit={organizationProjectsLimit}
user={user}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
membershipRole={membershipRole}
isMultiOrgEnabled={isMultiOrgEnabled}
isLicenseActive={active}
/>
<div id="mainContent" className="flex-1 overflow-y-auto bg-slate-50">
<TopControlBar
environment={environment}
environments={environments}
membershipRole={membershipRole}
productPermission={productPermission}
projectPermission={projectPermission}
/>
<div className="mt-14">{children}</div>
</div>
@@ -5,6 +5,7 @@ import { NavigationLink } from "@/app/(app)/environments/[environmentId]/compone
import { formbricksLogout } from "@/app/lib/formbricks";
import FBLogo from "@/images/formbricks-wordmark.svg";
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
import { ProjectSwitcher } from "@/modules/projects/components/project-switcher";
import { ProfileAvatar } from "@/modules/ui/components/avatars";
import { Button } from "@/modules/ui/components/button";
import {
@@ -22,15 +23,11 @@ import {
} from "@/modules/ui/components/dropdown-menu";
import {
ArrowUpRightIcon,
BlendIcon,
BlocksIcon,
ChevronRightIcon,
Cog,
CreditCardIcon,
GlobeIcon,
GlobeLockIcon,
KeyIcon,
LinkIcon,
LogOutIcon,
MessageCircle,
MousePointerClick,
@@ -54,7 +51,7 @@ import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TEnvironment } from "@formbricks/types/environment";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";
import { TProduct } from "@formbricks/types/product";
import { TProject } from "@formbricks/types/project";
import { TUser } from "@formbricks/types/user";
import packageJson from "../../../../../package.json";
@@ -63,10 +60,12 @@ interface NavigationProps {
organizations: TOrganization[];
user: TUser;
organization: TOrganization;
products: TProduct[];
projects: TProject[];
isMultiOrgEnabled: boolean;
isFormbricksCloud?: boolean;
membershipRole?: TOrganizationRole;
organizationProjectsLimit: number;
isLicenseActive: boolean;
}
export const MainNavigation = ({
@@ -74,10 +73,12 @@ export const MainNavigation = ({
organizations,
organization,
user,
products,
projects,
isMultiOrgEnabled,
isFormbricksCloud = true,
membershipRole,
organizationProjectsLimit,
isLicenseActive,
}: NavigationProps) => {
const router = useRouter();
const pathname = usePathname();
@@ -89,7 +90,7 @@ export const MainNavigation = ({
const [isTextVisible, setIsTextVisible] = useState(true);
const [latestVersion, setLatestVersion] = useState("");
const product = products.find((product) => product.id === environment.productId);
const project = projects.find((project) => project.id === environment.projectId);
const { isManager, isOwner, isMember, isBilling } = getAccessFlags(membershipRole);
const isOwnerOrManager = isManager || isOwner;
@@ -124,39 +125,31 @@ export const MainNavigation = ({
return [...organizations].sort((a, b) => a.name.localeCompare(b.name));
}, [organizations]);
const sortedProducts = useMemo(() => {
const sortedProjects = useMemo(() => {
const channelOrder: (string | null)[] = ["website", "app", "link", null];
const groupedProducts = products.reduce(
(acc, product) => {
const channel = product.config.channel;
const groupedProjects = projects.reduce(
(acc, project) => {
const channel = project.config.channel;
const key = channel !== null ? channel : "null";
acc[key] = acc[key] || [];
acc[key].push(product);
acc[key].push(project);
return acc;
},
{} as Record<string, typeof products>
{} as Record<string, typeof projects>
);
Object.keys(groupedProducts).forEach((channel) => {
groupedProducts[channel].sort((a, b) => a.name.localeCompare(b.name));
Object.keys(groupedProjects).forEach((channel) => {
groupedProjects[channel].sort((a, b) => a.name.localeCompare(b.name));
});
return channelOrder.flatMap((channel) => groupedProducts[channel !== null ? channel : "null"] || []);
}, [products]);
const handleEnvironmentChangeByProduct = (productId: string) => {
router.push(`/products/${productId}/`);
};
return channelOrder.flatMap((channel) => groupedProjects[channel !== null ? channel : "null"] || []);
}, [projects]);
const handleEnvironmentChangeByOrganization = (organizationId: string) => {
router.push(`/organizations/${organizationId}/`);
};
const handleAddProduct = (organizationId: string) => {
router.push(`/organizations/${organizationId}/products/new/mode`);
};
const mainNavigation = useMemo(
() => [
{
@@ -189,9 +182,9 @@ export const MainNavigation = ({
},
{
name: t("common.configuration"),
href: `/environments/${environment.id}/product/general`,
href: `/environments/${environment.id}/project/general`,
icon: Cog,
isActive: pathname?.includes("/product"),
isActive: pathname?.includes("/project"),
},
],
[environment.id, pathname, isMember]
@@ -247,7 +240,7 @@ export const MainNavigation = ({
return (
<>
{product && (
{project && (
<aside
className={cn(
"z-40 flex flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100",
@@ -319,102 +312,20 @@ export const MainNavigation = ({
</Link>
)}
{/* Product Switch */}
{/* Project Switch */}
{!isBilling && (
<DropdownMenu>
<DropdownMenuTrigger
asChild
id="productDropdownTrigger"
className="w-full rounded-br-xl border-t py-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-none">
<div
tabIndex={0}
className={cn(
"flex cursor-pointer flex-row items-center space-x-3",
isCollapsed ? "pl-2" : "pl-4"
)}>
<div className="rounded-lg bg-slate-900 p-1.5 text-slate-50">
{product.config.channel === "website" ? (
<GlobeIcon strokeWidth={1.5} />
) : product.config.channel === "app" ? (
<GlobeLockIcon strokeWidth={1.5} />
) : product.config.channel === "link" ? (
<LinkIcon strokeWidth={1.5} />
) : (
<BlendIcon strokeWidth={1.5} />
)}
</div>
{!isCollapsed && !isTextVisible && (
<>
<div>
<p
title={product.name}
className={cn(
"ph-no-capture ph-no-capture -mb-0.5 max-w-28 truncate text-sm font-bold text-slate-700 transition-opacity duration-200",
isTextVisible ? "opacity-0" : "opacity-100"
)}>
{product.name}
</p>
<p
className={cn(
"text-sm text-slate-500 transition-opacity duration-200",
isTextVisible ? "opacity-0" : "opacity-100"
)}>
{product.config.channel === "link"
? "Link & Email"
: capitalizeFirstLetter(product.config.channel)}
</p>
</div>
<ChevronRightIcon
className={cn(
"h-5 w-5 text-slate-700 transition-opacity duration-200 hover:text-slate-500",
isTextVisible ? "opacity-0" : "opacity-100"
)}
/>
</>
)}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
id="userDropdownInnerContentWrapper"
side="right"
sideOffset={10}
alignOffset={-1}
align="end">
<DropdownMenuRadioGroup
value={product!.id}
onValueChange={(v) => handleEnvironmentChangeByProduct(v)}>
{sortedProducts.map((product) => (
<DropdownMenuRadioItem
value={product.id}
className="cursor-pointer break-all"
key={product.id}>
<div>
{product.config.channel === "website" ? (
<GlobeIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />
) : product.config.channel === "app" ? (
<GlobeLockIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />
) : product.config.channel === "link" ? (
<LinkIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />
) : (
<BlendIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />
)}
</div>
<div className="">{product?.name}</div>
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
{isOwnerOrManager && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => handleAddProduct(organization.id)}
icon={<PlusIcon className="mr-2 h-4 w-4" />}>
<span>{t("common.add_product")}</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
<ProjectSwitcher
environmentId={environment.id}
projects={sortedProjects}
project={project}
isCollapsed={isCollapsed}
isFormbricksCloud={isFormbricksCloud}
isLicenseActive={isLicenseActive}
isOwnerOrManager={isOwnerOrManager}
isTextVisible={isTextVisible}
organization={organization}
organizationProjectsLimit={organizationProjectsLimit}
/>
)}
{/* User Switch */}
@@ -1,13 +1,13 @@
import Link from "next/link";
import { ReactNode } from "react";
interface ProductNavItemProps {
interface ProjectNavItemProps {
href: string;
children: ReactNode;
isActive: boolean;
}
export const ProductNavItem = ({ href, children, isActive }: ProductNavItemProps) => {
export const ProjectNavItem = ({ href, children, isActive }: ProjectNavItemProps) => {
const activeClass = "bg-slate-50 font-semibold";
const inactiveClass = "hover:bg-slate-50";
@@ -1,5 +1,5 @@
import { TopControlButtons } from "@/app/(app)/environments/[environmentId]/components/TopControlButtons";
import { TTeamPermission } from "@/modules/ee/teams/product-teams/types/teams";
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { TEnvironment } from "@formbricks/types/environment";
import { TOrganizationRole } from "@formbricks/types/memberships";
@@ -8,14 +8,14 @@ interface SideBarProps {
environment: TEnvironment;
environments: TEnvironment[];
membershipRole?: TOrganizationRole;
productPermission: TTeamPermission | null;
projectPermission: TTeamPermission | null;
}
export const TopControlBar = ({
environment,
environments,
membershipRole,
productPermission,
projectPermission,
}: 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">
@@ -26,7 +26,7 @@ export const TopControlBar = ({
environments={environments}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
membershipRole={membershipRole}
productPermission={productPermission}
projectPermission={projectPermission}
/>
</div>
</div>
@@ -1,7 +1,7 @@
"use client";
import { EnvironmentSwitch } from "@/app/(app)/environments/[environmentId]/components/EnvironmentSwitch";
import { TTeamPermission } from "@/modules/ee/teams/product-teams/types/teams";
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { Button } from "@/modules/ui/components/button";
import { CircleUserIcon, MessageCircleQuestionIcon, PlusIcon } from "lucide-react";
@@ -17,7 +17,7 @@ interface TopControlButtonsProps {
environments: TEnvironment[];
isFormbricksCloud: boolean;
membershipRole?: TOrganizationRole;
productPermission: TTeamPermission | null;
projectPermission: TTeamPermission | null;
}
export const TopControlButtons = ({
@@ -25,13 +25,13 @@ export const TopControlButtons = ({
environments,
isFormbricksCloud,
membershipRole,
productPermission,
projectPermission,
}: TopControlButtonsProps) => {
const t = useTranslations();
const router = useRouter();
const { isMember, isBilling } = getAccessFlags(membershipRole);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
return (
@@ -12,13 +12,13 @@ export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProp
const stati = {
notImplemented: {
icon: AlertTriangleIcon,
title: t("environments.product.app-connection.formbricks_sdk_not_connected"),
subtitle: t("environments.product.app-connection.formbricks_sdk_not_connected_description"),
title: t("environments.project.app-connection.formbricks_sdk_not_connected"),
subtitle: t("environments.project.app-connection.formbricks_sdk_not_connected_description"),
},
running: {
icon: CheckIcon,
title: t("environments.product.app-connection.receiving_data"),
subtitle: t("environments.product.app-connection.formbricks_sdk_connected"),
title: t("environments.project.app-connection.receiving_data"),
subtitle: t("environments.project.app-connection.formbricks_sdk_connected"),
},
};
@@ -5,8 +5,8 @@ import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"
import {
getOrganizationIdFromEnvironmentId,
getOrganizationIdFromIntegrationId,
getProductIdFromEnvironmentId,
getProductIdFromIntegrationId,
getProjectIdFromEnvironmentId,
getProjectIdFromIntegrationId,
} from "@/lib/utils/helper";
import { z } from "zod";
import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service";
@@ -30,9 +30,9 @@ export const createOrUpdateIntegrationAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "readWrite",
productId: await getProductIdFromEnvironmentId(parsedInput.environmentId),
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
},
],
});
@@ -56,8 +56,8 @@ export const deleteIntegrationAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
productId: await getProductIdFromIntegrationId(parsedInput.integrationId),
type: "projectTeam",
projectId: await getProjectIdFromIntegrationId(parsedInput.integrationId),
minPermission: "readWrite",
},
],
@@ -1,6 +1,6 @@
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
@@ -15,7 +15,7 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
import { getIntegrations } from "@formbricks/lib/integration/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationItem } from "@formbricks/types/integration";
@@ -40,9 +40,9 @@ const Page = async (props) => {
if (!environment) {
throw new Error(t("common.environment_not_found"));
}
const product = await getProductByEnvironmentId(params.environmentId);
if (!product) {
throw new Error(t("common.product_not_found"));
const project = await getProjectByEnvironmentId(params.environmentId);
if (!project) {
throw new Error(t("common.project_not_found"));
}
const airtableIntegration: TIntegrationAirtable | undefined = integrations?.find(
@@ -58,13 +58,13 @@ const Page = async (props) => {
const currentUserMembership = await getMembershipByUserIdOrganizationId(
session?.user.id,
product.organizationId
project.organizationId
);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const productPermission = await getProductPermissionByUserId(session?.user.id, environment?.productId);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
@@ -1,6 +1,6 @@
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
@@ -19,7 +19,7 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
import { getIntegrations } from "@formbricks/lib/integration/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
@@ -43,9 +43,9 @@ const Page = async (props) => {
if (!environment) {
throw new Error(t("common.environment_not_found"));
}
const product = await getProductByEnvironmentId(params.environmentId);
if (!product) {
throw new Error(t("common.product_not_found"));
const project = await getProjectByEnvironmentId(params.environmentId);
if (!project) {
throw new Error(t("common.project_not_found"));
}
const googleSheetIntegration: TIntegrationGoogleSheets | undefined = integrations?.find(
@@ -56,13 +56,13 @@ const Page = async (props) => {
const currentUserMembership = await getMembershipByUserIdOrganizationId(
session?.user.id,
product.organizationId
project.organizationId
);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const productPermission = await getProductPermissionByUserId(session?.user.id, environment?.productId);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
@@ -1,6 +1,6 @@
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
@@ -21,7 +21,7 @@ import { getIntegrationByType } from "@formbricks/lib/integration/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getNotionDatabases } from "@formbricks/lib/notion/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
@@ -51,9 +51,9 @@ const Page = async (props) => {
throw new Error(t("common.environment_not_found"));
}
const product = await getProductByEnvironmentId(params.environmentId);
if (!product) {
throw new Error(t("common.product_not_found"));
const project = await getProjectByEnvironmentId(params.environmentId);
if (!project) {
throw new Error(t("common.project_not_found"));
}
let databasesArray: TIntegrationNotionDatabase[] = [];
@@ -64,13 +64,13 @@ const Page = async (props) => {
const currentUserMembership = await getMembershipByUserIdOrganizationId(
session?.user.id,
product.organizationId
project.organizationId
);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const productPermission = await getProductPermissionByUserId(session?.user.id, environment?.productId);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
@@ -8,7 +8,7 @@ import SlackLogo from "@/images/slacklogo.png";
import WebhookLogo from "@/images/webhook.png";
import ZapierLogo from "@/images/zapier-small.png";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { Card } from "@/modules/ui/components/integration-card";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
@@ -66,9 +66,9 @@ const Page = async (props) => {
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember, isBilling } = getAccessFlags(currentUserMembership?.role);
const productPermission = await getProductPermissionByUserId(session?.user.id, environment?.productId);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
@@ -222,7 +222,7 @@ const Page = async (props) => {
docsHref: "https://formbricks.com/docs/app-surveys/quickstart",
docsText: t("common.docs"),
docsNewTab: true,
connectHref: `/environments/${environmentId}/product/app-connection`,
connectHref: `/environments/${environmentId}/project/app-connection`,
connectText: t("common.connect"),
connectNewTab: false,
label: "Javascript SDK",
@@ -2,7 +2,7 @@
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromEnvironmentId, getProductIdFromEnvironmentId } from "@/lib/utils/helper";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
import { z } from "zod";
import { getSlackChannels } from "@formbricks/lib/slack/service";
import { ZId } from "@formbricks/types/common";
@@ -23,8 +23,8 @@ export const getSlackChannelsAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
productId: await getProductIdFromEnvironmentId(parsedInput.environmentId),
type: "projectTeam",
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
minPermission: "readWrite",
},
],
@@ -1,6 +1,6 @@
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
@@ -14,7 +14,7 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
import { getIntegrationByType } from "@formbricks/lib/integration/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
@@ -39,22 +39,22 @@ const Page = async (props) => {
throw new Error(t("common.environment_not_found"));
}
const product = await getProductByEnvironmentId(params.environmentId);
if (!product) {
throw new Error(t("common.product_not_found"));
const project = await getProjectByEnvironmentId(params.environmentId);
if (!project) {
throw new Error(t("common.project_not_found"));
}
const locale = await findMatchingLocale();
const currentUserMembership = await getMembershipByUserIdOrganizationId(
session?.user.id,
product.organizationId
project.organizationId
);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const productPermission = await getProductPermissionByUserId(session?.user.id, environment?.productId);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
@@ -5,7 +5,7 @@ import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"
import {
getOrganizationIdFromEnvironmentId,
getOrganizationIdFromWebhookId,
getProductIdFromEnvironmentId,
getProjectIdFromEnvironmentId,
} from "@/lib/utils/helper";
import { z } from "zod";
import { createWebhook, deleteWebhook, updateWebhook } from "@formbricks/lib/webhook/service";
@@ -30,9 +30,9 @@ export const createWebhookAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "read",
productId: await getProductIdFromEnvironmentId(parsedInput.environmentId),
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
},
],
});
@@ -56,9 +56,9 @@ export const deleteWebhookAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "readWrite",
productId: await getProductIdFromEnvironmentId(parsedInput.id),
projectId: await getProjectIdFromEnvironmentId(parsedInput.id),
},
],
});
@@ -83,9 +83,9 @@ export const updateWebhookAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "readWrite",
productId: await getProductIdFromEnvironmentId(parsedInput.webhookId),
projectId: await getProjectIdFromEnvironmentId(parsedInput.webhookId),
},
],
});
@@ -3,7 +3,7 @@ import { WebhookRowData } from "@/app/(app)/environments/[environmentId]/integra
import { WebhookTable } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTable";
import { WebhookTableHeading } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTableHeading";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
@@ -44,9 +44,9 @@ const Page = async (props) => {
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const productPermission = await getProductPermissionByUserId(session?.user.id, environment?.productId);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
@@ -8,7 +8,7 @@ import { notFound, redirect } from "next/navigation";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getUser } from "@formbricks/lib/user/service";
import { AuthorizationError } from "@formbricks/types/errors";
import { FormbricksClient } from "../../components/FormbricksClient";
@@ -40,9 +40,9 @@ export const EnvLayout = async (props) => {
if (!organization) {
throw new Error(t("common.organization_not_found"));
}
const product = await getProductByEnvironmentId(params.environmentId);
if (!product) {
throw new Error(t("common.product_not_found"));
const project = await getProjectByEnvironmentId(params.environmentId);
if (!project) {
throw new Error(t("common.project_not_found"));
}
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
@@ -1,38 +0,0 @@
"use server";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromProductId } from "@/lib/utils/helper";
import { z } from "zod";
import { deleteProduct, getUserProducts } from "@formbricks/lib/product/service";
import { ZId } from "@formbricks/types/common";
const ZProductDeleteAction = z.object({
productId: ZId,
});
export const deleteProductAction = authenticatedActionClient
.schema(ZProductDeleteAction)
.action(async ({ ctx, parsedInput }) => {
const organizationId = await getOrganizationIdFromProductId(parsedInput.productId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
],
});
const availableProducts = (await getUserProducts(ctx.user.id, organizationId)) ?? null;
if (!!availableProducts && availableProducts?.length <= 1) {
throw new Error("You can't delete the last product in the environment.");
}
// delete product
return await deleteProduct(parsedInput.productId);
});
@@ -1,8 +0,0 @@
import { redirect } from "next/navigation";
const Page = async (props) => {
const params = await props.params;
return redirect(`/environments/${params.environmentId}/product/general`);
};
export default Page;
@@ -1,3 +0,0 @@
import { ProductTeams } from "@/modules/ee/teams/product-teams/page";
export default ProductTeams;
@@ -0,0 +1,3 @@
import { AppConnectionLoading } from "@/modules/projects/settings/(setup)/app-connection/loading";
export default AppConnectionLoading;
@@ -0,0 +1,3 @@
import { AppConnectionPage } from "@/modules/projects/settings/(setup)/app-connection/page";
export default AppConnectionPage;
@@ -0,0 +1,3 @@
import { APIKeysLoading } from "@/modules/projects/settings/api-keys/loading";
export default APIKeysLoading;
@@ -0,0 +1,3 @@
import { APIKeysPage } from "@/modules/projects/settings/api-keys/page";
export default APIKeysPage;
@@ -0,0 +1,3 @@
import { GeneralSettingsLoading } from "@/modules/projects/settings/general/loading";
export default GeneralSettingsLoading;
@@ -0,0 +1,3 @@
import { GeneralSettingsPage } from "@/modules/projects/settings/general/page";
export default GeneralSettingsPage;
@@ -0,0 +1,3 @@
import { LanguagesLoading } from "@/modules/ee/languages/loading";
export default LanguagesLoading;
@@ -0,0 +1,3 @@
import { LanguagesPage } from "@/modules/ee/languages/page";
export default LanguagesPage;
@@ -0,0 +1,3 @@
import { ProjectSettingsLayout } from "@/modules/projects/settings/layout";
export default ProjectSettingsLayout;
@@ -0,0 +1,3 @@
import { ProjectLookSettingsLoading } from "@/modules/projects/settings/look/loading";
export default ProjectLookSettingsLoading;
@@ -0,0 +1,3 @@
import { ProjectLookSettingsPage } from "@/modules/projects/settings/look/page";
export default ProjectLookSettingsPage;
@@ -0,0 +1,3 @@
import { ProjectSettingsPage } from "@/modules/projects/settings/page";
export default ProjectSettingsPage;
@@ -0,0 +1,3 @@
import { TagsLoading } from "@/modules/projects/settings/tags/loading";
export default TagsLoading;
@@ -0,0 +1,3 @@
import { TagsPage } from "@/modules/projects/settings/tags/page";
export default TagsPage;
@@ -0,0 +1,3 @@
import { ProjectTeams } from "@/modules/ee/teams/project-teams/page";
export default ProjectTeams;
@@ -2,7 +2,7 @@ import { authOptions } from "@/modules/auth/lib/authOptions";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
const AccountSettingsLayout = async (props) => {
const params = await props.params;
@@ -10,9 +10,9 @@ const AccountSettingsLayout = async (props) => {
const { children } = props;
const t = await getTranslations();
const [organization, product, session] = await Promise.all([
const [organization, project, session] = await Promise.all([
getOrganizationByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getProjectByEnvironmentId(params.environmentId),
getServerSession(authOptions),
]);
@@ -20,8 +20,8 @@ const AccountSettingsLayout = async (props) => {
throw new Error(t("common.organization_not_found"));
}
if (!product) {
throw new Error(t("common.product_not_found"));
if (!project) {
throw new Error(t("common.project_not_found"));
}
if (!session) {
@@ -38,7 +38,7 @@ export const EditAlerts = ({
{t("environments.settings.notifications.auto_subscribe_to_new_surveys")}
</p>
<NotificationSwitch
surveyOrProductOrOrganizationId={membership.organization.id}
surveyOrProjectOrOrganizationId={membership.organization.id}
notificationSettings={user.notificationSettings!}
notificationType={"unsubscribedOrganizationIds"}
autoDisableNotificationType={autoDisableNotificationType}
@@ -64,13 +64,13 @@ export const EditAlerts = ({
</TooltipProvider>
</div>
{membership.organization.products.some((product) =>
product.environments.some((environment) => environment.surveys.length > 0)
{membership.organization.projects.some((project) =>
project.environments.some((environment) => environment.surveys.length > 0)
) ? (
<div className="grid-cols-8 space-y-1 p-2">
{membership.organization.products.map((product) => (
<div key={product.id}>
{product.environments.map((environment) => (
{membership.organization.projects.map((project) => (
<div key={project.id}>
{project.environments.map((environment) => (
<div key={environment.id}>
{environment.surveys.map((survey) => (
<div
@@ -78,11 +78,11 @@ export const EditAlerts = ({
key={survey.name}>
<div className="col-span-2 text-left">
<div className="font-medium text-slate-900">{survey.name}</div>
<div className="text-xs text-slate-400">{product.name}</div>
<div className="text-xs text-slate-400">{project.name}</div>
</div>
<div className="col-span-1 text-center">
<NotificationSwitch
surveyOrProductOrOrganizationId={survey.id}
surveyOrProjectOrOrganizationId={survey.id}
notificationSettings={user.notificationSettings!}
notificationType={"alert"}
autoDisableNotificationType={autoDisableNotificationType}
@@ -24,18 +24,18 @@ export const EditWeeklySummary = ({ memberships, user, environmentId }: EditAler
</div>
<div className="mb-6 rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-3 content-center rounded-t-lg bg-slate-100 px-4 text-left text-sm font-semibold text-slate-900">
<div className="col-span-2">{t("common.product")}</div>
<div className="col-span-2">{t("common.project")}</div>
<div className="col-span-1 text-center">{t("common.weekly_summary")}</div>
</div>
<div className="space-y-1 p-2">
{membership.organization.products.map((product) => (
{membership.organization.projects.map((project) => (
<div
className="grid h-auto w-full cursor-pointer grid-cols-3 place-content-center justify-center rounded-lg px-2 py-2 text-left text-sm text-slate-900 hover:bg-slate-50"
key={product.id}>
<div className="col-span-2">{product?.name}</div>
key={project.id}>
<div className="col-span-2">{project?.name}</div>
<div className="col-span-1 flex items-center justify-center">
<NotificationSwitch
surveyOrProductOrOrganizationId={product.id}
surveyOrProjectOrOrganizationId={project.id}
notificationSettings={user.notificationSettings!}
notificationType={"weeklySummary"}
/>
@@ -8,7 +8,7 @@ import { TUserNotificationSettings } from "@formbricks/types/user";
import { updateNotificationSettingsAction } from "../actions";
interface NotificationSwitchProps {
surveyOrProductOrOrganizationId: string;
surveyOrProjectOrOrganizationId: string;
notificationSettings: TUserNotificationSettings;
notificationType: "alert" | "weeklySummary" | "unsubscribedOrganizationIds";
autoDisableNotificationType?: string;
@@ -16,7 +16,7 @@ interface NotificationSwitchProps {
}
export const NotificationSwitch = ({
surveyOrProductOrOrganizationId,
surveyOrProjectOrOrganizationId,
notificationSettings,
notificationType,
autoDisableNotificationType,
@@ -26,8 +26,8 @@ export const NotificationSwitch = ({
const t = useTranslations();
const isChecked =
notificationType === "unsubscribedOrganizationIds"
? !notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrProductOrOrganizationId)
: notificationSettings[notificationType][surveyOrProductOrOrganizationId] === true;
? !notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrProjectOrOrganizationId)
: notificationSettings[notificationType][surveyOrProjectOrOrganizationId] === true;
const handleSwitchChange = async () => {
setIsLoading(true);
@@ -35,19 +35,19 @@ export const NotificationSwitch = ({
let updatedNotificationSettings = { ...notificationSettings };
if (notificationType === "unsubscribedOrganizationIds") {
const unsubscribedOrganizationIds = updatedNotificationSettings.unsubscribedOrganizationIds ?? [];
if (unsubscribedOrganizationIds.includes(surveyOrProductOrOrganizationId)) {
if (unsubscribedOrganizationIds.includes(surveyOrProjectOrOrganizationId)) {
updatedNotificationSettings.unsubscribedOrganizationIds = unsubscribedOrganizationIds.filter(
(id) => id !== surveyOrProductOrOrganizationId
(id) => id !== surveyOrProjectOrOrganizationId
);
} else {
updatedNotificationSettings.unsubscribedOrganizationIds = [
...unsubscribedOrganizationIds,
surveyOrProductOrOrganizationId,
surveyOrProjectOrOrganizationId,
];
}
} else {
updatedNotificationSettings[notificationType][surveyOrProductOrOrganizationId] =
!updatedNotificationSettings[notificationType][surveyOrProductOrOrganizationId];
updatedNotificationSettings[notificationType][surveyOrProjectOrOrganizationId] =
!updatedNotificationSettings[notificationType][surveyOrProjectOrOrganizationId];
}
await updateNotificationSettingsAction({ notificationSettings: updatedNotificationSettings });
@@ -57,12 +57,12 @@ export const NotificationSwitch = ({
useEffect(() => {
if (
autoDisableNotificationType &&
autoDisableNotificationElementId === surveyOrProductOrOrganizationId &&
autoDisableNotificationElementId === surveyOrProjectOrOrganizationId &&
isChecked
) {
switch (notificationType) {
case "alert":
if (notificationSettings[notificationType][surveyOrProductOrOrganizationId] === true) {
if (notificationSettings[notificationType][surveyOrProjectOrOrganizationId] === true) {
handleSwitchChange();
toast.success(
t(
@@ -76,7 +76,7 @@ export const NotificationSwitch = ({
break;
case "unsubscribedOrganizationIds":
if (!notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrProductOrOrganizationId)) {
if (!notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrProjectOrOrganizationId)) {
handleSwitchChange();
toast.success(
t(
@@ -13,7 +13,7 @@ const Loading = () => {
skeletonLines: [{ classes: "h-6 w-28" }, { classes: "h-10 w-128" }, { classes: "h-10 w-128" }],
},
{
title: t("environments.settings.notifications.weekly_summary_products"),
title: t("environments.settings.notifications.weekly_summary_projects"),
description: t("environments.settings.notifications.stay_up_to_date_with_a_Weekly_every_Monday"),
skeletonLines: [{ classes: "h-6 w-28" }, { classes: "h-10 w-128" }, { classes: "h-10 w-128" }],
},
@@ -23,12 +23,12 @@ const setCompleteNotificationSettings = (
unsubscribedOrganizationIds: notificationSettings.unsubscribedOrganizationIds || [],
};
for (const membership of memberships) {
for (const product of membership.organization.products) {
for (const project of membership.organization.projects) {
// set default values for weekly summary
newNotificationSettings.weeklySummary[product.id] =
(notificationSettings.weeklySummary && notificationSettings.weeklySummary[product.id]) || false;
newNotificationSettings.weeklySummary[project.id] =
(notificationSettings.weeklySummary && notificationSettings.weeklySummary[project.id]) || false;
// set default values for alerts
for (const environment of product.environments) {
for (const environment of project.environments) {
for (const survey of environment.surveys) {
newNotificationSettings.alert[survey.id] =
notificationSettings[survey.id]?.responseFinished ||
@@ -50,17 +50,17 @@ const getMemberships = async (userId: string): Promise<Membership[]> => {
},
OR: [
{
// Fetch all products if user role is owner or manager
// Fetch all projects if user role is owner or manager
role: {
in: ["owner", "manager"],
},
},
{
// Filter products based on team membership if user is not owner or manager
// Filter projects based on team membership if user is not owner or manager
organization: {
products: {
projects: {
some: {
productTeams: {
projectTeams: {
some: {
team: {
teamUsers: {
@@ -82,12 +82,12 @@ const getMemberships = async (userId: string): Promise<Membership[]> => {
select: {
id: true,
name: true,
products: {
projects: {
// Apply conditional filtering based on user's role
where: {
OR: [
{
// Fetch all products if user is owner or manager
// Fetch all projects if user is owner or manager
organization: {
memberships: {
some: {
@@ -100,8 +100,8 @@ const getMemberships = async (userId: string): Promise<Membership[]> => {
},
},
{
// Only include products accessible through teams if user is not owner or manager
productTeams: {
// Only include projects accessible through teams if user is not owner or manager
projectTeams: {
some: {
team: {
teamUsers: {
@@ -180,7 +180,7 @@ const Page = async (props) => {
</SettingsCard>
<IntegrationsTip environmentId={params.environmentId} />
<SettingsCard
title={t("environments.settings.notifications.weekly_summary_products")}
title={t("environments.settings.notifications.weekly_summary_projects")}
description={t("environments.settings.notifications.stay_up_to_date_with_a_Weekly_every_Monday")}>
<EditWeeklySummary memberships={memberships} user={user} environmentId={params.environmentId} />
</SettingsCard>
@@ -4,7 +4,7 @@ export interface Membership {
organization: {
id: string;
name: string;
products: {
projects: {
id: string;
name: string;
environments: {
@@ -46,7 +46,7 @@ const Page = async (props) => {
const paidFeatures = [
{
title: t("environments.product.languages.multi_language_surveys"),
title: t("environments.project.languages.multi_language_surveys"),
comingSoon: false,
onRequest: false,
},
@@ -2,7 +2,7 @@ import { authOptions } from "@/modules/auth/lib/authOptions";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
const Layout = async (props) => {
const params = await props.params;
@@ -10,9 +10,9 @@ const Layout = async (props) => {
const { children } = props;
const t = await getTranslations();
const [organization, product, session] = await Promise.all([
const [organization, project, session] = await Promise.all([
getOrganizationByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getProjectByEnvironmentId(params.environmentId),
getServerSession(authOptions),
]);
@@ -20,8 +20,8 @@ const Layout = async (props) => {
throw new Error(t("common.organization_not_found"));
}
if (!product) {
throw new Error(t("common.product_not_found"));
if (!project) {
throw new Error(t("common.project_not_found"));
}
if (!session) {
@@ -1,8 +1,9 @@
"use server";
import { generateInsightsForSurvey } from "@/app/api/(internal)/insights/lib/utils";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromSurveyId, getProductIdFromSurveyId } from "@/lib/utils/helper";
import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { getResponseCountBySurveyId, getResponses } from "@formbricks/lib/response/service";
@@ -35,9 +36,9 @@ export const getResponsesAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "read",
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
},
],
});
@@ -69,9 +70,9 @@ export const getSurveySummaryAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "read",
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
},
],
});
@@ -98,12 +99,40 @@ export const getResponseCountAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "productTeam",
type: "projectTeam",
minPermission: "read",
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
},
],
});
return getResponseCountBySurveyId(parsedInput.surveyId, parsedInput.filterCriteria);
});
const ZGenerateInsightsForSurveyAction = z.object({
surveyId: ZId,
});
export const generateInsightsForSurveyAction = authenticatedActionClient
.schema(ZGenerateInsightsForSurveyAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
access: [
{
type: "organization",
schema: ZGenerateInsightsForSurveyAction,
data: parsedInput,
roles: ["owner", "manager"],
},
{
type: "projectTeam",
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
minPermission: "readWrite",
},
],
});
generateInsightsForSurvey(parsedInput.surveyId);
});
@@ -25,7 +25,7 @@ export const EmptyAppSurveys = ({ environment }: TEmptyAppSurveysProps) => {
{t("environments.surveys.summary.connect_your_website_or_app_with_formbricks_to_get_started")}
</p>
<Link className="mt-2" href={`/environments/${environment.id}/product/app-connection`}>
<Link className="mt-2" href={`/environments/${environment.id}/project/app-connection`}>
<Button size="sm" className="flex w-[120px] justify-center">
{t("common.connect")}
</Button>
@@ -5,7 +5,7 @@ import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surv
import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
@@ -20,7 +20,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 { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
@@ -45,9 +45,9 @@ const Page = async (props) => {
if (!survey) {
throw new Error(t("common.survey_not_found"));
}
const product = await getProductByEnvironmentId(environment.id);
if (!product) {
throw new Error(t("common.product_not_found"));
const project = await getProjectByEnvironmentId(environment.id);
if (!project) {
throw new Error(t("common.project_not_found"));
}
const user = await getUser(session.user.id);
@@ -66,7 +66,7 @@ const Page = async (props) => {
const { isMember } = getAccessFlags(currentUserMembership?.role);
const permission = await getProductPermissionByUserId(session.user.id, product.id);
const permission = await getProjectPermissionByUserId(session.user.id, project.id);
const { hasReadAccess } = getTeamPermissionFlags(permission);
const isReadOnly = isMember && hasReadAccess;

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