mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-18 04:49:17 -06:00
feat: Product Model Revamp (#4353)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com> Co-authored-by: Matthias Nannt <mail@matthiasnannt.com> Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
@@ -29,11 +29,11 @@ App surveys have 6-10x better conversion rates than emailed surveys. This tutori
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2. **Choose your Product Channel**: On this step, you have to choose between the various channels that you want your product to be created in, you can create both app and link surveys from all the channels, but for the onboarding, please choose between the app surveys or the public website options, upon doing this, you'll be prompted to connect your app / website to formbricks.
|
||||
2. **Choose your Project Channel**: On this step, you have to choose between the various channels that you want your project to be created in, you can create both app and link surveys from all the channels, but for the onboarding, please choose between the app surveys or the public website options, upon doing this, you'll be prompted to connect your app / website to formbricks.
|
||||
|
||||
<MdxImage
|
||||
src={I2}
|
||||
alt="Choose between app and website surveys product channels"
|
||||
alt="Choose between app and website surveys project channels"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
@@ -62,7 +62,7 @@ Available Recontact Options include:
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## Product-wide Global Waiting Time
|
||||
## Project-wide Global Waiting Time
|
||||
|
||||
The Global Waiting Time is a universal blocker to make sure that no user sees too many surveys. This is particularly helpful when several teams of large organisations use Formbricks at the same time.
|
||||
|
||||
@@ -71,13 +71,13 @@ The Global Waiting Time is a universal blocker to make sure that no user sees to
|
||||
To adjust the Global Waiting Time:
|
||||
|
||||
1. Visit Formbricks Settings
|
||||
2. Go to Product Settings
|
||||
2. Go to Project Settings
|
||||
3. Find the **Recontact Waiting Time** section
|
||||
4. Modify the interval (in days) as needed.
|
||||
|
||||
<MdxImage
|
||||
src={GlobalWaitTime}
|
||||
alt="Formbricks Product-Wide Wait Time"
|
||||
alt="Formbricks Project-Wide Wait Time"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -38,8 +38,7 @@ To display the Trial Conversion Survey in your app you want to proceed as follow
|
||||
3. Print that 💸
|
||||
|
||||
<Note>
|
||||
## Formbricks Widget running?
|
||||
We assume that you have already installed the Formbricks Widget in your web app. 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)
|
||||
</Note>
|
||||
|
||||
### 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.
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ Click on "Create Survey" and choose the template “Interview Prompt”:
|
||||
|
||||
### 2. Update prompt and CTA
|
||||
|
||||
Update the prompt, description and button text to match your products tonality. You can also update the button color in the Product Settings.
|
||||
Update the prompt, description and button text to match your products tonality. You can also update the button color in the Project Settings.
|
||||
|
||||
<MdxImage
|
||||
src={ChangeText}
|
||||
|
||||
@@ -66,7 +66,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 where the magic happens: The “Audience” tab.
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ Hit the below request to verify that you are authenticated with your API Key and
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Get the product details and environment type of your account.
|
||||
Get the project details and environment type of your account.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
@@ -115,9 +115,9 @@ Hit the below request to verify that you are authenticated with your API Key and
|
||||
"createdAt": "2023-08-08T18:04:59.922Z",
|
||||
"updatedAt": "2023-08-08T18:04:59.922Z",
|
||||
"type": "production",
|
||||
"product": {
|
||||
"project": {
|
||||
"id": "cll2m30r60003mx0hnemjfckr",
|
||||
"name": "My Product"
|
||||
"name": "My Project"
|
||||
},
|
||||
"appSetupCompleted": false,
|
||||
"websiteSetupCompleted": false,
|
||||
|
||||
@@ -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 | ✅ | ✅ | ❌ | ✅\* |
|
||||
|
||||
@@ -59,11 +59,11 @@ How to deliver a specific language depends on the survey type (app or link surve
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
3. Select the preferred language from the dropdown and assign an identifier Alias. Click the **Add language** button to add the language to your product.
|
||||
3. Select the preferred language from the dropdown and assign an identifier Alias. Click the **Add language** button to add the language to your project.
|
||||
|
||||
<MdxImage
|
||||
src={AddLanguages}
|
||||
alt="Add Multiple Languages to your Product"
|
||||
alt="Add Multiple Languages to your Project"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
@@ -73,7 +73,7 @@ You can come back to this page anytime to add more languages or remove existing
|
||||
|
||||
<MdxImage
|
||||
src={SurveysHome}
|
||||
alt="Add Multiple Languages to your Product"
|
||||
alt="Add Multiple Languages to your Project"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
@@ -19,8 +19,9 @@ export const metadata = {
|
||||
|
||||
Overwrite the global styling theme for individual surveys to create unique styles for each survey.
|
||||
|
||||
<Note>To set a styling theme for all surveys, please see the [Styling Theme](/global/styling-theme) manual. </Note>
|
||||
|
||||
<Note>
|
||||
To set a styling theme for all surveys, please see the [Styling Theme](/global/styling-theme) manual.{" "}
|
||||
</Note>
|
||||
|
||||
### Overwrite Styling Theme
|
||||
|
||||
@@ -30,16 +31,16 @@ Overwrite the global styling theme for individual surveys to create unique style
|
||||
src={StepNine}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2. Activate the **Add Custom Styles** toggle to override the default product styling:
|
||||
2. Activate the **Add Custom Styles** toggle to override the default project styling:
|
||||
|
||||
<MdxImage
|
||||
src={StepTen}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
3. Customize your survey's style as needed:
|
||||
@@ -48,7 +49,7 @@ Overwrite the global styling theme for individual surveys to create unique style
|
||||
src={StepEleven}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Voila! just hit the save button to apply your changes. Your survey is now ready to impress with its unique look!
|
||||
@@ -98,7 +99,7 @@ We have an example of this in our [Demo project](https://github.com/formbricks/f
|
||||
src={Mario}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
- **Hipster Living**: Does your monstera get enough water?
|
||||
@@ -107,7 +108,7 @@ We have an example of this in our [Demo project](https://github.com/formbricks/f
|
||||
src={HipsterLiving}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
- **Windows XP**: Hach, nostalgia. Made us wanna play Mafia.
|
||||
@@ -116,7 +117,7 @@ We have an example of this in our [Demo project](https://github.com/formbricks/f
|
||||
src={WindowsXp}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
- **Whosagooooodbooooy**: Things you've likely said to your dog.
|
||||
@@ -125,7 +126,7 @@ We have an example of this in our [Demo project](https://github.com/formbricks/f
|
||||
src={Doggo}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
@@ -48,7 +48,7 @@ In the left side bar, you find the `Configuration` page. On this page you find t
|
||||
src={FormSettings}
|
||||
alt="Form styling options UI"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
- **Brand Color**: Sets the primary color tone of the survey.
|
||||
@@ -62,7 +62,7 @@ In the left side bar, you find the `Configuration` page. On this page you find t
|
||||
src={CardSettings}
|
||||
alt="Card styling options UI"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
- **Roundness**: Adjusts the corner roundness of the survey card and its components (including input boxes, buttons).
|
||||
@@ -80,7 +80,7 @@ In the left side bar, you find the `Configuration` page. On this page you find t
|
||||
src={BackgroundSettings}
|
||||
alt="Background styling options UI"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
- **Color**: Pick any color for the background
|
||||
@@ -95,13 +95,13 @@ Customize your survey with your brand's logo.
|
||||
|
||||
<Note>Brand logos are only visible on Link Survey pages.</Note>
|
||||
|
||||
1. In the Look & Feel page itself in Product settings, scroll down to see the Logo Upload box.
|
||||
1. In the Look & Feel page itself in Project settings, scroll down to see the Logo Upload box.
|
||||
|
||||
<MdxImage
|
||||
src={StepFour}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2. Upload your logo
|
||||
@@ -110,7 +110,7 @@ Customize your survey with your brand's logo.
|
||||
src={StepFive}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
3. Add a background color: If 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"
|
||||
/>
|
||||
|
||||
<Note>The logo settings apply across all Link Surveys pages.</Note>
|
||||
|
||||
@@ -7,14 +7,14 @@ import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProductConfigChannel } from "@formbricks/types/product";
|
||||
import { TProjectConfigChannel } from "@formbricks/types/project";
|
||||
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
||||
|
||||
interface ConnectWithFormbricksProps {
|
||||
environment: TEnvironment;
|
||||
webAppUrl: string;
|
||||
widgetSetupCompleted: boolean;
|
||||
channel: TProductConfigChannel;
|
||||
channel: TProjectConfigChannel;
|
||||
}
|
||||
|
||||
export const ConnectWithFormbricks = ({
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useTranslations } from "next-intl";
|
||||
import "prismjs/themes/prism.css";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { TProductConfigChannel } from "@formbricks/types/product";
|
||||
import { TProjectConfigChannel } from "@formbricks/types/project";
|
||||
|
||||
const tabs = [
|
||||
{ id: "html", label: "HTML", icon: <Html5Icon /> },
|
||||
@@ -18,7 +18,7 @@ const tabs = [
|
||||
interface OnboardingSetupInstructionsProps {
|
||||
environmentId: string;
|
||||
webAppUrl: string;
|
||||
channel: TProductConfigChannel;
|
||||
channel: TProjectConfigChannel;
|
||||
widgetSetupCompleted: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { XIcon } from "lucide-react";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
|
||||
interface ConnectPageProps {
|
||||
params: Promise<{
|
||||
@@ -22,12 +22,12 @@ const Page = async (props: ConnectPageProps) => {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
|
||||
const product = await getProductByEnvironmentId(environment.id);
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
const project = await getProjectByEnvironmentId(environment.id);
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const channel = product.config.channel || null;
|
||||
const channel = project.config.channel || null;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full flex-col items-center justify-center py-10">
|
||||
|
||||
@@ -10,18 +10,18 @@ import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurveyCreateInput } from "@formbricks/types/surveys/types";
|
||||
import { TXMTemplate } from "@formbricks/types/templates";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
|
||||
interface XMTemplateListProps {
|
||||
product: TProduct;
|
||||
project: TProject;
|
||||
user: TUser;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export const XMTemplateList = ({ product, user, environmentId }: XMTemplateListProps) => {
|
||||
export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListProps) => {
|
||||
const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null);
|
||||
const t = useTranslations();
|
||||
const router = useRouter();
|
||||
@@ -48,7 +48,7 @@ export const XMTemplateList = ({ product, user, environmentId }: XMTemplateListP
|
||||
const handleTemplateClick = (templateIdx) => {
|
||||
setActiveTemplateId(templateIdx);
|
||||
const template = getXMTemplates(user.locale)[templateIdx];
|
||||
const newTemplate = replacePresetPlaceholders(template, product);
|
||||
const newTemplate = replacePresetPlaceholders(template, project);
|
||||
createSurvey(newTemplate);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { replaceQuestionPresetPlaceholders } from "@formbricks/lib/utils/templates";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TXMTemplate } from "@formbricks/types/templates";
|
||||
|
||||
// replace all occurences of productName with the actual product name in the current template
|
||||
export const replacePresetPlaceholders = (template: TXMTemplate, product: TProduct) => {
|
||||
// replace all occurences of projectName with the actual project name in the current template
|
||||
export const replacePresetPlaceholders = (template: TXMTemplate, project: TProject) => {
|
||||
const survey = structuredClone(template);
|
||||
survey.name = survey.name.replace("{{productName}}", product.name);
|
||||
survey.name = survey.name.replace("{{projectName}}", project.name);
|
||||
survey.questions = survey.questions.map((question) => {
|
||||
return replaceQuestionPresetPlaceholders(question, product);
|
||||
return replaceQuestionPresetPlaceholders(question, project);
|
||||
});
|
||||
return { ...template, ...survey };
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { XIcon } from "lucide-react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId, getUserProducts } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId, getUserProjects } from "@formbricks/lib/project/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
|
||||
interface XMTemplatePageProps {
|
||||
@@ -35,18 +35,18 @@ const Page = async (props: XMTemplatePageProps) => {
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
|
||||
|
||||
const product = await getProductByEnvironmentId(environment.id);
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
const project = await getProjectByEnvironmentId(environment.id);
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const products = await getUserProducts(session.user.id, organizationId);
|
||||
const projects = await getUserProjects(session.user.id, organizationId);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
<Header title={t("environments.xm-templates.headline")} />
|
||||
<XMTemplateList product={product} user={user} environmentId={environment.id} />
|
||||
{products.length >= 2 && (
|
||||
<XMTemplateList project={project} user={user} environmentId={environment.id} />
|
||||
{projects.length >= 2 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="minimal"
|
||||
|
||||
@@ -26,12 +26,12 @@ export const getTeamsByOrganizationId = reactCache(
|
||||
},
|
||||
});
|
||||
|
||||
const productTeams = teams.map((team) => ({
|
||||
const projectTeams = teams.map((team) => ({
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
}));
|
||||
|
||||
return productTeams;
|
||||
return projectTeams;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { TProductConfigChannel } from "@formbricks/types/product";
|
||||
import { TProjectConfigChannel } from "@formbricks/types/project";
|
||||
|
||||
export const getCustomHeadline = (channel?: TProductConfigChannel) => {
|
||||
export const getCustomHeadline = (channel?: TProjectConfigChannel) => {
|
||||
switch (channel) {
|
||||
case "website":
|
||||
return "organizations.products.new.settings.website_channel_headline";
|
||||
return "organizations.projects.new.settings.website_channel_headline";
|
||||
case "app":
|
||||
return "organizations.products.new.settings.app_channel_headline";
|
||||
return "organizations.projects.new.settings.app_channel_headline";
|
||||
default:
|
||||
return "organizations.products.new.settings.link_channel_headline";
|
||||
return "organizations.projects.new.settings.link_channel_headline";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getServerSession } from "next-auth";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getEnvironments } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getUserProducts } from "@formbricks/lib/product/service";
|
||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
||||
|
||||
const LandingLayout = async (props) => {
|
||||
const params = await props.params;
|
||||
@@ -21,11 +21,11 @@ const LandingLayout = async (props) => {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const products = await getUserProducts(session.user.id, params.organizationId);
|
||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
||||
|
||||
if (products.length !== 0) {
|
||||
const firstProduct = products[0];
|
||||
const environments = await getEnvironments(firstProduct.id);
|
||||
if (projects.length !== 0) {
|
||||
const firstProject = projects[0];
|
||||
const environments = await getEnvironments(firstProject.id);
|
||||
const prodEnvironment = environments.find((e) => e.type === "production");
|
||||
|
||||
if (prodEnvironment) {
|
||||
|
||||
@@ -39,8 +39,8 @@ const Page = async (props) => {
|
||||
<div className="flex-1">
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-12">
|
||||
<Header
|
||||
title={t("organizations.landing.no_products_warning_title")}
|
||||
subtitle={t("organizations.landing.no_products_warning_subtitle")}
|
||||
title={t("organizations.landing.no_projects_warning_title")}
|
||||
subtitle={t("organizations.landing.no_projects_warning_subtitle")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
|
||||
const ProductOnboardingLayout = async (props) => {
|
||||
const ProjectOnboardingLayout = async (props) => {
|
||||
const params = await props.params;
|
||||
|
||||
const { children } = props;
|
||||
@@ -50,4 +50,4 @@ const ProductOnboardingLayout = async (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductOnboardingLayout;
|
||||
export default ProjectOnboardingLayout;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { GlobeIcon, GlobeLockIcon, LinkIcon, XIcon } from "lucide-react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getUserProducts } from "@formbricks/lib/product/service";
|
||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
||||
|
||||
interface ChannelPageProps {
|
||||
params: Promise<{
|
||||
@@ -24,39 +24,39 @@ const Page = async (props: ChannelPageProps) => {
|
||||
const t = await getTranslations();
|
||||
const channelOptions = [
|
||||
{
|
||||
title: t("organizations.products.new.channel.public_website"),
|
||||
description: t("organizations.products.new.channel.public_website_description"),
|
||||
title: t("organizations.projects.new.channel.public_website"),
|
||||
description: t("organizations.projects.new.channel.public_website_description"),
|
||||
icon: GlobeIcon,
|
||||
iconText: t("organizations.products.new.channel.public_website_icon_text"),
|
||||
href: `/organizations/${params.organizationId}/products/new/settings?channel=website`,
|
||||
iconText: t("organizations.projects.new.channel.public_website_icon_text"),
|
||||
href: `/organizations/${params.organizationId}/projects/new/settings?channel=website`,
|
||||
},
|
||||
{
|
||||
title: t("organizations.products.new.channel.app_with_sign_up"),
|
||||
description: t("organizations.products.new.channel.app_with_sign_up_description"),
|
||||
title: t("organizations.projects.new.channel.app_with_sign_up"),
|
||||
description: t("organizations.projects.new.channel.app_with_sign_up_description"),
|
||||
icon: GlobeLockIcon,
|
||||
iconText: t("organizations.products.new.channel.app_with_sign_up_icon_text"),
|
||||
href: `/organizations/${params.organizationId}/products/new/settings?channel=app`,
|
||||
iconText: t("organizations.projects.new.channel.app_with_sign_up_icon_text"),
|
||||
href: `/organizations/${params.organizationId}/projects/new/settings?channel=app`,
|
||||
},
|
||||
{
|
||||
channel: "link",
|
||||
title: t("organizations.products.new.channel.link_and_email_surveys"),
|
||||
description: t("organizations.products.new.channel.link_and_email_surveys_description"),
|
||||
title: t("organizations.projects.new.channel.link_and_email_surveys"),
|
||||
description: t("organizations.projects.new.channel.link_and_email_surveys_description"),
|
||||
icon: LinkIcon,
|
||||
iconText: t("organizations.products.new.channel.link_and_email_surveys_icon_text"),
|
||||
href: `/organizations/${params.organizationId}/products/new/settings?channel=link`,
|
||||
iconText: t("organizations.projects.new.channel.link_and_email_surveys_icon_text"),
|
||||
href: `/organizations/${params.organizationId}/projects/new/settings?channel=link`,
|
||||
},
|
||||
];
|
||||
|
||||
const products = await getUserProducts(session.user.id, params.organizationId);
|
||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
<Header
|
||||
title={t("organizations.products.new.channel.channel_select_title")}
|
||||
subtitle={t("organizations.products.new.channel.channel_select_subtitle")}
|
||||
title={t("organizations.projects.new.channel.channel_select_title")}
|
||||
subtitle={t("organizations.projects.new.channel.channel_select_subtitle")}
|
||||
/>
|
||||
<OnboardingOptionsContainer options={channelOptions} />
|
||||
{products.length >= 1 && (
|
||||
{projects.length >= 1 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="minimal"
|
||||
@@ -1,13 +1,18 @@
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { getOrganizationProjectsCount } from "@formbricks/lib/project/service";
|
||||
|
||||
const OnboardingLayout = async (props) => {
|
||||
const params = await props.params;
|
||||
|
||||
const { children } = props;
|
||||
const t = await getTranslations();
|
||||
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session || !session.user) {
|
||||
@@ -18,6 +23,18 @@ const OnboardingLayout = async (props) => {
|
||||
const { isMember, isBilling } = getAccessFlags(membership?.role);
|
||||
if (isMember || isBilling) return notFound();
|
||||
|
||||
const organization = await getOrganization(params.organizationId);
|
||||
if (!organization) {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
const organizationProjectsLimit = await getOrganizationProjectsLimit(organization);
|
||||
const organizationProjectsCount = await getOrganizationProjectsCount(organization.id);
|
||||
|
||||
if (organizationProjectsCount >= organizationProjectsLimit) {
|
||||
return redirect(`/`);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getUserProducts } from "@formbricks/lib/product/service";
|
||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
||||
|
||||
interface ModePageProps {
|
||||
params: Promise<{
|
||||
@@ -24,26 +24,26 @@ const Page = async (props: ModePageProps) => {
|
||||
const t = await getTranslations();
|
||||
const channelOptions = [
|
||||
{
|
||||
title: t("organizations.products.new.mode.formbricks_surveys"),
|
||||
description: t("organizations.products.new.mode.formbricks_surveys_description"),
|
||||
title: t("organizations.projects.new.mode.formbricks_surveys"),
|
||||
description: t("organizations.projects.new.mode.formbricks_surveys_description"),
|
||||
icon: ListTodoIcon,
|
||||
href: `/organizations/${params.organizationId}/products/new/channel`,
|
||||
href: `/organizations/${params.organizationId}/projects/new/channel`,
|
||||
},
|
||||
{
|
||||
title: t("organizations.products.new.mode.formbricks_cx"),
|
||||
description: t("organizations.products.new.mode.formbricks_cx_description"),
|
||||
title: t("organizations.projects.new.mode.formbricks_cx"),
|
||||
description: t("organizations.projects.new.mode.formbricks_cx_description"),
|
||||
icon: HeartIcon,
|
||||
href: `/organizations/${params.organizationId}/products/new/settings?mode=cx`,
|
||||
href: `/organizations/${params.organizationId}/projects/new/settings?mode=cx`,
|
||||
},
|
||||
];
|
||||
|
||||
const products = await getUserProducts(session.user.id, params.organizationId);
|
||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
<Header title={t("organizations.products.new.mode.what_are_you_here_for")} />
|
||||
<Header title={t("organizations.projects.new.mode.what_are_you_here_for")} />
|
||||
<OnboardingOptionsContainer options={channelOptions} />
|
||||
{products.length >= 1 && (
|
||||
{projects.length >= 1 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="minimal"
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createProductAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { TOrganizationTeam } from "@/modules/ee/teams/product-teams/types/teams";
|
||||
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/teams";
|
||||
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { ColorPicker } from "@/modules/ui/components/color-picker";
|
||||
@@ -28,41 +28,41 @@ import { toast } from "react-hot-toast";
|
||||
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
|
||||
import { getPreviewSurvey } from "@formbricks/lib/styling/constants";
|
||||
import {
|
||||
TProductConfigChannel,
|
||||
TProductConfigIndustry,
|
||||
TProductMode,
|
||||
TProductUpdateInput,
|
||||
ZProductUpdateInput,
|
||||
} from "@formbricks/types/product";
|
||||
TProjectConfigChannel,
|
||||
TProjectConfigIndustry,
|
||||
TProjectMode,
|
||||
TProjectUpdateInput,
|
||||
ZProjectUpdateInput,
|
||||
} from "@formbricks/types/project";
|
||||
|
||||
interface ProductSettingsProps {
|
||||
interface ProjectSettingsProps {
|
||||
organizationId: string;
|
||||
productMode: TProductMode;
|
||||
channel: TProductConfigChannel;
|
||||
industry: TProductConfigIndustry;
|
||||
projectMode: TProjectMode;
|
||||
channel: TProjectConfigChannel;
|
||||
industry: TProjectConfigIndustry;
|
||||
defaultBrandColor: string;
|
||||
organizationTeams: TOrganizationTeam[];
|
||||
canDoRoleManagement: boolean;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export const ProductSettings = ({
|
||||
export const ProjectSettings = ({
|
||||
organizationId,
|
||||
productMode,
|
||||
projectMode,
|
||||
channel,
|
||||
industry,
|
||||
defaultBrandColor,
|
||||
organizationTeams,
|
||||
canDoRoleManagement = false,
|
||||
locale,
|
||||
}: ProductSettingsProps) => {
|
||||
}: ProjectSettingsProps) => {
|
||||
const [createTeamModalOpen, setCreateTeamModalOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const addProduct = async (data: TProductUpdateInput) => {
|
||||
const addProject = async (data: TProjectUpdateInput) => {
|
||||
try {
|
||||
const createProductResponse = await createProductAction({
|
||||
const createProjectResponse = await createProjectAction({
|
||||
organizationId,
|
||||
data: {
|
||||
...data,
|
||||
@@ -71,14 +71,14 @@ export const ProductSettings = ({
|
||||
},
|
||||
});
|
||||
|
||||
if (createProductResponse?.data) {
|
||||
if (createProjectResponse?.data) {
|
||||
// get production environment
|
||||
const productionEnvironment = createProductResponse.data.environments.find(
|
||||
const productionEnvironment = createProjectResponse.data.environments.find(
|
||||
(environment) => environment.type === "production"
|
||||
);
|
||||
if (productionEnvironment) {
|
||||
if (typeof window !== "undefined") {
|
||||
// Rmove filters when creating a new product
|
||||
// Rmove filters when creating a new project
|
||||
localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
|
||||
}
|
||||
}
|
||||
@@ -86,26 +86,26 @@ export const ProductSettings = ({
|
||||
router.push(`/environments/${productionEnvironment?.id}/connect`);
|
||||
} else if (channel === "link") {
|
||||
router.push(`/environments/${productionEnvironment?.id}/surveys`);
|
||||
} else if (productMode === "cx") {
|
||||
} else if (projectMode === "cx") {
|
||||
router.push(`/environments/${productionEnvironment?.id}/xm-templates`);
|
||||
}
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createProductResponse);
|
||||
const errorMessage = getFormattedErrorMessage(createProjectResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Product creation failed");
|
||||
toast.error(t("organizations.projects.new.settings.project_creation_failed"));
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const form = useForm<TProductUpdateInput>({
|
||||
const form = useForm<TProjectUpdateInput>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
styling: { allowStyleOverwrite: true, brandColor: { light: defaultBrandColor } },
|
||||
teamIds: [],
|
||||
},
|
||||
resolver: zodResolver(ZProductUpdateInput),
|
||||
resolver: zodResolver(ZProjectUpdateInput),
|
||||
});
|
||||
const logoUrl = form.watch("logo.url");
|
||||
const brandColor = form.watch("styling.brandColor.light") ?? defaultBrandColor;
|
||||
@@ -120,16 +120,16 @@ export const ProductSettings = ({
|
||||
<div className="mt-6 flex w-5/6 space-x-10 lg:w-2/3 2xl:w-1/2">
|
||||
<div className="flex w-1/2 flex-col space-y-4">
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(addProduct)} className="w-full space-y-4">
|
||||
<form onSubmit={form.handleSubmit(addProject)} className="w-full space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="styling.brandColor.light"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormItem className="w-full space-y-4">
|
||||
<div>
|
||||
<FormLabel>{t("organizations.products.new.settings.brand_color")}</FormLabel>
|
||||
<FormLabel>{t("organizations.projects.new.settings.brand_color")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("organizations.products.new.settings.brand_color_description")}
|
||||
{t("organizations.projects.new.settings.brand_color_description")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
@@ -151,9 +151,9 @@ export const ProductSettings = ({
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormItem className="w-full space-y-4">
|
||||
<div>
|
||||
<FormLabel>{t("organizations.products.new.settings.product_name")}</FormLabel>
|
||||
<FormLabel>{t("organizations.projects.new.settings.project_name")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("organizations.products.new.settings.product_name_description")}
|
||||
{t("organizations.projects.new.settings.project_name_description")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
@@ -181,14 +181,14 @@ export const ProductSettings = ({
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<FormLabel>Teams</FormLabel>
|
||||
<FormDescription>Who all can access this product?</FormDescription>
|
||||
<FormDescription>Who all can access this project?</FormDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => setCreateTeamModalOpen(true)}>
|
||||
{t("organizations.products.new.settings.create_new_team")}
|
||||
{t("organizations.projects.new.settings.create_new_team")}
|
||||
</Button>
|
||||
</div>
|
||||
<FormControl>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
||||
import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils";
|
||||
import { ProductSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/components/ProductSettings";
|
||||
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -11,22 +11,22 @@ import { getTranslations } from "next-intl/server";
|
||||
import { redirect } from "next/navigation";
|
||||
import { DEFAULT_BRAND_COLOR, DEFAULT_LOCALE } from "@formbricks/lib/constants";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { getUserProducts } from "@formbricks/lib/product/service";
|
||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
||||
import { getUserLocale } from "@formbricks/lib/user/service";
|
||||
import { TProductConfigChannel, TProductConfigIndustry, TProductMode } from "@formbricks/types/product";
|
||||
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
||||
|
||||
interface ProductSettingsPageProps {
|
||||
interface ProjectSettingsPageProps {
|
||||
params: Promise<{
|
||||
organizationId: string;
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
channel?: TProductConfigChannel;
|
||||
industry?: TProductConfigIndustry;
|
||||
mode?: TProductMode;
|
||||
channel?: TProjectConfigChannel;
|
||||
industry?: TProjectConfigIndustry;
|
||||
mode?: TProjectMode;
|
||||
}>;
|
||||
}
|
||||
|
||||
const Page = async (props: ProductSettingsPageProps) => {
|
||||
const Page = async (props: ProjectSettingsPageProps) => {
|
||||
const searchParams = await props.searchParams;
|
||||
const params = await props.params;
|
||||
const t = await getTranslations();
|
||||
@@ -41,7 +41,7 @@ const Page = async (props: ProductSettingsPageProps) => {
|
||||
const mode = searchParams.mode || "surveys";
|
||||
const locale = session?.user.id ? await getUserLocale(session.user.id) : undefined;
|
||||
const customHeadline = getCustomHeadline(channel);
|
||||
const products = await getUserProducts(session.user.id, params.organizationId);
|
||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
||||
|
||||
const organizationTeams = await getTeamsByOrganizationId(params.organizationId);
|
||||
|
||||
@@ -61,18 +61,18 @@ const Page = async (props: ProductSettingsPageProps) => {
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
{channel === "link" || mode === "cx" ? (
|
||||
<Header
|
||||
title={t("organizations.products.new.settings.channel_settings_title")}
|
||||
subtitle={t("organizations.products.new.settings.channel_settings_subtitle")}
|
||||
title={t("organizations.projects.new.settings.channel_settings_title")}
|
||||
subtitle={t("organizations.projects.new.settings.channel_settings_subtitle")}
|
||||
/>
|
||||
) : (
|
||||
<Header
|
||||
title={t(customHeadline)}
|
||||
subtitle={t("organizations.products.new.settings.channel_settings_description")}
|
||||
subtitle={t("organizations.projects.new.settings.channel_settings_description")}
|
||||
/>
|
||||
)}
|
||||
<ProductSettings
|
||||
<ProjectSettings
|
||||
organizationId={params.organizationId}
|
||||
productMode={mode}
|
||||
projectMode={mode}
|
||||
channel={channel}
|
||||
industry={industry}
|
||||
defaultBrandColor={DEFAULT_BRAND_COLOR}
|
||||
@@ -80,7 +80,7 @@ const Page = async (props: ProductSettingsPageProps) => {
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
locale={locale ?? DEFAULT_LOCALE}
|
||||
/>
|
||||
{products.length >= 1 && (
|
||||
{projects.length >= 1 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="minimal"
|
||||
@@ -4,12 +4,12 @@ import { actionClient, authenticatedActionClient } from "@/lib/utils/action-clie
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import {
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getOrganizationIdFromProductId,
|
||||
getOrganizationIdFromProjectId,
|
||||
getOrganizationIdFromSegmentId,
|
||||
getOrganizationIdFromSurveyId,
|
||||
getProductIdFromEnvironmentId,
|
||||
getProductIdFromSegmentId,
|
||||
getProductIdFromSurveyId,
|
||||
getProjectIdFromEnvironmentId,
|
||||
getProjectIdFromSegmentId,
|
||||
getProjectIdFromSurveyId,
|
||||
} from "@/lib/utils/helper";
|
||||
import { getSegment, getSurvey } from "@/lib/utils/services";
|
||||
import { getSurveyFollowUpsPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
@@ -18,7 +18,7 @@ import { z } from "zod";
|
||||
import { createActionClass } from "@formbricks/lib/actionClass/service";
|
||||
import { UNSPLASH_ACCESS_KEY, UNSPLASH_ALLOWED_DOMAINS } from "@formbricks/lib/constants";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { getProduct } from "@formbricks/lib/product/service";
|
||||
import { getProject } from "@formbricks/lib/project/service";
|
||||
import {
|
||||
cloneSegment,
|
||||
createSegment,
|
||||
@@ -66,8 +66,8 @@ export const updateSurveyAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
productId: await getProductIdFromSurveyId(parsedInput.id),
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.id),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
@@ -84,30 +84,30 @@ export const updateSurveyAction = authenticatedActionClient
|
||||
return await updateSurvey(parsedInput);
|
||||
});
|
||||
|
||||
const ZRefetchProductAction = z.object({
|
||||
productId: ZId,
|
||||
const ZRefetchProjectAction = z.object({
|
||||
projectId: ZId,
|
||||
});
|
||||
|
||||
export const refetchProductAction = authenticatedActionClient
|
||||
.schema(ZRefetchProductAction)
|
||||
export const refetchProjectAction = authenticatedActionClient
|
||||
.schema(ZRefetchProjectAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromProductId(parsedInput.productId),
|
||||
organizationId: await getOrganizationIdFromProjectId(parsedInput.projectId),
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
productId: parsedInput.productId,
|
||||
projectId: parsedInput.projectId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return await getProduct(parsedInput.productId);
|
||||
return await getProject(parsedInput.projectId);
|
||||
});
|
||||
|
||||
const ZCreateBasicSegmentAction = z.object({
|
||||
@@ -141,9 +141,9 @@ export const createBasicSegmentAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -188,9 +188,9 @@ export const updateBasicSegmentAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
productId: await getProductIdFromSegmentId(parsedInput.segmentId),
|
||||
projectId: await getProjectIdFromSegmentId(parsedInput.segmentId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -242,9 +242,9 @@ export const loadNewBasicSegmentAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -285,9 +285,9 @@ export const cloneBasicSegmentAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -311,9 +311,9 @@ export const resetBasicSegmentFiltersAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -416,9 +416,9 @@ export const createActionClassAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
productId: await getProductIdFromEnvironmentId(parsedInput.action.environmentId),
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.action.environmentId),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -13,16 +13,16 @@ import {
|
||||
getQuestionTypes,
|
||||
universalQuestionPresets,
|
||||
} from "@formbricks/lib/utils/questions";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
|
||||
interface AddQuestionButtonProps {
|
||||
addQuestion: (question: any) => void;
|
||||
product: TProduct;
|
||||
project: TProject;
|
||||
isCxMode: boolean;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export const AddQuestionButton = ({ addQuestion, product, isCxMode, locale }: AddQuestionButtonProps) => {
|
||||
export const AddQuestionButton = ({ addQuestion, project, isCxMode, locale }: AddQuestionButtonProps) => {
|
||||
const t = useTranslations();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [hoveredQuestionId, setHoveredQuestionId] = useState<string | null>(null);
|
||||
@@ -60,7 +60,7 @@ export const AddQuestionButton = ({ addQuestion, product, isCxMode, locale }: Ad
|
||||
onClick={() => {
|
||||
addQuestion({
|
||||
...universalQuestionPresets,
|
||||
...getQuestionDefaults(questionType.id, product, locale),
|
||||
...getQuestionDefaults(questionType.id, project, locale),
|
||||
id: createId(),
|
||||
type: questionType.id,
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { CheckIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TProductStyling } from "@formbricks/types/product";
|
||||
import { TProjectStyling } from "@formbricks/types/project";
|
||||
import { TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { SurveyBgSelectorTab } from "./SurveyBgSelectorTab";
|
||||
|
||||
@@ -21,7 +21,7 @@ interface BackgroundStylingCardProps {
|
||||
disabled?: boolean;
|
||||
environmentId: string;
|
||||
isUnsplashConfigured: boolean;
|
||||
form: UseFormReturn<TProductStyling | TSurveyStyling>;
|
||||
form: UseFormReturn<TProjectStyling | TSurveyStyling>;
|
||||
}
|
||||
|
||||
export const BackgroundStylingCard = ({
|
||||
|
||||
@@ -14,7 +14,7 @@ import React from "react";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
|
||||
import { TProduct, TProductStyling } from "@formbricks/types/product";
|
||||
import { TProject, TProjectStyling } from "@formbricks/types/project";
|
||||
import { TSurveyStyling, TSurveyType } from "@formbricks/types/surveys/types";
|
||||
|
||||
type CardStylingSettingsProps = {
|
||||
@@ -23,8 +23,8 @@ type CardStylingSettingsProps = {
|
||||
isSettingsPage?: boolean;
|
||||
surveyType?: TSurveyType;
|
||||
disabled?: boolean;
|
||||
product: TProduct;
|
||||
form: UseFormReturn<TProductStyling | TSurveyStyling>;
|
||||
project: TProject;
|
||||
form: UseFormReturn<TProjectStyling | TSurveyStyling>;
|
||||
};
|
||||
|
||||
export const CardStylingSettings = ({
|
||||
@@ -32,14 +32,14 @@ export const CardStylingSettings = ({
|
||||
surveyType,
|
||||
disabled,
|
||||
open,
|
||||
product,
|
||||
project,
|
||||
setOpen,
|
||||
form,
|
||||
}: CardStylingSettingsProps) => {
|
||||
const t = useTranslations();
|
||||
const isAppSurvey = surveyType === "app";
|
||||
const surveyTypeDerived = isAppSurvey ? "App" : "Link";
|
||||
const isLogoVisible = !!product.logo?.url;
|
||||
const isLogoVisible = !!project.logo?.url;
|
||||
|
||||
const linkCardArrangement = form.watch("cardArrangement.linkSurveys") ?? "straight";
|
||||
const appCardArrangement = form.watch("cardArrangement.appSurveys") ?? "straight";
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
getQuestionDefaults,
|
||||
getQuestionNameMap,
|
||||
} from "@formbricks/lib/utils/questions";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyEndScreenCard,
|
||||
@@ -41,7 +41,7 @@ interface EditorCardMenuProps {
|
||||
updateCard: (cardIdx: number, updatedAttributes: any) => void;
|
||||
addCard: (question: any, index?: number) => void;
|
||||
cardType: "question" | "ending";
|
||||
product?: TProduct;
|
||||
project?: TProject;
|
||||
isCxMode?: boolean;
|
||||
locale: string;
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export const EditorCardMenu = ({
|
||||
duplicateCard,
|
||||
deleteCard,
|
||||
moveCard,
|
||||
product,
|
||||
project,
|
||||
card,
|
||||
updateCard,
|
||||
addCard,
|
||||
@@ -83,7 +83,7 @@ export const EditorCardMenu = ({
|
||||
const { headline, required, subheader, imageUrl, videoUrl, buttonLabel, backButtonLabel } =
|
||||
card as TSurveyQuestion;
|
||||
|
||||
const questionDefaults = getQuestionDefaults(type, product, locale);
|
||||
const questionDefaults = getQuestionDefaults(type, project, locale);
|
||||
|
||||
if (
|
||||
(type === TSurveyQuestionTypeEnum.MultipleChoiceSingle &&
|
||||
@@ -115,7 +115,7 @@ export const EditorCardMenu = ({
|
||||
};
|
||||
|
||||
const addQuestionCardBelow = (type: TSurveyQuestionTypeEnum) => {
|
||||
const questionDefaults = getQuestionDefaults(type, product, locale);
|
||||
const questionDefaults = getQuestionDefaults(type, project, locale);
|
||||
|
||||
addCard(
|
||||
{
|
||||
|
||||
@@ -14,13 +14,13 @@ import { toast } from "react-hot-toast";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/common";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurvey, TSurveyFileUploadQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
interface FileUploadFormProps {
|
||||
localSurvey: TSurvey;
|
||||
product?: TProduct;
|
||||
project?: TProject;
|
||||
question: TSurveyFileUploadQuestion;
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyFileUploadQuestion>) => void;
|
||||
@@ -39,7 +39,7 @@ export const FileUploadQuestionForm = ({
|
||||
questionIdx,
|
||||
updateQuestion,
|
||||
isInvalid,
|
||||
product,
|
||||
project,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
@@ -53,7 +53,7 @@ export const FileUploadQuestionForm = ({
|
||||
billingInfo,
|
||||
error: billingInfoError,
|
||||
isLoading: billingInfoLoading,
|
||||
} = useGetBillingInfo(product?.organizationId ?? "");
|
||||
} = useGetBillingInfo(project?.organizationId ?? "");
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { UseFormReturn } from "react-hook-form";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
|
||||
import { mixColor } from "@formbricks/lib/utils/colors";
|
||||
import { TProductStyling } from "@formbricks/types/product";
|
||||
import { TProjectStyling } from "@formbricks/types/project";
|
||||
import { TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
|
||||
type FormStylingSettingsProps = {
|
||||
@@ -20,7 +20,7 @@ type FormStylingSettingsProps = {
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isSettingsPage?: boolean;
|
||||
disabled?: boolean;
|
||||
form: UseFormReturn<TProductStyling | TSurveyStyling>;
|
||||
form: UseFormReturn<TProjectStyling | TSurveyStyling>;
|
||||
};
|
||||
|
||||
export const FormStylingSettings = ({
|
||||
|
||||
@@ -175,7 +175,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment, locale
|
||||
</p>
|
||||
<p className="text-xs font-normal">
|
||||
<Link
|
||||
href={`/environments/${environment.id}/product/${option.id}-connection`}
|
||||
href={`/environments/${environment.id}/project/${option.id}-connection`}
|
||||
className="underline hover:text-amber-900"
|
||||
target="_blank">
|
||||
{t("common.connect_formbricks")}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { cn } from "@formbricks/lib/cn";
|
||||
import { QUESTIONS_ICON_MAP, getTSurveyQuestionTypeEnumName } from "@formbricks/lib/utils/questions";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
@@ -43,7 +43,7 @@ import { RatingQuestionForm } from "./RatingQuestionForm";
|
||||
|
||||
interface QuestionCardProps {
|
||||
localSurvey: TSurvey;
|
||||
product: TProduct;
|
||||
project: TProject;
|
||||
question: TSurveyQuestion;
|
||||
questionIdx: number;
|
||||
moveQuestion: (questionIndex: number, up: boolean) => void;
|
||||
@@ -65,7 +65,7 @@ interface QuestionCardProps {
|
||||
|
||||
export const QuestionCard = ({
|
||||
localSurvey,
|
||||
product,
|
||||
project,
|
||||
question,
|
||||
questionIdx,
|
||||
moveQuestion,
|
||||
@@ -228,7 +228,7 @@ export const QuestionCard = ({
|
||||
deleteCard={deleteQuestion}
|
||||
moveCard={moveQuestion}
|
||||
card={question}
|
||||
product={product}
|
||||
project={project}
|
||||
updateCard={updateQuestion}
|
||||
addCard={addQuestion}
|
||||
cardType="question"
|
||||
@@ -358,7 +358,7 @@ export const QuestionCard = ({
|
||||
) : question.type === TSurveyQuestionTypeEnum.FileUpload ? (
|
||||
<FileUploadQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
product={product}
|
||||
project={project}
|
||||
question={question}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { QuestionCard } from "./QuestionCard";
|
||||
|
||||
interface QuestionsDraggableProps {
|
||||
localSurvey: TSurvey;
|
||||
product: TProduct;
|
||||
project: TProject;
|
||||
moveQuestion: (questionIndex: number, up: boolean) => void;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
deleteQuestion: (questionIdx: number) => void;
|
||||
@@ -33,7 +33,7 @@ export const QuestionsDroppable = ({
|
||||
invalidQuestions,
|
||||
localSurvey,
|
||||
moveQuestion,
|
||||
product,
|
||||
project,
|
||||
selectedLanguageCode,
|
||||
setActiveQuestionId,
|
||||
setSelectedLanguageCode,
|
||||
@@ -54,7 +54,7 @@ export const QuestionsDroppable = ({
|
||||
<QuestionCard
|
||||
key={internalQuestionIdMap[question.id]}
|
||||
localSurvey={localSurvey}
|
||||
product={product}
|
||||
project={project}
|
||||
question={question}
|
||||
questionIdx={questionIdx}
|
||||
moveQuestion={moveQuestion}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { getDefaultEndingCard } from "@formbricks/lib/templates";
|
||||
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import {
|
||||
TConditionGroup,
|
||||
TSingleCondition,
|
||||
@@ -53,7 +53,7 @@ interface QuestionsViewProps {
|
||||
setLocalSurvey: React.Dispatch<SetStateAction<TSurvey>>;
|
||||
activeQuestionId: TSurveyQuestionId | null;
|
||||
setActiveQuestionId: (questionId: TSurveyQuestionId | null) => void;
|
||||
product: TProduct;
|
||||
project: TProject;
|
||||
invalidQuestions: string[] | null;
|
||||
setInvalidQuestions: React.Dispatch<SetStateAction<string[] | null>>;
|
||||
selectedLanguageCode: string;
|
||||
@@ -71,7 +71,7 @@ export const QuestionsView = ({
|
||||
setActiveQuestionId,
|
||||
localSurvey,
|
||||
setLocalSurvey,
|
||||
product,
|
||||
project,
|
||||
invalidQuestions,
|
||||
setInvalidQuestions,
|
||||
setSelectedLanguageCode,
|
||||
@@ -447,7 +447,7 @@ export const QuestionsView = ({
|
||||
collisionDetection={closestCorners}>
|
||||
<QuestionsDroppable
|
||||
localSurvey={localSurvey}
|
||||
product={product}
|
||||
project={project}
|
||||
moveQuestion={moveQuestion}
|
||||
updateQuestion={updateQuestion}
|
||||
duplicateQuestion={duplicateQuestion}
|
||||
@@ -466,7 +466,7 @@ export const QuestionsView = ({
|
||||
/>
|
||||
</DndContext>
|
||||
|
||||
<AddQuestionButton addQuestion={addQuestion} product={product} isCxMode={isCxMode} locale={locale} />
|
||||
<AddQuestionButton addQuestion={addQuestion} project={project} isCxMode={isCxMode} locale={locale} />
|
||||
<div className="mt-5 flex flex-col gap-5" ref={parent}>
|
||||
<hr className="border-t border-dashed" />
|
||||
<DndContext
|
||||
@@ -523,7 +523,7 @@ export const QuestionsView = ({
|
||||
|
||||
<MultiLanguageCard
|
||||
localSurvey={localSurvey}
|
||||
product={product}
|
||||
project={project}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
activeQuestionId={activeQuestionId}
|
||||
|
||||
@@ -195,7 +195,7 @@ export const RecontactOptionsCard = ({
|
||||
{t("environments.surveys.edit.this_setting_overwrites_your")}{" "}
|
||||
<Link
|
||||
className="decoration-brand-dark underline"
|
||||
href={`/environments/${environmentId}/product/general`}
|
||||
href={`/environments/${environmentId}/project/general`}
|
||||
target="_blank">
|
||||
{t("environments.surveys.edit.waiting_period")}
|
||||
</Link>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AdvancedTargetingCard } from "@/modules/ee/advanced-targeting/components/advanced-targeting-card";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/product-teams/types/teams";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams";
|
||||
import { TActionClass } from "@formbricks/types/action-classes";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
@@ -25,7 +25,7 @@ interface SettingsViewProps {
|
||||
isUserTargetingAllowed?: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
locale: string;
|
||||
productPermission: TTeamPermission | null;
|
||||
projectPermission: TTeamPermission | null;
|
||||
}
|
||||
|
||||
export const SettingsView = ({
|
||||
@@ -40,7 +40,7 @@ export const SettingsView = ({
|
||||
isUserTargetingAllowed = false,
|
||||
isFormbricksCloud,
|
||||
locale,
|
||||
productPermission,
|
||||
projectPermission,
|
||||
}: SettingsViewProps) => {
|
||||
const isAppSurvey = localSurvey.type === "app";
|
||||
|
||||
@@ -86,7 +86,7 @@ export const SettingsView = ({
|
||||
environmentId={environment.id}
|
||||
propActionClasses={actionClasses}
|
||||
membershipRole={membershipRole}
|
||||
productPermission={productPermission}
|
||||
projectPermission={projectPermission}
|
||||
/>
|
||||
|
||||
<ResponseOptionsCard
|
||||
|
||||
@@ -16,7 +16,7 @@ import React, { useEffect, useMemo, useState } from "react";
|
||||
import { UseFormReturn, useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProduct, TProductStyling } from "@formbricks/types/product";
|
||||
import { TProject, TProjectStyling } from "@formbricks/types/project";
|
||||
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { BackgroundStylingCard } from "./BackgroundStylingCard";
|
||||
import { CardStylingSettings } from "./CardStylingSettings";
|
||||
@@ -24,7 +24,7 @@ import { FormStylingSettings } from "./FormStylingSettings";
|
||||
|
||||
interface StylingViewProps {
|
||||
environment: TEnvironment;
|
||||
product: TProduct;
|
||||
project: TProject;
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurvey>>;
|
||||
colors: string[];
|
||||
@@ -39,7 +39,7 @@ interface StylingViewProps {
|
||||
export const StylingView = ({
|
||||
colors,
|
||||
environment,
|
||||
product,
|
||||
project,
|
||||
localSurvey,
|
||||
setLocalSurvey,
|
||||
setStyling,
|
||||
@@ -52,7 +52,7 @@ export const StylingView = ({
|
||||
const t = useTranslations();
|
||||
|
||||
const form = useForm<TSurveyStyling>({
|
||||
defaultValues: localSurvey.styling ?? product.styling,
|
||||
defaultValues: localSurvey.styling ?? project.styling,
|
||||
});
|
||||
|
||||
const overwriteThemeStyling = form.watch("overwriteThemeStyling");
|
||||
@@ -64,8 +64,8 @@ export const StylingView = ({
|
||||
const [confirmResetStylingModalOpen, setConfirmResetStylingModalOpen] = useState(false);
|
||||
|
||||
const onResetThemeStyling = () => {
|
||||
const { styling: productStyling } = product;
|
||||
const { allowStyleOverwrite, ...baseStyling } = productStyling ?? {};
|
||||
const { styling: projectStyling } = project;
|
||||
const { allowStyleOverwrite, ...baseStyling } = projectStyling ?? {};
|
||||
|
||||
setStyling({
|
||||
...baseStyling,
|
||||
@@ -101,12 +101,12 @@ export const StylingView = ({
|
||||
});
|
||||
}, [setLocalSurvey]);
|
||||
|
||||
const defaultProductStyling = useMemo(() => {
|
||||
const { styling: productStyling } = product;
|
||||
const { allowStyleOverwrite, ...baseStyling } = productStyling ?? {};
|
||||
const defaultProjectStyling = useMemo(() => {
|
||||
const { styling: projectStyling } = project;
|
||||
const { allowStyleOverwrite, ...baseStyling } = projectStyling ?? {};
|
||||
|
||||
return baseStyling;
|
||||
}, [product]);
|
||||
}, [project]);
|
||||
|
||||
const handleOverwriteToggle = (value: boolean) => {
|
||||
// survey styling from the server is surveyStyling, it could either be set or not
|
||||
@@ -114,12 +114,12 @@ export const StylingView = ({
|
||||
|
||||
setOverwriteThemeStyling(value);
|
||||
|
||||
// if the toggle is turned on, we set the local styling to the product styling
|
||||
// if the toggle is turned on, we set the local styling to the project styling
|
||||
if (value) {
|
||||
if (!styling) {
|
||||
// copy the product styling to the survey styling
|
||||
// copy the project styling to the survey styling
|
||||
setStyling({
|
||||
...defaultProductStyling,
|
||||
...defaultProjectStyling,
|
||||
overwriteThemeStyling: true,
|
||||
});
|
||||
return;
|
||||
@@ -129,23 +129,23 @@ export const StylingView = ({
|
||||
if (localStylingChanges) {
|
||||
setStyling(localStylingChanges);
|
||||
}
|
||||
// if there are no local styling changes, we set the styling to the product styling
|
||||
// if there are no local styling changes, we set the styling to the project styling
|
||||
else {
|
||||
setStyling({
|
||||
...defaultProductStyling,
|
||||
...defaultProjectStyling,
|
||||
overwriteThemeStyling: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// if the toggle is turned off, we store the local styling changes and set the styling to the product styling
|
||||
// if the toggle is turned off, we store the local styling changes and set the styling to the project styling
|
||||
else {
|
||||
// copy the styling to localStylingChanges
|
||||
setLocalStylingChanges(styling);
|
||||
|
||||
// copy the product styling to the survey styling
|
||||
// copy the project styling to the survey styling
|
||||
setStyling({
|
||||
...defaultProductStyling,
|
||||
...defaultProjectStyling,
|
||||
overwriteThemeStyling: false,
|
||||
});
|
||||
}
|
||||
@@ -184,7 +184,7 @@ export const StylingView = ({
|
||||
open={formStylingOpen}
|
||||
setOpen={setFormStylingOpen}
|
||||
disabled={!overwriteThemeStyling}
|
||||
form={form as UseFormReturn<TProductStyling | TSurveyStyling>}
|
||||
form={form as UseFormReturn<TProjectStyling | TSurveyStyling>}
|
||||
/>
|
||||
|
||||
<CardStylingSettings
|
||||
@@ -192,8 +192,8 @@ export const StylingView = ({
|
||||
setOpen={setCardStylingOpen}
|
||||
surveyType={localSurvey.type}
|
||||
disabled={!overwriteThemeStyling}
|
||||
product={product}
|
||||
form={form as UseFormReturn<TProductStyling | TSurveyStyling>}
|
||||
project={project}
|
||||
form={form as UseFormReturn<TProjectStyling | TSurveyStyling>}
|
||||
/>
|
||||
|
||||
{localSurvey.type === "link" && (
|
||||
@@ -204,7 +204,7 @@ export const StylingView = ({
|
||||
colors={colors}
|
||||
disabled={!overwriteThemeStyling}
|
||||
isUnsplashConfigured={isUnsplashConfigured}
|
||||
form={form as UseFormReturn<TProductStyling | TSurveyStyling>}
|
||||
form={form as UseFormReturn<TProjectStyling | TSurveyStyling>}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -225,7 +225,7 @@ export const StylingView = ({
|
||||
<p className="text-sm text-slate-500">
|
||||
{t("environments.surveys.edit.adjust_the_theme_in_the")}{" "}
|
||||
<Link
|
||||
href={`/environments/${environment.id}/product/look`}
|
||||
href={`/environments/${environment.id}/project/look`}
|
||||
target="_blank"
|
||||
className="font-semibold underline">
|
||||
{t("common.look_and_feel")}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { FollowUpsView } from "@/modules/ee/survey-follow-ups/components/follow-ups-view";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/product-teams/types/teams";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams";
|
||||
import { PreviewSurvey } from "@/modules/ui/components/preview-survey";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { extractLanguageCodes, getEnabledLanguages } from "@formbricks/lib/i18n/utils";
|
||||
@@ -12,11 +12,11 @@ import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { TSurvey, TSurveyEditorTabs, TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { refetchProductAction } from "../actions";
|
||||
import { refetchProjectAction } from "../actions";
|
||||
import { LoadingSkeleton } from "./LoadingSkeleton";
|
||||
import { QuestionsAudienceTabs } from "./QuestionsStylingSettingsTabs";
|
||||
import { QuestionsView } from "./QuestionsView";
|
||||
@@ -26,7 +26,7 @@ import { SurveyMenuBar } from "./SurveyMenuBar";
|
||||
|
||||
interface SurveyEditorProps {
|
||||
survey: TSurvey;
|
||||
product: TProduct;
|
||||
project: TProject;
|
||||
environment: TEnvironment;
|
||||
actionClasses: TActionClass[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
@@ -41,15 +41,15 @@ interface SurveyEditorProps {
|
||||
plan: TOrganizationBillingPlan;
|
||||
isCxMode: boolean;
|
||||
locale: TUserLocale;
|
||||
projectPermission: TTeamPermission | null;
|
||||
mailFrom: string;
|
||||
isSurveyFollowUpsAllowed: boolean;
|
||||
productPermission: TTeamPermission | null;
|
||||
userEmail: string;
|
||||
}
|
||||
|
||||
export const SurveyEditor = ({
|
||||
survey,
|
||||
product,
|
||||
project,
|
||||
environment,
|
||||
actionClasses,
|
||||
attributeClasses,
|
||||
@@ -64,9 +64,9 @@ export const SurveyEditor = ({
|
||||
plan,
|
||||
isCxMode = false,
|
||||
locale,
|
||||
projectPermission,
|
||||
mailFrom,
|
||||
isSurveyFollowUpsAllowed = false,
|
||||
productPermission,
|
||||
userEmail,
|
||||
}: SurveyEditorProps) => {
|
||||
const [activeView, setActiveView] = useState<TSurveyEditorTabs>("questions");
|
||||
@@ -75,19 +75,19 @@ export const SurveyEditor = ({
|
||||
const [invalidQuestions, setInvalidQuestions] = useState<string[] | null>(null);
|
||||
const [selectedLanguageCode, setSelectedLanguageCode] = useState<string>("default");
|
||||
const surveyEditorRef = useRef(null);
|
||||
const [localProduct, setLocalProduct] = useState<TProduct>(product);
|
||||
const [localProject, setLocalProject] = useState<TProject>(project);
|
||||
|
||||
const [styling, setStyling] = useState(localSurvey?.styling);
|
||||
const [localStylingChanges, setLocalStylingChanges] = useState<TSurveyStyling | null>(null);
|
||||
|
||||
const fetchLatestProduct = useCallback(async () => {
|
||||
const refetchProductResponse = await refetchProductAction({ productId: localProduct.id });
|
||||
if (refetchProductResponse?.data) {
|
||||
setLocalProduct(refetchProductResponse.data);
|
||||
const fetchLatestProject = useCallback(async () => {
|
||||
const refetchProjectResponse = await refetchProjectAction({ projectId: localProject.id });
|
||||
if (refetchProjectResponse?.data) {
|
||||
setLocalProject(refetchProjectResponse.data);
|
||||
}
|
||||
}, [localProduct.id]);
|
||||
}, [localProject.id]);
|
||||
|
||||
useDocumentVisibility(fetchLatestProduct);
|
||||
useDocumentVisibility(fetchLatestProject);
|
||||
|
||||
useEffect(() => {
|
||||
if (survey) {
|
||||
@@ -107,20 +107,20 @@ export const SurveyEditor = ({
|
||||
useEffect(() => {
|
||||
const listener = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
const fetchLatestProduct = async () => {
|
||||
const refetchProductResponse = await refetchProductAction({ productId: localProduct.id });
|
||||
if (refetchProductResponse?.data) {
|
||||
setLocalProduct(refetchProductResponse.data);
|
||||
const fetchLatestProject = async () => {
|
||||
const refetchProjectResponse = await refetchProjectAction({ projectId: localProject.id });
|
||||
if (refetchProjectResponse?.data) {
|
||||
setLocalProject(refetchProjectResponse.data);
|
||||
}
|
||||
};
|
||||
fetchLatestProduct();
|
||||
fetchLatestProject();
|
||||
}
|
||||
};
|
||||
document.addEventListener("visibilitychange", listener);
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", listener);
|
||||
};
|
||||
}, [localProduct.id]);
|
||||
}, [localProject.id]);
|
||||
|
||||
// when the survey type changes, we need to reset the active question id to the first question
|
||||
useEffect(() => {
|
||||
@@ -152,7 +152,7 @@ export const SurveyEditor = ({
|
||||
activeId={activeView}
|
||||
setActiveId={setActiveView}
|
||||
setInvalidQuestions={setInvalidQuestions}
|
||||
product={localProduct}
|
||||
project={localProject}
|
||||
responseCount={responseCount}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
@@ -167,7 +167,7 @@ export const SurveyEditor = ({
|
||||
activeId={activeView}
|
||||
setActiveId={setActiveView}
|
||||
isCxMode={isCxMode}
|
||||
isStylingTabVisible={!!product.styling.allowStyleOverwrite}
|
||||
isStylingTabVisible={!!project.styling.allowStyleOverwrite}
|
||||
isSurveyFollowUpsAllowed={isSurveyFollowUpsAllowed}
|
||||
/>
|
||||
|
||||
@@ -177,7 +177,7 @@ export const SurveyEditor = ({
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
activeQuestionId={activeQuestionId}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
product={localProduct}
|
||||
project={localProject}
|
||||
invalidQuestions={invalidQuestions}
|
||||
setInvalidQuestions={setInvalidQuestions}
|
||||
selectedLanguageCode={selectedLanguageCode ? selectedLanguageCode : "default"}
|
||||
@@ -191,13 +191,13 @@ export const SurveyEditor = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeView === "styling" && product.styling.allowStyleOverwrite && (
|
||||
{activeView === "styling" && project.styling.allowStyleOverwrite && (
|
||||
<StylingView
|
||||
colors={colors}
|
||||
environment={environment}
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
product={localProduct}
|
||||
project={localProject}
|
||||
styling={styling ?? null}
|
||||
setStyling={setStyling}
|
||||
localStylingChanges={localStylingChanges}
|
||||
@@ -220,7 +220,7 @@ export const SurveyEditor = ({
|
||||
isUserTargetingAllowed={isUserTargetingAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
locale={locale}
|
||||
productPermission={productPermission}
|
||||
projectPermission={projectPermission}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -241,7 +241,7 @@ export const SurveyEditor = ({
|
||||
<PreviewSurvey
|
||||
survey={localSurvey}
|
||||
questionId={activeQuestionId}
|
||||
product={localProduct}
|
||||
project={localProject}
|
||||
environment={environment}
|
||||
previewType={localSurvey.type === "app" ? "modal" : "fullwidth"}
|
||||
languageCode={selectedLanguageCode}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getLanguageLabel } from "@formbricks/lib/i18n/utils";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import {
|
||||
TSurvey,
|
||||
@@ -36,7 +36,7 @@ interface SurveyMenuBarProps {
|
||||
activeId: TSurveyEditorTabs;
|
||||
setActiveId: React.Dispatch<React.SetStateAction<TSurveyEditorTabs>>;
|
||||
setInvalidQuestions: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
product: TProduct;
|
||||
project: TProject;
|
||||
responseCount: number;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (selectedLanguage: string) => void;
|
||||
@@ -52,7 +52,7 @@ export const SurveyMenuBar = ({
|
||||
activeId,
|
||||
setActiveId,
|
||||
setInvalidQuestions,
|
||||
product,
|
||||
project,
|
||||
responseCount,
|
||||
selectedLanguageCode,
|
||||
isCxMode,
|
||||
@@ -326,7 +326,7 @@ export const SurveyMenuBar = ({
|
||||
{t("common.back")}
|
||||
</Button>
|
||||
)}
|
||||
<p className="hidden pl-4 font-semibold md:block">{product.name} / </p>
|
||||
<p className="hidden pl-4 font-semibold md:block">{project.name} / </p>
|
||||
<Input
|
||||
defaultValue={localSurvey.name}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { TPlacement } from "@formbricks/types/common";
|
||||
import { TSurvey, TSurveyProductOverwrites } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyProjectOverwrites } from "@formbricks/types/surveys/types";
|
||||
import { Placement } from "./Placement";
|
||||
|
||||
interface SurveyPlacementCardProps {
|
||||
@@ -26,17 +26,17 @@ export const SurveyPlacementCard = ({
|
||||
const t = useTranslations();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { productOverwrites } = localSurvey ?? {};
|
||||
const { placement, clickOutsideClose, darkOverlay } = productOverwrites ?? {};
|
||||
const { projectOverwrites } = localSurvey ?? {};
|
||||
const { placement, clickOutsideClose, darkOverlay } = projectOverwrites ?? {};
|
||||
|
||||
const setProductOverwrites = (productOverwrites: TSurveyProductOverwrites) => {
|
||||
setLocalSurvey({ ...localSurvey, productOverwrites });
|
||||
const setProjectOverwrites = (projectOverwrites: TSurveyProjectOverwrites) => {
|
||||
setLocalSurvey({ ...localSurvey, projectOverwrites: projectOverwrites });
|
||||
};
|
||||
|
||||
const togglePlacement = () => {
|
||||
if (setProductOverwrites) {
|
||||
setProductOverwrites({
|
||||
...productOverwrites,
|
||||
if (setProjectOverwrites) {
|
||||
setProjectOverwrites({
|
||||
...projectOverwrites,
|
||||
placement: !!placement ? null : "bottomRight",
|
||||
clickOutsideClose: false,
|
||||
darkOverlay: false,
|
||||
@@ -45,9 +45,9 @@ export const SurveyPlacementCard = ({
|
||||
};
|
||||
|
||||
const handlePlacementChange = (placement: TPlacement) => {
|
||||
if (setProductOverwrites) {
|
||||
setProductOverwrites({
|
||||
...productOverwrites,
|
||||
if (setProjectOverwrites) {
|
||||
setProjectOverwrites({
|
||||
...projectOverwrites,
|
||||
placement,
|
||||
});
|
||||
}
|
||||
@@ -56,18 +56,18 @@ export const SurveyPlacementCard = ({
|
||||
const handleOverlay = (overlayType: string) => {
|
||||
const darkOverlay = overlayType === "dark";
|
||||
|
||||
if (setProductOverwrites) {
|
||||
setProductOverwrites({
|
||||
...productOverwrites,
|
||||
if (setProjectOverwrites) {
|
||||
setProjectOverwrites({
|
||||
...projectOverwrites,
|
||||
darkOverlay,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickOutsideClose = (clickOutsideClose: boolean) => {
|
||||
if (setProductOverwrites) {
|
||||
setProductOverwrites({
|
||||
...productOverwrites,
|
||||
if (setProjectOverwrites) {
|
||||
setProjectOverwrites({
|
||||
...projectOverwrites,
|
||||
clickOutsideClose,
|
||||
});
|
||||
}
|
||||
@@ -141,7 +141,7 @@ export const SurveyPlacementCard = ({
|
||||
<div>
|
||||
<p className="text-xs text-slate-500">
|
||||
{t("environments.surveys.edit.to_keep_the_placement_over_all_surveys_consistent_you_can")}{" "}
|
||||
<Link href={`/environments/${environmentId}/product/look`} target="_blank">
|
||||
<Link href={`/environments/${environmentId}/project/look`} target="_blank">
|
||||
<span className="underline">
|
||||
{t("environments.surveys.edit.set_the_global_placement_in_the_look_feel_settings")}
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { TTeamPermission } from "@/modules/ee/teams/product-teams/types/teams";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -29,7 +29,7 @@ interface WhenToSendCardProps {
|
||||
environmentId: string;
|
||||
propActionClasses: TActionClass[];
|
||||
membershipRole?: TOrganizationRole;
|
||||
productPermission: TTeamPermission | null;
|
||||
projectPermission: TTeamPermission | null;
|
||||
}
|
||||
|
||||
export const WhenToSendCard = ({
|
||||
@@ -38,7 +38,7 @@ export const WhenToSendCard = ({
|
||||
setLocalSurvey,
|
||||
propActionClasses,
|
||||
membershipRole,
|
||||
productPermission,
|
||||
projectPermission,
|
||||
}: WhenToSendCardProps) => {
|
||||
const t = useTranslations();
|
||||
const [open, setOpen] = useState(localSurvey.type === "app" ? true : false);
|
||||
@@ -47,7 +47,7 @@ export const WhenToSendCard = ({
|
||||
const [randomizerToggle, setRandomizerToggle] = useState(localSurvey.displayPercentage ? true : false);
|
||||
|
||||
const { isMember } = getAccessFlags(membershipRole);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
getMultiLanguagePermission,
|
||||
getSurveyFollowUpsPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { ErrorComponent } from "@/modules/ui/components/error-component";
|
||||
import { getServerSession } from "next-auth";
|
||||
@@ -23,7 +23,7 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSegments } from "@formbricks/lib/segment/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
@@ -44,7 +44,7 @@ const Page = async (props) => {
|
||||
const t = await getTranslations();
|
||||
const [
|
||||
survey,
|
||||
product,
|
||||
project,
|
||||
environment,
|
||||
actionClasses,
|
||||
attributeClasses,
|
||||
@@ -54,7 +54,7 @@ const Page = async (props) => {
|
||||
segments,
|
||||
] = await Promise.all([
|
||||
getSurvey(params.surveyId),
|
||||
getProductByEnvironmentId(params.environmentId),
|
||||
getProjectByEnvironmentId(params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
getActionClasses(params.environmentId),
|
||||
getAttributeClasses(params.environmentId, undefined, { skipArchived: true }),
|
||||
@@ -72,16 +72,16 @@ const Page = async (props) => {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const productPermission = await getProductPermissionByUserId(session.user.id, product.id);
|
||||
const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isSurveyCreationDeletionDisabled = isMember && hasReadAccess;
|
||||
const locale = session.user.id ? await getUserLocale(session.user.id) : undefined;
|
||||
@@ -97,7 +97,7 @@ const Page = async (props) => {
|
||||
!environment ||
|
||||
!actionClasses ||
|
||||
!attributeClasses ||
|
||||
!product ||
|
||||
!project ||
|
||||
!userEmail ||
|
||||
isSurveyCreationDeletionDisabled
|
||||
) {
|
||||
@@ -109,13 +109,13 @@ const Page = async (props) => {
|
||||
return (
|
||||
<SurveyEditor
|
||||
survey={survey}
|
||||
product={product}
|
||||
project={project}
|
||||
environment={environment}
|
||||
actionClasses={actionClasses}
|
||||
attributeClasses={attributeClasses}
|
||||
responseCount={responseCount}
|
||||
membershipRole={currentUserMembership?.role}
|
||||
productPermission={productPermission}
|
||||
projectPermission={projectPermission}
|
||||
colors={SURVEY_BG_COLORS}
|
||||
segments={segments}
|
||||
isUserTargetingAllowed={isUserTargetingAllowed}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const getMinimalSurvey = (locale: string): TSurvey => ({
|
||||
surveyClosedMessage: {
|
||||
enabled: false,
|
||||
},
|
||||
productOverwrites: null,
|
||||
projectOverwrites: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
resultShareKey: null,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromEnvironmentId, getProductIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { generateObject } from "ai";
|
||||
@@ -32,8 +32,8 @@ export const createAISurveyAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
productId: await getProductIdFromEnvironmentId(parsedInput.environmentId),
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -10,22 +10,22 @@ import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { getCustomSurveyTemplate } from "@formbricks/lib/templates";
|
||||
import type { TEnvironment } from "@formbricks/types/environment";
|
||||
import type { TProduct, TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
|
||||
import type { TProject, TProjectConfigChannel, TProjectConfigIndustry } from "@formbricks/types/project";
|
||||
import type { TTemplate, TTemplateRole } from "@formbricks/types/templates";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { getMinimalSurvey } from "../../lib/minimalSurvey";
|
||||
|
||||
type TemplateContainerWithPreviewProps = {
|
||||
environmentId: string;
|
||||
product: TProduct;
|
||||
project: TProject;
|
||||
environment: TEnvironment;
|
||||
user: TUser;
|
||||
prefilledFilters: (TProductConfigChannel | TProductConfigIndustry | TTemplateRole | null)[];
|
||||
prefilledFilters: (TProjectConfigChannel | TProjectConfigIndustry | TTemplateRole | null)[];
|
||||
isAIEnabled: boolean;
|
||||
};
|
||||
|
||||
export const TemplateContainerWithPreview = ({
|
||||
product,
|
||||
project,
|
||||
environment,
|
||||
user,
|
||||
prefilledFilters,
|
||||
@@ -67,7 +67,7 @@ export const TemplateContainerWithPreview = ({
|
||||
|
||||
<TemplateList
|
||||
environment={environment}
|
||||
product={product}
|
||||
project={project}
|
||||
user={user}
|
||||
templateSearch={templateSearch ?? ""}
|
||||
onTemplateClick={(template) => {
|
||||
@@ -82,7 +82,7 @@ export const TemplateContainerWithPreview = ({
|
||||
<PreviewSurvey
|
||||
survey={{ ...getMinimalSurvey(user.locale), ...activeTemplate.preset }}
|
||||
questionId={activeQuestionId}
|
||||
product={product}
|
||||
project={project}
|
||||
environment={environment}
|
||||
languageCode={"default"}
|
||||
onFileUpload={async (file) => file.name}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
@@ -7,9 +7,9 @@ import { redirect } from "next/navigation";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
|
||||
import { TProjectConfigChannel, TProjectConfigIndustry } from "@formbricks/types/project";
|
||||
import { TTemplateRole } from "@formbricks/types/templates";
|
||||
import { TemplateContainerWithPreview } from "./components/TemplateContainer";
|
||||
|
||||
@@ -18,8 +18,8 @@ interface SurveyTemplateProps {
|
||||
environmentId: string;
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
channel?: TProductConfigChannel;
|
||||
industry?: TProductConfigIndustry;
|
||||
channel?: TProjectConfigChannel;
|
||||
industry?: TProjectConfigIndustry;
|
||||
role?: TTemplateRole;
|
||||
}>;
|
||||
}
|
||||
@@ -35,18 +35,18 @@ const Page = async (props: SurveyTemplateProps) => {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
|
||||
const [user, environment, product] = await Promise.all([
|
||||
const [user, environment, project] = await Promise.all([
|
||||
getUser(session.user.id),
|
||||
getEnvironment(environmentId),
|
||||
getProductByEnvironmentId(environmentId),
|
||||
getProjectByEnvironmentId(environmentId),
|
||||
]);
|
||||
|
||||
if (!user) {
|
||||
throw new Error(t("common.user_not_found"));
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
@@ -54,26 +54,26 @@ const Page = async (props: SurveyTemplateProps) => {
|
||||
}
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(
|
||||
session?.user.id,
|
||||
product.organizationId
|
||||
project.organizationId
|
||||
);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const productPermission = await getProductPermissionByUserId(session.user.id, product.id);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
if (isReadOnly) {
|
||||
return redirect(`/environments/${environment.id}/surveys`);
|
||||
}
|
||||
|
||||
const prefilledFilters = [product.config.channel, product.config.industry, searchParams.role ?? null];
|
||||
const prefilledFilters = [project.config.channel, project.config.industry, searchParams.role ?? null];
|
||||
|
||||
return (
|
||||
<TemplateContainerWithPreview
|
||||
environmentId={environmentId}
|
||||
user={user}
|
||||
environment={environment}
|
||||
product={product}
|
||||
project={project}
|
||||
prefilledFilters={prefilledFilters}
|
||||
// AI Survey Creation -- Need improvement
|
||||
isAIEnabled={false}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromEnvironmentId, getProductIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { z } from "zod";
|
||||
import { getSegmentsByAttributeClassName } from "@formbricks/lib/segment/service";
|
||||
import { ZAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -25,9 +25,9 @@ export const getSegmentsByAttributeClassAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "read",
|
||||
productId: await getProductIdFromEnvironmentId(parsedInput.environmentId),
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
@@ -13,7 +13,7 @@ import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { AttributeClassesTable } from "./components/AttributeClassesTable";
|
||||
|
||||
@@ -25,10 +25,10 @@ const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
let attributeClasses = await getAttributeClasses(params.environmentId);
|
||||
const t = await getTranslations();
|
||||
const product = await getProductByEnvironmentId(params.environmentId);
|
||||
const project = await getProjectByEnvironmentId(params.environmentId);
|
||||
const locale = await findMatchingLocale();
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const [organization, session] = await Promise.all([
|
||||
@@ -47,9 +47,9 @@ const Page = async (props) => {
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const productPermission = await getProductPermissionByUserId(session.user.id, product.id);
|
||||
const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
|
||||
const ConfigLayout = async (props) => {
|
||||
@@ -40,9 +40,9 @@ const ConfigLayout = async (props) => {
|
||||
return redirect(`/environments/${params.environmentId}/settings/billing`);
|
||||
}
|
||||
|
||||
const product = await getProductByEnvironmentId(params.environmentId);
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
const project = await getProjectByEnvironmentId(params.environmentId);
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
return children;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ResponseTimeline } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseTimeline";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getResponsesByPersonId } from "@formbricks/lib/response/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
@@ -33,26 +33,26 @@ export const ResponseSection = async ({
|
||||
|
||||
const t = await getTranslations();
|
||||
if (!session) {
|
||||
throw new Error(t("common.no_session_found"));
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
|
||||
if (!user) {
|
||||
throw new Error(t("common.no_user_found"));
|
||||
throw new Error(t("common.user_not_found"));
|
||||
}
|
||||
|
||||
if (!responses) {
|
||||
throw new Error(t("environments.people.no_responses_found"));
|
||||
}
|
||||
|
||||
const product = await getProductByEnvironmentId(environment.id);
|
||||
const project = await getProjectByEnvironmentId(environment.id);
|
||||
|
||||
if (!product) {
|
||||
throw new Error(t("common.no_product_found"));
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const productPermission = await getProductPermissionByUserId(session.user.id, product.id);
|
||||
const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
|
||||
|
||||
const locale = await findMatchingLocale();
|
||||
|
||||
@@ -65,7 +65,7 @@ export const ResponseSection = async ({
|
||||
environmentTags={environmentTags}
|
||||
attributeClasses={attributeClasses}
|
||||
locale={locale}
|
||||
productPermission={productPermission}
|
||||
projectPermission={projectPermission}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ResponseFeed } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponsesFeed";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/product-teams/types/teams";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams";
|
||||
import { ArrowDownUpIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -20,7 +20,7 @@ interface ResponseTimelineProps {
|
||||
environmentTags: TTag[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
locale: TUserLocale;
|
||||
productPermission: TTeamPermission | null;
|
||||
projectPermission: TTeamPermission | null;
|
||||
}
|
||||
|
||||
export const ResponseTimeline = ({
|
||||
@@ -31,7 +31,7 @@ export const ResponseTimeline = ({
|
||||
environmentTags,
|
||||
attributeClasses,
|
||||
locale,
|
||||
productPermission,
|
||||
projectPermission,
|
||||
}: ResponseTimelineProps) => {
|
||||
const t = useTranslations();
|
||||
const [sortedResponses, setSortedResponses] = useState(responses);
|
||||
@@ -64,7 +64,7 @@ export const ResponseTimeline = ({
|
||||
environmentTags={environmentTags}
|
||||
attributeClasses={attributeClasses}
|
||||
locale={locale}
|
||||
productPermission={productPermission}
|
||||
projectPermission={projectPermission}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { SingleResponseCard } from "@/modules/analysis/components/SingleResponseCard";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/product-teams/types/teams";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -23,7 +23,7 @@ interface ResponseTimelineProps {
|
||||
environmentTags: TTag[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
locale: TUserLocale;
|
||||
productPermission: TTeamPermission | null;
|
||||
projectPermission: TTeamPermission | null;
|
||||
}
|
||||
|
||||
export const ResponseFeed = ({
|
||||
@@ -34,7 +34,7 @@ export const ResponseFeed = ({
|
||||
environmentTags,
|
||||
attributeClasses,
|
||||
locale,
|
||||
productPermission,
|
||||
projectPermission,
|
||||
}: ResponseTimelineProps) => {
|
||||
const [fetchedResponses, setFetchedResponses] = useState(responses);
|
||||
|
||||
@@ -69,7 +69,7 @@ export const ResponseFeed = ({
|
||||
updateResponse={updateResponse}
|
||||
attributeClasses={attributeClasses}
|
||||
locale={locale}
|
||||
productPermission={productPermission}
|
||||
projectPermission={projectPermission}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
@@ -87,7 +87,7 @@ const ResponseSurveyCard = ({
|
||||
updateResponse,
|
||||
attributeClasses,
|
||||
locale,
|
||||
productPermission,
|
||||
projectPermission,
|
||||
}: {
|
||||
response: TResponse;
|
||||
surveys: TSurvey[];
|
||||
@@ -98,7 +98,7 @@ const ResponseSurveyCard = ({
|
||||
updateResponse: (responseId: string, response: TResponse) => void;
|
||||
attributeClasses: TAttributeClass[];
|
||||
locale: TUserLocale;
|
||||
productPermission: TTeamPermission | null;
|
||||
projectPermission: TTeamPermission | null;
|
||||
}) => {
|
||||
const survey = surveys.find((survey) => {
|
||||
return survey.id === response.surveyId;
|
||||
@@ -107,7 +107,7 @@ const ResponseSurveyCard = ({
|
||||
const { membershipRole } = useMembershipRole(survey?.environmentId || "", user.id);
|
||||
const { isMember } = getAccessFlags(membershipRole);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AttributesSection } from "@/app/(app)/environments/[environmentId]/(peo
|
||||
import { DeletePersonButton } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/DeletePersonButton";
|
||||
import { ResponseSection } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseSection";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
@@ -16,17 +16,17 @@ import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslations();
|
||||
const [environment, environmentTags, product, session, organization, person, attributes, attributeClasses] =
|
||||
const [environment, environmentTags, project, session, organization, person, attributes, attributeClasses] =
|
||||
await Promise.all([
|
||||
getEnvironment(params.environmentId),
|
||||
getTagsByEnvironmentId(params.environmentId),
|
||||
getProductByEnvironmentId(params.environmentId),
|
||||
getProjectByEnvironmentId(params.environmentId),
|
||||
getServerSession(authOptions),
|
||||
getOrganizationByEnvironmentId(params.environmentId),
|
||||
getPerson(params.personId),
|
||||
@@ -34,8 +34,8 @@ const Page = async (props) => {
|
||||
getAttributeClasses(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
@@ -57,8 +57,8 @@ const Page = async (props) => {
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const productPermission = await getProductPermissionByUserId(session.user.id, product.id);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"
|
||||
import {
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getOrganizationIdFromPersonId,
|
||||
getProductIdFromEnvironmentId,
|
||||
getProductIdFromPersonId,
|
||||
getProjectIdFromEnvironmentId,
|
||||
getProjectIdFromPersonId,
|
||||
} from "@/lib/utils/helper";
|
||||
import { z } from "zod";
|
||||
import { deletePerson, getPeople } from "@formbricks/lib/person/service";
|
||||
@@ -30,9 +30,9 @@ export const getPersonsAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "read",
|
||||
productId: await getProductIdFromEnvironmentId(parsedInput.environmentId),
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -56,9 +56,9 @@ export const deletePersonAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
productId: await getProductIdFromPersonId(parsedInput.personId),
|
||||
projectId: await getProjectIdFromPersonId(parsedInput.personId),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
|
||||
interface PersonSecondaryNavigationProps {
|
||||
activeId: string;
|
||||
@@ -14,13 +14,13 @@ export const PersonSecondaryNavigation = async ({
|
||||
environmentId,
|
||||
loading,
|
||||
}: PersonSecondaryNavigationProps) => {
|
||||
let product: TProduct | null = null;
|
||||
let project: TProject | null = null;
|
||||
const t = await getTranslations();
|
||||
if (!loading && environmentId) {
|
||||
product = await getProductByEnvironmentId(environmentId);
|
||||
project = await getProjectByEnvironmentId(environmentId);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PersonDataView } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonDataView";
|
||||
import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
@@ -14,7 +14,7 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
|
||||
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
const params = await props.params;
|
||||
@@ -35,17 +35,17 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
const product = await getProductByEnvironmentId(params.environmentId);
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
const project = await getProjectByEnvironmentId(params.environmentId);
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const productPermission = await getProductPermissionByUserId(session?.user.id, product.id);
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, project.id);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromSegmentId, getProductIdFromSegmentId } from "@/lib/utils/helper";
|
||||
import { getOrganizationIdFromSegmentId, getProjectIdFromSegmentId } from "@/lib/utils/helper";
|
||||
import { z } from "zod";
|
||||
import { deleteSegment, updateSegment } from "@formbricks/lib/segment/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
@@ -24,9 +24,9 @@ export const deleteBasicSegmentAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
productId: await getProductIdFromSegmentId(parsedInput.segmentId),
|
||||
projectId: await getProjectIdFromSegmentId(parsedInput.segmentId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -51,9 +51,9 @@ export const updateBasicSegmentAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
productId: await getProductIdFromSegmentId(parsedInput.segmentId),
|
||||
projectId: await getProjectIdFromSegmentId(parsedInput.segmentId),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SegmentTable } from "@/app/(app)/environments/[environmentId]/(people)/
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { CreateSegmentModal } from "@/modules/ee/advanced-targeting/components/create-segment-modal";
|
||||
import { getAdvancedTargetingPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
@@ -16,18 +16,18 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getSegments } from "@formbricks/lib/segment/service";
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslations();
|
||||
const [environment, segments, attributeClasses, organization, product] = await Promise.all([
|
||||
const [environment, segments, attributeClasses, organization, project] = await Promise.all([
|
||||
getEnvironment(params.environmentId),
|
||||
getSegments(params.environmentId),
|
||||
getAttributeClasses(params.environmentId, undefined, { skipArchived: true }),
|
||||
getOrganizationByEnvironmentId(params.environmentId),
|
||||
getProductByEnvironmentId(params.environmentId),
|
||||
getProjectByEnvironmentId(params.environmentId),
|
||||
]);
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
@@ -39,8 +39,8 @@ const Page = async (props) => {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
if (!organization) {
|
||||
@@ -56,9 +56,9 @@ const Page = async (props) => {
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const productPermission = await getProductPermissionByUserId(session?.user.id, product.id);
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, project.id);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
|
||||
@@ -2,71 +2,26 @@
|
||||
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getIsMultiOrgEnabled, getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import {
|
||||
getOrganizationProjectsLimit,
|
||||
getRoleManagementPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { createProject } from "@/modules/projects/settings/lib/project";
|
||||
import { z } from "zod";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
import { createOrganization, getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { createProduct } from "@formbricks/lib/product/service";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { getOrganizationProjectsCount } from "@formbricks/lib/project/service";
|
||||
import { updateUser } from "@formbricks/lib/user/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { ZProductUpdateInput } from "@formbricks/types/product";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
import { ZProjectUpdateInput } from "@formbricks/types/project";
|
||||
|
||||
const ZCreateOrganizationAction = z.object({
|
||||
organizationName: z.string(),
|
||||
});
|
||||
|
||||
export const createOrganizationAction = authenticatedActionClient
|
||||
.schema(ZCreateOrganizationAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
if (!isMultiOrgEnabled)
|
||||
throw new OperationNotAllowedError(
|
||||
"Creating Multiple organization is restricted on your instance of Formbricks"
|
||||
);
|
||||
|
||||
const newOrganization = await createOrganization({
|
||||
name: parsedInput.organizationName,
|
||||
});
|
||||
|
||||
await createMembership(newOrganization.id, ctx.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const product = await createProduct(newOrganization.id, {
|
||||
name: "My Product",
|
||||
});
|
||||
|
||||
const updatedNotificationSettings: TUserNotificationSettings = {
|
||||
...ctx.user.notificationSettings,
|
||||
alert: {
|
||||
...ctx.user.notificationSettings?.alert,
|
||||
},
|
||||
weeklySummary: {
|
||||
...ctx.user.notificationSettings?.weeklySummary,
|
||||
[product.id]: true,
|
||||
},
|
||||
unsubscribedOrganizationIds: Array.from(
|
||||
new Set([...(ctx.user.notificationSettings?.unsubscribedOrganizationIds || []), newOrganization.id])
|
||||
),
|
||||
};
|
||||
|
||||
await updateUser(ctx.user.id, {
|
||||
notificationSettings: updatedNotificationSettings,
|
||||
});
|
||||
|
||||
return newOrganization;
|
||||
});
|
||||
|
||||
const ZCreateProductAction = z.object({
|
||||
const ZCreateProjectAction = z.object({
|
||||
organizationId: ZId,
|
||||
data: ZProductUpdateInput,
|
||||
data: ZProjectUpdateInput,
|
||||
});
|
||||
|
||||
export const createProductAction = authenticatedActionClient
|
||||
.schema(ZCreateProductAction)
|
||||
export const createProjectAction = authenticatedActionClient
|
||||
.schema(ZCreateProjectAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
const { user } = ctx;
|
||||
|
||||
@@ -78,20 +33,27 @@ export const createProductAction = authenticatedActionClient
|
||||
access: [
|
||||
{
|
||||
data: parsedInput.data,
|
||||
schema: ZProductUpdateInput,
|
||||
schema: ZProjectUpdateInput,
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const organization = await getOrganization(organizationId);
|
||||
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const organizationProjectsLimit = await getOrganizationProjectsLimit(organization);
|
||||
const organizationProjectsCount = await getOrganizationProjectsCount(organization.id);
|
||||
|
||||
if (organizationProjectsCount >= organizationProjectsLimit) {
|
||||
throw new OperationNotAllowedError("Organization project limit reached");
|
||||
}
|
||||
|
||||
if (parsedInput.data.teamIds && parsedInput.data.teamIds.length > 0) {
|
||||
const organization = await getOrganization(organizationId);
|
||||
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const canDoRoleManagement = await getRoleManagementPermission(organization);
|
||||
|
||||
if (!canDoRoleManagement) {
|
||||
@@ -99,7 +61,7 @@ export const createProductAction = authenticatedActionClient
|
||||
}
|
||||
}
|
||||
|
||||
const product = await createProduct(parsedInput.organizationId, parsedInput.data);
|
||||
const project = await createProject(parsedInput.organizationId, parsedInput.data);
|
||||
const updatedNotificationSettings = {
|
||||
...user.notificationSettings,
|
||||
alert: {
|
||||
@@ -107,7 +69,7 @@ export const createProductAction = authenticatedActionClient
|
||||
},
|
||||
weeklySummary: {
|
||||
...user.notificationSettings?.weeklySummary,
|
||||
[product.id]: true,
|
||||
[project.id]: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -115,5 +77,5 @@ export const createProductAction = authenticatedActionClient
|
||||
notificationSettings: updatedNotificationSettings,
|
||||
});
|
||||
|
||||
return product;
|
||||
return project;
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { actionClient, authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromActionClassId, getProductIdFromActionClassId } from "@/lib/utils/helper";
|
||||
import { getOrganizationIdFromActionClassId, getProjectIdFromActionClassId } from "@/lib/utils/helper";
|
||||
import { z } from "zod";
|
||||
import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
|
||||
import { cache } from "@formbricks/lib/cache";
|
||||
@@ -27,9 +27,9 @@ export const deleteActionClassAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
productId: await getProductIdFromActionClassId(parsedInput.actionClassId),
|
||||
projectId: await getProjectIdFromActionClassId(parsedInput.actionClassId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -59,9 +59,9 @@ export const updateActionClassAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
productId: await getProductIdFromActionClassId(parsedInput.actionClassId),
|
||||
projectId: await getProjectIdFromActionClassId(parsedInput.actionClassId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -89,9 +89,9 @@ export const getActiveInactiveSurveysAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "read",
|
||||
productId: await getProductIdFromActionClassId(parsedInput.actionClassId),
|
||||
projectId: await getProjectIdFromActionClassId(parsedInput.actionClassId),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ActionClassDataRow } from "@/app/(app)/environments/[environmentId]/act
|
||||
import { ActionTableHeading } from "@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading";
|
||||
import { AddActionModal } from "@/app/(app)/environments/[environmentId]/actions/components/AddActionModal";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
@@ -16,7 +16,7 @@ import { getEnvironments } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -27,10 +27,10 @@ const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const t = await getTranslations();
|
||||
const [actionClasses, organization, product] = await Promise.all([
|
||||
const [actionClasses, organization, project] = await Promise.all([
|
||||
getActionClasses(params.environmentId),
|
||||
getOrganizationByEnvironmentId(params.environmentId),
|
||||
getProductByEnvironmentId(params.environmentId),
|
||||
getProjectByEnvironmentId(params.environmentId),
|
||||
]);
|
||||
const locale = await findMatchingLocale();
|
||||
|
||||
@@ -42,11 +42,11 @@ const Page = async (props) => {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const environments = await getEnvironments(product.id);
|
||||
const environments = await getEnvironments(project.id);
|
||||
const currentEnvironment = environments.find((env) => env.id === params.environmentId);
|
||||
|
||||
if (!currentEnvironment) {
|
||||
@@ -60,9 +60,9 @@ const Page = async (props) => {
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isMember, isBilling } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const productPermission = await getProductPermissionByUserId(session?.user.id, product.id);
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, project.id);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
if (isBilling) {
|
||||
return redirect(`/environments/${params.environmentId}/settings/billing`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
||||
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
|
||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getEnterpriseLicense, getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
||||
import { LimitsReachedBanner } from "@/modules/ui/components/limits-reached-banner";
|
||||
import { PendingDowngradeBanner } from "@/modules/ui/components/pending-downgrade-banner";
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
getOrganizationByEnvironmentId,
|
||||
getOrganizationsByUserId,
|
||||
} from "@formbricks/lib/organization/service";
|
||||
import { getUserProducts } from "@formbricks/lib/product/service";
|
||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
|
||||
interface EnvironmentLayoutProps {
|
||||
@@ -47,13 +47,13 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
|
||||
const [products, environments] = await Promise.all([
|
||||
getUserProducts(user.id, organization.id),
|
||||
getEnvironments(environment.productId),
|
||||
const [projects, environments] = await Promise.all([
|
||||
getUserProjects(user.id, organization.id),
|
||||
getEnvironments(environment.projectId),
|
||||
]);
|
||||
|
||||
if (!products || !environments || !organizations) {
|
||||
throw new Error(t("environments.products_environments_organizations_not_found"));
|
||||
if (!projects || !environments || !organizations) {
|
||||
throw new Error(t("environments.projects_environments_organizations_not_found"));
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
@@ -62,10 +62,10 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
||||
|
||||
const { features, lastChecked, isPendingDowngrade, active } = await getEnterpriseLicense();
|
||||
|
||||
const productPermission = await getProductPermissionByUserId(session.user.id, environment.productId);
|
||||
const projectPermission = await getProjectPermissionByUserId(session.user.id, environment.projectId);
|
||||
|
||||
if (isMember && !productPermission) {
|
||||
throw new Error(t("common.product_permission_not_found"));
|
||||
if (isMember && !projectPermission) {
|
||||
throw new Error(t("common.project_permission_not_found"));
|
||||
}
|
||||
|
||||
const isMultiOrgEnabled = features?.isMultiOrgEnabled ?? false;
|
||||
@@ -80,6 +80,8 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
||||
]);
|
||||
}
|
||||
|
||||
const organizationProjectsLimit = await getOrganizationProjectsLimit(organization);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen min-h-screen flex-col overflow-hidden">
|
||||
<DevEnvironmentBanner environment={environment} />
|
||||
@@ -105,18 +107,20 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
||||
environment={environment}
|
||||
organization={organization}
|
||||
organizations={organizations}
|
||||
products={products}
|
||||
projects={projects}
|
||||
organizationProjectsLimit={organizationProjectsLimit}
|
||||
user={user}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
membershipRole={membershipRole}
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
isLicenseActive={active}
|
||||
/>
|
||||
<div id="mainContent" className="flex-1 overflow-y-auto bg-slate-50">
|
||||
<TopControlBar
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
membershipRole={membershipRole}
|
||||
productPermission={productPermission}
|
||||
projectPermission={projectPermission}
|
||||
/>
|
||||
<div className="mt-14">{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NavigationLink } from "@/app/(app)/environments/[environmentId]/compone
|
||||
import { formbricksLogout } from "@/app/lib/formbricks";
|
||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
||||
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
||||
import { ProjectSwitcher } from "@/modules/projects/components/project-switcher";
|
||||
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import {
|
||||
@@ -22,15 +23,11 @@ import {
|
||||
} from "@/modules/ui/components/dropdown-menu";
|
||||
import {
|
||||
ArrowUpRightIcon,
|
||||
BlendIcon,
|
||||
BlocksIcon,
|
||||
ChevronRightIcon,
|
||||
Cog,
|
||||
CreditCardIcon,
|
||||
GlobeIcon,
|
||||
GlobeLockIcon,
|
||||
KeyIcon,
|
||||
LinkIcon,
|
||||
LogOutIcon,
|
||||
MessageCircle,
|
||||
MousePointerClick,
|
||||
@@ -54,7 +51,7 @@ import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import packageJson from "../../../../../package.json";
|
||||
|
||||
@@ -63,10 +60,12 @@ interface NavigationProps {
|
||||
organizations: TOrganization[];
|
||||
user: TUser;
|
||||
organization: TOrganization;
|
||||
products: TProduct[];
|
||||
projects: TProject[];
|
||||
isMultiOrgEnabled: boolean;
|
||||
isFormbricksCloud?: boolean;
|
||||
membershipRole?: TOrganizationRole;
|
||||
organizationProjectsLimit: number;
|
||||
isLicenseActive: boolean;
|
||||
}
|
||||
|
||||
export const MainNavigation = ({
|
||||
@@ -74,10 +73,12 @@ export const MainNavigation = ({
|
||||
organizations,
|
||||
organization,
|
||||
user,
|
||||
products,
|
||||
projects,
|
||||
isMultiOrgEnabled,
|
||||
isFormbricksCloud = true,
|
||||
membershipRole,
|
||||
organizationProjectsLimit,
|
||||
isLicenseActive,
|
||||
}: NavigationProps) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -89,7 +90,7 @@ export const MainNavigation = ({
|
||||
const [isTextVisible, setIsTextVisible] = useState(true);
|
||||
const [latestVersion, setLatestVersion] = useState("");
|
||||
|
||||
const product = products.find((product) => product.id === environment.productId);
|
||||
const project = projects.find((project) => project.id === environment.projectId);
|
||||
const { isManager, isOwner, isMember, isBilling } = getAccessFlags(membershipRole);
|
||||
|
||||
const isOwnerOrManager = isManager || isOwner;
|
||||
@@ -124,39 +125,31 @@ export const MainNavigation = ({
|
||||
return [...organizations].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [organizations]);
|
||||
|
||||
const sortedProducts = useMemo(() => {
|
||||
const sortedProjects = useMemo(() => {
|
||||
const channelOrder: (string | null)[] = ["website", "app", "link", null];
|
||||
|
||||
const groupedProducts = products.reduce(
|
||||
(acc, product) => {
|
||||
const channel = product.config.channel;
|
||||
const groupedProjects = projects.reduce(
|
||||
(acc, project) => {
|
||||
const channel = project.config.channel;
|
||||
const key = channel !== null ? channel : "null";
|
||||
acc[key] = acc[key] || [];
|
||||
acc[key].push(product);
|
||||
acc[key].push(project);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, typeof products>
|
||||
{} as Record<string, typeof projects>
|
||||
);
|
||||
|
||||
Object.keys(groupedProducts).forEach((channel) => {
|
||||
groupedProducts[channel].sort((a, b) => a.name.localeCompare(b.name));
|
||||
Object.keys(groupedProjects).forEach((channel) => {
|
||||
groupedProjects[channel].sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
return channelOrder.flatMap((channel) => groupedProducts[channel !== null ? channel : "null"] || []);
|
||||
}, [products]);
|
||||
|
||||
const handleEnvironmentChangeByProduct = (productId: string) => {
|
||||
router.push(`/products/${productId}/`);
|
||||
};
|
||||
return channelOrder.flatMap((channel) => groupedProjects[channel !== null ? channel : "null"] || []);
|
||||
}, [projects]);
|
||||
|
||||
const handleEnvironmentChangeByOrganization = (organizationId: string) => {
|
||||
router.push(`/organizations/${organizationId}/`);
|
||||
};
|
||||
|
||||
const handleAddProduct = (organizationId: string) => {
|
||||
router.push(`/organizations/${organizationId}/products/new/mode`);
|
||||
};
|
||||
|
||||
const mainNavigation = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -189,9 +182,9 @@ export const MainNavigation = ({
|
||||
},
|
||||
{
|
||||
name: t("common.configuration"),
|
||||
href: `/environments/${environment.id}/product/general`,
|
||||
href: `/environments/${environment.id}/project/general`,
|
||||
icon: Cog,
|
||||
isActive: pathname?.includes("/product"),
|
||||
isActive: pathname?.includes("/project"),
|
||||
},
|
||||
],
|
||||
[environment.id, pathname, isMember]
|
||||
@@ -247,7 +240,7 @@ export const MainNavigation = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{product && (
|
||||
{project && (
|
||||
<aside
|
||||
className={cn(
|
||||
"z-40 flex flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100",
|
||||
@@ -319,102 +312,20 @@ export const MainNavigation = ({
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Product Switch */}
|
||||
{/* Project Switch */}
|
||||
{!isBilling && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
id="productDropdownTrigger"
|
||||
className="w-full rounded-br-xl border-t py-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-none">
|
||||
<div
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
"flex cursor-pointer flex-row items-center space-x-3",
|
||||
isCollapsed ? "pl-2" : "pl-4"
|
||||
)}>
|
||||
<div className="rounded-lg bg-slate-900 p-1.5 text-slate-50">
|
||||
{product.config.channel === "website" ? (
|
||||
<GlobeIcon strokeWidth={1.5} />
|
||||
) : product.config.channel === "app" ? (
|
||||
<GlobeLockIcon strokeWidth={1.5} />
|
||||
) : product.config.channel === "link" ? (
|
||||
<LinkIcon strokeWidth={1.5} />
|
||||
) : (
|
||||
<BlendIcon strokeWidth={1.5} />
|
||||
)}
|
||||
</div>
|
||||
{!isCollapsed && !isTextVisible && (
|
||||
<>
|
||||
<div>
|
||||
<p
|
||||
title={product.name}
|
||||
className={cn(
|
||||
"ph-no-capture ph-no-capture -mb-0.5 max-w-28 truncate text-sm font-bold text-slate-700 transition-opacity duration-200",
|
||||
isTextVisible ? "opacity-0" : "opacity-100"
|
||||
)}>
|
||||
{product.name}
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm text-slate-500 transition-opacity duration-200",
|
||||
isTextVisible ? "opacity-0" : "opacity-100"
|
||||
)}>
|
||||
{product.config.channel === "link"
|
||||
? "Link & Email"
|
||||
: capitalizeFirstLetter(product.config.channel)}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRightIcon
|
||||
className={cn(
|
||||
"h-5 w-5 text-slate-700 transition-opacity duration-200 hover:text-slate-500",
|
||||
isTextVisible ? "opacity-0" : "opacity-100"
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
id="userDropdownInnerContentWrapper"
|
||||
side="right"
|
||||
sideOffset={10}
|
||||
alignOffset={-1}
|
||||
align="end">
|
||||
<DropdownMenuRadioGroup
|
||||
value={product!.id}
|
||||
onValueChange={(v) => handleEnvironmentChangeByProduct(v)}>
|
||||
{sortedProducts.map((product) => (
|
||||
<DropdownMenuRadioItem
|
||||
value={product.id}
|
||||
className="cursor-pointer break-all"
|
||||
key={product.id}>
|
||||
<div>
|
||||
{product.config.channel === "website" ? (
|
||||
<GlobeIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
||||
) : product.config.channel === "app" ? (
|
||||
<GlobeLockIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
||||
) : product.config.channel === "link" ? (
|
||||
<LinkIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
||||
) : (
|
||||
<BlendIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
||||
)}
|
||||
</div>
|
||||
<div className="">{product?.name}</div>
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
{isOwnerOrManager && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleAddProduct(organization.id)}
|
||||
icon={<PlusIcon className="mr-2 h-4 w-4" />}>
|
||||
<span>{t("common.add_product")}</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ProjectSwitcher
|
||||
environmentId={environment.id}
|
||||
projects={sortedProjects}
|
||||
project={project}
|
||||
isCollapsed={isCollapsed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isLicenseActive={isLicenseActive}
|
||||
isOwnerOrManager={isOwnerOrManager}
|
||||
isTextVisible={isTextVisible}
|
||||
organization={organization}
|
||||
organizationProjectsLimit={organizationProjectsLimit}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* User Switch */}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import Link from "next/link";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface ProductNavItemProps {
|
||||
interface ProjectNavItemProps {
|
||||
href: string;
|
||||
children: ReactNode;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export const ProductNavItem = ({ href, children, isActive }: ProductNavItemProps) => {
|
||||
export const ProjectNavItem = ({ href, children, isActive }: ProjectNavItemProps) => {
|
||||
const activeClass = "bg-slate-50 font-semibold";
|
||||
const inactiveClass = "hover:bg-slate-50";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TopControlButtons } from "@/app/(app)/environments/[environmentId]/components/TopControlButtons";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/product-teams/types/teams";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
@@ -8,14 +8,14 @@ interface SideBarProps {
|
||||
environment: TEnvironment;
|
||||
environments: TEnvironment[];
|
||||
membershipRole?: TOrganizationRole;
|
||||
productPermission: TTeamPermission | null;
|
||||
projectPermission: TTeamPermission | null;
|
||||
}
|
||||
|
||||
export const TopControlBar = ({
|
||||
environment,
|
||||
environments,
|
||||
membershipRole,
|
||||
productPermission,
|
||||
projectPermission,
|
||||
}: SideBarProps) => {
|
||||
return (
|
||||
<div className="fixed inset-0 top-0 z-30 flex h-14 w-full items-center justify-end bg-slate-50 px-6">
|
||||
@@ -26,7 +26,7 @@ export const TopControlBar = ({
|
||||
environments={environments}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
membershipRole={membershipRole}
|
||||
productPermission={productPermission}
|
||||
projectPermission={projectPermission}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { EnvironmentSwitch } from "@/app/(app)/environments/[environmentId]/components/EnvironmentSwitch";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/product-teams/types/teams";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { CircleUserIcon, MessageCircleQuestionIcon, PlusIcon } from "lucide-react";
|
||||
@@ -17,7 +17,7 @@ interface TopControlButtonsProps {
|
||||
environments: TEnvironment[];
|
||||
isFormbricksCloud: boolean;
|
||||
membershipRole?: TOrganizationRole;
|
||||
productPermission: TTeamPermission | null;
|
||||
projectPermission: TTeamPermission | null;
|
||||
}
|
||||
|
||||
export const TopControlButtons = ({
|
||||
@@ -25,13 +25,13 @@ export const TopControlButtons = ({
|
||||
environments,
|
||||
isFormbricksCloud,
|
||||
membershipRole,
|
||||
productPermission,
|
||||
projectPermission,
|
||||
}: TopControlButtonsProps) => {
|
||||
const t = useTranslations();
|
||||
const router = useRouter();
|
||||
|
||||
const { isMember, isBilling } = getAccessFlags(membershipRole);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,13 +12,13 @@ export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProp
|
||||
const stati = {
|
||||
notImplemented: {
|
||||
icon: AlertTriangleIcon,
|
||||
title: t("environments.product.app-connection.formbricks_sdk_not_connected"),
|
||||
subtitle: t("environments.product.app-connection.formbricks_sdk_not_connected_description"),
|
||||
title: t("environments.project.app-connection.formbricks_sdk_not_connected"),
|
||||
subtitle: t("environments.project.app-connection.formbricks_sdk_not_connected_description"),
|
||||
},
|
||||
running: {
|
||||
icon: CheckIcon,
|
||||
title: t("environments.product.app-connection.receiving_data"),
|
||||
subtitle: t("environments.product.app-connection.formbricks_sdk_connected"),
|
||||
title: t("environments.project.app-connection.receiving_data"),
|
||||
subtitle: t("environments.project.app-connection.formbricks_sdk_connected"),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"
|
||||
import {
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getOrganizationIdFromIntegrationId,
|
||||
getProductIdFromEnvironmentId,
|
||||
getProductIdFromIntegrationId,
|
||||
getProjectIdFromEnvironmentId,
|
||||
getProjectIdFromIntegrationId,
|
||||
} from "@/lib/utils/helper";
|
||||
import { z } from "zod";
|
||||
import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service";
|
||||
@@ -30,9 +30,9 @@ export const createOrUpdateIntegrationAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
productId: await getProductIdFromEnvironmentId(parsedInput.environmentId),
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -56,8 +56,8 @@ export const deleteIntegrationAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
productId: await getProductIdFromIntegrationId(parsedInput.integrationId),
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromIntegrationId(parsedInput.integrationId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
@@ -15,7 +15,7 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
@@ -40,9 +40,9 @@ const Page = async (props) => {
|
||||
if (!environment) {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
const product = await getProductByEnvironmentId(params.environmentId);
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
const project = await getProjectByEnvironmentId(params.environmentId);
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const airtableIntegration: TIntegrationAirtable | undefined = integrations?.find(
|
||||
@@ -58,13 +58,13 @@ const Page = async (props) => {
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(
|
||||
session?.user.id,
|
||||
product.organizationId
|
||||
project.organizationId
|
||||
);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const productPermission = await getProductPermissionByUserId(session?.user.id, environment?.productId);
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
@@ -19,7 +19,7 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||
@@ -43,9 +43,9 @@ const Page = async (props) => {
|
||||
if (!environment) {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
const product = await getProductByEnvironmentId(params.environmentId);
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
const project = await getProjectByEnvironmentId(params.environmentId);
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const googleSheetIntegration: TIntegrationGoogleSheets | undefined = integrations?.find(
|
||||
@@ -56,13 +56,13 @@ const Page = async (props) => {
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(
|
||||
session?.user.id,
|
||||
product.organizationId
|
||||
project.organizationId
|
||||
);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const productPermission = await getProductPermissionByUserId(session?.user.id, environment?.productId);
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
@@ -21,7 +21,7 @@ import { getIntegrationByType } from "@formbricks/lib/integration/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getNotionDatabases } from "@formbricks/lib/notion/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
|
||||
@@ -51,9 +51,9 @@ const Page = async (props) => {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
|
||||
const product = await getProductByEnvironmentId(params.environmentId);
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
const project = await getProjectByEnvironmentId(params.environmentId);
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
let databasesArray: TIntegrationNotionDatabase[] = [];
|
||||
@@ -64,13 +64,13 @@ const Page = async (props) => {
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(
|
||||
session?.user.id,
|
||||
product.organizationId
|
||||
project.organizationId
|
||||
);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const productPermission = await getProductPermissionByUserId(session?.user.id, environment?.productId);
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import SlackLogo from "@/images/slacklogo.png";
|
||||
import WebhookLogo from "@/images/webhook.png";
|
||||
import ZapierLogo from "@/images/zapier-small.png";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { Card } from "@/modules/ui/components/integration-card";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
@@ -66,9 +66,9 @@ const Page = async (props) => {
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isMember, isBilling } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const productPermission = await getProductPermissionByUserId(session?.user.id, environment?.productId);
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
@@ -222,7 +222,7 @@ const Page = async (props) => {
|
||||
docsHref: "https://formbricks.com/docs/app-surveys/quickstart",
|
||||
docsText: t("common.docs"),
|
||||
docsNewTab: true,
|
||||
connectHref: `/environments/${environmentId}/product/app-connection`,
|
||||
connectHref: `/environments/${environmentId}/project/app-connection`,
|
||||
connectText: t("common.connect"),
|
||||
connectNewTab: false,
|
||||
label: "Javascript SDK",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromEnvironmentId, getProductIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { z } from "zod";
|
||||
import { getSlackChannels } from "@formbricks/lib/slack/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
@@ -23,8 +23,8 @@ export const getSlackChannelsAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
productId: await getProductIdFromEnvironmentId(parsedInput.environmentId),
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
@@ -14,7 +14,7 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrationByType } from "@formbricks/lib/integration/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
||||
@@ -39,22 +39,22 @@ const Page = async (props) => {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
|
||||
const product = await getProductByEnvironmentId(params.environmentId);
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
const project = await getProjectByEnvironmentId(params.environmentId);
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const locale = await findMatchingLocale();
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(
|
||||
session?.user.id,
|
||||
product.organizationId
|
||||
project.organizationId
|
||||
);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const productPermission = await getProductPermissionByUserId(session?.user.id, environment?.productId);
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"
|
||||
import {
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getOrganizationIdFromWebhookId,
|
||||
getProductIdFromEnvironmentId,
|
||||
getProjectIdFromEnvironmentId,
|
||||
} from "@/lib/utils/helper";
|
||||
import { z } from "zod";
|
||||
import { createWebhook, deleteWebhook, updateWebhook } from "@formbricks/lib/webhook/service";
|
||||
@@ -30,9 +30,9 @@ export const createWebhookAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "read",
|
||||
productId: await getProductIdFromEnvironmentId(parsedInput.environmentId),
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -56,9 +56,9 @@ export const deleteWebhookAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
productId: await getProductIdFromEnvironmentId(parsedInput.id),
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.id),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -83,9 +83,9 @@ export const updateWebhookAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
productId: await getProductIdFromEnvironmentId(parsedInput.webhookId),
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.webhookId),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { WebhookRowData } from "@/app/(app)/environments/[environmentId]/integra
|
||||
import { WebhookTable } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTable";
|
||||
import { WebhookTableHeading } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTableHeading";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
@@ -44,9 +44,9 @@ const Page = async (props) => {
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const productPermission = await getProductPermissionByUserId(session?.user.id, environment?.productId);
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(productPermission);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { notFound, redirect } from "next/navigation";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { FormbricksClient } from "../../components/FormbricksClient";
|
||||
@@ -40,9 +40,9 @@ export const EnvLayout = async (props) => {
|
||||
if (!organization) {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
const product = await getProductByEnvironmentId(params.environmentId);
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
const project = await getProjectByEnvironmentId(params.environmentId);
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromProductId } from "@/lib/utils/helper";
|
||||
import { z } from "zod";
|
||||
import { deleteProduct, getUserProducts } from "@formbricks/lib/product/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
|
||||
const ZProductDeleteAction = z.object({
|
||||
productId: ZId,
|
||||
});
|
||||
|
||||
export const deleteProductAction = authenticatedActionClient
|
||||
.schema(ZProductDeleteAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromProductId(parsedInput.productId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const availableProducts = (await getUserProducts(ctx.user.id, organizationId)) ?? null;
|
||||
|
||||
if (!!availableProducts && availableProducts?.length <= 1) {
|
||||
throw new Error("You can't delete the last product in the environment.");
|
||||
}
|
||||
|
||||
// delete product
|
||||
return await deleteProduct(parsedInput.productId);
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
return redirect(`/environments/${params.environmentId}/product/general`);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,3 +0,0 @@
|
||||
import { ProductTeams } from "@/modules/ee/teams/product-teams/page";
|
||||
|
||||
export default ProductTeams;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { AppConnectionLoading } from "@/modules/projects/settings/(setup)/app-connection/loading";
|
||||
|
||||
export default AppConnectionLoading;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { AppConnectionPage } from "@/modules/projects/settings/(setup)/app-connection/page";
|
||||
|
||||
export default AppConnectionPage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { APIKeysLoading } from "@/modules/projects/settings/api-keys/loading";
|
||||
|
||||
export default APIKeysLoading;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { APIKeysPage } from "@/modules/projects/settings/api-keys/page";
|
||||
|
||||
export default APIKeysPage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { GeneralSettingsLoading } from "@/modules/projects/settings/general/loading";
|
||||
|
||||
export default GeneralSettingsLoading;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { GeneralSettingsPage } from "@/modules/projects/settings/general/page";
|
||||
|
||||
export default GeneralSettingsPage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { LanguagesLoading } from "@/modules/ee/languages/loading";
|
||||
|
||||
export default LanguagesLoading;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { LanguagesPage } from "@/modules/ee/languages/page";
|
||||
|
||||
export default LanguagesPage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ProjectSettingsLayout } from "@/modules/projects/settings/layout";
|
||||
|
||||
export default ProjectSettingsLayout;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ProjectLookSettingsLoading } from "@/modules/projects/settings/look/loading";
|
||||
|
||||
export default ProjectLookSettingsLoading;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ProjectLookSettingsPage } from "@/modules/projects/settings/look/page";
|
||||
|
||||
export default ProjectLookSettingsPage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ProjectSettingsPage } from "@/modules/projects/settings/page";
|
||||
|
||||
export default ProjectSettingsPage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { TagsLoading } from "@/modules/projects/settings/tags/loading";
|
||||
|
||||
export default TagsLoading;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { TagsPage } from "@/modules/projects/settings/tags/page";
|
||||
|
||||
export default TagsPage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ProjectTeams } from "@/modules/ee/teams/project-teams/page";
|
||||
|
||||
export default ProjectTeams;
|
||||
@@ -2,7 +2,7 @@ import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
|
||||
const AccountSettingsLayout = async (props) => {
|
||||
const params = await props.params;
|
||||
@@ -10,9 +10,9 @@ const AccountSettingsLayout = async (props) => {
|
||||
const { children } = props;
|
||||
|
||||
const t = await getTranslations();
|
||||
const [organization, product, session] = await Promise.all([
|
||||
const [organization, project, session] = await Promise.all([
|
||||
getOrganizationByEnvironmentId(params.environmentId),
|
||||
getProductByEnvironmentId(params.environmentId),
|
||||
getProjectByEnvironmentId(params.environmentId),
|
||||
getServerSession(authOptions),
|
||||
]);
|
||||
|
||||
@@ -20,8 +20,8 @@ const AccountSettingsLayout = async (props) => {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
throw new Error(t("common.product_not_found"));
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user