mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-01 11:50:43 -05:00
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:
+2
-2
@@ -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 = ({
|
||||
|
||||
+2
-2
@@ -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">
|
||||
|
||||
+4
-4
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
+5
-5
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+17
-17
@@ -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"
|
||||
+17
@@ -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}</>;
|
||||
};
|
||||
|
||||
+10
-10
@@ -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"
|
||||
+32
-32
@@ -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>
|
||||
+15
-15
@@ -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"
|
||||
+27
-27
@@ -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),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
+4
-4
@@ -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,
|
||||
});
|
||||
|
||||
+2
-2
@@ -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 = ({
|
||||
|
||||
+5
-5
@@ -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";
|
||||
|
||||
+5
-5
@@ -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(
|
||||
{
|
||||
|
||||
+4
-4
@@ -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) => {
|
||||
|
||||
+2
-2
@@ -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 = ({
|
||||
|
||||
+1
-1
@@ -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")}
|
||||
|
||||
+5
-5
@@ -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}
|
||||
|
||||
+4
-4
@@ -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}
|
||||
|
||||
+6
-6
@@ -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}
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+23
-23
@@ -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")}
|
||||
|
||||
+27
-27
@@ -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}
|
||||
|
||||
+4
-4
@@ -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) => {
|
||||
|
||||
+18
-18
@@ -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>
|
||||
|
||||
+4
-4
@@ -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;
|
||||
|
||||
|
||||
+11
-11
@@ -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}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ export const getMinimalSurvey = (locale: string): TSurvey => ({
|
||||
surveyClosedMessage: {
|
||||
enabled: false,
|
||||
},
|
||||
productOverwrites: null,
|
||||
projectOverwrites: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
resultShareKey: null,
|
||||
|
||||
+3
-3
@@ -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",
|
||||
},
|
||||
],
|
||||
|
||||
+6
-6
@@ -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}
|
||||
|
||||
+14
-14
@@ -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;
|
||||
|
||||
+9
-9
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+4
-4
@@ -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>
|
||||
);
|
||||
|
||||
+7
-7
@@ -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),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
+6
-6
@@ -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 */}
|
||||
|
||||
+2
-2
@@ -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 (
|
||||
|
||||
+4
-4
@@ -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;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { AppConnectionLoading } from "@/modules/projects/settings/(setup)/app-connection/loading";
|
||||
|
||||
export default AppConnectionLoading;
|
||||
+3
@@ -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) {
|
||||
|
||||
+8
-8
@@ -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}
|
||||
|
||||
+5
-5
@@ -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"}
|
||||
/>
|
||||
|
||||
+12
-12
@@ -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(
|
||||
|
||||
+1
-1
@@ -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" }],
|
||||
},
|
||||
|
||||
+13
-13
@@ -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>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ export interface Membership {
|
||||
organization: {
|
||||
id: string;
|
||||
name: string;
|
||||
products: {
|
||||
projects: {
|
||||
id: string;
|
||||
name: string;
|
||||
environments: {
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+36
-7
@@ -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);
|
||||
});
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+6
-6
@@ -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
Reference in New Issue
Block a user