Compare commits
27 Commits
experiment
...
test-websi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afaef99dfe | ||
|
|
b50bda8488 | ||
|
|
0d36e11bf4 | ||
|
|
102cdb4589 | ||
|
|
5b78487b94 | ||
|
|
f917d2171e | ||
|
|
295754480e | ||
|
|
db03ce70d2 | ||
|
|
a44198539d | ||
|
|
7dbac97883 | ||
|
|
df6cf5a1c5 | ||
|
|
ffa774db6a | ||
|
|
6d6a47a5ac | ||
|
|
50e373a98a | ||
|
|
b56b2adb54 | ||
|
|
af09e315c5 | ||
|
|
1848e062f1 | ||
|
|
c8f2f94361 | ||
|
|
be52763be4 | ||
|
|
5dd5816c34 | ||
|
|
16f5ce40d9 | ||
|
|
223937adcc | ||
|
|
4544cba858 | ||
|
|
1284adf91d | ||
|
|
4b13d19ed9 | ||
|
|
12a606a443 | ||
|
|
052f86b19f |
10
.env.example
@@ -88,7 +88,7 @@ PASSWORD_RESET_DISABLED=1
|
|||||||
# Email login. Disable the ability for users to login with email.
|
# Email login. Disable the ability for users to login with email.
|
||||||
# EMAIL_AUTH_DISABLED=1
|
# EMAIL_AUTH_DISABLED=1
|
||||||
|
|
||||||
# Team Invite. Disable the ability for invited users to create an account.
|
# Organization Invite. Disable the ability for invited users to create an account.
|
||||||
# INVITE_DISABLED=1
|
# INVITE_DISABLED=1
|
||||||
|
|
||||||
##########
|
##########
|
||||||
@@ -154,11 +154,11 @@ SLACK_CLIENT_SECRET=
|
|||||||
# Enterprise License Key
|
# Enterprise License Key
|
||||||
ENTERPRISE_LICENSE_KEY=
|
ENTERPRISE_LICENSE_KEY=
|
||||||
|
|
||||||
# Automatically assign new users to a specific team and role within that team
|
# Automatically assign new users to a specific organization and role within that organization
|
||||||
# Insert an existing team id or generate a valid CUID for a new one at https://www.getuniqueid.com/cuid (e.g. cjld2cjxh0000qzrmn831i7rn)
|
# Insert an existing organization id or generate a valid CUID for a new one at https://www.getuniqueid.com/cuid (e.g. cjld2cjxh0000qzrmn831i7rn)
|
||||||
# (Role Management is an Enterprise feature)
|
# (Role Management is an Enterprise feature)
|
||||||
# DEFAULT_TEAM_ID=
|
# DEFAULT_ORGANIZATION_ID=
|
||||||
# DEFAULT_TEAM_ROLE=admin
|
# DEFAULT_ORGANIZATION_ROLE=admin
|
||||||
|
|
||||||
# set to 1 to skip onboarding for new users
|
# set to 1 to skip onboarding for new users
|
||||||
# ONBOARDING_DISABLED=1
|
# ONBOARDING_DISABLED=1
|
||||||
|
|||||||
131
.github/workflows/kamal-deploy.yml
vendored
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
name: Kamal Deploy
|
||||||
|
concurrency:
|
||||||
|
group: deploy-to-kamal
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: production
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
IS_FORMBRICKS_CLOUD: ${{ vars.IS_FORMBRICKS_CLOUD }}
|
||||||
|
WEBAPP_URL: ${{ vars.WEBAPP_URL }}
|
||||||
|
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
|
||||||
|
NEXTAUTH_URL: ${{ vars.NEXTAUTH_URL }}
|
||||||
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||||
|
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
|
||||||
|
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
|
||||||
|
SHORT_URL_BASE: ${{ vars.SHORT_URL_BASE }}
|
||||||
|
MAIL_FROM: ${{ secrets.MAIL_FROM }}
|
||||||
|
SMTP_HOST: ${{ secrets.SMTP_HOST }}
|
||||||
|
SMTP_PORT: ${{ secrets.SMTP_PORT }}
|
||||||
|
SMTP_USER: ${{ secrets.SMTP_USER }}
|
||||||
|
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
|
||||||
|
PRIVACY_URL: ${{ vars.PRIVACY_URL }}
|
||||||
|
TERMS_URL: ${{ vars.TERMS_URL }}
|
||||||
|
IMPRINT_URL: ${{ vars.IMPRINT_URL }}
|
||||||
|
GITHUB_ID: ${{ secrets.FB_GITHUB_ID }}
|
||||||
|
GITHUB_SECRET: ${{ secrets.FB_GITHUB_SECRET }}
|
||||||
|
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||||
|
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||||
|
AZUREAD_CLIENT_ID: ${{ secrets.AZUREAD_CLIENT_ID }}
|
||||||
|
AZUREAD_CLIENT_SECRET: ${{ secrets.AZUREAD_CLIENT_SECRET }}
|
||||||
|
AZUREAD_TENANT_ID: ${{ secrets.AZUREAD_TENANT_ID }}
|
||||||
|
OIDC_CLIENT_ID: ${{ secrets.OIDC_CLIENT_ID }}
|
||||||
|
OIDC_CLIENT_SECRET: ${{ secrets.OIDC_CLIENT_SECRET }}
|
||||||
|
OIDC_ISSUER: ${{ secrets.OIDC_ISSUER }}
|
||||||
|
OIDC_DISPLAY_NAME: ${{ secrets.OIDC_DISPLAY_NAME }}
|
||||||
|
OIDC_SIGNING_ALGORITHM: ${{ secrets.OIDC_SIGNING_ALGORITHM }}
|
||||||
|
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||||
|
ASSET_PREFIX_URL: ${{ vars.ASSET_PREFIX_URL }}
|
||||||
|
NOTION_OAUTH_CLIENT_ID: ${{ secrets.NOTION_OAUTH_CLIENT_ID }}
|
||||||
|
NOTION_OAUTH_CLIENT_SECRET: ${{ secrets.NOTION_OAUTH_CLIENT_SECRET }}
|
||||||
|
SLACK_CLIENT_ID: ${{ secrets.SLACK_CLIENT_ID }}
|
||||||
|
SLACK_CLIENT_SECRET: ${{ secrets.SLACK_CLIENT_SECRET }}
|
||||||
|
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
||||||
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
||||||
|
GOOGLE_SHEETS_CLIENT_ID: ${{ secrets.GOOGLE_SHEETS_CLIENT_ID }}
|
||||||
|
GOOGLE_SHEETS_CLIENT_SECRET: ${{ secrets.GOOGLE_SHEETS_CLIENT_SECRET }}
|
||||||
|
GOOGLE_SHEETS_REDIRECT_URL: ${{ secrets.GOOGLE_SHEETS_REDIRECT_URL }}
|
||||||
|
AIRTABLE_CLIENT_ID: ${{ secrets.AIRTABLE_CLIENT_ID }}
|
||||||
|
ENTERPRISE_LICENSE_KEY: ${{ secrets.ENTERPRISE_LICENSE_KEY }}
|
||||||
|
DEFAULT_ORGANIZATION_ID: ${{ vars.DEFAULT_ORGANIZATION_ID }}
|
||||||
|
ONBOARDING_DISABLED: ${{ vars.ONBOARDING_DISABLED }}
|
||||||
|
CUSTOMER_IO_API_KEY: ${{ secrets.CUSTOMER_IO_API_KEY }}
|
||||||
|
CUSTOMER_IO_SITE_ID: ${{ secrets.CUSTOMER_IO_SITE_ID }}
|
||||||
|
NEXT_PUBLIC_POSTHOG_API_KEY: ${{ vars.NEXT_PUBLIC_POSTHOG_API_KEY }}
|
||||||
|
NEXT_PUBLIC_POSTHOG_API_HOST: ${{ vars.NEXT_PUBLIC_POSTHOG_API_HOST }}
|
||||||
|
NEXT_PUBLIC_FORMBRICKS_API_HOST: ${{ vars.NEXT_PUBLIC_FORMBRICKS_API_HOST }}
|
||||||
|
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID }}
|
||||||
|
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID }}
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
NODE_ENV: production
|
||||||
|
CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}
|
||||||
|
CLOUDFLARE_DNS_API_TOKEN: ${{ secrets.CLOUDFLARE_DNS_API_TOKEN }}
|
||||||
|
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
|
||||||
|
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
|
||||||
|
S3_REGION: ${{ vars.S3_REGION }}
|
||||||
|
S3_BUCKET_NAME: ${{ vars.S3_BUCKET_NAME }}
|
||||||
|
OPENTELEMETRY_LISTENER_URL: ${{ vars.OPENTELEMETRY_LISTENER_URL }}
|
||||||
|
RATE_LIMITING_DISABLED: ${{ vars.RATE_LIMITING_DISABLED }}
|
||||||
|
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
||||||
|
DB_HOST: ${{ secrets.DB_HOST }}
|
||||||
|
DB_USER: ${{ secrets.DB_USER }}
|
||||||
|
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
|
||||||
|
DB_NAME: ${{ secrets.DB_NAME }}
|
||||||
|
REDIS_URL: ${{ secrets.REDIS_URL }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Ruby
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: 3.3.0
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
gem install kamal
|
||||||
|
|
||||||
|
- uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
with:
|
||||||
|
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Create builder
|
||||||
|
run: docker buildx create --use --name formbricks-gh-actions-builder
|
||||||
|
if: steps.buildx.outputs.should_create_builder == 'true'
|
||||||
|
|
||||||
|
- name: Push env variables to Kamal
|
||||||
|
run: |
|
||||||
|
kamal() { command kamal "$@" -c kamal/deploy.yml; }
|
||||||
|
kamal env push
|
||||||
|
|
||||||
|
- name: Run deploy command
|
||||||
|
run: |
|
||||||
|
kamal() { command kamal "$@" -c kamal/deploy.yml; }
|
||||||
|
set +e
|
||||||
|
DEPLOY_OUTPUT=$(kamal deploy 2>&1)
|
||||||
|
DEPLOY_EXIT_CODE=$?
|
||||||
|
echo "$DEPLOY_OUTPUT"
|
||||||
|
if [[ "$DEPLOY_OUTPUT" == *"container not unhealthy (healthy)"* ]]; then
|
||||||
|
echo "Deployment reported healthy container. Considering as success."
|
||||||
|
kamal lock release
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
exit $DEPLOY_EXIT_CODE
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
128
.github/workflows/kamal-setup.yml
vendored
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
name: Kamal Setup
|
||||||
|
concurrency:
|
||||||
|
group: setup-kamal
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch: # Only to be triggered when accessories are updated
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Setup:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: production
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
IS_FORMBRICKS_CLOUD: ${{ vars.IS_FORMBRICKS_CLOUD }}
|
||||||
|
WEBAPP_URL: ${{ vars.WEBAPP_URL }}
|
||||||
|
NEXTAUTH_URL: ${{ vars.NEXTAUTH_URL }}
|
||||||
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||||
|
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
|
||||||
|
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
|
||||||
|
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
|
||||||
|
SHORT_URL_BASE: ${{ vars.SHORT_URL_BASE }}
|
||||||
|
MAIL_FROM: ${{ secrets.MAIL_FROM }}
|
||||||
|
SMTP_HOST: ${{ secrets.SMTP_HOST }}
|
||||||
|
SMTP_PORT: ${{ secrets.SMTP_PORT }}
|
||||||
|
SMTP_USER: ${{ secrets.SMTP_USER }}
|
||||||
|
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
|
||||||
|
PRIVACY_URL: ${{ vars.PRIVACY_URL }}
|
||||||
|
TERMS_URL: ${{ vars.TERMS_URL }}
|
||||||
|
IMPRINT_URL: ${{ vars.IMPRINT_URL }}
|
||||||
|
GITHUB_ID: ${{ secrets.FB_GITHUB_ID }}
|
||||||
|
GITHUB_SECRET: ${{ secrets.FB_GITHUB_SECRET }}
|
||||||
|
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||||
|
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||||
|
AZUREAD_CLIENT_ID: ${{ secrets.AZUREAD_CLIENT_ID }}
|
||||||
|
AZUREAD_CLIENT_SECRET: ${{ secrets.AZUREAD_CLIENT_SECRET }}
|
||||||
|
AZUREAD_TENANT_ID: ${{ secrets.AZUREAD_TENANT_ID }}
|
||||||
|
OIDC_CLIENT_ID: ${{ secrets.OIDC_CLIENT_ID }}
|
||||||
|
OIDC_CLIENT_SECRET: ${{ secrets.OIDC_CLIENT_SECRET }}
|
||||||
|
OIDC_ISSUER: ${{ secrets.OIDC_ISSUER }}
|
||||||
|
OIDC_DISPLAY_NAME: ${{ secrets.OIDC_DISPLAY_NAME }}
|
||||||
|
OIDC_SIGNING_ALGORITHM: ${{ secrets.OIDC_SIGNING_ALGORITHM }}
|
||||||
|
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||||
|
ASSET_PREFIX_URL: ${{ vars.ASSET_PREFIX_URL }}
|
||||||
|
NOTION_OAUTH_CLIENT_ID: ${{ secrets.NOTION_OAUTH_CLIENT_ID }}
|
||||||
|
NOTION_OAUTH_CLIENT_SECRET: ${{ secrets.NOTION_OAUTH_CLIENT_SECRET }}
|
||||||
|
SLACK_CLIENT_ID: ${{ secrets.SLACK_CLIENT_ID }}
|
||||||
|
SLACK_CLIENT_SECRET: ${{ secrets.SLACK_CLIENT_SECRET }}
|
||||||
|
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
||||||
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
||||||
|
GOOGLE_SHEETS_CLIENT_ID: ${{ secrets.GOOGLE_SHEETS_CLIENT_ID }}
|
||||||
|
GOOGLE_SHEETS_CLIENT_SECRET: ${{ secrets.GOOGLE_SHEETS_CLIENT_SECRET }}
|
||||||
|
GOOGLE_SHEETS_REDIRECT_URL: ${{ secrets.GOOGLE_SHEETS_REDIRECT_URL }}
|
||||||
|
AIRTABLE_CLIENT_ID: ${{ secrets.AIRTABLE_CLIENT_ID }}
|
||||||
|
ENTERPRISE_LICENSE_KEY: ${{ secrets.ENTERPRISE_LICENSE_KEY }}
|
||||||
|
DEFAULT_ORGANIZATION_ID: ${{ vars.DEFAULT_ORGANIZATION_ID }}
|
||||||
|
ONBOARDING_DISABLED: ${{ vars.ONBOARDING_DISABLED }}
|
||||||
|
CUSTOMER_IO_API_KEY: ${{ secrets.CUSTOMER_IO_API_KEY }}
|
||||||
|
CUSTOMER_IO_SITE_ID: ${{ secrets.CUSTOMER_IO_SITE_ID }}
|
||||||
|
NEXT_PUBLIC_POSTHOG_API_KEY: ${{ vars.NEXT_PUBLIC_POSTHOG_API_KEY }}
|
||||||
|
NEXT_PUBLIC_POSTHOG_API_HOST: ${{ vars.NEXT_PUBLIC_POSTHOG_API_HOST }}
|
||||||
|
NEXT_PUBLIC_FORMBRICKS_API_HOST: ${{ vars.NEXT_PUBLIC_FORMBRICKS_API_HOST }}
|
||||||
|
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID }}
|
||||||
|
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID }}
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
NODE_ENV: production
|
||||||
|
CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}
|
||||||
|
CLOUDFLARE_DNS_API_TOKEN: ${{ secrets.CLOUDFLARE_DNS_API_TOKEN }}
|
||||||
|
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
|
||||||
|
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
|
||||||
|
S3_REGION: ${{ vars.S3_REGION }}
|
||||||
|
S3_BUCKET_NAME: ${{ vars.S3_BUCKET_NAME }}
|
||||||
|
OPENTELEMETRY_LISTENER_URL: ${{ vars.OPENTELEMETRY_LISTENER_URL }}
|
||||||
|
RATE_LIMITING_DISABLED: ${{ vars.RATE_LIMITING_DISABLED }}
|
||||||
|
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
||||||
|
DB_HOST: ${{ secrets.DB_HOST }}
|
||||||
|
DB_USER: ${{ secrets.DB_USER }}
|
||||||
|
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
|
||||||
|
DB_NAME: ${{ secrets.DB_NAME }}
|
||||||
|
REDIS_URL: ${{ secrets.REDIS_URL }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Ruby
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: 3.3.0
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
gem install kamal
|
||||||
|
|
||||||
|
- uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
with:
|
||||||
|
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Create builder
|
||||||
|
run: docker buildx create --use --name formbricks-gh-actions-builder
|
||||||
|
if: steps.buildx.outputs.should_create_builder == 'true'
|
||||||
|
|
||||||
|
- name: Push env variables to Kamal
|
||||||
|
run: |
|
||||||
|
kamal() { command kamal "$@" -c kamal/deploy.yml; }
|
||||||
|
kamal env push
|
||||||
|
|
||||||
|
- name: Run setup command
|
||||||
|
run: |
|
||||||
|
kamal() { command kamal "$@" -c kamal/deploy.yml; }
|
||||||
|
set +e
|
||||||
|
DEPLOY_OUTPUT=$(kamal setup 2>&1)
|
||||||
|
DEPLOY_EXIT_CODE=$?
|
||||||
|
echo "$DEPLOY_OUTPUT"
|
||||||
|
if [[ "$DEPLOY_OUTPUT" == *"container not unhealthy (healthy)"* ]]; then
|
||||||
|
echo "Deployment reported healthy container. Considering as success."
|
||||||
|
kamal lock release
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
exit $DEPLOY_EXIT_CODE
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
19
.github/workflows/release-docker-github.yml
vendored
@@ -1,4 +1,9 @@
|
|||||||
name: Docker Release to GitHub
|
name: Docker Release to Github
|
||||||
|
|
||||||
|
# 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:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -48,17 +53,6 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set tags based on event type
|
|
||||||
id: set-tags
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event_name }}" == "push" ]]; then
|
|
||||||
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
|
||||||
echo "::set-output name=tags::latest,${{ github.ref }}"
|
|
||||||
fi
|
|
||||||
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
|
||||||
echo "::set-output name=tags::experimental"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract metadata (tags, labels) for Docker
|
# Extract metadata (tags, labels) for Docker
|
||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
- name: Extract Docker metadata
|
- name: Extract Docker metadata
|
||||||
@@ -66,7 +60,6 @@ jobs:
|
|||||||
uses: docker/metadata-action@v5 # v5.0.0
|
uses: docker/metadata-action@v5 # v5.0.0
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: ${{ steps.set-tags.outputs.tags }}
|
|
||||||
|
|
||||||
# Build and push Docker image with Buildx (don't push on PR)
|
# Build and push Docker image with Buildx (don't push on PR)
|
||||||
# https://github.com/docker/build-push-action
|
# https://github.com/docker/build-push-action
|
||||||
|
|||||||
44
.github/workflows/release-docker.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Release on Dockerhub
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-image-on-dockerhub:
|
||||||
|
name: Release on Dockerhub
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Get Release Tag
|
||||||
|
id: extract_release_tag
|
||||||
|
run: |
|
||||||
|
TAG=${{ github.ref }}
|
||||||
|
TAG=${TAG#refs/tags/v}
|
||||||
|
echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./apps/web/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKER_USERNAME }}/formbricks:${{ env.RELEASE_TAG }}
|
||||||
|
${{ secrets.DOCKER_USERNAME }}/formbricks:latest
|
||||||
@@ -82,7 +82,7 @@ Formbricks is both a free and open source survey platform - and a privacy-first
|
|||||||
|
|
||||||
- 🔗 Create shareable **link surveys**.
|
- 🔗 Create shareable **link surveys**.
|
||||||
|
|
||||||
- 👨👩👦 Invite your team members to **collaborate** on your surveys.
|
- 👨👩👦 Invite your organization members to **collaborate** on your surveys.
|
||||||
|
|
||||||
- 🔌 Integrate Formbricks with **Slack, Notion, Zapier, n8n and more**.
|
- 🔌 Integrate Formbricks with **Slack, Notion, Zapier, n8n and more**.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
|
|
||||||
|
|
||||||
interface SurveySwitchProps {
|
interface SurveySwitchProps {
|
||||||
value: "website" | "app";
|
value: "website" | "app";
|
||||||
formbricks: any;
|
formbricks: any;
|
||||||
@@ -7,23 +5,18 @@ interface SurveySwitchProps {
|
|||||||
|
|
||||||
export const SurveySwitch = ({ value, formbricks }: SurveySwitchProps) => {
|
export const SurveySwitch = ({ value, formbricks }: SurveySwitchProps) => {
|
||||||
return (
|
return (
|
||||||
<Select
|
<select
|
||||||
value={value}
|
value={value}
|
||||||
onValueChange={(v) => {
|
onChange={(v) => {
|
||||||
formbricks.logout();
|
formbricks.logout();
|
||||||
window.location.href = `/${v}`;
|
window.location.href = `/${v.target.value}`;
|
||||||
}}>
|
}}>
|
||||||
<SelectTrigger className="w-[180px] px-4">
|
<option value="website" className="h-10 px-4 hover:bg-slate-100">
|
||||||
<SelectValue placeholder="Theme" />
|
Website Surveys
|
||||||
</SelectTrigger>
|
</option>
|
||||||
<SelectContent>
|
<option value="app" className="hover:bg-slate-10 h-10 px-4">
|
||||||
<SelectItem value="website" className="h-10 px-4 hover:bg-slate-100">
|
App Surveys
|
||||||
Website Surveys
|
</option>
|
||||||
</SelectItem>
|
</select>
|
||||||
<SelectItem value="app" className="hover:bg-slate-10 h-10 px-4">
|
|
||||||
App Surveys
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
transpilePackages: ["@formbricks/ui"],
|
|
||||||
async redirects() {
|
async redirects() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -25,4 +24,4 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = nextConfig;
|
export default nextConfig;
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formbricks/js": "workspace:*",
|
"@formbricks/js": "workspace:*",
|
||||||
"@formbricks/ui": "workspace:*",
|
"@formbricks/ui": "workspace:*",
|
||||||
"lucide-react": "^0.378.0",
|
"lucide-react": "^0.379.0",
|
||||||
"next": "14.2.3",
|
"next": "14.2.3",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1"
|
"react-dom": "18.3.1"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import I2 from "./images/I2.webp";
|
|||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Using Actions in Formbricks | Fine-tuning User Moments",
|
title: "Using Actions in Formbricks | Fine-tuning User Moments",
|
||||||
description:
|
description:
|
||||||
"Dive deep into how actions in Formbricks help products and teams to engage users at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine user targeting and generate richer, more detailed user insights.",
|
"Dive deep into how actions in Formbricks help products and organizations to engage users at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine user targeting and generate richer, more detailed user insights.",
|
||||||
};
|
};
|
||||||
|
|
||||||
#### App Surveys
|
#### App Surveys
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ To run this survey properly, you should pre-segment your user base. As touched u
|
|||||||
|
|
||||||
- Check the time passed since sign-up (e.g. signed up 4 weeks ago)
|
- Check the time passed since sign-up (e.g. signed up 4 weeks ago)
|
||||||
- User has performed a specific action a certain number of times or (e.g. created 5 reports)
|
- User has performed a specific action a certain number of times or (e.g. created 5 reports)
|
||||||
- User has performed a combination of actions (e.g. created a report **and** invited a team member)
|
- User has performed a combination of actions (e.g. created a report **and** invited a organization member)
|
||||||
|
|
||||||
This way you make sure that you separate potentially misleading opinions from valuable insights.
|
This way you make sure that you separate potentially misleading opinions from valuable insights.
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 66 KiB |
@@ -21,7 +21,8 @@ export const metadata = {
|
|||||||
The Google Sheets integration allows you to automatically send responses to a Google Sheet of your choice.
|
The Google Sheets integration allows you to automatically send responses to a Google Sheet of your choice.
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
If you are on a self-hosted instance, you will need to configure this integration separately. Please follow the guides [here](/self-hosting/integrations) to configure integrations on your self-hosted instance.
|
If you are on a self-hosted instance, you will need to configure this integration separately. Please follow
|
||||||
|
the guides [here](/self-hosting/integrations) to configure integrations on your self-hosted instance.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
## Connect Google Sheets
|
## Connect Google Sheets
|
||||||
@@ -70,7 +71,7 @@ Before the next step, make sure that you have a Formbricks Survey with at least
|
|||||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
6. Select the Google Sheet you want to link with Formbricks and the Survey. On doing so, you will be asked with what questions' responses you want to feed in the Google Sheet. Select the questions and click on the "Link Sheet" button.
|
6. Enter the spreadsheet URL for the Google Sheet you want to link with Formbricks and the Survey. On doing so, you will be asked with what questions' responses you want to feed in the Google Sheet. Select the questions and click on the "Link Sheet" button.
|
||||||
|
|
||||||
<MdxImage
|
<MdxImage
|
||||||
src={LinkWithQuestions}
|
src={LinkWithQuestions}
|
||||||
@@ -115,7 +116,6 @@ To remove the integration with Google Account,
|
|||||||
For the above, we ask for:
|
For the above, we ask for:
|
||||||
|
|
||||||
1. **User Email**: To identify you (that's it, nothing else, we're opensource, see this in our codebase [here](https://github.com/formbricks/formbricks/blob/main/apps/web/app/api/google-sheet/callback/route.ts#L47C17-L47C25))
|
1. **User Email**: To identify you (that's it, nothing else, we're opensource, see this in our codebase [here](https://github.com/formbricks/formbricks/blob/main/apps/web/app/api/google-sheet/callback/route.ts#L47C17-L47C25))
|
||||||
1. **Google Drive API**: To list all your google sheets (that's it, nothing else, we're opensource, see this method in our codebase [here](https://github.com/formbricks/formbricks/blob/main/packages/lib/googleSheet/service.ts#L13))
|
|
||||||
1. **Google Spreadsheet API**: To write to the spreadsheet you select (that's it, nothing else, we're opensource, see this method in our codebase [here](https://github.com/formbricks/formbricks/blob/main/packages/lib/googleSheet/service.ts#L70))
|
1. **Google Spreadsheet API**: To write to the spreadsheet you select (that's it, nothing else, we're opensource, see this method in our codebase [here](https://github.com/formbricks/formbricks/blob/main/packages/lib/googleSheet/service.ts#L70))
|
||||||
|
|
||||||
<Note>We store as little personal information as possible.</Note>
|
<Note>We store as little personal information as possible.</Note>
|
||||||
|
|||||||
BIN
apps/docs/app/global/access-roles/images/add-member.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
apps/docs/app/global/access-roles/images/bulk-invite.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/docs/app/global/access-roles/images/individual-invite.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
apps/docs/app/global/access-roles/images/team-settings-menu.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
100
apps/docs/app/global/access-roles/page.mdx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { MdxImage } from "@/components/MdxImage";
|
||||||
|
import AddMember from "./images/add-member.webp";
|
||||||
|
import BulkInvite from "./images/bulk-invite.webp";
|
||||||
|
import IndvInvite from "./images/individual-invite.webp";
|
||||||
|
import MenuItem from "./images/team-settings-menu.webp";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Team Access Roles",
|
||||||
|
description:
|
||||||
|
"Assign different roles to team members to grant them specific rights like creating surveys, viewing responses, or managing team members.",
|
||||||
|
};
|
||||||
|
|
||||||
|
# Team Access Roles
|
||||||
|
|
||||||
|
Assign different roles to team members to grant them specific rights like creating surveys, viewing responses, or managing team members.
|
||||||
|
|
||||||
|
<Note>Access Roles is a feature of the **Enterprise Edition**. In the **Community Edition** and on the **Free** and **Startup** plan in the Cloud you can invite unlimited team members as `Admins`.</Note>
|
||||||
|
|
||||||
|
|
||||||
|
| Role | Rights |
|
||||||
|
| --- | --- |
|
||||||
|
| Owner | Full rights; there can only one owner per team. Ownership can be transferred. |
|
||||||
|
| Admin | Full access rights incl. managing team members |
|
||||||
|
| Developer | Full product access to setup and run surveys incl. global styling, actions and attribute management, etc |
|
||||||
|
| Editor | Create and edit surveys. No access to features related to setting up or maintaining Formbricks. |
|
||||||
|
| Viewer | View survey results only. |
|
||||||
|
|
||||||
|
## Inviting team members
|
||||||
|
|
||||||
|
There are two ways to invite team members: One by one or in bulk.
|
||||||
|
|
||||||
|
### Invite team members one by one
|
||||||
|
|
||||||
|
1. Go to the `Team Settings` page via the menu in the lower right corner:
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={MenuItem}
|
||||||
|
alt="Where to find the Menu Item for Team Settings"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
2. Click on the `Add Member` button:
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={AddMember}
|
||||||
|
alt="Add Member Button Position"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
3. In the modal, add the Name, Email and Role of the team member you want to invite:
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={IndvInvite}
|
||||||
|
alt="Individual Invite Modal Tab"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Note>Access Roles is a feature of the **Enterprise Edition**. In the **Community Edition** and on the **Free** and **Startup** plan in the Cloud you can invite unlimited team members as `Admins`.</Note>
|
||||||
|
|
||||||
|
|
||||||
|
Formbricks sends an email to the team member with an invitation link. The team member can accept the invitation or create a new account by clicking on the link.
|
||||||
|
|
||||||
|
|
||||||
|
### Invite team members in bulk
|
||||||
|
|
||||||
|
1. Go to the `Team Settings` page via the menu in the lower right corner:
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={MenuItem}
|
||||||
|
alt="Where to find the Menu Item for Team Settings"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
2. Click on the `Add Member` button:
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={AddMember}
|
||||||
|
alt="Add Member Button Position"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
3. In the modal, switch to `Bulk Invite`. You can download an example .CSV file to fill in the Name, Email and Role of the team members you want to invite:
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={BulkInvite}
|
||||||
|
alt="Individual Invite Modal Tab"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
4. Upload the filled .CSV file and invite the team members in bulk ✅
|
||||||
|
|
||||||
|
Formbricks sends an email to each team member in the CSV. The member can accept the invitation or create a new account by clicking on the link.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
import { MdxImage } from "@/components/MdxImage";
|
|
||||||
|
|
||||||
import PreRequisiteImage from "./images/Pre-requisite.webp";
|
|
||||||
import StepEight from "./images/StepEight.webp";
|
|
||||||
import StepEleven from "./images/StepEleven.webp";
|
|
||||||
import StepFive from "./images/StepFive.webp";
|
|
||||||
import StepFour from "./images/StepFour.webp";
|
|
||||||
import StepNine from "./images/StepNine.webp";
|
|
||||||
import StepOne from "./images/StepOne.webp";
|
|
||||||
import StepSeven from "./images/StepSeven.webp";
|
|
||||||
import StepSix from "./images/StepSix.webp";
|
|
||||||
import StepTen from "./images/StepTen.webp";
|
|
||||||
import StepThree from "./images/StepThree.webp";
|
|
||||||
import StepTwo from "./images/StepTwo.webp";
|
|
||||||
|
|
||||||
import Doggo from "./images/Doggo.jpg";
|
|
||||||
import HipsterLiving from "./images/HipsterLiving.jpg";
|
|
||||||
import Mario from "./images/Mario.webp";
|
|
||||||
import WindowsXp from "./images/WindowsXp.jpg";
|
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: "Custom Styling in Formbricks Surveys",
|
|
||||||
description:
|
|
||||||
"Style your surveys effortlessly with Formbricks to match your brand's unique aesthetic, from Super Mario themes to the minimalist Windows XP style. Customize everything from color schemes to adding your logo, all without needing any coding skills.",
|
|
||||||
};
|
|
||||||
|
|
||||||
# Custom Styling
|
|
||||||
|
|
||||||
Style your surveys effortlessly with Formbricks to match your brand's unique aesthetic, from Super Mario themes to the minimalist Windows XP style. Customize everything from color schemes to adding your logo, all without needing any coding skills. Don’t miss out some of our standout design surveys at the end!
|
|
||||||
|
|
||||||
### Product Styling
|
|
||||||
|
|
||||||
Easily apply a consistent style across all your current & future surveys:
|
|
||||||
|
|
||||||
**Pre-requisites:**
|
|
||||||
|
|
||||||
1. Navigate to the **Settings** tab on the Formbricks Dashboard.
|
|
||||||
2. Select **Look & Feel** under the **Product** section in the left navbar
|
|
||||||
|
|
||||||
<MdxImage
|
|
||||||
src={PreRequisiteImage}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
|
||||||
/>
|
|
||||||
|
|
||||||
**Switch between Link Survey & App Survey** to preview different styles
|
|
||||||
|
|
||||||
### **Styling Options**
|
|
||||||
|
|
||||||
1. **Form Styling:** Customize the survey card using the following settings
|
|
||||||
|
|
||||||
<MdxImage
|
|
||||||
src={StepOne}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
|
||||||
/>
|
|
||||||
|
|
||||||
- **Brand Color**: Sets the primary color tone of the survey.
|
|
||||||
- **Text Color**: This is a single color scheme that will be used across to display all the text on your survey. Ensures all text is readable against the background.
|
|
||||||
- **Input Color:** Alters the border color of input fields.
|
|
||||||
- **Input Border Color**: This is the color of the border of the form input field.
|
|
||||||
|
|
||||||
2. **Card Styling:** Adjust the look of the survey card
|
|
||||||
|
|
||||||
<MdxImage
|
|
||||||
src={StepTwo}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
|
||||||
/>
|
|
||||||
|
|
||||||
- **Roundness**: Adjusts the corner roundness of the survey card and its components (including input boxes, buttons).
|
|
||||||
- **Card Background Color**: Sets the card's main background color.
|
|
||||||
- **Card Border Color**: Changes the border color of the card
|
|
||||||
- **Card Shadow Color**: Adds a shadow effect for depth
|
|
||||||
- **Hide Progress Bar**: Optionally remove the progress bar to simplify the survey experience
|
|
||||||
- **Add Highlight Border**: Adds a distinct border for emphasis.
|
|
||||||
|
|
||||||
3. **Background Styling**: Customize the survey background with static colors, animations, or images (upload your own or get from Unsplash)
|
|
||||||
|
|
||||||
<Note>This is only available for Link Surveys</Note>
|
|
||||||
|
|
||||||
<MdxImage
|
|
||||||
src={StepThree}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
|
||||||
/>
|
|
||||||
|
|
||||||
- **Color**: Pick any color for the background
|
|
||||||
- **Animation**: Add dynamic animations to enhance user experience..
|
|
||||||
- **Upload**: Use a custom uploaded image for a personalized touch
|
|
||||||
- Image: Choose from Unsplash's extensive gallery. Note that these images will have a link and mention of the author & Unsplash on the bottom right to give them the credit for their awesome work!
|
|
||||||
- **Background Overlay**: Adjust the background's opacity
|
|
||||||
|
|
||||||
### Add Brand Logo:
|
|
||||||
|
|
||||||
Customize your survey with your brand's logo.
|
|
||||||
|
|
||||||
<Note>only available for link survyes</Note>
|
|
||||||
|
|
||||||
1. In the Look & Feel page itself in Product settings, scroll down to see the Logo Upload box.
|
|
||||||
|
|
||||||
<MdxImage
|
|
||||||
src={StepFour}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
|
||||||
/>
|
|
||||||
|
|
||||||
2. Upload your logo
|
|
||||||
|
|
||||||
<MdxImage
|
|
||||||
src={StepFive}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
|
||||||
/>
|
|
||||||
|
|
||||||
3. Add a background color: If you’ve uploaded a transparent image and want to add background to it, enable this toggle and select the color of your choice.
|
|
||||||
|
|
||||||
<MdxImage
|
|
||||||
src={StepSix}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
|
||||||
/>
|
|
||||||
|
|
||||||
4. Remember to save your changes!
|
|
||||||
|
|
||||||
<MdxImage
|
|
||||||
src={StepSeven}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Note>The logo setting applies across all link surveys</Note>
|
|
||||||
|
|
||||||
### Overwrite Product Styling
|
|
||||||
|
|
||||||
Create surveys with specific styles that differ from your general product style.
|
|
||||||
|
|
||||||
**Pre-requisites**:
|
|
||||||
|
|
||||||
- Enable the **Custom Styling** toggle in the **Look & Feel** settings
|
|
||||||
|
|
||||||
<MdxImage
|
|
||||||
src={StepEight}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
|
||||||
/>
|
|
||||||
|
|
||||||
**Steps:**
|
|
||||||
|
|
||||||
1. Open the survey editor you want to change the styling of and switch to the Styling tab.
|
|
||||||
|
|
||||||
<MdxImage
|
|
||||||
src={StepNine}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
|
||||||
/>
|
|
||||||
|
|
||||||
2. Activate the **Add Custom Styles** toggle to override the default product styling.
|
|
||||||
|
|
||||||
<MdxImage
|
|
||||||
src={StepTen}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
|
||||||
/>
|
|
||||||
|
|
||||||
3. Customize your survey's style as needed!
|
|
||||||
|
|
||||||
<MdxImage
|
|
||||||
src={StepEleven}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
|
||||||
/>
|
|
||||||
|
|
||||||
Voila! just hit the save button to apply your changes. Your survey is now ready to impress with its unique look!
|
|
||||||
|
|
||||||
## Overwrite CSS Styles for App & Website Surveys
|
|
||||||
|
|
||||||
You can overwrite the default CSS styles for the app & website surveys by adding the following CSS to your global CSS file (eg. `globals.css`):
|
|
||||||
|
|
||||||
Make sure that you do not change the CSS variable names as they are used by Formbricks to identify the CSS variables. You can change the values to your liking. We have filled in some sample values for you to change according to your desired appearance.
|
|
||||||
|
|
||||||
<Col>
|
|
||||||
<CodeGroup title="Overwrite Formbricks CSS">
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* Formbricks CSS */
|
|
||||||
--fb-brand-color: red;
|
|
||||||
--fb-brand-text-color: white;
|
|
||||||
--fb-border-color: green;
|
|
||||||
--fb-border-color-highlight: rgb(13, 13, 12);
|
|
||||||
--fb-focus-color: red;
|
|
||||||
--fb-heading-color: yellow;
|
|
||||||
--fb-subheading-color: green;
|
|
||||||
--fb-info-text-color: orange;
|
|
||||||
--fb-signature-text-color: blue;
|
|
||||||
--fb-survey-background-color: black;
|
|
||||||
--fb-accent-background-color: rgb(13, 13, 12);
|
|
||||||
--fb-accent-background-color-selected: red;
|
|
||||||
--fb-placeholder-color: white;
|
|
||||||
--fb-shadow-color: var(--fb-brand-color);
|
|
||||||
--fb-rating-fill: rgb(13, 13, 12);
|
|
||||||
--fb-rating-hover: green;
|
|
||||||
--fb-back-btn-border: blue;
|
|
||||||
--fb-submit-btn-border: transparent;
|
|
||||||
--fb-rating-selected: black;
|
|
||||||
```
|
|
||||||
|
|
||||||
</CodeGroup>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
We have an example of this in our [Demo project](https://github.com/formbricks/formbricks/blob/main/apps/demo/styles/globals.css) here.
|
|
||||||
|
|
||||||
### Funky Survey Examples
|
|
||||||
|
|
||||||
- **Super Mario:** I guess he won't let himself be limited by radio buttons and do all three things
|
|
||||||
|
|
||||||
<MdxImage
|
|
||||||
src={Mario}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
|
||||||
/>
|
|
||||||
|
|
||||||
- **Hipster Living**: Does your monstera get enough water?
|
|
||||||
|
|
||||||
<MdxImage
|
|
||||||
src={HipsterLiving}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
|
||||||
/>
|
|
||||||
|
|
||||||
- **Windows XP**: Hach, nostalgia. Made us wanna play Mafia.
|
|
||||||
<MdxImage
|
|
||||||
src={WindowsXp}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
|
||||||
/>
|
|
||||||
- **Whosagooooodbooooy**: Things you've likely said to your dog.
|
|
||||||
<MdxImage
|
|
||||||
src={Doggo}
|
|
||||||
alt="Choose a link survey template"
|
|
||||||
quality="100"
|
|
||||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
|
||||||
/>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
131
apps/docs/app/global/overwrite-styling/page.mdx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { MdxImage } from "@/components/MdxImage";
|
||||||
|
|
||||||
|
import StepEleven from "./images/StepEleven.webp";
|
||||||
|
import StepNine from "./images/StepNine.webp";
|
||||||
|
import StepTen from "./images/StepTen.webp";
|
||||||
|
|
||||||
|
import Doggo from "./images/Doggo.jpg";
|
||||||
|
import HipsterLiving from "./images/HipsterLiving.jpg";
|
||||||
|
import Mario from "./images/Mario.webp";
|
||||||
|
import WindowsXp from "./images/WindowsXp.jpg";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Overwrite Styling Theme on Individual Surveys",
|
||||||
|
description:
|
||||||
|
"Overwrite the global styling theme for individual surveys to create unique styles for each survey.",
|
||||||
|
};
|
||||||
|
|
||||||
|
# Overwrite Styling Theme on Individual Surveys
|
||||||
|
|
||||||
|
Overwrite the global styling theme for individual surveys to create unique styles for each survey.
|
||||||
|
|
||||||
|
<Note>To set a styling theme for all surveys, please see the [Styling Theme](/global/styling-theme) manual. </Note>
|
||||||
|
|
||||||
|
|
||||||
|
### Overwrite Styling Theme
|
||||||
|
|
||||||
|
1. In the Survey Editor of the survey you want to style, navigate to the **Styling** tab:
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={StepNine}
|
||||||
|
alt="Choose a link survey template"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
2. Activate the **Add Custom Styles** toggle to override the default product styling:
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={StepTen}
|
||||||
|
alt="Choose a link survey template"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
3. Customize your survey's style as needed:
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={StepEleven}
|
||||||
|
alt="Choose a link survey template"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
Voila! just hit the save button to apply your changes. Your survey is now ready to impress with its unique look!
|
||||||
|
|
||||||
|
## Overwrite CSS Styles for App & Website Surveys
|
||||||
|
|
||||||
|
You can overwrite the default CSS styles for the app & website surveys by adding the following CSS to your global CSS file (eg. `globals.css`):
|
||||||
|
|
||||||
|
Make sure that you do not change the CSS variable names as they are used by Formbricks to identify the CSS variables. You can change the values to your liking. We have filled in some sample values for you to change according to your desired appearance.
|
||||||
|
|
||||||
|
<Col>
|
||||||
|
<CodeGroup title="Overwrite Formbricks CSS">
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Formbricks CSS */
|
||||||
|
--fb-brand-color: red;
|
||||||
|
--fb-brand-text-color: white;
|
||||||
|
--fb-border-color: green;
|
||||||
|
--fb-border-color-highlight: rgb(13, 13, 12);
|
||||||
|
--fb-focus-color: red;
|
||||||
|
--fb-heading-color: yellow;
|
||||||
|
--fb-subheading-color: green;
|
||||||
|
--fb-info-text-color: orange;
|
||||||
|
--fb-signature-text-color: blue;
|
||||||
|
--fb-survey-background-color: black;
|
||||||
|
--fb-accent-background-color: rgb(13, 13, 12);
|
||||||
|
--fb-accent-background-color-selected: red;
|
||||||
|
--fb-placeholder-color: white;
|
||||||
|
--fb-shadow-color: var(--fb-brand-color);
|
||||||
|
--fb-rating-fill: rgb(13, 13, 12);
|
||||||
|
--fb-rating-hover: green;
|
||||||
|
--fb-back-btn-border: blue;
|
||||||
|
--fb-submit-btn-border: transparent;
|
||||||
|
--fb-rating-selected: black;
|
||||||
|
```
|
||||||
|
|
||||||
|
</CodeGroup>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
We have an example of this in our [Demo project](https://github.com/formbricks/formbricks/blob/main/apps/demo/styles/globals.css) here.
|
||||||
|
|
||||||
|
## Funky Survey Examples
|
||||||
|
|
||||||
|
- **Super Mario:** I guess he won't let himself be limited by radio buttons and do all three things
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={Mario}
|
||||||
|
alt="Choose a link survey template"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
- **Hipster Living**: Does your monstera get enough water?
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={HipsterLiving}
|
||||||
|
alt="Choose a link survey template"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
- **Windows XP**: Hach, nostalgia. Made us wanna play Mafia.
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={WindowsXp}
|
||||||
|
alt="Choose a link survey template"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
- **Whosagooooodbooooy**: Things you've likely said to your dog.
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={Doggo}
|
||||||
|
alt="Choose a link survey template"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
---
|
||||||
BIN
apps/docs/app/global/styling-theme/images/Doggo.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
apps/docs/app/global/styling-theme/images/HipsterLiving.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
apps/docs/app/global/styling-theme/images/Mario.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
apps/docs/app/global/styling-theme/images/Pre-requisite.webp
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
apps/docs/app/global/styling-theme/images/StepEight.webp
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
apps/docs/app/global/styling-theme/images/StepEleven.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/docs/app/global/styling-theme/images/StepFive.webp
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
apps/docs/app/global/styling-theme/images/StepFour.webp
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
apps/docs/app/global/styling-theme/images/StepNine.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
apps/docs/app/global/styling-theme/images/StepOne.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
apps/docs/app/global/styling-theme/images/StepSeven.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
apps/docs/app/global/styling-theme/images/StepSix.webp
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
apps/docs/app/global/styling-theme/images/StepTen.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
apps/docs/app/global/styling-theme/images/StepThree.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
apps/docs/app/global/styling-theme/images/StepTwo.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
apps/docs/app/global/styling-theme/images/WindowsXp.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
apps/docs/app/global/styling-theme/images/allow-overwrite.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 71 KiB |
BIN
apps/docs/app/global/styling-theme/images/card-settings.webp
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
apps/docs/app/global/styling-theme/images/form-settings.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
apps/docs/app/global/styling-theme/images/look-and-feel.webp
Normal file
|
After Width: | Height: | Size: 80 KiB |
149
apps/docs/app/global/styling-theme/page.mdx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { MdxImage } from "@/components/MdxImage";
|
||||||
|
|
||||||
|
import StepEight from "./images/StepEight.webp";
|
||||||
|
import StepEleven from "./images/StepEleven.webp";
|
||||||
|
import StepFive from "./images/StepFive.webp";
|
||||||
|
import StepFour from "./images/StepFour.webp";
|
||||||
|
import StepNine from "./images/StepNine.webp";
|
||||||
|
import StepSeven from "./images/StepSeven.webp";
|
||||||
|
import StepSix from "./images/StepSix.webp";
|
||||||
|
import StepTen from "./images/StepTen.webp";
|
||||||
|
import AllowOverwrite from "./images/allow-overwrite.webp";
|
||||||
|
import BackgroundSettings from "./images/background-settings.webp";
|
||||||
|
import CardSettings from "./images/card-settings.webp";
|
||||||
|
import FormSettings from "./images/form-settings.webp";
|
||||||
|
import LookAndFeelSettings from "./images/look-and-feel.webp";
|
||||||
|
|
||||||
|
import Doggo from "./images/Doggo.jpg";
|
||||||
|
import HipsterLiving from "./images/HipsterLiving.jpg";
|
||||||
|
import Mario from "./images/Mario.webp";
|
||||||
|
import WindowsXp from "./images/WindowsXp.jpg";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Styling Theme",
|
||||||
|
description:
|
||||||
|
"Keep the survey styling consistent over all surveys with a Styling Theme. Customize the colors, fonts, and other styling options to match your brand's aesthetic.",
|
||||||
|
};
|
||||||
|
|
||||||
|
# Styling Theme
|
||||||
|
|
||||||
|
Keep the survey styling consistent over all surveys with a Styling Theme. Customize the colors, fonts, and other styling options to match your brand's aesthetic.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
In the left side bar, you find the `Configuration` page. On this page you find the `Look & Feel` section:
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={LookAndFeelSettings}
|
||||||
|
alt="Choose a link survey template"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||||
|
/>
|
||||||
|
|
||||||
|
## **Styling Options**
|
||||||
|
|
||||||
|
1. **Form Styling:** Customize the survey card using the following settings
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={FormSettings}
|
||||||
|
alt="Form styling options UI"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
- **Brand Color**: Sets the primary color tone of the survey.
|
||||||
|
- **Text Color**: This is a single color scheme that will be used across to display all the text on your survey. Ensures all text is readable against the background.
|
||||||
|
- **Input Color:** Alters the border color of input fields.
|
||||||
|
- **Input Border Color**: This is the color of the border of the form input field.
|
||||||
|
|
||||||
|
2. **Card Styling:** Adjust the look of the survey card
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={CardSettings}
|
||||||
|
alt="Card styling options UI"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
- **Roundness**: Adjusts the corner roundness of the survey card and its components (including input boxes, buttons).
|
||||||
|
- **Card Background Color**: Sets the card's main background color.
|
||||||
|
- **Card Border Color**: Changes the border color of the card
|
||||||
|
- **Card Shadow Color**: Adds a shadow effect for depth
|
||||||
|
- **Hide Progress Bar**: Optionally remove the progress bar to simplify the survey experience
|
||||||
|
- **Add Highlight Border**: Adds a distinct border for emphasis.
|
||||||
|
|
||||||
|
3. **Background Styling**: Customize the survey background with static colors, animations, or images (upload your own or get from Unsplash)
|
||||||
|
|
||||||
|
<Note>This is only available for Link Surveys</Note>
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={BackgroundSettings}
|
||||||
|
alt="Background styling options UI"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
- **Color**: Pick any color for the background
|
||||||
|
- **Animation**: Add dynamic animations to enhance user experience..
|
||||||
|
- **Upload**: Use a custom uploaded image for a personalized touch
|
||||||
|
- Image: Choose from Unsplash's extensive gallery. Note that these images will have a link and mention of the author & Unsplash on the bottom right to give them the credit for their awesome work!
|
||||||
|
- **Background Overlay**: Adjust the background's opacity
|
||||||
|
|
||||||
|
## Add Brand Logo
|
||||||
|
|
||||||
|
Customize your survey with your brand's logo.
|
||||||
|
|
||||||
|
<Note>Brand logos are only visible on Link Survey pages.</Note>
|
||||||
|
|
||||||
|
1. In the Look & Feel page itself in Product settings, scroll down to see the Logo Upload box.
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={StepFour}
|
||||||
|
alt="Choose a link survey template"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
2. Upload your logo
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={StepFive}
|
||||||
|
alt="Choose a link survey template"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
3. Add a background color: If you’ve uploaded a transparent image and want to add background to it, enable this toggle and select the color of your choice.
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={StepSix}
|
||||||
|
alt="Choose a link survey template"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
4. Remember to save your changes!
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={StepSeven}
|
||||||
|
alt="Choose a link survey template"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Note>The logo settings apply across all Link Surveys pages.</Note>
|
||||||
|
|
||||||
|
## Overwrite Styling Theme
|
||||||
|
|
||||||
|
You can allow to overwrite the styling theme for individual surveys to create unique styles for each survey:
|
||||||
|
|
||||||
|
<MdxImage
|
||||||
|
src={AllowOverwrite}
|
||||||
|
alt="Choose a link survey template"
|
||||||
|
quality="100"
|
||||||
|
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||||
|
/>
|
||||||
|
|
||||||
|
In the survey editor, a tab called `Styling` will appear. Here you can overwrite the default styling theme.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -51,8 +51,8 @@ These variables are present inside your machine’s docker-compose file. Restart
|
|||||||
| INSTANCE_ID | Instance ID for Formbricks Cloud to be sent to Telemetry. | optional | |
|
| 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 | |
|
| 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_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_ORGANIZATION_ID | Automatically assign new users to a specific organization when joining | optional | |
|
||||||
| DEFAULT_TEAM_ROLE | Role of the user in the default team. | optional | admin |
|
| DEFAULT_ORGANIZATION_ROLE | Role of the user in the default organization. | optional | admin |
|
||||||
| ONBOARDING_DISABLED | Disables onboarding for new users if set to 1 | optional | |
|
| ONBOARDING_DISABLED | Disables onboarding for new users if set to 1 | optional | |
|
||||||
| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | 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_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ Integrating Google Sheets with a self-hosted Formbricks instance requires config
|
|||||||
1. Go to the **[Google Cloud Console](https://console.cloud.google.com/)** and **create a new project**.
|
1. Go to the **[Google Cloud Console](https://console.cloud.google.com/)** and **create a new project**.
|
||||||
2. Enable necessary APIs:
|
2. Enable necessary APIs:
|
||||||
- Now select the project you just created and go to the **APIs & Services** section.
|
- Now select the project you just created and go to the **APIs & Services** section.
|
||||||
- Click on the **Enable APIs and Services** button and search for **Google Sheets API** & **Google Drive API** and enable it.
|
- Click on the **Enable APIs and Services** button and search for **Google Sheets API** and enable it.
|
||||||
3. Configure OAuth Consent Screen:
|
3. Configure OAuth Consent Screen:
|
||||||
- Go to **OAuth Consent screen** and select the appropriate User Type (External or Internal). Select **Internal** if you want only the users of your Google Workspace to be able to use the integration.
|
- Go to **OAuth Consent screen** and select the appropriate User Type (External or Internal). Select **Internal** if you want only the users of your Google Workspace to be able to use the integration.
|
||||||
- Fill the required details:
|
- Fill the required details:
|
||||||
@@ -128,12 +128,11 @@ Integrating Google Sheets with a self-hosted Formbricks instance requires config
|
|||||||
- Click on the **Add or Remove Scopes** button and add the scopes:
|
- Click on the **Add or Remove Scopes** button and add the scopes:
|
||||||
- `https://www.googleapis.com/auth/userinfo.email`
|
- `https://www.googleapis.com/auth/userinfo.email`
|
||||||
- `https://www.googleapis.com/auth/spreadsheets`
|
- `https://www.googleapis.com/auth/spreadsheets`
|
||||||
- `https://www.googleapis.com/auth/drive`
|
|
||||||
- Click on the **Update** button. Verify the scopes and click on the **Save and Continue** button.
|
- Click on the **Update** button. Verify the scopes and click on the **Save and Continue** button.
|
||||||
- Skip the **Test Users** section and click on the **Save and Continue** button.
|
- Skip the **Test Users** section and click on the **Save and Continue** button.
|
||||||
|
|
||||||
1. View the OAuth Consent Screen summary and click on the **Back to Dashboard** button.
|
5. View the OAuth Consent Screen summary and click on the **Back to Dashboard** button.
|
||||||
2. Register OAuth Client:
|
6. Register OAuth Client:
|
||||||
|
|
||||||
- Navigate to **Credentials** > **Create Credentials** > **OAuth Client ID**.
|
- Navigate to **Credentials** > **Create Credentials** > **OAuth Client ID**.
|
||||||
- Select **Web Application** and set:
|
- Select **Web Application** and set:
|
||||||
@@ -142,13 +141,10 @@ Integrating Google Sheets with a self-hosted Formbricks instance requires config
|
|||||||
- Authorized redirect URIs: `https://<your-public-facing-url>/api/google-sheet/callback`
|
- Authorized redirect URIs: `https://<your-public-facing-url>/api/google-sheet/callback`
|
||||||
- Save and note the Client ID and Client Secret.
|
- Save and note the Client ID and Client Secret.
|
||||||
|
|
||||||
1. Copy the Client ID and Client Secret and set them as environment variables in your Formbricks instance:
|
7. Copy the Client ID and Client Secret and set them as environment variables in your Formbricks instance:
|
||||||
- `GOOGLE_SHEETS_CLIENT_ID`
|
- `GOOGLE_SHEETS_CLIENT_ID`
|
||||||
- `GOOGLE_SHEETS_CLIENT_SECRET`
|
- `GOOGLE_SHEETS_CLIENT_SECRET`
|
||||||
- `GOOGLE_SHEETS_REDIRECT_URL`
|
- `GOOGLE_SHEETS_REDIRECT_URL`
|
||||||
2. Enable Google Drive API:
|
|
||||||
- Go to the **APIs & Services** section and click on the **Enable APIs and Services** button.
|
|
||||||
- Search for **Google Drive API** and enable it.
|
|
||||||
|
|
||||||
Now just copy **GOOGLE_SHEETS_CLIENT_ID**, **GOOGLE_SHEETS_CLIENT_SECRET** and **GOOGLE_SHEETS_REDIRECT_URL** for your integration & add it to your **Formbricks environment variables** as in the docker compose file:
|
Now just copy **GOOGLE_SHEETS_CLIENT_ID**, **GOOGLE_SHEETS_CLIENT_SECRET** and **GOOGLE_SHEETS_REDIRECT_URL** for your integration & add it to your **Formbricks environment variables** as in the docker compose file:
|
||||||
|
|
||||||
|
|||||||
@@ -9,30 +9,40 @@ export const metadata = {
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Formbricks core source code is under AGPL license on GitHub. Additionally, we also have additional features for bigger teams & enterprises under the Enterprise license for Self Hosting customers. Let’s understand both in depth below:
|
The Formbricks Core source code is licensed under AGPLv3 and available on GitHub. Additionally, we offer features for bigger organisations & enterprises for self-hostesr under a separate Enterprise License.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Want to present a proof of concept? Request a free 30-day Enterprise Edition trial by [filling out the form
|
||||||
|
below.](#30-day-trial-license-request) No call needed or strings attached: Just give us 24h to set up the
|
||||||
|
key and send it over 🤙
|
||||||
|
</Note>
|
||||||
|
|
||||||
## Enterprise Edition License
|
## Enterprise Edition License
|
||||||
|
|
||||||
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** to unlock it. For the pricing, please refer to [Formbricks Pricing](https://formbricks.com/pricing).
|
Additional to the AGPLv3 licensed Formbricks core, the Formbricks repository contains code licensed under our **[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** to unlock it. For the pricing, please refer to [Formbricks Pricing](https://formbricks.com/pricing).
|
||||||
|
|
||||||
### When do I need an Enterprise License?
|
### When do I need an Enterprise License?
|
||||||
|
|
||||||
| | Community Edition | Enterprise License |
|
| | Community Edition | Enterprise License |
|
||||||
| -------------------------------------------------------- | ----------------- | -------------------- |
|
| ----------------------------------------------------------- | ----------------- | -------------------- |
|
||||||
| Self-host for commercial purposes | ✅ | No EE license needed |
|
| Self-host for commercial purposes | ✅ | No EE license needed |
|
||||||
| To make changes to the code base (happy to publish them) | ✅ | No EE license needed |
|
| Make changes to the code base (have to publish all changes) | ✅ | No EE license needed |
|
||||||
| Unlimited responses | ✅ | No EE license needed |
|
| Unlimited responses | ✅ | No EE license needed |
|
||||||
| Unlimited surveys | ✅ | No EE license needed |
|
| Unlimited surveys | ✅ | No EE license needed |
|
||||||
| Remove branding | ✅ | No EE license needed |
|
| Remove branding | ✅ | No EE license needed |
|
||||||
| SSO | ✅ | No EE license needed |
|
| SSO | ✅ | No EE license needed |
|
||||||
| Use any of the other 100 features | ✅ | No EE license needed |
|
| Use any of the other 100 features | ✅ | No EE license needed |
|
||||||
| Team roles | ❌ | ✅ |
|
| Organization access roles | ❌ | ✅ |
|
||||||
| Multi-language surveys | ❌ | ✅ |
|
| Multi-language surveys | ❌ | ✅ |
|
||||||
| Advanced targeting / Segments | ❌ | ✅ |
|
| Advanced targeting / Segments | ❌ | ✅ |
|
||||||
| Make code changes and keep private | ❌ | ✅ |
|
| Make code changes and **keep private** | ❌ | ✅ |
|
||||||
|
|
||||||
Ready to get started with the Enterprise Edition? Fill out our form below and we'll reach out to you.
|
Ready to get started with the Enterprise Edition? Fill out our form below and we'll reach out to you.
|
||||||
|
|
||||||
|
## 30-day Trial License Request
|
||||||
|
|
||||||
|
Many organisations want to do an internal test run with the Enterprise Edition. To make that really easy, we now offer a 30-day trial license. Just fill out the form below and we'll send you a license key within 24 hours (business days):
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
@@ -50,11 +60,11 @@ Ready to get started with the Enterprise Edition? Fill out our form below and we
|
|||||||
|
|
||||||
### The AGPL Formbricks Core
|
### 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 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 and **publish your complete code under the AGPLv3 license as well.**
|
||||||
|
|
||||||
### The Enterprise Edition
|
### 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** to unlock it.
|
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** to unlock it.
|
||||||
|
|
||||||
## White-Labeling Formbricks and Other Licensing Needs
|
## White-Labeling Formbricks and Other Licensing Needs
|
||||||
|
|
||||||
@@ -62,6 +72,6 @@ We currently do not offer Formbricks white-labeled. Any other needs? [Send us an
|
|||||||
|
|
||||||
## Why charge for Enterprise Features?
|
## 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.
|
The Enterprise Edition allows 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)
|
**Any more questions?**: [Send us an email](mailto:johannes@formbricks.com) or [book a call with us.](https://cal.com/johannes/license)
|
||||||
|
|||||||
@@ -17,14 +17,18 @@ Formbricks v2.0 comes with huge features such as Multi-Language Surveys and Adva
|
|||||||
the upgrade. Follow the below steps thoroughly to upgrade your Formbricks instance to v2.0.
|
the upgrade. Follow the below steps thoroughly to upgrade your Formbricks instance to v2.0.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
and
|
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
If you've used the Formbricks Enterprise Edition with a free beta license key, your instance will be
|
If you've used the Formbricks Enterprise Edition with a free beta license key, your instance will be
|
||||||
downgraded to the Community Edition 2.0. You find all license details on the [license
|
downgraded to the Community Edition 2.0. You find all license details on the [license
|
||||||
page.](/self-hosting/license/)
|
page.](/self-hosting/license/)
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
We are moving from DockerHub to Github Packages for our images. If you are still pulling the images from
|
||||||
|
DockerHub please change `image: formbricks/formbricks:latest` to
|
||||||
|
`image:ghcr.io/formbricks/formbricks:latest` in your `docker-compose.yml` file.
|
||||||
|
</Note>
|
||||||
|
|
||||||
### Steps to Migrate
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import StepTwo from "./images/StepTwo.webp";
|
|||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Using Actions in Formbricks | Fine-tuning Session Moments",
|
title: "Using Actions in Formbricks | Fine-tuning Session Moments",
|
||||||
description:
|
description:
|
||||||
"Dive deep into how actions in Formbricks help products and teams to engage active sessions at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine public facing websites' targeting and generate richer, more detailed insights.",
|
"Dive deep into how actions in Formbricks help products and organizations to engage active sessions at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine public facing websites' targeting and generate richer, more detailed insights.",
|
||||||
};
|
};
|
||||||
|
|
||||||
#### Website Surveys
|
#### Website Surveys
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const navigation: Array<NavGroup> = [
|
|||||||
{ title: "Recontact Options", href: "/app-surveys/recontact" },
|
{ title: "Recontact Options", href: "/app-surveys/recontact" },
|
||||||
{ title: "Multi Language Surveys", href: "/global/multi-language-surveys" }, // global
|
{ title: "Multi Language Surveys", href: "/global/multi-language-surveys" }, // global
|
||||||
{ title: "User Metadata", href: "/global/metadata" }, // global
|
{ title: "User Metadata", href: "/global/metadata" }, // global
|
||||||
{ title: "Custom Styling", href: "/global/custom-styling" }, // global
|
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
|
||||||
{ title: "Conditional Logic", href: "/global/conditional-logic" }, // global
|
{ title: "Conditional Logic", href: "/global/conditional-logic" }, // global
|
||||||
{ title: "Custom Start & End Conditions", href: "/global/custom-start-end-conditions" }, // global
|
{ title: "Custom Start & End Conditions", href: "/global/custom-start-end-conditions" }, // global
|
||||||
{ title: "Recall Functionality", href: "/global/recall" }, // global
|
{ title: "Recall Functionality", href: "/global/recall" }, // global
|
||||||
@@ -59,7 +59,7 @@ export const navigation: Array<NavGroup> = [
|
|||||||
{ title: "Recontact Options", href: "/app-surveys/recontact" },
|
{ title: "Recontact Options", href: "/app-surveys/recontact" },
|
||||||
{ title: "Multi Language Surveys", href: "/global/multi-language-surveys" }, // global
|
{ title: "Multi Language Surveys", href: "/global/multi-language-surveys" }, // global
|
||||||
{ title: "User Metadata", href: "/global/metadata" }, // global
|
{ title: "User Metadata", href: "/global/metadata" }, // global
|
||||||
{ title: "Custom Styling", href: "/global/custom-styling" }, // global
|
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
|
||||||
{ title: "Conditional Logic", href: "/global/conditional-logic" }, // global
|
{ title: "Conditional Logic", href: "/global/conditional-logic" }, // global
|
||||||
{ title: "Custom Start & End Conditions", href: "/global/custom-start-end-conditions" }, // global
|
{ title: "Custom Start & End Conditions", href: "/global/custom-start-end-conditions" }, // global
|
||||||
{ title: "Recall Functionality", href: "/global/recall" }, // global
|
{ title: "Recall Functionality", href: "/global/recall" }, // global
|
||||||
@@ -84,7 +84,7 @@ export const navigation: Array<NavGroup> = [
|
|||||||
{ title: "Embed Surveys Anywhere", href: "/link-surveys/embed-surveys" },
|
{ title: "Embed Surveys Anywhere", href: "/link-surveys/embed-surveys" },
|
||||||
{ title: "Multi Language Surveys", href: "/global/multi-language-surveys" },
|
{ title: "Multi Language Surveys", href: "/global/multi-language-surveys" },
|
||||||
{ title: "User Metadata", href: "/global/metadata" },
|
{ title: "User Metadata", href: "/global/metadata" },
|
||||||
{ title: "Custom Styling", href: "/global/custom-styling" },
|
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
|
||||||
{ title: "Conditional Logic", href: "/global/conditional-logic" },
|
{ title: "Conditional Logic", href: "/global/conditional-logic" },
|
||||||
{ title: "Custom Start & End Conditions", href: "/global/custom-start-end-conditions" },
|
{ title: "Custom Start & End Conditions", href: "/global/custom-start-end-conditions" },
|
||||||
{ title: "Recall Functionality", href: "/global/recall" },
|
{ title: "Recall Functionality", href: "/global/recall" },
|
||||||
@@ -95,6 +95,13 @@ export const navigation: Array<NavGroup> = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Core Features",
|
||||||
|
links: [
|
||||||
|
{ title: "Access Roles", href: "/global/access-roles" },
|
||||||
|
{ title: "Styling Theme", href: "/global/styling-theme" },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Self-Hosting",
|
title: "Self-Hosting",
|
||||||
links: [
|
links: [
|
||||||
|
|||||||
@@ -111,6 +111,11 @@ const nextConfig = {
|
|||||||
destination: "/developer-docs/integrations/:path",
|
destination: "/developer-docs/integrations/:path",
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/global/custom-styling",
|
||||||
|
destination: "/global/overwrite-styling",
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-mdx": "^3.0.1",
|
"remark-mdx": "^3.0.1",
|
||||||
"schema-dts": "^1.1.2",
|
"schema-dts": "^1.1.2",
|
||||||
"sharp": "^0.33.3",
|
"sharp": "^0.33.4",
|
||||||
"shiki": "^0.14.7",
|
"shiki": "^0.14.7",
|
||||||
"simple-functional-loader": "^1.2.1",
|
"simple-functional-loader": "^1.2.1",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { redirect } from "next/navigation";
|
|||||||
import { authOptions } from "@formbricks/lib/authOptions";
|
import { authOptions } from "@formbricks/lib/authOptions";
|
||||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
import { AuthorizationError } from "@formbricks/types/errors";
|
||||||
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
|
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
|
||||||
import { ToasterClient } from "@formbricks/ui/ToasterClient";
|
import { ToasterClient } from "@formbricks/ui/ToasterClient";
|
||||||
@@ -22,9 +22,9 @@ const EnvLayout = async ({ children, params }) => {
|
|||||||
throw new AuthorizationError("Not authorized");
|
throw new AuthorizationError("Not authorized");
|
||||||
}
|
}
|
||||||
|
|
||||||
const team = await getTeamByEnvironmentId(params.environmentId);
|
const organization = await getOrganizationByEnvironmentId(params.environmentId);
|
||||||
if (!team) {
|
if (!organization) {
|
||||||
throw new Error("Team not found");
|
throw new Error("Organization not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const environment = await getEnvironment(params.environmentId);
|
const environment = await getEnvironment(params.environmentId);
|
||||||
@@ -39,11 +39,11 @@ const EnvLayout = async ({ children, params }) => {
|
|||||||
<PosthogIdentify
|
<PosthogIdentify
|
||||||
session={session}
|
session={session}
|
||||||
environmentId={params.environmentId}
|
environmentId={params.environmentId}
|
||||||
teamId={team.id}
|
organizationId={organization.id}
|
||||||
teamName={team.name}
|
organizationName={organization.name}
|
||||||
inAppSurveyBillingStatus={team.billing.features.inAppSurvey.status}
|
inAppSurveyBillingStatus={organization.billing.features.inAppSurvey.status}
|
||||||
linkSurveyBillingStatus={team.billing.features.linkSurvey.status}
|
linkSurveyBillingStatus={organization.billing.features.linkSurvey.status}
|
||||||
userTargetingBillingStatus={team.billing.features.userTargeting.status}
|
userTargetingBillingStatus={organization.billing.features.userTargeting.status}
|
||||||
/>
|
/>
|
||||||
<FormbricksClient session={session} />
|
<FormbricksClient session={session} />
|
||||||
<ToasterClient />
|
<ToasterClient />
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { PlusIcon, TrashIcon } from "lucide-react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys";
|
import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys";
|
||||||
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
|
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
|
||||||
import { Button } from "@formbricks/ui/Button";
|
import { Button } from "@formbricks/ui/Button";
|
||||||
@@ -18,6 +19,7 @@ interface AddressQuestionFormProps {
|
|||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
selectedLanguageCode: string;
|
selectedLanguageCode: string;
|
||||||
setSelectedLanguageCode: (language: string) => void;
|
setSelectedLanguageCode: (language: string) => void;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddressQuestionForm = ({
|
export const AddressQuestionForm = ({
|
||||||
@@ -28,6 +30,7 @@ export const AddressQuestionForm = ({
|
|||||||
localSurvey,
|
localSurvey,
|
||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
|
attributeClasses,
|
||||||
}: AddressQuestionFormProps): JSX.Element => {
|
}: AddressQuestionFormProps): JSX.Element => {
|
||||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
|
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
|
||||||
@@ -43,6 +46,7 @@ export const AddressQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -58,6 +62,7 @@ export const AddressQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||||
|
|
||||||
import { LogicEditor } from "./LogicEditor";
|
import { LogicEditor } from "./LogicEditor";
|
||||||
@@ -8,6 +9,7 @@ interface AdvancedSettingsProps {
|
|||||||
questionIdx: number;
|
questionIdx: number;
|
||||||
localSurvey: TSurvey;
|
localSurvey: TSurvey;
|
||||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AdvancedSettings = ({
|
export const AdvancedSettings = ({
|
||||||
@@ -15,6 +17,7 @@ export const AdvancedSettings = ({
|
|||||||
questionIdx,
|
questionIdx,
|
||||||
localSurvey,
|
localSurvey,
|
||||||
updateQuestion,
|
updateQuestion,
|
||||||
|
attributeClasses,
|
||||||
}: AdvancedSettingsProps) => {
|
}: AdvancedSettingsProps) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -24,6 +27,7 @@ export const AdvancedSettings = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
localSurvey={localSurvey}
|
localSurvey={localSurvey}
|
||||||
questionIdx={questionIdx}
|
questionIdx={questionIdx}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys";
|
import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys";
|
||||||
import { Input } from "@formbricks/ui/Input";
|
import { Input } from "@formbricks/ui/Input";
|
||||||
import { Label } from "@formbricks/ui/Label";
|
import { Label } from "@formbricks/ui/Label";
|
||||||
@@ -18,6 +19,7 @@ interface CTAQuestionFormProps {
|
|||||||
selectedLanguageCode: string;
|
selectedLanguageCode: string;
|
||||||
setSelectedLanguageCode: (languageCode: string) => void;
|
setSelectedLanguageCode: (languageCode: string) => void;
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CTAQuestionForm = ({
|
export const CTAQuestionForm = ({
|
||||||
@@ -29,6 +31,7 @@ export const CTAQuestionForm = ({
|
|||||||
localSurvey,
|
localSurvey,
|
||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
|
attributeClasses,
|
||||||
}: CTAQuestionFormProps): JSX.Element => {
|
}: CTAQuestionFormProps): JSX.Element => {
|
||||||
const [firstRender, setFirstRender] = useState(true);
|
const [firstRender, setFirstRender] = useState(true);
|
||||||
|
|
||||||
@@ -43,6 +46,7 @@ export const CTAQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
@@ -95,6 +99,7 @@ export const CTAQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{questionIdx !== 0 && (
|
{questionIdx !== 0 && (
|
||||||
@@ -109,6 +114,7 @@ export const CTAQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -143,6 +149,7 @@ export const CTAQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { PlusIcon, TrashIcon } from "lucide-react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys";
|
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys";
|
||||||
import { Button } from "@formbricks/ui/Button";
|
import { Button } from "@formbricks/ui/Button";
|
||||||
import { Input } from "@formbricks/ui/Input";
|
import { Input } from "@formbricks/ui/Input";
|
||||||
@@ -17,6 +18,7 @@ interface CalQuestionFormProps {
|
|||||||
selectedLanguageCode: string;
|
selectedLanguageCode: string;
|
||||||
setSelectedLanguageCode: (language: string) => void;
|
setSelectedLanguageCode: (language: string) => void;
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CalQuestionForm = ({
|
export const CalQuestionForm = ({
|
||||||
@@ -27,6 +29,7 @@ export const CalQuestionForm = ({
|
|||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
isInvalid,
|
isInvalid,
|
||||||
|
attributeClasses,
|
||||||
}: CalQuestionFormProps): JSX.Element => {
|
}: CalQuestionFormProps): JSX.Element => {
|
||||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||||
@@ -42,6 +45,7 @@ export const CalQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
{showSubheader && (
|
{showSubheader && (
|
||||||
@@ -56,6 +60,7 @@ export const CalQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys";
|
import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys";
|
||||||
import { Label } from "@formbricks/ui/Label";
|
import { Label } from "@formbricks/ui/Label";
|
||||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||||
@@ -15,6 +16,7 @@ interface ConsentQuestionFormProps {
|
|||||||
selectedLanguageCode: string;
|
selectedLanguageCode: string;
|
||||||
setSelectedLanguageCode: (languageCode: string) => void;
|
setSelectedLanguageCode: (languageCode: string) => void;
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConsentQuestionForm = ({
|
export const ConsentQuestionForm = ({
|
||||||
@@ -25,6 +27,7 @@ export const ConsentQuestionForm = ({
|
|||||||
localSurvey,
|
localSurvey,
|
||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
|
attributeClasses,
|
||||||
}: ConsentQuestionFormProps): JSX.Element => {
|
}: ConsentQuestionFormProps): JSX.Element => {
|
||||||
const [firstRender, setFirstRender] = useState(true);
|
const [firstRender, setFirstRender] = useState(true);
|
||||||
|
|
||||||
@@ -39,6 +42,7 @@ export const ConsentQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
@@ -70,6 +74,7 @@ export const ConsentQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { PlusIcon, TrashIcon } from "lucide-react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys";
|
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys";
|
||||||
import { Button } from "@formbricks/ui/Button";
|
import { Button } from "@formbricks/ui/Button";
|
||||||
import { Label } from "@formbricks/ui/Label";
|
import { Label } from "@formbricks/ui/Label";
|
||||||
@@ -17,6 +18,7 @@ interface IDateQuestionFormProps {
|
|||||||
selectedLanguageCode: string;
|
selectedLanguageCode: string;
|
||||||
setSelectedLanguageCode: (language: string) => void;
|
setSelectedLanguageCode: (language: string) => void;
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateOptions = [
|
const dateOptions = [
|
||||||
@@ -42,6 +44,7 @@ export const DateQuestionForm = ({
|
|||||||
localSurvey,
|
localSurvey,
|
||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
|
attributeClasses,
|
||||||
}: IDateQuestionFormProps): JSX.Element => {
|
}: IDateQuestionFormProps): JSX.Element => {
|
||||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||||
@@ -57,6 +60,7 @@ export const DateQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
{showSubheader && (
|
{showSubheader && (
|
||||||
@@ -71,6 +75,7 @@ export const DateQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useState } from "react";
|
|||||||
|
|
||||||
import { cn } from "@formbricks/lib/cn";
|
import { cn } from "@formbricks/lib/cn";
|
||||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TSurvey } from "@formbricks/types/surveys";
|
import { TSurvey } from "@formbricks/types/surveys";
|
||||||
import { Input } from "@formbricks/ui/Input";
|
import { Input } from "@formbricks/ui/Input";
|
||||||
import { Label } from "@formbricks/ui/Label";
|
import { Label } from "@formbricks/ui/Label";
|
||||||
@@ -19,6 +20,7 @@ interface EditThankYouCardProps {
|
|||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
selectedLanguageCode: string;
|
selectedLanguageCode: string;
|
||||||
setSelectedLanguageCode: (languageCode: string) => void;
|
setSelectedLanguageCode: (languageCode: string) => void;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditThankYouCard = ({
|
export const EditThankYouCard = ({
|
||||||
@@ -29,6 +31,7 @@ export const EditThankYouCard = ({
|
|||||||
isInvalid,
|
isInvalid,
|
||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
|
attributeClasses,
|
||||||
}: EditThankYouCardProps) => {
|
}: EditThankYouCardProps) => {
|
||||||
// const [open, setOpen] = useState(false);
|
// const [open, setOpen] = useState(false);
|
||||||
let open = activeQuestionId == "end";
|
let open = activeQuestionId == "end";
|
||||||
@@ -117,6 +120,7 @@ export const EditThankYouCard = ({
|
|||||||
updateSurvey={updateSurvey}
|
updateSurvey={updateSurvey}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<QuestionFormInput
|
<QuestionFormInput
|
||||||
@@ -128,6 +132,7 @@ export const EditThankYouCard = ({
|
|||||||
updateSurvey={updateSurvey}
|
updateSurvey={updateSurvey}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
@@ -170,6 +175,7 @@ export const EditThankYouCard = ({
|
|||||||
updateSurvey={updateSurvey}
|
updateSurvey={updateSurvey}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useState } from "react";
|
|||||||
|
|
||||||
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
import { cn } from "@formbricks/lib/cn";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TSurvey } from "@formbricks/types/surveys";
|
import { TSurvey } from "@formbricks/types/surveys";
|
||||||
import { FileInput } from "@formbricks/ui/FileInput";
|
import { FileInput } from "@formbricks/ui/FileInput";
|
||||||
import { Label } from "@formbricks/ui/Label";
|
import { Label } from "@formbricks/ui/Label";
|
||||||
@@ -20,6 +21,7 @@ interface EditWelcomeCardProps {
|
|||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
selectedLanguageCode: string;
|
selectedLanguageCode: string;
|
||||||
setSelectedLanguageCode: (languageCode: string) => void;
|
setSelectedLanguageCode: (languageCode: string) => void;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditWelcomeCard = ({
|
export const EditWelcomeCard = ({
|
||||||
@@ -30,6 +32,7 @@ export const EditWelcomeCard = ({
|
|||||||
isInvalid,
|
isInvalid,
|
||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
|
attributeClasses,
|
||||||
}: EditWelcomeCardProps) => {
|
}: EditWelcomeCardProps) => {
|
||||||
const [firstRender, setFirstRender] = useState(true);
|
const [firstRender, setFirstRender] = useState(true);
|
||||||
const path = usePathname();
|
const path = usePathname();
|
||||||
@@ -130,6 +133,7 @@ export const EditWelcomeCard = ({
|
|||||||
updateSurvey={updateSurvey}
|
updateSurvey={updateSurvey}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
@@ -164,6 +168,7 @@ export const EditWelcomeCard = ({
|
|||||||
updateSurvey={updateSurvey}
|
updateSurvey={updateSurvey}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { toast } from "react-hot-toast";
|
|||||||
|
|
||||||
import { extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
import { extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||||
import { createI18nString } from "@formbricks/lib/i18n/utils";
|
import { createI18nString } from "@formbricks/lib/i18n/utils";
|
||||||
import { useGetBillingInfo } from "@formbricks/lib/team/hooks/useGetBillingInfo";
|
import { useGetBillingInfo } from "@formbricks/lib/organization/hooks/useGetBillingInfo";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/common";
|
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/common";
|
||||||
import { TProduct } from "@formbricks/types/product";
|
import { TProduct } from "@formbricks/types/product";
|
||||||
import { TSurvey, TSurveyFileUploadQuestion } from "@formbricks/types/surveys";
|
import { TSurvey, TSurveyFileUploadQuestion } from "@formbricks/types/surveys";
|
||||||
@@ -25,6 +26,7 @@ interface FileUploadFormProps {
|
|||||||
selectedLanguageCode: string;
|
selectedLanguageCode: string;
|
||||||
setSelectedLanguageCode: (languageCode: string) => void;
|
setSelectedLanguageCode: (languageCode: string) => void;
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileUploadQuestionForm = ({
|
export const FileUploadQuestionForm = ({
|
||||||
@@ -36,6 +38,7 @@ export const FileUploadQuestionForm = ({
|
|||||||
product,
|
product,
|
||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
|
attributeClasses,
|
||||||
}: FileUploadFormProps): JSX.Element => {
|
}: FileUploadFormProps): JSX.Element => {
|
||||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||||
const [extension, setExtension] = useState("");
|
const [extension, setExtension] = useState("");
|
||||||
@@ -43,7 +46,7 @@ export const FileUploadQuestionForm = ({
|
|||||||
billingInfo,
|
billingInfo,
|
||||||
error: billingInfoError,
|
error: billingInfoError,
|
||||||
isLoading: billingInfoLoading,
|
isLoading: billingInfoLoading,
|
||||||
} = useGetBillingInfo(product?.teamId ?? "");
|
} = useGetBillingInfo(product?.organizationId ?? "");
|
||||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||||
|
|
||||||
const handleInputChange = (event) => {
|
const handleInputChange = (event) => {
|
||||||
@@ -121,6 +124,7 @@ export const FileUploadQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
{showSubheader && (
|
{showSubheader && (
|
||||||
@@ -135,6 +139,7 @@ export const FileUploadQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -171,7 +176,7 @@ export const FileUploadQuestionForm = ({
|
|||||||
onToggle={() => updateQuestion(questionIdx, { allowMultipleFiles: !question.allowMultipleFiles })}
|
onToggle={() => updateQuestion(questionIdx, { allowMultipleFiles: !question.allowMultipleFiles })}
|
||||||
htmlId="allowMultipleFile"
|
htmlId="allowMultipleFile"
|
||||||
title="Allow Multiple Files"
|
title="Allow Multiple Files"
|
||||||
description="Let people upload up to 10 files at the same time."
|
description="Let people upload up to 25 files at the same time."
|
||||||
childBorder
|
childBorder
|
||||||
customContainerClass="p-0"></AdvancedOptionToggle>
|
customContainerClass="p-0"></AdvancedOptionToggle>
|
||||||
|
|
||||||
|
|||||||
@@ -94,18 +94,18 @@ export const HiddenFieldsCard = ({
|
|||||||
<Collapsible.CollapsibleContent className="px-4 pb-6">
|
<Collapsible.CollapsibleContent className="px-4 pb-6">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{localSurvey.hiddenFields?.fieldIds && localSurvey.hiddenFields?.fieldIds?.length > 0 ? (
|
{localSurvey.hiddenFields?.fieldIds && localSurvey.hiddenFields?.fieldIds?.length > 0 ? (
|
||||||
localSurvey.hiddenFields?.fieldIds?.map((question) => {
|
localSurvey.hiddenFields?.fieldIds?.map((fieldId) => {
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
key={question}
|
key={fieldId}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
updateSurvey({
|
updateSurvey({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== question),
|
fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== fieldId),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
tagId={question}
|
tagId={fieldId}
|
||||||
tagName={question}
|
tagName={fieldId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import { toast } from "react-hot-toast";
|
|||||||
|
|
||||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import {
|
import {
|
||||||
TSurvey,
|
TSurvey,
|
||||||
TSurveyLogic,
|
TSurveyLogic,
|
||||||
@@ -36,6 +37,7 @@ interface LogicEditorProps {
|
|||||||
questionIdx: number;
|
questionIdx: number;
|
||||||
question: TSurveyQuestion;
|
question: TSurveyQuestion;
|
||||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogicConditions = {
|
type LogicConditions = {
|
||||||
@@ -47,11 +49,17 @@ type LogicConditions = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LogicEditor = ({ localSurvey, question, questionIdx, updateQuestion }: LogicEditorProps) => {
|
export const LogicEditor = ({
|
||||||
|
localSurvey,
|
||||||
|
question,
|
||||||
|
questionIdx,
|
||||||
|
updateQuestion,
|
||||||
|
attributeClasses,
|
||||||
|
}: LogicEditorProps) => {
|
||||||
const [searchValue, setSearchValue] = useState<string>("");
|
const [searchValue, setSearchValue] = useState<string>("");
|
||||||
localSurvey = useMemo(() => {
|
localSurvey = useMemo(() => {
|
||||||
return checkForRecallInHeadline(localSurvey, "default");
|
return replaceHeadlineRecall(localSurvey, "default", attributeClasses);
|
||||||
}, [localSurvey]);
|
}, [localSurvey, attributeClasses]);
|
||||||
|
|
||||||
const questionValues = useMemo(() => {
|
const questionValues = useMemo(() => {
|
||||||
if ("choices" in question) {
|
if ("choices" in question) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useState } from "react";
|
|||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
import { createI18nString, extractLanguageCodes, getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes, getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys";
|
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys";
|
||||||
import { Button } from "@formbricks/ui/Button";
|
import { Button } from "@formbricks/ui/Button";
|
||||||
import { Label } from "@formbricks/ui/Label";
|
import { Label } from "@formbricks/ui/Label";
|
||||||
@@ -21,6 +22,7 @@ interface MatrixQuestionFormProps {
|
|||||||
selectedLanguageCode: string;
|
selectedLanguageCode: string;
|
||||||
setSelectedLanguageCode: (language: string) => void;
|
setSelectedLanguageCode: (language: string) => void;
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MatrixQuestionForm = ({
|
export const MatrixQuestionForm = ({
|
||||||
@@ -31,6 +33,7 @@ export const MatrixQuestionForm = ({
|
|||||||
localSurvey,
|
localSurvey,
|
||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
|
attributeClasses,
|
||||||
}: MatrixQuestionFormProps): JSX.Element => {
|
}: MatrixQuestionFormProps): JSX.Element => {
|
||||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||||
const languageCodes = extractLanguageCodes(localSurvey.languages);
|
const languageCodes = extractLanguageCodes(localSurvey.languages);
|
||||||
@@ -110,6 +113,7 @@ export const MatrixQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
{showSubheader && (
|
{showSubheader && (
|
||||||
@@ -124,6 +128,7 @@ export const MatrixQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -168,6 +173,7 @@ export const MatrixQuestionForm = ({
|
|||||||
isInvalid={
|
isInvalid={
|
||||||
isInvalid && !isLabelValidForAllLanguages(question.rows[index], localSurvey.languages)
|
isInvalid && !isLabelValidForAllLanguages(question.rows[index], localSurvey.languages)
|
||||||
}
|
}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
{question.rows.length > 2 && (
|
{question.rows.length > 2 && (
|
||||||
<TrashIcon
|
<TrashIcon
|
||||||
@@ -209,6 +215,7 @@ export const MatrixQuestionForm = ({
|
|||||||
isInvalid={
|
isInvalid={
|
||||||
isInvalid && !isLabelValidForAllLanguages(question.columns[index], localSurvey.languages)
|
isInvalid && !isLabelValidForAllLanguages(question.columns[index], localSurvey.languages)
|
||||||
}
|
}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
{question.columns.length > 2 && (
|
{question.columns.length > 2 && (
|
||||||
<TrashIcon
|
<TrashIcon
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
|
|
||||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import {
|
import {
|
||||||
TI18nString,
|
TI18nString,
|
||||||
TShuffleOption,
|
TShuffleOption,
|
||||||
@@ -31,6 +32,7 @@ interface OpenQuestionFormProps {
|
|||||||
selectedLanguageCode: string;
|
selectedLanguageCode: string;
|
||||||
setSelectedLanguageCode: (language: string) => void;
|
setSelectedLanguageCode: (language: string) => void;
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MultipleChoiceQuestionForm = ({
|
export const MultipleChoiceQuestionForm = ({
|
||||||
@@ -41,6 +43,7 @@ export const MultipleChoiceQuestionForm = ({
|
|||||||
localSurvey,
|
localSurvey,
|
||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
|
attributeClasses,
|
||||||
}: OpenQuestionFormProps): JSX.Element => {
|
}: OpenQuestionFormProps): JSX.Element => {
|
||||||
const lastChoiceRef = useRef<HTMLInputElement>(null);
|
const lastChoiceRef = useRef<HTMLInputElement>(null);
|
||||||
const [isNew, setIsNew] = useState(true);
|
const [isNew, setIsNew] = useState(true);
|
||||||
@@ -190,6 +193,7 @@ export const MultipleChoiceQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -205,6 +209,7 @@ export const MultipleChoiceQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -283,6 +288,7 @@ export const MultipleChoiceQuestionForm = ({
|
|||||||
question={question}
|
question={question}
|
||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
surveyLanguageCodes={surveyLanguageCodes}
|
surveyLanguageCodes={surveyLanguageCodes}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { PlusIcon, TrashIcon } from "lucide-react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TSurvey, TSurveyNPSQuestion } from "@formbricks/types/surveys";
|
import { TSurvey, TSurveyNPSQuestion } from "@formbricks/types/surveys";
|
||||||
import { Button } from "@formbricks/ui/Button";
|
import { Button } from "@formbricks/ui/Button";
|
||||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||||
@@ -17,6 +18,7 @@ interface NPSQuestionFormProps {
|
|||||||
selectedLanguageCode: string;
|
selectedLanguageCode: string;
|
||||||
setSelectedLanguageCode: (languageCode: string) => void;
|
setSelectedLanguageCode: (languageCode: string) => void;
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NPSQuestionForm = ({
|
export const NPSQuestionForm = ({
|
||||||
@@ -28,6 +30,7 @@ export const NPSQuestionForm = ({
|
|||||||
localSurvey,
|
localSurvey,
|
||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
|
attributeClasses,
|
||||||
}: NPSQuestionFormProps): JSX.Element => {
|
}: NPSQuestionFormProps): JSX.Element => {
|
||||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||||
@@ -42,6 +45,7 @@ export const NPSQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -57,6 +61,7 @@ export const NPSQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -99,6 +104,7 @@ export const NPSQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -111,6 +117,7 @@ export const NPSQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,6 +135,7 @@ export const NPSQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import {
|
import {
|
||||||
TSurvey,
|
TSurvey,
|
||||||
TSurveyOpenTextQuestion,
|
TSurveyOpenTextQuestion,
|
||||||
@@ -39,6 +40,7 @@ interface OpenQuestionFormProps {
|
|||||||
selectedLanguageCode: string;
|
selectedLanguageCode: string;
|
||||||
setSelectedLanguageCode: (language: string) => void;
|
setSelectedLanguageCode: (language: string) => void;
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OpenQuestionForm = ({
|
export const OpenQuestionForm = ({
|
||||||
@@ -49,6 +51,7 @@ export const OpenQuestionForm = ({
|
|||||||
localSurvey,
|
localSurvey,
|
||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
|
attributeClasses,
|
||||||
}: OpenQuestionFormProps): JSX.Element => {
|
}: OpenQuestionFormProps): JSX.Element => {
|
||||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||||
const defaultPlaceholder = getPlaceholderByInputType(question.inputType ?? "text");
|
const defaultPlaceholder = getPlaceholderByInputType(question.inputType ?? "text");
|
||||||
@@ -73,6 +76,7 @@ export const OpenQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -88,6 +92,7 @@ export const OpenQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -131,6 +136,7 @@ export const OpenQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
|||||||
|
|
||||||
import { cn } from "@formbricks/lib/cn";
|
import { cn } from "@formbricks/lib/cn";
|
||||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TSurvey, TSurveyPictureSelectionQuestion } from "@formbricks/types/surveys";
|
import { TSurvey, TSurveyPictureSelectionQuestion } from "@formbricks/types/surveys";
|
||||||
import { Button } from "@formbricks/ui/Button";
|
import { Button } from "@formbricks/ui/Button";
|
||||||
import { FileInput } from "@formbricks/ui/FileInput";
|
import { FileInput } from "@formbricks/ui/FileInput";
|
||||||
@@ -20,6 +21,7 @@ interface PictureSelectionFormProps {
|
|||||||
selectedLanguageCode: string;
|
selectedLanguageCode: string;
|
||||||
setSelectedLanguageCode: (language: string) => void;
|
setSelectedLanguageCode: (language: string) => void;
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PictureSelectionForm = ({
|
export const PictureSelectionForm = ({
|
||||||
@@ -30,6 +32,7 @@ export const PictureSelectionForm = ({
|
|||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
isInvalid,
|
isInvalid,
|
||||||
|
attributeClasses,
|
||||||
}: PictureSelectionFormProps): JSX.Element => {
|
}: PictureSelectionFormProps): JSX.Element => {
|
||||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||||
const environmentId = localSurvey.environmentId;
|
const environmentId = localSurvey.environmentId;
|
||||||
@@ -46,6 +49,7 @@ export const PictureSelectionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
{showSubheader && (
|
{showSubheader && (
|
||||||
@@ -60,6 +64,7 @@ export const PictureSelectionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { useState } from "react";
|
|||||||
|
|
||||||
import { cn } from "@formbricks/lib/cn";
|
import { cn } from "@formbricks/lib/cn";
|
||||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TProduct } from "@formbricks/types/product";
|
import { TProduct } from "@formbricks/types/product";
|
||||||
import { TI18nString, TSurvey, TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
|
import { TI18nString, TSurvey, TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||||
import { Label } from "@formbricks/ui/Label";
|
import { Label } from "@formbricks/ui/Label";
|
||||||
@@ -62,6 +63,7 @@ interface QuestionCardProps {
|
|||||||
selectedLanguageCode: string;
|
selectedLanguageCode: string;
|
||||||
setSelectedLanguageCode: (language: string) => void;
|
setSelectedLanguageCode: (language: string) => void;
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QuestionCard = ({
|
export const QuestionCard = ({
|
||||||
@@ -79,6 +81,7 @@ export const QuestionCard = ({
|
|||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
isInvalid,
|
isInvalid,
|
||||||
|
attributeClasses,
|
||||||
}: QuestionCardProps) => {
|
}: QuestionCardProps) => {
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||||
id: question.id,
|
id: question.id,
|
||||||
@@ -213,13 +216,21 @@ export const QuestionCard = ({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold">
|
<p className="text-sm font-semibold">
|
||||||
{recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
|
{recallToHeadline(
|
||||||
selectedLanguageCode
|
question.headline,
|
||||||
]
|
localSurvey,
|
||||||
|
true,
|
||||||
|
selectedLanguageCode,
|
||||||
|
attributeClasses
|
||||||
|
)[selectedLanguageCode]
|
||||||
? formatTextWithSlashes(
|
? formatTextWithSlashes(
|
||||||
recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
|
recallToHeadline(
|
||||||
selectedLanguageCode
|
question.headline,
|
||||||
] ?? ""
|
localSurvey,
|
||||||
|
true,
|
||||||
|
selectedLanguageCode,
|
||||||
|
attributeClasses
|
||||||
|
)[selectedLanguageCode] ?? ""
|
||||||
)
|
)
|
||||||
: getTSurveyQuestionTypeName(question.type)}
|
: getTSurveyQuestionTypeName(question.type)}
|
||||||
</p>
|
</p>
|
||||||
@@ -251,6 +262,7 @@ export const QuestionCard = ({
|
|||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
|
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
|
||||||
<MultipleChoiceQuestionForm
|
<MultipleChoiceQuestionForm
|
||||||
@@ -262,6 +274,7 @@ export const QuestionCard = ({
|
|||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
|
) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
|
||||||
<MultipleChoiceQuestionForm
|
<MultipleChoiceQuestionForm
|
||||||
@@ -273,6 +286,7 @@ export const QuestionCard = ({
|
|||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
) : question.type === TSurveyQuestionType.NPS ? (
|
) : question.type === TSurveyQuestionType.NPS ? (
|
||||||
<NPSQuestionForm
|
<NPSQuestionForm
|
||||||
@@ -284,6 +298,7 @@ export const QuestionCard = ({
|
|||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
) : question.type === TSurveyQuestionType.CTA ? (
|
) : question.type === TSurveyQuestionType.CTA ? (
|
||||||
<CTAQuestionForm
|
<CTAQuestionForm
|
||||||
@@ -295,6 +310,7 @@ export const QuestionCard = ({
|
|||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
) : question.type === TSurveyQuestionType.Rating ? (
|
) : question.type === TSurveyQuestionType.Rating ? (
|
||||||
<RatingQuestionForm
|
<RatingQuestionForm
|
||||||
@@ -306,6 +322,7 @@ export const QuestionCard = ({
|
|||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
) : question.type === TSurveyQuestionType.Consent ? (
|
) : question.type === TSurveyQuestionType.Consent ? (
|
||||||
<ConsentQuestionForm
|
<ConsentQuestionForm
|
||||||
@@ -316,6 +333,7 @@ export const QuestionCard = ({
|
|||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
) : question.type === TSurveyQuestionType.Date ? (
|
) : question.type === TSurveyQuestionType.Date ? (
|
||||||
<DateQuestionForm
|
<DateQuestionForm
|
||||||
@@ -327,6 +345,7 @@ export const QuestionCard = ({
|
|||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
) : question.type === TSurveyQuestionType.PictureSelection ? (
|
) : question.type === TSurveyQuestionType.PictureSelection ? (
|
||||||
<PictureSelectionForm
|
<PictureSelectionForm
|
||||||
@@ -338,6 +357,7 @@ export const QuestionCard = ({
|
|||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
) : question.type === TSurveyQuestionType.FileUpload ? (
|
) : question.type === TSurveyQuestionType.FileUpload ? (
|
||||||
<FileUploadQuestionForm
|
<FileUploadQuestionForm
|
||||||
@@ -350,6 +370,7 @@ export const QuestionCard = ({
|
|||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
) : question.type === TSurveyQuestionType.Cal ? (
|
) : question.type === TSurveyQuestionType.Cal ? (
|
||||||
<CalQuestionForm
|
<CalQuestionForm
|
||||||
@@ -361,6 +382,7 @@ export const QuestionCard = ({
|
|||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
) : question.type === TSurveyQuestionType.Matrix ? (
|
) : question.type === TSurveyQuestionType.Matrix ? (
|
||||||
<MatrixQuestionForm
|
<MatrixQuestionForm
|
||||||
@@ -372,6 +394,7 @@ export const QuestionCard = ({
|
|||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
) : question.type === TSurveyQuestionType.Address ? (
|
) : question.type === TSurveyQuestionType.Address ? (
|
||||||
<AddressQuestionForm
|
<AddressQuestionForm
|
||||||
@@ -383,6 +406,7 @@ export const QuestionCard = ({
|
|||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
@@ -423,6 +447,7 @@ export const QuestionCard = ({
|
|||||||
if (questionIdx === localSurvey.questions.length - 1) return;
|
if (questionIdx === localSurvey.questions.length - 1) return;
|
||||||
updateEmptyNextButtonLabels(translatedNextButtonLabel);
|
updateEmptyNextButtonLabels(translatedNextButtonLabel);
|
||||||
}}
|
}}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{questionIdx !== 0 && (
|
{questionIdx !== 0 && (
|
||||||
@@ -437,6 +462,7 @@ export const QuestionCard = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -456,6 +482,7 @@ export const QuestionCard = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -465,6 +492,7 @@ export const QuestionCard = ({
|
|||||||
questionIdx={questionIdx}
|
questionIdx={questionIdx}
|
||||||
localSurvey={localSurvey}
|
localSurvey={localSurvey}
|
||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</Collapsible.CollapsibleContent>
|
</Collapsible.CollapsibleContent>
|
||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||||
|
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TProduct } from "@formbricks/types/product";
|
import { TProduct } from "@formbricks/types/product";
|
||||||
import { TSurvey } from "@formbricks/types/surveys";
|
import { TSurvey } from "@formbricks/types/surveys";
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ interface QuestionsDraggableProps {
|
|||||||
setSelectedLanguageCode: (language: string) => void;
|
setSelectedLanguageCode: (language: string) => void;
|
||||||
invalidQuestions: string[] | null;
|
invalidQuestions: string[] | null;
|
||||||
internalQuestionIdMap: Record<string, string>;
|
internalQuestionIdMap: Record<string, string>;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QuestionsDroppable = ({
|
export const QuestionsDroppable = ({
|
||||||
@@ -33,6 +35,7 @@ export const QuestionsDroppable = ({
|
|||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
updateQuestion,
|
updateQuestion,
|
||||||
internalQuestionIdMap,
|
internalQuestionIdMap,
|
||||||
|
attributeClasses,
|
||||||
}: QuestionsDraggableProps) => {
|
}: QuestionsDraggableProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="group mb-5 grid w-full gap-5">
|
<div className="group mb-5 grid w-full gap-5">
|
||||||
@@ -54,6 +57,7 @@ export const QuestionsDroppable = ({
|
|||||||
setActiveQuestionId={setActiveQuestionId}
|
setActiveQuestionId={setActiveQuestionId}
|
||||||
lastQuestion={questionIdx === localSurvey.questions.length - 1}
|
lastQuestion={questionIdx === localSurvey.questions.length - 1}
|
||||||
isInvalid={invalidQuestions ? invalidQuestions.includes(question.id) : false}
|
isInvalid={invalidQuestions ? invalidQuestions.includes(question.id) : false}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
|
|||||||
@@ -16,10 +16,16 @@ import { MultiLanguageCard } from "@formbricks/ee/multiLanguage/components/Multi
|
|||||||
import { extractLanguageCodes, getLocalizedValue, translateQuestion } from "@formbricks/lib/i18n/utils";
|
import { extractLanguageCodes, getLocalizedValue, translateQuestion } from "@formbricks/lib/i18n/utils";
|
||||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||||
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
|
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TProduct } from "@formbricks/types/product";
|
import { TProduct } from "@formbricks/types/product";
|
||||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||||
|
|
||||||
import { isCardValid, validateQuestion, validateSurveyQuestionsInBatch } from "../lib/validation";
|
import {
|
||||||
|
findQuestionsWithCyclicLogic,
|
||||||
|
isCardValid,
|
||||||
|
validateQuestion,
|
||||||
|
validateSurveyQuestionsInBatch,
|
||||||
|
} from "../lib/validation";
|
||||||
import { AddQuestionButton } from "./AddQuestionButton";
|
import { AddQuestionButton } from "./AddQuestionButton";
|
||||||
import { EditThankYouCard } from "./EditThankYouCard";
|
import { EditThankYouCard } from "./EditThankYouCard";
|
||||||
import { EditWelcomeCard } from "./EditWelcomeCard";
|
import { EditWelcomeCard } from "./EditWelcomeCard";
|
||||||
@@ -38,6 +44,7 @@ interface QuestionsViewProps {
|
|||||||
setSelectedLanguageCode: (languageCode: string) => void;
|
setSelectedLanguageCode: (languageCode: string) => void;
|
||||||
isMultiLanguageAllowed?: boolean;
|
isMultiLanguageAllowed?: boolean;
|
||||||
isFormbricksCloud: boolean;
|
isFormbricksCloud: boolean;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QuestionsView = ({
|
export const QuestionsView = ({
|
||||||
@@ -52,6 +59,7 @@ export const QuestionsView = ({
|
|||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
isMultiLanguageAllowed,
|
isMultiLanguageAllowed,
|
||||||
isFormbricksCloud,
|
isFormbricksCloud,
|
||||||
|
attributeClasses,
|
||||||
}: QuestionsViewProps) => {
|
}: QuestionsViewProps) => {
|
||||||
const internalQuestionIdMap = useMemo(() => {
|
const internalQuestionIdMap = useMemo(() => {
|
||||||
return localSurvey.questions.reduce((acc, question) => {
|
return localSurvey.questions.reduce((acc, question) => {
|
||||||
@@ -89,8 +97,12 @@ export const QuestionsView = ({
|
|||||||
const isFirstQuestion = question.id === localSurvey.questions[0].id;
|
const isFirstQuestion = question.id === localSurvey.questions[0].id;
|
||||||
let temp = structuredClone(invalidQuestions);
|
let temp = structuredClone(invalidQuestions);
|
||||||
if (validateQuestion(question, surveyLanguages, isFirstQuestion)) {
|
if (validateQuestion(question, surveyLanguages, isFirstQuestion)) {
|
||||||
temp = invalidQuestions.filter((id) => id !== question.id);
|
// If question is valid, we now check for cyclic logic
|
||||||
setInvalidQuestions(temp);
|
const questionsWithCyclicLogic = findQuestionsWithCyclicLogic(localSurvey.questions);
|
||||||
|
if (!questionsWithCyclicLogic.includes(question.id)) {
|
||||||
|
temp = invalidQuestions.filter((id) => id !== question.id);
|
||||||
|
setInvalidQuestions(temp);
|
||||||
|
}
|
||||||
} else if (!invalidQuestions.includes(question.id)) {
|
} else if (!invalidQuestions.includes(question.id)) {
|
||||||
temp.push(question.id);
|
temp.push(question.id);
|
||||||
setInvalidQuestions(temp);
|
setInvalidQuestions(temp);
|
||||||
@@ -330,6 +342,7 @@ export const QuestionsView = ({
|
|||||||
isInvalid={invalidQuestions ? invalidQuestions.includes("start") : false}
|
isInvalid={invalidQuestions ? invalidQuestions.includes("start") : false}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -347,6 +360,7 @@ export const QuestionsView = ({
|
|||||||
setActiveQuestionId={setActiveQuestionId}
|
setActiveQuestionId={setActiveQuestionId}
|
||||||
invalidQuestions={invalidQuestions}
|
invalidQuestions={invalidQuestions}
|
||||||
internalQuestionIdMap={internalQuestionIdMap}
|
internalQuestionIdMap={internalQuestionIdMap}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
|
||||||
@@ -360,6 +374,7 @@ export const QuestionsView = ({
|
|||||||
isInvalid={invalidQuestions ? invalidQuestions.includes("end") : false}
|
isInvalid={invalidQuestions ? invalidQuestions.includes("end") : false}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{localSurvey.type === "link" ? (
|
{localSurvey.type === "link" ? (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { HashIcon, PlusIcon, SmileIcon, StarIcon, TrashIcon } from "lucide-react
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import { TSurvey, TSurveyRatingQuestion } from "@formbricks/types/surveys";
|
import { TSurvey, TSurveyRatingQuestion } from "@formbricks/types/surveys";
|
||||||
import { Button } from "@formbricks/ui/Button";
|
import { Button } from "@formbricks/ui/Button";
|
||||||
import { Label } from "@formbricks/ui/Label";
|
import { Label } from "@formbricks/ui/Label";
|
||||||
@@ -18,6 +19,7 @@ interface RatingQuestionFormProps {
|
|||||||
selectedLanguageCode: string;
|
selectedLanguageCode: string;
|
||||||
setSelectedLanguageCode: (language: string) => void;
|
setSelectedLanguageCode: (language: string) => void;
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RatingQuestionForm = ({
|
export const RatingQuestionForm = ({
|
||||||
@@ -28,6 +30,7 @@ export const RatingQuestionForm = ({
|
|||||||
localSurvey,
|
localSurvey,
|
||||||
selectedLanguageCode,
|
selectedLanguageCode,
|
||||||
setSelectedLanguageCode,
|
setSelectedLanguageCode,
|
||||||
|
attributeClasses,
|
||||||
}: RatingQuestionFormProps) => {
|
}: RatingQuestionFormProps) => {
|
||||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||||
@@ -43,6 +46,7 @@ export const RatingQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -58,6 +62,7 @@ export const RatingQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,6 +140,7 @@ export const RatingQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -148,6 +154,7 @@ export const RatingQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,6 +172,7 @@ export const RatingQuestionForm = ({
|
|||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
selectedLanguageCode={selectedLanguageCode}
|
selectedLanguageCode={selectedLanguageCode}
|
||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import toast from "react-hot-toast";
|
|||||||
|
|
||||||
import { cn } from "@formbricks/lib/cn";
|
import { cn } from "@formbricks/lib/cn";
|
||||||
import { createI18nString } from "@formbricks/lib/i18n/utils";
|
import { createI18nString } from "@formbricks/lib/i18n/utils";
|
||||||
|
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||||
import {
|
import {
|
||||||
TI18nString,
|
TI18nString,
|
||||||
TSurvey,
|
TSurvey,
|
||||||
@@ -35,6 +36,7 @@ interface ChoiceProps {
|
|||||||
question: TSurveyMultipleChoiceQuestion;
|
question: TSurveyMultipleChoiceQuestion;
|
||||||
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyMultipleChoiceQuestion>) => void;
|
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyMultipleChoiceQuestion>) => void;
|
||||||
surveyLanguageCodes: string[];
|
surveyLanguageCodes: string[];
|
||||||
|
attributeClasses: TAttributeClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectQuestionChoice = ({
|
export const SelectQuestionChoice = ({
|
||||||
@@ -54,6 +56,7 @@ export const SelectQuestionChoice = ({
|
|||||||
question,
|
question,
|
||||||
surveyLanguageCodes,
|
surveyLanguageCodes,
|
||||||
updateQuestion,
|
updateQuestion,
|
||||||
|
attributeClasses,
|
||||||
}: ChoiceProps) => {
|
}: ChoiceProps) => {
|
||||||
const isDragDisabled = choice.id === "other";
|
const isDragDisabled = choice.id === "other";
|
||||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
||||||
@@ -100,6 +103,7 @@ export const SelectQuestionChoice = ({
|
|||||||
isInvalid && !isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
|
isInvalid && !isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
|
||||||
}
|
}
|
||||||
className={`${choice.id === "other" ? "border border-dashed" : ""} mt-0`}
|
className={`${choice.id === "other" ? "border border-dashed" : ""} mt-0`}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
{choice.id === "other" && (
|
{choice.id === "other" && (
|
||||||
<QuestionFormInput
|
<QuestionFormInput
|
||||||
@@ -119,6 +123,7 @@ export const SelectQuestionChoice = ({
|
|||||||
isInvalid && !isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
|
isInvalid && !isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
|
||||||
}
|
}
|
||||||
className="border border-dashed"
|
className="border border-dashed"
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export const SurveyEditor = ({
|
|||||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||||
isMultiLanguageAllowed={isMultiLanguageAllowed}
|
isMultiLanguageAllowed={isMultiLanguageAllowed}
|
||||||
isFormbricksCloud={isFormbricksCloud}
|
isFormbricksCloud={isFormbricksCloud}
|
||||||
|
attributeClasses={attributeClasses}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ export const TargetingCard = ({
|
|||||||
<UpgradePlanNotice
|
<UpgradePlanNotice
|
||||||
message="For advanced targeting, please"
|
message="For advanced targeting, please"
|
||||||
textForUrl="request an Enterprise license."
|
textForUrl="request an Enterprise license."
|
||||||
url="https://formbricks.com/docs/self-hosting/enterprise"
|
url={`/environments/${environmentId}/settings/enterprise`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -246,12 +246,13 @@ export const validateId = (
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Checks if there is a cycle present in the survey data logic.
|
// Checks if there is a cycle present in the survey data logic and returns all questions responsible for the cycle.
|
||||||
export const isSurveyLogicCyclic = (questions: TSurveyQuestions) => {
|
export const findQuestionsWithCyclicLogic = (questions: TSurveyQuestions): string[] => {
|
||||||
const visited: Record<string, boolean> = {};
|
const visited: Record<string, boolean> = {};
|
||||||
const recStack: Record<string, boolean> = {};
|
const recStack: Record<string, boolean> = {};
|
||||||
|
const cyclicQuestions: Set<string> = new Set();
|
||||||
|
|
||||||
const checkForCycle = (questionId: string) => {
|
const checkForCyclicLogic = (questionId: string): boolean => {
|
||||||
if (!visited[questionId]) {
|
if (!visited[questionId]) {
|
||||||
visited[questionId] = true;
|
visited[questionId] = true;
|
||||||
recStack[questionId] = true;
|
recStack[questionId] = true;
|
||||||
@@ -261,12 +262,14 @@ export const isSurveyLogicCyclic = (questions: TSurveyQuestions) => {
|
|||||||
for (const logic of question.logic) {
|
for (const logic of question.logic) {
|
||||||
const destination = logic.destination;
|
const destination = logic.destination;
|
||||||
if (!destination) {
|
if (!destination) {
|
||||||
return false;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!visited[destination] && checkForCycle(destination)) {
|
if (!visited[destination] && checkForCyclicLogic(destination)) {
|
||||||
|
cyclicQuestions.add(questionId);
|
||||||
return true;
|
return true;
|
||||||
} else if (recStack[destination]) {
|
} else if (recStack[destination]) {
|
||||||
|
cyclicQuestions.add(questionId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,7 +277,7 @@ export const isSurveyLogicCyclic = (questions: TSurveyQuestions) => {
|
|||||||
// Handle default behavior
|
// Handle default behavior
|
||||||
const nextQuestionIndex = questions.findIndex((question) => question.id === questionId) + 1;
|
const nextQuestionIndex = questions.findIndex((question) => question.id === questionId) + 1;
|
||||||
const nextQuestion = questions[nextQuestionIndex];
|
const nextQuestion = questions[nextQuestionIndex];
|
||||||
if (nextQuestion && !visited[nextQuestion.id] && checkForCycle(nextQuestion.id)) {
|
if (nextQuestion && !visited[nextQuestion.id] && checkForCyclicLogic(nextQuestion.id)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,12 +289,10 @@ export const isSurveyLogicCyclic = (questions: TSurveyQuestions) => {
|
|||||||
|
|
||||||
for (const question of questions) {
|
for (const question of questions) {
|
||||||
const questionId = question.id;
|
const questionId = question.id;
|
||||||
if (checkForCycle(questionId)) {
|
checkForCyclicLogic(questionId);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return Array.from(cyclicQuestions);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isSurveyValid = (
|
export const isSurveyValid = (
|
||||||
@@ -455,7 +456,9 @@ export const isSurveyValid = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detecting any cyclic dependencies in survey logic.
|
// Detecting any cyclic dependencies in survey logic.
|
||||||
if (isSurveyLogicCyclic(survey.questions)) {
|
const questionsWithCyclicLogic = findQuestionsWithCyclicLogic(survey.questions);
|
||||||
|
if (questionsWithCyclicLogic.length > 0) {
|
||||||
|
setInvalidQuestions(questionsWithCyclicLogic);
|
||||||
toast.error("Cyclic logic detected. Please fix it before saving.");
|
toast.error("Cyclic logic detected. Please fix it before saving.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
|||||||
import { authOptions } from "@formbricks/lib/authOptions";
|
import { authOptions } from "@formbricks/lib/authOptions";
|
||||||
import { IS_FORMBRICKS_CLOUD, SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
|
import { IS_FORMBRICKS_CLOUD, SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
|
||||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||||
|
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||||
import { getSegments } from "@formbricks/lib/segment/service";
|
import { getSegments } from "@formbricks/lib/segment/service";
|
||||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
|
||||||
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
||||||
|
|
||||||
import { SurveyEditor } from "./components/SurveyEditor";
|
import { SurveyEditor } from "./components/SurveyEditor";
|
||||||
@@ -32,7 +32,7 @@ const Page = async ({ params }) => {
|
|||||||
actionClasses,
|
actionClasses,
|
||||||
attributeClasses,
|
attributeClasses,
|
||||||
responseCount,
|
responseCount,
|
||||||
team,
|
organization,
|
||||||
session,
|
session,
|
||||||
segments,
|
segments,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
@@ -42,7 +42,7 @@ const Page = async ({ params }) => {
|
|||||||
getActionClasses(params.environmentId),
|
getActionClasses(params.environmentId),
|
||||||
getAttributeClasses(params.environmentId),
|
getAttributeClasses(params.environmentId),
|
||||||
getResponseCountBySurveyId(params.surveyId),
|
getResponseCountBySurveyId(params.surveyId),
|
||||||
getTeamByEnvironmentId(params.environmentId),
|
getOrganizationByEnvironmentId(params.environmentId),
|
||||||
getServerSession(authOptions),
|
getServerSession(authOptions),
|
||||||
getSegments(params.environmentId),
|
getSegments(params.environmentId),
|
||||||
]);
|
]);
|
||||||
@@ -51,16 +51,16 @@ const Page = async ({ params }) => {
|
|||||||
throw new Error("Session not found");
|
throw new Error("Session not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!team) {
|
if (!organization) {
|
||||||
throw new Error("Team not found");
|
throw new Error("Organization not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
|
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||||
const { isViewer } = getAccessFlags(currentUserMembership?.role);
|
const { isViewer } = getAccessFlags(currentUserMembership?.role);
|
||||||
const isSurveyCreationDeletionDisabled = isViewer;
|
const isSurveyCreationDeletionDisabled = isViewer;
|
||||||
|
|
||||||
const isUserTargetingAllowed = await getAdvancedTargetingPermission(team);
|
const isUserTargetingAllowed = await getAdvancedTargetingPermission(organization);
|
||||||
const isMultiLanguageAllowed = await getMultiLanguagePermission(team);
|
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!survey ||
|
!survey ||
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { createTeamAction } from "@/app/(app)/environments/[environmentId]/actions";
|
import { createOrganizationAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||||
import FormbricksLogo from "@/images/logo.svg";
|
import FormbricksLogo from "@/images/logo.svg";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -15,28 +15,28 @@ type FormValues = {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateFirstTeam = () => {
|
export const CreateFirstOrganization = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { register, handleSubmit } = useForm<FormValues>();
|
const { register, handleSubmit } = useForm<FormValues>();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [teamName, setTeamName] = useState("");
|
const [organizationName, setOrganizationName] = useState("");
|
||||||
const isTeamNameValid = teamName.trim() !== "";
|
const isOrganizationNameValid = organizationName.trim() !== "";
|
||||||
|
|
||||||
const onCreateTeam = async (data: FormValues) => {
|
const onCreateOrganization = async (data: FormValues) => {
|
||||||
data.name = data.name.trim();
|
data.name = data.name.trim();
|
||||||
if (!data.name) return;
|
if (!data.name) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const newTeam = await createTeamAction(data.name);
|
const newOrganization = await createOrganizationAction(data.name);
|
||||||
|
|
||||||
toast.success("Team created successfully!");
|
toast.success("Organization created successfully!");
|
||||||
router.push(`/teams/${newTeam.id}`);
|
router.push(`/organizations/${newOrganization.id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error(`Unable to create team`);
|
toast.error(`Unable to create organization`);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -49,22 +49,22 @@ export const CreateFirstTeam = () => {
|
|||||||
<p className="text ml-4 text-2xl font-bold">Formbricks</p>
|
<p className="text ml-4 text-2xl font-bold">Formbricks</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-[calc(100%-12rem)] items-center justify-center border-red-800">
|
<div className="flex h-[calc(100%-12rem)] items-center justify-center border-red-800">
|
||||||
<form onSubmit={handleSubmit(onCreateTeam)}>
|
<form onSubmit={handleSubmit(onCreateOrganization)}>
|
||||||
<div className="mb-2 flex w-full justify-between space-y-4 rounded-lg px-6">
|
<div className="mb-2 flex w-full justify-between space-y-4 rounded-lg px-6">
|
||||||
<div className="grid w-full gap-3">
|
<div className="grid w-full gap-3">
|
||||||
<h1 className="text text-3xl font-extrabold text-slate-800">
|
<h1 className="text text-3xl font-extrabold text-slate-800">
|
||||||
Let's create a team <span className="text-primary-500">👇</span>
|
Let's create an organization <span className="text-primary-500">👇</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text text-md text-slate-700">
|
<p className="text text-md text-slate-700">
|
||||||
We couldn't find a team for you. Please create one
|
We couldn't find an organization for you. Please create one
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder="e.g. Power Puff Girls"
|
placeholder="e.g. Power Puff Girls"
|
||||||
{...register("name", { required: true })}
|
{...register("name", { required: true })}
|
||||||
value={teamName}
|
value={organizationName}
|
||||||
onChange={(e) => setTeamName(e.target.value)}
|
onChange={(e) => setOrganizationName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,8 +75,8 @@ export const CreateFirstTeam = () => {
|
|||||||
variant="darkCTA"
|
variant="darkCTA"
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={!isTeamNameValid}>
|
disabled={!isOrganizationNameValid}>
|
||||||
Create team
|
Create organization
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CreateFirstTeam } from "@/app/(app)/create-first-team/components/CreateFirstTeam";
|
import { CreateFirstOrganization } from "@/app/(app)/create-first-organization/components/CreateFirstOrganization";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ const Page = async () => {
|
|||||||
redirect("/auth/login");
|
redirect("/auth/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
return <CreateFirstTeam />;
|
return <CreateFirstOrganization />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
@@ -57,7 +57,7 @@ export const AttributeClassesTable = ({ attributeClasses }: AttributeClassesTabl
|
|||||||
{displayedAttributeClasses.map((attributeClass, index) => (
|
{displayedAttributeClasses.map((attributeClass, index) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleOpenAttributeDetailModalClick(attributeClass)}
|
onClick={() => handleOpenAttributeDetailModalClick(attributeClass)}
|
||||||
className="w-full"
|
className="w-full cursor-default"
|
||||||
key={attributeClass.id}>
|
key={attributeClass.id}>
|
||||||
<AttributeClassDataRow attributeClass={attributeClass} key={index} />
|
<AttributeClassDataRow attributeClass={attributeClass} key={index} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Badge } from "@formbricks/ui/Badge";
|
|||||||
|
|
||||||
export const AttributeClassDataRow = ({ attributeClass }) => {
|
export const AttributeClassDataRow = ({ attributeClass }) => {
|
||||||
return (
|
return (
|
||||||
<div className="m-2 grid h-16 grid-cols-5 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
<div className="m-2 grid h-16 cursor-pointer grid-cols-5 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||||
<div className="col-span-5 flex items-center pl-6 text-sm sm:col-span-3">
|
<div className="col-span-5 flex items-center pl-6 text-sm sm:col-span-3">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<TagIcon className="h-5 w-5 flex-shrink-0 text-slate-500" />
|
<TagIcon className="h-5 w-5 flex-shrink-0 text-slate-500" />
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const AttributeSettingsTab = async ({ attributeClass, setOpen }: Attribut
|
|||||||
<Label className="text-slate-600">Name</Label>
|
<Label className="text-slate-600">Name</Label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g. Product Team Info"
|
placeholder="e.g. Product Organization Info"
|
||||||
{...register("name", {
|
{...register("name", {
|
||||||
disabled: attributeClass.type === "automatic" || attributeClass.type === "code" ? true : false,
|
disabled: attributeClass.type === "automatic" || attributeClass.type === "code" ? true : false,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,42 +1,51 @@
|
|||||||
|
import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation";
|
||||||
import { TagIcon } from "lucide-react";
|
import { TagIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||||
|
import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||||
|
|
||||||
const Loading = () => {
|
const Loading = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-lg border border-slate-200">
|
<PageContentWrapper>
|
||||||
<div className="grid h-12 grid-cols-5 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
<PageHeader pageTitle="People">
|
||||||
<div className="col-span-3 pl-6 ">Name</div>
|
<PeopleSecondaryNavigation activeId="attributes" loading />
|
||||||
<div className="text-center">Created</div>
|
</PageHeader>
|
||||||
<div className="text-center">Last Updated</div>
|
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||||
</div>
|
<div className="grid h-12 grid-cols-5 content-center border-b text-left text-sm font-semibold text-slate-900">
|
||||||
</div>
|
<div className="col-span-3 pl-6 ">Name</div>
|
||||||
|
<div className="text-center">Created</div>
|
||||||
{[...Array(3)].map((_, index) => (
|
<div className="text-center">Last Updated</div>
|
||||||
<div key={index} className="m-2 grid h-16 grid-cols-5 content-center rounded-lg hover:bg-slate-100">
|
</div>
|
||||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
<div className="w-full">
|
||||||
<div className="flex items-center">
|
{[...Array(3)].map((_, index) => (
|
||||||
<div className="h-10 w-10 flex-shrink-0">
|
<div
|
||||||
<TagIcon className="h-8 w-8 flex-shrink-0 animate-pulse text-slate-500" />
|
key={index}
|
||||||
</div>
|
className="m-2 grid h-16 grid-cols-5 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||||
<div className="ml-4 text-left">
|
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||||
<div className="font-medium text-slate-900">
|
<div className="flex items-center">
|
||||||
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
|
<TagIcon className="h-5 w-5 flex-shrink-0 animate-pulse text-slate-500" />
|
||||||
|
<div className="ml-4 text-left">
|
||||||
|
<div className="font-medium text-slate-900">
|
||||||
|
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xs text-slate-400">
|
||||||
|
<div className="h-2 w-24 animate-pulse rounded-full bg-slate-200"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-slate-400">
|
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||||
<div className="h-2 w-24 animate-pulse rounded-full bg-slate-200"></div>
|
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
|
||||||
|
</div>
|
||||||
|
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||||
|
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
|
||||||
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
|
|
||||||
</div>
|
|
||||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
|
||||||
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</PageContentWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ActivityTimeline } from "@/app/(app)/environments/[environmentId]/(peop
|
|||||||
import { getActionsByPersonId } from "@formbricks/lib/action/service";
|
import { getActionsByPersonId } from "@formbricks/lib/action/service";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||||
|
|
||||||
export const ActivitySection = async ({
|
export const ActivitySection = async ({
|
||||||
environmentId,
|
environmentId,
|
||||||
@@ -12,15 +12,15 @@ export const ActivitySection = async ({
|
|||||||
environmentId: string;
|
environmentId: string;
|
||||||
personId: string;
|
personId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const team = await getTeamByEnvironmentId(environmentId);
|
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||||
|
|
||||||
if (!team) {
|
if (!organization) {
|
||||||
throw new Error("Team not found");
|
throw new Error("Organization not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// On Formbricks Cloud only render the timeline if the user targeting feature is booked
|
// On Formbricks Cloud only render the timeline if the user targeting feature is booked
|
||||||
const isUserTargetingEnabled = IS_FORMBRICKS_CLOUD
|
const isUserTargetingEnabled = IS_FORMBRICKS_CLOUD
|
||||||
? team.billing.features.userTargeting.status === "active"
|
? organization.billing.features.userTargeting.status === "active"
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
const [environment, actions] = await Promise.all([
|
const [environment, actions] = await Promise.all([
|
||||||
|
|||||||