feat: Product Model Revamp (#4353)

Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Piyush Gupta
2024-12-03 10:04:09 +05:30
committed by GitHub
parent 5dcd32050a
commit 35b2d12e18
315 changed files with 4344 additions and 3587 deletions

View File

@@ -29,11 +29,11 @@ App surveys have 6-10x better conversion rates than emailed surveys. This tutori
className="max-w-full rounded-lg sm:max-w-3xl"
/>
2. **Choose your Product Channel**: On this step, you have to choose between the various channels that you want your product to be created in, you can create both app and link surveys from all the channels, but for the onboarding, please choose between the app surveys or the public website options, upon doing this, you'll be prompted to connect your app / website to formbricks.
2. **Choose your Project Channel**: On this step, you have to choose between the various channels that you want your project to be created in, you can create both app and link surveys from all the channels, but for the onboarding, please choose between the app surveys or the public website options, upon doing this, you'll be prompted to connect your app / website to formbricks.
<MdxImage
src={I2}
alt="Choose between app and website surveys product channels"
alt="Choose between app and website surveys project channels"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>

View File

@@ -62,7 +62,7 @@ Available Recontact Options include:
className="max-w-full rounded-lg sm:max-w-3xl"
/>
## Product-wide Global Waiting Time
## Project-wide Global Waiting Time
The Global Waiting Time is a universal blocker to make sure that no user sees too many surveys. This is particularly helpful when several teams of large organisations use Formbricks at the same time.
@@ -71,13 +71,13 @@ The Global Waiting Time is a universal blocker to make sure that no user sees to
To adjust the Global Waiting Time:
1. Visit Formbricks Settings
2. Go to Product Settings
2. Go to Project Settings
3. Find the **Recontact Waiting Time** section
4. Modify the interval (in days) as needed.
<MdxImage
src={GlobalWaitTime}
alt="Formbricks Product-Wide Wait Time"
alt="Formbricks Project-Wide Wait Time"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>

View File

@@ -68,7 +68,7 @@ Youre free to update the question and answer options. However, based on our e
className="max-w-full rounded-lg sm:max-w-3xl"
/>
_Want to change the button color? You can do so in the product settings._
_Want to change the button color? You can do so in the project settings._
Save, and move over to the “Audience” tab.

View File

@@ -38,8 +38,7 @@ To display the Trial Conversion Survey in your app you want to proceed as follow
3. Print that 💸
<Note>
## Formbricks Widget running?
We assume that you have already installed the Formbricks Widget in your web app. Its required to display messages and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins max.)](/app-surveys/quickstart)
## Formbricks Widget running? We assume that you have already installed the Formbricks Widget in your web app. Its required to display messages and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins max.)](/app-surveys/quickstart)
</Note>
### 1. Create new Trial Conversion Survey
@@ -66,7 +65,7 @@ Youre free to update the questions and answer options. However, based on our
className="max-w-full rounded-lg sm:max-w-3xl"
/>
_Want to change the button color? You can do so in the product settings!_
_Want to change the button color? You can do so in the project settings!_
Save, and move over to the “Audience” tab.

View File

@@ -62,7 +62,7 @@ Click on "Create Survey" and choose the template “Interview Prompt”:
### 2. Update prompt and CTA
Update the prompt, description and button text to match your products tonality. You can also update the button color in the Product Settings.
Update the prompt, description and button text to match your products tonality. You can also update the button color in the Project Settings.
<MdxImage
src={ChangeText}

View File

@@ -66,7 +66,7 @@ Youre free to update the question and answer options. However, based on our e
className="max-w-full rounded-lg sm:max-w-3xl"
/>
_Want to change the button color? You can do so in the product settings!_
_Want to change the button color? You can do so in the project settings!_
Save, and move over to where the magic happens: The “Audience” tab.

View File

