diff --git a/apps/docs/app/app-surveys/quickstart/page.mdx b/apps/docs/app/app-surveys/quickstart/page.mdx index 86a1f440ef..a0a4d462d8 100644 --- a/apps/docs/app/app-surveys/quickstart/page.mdx +++ b/apps/docs/app/app-surveys/quickstart/page.mdx @@ -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. diff --git a/apps/docs/app/app-surveys/recontact/page.mdx b/apps/docs/app/app-surveys/recontact/page.mdx index 839c62a92d..286602fd55 100644 --- a/apps/docs/app/app-surveys/recontact/page.mdx +++ b/apps/docs/app/app-surveys/recontact/page.mdx @@ -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. diff --git a/apps/docs/app/best-practices/cancel-subscription/page.mdx b/apps/docs/app/best-practices/cancel-subscription/page.mdx index 4c26bfe200..570690a91f 100644 --- a/apps/docs/app/best-practices/cancel-subscription/page.mdx +++ b/apps/docs/app/best-practices/cancel-subscription/page.mdx @@ -68,7 +68,7 @@ You’re 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. diff --git a/apps/docs/app/best-practices/improve-trial-cr/page.mdx b/apps/docs/app/best-practices/improve-trial-cr/page.mdx index 551314b46a..9a29df30fb 100644 --- a/apps/docs/app/best-practices/improve-trial-cr/page.mdx +++ b/apps/docs/app/best-practices/improve-trial-cr/page.mdx @@ -38,8 +38,7 @@ To display the Trial Conversion Survey in your app you want to proceed as follow 3. Print that 💸 - ## Formbricks Widget running? - We assume that you have already installed the Formbricks Widget in your web app. It’s 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. It’s required to display messages and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins max.)](/app-surveys/quickstart) ### 1. Create new Trial Conversion Survey @@ -66,7 +65,7 @@ You’re 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. diff --git a/apps/docs/app/best-practices/interview-prompt/page.mdx b/apps/docs/app/best-practices/interview-prompt/page.mdx index 51a5106a9d..0f014c84da 100644 --- a/apps/docs/app/best-practices/interview-prompt/page.mdx +++ b/apps/docs/app/best-practices/interview-prompt/page.mdx @@ -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. -_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. diff --git a/apps/docs/app/developer-docs/rest-api/page.mdx b/apps/docs/app/developer-docs/rest-api/page.mdx index df5ffa922b..0e8a3f17e9 100644 --- a/apps/docs/app/developer-docs/rest-api/page.mdx +++ b/apps/docs/app/developer-docs/rest-api/page.mdx @@ -76,7 +76,7 @@ Hit the below request to verify that you are authenticated with your API Key and - 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, diff --git a/apps/docs/app/global/access-roles/page.mdx b/apps/docs/app/global/access-roles/page.mdx index ca9e9f4f5f..0af1b489b5 100644 --- a/apps/docs/app/global/access-roles/page.mdx +++ b/apps/docs/app/global/access-roles/page.mdx @@ -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 | ✅ | ✅ | ❌ | ✅\* | diff --git a/apps/docs/app/global/multi-language-surveys/page.mdx b/apps/docs/app/global/multi-language-surveys/page.mdx index 97cfbea24a..27f41a7c81 100644 --- a/apps/docs/app/global/multi-language-surveys/page.mdx +++ b/apps/docs/app/global/multi-language-surveys/page.mdx @@ -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. @@ -73,7 +73,7 @@ You can come back to this page anytime to add more languages or remove existing diff --git a/apps/docs/app/global/overwrite-styling/page.mdx b/apps/docs/app/global/overwrite-styling/page.mdx index f27c13dd63..4a66ff1514 100644 --- a/apps/docs/app/global/overwrite-styling/page.mdx +++ b/apps/docs/app/global/overwrite-styling/page.mdx @@ -19,8 +19,9 @@ export const metadata = { Overwrite the global styling theme for individual surveys to create unique styles for each survey. -To set a styling theme for all surveys, please see the [Styling Theme](/global/styling-theme) manual. - + + To set a styling theme for all surveys, please see the [Styling Theme](/global/styling-theme) manual.{" "} + ### 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: 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" /> --- diff --git a/apps/docs/app/global/styling-theme/page.mdx b/apps/docs/app/global/styling-theme/page.mdx index febc90ecab..f5b9544c34 100644 --- a/apps/docs/app/global/styling-theme/page.mdx +++ b/apps/docs/app/global/styling-theme/page.mdx @@ -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. Brand logos are only visible on Link Survey pages. -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. 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 you’ve 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" /> The logo settings apply across all Link Surveys pages. diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks.tsx index 5fe809a7e3..7e9627d18f 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks.tsx @@ -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 = ({ diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx index e757a55deb..9c8595f11c 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx @@ -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: }, @@ -18,7 +18,7 @@ const tabs = [ interface OnboardingSetupInstructionsProps { environmentId: string; webAppUrl: string; - channel: TProductConfigChannel; + channel: TProjectConfigChannel; widgetSetupCompleted: boolean; } diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx index ce56dc4a67..1b5f118e2e 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx @@ -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 (
diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList.tsx index 2a5e2c5c9d..fb9c419d4f 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList.tsx @@ -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(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); }; diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils.ts b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils.ts index 0b48a4479f..c93646425f 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils.ts +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils.ts @@ -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 }; }; diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/page.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/page.tsx index 061e289ae7..15ebc0ac15 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/page.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/page.tsx @@ -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 (
- - {products.length >= 2 && ( + + {projects.length >= 2 && (
diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/page.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/page.tsx similarity index 77% rename from apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/page.tsx rename to apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/page.tsx index d33fb08cff..0327ae6c2f 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/page.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/page.tsx @@ -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) => {
{channel === "link" || mode === "cx" ? (
) : (
)} - { canDoRoleManagement={canDoRoleManagement} locale={locale ?? DEFAULT_LOCALE} /> - {products.length >= 1 && ( + {projects.length >= 1 && ( )} -

{product.name} /

+

{project.name} /

{ diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyPlacementCard.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyPlacementCard.tsx index 9915bbf3ad..191489d598 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyPlacementCard.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyPlacementCard.tsx @@ -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 = ({

{t("environments.surveys.edit.to_keep_the_placement_over_all_surveys_consistent_you_can")}{" "} - + {t("environments.surveys.edit.set_the_global_placement_in_the_look_feel_settings")} diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/WhenToSendCard.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/WhenToSendCard.tsx index cacbd3b705..e8bd6db0f5 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/WhenToSendCard.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/WhenToSendCard.tsx @@ -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; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx index 3ef8049e3c..ca334a4524 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx @@ -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 ( ({ surveyClosedMessage: { enabled: false, }, - productOverwrites: null, + projectOverwrites: null, singleUse: null, styling: null, resultShareKey: null, diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/actions.ts b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/actions.ts index 4b69130ee3..4193e2b6a1 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/actions.ts +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/actions.ts @@ -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", }, ], diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/TemplateContainer.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/TemplateContainer.tsx index 4f2c7ecdee..71f6b7ef1e 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/TemplateContainer.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/TemplateContainer.tsx @@ -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 = ({ { @@ -82,7 +82,7 @@ export const TemplateContainerWithPreview = ({ file.name} diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx index 3d13753940..bd5f5952f8 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx @@ -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 ( { 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; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/layout.tsx index 82e7ca27ce..bfbf252938 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/layout.tsx @@ -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; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseSection.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseSection.tsx index 70941ea598..0f3188376f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseSection.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseSection.tsx @@ -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} /> ); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseTimeline.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseTimeline.tsx index cead3f9653..c399690ec4 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseTimeline.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseTimeline.tsx @@ -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} />

); diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponsesFeed.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponsesFeed.tsx index f43a031993..5da236fd22 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponsesFeed.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponsesFeed.tsx @@ -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; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/page.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/page.tsx index a91ed1ffe4..495fd9eb2b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/page.tsx @@ -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; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/actions.ts b/apps/web/app/(app)/environments/[environmentId]/(people)/people/actions.ts index eeaa757419..257fd860b6 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/actions.ts @@ -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), }, ], }); diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation.tsx index d37e6164f6..24278d12d3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation.tsx @@ -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")); } } diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/page.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/page.tsx index 8f4434832c..4b544208b2 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/page.tsx @@ -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; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/segments/actions.ts b/apps/web/app/(app)/environments/[environmentId]/(people)/segments/actions.ts index 031468c44d..441226d06a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/segments/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/segments/actions.ts @@ -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), }, ], }); diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/segments/page.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/segments/page.tsx index 7a91a8faa6..db135292ff 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/segments/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/segments/page.tsx @@ -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; diff --git a/apps/web/app/(app)/environments/[environmentId]/actions.ts b/apps/web/app/(app)/environments/[environmentId]/actions.ts index 2b13c3b654..847066d687 100644 --- a/apps/web/app/(app)/environments/[environmentId]/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/actions.ts @@ -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; }); diff --git a/apps/web/app/(app)/environments/[environmentId]/actions/actions.ts b/apps/web/app/(app)/environments/[environmentId]/actions/actions.ts index b1c186b15e..420343820b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/actions/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/actions/actions.ts @@ -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), }, ], }); diff --git a/apps/web/app/(app)/environments/[environmentId]/actions/page.tsx b/apps/web/app/(app)/environments/[environmentId]/actions/page.tsx index 24ec7f7c96..0c5abceee9 100644 --- a/apps/web/app/(app)/environments/[environmentId]/actions/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/actions/page.tsx @@ -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`); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.tsx b/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.tsx index b876e79a54..ab4a3bb4eb 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.tsx @@ -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 (
@@ -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} />
{children}
diff --git a/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx b/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx index cc38428384..90fdd8972a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx @@ -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 + {} as Record ); - 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 && (
diff --git a/apps/web/app/(app)/environments/[environmentId]/components/TopControlButtons.tsx b/apps/web/app/(app)/environments/[environmentId]/components/TopControlButtons.tsx index a5c35e7ded..d29d899cc3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/TopControlButtons.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/TopControlButtons.tsx @@ -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 ( diff --git a/apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.tsx b/apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.tsx index 0d1b7d8d0d..9529466271 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.tsx @@ -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"), }, }; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/actions.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/actions.ts index f46c2870ec..9378615fc6 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/actions.ts @@ -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", }, ], diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx index 2139ac59c3..6c1c6bfcf8 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx @@ -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; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx index 5686620e05..852fff787c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx @@ -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; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx index 6080183929..c7db627bfb 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx @@ -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; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx index 47263728ea..48b5be7dad 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx @@ -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", diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts index b3188cbc3d..708a156fa9 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/actions.ts @@ -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", }, ], diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx index 57cc28bb39..e4a8450af7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx @@ -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; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/actions.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/actions.ts index 49bd0963b6..59a787fa70 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/actions.ts @@ -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), }, ], }); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx index 2a0a06b414..4bd8609359 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx @@ -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; diff --git a/apps/web/app/(app)/environments/[environmentId]/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/layout.tsx index e1e9eb6f6f..d6e10c6d5b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/layout.tsx @@ -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); diff --git a/apps/web/app/(app)/environments/[environmentId]/product/general/actions.ts b/apps/web/app/(app)/environments/[environmentId]/product/general/actions.ts deleted file mode 100644 index 9c2e82dbf7..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/product/general/actions.ts +++ /dev/null @@ -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); - }); diff --git a/apps/web/app/(app)/environments/[environmentId]/product/page.tsx b/apps/web/app/(app)/environments/[environmentId]/product/page.tsx deleted file mode 100644 index edbb091e46..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/product/page.tsx +++ /dev/null @@ -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; diff --git a/apps/web/app/(app)/environments/[environmentId]/product/teams/page.tsx b/apps/web/app/(app)/environments/[environmentId]/product/teams/page.tsx deleted file mode 100644 index 4d8358c1e3..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/product/teams/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { ProductTeams } from "@/modules/ee/teams/product-teams/page"; - -export default ProductTeams; diff --git a/apps/web/app/(app)/environments/[environmentId]/project/(setup)/app-connection/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/project/(setup)/app-connection/loading.tsx new file mode 100644 index 0000000000..99f25bc677 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/project/(setup)/app-connection/loading.tsx @@ -0,0 +1,3 @@ +import { AppConnectionLoading } from "@/modules/projects/settings/(setup)/app-connection/loading"; + +export default AppConnectionLoading; diff --git a/apps/web/app/(app)/environments/[environmentId]/project/(setup)/app-connection/page.tsx b/apps/web/app/(app)/environments/[environmentId]/project/(setup)/app-connection/page.tsx new file mode 100644 index 0000000000..a6d96f27be --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/project/(setup)/app-connection/page.tsx @@ -0,0 +1,3 @@ +import { AppConnectionPage } from "@/modules/projects/settings/(setup)/app-connection/page"; + +export default AppConnectionPage; diff --git a/apps/web/app/(app)/environments/[environmentId]/project/api-keys/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/project/api-keys/loading.tsx new file mode 100644 index 0000000000..68619f57fb --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/project/api-keys/loading.tsx @@ -0,0 +1,3 @@ +import { APIKeysLoading } from "@/modules/projects/settings/api-keys/loading"; + +export default APIKeysLoading; diff --git a/apps/web/app/(app)/environments/[environmentId]/project/api-keys/page.tsx b/apps/web/app/(app)/environments/[environmentId]/project/api-keys/page.tsx new file mode 100644 index 0000000000..c631feeabc --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/project/api-keys/page.tsx @@ -0,0 +1,3 @@ +import { APIKeysPage } from "@/modules/projects/settings/api-keys/page"; + +export default APIKeysPage; diff --git a/apps/web/app/(app)/environments/[environmentId]/project/general/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/project/general/loading.tsx new file mode 100644 index 0000000000..2f245b546e --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/project/general/loading.tsx @@ -0,0 +1,3 @@ +import { GeneralSettingsLoading } from "@/modules/projects/settings/general/loading"; + +export default GeneralSettingsLoading; diff --git a/apps/web/app/(app)/environments/[environmentId]/project/general/page.tsx b/apps/web/app/(app)/environments/[environmentId]/project/general/page.tsx new file mode 100644 index 0000000000..71eda04bf1 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/project/general/page.tsx @@ -0,0 +1,3 @@ +import { GeneralSettingsPage } from "@/modules/projects/settings/general/page"; + +export default GeneralSettingsPage; diff --git a/apps/web/app/(app)/environments/[environmentId]/project/languages/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/project/languages/loading.tsx new file mode 100644 index 0000000000..0b5a9bd2d8 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/project/languages/loading.tsx @@ -0,0 +1,3 @@ +import { LanguagesLoading } from "@/modules/ee/languages/loading"; + +export default LanguagesLoading; diff --git a/apps/web/app/(app)/environments/[environmentId]/project/languages/page.tsx b/apps/web/app/(app)/environments/[environmentId]/project/languages/page.tsx new file mode 100644 index 0000000000..ee76ebc7d5 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/project/languages/page.tsx @@ -0,0 +1,3 @@ +import { LanguagesPage } from "@/modules/ee/languages/page"; + +export default LanguagesPage; diff --git a/apps/web/app/(app)/environments/[environmentId]/project/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/project/layout.tsx new file mode 100644 index 0000000000..7fea4aafdd --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/project/layout.tsx @@ -0,0 +1,3 @@ +import { ProjectSettingsLayout } from "@/modules/projects/settings/layout"; + +export default ProjectSettingsLayout; diff --git a/apps/web/app/(app)/environments/[environmentId]/project/look/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/project/look/loading.tsx new file mode 100644 index 0000000000..f191b177f2 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/project/look/loading.tsx @@ -0,0 +1,3 @@ +import { ProjectLookSettingsLoading } from "@/modules/projects/settings/look/loading"; + +export default ProjectLookSettingsLoading; diff --git a/apps/web/app/(app)/environments/[environmentId]/project/look/page.tsx b/apps/web/app/(app)/environments/[environmentId]/project/look/page.tsx new file mode 100644 index 0000000000..79a9c8a520 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/project/look/page.tsx @@ -0,0 +1,3 @@ +import { ProjectLookSettingsPage } from "@/modules/projects/settings/look/page"; + +export default ProjectLookSettingsPage; diff --git a/apps/web/app/(app)/environments/[environmentId]/project/page.tsx b/apps/web/app/(app)/environments/[environmentId]/project/page.tsx new file mode 100644 index 0000000000..e7943862dd --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/project/page.tsx @@ -0,0 +1,3 @@ +import { ProjectSettingsPage } from "@/modules/projects/settings/page"; + +export default ProjectSettingsPage; diff --git a/apps/web/app/(app)/environments/[environmentId]/project/tags/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/project/tags/loading.tsx new file mode 100644 index 0000000000..db598e3424 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/project/tags/loading.tsx @@ -0,0 +1,3 @@ +import { TagsLoading } from "@/modules/projects/settings/tags/loading"; + +export default TagsLoading; diff --git a/apps/web/app/(app)/environments/[environmentId]/project/tags/page.tsx b/apps/web/app/(app)/environments/[environmentId]/project/tags/page.tsx new file mode 100644 index 0000000000..4c5acbe27d --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/project/tags/page.tsx @@ -0,0 +1,3 @@ +import { TagsPage } from "@/modules/projects/settings/tags/page"; + +export default TagsPage; diff --git a/apps/web/app/(app)/environments/[environmentId]/project/teams/page.tsx b/apps/web/app/(app)/environments/[environmentId]/project/teams/page.tsx new file mode 100644 index 0000000000..60fe580b26 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/project/teams/page.tsx @@ -0,0 +1,3 @@ +import { ProjectTeams } from "@/modules/ee/teams/project-teams/page"; + +export default ProjectTeams; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.tsx index 9689fdecc1..6d7a953f7e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.tsx @@ -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) { diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.tsx index 66cc21a6eb..8fb7417912 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.tsx @@ -38,7 +38,7 @@ export const EditAlerts = ({ {t("environments.settings.notifications.auto_subscribe_to_new_surveys")}

- {membership.organization.products.some((product) => - product.environments.some((environment) => environment.surveys.length > 0) + {membership.organization.projects.some((project) => + project.environments.some((environment) => environment.surveys.length > 0) ) ? (
- {membership.organization.products.map((product) => ( -
- {product.environments.map((environment) => ( + {membership.organization.projects.map((project) => ( +
+ {project.environments.map((environment) => (
{environment.surveys.map((survey) => (
{survey.name}
-
{product.name}
+
{project.name}
-
{t("common.product")}
+
{t("common.project")}
{t("common.weekly_summary")}
- {membership.organization.products.map((product) => ( + {membership.organization.projects.map((project) => (
-
{product?.name}
+ key={project.id}> +
{project?.name}
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.tsx index fe962d0b57..3874cdfcf7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.tsx @@ -8,7 +8,7 @@ import { TUserNotificationSettings } from "@formbricks/types/user"; import { updateNotificationSettingsAction } from "../actions"; interface NotificationSwitchProps { - surveyOrProductOrOrganizationId: string; + surveyOrProjectOrOrganizationId: string; notificationSettings: TUserNotificationSettings; notificationType: "alert" | "weeklySummary" | "unsubscribedOrganizationIds"; autoDisableNotificationType?: string; @@ -16,7 +16,7 @@ interface NotificationSwitchProps { } export const NotificationSwitch = ({ - surveyOrProductOrOrganizationId, + surveyOrProjectOrOrganizationId, notificationSettings, notificationType, autoDisableNotificationType, @@ -26,8 +26,8 @@ export const NotificationSwitch = ({ const t = useTranslations(); const isChecked = notificationType === "unsubscribedOrganizationIds" - ? !notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrProductOrOrganizationId) - : notificationSettings[notificationType][surveyOrProductOrOrganizationId] === true; + ? !notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrProjectOrOrganizationId) + : notificationSettings[notificationType][surveyOrProjectOrOrganizationId] === true; const handleSwitchChange = async () => { setIsLoading(true); @@ -35,19 +35,19 @@ export const NotificationSwitch = ({ let updatedNotificationSettings = { ...notificationSettings }; if (notificationType === "unsubscribedOrganizationIds") { const unsubscribedOrganizationIds = updatedNotificationSettings.unsubscribedOrganizationIds ?? []; - if (unsubscribedOrganizationIds.includes(surveyOrProductOrOrganizationId)) { + if (unsubscribedOrganizationIds.includes(surveyOrProjectOrOrganizationId)) { updatedNotificationSettings.unsubscribedOrganizationIds = unsubscribedOrganizationIds.filter( - (id) => id !== surveyOrProductOrOrganizationId + (id) => id !== surveyOrProjectOrOrganizationId ); } else { updatedNotificationSettings.unsubscribedOrganizationIds = [ ...unsubscribedOrganizationIds, - surveyOrProductOrOrganizationId, + surveyOrProjectOrOrganizationId, ]; } } else { - updatedNotificationSettings[notificationType][surveyOrProductOrOrganizationId] = - !updatedNotificationSettings[notificationType][surveyOrProductOrOrganizationId]; + updatedNotificationSettings[notificationType][surveyOrProjectOrOrganizationId] = + !updatedNotificationSettings[notificationType][surveyOrProjectOrOrganizationId]; } await updateNotificationSettingsAction({ notificationSettings: updatedNotificationSettings }); @@ -57,12 +57,12 @@ export const NotificationSwitch = ({ useEffect(() => { if ( autoDisableNotificationType && - autoDisableNotificationElementId === surveyOrProductOrOrganizationId && + autoDisableNotificationElementId === surveyOrProjectOrOrganizationId && isChecked ) { switch (notificationType) { case "alert": - if (notificationSettings[notificationType][surveyOrProductOrOrganizationId] === true) { + if (notificationSettings[notificationType][surveyOrProjectOrOrganizationId] === true) { handleSwitchChange(); toast.success( t( @@ -76,7 +76,7 @@ export const NotificationSwitch = ({ break; case "unsubscribedOrganizationIds": - if (!notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrProductOrOrganizationId)) { + if (!notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrProjectOrOrganizationId)) { handleSwitchChange(); toast.success( t( diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.tsx index fe53c08d81..8ca8f72e5e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.tsx @@ -13,7 +13,7 @@ const Loading = () => { skeletonLines: [{ classes: "h-6 w-28" }, { classes: "h-10 w-128" }, { classes: "h-10 w-128" }], }, { - title: t("environments.settings.notifications.weekly_summary_products"), + title: t("environments.settings.notifications.weekly_summary_projects"), description: t("environments.settings.notifications.stay_up_to_date_with_a_Weekly_every_Monday"), skeletonLines: [{ classes: "h-6 w-28" }, { classes: "h-10 w-128" }, { classes: "h-10 w-128" }], }, diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx index feab128a50..becc60a674 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx @@ -23,12 +23,12 @@ const setCompleteNotificationSettings = ( unsubscribedOrganizationIds: notificationSettings.unsubscribedOrganizationIds || [], }; for (const membership of memberships) { - for (const product of membership.organization.products) { + for (const project of membership.organization.projects) { // set default values for weekly summary - newNotificationSettings.weeklySummary[product.id] = - (notificationSettings.weeklySummary && notificationSettings.weeklySummary[product.id]) || false; + newNotificationSettings.weeklySummary[project.id] = + (notificationSettings.weeklySummary && notificationSettings.weeklySummary[project.id]) || false; // set default values for alerts - for (const environment of product.environments) { + for (const environment of project.environments) { for (const survey of environment.surveys) { newNotificationSettings.alert[survey.id] = notificationSettings[survey.id]?.responseFinished || @@ -50,17 +50,17 @@ const getMemberships = async (userId: string): Promise => { }, OR: [ { - // Fetch all products if user role is owner or manager + // Fetch all projects if user role is owner or manager role: { in: ["owner", "manager"], }, }, { - // Filter products based on team membership if user is not owner or manager + // Filter projects based on team membership if user is not owner or manager organization: { - products: { + projects: { some: { - productTeams: { + projectTeams: { some: { team: { teamUsers: { @@ -82,12 +82,12 @@ const getMemberships = async (userId: string): Promise => { select: { id: true, name: true, - products: { + projects: { // Apply conditional filtering based on user's role where: { OR: [ { - // Fetch all products if user is owner or manager + // Fetch all projects if user is owner or manager organization: { memberships: { some: { @@ -100,8 +100,8 @@ const getMemberships = async (userId: string): Promise => { }, }, { - // Only include products accessible through teams if user is not owner or manager - productTeams: { + // Only include projects accessible through teams if user is not owner or manager + projectTeams: { some: { team: { teamUsers: { @@ -180,7 +180,7 @@ const Page = async (props) => { diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/types.ts b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/types.ts index ca3f2848a7..9378358236 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/types.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/types.ts @@ -4,7 +4,7 @@ export interface Membership { organization: { id: string; name: string; - products: { + projects: { id: string; name: string; environments: { diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx index 2b8c27852e..cf4e1b80e6 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx @@ -46,7 +46,7 @@ const Page = async (props) => { const paidFeatures = [ { - title: t("environments.product.languages.multi_language_surveys"), + title: t("environments.project.languages.multi_language_surveys"), comingSoon: false, onRequest: false, }, diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/layout.tsx index cbef90a105..151a07d279 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/layout.tsx @@ -2,7 +2,7 @@ import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; -import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; +import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; const Layout = async (props) => { const params = await props.params; @@ -10,9 +10,9 @@ const Layout = async (props) => { const { children } = props; const t = await getTranslations(); - const [organization, product, session] = await Promise.all([ + const [organization, project, session] = await Promise.all([ getOrganizationByEnvironmentId(params.environmentId), - getProductByEnvironmentId(params.environmentId), + getProjectByEnvironmentId(params.environmentId), getServerSession(authOptions), ]); @@ -20,8 +20,8 @@ const Layout = async (props) => { throw new Error(t("common.organization_not_found")); } - if (!product) { - throw new Error(t("common.product_not_found")); + if (!project) { + throw new Error(t("common.project_not_found")); } if (!session) { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts index 2e63bdca8c..7c7b68503f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts @@ -1,8 +1,9 @@ "use server"; +import { generateInsightsForSurvey } from "@/app/api/(internal)/insights/lib/utils"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; -import { getOrganizationIdFromSurveyId, getProductIdFromSurveyId } from "@/lib/utils/helper"; +import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import { getResponseCountBySurveyId, getResponses } from "@formbricks/lib/response/service"; @@ -35,9 +36,9 @@ export const getResponsesAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "read", - productId: await getProductIdFromSurveyId(parsedInput.surveyId), + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), }, ], }); @@ -69,9 +70,9 @@ export const getSurveySummaryAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "read", - productId: await getProductIdFromSurveyId(parsedInput.surveyId), + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), }, ], }); @@ -98,12 +99,40 @@ export const getResponseCountAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "read", - productId: await getProductIdFromSurveyId(parsedInput.surveyId), + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), }, ], }); return getResponseCountBySurveyId(parsedInput.surveyId, parsedInput.filterCriteria); }); + +const ZGenerateInsightsForSurveyAction = z.object({ + surveyId: ZId, +}); + +export const generateInsightsForSurveyAction = authenticatedActionClient + .schema(ZGenerateInsightsForSurveyAction) + .action(async ({ ctx, parsedInput }) => { + await checkAuthorizationUpdated({ + userId: ctx.user.id, + organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId), + access: [ + { + type: "organization", + schema: ZGenerateInsightsForSurveyAction, + data: parsedInput, + roles: ["owner", "manager"], + }, + { + type: "projectTeam", + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), + minPermission: "readWrite", + }, + ], + }); + + generateInsightsForSurvey(parsedInput.surveyId); + }); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys.tsx index 49acaa3878..45a1ac6ea5 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys.tsx @@ -25,7 +25,7 @@ export const EmptyAppSurveys = ({ environment }: TEmptyAppSurveysProps) => { {t("environments.surveys.summary.connect_your_website_or_app_with_formbricks_to_get_started")}

- + diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx index 452cc83d96..bd4a9fa36e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx @@ -5,7 +5,7 @@ import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surv import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils"; -import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; +import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; @@ -20,7 +20,7 @@ import { getEnvironment } from "@formbricks/lib/environment/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; -import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; +import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; import { getResponseCountBySurveyId } from "@formbricks/lib/response/service"; import { getSurvey } from "@formbricks/lib/survey/service"; import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service"; @@ -45,9 +45,9 @@ const Page = async (props) => { if (!survey) { throw new Error(t("common.survey_not_found")); } - const product = await getProductByEnvironmentId(environment.id); - if (!product) { - throw new Error(t("common.product_not_found")); + const project = await getProjectByEnvironmentId(environment.id); + if (!project) { + throw new Error(t("common.project_not_found")); } const user = await getUser(session.user.id); @@ -66,7 +66,7 @@ const Page = async (props) => { const { isMember } = getAccessFlags(currentUserMembership?.role); - const permission = await getProductPermissionByUserId(session.user.id, product.id); + const permission = await getProjectPermissionByUserId(session.user.id, project.id); const { hasReadAccess } = getTeamPermissionFlags(permission); const isReadOnly = isMember && hasReadAccess; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts index 27c8651f91..97da425d9b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts @@ -3,7 +3,7 @@ import { getEmailTemplateHtml } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; -import { getOrganizationIdFromSurveyId, getProductIdFromSurveyId } from "@/lib/utils/helper"; +import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper"; import { sendEmbedSurveyPreviewEmail } from "@/modules/email"; import { customAlphabet } from "nanoid"; import { z } from "zod"; @@ -27,9 +27,9 @@ export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "read", - productId: await getProductIdFromSurveyId(parsedInput.surveyId), + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), }, ], }); @@ -70,9 +70,9 @@ export const generateResultShareUrlAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "readWrite", - productId: await getProductIdFromSurveyId(parsedInput.surveyId), + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), }, ], }); @@ -108,8 +108,8 @@ export const getResultShareUrlAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", - productId: await getProductIdFromSurveyId(parsedInput.surveyId), + type: "projectTeam", + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), minPermission: "readWrite", }, ], @@ -139,9 +139,9 @@ export const deleteResultShareUrlAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "readWrite", - productId: await getProductIdFromSurveyId(parsedInput.surveyId), + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), }, ], }); @@ -170,9 +170,9 @@ export const getEmailHtmlAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "readWrite", - productId: await getProductIdFromSurveyId(parsedInput.surveyId), + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), }, ], }); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/AppTab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/AppTab.tsx index bf3c93ff18..17202985ba 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/AppTab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/AppTab.tsx @@ -64,7 +64,7 @@ const WebAppTab = ({ environmentId }) => {
  • {t("common.follow_these")}{" "} {t("environments.surveys.summary.setup_instructions")} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteTab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteTab.tsx index 0b825fbbd8..870b5b0bc5 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteTab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebsiteTab.tsx @@ -94,7 +94,7 @@ const PopupTab = ({ environmentId }) => {
  • {t("common.follow_these")}{" "} {t("environments.surveys.summary.setup_instructions")} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate.tsx index 4d23e983e9..3c65ef968c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate.tsx @@ -1,6 +1,6 @@ import { getPreviewEmailTemplateHtml } from "@/modules/email/components/preview-email-template"; import { WEBAPP_URL } from "@formbricks/lib/constants"; -import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; +import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; import { getSurvey } from "@formbricks/lib/survey/service"; import { getStyling } from "@formbricks/lib/utils/styling"; @@ -9,12 +9,12 @@ export const getEmailTemplateHtml = async (surveyId: string) => { if (!survey) { throw new Error("Survey not found"); } - const product = await getProductByEnvironmentId(survey.environmentId); - if (!product) { - throw new Error("Product not found"); + const project = await getProjectByEnvironmentId(survey.environmentId); + if (!project) { + throw new Error("Project not found"); } - const styling = getStyling(product, survey); + const styling = getStyling(project, survey); const surveyUrl = WEBAPP_URL + "/s/" + survey.id; const html = await getPreviewEmailTemplateHtml(survey, surveyUrl, styling); const doctype = diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx index ec8c8706c9..cb63ffe66c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx @@ -5,7 +5,7 @@ import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surv import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils"; -import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; +import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; @@ -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 { getSurvey } from "@formbricks/lib/survey/service"; import { getUser } from "@formbricks/lib/user/service"; @@ -54,9 +54,9 @@ const Page = async (props) => { throw new Error(t("common.survey_not_found")); } - const product = await getProductByEnvironmentId(environment.id); - if (!product) { - throw new Error(t("common.product_not_found")); + const project = await getProjectByEnvironmentId(environment.id); + if (!project) { + throw new Error(t("common.project_not_found")); } const user = await getUser(session.user.id); @@ -73,8 +73,8 @@ const Page = async (props) => { const totalResponseCount = await getResponseCountBySurveyId(params.surveyId); 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; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts index 5e648b1dac..3603d0f0bb 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts @@ -2,7 +2,7 @@ import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; -import { getOrganizationIdFromSurveyId, getProductIdFromSurveyId } from "@/lib/utils/helper"; +import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper"; import { getSurveyFollowUpsPermission } from "@/modules/ee/license-check/lib/utils"; import { checkMultiLanguagePermission } from "@/modules/ee/multi-language-surveys/lib/actions"; import { z } from "zod"; @@ -33,9 +33,9 @@ export const getResponsesDownloadUrlAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "read", - productId: await getProductIdFromSurveyId(parsedInput.surveyId), + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), }, ], }); @@ -65,9 +65,9 @@ export const getSurveyFilterDataAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "read", - productId: await getProductIdFromSurveyId(parsedInput.surveyId), + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), }, ], }); @@ -114,8 +114,8 @@ export const updateSurveyAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", - productId: await getProductIdFromSurveyId(parsedInput.id), + type: "projectTeam", + projectId: await getProjectIdFromSurveyId(parsedInput.id), minPermission: "readWrite", }, ], diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/actions.ts index e9224eaa47..7d63e7939c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/actions.ts @@ -6,12 +6,12 @@ import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware" import { getOrganizationIdFromEnvironmentId, getOrganizationIdFromSurveyId, - getProductIdFromEnvironmentId, - getProductIdFromSurveyId, + getProjectIdFromEnvironmentId, + getProjectIdFromSurveyId, } from "@/lib/utils/helper"; import { getEnvironment } from "@/lib/utils/services"; import { z } from "zod"; -import { getUserProducts } from "@formbricks/lib/product/service"; +import { getUserProjects } from "@formbricks/lib/project/service"; import { copySurveyToOtherEnvironment, deleteSurvey } from "@formbricks/lib/survey/service"; import { generateSurveySingleUseId } from "@formbricks/lib/utils/singleUseSurveys"; import { ZId } from "@formbricks/types/common"; @@ -34,9 +34,9 @@ export const getSurveyAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "read", - productId: await getProductIdFromSurveyId(parsedInput.surveyId), + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), }, ], }); @@ -72,9 +72,9 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "readWrite", - productId: sourceEnvironment.productId, + projectId: sourceEnvironment.projectId, }, ], }); @@ -88,9 +88,9 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "readWrite", - productId: targetEnvironment.productId, + projectId: targetEnvironment.projectId, }, ], }); @@ -103,12 +103,12 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient ); }); -const ZGetProductsByEnvironmentIdAction = z.object({ +const ZGetProjectsByEnvironmentIdAction = z.object({ environmentId: ZId, }); -export const getProductsByEnvironmentIdAction = authenticatedActionClient - .schema(ZGetProductsByEnvironmentIdAction) +export const getProjectsByEnvironmentIdAction = authenticatedActionClient + .schema(ZGetProjectsByEnvironmentIdAction) .action(async ({ ctx, parsedInput }) => { const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId); await checkAuthorizationUpdated({ @@ -120,14 +120,14 @@ export const getProductsByEnvironmentIdAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "readWrite", - productId: await getProductIdFromEnvironmentId(parsedInput.environmentId), + projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId), }, ], }); - return await getUserProducts(ctx.user.id, organizationId); + return await getUserProjects(ctx.user.id, organizationId); }); const ZDeleteSurveyAction = z.object({ @@ -146,8 +146,8 @@ export const deleteSurveyAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", - productId: await getProductIdFromSurveyId(parsedInput.surveyId), + type: "projectTeam", + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), minPermission: "readWrite", }, ], @@ -173,8 +173,8 @@ export const generateSingleUseIdAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", - productId: await getProductIdFromSurveyId(parsedInput.surveyId), + type: "projectTeam", + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), minPermission: "readWrite", }, ], @@ -204,9 +204,9 @@ export const getSurveysAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "read", - productId: await getProductIdFromEnvironmentId(parsedInput.environmentId), + projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId), }, ], }); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/CopySurveyForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/components/CopySurveyForm.tsx index 2ee6984a7d..e651979152 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/components/CopySurveyForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/components/CopySurveyForm.tsx @@ -14,15 +14,15 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslations } from "next-intl"; import { useFieldArray, useForm } from "react-hook-form"; import toast from "react-hot-toast"; -import { TProduct } from "@formbricks/types/product"; +import { TProject } from "@formbricks/types/project"; export const CopySurveyForm = ({ - defaultProducts, + defaultProjects, survey, onCancel, setOpen, }: { - defaultProducts: TProduct[]; + defaultProjects: TProject[]; survey: TSurvey; onCancel: () => void; setOpen: (value: boolean) => void; @@ -31,24 +31,24 @@ export const CopySurveyForm = ({ const form = useForm({ resolver: zodResolver(ZSurveyCopyFormValidation), defaultValues: { - products: defaultProducts.map((product) => ({ - product: product.id, + projects: defaultProjects.map((project) => ({ + project: project.id, environments: [], })), }, }); const formFields = useFieldArray({ - name: "products", + name: "projects", control: form.control, }); const onSubmit = async (data: TSurveyCopyFormData) => { - const filteredData = data.products.filter((product) => product.environments.length > 0); + const filteredData = data.projects.filter((project) => project.environments.length > 0); try { - filteredData.map(async (product) => { - product.environments.map(async (environment) => { + filteredData.map(async (project) => { + project.environments.map(async (environment) => { await copySurveyToOtherEnvironmentAction({ environmentId: survey.environmentId, surveyId: survey.id, @@ -70,23 +70,23 @@ export const CopySurveyForm = ({ onSubmit={form.handleSubmit(onSubmit)} className="relative flex h-full w-full flex-col gap-8 overflow-y-auto bg-white p-4">
    - {formFields.fields.map((field, productIndex) => { - const product = defaultProducts.find((product) => product.id === field.product); + {formFields.fields.map((field, projectIndex) => { + const project = defaultProjects.find((project) => project.id === field.project); return ( -
    +
    -

    {product?.name}

    +

    {project?.name}

    - {product?.environments.map((environment) => { + {project?.environments.map((environment) => { return ( { return ( diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyCopyOptions.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyCopyOptions.tsx index 47cfca9248..0b9f1f5c52 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyCopyOptions.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyCopyOptions.tsx @@ -1,12 +1,12 @@ "use client"; -import { getProductsByEnvironmentIdAction } from "@/app/(app)/environments/[environmentId]/surveys/actions"; +import { getProjectsByEnvironmentIdAction } from "@/app/(app)/environments/[environmentId]/surveys/actions"; import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { Loader2 } from "lucide-react"; import { useEffect, useState } from "react"; import toast from "react-hot-toast"; -import { TProduct } from "@formbricks/types/product"; +import { TProject } from "@formbricks/types/project"; import { CopySurveyForm } from "./CopySurveyForm"; interface SurveyCopyOptionsProps { @@ -17,26 +17,26 @@ interface SurveyCopyOptionsProps { } const SurveyCopyOptions = ({ environmentId, survey, onCancel, setOpen }: SurveyCopyOptionsProps) => { - const [products, setProducts] = useState([]); - const [productLoading, setProductLoading] = useState(true); + const [projects, setProjects] = useState([]); + const [projectLoading, setProjectLoading] = useState(true); useEffect(() => { - const fetchProducts = async () => { - const getProductsByEnvironmentIdResponse = await getProductsByEnvironmentIdAction({ environmentId }); - if (getProductsByEnvironmentIdResponse?.data) { - setProducts(getProductsByEnvironmentIdResponse?.data); + const fetchProjects = async () => { + const getProjectsByEnvironmentIdResponse = await getProjectsByEnvironmentIdAction({ environmentId }); + if (getProjectsByEnvironmentIdResponse?.data) { + setProjects(getProjectsByEnvironmentIdResponse?.data); } else { - const errorMessage = getFormattedErrorMessage(getProductsByEnvironmentIdResponse); + const errorMessage = getFormattedErrorMessage(getProjectsByEnvironmentIdResponse); toast.error(errorMessage); } - setProductLoading(false); + setProjectLoading(false); }; - fetchProducts(); + fetchProjects(); }, [environmentId]); - if (productLoading) { + if (projectLoading) { return (
    @@ -44,7 +44,7 @@ const SurveyCopyOptions = ({ environmentId, survey, onCancel, setOpen }: SurveyC ); } - return ; + return ; }; export default SurveyCopyOptions; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyFilters.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyFilters.tsx index 23872d7265..afcc0c2c18 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyFilters.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyFilters.tsx @@ -13,14 +13,14 @@ import { ChevronDownIcon, X } from "lucide-react"; import { useTranslations } from "next-intl"; import { useState } from "react"; import { useDebounce } from "react-use"; -import { TProductConfigChannel } from "@formbricks/types/product"; +import { TProjectConfigChannel } from "@formbricks/types/project"; import { TFilterOption, TSortOption, TSurveyFilters } from "@formbricks/types/surveys/types"; import { SurveyFilterDropdown } from "./SurveyFilterDropdown"; interface SurveyFilterProps { surveyFilters: TSurveyFilters; setSurveyFilters: React.Dispatch>; - currentProductChannel: TProductConfigChannel; + currentProjectChannel: TProjectConfigChannel; } const creatorOptions: TFilterOption[] = [ @@ -57,7 +57,7 @@ const sortOptions: TSortOption[] = [ export const SurveyFilters = ({ surveyFilters, setSurveyFilters, - currentProductChannel, + currentProjectChannel, }: SurveyFilterProps) => { const { createdBy, sortBy, status, type } = surveyFilters; const [name, setName] = useState(""); @@ -146,7 +146,7 @@ export const SurveyFilters = ({ toggleDropdown={toggleDropdown} />
    - {currentProductChannel !== "link" && ( + {currentProjectChannel !== "link" && (
    { const [surveys, setSurveys] = useState([]); @@ -138,7 +138,7 @@ export const SurveysList = ({ {surveys.length > 0 ? (
    diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx index 383937923f..e93d0ec723 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx @@ -1,6 +1,6 @@ import { SurveysList } from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyList"; 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 { TemplateList } from "@/modules/surveys/components/TemplateList"; import { Button } from "@/modules/ui/components/button"; @@ -16,7 +16,7 @@ import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/ser 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 { getSurveyCount } from "@formbricks/lib/survey/service"; import { getUser } from "@formbricks/lib/user/service"; import { findMatchingLocale } from "@formbricks/lib/utils/locale"; @@ -39,7 +39,7 @@ const Page = async (props: SurveyTemplateProps) => { const searchParams = await props.searchParams; const params = await props.params; const session = await getServerSession(authOptions); - const product = await getProductByEnvironmentId(params.environmentId); + const project = await getProjectByEnvironmentId(params.environmentId); const organization = await getOrganizationByEnvironmentId(params.environmentId); const t = await getTranslations(); if (!session) { @@ -51,21 +51,21 @@ const Page = async (props: SurveyTemplateProps) => { 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 (!organization) { throw new Error(t("common.organization_not_found")); } - const prefilledFilters = [product?.config.channel, product.config.industry, searchParams.role ?? null]; + const prefilledFilters = [project?.config.channel, project.config.industry, searchParams.role ?? null]; 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 { hasReadAccess } = getTeamPermissionFlags(productPermission); + const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id); + const { hasReadAccess } = getTeamPermissionFlags(projectPermission); const isReadOnly = isMember && hasReadAccess; @@ -80,10 +80,10 @@ const Page = async (props: SurveyTemplateProps) => { const surveyCount = await getSurveyCount(params.environmentId); - const environments = await getEnvironments(product.id); + const environments = await getEnvironments(project.id); const otherEnvironment = environments.find((e) => e.type !== environment.type)!; - const currentProductChannel = product.config.channel ?? null; + const currentProjectChannel = project.config.channel ?? null; const locale = await findMatchingLocale(); const CreateSurveyButton = () => { return ( @@ -105,7 +105,7 @@ const Page = async (props: SurveyTemplateProps) => { WEBAPP_URL={WEBAPP_URL} userId={session.user.id} surveysPerPage={SURVEYS_PER_PAGE} - currentProductChannel={currentProductChannel} + currentProjectChannel={currentProjectChannel} locale={locale} /> @@ -126,7 +126,7 @@ const Page = async (props: SurveyTemplateProps) => { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/types/surveys.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/types/surveys.ts index 6fd7b65658..b996248670 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/types/surveys.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/types/surveys.ts @@ -26,9 +26,9 @@ export const ZSurvey = z.object({ export type TSurvey = z.infer; export const ZSurveyCopyFormValidation = z.object({ - products: z.array( + projects: z.array( z.object({ - product: z.string(), + project: z.string(), environments: z.array(z.string()), }) ), diff --git a/apps/web/app/(redirects)/organizations/[organizationId]/route.ts b/apps/web/app/(redirects)/organizations/[organizationId]/route.ts index d53215e76f..6d9620b42c 100644 --- a/apps/web/app/(redirects)/organizations/[organizationId]/route.ts +++ b/apps/web/app/(redirects)/organizations/[organizationId]/route.ts @@ -6,7 +6,7 @@ import { notFound } from "next/navigation"; import { getEnvironments } from "@formbricks/lib/environment/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; -import { getUserProducts } from "@formbricks/lib/product/service"; +import { getUserProjects } from "@formbricks/lib/project/service"; import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors"; export const GET = async (_: Request, context: { params: Promise<{ organizationId: string }> }) => { @@ -22,14 +22,14 @@ export const GET = async (_: Request, context: { params: Promise<{ organizationI const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organizationId); const { isBilling } = getAccessFlags(currentUserMembership?.role); - // redirect to first product's production environment - const products = await getUserProducts(session.user.id, organizationId); - if (products.length === 0) { + // redirect to first project's production environment + const projects = await getUserProjects(session.user.id, organizationId); + if (projects.length === 0) { return redirect(`/organizations/${organizationId}/landing`); } - const firstProduct = products[0]; - const environments = await getEnvironments(firstProduct.id); + const firstProject = projects[0]; + const environments = await getEnvironments(firstProject.id); const prodEnvironment = environments.find((e) => e.type === "production"); if (!prodEnvironment) return notFound(); diff --git a/apps/web/app/(redirects)/products/[productId]/route.ts b/apps/web/app/(redirects)/projects/[projectId]/route.ts similarity index 70% rename from apps/web/app/(redirects)/products/[productId]/route.ts rename to apps/web/app/(redirects)/projects/[projectId]/route.ts index 761c40fe71..4c28c35fff 100644 --- a/apps/web/app/(redirects)/products/[productId]/route.ts +++ b/apps/web/app/(redirects)/projects/[projectId]/route.ts @@ -3,22 +3,22 @@ import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { notFound, redirect } from "next/navigation"; import { getEnvironments } from "@formbricks/lib/environment/service"; -import { getProduct } from "@formbricks/lib/product/service"; +import { getProject } from "@formbricks/lib/project/service"; import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors"; -export const GET = async (_: Request, context: { params: Promise<{ productId: string }> }) => { +export const GET = async (_: Request, context: { params: Promise<{ projectId: string }> }) => { const params = await context?.params; - const productId = params.productId; - if (!productId) return notFound(); + const projectId = params.projectId; + if (!projectId) return notFound(); // check auth const session = await getServerSession(authOptions); if (!session) throw new AuthenticationError("Not authenticated"); - const product = await getProduct(productId); - if (!product) return notFound(); - const hasAccess = await hasOrganizationAccess(session.user, product.organizationId); + const project = await getProject(projectId); + if (!project) return notFound(); + const hasAccess = await hasOrganizationAccess(session.user, project.organizationId); if (!hasAccess) throw new AuthorizationError("Unauthorized"); - // redirect to product's production environment - const environments = await getEnvironments(product.id); + // redirect to project's production environment + const environments = await getEnvironments(project.id); const prodEnvironment = environments.find((e) => e.type === "production"); if (!prodEnvironment) return notFound(); redirect(`/environments/${prodEnvironment.id}/`); diff --git a/apps/web/app/api/(internal)/pipeline/route.ts b/apps/web/app/api/(internal)/pipeline/route.ts index 1645ef76cb..c2a63432bb 100644 --- a/apps/web/app/api/(internal)/pipeline/route.ts +++ b/apps/web/app/api/(internal)/pipeline/route.ts @@ -117,7 +117,7 @@ export const POST = async (request: Request) => { memberships: { some: { organization: { - products: { + projects: { some: { environments: { some: { id: environmentId }, @@ -141,9 +141,9 @@ export const POST = async (request: Request) => { teamUsers: { some: { team: { - productTeams: { + projectTeams: { some: { - product: { + project: { environments: { some: { id: environmentId, diff --git a/apps/web/app/api/cron/weekly-summary/lib/notificationResponse.ts b/apps/web/app/api/cron/weekly-summary/lib/notificationResponse.ts index 9841b81955..79c0488a23 100644 --- a/apps/web/app/api/cron/weekly-summary/lib/notificationResponse.ts +++ b/apps/web/app/api/cron/weekly-summary/lib/notificationResponse.ts @@ -12,7 +12,7 @@ import { export const getNotificationResponse = ( environment: TWeeklySummaryEnvironmentData, - productName: string + projectName: string ): TWeeklySummaryNotificationResponse => { const insights = { totalCompletedResponses: 0, @@ -73,7 +73,7 @@ export const getNotificationResponse = ( environmentId: environment.id, currentDate: new Date(), lastWeekDate, - productName: productName, + projectName: projectName, surveys, insights, }; diff --git a/apps/web/app/api/cron/weekly-summary/lib/product.ts b/apps/web/app/api/cron/weekly-summary/lib/project.ts similarity index 92% rename from apps/web/app/api/cron/weekly-summary/lib/product.ts rename to apps/web/app/api/cron/weekly-summary/lib/project.ts index 569bf43fb9..a7fa57e442 100644 --- a/apps/web/app/api/cron/weekly-summary/lib/product.ts +++ b/apps/web/app/api/cron/weekly-summary/lib/project.ts @@ -1,13 +1,13 @@ import { prisma } from "@formbricks/database"; -import { TWeeklySummaryProductData } from "@formbricks/types/weekly-summary"; +import { TWeeklySummaryProjectData } from "@formbricks/types/weekly-summary"; -export const getProductsByOrganizationId = async ( +export const getProjectsByOrganizationId = async ( organizationId: string -): Promise => { +): Promise => { const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); - return await prisma.product.findMany({ + return await prisma.project.findMany({ where: { organizationId: organizationId, }, diff --git a/apps/web/app/api/cron/weekly-summary/route.ts b/apps/web/app/api/cron/weekly-summary/route.ts index a56a13dccc..11ecdbe1cd 100644 --- a/apps/web/app/api/cron/weekly-summary/route.ts +++ b/apps/web/app/api/cron/weekly-summary/route.ts @@ -5,7 +5,7 @@ import { CRON_SECRET } from "@formbricks/lib/constants"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { getNotificationResponse } from "./lib/notificationResponse"; import { getOrganizationIds } from "./lib/organization"; -import { getProductsByOrganizationId } from "./lib/product"; +import { getProjectsByOrganizationId } from "./lib/project"; const BATCH_SIZE = 500; @@ -24,28 +24,28 @@ export const POST = async (): Promise => { // Paginate through organizations for (let i = 0; i < organizationIds.length; i += BATCH_SIZE) { const batchedOrganizationIds = organizationIds.slice(i, i + BATCH_SIZE); - // Fetch products for batched organizations asynchronously - const batchedProductsPromises = batchedOrganizationIds.map((organizationId) => - getProductsByOrganizationId(organizationId) + // Fetch projects for batched organizations asynchronously + const batchedProjectsPromises = batchedOrganizationIds.map((organizationId) => + getProjectsByOrganizationId(organizationId) ); - const batchedProducts = await Promise.all(batchedProductsPromises); - for (const products of batchedProducts) { - for (const product of products) { - const organizationMembers = product.organization.memberships; + const batchedProjects = await Promise.all(batchedProjectsPromises); + for (const projects of batchedProjects) { + for (const project of projects) { + const organizationMembers = project.organization.memberships; const organizationMembersWithNotificationEnabled = organizationMembers.filter( (member) => member.user.notificationSettings?.weeklySummary && - member.user.notificationSettings.weeklySummary[product.id] + member.user.notificationSettings.weeklySummary[project.id] ); if (organizationMembersWithNotificationEnabled.length === 0) continue; - const notificationResponse = getNotificationResponse(product.environments[0], product.name); + const notificationResponse = getNotificationResponse(project.environments[0], project.name); if (notificationResponse.insights.numLiveSurvey === 0) { for (const organizationMember of organizationMembersWithNotificationEnabled) { - if (await hasUserEnvironmentAccess(organizationMember.user.id, product.environments[0].id)) { + if (await hasUserEnvironmentAccess(organizationMember.user.id, project.environments[0].id)) { emailSendingPromises.push( sendNoLiveSurveyNotificationEmail( organizationMember.user.email, @@ -59,7 +59,7 @@ export const POST = async (): Promise => { } for (const organizationMember of organizationMembersWithNotificationEnabled) { - if (await hasUserEnvironmentAccess(organizationMember.user.id, product.environments[0].id)) { + if (await hasUserEnvironmentAccess(organizationMember.user.id, project.environments[0].id)) { emailSendingPromises.push( sendWeeklySummaryNotificationEmail( organizationMember.user.email, diff --git a/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts b/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts index eb7dcecf60..0fb14684cf 100644 --- a/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/app/sync/[userId]/route.ts @@ -15,7 +15,7 @@ import { capturePosthogEnvironmentEvent, sendPlanLimitsReachedEventToPosthogWeekly, } from "@formbricks/lib/posthogServer"; -import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; +import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants"; import { getSyncSurveys } from "@formbricks/lib/survey/service"; import { TJsAppStateSync, ZJsPeopleUserIdInput } from "@formbricks/types/js"; @@ -58,10 +58,10 @@ export const GET = async ( throw new Error("Environment does not exist"); } - const product = await getProductByEnvironmentId(environmentId); + const project = await getProjectByEnvironmentId(environmentId); - if (!product) { - throw new Error("Product not found"); + if (!project) { + throw new Error("Project not found"); } if (!environment.appSetupCompleted) { @@ -91,7 +91,13 @@ export const GET = async ( try { await sendPlanLimitsReachedEventToPosthogWeekly(environmentId, { plan: organization.billing.plan, - limits: { monthly: { responses: monthlyResponseLimit, miu: null } }, + limits: { + projects: null, + monthly: { + responses: monthlyResponseLimit, + miu: null, + }, + }, }); } catch (error) { console.error(`Error sending plan limits reached event to Posthog: ${error}`); @@ -110,15 +116,15 @@ export const GET = async ( getActionClasses(environmentId), ]); - if (!product) { - throw new Error("Product not found"); + if (!project) { + throw new Error("Project not found"); } - const updatedProduct: any = { - ...product, - brandColor: product.styling.brandColor?.light ?? COLOR_DEFAULTS.brandColor, - ...(product.styling.highlightBorderColor?.light && { - highlightBorderColor: product.styling.highlightBorderColor.light, + const updatedProject: any = { + ...project, + brandColor: project.styling.brandColor?.light ?? COLOR_DEFAULTS.brandColor, + ...(project.styling.highlightBorderColor?.light && { + highlightBorderColor: project.styling.highlightBorderColor.light, }), }; const attributes = await getAttributes(person.id); @@ -135,7 +141,7 @@ export const GET = async ( : [], actionClasses, language, - product: updatedProduct, + project: updatedProject, }; return responses.successResponse({ ...state }, true); diff --git a/apps/web/app/api/v1/client/[environmentId]/environment/lib/environmentState.ts b/apps/web/app/api/v1/client/[environmentId]/environment/lib/environmentState.ts index ca99392067..0c9bf2ff7c 100644 --- a/apps/web/app/api/v1/client/[environmentId]/environment/lib/environmentState.ts +++ b/apps/web/app/api/v1/client/[environmentId]/environment/lib/environmentState.ts @@ -14,8 +14,8 @@ import { capturePosthogEnvironmentEvent, sendPlanLimitsReachedEventToPosthogWeekly, } from "@formbricks/lib/posthogServer"; -import { productCache } from "@formbricks/lib/product/cache"; -import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; +import { projectCache } from "@formbricks/lib/project/cache"; +import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; import { surveyCache } from "@formbricks/lib/survey/cache"; import { getSurveys } from "@formbricks/lib/survey/service"; import { ResourceNotFoundError } from "@formbricks/types/errors"; @@ -33,10 +33,10 @@ export const getEnvironmentState = async ( cache( async () => { let revalidateEnvironment = false; - const [environment, organization, product] = await Promise.all([ + const [environment, organization, project] = await Promise.all([ getEnvironment(environmentId), getOrganizationByEnvironmentId(environmentId), - getProductByEnvironmentId(environmentId), + getProjectByEnvironmentId(environmentId), ]); if (!environment) { @@ -47,8 +47,8 @@ export const getEnvironmentState = async ( throw new ResourceNotFoundError("organization", null); } - if (!product) { - throw new ResourceNotFoundError("product", null); + if (!project) { + throw new ResourceNotFoundError("project", null); } if (!environment.appSetupCompleted) { @@ -81,8 +81,9 @@ export const getEnvironmentState = async ( await sendPlanLimitsReachedEventToPosthogWeekly(environmentId, { plan: organization.billing.plan, limits: { + projects: null, monthly: { - miu: organization.billing.limits.monthly.miu, + miu: null, responses: organization.billing.limits.monthly.responses, }, }, @@ -104,7 +105,7 @@ export const getEnvironmentState = async ( const state: TJsEnvironmentState["data"] = { surveys: !isMonthlyResponsesLimitReached ? filteredSurveys : [], actionClasses, - product, + project: project, }; return { @@ -118,7 +119,7 @@ export const getEnvironmentState = async ( tags: [ environmentCache.tag.byId(environmentId), organizationCache.tag.byEnvironmentId(environmentId), - productCache.tag.byEnvironmentId(environmentId), + projectCache.tag.byEnvironmentId(environmentId), surveyCache.tag.byEnvironmentId(environmentId), actionClassCache.tag.byEnvironmentId(environmentId), ], diff --git a/apps/web/app/api/v1/client/[environmentId]/environment/route.ts b/apps/web/app/api/v1/client/[environmentId]/environment/route.ts index ba652cfb19..65da746d64 100644 --- a/apps/web/app/api/v1/client/[environmentId]/environment/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/environment/route.ts @@ -39,7 +39,7 @@ export const GET = async ( if (environmentState.revalidateEnvironment) { environmentCache.revalidate({ id: inputValidation.data.environmentId, - productId: environmentState.state.product.id, + projectId: environmentState.state.project.id, }); } diff --git a/apps/web/app/api/v1/management/me/route.ts b/apps/web/app/api/v1/management/me/route.ts index 3cb18af099..a12c337a22 100644 --- a/apps/web/app/api/v1/management/me/route.ts +++ b/apps/web/app/api/v1/management/me/route.ts @@ -17,7 +17,7 @@ export const GET = async () => { createdAt: true, updatedAt: true, type: true, - product: { + project: { select: { id: true, name: true, diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 91bed7907d..486f76e658 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -50,7 +50,7 @@ const Page = async () => { if (!environmentId) { console.error(t("common.failed_to_get_first_environment_of_user")); if (isOwner || isManager) { - return redirect(`/organizations/${userOrganizations[0].id}/products/new/mode`); + return redirect(`/organizations/${userOrganizations[0].id}/projects/new/mode`); } else { return redirect(`/organizations/${userOrganizations[0].id}/landing`); } diff --git a/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx b/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx index 3369a72f54..1622b5e63c 100644 --- a/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx +++ b/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx @@ -13,7 +13,7 @@ import { ResponseQueue } from "@formbricks/lib/responseQueue"; import { SurveyState } from "@formbricks/lib/surveyState"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; import { TJsFileUploadParams } from "@formbricks/types/js"; -import { TProduct } from "@formbricks/types/product"; +import { TProject } from "@formbricks/types/project"; import { TResponse, TResponseData, @@ -30,7 +30,7 @@ let setResponseData = (_: TResponseData) => {}; interface LinkSurveyProps { survey: TSurvey; - product: TProduct; + project: TProject; userId?: string; emailVerificationStatus?: string; singleUseId?: string; @@ -50,7 +50,7 @@ interface LinkSurveyProps { export const LinkSurvey = ({ survey, - product, + project, userId, emailVerificationStatus, singleUseId, @@ -173,7 +173,7 @@ export const LinkSurvey = ({ survey={survey} isErrorComponent={true} languageCode={languageCode} - styling={product.styling} + styling={project.styling} attributeClasses={attributeClasses} locale={locale} /> @@ -185,7 +185,7 @@ export const LinkSurvey = ({ singleUseId={suId ?? ""} survey={survey} languageCode={languageCode} - styling={product.styling} + styling={project.styling} attributeClasses={attributeClasses} locale={locale} /> @@ -193,23 +193,23 @@ export const LinkSurvey = ({ } const determineStyling = () => { - // allow style overwrite is disabled from the product - if (!product.styling.allowStyleOverwrite) { - return product.styling; + // allow style overwrite is disabled from the project + if (!project.styling.allowStyleOverwrite) { + return project.styling; } - // allow style overwrite is enabled from the product - if (product.styling.allowStyleOverwrite) { + // allow style overwrite is enabled from the project + if (project.styling.allowStyleOverwrite) { // survey style overwrite is disabled if (!survey.styling?.overwriteThemeStyling) { - return product.styling; + return project.styling; } // survey style overwrite is enabled return survey.styling; } - return product.styling; + return project.styling; }; const handleResetSurvey = () => { @@ -219,7 +219,7 @@ export const LinkSurvey = ({ return ( void) => { setIsError = f; diff --git a/apps/web/app/s/[surveyId]/components/LinkSurveyWrapper.tsx b/apps/web/app/s/[surveyId]/components/LinkSurveyWrapper.tsx index 0966b8b435..305e31cb32 100644 --- a/apps/web/app/s/[surveyId]/components/LinkSurveyWrapper.tsx +++ b/apps/web/app/s/[surveyId]/components/LinkSurveyWrapper.tsx @@ -5,16 +5,16 @@ import { MediaBackground } from "@/modules/ui/components/media-background"; import { ResetProgressButton } from "@/modules/ui/components/reset-progress-button"; import { type JSX, useState } from "react"; import { cn } from "@formbricks/lib/cn"; -import { TProduct, TProductStyling } from "@formbricks/types/product"; +import { TProject, TProjectStyling } from "@formbricks/types/project"; import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys/types"; interface LinkSurveyWrapperProps { children: JSX.Element; - product: TProduct; + project: TProject; survey: TSurvey; isPreview: boolean; isEmbed: boolean; - determineStyling: () => TSurveyStyling | TProductStyling; + determineStyling: () => TSurveyStyling | TProjectStyling; handleResetSurvey: () => void; IMPRINT_URL?: string; PRIVACY_URL?: string; @@ -24,7 +24,7 @@ interface LinkSurveyWrapperProps { export const LinkSurveyWrapper = ({ children, - product, + project, survey, isPreview, isEmbed, @@ -60,9 +60,9 @@ export const LinkSurveyWrapper = ({ return (
    - +
    - {!styling.isLogoHidden && product.logo?.url && } + {!styling.isLogoHidden && project.logo?.url && }
    {isPreview && (
    diff --git a/apps/web/app/s/[surveyId]/components/PinScreen.tsx b/apps/web/app/s/[surveyId]/components/PinScreen.tsx index 144bcb41d6..fdd3803e54 100644 --- a/apps/web/app/s/[surveyId]/components/PinScreen.tsx +++ b/apps/web/app/s/[surveyId]/components/PinScreen.tsx @@ -9,13 +9,13 @@ import { useTranslations } from "next-intl"; import { useCallback, useEffect, useState } from "react"; import { cn } from "@formbricks/lib/cn"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; -import { TProduct } from "@formbricks/types/product"; +import { TProject } from "@formbricks/types/project"; import { TResponse } from "@formbricks/types/responses"; import { TSurvey } from "@formbricks/types/surveys/types"; interface PinScreenProps { surveyId: string; - product: TProduct; + project: TProject; userId?: string; emailVerificationStatus?: string; singleUseId?: string; @@ -35,7 +35,7 @@ interface PinScreenProps { export const PinScreen = (props: PinScreenProps) => { const { surveyId, - product, + project, webAppUrl, emailVerificationStatus, userId, @@ -123,7 +123,7 @@ export const PinScreen = (props: PinScreenProps) => { return ( { } } - // get product and person - const product = await getProductByEnvironmentId(survey.environmentId); - if (!product) { - throw new Error("Product not found"); + // get project and person + const project = await getProjectByEnvironmentId(survey.environmentId); + if (!project) { + throw new Error("Project not found"); } const attributeClasses = await getAttributeClasses(survey.environmentId); @@ -165,7 +165,7 @@ const Page = async (props: LinkSurveyPageProps) => { return ( { return survey ? ( { throw new Error(t("common.survey_not_found")); } const environmentId = survey.environmentId; - const [environment, product, tags] = await Promise.all([ + const [environment, project, tags] = await Promise.all([ getEnvironment(environmentId), - getProductByEnvironmentId(environmentId), + getProjectByEnvironmentId(environmentId), getTagsByEnvironmentId(environmentId), ]); if (!environment) { 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")); } const totalResponseCount = await getResponseCountBySurveyId(surveyId); diff --git a/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx b/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx index c68c65c48a..0759855789 100644 --- a/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx +++ b/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx @@ -7,7 +7,7 @@ import { notFound } from "next/navigation"; import { getAttributeClasses } from "@formbricks/lib/attributeClass/service"; import { DEFAULT_LOCALE, 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"; import { getResponseCountBySurveyId } from "@formbricks/lib/response/service"; import { getSurvey, getSurveyIdByResultShareKey } from "@formbricks/lib/survey/service"; @@ -25,18 +25,18 @@ const Page = async (props) => { throw new Error(t("common.survey_not_found")); } const environmentId = survey.environmentId; - const [environment, attributeClasses, product] = await Promise.all([ + const [environment, attributeClasses, project] = await Promise.all([ getEnvironment(environmentId), getAttributeClasses(environmentId), - getProductByEnvironmentId(environmentId), + getProjectByEnvironmentId(environmentId), ]); if (!environment) { 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")); } const totalResponseCount = await getResponseCountBySurveyId(surveyId); diff --git a/apps/web/lib/cache/team.ts b/apps/web/lib/cache/team.ts index 6750dfd40d..b80cd7f1c7 100644 --- a/apps/web/lib/cache/team.ts +++ b/apps/web/lib/cache/team.ts @@ -3,7 +3,7 @@ import { revalidateTag } from "next/cache"; interface RevalidateProps { id?: string; userId?: string; - productId?: string; + projectId?: string; organizationId?: string; } @@ -12,8 +12,8 @@ export const teamCache = { byId(id: string) { return `team-${id}`; }, - byProductId(productId: string) { - return `product-teams-${productId}`; + byProjectId(projectId: string) { + return `project-teams-${projectId}`; }, byUserId(userId: string) { return `user-${userId}-teams`; @@ -22,12 +22,12 @@ export const teamCache = { return `organization-${organizationId}-teams`; }, }, - revalidate({ id, productId, userId, organizationId }: RevalidateProps): void { + revalidate({ id, projectId, userId, organizationId }: RevalidateProps): void { if (id) { revalidateTag(this.tag.byId(id)); } - if (productId) { - revalidateTag(this.tag.byProductId(productId)); + if (projectId) { + revalidateTag(this.tag.byProjectId(projectId)); } if (userId) { revalidateTag(this.tag.byUserId(userId)); diff --git a/apps/web/lib/utils/action-client-middleware.ts b/apps/web/lib/utils/action-client-middleware.ts index 4c755b1187..588d114118 100644 --- a/apps/web/lib/utils/action-client-middleware.ts +++ b/apps/web/lib/utils/action-client-middleware.ts @@ -1,5 +1,5 @@ -import { getProductPermissionByUserId, getTeamRoleByTeamIdUserId } from "@/modules/ee/teams/lib/roles"; -import { TTeamPermission } from "@/modules/ee/teams/product-teams/types/teams"; +import { getProjectPermissionByUserId, getTeamRoleByTeamIdUserId } from "@/modules/ee/teams/lib/roles"; +import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams"; import { TTeamRole } from "@/modules/ee/teams/team-list/types/teams"; import { returnValidationErrors } from "next-safe-action"; import { ZodIssue, z } from "zod"; @@ -20,21 +20,21 @@ const formatErrors = (issues: ZodIssue[]): Record export type TAccess = | { - type: "organization"; - schema?: z.ZodObject; - data?: z.ZodObject["_output"]; - roles: TOrganizationRole[]; - } + type: "organization"; + schema?: z.ZodObject; + data?: z.ZodObject["_output"]; + roles: TOrganizationRole[]; + } | { - type: "productTeam"; - minPermission?: TTeamPermission; - productId: string; - } + type: "projectTeam"; + minPermission?: TTeamPermission; + projectId: string; + } | { - type: "team"; - minPermission?: TTeamRole; - teamId: string; - }; + type: "team"; + minPermission?: TTeamRole; + teamId: string; + }; const teamPermissionWeight = { read: 1, @@ -73,12 +73,12 @@ export const checkAuthorizationUpdated = async ({ return true; } } else { - if (accessItem.type === "productTeam") { - const productPermission = await getProductPermissionByUserId(userId, accessItem.productId); + if (accessItem.type === "projectTeam") { + const projectPermission = await getProjectPermissionByUserId(userId, accessItem.projectId); if ( - !productPermission || + !projectPermission || (accessItem.minPermission !== undefined && - teamPermissionWeight[productPermission] < teamPermissionWeight[accessItem.minPermission]) + teamPermissionWeight[projectPermission] < teamPermissionWeight[accessItem.minPermission]) ) { continue; } diff --git a/apps/web/lib/utils/helper.ts b/apps/web/lib/utils/helper.ts index 923bf0c53c..a41c57f32a 100644 --- a/apps/web/lib/utils/helper.ts +++ b/apps/web/lib/utils/helper.ts @@ -9,7 +9,7 @@ import { getInvite, getLanguage, getPerson, - getProduct, + getProject, getResponse, getResponseNote, getSegment, @@ -42,13 +42,13 @@ export const getFormattedErrorMessage = (result) => { * GET organization ID from RESOURCE ID */ -export const getOrganizationIdFromProductId = async (productId: string) => { - const product = await getProduct(productId); - if (!product) { - throw new ResourceNotFoundError("product", productId); +export const getOrganizationIdFromProjectId = async (projectId: string) => { + const project = await getProject(projectId); + if (!project) { + throw new ResourceNotFoundError("project", projectId); } - return product.organizationId; + return project.organizationId; }; export const getOrganizationIdFromEnvironmentId = async (environmentId: string) => { @@ -57,7 +57,7 @@ export const getOrganizationIdFromEnvironmentId = async (environmentId: string) throw new ResourceNotFoundError("environment", environmentId); } - return await getOrganizationIdFromProductId(environment.productId); + return await getOrganizationIdFromProjectId(environment.projectId); }; export const getOrganizationIdFromSurveyId = async (surveyId: string) => { @@ -174,7 +174,7 @@ export const getOrganizationIdFromLanguageId = async (languageId: string) => { throw new ResourceNotFoundError("language", languageId); } - return await getOrganizationIdFromProductId(language.productId); + return await getOrganizationIdFromProjectId(language.projectId); }; export const getOrganizationIdFromTeamId = async (teamId: string) => { @@ -204,122 +204,122 @@ export const getOrganizationIdFromDocumentId = async (documentId: string) => { return await getOrganizationIdFromEnvironmentId(document.environmentId); }; -// product id helpers -export const getProductIdFromEnvironmentId = async (environmentId: string) => { +// project id helpers +export const getProjectIdFromEnvironmentId = async (environmentId: string) => { const environment = await getEnvironment(environmentId); if (!environment) { throw new ResourceNotFoundError("environment", environmentId); } - return environment.productId; + return environment.projectId; }; -export const getProductIdFromSurveyId = async (surveyId: string) => { +export const getProjectIdFromSurveyId = async (surveyId: string) => { const survey = await getSurvey(surveyId); if (!survey) { throw new ResourceNotFoundError("survey", surveyId); } - return await getProductIdFromEnvironmentId(survey.environmentId); + return await getProjectIdFromEnvironmentId(survey.environmentId); }; -export const getProductIdFromInsightId = async (insightId: string) => { +export const getProjectIdFromInsightId = async (insightId: string) => { const insight = await getInsight(insightId); if (!insight) { throw new ResourceNotFoundError("insight", insightId); } - return await getProductIdFromEnvironmentId(insight.environmentId); + return await getProjectIdFromEnvironmentId(insight.environmentId); }; -export const getProductIdFromSegmentId = async (segmentId: string) => { +export const getProjectIdFromSegmentId = async (segmentId: string) => { const segment = await getSegment(segmentId); if (!segment) { throw new ResourceNotFoundError("segment", segmentId); } - return await getProductIdFromEnvironmentId(segment.environmentId); + return await getProjectIdFromEnvironmentId(segment.environmentId); }; -export const getProductIdFromApiKeyId = async (apiKeyId: string) => { +export const getProjectIdFromApiKeyId = async (apiKeyId: string) => { const apiKey = await getApiKey(apiKeyId); if (!apiKey) { throw new ResourceNotFoundError("apiKey", apiKeyId); } - return await getProductIdFromEnvironmentId(apiKey.environmentId); + return await getProjectIdFromEnvironmentId(apiKey.environmentId); }; -export const getProductIdFromActionClassId = async (actionClassId: string) => { +export const getProjectIdFromActionClassId = async (actionClassId: string) => { const actionClass = await getActionClass(actionClassId); if (!actionClass) { throw new ResourceNotFoundError("actionClass", actionClassId); } - return await getProductIdFromEnvironmentId(actionClass.environmentId); + return await getProjectIdFromEnvironmentId(actionClass.environmentId); }; -export const getProductIdFromTagId = async (tagId: string) => { +export const getProjectIdFromTagId = async (tagId: string) => { const tag = await getTag(tagId); if (!tag) { throw new ResourceNotFoundError("tag", tagId); } - return await getProductIdFromEnvironmentId(tag.environmentId); + return await getProjectIdFromEnvironmentId(tag.environmentId); }; -export const getProductIdFromLanguageId = async (languageId: string) => { +export const getProjectIdFromLanguageId = async (languageId: string) => { const language = await getLanguage(languageId); if (!language) { throw new ResourceNotFoundError("language", languageId); } - return language.productId; + return language.projectId; }; -export const getProductIdFromResponseId = async (responseId: string) => { +export const getProjectIdFromResponseId = async (responseId: string) => { const response = await getResponse(responseId); if (!response) { throw new ResourceNotFoundError("response", responseId); } - return await getProductIdFromSurveyId(response.surveyId); + return await getProjectIdFromSurveyId(response.surveyId); }; -export const getProductIdFromResponseNoteId = async (responseNoteId: string) => { +export const getProjectIdFromResponseNoteId = async (responseNoteId: string) => { const responseNote = await getResponseNote(responseNoteId); if (!responseNote) { throw new ResourceNotFoundError("responseNote", responseNoteId); } - return await getProductIdFromResponseId(responseNote.responseId); + return await getProjectIdFromResponseId(responseNote.responseId); }; -export const getProductIdFromPersonId = async (personId: string) => { +export const getProjectIdFromPersonId = async (personId: string) => { const person = await getPerson(personId); if (!person) { throw new ResourceNotFoundError("person", personId); } - return await getProductIdFromEnvironmentId(person.environmentId); + return await getProjectIdFromEnvironmentId(person.environmentId); }; -export const getProductIdFromDocumentId = async (documentId: string) => { +export const getProjectIdFromDocumentId = async (documentId: string) => { const document = await getDocument(documentId); if (!document) { throw new ResourceNotFoundError("document", documentId); } - return await getProductIdFromEnvironmentId(document.environmentId); + return await getProjectIdFromEnvironmentId(document.environmentId); }; -export const getProductIdFromIntegrationId = async (integrationId: string) => { +export const getProjectIdFromIntegrationId = async (integrationId: string) => { const integration = await getIntegration(integrationId); if (!integration) { throw new ResourceNotFoundError("integration", integrationId); } - return await getProductIdFromEnvironmentId(integration.environmentId); + return await getProjectIdFromEnvironmentId(integration.environmentId); }; // environment id helpers diff --git a/apps/web/lib/utils/services.ts b/apps/web/lib/utils/services.ts index e2cf91f466..1200c50ce1 100644 --- a/apps/web/lib/utils/services.ts +++ b/apps/web/lib/utils/services.ts @@ -12,7 +12,7 @@ import { environmentCache } from "@formbricks/lib/environment/cache"; import { integrationCache } from "@formbricks/lib/integration/cache"; import { inviteCache } from "@formbricks/lib/invite/cache"; import { personCache } from "@formbricks/lib/person/cache"; -import { productCache } from "@formbricks/lib/product/cache"; +import { projectCache } from "@formbricks/lib/project/cache"; import { responseCache } from "@formbricks/lib/response/cache"; import { responseNoteCache } from "@formbricks/lib/responseNote/cache"; import { segmentCache } from "@formbricks/lib/segment/cache"; @@ -119,7 +119,7 @@ export const getAttributeClass = reactCache( ); export const getEnvironment = reactCache( - async (environmentId: string): Promise<{ productId: string } | null> => + async (environmentId: string): Promise<{ projectId: string } | null> => cache( async () => { validateInputs([environmentId, ZId]); @@ -130,7 +130,7 @@ export const getEnvironment = reactCache( id: environmentId, }, select: { - productId: true, + projectId: true, }, }); return environment; @@ -210,13 +210,13 @@ export const getInvite = reactCache( )() ); -export const getLanguage = async (languageId: string): Promise<{ productId: string }> => { +export const getLanguage = async (languageId: string): Promise<{ projectId: string }> => { try { validateInputs([languageId, ZId]); const language = await prisma.language.findFirst({ where: { id: languageId }, - select: { productId: true }, + select: { projectId: true }, }); if (!language) { @@ -261,20 +261,20 @@ export const getPerson = reactCache( )() ); -export const getProduct = reactCache( - async (productId: string): Promise<{ organizationId: string } | null> => +export const getProject = reactCache( + async (projectId: string): Promise<{ organizationId: string } | null> => cache( async () => { - let productPrisma; + let projectPrisma; try { - productPrisma = await prisma.product.findUnique({ + projectPrisma = await prisma.project.findUnique({ where: { - id: productId, + id: projectId, }, select: { organizationId: true }, }); - return productPrisma; + return projectPrisma; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError(error.message); @@ -282,9 +282,9 @@ export const getProduct = reactCache( throw error; } }, - [`utils-getProduct-${productId}`], + [`utils-getProject-${projectId}`], { - tags: [productCache.tag.byId(productId)], + tags: [projectCache.tag.byId(projectId)], } )() ); @@ -561,17 +561,17 @@ export const getDocument = reactCache( )() ); -export const isProductPartOfOrganization = async ( +export const isProjectPartOfOrganization = async ( organizationId: string, - productId: string + projectId: string ): Promise => { try { - const product = await getProduct(productId); - if (!product) { - throw new ResourceNotFoundError("Product", productId); + const project = await getProject(projectId); + if (!project) { + throw new ResourceNotFoundError("Project", projectId); } - return product.organizationId === organizationId; + return project.organizationId === organizationId; } catch (error) { throw error; } diff --git a/apps/web/modules/analysis/components/SingleResponseCard/actions.ts b/apps/web/modules/analysis/components/SingleResponseCard/actions.ts index 5213d934bb..8e953980e1 100644 --- a/apps/web/modules/analysis/components/SingleResponseCard/actions.ts +++ b/apps/web/modules/analysis/components/SingleResponseCard/actions.ts @@ -7,9 +7,9 @@ import { getOrganizationIdFromEnvironmentId, getOrganizationIdFromResponseId, getOrganizationIdFromResponseNoteId, - getProductIdFromEnvironmentId, - getProductIdFromResponseId, - getProductIdFromResponseNoteId, + getProjectIdFromEnvironmentId, + getProjectIdFromResponseId, + getProjectIdFromResponseNoteId, } from "@/lib/utils/helper"; import { getTag } from "@/lib/utils/services"; import { z } from "zod"; @@ -40,8 +40,8 @@ export const createTagAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", - productId: await getProductIdFromEnvironmentId(parsedInput.environmentId), + type: "projectTeam", + projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId), minPermission: "readWrite", }, ], @@ -78,8 +78,8 @@ export const createTagToResponseAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", - productId: await getProductIdFromEnvironmentId(responseEnvironmentId), + type: "projectTeam", + projectId: await getProjectIdFromEnvironmentId(responseEnvironmentId), minPermission: "readWrite", }, ], @@ -116,8 +116,8 @@ export const deleteTagOnResponseAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", - productId: await getProductIdFromEnvironmentId(responseEnvironmentId), + type: "projectTeam", + projectId: await getProjectIdFromEnvironmentId(responseEnvironmentId), minPermission: "readWrite", }, ], @@ -142,8 +142,8 @@ export const deleteResponseAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", - productId: await getProductIdFromResponseId(parsedInput.responseId), + type: "projectTeam", + projectId: await getProjectIdFromResponseId(parsedInput.responseId), minPermission: "readWrite", }, ], @@ -169,8 +169,8 @@ export const updateResponseNoteAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", - productId: await getProductIdFromResponseNoteId(parsedInput.responseNoteId), + type: "projectTeam", + projectId: await getProjectIdFromResponseNoteId(parsedInput.responseNoteId), minPermission: "readWrite", }, ], @@ -195,8 +195,8 @@ export const resolveResponseNoteAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", - productId: await getProductIdFromResponseNoteId(parsedInput.responseNoteId), + type: "projectTeam", + projectId: await getProjectIdFromResponseNoteId(parsedInput.responseNoteId), minPermission: "readWrite", }, ], @@ -222,8 +222,8 @@ export const createResponseNoteAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", - productId: await getProductIdFromResponseId(parsedInput.responseId), + type: "projectTeam", + projectId: await getProjectIdFromResponseId(parsedInput.responseId), minPermission: "readWrite", }, ], @@ -248,9 +248,9 @@ export const getResponseAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "read", - productId: await getProductIdFromResponseId(parsedInput.responseId), + projectId: await getProjectIdFromResponseId(parsedInput.responseId), }, ], }); diff --git a/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.tsx b/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.tsx index 03e667f303..74917c6ab6 100644 --- a/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.tsx +++ b/apps/web/modules/analysis/components/SingleResponseCard/components/ResponseTagsWrapper.tsx @@ -66,7 +66,7 @@ export const ResponseTagsWrapper: React.FC = ({ size="sm" className="cursor-pointer p-0" onClick={() => { - router.push(`/environments/${environmentId}/product/tags`); + router.push(`/environments/${environmentId}/project/tags`); }}> diff --git a/apps/web/modules/ee/advanced-targeting/lib/actions.ts b/apps/web/modules/ee/advanced-targeting/lib/actions.ts index ffd1516b72..22b85c3e6b 100644 --- a/apps/web/modules/ee/advanced-targeting/lib/actions.ts +++ b/apps/web/modules/ee/advanced-targeting/lib/actions.ts @@ -8,9 +8,9 @@ import { getOrganizationIdFromEnvironmentId, getOrganizationIdFromSegmentId, getOrganizationIdFromSurveyId, - getProductIdFromEnvironmentId, - getProductIdFromSegmentId, - getProductIdFromSurveyId, + getProjectIdFromEnvironmentId, + getProjectIdFromSegmentId, + getProjectIdFromSurveyId, } from "@/lib/utils/helper"; import { getAdvancedTargetingPermission } from "@/modules/ee/license-check/lib/utils"; import { z } from "zod"; @@ -63,9 +63,9 @@ export const createSegmentAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "readWrite", - productId: await getProductIdFromEnvironmentId(parsedInput.environmentId), + projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId), }, ], }); @@ -104,9 +104,9 @@ export const updateSegmentAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "readWrite", - productId: await getProductIdFromSegmentId(parsedInput.segmentId), + projectId: await getProjectIdFromSegmentId(parsedInput.segmentId), }, ], }); @@ -153,9 +153,9 @@ export const loadNewSegmentAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "readWrite", - productId: await getProductIdFromEnvironmentId(surveyEnvironmentId), + projectId: await getProjectIdFromEnvironmentId(surveyEnvironmentId), }, ], }); @@ -191,9 +191,9 @@ export const cloneSegmentAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "readWrite", - productId: await getProductIdFromEnvironmentId(surveyEnvironmentId), + projectId: await getProjectIdFromEnvironmentId(surveyEnvironmentId), }, ], }); @@ -221,9 +221,9 @@ export const deleteSegmentAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "readWrite", - productId: await getProductIdFromSegmentId(parsedInput.segmentId), + projectId: await getProjectIdFromSegmentId(parsedInput.segmentId), }, ], }); @@ -251,9 +251,9 @@ export const resetSegmentFiltersAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "readWrite", - productId: await getProductIdFromSurveyId(parsedInput.surveyId), + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), }, ], }); diff --git a/apps/web/modules/ee/billing/api/lib/constants.ts b/apps/web/modules/ee/billing/api/lib/constants.ts index dfb3d027cd..0e23e1c235 100644 --- a/apps/web/modules/ee/billing/api/lib/constants.ts +++ b/apps/web/modules/ee/billing/api/lib/constants.ts @@ -9,6 +9,7 @@ export const CLOUD_PRICING_DATA = { mainFeatures: [ "environments.settings.billing.unlimited_surveys", "environments.settings.billing.unlimited_team_members", + "environments.settings.billing.3_projects", "environments.settings.billing.1500_monthly_responses", "environments.settings.billing.2000_monthly_identified_users", "environments.settings.billing.website_surveys", @@ -33,6 +34,7 @@ export const CLOUD_PRICING_DATA = { "environments.settings.billing.unlimited_surveys", "environments.settings.billing.remove_branding", "environments.settings.billing.email_support", + "environments.settings.billing.3_projects", "environments.settings.billing.5000_monthly_responses", "environments.settings.billing.7500_monthly_identified_users", ], @@ -50,6 +52,7 @@ export const CLOUD_PRICING_DATA = { "environments.settings.billing.multi_language_surveys", "environments.settings.billing.advanced_targeting", "environments.settings.billing.priority_support", + "environments.settings.billing.5_projects", "environments.settings.billing.10000_monthly_responses", "environments.settings.billing.30000_monthly_identified_users", ], @@ -66,6 +69,7 @@ export const CLOUD_PRICING_DATA = { }, mainFeatures: [ "environments.settings.billing.everything_in_scale", + "environments.settings.billing.custom_project_limit", "environments.settings.billing.custom_miu_limit", "environments.settings.billing.premium_support_with_slas", "environments.settings.billing.uptime_sla_99", diff --git a/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts b/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts index e2db72d289..514b6b5564 100644 --- a/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts +++ b/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts @@ -1,5 +1,5 @@ import Stripe from "stripe"; -import { PRODUCT_FEATURE_KEYS, STRIPE_API_VERSION } from "@formbricks/lib/constants"; +import { PROJECT_FEATURE_KEYS, STRIPE_API_VERSION } from "@formbricks/lib/constants"; import { env } from "@formbricks/lib/env"; import { getOrganization, updateOrganization } from "@formbricks/lib/organization/service"; import { ResourceNotFoundError } from "@formbricks/types/errors"; @@ -53,6 +53,7 @@ export const handleSubscriptionCreatedOrUpdated = async (event: Stripe.Event) => let responses: number | null = null; let miu: number | null = null; + let projects: number | null = null; if (product.metadata.responses === "unlimited") { responses = null; @@ -72,23 +73,32 @@ export const handleSubscriptionCreatedOrUpdated = async (event: Stripe.Event) => throw new Error("Invalid miu metadata in product"); } + if (product.metadata.projects === "unlimited") { + projects = null; + } else if (parseInt(product.metadata.projects) > 0) { + projects = parseInt(product.metadata.projects); + } else { + console.error("Invalid projects metadata in product: ", product.metadata.projects); + throw new Error("Invalid projects metadata in product"); + } + const plan = ZOrganizationBillingPlan.parse(product.metadata.plan); switch (plan) { - case PRODUCT_FEATURE_KEYS.FREE: - updatedBillingPlan = PRODUCT_FEATURE_KEYS.STARTUP; + case PROJECT_FEATURE_KEYS.FREE: + updatedBillingPlan = PROJECT_FEATURE_KEYS.STARTUP; break; - case PRODUCT_FEATURE_KEYS.STARTUP: - updatedBillingPlan = PRODUCT_FEATURE_KEYS.STARTUP; + case PROJECT_FEATURE_KEYS.STARTUP: + updatedBillingPlan = PROJECT_FEATURE_KEYS.STARTUP; break; - case PRODUCT_FEATURE_KEYS.SCALE: - updatedBillingPlan = PRODUCT_FEATURE_KEYS.SCALE; + case PROJECT_FEATURE_KEYS.SCALE: + updatedBillingPlan = PROJECT_FEATURE_KEYS.SCALE; break; - case PRODUCT_FEATURE_KEYS.ENTERPRISE: - updatedBillingPlan = PRODUCT_FEATURE_KEYS.ENTERPRISE; + case PROJECT_FEATURE_KEYS.ENTERPRISE: + updatedBillingPlan = PROJECT_FEATURE_KEYS.ENTERPRISE; break; } @@ -99,6 +109,7 @@ export const handleSubscriptionCreatedOrUpdated = async (event: Stripe.Event) => plan: updatedBillingPlan, period, limits: { + projects, monthly: { responses, miu, diff --git a/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts b/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts index 7f816ad97c..d81299c4be 100644 --- a/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts +++ b/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts @@ -1,5 +1,5 @@ import Stripe from "stripe"; -import { BILLING_LIMITS, PRODUCT_FEATURE_KEYS } from "@formbricks/lib/constants"; +import { BILLING_LIMITS, PROJECT_FEATURE_KEYS } from "@formbricks/lib/constants"; import { getOrganization, updateOrganization } from "@formbricks/lib/organization/service"; import { ResourceNotFoundError } from "@formbricks/types/errors"; @@ -17,8 +17,9 @@ export const handleSubscriptionDeleted = async (event: Stripe.Event) => { await updateOrganization(organizationId, { billing: { ...organization.billing, - plan: PRODUCT_FEATURE_KEYS.FREE, + plan: PROJECT_FEATURE_KEYS.FREE, limits: { + projects: BILLING_LIMITS.FREE.PROJECTS, monthly: { responses: BILLING_LIMITS.FREE.RESPONSES, miu: BILLING_LIMITS.FREE.MIU, diff --git a/apps/web/modules/ee/billing/components/pricing-card.tsx b/apps/web/modules/ee/billing/components/pricing-card.tsx index 3b95a50d8d..ff0a72cf33 100644 --- a/apps/web/modules/ee/billing/components/pricing-card.tsx +++ b/apps/web/modules/ee/billing/components/pricing-card.tsx @@ -23,7 +23,7 @@ interface PricingCardProps { organization: TOrganization; onUpgrade: () => Promise; onManageSubscription: () => Promise; - productFeatureKeys: { + projectFeatureKeys: { FREE: string; STARTUP: string; SCALE: string; @@ -37,20 +37,20 @@ export const PricingCard = ({ onUpgrade, onManageSubscription, organization, - productFeatureKeys, + projectFeatureKeys, }: PricingCardProps) => { const t = useTranslations(); const [loading, setLoading] = useState(false); const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); const isCurrentPlan = useMemo(() => { - if (organization.billing.plan === productFeatureKeys.FREE && plan.id === productFeatureKeys.FREE) { + if (organization.billing.plan === projectFeatureKeys.FREE && plan.id === projectFeatureKeys.FREE) { return true; } if ( - organization.billing.plan === productFeatureKeys.ENTERPRISE && - plan.id === productFeatureKeys.ENTERPRISE + organization.billing.plan === projectFeatureKeys.ENTERPRISE && + plan.id === projectFeatureKeys.ENTERPRISE ) { return true; } @@ -61,8 +61,8 @@ export const PricingCard = ({ organization.billing.plan, plan.id, planPeriod, - productFeatureKeys.ENTERPRISE, - productFeatureKeys.FREE, + projectFeatureKeys.ENTERPRISE, + projectFeatureKeys.FREE, ]); const CTAButton = useMemo(() => { @@ -70,8 +70,8 @@ export const PricingCard = ({ return null; } - if (plan.id !== productFeatureKeys.ENTERPRISE && plan.id !== productFeatureKeys.FREE) { - if (organization.billing.plan === productFeatureKeys.FREE) { + if (plan.id !== projectFeatureKeys.ENTERPRISE && plan.id !== projectFeatureKeys.FREE) { + if (organization.billing.plan === projectFeatureKeys.FREE) { return ( diff --git a/apps/web/modules/ee/billing/components/pricing-table.tsx b/apps/web/modules/ee/billing/components/pricing-table.tsx index ce5c0d958c..9f0e929b07 100644 --- a/apps/web/modules/ee/billing/components/pricing-table.tsx +++ b/apps/web/modules/ee/billing/components/pricing-table.tsx @@ -19,13 +19,14 @@ interface PricingTableProps { environmentId: string; peopleCount: number; responseCount: number; + projectCount: number; stripePriceLookupKeys: { STARTUP_MONTHLY: string; STARTUP_YEARLY: string; SCALE_MONTHLY: string; SCALE_YEARLY: string; }; - productFeatureKeys: { + projectFeatureKeys: { FREE: string; STARTUP: string; SCALE: string; @@ -38,8 +39,9 @@ export const PricingTable = ({ environmentId, organization, peopleCount, - productFeatureKeys, + projectFeatureKeys, responseCount, + projectCount, stripePriceLookupKeys, hasBillingRights, }: PricingTableProps) => { @@ -137,6 +139,8 @@ export const PricingTable = ({ organization.billing.plan === "enterprise" && organization.billing.limits.monthly.responses === null; const peopleUnlimitedCheck = organization.billing.plan === "enterprise" && organization.billing.limits.monthly.miu === null; + const projectsUnlimitedCheck = + organization.billing.plan === "enterprise" && organization.billing.limits.projects === null; return (
    @@ -197,7 +201,7 @@ export const PricingTable = ({

    @@ -205,7 +209,7 @@ export const PricingTable = ({

    {organization.billing.limits.monthly.miu && ( )} - {peopleUnlimitedCheck && } + {peopleUnlimitedCheck && ( + + )} +
    + +
    +

    {t("common.projects")}

    + {organization.billing.limits.projects && ( + + )} + + {projectsUnlimitedCheck && ( + + )}
    @@ -254,7 +285,7 @@ export const PricingTable = ({ await onUpgrade(plan.id); }} organization={organization} - productFeatureKeys={productFeatureKeys} + projectFeatureKeys={projectFeatureKeys} onManageSubscription={openCustomerPortal} /> ))} diff --git a/apps/web/modules/ee/billing/page.tsx b/apps/web/modules/ee/billing/page.tsx index 9a1f05fe29..fcc01c1c3d 100644 --- a/apps/web/modules/ee/billing/page.tsx +++ b/apps/web/modules/ee/billing/page.tsx @@ -7,7 +7,7 @@ import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { notFound } from "next/navigation"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; -import { PRODUCT_FEATURE_KEYS, STRIPE_PRICE_LOOKUP_KEYS } from "@formbricks/lib/constants"; +import { PROJECT_FEATURE_KEYS, STRIPE_PRICE_LOOKUP_KEYS } from "@formbricks/lib/constants"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { @@ -15,6 +15,7 @@ import { getMonthlyOrganizationResponseCount, getOrganizationByEnvironmentId, } from "@formbricks/lib/organization/service"; +import { getOrganizationProjectsCount } from "@formbricks/lib/project/service"; import { PricingTable } from "./components/pricing-table"; export const PricingPage = async (props) => { @@ -35,9 +36,10 @@ export const PricingPage = async (props) => { throw new Error(t("common.not_authorized")); } - const [peopleCount, responseCount] = await Promise.all([ + const [peopleCount, responseCount, projectCount] = await Promise.all([ getMonthlyActiveOrganizationPeopleCount(organization.id), getMonthlyOrganizationResponseCount(organization.id), + getOrganizationProjectsCount(organization.id), ]); const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id); @@ -63,8 +65,9 @@ export const PricingPage = async (props) => { environmentId={params.environmentId} peopleCount={peopleCount} responseCount={responseCount} + projectCount={projectCount} stripePriceLookupKeys={STRIPE_PRICE_LOOKUP_KEYS} - productFeatureKeys={PRODUCT_FEATURE_KEYS} + projectFeatureKeys={PROJECT_FEATURE_KEYS} hasBillingRights={hasBillingRights} /> diff --git a/apps/web/modules/ee/insights/actions.ts b/apps/web/modules/ee/insights/actions.ts index 07763016bc..154bbf1ce8 100644 --- a/apps/web/modules/ee/insights/actions.ts +++ b/apps/web/modules/ee/insights/actions.ts @@ -3,7 +3,7 @@ import { generateInsightsForSurvey } from "@/app/api/(internal)/insights/lib/utils"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; -import { getOrganizationIdFromSurveyId, getProductIdFromSurveyId } from "@/lib/utils/helper"; +import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper"; import { getIsAIEnabled, getIsOrganizationAIReady } from "@/modules/ee/license-check/lib/utils"; import { z } from "zod"; import { getOrganization, updateOrganization } from "@formbricks/lib/organization/service"; @@ -45,8 +45,8 @@ export const generateInsightsForSurveyAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", - productId: await getProductIdFromSurveyId(parsedInput.surveyId), + type: "projectTeam", + projectId: await getProjectIdFromSurveyId(parsedInput.surveyId), minPermission: "readWrite", }, ], diff --git a/apps/web/modules/ee/insights/components/insight-sheet/actions.ts b/apps/web/modules/ee/insights/components/insight-sheet/actions.ts index c44ba27bb0..96f5d47167 100644 --- a/apps/web/modules/ee/insights/components/insight-sheet/actions.ts +++ b/apps/web/modules/ee/insights/components/insight-sheet/actions.ts @@ -8,9 +8,9 @@ import { getOrganizationIdFromDocumentId, getOrganizationIdFromEnvironmentId, getOrganizationIdFromInsightId, - getProductIdFromDocumentId, - getProductIdFromEnvironmentId, - getProductIdFromInsightId, + getProjectIdFromDocumentId, + getProjectIdFromEnvironmentId, + getProjectIdFromInsightId, } from "@/lib/utils/helper"; import { checkAIPermission } from "@/modules/ee/insights/actions"; import { @@ -52,9 +52,9 @@ export const getDocumentsByInsightIdSurveyIdQuestionIdAction = authenticatedActi roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "read", - productId: await getProductIdFromEnvironmentId(surveyEnvironmentId), + projectId: await getProjectIdFromEnvironmentId(surveyEnvironmentId), }, ], }); @@ -90,9 +90,9 @@ export const getDocumentsByInsightIdAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "read", - productId: await getProductIdFromInsightId(parsedInput.insightId), + projectId: await getProjectIdFromInsightId(parsedInput.insightId), }, ], }); @@ -129,9 +129,9 @@ export const updateDocumentAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "readWrite", - productId: await getProductIdFromDocumentId(parsedInput.documentId), + projectId: await getProjectIdFromDocumentId(parsedInput.documentId), }, ], }); diff --git a/apps/web/modules/ee/insights/experience/actions.ts b/apps/web/modules/ee/insights/experience/actions.ts index 921af8cad8..f14304ef73 100644 --- a/apps/web/modules/ee/insights/experience/actions.ts +++ b/apps/web/modules/ee/insights/experience/actions.ts @@ -5,8 +5,8 @@ import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware" import { getOrganizationIdFromEnvironmentId, getOrganizationIdFromInsightId, - getProductIdFromEnvironmentId, - getProductIdFromInsightId, + getProjectIdFromEnvironmentId, + getProjectIdFromInsightId, } from "@/lib/utils/helper"; import { checkAIPermission } from "@/modules/ee/insights/actions"; import { z } from "zod"; @@ -35,9 +35,9 @@ export const getEnvironmentInsightsAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "read", - productId: await getProductIdFromEnvironmentId(parsedInput.environmentId), + projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId), }, ], }); @@ -70,9 +70,9 @@ export const getStatsAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", + type: "projectTeam", minPermission: "read", - productId: await getProductIdFromEnvironmentId(parsedInput.environmentId), + projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId), }, ], }); @@ -101,8 +101,8 @@ export const updateInsightAction = authenticatedActionClient roles: ["owner", "manager"], }, { - type: "productTeam", - productId: await getProductIdFromInsightId(parsedInput.insightId), + type: "projectTeam", + projectId: await getProjectIdFromInsightId(parsedInput.insightId), minPermission: "readWrite", }, ], diff --git a/apps/web/modules/ee/insights/experience/components/dashboard.tsx b/apps/web/modules/ee/insights/experience/components/dashboard.tsx index 5ccc45a68b..dfc14479ad 100644 --- a/apps/web/modules/ee/insights/experience/components/dashboard.tsx +++ b/apps/web/modules/ee/insights/experience/components/dashboard.tsx @@ -9,13 +9,13 @@ import { Tabs, TabsList, TabsTrigger } from "@/modules/ui/components/tabs"; import { useTranslations } from "next-intl"; import { useState } from "react"; import { TEnvironment } from "@formbricks/types/environment"; -import { TProduct } from "@formbricks/types/product"; +import { TProject } from "@formbricks/types/project"; import { TUser, TUserLocale } from "@formbricks/types/user"; interface DashboardProps { user: TUser; environment: TEnvironment; - product: TProduct; + project: TProject; insightsPerPage: number; documentsPerPage: number; locale: TUserLocale; @@ -23,7 +23,7 @@ interface DashboardProps { export const Dashboard = ({ environment, - product, + project, user, insightsPerPage, documentsPerPage, @@ -65,7 +65,7 @@ export const Dashboard = ({ - {t("environments.experience.insights_for_product", { productName })} + {t("environments.experience.insights_for_project", { projectName })} {t("environments.experience.insights_description")} diff --git a/apps/web/modules/ee/insights/experience/components/templates-card.tsx b/apps/web/modules/ee/insights/experience/components/templates-card.tsx index 391ad42712..fa7d6c4ad4 100644 --- a/apps/web/modules/ee/insights/experience/components/templates-card.tsx +++ b/apps/web/modules/ee/insights/experience/components/templates-card.tsx @@ -4,18 +4,18 @@ import { TemplateList } from "@/modules/surveys/components/TemplateList"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/modules/ui/components/card"; import { useTranslations } from "next-intl"; import { TEnvironment } from "@formbricks/types/environment"; -import { TProduct } from "@formbricks/types/product"; +import { TProject } from "@formbricks/types/project"; import { TTemplateFilter } from "@formbricks/types/templates"; import { TUser } from "@formbricks/types/user"; interface TemplatesCardProps { environment: TEnvironment; - product: TProduct; + project: TProject; user: TUser; prefilledFilters: TTemplateFilter[]; } -export const TemplatesCard = ({ environment, product, user, prefilledFilters }: TemplatesCardProps) => { +export const TemplatesCard = ({ environment, project, user, prefilledFilters }: TemplatesCardProps) => { const t = useTranslations(); return ( @@ -26,7 +26,7 @@ export const TemplatesCard = ({ environment, product, user, prefilledFilters }: { throw new Error("User not found"); } - const [environment, product, organization] = await Promise.all([ + const [environment, project, organization] = await Promise.all([ getEnvironment(params.environmentId), - getProductByEnvironmentId(params.environmentId), + getProjectByEnvironmentId(params.environmentId), getOrganizationByEnvironmentId(params.environmentId), ]); @@ -36,8 +36,8 @@ export const ExperiencePage = async (props) => { throw new Error("Environment not found"); } - if (!product) { - throw new Error("Product not found"); + if (!project) { + throw new Error("Project not found"); } if (!organization) { @@ -62,7 +62,7 @@ export const ExperiencePage = async (props) => { { +export const LanguagesLoading = () => { const t = useTranslations(); return ( - + + title={t("environments.project.languages.multi_language_surveys")} + description={t("environments.project.languages.multi_language_surveys_description")}>
    {[...Array(3)].map((_, idx) => ( @@ -31,5 +31,3 @@ const Loading = () => { ); }; - -export default Loading; diff --git a/apps/web/app/(app)/environments/[environmentId]/product/languages/page.tsx b/apps/web/modules/ee/languages/page.tsx similarity index 71% rename from apps/web/app/(app)/environments/[environmentId]/product/languages/page.tsx rename to apps/web/modules/ee/languages/page.tsx index a251607890..79d6b421bf 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/languages/page.tsx +++ b/apps/web/modules/ee/languages/page.tsx @@ -1,4 +1,3 @@ -import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation"; import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard"; import { authOptions } from "@/modules/auth/lib/authOptions"; import { @@ -6,8 +5,9 @@ import { getRoleManagementPermission, } from "@/modules/ee/license-check/lib/utils"; import { EditLanguage } from "@/modules/ee/multi-language-surveys/components/edit-language"; -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 { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { getServerSession } from "next-auth"; @@ -16,19 +16,19 @@ import { notFound } 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 { getProductByEnvironmentId } from "@formbricks/lib/product/service"; +import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; import { getUser } from "@formbricks/lib/user/service"; -const Page = async (props: { params: Promise<{ environmentId: string }> }) => { +export const LanguagesPage = async (props: { params: Promise<{ environmentId: string }> }) => { const params = await props.params; const t = await getTranslations(); - const product = await getProductByEnvironmentId(params.environmentId); + const project = await getProjectByEnvironmentId(params.environmentId); - if (!product) { - throw new Error(t("common.product_not_found")); + if (!project) { + throw new Error(t("common.project_not_found")); } - const organization = await getOrganization(product?.organizationId); + const organization = await getOrganization(project?.organizationId); if (!organization) { throw new Error(t("common.organization_not_found")); @@ -56,15 +56,15 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => { const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id); const { isMember } = getAccessFlags(currentUserMembership?.role); - const productPermission = await getProductPermissionByUserId(session.user.id, product.id); - const { hasManageAccess } = getTeamPermissionFlags(productPermission); + const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id); + const { hasManageAccess } = getTeamPermissionFlags(projectPermission); const isReadOnly = isMember && !hasManageAccess; return ( - }) => { /> - + title={t("environments.project.languages.multi_language_surveys")} + description={t("environments.project.languages.multi_language_surveys_description")}> + ); }; - -export default Page; diff --git a/apps/web/modules/ee/license-check/lib/utils.ts b/apps/web/modules/ee/license-check/lib/utils.ts index 91fa7463e5..b797091b3a 100644 --- a/apps/web/modules/ee/license-check/lib/utils.ts +++ b/apps/web/modules/ee/license-check/lib/utils.ts @@ -14,7 +14,7 @@ import { ENTERPRISE_LICENSE_KEY, IS_AI_CONFIGURED, IS_FORMBRICKS_CLOUD, - PRODUCT_FEATURE_KEYS, + PROJECT_FEATURE_KEYS, } from "@formbricks/lib/constants"; import { env } from "@formbricks/lib/env"; import { hashString } from "@formbricks/lib/hashString"; @@ -81,7 +81,7 @@ const fetchLicenseForE2ETesting = async (): Promise<{ // first call const newResult = { active: true, - features: { isMultiOrgEnabled: true, twoFactorAuth: true, sso: true }, + features: { isMultiOrgEnabled: true, projects: 3, twoFactorAuth: true, sso: true }, lastChecked: currentTime, }; await setPreviousResult(newResult); @@ -138,7 +138,7 @@ export const getEnterpriseLicense = async (): Promise<{ if (isValid === null) { const newResult = { active: false, - features: { isMultiOrgEnabled: false, twoFactorAuth: false, sso: false }, + features: { isMultiOrgEnabled: false, projects: 3, twoFactorAuth: false, sso: false }, lastChecked: new Date(), }; @@ -244,28 +244,32 @@ export const fetchLicense = reactCache( ); export const getRemoveInAppBrandingPermission = (organization: TOrganization): boolean => { - if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PRODUCT_FEATURE_KEYS.FREE; + if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PROJECT_FEATURE_KEYS.FREE; else if (!IS_FORMBRICKS_CLOUD) return true; return false; }; export const getRemoveLinkBrandingPermission = (organization: TOrganization): boolean => { - if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PRODUCT_FEATURE_KEYS.FREE; + if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PROJECT_FEATURE_KEYS.FREE; else if (!IS_FORMBRICKS_CLOUD) return true; return false; }; export const getSurveyFollowUpsPermission = async (organization: TOrganization): Promise => { - if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PRODUCT_FEATURE_KEYS.FREE; + if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PROJECT_FEATURE_KEYS.FREE; else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active; return false; }; export const getRoleManagementPermission = async (organization: TOrganization): Promise => { + if (E2E_TESTING) { + const previousResult = await fetchLicenseForE2ETesting(); + return previousResult && previousResult.active !== null ? previousResult.active : false; + } if (IS_FORMBRICKS_CLOUD) return ( - organization.billing.plan === PRODUCT_FEATURE_KEYS.SCALE || - organization.billing.plan === PRODUCT_FEATURE_KEYS.ENTERPRISE + organization.billing.plan === PROJECT_FEATURE_KEYS.SCALE || + organization.billing.plan === PROJECT_FEATURE_KEYS.ENTERPRISE ); else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active; return false; @@ -274,15 +278,15 @@ export const getRoleManagementPermission = async (organization: TOrganization): export const getAdvancedTargetingPermission = async (organization: TOrganization): Promise => { if (IS_FORMBRICKS_CLOUD) return ( - organization.billing.plan === PRODUCT_FEATURE_KEYS.SCALE || - organization.billing.plan === PRODUCT_FEATURE_KEYS.ENTERPRISE + organization.billing.plan === PROJECT_FEATURE_KEYS.SCALE || + organization.billing.plan === PROJECT_FEATURE_KEYS.ENTERPRISE ); else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active; else return false; }; export const getBiggerUploadFileSizePermission = async (organization: TOrganization): Promise => { - if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PRODUCT_FEATURE_KEYS.FREE; + if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PROJECT_FEATURE_KEYS.FREE; else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active; return false; }; @@ -294,8 +298,8 @@ export const getMultiLanguagePermission = async (organization: TOrganization): P } if (IS_FORMBRICKS_CLOUD) return ( - organization.billing.plan === PRODUCT_FEATURE_KEYS.SCALE || - organization.billing.plan === PRODUCT_FEATURE_KEYS.ENTERPRISE + organization.billing.plan === PROJECT_FEATURE_KEYS.SCALE || + organization.billing.plan === PROJECT_FEATURE_KEYS.ENTERPRISE ); else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active; return false; @@ -337,9 +341,9 @@ export const getIsOrganizationAIReady = async (billingPlan: TOrganizationBilling return ( IS_AI_CONFIGURED && (await getEnterpriseLicense()).active && - (billingPlan === PRODUCT_FEATURE_KEYS.STARTUP || - billingPlan === PRODUCT_FEATURE_KEYS.SCALE || - billingPlan === PRODUCT_FEATURE_KEYS.ENTERPRISE) + (billingPlan === PROJECT_FEATURE_KEYS.STARTUP || + billingPlan === PROJECT_FEATURE_KEYS.SCALE || + billingPlan === PROJECT_FEATURE_KEYS.ENTERPRISE) ); } @@ -349,3 +353,25 @@ export const getIsOrganizationAIReady = async (billingPlan: TOrganizationBilling export const getIsAIEnabled = async (organization: TOrganization) => { return organization.isAIEnabled && (await getIsOrganizationAIReady(organization.billing.plan)); }; + +export const getOrganizationProjectsLimit = async (organization: TOrganization): Promise => { + if (E2E_TESTING) { + const previousResult = await fetchLicenseForE2ETesting(); + return previousResult && previousResult.features ? (previousResult.features.projects ?? Infinity) : 3; + } + + let limit: number; + + if (IS_FORMBRICKS_CLOUD && (await getEnterpriseLicense()).active) { + limit = organization.billing.limits.projects ?? Infinity; + } else { + const licenseFeatures = await getLicenseFeatures(); + if (!licenseFeatures) { + limit = 3; + } else { + limit = licenseFeatures.projects ?? Infinity; + } + } + + return limit; +}; diff --git a/apps/web/modules/ee/license-check/types/enterprise-license.ts b/apps/web/modules/ee/license-check/types/enterprise-license.ts index ee3a0a1531..daa1b9c831 100644 --- a/apps/web/modules/ee/license-check/types/enterprise-license.ts +++ b/apps/web/modules/ee/license-check/types/enterprise-license.ts @@ -6,6 +6,7 @@ export type TEnterpriseLicenseStatus = z.infer; const ZEnterpriseLicenseFeatures = z.object({ isMultiOrgEnabled: z.boolean(), + projects: z.number().nullable(), twoFactorAuth: z.boolean(), sso: z.boolean(), }); diff --git a/apps/web/modules/ee/multi-language-surveys/components/default-language-select.tsx b/apps/web/modules/ee/multi-language-surveys/components/default-language-select.tsx index e5eedfbac7..645c6d62ad 100644 --- a/apps/web/modules/ee/multi-language-surveys/components/default-language-select.tsx +++ b/apps/web/modules/ee/multi-language-surveys/components/default-language-select.tsx @@ -8,13 +8,13 @@ import { } from "@/modules/ui/components/select"; import { useTranslations } from "next-intl"; import { getLanguageLabel } from "@formbricks/lib/i18n/utils"; -import type { TLanguage, TProduct } from "@formbricks/types/product"; +import type { TLanguage, TProject } from "@formbricks/types/project"; import type { ConfirmationModalProps } from "./multi-language-card"; interface DefaultLanguageSelectProps { defaultLanguage?: TLanguage; handleDefaultLanguageChange: (languageCode: string) => void; - product: TProduct; + project: TProject; setConfirmationModalInfo: (confirmationModal: ConfirmationModalProps) => void; locale: string; } @@ -22,7 +22,7 @@ interface DefaultLanguageSelectProps { export function DefaultLanguageSelect({ defaultLanguage, handleDefaultLanguageChange, - product, + project, setConfirmationModalInfo, locale, }: DefaultLanguageSelectProps) { @@ -60,7 +60,7 @@ export function DefaultLanguageSelect({ - {product.languages.map((language) => ( + {project.languages.map((language) => ( string) = .map((language) => language.alias!.toLowerCase().trim()); if (languageCodes.includes("")) { - toast.error(t("environments.product.languages.please_select_a_language"), { duration: 2000 }); + toast.error(t("environments.project.languages.please_select_a_language"), { duration: 2000 }); return false; } // Check for duplicates within the languageCodes and languageAliases if (checkIfDuplicateExists(languageAliases) || checkIfDuplicateExists(languageCodes)) { - toast.error(t("environments.product.languages.duplicate_language_or_language_id"), { duration: 4000 }); + toast.error(t("environments.project.languages.duplicate_language_or_language_id"), { duration: 4000 }); return false; } // Check if any alias matches the identifier of any added languages if (languageCodes.some((code) => languageAliases.includes(code))) { - toast.error(t("environments.product.languages.conflict_between_identifier_and_alias"), { + toast.error(t("environments.project.languages.conflict_between_identifier_and_alias"), { duration: 6000, }); return false; @@ -57,7 +57,7 @@ const validateLanguages = (languages: TLanguage[], t: (key: string) => string) = // Check if the chosen alias matches an ISO identifier of a language that hasn’t been added for (const alias of languageAliases) { if (iso639Languages.some((language) => language.alpha2 === alias && !languageCodes.includes(alias))) { - toast.error(t("environments.product.languages.conflict_between_selected_alias_and_another_language"), { + toast.error(t("environments.project.languages.conflict_between_selected_alias_and_another_language"), { duration: 6000, }); return false; @@ -67,9 +67,9 @@ const validateLanguages = (languages: TLanguage[], t: (key: string) => string) = return true; }; -export function EditLanguage({ product, locale, isReadOnly }: EditLanguageProps) { +export function EditLanguage({ project, locale, isReadOnly }: EditLanguageProps) { const t = useTranslations(); - const [languages, setLanguages] = useState(product.languages); + const [languages, setLanguages] = useState(project.languages); const [isEditing, setIsEditing] = useState(false); const [confirmationModal, setConfirmationModal] = useState({ isOpen: false, @@ -79,8 +79,8 @@ export function EditLanguage({ product, locale, isReadOnly }: EditLanguageProps) }); useEffect(() => { - setLanguages(product.languages); - }, [product.languages]); + setLanguages(project.languages); + }, [project.languages]); const handleAddLanguage = () => { const newLanguage = { id: "new", createdAt: new Date(), updatedAt: new Date(), code: "", alias: "" }; @@ -102,14 +102,14 @@ export function EditLanguage({ product, locale, isReadOnly }: EditLanguageProps) setConfirmationModal({ isOpen: true, languageId, - text: `${t("environments.product.languages.cannot_remove_language_warning")}:\n\n${surveyList}\n\n${t("environments.product.languages.remove_language_from_surveys_to_remove_it_from_product")}`, + text: `${t("environments.project.languages.cannot_remove_language_warning")}:\n\n${surveyList}\n\n${t("environments.project.languages.remove_language_from_surveys_to_remove_it_from_project")}`, isButtonDisabled: true, }); } else { setConfirmationModal({ isOpen: true, languageId, - text: t("environments.product.languages.delete_language_confirmation"), + text: t("environments.project.languages.delete_language_confirmation"), isButtonDisabled: false, }); } @@ -124,9 +124,9 @@ export function EditLanguage({ product, locale, isReadOnly }: EditLanguageProps) const performLanguageDeletion = async (languageId: string) => { try { - await deleteLanguageAction({ languageId, productId: product.id }); + await deleteLanguageAction({ languageId, projectId: project.id }); setLanguages((prev) => prev.filter((lang) => lang.id !== languageId)); - toast.success(t("environments.product.languages.language_deleted_successfully")); + toast.success(t("environments.project.languages.language_deleted_successfully")); // Close the modal after deletion setConfirmationModal((prev) => ({ ...prev, isOpen: false })); } catch (err) { @@ -136,7 +136,7 @@ export function EditLanguage({ product, locale, isReadOnly }: EditLanguageProps) }; const handleCancelChanges = async () => { - setLanguages(product.languages); + setLanguages(project.languages); setIsEditing(false); }; @@ -146,24 +146,24 @@ export function EditLanguage({ product, locale, isReadOnly }: EditLanguageProps) languages.map((lang) => { return lang.id === "new" ? createLanguageAction({ - productId: product.id, + projectId: project.id, languageInput: { code: lang.code, alias: lang.alias }, }) : updateLanguageAction({ - productId: product.id, + projectId: project.id, languageId: lang.id, languageInput: { code: lang.code, alias: lang.alias }, }); }) ); - toast.success(t("environments.product.languages.languages_updated_successfully")); + toast.success(t("environments.project.languages.languages_updated_successfully")); setIsEditing(false); }; const AddLanguageButton: React.FC<{ onClick: () => void }> = ({ onClick }) => - isEditing && languages.length === product.languages.length ? ( + isEditing && languages.length === project.languages.length ? ( ) : null; @@ -191,7 +191,7 @@ export function EditLanguage({ product, locale, isReadOnly }: EditLanguageProps) ) : (

    - {t("environments.product.languages.no_language_found")} + {t("environments.project.languages.no_language_found")}

    )} @@ -208,7 +208,7 @@ export function EditLanguage({ product, locale, isReadOnly }: EditLanguageProps) /> )} performLanguageDeletion(confirmationModal.languageId)} open={confirmationModal.isOpen} @@ -216,7 +216,7 @@ export function EditLanguage({ product, locale, isReadOnly }: EditLanguageProps) setConfirmationModal((prev) => ({ ...prev, isOpen: !prev.isOpen })); }} text={confirmationModal.text} - title={t("environments.product.languages.remove_language")} + title={t("environments.project.languages.remove_language")} />
    ); @@ -240,6 +240,6 @@ const EditSaveButtons: React.FC<{
    ) : ( ); diff --git a/apps/web/modules/ee/multi-language-surveys/components/language-labels.tsx b/apps/web/modules/ee/multi-language-surveys/components/language-labels.tsx index 2c4b7c7029..639bfbfec7 100644 --- a/apps/web/modules/ee/multi-language-surveys/components/language-labels.tsx +++ b/apps/web/modules/ee/multi-language-surveys/components/language-labels.tsx @@ -7,10 +7,10 @@ export function LanguageLabels() { const t = useTranslations(); return (
    - - + +
    ); @@ -25,7 +25,7 @@ function AliasTooltip({ t }: { t: (key: string) => string }) {
    - {t("environments.product.languages.alias_tooltip")} + {t("environments.project.languages.alias_tooltip")} ); diff --git a/apps/web/modules/ee/multi-language-surveys/components/language-row.tsx b/apps/web/modules/ee/multi-language-surveys/components/language-row.tsx index 060871c4aa..b073a88c66 100644 --- a/apps/web/modules/ee/multi-language-surveys/components/language-row.tsx +++ b/apps/web/modules/ee/multi-language-surveys/components/language-row.tsx @@ -1,7 +1,7 @@ import { Button } from "@/modules/ui/components/button"; import { Input } from "@/modules/ui/components/input"; import { useTranslations } from "next-intl"; -import type { TLanguage } from "@formbricks/types/product"; +import type { TLanguage } from "@formbricks/types/project"; import { TUserLocale } from "@formbricks/types/user"; import { LanguageSelect } from "./language-select"; diff --git a/apps/web/modules/ee/multi-language-surveys/components/language-select.tsx b/apps/web/modules/ee/multi-language-surveys/components/language-select.tsx index 2b8deda35e..b079df84ed 100644 --- a/apps/web/modules/ee/multi-language-surveys/components/language-select.tsx +++ b/apps/web/modules/ee/multi-language-surveys/components/language-select.tsx @@ -6,7 +6,7 @@ import { useEffect, useRef, useState } from "react"; import type { TIso639Language } from "@formbricks/lib/i18n/utils"; import { iso639Languages } from "@formbricks/lib/i18n/utils"; import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside"; -import type { TLanguage } from "@formbricks/types/product"; +import type { TLanguage } from "@formbricks/types/project"; import { TUserLocale } from "@formbricks/types/user"; interface LanguageSelectProps { @@ -70,7 +70,7 @@ export function LanguageSelect({ language, onLanguageChange, disabled, locale }: onChange={(e) => { setSearchTerm(e.target.value); }} - placeholder={t("environments.product.languages.search_items")} + placeholder={t("environments.project.languages.search_items")} ref={inputRef} type="text" value={searchTerm} diff --git a/apps/web/modules/ee/multi-language-surveys/components/language-toggle.tsx b/apps/web/modules/ee/multi-language-surveys/components/language-toggle.tsx index 3a2d4b6cdc..42a443066b 100644 --- a/apps/web/modules/ee/multi-language-surveys/components/language-toggle.tsx +++ b/apps/web/modules/ee/multi-language-surveys/components/language-toggle.tsx @@ -2,7 +2,7 @@ import { Label } from "@/modules/ui/components/label"; import { Switch } from "@/modules/ui/components/switch"; import { useTranslations } from "next-intl"; import { getLanguageLabel } from "@formbricks/lib/i18n/utils"; -import type { TLanguage } from "@formbricks/types/product"; +import type { TLanguage } from "@formbricks/types/project"; import type { TUserLocale } from "@formbricks/types/user"; interface LanguageToggleProps { diff --git a/apps/web/modules/ee/multi-language-surveys/components/localized-editor.tsx b/apps/web/modules/ee/multi-language-surveys/components/localized-editor.tsx index 0c23073ad2..021c189fa5 100644 --- a/apps/web/modules/ee/multi-language-surveys/components/localized-editor.tsx +++ b/apps/web/modules/ee/multi-language-surveys/components/localized-editor.tsx @@ -94,7 +94,7 @@ export function LocalizedEditor({ {value && selectedLanguageCode !== "default" && value.default ? (
    - {t("environments.product.languages.translate")}: + {t("environments.project.languages.translate")}:
    ) : null}
    diff --git a/apps/web/modules/ee/multi-language-surveys/components/multi-language-card.tsx b/apps/web/modules/ee/multi-language-surveys/components/multi-language-card.tsx index b4183d49d5..b31d9687e3 100644 --- a/apps/web/modules/ee/multi-language-surveys/components/multi-language-card.tsx +++ b/apps/web/modules/ee/multi-language-surveys/components/multi-language-card.tsx @@ -15,7 +15,7 @@ import type { FC } from "react"; import { useEffect, useMemo, useState } from "react"; import { cn } from "@formbricks/lib/cn"; import { addMultiLanguageLabels, extractLanguageCodes } from "@formbricks/lib/i18n/utils"; -import type { TLanguage, TProduct } from "@formbricks/types/product"; +import type { TLanguage, TProject } from "@formbricks/types/project"; import type { TSurvey, TSurveyLanguage, TSurveyQuestionId } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; import { DefaultLanguageSelect } from "./default-language-select"; @@ -23,7 +23,7 @@ import { SecondaryLanguageSelect } from "./secondary-language-select"; interface MultiLanguageCardProps { localSurvey: TSurvey; - product: TProduct; + project: TProject; setLocalSurvey: (survey: TSurvey) => void; activeQuestionId: TSurveyQuestionId | null; setActiveQuestionId: (questionId: TSurveyQuestionId | null) => void; @@ -44,7 +44,7 @@ export interface ConfirmationModalProps { export const MultiLanguageCard: FC = ({ activeQuestionId, - product, + project, localSurvey, setActiveQuestionId, setLocalSurvey, @@ -120,7 +120,7 @@ export const MultiLanguageCard: FC = ({ }; const handleDefaultLanguageChange = (languageCode: string) => { - const language = product.languages.find((lang) => lang.code === languageCode); + const language = project.languages.find((lang) => lang.code === languageCode); if (language) { let languageExists = false; @@ -213,7 +213,7 @@ export const MultiLanguageCard: FC = ({ { handleActivationSwitchLogic(); @@ -238,16 +238,16 @@ export const MultiLanguageCard: FC = ({ /> ) : ( <> - {product.languages.length <= 1 && ( + {project.languages.length <= 1 && (
    - {product.languages.length === 0 + {project.languages.length === 0 ? t("environments.surveys.edit.no_languages_found_add_first_one_to_get_started") : t( - "environments.surveys.edit.you_need_to_have_two_or_more_languages_set_up_in_your_product_to_work_with_translations" + "environments.surveys.edit.you_need_to_have_two_or_more_languages_set_up_in_your_project_to_work_with_translations" )}
    )} - {product.languages.length > 1 && ( + {project.languages.length > 1 && (
    {isMultiLanguageAllowed && !isMultiLanguageActivated ? ( @@ -262,7 +262,7 @@ export const MultiLanguageCard: FC = ({ @@ -270,7 +270,7 @@ export const MultiLanguageCard: FC = ({ = ({
    )} - +