mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-05 21:32:02 -06:00
feat: move env vars from build-time to run-time to better support self-hosting environments (#789)
* feat: privacy, imprint, and terms URL env vars now do not need rebuilding * feat: disable_singup env var now do not need rebuilding * feat: password_reset_disabled env var now do not need rebuilding * feat: email_verification_disabled env var now do not need rebuilding * feat: github_oauth & google_oauth env var now do not need rebuilding * feat: move logic of env vars to serverside and send boolean client-side * feat: invite_disabled env var now do not need rebuilding * feat: rename vars logically * feat: migration guide * feat: update docker-compose as per v1.1 * deprecate: unused NEXT_PUBLIC_VERCEL_URL & VERCEL_URL * deprecate: unused RAILWAY_STATIC_URL * deprecate: unused RENDER_EXTERNAL_URL * deprecate: unused HEROKU_APP_NAME * fix: define WEBAPP_URL & replace NEXT_WEBAPP_URL with it * migrate: NEXT_PUBLIC_IS_FORMBRICKS_CLOUD to IS_FORMBRICKS_CLOUD * chore: move all env parsing to a constants.ts from page files * feat: migrate client side envs to server side * redo: isFormbricksCloud to navbar serverside page * fix: constants is now a server only file * fix: removal of use swr underway * fix: move 1 tag away from swr to service * feat: move away from tags swr * feat: move away from surveys swr * feat: move away from eventClass swr * feat: move away from event swr * fix: make constants server-only * remove comments from .env.example, use constants in MetaInformation * clean up services * rename tag function * fix build error * fix smaller bugs, fix Response % not working in summary --------- Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
3c2087452c
commit
13afba7615
20
.env.docker
20
.env.docker
@@ -7,7 +7,7 @@
|
||||
# BASICS #
|
||||
############
|
||||
|
||||
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||
WEBAPP_URL=http://localhost:3000
|
||||
|
||||
##############
|
||||
# DATABASE #
|
||||
@@ -63,25 +63,25 @@ NEXTAUTH_URL=http://localhost:3000
|
||||
#####################
|
||||
|
||||
# Email Verification. If you enable Email Verification you have to setup SMTP-Settings, too.
|
||||
NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1
|
||||
EMAIL_VERIFICATION_DISABLED=1
|
||||
|
||||
# Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too.
|
||||
NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1
|
||||
PASSWORD_RESET_DISABLED=1
|
||||
|
||||
# Signup. Disable the ability for new users to create an account.
|
||||
# NEXT_PUBLIC_SIGNUP_DISABLED=1
|
||||
# SIGNUP_DISABLED=1
|
||||
|
||||
# Team Invite. Disable the ability for invited users to create an account.
|
||||
# NEXT_PUBLIC_INVITE_DISABLED=1
|
||||
# INVITE_DISABLED=1
|
||||
|
||||
##########
|
||||
# Other #
|
||||
##########
|
||||
|
||||
# Display privacy policy, imprint and terms of service links in the footer of signup & public pages.
|
||||
NEXT_PUBLIC_PRIVACY_URL=
|
||||
NEXT_PUBLIC_TERMS_URL=
|
||||
NEXT_PUBLIC_IMPRINT_URL=
|
||||
PRIVACY_URL=
|
||||
TERMS_URL=
|
||||
IMPRINT_URL=
|
||||
|
||||
# Disable Sentry warning
|
||||
SENTRY_IGNORE_API_RESOLUTION_ERROR=1
|
||||
@@ -90,12 +90,12 @@ SENTRY_IGNORE_API_RESOLUTION_ERROR=1
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
|
||||
# Configure Github Login
|
||||
NEXT_PUBLIC_GITHUB_AUTH_ENABLED=0
|
||||
GITHUB_AUTH_ENABLED=0
|
||||
GITHUB_ID=
|
||||
GITHUB_SECRET=
|
||||
|
||||
# Configure Google Login
|
||||
NEXT_PUBLIC_GOOGLE_AUTH_ENABLED=0
|
||||
GOOGLE_AUTH_ENABLED=0
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
|
||||
24
.env.example
24
.env.example
@@ -8,7 +8,7 @@
|
||||
# BASICS #
|
||||
############
|
||||
|
||||
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||
WEBAPP_URL=http://localhost:3000
|
||||
|
||||
##############
|
||||
# DATABASE #
|
||||
@@ -20,6 +20,7 @@ DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=pu
|
||||
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
||||
# PRISMA_GENERATE_DATAPROXY=true
|
||||
# i dont think we use it so ask Matti and remove it
|
||||
PRISMA_GENERATE_DATAPROXY=
|
||||
|
||||
###############
|
||||
@@ -34,9 +35,6 @@ NEXTAUTH_SECRET=RANDOM_STRING
|
||||
# You do not need the NEXTAUTH_URL environment variable in Vercel.
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# If you encounter NEXT_AUTH URL problems this should always be localhost:3000 (or whatever port your app is running on)
|
||||
# NEXTAUTH_URL_INTERNAL=http://localhost:3000
|
||||
|
||||
################
|
||||
# MAIL SETUP #
|
||||
################
|
||||
@@ -64,33 +62,33 @@ SMTP_PASSWORD=smtpPassword
|
||||
#####################
|
||||
|
||||
# Email Verification. If you enable Email Verification you have to setup SMTP-Settings, too.
|
||||
# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1
|
||||
# EMAIL_VERIFICATION_DISABLED=1
|
||||
|
||||
# Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too.
|
||||
# NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1
|
||||
# PASSWORD_RESET_DISABLED=1
|
||||
|
||||
# Signup. Disable the ability for new users to create an account.
|
||||
# NEXT_PUBLIC_SIGNUP_DISABLED=1
|
||||
# SIGNUP_DISABLED=1
|
||||
|
||||
# Team Invite. Disable the ability for invited users to create an account.
|
||||
# NEXT_PUBLIC_INVITE_DISABLED=1
|
||||
# INVITE_DISABLED=1
|
||||
|
||||
##########
|
||||
# Other #
|
||||
##########
|
||||
|
||||
# Display privacy policy, imprint and terms of service links in the footer of signup & public pages.
|
||||
NEXT_PUBLIC_PRIVACY_URL=
|
||||
NEXT_PUBLIC_TERMS_URL=
|
||||
NEXT_PUBLIC_IMPRINT_URL=
|
||||
PRIVACY_URL=
|
||||
TERMS_URL=
|
||||
IMPRINT_URL=
|
||||
|
||||
# Configure Github Login
|
||||
NEXT_PUBLIC_GITHUB_AUTH_ENABLED=0
|
||||
GITHUB_AUTH_ENABLED=0
|
||||
GITHUB_ID=
|
||||
GITHUB_SECRET=
|
||||
|
||||
# Configure Google Login
|
||||
NEXT_PUBLIC_GOOGLE_AUTH_ENABLED=0
|
||||
GOOGLE_AUTH_ENABLED=0
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ tasks:
|
||||
command: |
|
||||
gp sync-await init &&
|
||||
cp .env.example .env &&
|
||||
sed -i -r "s#^(NEXT_PUBLIC_WEBAPP_URL=).*#\1 $(gp url 3000)#" .env &&
|
||||
sed -i -r "s#^(WEBAPP_URL=).*#\1 $(gp url 3000)#" .env &&
|
||||
sed -i -r "s#^(NEXTAUTH_URL=).*#\1 $(gp url 3000)#" .env &&
|
||||
turbo --filter "@formbricks/web" go
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Ensure `docker` & `docker compose` are installed on your server/system. Both are
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
2. **Modify the `.env.docker` file as required by your setup.** <br/> This file comes with a basic setup and Formbricks works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings. If you configured your email credentials, you can also comment the following lines to enable email verification (`# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1`)
|
||||
2. **Modify the `.env.docker` file as required by your setup.** <br/> This file comes with a basic setup and Formbricks works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings. If you configured your email credentials, you can also comment the following lines to enable email verification (`# EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# PASSWORD_RESET_DISABLED=1`)
|
||||
|
||||
<Note>
|
||||
## Editing a NEXT_PUBLIC_* variable?
|
||||
@@ -50,67 +50,64 @@ Ensure `docker` & `docker compose` are installed on your server/system. Both are
|
||||
|
||||
You can now access the app on [http://localhost:3000](http://localhost:3000). You will be automatically redirected to the login. To use your local installation of Formbricks, create a new account.
|
||||
|
||||
### Important Run-time Variables
|
||||
## Important Run-time Variables
|
||||
|
||||
These variables must also be provided at runtime.
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------- | --- |
|
||||
| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` |
|
||||
| DATABASE_URL | Database URL with credentials. | required | `postgresql://postgres:postgres@postgres:5432/formbricks?schema=public` |
|
||||
| PRISMA_GENERATE_DATAPROXY | Enables a dedicated connection pool for Prisma using Prisma Data Proxy. Uncomment to enable. | optional | |
|
||||
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |
|
||||
| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` |
|
||||
| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to `1` else to `0`. | optional (required if email services are to be enabled) | |
|
||||
| GITHUB_ID | Client ID for GitHub. | optional (required if GitHub auth is enabled) | |
|
||||
| GITHUB_SECRET | Secret for GitHub. | optional (required if GitHub auth is enabled) | |
|
||||
| GOOGLE_CLIENT_ID | Client ID for Google. | optional (required if Google auth is enabled) | |
|
||||
| GOOGLE_CLIENT_SECRET | Secret for Google. | optional (required if Google auth is enabled) | |
|
||||
| CRON_SECRET | API Secret for running cron jobs. | optional | |
|
||||
| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | |
|
||||
| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | |
|
||||
| TELEMETRY_DISABLED | Disables telemetry if set to `1`. | optional | |
|
||||
| INSTANCE_ID | Instance ID for Formbricks Cloud to be sent to Telemetry. | optional | |
|
||||
| INTERNAL_SECRET | Internal Secret (Currently we do not use this anywhere). | optional | |
|
||||
| RENDER_EXTERNAL_URL | External URL for rendering (used instead of WEBAPP_URL). | optional | |
|
||||
| HEROKU_APP_NAME | Heroku app name (used instead of WEBAPP_URL). | optional | |
|
||||
| RAILWAY_STATIC_URL | Railway static URL (used instead of WEBAPP_URL). | optional | | |
|
||||
| Variable | Description | Required | Default |
|
||||
| --------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` |
|
||||
| SURVEY_BASE_URL | Base URL of the link surveys. | required | `http://localhost:3000/s/` |
|
||||
| DATABASE_URL | Database URL with credentials. | required | `postgresql://postgres:postgres@postgres:5432/formbricks?schema=public` |
|
||||
| PRISMA_GENERATE_DATAPROXY | Enables a dedicated connection pool for Prisma using Prisma Data Proxy. Uncomment to enable. | optional | |
|
||||
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |
|
||||
| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` |
|
||||
| PRIVACY_URL | URL for privacy policy. | optional | |
|
||||
| TERMS_URL | URL for terms of service. | optional | |
|
||||
| IMPRINT_URL | URL for imprint. | optional | |
|
||||
| SIGNUP_DISABLED | Disables the ability for new users to create an account if set to `1`. | optional | |
|
||||
| PASSWORD_RESET_DISABLED | Disables password reset functionality if set to `1`. | optional | |
|
||||
| EMAIL_VERIFICATION_DISABLED | Disables email verification if set to `1`. | optional | |
|
||||
| INVITE_DISABLED | Disables the ability for invited users to create an account if set to `1`. | optional | |
|
||||
| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to `1` else to `0`. | optional (required if email services are to be enabled) | |
|
||||
| GITHUB_AUTH_ENABLED | Enables GitHub login if set to `1`. | optional | |
|
||||
| GOOGLE_AUTH_ENABLED | Enables Google login if set to `1`. | optional | |
|
||||
| GITHUB_ID | Client ID for GitHub. | optional (required if GitHub auth is enabled) | |
|
||||
| GITHUB_SECRET | Secret for GitHub. | optional (required if GitHub auth is enabled) | |
|
||||
| GOOGLE_CLIENT_ID | Client ID for Google. | optional (required if Google auth is enabled) | |
|
||||
| GOOGLE_CLIENT_SECRET | Secret for Google. | optional (required if Google auth is enabled) | |
|
||||
| CRON_SECRET | API Secret for running cron jobs. | optional | |
|
||||
| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | |
|
||||
| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | |
|
||||
| TELEMETRY_DISABLED | Disables telemetry if set to `1`. | optional | |
|
||||
| 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 | |
|
||||
| IS_FORMBRICKS_CLOUD | Uses Formbricks Cloud if set to `1` | optional | |
|
||||
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | `#64748b` |
|
||||
|
||||
### Build-time Variables
|
||||
## Build-time Variables
|
||||
|
||||
These variables must be provided at the time of the docker build and can be provided by updating the `.env` file.
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
| -------------------------------------------------- | -------------------------------------------------------------------------- | -------- | ----------------------- |
|
||||
| NEXT_PUBLIC_WEBAPP_URL | Base URL injected into static files. | required | `http://localhost:3000` |
|
||||
| NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED | Disables email verification if set to `1`. | optional | |
|
||||
| NEXT_PUBLIC_PASSWORD_RESET_DISABLED | Disables password reset functionality if set to `1`. | optional | |
|
||||
| NEXT_PUBLIC_SIGNUP_DISABLED | Disables the ability for new users to create an account if set to `1`. | optional | |
|
||||
| NEXT_PUBLIC_INVITE_DISABLED | Disables the ability for invited users to create an account if set to `1`. | optional | |
|
||||
| NEXT_PUBLIC_PRIVACY_URL | URL for privacy policy. | optional | |
|
||||
| NEXT_PUBLIC_TERMS_URL | URL for terms of service. | optional | |
|
||||
| NEXT_PUBLIC_IMPRINT_URL | URL for imprint. | optional | |
|
||||
| NEXT_PUBLIC_GITHUB_AUTH_ENABLED | Enables GitHub login if set to `1`. | optional | |
|
||||
| NEXT_PUBLIC_GOOGLE_AUTH_ENABLED | Enables Google login if set to `1`. | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_API_HOST | Host for the Formbricks API. | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID | If you already have a preset environment ID of the environment. | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID | If you already have an onboarding survey ID to show new users. | optional | |
|
||||
| NEXT_PUBLIC_IS_FORMBRICKS_CLOUD | Uses Formbricks Cloud if set to `1` | optional | |
|
||||
| NEXT_PUBLIC_POSTHOG_API_KEY | API key for PostHog analytics. | optional | |
|
||||
| NEXT_PUBLIC_POSTHOG_API_HOST | Host for PostHog analytics. | optional | |
|
||||
| NEXT_PUBLIC_SENTRY_DSN | DSN for Sentry error tracking. | optional | |
|
||||
| NEXT_PUBLIC_DOCSEARCH_APP_ID | ID of the DocSearch application. | optional | |
|
||||
| NEXT_PUBLIC_DOCSEARCH_API_KEY | API key of the DocSearch application. | optional | |
|
||||
| NEXT_PUBLIC_DOCSEARCH_INDEX_NAME | Name of the DocSearch index. | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID | Environment ID for Formbricks (if you already have one) | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID | Survey ID for the feedback survey on the docs site. | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_COM_API_HOST | Host for the Formbricks API. | optional | |
|
||||
| NEXT_PUBLIC_VERCEL_URL | Vercel URL (used instead of WEBAPP_URL). | optional |
|
||||
| Variable | Description | Required | Default |
|
||||
| -------------------------------------------------- | --------------------------------------------------------------- | -------- | ------- |
|
||||
| NEXT_PUBLIC_FORMBRICKS_API_HOST | Host for the Formbricks API. | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID | If you already have a preset environment ID of the environment. | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID | If you already have an onboarding survey ID to show new users. | optional | |
|
||||
| NEXT_PUBLIC_POSTHOG_API_KEY | API key for PostHog analytics. | optional | |
|
||||
| NEXT_PUBLIC_POSTHOG_API_HOST | Host for PostHog analytics. | optional | |
|
||||
| NEXT_PUBLIC_SENTRY_DSN | DSN for Sentry error tracking. | optional | |
|
||||
| NEXT_PUBLIC_DOCSEARCH_APP_ID | ID of the DocSearch application. | optional | |
|
||||
| NEXT_PUBLIC_DOCSEARCH_API_KEY | API key of the DocSearch application. | optional | |
|
||||
| NEXT_PUBLIC_DOCSEARCH_INDEX_NAME | Name of the DocSearch index. | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID | Environment ID for Formbricks (if you already have one) | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID | Survey ID for the feedback survey on the docs site. | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_COM_API_HOST | Host for the Formbricks API. | optional | |
|
||||
|
||||
## Debugging
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
export const meta = {
|
||||
title: "Migrating Formbricks to v1.1",
|
||||
description:
|
||||
"Formbricks v1.1 comes with an amazing set of features including the ability to define most environment variables at runtime itself! No need to build the image again! This guide will help you migrate your existing Formbricks instance to v1.1",
|
||||
};
|
||||
|
||||
#### Self-Hosting
|
||||
|
||||
# Migrating to v1.1
|
||||
|
||||
Formbricks v1.1 includes a lot of new features and improvements. However, it also comes with a few breaking changes specifically with the environment variables. This guide will help you migrate your existing Formbricks instance to v1.1 without losing any data.
|
||||
|
||||
## Changes in .env
|
||||
|
||||
### Renamed Environment Variables
|
||||
This was introduced because we got a lot of requests from our users for the ability to define some common environment variables at runtime itself i.e. without having to rebuild the image for the changes to take effect.
|
||||
This is now possible with v1.1. However, due to Next.JS best practices, we had to deprecate the prefix **NEXT_PUBLIC_** in the following environment variables:
|
||||
|
||||
| till v1.0 | v1.1 |
|
||||
| ------------------------------------------- | --------------------------- |
|
||||
| **NEXT_PUBLIC_**EMAIL_VERIFICATION_DISABLED | EMAIL_VERIFICATION_DISABLED |
|
||||
| **NEXT_PUBLIC_**PASSWORD_RESET_DISABLED | PASSWORD_RESET_DISABLED |
|
||||
| **NEXT_PUBLIC_**SIGNUP_DISABLED | SIGNUP_DISABLED |
|
||||
| **NEXT_PUBLIC_**INVITE_DISABLED | INVITE_DISABLED |
|
||||
| **NEXT_PUBLIC_**PRIVACY_URL | PRIVACY_URL |
|
||||
| **NEXT_PUBLIC_**TERMS_URL | TERMS_URL |
|
||||
| **NEXT_PUBLIC_**IMPRINT_URL | IMPRINT_URL |
|
||||
| **NEXT_PUBLIC_**GITHUB_AUTH_ENABLED | GITHUB_AUTH_ENABLED |
|
||||
| **NEXT_PUBLIC_**GOOGLE_AUTH_ENABLED | GOOGLE_AUTH_ENABLED |
|
||||
| **NEXT_PUBLIC_**WEBAPP_URL | WEBAPP_URL |
|
||||
| **NEXT_PUBLIC_**IS_FORMBRICKS_CLOUD | IS_FORMBRICKS_CLOUD |
|
||||
| **NEXT_PUBLIC_**SURVEY_BASE_URL | SURVEY_BASE_URL |
|
||||
|
||||
<Note>
|
||||
Please note that their values and the logic remains exactly the same. Only the prefix has been deprecated. The other environment variables remain the same as well.
|
||||
</Note>
|
||||
|
||||
### Deprecated Environment Variables
|
||||
|
||||
- **NEXT_PUBLIC_VERCEL_URL**: Was used as Vercel URL (used instead of WEBAPP_URL), but from v1.1, you can just set the WEBAPP_URL environment variable to your Vercel URL.
|
||||
- **RAILWAY_STATIC_URL**: Was used as Railway Static URL (used instead of WEBAPP_URL), but from v1.1, you can just set the WEBAPP_URL environment variable.
|
||||
- **RENDER_EXTERNAL_URL**: Was used as an external URL to Render (used instead of WEBAPP_URL), but from v1.1, you can just set the WEBAPP_URL environment variable.
|
||||
- **HEROKU_APP_NAME**: Was used to build the App name on a Heroku hosted webapp, but from v1.1, you can just set the WEBAPP_URL environment variable.
|
||||
- **NEXT_PUBLIC_WEBAPP_URL**: Was used for the same purpose as WEBAPP_URL, but from v1.1, you can just set the WEBAPP_URL environment variable.
|
||||
- **PRISMA_GENERATE_DATAPROXY**: Was used to tell Prisma that it should generate the runtime for Dataproxy usage. But its officially deprecated now.
|
||||
|
||||
## Helper Shell Script
|
||||
For a seamless migration, below is a shell script for your self-hosted instance that will automatically update your environment variables to be compliant with the new naming conventions.
|
||||
|
||||
### Building From Source
|
||||
The below script will:
|
||||
1. Create a backup of your existing .env file as `.env.old`
|
||||
2. Update the .env file to be compliant with the new naming conventions
|
||||
|
||||
<CodeGroup title="Run the below in your terminal in the directory of your .env">
|
||||
```shell {{ title: '.env file' }}
|
||||
for var in NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED NEXT_PUBLIC_PASSWORD_RESET_DISABLED NEXT_PUBLIC_SIGNUP_DISABLED NEXT_PUBLIC_INVITE_DISABLED NEXT_PUBLIC_PRIVACY_URL NEXT_PUBLIC_TERMS_URL NEXT_PUBLIC_IMPRINT_URL NEXT_PUBLIC_GITHUB_AUTH_ENABLED NEXT_PUBLIC_GOOGLE_AUTH_ENABLED NEXT_PUBLIC_WEBAPP_URL NEXT_PUBLIC_IS_FORMBRICKS_CLOUD NEXT_PUBLIC_SURVEY_BASE_URL; do sed -i.old "s/^$var=/$(echo $var | sed 's/NEXT_PUBLIC_//')=/" .env; done; echo "Formbricks environment variables have been migrated as per v1.1! You are good to go."
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
### Docker & Single Script Setup
|
||||
|
||||
Now that these variables can be defined at runtime, you can append them inside your `x-environment` in the `docker-compose.yml` itself.
|
||||
For a more detailed guide on these environment variables, please refer to the [Important Runtime Variables](/docs/self-hosting/from-source#important-run-time-variables) section.
|
||||
|
||||
<CodeGroup title="docker-compose.yml">
|
||||
```yaml {{ title: 'docker-compose.yml' }}
|
||||
version: "3.3"
|
||||
x-environment: &environment
|
||||
environment:
|
||||
# The url of your Formbricks instance used in the admin panel
|
||||
WEBAPP_URL:
|
||||
|
||||
# PostgreSQL DB for Formbricks to connect to
|
||||
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/formbricks?schema=public"
|
||||
|
||||
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
||||
# PRISMA_GENERATE_DATAPROXY=true
|
||||
PRISMA_GENERATE_DATAPROXY:
|
||||
|
||||
# NextJS Auth
|
||||
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
||||
# You can use: `openssl rand -base64 32` to generate one
|
||||
NEXTAUTH_SECRET:
|
||||
|
||||
# Set this to your public-facing URL, e.g., https://example.com
|
||||
# You do not need the NEXTAUTH_URL environment variable in Vercel.
|
||||
NEXTAUTH_URL: http://localhost:3000
|
||||
|
||||
# PostgreSQL password
|
||||
POSTGRES_PASSWORD: postgres
|
||||
|
||||
# Email Configuration
|
||||
MAIL_FROM:
|
||||
SMTP_HOST:
|
||||
SMTP_PORT:
|
||||
SMTP_SECURE_ENABLED:
|
||||
SMTP_USER:
|
||||
SMTP_PASSWORD:
|
||||
|
||||
# Uncomment the below and set it to 1 to disable Email Verification for new signups
|
||||
# EMAIL_VERIFICATION_DISABLED:
|
||||
|
||||
# Uncomment the below and set it to 1 to disable Password Reset
|
||||
# PASSWORD_RESET_DISABLED:
|
||||
|
||||
# Uncomment the below and set it to 1 to disable Signups
|
||||
# SIGNUP_DISABLED:
|
||||
|
||||
# Uncomment the below and set it to 1 to disable Invites
|
||||
# INVITE_DISABLED:
|
||||
|
||||
# Uncomment the below and set a value to have your own Privacy Page URL on the signup & login page
|
||||
# PRIVACY_URL:
|
||||
|
||||
# Uncomment the below and set a value to have your own Terms Page URL on the auth and the surveys page
|
||||
# TERMS_URL:
|
||||
|
||||
# Uncomment the below and set a value to have your own Imprint Page URL on the auth and the surveys page
|
||||
# IMPRINT_URL:
|
||||
|
||||
# Uncomment the below and set to 1 if you want to enable GitHub OAuth
|
||||
# GITHUB_AUTH_ENABLED:
|
||||
# GITHUB_ID:
|
||||
# GITHUB_SECRET:
|
||||
|
||||
# Uncomment the below and set to 1 if you want to enable Google OAuth
|
||||
# GOOGLE_AUTH_ENABLED:
|
||||
# GOOGLE_CLIENT_ID:
|
||||
# GOOGLE_CLIENT_SECRET:
|
||||
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Did we miss something? Are you still facing issues migrating your app? [Join our Discord!](https://formbricks.com/discord) We'd be happy to help!
|
||||
@@ -246,6 +246,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Production", href: "/docs/self-hosting/production" },
|
||||
{ title: "Docker", href: "/docs/self-hosting/docker" },
|
||||
{ title: "From Source", href: "/docs/self-hosting/from-source" },
|
||||
{ title: "Migration to v1.1", href: "/docs/self-hosting/migrating-to-1.1" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -22,5 +22,5 @@ export default function ActionsAttributesTabs({ activeId, environmentId }: Actio
|
||||
},
|
||||
];
|
||||
|
||||
return <SecondNavbar tabs={tabs} activeId={activeId} />;
|
||||
return <SecondNavbar tabs={tabs} activeId={activeId} environmentId={environmentId} />;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { ErrorComponent } from "@formbricks/ui";
|
||||
import { Label } from "@formbricks/ui";
|
||||
import { useEventClass } from "@/lib/eventClasses/eventClasses";
|
||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@/lib/utils";
|
||||
import { CodeBracketIcon, CursorArrowRaysIcon, SparklesIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
import { TActionClass } from "@formbricks/types/v1/actionClasses";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
getActionCountInLastHourAction,
|
||||
getActionCountInLast24HoursAction,
|
||||
getActionCountInLast7DaysAction,
|
||||
GetActiveInactiveSurveysAction,
|
||||
} from "./actions";
|
||||
interface ActivityTabProps {
|
||||
environmentId: string;
|
||||
actionClassId: string;
|
||||
actionClass: TActionClass;
|
||||
}
|
||||
|
||||
export default function EventActivityTab({ environmentId, actionClassId }: ActivityTabProps) {
|
||||
const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, actionClassId);
|
||||
export default function EventActivityTab({ actionClass }: ActivityTabProps) {
|
||||
// const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, actionClass.id);
|
||||
|
||||
if (isLoadingEventClass) return <LoadingSpinner />;
|
||||
if (isErrorEventClass) return <ErrorComponent />;
|
||||
const [numEventsLastHour, setNumEventsLastHour] = useState<number | undefined>();
|
||||
const [numEventsLast24Hours, setNumEventsLast24Hours] = useState<number | undefined>();
|
||||
const [numEventsLast7Days, setNumEventsLast7Days] = useState<number | undefined>();
|
||||
const [activeSurveys, setActiveSurveys] = useState<string[] | undefined>();
|
||||
const [inactiveSurveys, setInactiveSurveys] = useState<string[] | undefined>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
updateState();
|
||||
|
||||
async function updateState() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [
|
||||
numEventsLastHourData,
|
||||
numEventsLast24HoursData,
|
||||
numEventsLast7DaysData,
|
||||
activeInactiveSurveys,
|
||||
] = await Promise.all([
|
||||
getActionCountInLastHourAction(actionClass.id),
|
||||
getActionCountInLast24HoursAction(actionClass.id),
|
||||
getActionCountInLast7DaysAction(actionClass.id),
|
||||
GetActiveInactiveSurveysAction(actionClass.id),
|
||||
]);
|
||||
setNumEventsLastHour(numEventsLastHourData);
|
||||
setNumEventsLast24Hours(numEventsLast24HoursData);
|
||||
setNumEventsLast7Days(numEventsLast7DaysData);
|
||||
setActiveSurveys(activeInactiveSurveys.activeSurveys);
|
||||
setInactiveSurveys(activeInactiveSurveys.inactiveSurveys);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [actionClass.id]);
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <ErrorComponent />;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 pb-2">
|
||||
@@ -24,15 +70,15 @@ export default function EventActivityTab({ environmentId, actionClassId }: Activ
|
||||
<Label className="text-slate-500">Ocurrances</Label>
|
||||
<div className="mt-1 grid w-fit grid-cols-3 rounded-lg border-slate-100 bg-slate-50">
|
||||
<div className="border-r border-slate-200 px-4 py-2 text-center">
|
||||
<p className="font-bold text-slate-800">{eventClass.numEventsLastHour}</p>
|
||||
<p className="font-bold text-slate-800">{numEventsLastHour}</p>
|
||||
<p className="text-xs text-slate-500">last hour</p>
|
||||
</div>
|
||||
<div className="border-r border-slate-200 px-4 py-2 text-center">
|
||||
<p className="font-bold text-slate-800">{eventClass.numEventsLast24Hours}</p>
|
||||
<p className="font-bold text-slate-800">{numEventsLast24Hours}</p>
|
||||
<p className="text-xs text-slate-500">last 24 hours</p>
|
||||
</div>
|
||||
<div className="px-4 py-2 text-center">
|
||||
<p className="font-bold text-slate-800">{eventClass.numEventsLast7Days}</p>
|
||||
<p className="font-bold text-slate-800">{numEventsLast7Days}</p>
|
||||
<p className="text-xs text-slate-500">last week</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,8 +86,8 @@ export default function EventActivityTab({ environmentId, actionClassId }: Activ
|
||||
|
||||
<div>
|
||||
<Label className="text-slate-500">Active surveys</Label>
|
||||
{eventClass.activeSurveys.length === 0 && <p className="text-sm text-slate-900">-</p>}
|
||||
{eventClass.activeSurveys.map((surveyName) => (
|
||||
{activeSurveys?.length === 0 && <p className="text-sm text-slate-900">-</p>}
|
||||
{activeSurveys?.map((surveyName) => (
|
||||
<p key={surveyName} className="text-sm text-slate-900">
|
||||
{surveyName}
|
||||
</p>
|
||||
@@ -49,8 +95,8 @@ export default function EventActivityTab({ environmentId, actionClassId }: Activ
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500">Inactive surveys</Label>
|
||||
{eventClass.inactiveSurveys.length === 0 && <p className="text-sm text-slate-900">-</p>}
|
||||
{eventClass.inactiveSurveys.map((surveyName) => (
|
||||
{inactiveSurveys?.length === 0 && <p className="text-sm text-slate-900">-</p>}
|
||||
{inactiveSurveys?.map((surveyName) => (
|
||||
<p key={surveyName} className="text-sm text-slate-900">
|
||||
{surveyName}
|
||||
</p>
|
||||
@@ -61,28 +107,28 @@ export default function EventActivityTab({ environmentId, actionClassId }: Activ
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">Created on</Label>
|
||||
<p className=" text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(eventClass.createdAt?.toString())}
|
||||
{convertDateTimeStringShort(actionClass.createdAt?.toString())}
|
||||
</p>
|
||||
</div>{" "}
|
||||
<div>
|
||||
<Label className=" text-xs font-normal text-slate-500">Last updated</Label>
|
||||
<p className=" text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(eventClass.updatedAt?.toString())}
|
||||
{convertDateTimeStringShort(actionClass.updatedAt?.toString())}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="block text-xs font-normal text-slate-500">Type</Label>
|
||||
<div className="mt-1 flex items-center">
|
||||
<div className="mr-1.5 h-4 w-4 text-slate-600">
|
||||
{eventClass.type === "code" ? (
|
||||
{actionClass.type === "code" ? (
|
||||
<CodeBracketIcon />
|
||||
) : eventClass.type === "noCode" ? (
|
||||
) : actionClass.type === "noCode" ? (
|
||||
<CursorArrowRaysIcon />
|
||||
) : eventClass.type === "automatic" ? (
|
||||
) : actionClass.type === "automatic" ? (
|
||||
<SparklesIcon />
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 ">{capitalizeFirstLetter(eventClass.type)}</p>
|
||||
<p className="text-sm text-slate-700 ">{capitalizeFirstLetter(actionClass.type)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function ActionDetailModal({
|
||||
const tabs = [
|
||||
{
|
||||
title: "Activity",
|
||||
children: <EventActivityTab environmentId={environmentId} actionClassId={actionClass.id} />,
|
||||
children: <EventActivityTab actionClass={actionClass} />,
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"use server";
|
||||
|
||||
import {
|
||||
getActionCountInLast24Hours,
|
||||
getActionCountInLast7Days,
|
||||
getActionCountInLastHour,
|
||||
} from "@formbricks/lib/services/actions";
|
||||
import { getSurveysByActionClassId } from "@formbricks/lib/services/survey";
|
||||
|
||||
export const getActionCountInLastHourAction = async (actionClassId: string) => {
|
||||
return await getActionCountInLastHour(actionClassId);
|
||||
};
|
||||
|
||||
export const getActionCountInLast24HoursAction = async (actionClassId: string) => {
|
||||
return await getActionCountInLast24Hours(actionClassId);
|
||||
};
|
||||
|
||||
export const getActionCountInLast7DaysAction = async (actionClassId: string) => {
|
||||
return await getActionCountInLast7Days(actionClassId);
|
||||
};
|
||||
|
||||
export const GetActiveInactiveSurveysAction = async (
|
||||
actionClassId: string
|
||||
): Promise<{ activeSurveys: string[]; inactiveSurveys: string[] }> => {
|
||||
const surveys = await getSurveysByActionClassId(actionClassId);
|
||||
const response = {
|
||||
activeSurveys: surveys.filter((s) => s.status === "inProgress").map((survey) => survey.name),
|
||||
inactiveSurveys: surveys.filter((s) => s.status !== "inProgress").map((survey) => survey.name),
|
||||
};
|
||||
return response;
|
||||
};
|
||||
@@ -1,31 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { GetActiveInactiveSurveysAction } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions";
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { useAttributeClass } from "@/lib/attributeClasses/attributeClasses";
|
||||
import { capitalizeFirstLetter } from "@/lib/utils";
|
||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
||||
import { TAttributeClass } from "@formbricks/types/v1/attributeClasses";
|
||||
import { ErrorComponent, Label } from "@formbricks/ui";
|
||||
import { TagIcon } from "@heroicons/react/24/solid";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface EventActivityTabProps {
|
||||
attributeClassId: string;
|
||||
environmentId: string;
|
||||
attributeClass: TAttributeClass;
|
||||
}
|
||||
|
||||
export default function AttributeActivityTab({ environmentId, attributeClassId }: EventActivityTabProps) {
|
||||
const { attributeClass, isLoadingAttributeClass, isErrorAttributeClass } = useAttributeClass(
|
||||
environmentId,
|
||||
attributeClassId
|
||||
);
|
||||
export default function AttributeActivityTab({ attributeClass }: EventActivityTabProps) {
|
||||
const [activeSurveys, setActiveSurveys] = useState<string[] | undefined>();
|
||||
const [inactiveSurveys, setInactiveSurveys] = useState<string[] | undefined>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
if (isLoadingAttributeClass) return <LoadingSpinner />;
|
||||
if (isErrorAttributeClass) return <ErrorComponent />;
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
||||
getSurveys();
|
||||
|
||||
async function getSurveys() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const activeInactive = await GetActiveInactiveSurveysAction(attributeClass.id);
|
||||
setActiveSurveys(activeInactive.activeSurveys);
|
||||
setInactiveSurveys(activeInactive.inactiveSurveys);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [attributeClass.id]);
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <ErrorComponent />;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 pb-2">
|
||||
<div className="col-span-2 space-y-4 pr-6">
|
||||
<div>
|
||||
<Label className="text-slate-500">Active surveys</Label>
|
||||
{attributeClass.activeSurveys.length === 0 && <p className="text-sm text-slate-900">-</p>}
|
||||
{attributeClass.activeSurveys.map((surveyName) => (
|
||||
{activeSurveys?.length === 0 && <p className="text-sm text-slate-900">-</p>}
|
||||
{activeSurveys?.map((surveyName) => (
|
||||
<p key={surveyName} className="text-sm text-slate-900">
|
||||
{surveyName}
|
||||
</p>
|
||||
@@ -33,8 +55,8 @@ export default function AttributeActivityTab({ environmentId, attributeClassId }
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500">Inactive surveys</Label>
|
||||
{attributeClass.inactiveSurveys.length === 0 && <p className="text-sm text-slate-900">-</p>}
|
||||
{attributeClass.inactiveSurveys.map((surveyName) => (
|
||||
{inactiveSurveys?.length === 0 && <p className="text-sm text-slate-900">-</p>}
|
||||
{inactiveSurveys?.map((surveyName) => (
|
||||
<p key={surveyName} className="text-sm text-slate-900">
|
||||
{surveyName}
|
||||
</p>
|
||||
|
||||
@@ -8,11 +8,9 @@ import { useMemo } from "react";
|
||||
import { TAttributeClass } from "@formbricks/types/v1/attributeClasses";
|
||||
|
||||
export default function AttributeClassesTable({
|
||||
environmentId,
|
||||
attributeClasses,
|
||||
children: [TableHeading, howToAddAttributeButton, attributeRows],
|
||||
}: {
|
||||
environmentId: string;
|
||||
attributeClasses: TAttributeClass[];
|
||||
children: [JSX.Element, JSX.Element, JSX.Element[]];
|
||||
}) {
|
||||
@@ -69,7 +67,6 @@ export default function AttributeClassesTable({
|
||||
))}
|
||||
</div>
|
||||
<AttributeDetailModal
|
||||
environmentId={environmentId}
|
||||
open={isAttributeDetailModalOpen}
|
||||
setOpen={setAttributeDetailModalOpen}
|
||||
attributeClass={activeAttributeClass}
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
import ModalWithTabs from "@/components/shared/ModalWithTabs";
|
||||
import { TagIcon } from "@heroicons/react/24/solid";
|
||||
import type { AttributeClass } from "@prisma/client";
|
||||
import AttributeActivityTab from "./AttributeActivityTab";
|
||||
import AttributeSettingsTab from "./AttributeSettingsTab";
|
||||
import { TAttributeClass } from "@formbricks/types/v1/attributeClasses";
|
||||
|
||||
interface AttributeDetailModalProps {
|
||||
environmentId: string;
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
attributeClass: AttributeClass;
|
||||
attributeClass: TAttributeClass;
|
||||
}
|
||||
|
||||
export default function AttributeDetailModal({
|
||||
environmentId,
|
||||
open,
|
||||
setOpen,
|
||||
attributeClass,
|
||||
}: AttributeDetailModalProps) {
|
||||
export default function AttributeDetailModal({ open, setOpen, attributeClass }: AttributeDetailModalProps) {
|
||||
const tabs = [
|
||||
{
|
||||
title: "Activity",
|
||||
children: <AttributeActivityTab environmentId={environmentId} attributeClassId={attributeClass.id} />,
|
||||
children: <AttributeActivityTab attributeClass={attributeClass} />,
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
"use server";
|
||||
|
||||
import { getSurveysByAttributeClassId } from "@formbricks/lib/services/survey";
|
||||
|
||||
export const GetActiveInactiveSurveysAction = async (
|
||||
attributeClassId: string
|
||||
): Promise<{ activeSurveys: string[]; inactiveSurveys: string[] }> => {
|
||||
const surveys = await getSurveysByAttributeClassId(attributeClassId);
|
||||
const response = {
|
||||
activeSurveys: surveys.filter((s) => s.status === "inProgress").map((survey) => survey.name),
|
||||
inactiveSurveys: surveys.filter((s) => s.status !== "inProgress").map((survey) => survey.name),
|
||||
};
|
||||
return response;
|
||||
};
|
||||
@@ -14,9 +14,10 @@ export const metadata: Metadata = {
|
||||
|
||||
export default async function AttributesPage({ params }) {
|
||||
let attributeClasses = await getAttributeClasses(params.environmentId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AttributeClassesTable environmentId={params.environmentId} attributeClasses={attributeClasses}>
|
||||
<AttributeClassesTable attributeClasses={attributeClasses}>
|
||||
<AttributeTableHeading />
|
||||
<HowToAddAttributesButton />
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { createProductAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { createProduct } from "@/lib/products/products";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { PlusCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -19,9 +19,9 @@ export default function AddProductModal({ environmentId, open, setOpen }: AddPro
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register, handleSubmit } = useForm();
|
||||
|
||||
const submitProduct = async (data) => {
|
||||
const submitProduct = async (data: { name: string }) => {
|
||||
setLoading(true);
|
||||
const newEnv = await createProduct(environmentId, data);
|
||||
const newEnv = await createProductAction(environmentId, data.name);
|
||||
router.push(`/environments/${newEnv.id}/`);
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import Navigation from "@/app/(app)/environments/[environmentId]/Navigation";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD, REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/services/environment";
|
||||
import { getProducts } from "@formbricks/lib/services/product";
|
||||
import { getTeamByEnvironmentId, getTeamsByUserId } from "@formbricks/lib/services/team";
|
||||
@@ -11,6 +11,7 @@ import type { Session } from "next-auth";
|
||||
interface EnvironmentsNavbarProps {
|
||||
environmentId: string;
|
||||
session: Session;
|
||||
isFormbricksCloud: boolean;
|
||||
}
|
||||
|
||||
export default async function EnvironmentsNavbar({ environmentId, session }: EnvironmentsNavbarProps) {
|
||||
@@ -41,6 +42,7 @@ export default async function EnvironmentsNavbar({ environmentId, session }: Env
|
||||
products={products}
|
||||
environments={environments}
|
||||
session={session}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import { formbricksLogout } from "@/lib/formbricks";
|
||||
import { capitalizeFirstLetter, truncate } from "@/lib/utils";
|
||||
import formbricks from "@formbricks/js";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { TTeam } from "@formbricks/types/v1/teams";
|
||||
@@ -71,6 +70,7 @@ interface NavigationProps {
|
||||
team: TTeam;
|
||||
products: TProduct[];
|
||||
environments: TEnvironment[];
|
||||
isFormbricksCloud: boolean;
|
||||
}
|
||||
|
||||
export default function Navigation({
|
||||
@@ -80,6 +80,7 @@ export default function Navigation({
|
||||
session,
|
||||
products,
|
||||
environments,
|
||||
isFormbricksCloud,
|
||||
}: NavigationProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -171,7 +172,7 @@ export default function Navigation({
|
||||
icon: CreditCardIcon,
|
||||
label: "Billing & Plan",
|
||||
href: `/environments/${environment.id}/settings/billing`,
|
||||
hidden: !IS_FORMBRICKS_CLOUD,
|
||||
hidden: !isFormbricksCloud,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -453,7 +454,7 @@ export default function Navigation({
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
{IS_FORMBRICKS_CLOUD && (
|
||||
{isFormbricksCloud && (
|
||||
<DropdownMenuItem>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { deleteSurvey, getSurvey } from "@formbricks/lib/services/survey";
|
||||
import { Team } from "@prisma/client";
|
||||
import { Prisma as prismaClient } from "@prisma/client/";
|
||||
import { createProduct } from "@formbricks/lib/services/product";
|
||||
|
||||
export async function createTeam(teamName: string, ownerUserId: string): Promise<Team> {
|
||||
const newTeam = await prisma.team.create({
|
||||
@@ -303,3 +304,10 @@ export async function copyToOtherEnvironmentAction(
|
||||
export const deleteSurveyAction = async (surveyId: string) => {
|
||||
await deleteSurvey(surveyId);
|
||||
};
|
||||
|
||||
export const createProductAction = async (environmentId: string, productName: string) => {
|
||||
const productCreated = await createProduct(environmentId, productName);
|
||||
|
||||
const newEnvironment = productCreated.environments[0];
|
||||
return newEnvironment;
|
||||
};
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function AddIntegrationModal({
|
||||
setSelectedQuestions(questionIds);
|
||||
}
|
||||
}
|
||||
}, [selectedSurvey]);
|
||||
}, [selectedIntegration, selectedSurvey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIntegration) {
|
||||
@@ -85,7 +85,7 @@ export default function AddIntegrationModal({
|
||||
return;
|
||||
}
|
||||
resetForm();
|
||||
}, [selectedIntegration]);
|
||||
}, [selectedIntegration, surveys]);
|
||||
|
||||
const linkSheet = async () => {
|
||||
try {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import GoogleSheetLogo from "@/images/google-sheets-small.png";
|
||||
import FormbricksLogo from "@/images/logo.svg";
|
||||
import { authorize } from "@formbricks/lib/client/google";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
@@ -12,13 +11,14 @@ import { useState } from "react";
|
||||
interface ConnectProps {
|
||||
enabled: boolean;
|
||||
environmentId: string;
|
||||
webAppUrl: string;
|
||||
}
|
||||
|
||||
export default function Connect({ enabled, environmentId }: ConnectProps) {
|
||||
export default function Connect({ enabled, environmentId, webAppUrl }: ConnectProps) {
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const handleGoogleLogin = async () => {
|
||||
setIsConnecting(true);
|
||||
authorize(environmentId, WEBAPP_URL).then((url: string) => {
|
||||
authorize(environmentId, webAppUrl).then((url: string) => {
|
||||
if (url) {
|
||||
window.location.replace(url);
|
||||
}
|
||||
|
||||
@@ -11,21 +11,24 @@ import {
|
||||
} from "@formbricks/types/v1/integrations";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { refreshSheetAction } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/actions";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
|
||||
interface GoogleSheetWrapperProps {
|
||||
enabled: boolean;
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
surveys: TSurvey[];
|
||||
spreadSheetArray: TGoogleSpreadsheet[];
|
||||
googleSheetIntegration: TGoogleSheetIntegration | undefined;
|
||||
webAppUrl: string;
|
||||
}
|
||||
|
||||
export default function GoogleSheetWrapper({
|
||||
enabled,
|
||||
environmentId,
|
||||
environment,
|
||||
surveys,
|
||||
spreadSheetArray,
|
||||
googleSheetIntegration,
|
||||
webAppUrl,
|
||||
}: GoogleSheetWrapperProps) {
|
||||
const [isConnected, setIsConnected] = useState(
|
||||
googleSheetIntegration ? googleSheetIntegration.config?.key : false
|
||||
@@ -37,7 +40,7 @@ export default function GoogleSheetWrapper({
|
||||
>(null);
|
||||
|
||||
const refreshSheet = async () => {
|
||||
const latestSpreadsheets = await refreshSheetAction(environmentId);
|
||||
const latestSpreadsheets = await refreshSheetAction(environment.id);
|
||||
setSpreadsheets(latestSpreadsheets);
|
||||
};
|
||||
|
||||
@@ -46,7 +49,7 @@ export default function GoogleSheetWrapper({
|
||||
{isConnected && googleSheetIntegration ? (
|
||||
<>
|
||||
<AddIntegrationModal
|
||||
environmentId={environmentId}
|
||||
environmentId={environment.id}
|
||||
surveys={surveys}
|
||||
open={isModalOpen}
|
||||
setOpen={setModalOpen}
|
||||
@@ -55,7 +58,7 @@ export default function GoogleSheetWrapper({
|
||||
selectedIntegration={selectedIntegration}
|
||||
/>
|
||||
<Home
|
||||
environmentId={environmentId}
|
||||
environment={environment}
|
||||
googleSheetIntegration={googleSheetIntegration}
|
||||
setOpenAddIntegrationModal={setModalOpen}
|
||||
setIsConnected={setIsConnected}
|
||||
@@ -64,7 +67,7 @@ export default function GoogleSheetWrapper({
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Connect enabled={enabled} environmentId={environmentId} />
|
||||
<Connect enabled={enabled} environmentId={environment.id} webAppUrl={webAppUrl} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,13 +4,14 @@ import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TGoogleSheetIntegration, TGoogleSheetsConfigData } from "@formbricks/types/v1/integrations";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface HomeProps {
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
googleSheetIntegration: TGoogleSheetIntegration;
|
||||
setOpenAddIntegrationModal: (v: boolean) => void;
|
||||
setIsConnected: (v: boolean) => void;
|
||||
@@ -19,7 +20,7 @@ interface HomeProps {
|
||||
}
|
||||
|
||||
export default function Home({
|
||||
environmentId,
|
||||
environment,
|
||||
googleSheetIntegration,
|
||||
setOpenAddIntegrationModal,
|
||||
setIsConnected,
|
||||
@@ -83,7 +84,7 @@ export default function Home({
|
||||
<div className="mt-4 w-full">
|
||||
<EmptySpaceFiller
|
||||
type="table"
|
||||
environmentId={environmentId}
|
||||
environment={environment}
|
||||
noWidgetRequired={true}
|
||||
emptyMessage="Your google sheet integrations will appear here as soon as you add them. ⏲️"
|
||||
/>
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import GoogleSheetWrapper from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/GoogleSheetWrapper";
|
||||
import GoBackButton from "@/components/shared/GoBackButton";
|
||||
import { env } from "@/env.mjs";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getSpreadSheets } from "@formbricks/lib/services/googleSheet";
|
||||
import { getIntegrations } from "@formbricks/lib/services/integrations";
|
||||
import { getSurveys } from "@formbricks/lib/services/survey";
|
||||
import { TGoogleSheetIntegration, TGoogleSpreadsheet } from "@formbricks/types/v1/integrations";
|
||||
import {
|
||||
GOOGLE_SHEETS_CLIENT_ID,
|
||||
WEBAPP_URL,
|
||||
GOOGLE_SHEETS_CLIENT_SECRET,
|
||||
GOOGLE_SHEETS_REDIRECT_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
|
||||
export default async function GoogleSheet({ params }) {
|
||||
const enabled = !!(
|
||||
env.GOOGLE_SHEETS_CLIENT_ID &&
|
||||
env.GOOGLE_SHEETS_CLIENT_SECRET &&
|
||||
env.GOOGLE_SHEETS_REDIRECT_URL
|
||||
);
|
||||
const surveys = await getSurveys(params.environmentId);
|
||||
let spreadSheetArray: TGoogleSpreadsheet[] = [];
|
||||
const integrations = await getIntegrations(params.environmentId);
|
||||
const enabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
|
||||
const [surveys, integrations, environment] = await Promise.all([
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrations(params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
const googleSheetIntegration: TGoogleSheetIntegration | undefined = integrations?.find(
|
||||
(integration): integration is TGoogleSheetIntegration => integration.type === "googleSheets"
|
||||
);
|
||||
let spreadSheetArray: TGoogleSpreadsheet[] = [];
|
||||
if (googleSheetIntegration && googleSheetIntegration.config.key) {
|
||||
spreadSheetArray = await getSpreadSheets(params.environmentId);
|
||||
}
|
||||
@@ -28,10 +36,11 @@ export default async function GoogleSheet({ params }) {
|
||||
<div className="h-[75vh] w-full">
|
||||
<GoogleSheetWrapper
|
||||
enabled={enabled}
|
||||
environmentId={params.environmentId}
|
||||
environment={environment}
|
||||
surveys={surveys}
|
||||
spreadSheetArray={spreadSheetArray}
|
||||
googleSheetIntegration={googleSheetIntegration}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -8,14 +8,15 @@ import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import WebhookModal from "@/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookDetailModal";
|
||||
import { Webhook } from "lucide-react";
|
||||
import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
|
||||
export default function WebhookTable({
|
||||
environmentId,
|
||||
environment,
|
||||
webhooks,
|
||||
surveys,
|
||||
children: [TableHeading, webhookRows],
|
||||
}: {
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
webhooks: TWebhook[];
|
||||
surveys: TSurvey[];
|
||||
children: [JSX.Element, JSX.Element[]];
|
||||
@@ -24,7 +25,7 @@ export default function WebhookTable({
|
||||
const [isAddWebhookModalOpen, setAddWebhookModalOpen] = useState(false);
|
||||
|
||||
const [activeWebhook, setActiveWebhook] = useState<TWebhook>({
|
||||
environmentId,
|
||||
environmentId: environment.id,
|
||||
id: "",
|
||||
name: "",
|
||||
url: "",
|
||||
@@ -57,7 +58,7 @@ export default function WebhookTable({
|
||||
{webhooks.length === 0 ? (
|
||||
<EmptySpaceFiller
|
||||
type="table"
|
||||
environmentId={environmentId}
|
||||
environment={environment}
|
||||
noWidgetRequired={true}
|
||||
emptyMessage="Your webhooks will appear here as soon as you add them. ⏲️"
|
||||
/>
|
||||
@@ -80,14 +81,14 @@ export default function WebhookTable({
|
||||
)}
|
||||
|
||||
<WebhookModal
|
||||
environmentId={environmentId}
|
||||
environmentId={environment.id}
|
||||
open={isWebhookDetailModalOpen}
|
||||
setOpen={setWebhookDetailModalOpen}
|
||||
webhook={activeWebhook}
|
||||
surveys={surveys}
|
||||
/>
|
||||
<AddWebhookModal
|
||||
environmentId={environmentId}
|
||||
environmentId={environment.id}
|
||||
surveys={surveys}
|
||||
open={isAddWebhookModalOpen}
|
||||
setOpen={setAddWebhookModalOpen}
|
||||
|
||||
@@ -7,18 +7,26 @@ import GoBackButton from "@/components/shared/GoBackButton";
|
||||
import { getSurveys } from "@formbricks/lib/services/survey";
|
||||
import { getWebhooks } from "@formbricks/lib/services/webhook";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
|
||||
export default async function CustomWebhookPage({ params }) {
|
||||
const webhooks = (await getWebhooks(params.environmentId)).sort((a, b) => {
|
||||
const [webhooksUnsorted, surveys, environment] = await Promise.all([
|
||||
getWebhooks(params.environmentId),
|
||||
getSurveys(params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
const webhooks = webhooksUnsorted.sort((a, b) => {
|
||||
if (a.createdAt > b.createdAt) return -1;
|
||||
if (a.createdAt < b.createdAt) return 1;
|
||||
return 0;
|
||||
});
|
||||
const surveys = await getSurveys(params.environmentId);
|
||||
return (
|
||||
<>
|
||||
<GoBackButton />
|
||||
<WebhookTable environmentId={params.environmentId} webhooks={webhooks} surveys={surveys}>
|
||||
<WebhookTable environment={environment} webhooks={webhooks} surveys={surveys}>
|
||||
<WebhookTableHeading />
|
||||
{webhooks.map((webhook) => (
|
||||
<WebhookRowData key={webhook.id} webhook={webhook} surveys={surveys} />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import FormbricksClient from "../../FormbricksClient";
|
||||
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/ResponseFilterContext";
|
||||
import { hasUserEnvironmentAccess } from "@/lib/api/apiHelper";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
|
||||
export default async function EnvironmentLayout({ children, params }) {
|
||||
const session = await getServerSession(authOptions);
|
||||
@@ -22,7 +23,11 @@ export default async function EnvironmentLayout({ children, params }) {
|
||||
<ResponseFilterProvider>
|
||||
<FormbricksClient session={session} />
|
||||
<ToasterClient />
|
||||
<EnvironmentsNavbar environmentId={params.environmentId} session={session} />
|
||||
<EnvironmentsNavbar
|
||||
environmentId={params.environmentId}
|
||||
session={session}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
/>
|
||||
<main className="h-full flex-1 overflow-y-auto bg-slate-50">
|
||||
{children}
|
||||
<main />
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
|
||||
import { ActivityItemContent, ActivityItemIcon, ActivityItemPopover } from "./ActivityItemComponents";
|
||||
import { TActivityFeedItem } from "@formbricks/types/v1/activity";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
|
||||
interface ActivityFeedProps {
|
||||
activities: TActivityFeedItem[];
|
||||
sortByDate: boolean;
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
}
|
||||
|
||||
export default function ActivityFeed({ activities, sortByDate, environmentId }: ActivityFeedProps) {
|
||||
export default function ActivityFeed({ activities, sortByDate, environment }: ActivityFeedProps) {
|
||||
const sortedActivities: TActivityFeedItem[] = activities.sort((a, b) =>
|
||||
sortByDate
|
||||
? new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
@@ -17,7 +18,7 @@ export default function ActivityFeed({ activities, sortByDate, environmentId }:
|
||||
return (
|
||||
<>
|
||||
{sortedActivities.length === 0 ? (
|
||||
<EmptySpaceFiller type={"event"} environmentId={environmentId} />
|
||||
<EmptySpaceFiller type={"event"} environment={environment} />
|
||||
) : (
|
||||
<div>
|
||||
{sortedActivities.map((activityItem) => (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ActivityTimeline from "@/app/(app)/environments/[environmentId]/people/[personId]/(activitySection)/ActivityTimeline";
|
||||
import { getActivityTimeline } from "@formbricks/lib/services/activity";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
|
||||
export default async function ActivitySection({
|
||||
environmentId,
|
||||
@@ -8,11 +9,17 @@ export default async function ActivitySection({
|
||||
environmentId: string;
|
||||
personId: string;
|
||||
}) {
|
||||
const activities = await getActivityTimeline(personId);
|
||||
const [activities, environment] = await Promise.all([
|
||||
getActivityTimeline(personId),
|
||||
getEnvironment(environmentId),
|
||||
]);
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="md:col-span-1">
|
||||
<ActivityTimeline environmentId={environmentId} activities={activities} />
|
||||
<ActivityTimeline environment={environment} activities={activities} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
import ActivityFeed from "@/app/(app)/environments/[environmentId]/people/[personId]/(activitySection)/ActivityFeed";
|
||||
import { TActivityFeedItem } from "@formbricks/types/v1/activity";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { ArrowsUpDownIcon } from "@heroicons/react/24/outline";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function ActivityTimeline({
|
||||
environmentId,
|
||||
environment,
|
||||
activities,
|
||||
}: {
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
activities: TActivityFeedItem[];
|
||||
}) {
|
||||
const [activityAscending, setActivityAscending] = useState(true);
|
||||
@@ -30,7 +31,7 @@ export default function ActivityTimeline({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ActivityFeed activities={activities} sortByDate={activityAscending} environmentId={environmentId} />
|
||||
<ActivityFeed activities={activities} sortByDate={activityAscending} environment={environment} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import ResponseTimeline from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseTimeline";
|
||||
import { getResponsesByPersonId } from "@formbricks/lib/services/response";
|
||||
import { getSurveys } from "@formbricks/lib/services/survey";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TResponseWithSurvey } from "@formbricks/types/v1/responses";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
|
||||
export default async function ResponseSection({
|
||||
environmentId,
|
||||
environment,
|
||||
personId,
|
||||
}: {
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
personId: string;
|
||||
}) {
|
||||
const responses = await getResponsesByPersonId(personId);
|
||||
const surveyIds = responses?.map((response) => response.surveyId) || [];
|
||||
const surveys: TSurvey[] = surveyIds.length === 0 ? [] : (await getSurveys(environmentId)) ?? [];
|
||||
const surveys: TSurvey[] = surveyIds.length === 0 ? [] : (await getSurveys(environment.id)) ?? [];
|
||||
const responsesWithSurvey: TResponseWithSurvey[] =
|
||||
responses?.reduce((acc: TResponseWithSurvey[], response) => {
|
||||
const thisSurvey = surveys.find((survey) => survey?.id === response.surveyId);
|
||||
@@ -26,5 +27,5 @@ export default async function ResponseSection({
|
||||
return acc;
|
||||
}, []) || [];
|
||||
|
||||
return <ResponseTimeline environmentId={environmentId} responses={responsesWithSurvey} />;
|
||||
return <ResponseTimeline environment={environment} responses={responsesWithSurvey} />;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import ResponseFeed from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponsesFeed";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TResponseWithSurvey } from "@formbricks/types/v1/responses";
|
||||
import { ArrowsUpDownIcon } from "@heroicons/react/24/outline";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function ResponseTimeline({
|
||||
environmentId,
|
||||
environment,
|
||||
responses,
|
||||
}: {
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
responses: TResponseWithSurvey[];
|
||||
}) {
|
||||
const [responsesAscending, setResponsesAscending] = useState(true);
|
||||
@@ -29,7 +30,7 @@ export default function ResponseTimeline({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ResponseFeed responses={responses} sortByDate={responsesAscending} environmentId={environmentId} />
|
||||
<ResponseFeed responses={responses} sortByDate={responsesAscending} environment={environment} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,20 +3,21 @@ import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
|
||||
import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator";
|
||||
import { TResponseWithSurvey } from "@formbricks/types/v1/responses";
|
||||
import Link from "next/link";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
|
||||
export default function ResponseFeed({
|
||||
responses,
|
||||
sortByDate,
|
||||
environmentId,
|
||||
environment,
|
||||
}: {
|
||||
responses: TResponseWithSurvey[];
|
||||
sortByDate: boolean;
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{responses.length === 0 ? (
|
||||
<EmptySpaceFiller type="response" environmentId={environmentId} />
|
||||
<EmptySpaceFiller type="response" environment={environment} />
|
||||
) : (
|
||||
<div>
|
||||
{responses
|
||||
@@ -53,12 +54,12 @@ export default function ResponseFeed({
|
||||
<div className="flex items-center justify-center space-x-2 rounded-full bg-slate-50 px-3 py-1 text-sm text-slate-600">
|
||||
<Link
|
||||
className="hover:underline"
|
||||
href={`/environments/${environmentId}/surveys/${response.survey.id}/summary`}>
|
||||
href={`/environments/${environment.id}/surveys/${response.survey.id}/summary`}>
|
||||
{response.survey.name}
|
||||
</Link>
|
||||
<SurveyStatusIndicator
|
||||
status={response.survey.status}
|
||||
environmentId={environmentId}
|
||||
environment={environment}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,13 @@ import AttributesSection from "@/app/(app)/environments/[environmentId]/people/[
|
||||
import ResponseSection from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection";
|
||||
import HeadingSection from "@/app/(app)/environments/[environmentId]/people/[personId]/HeadingSection";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
|
||||
export default async function PersonPage({ params }) {
|
||||
const environment = await getEnvironment(params.environmentId);
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<main className="mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@@ -15,7 +20,7 @@ export default async function PersonPage({ params }) {
|
||||
<section className="pb-24 pt-6">
|
||||
<div className="grid grid-cols-1 gap-x-8 md:grid-cols-4">
|
||||
<AttributesSection personId={params.personId} />
|
||||
<ResponseSection environmentId={params.environmentId} personId={params.personId} />
|
||||
<ResponseSection environment={environment} personId={params.personId} />
|
||||
<ActivitySection environmentId={params.environmentId} personId={params.personId} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -3,6 +3,7 @@ export const revalidate = REVALIDATION_INTERVAL;
|
||||
import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
|
||||
import { truncateMiddle } from "@/lib/utils";
|
||||
import { PEOPLE_PER_PAGE, REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getPeople, getPeopleCount } from "@formbricks/lib/services/person";
|
||||
import { TPerson } from "@formbricks/types/v1/people";
|
||||
import { Pagination, PersonAvatar } from "@formbricks/ui";
|
||||
@@ -19,7 +20,13 @@ export default async function PeoplePage({
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
}) {
|
||||
const pageNumber = searchParams.page ? parseInt(searchParams.page as string) : 1;
|
||||
const totalPeople = await getPeopleCount(params.environmentId);
|
||||
const [environment, totalPeople] = await Promise.all([
|
||||
getEnvironment(params.environmentId),
|
||||
getPeopleCount(params.environmentId),
|
||||
]);
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
const maxPageNumber = Math.ceil(totalPeople / PEOPLE_PER_PAGE);
|
||||
let hidePagination = false;
|
||||
|
||||
@@ -37,7 +44,7 @@ export default async function PeoplePage({
|
||||
{people.length === 0 ? (
|
||||
<EmptySpaceFiller
|
||||
type="table"
|
||||
environmentId={params.environmentId}
|
||||
environment={environment}
|
||||
emptyMessage="Your users will appear here as soon as they use your app ⏲️"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useProduct } from "@/lib/products/products";
|
||||
import { useTeam } from "@/lib/teams/teams";
|
||||
import { truncate } from "@/lib/utils";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { TTeam } from "@formbricks/types/v1/teams";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui";
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
@@ -25,10 +24,18 @@ import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
export default function SettingsNavbar({ environmentId }: { environmentId: string }) {
|
||||
export default function SettingsNavbar({
|
||||
environmentId,
|
||||
isFormbricksCloud,
|
||||
team,
|
||||
product,
|
||||
}: {
|
||||
environmentId: string;
|
||||
isFormbricksCloud: boolean;
|
||||
team: TTeam;
|
||||
product: TProduct;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const { team } = useTeam(environmentId);
|
||||
const { product } = useProduct(environmentId);
|
||||
const [mobileNavMenuOpen, setMobileNavMenuOpen] = useState(false);
|
||||
|
||||
interface NavigationLink {
|
||||
@@ -113,7 +120,7 @@ export default function SettingsNavbar({ environmentId }: { environmentId: strin
|
||||
name: "Billing & Plan",
|
||||
href: `/environments/${environmentId}/settings/billing`,
|
||||
icon: CreditCardIcon,
|
||||
hidden: !IS_FORMBRICKS_CLOUD,
|
||||
hidden: !isFormbricksCloud,
|
||||
current: pathname?.includes("/billing"),
|
||||
},
|
||||
],
|
||||
@@ -152,21 +159,21 @@ export default function SettingsNavbar({ environmentId }: { environmentId: strin
|
||||
href: "https://formbricks.com/gdpr",
|
||||
icon: LinkIcon,
|
||||
target: "_blank",
|
||||
hidden: !IS_FORMBRICKS_CLOUD,
|
||||
hidden: !isFormbricksCloud,
|
||||
},
|
||||
{
|
||||
name: "Privacy",
|
||||
href: "https://formbricks.com/privacy",
|
||||
icon: LinkIcon,
|
||||
target: "_blank",
|
||||
hidden: !IS_FORMBRICKS_CLOUD,
|
||||
hidden: !isFormbricksCloud,
|
||||
},
|
||||
{
|
||||
name: "Terms",
|
||||
href: "https://formbricks.com/terms",
|
||||
icon: LinkIcon,
|
||||
target: "_blank",
|
||||
hidden: !IS_FORMBRICKS_CLOUD,
|
||||
hidden: !isFormbricksCloud,
|
||||
},
|
||||
{
|
||||
name: "License",
|
||||
@@ -178,7 +185,7 @@ export default function SettingsNavbar({ environmentId }: { environmentId: strin
|
||||
],
|
||||
},
|
||||
],
|
||||
[environmentId, pathname]
|
||||
[environmentId, isFormbricksCloud, pathname]
|
||||
);
|
||||
|
||||
if (!navigation) return null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import { IS_FORMBRICKS_CLOUD, REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/services/team";
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
import { Metadata } from "next";
|
||||
import SettingsNavbar from "./SettingsNavbar";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/services/team";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Settings",
|
||||
};
|
||||
|
||||
export default function SettingsLayout({ children, params }) {
|
||||
export default async function SettingsLayout({ children, params }) {
|
||||
const [team, product] = await Promise.all([
|
||||
getTeamByEnvironmentId(params.environmentId),
|
||||
getProductByEnvironmentId(params.environmentId),
|
||||
]);
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="sm:flex">
|
||||
<SettingsNavbar environmentId={params.environmentId} />
|
||||
<SettingsNavbar
|
||||
environmentId={params.environmentId}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
team={team}
|
||||
product={product}
|
||||
/>
|
||||
<div className="w-full md:ml-64">
|
||||
<div className="max-w-4xl px-6 pb-6 pt-14 md:pt-6">
|
||||
<div>{children}</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { Button, ColorPicker, Label, Switch } from "@formbricks/ui";
|
||||
import { useState } from "react";
|
||||
@@ -10,11 +9,12 @@ import { updateProductAction } from "./actions";
|
||||
|
||||
interface EditHighlightBorderProps {
|
||||
product: TProduct;
|
||||
defaultBrandColor: string;
|
||||
}
|
||||
|
||||
export const EditHighlightBorder = ({ product }: EditHighlightBorderProps) => {
|
||||
export const EditHighlightBorder = ({ product, defaultBrandColor }: EditHighlightBorderProps) => {
|
||||
const [showHighlightBorder, setShowHighlightBorder] = useState(product.highlightBorderColor ? true : false);
|
||||
const [color, setColor] = useState<string | null>(product.highlightBorderColor || DEFAULT_BRAND_COLOR);
|
||||
const [color, setColor] = useState<string | null>(product.highlightBorderColor || defaultBrandColor);
|
||||
const [updatingBorder, setUpdatingBorder] = useState(false);
|
||||
|
||||
const handleUpdateHighlightBorder = async () => {
|
||||
@@ -32,7 +32,7 @@ export const EditHighlightBorder = ({ product }: EditHighlightBorderProps) => {
|
||||
const handleSwitch = (checked: boolean) => {
|
||||
if (checked) {
|
||||
if (!color) {
|
||||
setColor(DEFAULT_BRAND_COLOR);
|
||||
setColor(defaultBrandColor);
|
||||
setShowHighlightBorder(true);
|
||||
} else {
|
||||
setShowHighlightBorder(true);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { EditFormbricksSignature } from "./EditSignature";
|
||||
import { EditBrandColor } from "./EditBrandColor";
|
||||
import { EditPlacement } from "./EditPlacement";
|
||||
import { EditHighlightBorder } from "./EditHighlightBorder";
|
||||
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
|
||||
|
||||
export default async function ProfileSettingsPage({ params }: { params: { environmentId: string } }) {
|
||||
const product = await getProductByEnvironmentId(params.environmentId);
|
||||
@@ -29,7 +30,7 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro
|
||||
noPadding
|
||||
title="Highlight Border"
|
||||
description="Make sure your users notice the survey you display">
|
||||
<EditHighlightBorder product={product} />
|
||||
<EditHighlightBorder product={product} defaultBrandColor={DEFAULT_BRAND_COLOR} />
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
title="Formbricks Signature"
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from "@/app/(app)/environments/[environmentId]/settings/members/actions";
|
||||
import CustomDialog from "@/components/shared/CustomDialog";
|
||||
import CreateTeamModal from "@/components/team/CreateTeamModal";
|
||||
import { env } from "@/env.mjs";
|
||||
import { TMembershipRole } from "@formbricks/types/v1/memberships";
|
||||
import { TTeam } from "@formbricks/types/v1/teams";
|
||||
import { Button } from "@formbricks/ui";
|
||||
@@ -20,9 +19,16 @@ type TeamActionsProps = {
|
||||
isAdminOrOwner: boolean;
|
||||
isLeaveTeamDisabled: boolean;
|
||||
team: TTeam;
|
||||
isInviteDisabled: boolean;
|
||||
};
|
||||
|
||||
export default function TeamActions({ isAdminOrOwner, role, team, isLeaveTeamDisabled }: TeamActionsProps) {
|
||||
export default function TeamActions({
|
||||
isAdminOrOwner,
|
||||
role,
|
||||
team,
|
||||
isLeaveTeamDisabled,
|
||||
isInviteDisabled,
|
||||
}: TeamActionsProps) {
|
||||
const router = useRouter();
|
||||
const [isLeaveTeamModalOpen, setLeaveTeamModalOpen] = useState(false);
|
||||
const [isCreateTeamModalOpen, setCreateTeamModalOpen] = useState(false);
|
||||
@@ -69,7 +75,7 @@ export default function TeamActions({ isAdminOrOwner, role, team, isLeaveTeamDis
|
||||
}}>
|
||||
Create New Team
|
||||
</Button>
|
||||
{env.NEXT_PUBLIC_INVITE_DISABLED !== "1" && isAdminOrOwner && (
|
||||
{!isInviteDisabled && isAdminOrOwner && (
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
onClick={() => {
|
||||
|
||||
@@ -23,7 +23,7 @@ import { TMembershipRole, TMembershipUpdateInput } from "@formbricks/types/v1/me
|
||||
import { TTeamUpdateInput } from "@formbricks/types/v1/teams";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { hasTeamAccess, hasTeamAuthority, hasTeamOwnership, isOwner } from "@formbricks/lib/auth";
|
||||
import { env } from "@/env.mjs";
|
||||
import { INVITE_DISABLED } from "@formbricks/lib/constants";
|
||||
|
||||
export const updateTeamAction = async (teamId: string, data: TTeamUpdateInput) => {
|
||||
return await updateTeam(teamId, data);
|
||||
@@ -154,7 +154,7 @@ export const inviteUserAction = async (
|
||||
|
||||
const isUserAuthorized = await hasTeamAuthority(session.user.id, teamId);
|
||||
|
||||
if (env.NEXT_PUBLIC_INVITE_DISABLED === "1") {
|
||||
if (INVITE_DISABLED) {
|
||||
throw new AuthenticationError("Invite disabled");
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import SettingsTitle from "../SettingsTitle";
|
||||
import DeleteTeam from "./DeleteTeam";
|
||||
import { EditMemberships } from "./EditMemberships";
|
||||
import EditTeamName from "./EditTeamName";
|
||||
import { INVITE_DISABLED } from "@formbricks/lib/constants";
|
||||
|
||||
const MembersLoading = () => (
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
@@ -65,6 +66,7 @@ export default async function MembersSettingsPage({ params }: { params: { enviro
|
||||
isAdminOrOwner={isUserAdminOrOwner}
|
||||
role={currentUserRole}
|
||||
isLeaveTeamDisabled={isLeaveTeamDisabled}
|
||||
isInviteDisabled={INVITE_DISABLED}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TProduct } from "@formbricks/types/v1/product";
|
||||
import DeleteProductRender from "@/app/(app)/environments/[environmentId]/settings/product/DeleteProductRender";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/services/membership";
|
||||
|
||||
type DeleteProductProps = {
|
||||
environmentId: string;
|
||||
@@ -12,10 +13,20 @@ type DeleteProductProps = {
|
||||
|
||||
export default async function DeleteProduct({ environmentId, product }: DeleteProductProps) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
const availableProducts = team ? await getProducts(team.id) : null;
|
||||
|
||||
const role = team ? session?.user.teams.find((foundTeam) => foundTeam.id === team.id)?.role : null;
|
||||
const membership = await getMembershipByUserIdTeamId(session.user.id, team.id);
|
||||
if (!membership) {
|
||||
throw new Error("Membership not found");
|
||||
}
|
||||
const role = membership.role;
|
||||
const availableProductsLength = availableProducts ? availableProducts.length : 0;
|
||||
const isUserAdminOrOwner = role === "admin" || role === "owner";
|
||||
const isDeleteDisabled = availableProductsLength <= 1 || !isUserAdminOrOwner;
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import CodeBlock from "@/components/shared/CodeBlock";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { TabBar } from "@formbricks/ui";
|
||||
import Link from "next/link";
|
||||
import "prismjs/themes/prism.css";
|
||||
import { useState } from "react";
|
||||
import { IoLogoHtml5, IoLogoNpm } from "react-icons/io5";
|
||||
import packageJson from "@/package.json";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
|
||||
const tabs = [
|
||||
{ id: "npm", label: "NPM", icon: <IoLogoNpm /> },
|
||||
{ id: "html", label: "HTML", icon: <IoLogoHtml5 /> },
|
||||
];
|
||||
|
||||
export default function SetupInstructions({ environmentId }) {
|
||||
export default function SetupInstructions({
|
||||
environmentId,
|
||||
webAppUrl,
|
||||
isFormbricksCloud,
|
||||
}: {
|
||||
environmentId: string;
|
||||
webAppUrl: string;
|
||||
isFormbricksCloud: boolean;
|
||||
}) {
|
||||
const [activeTab, setActiveTab] = useState(tabs[0].id);
|
||||
|
||||
return (
|
||||
@@ -33,7 +39,7 @@ export default function SetupInstructions({ environmentId }) {
|
||||
if (typeof window !== "undefined") {
|
||||
formbricks.init({
|
||||
environmentId: "${environmentId}",
|
||||
apiHost: "${WEBAPP_URL}",
|
||||
apiHost: "${webAppUrl}",
|
||||
debug: true, // remove when in production
|
||||
});
|
||||
}`}</CodeBlock>
|
||||
@@ -142,7 +148,7 @@ if (typeof window !== "undefined") {
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
{!IS_FORMBRICKS_CLOUD && (
|
||||
{!isFormbricksCloud && (
|
||||
<div>
|
||||
<hr className="my-3" />
|
||||
<p className="flex w-full justify-end text-sm text-slate-700">
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ErrorComponent } from "@formbricks/ui";
|
||||
import SettingsCard from "../SettingsCard";
|
||||
import SettingsTitle from "../SettingsTitle";
|
||||
import SetupInstructions from "./SetupInstructions";
|
||||
import { IS_FORMBRICKS_CLOUD, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
|
||||
export default async function ProfileSettingsPage({ params }) {
|
||||
const [environment, actions] = await Promise.all([
|
||||
@@ -42,7 +43,11 @@ export default async function ProfileSettingsPage({ params }) {
|
||||
title="How to setup"
|
||||
description="Follow these steps to setup the Formbricks widget within your app"
|
||||
noPadding>
|
||||
<SetupInstructions environmentId={params.environmentId} />
|
||||
<SetupInstructions
|
||||
environmentId={params.environmentId}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
/>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,37 +3,46 @@
|
||||
import MergeTagsCombobox from "@/app/(app)/environments/[environmentId]/settings/tags/MergeTagsCombobox";
|
||||
import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { useDeleteTag, useMergeTags, useUpdateTag } from "@/lib/tags/mutateTags";
|
||||
import { useTagsCountForEnvironment, useTagsForEnvironment } from "@/lib/tags/tags";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TTag, TTagsCount } from "@formbricks/types/v1/tags";
|
||||
import { Button, Input } from "@formbricks/ui";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
deleteTagAction,
|
||||
mergeTagsAction,
|
||||
updateTagNameAction,
|
||||
} from "@/app/(app)/environments/[environmentId]/settings/tags/actions";
|
||||
import { ExclamationCircleIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
interface IEditTagsWrapperProps {
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
environmentTags: TTag[];
|
||||
environmentTagsCount: TTagsCount;
|
||||
}
|
||||
|
||||
const SingleTag: React.FC<{
|
||||
tagId: string;
|
||||
tagName: string;
|
||||
environmentId: string;
|
||||
tagCount?: number;
|
||||
tagCountLoading?: boolean;
|
||||
updateTagsCount?: () => void;
|
||||
environmentTags: TTag[];
|
||||
}> = ({
|
||||
environmentId,
|
||||
tagId,
|
||||
tagName,
|
||||
tagCount = 0,
|
||||
tagCountLoading = false,
|
||||
updateTagsCount = () => {},
|
||||
environmentTags,
|
||||
}) => {
|
||||
const { mutate: refetchEnvironmentTags, data: environmentTags } = useTagsForEnvironment(environmentId);
|
||||
const { deleteTag, isDeletingTag } = useDeleteTag(environmentId, tagId);
|
||||
|
||||
const { updateTag, updateTagError } = useUpdateTag(environmentId, tagId);
|
||||
const { mergeTags, isMergingTags } = useMergeTags(environmentId);
|
||||
const router = useRouter();
|
||||
// const { updateTag, updateTagError } = useUpdateTag(environment.id, tagId);
|
||||
// const { mergeTags, isMergingTags } = useMergeTags(environment.id);
|
||||
const [updateTagError, setUpdateTagError] = useState(false);
|
||||
const [isMergingTags, setIsMergingTags] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="w-full" key={tagId}>
|
||||
@@ -49,18 +58,24 @@ const SingleTag: React.FC<{
|
||||
)}
|
||||
defaultValue={tagName}
|
||||
onBlur={(e) => {
|
||||
updateTag(
|
||||
{ name: e.target.value.trim() },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success("Tag updated");
|
||||
refetchEnvironmentTags();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error?.message ?? "Failed to update tag");
|
||||
},
|
||||
}
|
||||
);
|
||||
updateTagNameAction(tagId, e.target.value.trim())
|
||||
.then(() => {
|
||||
setUpdateTagError(false);
|
||||
toast.success("Tag updated");
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error?.message.includes("Unique constraint failed on the fields")) {
|
||||
toast.error("Tag already exists", {
|
||||
duration: 2000,
|
||||
icon: <ExclamationCircleIcon className="h-5 w-5 text-orange-500" />,
|
||||
});
|
||||
} else {
|
||||
toast.error(error?.message ?? "Something went wrong", {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
setUpdateTagError(true);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -84,19 +99,19 @@ const SingleTag: React.FC<{
|
||||
?.map((tag) => ({ label: tag.name, value: tag.id })) ?? []
|
||||
}
|
||||
onSelect={(newTagId) => {
|
||||
mergeTags(
|
||||
{
|
||||
originalTagId: tagId,
|
||||
newTagId,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success("Tags merged");
|
||||
refetchEnvironmentTags();
|
||||
updateTagsCount();
|
||||
},
|
||||
}
|
||||
);
|
||||
setIsMergingTags(true);
|
||||
mergeTagsAction(tagId, newTagId)
|
||||
.then(() => {
|
||||
toast.success("Tags merged");
|
||||
updateTagsCount();
|
||||
router.refresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error?.message ?? "Something went wrong");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsMergingTags(false);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -106,16 +121,14 @@ const SingleTag: React.FC<{
|
||||
<Button
|
||||
variant="alert"
|
||||
size="sm"
|
||||
loading={isDeletingTag}
|
||||
// loading={isDeletingTag}
|
||||
className="font-medium text-slate-50 focus:border-transparent focus:shadow-transparent focus:outline-transparent focus:ring-0 focus:ring-transparent"
|
||||
onClick={() => {
|
||||
if (confirm("Are you sure you want to delete this tag?")) {
|
||||
deleteTag(null, {
|
||||
onSuccess: () => {
|
||||
toast.success("Tag deleted");
|
||||
refetchEnvironmentTags();
|
||||
updateTagsCount();
|
||||
},
|
||||
deleteTagAction(tagId).then(() => {
|
||||
toast.success("Tag deleted");
|
||||
updateTagsCount();
|
||||
router.refresh();
|
||||
});
|
||||
}
|
||||
}}>
|
||||
@@ -129,19 +142,7 @@ const SingleTag: React.FC<{
|
||||
};
|
||||
|
||||
const EditTagsWrapper: React.FC<IEditTagsWrapperProps> = (props) => {
|
||||
const { environmentId } = props;
|
||||
const { data: environmentTags, isLoading: isLoadingEnvironmentTags } = useTagsForEnvironment(environmentId);
|
||||
|
||||
const { tagsCount, isLoadingTagsCount, mutateTagsCount } = useTagsCountForEnvironment(environmentId);
|
||||
|
||||
if (isLoadingEnvironmentTags) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { environment, environmentTags, environmentTagsCount } = props;
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
@@ -152,18 +153,16 @@ const EditTagsWrapper: React.FC<IEditTagsWrapperProps> = (props) => {
|
||||
</div>
|
||||
|
||||
{!environmentTags?.length ? (
|
||||
<EmptySpaceFiller environmentId={environmentId} type="tag" noWidgetRequired />
|
||||
<EmptySpaceFiller environment={environment} type="tag" noWidgetRequired />
|
||||
) : null}
|
||||
|
||||
{environmentTags?.map((tag) => (
|
||||
<SingleTag
|
||||
key={tag.id}
|
||||
environmentId={environmentId}
|
||||
tagId={tag.id}
|
||||
tagName={tag.name}
|
||||
tagCount={tagsCount?.find((count) => count.tagId === tag.id)?.count ?? 0}
|
||||
tagCountLoading={isLoadingTagsCount}
|
||||
updateTagsCount={mutateTagsCount}
|
||||
tagCount={environmentTagsCount?.find((count) => count.tagId === tag.id)?.count ?? 0}
|
||||
environmentTags={environmentTags}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
"use server";
|
||||
|
||||
import { deleteTag, mergeTags, updateTagName } from "@formbricks/lib/services/tag";
|
||||
|
||||
export const deleteTagAction = async (tagId: string) => {
|
||||
return await deleteTag(tagId);
|
||||
};
|
||||
|
||||
export const updateTagNameAction = async (tagId: string, name: string) => {
|
||||
return await updateTagName(tagId, name);
|
||||
};
|
||||
|
||||
export const mergeTagsAction = async (originalTagId: string, newTagId: string) => {
|
||||
return await mergeTags(originalTagId, newTagId);
|
||||
};
|
||||
@@ -1,11 +1,25 @@
|
||||
import EditTagsWrapper from "./EditTagsWrapper";
|
||||
import SettingsTitle from "../SettingsTitle";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/services/tag";
|
||||
import { getTagsOnResponsesCount } from "@formbricks/lib/services/tagOnResponse";
|
||||
|
||||
export default async function MembersSettingsPage({ params }) {
|
||||
const environment = await getEnvironment(params.environmentId);
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
const tags = await getTagsByEnvironmentId(params.environmentId);
|
||||
const environmentTagsCount = await getTagsOnResponsesCount();
|
||||
|
||||
export default function MembersSettingsPage({ params }) {
|
||||
return (
|
||||
<div>
|
||||
<SettingsTitle title="Tags" />
|
||||
<EditTagsWrapper environmentId={params.environmentId} />
|
||||
<EditTagsWrapper
|
||||
environment={environment}
|
||||
environmentTags={tags}
|
||||
environmentTagsCount={environmentTagsCount}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function SurveyDropDownMenu({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const surveyUrl = useMemo(() => surveyBaseUrl + survey.id, [survey]);
|
||||
const surveyUrl = useMemo(() => surveyBaseUrl + survey.id, [survey.id, surveyBaseUrl]);
|
||||
|
||||
const handleDeleteSurvey = async (survey) => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -76,11 +76,7 @@ export default async function SurveysList({ environmentId }: { environmentId: st
|
||||
<div className="flex items-center">
|
||||
{survey.status !== "draft" && (
|
||||
<>
|
||||
<SurveyStatusIndicator
|
||||
status={survey.status}
|
||||
tooltip
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
<SurveyStatusIndicator status={survey.status} tooltip environment={environment} />
|
||||
</>
|
||||
)}
|
||||
{survey.status === "draft" && (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IS_FORMBRICKS_CLOUD, RESPONSES_LIMIT_FREE } from "@formbricks/lib/constants";
|
||||
import { RESPONSES_LIMIT_FREE } from "@formbricks/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getSurveyResponses } from "@formbricks/lib/services/response";
|
||||
import { getSurveyWithAnalytics } from "@formbricks/lib/services/survey";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/services/team";
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"use server";
|
||||
|
||||
import { deleteResponse } from "@formbricks/lib/services/response";
|
||||
import { updateResponseNote, resolveResponseNote } from "@formbricks/lib/services/responseNote";
|
||||
import { createTag } from "@formbricks/lib/services/tag";
|
||||
import { addTagToRespone, deleteTagFromResponse } from "@formbricks/lib/services/tagOnResponse";
|
||||
|
||||
export const updateResponseNoteAction = async (responseNoteId: string, text: string) => {
|
||||
await updateResponseNote(responseNoteId, text);
|
||||
@@ -9,3 +12,19 @@ export const updateResponseNoteAction = async (responseNoteId: string, text: str
|
||||
export const resolveResponseNoteAction = async (responseNoteId: string) => {
|
||||
await resolveResponseNote(responseNoteId);
|
||||
};
|
||||
|
||||
export const deleteResponseAction = async (responseId: string) => {
|
||||
return await deleteResponse(responseId);
|
||||
};
|
||||
|
||||
export const createTagAction = async (environmentId: string, tagName: string) => {
|
||||
return await createTag(environmentId, tagName);
|
||||
};
|
||||
|
||||
export const addTagToResponeAction = async (responseId: string, tagId: string) => {
|
||||
return await addTagToRespone(responseId, tagId);
|
||||
};
|
||||
|
||||
export const removeTagFromResponseAction = async (responseId: string, tagId: string) => {
|
||||
return await deleteTagFromResponse(responseId, tagId);
|
||||
};
|
||||
|
||||
@@ -10,16 +10,29 @@ import { TResponse } from "@formbricks/types/v1/responses";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
|
||||
interface ResponsePageProps {
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
surveyId: string;
|
||||
responses: TResponse[];
|
||||
surveyBaseUrl: string;
|
||||
product: TProduct;
|
||||
environmentTags: TTag[];
|
||||
}
|
||||
|
||||
const ResponsePage = ({ environmentId, survey, surveyId, responses, surveyBaseUrl }: ResponsePageProps) => {
|
||||
const ResponsePage = ({
|
||||
environment,
|
||||
survey,
|
||||
surveyId,
|
||||
responses,
|
||||
surveyBaseUrl,
|
||||
product,
|
||||
environmentTags,
|
||||
}: ResponsePageProps) => {
|
||||
const { selectedFilter, dateRange, resetState } = useResponseFilter();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
@@ -37,23 +50,25 @@ const ResponsePage = ({ environmentId, survey, surveyId, responses, surveyBaseUr
|
||||
return (
|
||||
<ContentWrapper>
|
||||
<SummaryHeader
|
||||
environmentId={environmentId}
|
||||
environment={environment}
|
||||
survey={survey}
|
||||
surveyId={surveyId}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
product={product}
|
||||
/>
|
||||
<CustomFilter
|
||||
environmentId={environmentId}
|
||||
environmentTags={environmentTags}
|
||||
responses={filterResponses}
|
||||
survey={survey}
|
||||
totalResponses={responses}
|
||||
/>
|
||||
<SurveyResultsTabs activeId="responses" environmentId={environmentId} surveyId={surveyId} />
|
||||
<SurveyResultsTabs activeId="responses" environmentId={environment.id} surveyId={surveyId} />
|
||||
<ResponseTimeline
|
||||
environmentId={environmentId}
|
||||
environment={environment}
|
||||
surveyId={surveyId}
|
||||
responses={filterResponses}
|
||||
survey={survey}
|
||||
environmentTags={environmentTags}
|
||||
/>
|
||||
</ContentWrapper>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import TagsCombobox from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/TagsCombobox";
|
||||
import { removeTagFromResponse, useAddTagToResponse, useCreateTag } from "@/lib/tags/mutateTags";
|
||||
import { useTagsForEnvironment } from "@/lib/tags/tags";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { Tag } from "./Tag";
|
||||
import { ExclamationCircleIcon, Cog6ToothIcon } from "@heroicons/react/24/solid";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
import {
|
||||
addTagToResponeAction,
|
||||
createTagAction,
|
||||
removeTagFromResponseAction,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions";
|
||||
|
||||
interface ResponseTagsWrapperProps {
|
||||
tags: {
|
||||
@@ -16,15 +20,15 @@ interface ResponseTagsWrapperProps {
|
||||
tagName: string;
|
||||
}[];
|
||||
environmentId: string;
|
||||
surveyId: string;
|
||||
responseId: string;
|
||||
environmentTags: TTag[];
|
||||
}
|
||||
|
||||
const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
|
||||
tags,
|
||||
environmentId,
|
||||
responseId,
|
||||
surveyId,
|
||||
environmentTags,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
@@ -32,13 +36,9 @@ const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
|
||||
const [tagsState, setTagsState] = useState(tags);
|
||||
const [tagIdToHighlight, setTagIdToHighlight] = useState("");
|
||||
|
||||
const { createTag } = useCreateTag(environmentId);
|
||||
const { data: environmentTags, mutate: refetchEnvironmentTags } = useTagsForEnvironment(environmentId);
|
||||
const { addTagToRespone } = useAddTagToResponse(environmentId, surveyId, responseId);
|
||||
|
||||
const onDelete = async (tagId: string) => {
|
||||
try {
|
||||
await removeTagFromResponse(environmentId, surveyId, responseId, tagId);
|
||||
await removeTagFromResponseAction(responseId, tagId);
|
||||
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
@@ -79,60 +79,38 @@ const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
|
||||
setSearchValue={setSearchValue}
|
||||
tags={environmentTags?.map((tag) => ({ value: tag.id, label: tag.name })) ?? []}
|
||||
currentTags={tagsState.map((tag) => ({ value: tag.tagId, label: tag.tagName }))}
|
||||
createTag={(tagName) => {
|
||||
createTag(
|
||||
{
|
||||
name: tagName?.trim() ?? "",
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
setTagsState((prevTags) => [
|
||||
...prevTags,
|
||||
{
|
||||
tagId: data.id,
|
||||
tagName: data.name,
|
||||
},
|
||||
]);
|
||||
addTagToRespone(
|
||||
{
|
||||
tagIdToAdd: data.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setSearchValue("");
|
||||
setOpen(false);
|
||||
|
||||
refetchEnvironmentTags();
|
||||
router.refresh();
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err?.cause === "DUPLICATE_RECORD") {
|
||||
toast.error(err?.message ?? "Something went wrong", {
|
||||
duration: 2000,
|
||||
icon: <ExclamationCircleIcon className="h-5 w-5 text-orange-500" />,
|
||||
});
|
||||
|
||||
const tag = tags.find((tag) => tag.tagName === tagName?.trim() ?? "");
|
||||
setTagIdToHighlight(tag?.tagId ?? "");
|
||||
} else {
|
||||
toast.error(err?.message ?? "Something went wrong", {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
createTag={async (tagName) => {
|
||||
await createTagAction(environmentId, tagName?.trim() ?? "")
|
||||
.then((tag) => {
|
||||
setTagsState((prevTags) => [
|
||||
...prevTags,
|
||||
{
|
||||
tagId: tag.id,
|
||||
tagName: tag.name,
|
||||
},
|
||||
]);
|
||||
addTagToResponeAction(responseId, tag.id).then(() => {
|
||||
setSearchValue("");
|
||||
setOpen(false);
|
||||
|
||||
refetchEnvironmentTags();
|
||||
|
||||
router.refresh();
|
||||
},
|
||||
throwOnError: false,
|
||||
}
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err?.message.includes("Unique constraint failed on the fields")) {
|
||||
toast.error("Tag already exists", {
|
||||
duration: 2000,
|
||||
icon: <ExclamationCircleIcon className="h-5 w-5 text-orange-500" />,
|
||||
});
|
||||
} else {
|
||||
toast.error(err?.message ?? "Something went wrong", {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
setSearchValue("");
|
||||
setOpen(false);
|
||||
router.refresh();
|
||||
});
|
||||
}}
|
||||
addTag={(tagId) => {
|
||||
setTagsState((prevTags) => [
|
||||
@@ -143,20 +121,11 @@ const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
|
||||
},
|
||||
]);
|
||||
|
||||
addTagToRespone(
|
||||
{
|
||||
tagIdToAdd: tagId,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setSearchValue("");
|
||||
setOpen(false);
|
||||
refetchEnvironmentTags();
|
||||
|
||||
router.refresh();
|
||||
},
|
||||
}
|
||||
);
|
||||
addTagToResponeAction(responseId, tagId).then(() => {
|
||||
setSearchValue("");
|
||||
setOpen(false);
|
||||
router.refresh();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,19 +6,23 @@ import { createId } from "@paralleldrive/cuid2";
|
||||
import { useMemo } from "react";
|
||||
import SingleResponse from "./SingleResponse";
|
||||
import EmptyInAppSurveys from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
|
||||
interface ResponseTimelineProps {
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
surveyId: string;
|
||||
responses: TResponse[];
|
||||
survey: TSurvey;
|
||||
environmentTags: TTag[];
|
||||
}
|
||||
|
||||
export default function ResponseTimeline({
|
||||
environmentId,
|
||||
environment,
|
||||
surveyId,
|
||||
responses,
|
||||
survey,
|
||||
environmentTags,
|
||||
}: ResponseTimelineProps) {
|
||||
const matchQandA = useMemo(() => {
|
||||
if (survey && responses) {
|
||||
@@ -67,11 +71,13 @@ export default function ResponseTimeline({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{survey.type === "web" && responses.length === 0 && <EmptyInAppSurveys environmentId={environmentId} />}
|
||||
{survey.type === "web" && responses.length === 0 && (
|
||||
<EmptyInAppSurveys environmentId={environment.id} />
|
||||
)}
|
||||
{survey.type !== "web" && responses.length === 0 ? (
|
||||
<EmptySpaceFiller
|
||||
type="response"
|
||||
environmentId={environmentId}
|
||||
environment={environment}
|
||||
noWidgetRequired={survey.type === "link"}
|
||||
/>
|
||||
) : (
|
||||
@@ -82,7 +88,8 @@ export default function ResponseTimeline({
|
||||
key={updatedResponse.id}
|
||||
data={updatedResponse}
|
||||
surveyId={surveyId}
|
||||
environmentId={environmentId}
|
||||
environmentId={environment.id}
|
||||
environmentTags={environmentTags}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import { deleteSubmission } from "@/lib/responses/responses";
|
||||
import { truncate } from "@/lib/utils";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { QuestionType } from "@formbricks/types/questions";
|
||||
@@ -17,6 +16,8 @@ import toast from "react-hot-toast";
|
||||
import ResponseNote from "./ResponseNote";
|
||||
import ResponseTagsWrapper from "./ResponseTagsWrapper";
|
||||
import { RatingResponse } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/RatingResponse";
|
||||
import { deleteResponseAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
|
||||
export interface OpenTextSummaryProps {
|
||||
environmentId: string;
|
||||
@@ -31,6 +32,7 @@ export interface OpenTextSummaryProps {
|
||||
range?: number;
|
||||
}[];
|
||||
};
|
||||
environmentTags: TTag[];
|
||||
}
|
||||
|
||||
function findEmail(person) {
|
||||
@@ -59,7 +61,12 @@ function TooltipRenderer(props: TooltipRendererProps) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export default function SingleResponse({ data, environmentId, surveyId }: OpenTextSummaryProps) {
|
||||
export default function SingleResponse({
|
||||
data,
|
||||
environmentId,
|
||||
surveyId,
|
||||
environmentTags,
|
||||
}: OpenTextSummaryProps) {
|
||||
const router = useRouter();
|
||||
const email = data.person && findEmail(data.person);
|
||||
const displayIdentifier = email || (data.person && truncate(data.person.id, 16)) || null;
|
||||
@@ -69,9 +76,9 @@ export default function SingleResponse({ data, environmentId, surveyId }: OpenTe
|
||||
|
||||
const handleDeleteSubmission = async () => {
|
||||
setIsDeleting(true);
|
||||
const deleteResponse = await deleteSubmission(environmentId, data?.surveyId, data?.id);
|
||||
const deleteResponseStatus = await deleteResponseAction(data?.id);
|
||||
router.refresh();
|
||||
if (deleteResponse?.id?.length > 0) toast.success("Submission deleted successfully.");
|
||||
if (deleteResponseStatus) toast.success("Submission deleted successfully.");
|
||||
setDeleteDialogOpen(false);
|
||||
setIsDeleting(false);
|
||||
};
|
||||
@@ -182,10 +189,10 @@ export default function SingleResponse({ data, environmentId, surveyId }: OpenTe
|
||||
|
||||
<ResponseTagsWrapper
|
||||
environmentId={environmentId}
|
||||
surveyId={surveyId}
|
||||
responseId={data.id}
|
||||
tags={data.tags.map((tag) => ({ tagId: tag.id, tagName: tag.name }))}
|
||||
key={data.tags.map((tag) => tag.id).join("-")}
|
||||
environmentTags={environmentTags}
|
||||
/>
|
||||
|
||||
<DeleteDialog
|
||||
|
||||
@@ -4,26 +4,41 @@ import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import ResponsePage from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
|
||||
import { getAnalysisData } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { SURVEY_BASE_URL } from "@formbricks/lib/constants";
|
||||
import { REVALIDATION_INTERVAL, SURVEY_BASE_URL } from "@formbricks/lib/constants";
|
||||
import ResponsesLimitReachedBanner from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/ResponsesLimitReachedBanner";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/services/tag";
|
||||
|
||||
export default async function Page({ params }) {
|
||||
const surveyBaseUrl = SURVEY_BASE_URL;
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
const { responses, survey } = await getAnalysisData(params.surveyId, params.environmentId);
|
||||
const [{ responses, survey }, environment] = await Promise.all([
|
||||
getAnalysisData(params.surveyId, params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
const product = await getProductByEnvironmentId(environment.id);
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
const tags = await getTagsByEnvironmentId(params.environmentId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResponsesLimitReachedBanner environmentId={params.environmentId} surveyId={params.surveyId} />
|
||||
<ResponsePage
|
||||
environmentId={params.environmentId}
|
||||
environment={environment}
|
||||
responses={responses}
|
||||
survey={survey}
|
||||
surveyId={params.surveyId}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
surveyBaseUrl={SURVEY_BASE_URL}
|
||||
product={product}
|
||||
environmentTags={tags}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function LinkSurveyModal({ survey, open, setOpen, surveyBaseUrl }
|
||||
const linkTextRef = useRef(null);
|
||||
const [showEmbed, setShowEmbed] = useState(false);
|
||||
|
||||
const surveyUrl = useMemo(() => surveyBaseUrl + survey.id, [survey]);
|
||||
const surveyUrl = useMemo(() => surveyBaseUrl + survey.id, [survey.id, surveyBaseUrl]);
|
||||
|
||||
const iframeCode = `<div style="position: relative; height:100vh; max-height:100vh;
|
||||
overflow:auto;">
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import SurveyStatusDropdown from "@/components/shared/SurveyStatusDropdown";
|
||||
import { useEnvironment } from "@/lib/environments/environments";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { ErrorComponent } from "@formbricks/ui";
|
||||
|
||||
interface StatusDropdownProps {
|
||||
survey: TSurvey;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export default function StatusDropdown({ survey, environmentId }: StatusDropdownProps) {
|
||||
const { environment, isLoadingEnvironment, isErrorEnvironment } = useEnvironment(environmentId);
|
||||
|
||||
if (isLoadingEnvironment) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (isErrorEnvironment) {
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{environment.widgetSetupCompleted || survey.type === "link" ? (
|
||||
<SurveyStatusDropdown surveyId={survey.id} environmentId={environmentId} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { useEnvironment } from "@/lib/environments/environments";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { Confetti } from "@formbricks/ui";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import LinkSurveyModal from "./LinkSurveyModal";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
|
||||
interface SummaryMetadataProps {
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
surveyBaseUrl: string;
|
||||
}
|
||||
|
||||
export default function SuccessMessage({ environmentId, survey, surveyBaseUrl }: SummaryMetadataProps) {
|
||||
const { environment } = useEnvironment(environmentId);
|
||||
export default function SuccessMessage({ environment, survey, surveyBaseUrl }: SummaryMetadataProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const [showLinkModal, setShowLinkModal] = useState(false);
|
||||
const [confetti, setConfetti] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (environment) {
|
||||
const newSurveyParam = searchParams?.get("success");
|
||||
if (newSurveyParam && survey && environment) {
|
||||
setConfetti(true);
|
||||
toast.success(
|
||||
survey.type === "web" && !environment.widgetSetupCompleted
|
||||
? "Almost there! Install widget to start receiving responses."
|
||||
: "Congrats! Your survey is live.",
|
||||
{
|
||||
icon: survey.type === "web" && !environment.widgetSetupCompleted ? "🤏" : "🎉",
|
||||
duration: 5000,
|
||||
position: "bottom-right",
|
||||
}
|
||||
);
|
||||
if (survey.type === "link") {
|
||||
setShowLinkModal(true);
|
||||
const newSurveyParam = searchParams?.get("success");
|
||||
if (newSurveyParam && survey && environment) {
|
||||
setConfetti(true);
|
||||
toast.success(
|
||||
survey.type === "web" && !environment.widgetSetupCompleted
|
||||
? "Almost there! Install widget to start receiving responses."
|
||||
: "Congrats! Your survey is live.",
|
||||
{
|
||||
icon: survey.type === "web" && !environment.widgetSetupCompleted ? "🤏" : "🎉",
|
||||
duration: 5000,
|
||||
position: "bottom-right",
|
||||
}
|
||||
// Remove success param from url
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete("success");
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
);
|
||||
if (survey.type === "link") {
|
||||
setShowLinkModal(true);
|
||||
}
|
||||
// Remove success param from url
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete("success");
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
}
|
||||
}, [environment, searchParams, survey]);
|
||||
|
||||
|
||||
@@ -20,14 +20,15 @@ import NPSSummary from "./NPSSummary";
|
||||
import OpenTextSummary from "./OpenTextSummary";
|
||||
import RatingSummary from "./RatingSummary";
|
||||
import EmptyInAppSurveys from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
|
||||
interface SummaryListProps {
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
responses: TResponse[];
|
||||
}
|
||||
|
||||
export default function SummaryList({ environmentId, survey, responses }: SummaryListProps) {
|
||||
export default function SummaryList({ environment, survey, responses }: SummaryListProps) {
|
||||
const getSummaryData = (): QuestionSummary<TSurveyQuestion>[] =>
|
||||
survey.questions.map((question) => {
|
||||
const questionResponses = responses
|
||||
@@ -48,13 +49,13 @@ export default function SummaryList({ environmentId, survey, responses }: Summar
|
||||
<>
|
||||
<div className="mt-10 space-y-8">
|
||||
{survey.type === "web" && responses.length === 0 && (
|
||||
<EmptyInAppSurveys environmentId={environmentId} />
|
||||
<EmptyInAppSurveys environmentId={environment.id} />
|
||||
)}
|
||||
|
||||
{survey.type !== "web" && responses.length === 0 ? (
|
||||
<EmptySpaceFiller
|
||||
type="response"
|
||||
environmentId={environmentId}
|
||||
environment={environment}
|
||||
noWidgetRequired={survey.type === "link"}
|
||||
/>
|
||||
) : (
|
||||
@@ -65,7 +66,7 @@ export default function SummaryList({ environmentId, survey, responses }: Summar
|
||||
<OpenTextSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary as QuestionSummary<TSurveyOpenTextQuestion>}
|
||||
environmentId={environmentId}
|
||||
environmentId={environment.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -81,7 +82,7 @@ export default function SummaryList({ environmentId, survey, responses }: Summar
|
||||
TSurveyMultipleChoiceMultiQuestion | TSurveyMultipleChoiceSingleQuestion
|
||||
>
|
||||
}
|
||||
environmentId={environmentId}
|
||||
environmentId={environment.id}
|
||||
surveyType={survey.type}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -12,16 +12,29 @@ import { TResponse } from "@formbricks/types/v1/responses";
|
||||
import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
|
||||
interface SummaryPageProps {
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
survey: TSurveyWithAnalytics;
|
||||
surveyId: string;
|
||||
responses: TResponse[];
|
||||
surveyBaseUrl: string;
|
||||
product: TProduct;
|
||||
environmentTags: TTag[];
|
||||
}
|
||||
|
||||
const SummaryPage = ({ environmentId, survey, surveyId, responses, surveyBaseUrl }: SummaryPageProps) => {
|
||||
const SummaryPage = ({
|
||||
environment,
|
||||
survey,
|
||||
surveyId,
|
||||
responses,
|
||||
surveyBaseUrl,
|
||||
product,
|
||||
environmentTags,
|
||||
}: SummaryPageProps) => {
|
||||
const { selectedFilter, dateRange, resetState } = useResponseFilter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -30,6 +43,7 @@ const SummaryPage = ({ environmentId, survey, surveyId, responses, surveyBaseUrl
|
||||
resetState();
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
// get the filtered array when the selected filter value changes
|
||||
const filterResponses: TResponse[] = useMemo(() => {
|
||||
return getFilterResponses(responses, selectedFilter, survey, dateRange);
|
||||
@@ -38,20 +52,21 @@ const SummaryPage = ({ environmentId, survey, surveyId, responses, surveyBaseUrl
|
||||
return (
|
||||
<ContentWrapper>
|
||||
<SummaryHeader
|
||||
environmentId={environmentId}
|
||||
environment={environment}
|
||||
survey={survey}
|
||||
surveyId={surveyId}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
product={product}
|
||||
/>
|
||||
<CustomFilter
|
||||
environmentId={environmentId}
|
||||
environmentTags={environmentTags}
|
||||
responses={filterResponses}
|
||||
survey={survey}
|
||||
totalResponses={responses}
|
||||
/>
|
||||
<SurveyResultsTabs activeId="summary" environmentId={environmentId} surveyId={surveyId} />
|
||||
<SurveyResultsTabs activeId="summary" environmentId={environment.id} surveyId={surveyId} />
|
||||
<SummaryMetadata responses={filterResponses} survey={survey} />
|
||||
<SummaryList responses={filterResponses} survey={survey} environmentId={environmentId} />
|
||||
<SummaryList responses={filterResponses} survey={survey} environment={environment} />
|
||||
</ContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,9 @@ import { getAnalysisData } from "@/app/(app)/environments/[environmentId]/survey
|
||||
import SummaryPage from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { REVALIDATION_INTERVAL, SURVEY_BASE_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/services/tag";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
export default async function Page({ params }) {
|
||||
@@ -12,17 +15,31 @@ export default async function Page({ params }) {
|
||||
if (!session) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
const { responses, survey } = await getAnalysisData(params.surveyId, params.environmentId);
|
||||
const [{ responses, survey }, environment] = await Promise.all([
|
||||
getAnalysisData(params.surveyId, params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
const product = await getProductByEnvironmentId(environment.id);
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
const tags = await getTagsByEnvironmentId(params.environmentId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResponsesLimitReachedBanner environmentId={params.environmentId} surveyId={params.surveyId} />
|
||||
<SummaryPage
|
||||
environmentId={params.environmentId}
|
||||
environment={environment}
|
||||
responses={responses}
|
||||
survey={survey}
|
||||
surveyId={params.surveyId}
|
||||
surveyBaseUrl={SURVEY_BASE_URL}
|
||||
product={product}
|
||||
environmentTags={tags}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import ResponseFilter from "./ResponseFilter";
|
||||
import { DateRange, useResponseFilter } from "@/app/(app)/environments/[environmentId]/ResponseFilterContext";
|
||||
import { useTagsForEnvironment } from "@/lib/tags/tags";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
|
||||
enum DateSelected {
|
||||
FROM = "from",
|
||||
@@ -44,7 +44,7 @@ enum FilterDropDownLabels {
|
||||
}
|
||||
|
||||
interface CustomFilterProps {
|
||||
environmentId: string;
|
||||
environmentTags: TTag[];
|
||||
survey: TSurvey;
|
||||
responses: TResponse[];
|
||||
totalResponses: TResponse[];
|
||||
@@ -61,7 +61,7 @@ const getDifferenceOfDays = (from, to) => {
|
||||
}
|
||||
};
|
||||
|
||||
const CustomFilter = ({ environmentId, responses, survey, totalResponses }: CustomFilterProps) => {
|
||||
const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: CustomFilterProps) => {
|
||||
const { setSelectedOptions, dateRange, setDateRange } = useResponseFilter();
|
||||
const [filterRange, setFilterRange] = useState<FilterDropDownLabels>(
|
||||
dateRange.from && dateRange.to
|
||||
@@ -73,7 +73,6 @@ const CustomFilter = ({ environmentId, responses, survey, totalResponses }: Cust
|
||||
const [isFilterDropDownOpen, setIsFilterDropDownOpen] = useState<boolean>(false);
|
||||
const [isDownloadDropDownOpen, setIsDownloadDropDownOpen] = useState<boolean>(false);
|
||||
const [hoveredRange, setHoveredRange] = useState<DateRange | null>(null);
|
||||
const { data: environmentTags } = useTagsForEnvironment(environmentId);
|
||||
|
||||
// when the page loads we get total responses and iterate over the responses and questions, tags and attributes to create the filter options
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { QuestionType } from "@formbricks/types/questions";
|
||||
import { TSurveyQuestionType } from "@formbricks/types/v1/surveys";
|
||||
import QuestionsComboBox, { QuestionOption, OptionsType } from "./QuestionsComboBox";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Popover, PopoverTrigger, PopoverContent, Button, Checkbox } from "@formbricks/ui";
|
||||
import { ChevronDown, ChevronUp, Plus } from "lucide-react";
|
||||
import { TrashIcon } from "@heroicons/react/24/solid";
|
||||
@@ -11,7 +11,7 @@ import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/Resp
|
||||
import clsx from "clsx";
|
||||
|
||||
export type QuestionFilterOptions = {
|
||||
type: QuestionType | "Attributes" | "Tags";
|
||||
type: TSurveyQuestionType | "Attributes" | "Tags";
|
||||
filterOptions: string[];
|
||||
filterComboBoxOptions: string[];
|
||||
id: string;
|
||||
@@ -47,6 +47,14 @@ const ResponseFilter = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// when filter is opened and added a filter without selecting any option clear out that value
|
||||
const clearItem = () => {
|
||||
setSelectedFilter({
|
||||
filter: [...selectedFilter.filter.filter((s) => s.questionType.hasOwnProperty("label"))],
|
||||
onlyComplete: selectedFilter.onlyComplete,
|
||||
});
|
||||
};
|
||||
|
||||
// remove the added filter if nothing is selected when filter is closed
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
@@ -76,14 +84,6 @@ const ResponseFilter = () => {
|
||||
setSelectedFilter({ ...selectedFilter });
|
||||
};
|
||||
|
||||
// when filter is opened and added a filter without selecting any option clear out that value
|
||||
const clearItem = () => {
|
||||
setSelectedFilter({
|
||||
filter: [...selectedFilter.filter.filter((s) => s.questionType.hasOwnProperty("label"))],
|
||||
onlyComplete: selectedFilter.onlyComplete,
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnChangeFilterComboBoxValue = (o: string | string[], index: number) => {
|
||||
selectedFilter.filter[index] = {
|
||||
...selectedFilter.filter[index],
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useEnvironment } from "@/lib/environments/environments";
|
||||
import { useProduct } from "@/lib/products/products";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import {
|
||||
Button,
|
||||
@@ -15,41 +13,32 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
ErrorComponent,
|
||||
} from "@formbricks/ui";
|
||||
import { PencilSquareIcon, EllipsisHorizontalIcon } from "@heroicons/react/24/solid";
|
||||
import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator";
|
||||
import { useSurveyMutation } from "@/lib/surveys/mutateSurveys";
|
||||
import toast from "react-hot-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import SuccessMessage from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage";
|
||||
import LinkSurveyShareButton from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkModalButton";
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import SurveyStatusDropdown from "@/components/shared/SurveyStatusDropdown";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { surveyMutateAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||
|
||||
interface SummaryHeaderProps {
|
||||
surveyId: string;
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
surveyBaseUrl: string;
|
||||
product: TProduct;
|
||||
}
|
||||
const SummaryHeader = ({ surveyId, environmentId, survey, surveyBaseUrl }: SummaryHeaderProps) => {
|
||||
const SummaryHeader = ({ surveyId, environment, survey, surveyBaseUrl, product }: SummaryHeaderProps) => {
|
||||
const router = useRouter();
|
||||
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
|
||||
const { environment, isLoadingEnvironment, isErrorEnvironment } = useEnvironment(environmentId);
|
||||
const { triggerSurveyMutate } = useSurveyMutation(environmentId, surveyId);
|
||||
|
||||
const isCloseOnDateEnabled = survey.closeOnDate !== null;
|
||||
const closeOnDate = survey.closeOnDate ? new Date(survey.closeOnDate) : null;
|
||||
const isStatusChangeDisabled = (isCloseOnDateEnabled && closeOnDate && closeOnDate < new Date()) ?? false;
|
||||
|
||||
if (isLoadingProduct || isLoadingEnvironment) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (isErrorProduct || isErrorEnvironment) {
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
return (
|
||||
<div className="mb-11 mt-6 flex flex-wrap items-center justify-between">
|
||||
<div>
|
||||
@@ -59,12 +48,12 @@ const SummaryHeader = ({ surveyId, environmentId, survey, surveyBaseUrl }: Summa
|
||||
<div className="hidden justify-end gap-x-1.5 sm:flex">
|
||||
{survey.type === "link" && <LinkSurveyShareButton survey={survey} surveyBaseUrl={surveyBaseUrl} />}
|
||||
{(environment?.widgetSetupCompleted || survey.type === "link") && survey?.status !== "draft" ? (
|
||||
<SurveyStatusDropdown environmentId={environmentId} surveyId={surveyId} />
|
||||
<SurveyStatusDropdown environment={environment} survey={survey} />
|
||||
) : null}
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
className="h-full w-full px-3 lg:px-6"
|
||||
href={`/environments/${environmentId}/surveys/${surveyId}/edit`}>
|
||||
href={`/environments/${environment.id}/surveys/${surveyId}/edit`}>
|
||||
Edit
|
||||
<PencilSquareIcon className="ml-1 h-4" />
|
||||
</Button>
|
||||
@@ -94,7 +83,7 @@ const SummaryHeader = ({ surveyId, environmentId, survey, surveyBaseUrl }: Summa
|
||||
disabled={isStatusChangeDisabled}
|
||||
style={isStatusChangeDisabled ? { pointerEvents: "none", opacity: 0.5 } : {}}>
|
||||
<div className="flex items-center">
|
||||
<SurveyStatusIndicator status={survey.status} environmentId={environmentId} />
|
||||
<SurveyStatusIndicator status={survey.status} environment={environment} />
|
||||
<span className="ml-1 text-sm text-slate-700">
|
||||
{survey.status === "inProgress" && "In-progress"}
|
||||
{survey.status === "paused" && "Paused"}
|
||||
@@ -107,7 +96,8 @@ const SummaryHeader = ({ surveyId, environmentId, survey, surveyBaseUrl }: Summa
|
||||
<DropdownMenuRadioGroup
|
||||
value={survey.status}
|
||||
onValueChange={(value) => {
|
||||
triggerSurveyMutate({ status: value })
|
||||
const castedValue = value as "draft" | "inProgress" | "paused" | "completed";
|
||||
surveyMutateAction({ ...survey, status: castedValue })
|
||||
.then(() => {
|
||||
toast.success(
|
||||
value === "inProgress"
|
||||
@@ -152,14 +142,14 @@ const SummaryHeader = ({ surveyId, environmentId, survey, surveyBaseUrl }: Summa
|
||||
variant="darkCTA"
|
||||
size="sm"
|
||||
className="flex h-full w-full justify-center px-3 lg:px-6"
|
||||
href={`/environments/${environmentId}/surveys/${surveyId}/edit`}>
|
||||
href={`/environments/${environment.id}/surveys/${surveyId}/edit`}>
|
||||
Edit
|
||||
<PencilSquareIcon className="ml-1 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<SuccessMessage environmentId={environmentId} survey={survey} surveyBaseUrl={surveyBaseUrl} />
|
||||
<SuccessMessage environment={environment} survey={survey} surveyBaseUrl={surveyBaseUrl} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { useProduct } from "@/lib/products/products";
|
||||
import { getQuestionDefaults, questionTypes, universalQuestionPresets } from "@/lib/questions";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { ErrorComponent } from "@formbricks/ui";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { PlusIcon } from "@heroicons/react/24/solid";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
@@ -12,17 +10,12 @@ import { useState } from "react";
|
||||
|
||||
interface AddQuestionButtonProps {
|
||||
addQuestion: (question: any) => void;
|
||||
environmentId: string;
|
||||
product: TProduct;
|
||||
}
|
||||
|
||||
export default function AddQuestionButton({ addQuestion, environmentId }: AddQuestionButtonProps) {
|
||||
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
|
||||
|
||||
export default function AddQuestionButton({ addQuestion, product }: AddQuestionButtonProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
if (isLoadingProduct) return <LoadingSpinner />;
|
||||
if (isErrorProduct) return <ErrorComponent />;
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
|
||||
@@ -9,16 +9,17 @@ import AddQuestionButton from "./AddQuestionButton";
|
||||
import EditThankYouCard from "./EditThankYouCard";
|
||||
import QuestionCard from "./QuestionCard";
|
||||
import { StrictModeDroppable } from "./StrictModeDroppable";
|
||||
import { Question } from "@formbricks/types/questions";
|
||||
import { TSurveyQuestion } from "@formbricks/types/v1/surveys";
|
||||
import { validateQuestion } from "./Validation";
|
||||
import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
|
||||
interface QuestionsViewProps {
|
||||
localSurvey: TSurveyWithAnalytics;
|
||||
setLocalSurvey: (survey: TSurveyWithAnalytics) => void;
|
||||
activeQuestionId: string | null;
|
||||
setActiveQuestionId: (questionId: string | null) => void;
|
||||
environmentId: string;
|
||||
product: TProduct;
|
||||
invalidQuestions: String[] | null;
|
||||
setInvalidQuestions: (invalidQuestions: String[] | null) => void;
|
||||
}
|
||||
@@ -28,7 +29,7 @@ export default function QuestionsView({
|
||||
setActiveQuestionId,
|
||||
localSurvey,
|
||||
setLocalSurvey,
|
||||
environmentId,
|
||||
product,
|
||||
invalidQuestions,
|
||||
setInvalidQuestions,
|
||||
}: QuestionsViewProps) {
|
||||
@@ -58,7 +59,7 @@ export default function QuestionsView({
|
||||
};
|
||||
|
||||
// function to validate individual questions
|
||||
const validateSurvey = (question: Question) => {
|
||||
const validateSurvey = (question: TSurveyQuestion) => {
|
||||
// prevent this function to execute further if user hasnt still tried to save the survey
|
||||
if (invalidQuestions === null) {
|
||||
return;
|
||||
@@ -212,7 +213,7 @@ export default function QuestionsView({
|
||||
</StrictModeDroppable>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
<AddQuestionButton addQuestion={addQuestion} environmentId={environmentId} />
|
||||
<AddQuestionButton addQuestion={addQuestion} product={product} />
|
||||
<div className="mt-5">
|
||||
<EditThankYouCard
|
||||
localSurvey={localSurvey}
|
||||
|
||||
@@ -146,7 +146,7 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
|
||||
setCloseOnDate(localSurvey.closeOnDate);
|
||||
setSurveyCloseOnDateToggle(true);
|
||||
}
|
||||
}, []);
|
||||
}, [localSurvey]);
|
||||
|
||||
const handleCheckMark = () => {
|
||||
if (autoComplete) {
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function SurveyEditor({
|
||||
|
||||
// when the survey type changes, we need to reset the active question id to the first question
|
||||
useEffect(() => {
|
||||
if (localSurvey && localSurvey.questions?.length > 0) {
|
||||
if (localSurvey?.questions?.length && localSurvey.questions.length > 0) {
|
||||
setActiveQuestionId(localSurvey.questions[0].id);
|
||||
}
|
||||
}, [localSurvey?.type]);
|
||||
@@ -77,7 +77,7 @@ export default function SurveyEditor({
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
activeQuestionId={activeQuestionId}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
environmentId={environment.id}
|
||||
product={product}
|
||||
invalidQuestions={invalidQuestions}
|
||||
setInvalidQuestions={setInvalidQuestions}
|
||||
/>
|
||||
|
||||
@@ -205,8 +205,8 @@ export default function SurveyMenuBar({
|
||||
<div className="mt-3 flex sm:ml-4 sm:mt-0">
|
||||
<div className="mr-4 flex items-center">
|
||||
<SurveyStatusDropdown
|
||||
surveyId={localSurvey.id}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
environment={environment}
|
||||
updateLocalSurveyStatus={updateLocalSurveyStatus}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from "@formbricks/ui";
|
||||
import { CheckCircleIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/solid";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
|
||||
import { TActionClass } from "@formbricks/types/v1/actionClasses";
|
||||
|
||||
@@ -40,30 +40,36 @@ export default function WhenToSendCard({
|
||||
|
||||
const autoClose = localSurvey.autoClose !== null;
|
||||
|
||||
let newTrigger = {
|
||||
id: "", // Set the appropriate value for the id
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "",
|
||||
type: "code" as const, // Set the appropriate value for the type
|
||||
environmentId: "",
|
||||
description: null,
|
||||
noCodeConfig: null,
|
||||
};
|
||||
let newTrigger = useMemo(
|
||||
() => ({
|
||||
id: "", // Set the appropriate value for the id
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "",
|
||||
type: "code" as const, // Set the appropriate value for the type
|
||||
environmentId: "",
|
||||
description: null,
|
||||
noCodeConfig: null,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const addTriggerEvent = () => {
|
||||
const addTriggerEvent = useCallback(() => {
|
||||
const updatedSurvey = { ...localSurvey };
|
||||
updatedSurvey.triggers = [...localSurvey.triggers, newTrigger];
|
||||
setLocalSurvey(updatedSurvey);
|
||||
};
|
||||
}, [newTrigger, localSurvey, setLocalSurvey]);
|
||||
|
||||
const setTriggerEvent = (idx: number, actionClassId: string) => {
|
||||
const updatedSurvey = { ...localSurvey };
|
||||
updatedSurvey.triggers[idx] = actionClassArray!.find((actionClass) => {
|
||||
return actionClass.id === actionClassId;
|
||||
})!;
|
||||
setLocalSurvey(updatedSurvey);
|
||||
};
|
||||
const setTriggerEvent = useCallback(
|
||||
(idx: number, actionClassId: string) => {
|
||||
const updatedSurvey = { ...localSurvey };
|
||||
updatedSurvey.triggers[idx] = actionClassArray!.find((actionClass) => {
|
||||
return actionClass.id === actionClassId;
|
||||
})!;
|
||||
setLocalSurvey(updatedSurvey);
|
||||
},
|
||||
[actionClassArray, localSurvey, setLocalSurvey]
|
||||
);
|
||||
|
||||
const removeTriggerEvent = (idx: number) => {
|
||||
const updatedSurvey = { ...localSurvey };
|
||||
@@ -95,11 +101,12 @@ export default function WhenToSendCard({
|
||||
const updatedSurvey: TSurveyWithAnalytics = { ...localSurvey, delay: value };
|
||||
setLocalSurvey(updatedSurvey);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (activeIndex !== null) {
|
||||
setTriggerEvent(activeIndex, actionClassArray[actionClassArray.length - 1].id);
|
||||
}
|
||||
}, [actionClassArray]);
|
||||
}, [actionClassArray, activeIndex, setTriggerEvent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (localSurvey.type === "link") {
|
||||
@@ -112,7 +119,7 @@ export default function WhenToSendCard({
|
||||
if (localSurvey.triggers.length === 0) {
|
||||
addTriggerEvent();
|
||||
}
|
||||
}, []);
|
||||
}, [addTriggerEvent, localSurvey.triggers.length]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -2,6 +2,12 @@ import { SigninForm } from "@/components/auth/SigninForm";
|
||||
import Testimonial from "@/components/auth/Testimonial";
|
||||
import FormWrapper from "@/components/auth/FormWrapper";
|
||||
import { Metadata } from "next";
|
||||
import {
|
||||
GITHUB_OAUTH_ENABLED,
|
||||
GOOGLE_OAUTH_ENABLED,
|
||||
PASSWORD_RESET_DISABLED,
|
||||
SIGNUP_ENABLED,
|
||||
} from "@formbricks/lib/constants";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Login",
|
||||
@@ -16,7 +22,12 @@ export default function SignInPage() {
|
||||
</div>
|
||||
<div className="col-span-3 flex flex-col items-center justify-center">
|
||||
<FormWrapper>
|
||||
<SigninForm />
|
||||
<SigninForm
|
||||
publicSignUpEnabled={SIGNUP_ENABLED}
|
||||
passwordResetEnabled={!PASSWORD_RESET_DISABLED}
|
||||
googleOAuthEnabled={GOOGLE_OAUTH_ENABLED}
|
||||
githubOAuthEnabled={GITHUB_OAUTH_ENABLED}
|
||||
/>
|
||||
</FormWrapper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { SignupForm } from "@/components/auth/SignupForm";
|
||||
import FormWrapper from "@/components/auth/FormWrapper";
|
||||
import Testimonial from "@/components/auth/Testimonial";
|
||||
import { env } from "@/env.mjs";
|
||||
import {
|
||||
EMAIL_VERIFICATION_DISABLED,
|
||||
GITHUB_OAUTH_ENABLED,
|
||||
GOOGLE_OAUTH_ENABLED,
|
||||
INVITE_DISABLED,
|
||||
PASSWORD_RESET_DISABLED,
|
||||
PRIVACY_URL,
|
||||
SIGNUP_ENABLED,
|
||||
TERMS_URL,
|
||||
WEBAPP_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
|
||||
export default function SignUpPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const inviteToken = searchParams?.get("inviteToken");
|
||||
export default function SignUpPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
}) {
|
||||
const inviteToken = searchParams["inviteToken"] ?? null;
|
||||
|
||||
return (
|
||||
<div className="grid min-h-screen w-full bg-gradient-to-tr from-slate-100 to-slate-50 lg:grid-cols-5">
|
||||
@@ -18,9 +28,7 @@ export default function SignUpPage() {
|
||||
</div>
|
||||
<div className="col-span-3 flex flex-col items-center justify-center">
|
||||
<FormWrapper>
|
||||
{(
|
||||
inviteToken ? env.NEXT_PUBLIC_INVITE_DISABLED === "1" : env.NEXT_PUBLIC_SIGNUP_DISABLED === "1"
|
||||
) ? (
|
||||
{(inviteToken ? INVITE_DISABLED : !SIGNUP_ENABLED) ? (
|
||||
<>
|
||||
<h1 className="leading-2 mb-4 text-center font-bold">Sign up disabled</h1>
|
||||
<p className="text-center">
|
||||
@@ -35,7 +43,15 @@ export default function SignUpPage() {
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<SignupForm />
|
||||
<SignupForm
|
||||
webAppUrl={WEBAPP_URL}
|
||||
termsUrl={TERMS_URL}
|
||||
privacyUrl={PRIVACY_URL}
|
||||
passwordResetEnabled={!PASSWORD_RESET_DISABLED}
|
||||
emailVerificationDisabled={EMAIL_VERIFICATION_DISABLED}
|
||||
googleOAuthEnabled={GOOGLE_OAUTH_ENABLED}
|
||||
githubOAuthEnabled={GITHUB_OAUTH_ENABLED}
|
||||
/>
|
||||
)}
|
||||
</FormWrapper>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { sendInviteAcceptedEmail } from "@/lib/email";
|
||||
import { verifyInviteToken } from "@formbricks/lib/jwt";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { env } from "process";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import {
|
||||
NotLoggedInContent,
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
UsedContent,
|
||||
RightAccountContent,
|
||||
} from "./InviteContentComponents";
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
export default async function JoinTeam({ searchParams }) {
|
||||
const currentUser = await getServerSession(authOptions);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { env } from "@/env.mjs";
|
||||
import { verifyPassword } from "@/lib/auth";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { EMAIL_VERIFICATION_DISABLED, INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { verifyToken } from "@formbricks/lib/jwt";
|
||||
import { getProfileByEmail } from "@formbricks/lib/services/profile";
|
||||
import type { IdentityProvider } from "@prisma/client";
|
||||
@@ -166,7 +166,7 @@ export const authOptions: NextAuthOptions = {
|
||||
},
|
||||
async signIn({ user, account }: any) {
|
||||
if (account.provider === "credentials" || account.provider === "token") {
|
||||
if (!user.emailVerified && env.NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED !== "1") {
|
||||
if (!user.emailVerified && !EMAIL_VERIFICATION_DISABLED) {
|
||||
return `/auth/verification-requested?email=${encodeURIComponent(user.email)}`;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { env } from "@/env.mjs";
|
||||
import { responses } from "@/lib/api/response";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { CRON_SECRET } from "@formbricks/lib/constants";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export async function POST() {
|
||||
const headersList = headers();
|
||||
const apiKey = headersList.get("x-api-key");
|
||||
|
||||
if (!apiKey || apiKey !== env.CRON_SECRET) {
|
||||
if (!apiKey || apiKey !== CRON_SECRET) {
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { env } from "@/env.mjs";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import {
|
||||
GOOGLE_SHEETS_CLIENT_ID,
|
||||
WEBAPP_URL,
|
||||
GOOGLE_SHEETS_CLIENT_SECRET,
|
||||
GOOGLE_SHEETS_REDIRECT_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { google } from "googleapis";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
@@ -18,9 +22,9 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json({ message: "`code` must be a string" }, { status: 400 });
|
||||
}
|
||||
|
||||
const client_id = env.GOOGLE_SHEETS_CLIENT_ID;
|
||||
const client_secret = env.GOOGLE_SHEETS_CLIENT_SECRET;
|
||||
const redirect_uri = env.GOOGLE_SHEETS_REDIRECT_URL;
|
||||
const client_id = GOOGLE_SHEETS_CLIENT_ID;
|
||||
const client_secret = GOOGLE_SHEETS_CLIENT_SECRET;
|
||||
const redirect_uri = GOOGLE_SHEETS_REDIRECT_URL;
|
||||
if (!client_id) return NextResponse.json({ Error: "Google client id is missing" }, { status: 400 });
|
||||
if (!client_secret) return NextResponse.json({ Error: "Google client secret is missing" }, { status: 400 });
|
||||
if (!redirect_uri) return NextResponse.json({ Error: "Google redirect url is missing" }, { status: 400 });
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { hasUserEnvironmentAccess } from "@/lib/api/apiHelper";
|
||||
import { env } from "@/env.mjs";
|
||||
import {
|
||||
GOOGLE_SHEETS_CLIENT_ID,
|
||||
GOOGLE_SHEETS_CLIENT_SECRET,
|
||||
GOOGLE_SHEETS_REDIRECT_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { google } from "googleapis";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
@@ -24,9 +28,9 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json({ Error: "You dont have access to environment" }, { status: 401 });
|
||||
}
|
||||
|
||||
const client_id = env.GOOGLE_SHEETS_CLIENT_ID;
|
||||
const client_secret = env.GOOGLE_SHEETS_CLIENT_SECRET;
|
||||
const redirect_uri = env.GOOGLE_SHEETS_REDIRECT_URL;
|
||||
const client_id = GOOGLE_SHEETS_CLIENT_ID;
|
||||
const client_secret = GOOGLE_SHEETS_CLIENT_SECRET;
|
||||
const redirect_uri = GOOGLE_SHEETS_REDIRECT_URL;
|
||||
if (!client_id) return NextResponse.json({ Error: "Google client id is missing" }, { status: 400 });
|
||||
if (!client_secret) return NextResponse.json({ Error: "Google client secret is missing" }, { status: 400 });
|
||||
if (!redirect_uri) return NextResponse.json({ Error: "Google redirect url is missing" }, { status: 400 });
|
||||
|
||||
@@ -3,13 +3,18 @@ import { verifyInviteToken } from "@formbricks/lib/jwt";
|
||||
import { populateEnvironment } from "@/lib/populate";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { NextResponse } from "next/server";
|
||||
import { env } from "@/env.mjs";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import {
|
||||
EMAIL_VERIFICATION_DISABLED,
|
||||
INTERNAL_SECRET,
|
||||
INVITE_DISABLED,
|
||||
SIGNUP_ENABLED,
|
||||
WEBAPP_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
let { inviteToken, ...user } = await request.json();
|
||||
if (inviteToken ? env.NEXT_PUBLIC_INVITE_DISABLED === "1" : env.NEXT_PUBLIC_SIGNUP_DISABLED === "1") {
|
||||
if (inviteToken ? INVITE_DISABLED : !SIGNUP_ENABLED) {
|
||||
return NextResponse.json({ error: "Signup disabled" }, { status: 403 });
|
||||
}
|
||||
user = { ...user, ...{ email: user.email.toLowerCase() } };
|
||||
@@ -117,7 +122,7 @@ export async function POST(request: Request) {
|
||||
await prisma.invite.delete({ where: { id: inviteId } });
|
||||
}
|
||||
|
||||
if (env.NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED !== "1") {
|
||||
if (!EMAIL_VERIFICATION_DISABLED) {
|
||||
await sendVerificationEmail(userData);
|
||||
}
|
||||
return NextResponse.json(userData);
|
||||
|
||||
@@ -1,28 +1,10 @@
|
||||
import ClientLogout from "@/app/ClientLogout";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironmentByUser } from "@formbricks/lib/services/environment";
|
||||
import type { Session } from "next-auth";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
async function getEnvironment() {
|
||||
const cookie = headers().get("cookie") || "";
|
||||
const res = await fetch(`${WEBAPP_URL}/api/v1/environments/find-first`, {
|
||||
headers: {
|
||||
cookie,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
console.error(error);
|
||||
throw new Error("Failed to fetch data");
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export default async function Home() {
|
||||
const session: Session | null = await getServerSession(authOptions);
|
||||
|
||||
@@ -36,7 +18,7 @@ export default async function Home() {
|
||||
|
||||
let environment;
|
||||
try {
|
||||
environment = await getEnvironment();
|
||||
environment = await getEnvironmentByUser(session?.user);
|
||||
} catch (error) {
|
||||
console.error("error getting environment", error);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { env } from "@/env.mjs";
|
||||
import { IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function LegalFooter() {
|
||||
if (!env.NEXT_PUBLIC_IMPRINT_URL && !env.NEXT_PUBLIC_PRIVACY_URL) return null;
|
||||
if (!IMPRINT_URL && !PRIVACY_URL) return null;
|
||||
return (
|
||||
<div className="h-10 w-full border-t border-slate-200">
|
||||
<div className="mx-auto max-w-lg p-3 text-center text-sm text-slate-400">
|
||||
{env.NEXT_PUBLIC_IMPRINT_URL && (
|
||||
<Link href={env.NEXT_PUBLIC_IMPRINT_URL} target="_blank">
|
||||
{IMPRINT_URL && (
|
||||
<Link href={IMPRINT_URL} target="_blank">
|
||||
Imprint
|
||||
</Link>
|
||||
)}
|
||||
{env.NEXT_PUBLIC_IMPRINT_URL && env.NEXT_PUBLIC_PRIVACY_URL && <span> | </span>}
|
||||
{env.NEXT_PUBLIC_PRIVACY_URL && (
|
||||
<Link href={env.NEXT_PUBLIC_PRIVACY_URL} target="_blank">
|
||||
{IMPRINT_URL && PRIVACY_URL && <span> | </span>}
|
||||
{PRIVACY_URL && (
|
||||
<Link href={PRIVACY_URL} target="_blank">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import ContentWrapper from "@/components/shared/ContentWrapper";
|
||||
import { SurveyInline } from "@/components/shared/Survey";
|
||||
import { createDisplay } from "@formbricks/lib/client/display";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { ResponseQueue } from "@formbricks/lib/responseQueue";
|
||||
import { SurveyState } from "@formbricks/lib/surveyState";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
@@ -21,6 +20,7 @@ interface LinkSurveyProps {
|
||||
personId?: string;
|
||||
emailVerificationStatus?: string;
|
||||
prefillAnswer?: string;
|
||||
webAppUrl: string;
|
||||
}
|
||||
|
||||
export default function LinkSurvey({
|
||||
@@ -29,6 +29,7 @@ export default function LinkSurvey({
|
||||
personId,
|
||||
emailVerificationStatus,
|
||||
prefillAnswer,
|
||||
webAppUrl,
|
||||
}: LinkSurveyProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const isPreview = searchParams?.get("preview") === "true";
|
||||
@@ -42,7 +43,7 @@ export default function LinkSurvey({
|
||||
() =>
|
||||
new ResponseQueue(
|
||||
{
|
||||
apiHost: WEBAPP_URL,
|
||||
apiHost: webAppUrl,
|
||||
retryAttempts: 2,
|
||||
onResponseSendingFailed: (response) => {
|
||||
alert(`Failed to send response: ${JSON.stringify(response, null, 2)}`);
|
||||
@@ -52,7 +53,7 @@ export default function LinkSurvey({
|
||||
},
|
||||
surveyState
|
||||
),
|
||||
[]
|
||||
[personId, webAppUrl]
|
||||
);
|
||||
const [autoFocus, setAutofocus] = useState(false);
|
||||
|
||||
@@ -63,6 +64,10 @@ export default function LinkSurvey({
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
responseQueue.updateSurveyState(surveyState);
|
||||
}, [responseQueue, surveyState]);
|
||||
|
||||
if (emailVerificationStatus && emailVerificationStatus !== "verified") {
|
||||
if (emailVerificationStatus === "fishy") {
|
||||
return <VerifyEmail survey={survey} isErrorComponent={true} />;
|
||||
@@ -89,9 +94,16 @@ export default function LinkSurvey({
|
||||
survey={survey}
|
||||
brandColor={product.brandColor}
|
||||
formbricksSignature={product.formbricksSignature}
|
||||
onDisplay={() => createDisplay({ surveyId: survey.id }, window?.location?.origin)}
|
||||
onDisplay={async () => {
|
||||
if (!isPreview) {
|
||||
const { id } = await createDisplay({ surveyId: survey.id }, window?.location?.origin);
|
||||
const newSurveyState = surveyState.copy();
|
||||
newSurveyState.updateDisplayId(id);
|
||||
setSurveyState(newSurveyState);
|
||||
}
|
||||
}}
|
||||
onResponse={(responseUpdate) => {
|
||||
responseQueue.add(responseUpdate);
|
||||
!isPreview && responseQueue.add(responseUpdate);
|
||||
}}
|
||||
onActiveQuestionChange={(questionId) => setActiveQuestionId(questionId)}
|
||||
activeQuestionId={activeQuestionId}
|
||||
|
||||
@@ -2,7 +2,7 @@ export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import LinkSurvey from "@/app/s/[surveyId]/LinkSurvey";
|
||||
import SurveyInactive from "@/app/s/[surveyId]/SurveyInactive";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { REVALIDATION_INTERVAL, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getOrCreatePersonByUserId } from "@formbricks/lib/services/person";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getSurvey } from "@formbricks/lib/services/survey";
|
||||
@@ -59,6 +59,7 @@ export default async function LinkSurveyPage({ params, searchParams }) {
|
||||
personId={person?.id}
|
||||
emailVerificationStatus={emailVerificationStatus}
|
||||
prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { GoogleButton } from "@/components/auth/GoogleButton";
|
||||
import { env } from "@/env.mjs";
|
||||
import { Button, PasswordInput } from "@formbricks/ui";
|
||||
import { XCircleIcon } from "@heroicons/react/24/solid";
|
||||
import { signIn } from "next-auth/react";
|
||||
@@ -10,7 +9,17 @@ import { useSearchParams } from "next/navigation";
|
||||
import { useRef, useState } from "react";
|
||||
import { GithubButton } from "./GithubButton";
|
||||
|
||||
export const SigninForm = () => {
|
||||
export const SigninForm = ({
|
||||
publicSignUpEnabled,
|
||||
passwordResetEnabled,
|
||||
googleOAuthEnabled,
|
||||
githubOAuthEnabled,
|
||||
}: {
|
||||
publicSignUpEnabled: boolean;
|
||||
passwordResetEnabled: boolean;
|
||||
googleOAuthEnabled: boolean;
|
||||
githubOAuthEnabled: boolean;
|
||||
}) => {
|
||||
const searchParams = useSearchParams();
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -79,7 +88,7 @@ export const SigninForm = () => {
|
||||
className="focus:border-brand focus:ring-brand block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
{env.NEXT_PUBLIC_PASSWORD_RESET_DISABLED !== "1" && isPasswordFocused && (
|
||||
{passwordResetEnabled && isPasswordFocused && (
|
||||
<div className="ml-1 text-right transition-all duration-500 ease-in-out">
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
@@ -109,18 +118,18 @@ export const SigninForm = () => {
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED === "1" && (
|
||||
{googleOAuthEnabled && (
|
||||
<>
|
||||
<GoogleButton inviteUrl={callbackUrl} />
|
||||
</>
|
||||
)}
|
||||
{env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED === "1" && (
|
||||
{githubOAuthEnabled && (
|
||||
<>
|
||||
<GithubButton inviteUrl={callbackUrl} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{env.NEXT_PUBLIC_SIGNUP_DISABLED !== "1" && (
|
||||
{publicSignUpEnabled && (
|
||||
<div className="mt-9 text-center text-xs ">
|
||||
<span className="leading-5 text-slate-500">New to Formbricks?</span>
|
||||
<br />
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { GoogleButton } from "@/components/auth/GoogleButton";
|
||||
import IsPasswordValid from "@/components/auth/IsPasswordValid";
|
||||
import { env } from "@/env.mjs";
|
||||
import { createUser } from "@/lib/users/users";
|
||||
import { Button, PasswordInput } from "@formbricks/ui";
|
||||
import { XCircleIcon } from "@heroicons/react/24/solid";
|
||||
@@ -11,7 +10,23 @@ import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { GithubButton } from "./GithubButton";
|
||||
|
||||
export const SignupForm = () => {
|
||||
export const SignupForm = ({
|
||||
webAppUrl,
|
||||
privacyUrl,
|
||||
termsUrl,
|
||||
passwordResetEnabled,
|
||||
emailVerificationDisabled,
|
||||
googleOAuthEnabled,
|
||||
githubOAuthEnabled,
|
||||
}: {
|
||||
webAppUrl: string;
|
||||
privacyUrl: string | undefined;
|
||||
termsUrl: string | undefined;
|
||||
passwordResetEnabled: boolean;
|
||||
emailVerificationDisabled: boolean;
|
||||
googleOAuthEnabled: boolean;
|
||||
githubOAuthEnabled: boolean;
|
||||
}) => {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const [error, setError] = useState<string>("");
|
||||
@@ -19,15 +34,13 @@ export const SignupForm = () => {
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const inviteToken = searchParams?.get("inviteToken");
|
||||
// const callbackUrl = env.NEXT_PUBLIC_WEBAPP_URL + "/invite?token=" + inviteToken;
|
||||
|
||||
const callbackUrl = useMemo(() => {
|
||||
if (inviteToken) {
|
||||
return env.NEXT_PUBLIC_WEBAPP_URL + "/invite?token=" + inviteToken;
|
||||
return webAppUrl + "/invite?token=" + inviteToken;
|
||||
} else {
|
||||
return env.NEXT_PUBLIC_WEBAPP_URL;
|
||||
return webAppUrl;
|
||||
}
|
||||
}, [inviteToken]);
|
||||
}, [inviteToken, webAppUrl]);
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
@@ -45,10 +58,9 @@ export const SignupForm = () => {
|
||||
e.target.elements.password.value,
|
||||
inviteToken
|
||||
);
|
||||
const url =
|
||||
env.NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED === "1"
|
||||
? `/auth/signup-without-verification-success`
|
||||
: `/auth/verification-requested?email=${encodeURIComponent(e.target.elements.email.value)}`;
|
||||
const url = emailVerificationDisabled
|
||||
? `/auth/signup-without-verification-success`
|
||||
: `/auth/verification-requested?email=${encodeURIComponent(e.target.elements.email.value)}`;
|
||||
|
||||
router.push(url);
|
||||
} catch (e: any) {
|
||||
@@ -144,7 +156,7 @@ export const SignupForm = () => {
|
||||
className="focus:border-brand focus:ring-brand block w-full rounded-md shadow-sm sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
{env.NEXT_PUBLIC_PASSWORD_RESET_DISABLED !== "1" && isPasswordFocused && (
|
||||
{passwordResetEnabled && isPasswordFocused && (
|
||||
<div className="ml-1 text-right transition-all duration-500 ease-in-out">
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
@@ -176,38 +188,30 @@ export const SignupForm = () => {
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED === "1" && (
|
||||
{googleOAuthEnabled && (
|
||||
<>
|
||||
<GoogleButton inviteUrl={callbackUrl} />
|
||||
</>
|
||||
)}
|
||||
{env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED === "1" && (
|
||||
{githubOAuthEnabled && (
|
||||
<>
|
||||
<GithubButton inviteUrl={callbackUrl} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(env.NEXT_PUBLIC_TERMS_URL || env.NEXT_PUBLIC_PRIVACY_URL) && (
|
||||
{(termsUrl || privacyUrl) && (
|
||||
<div className="mt-3 text-center text-xs text-slate-500">
|
||||
By signing up, you agree to our
|
||||
<br />
|
||||
{env.NEXT_PUBLIC_TERMS_URL && (
|
||||
<Link
|
||||
className="font-semibold"
|
||||
href={env.NEXT_PUBLIC_TERMS_URL}
|
||||
rel="noreferrer"
|
||||
target="_blank">
|
||||
{termsUrl && (
|
||||
<Link className="font-semibold" href={termsUrl} rel="noreferrer" target="_blank">
|
||||
Terms of Service
|
||||
</Link>
|
||||
)}
|
||||
{env.NEXT_PUBLIC_TERMS_URL && env.NEXT_PUBLIC_PRIVACY_URL && <span> and </span>}
|
||||
{env.NEXT_PUBLIC_PRIVACY_URL && (
|
||||
<Link
|
||||
className="font-semibold"
|
||||
href={env.NEXT_PUBLIC_PRIVACY_URL}
|
||||
rel="noreferrer"
|
||||
target="_blank">
|
||||
{termsUrl && privacyUrl && <span> and </span>}
|
||||
{privacyUrl && (
|
||||
<Link className="font-semibold" href={privacyUrl} rel="noreferrer" target="_blank">
|
||||
Privacy Policy.
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -1,27 +1,39 @@
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import SurveyNavBarName from "@/components/shared/SurveyNavBarName";
|
||||
import Link from "next/link";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getSurvey } from "@formbricks/lib/services/survey";
|
||||
|
||||
interface SecondNavbarProps {
|
||||
tabs: { id: string; label: string; href: string; icon?: React.ReactNode }[];
|
||||
activeId: string;
|
||||
surveyId?: string;
|
||||
environmentId?: string;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export default function SecondNavbar({
|
||||
export default async function SecondNavbar({
|
||||
tabs,
|
||||
activeId,
|
||||
surveyId,
|
||||
environmentId,
|
||||
...props
|
||||
}: SecondNavbarProps) {
|
||||
const product = await getProductByEnvironmentId(environmentId!);
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
let survey;
|
||||
if (surveyId) {
|
||||
survey = await getSurvey(surveyId);
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
<div className="grid h-14 w-full grid-cols-3 items-center justify-items-stretch border-b bg-white px-4">
|
||||
<div className="justify-self-start">
|
||||
{surveyId && environmentId && (
|
||||
<SurveyNavBarName surveyId={surveyId} environmentId={environmentId} />
|
||||
{survey && environmentId && (
|
||||
<SurveyNavBarName surveyName={survey.name} productName={product.name} />
|
||||
)}
|
||||
</div>{" "}
|
||||
<nav className="flex h-full items-center space-x-4 justify-self-center" aria-label="Tabs">
|
||||
|
||||
@@ -2,27 +2,21 @@
|
||||
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { useEnvironment } from "@/lib/environments/environments";
|
||||
import LoadingSpinner from "./LoadingSpinner";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
|
||||
type EmptySpaceFillerProps = {
|
||||
type: "table" | "response" | "event" | "linkResponse" | "tag";
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
noWidgetRequired?: boolean;
|
||||
emptyMessage?: string;
|
||||
};
|
||||
|
||||
const EmptySpaceFiller: React.FC<EmptySpaceFillerProps> = ({
|
||||
type,
|
||||
environmentId,
|
||||
environment,
|
||||
noWidgetRequired,
|
||||
emptyMessage,
|
||||
}) => {
|
||||
const { environment, isErrorEnvironment, isLoadingEnvironment } = useEnvironment(environmentId);
|
||||
|
||||
if (isLoadingEnvironment) return <LoadingSpinner />;
|
||||
if (isErrorEnvironment) return <span>Error</span>;
|
||||
|
||||
if (type === "table") {
|
||||
return (
|
||||
<div className="group">
|
||||
@@ -34,7 +28,7 @@ const EmptySpaceFiller: React.FC<EmptySpaceFillerProps> = ({
|
||||
{!environment.widgetSetupCompleted && !noWidgetRequired && (
|
||||
<Link
|
||||
className="flex w-full items-center justify-center"
|
||||
href={`/environments/${environmentId}/settings/setup`}>
|
||||
href={`/environments/${environment.id}/settings/setup`}>
|
||||
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
|
||||
Install Formbricks Widget. <strong>Go to Setup Checklist 👉</strong>
|
||||
</span>
|
||||
@@ -63,7 +57,7 @@ const EmptySpaceFiller: React.FC<EmptySpaceFillerProps> = ({
|
||||
{!environment.widgetSetupCompleted && !noWidgetRequired && (
|
||||
<Link
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
href={`/environments/${environmentId}/settings/setup`}>
|
||||
href={`/environments/${environment.id}/settings/setup`}>
|
||||
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
|
||||
Install Formbricks Widget. <strong>Go to Setup Checklist 👉</strong>
|
||||
</span>
|
||||
@@ -94,7 +88,7 @@ const EmptySpaceFiller: React.FC<EmptySpaceFillerProps> = ({
|
||||
{!environment.widgetSetupCompleted && !noWidgetRequired && (
|
||||
<Link
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
href={`/environments/${environmentId}/settings/setup`}>
|
||||
href={`/environments/${environment.id}/settings/setup`}>
|
||||
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
|
||||
Install Formbricks Widget. <strong>Go to Setup Checklist 👉</strong>
|
||||
</span>
|
||||
@@ -122,7 +116,7 @@ const EmptySpaceFiller: React.FC<EmptySpaceFillerProps> = ({
|
||||
{!environment.widgetSetupCompleted && !noWidgetRequired && (
|
||||
<Link
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
href={`/environments/${environmentId}/settings/setup`}>
|
||||
href={`/environments/${environment.id}/settings/setup`}>
|
||||
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
|
||||
Install Formbricks Widget. <strong>Go to Setup Checklist 👉</strong>
|
||||
</span>
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useProduct } from "@/lib/products/products";
|
||||
import { useSurvey } from "@/lib/surveys/surveys";
|
||||
|
||||
interface SurveyNavBarNameProps {
|
||||
surveyId: string;
|
||||
environmentId: string;
|
||||
surveyName: string;
|
||||
productName: string;
|
||||
}
|
||||
|
||||
export default function SurveyNavBarName({ surveyId, environmentId }: SurveyNavBarNameProps) {
|
||||
const { survey, isLoadingSurvey, isErrorSurvey } = useSurvey(environmentId, surveyId);
|
||||
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
|
||||
|
||||
if (isLoadingSurvey || isLoadingProduct) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isErrorProduct || isErrorSurvey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function SurveyNavBarName({ surveyName, productName }: SurveyNavBarNameProps) {
|
||||
return (
|
||||
<div className="hidden items-center space-x-2 whitespace-nowrap md:flex">
|
||||
{/* <Button
|
||||
@@ -30,8 +14,8 @@ export default function SurveyNavBarName({ surveyId, environmentId }: SurveyNavB
|
||||
}}>
|
||||
Back
|
||||
</Button> */}
|
||||
<p className="pl-4 font-semibold">{product.name} / </p>
|
||||
<span>{survey.name}</span>
|
||||
<p className="pl-4 font-semibold">{productName} / </p>
|
||||
<span>{surveyName}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { surveyMutateAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||
import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator";
|
||||
import { useSurveyMutation } from "@/lib/surveys/mutateSurveys";
|
||||
import { useSurvey } from "@/lib/surveys/surveys";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import {
|
||||
ErrorComponent,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
@@ -20,43 +19,32 @@ import { CheckCircleIcon, PauseCircleIcon, PlayCircleIcon } from "@heroicons/rea
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export default function SurveyStatusDropdown({
|
||||
surveyId,
|
||||
environmentId,
|
||||
environment,
|
||||
updateLocalSurveyStatus,
|
||||
survey,
|
||||
}: {
|
||||
surveyId: string;
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
updateLocalSurveyStatus?: (status: "draft" | "inProgress" | "paused" | "completed" | "archived") => void;
|
||||
survey: TSurvey;
|
||||
}) {
|
||||
const { survey, isLoadingSurvey, isErrorSurvey } = useSurvey(environmentId, surveyId);
|
||||
const { triggerSurveyMutate } = useSurveyMutation(environmentId, surveyId);
|
||||
|
||||
if (isLoadingSurvey) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (isErrorSurvey) {
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
|
||||
const isCloseOnDateEnabled = survey.closeOnDate !== null;
|
||||
const closeOnDate = survey.closeOnDate ? new Date(survey.closeOnDate) : null;
|
||||
const isStatusChangeDisabled = (isCloseOnDateEnabled && closeOnDate && closeOnDate < new Date()) ?? false;
|
||||
|
||||
return (
|
||||
<>
|
||||
{survey.status === "draft" || survey.status === "archived" ? (
|
||||
{survey.status === "draft" ? (
|
||||
<div className="flex items-center">
|
||||
<SurveyStatusIndicator status={survey.status} environmentId={environmentId} />
|
||||
<SurveyStatusIndicator status={survey.status} environment={environment} />
|
||||
{survey.status === "draft" && <p className="text-sm italic text-slate-600">Draft</p>}
|
||||
{survey.status === "archived" && <p className="text-sm italic text-slate-600">Archived</p>}
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
value={survey.status}
|
||||
disabled={isStatusChangeDisabled}
|
||||
onValueChange={(value) => {
|
||||
triggerSurveyMutate({ status: value })
|
||||
const castedValue = value as "draft" | "inProgress" | "paused" | "completed";
|
||||
surveyMutateAction({ ...survey, status: castedValue })
|
||||
.then(() => {
|
||||
toast.success(
|
||||
value === "inProgress"
|
||||
@@ -81,13 +69,11 @@ export default function SurveyStatusDropdown({
|
||||
<SelectTrigger className="w-[170px] bg-white py-6 md:w-[200px]">
|
||||
<SelectValue>
|
||||
<div className="flex items-center">
|
||||
<SurveyStatusIndicator status={survey.status} environmentId={environmentId} />
|
||||
<SurveyStatusIndicator status={survey.status} environment={environment} />
|
||||
<span className="ml-2 text-sm text-slate-700">
|
||||
{survey.status === "draft" && "Draft"}
|
||||
{survey.status === "inProgress" && "In-progress"}
|
||||
{survey.status === "paused" && "Paused"}
|
||||
{survey.status === "completed" && "Completed"}
|
||||
{survey.status === "archived" && "Archived"}
|
||||
</span>
|
||||
</div>
|
||||
</SelectValue>
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { useEnvironment } from "@/lib/environments/environments";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui";
|
||||
import { ArchiveBoxIcon, CheckIcon, PauseIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
interface SurveyStatusIndicatorProps {
|
||||
status: string;
|
||||
tooltip?: boolean;
|
||||
environmentId: string;
|
||||
environment: TEnvironment;
|
||||
}
|
||||
|
||||
export default function SurveyStatusIndicator({
|
||||
status,
|
||||
tooltip,
|
||||
environmentId,
|
||||
}: SurveyStatusIndicatorProps) {
|
||||
const { environment, isErrorEnvironment, isLoadingEnvironment } = useEnvironment(environmentId);
|
||||
|
||||
if (isLoadingEnvironment) return <></>;
|
||||
if (isErrorEnvironment) return <></>;
|
||||
|
||||
export default function SurveyStatusIndicator({ status, tooltip, environment }: SurveyStatusIndicatorProps) {
|
||||
if (!environment.widgetSetupCompleted) return null;
|
||||
if (tooltip) {
|
||||
return (
|
||||
|
||||
@@ -25,38 +25,41 @@ export const env = createEnv({
|
||||
STRIPE_SECRET_KEY: z.string().optional(),
|
||||
STRIPE_WEBHOOK_SECRET: z.string().optional(),
|
||||
CRON_SECRET: z.string().optional(),
|
||||
EMAIL_VERIFICATION_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
PASSWORD_RESET_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
SIGNUP_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
PRIVACY_URL: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.or(z.string().refine((str) => str === "")),
|
||||
TERMS_URL: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.or(z.string().refine((str) => str === "")),
|
||||
IMPRINT_URL: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.or(z.string().refine((str) => str === "")),
|
||||
GITHUB_AUTH_ENABLED: z.enum(["1", "0"]).optional(),
|
||||
GOOGLE_AUTH_ENABLED: z.enum(["1", "0"]).optional(),
|
||||
INVITE_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(),
|
||||
VERCEL_URL: z.string().url().optional(),
|
||||
SURVEY_BASE_URL: z.string().optional(),
|
||||
GOOGLE_SHEETS_CLIENT_ID: z.string().optional(),
|
||||
GOOGLE_SHEETS_CLIENT_SECRET: z.string().optional(),
|
||||
GOOGLE_SHEETS_REDIRECT_URL: z.string().optional(),
|
||||
},
|
||||
|
||||
/*
|
||||
* Environment variables available on the client (and server).
|
||||
*
|
||||
* 💡 You'll get type errors if these are not prefixed with NEXT_PUBLIC_.
|
||||
*/
|
||||
client: {
|
||||
NEXT_PUBLIC_WEBAPP_URL: z.string().url().optional(),
|
||||
NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
NEXT_PUBLIC_PASSWORD_RESET_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
NEXT_PUBLIC_SIGNUP_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
NEXT_PUBLIC_INVITE_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
NEXT_PUBLIC_PRIVACY_URL: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.or(z.string().refine((str) => str === "")),
|
||||
NEXT_PUBLIC_TERMS_URL: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.or(z.string().refine((str) => str === "")),
|
||||
NEXT_PUBLIC_IMPRINT_URL: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.or(z.string().refine((str) => str === "")),
|
||||
NEXT_PUBLIC_GITHUB_AUTH_ENABLED: z.enum(["1", "0"]).optional(),
|
||||
NEXT_PUBLIC_GOOGLE_AUTH_ENABLED: z.enum(["1", "0"]).optional(),
|
||||
NEXT_PUBLIC_FORMBRICKS_API_HOST: z
|
||||
.string()
|
||||
.url()
|
||||
@@ -64,11 +67,9 @@ export const env = createEnv({
|
||||
.or(z.string().refine((str) => str === "")),
|
||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: z.string().optional(),
|
||||
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: z.string().optional(),
|
||||
NEXT_PUBLIC_IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(),
|
||||
NEXT_PUBLIC_POSTHOG_API_KEY: z.string().optional(),
|
||||
NEXT_PUBLIC_POSTHOG_API_HOST: z.string().optional(),
|
||||
NEXT_PUBLIC_SENTRY_DSN: z.string().optional(),
|
||||
NEXT_PUBLIC_SURVEY_BASE_URL: z.string().optional(),
|
||||
},
|
||||
/*
|
||||
* Due to how Next.js bundles environment variables on Edge and Client,
|
||||
@@ -95,24 +96,26 @@ export const env = createEnv({
|
||||
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
|
||||
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
|
||||
CRON_SECRET: process.env.CRON_SECRET,
|
||||
EMAIL_VERIFICATION_DISABLED: process.env.EMAIL_VERIFICATION_DISABLED,
|
||||
PASSWORD_RESET_DISABLED: process.env.PASSWORD_RESET_DISABLED,
|
||||
SIGNUP_DISABLED: process.env.SIGNUP_DISABLED,
|
||||
INVITE_DISABLED: process.env.INVITE_DISABLED,
|
||||
PRIVACY_URL: process.env.PRIVACY_URL,
|
||||
TERMS_URL: process.env.TERMS_URL,
|
||||
IMPRINT_URL: process.env.IMPRINT_URL,
|
||||
GITHUB_AUTH_ENABLED: process.env.GITHUB_AUTH_ENABLED,
|
||||
GOOGLE_AUTH_ENABLED: process.env.GOOGLE_AUTH_ENABLED,
|
||||
GOOGLE_SHEETS_CLIENT_ID: process.env.GOOGLE_SHEETS_CLIENT_ID,
|
||||
GOOGLE_SHEETS_CLIENT_SECRET: process.env.GOOGLE_SHEETS_CLIENT_SECRET,
|
||||
GOOGLE_SHEETS_REDIRECT_URL: process.env.GOOGLE_SHEETS_REDIRECT_URL,
|
||||
NEXT_PUBLIC_WEBAPP_URL: process.env.NEXT_PUBLIC_WEBAPP_URL,
|
||||
NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED: process.env.NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED,
|
||||
NEXT_PUBLIC_PASSWORD_RESET_DISABLED: process.env.NEXT_PUBLIC_PASSWORD_RESET_DISABLED,
|
||||
NEXT_PUBLIC_SIGNUP_DISABLED: process.env.NEXT_PUBLIC_SIGNUP_DISABLED,
|
||||
NEXT_PUBLIC_INVITE_DISABLED: process.env.NEXT_PUBLIC_INVITE_DISABLED,
|
||||
NEXT_PUBLIC_PRIVACY_URL: process.env.NEXT_PUBLIC_PRIVACY_URL,
|
||||
NEXT_PUBLIC_TERMS_URL: process.env.NEXT_PUBLIC_TERMS_URL,
|
||||
NEXT_PUBLIC_IMPRINT_URL: process.env.NEXT_PUBLIC_IMPRINT_URL,
|
||||
NEXT_PUBLIC_GITHUB_AUTH_ENABLED: process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED,
|
||||
NEXT_PUBLIC_GOOGLE_AUTH_ENABLED: process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED,
|
||||
NEXT_PUBLIC_FORMBRICKS_API_HOST: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
|
||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
|
||||
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: process.env.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID,
|
||||
NEXT_PUBLIC_IS_FORMBRICKS_CLOUD: process.env.NEXT_PUBLIC_IS_FORMBRICKS_CLOUD,
|
||||
IS_FORMBRICKS_CLOUD: process.env.IS_FORMBRICKS_CLOUD,
|
||||
NEXT_PUBLIC_POSTHOG_API_KEY: process.env.NEXT_PUBLIC_POSTHOG_API_KEY,
|
||||
NEXT_PUBLIC_POSTHOG_API_HOST: process.env.NEXT_PUBLIC_POSTHOG_API_HOST,
|
||||
VERCEL_URL: process.env.VERCEL_URL,
|
||||
SURVEY_BASE_URL: process.env.SURVEY_BASE_URL,
|
||||
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import useSWR from "swr";
|
||||
import { fetcher } from "@formbricks/lib/fetcher";
|
||||
|
||||
export const useApiKeys = (environmentId: string) => {
|
||||
const { data, error, mutate } = useSWR(`/api/v1/environments/${environmentId}/api-keys`, fetcher);
|
||||
|
||||
return {
|
||||
apiKeys: data,
|
||||
isLoadingApiKeys: !error && !data,
|
||||
isErrorApiKeys: error,
|
||||
mutateApiKeys: mutate,
|
||||
};
|
||||
};
|
||||
|
||||
export const createApiKey = async (environmentId: string, apiKey = {}) => {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/environments/${environmentId}/api-keys`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(apiKey),
|
||||
});
|
||||
return await res.json();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw Error(`createApiKey: unable to create api-key: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteApiKey = async (environmentId: string, apiKey) => {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/environments/${environmentId}/api-keys/${apiKey.id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
return await res.json();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw Error(`deleteApiKey: unable to delete api-key: ${error.message}`);
|
||||
}
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import useSWR from "swr";
|
||||
import { fetcher } from "@formbricks/lib/fetcher";
|
||||
import type { AttributeClass } from "@prisma/client";
|
||||
|
||||
export const useAttributeClasses = (environmentId) => {
|
||||
const { data, isLoading, error, mutate, isValidating } = useSWR(
|
||||
`/api/v1/environments/${environmentId}/attribute-classes`,
|
||||
fetcher
|
||||
);
|
||||
|
||||
return {
|
||||
attributeClasses: data,
|
||||
isLoadingAttributeClasses: isLoading,
|
||||
isErrorAttributeClasses: error,
|
||||
isValidatingAttributeClasses: isValidating,
|
||||
mutateAttributeClasses: mutate,
|
||||
};
|
||||
};
|
||||
|
||||
type AttributeClassWithSurvey = AttributeClass & {
|
||||
activeSurveys: string[];
|
||||
inactiveSurveys: string[];
|
||||
};
|
||||
|
||||
export const useAttributeClass = (environmentId: string, attributeClassId: string) => {
|
||||
const { data, isLoading, error, mutate, isValidating } = useSWR(
|
||||
`/api/v1/environments/${environmentId}/attribute-classes/${attributeClassId}`,
|
||||
fetcher
|
||||
);
|
||||
|
||||
return {
|
||||
attributeClass: data as AttributeClassWithSurvey,
|
||||
isLoadingAttributeClass: isLoading,
|
||||
isErrorAttributeClass: error,
|
||||
isValidatingAttributeClass: isValidating,
|
||||
mutateAttributeClass: mutate,
|
||||
};
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import useSWRMutation from "swr/mutation";
|
||||
import { updateRessource } from "@formbricks/lib/fetcher";
|
||||
|
||||
export function useAttributeClassMutation(environmentId: string, attributeClassId: string) {
|
||||
const { trigger, isMutating } = useSWRMutation(
|
||||
`/api/v1/environments/${environmentId}/attribute-classes/${attributeClassId}`,
|
||||
updateRessource
|
||||
);
|
||||
|
||||
return {
|
||||
triggerAttributeClassMutate: trigger,
|
||||
isMutatingAttributeClass: isMutating,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
import { env } from "@/env.mjs";
|
||||
import { getQuestionResponseMapping } from "@/lib/responses/questionResponseMapping";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import {
|
||||
MAIL_FROM,
|
||||
SMTP_HOST,
|
||||
SMTP_PASSWORD,
|
||||
SMTP_PORT,
|
||||
SMTP_SECURE_ENABLED,
|
||||
SMTP_USER,
|
||||
WEBAPP_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { Question } from "@formbricks/types/questions";
|
||||
import { TResponse } from "@formbricks/types/v1/responses";
|
||||
import { withEmailTemplate } from "./email-template";
|
||||
@@ -18,18 +25,18 @@ interface sendEmailData {
|
||||
|
||||
export const sendEmail = async (emailData: sendEmailData) => {
|
||||
let transporter = nodemailer.createTransport({
|
||||
host: env.SMTP_HOST,
|
||||
port: env.SMTP_PORT,
|
||||
secure: env.SMTP_SECURE_ENABLED === "1", // true for 465, false for other ports
|
||||
host: SMTP_HOST,
|
||||
port: SMTP_PORT,
|
||||
secure: SMTP_SECURE_ENABLED, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: env.SMTP_USER,
|
||||
pass: env.SMTP_PASSWORD,
|
||||
user: SMTP_USER,
|
||||
pass: SMTP_PASSWORD,
|
||||
},
|
||||
// logger: true,
|
||||
// debug: true,
|
||||
});
|
||||
const emailDefaults = {
|
||||
from: `Formbricks <${env.MAIL_FROM || "noreply@formbricks.com"}>`,
|
||||
from: `Formbricks <${MAIL_FROM || "noreply@formbricks.com"}>`,
|
||||
};
|
||||
await transporter.sendMail({ ...emailDefaults, ...emailData });
|
||||
};
|
||||
@@ -147,7 +154,7 @@ export const sendResponseFinishedEmail = async (
|
||||
subject: personEmail
|
||||
? `${personEmail} just completed your ${survey.name} survey ✅`
|
||||
: `A response for ${survey.name} was completed ✅`,
|
||||
replyTo: personEmail?.toString() || env.MAIL_FROM,
|
||||
replyTo: personEmail?.toString() || MAIL_FROM,
|
||||
html: withEmailTemplate(`<h1>Hey 👋</h1>Someone just completed your survey <strong>${
|
||||
survey.name
|
||||
}</strong><br/>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import useSWR from "swr";
|
||||
import { fetcher } from "@formbricks/lib/fetcher";
|
||||
|
||||
export const useEnvironment = (environmentId: string) => {
|
||||
const { data, isLoading, error, mutate, isValidating } = useSWR(
|
||||
`/api/v1/environments/${environmentId}/`,
|
||||
fetcher
|
||||
);
|
||||
|
||||
return {
|
||||
environment: data,
|
||||
isLoadingEnvironment: isLoading,
|
||||
isErrorEnvironment: error,
|
||||
isValidatingEnvironment: isValidating,
|
||||
mutateEnvironment: mutate,
|
||||
};
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import useSWRMutation from "swr/mutation";
|
||||
import { updateRessource } from "@formbricks/lib/fetcher";
|
||||
|
||||
export function useEnvironmentMutation(environmentId: string) {
|
||||
const { trigger, isMutating } = useSWRMutation(`/api/v1/environments/${environmentId}`, updateRessource);
|
||||
|
||||
return {
|
||||
triggerEnvironmentMutate: trigger,
|
||||
isMutatingEnvironment: isMutating,
|
||||
};
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import useSWR from "swr";
|
||||
import { fetcher } from "@formbricks/lib/fetcher";
|
||||
import type { Event } from "@formbricks/types/events";
|
||||
|
||||
export const useEventClasses = (environmentId) => {
|
||||
const { data, isLoading, error, mutate, isValidating } = useSWR(
|
||||
`/api/v1/environments/${environmentId}/event-classes`,
|
||||
fetcher
|
||||
);
|
||||
|
||||
return {
|
||||
eventClasses: data,
|
||||
isLoadingEventClasses: isLoading,
|
||||
isErrorEventClasses: error,
|
||||
isValidatingEventClasses: isValidating,
|
||||
mutateEventClasses: mutate,
|
||||
};
|
||||
};
|
||||
|
||||
export const useEventClass = (environmentId, eventClassId) => {
|
||||
const { data, isLoading, error, mutate, isValidating } = useSWR(
|
||||
`/api/v1/environments/${environmentId}/event-classes/${eventClassId}`,
|
||||
fetcher
|
||||
);
|
||||
|
||||
return {
|
||||
eventClass: data,
|
||||
isLoadingEventClass: isLoading,
|
||||
isErrorEventClass: error,
|
||||
isValidatingEventClass: isValidating,
|
||||
mutateEventClass: mutate,
|
||||
};
|
||||
};
|
||||
|
||||
export const createEventClass = async (environmentId, eventClass: Event) => {
|
||||
const response = await fetch(`/api/v1/environments/${environmentId}/event-classes`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(eventClass),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 409) {
|
||||
throw Error("Action with this name already exists");
|
||||
}
|
||||
throw Error(`Unable to create Action: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const deleteEventClass = async (environmentId: string, eventClassId: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/environments/${environmentId}/event-classes/${eventClassId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw Error(`deleteEventClass: unable to delete eventClass: ${res.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw Error(`deleteEventClass: unable to delete eventClass: ${error.message}`);
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user