@@ -76,7 +76,7 @@ Hit the below request to verify that you are authenticated with your API Key and
<Row>
<Col>
Get the product details and environment type of your account.
Get the project details and environment type of your account.
### Mandatory Headers
@@ -115,9 +115,9 @@ Hit the below request to verify that you are authenticated with your API Key and
"createdAt": "2023-08-08T18:04:59.922Z",
"updatedAt": "2023-08-08T18:04:59.922Z",
"type": "production",
"product": {
"project": {
"id": "cll2m30r60003mx0hnemjfckr",
"name": "My Product"
"name": "My Project"
},
"appSetupCompleted": false,
"websiteSetupCompleted": false,

View File

@@ -36,20 +36,20 @@ Here are the different access permissions, ranked from highest to lowest access
All users and their organization-level roles are listed in **Organization Settings > General**. Users can hold any of the following org-level roles:
- **Billing** users can manage payment and compliance details in the organization.
- **Org Members** can view most data in the organization and act in the products they are members of. They cannot join products on their own and need to be assigned.
- **Org Managers** have full management access to all teams and products. They can also manage the organization's membership. Org Managers can perform Team Admin actions without needing to join the team. They cannot change other organization settings.
- **Org Members** can view most data in the organization and act in the projects they are members of. They cannot join projects on their own and need to be assigned.
- **Org Managers** have full management access to all teams and projects. They can also manage the organization's membership. Org Managers can perform Team Admin actions without needing to join the team. They cannot change other organization settings.
- **Org Owners** have full access to the organization, its data, and settings. Org Owners can perform Team Admin actions without needing to join the team.
### Permissions at product level
### Permissions at project level
- **read**: read access to all resources (except settings) in the product.
- **read & write**: read & write access to all resources (except settings) in the product.
- **manage**: read & write access to all resources including settings in the product.
- **read**: read access to all resources (except settings) in the project.
- **read & write**: read & write access to all resources (except settings) in the project.
- **manage**: read & write access to all resources including settings in the project.
### Team-level Roles
- **Team Contributors** can view and act on surveys and responses.
- **Team Admins** have additional permissions to manage their team's membership and products. These permissions are granted at the team-level, and don't apply to teams where they're not a Team Admin.
- **Team Admins** have additional permissions to manage their team's membership and projects. These permissions are granted at the team-level, and don't apply to teams where they're not a Team Admin.
For more information on user roles & permissions, see below:
@@ -62,13 +62,13 @@ For more information on user roles & permissions, see below:
| Delete Member | ✅ | ✅ | ❌ | ❌ |
| Update Member Access | ✅ | ✅ | ❌ | ❌ |
| Update Billing | ✅ | ✅ | ✅ | ❌ |
| **Product** | | | | |
| Create Product | ✅ | ✅ | ❌ | ❌ |
| Update Product Name | ✅ | ✅ | ❌ | ✅\*\* |
| Update Product Recontact Options | ✅ | ✅ | ❌ | ✅\*\* |
| **Project** | | | | |
| Create Project | ✅ | ✅ | ❌ | ❌ |
| Update Project Name | ✅ | ✅ | ❌ | ✅\*\* |
| Update Project Recontact Options | ✅ | ✅ | ❌ | ✅\*\* |
| Update Look & Feel | ✅ | ✅ | ❌ | ✅\*\* |
| Update Survey Languages | ✅ | ✅ | ❌ | ✅\*\* |
| Delete Product | ✅ | ✅ | ❌ | ❌ |
| Delete Project | ✅ | ✅ | ❌ | ❌ |
| **Surveys** | | | | |
| Create New Survey | ✅ | ✅ | ❌ | ✅\* |
| Edit Survey | ✅ | ✅ | ❌ | ✅\* |

View File

@@ -59,11 +59,11 @@ How to deliver a specific language depends on the survey type (app or link surve
className="max-w-full rounded-lg sm:max-w-3xl"
/>
3. Select the preferred language from the dropdown and assign an identifier Alias. Click the **Add language** button to add the language to your product.
3. Select the preferred language from the dropdown and assign an identifier Alias. Click the **Add language** button to add the language to your project.
<MdxImage
src={AddLanguages}
alt="Add Multiple Languages to your Product"
alt="Add Multiple Languages to your Project"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
@@ -73,7 +73,7 @@ You can come back to this page anytime to add more languages or remove existing
<MdxImage
src={SurveysHome}
alt="Add Multiple Languages to your Product"
alt="Add Multiple Languages to your Project"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>

View File

@@ -19,8 +19,9 @@ export const metadata = {
Overwrite the global styling theme for individual surveys to create unique styles for each survey.
<Note>To set a styling theme for all surveys, please see the [Styling Theme](/global/styling-theme) manual. </Note>
<Note>
To set a styling theme for all surveys, please see the [Styling Theme](/global/styling-theme) manual.{" "}
</Note>
### Overwrite Styling Theme
@@ -30,16 +31,16 @@ Overwrite the global styling theme for individual surveys to create unique style
src={StepNine}
alt="Choose a link survey template"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
className="max-w-full rounded-lg sm:max-w-3xl"
/>
2. Activate the **Add Custom Styles** toggle to override the default product styling:
2. Activate the **Add Custom Styles** toggle to override the default project styling:
<MdxImage
src={StepTen}
alt="Choose a link survey template"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
className="max-w-full rounded-lg sm:max-w-3xl"
/>
3. Customize your survey's style as needed:
@@ -48,7 +49,7 @@ Overwrite the global styling theme for individual surveys to create unique style
src={StepEleven}
alt="Choose a link survey template"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
className="max-w-full rounded-lg sm:max-w-3xl"
/>
Voila! just hit the save button to apply your changes. Your survey is now ready to impress with its unique look!
@@ -98,7 +99,7 @@ We have an example of this in our [Demo project](https://github.com/formbricks/f
src={Mario}
alt="Choose a link survey template"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- **Hipster Living**: Does your monstera get enough water?
@@ -107,7 +108,7 @@ We have an example of this in our [Demo project](https://github.com/formbricks/f
src={HipsterLiving}
alt="Choose a link survey template"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- **Windows XP**: Hach, nostalgia. Made us wanna play Mafia.
@@ -116,7 +117,7 @@ We have an example of this in our [Demo project](https://github.com/formbricks/f
src={WindowsXp}
alt="Choose a link survey template"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- **Whosagooooodbooooy**: Things you've likely said to your dog.
@@ -125,7 +126,7 @@ We have an example of this in our [Demo project](https://github.com/formbricks/f
src={Doggo}
alt="Choose a link survey template"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
className="max-w-full rounded-lg sm:max-w-3xl"
/>
---

View File

@@ -48,7 +48,7 @@ In the left side bar, you find the `Configuration` page. On this page you find t
src={FormSettings}
alt="Form styling options UI"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- **Brand Color**: Sets the primary color tone of the survey.
@@ -62,7 +62,7 @@ In the left side bar, you find the `Configuration` page. On this page you find t
src={CardSettings}
alt="Card styling options UI"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- **Roundness**: Adjusts the corner roundness of the survey card and its components (including input boxes, buttons).
@@ -80,7 +80,7 @@ In the left side bar, you find the `Configuration` page. On this page you find t
src={BackgroundSettings}
alt="Background styling options UI"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- **Color**: Pick any color for the background
@@ -95,13 +95,13 @@ Customize your survey with your brand's logo.
<Note>Brand logos are only visible on Link Survey pages.</Note>
1. In the Look & Feel page itself in Product settings, scroll down to see the Logo Upload box.
1. In the Look & Feel page itself in Project settings, scroll down to see the Logo Upload box.
<MdxImage
src={StepFour}
alt="Choose a link survey template"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
className="max-w-full rounded-lg sm:max-w-3xl"
/>
2. Upload your logo
@@ -110,7 +110,7 @@ Customize your survey with your brand's logo.
src={StepFive}
alt="Choose a link survey template"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
className="max-w-full rounded-lg sm:max-w-3xl"
/>
3. Add a background color: If youve uploaded a transparent image and want to add background to it, enable this toggle and select the color of your choice.
@@ -119,7 +119,7 @@ Customize your survey with your brand's logo.
src={StepSix}
alt="Choose a link survey template"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
className="max-w-full rounded-lg sm:max-w-3xl"
/>
4. Remember to save your changes!
@@ -128,7 +128,7 @@ Customize your survey with your brand's logo.
src={StepSeven}
alt="Choose a link survey template"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<Note>The logo settings apply across all Link Surveys pages.</Note>

View File

@@ -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 = ({

View File

@@ -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;
}

View File

@@ -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">

View File

@@ -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);
};

View File

@@ -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 };
};

View File

@@ -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"

View File

@@ -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);

View File

@@ -1,12 +1,12 @@
import { TProductConfigChannel } from "@formbricks/types/product";
import { TProjectConfigChannel } from "@formbricks/types/project";
export const getCustomHeadline = (channel?: TProductConfigChannel) => {
export const getCustomHeadline = (channel?: TProjectConfigChannel) => {
switch (channel) {
case "website":
return "organizations.products.new.settings.website_channel_headline";
return "organizations.projects.new.settings.website_channel_headline";
case "app":
return "organizations.products.new.settings.app_channel_headline";
return "organizations.projects.new.settings.app_channel_headline";
default:
return "organizations.products.new.settings.link_channel_headline";
return "organizations.projects.new.settings.link_channel_headline";
}
};

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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;

View File

@@ -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"

View File

@@ -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}</>;
};

View File

@@ -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"

View File

@@ -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>

View File

@@ -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"

View File

@@ -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),
},
],
});

