Compare commits
8 Commits
ReviewBot/
...
feature/1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aea4b8e1b8 | ||
|
|
8f9d62272c | ||
|
|
c9954562d5 | ||
|
|
102590db6a | ||
|
|
e1e1c1f497 | ||
|
|
0f752e29c8 | ||
|
|
0920bf4e35 | ||
|
|
f7f1813d63 |
@@ -1,7 +1,7 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
# **/node_modules
|
||||
**/node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
.pnpm-store/
|
||||
|
||||
37
.env.example
@@ -1,7 +1,8 @@
|
||||
########################################################################
|
||||
# ------------ MANDATORY (CHANGE ACCORDING TO YOUR SETUP) ------------ #
|
||||
# ------------ MANDATORY (CHANGE ACCORDING TO YOUR SETUP) ------------#
|
||||
########################################################################
|
||||
|
||||
|
||||
############
|
||||
# BASICS #
|
||||
############
|
||||
@@ -50,25 +51,13 @@ SMTP_SECURE_ENABLED=0
|
||||
SMTP_USER=smtpUser
|
||||
SMTP_PASSWORD=smtpPassword
|
||||
|
||||
|
||||
########################################################################
|
||||
# ------------------------------ OPTIONAL -----------------------------#
|
||||
########################################################################
|
||||
|
||||
# Uncomment the variables you would like to use and customize the values.
|
||||
|
||||
##############
|
||||
# S3 STORAGE #
|
||||
##############
|
||||
|
||||
# S3 Storage is required for the file uplaod in serverless environments like Vercel
|
||||
S3_ACCESS_KEY=
|
||||
S3_SECRET_KEY=
|
||||
S3_REGION=
|
||||
S3_BUCKET_NAME=
|
||||
# Configure a third party S3 compatible storage service endpoint like StorJ leave empty if you use Amazon S3
|
||||
# e.g., https://gateway.storjshare.io
|
||||
S3_ENDPOINT_URL=
|
||||
|
||||
#####################
|
||||
# Disable Features #
|
||||
#####################
|
||||
@@ -82,9 +71,6 @@ PASSWORD_RESET_DISABLED=1
|
||||
# Signup. Disable the ability for new users to create an account.
|
||||
# SIGNUP_DISABLED=1
|
||||
|
||||
# Email login. Disable the ability for users to login with email.
|
||||
# EMAIL_AUTH_DISABLED=1
|
||||
|
||||
# Team Invite. Disable the ability for invited users to create an account.
|
||||
# INVITE_DISABLED=1
|
||||
|
||||
@@ -98,25 +84,21 @@ TERMS_URL=
|
||||
IMPRINT_URL=
|
||||
|
||||
# Configure Github Login
|
||||
GITHUB_AUTH_ENABLED=0
|
||||
GITHUB_ID=
|
||||
GITHUB_SECRET=
|
||||
|
||||
# Configure Google Login
|
||||
GOOGLE_AUTH_ENABLED=0
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# Configure Azure Active Directory Login
|
||||
AZUREAD_AUTH_ENABLED=0
|
||||
AZUREAD_CLIENT_ID=
|
||||
AZUREAD_CLIENT_SECRET=
|
||||
AZUREAD_TENANT_ID=
|
||||
|
||||
# OpenID Connect (OIDC) configuration
|
||||
# OIDC_CLIENT_ID=
|
||||
# OIDC_CLIENT_SECRET=
|
||||
# OIDC_ISSUER=
|
||||
# OIDC_DISPLAY_NAME=
|
||||
# OIDC_SIGNING_ALGORITHM=
|
||||
|
||||
# Cron Secret
|
||||
CRON_SECRET=
|
||||
|
||||
@@ -136,12 +118,12 @@ NEXT_PUBLIC_FORMBRICKS_API_HOST=
|
||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=
|
||||
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID=
|
||||
|
||||
# Oauth credentials for Google sheet integration
|
||||
# Oauth credentials for Google sheet integration
|
||||
GOOGLE_SHEETS_CLIENT_ID=
|
||||
GOOGLE_SHEETS_CLIENT_SECRET=
|
||||
GOOGLE_SHEETS_REDIRECT_URL=
|
||||
|
||||
# Oauth credentials for Airtable integration
|
||||
# Oauth credentials for Airtable integration
|
||||
AIRTABLE_CLIENT_ID=
|
||||
|
||||
# Enterprise License Key
|
||||
@@ -159,6 +141,3 @@ ENTERPRISE_LICENSE_KEY=
|
||||
# Send new users to customer.io
|
||||
# CUSTOMER_IO_API_KEY=
|
||||
# CUSTOMER_IO_SITE_ID=
|
||||
|
||||
# Ignore Rate Limiting across the Formbricks app
|
||||
# RATE_LIMITING_DISABLED=1
|
||||
|
||||
53
.github/actions/cache-build-web/action.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: Build & Cache Web App
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Cache Build
|
||||
uses: actions/cache@v3
|
||||
id: cache-build
|
||||
env:
|
||||
cache-name: prod-build
|
||||
key-1: ${{ hashFiles('pnpm-lock.yaml') }}
|
||||
key-2: ${{ hashFiles('apps/**/**.[jt]s', 'apps/**/**.[jt]sx', 'packages/**/**.[jt]s', 'packages/**/**.[jt]sx', '!**/node_modules') }}
|
||||
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/apps/web/.next
|
||||
**/.turbo/**
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
shell: bash
|
||||
|
||||
- name: Generate Random ENCRYPTION_KEY
|
||||
run: |
|
||||
SECRET=$(openssl rand -hex 32)
|
||||
echo "ENCRYPTION_KEY=$SECRET" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- run: |
|
||||
pnpm build --filter=web...
|
||||
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
@@ -1,10 +0,0 @@
|
||||
name: Dangerous git Checkout
|
||||
description: "Git Checkout from PR code so we can run checks from forks"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 2
|
||||
8
.github/workflows/build-web.yml
vendored
@@ -8,13 +8,13 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
node-version: 18.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
41
.github/workflows/e2e.yml
vendored
@@ -7,18 +7,29 @@ jobs:
|
||||
name: Run E2E Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Build & Cache Web Binaries
|
||||
uses: ./.github/actions/cache-build-web
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
- name: Install Docker Compose
|
||||
run: sudo apt-get update && sudo apt-get install -y docker-compose
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: npm install -g pnpm && pnpm install
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install --with-deps
|
||||
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Generate ENCRYPTION_KEY
|
||||
run: |
|
||||
SECRET=$(openssl rand -hex 32)
|
||||
echo "ENCRYPTION_KEY=$SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Start PostgreSQL
|
||||
run: |
|
||||
@@ -32,6 +43,11 @@ jobs:
|
||||
sleep 5
|
||||
done
|
||||
pnpm db:migrate:dev
|
||||
|
||||
- name: Build App in dev mode without external dependencies
|
||||
run: |
|
||||
pnpm build:dev --filter=web...
|
||||
|
||||
- name: Serve packages for lazy loading
|
||||
run: |
|
||||
cd packages/surveys && pnpm serve &
|
||||
@@ -52,17 +68,6 @@ jobs:
|
||||
run: |
|
||||
curl -s http://localhost:3003
|
||||
|
||||
- name: Cache Playwright
|
||||
uses: actions/cache@v3
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install Playwright
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: pnpm exec playwright install --with-deps
|
||||
|
||||
- name: Run E2E Tests
|
||||
run: |
|
||||
pnpm test:e2e
|
||||
|
||||
30
.github/workflows/ecs-deployment.yml
vendored
@@ -7,11 +7,14 @@ name: ECS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch: # Add manual trigger support
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: ghcr.io
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: formbricks/formbricks-experimental
|
||||
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
|
||||
|
||||
@@ -21,13 +24,20 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write # Only necessary for sigstore/fulcio outside PRs
|
||||
# This is used to complete the identity challenge
|
||||
# with sigstore/fulcio when running outside of PRs.
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Generate Secrets
|
||||
- name: Generate Random NEXTAUTH_SECRET
|
||||
run: |
|
||||
echo "NEXTAUTH_SECRET=$(openssl rand -hex 32)" >> $GITHUB_ENV
|
||||
echo "ENCRYPTION_KEY=$(openssl rand -hex 32)" >> $GITHUB_ENV
|
||||
SECRET=$(openssl rand -hex 32)
|
||||
echo "NEXTAUTH_SECRET=$SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate Random ENCRYPTION_KEY
|
||||
run: |
|
||||
SECRET=$(openssl rand -hex 32)
|
||||
echo "ENCRYPTION_KEY=$SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
@@ -43,7 +53,7 @@ jobs:
|
||||
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v3 # v3.0.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -58,6 +68,7 @@ jobs:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha,format=long
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
# Build and push Docker image with Buildx
|
||||
# https://github.com/docker/build-push-action
|
||||
@@ -82,7 +93,7 @@ jobs:
|
||||
DATABASE_URL=${{ env.DATABASE_URL }}
|
||||
ENCRYPTION_KEY=${{ env.ENCRYPTION_KEY }}
|
||||
NEXT_PUBLIC_SENTRY_DSN=${{ env.NEXT_PUBLIC_SENTRY_DSN }}
|
||||
|
||||
|
||||
- name: Sign the images with GitHub OIDC Token
|
||||
env:
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
@@ -93,9 +104,6 @@ jobs:
|
||||
images+="${tag}@${DIGEST} "
|
||||
done
|
||||
cosign sign --yes ${images}
|
||||
|
||||
outputs:
|
||||
image_tag_sha: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
@@ -118,7 +126,7 @@ jobs:
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: prod-webapp-container
|
||||
image: ${{ needs.build.outputs.image_tag_sha }}
|
||||
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- name: Deploy Amazon ECS task definition
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
|
||||
4
.github/workflows/lint.yml
vendored
@@ -8,8 +8,8 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
25
.github/workflows/pr.yml
vendored
@@ -12,48 +12,23 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
has-files-requiring-all-checks:
|
||||
- "!(**.md|.github/CODEOWNERS)"
|
||||
|
||||
test:
|
||||
name: Run Tests
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/test.yml
|
||||
secrets: inherit
|
||||
|
||||
lint:
|
||||
name: Run Linters
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Build Formbricks-web
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/build-web.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-test:
|
||||
name: Run E2E Tests
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
name: Docker for Data Migrations
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: formbricks/data-migrations
|
||||
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/formbricks?schema=public"
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
# This is used to complete the identity challenge
|
||||
# with sigstore/fulcio when running outside of PRs.
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1
|
||||
with:
|
||||
cosign-release: "v2.1.1"
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=raw,value=${{ github.ref_name }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: ./packages/database/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
DATABASE_URL=${{ env.DATABASE_URL }}
|
||||
|
||||
- name: Sign the published Docker image
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
run: |
|
||||
cosign sign --yes ghcr.io/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
|
||||
17
.github/workflows/test.yml
vendored
@@ -7,14 +7,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
node-version: 18.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
@@ -30,5 +34,8 @@ jobs:
|
||||
SECRET=$(openssl rand -hex 32)
|
||||
echo "ENCRYPTION_KEY=$SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Build formbricks-js dependencies
|
||||
run: pnpm build --filter=js
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
3
.gitignore
vendored
@@ -51,6 +51,3 @@ Zone.Identifier
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
# uploads
|
||||
packages/lib/uploads
|
||||
|
||||
108
README.md
@@ -1,46 +1,50 @@
|
||||
<div id="top"></div>
|
||||
<div id="top"></div>
|
||||
|
||||
<p align="center">
|
||||
<p align="center">
|
||||
|
||||
<a href="https://formbricks.com">
|
||||
<a href="https://formbricks.com">
|
||||
|
||||
<img width="120" alt="Open Source Privacy First Experience Management Solution Qualtrics Alternative Logo" src="https://github.com/formbricks/formbricks/assets/72809645/0086704f-bee7-4d38-9cc8-fa42ee59e004">
|
||||
<img width="120" alt="Open Source Privacy First Experience Management Solution Qualtrics Alternative Logo" src="https://github.com/formbricks/formbricks/assets/72809645/0086704f-bee7-4d38-9cc8-fa42ee59e004">
|
||||
|
||||
</a>
|
||||
|
||||
<h3 align="center">Formbricks</h3>
|
||||
<h3 align="center">Formbricks</h3>
|
||||
|
||||
<p align="center">
|
||||
<p align="center">
|
||||
Harvest user-insights, build irresistible experiences.
|
||||
<br />
|
||||
<a href="https://formbricks.com/">Website</a> | <a href="https://formbricks.com/discord">Join Discord community</a>
|
||||
<br />
|
||||
<a href="https://formbricks.com/">Website</a> | <a href="https://formbricks.com/discord">Join Discord community</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/formbricks/formbricks/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL-purple" alt="License"></a> <a href="https://formbricks.com/discord"><img src="https://img.shields.io/discord/979077669410979880?label=Discord&logo=discord&logoColor=%23fff" alt="Join Formbricks Discord"></a> <a href="https://github.com/formbricks/formbricks/stargazers"><img src="https://img.shields.io/github/stars/formbricks/formbricks?logo=github" alt="Github Stars"></a>
|
||||
<a href="https://news.ycombinator.com/item?id=32303986"><img src="https://img.shields.io/badge/Hacker%20News-122-%23FF6600" alt="Hacker News"></a>
|
||||
<a href="[https://www.producthunt.com/products/formbricks](https://www.producthunt.com/posts/formbricks)"><img src="https://img.shields.io/badge/Product%20Hunt-455-orange?logo=producthunt&logoColor=%23fff" alt="Product Hunt"></a>
|
||||
<a href="https://github.blog/2023-04-12-github-accelerator-our-first-cohort-and-whats-next/"><img src="https://img.shields.io/badge/2023-blue?logo=github&label=Github%20Accelerator" alt="Github Accelerator"></a>
|
||||
<a href="https://github.com/formbricks/formbricks/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
||||
<p align="center">
|
||||
<a href="https://github.com/formbricks/formbricks/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL-purple" alt="License"></a> <a href="https://formbricks.com/discord"><img src="https://img.shields.io/discord/979077669410979880?label=Discord&logo=discord&logoColor=%23fff" alt="Join Formbricks Discord"></a> <a href="https://github.com/formbricks/formbricks/stargazers"><img src="https://img.shields.io/github/stars/formbricks/formbricks?logo=github" alt="Github Stars"></a>
|
||||
<a href="https://news.ycombinator.com/item?id=32303986"><img src="https://img.shields.io/badge/Hacker%20News-122-%23FF6600" alt="Hacker News"></a>
|
||||
<a href="[https://www.producthunt.com/products/formbricks](https://www.producthunt.com/posts/formbricks)"><img src="https://img.shields.io/badge/Product%20Hunt-455-orange?logo=producthunt&logoColor=%23fff" alt="Product Hunt"></a>
|
||||
<a href="https://github.blog/2023-04-12-github-accelerator-our-first-cohort-and-whats-next/"><img src="https://img.shields.io/badge/2023-blue?logo=github&label=Github%20Accelerator" alt="Github Accelerator"></a>
|
||||
<a href="https://github.com/formbricks/formbricks/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
|
||||
<div style="background-color:#f8fafc; border-radius:5px;">
|
||||
<p align="center">
|
||||
<i>Trusted by</i><br/>
|
||||
<img width="867" alt="clients-hi-res" src="https://github.com/formbricks/formbricks/assets/72809645/924d3693-f66a-4063-bb31-6e5789a8175a">
|
||||
<p align="center">
|
||||
<i>Trusted by</i>
|
||||
<a href="https://flixbus.com"><img src="https://github.com/formbricks/formbricks/assets/72809645/d6c91d89-7633-4845-ae1e-03bbd2ce0946" height="35px"></a>
|
||||
<a href="https://github.com/calcom/cal.com/"><img src="https://github.com/formbricks/formbricks/assets/675065/1a8763cf-f47e-4960-90f6-334f6dc12a17#gh-light-mode-only" height="20px"></a>
|
||||
<a href="https://github.com/CrowdDotDev/crowd.dev"><img src="https://github.com/formbricks/formbricks/assets/675065/59b1a4d4-25e4-4ef3-b0bf-4426446fbfd0#gh-light-mode-only" height="20px"></a>
|
||||
<a href="https://neverinstall.com/"><img src="https://github.com/formbricks/formbricks/assets/675065/72e5e37b-8ef7-4340-b06e-f1d12a05330f#gh-light-mode-only" height="20px"></a>
|
||||
<a href="https://clovyr.io/"><img src="https://github.com/formbricks/formbricks/assets/675065/9291c8df-9aac-423a-a430-a9a581240075" height="20px"></a>
|
||||
</p>
|
||||
<div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/2570" target="_blank"><img src="https://trendshift.io/api/badge/repositories/2570" alt="Trendshift Badge for formbricks/formbricks" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/2570" target="_blank"><img src="https://trendshift.io/api/badge/repositories/2570" alt="Trendshift Badge for formbricks/formbricks" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</p>
|
||||
|
||||
## ✨ About Formbricks
|
||||
|
||||
<img width="1527" alt="formbricks-sneak" src="https://github-production-user-asset-6210df.s3.amazonaws.com/675065/249441967-ccb89ea3-82b4-4bf2-8d2c-528721ec313b.png">
|
||||
<img width="1527" alt="formbricks-sneak" src="https://github-production-user-asset-6210df.s3.amazonaws.com/675065/249441967-ccb89ea3-82b4-4bf2-8d2c-528721ec313b.png">
|
||||
|
||||
Formbricks provides a free and open source surveying platform. Gather feedback at every point in the user journey with beautiful in-app, website, link and email surveys. Build on top of Formbricks or leverage prebuilt data analysis capabilities.
|
||||
|
||||
@@ -66,11 +70,11 @@ Formbricks is both a free and open source survey platform - and a privacy-first
|
||||
|
||||
- [Contact](#contact-us)
|
||||
|
||||
- [Security](#security)
|
||||
|
||||
- [License](#license)
|
||||
|
||||
<a id="features"></a>
|
||||
- [Security](#security)
|
||||
|
||||
<a id="features"></a>
|
||||
|
||||
### Features
|
||||
|
||||
@@ -104,21 +108,19 @@ Formbricks is both a free and open source survey platform - and a privacy-first
|
||||
|
||||
- 🧘♂️ [Zod](https://zod.dev/)
|
||||
|
||||
- 🐛 [Vitest](https://vitest.dev/)
|
||||
|
||||
<a id="getting-started"></a>
|
||||
<a id="getting-started"></a>
|
||||
|
||||
## 🚀 Getting started
|
||||
|
||||
We've got several options depending on your need to help you quickly get started with Formbricks.
|
||||
|
||||
<a id="cloud-version"></a>
|
||||
<a id="cloud-version"></a>
|
||||
|
||||
### ☁️ Cloud Version
|
||||
|
||||
Formbricks has a hosted cloud offering with a generous free plan to get you up and running as quickly as possible. To get started, please visit [formbricks.com](https://app.formbricks.com/auth/signup).
|
||||
|
||||
<a id="self-hosted-version"></a>
|
||||
<a id="self-hosted-version"></a>
|
||||
|
||||
### 🐳 Self-hosting Formbricks
|
||||
|
||||
@@ -138,11 +140,11 @@ You can deploy Formbricks on [Railway](https://railway.app) using the button bel
|
||||
|
||||
[](https://railway.app/new/template/PPDzCd)
|
||||
|
||||
<a id="development"></a>
|
||||
<a id="development"></a>
|
||||
|
||||
## 👨💻 Development
|
||||
### 👨💻 Development
|
||||
|
||||
### Prerequisites
|
||||
#### Prerequisites
|
||||
|
||||
Here is what you need to be able to run Formbricks:
|
||||
|
||||
@@ -152,11 +154,11 @@ Here is what you need to be able to run Formbricks:
|
||||
|
||||
- [Docker](https://www.docker.com/) - to run PostgreSQL and MailHog
|
||||
|
||||
### Local Setup
|
||||
#### Local Setup
|
||||
|
||||
To get started locally, we've got a [guide to help you](https://formbricks.com/docs/contributing/setup).
|
||||
|
||||
### Gitpod Setup
|
||||
#### Gitpod Setup
|
||||
|
||||
1. Click the button below to open this project in Gitpod.
|
||||
|
||||
@@ -164,7 +166,7 @@ To get started locally, we've got a [guide to help you](https://formbricks.com/d
|
||||
|
||||
[](https://gitpod.io/#https://github.com/formbricks/formbricks)
|
||||
|
||||
<a id="contribution"></a>
|
||||
<a id="contribution"></a>
|
||||
|
||||
## ✍️ Contribution
|
||||
|
||||
@@ -182,46 +184,30 @@ Please check out [our contribution guide](https://formbricks.com/docs/contributi
|
||||
|
||||
## All Thanks To Our Contributors
|
||||
|
||||
<a href="https://github.com/formbricks/formbricks/graphs/contributors">
|
||||
<a href="https://github.com/formbricks/formbricks/graphs/contributors">
|
||||
|
||||
<img src="https://contrib.rocks/image?repo=formbricks/formbricks" />
|
||||
<img src="https://contrib.rocks/image?repo=formbricks/formbricks" />
|
||||
|
||||
</a>
|
||||
|
||||
<a id="contact-us"></a>
|
||||
<a id="contact-us"></a>
|
||||
|
||||
## 📆 Contact us
|
||||
|
||||
Let's have a chat about your survey needs and get you started.
|
||||
|
||||
<a href="https://cal.com/johannes/onboarding?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a>
|
||||
<a href="https://cal.com/johannes/onboarding?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a>
|
||||
|
||||
<a id="license"></a>
|
||||
<a id="license"></a>
|
||||
|
||||
<a id="security"></a>
|
||||
## ⚖️ License
|
||||
|
||||
Distributed under the AGPLv3 License. See [`LICENSE`](./LICENSE) for more information.
|
||||
|
||||
<a id="security"></a>
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
We take security very seriously. If you come across any security vulnerabilities, please disclose them by sending an email to security@formbricks.com. We appreciate your help in making our platform as secure as possible and are committed to working with you to resolve any issues quickly and efficiently. See [`SECURITY.md`](./SECURITY.md) for more information.
|
||||
|
||||
<a id="license"></a>
|
||||
|
||||
## 👩⚖️ License
|
||||
|
||||
### The AGPL Formbricks Core
|
||||
|
||||
The Formbricks core application is licensed under the [AGPLv3 Open Source License](https://github.com/formbricks/formbricks/blob/main/LICENSE). The core application is fully functional and includes everything you need to design & run link surveys, website surveys and in-app surveys. You can use the software for free for personal and commercial use. You're also allowed to create and distribute modified versions as long as you document the changes you make incl. date. The AGPL license requires you to publish your modified version under the AGPLv3 license as well.
|
||||
|
||||
### The Enterprise Edition
|
||||
|
||||
Additional to the AGPL licensed Formbricks core, this repository contains code licensed under an Enterprise license. The [code](https://github.com/formbricks/formbricks/tree/main/packages/ee) and [license](https://github.com/formbricks/formbricks/blob/main/packages/ee/LICENSE) for the enterprise functionality can be found in the `/packages/ee` folder of this repository. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an [Enterprise License Key](https://formbricks.com/docs/self-hosting/enterprise) to unlock it.
|
||||
|
||||
### White-Labeling Formbricks and Other Licensing Needs
|
||||
|
||||
If you have other licensing requirements such as White-Labeling please [send us an email](mailto:hola@formbricks.com).
|
||||
|
||||
### Why charge for Enterprise Features?
|
||||
|
||||
The Enterprise Edition and White-Label Licenses allow us to fund the development of Formbricks sustainably. It guarantees that the open-source surveying infrastructure we're building will be around for decades to come.
|
||||
|
||||
<p align="right"><a href="#top">🔼 Back to top</a></p>
|
||||
<p align="right"><a href="#top">🔼 Back to top</a></p>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"dependencies": {
|
||||
"@formbricks/js": "workspace:*",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"next": "14.1.1",
|
||||
"next": "14.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
|
||||
@@ -23,12 +23,13 @@ export default function AppPage({}) {
|
||||
useEffect(() => {
|
||||
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
|
||||
const isUserId = window.location.href.includes("userId=true");
|
||||
const attributes = isUserId ? { "Init Attribute 1": "eight", "Init Attribute 2": "two" } : undefined;
|
||||
const userId = isUserId ? "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING" : undefined;
|
||||
const attributes = isUserId ? { "Init Attribute 1": "eight", "Init Attribute 2": "two" } : undefined;
|
||||
formbricks.init({
|
||||
environmentId: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
|
||||
apiHost: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
|
||||
userId,
|
||||
debug: true,
|
||||
attributes,
|
||||
});
|
||||
window.formbricks = formbricks;
|
||||
@@ -104,8 +105,8 @@ export default function AppPage({}) {
|
||||
Reset person / pull data from Formbricks app
|
||||
</h3>
|
||||
<p className="text-slate-700 dark:text-slate-300">
|
||||
On formbricks.reset() the local state will <strong>be deleted</strong> and formbricks gets{" "}
|
||||
<strong>reinitialized</strong>.
|
||||
On formbricks.reset() a few things happen: <strong>New person is created</strong> and{" "}
|
||||
<strong>surveys & no-code actions are pulled from Formbricks:</strong>.
|
||||
</p>
|
||||
<button
|
||||
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
|
||||
|
||||
@@ -45,7 +45,8 @@ Adds an Actions for a given User by their User ID
|
||||
curl --location --request POST 'https://app.formbricks.com/api/v1/client/<environment-id>/actions' \
|
||||
--data-raw '{
|
||||
"userId": "1",
|
||||
"name": "new_action_v2"
|
||||
"name": "new_action_v2",
|
||||
"properties":{}
|
||||
|
||||
}'
|
||||
```
|
||||
@@ -53,7 +54,8 @@ Adds an Actions for a given User by their User ID
|
||||
```json {{ title: 'Example Request Body' }}
|
||||
{
|
||||
"userId": "1",
|
||||
"name": "new_action_v3"
|
||||
"name": "new_action_v3",
|
||||
"properties":{}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { Fence } from "@/components/shared/Fence";
|
||||
import { generateManagementApiMetadata } from "@/lib/utils";
|
||||
import {generateManagementApiMetadata} from "@/lib/utils"
|
||||
|
||||
export const metadata = generateManagementApiMetadata("Responses", ["Fetch", "Delete"]);
|
||||
export const metadata = generateManagementApiMetadata("Responses",["Fetch","Delete"])
|
||||
|
||||
#### Management API
|
||||
|
||||
# Responses API
|
||||
|
||||
This set of API can be used to
|
||||
|
||||
- [List Responses](#list-all-responses)
|
||||
- [List all Responses by surveyId](#list-all-responses-by-survey-id)
|
||||
- [Get Response](#get-response-by-id)
|
||||
- [Delete Response](#delete-a-response)
|
||||
|
||||
@@ -109,97 +107,6 @@ This set of API can be used to
|
||||
|
||||
---
|
||||
|
||||
## List all Responses by surveyId {{ tag: 'GET', label: '/api/v1/management/responses?surveyId=<survey-Id>' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Retrieve all the responses received in your survey.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-Api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/responses?surveyId=<survey-Id>">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/responses?surveyId=<survey-Id>' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data":[
|
||||
{
|
||||
"id": "cln8k0tqv00pcz87no4qrw333",
|
||||
"createdAt": "2023-10-02T07:13:20.023Z",
|
||||
"updatedAt": "2023-10-02T07:13:20.023Z",
|
||||
"surveyId": "cln8k0tqu00p7z87nqr4thi3k",
|
||||
"finished": true,
|
||||
"data": {
|
||||
"interview-prompt": "clicked"
|
||||
},
|
||||
"meta": {
|
||||
"userAgent": {
|
||||
"os": "MacOS",
|
||||
"browser": "Chrome"
|
||||
}
|
||||
},
|
||||
"personAttributes": null,
|
||||
"person": {
|
||||
"id": "e0x4i5tvsp8puxfztyrwykvn",
|
||||
"attributes": {
|
||||
"userId": "CYO675",
|
||||
"email": "ravi@netflix.com",
|
||||
"Name": "Ravi Kumar",
|
||||
"Role": "Manager",
|
||||
"Company": "Netflix",
|
||||
"Experience": "6 years",
|
||||
"Usage Frequency": "Monthly",
|
||||
"Company Size": "4610 employees",
|
||||
"Product Satisfaction Score": "43",
|
||||
"Recommendation Likelihood": "4"
|
||||
},
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck",
|
||||
"createdAt": "2023-10-02T07:13:19.444Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.444Z"
|
||||
},
|
||||
"notes": [],
|
||||
"tags": []
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
## Get Response by ID {{ tag: 'GET', label: '/api/v1/management/responses/<response-id>' }}
|
||||
|
||||
<Row>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import CorsHandling from "./cors-handling-in-api.webp";
|
||||
|
||||
export const metadata = {
|
||||
@@ -26,7 +25,7 @@ Thank you for choosing to contribute to Formbricks. Before you start, please fam
|
||||
- Constants should be in the packages folder
|
||||
- Types should be in the packages folder
|
||||
- How we handle Pull Requests
|
||||
- Read server-side environment variables from `constants.ts`
|
||||
- Read environment variables from `.env.mjs`
|
||||
|
||||
---
|
||||
|
||||
@@ -84,9 +83,9 @@ You should store constants in `packages/lib/constants`
|
||||
|
||||
You should store type in `packages/types`
|
||||
|
||||
## Read server-side environment variables from `constants.ts`
|
||||
## Read environment variables from `.env.mjs`
|
||||
|
||||
Server-side environment variables (`process.env`) shouldn’t be accessed directly but included into the `constants.ts` file and read from there. This way we can assure they are used only on the server side and are also type-safe.
|
||||
Environment variables (`process.env`) shouldn’t be accessed directly but be added in the `.env.mjs` and should be accessed from here. This practice helps us ensure that the variables are typesafe.
|
||||
|
||||
## How we handle Pull Requests
|
||||
|
||||
|
||||
@@ -46,13 +46,13 @@ All you need to do is copy a `<script>` tag to your HTML head, and that’s abou
|
||||
```html {{ title: 'index.html' }}
|
||||
<!-- START Formbricks Surveys -->
|
||||
<script type="text/javascript">
|
||||
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://unpkg.com/@formbricks/js@^1.6.0/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: "<your-environment-id>", apiHost: "<api-host>"})},500)}();
|
||||
</script>
|
||||
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://unpkg.com/@formbricks/js@^1.4.0/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: "<your-environment-id>", apiHost: "<api-host>"})},500)}();
|
||||
</script>
|
||||
<!-- END Formbricks Surveys -->
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
### Required customizations to be made
|
||||
### Required Customizations to be Made
|
||||
|
||||
<Properties>
|
||||
<Property name="environment-id" type="string">
|
||||
@@ -112,7 +112,7 @@ export default App;
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
### Required customizations to be made
|
||||
### Required Customizations to be Made
|
||||
|
||||
<Properties>
|
||||
<Property name="environment-id" type="string">
|
||||
@@ -256,7 +256,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
</Col>
|
||||
Refer to our [Example NextJS Pages Directory project](https://github.com/formbricks/examples/tree/main/nextjs-pages) for more help!
|
||||
|
||||
### Required customizations to be made
|
||||
### Required Customizations to be Made
|
||||
|
||||
<Properties>
|
||||
<Property name="environment-id" type="string">
|
||||
@@ -345,7 +345,7 @@ router.afterEach((to, from) => {
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
### Required customizations to be made
|
||||
### Required Customizations to be Made
|
||||
|
||||
<Properties>
|
||||
<Property name="environment-id" type="string">
|
||||
@@ -388,55 +388,6 @@ To this:
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## Debugging Formbricks Integration
|
||||
|
||||
Enabling Formbricks debug mode in your browser is a useful troubleshooting step for identifying and resolving complex issues. This section outlines how to activate debug mode, covers common use cases, and provides insights into specific debug log messages.
|
||||
|
||||
### Activate Debug Mode
|
||||
|
||||
To activate Formbricks debug mode:
|
||||
|
||||
1. **In Your Integration Code:**
|
||||
|
||||
- Locate the initialization code for Formbricks in your application (HTML, ReactJS, NextJS, VueJS).
|
||||
- Set the `debug` option to `true` when initializing Formbricks.
|
||||
|
||||
2. **View Debug Logs:**
|
||||
|
||||
- Open your browser's developer tools by pressing `F12` or right-clicking and selecting "Inspect."
|
||||
- Navigate to the "Console" tab to view Formbricks debugging information.
|
||||
|
||||
**How to Open Browser Console:**
|
||||
|
||||
- **Google Chrome:** Press `F12` or right-click, select "Inspect," and go to the "Console" tab.
|
||||
- **Firefox:** Press `F12` or right-click, select "Inspect Element," and go to the "Console" tab.
|
||||
- **Safari:** Press `Option + Command + C` to open the developer tools and navigate to the "Console" tab.
|
||||
- **Edge:** Press `F12` or right-click, select "Inspect Element," and go to the "Console" tab.
|
||||
|
||||
3. **Via URL Parameter:**
|
||||
|
||||
- For quick activation, add `?formbricksDebug=true` to your application's URL.
|
||||
|
||||
This parameter will enable debugging for the current session.
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
Debug mode is beneficial for scenarios such as:
|
||||
|
||||
- Verifying Formbricks functionality.
|
||||
- Identifying integration issues.
|
||||
- Troubleshooting unexpected behavior.
|
||||
|
||||
### Debug Log Messages
|
||||
|
||||
Specific debug log messages may provide insights into:
|
||||
|
||||
- API calls and responses.
|
||||
- Event tracking and form interactions.
|
||||
- Integration errors.
|
||||
|
||||
**Note:** Disable debugging in production to prevent unnecessary logs and improve performance.
|
||||
|
||||
## Overwrite CSS Styles for In-App Surveys
|
||||
|
||||
You can overwrite the default CSS styles for the in-app surveys by adding the following CSS to your global CSS file (eg. `globals.css`):
|
||||
|
||||
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
@@ -1,97 +0,0 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import FilledHiddenFields from "./filled-hidden-fields.webp";
|
||||
import HiddenFieldResponses from "./hidden-field-responses.webp";
|
||||
import HiddenFields from "./hidden-fields.webp";
|
||||
import InputHiddenFields from "./input-hidden-fields.webp";
|
||||
import SettingsPage from "./settings.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Hidden Fields",
|
||||
description: "Add hidden fields to your surveys to capture additional data without requiring user inputs!",
|
||||
};
|
||||
|
||||
#### Link Surveys
|
||||
|
||||
# Hidden Fields
|
||||
|
||||
Hidden fields are a powerful feature in Formbricks that allows you to add data to a submission without asking the user to type it in. This feature is especially useful when you already have information about a user that you want to use in the analysis of the survey results (e.g. `payment plan` or `email`)
|
||||
|
||||
## How to Add Hidden Fields
|
||||
|
||||
### Enable them in the Survey Builder
|
||||
|
||||
1. Edit the survey you want to add hidden fields to & open it's settings, make sure it's selected as a **Link Survey**.
|
||||
|
||||
<Image
|
||||
src={SettingsPage}
|
||||
alt="Select the Survey Type as Link Survey"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2. Switch to the Questions tab and scroll down to the bottom of the page. You will see a section called **Hidden Fields**. Make sure to enable it by toggling the switch.
|
||||
|
||||
<Image
|
||||
src={HiddenFields}
|
||||
alt="Enable Hidden Fields"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
3. Now click on it to add a new hidden field ID. You can add as many hidden fields as you want.
|
||||
|
||||
<Image
|
||||
src={InputHiddenFields}
|
||||
alt="Add Hidden Fields"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
<Image
|
||||
src={FilledHiddenFields}
|
||||
alt="Filled Hidden Fields"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Set Single Hidden Field
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Example Screen from which the User filled it">
|
||||
|
||||
```sh
|
||||
https://formbricks.com/clin3dxja02k8l80hpwmx4bjy?screen=pricing
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
### Set Multiple Hidden Fields
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Example Screen from which the User filled it">
|
||||
|
||||
```sh
|
||||
https://formbricks.com/clin3dxja02k8l80hpwmx4bjy?screen=landing_page&job=Founder
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
## View Hidden Fields in Responses
|
||||
|
||||
These hidden fields will now be visible in the responses tab just like other fields in the Summary as well as the Response Cards, and you can use them to filter and analyze your responses.
|
||||
|
||||
<Image
|
||||
src={HiddenFieldResponses}
|
||||
alt="Hidden Field Responses"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Tracking Source**: You can add a hidden field to track the source of the survey. For a detailed guide on Source Tracking, check out the [Source Tracking](/docs/link-surveys/source-tracking) guide.
|
||||
- **User Metadata**: You can add hidden fields to capture user metadata such as user ID, email, or any other user-specific information.
|
||||
- **Survey Metadata**: You can add hidden fields to capture other metadata, e.g. the screen from which the survey was filled, or any other app specific information.
|
||||
|
Before Width: | Height: | Size: 39 KiB |
@@ -10,7 +10,7 @@ export const metadata = {
|
||||
|
||||
Quickly set up and start using Formbricks without getting into the technicalities of the build process. You'll be using an image that we've already built for you. The pre-built image is ready-to-run, and it only requires minimal configuration on your part. This approach is perfect for getting a functional instance of Formbricks up and running with minimal hassle. It's as easy as downloading the Docker image and firing up the container.
|
||||
|
||||
This is ideal for those who are testing Formbricks or want to run it with minimal to no modifications. For this we use the [public Docker image](https://github.com/formbricks/formbricks/pkgs/container/formbricks) and a simple docker-compose file.
|
||||
This is ideal for those who are testing Formbricks or want to run it with minimal to no modifications. For this we use the [public Docker image](https://hub.docker.com/r/formbricks/formbricks) and a simple docker-compose file.
|
||||
|
||||
### Requirements
|
||||
|
||||
@@ -201,10 +201,69 @@ formbricks-quickstart-formbricks-1 | Listening on port 3000 url: http://<random
|
||||
You can close the logs again with `CTRL + C`.
|
||||
|
||||
<Note>
|
||||
## Customizing environment variables
|
||||
## Customizing the Build Time variables
|
||||
|
||||
To edit any of the available environment variables, check out our [Configure](/docs/self-hosting/external-auth-providers) section!
|
||||
To edit any of the variables that start with `NEXT_PUBLIC_`, you need to rebuild the Formbricks Docker Image!
|
||||
|
||||
</Note>
|
||||
|
||||
## Important Run-time Variables
|
||||
|
||||
These variables can be provided at the runtime i.e. in your docker-compose file.
|
||||
|
||||
| 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` |
|
||||
| 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 | |
|
||||
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | `#64748b` |
|
||||
| DEFAULT_TEAM_ID | Automatically assign new users to a specific team when joining | optional | |
|
||||
| DEFAULT_TEAM_ROLE | Role of the user in the default team. | optional | `admin` |
|
||||
| ONBOARDING_DISABLED | Disables onboarding for new users if set to `1` | optional | |
|
||||
|
||||
## Build-time Variables
|
||||
|
||||
These variables must be provided at the time of the docker build and would require rebuilding the image.
|
||||
|
||||
| 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 | |
|
||||
|
||||
Still facing issues? [Join our Discord!](https://formbricks.com/discord) and we'd be glad to assist you!
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
export const metadata = {
|
||||
title: "Enterprise License to unlock advanced functionality",
|
||||
description:
|
||||
"Request a self-hosting licenses to unlock advanced enterprise functionality",
|
||||
};
|
||||
|
||||
#### Self-Hosting
|
||||
|
||||
# Enterprise License Request (Beta)
|
||||
|
||||
We're handing out free enterprise license keys during our beta.
|
||||
|
||||
Additional to the AGPL licensed Formbricks core, the Formbricks repository contains code licensed under an [Enterprise license](https://github.com/formbricks/formbricks/blob/main/packages/ee/LICENSE). This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an [Enterprise License Key](https://formbricks.com/docs/self-hosting/license) to unlock it.
|
||||
|
||||
**Please note:** Sooner than later we will introduce a enterprise license pricing. For a free beta key, fill out this form:
|
||||
|
||||
<div style={{ position: 'relative', height: '100vh', maxHeight: '100vh', overflow: 'auto', borderRadius:'12px' }}>
|
||||
<iframe
|
||||
src="https://app.formbricks.com/s/clrf4z8zg1u3912250j7shqb5"
|
||||
style={{ position: 'absolute', left: 0, top: 0, width: '100%', height: '100%', border: 0 }}
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
|
||||
**Can’t figure it out?**: [Join our Discord!](https://formbricks.com/discord)
|
||||
@@ -40,11 +40,12 @@ Integrating Google OAuth with your Formbricks instance allows users to log in us
|
||||
- Ensure to specify authorized JavaScript origins and authorized redirect URIs.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Configuration URLs">
|
||||
``` {{ title: "Redirect & Origin URLs" }}
|
||||
Authorized JavaScript origins: {WEBAPP_URL}
|
||||
Authorized redirect URIs: {WEBAPP_URL}/api/auth/callback/google ```
|
||||
</CodeGroup>
|
||||
<CodeGroup title="Configuration URLs">
|
||||
``` {{ title: 'Redirect & Origin URLs' }}
|
||||
Authorized JavaScript origins: {WEBAPP_URL}
|
||||
Authorized redirect URIs: {WEBAPP_URL}/api/auth/callback/google
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
5. **Update Environment Variables in Docker**:
|
||||
@@ -57,12 +58,14 @@ Integrating Google OAuth with your Formbricks instance allows users to log in us
|
||||
|
||||
```sh {{ title: 'Shell commands' }}
|
||||
docker exec -it container_id /bin/bash
|
||||
export GOOGLE_AUTH_ENABLED=1
|
||||
export GOOGLE_CLIENT_ID=your-client-id-here
|
||||
export GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||
exit
|
||||
```
|
||||
|
||||
```sh {{ title: 'env file' }}
|
||||
GOOGLE_AUTH_ENABLED=1
|
||||
GOOGLE_CLIENT_ID=your-client-id-here
|
||||
GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||
```
|
||||
@@ -75,103 +78,3 @@ GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||
- Once the environment variables have been updated, it's crucial to restart your Docker containers to apply the changes. This ensures that your Formbricks instance can utilize the new Google OAuth configuration for user authentication. Here's how you can do it:
|
||||
- Navigate to your Docker setup directory where your `docker-compose.yml` file is located.
|
||||
- Run the following command to bring down your current Docker containers and then bring them back up with the updated environment configuration:
|
||||
|
||||
## OpenID Integration
|
||||
|
||||
Integrating your own OIDC (OpenID Connect) instance with your Formbricks instance allows users to log in using their OIDC credentials, ensuring a secure and streamlined user experience. Please follow the steps below to set up OIDC for your Formbricks instance.
|
||||
|
||||
1. Configure your OIDC provider & get the following variables:
|
||||
- `OIDC_CLIENT_ID`
|
||||
- `OIDC_CLIENT_SECRET`
|
||||
- `OIDC_ISSUER`
|
||||
- `OIDC_SIGNING_ALGORITHM`
|
||||
|
||||
<Note>
|
||||
Make sure the Redirect URI for your OIDC Client is set to `{WEBAPP_URL}/api/auth/callback/openid`.
|
||||
</Note>
|
||||
|
||||
2. Update these environment variables in your `docker-compose.yml` or pass it directly to the running container.
|
||||
|
||||
An example configuration for a FusionAuth OpenID Connect in Formbricks would look like:
|
||||
<Col>
|
||||
<CodeGroup title="Formbricks Env for FusionAuth OIDC">
|
||||
```yml {{ title: '.env' }}
|
||||
OIDC_CLIENT_ID=59cada54-56d4-4aa8-a5e7-5823bbe0e5b7
|
||||
OIDC_CLIENT_SECRET=4f4dwP0ZoOAqMW8fM9290A7uIS3E8Xg29xe1umhlB_s
|
||||
OIDC_ISSUER=http://localhost:9011
|
||||
OIDC_DISPLAY_NAME=FusionAuth
|
||||
OIDC_SIGNING_ALGORITHM=HS256
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. Set an environment variable `OIDC_DISPLAY_NAME` to the display name of your OIDC provider.
|
||||
|
||||
4. Restart your Formbricks instance.
|
||||
|
||||
5. You're all set! Users can now signup & log in using their OIDC credentials.
|
||||
|
||||
## Important Run-time Variables
|
||||
|
||||
These variables can be provided at the runtime i.e. in your docker-compose file.
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
| --------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------- |
|
||||
| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` |
|
||||
| DATABASE_URL | Database URL with credentials. | required | |
|
||||
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |
|
||||
| ENCRYPTION_KEY | Secret for used by Formbricks for data 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` |
|
||||
| S3_ACCESS_KEY | Access key for S3. | optional (required if S3 is enabled) | |
|
||||
| S3_SECRET_KEY | Secret key for S3. | optional (required if S3 is enabled) | |
|
||||
| S3_REGION | Region for S3. | optional (required if S3 is enabled) | |
|
||||
| S3_BUCKET | Bucket name for S3. | optional (required if S3 is enabled) | |
|
||||
| S3_ENDPOINT | Endpoint for S3. | optional (required if S3 is enabled) | |
|
||||
| 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 | |
|
||||
| EMAIL_AUTH_DISABLED | Disables the ability for users to signup or login via email and password 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 | |
|
||||
| RATE_LIMITING_DISABLED | Disables rate limiting 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_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 | |
|
||||
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | `#64748b` |
|
||||
| DEFAULT_TEAM_ID | Automatically assign new users to a specific team when joining | optional | |
|
||||
| DEFAULT_TEAM_ROLE | Role of the user in the default team. | optional | `admin` |
|
||||
| ONBOARDING_DISABLED | Disables onboarding for new users if set to `1` | optional | |
|
||||
| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | |
|
||||
| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
|
||||
| OIDC_CLIENT_SECRET | Secret for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
|
||||
| OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have `.well-known` configured at this) | optional (required if OIDC auth is enabled) | |
|
||||
| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | `RS256` |
|
||||
|
||||
## Build-time Variables
|
||||
|
||||
These variables must be provided at the time of the docker build and would require rebuilding the image.
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
| ------------------------------------- | ----------------------------------------------------- | -------- | ------- |
|
||||
| NEXT_PUBLIC_FORMBRICKS_API_HOST | Host for the Formbricks API to use inside Formbricks. | optional | |
|
||||
| NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID | Formbricks environment ID for use inside Formbricks. | optional | |
|
||||
| NEXT_PUBLIC_POSTHOG_API_KEY | API key to use PostHog analytics inside Formbricks. | optional | |
|
||||
| NEXT_PUBLIC_POSTHOG_API_HOST | Host to use PostHog analytics inside Formbricks. | optional | |
|
||||
| NEXT_PUBLIC_SENTRY_DSN | DSN for Sentry error tracking inside Formbricks. | optional | |
|
||||
|
||||
Still facing issues? [Join our Discord!](https://formbricks.com/discord) and we'd be glad to assist you!
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
export const metadata = {
|
||||
title: "About the Formbricks Open-Source License",
|
||||
title: "Self-hosting License to run Formbricks on premises",
|
||||
description:
|
||||
"The Formbricks core is available under the AGPLv3 license",
|
||||
"Request a self-hosting licenses to run Formbricks on your own servers.",
|
||||
};
|
||||
|
||||
#### Self-Hosting
|
||||
|
||||
# License
|
||||
# License Request (Beta)
|
||||
|
||||
## The AGPL Formbricks Core
|
||||
We're handing out free self-hosting license keys during our Beta.
|
||||
|
||||
The Formbricks core application is licensed under the [AGPLv3 Open Source License](https://github.com/formbricks/formbricks/blob/main/LICENSE). The core application is fully functional and includes everything you need to design & run link surveys, website surveys and in-app surveys. You can use the software for free for personal and commercial use. You're also allowed to create and distribute modified versions as long as you document the changes you make incl. date. The AGPL license requires you to publish your modified version under the AGPLv3 license as well.
|
||||
**Please note:** Sooner than later we will introduce a self-hosting license pricing. For a free beta key, fill out this form:
|
||||
|
||||
## The Enterprise Edition
|
||||
<div style={{ position: 'relative', height: '100vh', maxHeight: '100vh', overflow: 'auto', borderRadius:'12px' }}>
|
||||
<iframe
|
||||
src="https://app.formbricks.com/s/clrf4z8zg1u3912250j7shqb5"
|
||||
style={{ position: 'absolute', left: 0, top: 0, width: '100%', height: '100%', border: 0 }}
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
Additional to the AGPL licensed Formbricks core, this repository contains code licensed under an Enterprise license. The [code](https://github.com/formbricks/formbricks/tree/main/packages/ee) and [license](https://github.com/formbricks/formbricks/blob/main/packages/ee/LICENSE) for the enterprise functionality can be found in the `/packages/ee` folder of this repository. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an [Enterprise License Key](https://formbricks.com/docs/self-hosting/enterprise) to unlock it.
|
||||
|
||||
## White-Labeling Formbricks and Other Licensing Needs
|
||||
|
||||
If you have other licensing requirements such as White-Labeling please [send us an email](mailto:hola@formbricks.com).
|
||||
|
||||
## Why charge for Enterprise Features?
|
||||
|
||||
The Enterprise Edition and White-Label Licenses allow us to fund the development of Formbricks sustainably. It guarantees that the open-source surveying infrastructure we're building will be around for decades to come.
|
||||
|
||||
**Having more questions?**: [Join our Discord!](https://formbricks.com/discord)
|
||||
**Can’t figure it out?**: [Join our Discord!](https://formbricks.com/discord)
|
||||
|
||||
@@ -8,101 +8,16 @@ export const metadata = {
|
||||
|
||||
# Migration Guide
|
||||
|
||||
## v1.6
|
||||
|
||||
Formbricks v1.6 comes with a big new features like Advanced Targeting & Segmentation of your end-users along with on-the-fly triggers for surveys and a ton of stability improvements & features. This also involves a few changes in our environment variables. This guide will help you migrate your existing Formbricks instance to v1.6 without any hassles or build errors.
|
||||
|
||||
<Note>
|
||||
This upgrade requires a **data migration**. Please make sure to backup your database before proceeding with
|
||||
the upgrade. Follow the below steps thoroughly to upgrade your Formbricks instance to v1.6.
|
||||
</Note>
|
||||
|
||||
### Steps to Migrate
|
||||
|
||||
This guide is for users who are self-hosting Formbricks using our one-click setup. If you are using a different setup, you might adjust the commands accordingly.
|
||||
|
||||
To run all these steps, please navigate to the `formbricks` folder where your `docker-compose.yml` file is located.
|
||||
|
||||
1. **Backup your Database**: This is a crucial step. Please make sure to backup your database before proceeding with the upgrade. You can use the following command to backup your database:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Backup Postgres">
|
||||
|
||||
```bash
|
||||
docker exec formbricks-quickstart-postgres-1 pg_dump -U postgres -d formbricks > formbricks_pre_v1.6_$(date +%Y%m%d_%H%M%S).sql
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
2. Stop the running Formbricks instance & remove the related containers:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the containers">
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. Restarting the containers will automatically pull the latest version of Formbricks:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Restart the containers">
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
4. Now let's migrate the data to the latest schema:
|
||||
|
||||
<Note>To find your Docker Network name for your Postgres Database, find it using `docker network ps`</Note>
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
--network=formbricks_default \
|
||||
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
|
||||
-e UPGRADE_TO_VERSION="v1.6" \
|
||||
ghcr.io/formbricks/data-migrations:v1.6
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
The above command will migrate your data to the latest schema. This is a crucial step to migrate your existing data to the new structure. Only if the script runs successful, changes are made to the database. The script can safely run multiple times.
|
||||
|
||||
5. That's it! Once the migration is complete, you can **now access your Formbricks instance** at the same URL as before.
|
||||
|
||||
### In-App Surveys with @formbricks/js
|
||||
|
||||
If you are using the `@formbricks/js` package, please make sure to update it to version 1.6.0 to use the latest features and improvements.
|
||||
|
||||
### Deprecated Environment Variables
|
||||
|
||||
| Environment Variable | Comments |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| GITHUB_AUTH_ENABLED | Was used to enable GitHub OAuth, but from v1.6, you can just set the `GITHUB_ID` and `GITHUB_SECRET` environment variables. |
|
||||
| GOOGLE_AUTH_ENABLED | Was used to enable Google OAuth, but from v1.6, you can just set the `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` environment variables. |
|
||||
| AZUREAD_AUTH_ENABLED | Was used to enable AzureAD OAuth, but from v1.6, you can just set the `AZUREAD_CLIENT_ID`, `AZUREAD_CLIENT_SECRET` & `AZUREAD_TENANT_ID` environment variables. |
|
||||
|
||||
## v1.2
|
||||
## v1.1 -> v1.2
|
||||
|
||||
Formbricks v1.2 ships a lot of features targeting our Link Surveys. We have also improved our security posture to be as robust as ever. 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.2 without any hassles or build errors.
|
||||
|
||||
### New Environment Variables
|
||||
|
||||
| Environment Variable | Required | Recommended Generation | Comments |
|
||||
| -------------------- | -------- | ----------------------- | ----------------------------------------------------------- |
|
||||
| ENCRYPTION_KEY | true | `openssl rand -hex 32` | Needed for 2 Factor Authentication |
|
||||
| SHORT_URL_BASE | false | `<your-short-base-url>` | Needed if you want to enable shorter links for Link Surveys |
|
||||
| Environment Variable | Required | Recommended Generation | Comments |
|
||||
| -------------------- | -------- | ------------------------------ | ----------------------------------------------------------- |
|
||||
| ENCRYPTION_KEY | true | `openssl rand -hex 32` | Needed for 2 Factor Authentication |
|
||||
| SHORT_URL_BASE | false | `<your-short-base-url>` | Needed if you want to enable shorter links for Link Surveys |
|
||||
|
||||
### Deprecated / Removed Environment Variables
|
||||
|
||||
@@ -110,7 +25,7 @@ Formbricks v1.2 ships a lot of features targeting our Link Surveys. We have also
|
||||
| -------------------- | ------------------------------------------------------------------------- |
|
||||
| SURVEY_BASE_URL | The WEBAPP_URL is now used to determine the survey base url in all places |
|
||||
|
||||
## v1.1
|
||||
## v1.0 -> 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.
|
||||
|
||||
@@ -198,9 +113,6 @@ x-environment: &environment
|
||||
# Uncomment the below and set it to 1 to disable Signups
|
||||
# SIGNUP_DISABLED:
|
||||
|
||||
# Uncomment the below and set it to 1 to disable loging in with email
|
||||
# EMAIL_AUTH_DISABLED:
|
||||
|
||||
# Uncomment the below and set it to 1 to disable Invites
|
||||
# INVITE_DISABLED:
|
||||
|
||||
@@ -224,7 +136,6 @@ x-environment: &environment
|
||||
# GOOGLE_CLIENT_SECRET:
|
||||
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
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!
|
||||
|
||||
@@ -10,16 +10,8 @@ export const metadata = {
|
||||
|
||||
If you want to quickly set up a production instance of Formbricks on a server running Ubuntu, we've got you covered! This method utilizes a convenient shell script that takes care of everything, including Docker, Postgres DB, and SSL certificate configuration. The shell script will automatically install all the required dependencies and configure your server, making the process a breeze.
|
||||
|
||||
<Note>
|
||||
This setup installs **Traefik** to work as a **reverse proxy**. This configuration is crucial for directing incoming traffic to the correct container, allowing Formbricks to be accessible from the internet securely.
|
||||
Traefik is chosen for its simplicity and automatic SSL management with Let's Encrypt.
|
||||
</Note>
|
||||
|
||||
|
||||
This is the quickest way to get Formbricks up and running on an Ubuntu server. The shell script will automatically install all the required dependencies and configure your server, making the process a breeze.
|
||||
|
||||
If you want to run Formbricks on a different OS or have more control over the installation process, you can follow the [advanced installation guide](/docs/self-hosting/docker) with Docker.
|
||||
|
||||
### Requirements
|
||||
|
||||
Before you proceed, make sure you have the following:
|
||||
|
||||
@@ -23,7 +23,7 @@ const variantStyles = {
|
||||
"rounded-full bg-slate-900 py-1 px-3 text-white hover:bg-slate-700 dark:bg-teal-500 dark:text-white dark:hover:bg-teal-400",
|
||||
outline:
|
||||
"rounded-full py-1 px-3 text-slate-700 ring-1 ring-inset ring-slate-900/10 hover:bg-slate-900/2.5 hover:text-slate-900 dark:text-slate-400 dark:ring-white/10 dark:hover:bg-white/5 dark:hover:text-white",
|
||||
text: "text-teal-500 hover:text-teal-700 dark:text-teal-400 dark:hover:text-teal-300",
|
||||
text: "text-teal-500 hover:text-teal-700 dark:text-teal-400 dark:hover:text-teal-600",
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
|
||||
@@ -42,7 +42,7 @@ export function Libraries() {
|
||||
<a
|
||||
key={library.name}
|
||||
href={library.href}
|
||||
className="flex flex-row-reverse gap-6 rounded-2xl p-6 transition-all duration-100 ease-in-out hover:cursor-pointer hover:bg-slate-100/50 dark:hover:bg-slate-800/50">
|
||||
className="flex flex-row-reverse gap-6 rounded-2xl p-6 transition-all duration-100 ease-in-out hover:cursor-pointer hover:bg-slate-100/50">
|
||||
<div className="flex-auto">
|
||||
<h3 className="text-sm font-semibold text-slate-900 dark:text-white">{library.name}</h3>
|
||||
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">{library.description}</p>
|
||||
|
||||
@@ -207,7 +207,6 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Identify Users", href: "/docs/link-surveys/user-identification" },
|
||||
{ title: "Single Use Links", href: "/docs/link-surveys/single-use-links" },
|
||||
{ title: "Source Tracking", href: "/docs/link-surveys/source-tracking" },
|
||||
{ title: "Hidden Fields", href: "/docs/link-surveys/hidden-fields" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -243,8 +242,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Advanced Setup", href: "/docs/self-hosting/docker" },
|
||||
{ title: "Configure", href: "/docs/self-hosting/external-auth-providers" },
|
||||
{ title: "Migration Guide", href: "/docs/self-hosting/migration-guide" },
|
||||
{ title: "License", href: "/docs/self-hosting/license" },
|
||||
{ title: "Enterprise License", href: "/docs/self-hosting/enterprise" },
|
||||
{ title: "Self-hosting License", href: "/docs/self-hosting/license" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,8 +5,6 @@ import CrowdLogoLight from "@/images/clients/crowd-logo-light.svg";
|
||||
import FlixbusLogo from "@/images/clients/flixbus-white.svg";
|
||||
import NILogoDark from "@/images/clients/niLogoDark.svg";
|
||||
import NILogoLight from "@/images/clients/niLogoWhite.svg";
|
||||
import OptimoleLogo from "@/images/clients/optimole-logo.svg";
|
||||
import ThemeisleLogo from "@/images/clients/themeisle-logo.webp";
|
||||
import AnimationFallback from "@/public/animations/opensource-xm-platform-formbricks-fallback.png";
|
||||
import { ShieldCheckIcon, StarIcon } from "@heroicons/react/24/outline";
|
||||
import { usePlausible } from "next-plausible";
|
||||
@@ -47,42 +45,48 @@ export const Hero: React.FC = ({}) => {
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div className="mx-auto mt-5 max-w-3xl items-center px-4 sm:flex sm:justify-center md:mt-6 md:space-x-8 md:px-0">
|
||||
<div className="grid grid-cols-6 items-center gap-6 pt-2 md:gap-8">
|
||||
<div className="mx-auto mt-5 max-w-2xl items-center px-4 sm:flex sm:justify-center md:mt-6 md:space-x-8 md:px-0">
|
||||
<div className="grid grid-cols-4 items-center gap-6 pt-2 md:gap-8">
|
||||
<Image
|
||||
src={FlixbusLogo}
|
||||
alt="Flixbus Flix Flixtrain Logo"
|
||||
className="rounded-lg pb-1 "
|
||||
className="rounded-lg pb-1 hover:opacity-100 md:opacity-50"
|
||||
width={200}
|
||||
/>
|
||||
<Image src={CalLogoLight} alt="Cal Logo" className="block rounded-lg dark:hidden" width={170} />
|
||||
<Image src={CalLogoDark} alt="Cal Logo" className="hidden rounded-lg dark:block" width={170} />
|
||||
<Image src={ThemeisleLogo} alt="Neverinstall Logo" className="pb-1" width={200} />
|
||||
|
||||
<Image
|
||||
src={CalLogoLight}
|
||||
alt="Cal Logo"
|
||||
className="block rounded-lg hover:opacity-100 md:opacity-50 dark:hidden"
|
||||
width={170}
|
||||
/>
|
||||
<Image
|
||||
src={CalLogoDark}
|
||||
alt="Cal Logo"
|
||||
className="hidden rounded-lg hover:opacity-100 md:opacity-50 dark:block"
|
||||
width={170}
|
||||
/>
|
||||
<Image
|
||||
src={CrowdLogoLight}
|
||||
alt="Crowd.dev Logo"
|
||||
className="block rounded-lg pb-1 dark:hidden"
|
||||
className="block rounded-lg pb-1 hover:opacity-100 md:opacity-50 dark:hidden"
|
||||
width={200}
|
||||
/>
|
||||
<Image
|
||||
src={CrowdLogoDark}
|
||||
alt="Crowd.dev Logo"
|
||||
className="hidden rounded-lg pb-1 dark:block"
|
||||
className="hidden rounded-lg pb-1 hover:opacity-100 md:opacity-50 dark:block"
|
||||
width={200}
|
||||
/>
|
||||
<Image src={OptimoleLogo} alt="Neverinstall Logo" className="pb-1" width={200} />
|
||||
<Image src={NILogoDark} alt="Neverinstall Logo" className="block pb-1 dark:hidden" width={200} />
|
||||
<Image
|
||||
src={NILogoLight}
|
||||
src={NILogoDark}
|
||||
alt="Neverinstall Logo"
|
||||
className="hidden pb-1 dark:block"
|
||||
className="block pb-1 hover:opacity-100 md:opacity-50 dark:hidden"
|
||||
width={200}
|
||||
/>
|
||||
<Image
|
||||
src={NILogoLight}
|
||||
alt="Neverinstall Logo"
|
||||
className="hidden pb-1 dark:block"
|
||||
className="hidden pb-1 hover:opacity-100 md:opacity-50 dark:block"
|
||||
width={200}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,12 @@ export const Steps: React.FC = () => {
|
||||
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
|
||||
<div className="order-last w-full rounded-lg bg-slate-100 p-4 sm:py-8 md:order-first dark:bg-slate-800">
|
||||
<div className="flex h-40 items-center justify-center">
|
||||
<Button variant="primary">
|
||||
<Button
|
||||
variant="primary"
|
||||
className=""
|
||||
onClick={() => {
|
||||
setAddEventModalOpen(true);
|
||||
}}>
|
||||
<CursorArrowRaysIcon className="mr-2 h-5 w-5 text-white" />
|
||||
Add Action
|
||||
</Button>
|
||||
|
||||
@@ -13,7 +13,7 @@ const styles: Record<string, CSSProperties> = {
|
||||
marginTop: "1rem",
|
||||
borderRadius: "0.375rem",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: "normal",
|
||||
fontWeight: "lighter",
|
||||
color: "#e5e7eb",
|
||||
},
|
||||
pre: {
|
||||
@@ -21,7 +21,7 @@ const styles: Record<string, CSSProperties> = {
|
||||
},
|
||||
code: {
|
||||
textShadow: "none",
|
||||
color: "#333333",
|
||||
color: "#fbbf24",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
@@ -200,11 +200,6 @@ const nextConfig = {
|
||||
destination: "https://app.formbricks.com/auth/signup",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/blog/preseed-announcement",
|
||||
destination: "/blog",
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
async rewrites() {
|
||||
|
||||
@@ -1,7 +1,41 @@
|
||||
import { Head, Html, Main, NextScript } from "next/document";
|
||||
|
||||
const themeScript = `
|
||||
document.documentElement.classList.remove('dark');
|
||||
let isDarkMode = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
|
||||
function updateTheme(theme) {
|
||||
theme = theme ?? window.localStorage.theme ?? 'system'
|
||||
|
||||
if (theme === 'dark' || (theme === 'system' && isDarkMode.matches)) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else if (theme === 'light' || (theme === 'system' && !isDarkMode.matches)) {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
function updateThemeWithoutTransitions(theme) {
|
||||
updateTheme(theme)
|
||||
document.documentElement.classList.add('[&_*]:!transition-none')
|
||||
window.setTimeout(() => {
|
||||
document.documentElement.classList.remove('[&_*]:!transition-none')
|
||||
}, 0)
|
||||
}
|
||||
|
||||
document.documentElement.setAttribute('data-theme', updateTheme())
|
||||
|
||||
new MutationObserver(([{ oldValue }]) => {
|
||||
let newValue = document.documentElement.getAttribute('data-theme')
|
||||
if (newValue !== oldValue) {
|
||||
try {
|
||||
window.localStorage.setItem('theme', newValue)
|
||||
} catch {}
|
||||
updateThemeWithoutTransitions(newValue)
|
||||
}
|
||||
}).observe(document.documentElement, { attributeFilter: ['data-theme'], attributeOldValue: true })
|
||||
|
||||
isDarkMode.addEventListener('change', () => updateThemeWithoutTransitions())
|
||||
`;
|
||||
|
||||
export default function Document() {
|
||||
|
||||
@@ -18,7 +18,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
{
|
||||
name: "Argos",
|
||||
description: "Argos provides the developer tools to debug tests and detect visual regressions.",
|
||||
description: "Argos provides the developer tools to debug tests and detect visual regressions..",
|
||||
href: "https://argos-ci.com",
|
||||
},
|
||||
{
|
||||
@@ -33,12 +33,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
"Cal.com is a scheduling tool that helps you schedule meetings without the back-and-forth emails.",
|
||||
href: "https://cal.com",
|
||||
},
|
||||
{
|
||||
name: "ClassroomIO.com",
|
||||
description:
|
||||
"ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease.",
|
||||
href: "https://www.classroomio.com",
|
||||
},
|
||||
{
|
||||
name: "Crowd.dev",
|
||||
description:
|
||||
@@ -159,12 +153,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
"Open-Source Docsend Alternative to securely share documents with real-time analytics.",
|
||||
href: "https://www.papermark.io/",
|
||||
},
|
||||
{
|
||||
name: "Prisma",
|
||||
description:
|
||||
"Simplify working with databases. Build, optimize, and grow your app easily with an intuitive data model, type-safety, automated migrations, connection pooling, caching, and real-time db subscriptions.",
|
||||
href: "https://www.prisma.io",
|
||||
},
|
||||
{
|
||||
name: "Requestly",
|
||||
description:
|
||||
@@ -199,12 +187,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
"The .NET Web Framework for Makers. Build production ready, full-stack web applications fast without sweating the small stuff.",
|
||||
href: "https://spark-framework.net",
|
||||
},
|
||||
{
|
||||
name: "Tiledesk",
|
||||
description:
|
||||
"The innovative open-source framework for developing LLM-enabled chatbots, Tiledesk empowers developers to create advanced, conversational AI agents.",
|
||||
href: "https://tiledesk.com",
|
||||
},
|
||||
{
|
||||
name: "Tolgee",
|
||||
description: "Software localization from A to Z made really easy.",
|
||||
@@ -228,12 +210,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
"A modern CRM offering the flexibility of open-source, advanced features and sleek design.",
|
||||
href: "https://twenty.com",
|
||||
},
|
||||
{
|
||||
name: "UnInbox",
|
||||
description:
|
||||
"Modern email for teams and professionals. Bringing the best of email and messaging into a single, modern, and secure platform.",
|
||||
href: "https://uninbox.com",
|
||||
},
|
||||
{
|
||||
name: "Unkey",
|
||||
description:
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import Image from "next/image";
|
||||
import HeaderImage from "./2023-title-best-open-source-survey-software-tools-and-alternatives.png";
|
||||
import LimeSurvey from "./free-survey-tool-limesurvey-open-source-software-opensource.png";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import Formbricks from "./open-source-survey-software-free-2023-formbricks-typeform-alternative.png";
|
||||
import OpnForm from "./opnform-free-open-source-form-survey-tools-builder-2023-self-hostign.jpg";
|
||||
import SurveyJS from "./surveyjs-free-opensource-form-survey-tool-software-to-make-surveys-2023.png";
|
||||
import Typebot from "./typebot-open-source-free-conversational-form-builder-survey-software-opensource.jpg";
|
||||
import LimeSurvey from "./free-survey-tool-limesurvey-open-source-software-opensource.png";
|
||||
import OpnForm from "./opnform-free-open-source-form-survey-tools-builder-2023-self-hostign.jpg";
|
||||
import HeaderImage from "./2023-title-best-open-source-survey-software-tools-and-alternatives.png";
|
||||
import SurveyJS from "./surveyjs-free-opensource-form-survey-tool-software-to-make-surveys-2023.png";
|
||||
|
||||
export const meta = {
|
||||
title: "5 Open Source Survey and Form Tools maintained in 2024",
|
||||
title: "5 Open Source Survey and Form Tools maintained in 2023",
|
||||
description:
|
||||
"Most open source projects get abandoned after a while. But these 5 open source survey tools are still alive and kicking in 2024.",
|
||||
"Most open source projects get abandoned after a while. But these 5 open source survey tools are still alive and kicking in 2023.",
|
||||
date: "2023-04-12",
|
||||
publishedTime: "2023-04-12T12:00:00",
|
||||
authors: ["Johannes"],
|
||||
@@ -20,17 +21,17 @@ export const meta = {
|
||||
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" author={"Johannes"} />
|
||||
|
||||
_Most open source projects get abandoned after a while. But these 5 open source survey tools are still alive and kicking in 2024._
|
||||
_Most open source projects get abandoned after a while. But these 5 open source survey tools are still alive and kicking in 2023._
|
||||
|
||||
<Image
|
||||
src={HeaderImage}
|
||||
alt="Open source survey tool self-hostable: Find the 5 best (and maintained) open source survey tool 2024."
|
||||
alt="Open source survey tool self-hostable: Find the 5 best (and maintained) open source survey tool 2023."
|
||||
className="rounded-lg w-full"
|
||||
/>
|
||||
|
||||
Looking for the perfect open source survey tool to help you gather valuable insights and improve your business? Look no further!
|
||||
|
||||
We've compiled a list of the top 5 open source form and survey tools that are still maintained in 2024. In app surveys, conversational bots, AI-generated surveys: These open source tools offer various features that cater to different needs.
|
||||
We've compiled a list of the top 5 open source form and survey tools that are still maintained in 2023. In app surveys, conversational bots, AI-generated surveys: These open source tools offer various features that cater to different needs.
|
||||
|
||||
## 1. Formbricks - In app micro surveys
|
||||
|
||||
@@ -123,7 +124,7 @@ LimeSurvey has been around for at least a decade. It's a powerful survey tool ma
|
||||
|
||||
## Summary ☟
|
||||
|
||||
In this article, we've rounded up the top 5 open source form and survey tools that are still rocking it in 2024. Perfect for devs who are always on the lookout for the latest and greatest!
|
||||
In this article, we've rounded up the top 5 open source form and survey tools that are still rocking it in 2023. Perfect for devs who are always on the lookout for the latest and greatest!
|
||||
|
||||
1. Formbricks: A game-changer for in app micro surveys, letting you target specific customer segments at any point in their journey. It's still early days, but this bad boy is worth keeping an eye on.
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import Image from "next/image";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import Preseed from "./preseed-header.webp";
|
||||
|
||||
|
||||
export const meta = {
|
||||
title: "We raised Pre-Seed Funding 💸",
|
||||
description:
|
||||
"We’re delighted to announce that Formbricks successfully acquired pre-seed funding in May 2023.",
|
||||
date: "2023-11-01",
|
||||
publishedTime: "2023-11-01T12:00:00",
|
||||
authors: ["Johannes"],
|
||||
section: "Open Source Surveys",
|
||||
tags: ["Open Source Surveys", "Formbricks", "Typeform", "SurveyJS", "Typebot", "OpnForm", "LimeSurvey"],
|
||||
};
|
||||
|
||||
<Image
|
||||
src={Preseed}
|
||||
alt="Formbricks raises a preseed round led by OSS Capital"
|
||||
className="w-full rounded-lg"
|
||||
/>
|
||||
|
||||
_We’re delighted to announce that Formbricks successfully acquired pre-seed funding in May 2023._
|
||||
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="November 1st, 2023" duration="2" author={"Johannes"}/>
|
||||
|
||||
The Formbricks pre-seed round was led by [OSS Capital](https://oss.capital/portfolio) with participation of Peer Richelsen, co-founder at [Cal.com](http://Cal.com), as well as other angel investors.
|
||||
|
||||
## OSS Capital leads the round
|
||||
|
||||
We are both humbled and excited to have won such highly specialized and experienced investors for our mission! This influx of capital allows us to be even more ambitious in shaping the future of Experience Management globally.
|
||||
|
||||
We are on a mission to enable organizations to **gather, analyze and leverage** qualitative data to improve customer experience from first principles. Building Open Source uniquely allows us to offer a privacy-first solution which can be natively embedded into existing products and services.
|
||||
|
||||
Even in large organizations, every product decision **can** be based on user insight. We make that possible.
|
||||
|
||||
## How it started, and how it's going
|
||||
|
||||
Matti and Johannes kicked this off as a side project a good year ago. Today, we have several thousand users collecting valuable insights with both in-product surveys and standalone surveys.
|
||||
|
||||
Over the past couple of months, we gathered:
|
||||
|
||||
- 4.000+ GitHub stars ⭐
|
||||
- 3.200+ Dockerhub clones 🐳
|
||||
- 750+ Discord Members 👻
|
||||
- 120+ community contributors 👨👩👧
|
||||
|
||||
… and we are just warming up!
|
||||
|
||||
Our journey has just begun, and with the support of our investors and the enthusiasm of our community, we can’t wait to see where we stand 6 months from now!
|
||||
|
||||
## Onwards,
|
||||
|
||||
The Formbricks Team 🤍
|
||||
|
||||
export default ({ children }) => <LayoutMdx meta={meta}>{children}</LayoutMdx>;
|
||||
|
After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
@@ -1,16 +1,17 @@
|
||||
import Image from "next/image";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import Image from "next/image";
|
||||
import Awareness from './awareness-urgency-waitlist-questions-survey-form.png';
|
||||
import Formbricks from './formbricks-dashboard.png';
|
||||
import Header from './cover-waitlist-survey-questions-open-source-free-tool copy.webp';
|
||||
import Waiting from './waitlist-survey-best-practices-waiting-for-submissions.webp';
|
||||
import Awareness from './awareness-urgency-waitlist-questions-survey-form.png';
|
||||
import Demographics from './demographics-contact-waitlist-survey-questions-for-more-leads.png';
|
||||
import Features from './features-wip-waitlist-form-questions-perfect-open-source.png';
|
||||
import Formbricks from './formbricks-dashboard.png';
|
||||
import Expectations from './pref-expectations-what-to-ask-waitlist-survey-form-open-source.png';
|
||||
import Testing from './testing-feedback-waitlist-questions-form-survey.png';
|
||||
import Waiting from './waitlist-survey-best-practices-waiting-for-submissions.webp';
|
||||
|
||||
export const meta = {
|
||||
title: "The perfect Waitlist Survey - What to ask, why, and how to get the most out of your leads",
|
||||
title: "The Perfect Waitlist Survey - What To Ask, Why, And How To Get The Most Out Of Your Leads",
|
||||
description:
|
||||
"Using a waitlist for product validation is tough. Use this waitlist best practice to gain clarity with actionable tips on questions to ask and insights utilization in this article.",
|
||||
date: "2024-01-23",
|
||||
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
@@ -2,24 +2,23 @@ import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import Image from "next/image";
|
||||
|
||||
import Container from "./container.webp";
|
||||
import DockerCompose from "./docker-compose.webp";
|
||||
import Docker from "./docker.webp";
|
||||
import EndingGif from "./ending.gif";
|
||||
import Header from "./header.webp";
|
||||
import SelfHostGif from "./self-host.gif";
|
||||
import Container from "./container.webp";
|
||||
import Docker from "./docker.webp";
|
||||
import DockerCompose from "./docker-compose.webp";
|
||||
import SmartestPersonGif from "./smartest-person.gif";
|
||||
import SelfHostGif from "./self-host.gif";
|
||||
import SuperSonicGif from "./supersonic.gif";
|
||||
import EndingGif from "./ending.gif";
|
||||
|
||||
export const meta = {
|
||||
title: "Understand Formbricks Self Hosting",
|
||||
description:
|
||||
"We explain how we internally built Formbricks self hosting architecture with Docker, Docker Compose, and Bash Script. It's easy and to-the-point!",
|
||||
description:"We explain how we internally built Formbricks self hosting architecture with Docker, Docker Compose, and Bash Script. It's easy and to-the-point!",
|
||||
date: "2024-01-23",
|
||||
publishedTime: "2024-01-23T12:00:00",
|
||||
authors: ["Shubham Palriwala"],
|
||||
section: "Infrastructure",
|
||||
tags: ["Infrastructure", "Docker", "Docker Compose", "Bash Script", "Self-Hosting"],
|
||||
tags: ["Infrastructure", "Docker", "Docker Compose", "Bash Script", "Self Hosting"],
|
||||
};
|
||||
|
||||
<Image src={Header} alt="How to effectively self host Formbricks" className="w-full rounded-lg" />
|
||||
@@ -34,7 +33,7 @@ export const meta = {
|
||||
|
||||
_Embracing the open-source ethos of sharing knowledge, we're unveiling the inner workings of self-hosting a Formbricks instance. This initiative is designed to share & learn with [our community](https://formbricks.com/discord), providing deep insights into Formbricks' features for all, from developers to end-users._
|
||||
|
||||
## What is Self-Hosting?
|
||||
## What is Self Hosting?
|
||||
|
||||
Self-hosting allows you to manage our Formbricks app **on your own server infrastructure**, providing complete control over its environment. This contrasts with relying on external cloud services. At Formbricks, we facilitate a swift and straightforward self-hosting process, enabling quick setup of your instance. For those who prefer a quicker way to get things going rather than managing it on your own, our cloud-based solution is readily accessible at [app.formbricks.com](https://app.formbricks.com).
|
||||
|
||||
@@ -55,6 +54,7 @@ Deciding to self-host is particularly advantageous in certain scenarios:
|
||||
|
||||
In these situations, self-hosting offers an unmatched level of control and customization.
|
||||
|
||||
|
||||
### What to expect from this guide?
|
||||
|
||||
Embark on an insightful journey into the world of Formbricks self-hosting. We'll dive deep into two key methods: the [Advanced Docker Setup](https://formbricks.com/docs/self-hosting/docker) and the [Single Script Setup](https://formbricks.com/docs/self-hosting/production). Along the way, we'll demystify several core concepts:
|
||||
@@ -95,14 +95,12 @@ Docker is a container runtime that enables you to abstract your projects into re
|
||||
Building on the earlier analogy, Docker can be seen as the fleet of trucks that transport these secure boxes (containers). Each truck (Docker) is equipped to carry a container, ensuring it reaches its destination intact and operates smoothly. Docker takes care of the driving, navigating through different environments (operating systems), and making sure each container is running as it should, regardless of where it's deployed.
|
||||
|
||||
<Note>
|
||||
It's important to note that Docker is a **runtime environment** — it's responsible for running the
|
||||
containers, rather than being a technology that creates them. While Docker is widely used, there are other
|
||||
alternatives in the field, such as Podman and Containerd, which also offer container runtime capabilities.
|
||||
It's important to note that Docker is a **runtime environment** — it's responsible for running the containers, rather than being a technology that creates them. While Docker is widely used, there are other alternatives in the field, such as Podman and Containerd, which also offer container runtime capabilities.
|
||||
</Note>
|
||||
|
||||
### Understanding Formbricks Dockerfile
|
||||
|
||||
The Dockerfile for our Formbricks application is structured into two key stages - the Builder and Runner stages. It’s currently hosted [here](https://github.com/formbricks/formbricks/blob/main/apps/web/Dockerfile).
|
||||
The Dockerfile for our Formbricks application is structured into two key stages - the Builder and Runner stages. It’s currently hosted [here](https://github.com/formbricks/formbricks/blob/main/apps/web/Dockerfile).
|
||||
|
||||
<CodeGroup title="Dockerfile">
|
||||
|
||||
@@ -208,6 +206,7 @@ CMD supercronic -quiet /app/docker/cronjobs & \
|
||||
4. **Port and Volume:** Exposes port 3000 and sets up a persistent volume mapping for uploads.
|
||||
5. **Application Launch:** Uses a command sequence to run
|
||||
|
||||
|
||||
### 3. What is Docker Compose
|
||||
|
||||
Docker Compose is a tool designed to manage multiple containers, streamlining the process of running multi-container Docker applications. It allows for the configuration of how these containers interact, including mapping ports, creating, and sharing data volumes. This orchestration is crucial when your application comprises several interconnected containers, each with its specific role.
|
||||
@@ -286,11 +285,13 @@ x-environment: &environment
|
||||
# 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 if you want to enable GitHub OAuth
|
||||
# Uncomment the below and set to 1 if you want to enable GitHub OAuth
|
||||
# GITHUB_AUTH_ENABLED:
|
||||
# GITHUB_ID:
|
||||
# GITHUB_SECRET:
|
||||
|
||||
# Uncomment the below if you want to enable Google OAuth
|
||||
# Uncomment the below and set to 1 if you want to enable Google OAuth
|
||||
# GOOGLE_AUTH_ENABLED:
|
||||
# GOOGLE_CLIENT_ID:
|
||||
# GOOGLE_CLIENT_SECRET:
|
||||
|
||||
@@ -331,28 +332,28 @@ volumes:
|
||||
</CodeGroup>
|
||||
|
||||
- Version Specification:
|
||||
- **`version: "3.3"`**: Specifies the Docker Compose file version, which determines the syntax and functionalities available.
|
||||
- **`version: "3.3"`**: Specifies the Docker Compose file version, which determines the syntax and functionalities available.
|
||||
- Environment Variables Setup (x-environment):
|
||||
- **`x-environment: &environment`**: A reusable anchor for environment variables, detailing settings for the Formbricks instance, database connections, authentication, and optional configurations like email, OAuth, and URLs for terms/privacy.
|
||||
- **`x-environment: &environment`**: A reusable anchor for environment variables, detailing settings for the Formbricks instance, database connections, authentication, and optional configurations like email, OAuth, and URLs for terms/privacy.
|
||||
- Services Configuration:
|
||||
1. **Postgres Service**:
|
||||
- **`postgres:`**: Defines the PostgreSQL service.
|
||||
- **`restart: always`**: Ensures the container restarts automatically if it stops.
|
||||
- **`image: postgres:15-alpine`**: Uses the Postgres 15 image on Alpine Linux, a lightweight version.
|
||||
- **`volumes: - postgres:/var/lib/postgresql/data`**: Maps a named volume **`postgres`** to persist database data.
|
||||
- **`<<: *environment`**: Inherits the environment settings defined earlier.
|
||||
2. **Formbricks Service**:
|
||||
- **`formbricks:`**: Defines the Formbricks application service.
|
||||
- **`restart: always`**: Similar to Postgres, configures the container to restart automatically.
|
||||
- **`image: ghcr.io/formbricks/formbricks:latest`**: Pulls the latest Formbricks image from GitHub Container Registry.
|
||||
- **`depends_on: - postgres`**: Specifies that Formbricks depends on the PostgreSQL service.
|
||||
- **`ports: - 3000:3000`**: Maps port 3000 from the container to the host, allowing web access to the application.
|
||||
- **`volumes: - uploads:/home/nextjs/apps/web/uploads/`**: Sets a volume for uploads.
|
||||
- **`<<: *environment`**: Inherits environment settings.
|
||||
1. **Postgres Service**:
|
||||
- **`postgres:`**: Defines the PostgreSQL service.
|
||||
- **`restart: always`**: Ensures the container restarts automatically if it stops.
|
||||
- **`image: postgres:15-alpine`**: Uses the Postgres 15 image on Alpine Linux, a lightweight version.
|
||||
- **`volumes: - postgres:/var/lib/postgresql/data`**: Maps a named volume **`postgres`** to persist database data.
|
||||
- **`<<: *environment`**: Inherits the environment settings defined earlier.
|
||||
2. **Formbricks Service**:
|
||||
- **`formbricks:`**: Defines the Formbricks application service.
|
||||
- **`restart: always`**: Similar to Postgres, configures the container to restart automatically.
|
||||
- **`image: ghcr.io/formbricks/formbricks:latest`**: Pulls the latest Formbricks image from GitHub Container Registry.
|
||||
- **`depends_on: - postgres`**: Specifies that Formbricks depends on the PostgreSQL service.
|
||||
- **`ports: - 3000:3000`**: Maps port 3000 from the container to the host, allowing web access to the application.
|
||||
- **`volumes: - uploads:/home/nextjs/apps/web/uploads/`**: Sets a volume for uploads.
|
||||
- **`<<: *environment`**: Inherits environment settings.
|
||||
- Volumes Definition:
|
||||
- **`volumes:`**
|
||||
- **`postgres: { driver: local }`**: Defines a local volume for PostgreSQL data, ensuring data persistence.
|
||||
- **`uploads:`**: Sets up a volume for storing uploads in the Formbricks application.
|
||||
- **`volumes:`**
|
||||
- **`postgres: { driver: local }`**: Defines a local volume for PostgreSQL data, ensuring data persistence.
|
||||
- **`uploads:`**: Sets up a volume for storing uploads in the Formbricks application.
|
||||
|
||||
This Docker Compose file orchestrates the Formbricks application and its database, providing a harmonized and efficient deployment setup. It highlights the ease of configuring and running a multi-container application, where each service is finely tuned and interconnected.
|
||||
|
||||
@@ -362,18 +363,20 @@ This Docker Compose file orchestrates the Formbricks application and its databas
|
||||
className="w-full rounded-lg"
|
||||
/>
|
||||
|
||||
|
||||
That’s it! **You’ve understood our Advanced Docker Setup** in the Self Hosting Stack! Congratulations! Now lets go a step further and understand our cool Single Script Setup too!
|
||||
|
||||
|
||||
### Quick Server Concepts before we understand our Single Script Setup
|
||||
|
||||
1. **Proxy**:
|
||||
A proxy server acts as an intermediary between a user's computer and the internet. It's used to request resources from other servers, offering benefits like improved security and performance, and controlled access.
|
||||
A proxy server acts as an intermediary between a user's computer and the internet. It's used to request resources from other servers, offering benefits like improved security and performance, and controlled access.
|
||||
2. **Reverse Proxy**:
|
||||
A reverse proxy sits in front of web servers and forwards client requests to those web servers. It's key for load balancing, providing SSL termination, and ensuring secure and anonymous browsing.
|
||||
A reverse proxy sits in front of web servers and forwards client requests to those web servers. It's key for load balancing, providing SSL termination, and ensuring secure and anonymous browsing.
|
||||
3. **SSL (Secure Sockets Layer)**:
|
||||
SSL is a standard security technology for establishing an encrypted link between a web server and a browser. It ensures that all data passed between the web server and browsers remain private and integral, a must-have for securing online transactions.
|
||||
SSL is a standard security technology for establishing an encrypted link between a web server and a browser. It ensures that all data passed between the web server and browsers remain private and integral, a must-have for securing online transactions.
|
||||
4. **Cronjobs**:
|
||||
Cronjobs are scheduled tasks that automate scripts at specified times. They're crucial for routine tasks like backups and system updates. However, managing cronjobs in Docker can be challenging due to its isolated environment.
|
||||
Cronjobs are scheduled tasks that automate scripts at specified times. They're crucial for routine tasks like backups and system updates. However, managing cronjobs in Docker can be challenging due to its isolated environment.
|
||||
|
||||
### Struggles with running Cronjobs in Docker env
|
||||
|
||||
@@ -428,11 +431,11 @@ This script encapsulates all necessary steps for a user-friendly setup and maint
|
||||
|
||||
<Image
|
||||
src={EndingGif}
|
||||
alt="Explained Formbricks Self-Hosting Setup in an easy to understand way"
|
||||
alt="Explained Formbricks Self Hosting Setup in an easy to understand way"
|
||||
className="w-full rounded-lg"
|
||||
/>
|
||||
|
||||
That’s it amigo! If you’re reading this, we hope you’ve understood our Self-Hosting stack inside out now! Thank you for taking the time to read through this comprehensive guide on Formbricks self-hosting. Don't forget to check out the [Slides](https://pitch.com/v/understanding-Formbricks-e2e-tests-i54auf) for this session.
|
||||
That’s it amigo! If you’re reading this, we hope you’ve understood our Self Hosting stack inside out now! Thank you for taking the time to read through this comprehensive guide on Formbricks self-hosting. Don't forget to check out the [Slides](https://pitch.com/v/understanding-Formbricks-e2e-tests-i54auf) for this session.
|
||||
|
||||
Your participation and feedback are vital in shaping our community and its resources. So, [join us on Discord](https://formbricks.com/discord), to be a part of the next session, engage in lively discussions, connect with fellow enthusiasts and Formbricks team!
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ const FAQ = [
|
||||
{
|
||||
question: "Why is there a Commercial plan?",
|
||||
answer:
|
||||
"The commercial plan is for features who break the OSS WIN-WIN Loop or incur additional cost. We charge 30$ if you want a custom domain, remove Formbricks branding, collect large files in surveys or collect payments. We think that’s fair :)",
|
||||
"The commercial plan is for features who break the OSS WIN-WIN Loop or incur additional cost. We charge 29$ if you want a custom domain, remove Formbricks branding, collect large files in surveys or collect payments. We think that’s fair :)",
|
||||
},
|
||||
{
|
||||
question: "Are your in app surveys also free forever?",
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function FeatureChaserPage() {
|
||||
</h3>
|
||||
<p className="text-slate-600 dark:text-slate-400">
|
||||
Once you've embedded the Formbricks Widget in your application, you can start following user
|
||||
actions. Simply use our No-Code Action wizard to keep track of different actions users perform -
|
||||
actions. Simply use our No-Code Action wizard to keep track of different actions users perfrom -
|
||||
100% GPDR compliant.
|
||||
</p>
|
||||
<UseCaseCTA href="/docs/best-practices/feature-chaser" />
|
||||
|
||||
@@ -22,7 +22,8 @@ const inProductSurveys = {
|
||||
{ name: "API Access", free: true, paid: true },
|
||||
{ name: "30+ Templates", free: true, paid: true },
|
||||
{ name: "Unlimited Responses per Survey", free: false, paid: true },
|
||||
{ name: "Team Role Management", free: false, paid: true },
|
||||
{ name: "Team Role Management", free: false, paid: true, comingSoon: true },
|
||||
{ name: "Advanced User Targeting", free: false, paid: true, comingSoon: true },
|
||||
{ name: "Multi Language Surveys", free: false, paid: true, comingSoon: true },
|
||||
],
|
||||
endRow: {
|
||||
@@ -57,11 +58,11 @@ const userSegmentation = {
|
||||
{ name: "Identify Users", free: true, paid: true },
|
||||
{ name: "Collect Events", free: true, paid: true },
|
||||
{ name: "Collect Attributes", free: true, paid: true },
|
||||
{ name: "Reusable Segments", free: true, paid: true },
|
||||
{ name: "Advanced Targeting", free: false, paid: true },
|
||||
{ name: "Advanced User Targeting", free: false, paid: true, comingSoon: true },
|
||||
{ name: "Reusable Segments", free: false, paid: true, comingSoon: true },
|
||||
],
|
||||
endRow: {
|
||||
title: "User Segmentation",
|
||||
title: "User Segmentation like Segment",
|
||||
free: "Free",
|
||||
paid: (
|
||||
<div>
|
||||
@@ -90,14 +91,13 @@ const linkSurveys = {
|
||||
{ name: "Unlimited Responses", free: true, paid: true },
|
||||
{ name: "Partial Responses", free: true, paid: true },
|
||||
{ name: "Multi-media Backgrounds", free: true, paid: true },
|
||||
{ name: "File Upload", free: true, paid: true },
|
||||
{ name: "File Upload", free: true, paid: true, comingSoon: true },
|
||||
{ name: "Hidden Fields", free: true, paid: true },
|
||||
{ name: "Single Use Survey Links", free: true, paid: true },
|
||||
{ name: "Pin-protected Surveys", free: true, paid: true },
|
||||
{ name: "Custom Styling", free: true, paid: true, comingSoon: true },
|
||||
{ name: "Recall Information", free: true, paid: true },
|
||||
{ name: "Book appointments (powered by cal.com)", free: true, paid: true },
|
||||
{ name: "Collect Payments and Signatures", free: true, paid: true, comingSoon: true },
|
||||
{ name: "Recall Information", free: true, paid: true, comingSoon: true },
|
||||
{ name: "Collect Payments, Signatures and Appointments", free: true, paid: true, comingSoon: true },
|
||||
{ name: "Custom URL", free: false, paid: true, comingSoon: true },
|
||||
{ name: "Remove Formbricks Branding", free: false, paid: true },
|
||||
],
|
||||
|
||||
@@ -22,18 +22,18 @@ export default function typographyStyles({ theme }: PluginUtils) {
|
||||
"--tw-prose-th-borders": theme("colors.slate.300"),
|
||||
"--tw-prose-td-borders": theme("colors.slate.200"),
|
||||
|
||||
"--tw-prose-invert-body": theme("colors.slate.200"),
|
||||
"--tw-prose-invert-body": theme("colors.slate.400"),
|
||||
"--tw-prose-invert-headings": theme("colors.white"),
|
||||
"--tw-prose-invert-links": theme("colors.teal.200"),
|
||||
"--tw-prose-invert-links": theme("colors.teal.400"),
|
||||
"--tw-prose-invert-links-hover": theme("colors.teal.500"),
|
||||
"--tw-prose-invert-links-underline": theme("colors.teal.500 / 0.3"),
|
||||
"--tw-prose-invert-bold": theme("colors.white"),
|
||||
"--tw-prose-invert-counters": theme("colors.slate.200"),
|
||||
"--tw-prose-invert-counters": theme("colors.slate.400"),
|
||||
"--tw-prose-invert-bullets": theme("colors.slate.600"),
|
||||
"--tw-prose-invert-hr": theme("colors.white / 0.05"),
|
||||
"--tw-prose-invert-quotes": theme("colors.slate.100"),
|
||||
"--tw-prose-invert-quote-borders": theme("colors.slate.700"),
|
||||
"--tw-prose-invert-captions": theme("colors.slate.200"),
|
||||
"--tw-prose-invert-captions": theme("colors.slate.400"),
|
||||
"--tw-prose-invert-code": theme("colors.white"),
|
||||
"--tw-prose-invert-code-bg": theme("colors.slate.700 / 0.15"),
|
||||
"--tw-prose-invert-code-ring": theme("colors.white / 0.1"),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
import base from "../../packages/tailwind-config/tailwind.config";
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,27 +1,11 @@
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
#
|
||||
## step 1: Prune monorepo
|
||||
#
|
||||
FROM base AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
RUN yarn global add turbo
|
||||
COPY . .
|
||||
RUN turbo prune @formbricks/web --docker
|
||||
|
||||
#
|
||||
## step 2: Install & build
|
||||
#
|
||||
FROM base AS installer
|
||||
# Installer stage: Building the application
|
||||
FROM node:20-alpine AS installer
|
||||
|
||||
# Enable corepack and prepare pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
# Install necessary build tools and compilers
|
||||
RUN apk update && apk add --no-cache g++ cmake make gcc python3 openssl-dev jq
|
||||
RUN apk update && apk add --no-cache g++ cmake make gcc python3 openssl-dev
|
||||
|
||||
# Install Supercronic (cron for containers without super user privileges)
|
||||
RUN apk add --no-cache curl \
|
||||
@@ -44,30 +28,22 @@ ARG NEXT_PUBLIC_SENTRY_DSN
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the package information
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
# Copy the application files
|
||||
COPY . .
|
||||
|
||||
# Create a .env file
|
||||
RUN touch /app/apps/web/.env
|
||||
|
||||
# Install the dependencies
|
||||
RUN pnpm install
|
||||
|
||||
# Prepare the build
|
||||
COPY --from=builder /app/out/full/ .
|
||||
# Create a .env file
|
||||
RUN touch /app/apps/web/.env
|
||||
|
||||
# Build the project
|
||||
RUN pnpm post-install --filter=web...
|
||||
RUN pnpm turbo run build --filter=web...
|
||||
|
||||
# Extract Prisma version
|
||||
RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_version.txt
|
||||
|
||||
#
|
||||
## step 3: setup production runner
|
||||
#
|
||||
FROM base AS runner
|
||||
# Runner stage: Setting up the runtime environment
|
||||
FROM node:20-alpine AS runner
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
RUN apk add --no-cache curl \
|
||||
@@ -85,8 +61,7 @@ COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/static ./apps/we
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migrations ./packages/database/migrations
|
||||
COPY --from=installer --chown=nextjs:nextjs /prisma_version.txt .
|
||||
COPY /docker/cronjobs /app/docker/cronjobs
|
||||
COPY --from=installer /app/docker/cronjobs /app/docker/cronjobs
|
||||
|
||||
EXPOSE 3000
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
@@ -96,12 +71,11 @@ USER nextjs
|
||||
RUN mkdir -p /home/nextjs/apps/web/uploads/
|
||||
VOLUME /home/nextjs/apps/web/uploads/
|
||||
|
||||
CMD PRISMA_VERSION=$(cat prisma_version.txt) && \
|
||||
supercronic -quiet /app/docker/cronjobs & \
|
||||
CMD supercronic -quiet /app/docker/cronjobs & \
|
||||
if [ "$NEXTAUTH_SECRET" != "RANDOM_STRING" ]; then \
|
||||
pnpm dlx prisma@$PRISMA_VERSION migrate deploy && \
|
||||
pnpm dlx prisma migrate deploy && \
|
||||
exec node apps/web/server.js; \
|
||||
else \
|
||||
echo "ERROR: Please set a value for NEXTAUTH_SECRET in your docker compose variables!" >&2; \
|
||||
exit 1; \
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -4,7 +4,7 @@ import { formbricksEnabled } from "@/app/lib/formbricks";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import formbricks from "@formbricks/js";
|
||||
import { env } from "@formbricks/lib/env";
|
||||
import { env } from "@formbricks/lib/env.mjs";
|
||||
|
||||
type UsageAttributesUpdaterProps = {
|
||||
numSurveys: number;
|
||||
|
||||
21
apps/web/app/(app)/components/PosthogIdentify.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import type { Session } from "next-auth";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { env } from "@formbricks/lib/env.mjs";
|
||||
|
||||
const posthogEnabled = env.NEXT_PUBLIC_POSTHOG_API_KEY && env.NEXT_PUBLIC_POSTHOG_API_HOST;
|
||||
|
||||
export default function PosthogIdentify({ session }: { session: Session }) {
|
||||
const posthog = usePostHog();
|
||||
|
||||
useEffect(() => {
|
||||
if (posthogEnabled && session.user && posthog) {
|
||||
posthog.identify(session.user.id, { name: session.user.name, email: session.user.email });
|
||||
}
|
||||
}, [session, posthog]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -116,7 +116,7 @@ export const getActionCountInLast7DaysAction = async (actionClassId: string, env
|
||||
return await getActionCountInLast7Days(actionClassId);
|
||||
};
|
||||
|
||||
export const getActiveInactiveSurveysAction = async (
|
||||
export const GetActiveInactiveSurveysAction = async (
|
||||
actionClassId: string,
|
||||
environmentId: string
|
||||
): Promise<{ activeSurveys: string[]; inactiveSurveys: string[] }> => {
|
||||
|
||||
@@ -11,10 +11,10 @@ import { Label } from "@formbricks/ui/Label";
|
||||
import LoadingSpinner from "@formbricks/ui/LoadingSpinner";
|
||||
|
||||
import {
|
||||
GetActiveInactiveSurveysAction,
|
||||
getActionCountInLast7DaysAction,
|
||||
getActionCountInLast24HoursAction,
|
||||
getActionCountInLastHourAction,
|
||||
getActiveInactiveSurveysAction,
|
||||
} from "../actions";
|
||||
|
||||
interface ActivityTabProps {
|
||||
@@ -49,7 +49,7 @@ export default function EventActivityTab({ actionClass, environmentId }: Activit
|
||||
getActionCountInLastHourAction(actionClass.id, environmentId),
|
||||
getActionCountInLast24HoursAction(actionClass.id, environmentId),
|
||||
getActionCountInLast7DaysAction(actionClass.id, environmentId),
|
||||
getActiveInactiveSurveysAction(actionClass.id, environmentId),
|
||||
GetActiveInactiveSurveysAction(actionClass.id, environmentId),
|
||||
]);
|
||||
setNumEventsLastHour(numEventsLastHourData);
|
||||
setNumEventsLast24Hours(numEventsLast24HoursData);
|
||||
|
||||
@@ -4,6 +4,9 @@ import {
|
||||
deleteActionClassAction,
|
||||
updateActionClassAction,
|
||||
} from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions";
|
||||
import { CssSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/CssSelector";
|
||||
import { InnerHtmlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/InnerHtmlSelector";
|
||||
import { PageUrlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/PageUrlSelector";
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
@@ -13,7 +16,6 @@ import { toast } from "react-hot-toast";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { TActionClassInput, TActionClassNoCodeConfig, TNoCodeConfig } from "@formbricks/types/actionClasses";
|
||||
import { TMembershipRole } from "@formbricks/types/memberships";
|
||||
import { CssSelector, InnerHtmlSelector, PageUrlSelector } from "@formbricks/ui/Actions";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { createActionClassAction } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions";
|
||||
import { CssSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/CssSelector";
|
||||
import { InnerHtmlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/InnerHtmlSelector";
|
||||
import { PageUrlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/PageUrlSelector";
|
||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
|
||||
import { Terminal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
@@ -8,7 +11,6 @@ import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { TActionClass, TActionClassInput, TActionClassNoCodeConfig } from "@formbricks/types/actionClasses";
|
||||
import { CssSelector, InnerHtmlSelector, PageUrlSelector } from "@formbricks/ui/Actions";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@formbricks/ui/Alert";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UseFormRegister } from "react-hook-form";
|
||||
|
||||
import { AdvancedOptionToggle } from "../../AdvancedOptionToggle";
|
||||
import { Input } from "../../Input";
|
||||
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
|
||||
interface CssSelectorProps {
|
||||
isCssSelector: boolean;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UseFormRegister } from "react-hook-form";
|
||||
|
||||
import { AdvancedOptionToggle } from "../../AdvancedOptionToggle";
|
||||
import { Input } from "../../Input";
|
||||
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
|
||||
interface InnerHtmlSelectorProps {
|
||||
isInnerHtml: boolean;
|
||||
@@ -2,10 +2,10 @@ import { Label } from "@radix-ui/react-dropdown-menu";
|
||||
import clsx from "clsx";
|
||||
import { Control, Controller, UseFormRegister } from "react-hook-form";
|
||||
|
||||
import { AdvancedOptionToggle } from "../../AdvancedOptionToggle";
|
||||
import { Button } from "../../Button";
|
||||
import { Input } from "../../Input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../Select";
|
||||
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
|
||||
|
||||
interface PageUrlSelectorProps {
|
||||
isPageUrl: boolean;
|
||||
@@ -4,39 +4,22 @@ import { getServerSession } from "next-auth";
|
||||
|
||||
import { canUserAccessAttributeClass } from "@formbricks/lib/attributeClass/auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getSegmentsByAttributeClassName } from "@formbricks/lib/segment/service";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { getSurveysByAttributeClassId } from "@formbricks/lib/survey/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
|
||||
export const getSegmentsByAttributeClassAction = async (
|
||||
environmentId: string,
|
||||
attributeClass: TAttributeClass
|
||||
export const GetActiveInactiveSurveysAction = async (
|
||||
attributeClassId: string
|
||||
): Promise<{ activeSurveys: string[]; inactiveSurveys: string[] }> => {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessAttributeClass(session.user.id, attributeClass.id);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
const segments = await getSegmentsByAttributeClassName(environmentId, attributeClass.name);
|
||||
const isAuthorized = await canUserAccessAttributeClass(session.user.id, attributeClassId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
// segments is an array of segments, each segment has a survey array with objects with properties: id, name and status.
|
||||
// We need the name of the surveys only and we need to filter out the surveys that are both in progress and not in progress.
|
||||
|
||||
const activeSurveys = segments
|
||||
.map((segment) =>
|
||||
segment.surveys.filter((survey) => survey.status === "inProgress").map((survey) => survey.name)
|
||||
)
|
||||
.flat();
|
||||
const inactiveSurveys = segments
|
||||
.map((segment) =>
|
||||
segment.surveys.filter((survey) => survey.status !== "inProgress").map((survey) => survey.name)
|
||||
)
|
||||
.flat();
|
||||
|
||||
return { activeSurveys, inactiveSurveys };
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { getSegmentsByAttributeClassAction } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions";
|
||||
import { GetActiveInactiveSurveysAction } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions";
|
||||
import { TagIcon } from "@heroicons/react/24/solid";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
@@ -29,20 +29,16 @@ export default function AttributeActivityTab({ attributeClass }: EventActivityTa
|
||||
async function getSurveys() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const segmentsWithAttributeClassName = await getSegmentsByAttributeClassAction(
|
||||
attributeClass.environmentId,
|
||||
attributeClass
|
||||
);
|
||||
|
||||
setActiveSurveys(segmentsWithAttributeClassName.activeSurveys);
|
||||
setInactiveSurveys(segmentsWithAttributeClassName.inactiveSurveys);
|
||||
const activeInactive = await GetActiveInactiveSurveysAction(attributeClass.id);
|
||||
setActiveSurveys(activeInactive.activeSurveys);
|
||||
setInactiveSurveys(activeInactive.inactiveSurveys);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [attributeClass, attributeClass.environmentId, attributeClass.id, attributeClass.name]);
|
||||
}, [attributeClass.id]);
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <ErrorComponent />;
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function AttributeClassesTable({
|
||||
}) {
|
||||
const [isAttributeDetailModalOpen, setAttributeDetailModalOpen] = useState(false);
|
||||
const [isUploadCSVModalOpen, setUploadCSVModalOpen] = useState(false);
|
||||
const [activeAttributeClass, setActiveAttributeClass] = useState<TAttributeClass | null>(null);
|
||||
const [activeAttributeClass, setActiveAttributeClass] = useState("" as any);
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
|
||||
const displayedAttributeClasses = useMemo(() => {
|
||||
@@ -33,7 +33,8 @@ export default function AttributeClassesTable({
|
||||
return attributeClasses ? attributeClasses.some((ac) => ac.archived) : false;
|
||||
}, [attributeClasses]);
|
||||
|
||||
const handleOpenAttributeDetailModalClick = (attributeClass: TAttributeClass) => {
|
||||
const handleOpenAttributeDetailModalClick = (e, attributeClass) => {
|
||||
e.preventDefault();
|
||||
setActiveAttributeClass(attributeClass);
|
||||
setAttributeDetailModalOpen(true);
|
||||
};
|
||||
@@ -58,21 +59,20 @@ export default function AttributeClassesTable({
|
||||
<div className="grid-cols-7">
|
||||
{displayedAttributeClasses.map((attributeClass, index) => (
|
||||
<button
|
||||
onClick={() => handleOpenAttributeDetailModalClick(attributeClass)}
|
||||
onClick={(e) => {
|
||||
handleOpenAttributeDetailModalClick(e, attributeClass);
|
||||
}}
|
||||
className="w-full"
|
||||
key={attributeClass.id}>
|
||||
{attributeRows[index]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{activeAttributeClass && (
|
||||
<AttributeDetailModal
|
||||
open={isAttributeDetailModalOpen}
|
||||
setOpen={setAttributeDetailModalOpen}
|
||||
attributeClass={activeAttributeClass}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AttributeDetailModal
|
||||
open={isAttributeDetailModalOpen}
|
||||
setOpen={setAttributeDetailModalOpen}
|
||||
attributeClass={activeAttributeClass}
|
||||
/>
|
||||
<UploadAttributesModal open={isUploadCSVModalOpen} setOpen={setUploadCSVModalOpen} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import SecondNavbar from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/components/SecondNavbar";
|
||||
import { UserGroupIcon, UserIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
interface PeopleSegmentsTabsProps {
|
||||
activeId: string;
|
||||
environmentId: string;
|
||||
isUserTargetingAllowed?: boolean;
|
||||
}
|
||||
|
||||
export default function PeopleSegmentsTabs({ activeId, environmentId }: PeopleSegmentsTabsProps) {
|
||||
let tabs = [
|
||||
{
|
||||
id: "people",
|
||||
label: "People",
|
||||
icon: <UserIcon />,
|
||||
href: `/environments/${environmentId}/people`,
|
||||
},
|
||||
{
|
||||
id: "segments",
|
||||
label: "Segments",
|
||||
icon: <UserGroupIcon />,
|
||||
href: `/environments/${environmentId}/segments`,
|
||||
},
|
||||
];
|
||||
|
||||
return <SecondNavbar tabs={tabs} activeId={activeId} environmentId={environmentId} />;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import PeopleSegmentsTabs from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/PeopleSegmentsTabs";
|
||||
import { Metadata } from "next";
|
||||
|
||||
import ContentWrapper from "@formbricks/ui/ContentWrapper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "People",
|
||||
};
|
||||
|
||||
export default async function PeopleLayout({ params, children }) {
|
||||
return (
|
||||
<>
|
||||
<PeopleSegmentsTabs activeId="people" environmentId={params.environmentId} />
|
||||
<ContentWrapper>{children}</ContentWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { UserGroupIcon } from "@heroicons/react/20/solid";
|
||||
import { FilterIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { createSegmentAction } from "@formbricks/ee/advancedTargeting/lib/actions";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TBaseFilter, TSegment, ZSegmentFilters } from "@formbricks/types/segment";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Modal } from "@formbricks/ui/Modal";
|
||||
import BasicAddFilterModal from "@formbricks/ui/Targeting/BasicAddFilterModal";
|
||||
import BasicSegmentEditor from "@formbricks/ui/Targeting/BasicSegmentEditor";
|
||||
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
|
||||
|
||||
type TCreateSegmentModalProps = {
|
||||
environmentId: string;
|
||||
attributeClasses: TAttributeClass[];
|
||||
isFormbricksCloud: boolean;
|
||||
};
|
||||
const BasicCreateSegmentModal = ({
|
||||
environmentId,
|
||||
attributeClasses,
|
||||
isFormbricksCloud,
|
||||
}: TCreateSegmentModalProps) => {
|
||||
const router = useRouter();
|
||||
const initialSegmentState = {
|
||||
title: "",
|
||||
description: "",
|
||||
isPrivate: false,
|
||||
filters: [],
|
||||
environmentId,
|
||||
id: "",
|
||||
surveys: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
|
||||
const [segment, setSegment] = useState<TSegment>(initialSegmentState);
|
||||
const [isCreatingSegment, setIsCreatingSegment] = useState(false);
|
||||
|
||||
const handleResetState = () => {
|
||||
setSegment(initialSegmentState);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleAddFilterInGroup = (filter: TBaseFilter) => {
|
||||
const updatedSegment = structuredClone(segment);
|
||||
if (updatedSegment?.filters?.length === 0) {
|
||||
updatedSegment.filters.push({
|
||||
...filter,
|
||||
connector: null,
|
||||
});
|
||||
} else {
|
||||
updatedSegment?.filters.push(filter);
|
||||
}
|
||||
|
||||
setSegment(updatedSegment);
|
||||
};
|
||||
|
||||
const handleCreateSegment = async () => {
|
||||
if (!segment.title) {
|
||||
toast.error("Title is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsCreatingSegment(true);
|
||||
await createSegmentAction({
|
||||
title: segment.title,
|
||||
description: segment.description ?? "",
|
||||
isPrivate: segment.isPrivate,
|
||||
filters: segment.filters,
|
||||
environmentId,
|
||||
surveyId: "",
|
||||
});
|
||||
|
||||
setIsCreatingSegment(false);
|
||||
toast.success("Segment created successfully!");
|
||||
} catch (err: any) {
|
||||
// parse the segment filters to check if they are valid
|
||||
const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
|
||||
if (!parsedFilters.success) {
|
||||
toast.error("Invalid filters. Please check the filters and try again.");
|
||||
} else {
|
||||
toast.error("Something went wrong. Please try again.");
|
||||
}
|
||||
setIsCreatingSegment(false);
|
||||
return;
|
||||
}
|
||||
|
||||
handleResetState();
|
||||
setIsCreatingSegment(false);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
const isSaveDisabled = useMemo(() => {
|
||||
// check if title is empty
|
||||
|
||||
if (!segment.title) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// parse the filters to check if they are valid
|
||||
const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
|
||||
if (!parsedFilters.success) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [segment]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Button variant="darkCTA" onClick={() => setOpen(true)}>
|
||||
Create Segment
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={open}
|
||||
setOpen={() => {
|
||||
handleResetState();
|
||||
}}
|
||||
noPadding
|
||||
closeOnOutsideClick={false}
|
||||
size="lg">
|
||||
<div className="rounded-lg bg-slate-50">
|
||||
<div className="rounded-t-lg bg-slate-100">
|
||||
<div className="flex w-full items-center gap-4 p-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="mr-1.5 h-6 w-6 text-slate-500">
|
||||
<UserGroupIcon />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-medium">Create Segment</h3>
|
||||
<p className="text-sm text-slate-600">
|
||||
Segments help you target the users with the same characteristics easily.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col overflow-auto rounded-lg bg-white p-6">
|
||||
<div className="flex w-full items-center gap-4">
|
||||
<div className="flex w-1/2 flex-col gap-2">
|
||||
<label className="text-sm font-medium text-slate-900">Title</label>
|
||||
<div className="relative flex flex-col gap-1">
|
||||
<Input
|
||||
placeholder="Ex. Power Users"
|
||||
onChange={(e) => {
|
||||
setSegment((prev) => ({
|
||||
...prev,
|
||||
title: e.target.value,
|
||||
}));
|
||||
}}
|
||||
className="w-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-1/2 flex-col gap-2">
|
||||
<label className="text-sm font-medium text-slate-900">Description</label>
|
||||
<Input
|
||||
placeholder="Ex. Fully activated recurring users"
|
||||
onChange={(e) => {
|
||||
setSegment((prev) => ({
|
||||
...prev,
|
||||
description: e.target.value,
|
||||
}));
|
||||
}}
|
||||
className="w-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="my-4 text-sm font-medium text-slate-900">Targeting</label>
|
||||
<div className="filter-scrollbar flex w-full flex-col gap-4 overflow-auto rounded-lg border border-slate-200 bg-slate-50 p-4">
|
||||
{segment?.filters?.length === 0 && (
|
||||
<div className="-mb-2 flex items-center gap-1">
|
||||
<FilterIcon className="h-5 w-5 text-slate-700" />
|
||||
<h3 className="text-sm font-medium text-slate-700">Add your first filter to get started</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<BasicSegmentEditor
|
||||
environmentId={environmentId}
|
||||
segment={segment}
|
||||
setSegment={setSegment}
|
||||
group={segment.filters}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="w-fit"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setAddFilterModalOpen(true)}>
|
||||
Add Filter
|
||||
</Button>
|
||||
|
||||
<BasicAddFilterModal
|
||||
onAddFilter={(filter) => {
|
||||
handleAddFilterInGroup(filter);
|
||||
}}
|
||||
open={addFilterModalOpen}
|
||||
setOpen={setAddFilterModalOpen}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isFormbricksCloud ? (
|
||||
<UpgradePlanNotice
|
||||
message="For advanced targeting, please"
|
||||
textForUrl="upgrade to the User Identification plan."
|
||||
url={`/environments/${environmentId}/settings/billing`}
|
||||
/>
|
||||
) : (
|
||||
<UpgradePlanNotice
|
||||
message="For advanced targeting, please"
|
||||
textForUrl="request an Enterprise license."
|
||||
url="https://formbricks.com/docs/self-hosting/enterprise"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="minimal"
|
||||
onClick={() => {
|
||||
handleResetState();
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
type="submit"
|
||||
loading={isCreatingSegment}
|
||||
disabled={isSaveDisabled}
|
||||
onClick={() => {
|
||||
handleCreateSegment();
|
||||
}}>
|
||||
Create Segment
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicCreateSegmentModal;
|
||||
@@ -1,268 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
deleteBasicSegmentAction,
|
||||
updateBasicSegmentAction,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||
import { FilterIcon, Trash2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { isAdvancedSegment } from "@formbricks/lib/segment/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TBaseFilter, TSegment, TSegmentWithSurveyNames, ZSegmentFilters } from "@formbricks/types/segment";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import BasicAddFilterModal from "@formbricks/ui/Targeting/BasicAddFilterModal";
|
||||
import BasicSegmentEditor from "@formbricks/ui/Targeting/BasicSegmentEditor";
|
||||
import ConfirmDeleteSegmentModal from "@formbricks/ui/Targeting/ConfirmDeleteSegmentModal";
|
||||
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
|
||||
|
||||
type TBasicSegmentSettingsTabProps = {
|
||||
environmentId: string;
|
||||
setOpen: (open: boolean) => void;
|
||||
initialSegment: TSegmentWithSurveyNames;
|
||||
attributeClasses: TAttributeClass[];
|
||||
isFormbricksCloud: boolean;
|
||||
};
|
||||
|
||||
const BasicSegmentSettings = ({
|
||||
environmentId,
|
||||
initialSegment,
|
||||
setOpen,
|
||||
attributeClasses,
|
||||
isFormbricksCloud,
|
||||
}: TBasicSegmentSettingsTabProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
|
||||
const [segment, setSegment] = useState<TSegment>(initialSegment);
|
||||
|
||||
const [isUpdatingSegment, setIsUpdatingSegment] = useState(false);
|
||||
const [isDeletingSegment, setIsDeletingSegment] = useState(false);
|
||||
|
||||
const [isDeleteSegmentModalOpen, setIsDeleteSegmentModalOpen] = useState(false);
|
||||
|
||||
const handleResetState = () => {
|
||||
setSegment(initialSegment);
|
||||
setOpen(false);
|
||||
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
const handleAddFilterInGroup = (filter: TBaseFilter) => {
|
||||
const updatedSegment = structuredClone(segment);
|
||||
if (updatedSegment?.filters?.length === 0) {
|
||||
updatedSegment.filters.push({
|
||||
...filter,
|
||||
connector: null,
|
||||
});
|
||||
} else {
|
||||
updatedSegment?.filters.push(filter);
|
||||
}
|
||||
|
||||
setSegment(updatedSegment);
|
||||
};
|
||||
|
||||
const handleUpdateSegment = async () => {
|
||||
if (!segment.title) {
|
||||
toast.error("Title is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsUpdatingSegment(true);
|
||||
await updateBasicSegmentAction(segment.environmentId, segment.id, {
|
||||
title: segment.title,
|
||||
description: segment.description ?? "",
|
||||
isPrivate: segment.isPrivate,
|
||||
filters: segment.filters,
|
||||
});
|
||||
|
||||
setIsUpdatingSegment(false);
|
||||
toast.success("Segment updated successfully!");
|
||||
} catch (err: any) {
|
||||
// parse the segment filters to check if they are valid
|
||||
const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
|
||||
if (!parsedFilters.success) {
|
||||
toast.error("Invalid filters. Please check the filters and try again.");
|
||||
} else {
|
||||
toast.error("Something went wrong. Please try again.");
|
||||
}
|
||||
setIsUpdatingSegment(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpdatingSegment(false);
|
||||
handleResetState();
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
const handleDeleteSegment = async () => {
|
||||
try {
|
||||
setIsDeletingSegment(true);
|
||||
await deleteBasicSegmentAction(segment.environmentId, segment.id);
|
||||
|
||||
setIsDeletingSegment(false);
|
||||
toast.success("Segment deleted successfully!");
|
||||
handleResetState();
|
||||
} catch (err: any) {
|
||||
toast.error("Something went wrong. Please try again.");
|
||||
}
|
||||
|
||||
setIsDeletingSegment(false);
|
||||
};
|
||||
|
||||
const isSaveDisabled = useMemo(() => {
|
||||
// check if title is empty
|
||||
|
||||
if (!segment.title) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// parse the filters to check if they are valid
|
||||
const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
|
||||
if (!parsedFilters.success) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [segment]);
|
||||
|
||||
if (isAdvancedSegment(segment.filters)) {
|
||||
return (
|
||||
<p className="italic text-slate-600">
|
||||
This is an advanced segment, you cannot edit it. Please upgrade your plan to edit this segment.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<div className="rounded-lg bg-slate-50">
|
||||
<div className="flex flex-col overflow-auto rounded-lg bg-white">
|
||||
<div className="flex w-full items-center gap-4">
|
||||
<div className="flex w-1/2 flex-col gap-2">
|
||||
<label className="text-sm font-medium text-slate-900">Title</label>
|
||||
<div className="relative flex flex-col gap-1">
|
||||
<Input
|
||||
value={segment.title}
|
||||
placeholder="Ex. Power Users"
|
||||
onChange={(e) => {
|
||||
setSegment((prev) => ({
|
||||
...prev,
|
||||
title: e.target.value,
|
||||
}));
|
||||
}}
|
||||
className="w-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-1/2 flex-col gap-2">
|
||||
<label className="text-sm font-medium text-slate-900">Description</label>
|
||||
<div className="relative flex flex-col gap-1">
|
||||
<Input
|
||||
value={segment.description ?? ""}
|
||||
placeholder="Ex. Power Users"
|
||||
onChange={(e) => {
|
||||
setSegment((prev) => ({
|
||||
...prev,
|
||||
description: e.target.value,
|
||||
}));
|
||||
}}
|
||||
className="w-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="my-4 text-sm font-medium text-slate-900">Targeting</label>
|
||||
<div className="filter-scrollbar flex max-h-96 w-full flex-col gap-4 overflow-auto rounded-lg border border-slate-200 bg-slate-50 p-4">
|
||||
{segment?.filters?.length === 0 && (
|
||||
<div className="-mb-2 flex items-center gap-1">
|
||||
<FilterIcon className="h-5 w-5 text-slate-700" />
|
||||
<h3 className="text-sm font-medium text-slate-700">Add your first filter to get started</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<BasicSegmentEditor
|
||||
environmentId={environmentId}
|
||||
segment={segment}
|
||||
setSegment={setSegment}
|
||||
group={segment.filters}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button variant="secondary" size="sm" onClick={() => setAddFilterModalOpen(true)}>
|
||||
Add Filter
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<BasicAddFilterModal
|
||||
onAddFilter={(filter) => {
|
||||
handleAddFilterInGroup(filter);
|
||||
}}
|
||||
open={addFilterModalOpen}
|
||||
setOpen={setAddFilterModalOpen}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isFormbricksCloud ? (
|
||||
<UpgradePlanNotice
|
||||
message="For advanced targeting, please"
|
||||
textForUrl="upgrade to the User Identification plan."
|
||||
url={`/environments/${environmentId}/settings/billing`}
|
||||
/>
|
||||
) : (
|
||||
<UpgradePlanNotice
|
||||
message="For advanced targeting, please"
|
||||
textForUrl="request an Enterprise license."
|
||||
url="https://formbricks.com/docs/self-hosting/enterprise"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex w-full items-center justify-between pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="warn"
|
||||
loading={isDeletingSegment}
|
||||
onClick={() => {
|
||||
setIsDeleteSegmentModalOpen(true);
|
||||
}}
|
||||
EndIcon={Trash2}
|
||||
endIconClassName="p-0.5">
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
type="submit"
|
||||
loading={isUpdatingSegment}
|
||||
onClick={() => {
|
||||
handleUpdateSegment();
|
||||
}}
|
||||
disabled={isSaveDisabled}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isDeleteSegmentModalOpen && (
|
||||
<ConfirmDeleteSegmentModal
|
||||
onDelete={handleDeleteSegment}
|
||||
open={isDeleteSegmentModalOpen}
|
||||
segment={initialSegment}
|
||||
setOpen={setIsDeleteSegmentModalOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicSegmentSettings;
|
||||
@@ -1,86 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { UserGroupIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
import SegmentSettings from "@formbricks/ee/advancedTargeting/components/SegmentSettings";
|
||||
import { TActionClass } from "@formbricks/types/actionClasses";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSegment, TSegmentWithSurveyNames } from "@formbricks/types/segment";
|
||||
import ModalWithTabs from "@formbricks/ui/ModalWithTabs";
|
||||
|
||||
import BasicSegmentSettings from "./BasicSegmentSettings";
|
||||
import SegmentActivityTab from "./SegmentActivityTab";
|
||||
|
||||
interface EditSegmentModalProps {
|
||||
environmentId: string;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
currentSegment: TSegmentWithSurveyNames;
|
||||
segments: TSegment[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
actionClasses: TActionClass[];
|
||||
isAdvancedTargetingAllowed: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
}
|
||||
|
||||
export default function EditSegmentModal({
|
||||
environmentId,
|
||||
open,
|
||||
setOpen,
|
||||
currentSegment,
|
||||
actionClasses,
|
||||
attributeClasses,
|
||||
segments,
|
||||
isAdvancedTargetingAllowed,
|
||||
isFormbricksCloud,
|
||||
}: EditSegmentModalProps) {
|
||||
const SettingsTab = () => {
|
||||
if (isAdvancedTargetingAllowed) {
|
||||
return (
|
||||
<SegmentSettings
|
||||
actionClasses={actionClasses}
|
||||
attributeClasses={attributeClasses}
|
||||
environmentId={environmentId}
|
||||
initialSegment={currentSegment}
|
||||
segments={segments}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BasicSegmentSettings
|
||||
attributeClasses={attributeClasses}
|
||||
environmentId={environmentId}
|
||||
initialSegment={currentSegment}
|
||||
setOpen={setOpen}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
title: "Activity",
|
||||
children: <SegmentActivityTab environmentId={environmentId} currentSegment={currentSegment} />,
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
children: <SettingsTab />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalWithTabs
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
tabs={tabs}
|
||||
icon={<UserGroupIcon />}
|
||||
label={currentSegment.title}
|
||||
description={currentSegment.description || ""}
|
||||
closeOnOutsideClick={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
|
||||
interface SegmentActivityTabProps {
|
||||
environmentId: string;
|
||||
currentSegment: TSegment & {
|
||||
activeSurveys: string[];
|
||||
inactiveSurveys: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export default function SegmentActivityTab({ currentSegment }: SegmentActivityTabProps) {
|
||||
const activeSurveys = currentSegment?.activeSurveys;
|
||||
const inactiveSurveys = currentSegment?.inactiveSurveys;
|
||||
|
||||
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>
|
||||
{!activeSurveys?.length && <p className="text-sm text-slate-900">-</p>}
|
||||
|
||||
{activeSurveys?.map((survey) => <p className="text-sm text-slate-900">{survey}</p>)}
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500">Inactive surveys</Label>
|
||||
{!inactiveSurveys?.length && <p className="text-sm text-slate-900">-</p>}
|
||||
|
||||
{inactiveSurveys?.map((survey) => <p className="text-sm text-slate-900">{survey}</p>)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 space-y-3 rounded-lg border border-slate-100 bg-slate-50 p-2">
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">Created on</Label>
|
||||
<p className=" text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(currentSegment.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(currentSegment.updatedAt?.toString())}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { TActionClass } from "@formbricks/types/actionClasses";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
|
||||
import SegmentTableDataRowContainer from "./SegmentTableDataRowContainer";
|
||||
|
||||
type TSegmentTableProps = {
|
||||
segments: TSegment[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
actionClasses: TActionClass[];
|
||||
isAdvancedTargetingAllowed: boolean;
|
||||
};
|
||||
const SegmentTable = ({
|
||||
segments,
|
||||
actionClasses,
|
||||
attributeClasses,
|
||||
isAdvancedTargetingAllowed,
|
||||
}: TSegmentTableProps) => {
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-7 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-4 pl-6">Title</div>
|
||||
<div className="col-span-1 hidden text-center sm:block">Surveys</div>
|
||||
<div className="col-span-1 hidden text-center sm:block">Updated</div>
|
||||
<div className="col-span-1 hidden text-center sm:block">Created</div>
|
||||
</div>
|
||||
{segments.map((segment) => (
|
||||
<SegmentTableDataRowContainer
|
||||
currentSegment={segment}
|
||||
segments={segments}
|
||||
actionClasses={actionClasses}
|
||||
attributeClasses={attributeClasses}
|
||||
isAdvancedTargetingAllowed={isAdvancedTargetingAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SegmentTable;
|
||||
@@ -1,80 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { UserGroupIcon } from "@heroicons/react/24/solid";
|
||||
import { format, formatDistanceToNow } from "date-fns";
|
||||
import { useState } from "react";
|
||||
|
||||
import { TActionClass } from "@formbricks/types/actionClasses";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSegment, TSegmentWithSurveyNames } from "@formbricks/types/segment";
|
||||
|
||||
import EditSegmentModal from "./EditSegmentModal";
|
||||
|
||||
type TSegmentTableDataRowProps = {
|
||||
currentSegment: TSegmentWithSurveyNames;
|
||||
segments: TSegment[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
actionClasses: TActionClass[];
|
||||
isAdvancedTargetingAllowed: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
};
|
||||
|
||||
const SegmentTableDataRow = ({
|
||||
currentSegment,
|
||||
actionClasses,
|
||||
attributeClasses,
|
||||
segments,
|
||||
isAdvancedTargetingAllowed,
|
||||
isFormbricksCloud,
|
||||
}: TSegmentTableDataRowProps) => {
|
||||
const { createdAt, environmentId, id, surveys, title, updatedAt, description } = currentSegment;
|
||||
const [isEditSegmentModalOpen, setIsEditSegmentModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
key={id}
|
||||
className="m-2 grid h-16 cursor-pointer grid-cols-7 content-center rounded-lg hover:bg-slate-100"
|
||||
onClick={() => setIsEditSegmentModalOpen(true)}>
|
||||
<div className="col-span-4 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="ph-no-capture h-8 w-8 flex-shrink-0 text-slate-700">
|
||||
<UserGroupIcon />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="ph-no-capture font-medium text-slate-900">{title}</div>
|
||||
<div className="ph-no-capture text-xs font-medium text-slate-500">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
|
||||
<div className="ph-no-capture text-slate-900">{surveys?.length}</div>
|
||||
</div>
|
||||
<div className="whitespace-wrap col-span-1 my-auto hidden text-center text-sm text-slate-500 sm:block">
|
||||
<div className="ph-no-capture text-slate-900">
|
||||
{formatDistanceToNow(updatedAt, {
|
||||
addSuffix: true,
|
||||
}).replace("about", "")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 my-auto hidden whitespace-normal text-center text-sm text-slate-500 sm:block">
|
||||
<div className="ph-no-capture text-slate-900">{format(createdAt, "do 'of' MMMM, yyyy")}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditSegmentModal
|
||||
environmentId={environmentId}
|
||||
open={isEditSegmentModalOpen}
|
||||
setOpen={setIsEditSegmentModalOpen}
|
||||
currentSegment={currentSegment}
|
||||
actionClasses={actionClasses}
|
||||
attributeClasses={attributeClasses}
|
||||
segments={segments}
|
||||
isAdvancedTargetingAllowed={isAdvancedTargetingAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SegmentTableDataRow;
|
||||
@@ -1,50 +0,0 @@
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getSurveysBySegmentId } from "@formbricks/lib/survey/service";
|
||||
import { TActionClass } from "@formbricks/types/actionClasses";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
|
||||
import SegmentTableDataRow from "./SegmentTableDataRow";
|
||||
|
||||
type TSegmentTableDataRowProps = {
|
||||
currentSegment: TSegment;
|
||||
segments: TSegment[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
actionClasses: TActionClass[];
|
||||
isAdvancedTargetingAllowed: boolean;
|
||||
};
|
||||
|
||||
const SegmentTableDataRowContainer = async ({
|
||||
currentSegment,
|
||||
segments,
|
||||
actionClasses,
|
||||
attributeClasses,
|
||||
isAdvancedTargetingAllowed,
|
||||
}: TSegmentTableDataRowProps) => {
|
||||
const surveys = await getSurveysBySegmentId(currentSegment.id);
|
||||
|
||||
const activeSurveys = surveys?.length
|
||||
? surveys.filter((survey) => survey.status === "inProgress").map((survey) => survey.name)
|
||||
: [];
|
||||
|
||||
const inactiveSurveys = surveys?.length
|
||||
? surveys.filter((survey) => ["draft", "paused"].includes(survey.status)).map((survey) => survey.name)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<SegmentTableDataRow
|
||||
currentSegment={{
|
||||
...currentSegment,
|
||||
activeSurveys,
|
||||
inactiveSurveys,
|
||||
}}
|
||||
segments={segments}
|
||||
actionClasses={actionClasses}
|
||||
attributeClasses={attributeClasses}
|
||||
isAdvancedTargetingAllowed={isAdvancedTargetingAllowed}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SegmentTableDataRowContainer;
|
||||
@@ -1,31 +0,0 @@
|
||||
import PeopleSegmentsTabs from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/PeopleSegmentsTabs";
|
||||
import { Metadata } from "next";
|
||||
|
||||
import { getAdvancedTargetingPermission } from "@formbricks/ee/lib/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import ContentWrapper from "@formbricks/ui/ContentWrapper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Segments",
|
||||
};
|
||||
|
||||
export default async function PeopleLayout({ params, children }) {
|
||||
const team = await getTeamByEnvironmentId(params.environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
|
||||
const isUserTargetingAllowed = getAdvancedTargetingPermission(team);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PeopleSegmentsTabs
|
||||
activeId="segments"
|
||||
environmentId={params.environmentId}
|
||||
isUserTargetingAllowed={isUserTargetingAllowed}
|
||||
/>
|
||||
<ContentWrapper>{children}</ContentWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import BasicCreateSegmentModal from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/segments/components/BasicCreateSegmentModal";
|
||||
import SegmentTable from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/segments/components/SegmentTable";
|
||||
|
||||
import CreateSegmentModal from "@formbricks/ee/advancedTargeting/components/CreateSegmentModal";
|
||||
import { ACTIONS_TO_EXCLUDE } from "@formbricks/ee/advancedTargeting/lib/constants";
|
||||
import { getAdvancedTargetingPermission } from "@formbricks/ee/lib/service";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { IS_FORMBRICKS_CLOUD, REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getSegments } from "@formbricks/lib/segment/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
|
||||
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
export default async function SegmentsPage({ params }) {
|
||||
const [environment, segments, attributeClasses, actionClassesFromServer, team] = await Promise.all([
|
||||
getEnvironment(params.environmentId),
|
||||
getSegments(params.environmentId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
getActionClasses(params.environmentId),
|
||||
getTeamByEnvironmentId(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
|
||||
const isAdvancedTargetingAllowed = getAdvancedTargetingPermission(team);
|
||||
|
||||
if (!segments) {
|
||||
throw new Error("Failed to fetch segments");
|
||||
}
|
||||
|
||||
const filteredSegments = segments.filter((segment) => !segment.isPrivate);
|
||||
|
||||
const actionClasses = actionClassesFromServer.filter((actionClass) => {
|
||||
if (actionClass.type === "automatic") {
|
||||
if (ACTIONS_TO_EXCLUDE.includes(actionClass.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAdvancedTargetingAllowed ? (
|
||||
<CreateSegmentModal
|
||||
environmentId={params.environmentId}
|
||||
actionClasses={actionClasses}
|
||||
attributeClasses={attributeClasses}
|
||||
segments={filteredSegments}
|
||||
/>
|
||||
) : (
|
||||
<BasicCreateSegmentModal
|
||||
attributeClasses={attributeClasses}
|
||||
environmentId={params.environmentId}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
/>
|
||||
)}
|
||||
|
||||
{filteredSegments.length === 0 ? (
|
||||
<EmptySpaceFiller
|
||||
type="table"
|
||||
environment={environment}
|
||||
emptyMessage="No segments yet. Add your first one to get started."
|
||||
/>
|
||||
) : (
|
||||
<SegmentTable
|
||||
segments={filteredSegments}
|
||||
actionClasses={actionClasses}
|
||||
attributeClasses={attributeClasses}
|
||||
isAdvancedTargetingAllowed={isAdvancedTargetingAllowed}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -66,7 +66,6 @@ export default function AddProductModal({ environmentId, open, setOpen }: AddPro
|
||||
<div>
|
||||
<Label>Name</Label>
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder="e.g. My New Product"
|
||||
{...register("name", { required: true })}
|
||||
value={productName}
|
||||
|
||||
@@ -120,10 +120,11 @@ export default function Navigation({
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
name: "People & Segments",
|
||||
name: "People",
|
||||
href: `/environments/${environment.id}/people`,
|
||||
icon: CustomersIcon,
|
||||
current: pathname?.includes("/people") || pathname?.includes("/segments"),
|
||||
current: pathname?.includes("/people"),
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
name: "Actions & Attributes",
|
||||
@@ -150,7 +151,7 @@ export default function Navigation({
|
||||
[environment.id, pathname, isViewer]
|
||||
);
|
||||
|
||||
const dropdownNavigation = [
|
||||
const dropdownnavigation = [
|
||||
{
|
||||
title: "Survey",
|
||||
links: [
|
||||
@@ -458,7 +459,7 @@ export default function Navigation({
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
|
||||
{dropdownNavigation.map((item) => (
|
||||
{dropdownnavigation.map((item) => (
|
||||
<DropdownMenuGroup key={item.title}>
|
||||
<DropdownMenuSeparator />
|
||||
{item.links.map(
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { Session } from "next-auth";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { env } from "@formbricks/lib/env";
|
||||
import { TSubscriptionStatus } from "@formbricks/types/teams";
|
||||
|
||||
const posthogEnabled = env.NEXT_PUBLIC_POSTHOG_API_KEY && env.NEXT_PUBLIC_POSTHOG_API_HOST;
|
||||
|
||||
export default function PosthogIdentify({
|
||||
session,
|
||||
environmentId,
|
||||
teamId,
|
||||
teamName,
|
||||
inAppSurveyBillingStatus,
|
||||
linkSurveyBillingStatus,
|
||||
userTargetingBillingStatus,
|
||||
}: {
|
||||
session: Session;
|
||||
environmentId?: string;
|
||||
teamId?: string;
|
||||
teamName?: string;
|
||||
inAppSurveyBillingStatus?: TSubscriptionStatus;
|
||||
linkSurveyBillingStatus?: TSubscriptionStatus;
|
||||
userTargetingBillingStatus?: TSubscriptionStatus;
|
||||
}) {
|
||||
const posthog = usePostHog();
|
||||
|
||||
useEffect(() => {
|
||||
if (posthogEnabled && session.user && posthog) {
|
||||
posthog.identify(session.user.id, { name: session.user.name, email: session.user.email });
|
||||
if (environmentId) {
|
||||
posthog.group("environment", environmentId, { name: environmentId });
|
||||
}
|
||||
if (teamId) {
|
||||
posthog.group("team", teamId, {
|
||||
name: teamName,
|
||||
inAppSurveyBillingStatus,
|
||||
linkSurveyBillingStatus,
|
||||
userTargetingBillingStatus,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [
|
||||
posthog,
|
||||
session.user,
|
||||
environmentId,
|
||||
teamId,
|
||||
teamName,
|
||||
inAppSurveyBillingStatus,
|
||||
linkSurveyBillingStatus,
|
||||
userTargetingBillingStatus,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/
|
||||
import { getTodayDate } from "@/app/lib/surveys/surveys";
|
||||
import { createContext, useCallback, useContext, useState } from "react";
|
||||
|
||||
export interface FilterValue {
|
||||
interface FilterValue {
|
||||
questionType: Partial<QuestionOption>;
|
||||
filterType: {
|
||||
filterValue: string | undefined;
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useEffect, useState } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationAirtable,
|
||||
@@ -334,7 +333,7 @@ export default function AddIntegrationModal(props: AddIntegrationModalProps) {
|
||||
<Label htmlFor="Surveys">Questions</Label>
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{checkForRecallInHeadline(selectedSurvey)?.questions.map((question) => (
|
||||
{selectedSurvey?.questions.map((question) => (
|
||||
<Controller
|
||||
key={question.id}
|
||||
control={control}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
@@ -196,7 +195,7 @@ export default function AddIntegrationModal({
|
||||
{!disabled && (
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
className="z-50 max-h-[10rem] min-w-[220px] overflow-auto rounded-md bg-white text-sm text-slate-800 shadow-md"
|
||||
className="z-50 min-w-[220px] rounded-md bg-white text-sm text-slate-800 shadow-md"
|
||||
align="start">
|
||||
{items &&
|
||||
items.map((item) => (
|
||||
@@ -274,7 +273,7 @@ export default function AddIntegrationModal({
|
||||
<Label htmlFor="Surveys">Questions</Label>
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{checkForRecallInHeadline(selectedSurvey)?.questions.map((question) => (
|
||||
{selectedSurvey?.questions.map((question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
|
||||
@@ -13,7 +13,6 @@ import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TIntegrationInput } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationNotion,
|
||||
@@ -106,13 +105,12 @@ export default function AddIntegrationModal({
|
||||
}, [selectedDatabase?.id]);
|
||||
|
||||
const questionItems = useMemo(() => {
|
||||
const questions = selectedSurvey
|
||||
? checkForRecallInHeadline(selectedSurvey)?.questions.map((q) => ({
|
||||
id: q.id,
|
||||
name: q.headline,
|
||||
type: q.type,
|
||||
}))
|
||||
: [];
|
||||
const questions =
|
||||
selectedSurvey?.questions.map((q) => ({
|
||||
id: q.id,
|
||||
name: q.headline,
|
||||
type: q.type,
|
||||
})) || [];
|
||||
|
||||
const hiddenFields = selectedSurvey?.hiddenFields.enabled
|
||||
? selectedSurvey?.hiddenFields.fieldIds?.map((fId) => ({
|
||||
|
||||
@@ -53,8 +53,7 @@ export default function AddWebhookModal({ environmentId, surveys, open, setOpen
|
||||
return true;
|
||||
} catch (err) {
|
||||
setHittingEndpoint(false);
|
||||
toast.error("Unable to ping the webhook! Please check browser console for logs");
|
||||
console.error("Webhook Test Failed due to: ", err.message);
|
||||
toast.error("Oh no! We are unable to ping the webhook!");
|
||||
setEndpointAccessible(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -63,8 +63,7 @@ export default function WebhookSettingsTab({
|
||||
return true;
|
||||
} catch (err) {
|
||||
setHittingEndpoint(false);
|
||||
toast.error("Unable to ping the webhook! Please check browser console for logs");
|
||||
console.error("Webhook Test Failed due to: ", err.message);
|
||||
toast.error("Oh no! We are unable to ping the webhook!");
|
||||
setEndpointAccessible(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,10 @@ import { redirect } from "next/navigation";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import ToasterClient from "@formbricks/ui/ToasterClient";
|
||||
|
||||
import FormbricksClient from "../../components/FormbricksClient";
|
||||
import PosthogIdentify from "./components/PosthogIdentify";
|
||||
|
||||
export default async function EnvironmentLayout({ children, params }) {
|
||||
const session = await getServerSession(authOptions);
|
||||
@@ -23,23 +21,9 @@ export default async function EnvironmentLayout({ children, params }) {
|
||||
throw new AuthorizationError("Not authorized");
|
||||
}
|
||||
|
||||
const team = await getTeamByEnvironmentId(params.environmentId);
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResponseFilterProvider>
|
||||
<PosthogIdentify
|
||||
session={session}
|
||||
environmentId={params.environmentId}
|
||||
teamId={team.id}
|
||||
teamName={team.name}
|
||||
inAppSurveyBillingStatus={team.billing.features.inAppSurvey.status}
|
||||
linkSurveyBillingStatus={team.billing.features.linkSurvey.status}
|
||||
userTargetingBillingStatus={team.billing.features.userTargeting.status}
|
||||
/>
|
||||
<FormbricksClient session={session} />
|
||||
<ToasterClient />
|
||||
<EnvironmentsNavbar
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ActivityTimeline from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ActivityTimeline";
|
||||
import ActivityTimeline from "@/app/(app)/environments/[environmentId]/people/[personId]/components/ActivityTimeline";
|
||||
|
||||
import { getActionsByPersonId } from "@formbricks/lib/action/service";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { deletePersonAction } from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/actions";
|
||||
import { deletePersonAction } from "@/app/(app)/environments/[environmentId]/people/[personId]/actions";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||