chore: Rename Teams to Organizations (#2656)

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Piyush Gupta
2024-05-27 11:11:21 +05:30
committed by GitHub
parent db03ce70d2
commit 295754480e
166 changed files with 1863 additions and 1459 deletions

View File

@@ -88,7 +88,7 @@ PASSWORD_RESET_DISABLED=1
# Email login. Disable the ability for users to login with email.
# EMAIL_AUTH_DISABLED=1
# Team Invite. Disable the ability for invited users to create an account.
# Organization Invite. Disable the ability for invited users to create an account.
# INVITE_DISABLED=1
##########
@@ -154,11 +154,11 @@ SLACK_CLIENT_SECRET=
# Enterprise License Key
ENTERPRISE_LICENSE_KEY=
# Automatically assign new users to a specific team and role within that team
# Insert an existing team id or generate a valid CUID for a new one at https://www.getuniqueid.com/cuid (e.g. cjld2cjxh0000qzrmn831i7rn)
# Automatically assign new users to a specific organization and role within that organization
# Insert an existing organization id or generate a valid CUID for a new one at https://www.getuniqueid.com/cuid (e.g. cjld2cjxh0000qzrmn831i7rn)
# (Role Management is an Enterprise feature)
# DEFAULT_TEAM_ID=
# DEFAULT_TEAM_ROLE=admin
# DEFAULT_ORGANIZATION_ID=
# DEFAULT_ORGANIZATION_ROLE=admin
# set to 1 to skip onboarding for new users
# ONBOARDING_DISABLED=1

131
.github/workflows/kamal-deploy.yml vendored Normal file
View File

@@ -0,0 +1,131 @@
name: Kamal Deploy
concurrency:
group: deploy-to-kamal
cancel-in-progress: false
on:
workflow_dispatch:
push:
branches:
- main
jobs:
Deploy:
runs-on: ubuntu-latest
environment: production
env:
DOCKER_BUILDKIT: 1
IS_FORMBRICKS_CLOUD: ${{ vars.IS_FORMBRICKS_CLOUD }}
WEBAPP_URL: ${{ vars.WEBAPP_URL }}
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
NEXTAUTH_URL: ${{ vars.NEXTAUTH_URL }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
SHORT_URL_BASE: ${{ vars.SHORT_URL_BASE }}
MAIL_FROM: ${{ secrets.MAIL_FROM }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_PORT: ${{ secrets.SMTP_PORT }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
PRIVACY_URL: ${{ vars.PRIVACY_URL }}
TERMS_URL: ${{ vars.TERMS_URL }}
IMPRINT_URL: ${{ vars.IMPRINT_URL }}
GITHUB_ID: ${{ secrets.FB_GITHUB_ID }}
GITHUB_SECRET: ${{ secrets.FB_GITHUB_SECRET }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
AZUREAD_CLIENT_ID: ${{ secrets.AZUREAD_CLIENT_ID }}
AZUREAD_CLIENT_SECRET: ${{ secrets.AZUREAD_CLIENT_SECRET }}
AZUREAD_TENANT_ID: ${{ secrets.AZUREAD_TENANT_ID }}
OIDC_CLIENT_ID: ${{ secrets.OIDC_CLIENT_ID }}
OIDC_CLIENT_SECRET: ${{ secrets.OIDC_CLIENT_SECRET }}
OIDC_ISSUER: ${{ secrets.OIDC_ISSUER }}
OIDC_DISPLAY_NAME: ${{ secrets.OIDC_DISPLAY_NAME }}
OIDC_SIGNING_ALGORITHM: ${{ secrets.OIDC_SIGNING_ALGORITHM }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
ASSET_PREFIX_URL: ${{ vars.ASSET_PREFIX_URL }}
NOTION_OAUTH_CLIENT_ID: ${{ secrets.NOTION_OAUTH_CLIENT_ID }}
NOTION_OAUTH_CLIENT_SECRET: ${{ secrets.NOTION_OAUTH_CLIENT_SECRET }}
SLACK_CLIENT_ID: ${{ secrets.SLACK_CLIENT_ID }}
SLACK_CLIENT_SECRET: ${{ secrets.SLACK_CLIENT_SECRET }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
GOOGLE_SHEETS_CLIENT_ID: ${{ secrets.GOOGLE_SHEETS_CLIENT_ID }}
GOOGLE_SHEETS_CLIENT_SECRET: ${{ secrets.GOOGLE_SHEETS_CLIENT_SECRET }}
GOOGLE_SHEETS_REDIRECT_URL: ${{ secrets.GOOGLE_SHEETS_REDIRECT_URL }}
AIRTABLE_CLIENT_ID: ${{ secrets.AIRTABLE_CLIENT_ID }}
ENTERPRISE_LICENSE_KEY: ${{ secrets.ENTERPRISE_LICENSE_KEY }}
DEFAULT_ORGANIZATION_ID: ${{ vars.DEFAULT_ORGANIZATION_ID }}
ONBOARDING_DISABLED: ${{ vars.ONBOARDING_DISABLED }}
CUSTOMER_IO_API_KEY: ${{ secrets.CUSTOMER_IO_API_KEY }}
CUSTOMER_IO_SITE_ID: ${{ secrets.CUSTOMER_IO_SITE_ID }}
NEXT_PUBLIC_POSTHOG_API_KEY: ${{ vars.NEXT_PUBLIC_POSTHOG_API_KEY }}
NEXT_PUBLIC_POSTHOG_API_HOST: ${{ vars.NEXT_PUBLIC_POSTHOG_API_HOST }}
NEXT_PUBLIC_FORMBRICKS_API_HOST: ${{ vars.NEXT_PUBLIC_FORMBRICKS_API_HOST }}
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID }}
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID }}
NEXT_PUBLIC_SENTRY_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
NODE_ENV: production
CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}
CLOUDFLARE_DNS_API_TOKEN: ${{ secrets.CLOUDFLARE_DNS_API_TOKEN }}
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
S3_REGION: ${{ vars.S3_REGION }}
S3_BUCKET_NAME: ${{ vars.S3_BUCKET_NAME }}
OPENTELEMETRY_LISTENER_URL: ${{ vars.OPENTELEMETRY_LISTENER_URL }}
RATE_LIMITING_DISABLED: ${{ vars.RATE_LIMITING_DISABLED }}
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
DB_HOST: ${{ secrets.DB_HOST }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
DB_NAME: ${{ secrets.DB_NAME }}
REDIS_URL: ${{ secrets.REDIS_URL }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.0
bundler-cache: true
- name: Install dependencies
run: |
gem install kamal
- uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Create builder
run: docker buildx create --use --name formbricks-gh-actions-builder
if: steps.buildx.outputs.should_create_builder == 'true'
- name: Push env variables to Kamal
run: |
kamal() { command kamal "$@" -c kamal/deploy.yml; }
kamal env push
- name: Run deploy command
run: |
kamal() { command kamal "$@" -c kamal/deploy.yml; }
set +e
DEPLOY_OUTPUT=$(kamal deploy 2>&1)
DEPLOY_EXIT_CODE=$?
echo "$DEPLOY_OUTPUT"
if [[ "$DEPLOY_OUTPUT" == *"container not unhealthy (healthy)"* ]]; then
echo "Deployment reported healthy container. Considering as success."
kamal lock release
exit 0
else
exit $DEPLOY_EXIT_CODE
fi
shell: bash

128
.github/workflows/kamal-setup.yml vendored Normal file
View File

@@ -0,0 +1,128 @@
name: Kamal Setup
concurrency:
group: setup-kamal
cancel-in-progress: false
on:
workflow_dispatch: # Only to be triggered when accessories are updated
jobs:
Setup:
runs-on: ubuntu-latest
environment: production
env:
DOCKER_BUILDKIT: 1
IS_FORMBRICKS_CLOUD: ${{ vars.IS_FORMBRICKS_CLOUD }}
WEBAPP_URL: ${{ vars.WEBAPP_URL }}
NEXTAUTH_URL: ${{ vars.NEXTAUTH_URL }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
SHORT_URL_BASE: ${{ vars.SHORT_URL_BASE }}
MAIL_FROM: ${{ secrets.MAIL_FROM }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_PORT: ${{ secrets.SMTP_PORT }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
PRIVACY_URL: ${{ vars.PRIVACY_URL }}
TERMS_URL: ${{ vars.TERMS_URL }}
IMPRINT_URL: ${{ vars.IMPRINT_URL }}
GITHUB_ID: ${{ secrets.FB_GITHUB_ID }}
GITHUB_SECRET: ${{ secrets.FB_GITHUB_SECRET }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
AZUREAD_CLIENT_ID: ${{ secrets.AZUREAD_CLIENT_ID }}
AZUREAD_CLIENT_SECRET: ${{ secrets.AZUREAD_CLIENT_SECRET }}
AZUREAD_TENANT_ID: ${{ secrets.AZUREAD_TENANT_ID }}
OIDC_CLIENT_ID: ${{ secrets.OIDC_CLIENT_ID }}
OIDC_CLIENT_SECRET: ${{ secrets.OIDC_CLIENT_SECRET }}
OIDC_ISSUER: ${{ secrets.OIDC_ISSUER }}
OIDC_DISPLAY_NAME: ${{ secrets.OIDC_DISPLAY_NAME }}
OIDC_SIGNING_ALGORITHM: ${{ secrets.OIDC_SIGNING_ALGORITHM }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
ASSET_PREFIX_URL: ${{ vars.ASSET_PREFIX_URL }}
NOTION_OAUTH_CLIENT_ID: ${{ secrets.NOTION_OAUTH_CLIENT_ID }}
NOTION_OAUTH_CLIENT_SECRET: ${{ secrets.NOTION_OAUTH_CLIENT_SECRET }}
SLACK_CLIENT_ID: ${{ secrets.SLACK_CLIENT_ID }}
SLACK_CLIENT_SECRET: ${{ secrets.SLACK_CLIENT_SECRET }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
GOOGLE_SHEETS_CLIENT_ID: ${{ secrets.GOOGLE_SHEETS_CLIENT_ID }}
GOOGLE_SHEETS_CLIENT_SECRET: ${{ secrets.GOOGLE_SHEETS_CLIENT_SECRET }}
GOOGLE_SHEETS_REDIRECT_URL: ${{ secrets.GOOGLE_SHEETS_REDIRECT_URL }}
AIRTABLE_CLIENT_ID: ${{ secrets.AIRTABLE_CLIENT_ID }}
ENTERPRISE_LICENSE_KEY: ${{ secrets.ENTERPRISE_LICENSE_KEY }}
DEFAULT_ORGANIZATION_ID: ${{ vars.DEFAULT_ORGANIZATION_ID }}
ONBOARDING_DISABLED: ${{ vars.ONBOARDING_DISABLED }}
CUSTOMER_IO_API_KEY: ${{ secrets.CUSTOMER_IO_API_KEY }}
CUSTOMER_IO_SITE_ID: ${{ secrets.CUSTOMER_IO_SITE_ID }}
NEXT_PUBLIC_POSTHOG_API_KEY: ${{ vars.NEXT_PUBLIC_POSTHOG_API_KEY }}
NEXT_PUBLIC_POSTHOG_API_HOST: ${{ vars.NEXT_PUBLIC_POSTHOG_API_HOST }}
NEXT_PUBLIC_FORMBRICKS_API_HOST: ${{ vars.NEXT_PUBLIC_FORMBRICKS_API_HOST }}
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID }}
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID }}
NEXT_PUBLIC_SENTRY_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
NODE_ENV: production
CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}
CLOUDFLARE_DNS_API_TOKEN: ${{ secrets.CLOUDFLARE_DNS_API_TOKEN }}
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
S3_REGION: ${{ vars.S3_REGION }}
S3_BUCKET_NAME: ${{ vars.S3_BUCKET_NAME }}
OPENTELEMETRY_LISTENER_URL: ${{ vars.OPENTELEMETRY_LISTENER_URL }}
RATE_LIMITING_DISABLED: ${{ vars.RATE_LIMITING_DISABLED }}
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
DB_HOST: ${{ secrets.DB_HOST }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
DB_NAME: ${{ secrets.DB_NAME }}
REDIS_URL: ${{ secrets.REDIS_URL }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.0
bundler-cache: true
- name: Install dependencies
run: |
gem install kamal
- uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Create builder
run: docker buildx create --use --name formbricks-gh-actions-builder
if: steps.buildx.outputs.should_create_builder == 'true'
- name: Push env variables to Kamal
run: |
kamal() { command kamal "$@" -c kamal/deploy.yml; }
kamal env push
- name: Run setup command
run: |
kamal() { command kamal "$@" -c kamal/deploy.yml; }
set +e
DEPLOY_OUTPUT=$(kamal setup 2>&1)
DEPLOY_EXIT_CODE=$?
echo "$DEPLOY_OUTPUT"
if [[ "$DEPLOY_OUTPUT" == *"container not unhealthy (healthy)"* ]]; then
echo "Deployment reported healthy container. Considering as success."
kamal lock release
exit 0
else
exit $DEPLOY_EXIT_CODE
fi
shell: bash

View File

@@ -82,7 +82,7 @@ Formbricks is both a free and open source survey platform - and a privacy-first
- 🔗 Create shareable **link surveys**.
- 👨‍👩‍👦 Invite your team members to **collaborate** on your surveys.
- 👨‍👩‍👦 Invite your organization members to **collaborate** on your surveys.
- 🔌 Integrate Formbricks with **Slack, Notion, Zapier, n8n and more**.

View File

@@ -6,7 +6,7 @@ import I2 from "./images/I2.webp";
export const metadata = {
title: "Using Actions in Formbricks | Fine-tuning User Moments",
description:
"Dive deep into how actions in Formbricks help products and teams to engage users at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine user targeting and generate richer, more detailed user insights.",
"Dive deep into how actions in Formbricks help products and organizations to engage users at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine user targeting and generate richer, more detailed user insights.",
};
#### App Surveys

View File

@@ -85,7 +85,7 @@ To run this survey properly, you should pre-segment your user base. As touched u
- Check the time passed since sign-up (e.g. signed up 4 weeks ago)
- User has performed a specific action a certain number of times or (e.g. created 5 reports)
- User has performed a combination of actions (e.g. created a report **and** invited a team member)
- User has performed a combination of actions (e.g. created a report **and** invited a organization member)
This way you make sure that you separate potentially misleading opinions from valuable insights.

View File

@@ -51,8 +51,8 @@ These variables are present inside your machines docker-compose file. Restart
| INSTANCE_ID | Instance ID for Formbricks Cloud to be sent to Telemetry. | optional | |
| INTERNAL_SECRET | Internal Secret (Currently we overwrite the value with a random value). | optional | |
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b |
| DEFAULT_TEAM_ID | Automatically assign new users to a specific team when joining | optional | |
| DEFAULT_TEAM_ROLE | Role of the user in the default team. | optional | admin |
| DEFAULT_ORGANIZATION_ID | Automatically assign new users to a specific organization when joining | optional | |
| DEFAULT_ORGANIZATION_ROLE | Role of the user in the default organization. | optional | admin |
| ONBOARDING_DISABLED | Disables onboarding for new users if set to 1 | optional | |
| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | |
| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |

View File

@@ -12,7 +12,9 @@ export const metadata = {
The Formbricks Core source code is licensed under AGPLv3 and available on GitHub. Additionally, we offer features for bigger organisations & enterprises for self-hostesr under a separate Enterprise License.
<Note>
Want to present a proof of concept? Request a free 30-day Enterprise Edition trial by [filling out the form below.](#30-day-trial-license-request) No call needed or strings attached: Just give us 24h to set up the key and send it over 🤙
Want to present a proof of concept? Request a free 30-day Enterprise Edition trial by [filling out the form
below.](#30-day-trial-license-request) No call needed or strings attached: Just give us 24h to set up the
key and send it over 🤙
</Note>
## Enterprise Edition License
@@ -21,23 +23,24 @@ Additional to the AGPLv3 licensed Formbricks core, the Formbricks repository con
### When do I need an Enterprise License?
| | Community Edition | Enterprise License |
| -------------------------------------------------------- | ----------------- | -------------------- |
| Self-host for commercial purposes | ✅ | No EE license needed |
| | Community Edition | Enterprise License |
| ----------------------------------------------------------- | ----------------- | -------------------- |
| Self-host for commercial purposes | ✅ | No EE license needed |
| Make changes to the code base (have to publish all changes) | ✅ | No EE license needed |
| Unlimited responses | ✅ | No EE license needed |
| Unlimited surveys | ✅ | No EE license needed |
| Remove branding | ✅ | No EE license needed |
| SSO | ✅ | No EE license needed |
| Use any of the other 100 features | ✅ | No EE license needed |
| Team access roles | ❌ | ✅ |
| Multi-language surveys | ❌ | ✅ |
| Advanced targeting / Segments | ❌ | ✅ |
| Make code changes and **keep private** | ❌ | ✅ |
| Unlimited responses | ✅ | No EE license needed |
| Unlimited surveys | ✅ | No EE license needed |
| Remove branding | ✅ | No EE license needed |
| SSO | ✅ | No EE license needed |
| Use any of the other 100 features | ✅ | No EE license needed |
| Organization access roles | ❌ | ✅ |
| Multi-language surveys | ❌ | ✅ |
| Advanced targeting / Segments | ❌ | ✅ |
| Make code changes and **keep private** | ❌ | ✅ |
Ready to get started with the Enterprise Edition? Fill out our form below and we'll reach out to you.
## 30-day Trial License Request
Many organisations want to do an internal test run with the Enterprise Edition. To make that really easy, we now offer a 30-day trial license. Just fill out the form below and we'll send you a license key within 24 hours (business days):
<div

View File

@@ -6,7 +6,7 @@ import StepTwo from "./images/StepTwo.webp";
export const metadata = {
title: "Using Actions in Formbricks | Fine-tuning Session Moments",
description:
"Dive deep into how actions in Formbricks help products and teams to engage active sessions at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine public facing websites' targeting and generate richer, more detailed insights.",
"Dive deep into how actions in Formbricks help products and organizations to engage active sessions at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine public facing websites' targeting and generate richer, more detailed insights.",
};
#### Website Surveys

View File

@@ -7,7 +7,7 @@ import { redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { AuthorizationError } from "@formbricks/types/errors";
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
import { ToasterClient } from "@formbricks/ui/ToasterClient";
@@ -22,9 +22,9 @@ const EnvLayout = async ({ children, params }) => {
throw new AuthorizationError("Not authorized");
}
const team = await getTeamByEnvironmentId(params.environmentId);
if (!team) {
throw new Error("Team not found");
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!organization) {
throw new Error("Organization not found");
}
const environment = await getEnvironment(params.environmentId);
@@ -39,11 +39,11 @@ const EnvLayout = async ({ children, params }) => {
<PosthogIdentify
session={session}
environmentId={params.environmentId}
teamId={team.id}
teamName={team.name}
inAppSurveyBillingStatus={team.billing.features.inAppSurvey.status}
linkSurveyBillingStatus={team.billing.features.linkSurvey.status}
userTargetingBillingStatus={team.billing.features.userTargeting.status}
organizationId={organization.id}
organizationName={organization.name}
inAppSurveyBillingStatus={organization.billing.features.inAppSurvey.status}
linkSurveyBillingStatus={organization.billing.features.linkSurvey.status}
userTargetingBillingStatus={organization.billing.features.userTargeting.status}
/>
<FormbricksClient session={session} />
<ToasterClient />

View File

@@ -6,7 +6,7 @@ import { toast } from "react-hot-toast";
import { extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { createI18nString } from "@formbricks/lib/i18n/utils";
import { useGetBillingInfo } from "@formbricks/lib/team/hooks/useGetBillingInfo";
import { useGetBillingInfo } from "@formbricks/lib/organization/hooks/useGetBillingInfo";
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/common";
import { TProduct } from "@formbricks/types/product";
import { TSurvey, TSurveyFileUploadQuestion } from "@formbricks/types/surveys";
@@ -43,7 +43,7 @@ export const FileUploadQuestionForm = ({
billingInfo,
error: billingInfoError,
isLoading: billingInfoLoading,
} = useGetBillingInfo(product?.teamId ?? "");
} = useGetBillingInfo(product?.organizationId ?? "");
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const handleInputChange = (event) => {

View File

@@ -6,13 +6,13 @@ import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { IS_FORMBRICKS_CLOUD, SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/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 { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
import { getSegments } from "@formbricks/lib/segment/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { SurveyEditor } from "./components/SurveyEditor";
@@ -32,7 +32,7 @@ const Page = async ({ params }) => {
actionClasses,
attributeClasses,
responseCount,
team,
organization,
session,
segments,
] = await Promise.all([
@@ -42,7 +42,7 @@ const Page = async ({ params }) => {
getActionClasses(params.environmentId),
getAttributeClasses(params.environmentId),
getResponseCountBySurveyId(params.surveyId),
getTeamByEnvironmentId(params.environmentId),
getOrganizationByEnvironmentId(params.environmentId),
getServerSession(authOptions),
getSegments(params.environmentId),
]);
@@ -51,16 +51,16 @@ const Page = async ({ params }) => {
throw new Error("Session not found");
}
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isViewer } = getAccessFlags(currentUserMembership?.role);
const isSurveyCreationDeletionDisabled = isViewer;
const isUserTargetingAllowed = await getAdvancedTargetingPermission(team);
const isMultiLanguageAllowed = await getMultiLanguagePermission(team);
const isUserTargetingAllowed = await getAdvancedTargetingPermission(organization);
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
if (
!survey ||

View File

@@ -1,6 +1,6 @@
"use client";
import { createTeamAction } from "@/app/(app)/environments/[environmentId]/actions";
import { createOrganizationAction } from "@/app/(app)/environments/[environmentId]/actions";
import FormbricksLogo from "@/images/logo.svg";
import Image from "next/image";
import { useRouter } from "next/navigation";
@@ -15,28 +15,28 @@ type FormValues = {
name: string;
};
export const CreateFirstTeam = () => {
export const CreateFirstOrganization = () => {
const router = useRouter();
const { register, handleSubmit } = useForm<FormValues>();
const [loading, setLoading] = useState(false);
const [teamName, setTeamName] = useState("");
const isTeamNameValid = teamName.trim() !== "";
const [organizationName, setOrganizationName] = useState("");
const isOrganizationNameValid = organizationName.trim() !== "";
const onCreateTeam = async (data: FormValues) => {
const onCreateOrganization = async (data: FormValues) => {
data.name = data.name.trim();
if (!data.name) return;
try {
setLoading(true);
const newTeam = await createTeamAction(data.name);
const newOrganization = await createOrganizationAction(data.name);
toast.success("Team created successfully!");
router.push(`/teams/${newTeam.id}`);
toast.success("Organization created successfully!");
router.push(`/organizations/${newOrganization.id}`);
} catch (error) {
console.error(error);
toast.error(`Unable to create team`);
toast.error(`Unable to create organization`);
} finally {
setLoading(false);
}
@@ -49,22 +49,22 @@ export const CreateFirstTeam = () => {
<p className="text ml-4 text-2xl font-bold">Formbricks</p>
</div>
<div className="flex h-[calc(100%-12rem)] items-center justify-center border-red-800">
<form onSubmit={handleSubmit(onCreateTeam)}>
<form onSubmit={handleSubmit(onCreateOrganization)}>
<div className="mb-2 flex w-full justify-between space-y-4 rounded-lg px-6">
<div className="grid w-full gap-3">
<h1 className="text text-3xl font-extrabold text-slate-800">
Let&apos;s create a team <span className="text-primary-500">👇</span>
Let&apos;s create an organization <span className="text-primary-500">👇</span>
</h1>
<p className="text text-md text-slate-700">
We couldn&apos;t find a team for you. Please create one
We couldn&apos;t find an organization for you. Please create one
</p>
<div>
<Input
autoFocus
placeholder="e.g. Power Puff Girls"
{...register("name", { required: true })}
value={teamName}
onChange={(e) => setTeamName(e.target.value)}
value={organizationName}
onChange={(e) => setOrganizationName(e.target.value)}
/>
</div>
</div>
@@ -75,8 +75,8 @@ export const CreateFirstTeam = () => {
variant="darkCTA"
type="submit"
loading={loading}
disabled={!isTeamNameValid}>
Create team
disabled={!isOrganizationNameValid}>
Create organization
</Button>
</div>
</form>

View File

@@ -1,4 +1,4 @@
import { CreateFirstTeam } from "@/app/(app)/create-first-team/components/CreateFirstTeam";
import { CreateFirstOrganization } from "@/app/(app)/create-first-organization/components/CreateFirstOrganization";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
@@ -11,7 +11,7 @@ const Page = async () => {
redirect("/auth/login");
}
return <CreateFirstTeam />;
return <CreateFirstOrganization />;
};
export default Page;

View File

@@ -45,7 +45,7 @@ export const AttributeSettingsTab = async ({ attributeClass, setOpen }: Attribut
<Label className="text-slate-600">Name</Label>
<Input
type="text"
placeholder="e.g. Product Team Info"
placeholder="e.g. Product Organization Info"
{...register("name", {
disabled: attributeClass.type === "automatic" || attributeClass.type === "code" ? true : false,
})}

View File

@@ -3,7 +3,7 @@ import { ActivityTimeline } from "@/app/(app)/environments/[environmentId]/(peop
import { getActionsByPersonId } from "@formbricks/lib/action/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
export const ActivitySection = async ({
environmentId,
@@ -12,15 +12,15 @@ export const ActivitySection = async ({
environmentId: string;
personId: string;
}) => {
const team = await getTeamByEnvironmentId(environmentId);
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
// On Formbricks Cloud only render the timeline if the user targeting feature is booked
const isUserTargetingEnabled = IS_FORMBRICKS_CLOUD
? team.billing.features.userTargeting.status === "active"
? organization.billing.features.userTargeting.status === "active"
: true;
const [environment, actions] = await Promise.all([

View File

@@ -7,26 +7,27 @@ import { getServerSession } from "next-auth";
import { getAttributes } from "@formbricks/lib/attribute/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
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 { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
const Page = async ({ params }) => {
const [environment, environmentTags, product, session, team, person, attributes] = await Promise.all([
getEnvironment(params.environmentId),
getTagsByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getServerSession(authOptions),
getTeamByEnvironmentId(params.environmentId),
getPerson(params.personId),
getAttributes(params.personId),
]);
const [environment, environmentTags, product, session, organization, person, attributes] =
await Promise.all([
getEnvironment(params.environmentId),
getTagsByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getServerSession(authOptions),
getOrganizationByEnvironmentId(params.environmentId),
getPerson(params.personId),
getAttributes(params.personId),
]);
if (!product) {
throw new Error("Product not found");
@@ -40,15 +41,15 @@ const Page = async ({ params }) => {
throw new Error("Session not found");
}
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
if (!person) {
throw new Error("Person not found");
}
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isViewer } = getAccessFlags(currentUserMembership?.role);
const getDeletePersonButton = () => {

View File

@@ -9,29 +9,29 @@ import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getSegments } from "@formbricks/lib/segment/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
const Page = async ({ params }) => {
const [environment, segments, attributeClasses, actionClassesFromServer, team] = await Promise.all([
const [environment, segments, attributeClasses, actionClassesFromServer, organization] = await Promise.all([
getEnvironment(params.environmentId),
getSegments(params.environmentId),
getAttributeClasses(params.environmentId),
getActionClasses(params.environmentId),
getTeamByEnvironmentId(params.environmentId),
getOrganizationByEnvironmentId(params.environmentId),
]);
if (!environment) {
throw new Error("Environment not found");
}
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const isAdvancedTargetingAllowed = await getAdvancedTargetingPermission(team);
const isAdvancedTargetingAllowed = await getAdvancedTargetingPermission(organization);
if (!segments) {
throw new Error("Failed to fetch segments");

View File

@@ -1,15 +1,15 @@
"use server";
import { Team } from "@prisma/client";
import { Organization } from "@prisma/client";
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { SHORT_URL_BASE, WEBAPP_URL } from "@formbricks/lib/constants";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { createMembership } from "@formbricks/lib/membership/service";
import { createOrganization, getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { createProduct } from "@formbricks/lib/product/service";
import { createShortUrl } from "@formbricks/lib/shortUrl/service";
import { createTeam, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { updateUser } from "@formbricks/lib/user/service";
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
@@ -27,20 +27,20 @@ export const createShortUrlAction = async (url: string) => {
return fullShortUrl;
};
export const createTeamAction = async (teamName: string): Promise<Team> => {
export const createOrganizationAction = async (organizationName: string): Promise<Organization> => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const newTeam = await createTeam({
name: teamName,
const newOrganization = await createOrganization({
name: organizationName,
});
await createMembership(newTeam.id, session.user.id, {
await createMembership(newOrganization.id, session.user.id, {
role: "owner",
accepted: true,
});
const product = await createProduct(newTeam.id, {
const product = await createProduct(newOrganization.id, {
name: "My Product",
});
@@ -59,7 +59,7 @@ export const createTeamAction = async (teamName: string): Promise<Team> => {
notificationSettings: updatedNotificationSettings,
});
return newTeam;
return newOrganization;
};
export const createProductAction = async (environmentId: string, productName: string) => {
@@ -69,10 +69,10 @@ export const createProductAction = async (environmentId: string, productName: st
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
const team = await getTeamByEnvironmentId(environmentId);
if (!team) throw new ResourceNotFoundError("Team from environment", environmentId);
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!organization) throw new ResourceNotFoundError("Organization from environment", environmentId);
const product = await createProduct(team.id, {
const product = await createProduct(organization.id, {
name: productName,
});
const updatedNotificationSettings = {

View File

@@ -11,8 +11,8 @@ import { canUserUpdateActionClass, verifyUserRoleAccess } from "@formbricks/lib/
import { createActionClass, deleteActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getSurveysByActionClassId } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { TActionClassInput } from "@formbricks/types/actionClasses";
import { AuthorizationError } from "@formbricks/types/errors";
@@ -20,10 +20,10 @@ export const deleteActionClassAction = async (environmentId, actionClassId: stri
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const team = await getTeamByEnvironmentId(environmentId);
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
@@ -43,10 +43,10 @@ export const updateActionClassAction = async (
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const team = await getTeamByEnvironmentId(environmentId);
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
@@ -72,10 +72,10 @@ export const getActionCountInLastHourAction = async (actionClassId: string, envi
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const team = await getTeamByEnvironmentId(environmentId);
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
@@ -88,10 +88,10 @@ export const getActionCountInLast24HoursAction = async (actionClassId: string, e
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const team = await getTeamByEnvironmentId(environmentId);
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
@@ -104,10 +104,10 @@ export const getActionCountInLast7DaysAction = async (actionClassId: string, env
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const team = await getTeamByEnvironmentId(environmentId);
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
@@ -123,10 +123,10 @@ export const getActiveInactiveSurveysAction = async (
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const team = await getTeamByEnvironmentId(environmentId);
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);

View File

@@ -6,7 +6,7 @@ import { Metadata } from "next";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
@@ -15,18 +15,18 @@ export const metadata: Metadata = {
};
const Page = async ({ params }) => {
const [actionClasses, team] = await Promise.all([
const [actionClasses, organization] = await Promise.all([
getActionClasses(params.environmentId),
getTeamByEnvironmentId(params.environmentId),
getOrganizationByEnvironmentId(params.environmentId),
]);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
// On Formbricks Cloud only render the timeline if the user targeting feature is booked
const isUserTargetingEnabled = IS_FORMBRICKS_CLOUD
? team.billing.features.userTargeting.status === "active"
? organization.billing.features.userTargeting.status === "active"
: true;
const renderAddActionButton = () => (

View File

@@ -55,7 +55,7 @@ export const AddProductModal = ({ environmentId, open, setOpen }: AddProductModa
</div>
<div>
<div className="text-xl font-medium text-slate-700">Add Product</div>
<div className="text-sm text-slate-500">Create a new product for your team.</div>
<div className="text-sm text-slate-500">Create a new product for your organization.</div>
</div>
</div>
</div>

View File

@@ -4,9 +4,12 @@ import type { Session } from "next-auth";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import {
getOrganizationByEnvironmentId,
getOrganizationsByUserId,
} from "@formbricks/lib/organization/service";
import { getProducts } from "@formbricks/lib/product/service";
import { getTeamByEnvironmentId, getTeamsByUserId } from "@formbricks/lib/team/service";
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
@@ -17,25 +20,25 @@ interface EnvironmentLayoutProps {
}
export const EnvironmentLayout = async ({ environmentId, session, children }: EnvironmentLayoutProps) => {
const [environment, teams, team] = await Promise.all([
const [environment, organizations, organization] = await Promise.all([
getEnvironment(environmentId),
getTeamsByUserId(session.user.id),
getTeamByEnvironmentId(environmentId),
getOrganizationsByUserId(session.user.id),
getOrganizationByEnvironmentId(environmentId),
]);
if (!team || !environment) {
if (!organization || !environment) {
return <ErrorComponent />;
}
const [products, environments] = await Promise.all([
getProducts(team.id),
getProducts(organization.id),
getEnvironments(environment.productId),
]);
if (!products || !environments || !teams) {
if (!products || !environments || !organizations) {
return <ErrorComponent />;
}
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
return (
<div className="flex h-screen min-h-screen flex-col overflow-hidden">
@@ -43,8 +46,8 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
<div className="flex h-full">
<MainNavigation
environment={environment}
team={team}
teams={teams}
organization={organization}
organizations={organizations}
products={products}
session={session}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}

View File

@@ -32,11 +32,11 @@ import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { capitalizeFirstLetter, truncate } from "@formbricks/lib/strings";
import { TEnvironment } from "@formbricks/types/environment";
import { TMembershipRole } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";
import { TProduct } from "@formbricks/types/product";
import { TTeam } from "@formbricks/types/teams";
import { ProfileAvatar } from "@formbricks/ui/Avatars";
import { Button } from "@formbricks/ui/Button";
import { CreateTeamModal } from "@formbricks/ui/CreateTeamModal";
import { CreateOrganizationModal } from "@formbricks/ui/CreateOrganizationModal";
import {
DropdownMenu,
DropdownMenuContent,
@@ -55,9 +55,9 @@ import { AddProductModal } from "./AddProductModal";
interface NavigationProps {
environment: TEnvironment;
teams: TTeam[];
organizations: TOrganization[];
session: Session;
team: TTeam;
organization: TOrganization;
products: TProduct[];
isFormbricksCloud: boolean;
membershipRole?: TMembershipRole;
@@ -65,8 +65,8 @@ interface NavigationProps {
export const MainNavigation = ({
environment,
teams,
team,
organizations,
organization,
session,
products,
isFormbricksCloud,
@@ -75,10 +75,10 @@ export const MainNavigation = ({
const router = useRouter();
const pathname = usePathname();
const [currentTeamName, setCurrentTeamName] = useState("");
const [currentTeamId, setCurrentTeamId] = useState("");
const [currentOrganizationName, setCurrentOrganizationName] = useState("");
const [currentOrganizationId, setCurrentOrganizationId] = useState("");
const [showAddProductModal, setShowAddProductModal] = useState(false);
const [showCreateTeamModal, setShowCreateTeamModal] = useState(false);
const [showCreateOrganizationModal, setShowCreateOrganizationModal] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(true);
const [isTextVisible, setIsTextVisible] = useState(true);
@@ -105,15 +105,15 @@ export const MainNavigation = ({
}, [isCollapsed]);
useEffect(() => {
if (team && team.name !== "") {
setCurrentTeamName(team.name);
setCurrentTeamId(team.id);
if (organization && organization.name !== "") {
setCurrentOrganizationName(organization.name);
setCurrentOrganizationId(organization.id);
}
}, [team]);
}, [organization]);
const sortedTeams = useMemo(() => {
return [...teams].sort((a, b) => a.name.localeCompare(b.name));
}, [teams]);
const sortedOrganizations = useMemo(() => {
return [...organizations].sort((a, b) => a.name.localeCompare(b.name));
}, [organizations]);
const sortedProducts = useMemo(() => {
return [...products].sort((a, b) => a.name.localeCompare(b.name));
@@ -123,8 +123,8 @@ export const MainNavigation = ({
router.push(`/products/${productId}/`);
};
const handleEnvironmentChangeByTeam = (teamId: string) => {
router.push(`/teams/${teamId}/`);
const handleEnvironmentChangeByOrganization = (organizationId: string) => {
router.push(`/organizations/${organizationId}/`);
};
const mainNavigation = useMemo(
@@ -177,7 +177,7 @@ export const MainNavigation = ({
icon: UserCircleIcon,
},
{
label: "Team",
label: "Organization",
href: `/environments/${environment.id}/settings/members`,
icon: UsersIcon,
},
@@ -363,7 +363,9 @@ export const MainNavigation = ({
<span>{truncate(session?.user?.email, 30)}</span>
)}
</p>
<p className={cn("text-sm text-slate-500")}>{capitalizeFirstLetter(team?.name)}</p>
<p className={cn("text-sm text-slate-500")}>
{capitalizeFirstLetter(organization?.name)}
</p>
</div>
<ChevronRightIcon className={cn("h-5 w-5 text-slate-700 hover:text-slate-500")} />
</>
@@ -410,13 +412,13 @@ export const MainNavigation = ({
Logout
</DropdownMenuItem>
{/* Team Switch */}
{/* Organization Switch */}
<DropdownMenuSub>
<DropdownMenuSubTrigger className="rounded-lg">
<div>
<p>{currentTeamName}</p>
<p className="block text-xs text-slate-500">Switch team</p>
<p>{currentOrganizationName}</p>
<p className="block text-xs text-slate-500">Switch organization</p>
</div>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
@@ -425,21 +427,25 @@ export const MainNavigation = ({
sideOffset={10}
alignOffset={5}>
<DropdownMenuRadioGroup
value={currentTeamId}
onValueChange={(teamId) => handleEnvironmentChangeByTeam(teamId)}>
{sortedTeams.map((team) => (
value={currentOrganizationId}
onValueChange={(organizationId) =>
handleEnvironmentChangeByOrganization(organizationId)
}>
{sortedOrganizations.map((organization) => (
<DropdownMenuRadioItem
value={team.id}
value={organization.id}
className="cursor-pointer rounded-lg"
key={team.id}>
{team.name}
key={organization.id}>
{organization.name}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setShowCreateTeamModal(true)} className="rounded-lg">
<DropdownMenuItem
onClick={() => setShowCreateOrganizationModal(true)}
className="rounded-lg">
<PlusIcon className="mr-2 h-4 w-4" />
<span>Create new team</span>
<span>Create new organization</span>
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
@@ -450,7 +456,10 @@ export const MainNavigation = ({
</div>
</aside>
)}
<CreateTeamModal open={showCreateTeamModal} setOpen={(val) => setShowCreateTeamModal(val)} />
<CreateOrganizationModal
open={showCreateOrganizationModal}
setOpen={(val) => setShowCreateOrganizationModal(val)}
/>
<AddProductModal
open={showAddProductModal}
setOpen={(val) => setShowAddProductModal(val)}

View File

@@ -5,15 +5,15 @@ import { usePostHog } from "posthog-js/react";
import { useEffect } from "react";
import { env } from "@formbricks/lib/env";
import { TSubscriptionStatus } from "@formbricks/types/teams";
import { TSubscriptionStatus } from "@formbricks/types/organizations";
const posthogEnabled = env.NEXT_PUBLIC_POSTHOG_API_KEY && env.NEXT_PUBLIC_POSTHOG_API_HOST;
interface PosthogIdentifyProps {
session: Session;
environmentId?: string;
teamId?: string;
teamName?: string;
organizationId?: string;
organizationName?: string;
inAppSurveyBillingStatus?: TSubscriptionStatus;
linkSurveyBillingStatus?: TSubscriptionStatus;
userTargetingBillingStatus?: TSubscriptionStatus;
@@ -22,8 +22,8 @@ interface PosthogIdentifyProps {
export const PosthogIdentify = ({
session,
environmentId,
teamId,
teamName,
organizationId,
organizationName,
inAppSurveyBillingStatus,
linkSurveyBillingStatus,
userTargetingBillingStatus,
@@ -41,9 +41,9 @@ export const PosthogIdentify = ({
if (environmentId) {
posthog.group("environment", environmentId, { name: environmentId });
}
if (teamId) {
posthog.group("team", teamId, {
name: teamName,
if (organizationId) {
posthog.group("organization", organizationId, {
name: organizationName,
inAppSurveyBillingStatus,
linkSurveyBillingStatus,
userTargetingBillingStatus,
@@ -54,8 +54,8 @@ export const PosthogIdentify = ({
posthog,
session.user,
environmentId,
teamId,
teamName,
organizationId,
organizationName,
inAppSurveyBillingStatus,
linkSurveyBillingStatus,
userTargetingBillingStatus,

View File

@@ -13,9 +13,9 @@ import Image from "next/image";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getIntegrations } from "@formbricks/lib/integration/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getWebhookCountBySource } from "@formbricks/lib/webhook/service";
import { TIntegrationType } from "@formbricks/types/integration";
import { Card } from "@formbricks/ui/Card";
@@ -29,7 +29,7 @@ const Page = async ({ params }) => {
const [
environment,
integrations,
team,
organization,
session,
userWebhookCount,
zapierWebhookCount,
@@ -38,7 +38,7 @@ const Page = async ({ params }) => {
] = await Promise.all([
getEnvironment(environmentId),
getIntegrations(environmentId),
getTeamByEnvironmentId(params.environmentId),
getOrganizationByEnvironmentId(params.environmentId),
getServerSession(authOptions),
getWebhookCountBySource(environmentId, "user"),
getWebhookCountBySource(environmentId, "zapier"),
@@ -52,11 +52,11 @@ const Page = async ({ params }) => {
throw new Error("Session not found");
}
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isViewer } = getAccessFlags(currentUserMembership?.role);
const isGoogleSheetsIntegrationConnected = isIntegrationConnected("googleSheets");

View File

@@ -5,7 +5,7 @@ import { redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { AuthorizationError } from "@formbricks/types/errors";
import { ToasterClient } from "@formbricks/ui/ToasterClient";
@@ -22,9 +22,9 @@ const EnvLayout = async ({ children, params }) => {
throw new AuthorizationError("Not authorized");
}
const team = await getTeamByEnvironmentId(params.environmentId);
if (!team) {
throw new Error("Team not found");
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!organization) {
throw new Error("Organization not found");
}
return (
@@ -33,11 +33,11 @@ const EnvLayout = async ({ children, params }) => {
<PosthogIdentify
session={session}
environmentId={params.environmentId}
teamId={team.id}
teamName={team.name}
inAppSurveyBillingStatus={team.billing.features.inAppSurvey.status}
linkSurveyBillingStatus={team.billing.features.linkSurvey.status}
userTargetingBillingStatus={team.billing.features.userTargeting.status}
organizationId={organization.id}
organizationName={organization.name}
inAppSurveyBillingStatus={organization.billing.features.inAppSurvey.status}
linkSurveyBillingStatus={organization.billing.features.linkSurvey.status}
userTargetingBillingStatus={organization.billing.features.userTargeting.status}
/>
<FormbricksClient session={session} />
<ToasterClient />

View File

@@ -4,9 +4,9 @@ import { getServerSession } from "next-auth";
import { getMultiLanguagePermission } from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { EnvironmentNotice } from "@formbricks/ui/EnvironmentNotice";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
@@ -17,22 +17,22 @@ import { ApiKeyList } from "./components/ApiKeyList";
const Page = async ({ params }) => {
const environment = await getEnvironment(params.environmentId);
const team = await getTeamByEnvironmentId(params.environmentId);
const organization = await getOrganizationByEnvironmentId(params.environmentId);
const session = await getServerSession(authOptions);
if (!environment) {
throw new Error("Environment not found");
}
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
if (!session) {
throw new Error("Unauthenticated");
}
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isViewer } = getAccessFlags(currentUserMembership?.role);
const isMultiLanguageAllowed = await getMultiLanguagePermission(team);
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
return !isViewer ? (
<PageContentWrapper>

View File

@@ -5,9 +5,9 @@ import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { deleteProduct, getProducts, updateProduct } from "@formbricks/lib/product/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { TEnvironment } from "@formbricks/types/environment";
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TProduct, TProductUpdateInput } from "@formbricks/types/product";
@@ -40,8 +40,10 @@ export const updateProductAction = async (
throw new AuthorizationError("Not authorized");
}
const team = await getTeamByEnvironmentId(environmentId);
const membership = team ? await getMembershipByUserIdTeamId(session.user.id, team.id) : null;
const organization = await getOrganizationByEnvironmentId(environmentId);
const membership = organization
? await getMembershipByUserIdOrganizationId(session.user.id, organization.id)
: null;
if (!membership) {
throw new AuthorizationError("Not authorized");
@@ -52,7 +54,7 @@ export const updateProductAction = async (
}
if (membership.role === "developer") {
if (!!data.name || !!data.brandColor || !!data.teamId || !!data.environments) {
if (!!data.name || !!data.brandColor || !!data.organizationId || !!data.environments) {
throw new AuthorizationError("Not authorized");
}
}
@@ -85,14 +87,14 @@ export const deleteProductAction = async (environmentId: string, userId: string,
throw new AuthorizationError("Not authorized");
}
const team = await getTeamByEnvironmentId(environmentId);
const membership = team ? await getMembershipByUserIdTeamId(userId, team.id) : null;
const organization = await getOrganizationByEnvironmentId(environmentId);
const membership = organization ? await getMembershipByUserIdOrganizationId(userId, organization.id) : null;
if (membership?.role !== "admin" && membership?.role !== "owner") {
throw new AuthorizationError("You are not allowed to delete products.");
}
const availableProducts = team ? await getProducts(team.id) : null;
const availableProducts = organization ? await getProducts(organization.id) : null;
if (!!availableProducts && availableProducts?.length <= 1) {
throw new Error("You can't delete the last product in the environment.");

View File

@@ -2,9 +2,9 @@ import { DeleteProductRender } from "@/app/(app)/environments/[environmentId]/pr
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProducts } from "@formbricks/lib/product/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { TProduct } from "@formbricks/types/product";
type DeleteProductProps = {
@@ -17,13 +17,13 @@ export const DeleteProduct = async ({ environmentId, product }: DeleteProductPro
if (!session) {
throw new Error("Session not found");
}
const team = await getTeamByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team not found");
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!organization) {
throw new Error("Organization not found");
}
const availableProducts = team ? await getProducts(team.id) : null;
const availableProducts = organization ? await getProducts(organization.id) : null;
const membership = await getMembershipByUserIdTeamId(session.user.id, team.id);
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
if (!membership) {
throw new Error("Membership not found");
}

View File

@@ -3,11 +3,10 @@ import { getServerSession } from "next-auth";
import { getMultiLanguagePermission } from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/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 { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
@@ -19,11 +18,10 @@ import { EditProductNameForm } from "./components/EditProductNameForm";
import { EditWaitingTimeForm } from "./components/EditWaitingTimeForm";
const Page = async ({ params }: { params: { environmentId: string } }) => {
const [, product, session, team] = await Promise.all([
getEnvironment(params.environmentId),
const [product, session, organization] = await Promise.all([
getProductByEnvironmentId(params.environmentId),
getServerSession(authOptions),
getTeamByEnvironmentId(params.environmentId),
getOrganizationByEnvironmentId(params.environmentId),
]);
if (!product) {
@@ -32,11 +30,11 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
if (!session) {
throw new Error("Unauthorized");
}
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isDeveloper, isViewer } = getAccessFlags(currentUserMembership?.role);
const isProductNameEditDisabled = isDeveloper ? true : isViewer;
@@ -44,7 +42,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
return <ErrorComponent />;
}
const isMultiLanguageAllowed = await getMultiLanguagePermission(team);
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
return (
<PageContentWrapper>

View File

@@ -4,8 +4,8 @@ import { notFound } from "next/navigation";
import { getMultiLanguagePermission } from "@formbricks/ee/lib/service";
import { EditLanguage } from "@formbricks/ee/multiLanguage/components/EditLanguage";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getTeam } from "@formbricks/lib/team/service";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
@@ -16,13 +16,13 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
throw new Error("Product not found");
}
const team = await getTeam(product?.teamId);
const organization = await getOrganization(product?.organizationId);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const isMultiLanguageAllowed = await getMultiLanguagePermission(team);
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
if (!isMultiLanguageAllowed) {
notFound();

View File

@@ -2,22 +2,22 @@ import { Metadata } from "next";
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
export const metadata: Metadata = {
title: "Config",
};
const ConfigLayout = async ({ children, params }) => {
const [team, product, session] = await Promise.all([
getTeamByEnvironmentId(params.environmentId),
const [organization, product, session] = await Promise.all([
getOrganizationByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getServerSession(authOptions),
]);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
if (!product) {

View File

@@ -17,7 +17,7 @@ export const updateProductAction = async (productId: string, inputProduct: TProd
const product = await getProduct(productId);
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(product!.teamId, session.user.id);
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(product!.organizationId, session.user.id);
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
return await updateProduct(productId, inputProduct);

View File

@@ -9,10 +9,10 @@ import {
} from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/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 { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
@@ -23,9 +23,9 @@ import { EditPlacementForm } from "./components/EditPlacementForm";
import { ThemeStyling } from "./components/ThemeStyling";
const Page = async ({ params }: { params: { environmentId: string } }) => {
const [session, team, product] = await Promise.all([
const [session, organization, product] = await Promise.all([
getServerSession(authOptions),
getTeamByEnvironmentId(params.environmentId),
getOrganizationByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
]);
@@ -35,21 +35,21 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
if (!session) {
throw new Error("Unauthorized");
}
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const canRemoveInAppBranding = getRemoveInAppBrandingPermission(team);
const canRemoveLinkBranding = getRemoveLinkBrandingPermission(team);
const canRemoveInAppBranding = getRemoveInAppBrandingPermission(organization);
const canRemoveLinkBranding = getRemoveLinkBrandingPermission(organization);
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isViewer } = getAccessFlags(currentUserMembership?.role);
if (isViewer) {
return <ErrorComponent />;
}
const isMultiLanguageAllowed = await getMultiLanguagePermission(team);
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
return (
<PageContentWrapper>

View File

@@ -4,7 +4,7 @@ import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId
import { getMultiLanguagePermission } from "@formbricks/ee/lib/service";
import { IS_FORMBRICKS_CLOUD, WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { EnvironmentNotice } from "@formbricks/ui/EnvironmentNotice";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
@@ -14,20 +14,20 @@ import { EnvironmentIdField } from "./components/EnvironmentIdField";
import { SetupInstructions } from "./components/SetupInstructions";
const Page = async ({ params }) => {
const [environment, team] = await Promise.all([
const [environment, organization] = await Promise.all([
getEnvironment(params.environmentId),
getTeamByEnvironmentId(params.environmentId),
getOrganizationByEnvironmentId(params.environmentId),
]);
if (!environment) {
throw new Error("Environment not found");
}
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const isMultiLanguageAllowed = await getMultiLanguagePermission(team);
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
return (
<PageContentWrapper>

View File

@@ -5,11 +5,11 @@ import { getServerSession } from "next-auth";
import { getMultiLanguagePermission } from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
import { getTagsOnResponsesCount } from "@formbricks/lib/tagOnResponse/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
@@ -23,25 +23,25 @@ const Page = async ({ params }) => {
}
const tags = await getTagsByEnvironmentId(params.environmentId);
const environmentTagsCount = await getTagsOnResponsesCount(params.environmentId);
const team = await getTeamByEnvironmentId(params.environmentId);
const organization = await getOrganizationByEnvironmentId(params.environmentId);
const session = await getServerSession(authOptions);
if (!environment) {
throw new Error("Environment not found");
}
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
if (!session) {
throw new Error("Unauthenticated");
}
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isViewer } = getAccessFlags(currentUserMembership?.role);
const isTagSettingDisabled = isViewer;
const isMultiLanguageAllowed = await getMultiLanguagePermission(team);
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
return !isTagSettingDisabled ? (
<PageContentWrapper>

View File

@@ -1,18 +1,18 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
const AccountSettingsLayout = async ({ children, params }) => {
const [team, product, session] = await Promise.all([
getTeamByEnvironmentId(params.environmentId),
const [organization, product, session] = await Promise.all([
getOrganizationByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getServerSession(authOptions),
]);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
if (!product) {

View File

@@ -30,15 +30,15 @@ export const EditAlerts = ({
<div className="col-span-3 flex items-center space-x-3">
<UsersIcon className="h-6 w-7 text-slate-600" />
<p className="text-sm font-medium text-slate-800">{membership.team.name}</p>
<p className="text-sm font-medium text-slate-800">{membership.organization.name}</p>
</div>
<div className="col-span-3 flex items-center justify-end pr-2">
<p className="pr-4 text-sm text-slate-600">Auto-subscribe to new surveys</p>
<NotificationSwitch
surveyOrProductOrTeamId={membership.team.id}
surveyOrProductOrOrganizationId={membership.organization.id}
notificationSettings={user.notificationSettings!}
notificationType={"unsubscribedTeamIds"}
notificationType={"unsubscribedOrganizationIds"}
autoDisableNotificationType={autoDisableNotificationType}
autoDisableNotificationElementId={autoDisableNotificationElementId}
/>
@@ -60,11 +60,11 @@ export const EditAlerts = ({
</TooltipProvider>
</div>
{membership.team.products.some((product) =>
{membership.organization.products.some((product) =>
product.environments.some((environment) => environment.surveys.length > 0)
) ? (
<div className="grid-cols-8 space-y-1 p-2">
{membership.team.products.map((product) => (
{membership.organization.products.map((product) => (
<div key={product.id}>
{product.environments.map((environment) => (
<div key={environment.id}>
@@ -78,7 +78,7 @@ export const EditAlerts = ({
</div>
<div className="col-span-1 text-center">
<NotificationSwitch
surveyOrProductOrTeamId={survey.id}
surveyOrProductOrOrganizationId={survey.id}
notificationSettings={user.notificationSettings!}
notificationType={"alert"}
autoDisableNotificationType={autoDisableNotificationType}
@@ -98,7 +98,7 @@ export const EditAlerts = ({
</div>
)}
<p className="pb-3 pl-4 text-xs text-slate-400">
Want to loop in team mates?{" "}
Want to loop in organization mates?{" "}
<Link className="font-semibold" href={`/environments/${environmentId}/settings/members`}>
Invite them.
</Link>

View File

@@ -20,7 +20,7 @@ export const EditWeeklySummary = ({ memberships, user, environmentId }: EditAler
<div className="mb-5 flex items-center space-x-3 text-sm font-medium">
<UsersIcon className="h-6 w-7 text-slate-600" />
<p className="text-slate-800">{membership.team.name}</p>
<p className="text-slate-800">{membership.organization.name}</p>
</div>
<div className="mb-6 rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-3 content-center rounded-t-lg bg-slate-100 px-4 text-left text-sm font-semibold text-slate-900">
@@ -28,14 +28,14 @@ export const EditWeeklySummary = ({ memberships, user, environmentId }: EditAler
<div className="col-span-1 text-center">Weekly Summary</div>
</div>
<div className="space-y-1 p-2">
{membership.team.products.map((product) => (
{membership.organization.products.map((product) => (
<div
className="grid h-auto w-full cursor-pointer grid-cols-3 place-content-center justify-center rounded-lg px-2 py-2 text-left text-sm text-slate-900 hover:bg-slate-50"
key={product.id}>
<div className="col-span-2">{product?.name}</div>
<div className="col-span-1 flex items-center justify-center">
<NotificationSwitch
surveyOrProductOrTeamId={product.id}
surveyOrProductOrOrganizationId={product.id}
notificationSettings={user.notificationSettings!}
notificationType={"weeklySummary"}
/>
@@ -44,7 +44,7 @@ export const EditWeeklySummary = ({ memberships, user, environmentId }: EditAler
))}
</div>
<p className="pb-3 pl-4 text-xs text-slate-400">
Want to loop in team mates?{" "}
Want to loop in organization mates?{" "}
<Link className="font-semibold" href={`/environments/${environmentId}/settings/members`}>
Invite them.
</Link>

View File

@@ -9,15 +9,15 @@ import { Switch } from "@formbricks/ui/Switch";
import { updateNotificationSettingsAction } from "../actions";
interface NotificationSwitchProps {
surveyOrProductOrTeamId: string;
surveyOrProductOrOrganizationId: string;
notificationSettings: TUserNotificationSettings;
notificationType: "alert" | "weeklySummary" | "unsubscribedTeamIds";
notificationType: "alert" | "weeklySummary" | "unsubscribedOrganizationIds";
autoDisableNotificationType?: string;
autoDisableNotificationElementId?: string;
}
export const NotificationSwitch = ({
surveyOrProductOrTeamId,
surveyOrProductOrOrganizationId,
notificationSettings,
notificationType,
autoDisableNotificationType,
@@ -26,26 +26,29 @@ export const NotificationSwitch = ({
const [isLoading, setIsLoading] = useState(false);
const isChecked =
notificationType === "unsubscribedTeamIds"
? !notificationSettings.unsubscribedTeamIds?.includes(surveyOrProductOrTeamId)
: notificationSettings[notificationType][surveyOrProductOrTeamId] === true;
notificationType === "unsubscribedOrganizationIds"
? !notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrProductOrOrganizationId)
: notificationSettings[notificationType][surveyOrProductOrOrganizationId] === true;
const handleSwitchChange = async () => {
setIsLoading(true);
let updatedNotificationSettings = { ...notificationSettings };
if (notificationType === "unsubscribedTeamIds") {
const unsubscribedTeamIds = updatedNotificationSettings.unsubscribedTeamIds ?? [];
if (unsubscribedTeamIds.includes(surveyOrProductOrTeamId)) {
updatedNotificationSettings.unsubscribedTeamIds = unsubscribedTeamIds.filter(
(id) => id !== surveyOrProductOrTeamId
if (notificationType === "unsubscribedOrganizationIds") {
const unsubscribedOrganizationIds = updatedNotificationSettings.unsubscribedOrganizationIds ?? [];
if (unsubscribedOrganizationIds.includes(surveyOrProductOrOrganizationId)) {
updatedNotificationSettings.unsubscribedOrganizationIds = unsubscribedOrganizationIds.filter(
(id) => id !== surveyOrProductOrOrganizationId
);
} else {
updatedNotificationSettings.unsubscribedTeamIds = [...unsubscribedTeamIds, surveyOrProductOrTeamId];
updatedNotificationSettings.unsubscribedOrganizationIds = [
...unsubscribedOrganizationIds,
surveyOrProductOrOrganizationId,
];
}
} else {
updatedNotificationSettings[notificationType][surveyOrProductOrTeamId] =
!updatedNotificationSettings[notificationType][surveyOrProductOrTeamId];
updatedNotificationSettings[notificationType][surveyOrProductOrOrganizationId] =
!updatedNotificationSettings[notificationType][surveyOrProductOrOrganizationId];
}
await updateNotificationSettingsAction(updatedNotificationSettings);
@@ -55,12 +58,12 @@ export const NotificationSwitch = ({
useEffect(() => {
if (
autoDisableNotificationType &&
autoDisableNotificationElementId === surveyOrProductOrTeamId &&
autoDisableNotificationElementId === surveyOrProductOrOrganizationId &&
isChecked
) {
switch (notificationType) {
case "alert":
if (notificationSettings[notificationType][surveyOrProductOrTeamId] === true) {
if (notificationSettings[notificationType][surveyOrProductOrOrganizationId] === true) {
handleSwitchChange();
toast.success("You will not receive any more emails for responses on this survey!", {
id: "notification-switch",
@@ -68,10 +71,10 @@ export const NotificationSwitch = ({
}
break;
case "unsubscribedTeamIds":
if (!notificationSettings.unsubscribedTeamIds?.includes(surveyOrProductOrTeamId)) {
case "unsubscribedOrganizationIds":
if (!notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrProductOrOrganizationId)) {
handleSwitchChange();
toast.success("You will not be auto-subscribed to this team's surveys anymore!", {
toast.success("You will not be auto-subscribed to this organization's surveys anymore!", {
id: "notification-switch",
});
}

View File

@@ -21,10 +21,10 @@ const setCompleteNotificationSettings = (
const newNotificationSettings = {
alert: {},
weeklySummary: {},
unsubscribedTeamIds: notificationSettings.unsubscribedTeamIds || [],
unsubscribedOrganizationIds: notificationSettings.unsubscribedOrganizationIds || [],
};
for (const membership of memberships) {
for (const product of membership.team.products) {
for (const product of membership.organization.products) {
// set default values for weekly summary
newNotificationSettings.weeklySummary[product.id] =
(notificationSettings.weeklySummary && notificationSettings.weeklySummary[product.id]) || false;
@@ -48,7 +48,7 @@ const getMemberships = async (userId: string): Promise<Membership[]> => {
userId,
},
select: {
team: {
organization: {
select: {
id: true,
name: true,

View File

@@ -1,7 +1,7 @@
import { TUserNotificationSettings } from "@formbricks/types/user";
export interface Membership {
team: {
organization: {
id: string;
name: string;
products: {

View File

@@ -72,12 +72,12 @@ const DeleteAccountModal = ({ setOpen, open, session, IS_FORMBRICKS_CLOUD }: Del
<ul className="list-disc pb-6 pl-6">
<li>Permanent removal of all of your personal information and data.</li>
<li>
If you are the owner of a team with other admins, the ownership of that team will be transferred
to another admin.
If you are the owner of an organization with other admins, the ownership of that organization will
be transferred to another admin.
</li>
<li>
If you are the only member of a team or there is no other admin present, the team will be
irreversibly deleted along with all associated data.
If you are the only member of an organization or there is no other admin present, the organization
will be irreversibly deleted along with all associated data.
</li>
<li>This action cannot be undone. If it&apos;s gone, it&apos;s gone.</li>
</ul>

View File

@@ -33,7 +33,7 @@ const Loading = () => {
},
{
title: "Avatar",
description: "Assist your team in identifying you on Formbricks.",
description: "Assist your organization in identifying you on Formbricks.",
skeletonLines: [{ classes: "h-10 w-10" }, { classes: "h-8 w-24" }],
},
{

View File

@@ -32,7 +32,9 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
<SettingsCard title="Personal information" description="Update your personal information.">
<EditName user={user} />
</SettingsCard>
<SettingsCard title="Avatar" description="Assist your team in identifying you on Formbricks.">
<SettingsCard
title="Avatar"
description="Assist your organization in identifying you on Formbricks.">
<EditAvatar session={session} environmentId={environmentId} />
</SettingsCard>
{user.identityProvider === "email" && (

View File

@@ -8,56 +8,56 @@ import { createSubscription } from "@formbricks/ee/billing/lib/createSubscriptio
import { removeSubscription } from "@formbricks/ee/billing/lib/removeSubscription";
import { authOptions } from "@formbricks/lib/authOptions";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { canUserAccessTeam } from "@formbricks/lib/team/auth";
import { getTeam } from "@formbricks/lib/team/service";
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
import { getOrganization } from "@formbricks/lib/organization/service";
import { AuthorizationError } from "@formbricks/types/errors";
export const upgradePlanAction = async (
teamId: string,
organizationId: string,
environmentId: string,
priceLookupKeys: StripePriceLookupKeys[]
) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const isAuthorized = await canUserAccessTeam(session.user.id, teamId);
const isAuthorized = await canUserAccessOrganization(session.user.id, organizationId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
const subscriptionSession = await createSubscription(teamId, environmentId, priceLookupKeys);
const subscriptionSession = await createSubscription(organizationId, environmentId, priceLookupKeys);
return subscriptionSession;
};
export const manageSubscriptionAction = async (teamId: string, environmentId: string) => {
export const manageSubscriptionAction = async (organizationId: string, environmentId: string) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const isAuthorized = await canUserAccessTeam(session.user.id, teamId);
const isAuthorized = await canUserAccessOrganization(session.user.id, organizationId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
const team = await getTeam(teamId);
if (!team || !team.billing.stripeCustomerId)
const organization = await getOrganization(organizationId);
if (!organization || !organization.billing.stripeCustomerId)
throw new AuthorizationError("You do not have an associated Stripe CustomerId");
const sessionUrl = await createCustomerPortalSession(
team.billing.stripeCustomerId,
organization.billing.stripeCustomerId,
`${WEBAPP_URL}/environments/${environmentId}/settings/billing`
);
return sessionUrl;
};
export const removeSubscriptionAction = async (
teamId: string,
organizationId: string,
environmentId: string,
priceLookupKeys: StripePriceLookupKeys[]
) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const isAuthorized = await canUserAccessTeam(session.user.id, teamId);
const isAuthorized = await canUserAccessOrganization(session.user.id, organizationId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
const removedSubscription = await removeSubscription(teamId, environmentId, priceLookupKeys);
const removedSubscription = await removeSubscription(organizationId, environmentId, priceLookupKeys);
return removedSubscription.url;
};

View File

@@ -4,20 +4,20 @@ import {
manageSubscriptionAction,
removeSubscriptionAction,
upgradePlanAction,
} from "@/app/(app)/environments/[environmentId]/settings/(team)/billing/actions";
} from "@/app/(app)/environments/[environmentId]/settings/(organization)/billing/actions";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
import { ProductFeatureKeys, StripePriceLookupKeys } from "@formbricks/ee/billing/lib/constants";
import { TTeam } from "@formbricks/types/teams";
import { TOrganization } from "@formbricks/types/organizations";
import { AlertDialog } from "@formbricks/ui/AlertDialog";
import { Button } from "@formbricks/ui/Button";
import { LoadingSpinner } from "@formbricks/ui/LoadingSpinner";
import { PricingCard } from "@formbricks/ui/PricingCard";
interface PricingTableProps {
team: TTeam;
organization: TOrganization;
environmentId: string;
peopleCount: number;
responseCount: number;
@@ -26,7 +26,7 @@ interface PricingTableProps {
}
export const PricingTable = ({
team,
organization,
environmentId,
peopleCount,
responseCount,
@@ -41,7 +41,7 @@ export const PricingTable = ({
const openCustomerPortal = async () => {
setLoadingCustomerPortal(true);
const sessionUrl = await manageSubscriptionAction(team.id, environmentId);
const sessionUrl = await manageSubscriptionAction(organization.id, environmentId);
router.push(sessionUrl);
setLoadingCustomerPortal(false);
};
@@ -49,7 +49,11 @@ export const PricingTable = ({
const upgradePlan = async (priceLookupKeys: StripePriceLookupKeys[]) => {
try {
setUpgradingPlan(true);
const { status, newPlan, url } = await upgradePlanAction(team.id, environmentId, priceLookupKeys);
const { status, newPlan, url } = await upgradePlanAction(
organization.id,
environmentId,
priceLookupKeys
);
setUpgradingPlan(false);
if (status != 200) {
throw new Error("Something went wrong");
@@ -82,7 +86,7 @@ export const PricingTable = ({
const handleDeleteSubscription = async () => {
try {
if (!activeLookupKey) throw new Error("No active lookup key");
await removeSubscriptionAction(team.id, environmentId, [activeLookupKey]);
await removeSubscriptionAction(organization.id, environmentId, [activeLookupKey]);
router.refresh();
toast.success("Subscription deleted successfully");
} catch (err) {
@@ -98,7 +102,7 @@ export const PricingTable = ({
comingSoon: false,
},
{
title: "Team Roles",
title: "Organization Roles",
comingSoon: false,
},
{
@@ -174,7 +178,7 @@ export const PricingTable = ({
</div>
)}
<div className="justify-between gap-4 rounded-lg">
{team.billing.stripeCustomerId ? (
{organization.billing.stripeCustomerId ? (
<div className="flex w-full justify-end">
<Button
variant="minimal"
@@ -189,7 +193,9 @@ export const PricingTable = ({
className="justify-center py-2 shadow-sm"
loading={loadingCustomerPortal}
onClick={openCustomerPortal}>
{team.billing.features.inAppSurvey.unlimited ? "Manage Subscription" : "Manage Card details"}
{organization.billing.features.inAppSurvey.unlimited
? "Manage Subscription"
: "Manage Card details"}
</Button>
</div>
) : (
@@ -283,13 +289,13 @@ export const PricingTable = ({
featureName={ProductFeatureKeys[ProductFeatureKeys.inAppSurvey]}
monthlyPrice={0}
actionText={"Starting at"}
team={team}
organization={organization}
metric="responses"
sliderValue={responseCount}
sliderLimit={350}
freeTierLimit={appSurveyFreeResponses}
paidFeatures={coreAndWebAppSurveyFeatures.filter((feature) => {
if (team.billing.features.inAppSurvey.unlimited) {
if (organization.billing.features.inAppSurvey.unlimited) {
return feature.unlimited !== false;
} else {
return feature.unlimited !== true;
@@ -307,7 +313,7 @@ export const PricingTable = ({
featureName={ProductFeatureKeys[ProductFeatureKeys.linkSurvey]}
monthlyPrice={30}
actionText={""}
team={team}
organization={organization}
paidFeatures={linkSurveysFeatures}
loading={upgradingPlan}
onUpgrade={() => upgradePlan([StripePriceLookupKeys.linkSurvey])}
@@ -320,13 +326,13 @@ export const PricingTable = ({
featureName={ProductFeatureKeys[ProductFeatureKeys.userTargeting]}
monthlyPrice={0}
actionText={"Starting at"}
team={team}
organization={organization}
metric="people"
sliderValue={peopleCount}
sliderLimit={3500}
freeTierLimit={userTargetingFreeMtu}
paidFeatures={userTargetingFeatures.filter((feature) => {
if (team.billing.features.userTargeting.unlimited) {
if (organization.billing.features.userTargeting.unlimited) {
return feature.unlimited !== false;
} else {
return feature.unlimited !== true;

View File

@@ -4,9 +4,9 @@ import { notFound } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
export const metadata: Metadata = {
@@ -19,16 +19,16 @@ const BillingLayout = async ({ children, params }) => {
}
const session = await getServerSession(authOptions);
const team = await getTeamByEnvironmentId(params.environmentId);
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!session) {
throw new Error("Unauthorized");
}
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isAdmin, isOwner } = getAccessFlags(currentUserMembership?.role);
const isPricingDisabled = !isOwner && !isAdmin;

View File

@@ -1,4 +1,4 @@
import { TeamSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(team)/components/TeamSettingsNavbar";
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
@@ -7,21 +7,21 @@ import {
PRICING_APPSURVEYS_FREE_RESPONSES,
PRICING_USERTARGETING_FREE_MTU,
} from "@formbricks/lib/constants";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import {
getMonthlyActiveTeamPeopleCount,
getMonthlyTeamResponseCount,
getTeamByEnvironmentId,
} from "@formbricks/lib/team/service";
getMonthlyActiveOrganizationPeopleCount,
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
} from "@formbricks/lib/organization/service";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
import { PricingTable } from "./components/PricingTable";
const Page = async ({ params }) => {
const team = await getTeamByEnvironmentId(params.environmentId);
if (!team) {
throw new Error("Team not found");
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!organization) {
throw new Error("Organization not found");
}
const session = await getServerSession(authOptions);
@@ -30,16 +30,16 @@ const Page = async ({ params }) => {
}
const [peopleCount, responseCount] = await Promise.all([
getMonthlyActiveTeamPeopleCount(team.id),
getMonthlyTeamResponseCount(team.id),
getMonthlyActiveOrganizationPeopleCount(organization.id),
getMonthlyOrganizationResponseCount(organization.id),
]);
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
return (
<PageContentWrapper>
<PageHeader pageTitle="Team Settings">
<TeamSettingsNavbar
<PageHeader pageTitle="Organization Settings">
<OrganizationSettingsNavbar
environmentId={params.environmentId}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
membershipRole={currentUserMembership?.role}
@@ -47,7 +47,7 @@ const Page = async ({ params }) => {
/>
</PageHeader>
<PricingTable
team={team}
organization={organization}
environmentId={params.environmentId}
peopleCount={peopleCount}
responseCount={responseCount}

View File

@@ -1,17 +1,17 @@
import { redirect } from "next/navigation";
import { StripePriceLookupKeys } from "@formbricks/ee/billing/lib/constants";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { upgradePlanAction } from "../actions";
const Page = async ({ params }) => {
const team = await getTeamByEnvironmentId(params.environmentId);
if (!team) {
throw new Error("Team not found");
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!organization) {
throw new Error("Organization not found");
}
const { status, newPlan, url } = await upgradePlanAction(team.id, params.environmentId, [
const { status, newPlan, url } = await upgradePlanAction(organization.id, params.environmentId, [
StripePriceLookupKeys.inAppSurveyUnlimitedPlan90,
StripePriceLookupKeys.linkSurveyUnlimitedPlan19,
StripePriceLookupKeys.userTargetingUnlimitedPlan90,

View File

@@ -1,17 +1,17 @@
import { redirect } from "next/navigation";
import { StripePriceLookupKeys } from "@formbricks/ee/billing/lib/constants";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { upgradePlanAction } from "../actions";
const Page = async ({ params }) => {
const team = await getTeamByEnvironmentId(params.environmentId);
if (!team) {
throw new Error("Team not found");
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!organization) {
throw new Error("Organization not found");
}
const { status, newPlan, url } = await upgradePlanAction(team.id, params.environmentId, [
const { status, newPlan, url } = await upgradePlanAction(organization.id, params.environmentId, [
StripePriceLookupKeys.inAppSurveyUnlimitedPlan33,
StripePriceLookupKeys.linkSurveyUnlimitedPlan33,
StripePriceLookupKeys.userTargetingUnlimitedPlan33,

View File

@@ -7,7 +7,7 @@ import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { TMembershipRole } from "@formbricks/types/memberships";
import { SecondaryNavigation } from "@formbricks/ui/SecondaryNavigation";
export const TeamSettingsNavbar = ({
export const OrganizationSettingsNavbar = ({
environmentId,
isFormbricksCloud,
membershipRole,

View File

@@ -1,4 +1,4 @@
import { TeamSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(team)/components/TeamSettingsNavbar";
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { CheckIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { notFound } from "next/navigation";
@@ -6,9 +6,9 @@ import { notFound } from "next/navigation";
import { getIsEnterpriseEdition } from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { Button } from "@formbricks/ui/Button";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
@@ -20,17 +20,17 @@ const Page = async ({ params }) => {
const session = await getServerSession(authOptions);
const team = await getTeamByEnvironmentId(params.environmentId);
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!session) {
throw new Error("Unauthorized");
}
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isAdmin, isOwner } = getAccessFlags(currentUserMembership?.role);
const isPricingDisabled = !isOwner && !isAdmin;
@@ -47,7 +47,7 @@ const Page = async ({ params }) => {
onRequest: false,
},
{
title: "Team Access Roles (Admin, Editor, Developer, etc.)",
title: "Organization Roles (Admin, Editor, Developer, etc.)",
comingSoon: false,
onRequest: false,
},
@@ -85,8 +85,8 @@ const Page = async ({ params }) => {
return (
<PageContentWrapper>
<PageHeader pageTitle="Team Settings">
<TeamSettingsNavbar
<PageHeader pageTitle="Organization Settings">
<OrganizationSettingsNavbar
environmentId={params.environmentId}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
membershipRole={currentUserMembership?.role}

View File

@@ -1,18 +1,18 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
const Layout = async ({ children, params }) => {
const [team, product, session] = await Promise.all([
getTeamByEnvironmentId(params.environmentId),
const [organization, product, session] = await Promise.all([
getOrganizationByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getServerSession(authOptions),
]);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
if (!product) {

View File

@@ -3,43 +3,43 @@
import { getServerSession } from "next-auth";
import { sendInviteMemberEmail } from "@formbricks/email";
import { hasTeamAuthority } from "@formbricks/lib/auth";
import { hasOrganizationAuthority } from "@formbricks/lib/auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { INVITE_DISABLED } from "@formbricks/lib/constants";
import { deleteInvite, getInvite, inviteUser, resendInvite } from "@formbricks/lib/invite/service";
import { createInviteToken } from "@formbricks/lib/jwt";
import {
deleteMembership,
getMembershipByUserIdTeamId,
getMembershipByUserIdOrganizationId,
getMembershipsByUserId,
} from "@formbricks/lib/membership/service";
import { verifyUserRoleAccess } from "@formbricks/lib/team/auth";
import { deleteTeam, updateTeam } from "@formbricks/lib/team/service";
import { verifyUserRoleAccess } from "@formbricks/lib/organization/auth";
import { deleteOrganization, updateOrganization } from "@formbricks/lib/organization/service";
import { AuthenticationError, AuthorizationError, ValidationError } from "@formbricks/types/errors";
import { TMembershipRole } from "@formbricks/types/memberships";
export const updateTeamNameAction = async (teamId: string, teamName: string) => {
export const updateOrganizationNameAction = async (organizationId: string, organizationName: string) => {
const session = await getServerSession(authOptions);
if (!session) {
throw new AuthenticationError("Not authenticated");
}
const isUserAuthorized = await hasTeamAuthority(session.user.id, teamId);
const isUserAuthorized = await hasOrganizationAuthority(session.user.id, organizationId);
if (!isUserAuthorized) {
throw new AuthenticationError("Not authorized");
}
return await updateTeam(teamId, { name: teamName });
return await updateOrganization(organizationId, { name: organizationName });
};
export const deleteInviteAction = async (inviteId: string, teamId: string) => {
export const deleteInviteAction = async (inviteId: string, organizationId: string) => {
const session = await getServerSession(authOptions);
if (!session) {
throw new AuthenticationError("Not authenticated");
}
const isUserAuthorized = await hasTeamAuthority(session.user.id, teamId);
const isUserAuthorized = await hasOrganizationAuthority(session.user.id, organizationId);
if (!isUserAuthorized) {
throw new AuthenticationError("Not authorized");
@@ -48,54 +48,54 @@ export const deleteInviteAction = async (inviteId: string, teamId: string) => {
return await deleteInvite(inviteId);
};
export const deleteMembershipAction = async (userId: string, teamId: string) => {
export const deleteMembershipAction = async (userId: string, organizationId: string) => {
const session = await getServerSession(authOptions);
if (!session) {
throw new AuthenticationError("Not authenticated");
}
const isUserAuthorized = await hasTeamAuthority(session.user.id, teamId);
const isUserAuthorized = await hasOrganizationAuthority(session.user.id, organizationId);
if (!isUserAuthorized) {
throw new AuthenticationError("Not authorized");
}
const { hasDeleteMembersAccess } = await verifyUserRoleAccess(teamId, session.user.id);
const { hasDeleteMembersAccess } = await verifyUserRoleAccess(organizationId, session.user.id);
if (!hasDeleteMembersAccess) {
throw new AuthenticationError("Not authorized");
}
if (userId === session.user.id) {
throw new AuthenticationError("You cannot delete yourself from the team");
throw new AuthenticationError("You cannot delete yourself from the organization");
}
return await deleteMembership(userId, teamId);
return await deleteMembership(userId, organizationId);
};
export const leaveTeamAction = async (teamId: string) => {
export const leaveOrganizationAction = async (organizationId: string) => {
const session = await getServerSession(authOptions);
if (!session) {
throw new AuthenticationError("Not authenticated");
}
const membership = await getMembershipByUserIdTeamId(session.user.id, teamId);
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organizationId);
if (!membership) {
throw new AuthenticationError("Not a member of this team");
throw new AuthenticationError("Not a member of this organization");
}
if (membership.role === "owner") {
throw new ValidationError("You cannot leave a team you own");
throw new ValidationError("You cannot leave a organization you own");
}
const memberships = await getMembershipsByUserId(session.user.id);
if (!memberships || memberships?.length <= 1) {
throw new ValidationError("You cannot leave the only team you are a member of");
throw new ValidationError("You cannot leave the only organization you are a member of");
}
await deleteMembership(session.user.id, teamId);
await deleteMembership(session.user.id, organizationId);
};
export const createInviteTokenAction = async (inviteId: string) => {
@@ -110,14 +110,14 @@ export const createInviteTokenAction = async (inviteId: string) => {
return { inviteToken: encodeURIComponent(inviteToken) };
};
export const resendInviteAction = async (inviteId: string, teamId: string) => {
export const resendInviteAction = async (inviteId: string, organizationId: string) => {
const session = await getServerSession(authOptions);
if (!session) {
throw new AuthenticationError("Not authenticated");
}
const isUserAuthorized = await hasTeamAuthority(session.user.id, teamId);
const isUserAuthorized = await hasOrganizationAuthority(session.user.id, organizationId);
if (INVITE_DISABLED) {
throw new AuthenticationError("Invite disabled");
@@ -127,7 +127,7 @@ export const resendInviteAction = async (inviteId: string, teamId: string) => {
throw new AuthenticationError("Not authorized");
}
const { hasCreateOrUpdateMembersAccess } = await verifyUserRoleAccess(teamId, session.user.id);
const { hasCreateOrUpdateMembersAccess } = await verifyUserRoleAccess(organizationId, session.user.id);
if (!hasCreateOrUpdateMembersAccess) {
throw new AuthenticationError("Not authorized");
}
@@ -143,7 +143,7 @@ export const resendInviteAction = async (inviteId: string, teamId: string) => {
};
export const inviteUserAction = async (
teamId: string,
organizationId: string,
email: string,
name: string,
role: TMembershipRole
@@ -154,7 +154,7 @@ export const inviteUserAction = async (
throw new AuthenticationError("Not authenticated");
}
const isUserAuthorized = await hasTeamAuthority(session.user.id, teamId);
const isUserAuthorized = await hasOrganizationAuthority(session.user.id, organizationId);
if (INVITE_DISABLED) {
throw new AuthenticationError("Invite disabled");
@@ -164,13 +164,13 @@ export const inviteUserAction = async (
throw new AuthenticationError("Not authorized");
}
const { hasCreateOrUpdateMembersAccess } = await verifyUserRoleAccess(teamId, session.user.id);
const { hasCreateOrUpdateMembersAccess } = await verifyUserRoleAccess(organizationId, session.user.id);
if (!hasCreateOrUpdateMembersAccess) {
throw new AuthenticationError("Not authorized");
}
const invite = await inviteUser({
teamId,
organizationId,
currentUser: { id: session.user.id, name: session.user.name },
invitee: {
email,
@@ -186,17 +186,17 @@ export const inviteUserAction = async (
return invite;
};
export const deleteTeamAction = async (teamId: string) => {
export const deleteOrganizationAction = async (organizationId: string) => {
const session = await getServerSession(authOptions);
if (!session) {
throw new AuthenticationError("Not authenticated");
}
const { hasDeleteAccess } = await verifyUserRoleAccess(teamId, session.user.id);
const { hasDeleteAccess } = await verifyUserRoleAccess(organizationId, session.user.id);
if (!hasDeleteAccess) {
throw new AuthorizationError("Not authorized");
}
return await deleteTeam(teamId);
return await deleteOrganization(organizationId);
};

View File

@@ -55,7 +55,7 @@ export const AddMemberModal = ({
open={open}
setOpen={setOpen}
tabs={tabs}
label={"Invite Team Member"}
label={"Invite Organization Member"}
closeOnOutsideClick={true}
/>
</>

View File

@@ -89,7 +89,7 @@ export const BulkInviteTab = ({ setOpen, onSubmit, canDoRoleManagement }: BulkIn
<Alert variant="destructive" className="mt-1.5 flex items-start bg-slate-50">
<AlertDescription className="ml-2">
<p className="text-sm text-slate-700 ">
<strong>Warning: </strong> Please note that on the Free Plan, all team members are
<strong>Warning: </strong> Please note that on the Free Plan, all organization members are
automatically assigned the &quot;Admin&quot; role regardless of the role specified in the CSV
file.
</p>
@@ -101,7 +101,7 @@ export const BulkInviteTab = ({ setOpen, onSubmit, canDoRoleManagement }: BulkIn
<div className="flex space-x-2">
<Link
download
href="/sample-csv/formbricks-team-members-template.csv"
href="/sample-csv/formbricks-organization-members-template.csv"
target="_blank"
rel="noopener noreferrer">
<Button variant="minimal" size="sm">

View File

@@ -1,36 +1,40 @@
"use client";
import { deleteTeamAction } from "@/app/(app)/environments/[environmentId]/settings/(team)/members/actions";
import { deleteOrganizationAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/members/actions";
import { useRouter } from "next/navigation";
import { Dispatch, SetStateAction, useState } from "react";
import toast from "react-hot-toast";
import { TTeam } from "@formbricks/types/teams";
import { TOrganization } from "@formbricks/types/organizations";
import { Button } from "@formbricks/ui/Button";
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";
import { Input } from "@formbricks/ui/Input";
type DeleteTeamProps = {
team: TTeam;
type DeleteOrganizationProps = {
organization: TOrganization;
isDeleteDisabled?: boolean;
isUserOwner?: boolean;
};
export const DeleteTeam = ({ team, isDeleteDisabled = false, isUserOwner = false }: DeleteTeamProps) => {
export const DeleteOrganization = ({
organization,
isDeleteDisabled = false,
isUserOwner = false,
}: DeleteOrganizationProps) => {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const router = useRouter();
const handleDeleteTeam = async () => {
const handleDeleteOrganization = async () => {
setIsDeleting(true);
try {
await deleteTeamAction(team.id);
toast.success("Team deleted successfully.");
await deleteOrganizationAction(organization.id);
toast.success("Organization deleted successfully.");
router.push("/");
} catch (err) {
toast.error("Error deleting team. Please try again.");
toast.error("Error deleting organization. Please try again.");
}
setIsDeleteDialogOpen(false);
@@ -57,31 +61,37 @@ export const DeleteTeam = ({ team, isDeleteDisabled = false, isUserOwner = false
{isDeleteDisabled && (
<p className="text-sm text-red-700">
{!isUserOwner
? "Only Owner can delete the team."
: "This is your only team, it cannot be deleted. Create a new team first."}
? "Only Owner can delete the organization."
: "This is your only organization, it cannot be deleted. Create a new organization first."}
</p>
)}
<DeleteTeamModal
<DeleteOrganizationModal
open={isDeleteDialogOpen}
setOpen={setIsDeleteDialogOpen}
teamData={team}
deleteTeam={handleDeleteTeam}
organizationData={organization}
deleteOrganization={handleDeleteOrganization}
isDeleting={isDeleting}
/>
</div>
);
};
interface DeleteTeamModalProps {
interface DeleteOrganizationModalProps {
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
// teamData: { name: string; id: string; plan: string };
teamData: TTeam;
deleteTeam: () => void;
// organizationData: { name: string; id: string; plan: string };
organizationData: TOrganization;
deleteOrganization: () => void;
isDeleting?: boolean;
}
const DeleteTeamModal = ({ setOpen, open, teamData, deleteTeam, isDeleting }: DeleteTeamModalProps) => {
const DeleteOrganizationModal = ({
setOpen,
open,
organizationData,
deleteOrganization,
isDeleting,
}: DeleteOrganizationModalProps) => {
const [inputValue, setInputValue] = useState("");
const handleInputChange = (e) => {
@@ -92,32 +102,32 @@ const DeleteTeamModal = ({ setOpen, open, teamData, deleteTeam, isDeleting }: De
<DeleteDialog
open={open}
setOpen={setOpen}
deleteWhat="team"
onDelete={deleteTeam}
text="Before you proceed with deleting this team, please be aware of the following consequences:"
disabled={inputValue !== teamData?.name}
deleteWhat="organization"
onDelete={deleteOrganization}
text="Before you proceed with deleting this organization, please be aware of the following consequences:"
disabled={inputValue !== organizationData?.name}
isDeleting={isDeleting}>
<div className="py-5">
<ul className="list-disc pb-6 pl-6">
<li>
Permanent removal of all <b>products linked to this team</b>. This includes all surveys,
Permanent removal of all <b>products linked to this organization</b>. This includes all surveys,
responses, user actions and attributes associated with these products.
</li>
<li>This action cannot be undone. If it&apos;s gone, it&apos;s gone.</li>
</ul>
<form onSubmit={(e) => e.preventDefault()}>
<label htmlFor="deleteTeamConfirmation">
Please enter <b>{teamData?.name}</b> in the following field to confirm the definitive deletion of
this team:
<label htmlFor="deleteOrganizationConfirmation">
Please enter <b>{organizationData?.name}</b> in the following field to confirm the definitive
deletion of this organization:
</label>
<Input
value={inputValue}
onChange={handleInputChange}
placeholder={teamData?.name}
placeholder={organizationData?.name}
className="mt-5"
type="text"
id="deleteTeamConfirmation"
name="deleteTeamConfirmation"
id="deleteOrganizationConfirmation"
name="deleteOrganizationConfirmation"
/>
</form>
</div>

View File

@@ -1,29 +1,29 @@
import { MembersInfo } from "@/app/(app)/environments/[environmentId]/settings/(team)/members/components/EditMemberships/MembersInfo";
import { MembersInfo } from "@/app/(app)/environments/[environmentId]/settings/(organization)/members/components/EditMemberships/MembersInfo";
import { getRoleManagementPermission } from "@formbricks/ee/lib/service";
import { getInvitesByTeamId } from "@formbricks/lib/invite/service";
import { getMembersByTeamId } from "@formbricks/lib/membership/service";
import { getInvitesByOrganizationId } from "@formbricks/lib/invite/service";
import { getMembersByOrganizationId } from "@formbricks/lib/membership/service";
import { TMembership } from "@formbricks/types/memberships";
import { TTeam } from "@formbricks/types/teams";
import { TOrganization } from "@formbricks/types/organizations";
type EditMembershipsProps = {
team: TTeam;
organization: TOrganization;
currentUserId: string;
currentUserMembership: TMembership;
allMemberships: TMembership[];
};
export const EditMemberships = async ({
team,
organization,
currentUserId,
currentUserMembership: membership,
}: EditMembershipsProps) => {
const members = await getMembersByTeamId(team.id);
const invites = await getInvitesByTeamId(team.id);
const members = await getMembersByOrganizationId(organization.id);
const invites = await getInvitesByOrganizationId(organization.id);
const currentUserRole = membership?.role;
const isUserAdminOrOwner = membership?.role === "admin" || membership?.role === "owner";
const canDoRoleManagement = await getRoleManagementPermission(team);
const canDoRoleManagement = await getRoleManagementPermission(organization);
return (
<div>
@@ -37,7 +37,7 @@ export const EditMemberships = async ({
{currentUserRole && (
<MembersInfo
team={team}
organization={organization}
currentUserId={currentUserId}
invites={invites ?? []}
members={members ?? []}

View File

@@ -5,8 +5,8 @@ import {
deleteInviteAction,
deleteMembershipAction,
resendInviteAction,
} from "@/app/(app)/environments/[environmentId]/settings/(team)/members/actions";
import { ShareInviteModal } from "@/app/(app)/environments/[environmentId]/settings/(team)/members/components/ShareInviteModal";
} from "@/app/(app)/environments/[environmentId]/settings/(organization)/members/actions";
import { ShareInviteModal } from "@/app/(app)/environments/[environmentId]/settings/(organization)/members/components/ShareInviteModal";
import { SendHorizonalIcon, ShareIcon, TrashIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import React, { useMemo, useState } from "react";
@@ -14,19 +14,19 @@ import toast from "react-hot-toast";
import { TInvite } from "@formbricks/types/invites";
import { TMember } from "@formbricks/types/memberships";
import { TTeam } from "@formbricks/types/teams";
import { TOrganization } from "@formbricks/types/organizations";
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
type MemberActionsProps = {
team: TTeam;
organization: TOrganization;
member?: TMember;
invite?: TInvite;
isAdminOrOwner: boolean;
showDeleteButton?: boolean;
};
export const MemberActions = ({ team, member, invite, showDeleteButton }: MemberActionsProps) => {
export const MemberActions = ({ organization, member, invite, showDeleteButton }: MemberActionsProps) => {
const router = useRouter();
const [isDeleteMemberModalOpen, setDeleteMemberModalOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
@@ -40,14 +40,14 @@ export const MemberActions = ({ team, member, invite, showDeleteButton }: Member
if (!member && invite) {
// This is an invite
await deleteInviteAction(invite?.id, team.id);
await deleteInviteAction(invite?.id, organization.id);
toast.success("Invite deleted successfully");
}
if (member && !invite) {
// This is a member
await deleteMembershipAction(member.userId, team.id);
await deleteMembershipAction(member.userId, organization.id);
toast.success("Member deleted successfully");
}
@@ -87,7 +87,7 @@ export const MemberActions = ({ team, member, invite, showDeleteButton }: Member
try {
if (!invite) return;
await resendInviteAction(invite.id, team.id);
await resendInviteAction(invite.id, organization.id);
toast.success("Invitation sent once more.");
} catch (err) {
toast.error(`Error: ${err.message}`);
@@ -140,7 +140,7 @@ export const MemberActions = ({ team, member, invite, showDeleteButton }: Member
<DeleteDialog
open={isDeleteMemberModalOpen}
setOpen={setDeleteMemberModalOpen}
deleteWhat={memberName + " from your team"}
deleteWhat={memberName + " from your organization"}
onDelete={handleDeleteMember}
isDeleting={isDeleting}
/>

View File

@@ -1,14 +1,14 @@
import { MemberActions } from "@/app/(app)/environments/[environmentId]/settings/(team)/members/components/EditMemberships/MemberActions";
import { MemberActions } from "@/app/(app)/environments/[environmentId]/settings/(organization)/members/components/EditMemberships/MemberActions";
import { isInviteExpired } from "@/app/lib/utils";
import { EditMembershipRole } from "@formbricks/ee/RoleManagement/components/EditMembershipRole";
import { TInvite } from "@formbricks/types/invites";
import { TMember, TMembershipRole } from "@formbricks/types/memberships";
import { TTeam } from "@formbricks/types/teams";
import { TOrganization } from "@formbricks/types/organizations";
import { Badge } from "@formbricks/ui/Badge";
type MembersInfoProps = {
team: TTeam;
organization: TOrganization;
members: TMember[];
invites: TInvite[];
isUserAdminOrOwner: boolean;
@@ -23,7 +23,7 @@ const isInvitee = (member: TMember | TInvite): member is TInvite => {
};
export const MembersInfo = async ({
team,
organization,
invites,
isUserAdminOrOwner,
members,
@@ -53,7 +53,7 @@ export const MembersInfo = async ({
memberRole={member.role}
memberId={!isInvitee(member) ? member.userId : ""}
memberName={member.name ?? ""}
teamId={team.id}
organizationId={organization.id}
userId={currentUserId}
memberAccepted={member.accepted}
inviteId={isInvitee(member) ? member.id : ""}
@@ -72,7 +72,7 @@ export const MembersInfo = async ({
))}
<MemberActions
team={team}
organization={organization}
member={!isInvitee(member) ? member : undefined}
invite={isInvitee(member) ? member : undefined}
isAdminOrOwner={isUserAdminOrOwner}

View File

@@ -2,52 +2,52 @@
import {
inviteUserAction,
leaveTeamAction,
} from "@/app/(app)/environments/[environmentId]/settings/(team)/members/actions";
import { AddMemberModal } from "@/app/(app)/environments/[environmentId]/settings/(team)/members/components/AddMemberModal";
leaveOrganizationAction,
} from "@/app/(app)/environments/[environmentId]/settings/(organization)/members/actions";
import { AddMemberModal } from "@/app/(app)/environments/[environmentId]/settings/(organization)/members/components/AddMemberModal";
import { XIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
import { TInvitee } from "@formbricks/types/invites";
import { TTeam } from "@formbricks/types/teams";
import { TOrganization } from "@formbricks/types/organizations";
import { Button } from "@formbricks/ui/Button";
import { CreateTeamModal } from "@formbricks/ui/CreateTeamModal";
import { CreateOrganizationModal } from "@formbricks/ui/CreateOrganizationModal";
import { CustomDialog } from "@formbricks/ui/CustomDialog";
type TeamActionsProps = {
type OrganizationActionsProps = {
role: string;
isAdminOrOwner: boolean;
isLeaveTeamDisabled: boolean;
team: TTeam;
isLeaveOrganizationDisabled: boolean;
organization: TOrganization;
isInviteDisabled: boolean;
canDoRoleManagement: boolean;
isFormbricksCloud: boolean;
environmentId: string;
};
export const TeamActions = ({
export const OrganizationActions = ({
isAdminOrOwner,
role,
team,
isLeaveTeamDisabled,
organization,
isLeaveOrganizationDisabled,
isInviteDisabled,
canDoRoleManagement,
isFormbricksCloud,
environmentId,
}: TeamActionsProps) => {
}: OrganizationActionsProps) => {
const router = useRouter();
const [isLeaveTeamModalOpen, setLeaveTeamModalOpen] = useState(false);
const [isCreateTeamModalOpen, setCreateTeamModalOpen] = useState(false);
const [isLeaveOrganizationModalOpen, setLeaveOrganizationModalOpen] = useState(false);
const [isCreateOrganizationModalOpen, setCreateOrganizationModalOpen] = useState(false);
const [isAddMemberModalOpen, setAddMemberModalOpen] = useState(false);
const [loading, setLoading] = useState(false);
const handleLeaveTeam = async () => {
const handleLeaveOrganization = async () => {
setLoading(true);
try {
await leaveTeamAction(team.id);
toast.success("You left the team successfully");
await leaveOrganizationAction(organization.id);
toast.success("You left the organization successfully");
router.refresh();
setLoading(false);
router.push("/");
@@ -61,7 +61,7 @@ export const TeamActions = ({
try {
await Promise.all(
data.map(async ({ name, email, role }) => {
await inviteUserAction(team.id, email, name, role);
await inviteUserAction(organization.id, email, name, role);
})
);
toast.success("Member invited successfully");
@@ -75,17 +75,21 @@ export const TeamActions = ({
<>
<div className="mb-4 flex justify-end space-x-2 text-right">
{role !== "owner" && (
<Button EndIcon={XIcon} variant="secondary" size="sm" onClick={() => setLeaveTeamModalOpen(true)}>
Leave team
<Button
EndIcon={XIcon}
variant="secondary"
size="sm"
onClick={() => setLeaveOrganizationModalOpen(true)}>
Leave organization
</Button>
)}
<Button
variant="secondary"
size="sm"
onClick={() => {
setCreateTeamModalOpen(true);
setCreateOrganizationModalOpen(true);
}}>
Create new team
Create new organization
</Button>
{!isInviteDisabled && isAdminOrOwner && (
<Button
@@ -98,8 +102,10 @@ export const TeamActions = ({
</Button>
)}
</div>
<CreateTeamModal open={isCreateTeamModalOpen} setOpen={(val) => setCreateTeamModalOpen(val)} />
<CreateOrganizationModal
open={isCreateOrganizationModalOpen}
setOpen={(val) => setCreateOrganizationModalOpen(val)}
/>
<AddMemberModal
open={isAddMemberModalOpen}
setOpen={setAddMemberModalOpen}
@@ -110,17 +116,18 @@ export const TeamActions = ({
/>
<CustomDialog
open={isLeaveTeamModalOpen}
setOpen={setLeaveTeamModalOpen}
open={isLeaveOrganizationModalOpen}
setOpen={setLeaveOrganizationModalOpen}
title="Are you sure?"
text="You wil leave this team and loose access to all surveys and responses. You can only rejoin if you are invited again."
onOk={handleLeaveTeam}
okBtnText="Yes, leave team"
disabled={isLeaveTeamDisabled}
text="You wil leave this organization and loose access to all surveys and responses. You can only rejoin if you are invited again."
onOk={handleLeaveOrganization}
okBtnText="Yes, leave organization"
disabled={isLeaveOrganizationDisabled}
isLoading={loading}>
{isLeaveTeamDisabled && (
{isLeaveOrganizationDisabled && (
<p className="mt-2 text-sm text-red-700">
You cannot leave this team as it is your only team. Create a new team first.
You cannot leave this organization as it is your only organization. Create a new organization
first.
</p>
)}
</CustomDialog>

View File

@@ -0,0 +1,96 @@
"use client";
import { updateOrganizationNameAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/members/actions";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { SubmitHandler, useForm, useWatch } from "react-hook-form";
import toast from "react-hot-toast";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { TMembershipRole } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
interface EditOrganizationNameForm {
name: string;
}
interface EditOrganizationNameProps {
environmentId: string;
organization: TOrganization;
membershipRole?: TMembershipRole;
}
export const EditOrganizationName = ({ organization, membershipRole }: EditOrganizationNameProps) => {
const router = useRouter();
const {
register,
control,
handleSubmit,
formState: { errors },
} = useForm<EditOrganizationNameForm>({
defaultValues: {
name: organization.name,
},
});
const [isUpdatingOrganization, setIsUpdatingOrganization] = useState(false);
const { isViewer } = getAccessFlags(membershipRole);
const organizationName = useWatch({
control,
name: "name",
});
const isOrganizationNameInputEmpty = !organizationName?.trim();
const currentOrganizationName = organizationName?.trim().toLowerCase() ?? "";
const previousOrganizationName = organization?.name?.trim().toLowerCase() ?? "";
const handleUpdateOrganizationName: SubmitHandler<EditOrganizationNameForm> = async (data) => {
try {
data.name = data.name.trim();
setIsUpdatingOrganization(true);
await updateOrganizationNameAction(organization.id, data.name);
setIsUpdatingOrganization(false);
toast.success("Organization name updated successfully.");
router.refresh();
} catch (err) {
setIsUpdatingOrganization(false);
toast.error(`Error: ${err.message}`);
}
};
return isViewer ? (
<p className="text-sm text-red-700">You are not authorized to perform this action.</p>
) : (
<form className="w-full max-w-sm items-center" onSubmit={handleSubmit(handleUpdateOrganizationName)}>
<Label htmlFor="organizationname">Organization Name</Label>
<Input
type="text"
id="organizationname"
defaultValue={organization?.name ?? ""}
{...register("name", {
required: {
message: "Organization name is required.",
value: true,
},
})}
/>
{errors?.name?.message && <p className="text-xs text-red-500">{errors.name.message}</p>}
<Button
type="submit"
className="mt-4"
variant="darkCTA"
size="sm"
loading={isUpdatingOrganization}
disabled={isOrganizationNameInputEmpty || currentOrganizationName === previousOrganizationName}>
Update
</Button>
</form>
);
};

View File

@@ -36,9 +36,13 @@ export const ShareInviteModal = ({ inviteToken, open, setOpen }: ShareInviteModa
<CheckIcon className="h-6 w-6 text-teal-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-semibold leading-6 text-slate-900">Your team invite link is ready!</h3>
<h3 className="text-lg font-semibold leading-6 text-slate-900">
Your organization invite link is ready!
</h3>
<div className="mt-4">
<p className="text-sm text-slate-500">Share this link to let your team member join your team:</p>
<p className="text-sm text-slate-500">
Share this link to let your organization member join your organization:
</p>
<p
ref={linkTextRef}
className="relative mt-3 w-full truncate rounded-lg border border-slate-300 bg-slate-50 p-3 text-center text-slate-800"

View File

@@ -24,7 +24,7 @@ const Loading = () => {
const cards = [
{
title: "Manage members",
description: "Add or remove members in your team",
description: "Add or remove members in your organization",
skeleton: (
<div className="flex flex-col space-y-4 p-4">
<div className="flex items-center justify-end gap-4">
@@ -47,8 +47,8 @@ const Loading = () => {
),
},
{
title: "Team Name",
description: "Give your team a descriptive name",
title: "Organization Name",
description: "Give your organization a descriptive name",
skeleton: (
<div className="flex flex-col p-4">
<Skeleton className="mb-2 h-5 w-32" />

View File

@@ -1,23 +1,26 @@
import { TeamSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(team)/components/TeamSettingsNavbar";
import { TeamActions } from "@/app/(app)/environments/[environmentId]/settings/(team)/members/components/EditMemberships/TeamActions";
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { OrganizationActions } from "@/app/(app)/environments/[environmentId]/settings/(organization)/members/components/EditMemberships/OrganizationActions";
import { getServerSession } from "next-auth";
import { Suspense } from "react";
import { getRoleManagementPermission } from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { INVITE_DISABLED, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getMembershipByUserIdTeamId, getMembershipsByUserId } from "@formbricks/lib/membership/service";
import {
getMembershipByUserIdOrganizationId,
getMembershipsByUserId,
} from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
import { SettingsId } from "@formbricks/ui/SettingsId";
import { Skeleton } from "@formbricks/ui/Skeleton";
import { SettingsCard } from "../../components/SettingsCard";
import { DeleteTeam } from "./components/DeleteTeam";
import { DeleteOrganization } from "./components/DeleteOrganization";
import { EditMemberships } from "./components/EditMemberships";
import { EditTeamName } from "./components/EditTeamName";
import { EditOrganizationName } from "./components/EditOrganizationName";
const MembersLoading = () => (
<div className="rounded-lg border border-slate-200">
@@ -48,40 +51,40 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
if (!session) {
throw new Error("Unauthenticated");
}
const team = await getTeamByEnvironmentId(params.environmentId);
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const canDoRoleManagement = await getRoleManagementPermission(team);
const canDoRoleManagement = await getRoleManagementPermission(organization);
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isOwner, isAdmin } = getAccessFlags(currentUserMembership?.role);
const userMemberships = await getMembershipsByUserId(session.user.id);
const isDeleteDisabled = !isOwner;
const currentUserRole = currentUserMembership?.role;
const isLeaveTeamDisabled = userMemberships.length <= 1;
const isLeaveOrganizationDisabled = userMemberships.length <= 1;
const isUserAdminOrOwner = isAdmin || isOwner;
return (
<PageContentWrapper>
<PageHeader pageTitle="Team Settings">
<TeamSettingsNavbar
<PageHeader pageTitle="Organization Settings">
<OrganizationSettingsNavbar
environmentId={params.environmentId}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
membershipRole={currentUserMembership?.role}
activeId="members"
/>
</PageHeader>
<SettingsCard title="Manage members" description="Add or remove members in your team.">
<SettingsCard title="Manage members" description="Add or remove members in your organization.">
{currentUserRole && (
<TeamActions
team={team}
<OrganizationActions
organization={organization}
isAdminOrOwner={isUserAdminOrOwner}
role={currentUserRole}
isLeaveTeamDisabled={isLeaveTeamDisabled}
isLeaveOrganizationDisabled={isLeaveOrganizationDisabled}
isInviteDisabled={INVITE_DISABLED}
canDoRoleManagement={canDoRoleManagement}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
@@ -92,7 +95,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
{currentUserMembership && (
<Suspense fallback={<MembersLoading />}>
<EditMemberships
team={team}
organization={organization}
currentUserId={session.user?.id}
allMemberships={userMemberships}
currentUserMembership={currentUserMembership}
@@ -100,23 +103,23 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
</Suspense>
)}
</SettingsCard>
<SettingsCard title="Team Name" description="Give your team a descriptive name.">
<EditTeamName
team={team}
<SettingsCard title="Organization Name" description="Give your organization a descriptive name.">
<EditOrganizationName
organization={organization}
environmentId={params.environmentId}
membershipRole={currentUserMembership?.role}
/>
</SettingsCard>
<SettingsCard
title="Delete Team"
description="Delete team with all its products including all surveys, responses, people, actions and attributes">
<DeleteTeam
team={team}
title="Delete Organization"
description="Delete organization with all its products including all surveys, responses, people, actions and attributes">
<DeleteOrganization
organization={organization}
isDeleteDisabled={isDeleteDisabled}
isUserOwner={currentUserRole === "owner"}
/>
</SettingsCard>
<SettingsId title="Team" id={team.id}></SettingsId>
<SettingsId title="Organization" id={organization.id}></SettingsId>
</PageContentWrapper>
);
};

View File

@@ -1,96 +0,0 @@
"use client";
import { updateTeamNameAction } from "@/app/(app)/environments/[environmentId]/settings/(team)/members/actions";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { SubmitHandler, useForm, useWatch } from "react-hook-form";
import toast from "react-hot-toast";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { TMembershipRole } from "@formbricks/types/memberships";
import { TTeam } from "@formbricks/types/teams";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
interface EditTeamNameForm {
name: string;
}
interface EditTeamNameProps {
environmentId: string;
team: TTeam;
membershipRole?: TMembershipRole;
}
export const EditTeamName = ({ team, membershipRole }: EditTeamNameProps) => {
const router = useRouter();
const {
register,
control,
handleSubmit,
formState: { errors },
} = useForm<EditTeamNameForm>({
defaultValues: {
name: team.name,
},
});
const [isUpdatingTeam, setIsUpdatingTeam] = useState(false);
const { isViewer } = getAccessFlags(membershipRole);
const teamName = useWatch({
control,
name: "name",
});
const isTeamNameInputEmpty = !teamName?.trim();
const currentTeamName = teamName?.trim().toLowerCase() ?? "";
const previousTeamName = team?.name?.trim().toLowerCase() ?? "";
const handleUpdateTeamName: SubmitHandler<EditTeamNameForm> = async (data) => {
try {
data.name = data.name.trim();
setIsUpdatingTeam(true);
await updateTeamNameAction(team.id, data.name);
setIsUpdatingTeam(false);
toast.success("Team name updated successfully.");
router.refresh();
} catch (err) {
setIsUpdatingTeam(false);
toast.error(`Error: ${err.message}`);
}
};
return isViewer ? (
<p className="text-sm text-red-700">You are not authorized to perform this action.</p>
) : (
<form className="w-full max-w-sm items-center" onSubmit={handleSubmit(handleUpdateTeamName)}>
<Label htmlFor="teamname">Team Name</Label>
<Input
type="text"
id="teamname"
defaultValue={team?.name ?? ""}
{...register("name", {
required: {
message: "Team name is required.",
value: true,
},
})}
/>
{errors?.name?.message && <p className="text-xs text-red-500">{errors.name.message}</p>}
<Button
type="submit"
className="mt-4"
variant="darkCTA"
size="sm"
loading={isUpdatingTeam}
disabled={isTeamNameInputEmpty || currentTeamName === previousTeamName}>
Update
</Button>
</form>
);
};

View File

@@ -3,7 +3,7 @@
import { EmptyAppSurveys } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
import { useEffect, useRef, useState } from "react";
import { getMembershipByUserIdTeamIdAction } from "@formbricks/lib/membership/hooks/actions";
import { getMembershipByUserIdOrganizationIdAction } from "@formbricks/lib/membership/hooks/actions";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { TEnvironment } from "@formbricks/types/environment";
import { TResponse } from "@formbricks/types/responses";
@@ -76,7 +76,7 @@ export const ResponseTimeline = ({
const getRole = async () => {
if (isSharingPage) return setIsViewer(true);
const membershipRole = await getMembershipByUserIdTeamIdAction(survey.environmentId);
const membershipRole = await getMembershipByUserIdOrganizationIdAction(survey.environmentId);
const { isViewer } = getAccessFlags(membershipRole);
setIsViewer(isViewer);
};

View File

@@ -6,13 +6,13 @@ import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { RESPONSES_PER_PAGE, WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/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 { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getUser } from "@formbricks/lib/user/service";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
@@ -43,13 +43,13 @@ const Page = async ({ params }) => {
throw new Error("User not found");
}
const tags = await getTagsByEnvironmentId(params.environmentId);
const team = await getTeamByEnvironmentId(params.environmentId);
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const totalResponseCount = await getResponseCountBySurveyId(params.surveyId);
const { isViewer } = getAccessFlags(currentUserMembership?.role);

View File

@@ -81,8 +81,8 @@ export const ShareSurveyResults = ({
You are about to release these survey results to the public.
</p>
<p className="text-balanced mt-2 text-sm text-slate-500">
Your survey results will be public. Anyone outside your team can access them if they have the
link.
Your survey results will be public. Anyone outside your organization can access them if they
have the link.
</p>
</div>
<Button

View File

@@ -7,12 +7,12 @@ import { notFound } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/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 { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getUser } from "@formbricks/lib/user/service";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
@@ -50,12 +50,12 @@ const Page = async ({ params }) => {
throw new Error("User not found");
}
const team = await getTeamByEnvironmentId(params.environmentId);
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const totalResponseCount = await getResponseCountBySurveyId(params.surveyId);
const { isViewer } = getAccessFlags(currentUserMembership?.role);

View File

@@ -6,11 +6,11 @@ import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { SURVEYS_PER_PAGE, WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/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 { getSurveyCount } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { Button } from "@formbricks/ui/Button";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
@@ -23,7 +23,7 @@ export const metadata: Metadata = {
const Page = async ({ params }) => {
const session = await getServerSession(authOptions);
const product = await getProductByEnvironmentId(params.environmentId);
const team = await getTeamByEnvironmentId(params.environmentId);
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!session) {
throw new Error("Session not found");
}
@@ -32,11 +32,11 @@ const Page = async ({ params }) => {
throw new Error("Product not found");
}
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isViewer } = getAccessFlags(currentUserMembership?.role);
const environment = await getEnvironment(params.environmentId);

View File

@@ -3,16 +3,16 @@
import { getServerSession } from "next-auth";
import { sendInviteMemberEmail } from "@formbricks/email";
import { hasTeamAuthority } from "@formbricks/lib/auth";
import { hasOrganizationAuthority } from "@formbricks/lib/auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { INVITE_DISABLED } from "@formbricks/lib/constants";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { inviteUser } from "@formbricks/lib/invite/service";
import { verifyUserRoleAccess } from "@formbricks/lib/organization/auth";
import { canUserAccessProduct } from "@formbricks/lib/product/auth";
import { getProduct, updateProduct } from "@formbricks/lib/product/service";
import { createSurvey } from "@formbricks/lib/survey/service";
import { verifyUserRoleAccess } from "@formbricks/lib/team/auth";
import { updateUser } from "@formbricks/lib/user/service";
import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors";
import { TMembershipRole } from "@formbricks/types/memberships";
@@ -21,8 +21,8 @@ import { TSurveyInput, TSurveyType } from "@formbricks/types/surveys";
import { TTemplate } from "@formbricks/types/templates";
import { TUserUpdateInput } from "@formbricks/types/user";
export const inviteTeamMateAction = async (
teamId: string,
export const inviteOrganizationMemberAction = async (
organizationId: string,
email: string,
role: TMembershipRole,
inviteMessage: string
@@ -33,7 +33,7 @@ export const inviteTeamMateAction = async (
throw new AuthenticationError("Not authenticated");
}
const isUserAuthorized = await hasTeamAuthority(session.user.id, teamId);
const isUserAuthorized = await hasOrganizationAuthority(session.user.id, organizationId);
if (INVITE_DISABLED) {
throw new AuthenticationError("Invite disabled");
@@ -43,13 +43,13 @@ export const inviteTeamMateAction = async (
throw new AuthenticationError("Not authorized");
}
const { hasCreateOrUpdateMembersAccess } = await verifyUserRoleAccess(teamId, session.user.id);
const { hasCreateOrUpdateMembersAccess } = await verifyUserRoleAccess(organizationId, session.user.id);
if (!hasCreateOrUpdateMembersAccess) {
throw new AuthenticationError("Not authorized");
}
const invite = await inviteUser({
teamId,
organizationId,
currentUser: { id: session.user.id, name: session.user.name },
invitee: {
email,
@@ -136,7 +136,7 @@ export const updateProductAction = async (
const product = await getProduct(productId);
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(product!.teamId, session.user.id);
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(product!.organizationId, session.user.id);
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
return await updateProduct(productId, updatedProduct);

View File

@@ -24,7 +24,7 @@ const goToProduct = async (router) => {
router.push("/");
};
const goToTeamInvitePage = async () => {
const goToOrganizationInvitePage = async () => {
localStorage.setItem("onboardingCurrentStep", "5");
};
@@ -75,7 +75,7 @@ const ConnectedState = ({ goToProduct }) => {
);
};
const NotConnectedState = ({ environment, webAppUrl, jsPackageVersion, goToTeamInvitePage }) => {
const NotConnectedState = ({ environment, webAppUrl, jsPackageVersion, goToOrganizationInvitePage }) => {
return (
<div className="mb-8 w-full max-w-xl space-y-8">
<OnboardingTitle
@@ -98,7 +98,7 @@ const NotConnectedState = ({ environment, webAppUrl, jsPackageVersion, goToTeamI
id="onboarding-inapp-connect-not-sure-how-to-do-this"
className="mt-8 font-normal text-slate-400"
variant="minimal"
onClick={goToTeamInvitePage}>
onClick={goToOrganizationInvitePage}>
Skip
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
@@ -145,10 +145,10 @@ export const ConnectWithFormbricks = ({
jsPackageVersion={jsPackageVersion}
webAppUrl={webAppUrl}
environment={environment}
goToTeamInvitePage={() => {
goToOrganizationInvitePage={() => {
setCurrentStep(5);
localStorage.setItem("onboardingCurrentStep", "5");
goToTeamInvitePage();
goToOrganizationInvitePage();
}}
/>
);

View File

@@ -5,14 +5,14 @@ import { useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { TTeam } from "@formbricks/types/teams";
import { TOrganization } from "@formbricks/types/organizations";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { finishOnboardingAction, inviteTeamMateAction } from "../../actions";
import { finishOnboardingAction, inviteOrganizationMemberAction } from "../../actions";
interface InviteTeamMateProps {
team: TTeam;
interface InviteOrganizationMemberProps {
organization: TOrganization;
environmentId: string;
setCurrentStep: (currentStep: number) => void;
}
@@ -38,7 +38,11 @@ const InviteMessageInput = ({ value, onChange }) => {
);
};
export const InviteTeamMate = ({ team, environmentId, setCurrentStep }: InviteTeamMateProps) => {
export const InviteOrganizationMember = ({
organization,
environmentId,
setCurrentStep,
}: InviteOrganizationMemberProps) => {
const [formState, setFormState] = useState(INITIAL_FORM_STATE);
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
@@ -54,7 +58,12 @@ export const InviteTeamMate = ({ team, environmentId, setCurrentStep }: InviteTe
return;
}
try {
await inviteTeamMateAction(team.id, formState.email, "developer", formState.inviteMessage);
await inviteOrganizationMemberAction(
organization.id,
formState.email,
"developer",
formState.inviteMessage
);
toast.success("Invite sent successful");
goToProduct();
} catch (error) {
@@ -85,7 +94,7 @@ export const InviteTeamMate = ({ team, environmentId, setCurrentStep }: InviteTe
return (
<div className="mb-8 w-full max-w-xl space-y-8">
<OnboardingTitle
title="Invite your team to help out"
title="Invite your organization to help out"
subtitle="Ask your tech-savvy co-worker to finish the setup:"
/>
<div className="flex h-[65vh] flex-col justify-between">

View File

@@ -3,7 +3,7 @@
import jsPackageJson from "@/../../packages/js/package.json";
import { finishOnboardingAction } from "@/app/(app)/onboarding/actions";
import { ConnectWithFormbricks } from "@/app/(app)/onboarding/components/inapp/ConnectWithFormbricks";
import { InviteTeamMate } from "@/app/(app)/onboarding/components/inapp/InviteTeamMate";
import { InviteOrganizationMember } from "@/app/(app)/onboarding/components/inapp/InviteOrganizationMate";
import { Objective } from "@/app/(app)/onboarding/components/inapp/SurveyObjective";
import { Role } from "@/app/(app)/onboarding/components/inapp/SurveyRole";
import { CreateFirstSurvey } from "@/app/(app)/onboarding/components/link/CreateFirstSurvey";
@@ -12,7 +12,7 @@ import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TTeam } from "@formbricks/types/teams";
import { TOrganization } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user";
import { PathwaySelect } from "./PathwaySelect";
@@ -23,7 +23,7 @@ interface OnboardingProps {
session: Session;
environment: TEnvironment;
user: TUser;
team: TTeam;
organization: TOrganization;
webAppUrl: string;
}
@@ -32,7 +32,7 @@ export const Onboarding = ({
session,
environment,
user,
team,
organization,
webAppUrl,
}: OnboardingProps) => {
const router = useRouter();
@@ -140,7 +140,11 @@ export const Onboarding = ({
return selectedPathway === "link" ? (
<CreateFirstSurvey environmentId={environment.id} />
) : (
<InviteTeamMate environmentId={environment.id} team={team} setCurrentStep={setCurrentStep} />
<InviteOrganizationMember
environmentId={environment.id}
organization={organization}
setCurrentStep={setCurrentStep}
/>
);
default:
return null;

View File

@@ -5,7 +5,7 @@ import { redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { IS_FORMBRICKS_CLOUD, WEBAPP_URL } from "@formbricks/lib/constants";
import { getFirstEnvironmentByUserId } from "@formbricks/lib/environment/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
const Page = async () => {
@@ -24,11 +24,11 @@ const Page = async () => {
const userId = session.user.id;
const environment = await getFirstEnvironmentByUserId(userId);
const user = await getUser(userId);
const team = environment ? await getTeamByEnvironmentId(environment.id) : null;
const organization = environment ? await getOrganizationByEnvironmentId(environment.id) : null;
// Ensure all necessary data is available
if (!environment || !user || !team) {
throw new Error("Failed to get necessary user, environment, or team information");
if (!environment || !user || !organization) {
throw new Error("Failed to get necessary user, environment, or organization information");
}
return (
@@ -37,7 +37,7 @@ const Page = async () => {
session={session}
environment={environment}
user={user}
team={team}
organization={organization}
webAppUrl={WEBAPP_URL}
/>
);

View File

@@ -51,7 +51,7 @@ export const WrongAccountContent = () => {
export const RightAccountContent = () => {
return (
<ContentLayout headline="Youre in 🎉" description="Welcome to the team.">
<ContentLayout headline="Youre in 🎉" description="Welcome to the organization.">
<Button variant="darkCTA" href="/">
Go to app
</Button>

View File

@@ -41,7 +41,7 @@ const Page = async ({ searchParams }) => {
} else if (session.user?.email !== email) {
return <WrongAccountContent />;
} else {
await createMembership(invite.teamId, session.user.id, { accepted: true, role: invite.role });
await createMembership(invite.organizationId, session.user.id, { accepted: true, role: invite.role });
await deleteInvite(inviteId);
sendInviteAcceptedEmail(invite.creator.name ?? "", session.user?.name ?? "", invite.creator.email);

View File

@@ -1,4 +1,4 @@
import { hasTeamAccess } from "@/app/lib/api/apiHelper";
import { hasOrganizationAccess } from "@/app/lib/api/apiHelper";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { notFound } from "next/navigation";
@@ -8,16 +8,16 @@ import { getEnvironments } from "@formbricks/lib/environment/service";
import { getProducts } from "@formbricks/lib/product/service";
import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors";
export const GET = async (_: Request, context: { params: { teamId: string } }) => {
const teamId = context?.params?.teamId;
if (!teamId) return notFound();
export const GET = async (_: Request, context: { params: { organizationId: string } }) => {
const organizationId = context?.params?.organizationId;
if (!organizationId) return notFound();
// check auth
const session = await getServerSession(authOptions);
if (!session) throw new AuthenticationError("Not authenticated");
const hasAccess = await hasTeamAccess(session.user, teamId);
const hasAccess = await hasOrganizationAccess(session.user, organizationId);
if (!hasAccess) throw new AuthorizationError("Unauthorized");
// redirect to first product's production environment
const products = await getProducts(teamId);
const products = await getProducts(organizationId);
if (products.length === 0) return notFound();
const firstProduct = products[0];
const environments = await getEnvironments(firstProduct.id);

View File

@@ -1,4 +1,4 @@
import { hasTeamAccess } from "@/app/lib/api/apiHelper";
import { hasOrganizationAccess } from "@/app/lib/api/apiHelper";
import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation";
@@ -15,7 +15,7 @@ export const GET = async (_: Request, context: { params: { productId: string } }
if (!session) throw new AuthenticationError("Not authenticated");
const product = await getProduct(productId);
if (!product) return notFound();
const hasAccess = await hasTeamAccess(session.user, product.teamId);
const hasAccess = await hasOrganizationAccess(session.user, product.organizationId);
if (!hasAccess) throw new AuthorizationError("Unauthorized");
// redirect to product's production environment
const environments = await getEnvironments(product.id);

View File

@@ -5,14 +5,14 @@ import { ProductFeatureKeys } from "@formbricks/ee/billing/lib/constants";
import { reportUsageToStripe } from "@formbricks/ee/billing/lib/reportUsage";
import { CRON_SECRET, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import {
getMonthlyActiveTeamPeopleCount,
getMonthlyTeamResponseCount,
getTeamsWithPaidPlan,
} from "@formbricks/lib/team/service";
import { TTeam } from "@formbricks/types/teams";
getMonthlyActiveOrganizationPeopleCount,
getMonthlyOrganizationResponseCount,
getOrganizationsWithPaidPlan,
} from "@formbricks/lib/organization/service";
import { TOrganization } from "@formbricks/types/organizations";
const reportTeamUsage = async (team: TTeam) => {
const stripeCustomerId = team.billing.stripeCustomerId;
const reportOrganizationUsage = async (organization: TOrganization) => {
const stripeCustomerId = organization.billing.stripeCustomerId;
if (!stripeCustomerId) {
return;
}
@@ -22,16 +22,17 @@ const reportTeamUsage = async (team: TTeam) => {
}
let calculateResponses =
team.billing.features.inAppSurvey.status !== "inactive" && !team.billing.features.inAppSurvey.unlimited;
organization.billing.features.inAppSurvey.status !== "inactive" &&
!organization.billing.features.inAppSurvey.unlimited;
let calculatePeople =
team.billing.features.userTargeting.status !== "inactive" &&
!team.billing.features.userTargeting.unlimited;
organization.billing.features.userTargeting.status !== "inactive" &&
!organization.billing.features.userTargeting.unlimited;
if (!calculatePeople && !calculateResponses) {
return;
}
let people = await getMonthlyActiveTeamPeopleCount(team.id);
let responses = await getMonthlyTeamResponseCount(team.id);
let people = await getMonthlyActiveOrganizationPeopleCount(organization.id);
let responses = await getMonthlyOrganizationResponseCount(organization.id);
if (calculatePeople) {
await reportUsageToStripe(
@@ -60,8 +61,8 @@ export const POST = async (): Promise<Response> => {
}
try {
const teamsWithPaidPlan = await getTeamsWithPaidPlan();
await Promise.all(teamsWithPaidPlan.map(reportTeamUsage));
const organizationsWithPaidPlan = await getOrganizationsWithPaidPlan();
await Promise.all(organizationsWithPaidPlan.map(reportOrganizationUsage));
return responses.successResponse({}, true);
} catch (error) {

View File

@@ -25,41 +25,43 @@ export const POST = async (): Promise<Response> => {
const emailSendingPromises: Promise<void>[] = [];
// Fetch all team IDs
const teamIds = await getTeamIds();
// Fetch all organization IDs
const organizationIds = await getOrganizationIds();
// Paginate through teams
for (let i = 0; i < teamIds.length; i += BATCH_SIZE) {
const batchedTeamIds = teamIds.slice(i, i + BATCH_SIZE);
// Fetch products for batched teams asynchronously
const batchedProductsPromises = batchedTeamIds.map((teamId) => getProductsByTeamId(teamId));
// Paginate through organizations
for (let i = 0; i < organizationIds.length; i += BATCH_SIZE) {
const batchedOrganizationIds = organizationIds.slice(i, i + BATCH_SIZE);
// Fetch products for batched organizations asynchronously
const batchedProductsPromises = batchedOrganizationIds.map((organizationId) =>
getProductsByOrganizationId(organizationId)
);
const batchedProducts = await Promise.all(batchedProductsPromises);
for (const products of batchedProducts) {
for (const product of products) {
const teamMembers = product.team.memberships;
const teamMembersWithNotificationEnabled = teamMembers.filter(
const organizationMembers = product.organization.memberships;
const organizationMembersWithNotificationEnabled = organizationMembers.filter(
(member) =>
member.user.notificationSettings?.weeklySummary &&
member.user.notificationSettings.weeklySummary[product.id]
);
if (teamMembersWithNotificationEnabled.length === 0) continue;
if (organizationMembersWithNotificationEnabled.length === 0) continue;
const notificationResponse = getNotificationResponse(product.environments[0], product.name);
if (notificationResponse.insights.numLiveSurvey === 0) {
for (const teamMember of teamMembersWithNotificationEnabled) {
for (const organizationMember of organizationMembersWithNotificationEnabled) {
emailSendingPromises.push(
sendNoLiveSurveyNotificationEmail(teamMember.user.email, notificationResponse)
sendNoLiveSurveyNotificationEmail(organizationMember.user.email, notificationResponse)
);
}
continue;
}
for (const teamMember of teamMembersWithNotificationEnabled) {
for (const organizationMember of organizationMembersWithNotificationEnabled) {
emailSendingPromises.push(
sendWeeklySummaryNotificationEmail(teamMember.user.email, notificationResponse)
sendWeeklySummaryNotificationEmail(organizationMember.user.email, notificationResponse)
);
}
}
@@ -70,22 +72,22 @@ export const POST = async (): Promise<Response> => {
return responses.successResponse({}, true);
};
const getTeamIds = async (): Promise<string[]> => {
const teams = await prisma.team.findMany({
const getOrganizationIds = async (): Promise<string[]> => {
const organizations = await prisma.organization.findMany({
select: {
id: true,
},
});
return teams.map((team) => team.id);
return organizations.map((organization) => organization.id);
};
const getProductsByTeamId = async (teamId: string): Promise<TWeeklySummaryProductData[]> => {
const getProductsByOrganizationId = async (organizationId: string): Promise<TWeeklySummaryProductData[]> => {
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
return await prisma.product.findMany({
where: {
teamId: teamId,
organizationId: organizationId,
},
select: {
id: true,
@@ -152,7 +154,7 @@ const getProductsByTeamId = async (teamId: string): Promise<TWeeklySummaryProduc
},
},
},
team: {
organization: {
select: {
memberships: {
select: {

View File

@@ -81,12 +81,12 @@ export const POST = async (request: Request) => {
if (event === "responseFinished") {
// check for email notifications
// get all users that have a membership of this environment's team
// get all users that have a membership of this environment's organization
const users = await prisma.user.findMany({
where: {
memberships: {
some: {
team: {
organization: {
products: {
some: {
environments: {

View File

@@ -3,12 +3,12 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { updateAttributes } from "@formbricks/lib/attribute/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { personCache } from "@formbricks/lib/person/cache";
import { getPerson } from "@formbricks/lib/person/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { getSyncSurveys } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { ZJsPeopleAttributeInput } from "@formbricks/types/js";
interface Context {
@@ -58,10 +58,10 @@ export const POST = async (req: Request, context: Context): Promise<Response> =>
environmentId,
});
const team = await getTeamByEnvironmentId(environmentId);
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const [surveys, noCodeActionClasses, product] = await Promise.all([

View File

@@ -3,12 +3,12 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { updateAttributes } from "@formbricks/lib/attribute/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { personCache } from "@formbricks/lib/person/cache";
import { getPerson } from "@formbricks/lib/person/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { getSyncSurveys } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { ZJsPeopleAttributeInput } from "@formbricks/types/js";
interface Context {
@@ -57,10 +57,10 @@ export const POST = async (req: Request, context: Context): Promise<Response> =>
environmentId,
});
const team = await getTeamByEnvironmentId(environmentId);
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team not found");
if (!organization) {
throw new Error("Organization not found");
}
const [surveys, noCodeActionClasses, product] = await Promise.all([

View File

@@ -7,15 +7,15 @@ import {
} from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { reverseTranslateSurvey } from "@formbricks/lib/i18n/reverseTranslation";
import {
getMonthlyActiveOrganizationPeopleCount,
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
} from "@formbricks/lib/organization/service";
import { getPerson } from "@formbricks/lib/person/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { getSurveys, getSyncSurveys } from "@formbricks/lib/survey/service";
import {
getMonthlyActiveTeamPeopleCount,
getMonthlyTeamResponseCount,
getTeamByEnvironmentId,
} from "@formbricks/lib/team/service";
import { TEnvironment } from "@formbricks/types/environment";
import { TJsLegacyState, TSurveyWithTriggers } from "@formbricks/types/js";
import { TPerson } from "@formbricks/types/people";
@@ -43,19 +43,19 @@ export const getUpdatedState = async (environmentId: string, personId?: string):
throw new Error("Environment does not exist");
}
// check team subscriptons
const team = await getTeamByEnvironmentId(environmentId);
// check organization subscriptons
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team does not exist");
if (!organization) {
throw new Error("Organization does not exist");
}
// check if Monthly Active Users limit is reached
if (IS_FORMBRICKS_CLOUD) {
const hasUserTargetingSubscription =
team?.billing?.features.userTargeting.status &&
["active", "canceled"].includes(team?.billing?.features.userTargeting.status);
const currentMau = await getMonthlyActiveTeamPeopleCount(team.id);
organization?.billing?.features.userTargeting.status &&
["active", "canceled"].includes(organization?.billing?.features.userTargeting.status);
const currentMau = await getMonthlyActiveOrganizationPeopleCount(organization.id);
const isMauLimitReached = !hasUserTargetingSubscription && currentMau >= PRICING_USERTARGETING_FREE_MTU;
if (isMauLimitReached) {
const errorMessage = `Monthly Active Users limit reached in ${environmentId} (${currentMau}/${MAU_LIMIT})`;
@@ -82,9 +82,9 @@ export const getUpdatedState = async (environmentId: string, personId?: string):
let isAppSurveyLimitReached = false;
if (IS_FORMBRICKS_CLOUD) {
const hasAppSurveySubscription =
team?.billing?.features.inAppSurvey.status &&
["active", "canceled"].includes(team?.billing?.features.inAppSurvey.status);
const monthlyResponsesCount = await getMonthlyTeamResponseCount(team.id);
organization?.billing?.features.inAppSurvey.status &&
["active", "canceled"].includes(organization?.billing?.features.inAppSurvey.status);
const monthlyResponsesCount = await getMonthlyOrganizationResponseCount(organization.id);
isAppSurveyLimitReached =
IS_FORMBRICKS_CLOUD &&
!hasAppSurveySubscription &&

View File

@@ -3,7 +3,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
import { createAction } from "@formbricks/lib/action/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { ZActionInput } from "@formbricks/types/actions";
interface Context {
@@ -36,8 +36,8 @@ export const POST = async (req: Request, context: Context): Promise<Response> =>
// Formbricks Cloud: Make sure environment is part of a paid plan
if (IS_FORMBRICKS_CLOUD) {
const team = await getTeamByEnvironmentId(context.params.environmentId);
if (!team || team.billing.features.userTargeting.status !== "active") {
const organization = await getOrganizationByEnvironmentId(context.params.environmentId);
if (!organization || organization.billing.features.userTargeting.status !== "active") {
// temporary return status code 200 to avoid CORS issues; will be changed to 400 in the future
return responses.successResponse({}, true);
//return responses.badRequestResponse("Storing actions is only possible in a paid plan", {}, true);

View File

@@ -11,15 +11,15 @@ import {
PRICING_USERTARGETING_FREE_MTU,
} from "@formbricks/lib/constants";
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
import {
getMonthlyActiveOrganizationPeopleCount,
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
} from "@formbricks/lib/organization/service";
import { createPerson, getIsPersonMonthlyActive, getPersonByUserId } from "@formbricks/lib/person/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { getSyncSurveys, transformToLegacySurvey } from "@formbricks/lib/survey/service";
import {
getMonthlyActiveTeamPeopleCount,
getMonthlyTeamResponseCount,
getTeamByEnvironmentId,
} from "@formbricks/lib/team/service";
import { isVersionGreaterThanOrEqualTo } from "@formbricks/lib/utils/version";
import { TLegacySurvey } from "@formbricks/types/LegacySurvey";
import { TEnvironment } from "@formbricks/types/environment";
@@ -75,11 +75,11 @@ export const GET = async (
await updateEnvironment(environment.id, { widgetSetupCompleted: true });
}
// check team subscriptions
const team = await getTeamByEnvironmentId(environmentId);
// check organization subscriptions
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team does not exist");
if (!organization) {
throw new Error("Organization does not exist");
}
// check if MAU limit is reached
@@ -88,15 +88,15 @@ export const GET = async (
if (IS_FORMBRICKS_CLOUD) {
// check userTargeting subscription
const hasUserTargetingSubscription =
team.billing.features.userTargeting.status &&
["active", "canceled"].includes(team.billing.features.userTargeting.status);
const currentMau = await getMonthlyActiveTeamPeopleCount(team.id);
organization.billing.features.userTargeting.status &&
["active", "canceled"].includes(organization.billing.features.userTargeting.status);
const currentMau = await getMonthlyActiveOrganizationPeopleCount(organization.id);
isMauLimitReached = !hasUserTargetingSubscription && currentMau >= PRICING_USERTARGETING_FREE_MTU;
// check inAppSurvey subscription
const hasInAppSurveySubscription =
team.billing.features.inAppSurvey.status &&
["active", "canceled"].includes(team.billing.features.inAppSurvey.status);
const currentResponseCount = await getMonthlyTeamResponseCount(team.id);
organization.billing.features.inAppSurvey.status &&
["active", "canceled"].includes(organization.billing.features.inAppSurvey.status);
const currentResponseCount = await getMonthlyOrganizationResponseCount(organization.id);
isInAppSurveyLimitReached =
!hasInAppSurveySubscription && currentResponseCount >= PRICING_APPSURVEYS_FREE_RESPONSES;
}

View File

@@ -7,9 +7,9 @@ import { NextRequest } from "next/server";
import { ENCRYPTION_KEY, UPLOADS_DIR } from "@formbricks/lib/constants";
import { validateLocalSignedUrl } from "@formbricks/lib/crypto";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { putFileToLocalStorage } from "@formbricks/lib/storage/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
interface Context {
params: {
@@ -69,14 +69,17 @@ export const POST = async (req: NextRequest, context: Context): Promise<Response
return responses.unauthorizedResponse();
}
const [survey, team] = await Promise.all([getSurvey(surveyId), getTeamByEnvironmentId(environmentId)]);
const [survey, organization] = await Promise.all([
getSurvey(surveyId),
getOrganizationByEnvironmentId(environmentId),
]);
if (!survey) {
return responses.notFoundResponse("Survey", surveyId);
}
if (!team) {
return responses.notFoundResponse("TeamByEnvironmentId", environmentId);
if (!organization) {
return responses.notFoundResponse("OrganizationByEnvironmentId", environmentId);
}
const fileName = decodeURIComponent(encodedFileName);
@@ -105,7 +108,9 @@ export const POST = async (req: NextRequest, context: Context): Promise<Response
}
try {
const plan = ["active", "canceled"].includes(team.billing.features.linkSurvey.status) ? "pro" : "free";
const plan = ["active", "canceled"].includes(organization.billing.features.linkSurvey.status)
? "pro"
: "free";
const bytes = await file.arrayBuffer();
const fileBuffer = Buffer.from(bytes);

View File

@@ -1,8 +1,8 @@
import { responses } from "@/app/lib/api/response";
import { NextRequest } from "next/server";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { uploadPrivateFile } from "./lib/uploadPrivateFile";
@@ -39,17 +39,22 @@ export const POST = async (req: NextRequest, context: Context): Promise<Response
return responses.badRequestResponse("contentType is required");
}
const [survey, team] = await Promise.all([getSurvey(surveyId), getTeamByEnvironmentId(environmentId)]);
const [survey, organization] = await Promise.all([
getSurvey(surveyId),
getOrganizationByEnvironmentId(environmentId),
]);
if (!survey) {
return responses.notFoundResponse("Survey", surveyId);
}
if (!team) {
return responses.notFoundResponse("TeamByEnvironmentId", environmentId);
if (!organization) {
return responses.notFoundResponse("OrganizationByEnvironmentId", environmentId);
}
const plan = ["active", "canceled"].includes(team.billing.features.linkSurvey.status) ? "pro" : "free";
const plan = ["active", "canceled"].includes(organization.billing.features.linkSurvey.status)
? "pro"
: "free";
return await uploadPrivateFile(fileName, environmentId, fileType, plan);
};

View File

@@ -10,10 +10,13 @@ import {
WEBAPP_URL,
} from "@formbricks/lib/constants";
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
import {
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
} from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { createSurvey, getSurveys, transformToLegacySurvey } from "@formbricks/lib/survey/service";
import { getMonthlyTeamResponseCount, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getExampleSurveyTemplate } from "@formbricks/lib/templates";
import { isVersionGreaterThanOrEqualTo } from "@formbricks/lib/utils/version";
import { TLegacySurvey } from "@formbricks/types/LegacySurvey";
@@ -50,9 +53,9 @@ export const GET = async (
const { environmentId } = syncInputValidation.data;
const environment = await getEnvironment(environmentId);
const team = await getTeamByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team does not exist");
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!organization) {
throw new Error("Organization does not exist");
}
if (!environment) {
@@ -62,13 +65,13 @@ export const GET = async (
// check if MAU limit is reached
let isInAppSurveyLimitReached = false;
if (IS_FORMBRICKS_CLOUD) {
// check team subscriptons
// check organization subscriptons
// check inAppSurvey subscription
const hasInAppSurveySubscription =
team.billing.features.inAppSurvey.status &&
["active", "canceled"].includes(team.billing.features.inAppSurvey.status);
const currentResponseCount = await getMonthlyTeamResponseCount(team.id);
organization.billing.features.inAppSurvey.status &&
["active", "canceled"].includes(organization.billing.features.inAppSurvey.status);
const currentResponseCount = await getMonthlyOrganizationResponseCount(organization.id);
isInAppSurveyLimitReached =
!hasInAppSurveySubscription && currentResponseCount >= PRICING_APPSURVEYS_FREE_RESPONSES;
if (isInAppSurveyLimitReached) {

View File

@@ -53,12 +53,12 @@ const deleteUser = async (userId: string) => {
});
};
const updateUserMembership = async (teamId: string, userId: string, role: MembershipRole) => {
const updateUserMembership = async (organizationId: string, userId: string, role: MembershipRole) => {
await prisma.membership.update({
where: {
userId_teamId: {
userId_organizationId: {
userId,
teamId,
organizationId,
},
},
data: {
@@ -70,10 +70,10 @@ const updateUserMembership = async (teamId: string, userId: string, role: Member
const getAdminMemberships = (memberships: Membership[]) =>
memberships.filter((membership) => membership.role === MembershipRole.admin);
const deleteTeam = async (teamId: string) => {
await prisma.team.delete({
const deleteOrganization = async (organizationId: string) => {
await prisma.organization.delete({
where: {
id: teamId,
id: organizationId,
},
});
};
@@ -93,7 +93,7 @@ export const DELETE = async () => {
userId: currentUser.id,
},
include: {
team: {
organization: {
select: {
id: true,
name: true,
@@ -109,22 +109,22 @@ export const DELETE = async () => {
});
for (const currentUserMembership of currentUserMemberships) {
const teamMemberships = currentUserMembership.team.memberships;
const organizationMemberships = currentUserMembership.organization.memberships;
const role = currentUserMembership.role;
const teamId = currentUserMembership.teamId;
const organizationId = currentUserMembership.organizationId;
const teamAdminMemberships = getAdminMemberships(teamMemberships);
const teamHasAtLeastOneAdmin = teamAdminMemberships.length > 0;
const teamHasOnlyOneMember = teamMemberships.length === 1;
const currentUserIsTeamOwner = role === MembershipRole.owner;
const organizationAdminMemberships = getAdminMemberships(organizationMemberships);
const organizationHasAtLeastOneAdmin = organizationAdminMemberships.length > 0;
const organizationHasOnlyOneMember = organizationMemberships.length === 1;
const currentUserIsOrganizationOwner = role === MembershipRole.owner;
if (teamHasOnlyOneMember) {
await deleteTeam(teamId);
} else if (currentUserIsTeamOwner && teamHasAtLeastOneAdmin) {
const firstAdmin = teamAdminMemberships[0];
await updateUserMembership(teamId, firstAdmin.userId, MembershipRole.owner);
} else if (currentUserIsTeamOwner) {
await deleteTeam(teamId);
if (organizationHasOnlyOneMember) {
await deleteOrganization(organizationId);
} else if (currentUserIsOrganizationOwner && organizationHasAtLeastOneAdmin) {
const firstAdmin = organizationAdminMemberships[0];
await updateUserMembership(organizationId, firstAdmin.userId, MembershipRole.owner);
} else if (currentUserIsOrganizationOwner) {
await deleteOrganization(organizationId);
}
}

View File

@@ -1,8 +1,8 @@
import { prisma } from "@formbricks/database";
import { sendInviteAcceptedEmail, sendVerificationEmail } from "@formbricks/email";
import {
DEFAULT_TEAM_ID,
DEFAULT_TEAM_ROLE,
DEFAULT_ORGANIZATION_ID,
DEFAULT_ORGANIZATION_ROLE,
EMAIL_AUTH_ENABLED,
EMAIL_VERIFICATION_DISABLED,
INVITE_DISABLED,
@@ -11,8 +11,8 @@ import {
import { deleteInvite } from "@formbricks/lib/invite/service";
import { verifyInviteToken } from "@formbricks/lib/jwt";
import { createMembership } from "@formbricks/lib/membership/service";
import { createOrganization, getOrganization } from "@formbricks/lib/organization/service";
import { createProduct } from "@formbricks/lib/product/service";
import { createTeam, getTeam } from "@formbricks/lib/team/service";
import { createUser, updateUser } from "@formbricks/lib/user/service";
export const POST = async (request: Request) => {
@@ -53,10 +53,10 @@ export const POST = async (request: Request) => {
// create the user
user = await createUser(user);
// User is invited to team
// User is invited to organization
if (isInviteValid) {
// assign user to existing team
await createMembership(invite.teamId, user.id, {
// assign user to existing organization
await createMembership(invite.organizationId, user.id, {
accepted: true,
role: invite.role,
});
@@ -72,24 +72,27 @@ export const POST = async (request: Request) => {
}
// User signs up without invite
// Default team assignment is enabled
if (DEFAULT_TEAM_ID && DEFAULT_TEAM_ID.length > 0) {
// check if team exists
let team = await getTeam(DEFAULT_TEAM_ID);
let isNewTeam = false;
if (!team) {
// create team with id from env
team = await createTeam({ id: DEFAULT_TEAM_ID, name: user.name + "'s Team" });
isNewTeam = true;
// Default organization assignment is enabled
if (DEFAULT_ORGANIZATION_ID && DEFAULT_ORGANIZATION_ID.length > 0) {
// check if organization exists
let organization = await getOrganization(DEFAULT_ORGANIZATION_ID);
let isNewOrganization = false;
if (!organization) {
// create organization with id from env
organization = await createOrganization({
id: DEFAULT_ORGANIZATION_ID,
name: user.name + "'s Organization",
});
isNewOrganization = true;
}
const role = isNewTeam ? "owner" : DEFAULT_TEAM_ROLE || "admin";
await createMembership(team.id, user.id, { role, accepted: true });
const role = isNewOrganization ? "owner" : DEFAULT_ORGANIZATION_ROLE || "admin";
await createMembership(organization.id, user.id, { role, accepted: true });
}
// Without default team assignment
// Without default organization assignment
else {
const team = await createTeam({ name: user.name + "'s Team" });
await createMembership(team.id, user.id, { role: "owner", accepted: true });
const product = await createProduct(team.id, { name: "My Product" });
const organization = await createOrganization({ name: user.name + "'s Organization" });
await createMembership(organization.id, user.id, { role: "owner", accepted: true });
const product = await createProduct(organization.id, { name: "My Product" });
const updatedNotificationSettings = {
...user.notificationSettings,

View File

@@ -49,12 +49,12 @@ export const hasApiEnvironmentAccess = async (apiKey, environmentId) => {
return false;
};
export const hasTeamAccess = async (user, teamId) => {
export const hasOrganizationAccess = async (user, organizationId) => {
const membership = await prisma.membership.findUnique({
where: {
userId_teamId: {
userId_organizationId: {
userId: user.id,
teamId: teamId,
organizationId: organizationId,
},
},
});
@@ -75,12 +75,12 @@ export const getSessionUser = async (req?: NextApiRequest, res?: NextApiResponse
if (session && "user" in session) return session.user;
};
export const isOwner = async (user, teamId) => {
export const isOwner = async (user, organizationId) => {
const membership = await prisma.membership.findUnique({
where: {
userId_teamId: {
userId_organizationId: {
userId: user.id,
teamId: teamId,
organizationId: organizationId,
},
},
});
@@ -90,12 +90,12 @@ export const isOwner = async (user, teamId) => {
return false;
};
export const isAdminOrOwner = async (user, teamId) => {
export const isAdminOrOwner = async (user, organizationId) => {
const membership = await prisma.membership.findUnique({
where: {
userId_teamId: {
userId_organizationId: {
userId: user.id,
teamId: teamId,
organizationId: organizationId,
},
},
});

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