View File

@@ -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,
});

View File

@@ -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 = ({

View File

@@ -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";

View File

@@ -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(
{

View File

@@ -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) => {

View File

@@ -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 = ({

View File

@@ -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")}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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>

View File

@@ -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

View File

@@ -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")}

View File

@@ -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}

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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;

View File

@@ -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}

View File

@@ -29,7 +29,7 @@ export const getMinimalSurvey = (locale: string): TSurvey => ({
surveyClosedMessage: {
enabled: false,
},
productOverwrites: null,
projectOverwrites: null,
singleUse: null,
styling: null,
resultShareKey: null,

View File

@@ -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",
},
],

View File

@@ -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}

View File

@@ -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}

View File

@@ -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),
},
],
});

View File

@@ -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;

View File

@@ -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;

View File

@@ -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}
/>
);
};

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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),
},
],
});

View File

@@ -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"));
}
}

View File

@@ -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;

View File

@@ -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),
},
],
});

View File

@@ -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;

View File

@@ -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;
});

View File

@@ -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),
},
],
});

View File

@@ -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`);

View File

@@ -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>

View File

@@ -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 */}

View File

@@ -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";

View File

@@ -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>

View File

@@ -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 (

View File

@@ -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"),
},
};

View File

@@ -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",
},
],

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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",

View File

@@ -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",
},
],

View File

@@ -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;

View File

@@ -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),
},
],
});

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
});

View File

@@ -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;

View File

@@ -1,3 +0,0 @@
import { ProductTeams } from "@/modules/ee/teams/product-teams/page";
export default ProductTeams;

View File

@@ -0,0 +1,3 @@
import { AppConnectionLoading } from "@/modules/projects/settings/(setup)/app-connection/loading";
export default AppConnectionLoading;

View File

@@ -0,0 +1,3 @@
import { AppConnectionPage } from "@/modules/projects/settings/(setup)/app-connection/page";
export default AppConnectionPage;

View File

@@ -0,0 +1,3 @@
import { APIKeysLoading } from "@/modules/projects/settings/api-keys/loading";
export default APIKeysLoading;

View File

@@ -0,0 +1,3 @@
import { APIKeysPage } from "@/modules/projects/settings/api-keys/page";
export default APIKeysPage;

View File

@@ -0,0 +1,3 @@
import { GeneralSettingsLoading } from "@/modules/projects/settings/general/loading";
export default GeneralSettingsLoading;

View File

@@ -0,0 +1,3 @@
import { GeneralSettingsPage } from "@/modules/projects/settings/general/page";
export default GeneralSettingsPage;

View File

@@ -0,0 +1,3 @@
import { LanguagesLoading } from "@/modules/ee/languages/loading";
export default LanguagesLoading;

View File

@@ -0,0 +1,3 @@
import { LanguagesPage } from "@/modules/ee/languages/page";
export default LanguagesPage;

View File

@@ -0,0 +1,3 @@
import { ProjectSettingsLayout } from "@/modules/projects/settings/layout";
export default ProjectSettingsLayout;

View File

@@ -0,0 +1,3 @@
import { ProjectLookSettingsLoading } from "@/modules/projects/settings/look/loading";
export default ProjectLookSettingsLoading;

View File

@@ -0,0 +1,3 @@
import { ProjectLookSettingsPage } from "@/modules/projects/settings/look/page";
export default ProjectLookSettingsPage;

View File

@@ -0,0 +1,3 @@
import { ProjectSettingsPage } from "@/modules/projects/settings/page";
export default ProjectSettingsPage;

View File

@@ -0,0 +1,3 @@
import { TagsLoading } from "@/modules/projects/settings/tags/loading";
export default TagsLoading;

View File

@@ -0,0 +1,3 @@
import { TagsPage } from "@/modules/projects/settings/tags/page";
export default TagsPage;

View File

@@ -0,0 +1,3 @@
import { ProjectTeams } from "@/modules/ee/teams/project-teams/page";
export default ProjectTeams;

View File

@@ -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) {

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