mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
chore: Rename Teams to Organizations (#2656)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
10
.env.example
10
.env.example
@@ -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
131
.github/workflows/kamal-deploy.yml
vendored
Normal 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
128
.github/workflows/kamal-setup.yml
vendored
Normal 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
|
||||
@@ -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**.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -51,8 +51,8 @@ These variables are present inside your machine’s 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) | |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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's create a team <span className="text-primary-500">👇</span>
|
||||
Let's create an organization <span className="text-primary-500">👇</span>
|
||||
</h1>
|
||||
<p className="text text-md text-slate-700">
|
||||
We couldn't find a team for you. Please create one
|
||||
We couldn'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>
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
})}
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = () => (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
|
||||
export interface Membership {
|
||||
team: {
|
||||
organization: {
|
||||
id: string;
|
||||
name: string;
|
||||
products: {
|
||||
|
||||
@@ -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's gone, it's gone.</li>
|
||||
</ul>
|
||||
|
||||
@@ -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" }],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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" && (
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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}
|
||||
@@ -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) {
|
||||
@@ -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);
|
||||
};
|
||||
@@ -55,7 +55,7 @@ export const AddMemberModal = ({
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
tabs={tabs}
|
||||
label={"Invite Team Member"}
|
||||
label={"Invite Organization Member"}
|
||||
closeOnOutsideClick={true}
|
||||
/>
|
||||
</>
|
||||
@@ -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 "Admin" 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">
|
||||
@@ -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's gone, it'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>
|
||||
@@ -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 ?? []}
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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"
|
||||
@@ -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" />
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -51,7 +51,7 @@ export const WrongAccountContent = () => {
|
||||
|
||||
export const RightAccountContent = () => {
|
||||
return (
|
||||
<ContentLayout headline="You’re in 🎉" description="Welcome to the team.">
|
||||
<ContentLayout headline="You’re in 🎉" description="Welcome to the organization.">
|
||||
<Button variant="darkCTA" href="/">
|
||||
Go to app
|
||||
</Button>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user