Compare commits
25 Commits
v2.0.3
...
testing/ts
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
449b56fd40 | ||
|
|
2b1ffa90f9 | ||
|
|
d595794fb6 | ||
|
|
2d3dec7834 | ||
|
|
bbfdba7615 | ||
|
|
681c559c79 | ||
|
|
4e39f45446 | ||
|
|
62c514acf2 | ||
|
|
48638e8ca2 | ||
|
|
cb44b575c2 | ||
|
|
1565fd33f7 | ||
|
|
2bf04e9818 | ||
|
|
a5f6ecb992 | ||
|
|
a211e64f0e | ||
|
|
9d33aa034a | ||
|
|
a91c9db4e0 | ||
|
|
291f628415 | ||
|
|
d53ceaaaac | ||
|
|
aa981fd891 | ||
|
|
b50bda8488 | ||
|
|
0d36e11bf4 | ||
|
|
102cdb4589 | ||
|
|
5b78487b94 | ||
|
|
f917d2171e | ||
|
|
295754480e |
10
.env.example
@@ -88,7 +88,7 @@ PASSWORD_RESET_DISABLED=1
|
||||
# Email login. Disable the ability for users to login with email.
|
||||
# EMAIL_AUTH_DISABLED=1
|
||||
|
||||
# Team Invite. Disable the ability for invited users to create an account.
|
||||
# Organization Invite. Disable the ability for invited users to create an account.
|
||||
# INVITE_DISABLED=1
|
||||
|
||||
##########
|
||||
@@ -154,11 +154,11 @@ SLACK_CLIENT_SECRET=
|
||||
# Enterprise License Key
|
||||
ENTERPRISE_LICENSE_KEY=
|
||||
|
||||
# Automatically assign new users to a specific team and role within that team
|
||||
# Insert an existing team id or generate a valid CUID for a new one at https://www.getuniqueid.com/cuid (e.g. cjld2cjxh0000qzrmn831i7rn)
|
||||
# Automatically assign new users to a specific organization and role within that organization
|
||||
# Insert an existing organization id or generate a valid CUID for a new one at https://www.getuniqueid.com/cuid (e.g. cjld2cjxh0000qzrmn831i7rn)
|
||||
# (Role Management is an Enterprise feature)
|
||||
# DEFAULT_TEAM_ID=
|
||||
# DEFAULT_TEAM_ROLE=admin
|
||||
# DEFAULT_ORGANIZATION_ID=
|
||||
# DEFAULT_ORGANIZATION_ROLE=admin
|
||||
|
||||
# set to 1 to skip onboarding for new users
|
||||
# ONBOARDING_DISABLED=1
|
||||
|
||||
131
.github/workflows/kamal-deploy.yml
vendored
Normal file
@@ -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
|
||||
@@ -82,7 +82,7 @@ Formbricks is both a free and open source survey platform - and a privacy-first
|
||||
|
||||
- 🔗 Create shareable **link surveys**.
|
||||
|
||||
- 👨👩👦 Invite your team members to **collaborate** on your surveys.
|
||||
- 👨👩👦 Invite your organization members to **collaborate** on your surveys.
|
||||
|
||||
- 🔌 Integrate Formbricks with **Slack, Notion, Zapier, n8n and more**.
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
|
||||
|
||||
interface SurveySwitchProps {
|
||||
value: "website" | "app";
|
||||
formbricks: any;
|
||||
@@ -7,23 +5,18 @@ interface SurveySwitchProps {
|
||||
|
||||
export const SurveySwitch = ({ value, formbricks }: SurveySwitchProps) => {
|
||||
return (
|
||||
<Select
|
||||
<select
|
||||
value={value}
|
||||
onValueChange={(v) => {
|
||||
onChange={(v) => {
|
||||
formbricks.logout();
|
||||
window.location.href = `/${v}`;
|
||||
window.location.href = `/${v.target.value}`;
|
||||
}}>
|
||||
<SelectTrigger className="w-[180px] px-4">
|
||||
<SelectValue placeholder="Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="website" className="h-10 px-4 hover:bg-slate-100">
|
||||
Website Surveys
|
||||
</SelectItem>
|
||||
<SelectItem value="app" className="hover:bg-slate-10 h-10 px-4">
|
||||
App Surveys
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<option value="website" className="h-10 px-4 hover:bg-slate-100">
|
||||
Website Surveys
|
||||
</option>
|
||||
<option value="app" className="hover:bg-slate-10 h-10 px-4">
|
||||
App Surveys
|
||||
</option>
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ["@formbricks/ui"],
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
@@ -25,4 +24,4 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
export default nextConfig;
|
||||
@@ -13,7 +13,7 @@
|
||||
"dependencies": {
|
||||
"@formbricks/js": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"lucide-react": "^0.378.0",
|
||||
"lucide-react": "^0.379.0",
|
||||
"next": "14.2.3",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
|
||||
@@ -6,7 +6,7 @@ import I2 from "./images/I2.webp";
|
||||
export const metadata = {
|
||||
title: "Using Actions in Formbricks | Fine-tuning User Moments",
|
||||
description:
|
||||
"Dive deep into how actions in Formbricks help products and teams to engage users at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine user targeting and generate richer, more detailed user insights.",
|
||||
"Dive deep into how actions in Formbricks help products and organizations to engage users at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine user targeting and generate richer, more detailed user insights.",
|
||||
};
|
||||
|
||||
#### App Surveys
|
||||
|
||||
@@ -85,7 +85,7 @@ To run this survey properly, you should pre-segment your user base. As touched u
|
||||
|
||||
- Check the time passed since sign-up (e.g. signed up 4 weeks ago)
|
||||
- User has performed a specific action a certain number of times or (e.g. created 5 reports)
|
||||
- User has performed a combination of actions (e.g. created a report **and** invited a team member)
|
||||
- User has performed a combination of actions (e.g. created a report **and** invited a organization member)
|
||||
|
||||
This way you make sure that you separate potentially misleading opinions from valuable insights.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
@@ -1,41 +1,89 @@
|
||||
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";
|
||||
import MenuItem from "./images/organization-settings-menu.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Team Access Roles",
|
||||
title: "Organization Access Roles",
|
||||
description:
|
||||
"Assign different roles to team members to grant them specific rights like creating surveys, viewing responses, or managing team members.",
|
||||
"Assign different roles to organization members to grant them specific rights like creating surveys, viewing responses, or managing organization members.",
|
||||
};
|
||||
|
||||
# Team Access Roles
|
||||
# Organization Access Roles
|
||||
|
||||
Assign different roles to team members to grant them specific rights like creating surveys, viewing responses, or managing team members.
|
||||
Assign different roles to organization members to grant them specific rights like creating surveys, viewing responses, or managing organization 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>
|
||||
<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 organization members as `Admins`.
|
||||
</Note>
|
||||
|
||||
Here are the different access permissions, ranked from highest to lowest access
|
||||
|
||||
| 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. |
|
||||
1. Owner
|
||||
2. Admin
|
||||
3. Developer
|
||||
4. Editor
|
||||
5. Viewer
|
||||
|
||||
## Inviting team members
|
||||
For more information on user roles & permissions, see below:
|
||||
|
||||
There are two ways to invite team members: One by one or in bulk.
|
||||
| | Owner | Admin | Editor | Developer | Viewer |
|
||||
| -------------------------------- | ----- | ----- | ------ | --------- | ------ |
|
||||
| **Organization** | | | | | |
|
||||
| Update organization | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| Delete organization | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| Add new Member | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| Delete Member | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| Update Member Access | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| Update Billing | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **Product** | | | | | |
|
||||
| Create Product | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Update Product Name | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| Update Product Name | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| Update Product Recontact Options | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Update Look & Feel | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Update Survey Languages | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Delete Product | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **Surveys** | | | | | |
|
||||
| Create New Survey | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Edit Survey | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Delete Survey | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| View survey results | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Response** | | | | | |
|
||||
| Delete response | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Add tags on response | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Edit tags on response | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **Actions** | | | | | |
|
||||
| Create Action | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Update Action | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Delete Action | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **API Keys** | | | | | |
|
||||
| Create API key | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Update API key | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Delete API key | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **Tags** | | | | | |
|
||||
| Create tags | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Update tags | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| Delete tags | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **People** | | | | | |
|
||||
| Delete Person | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **Integrations** | | | | | |
|
||||
| Manage Integrations | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
|
||||
### Invite team members one by one
|
||||
## Inviting organization members
|
||||
|
||||
1. Go to the `Team Settings` page via the menu in the lower right corner:
|
||||
There are two ways to invite organization members: One by one or in bulk.
|
||||
|
||||
<MdxImage
|
||||
src={MenuItem}
|
||||
alt="Where to find the Menu Item for Team Settings"
|
||||
### Invite organization members one by one
|
||||
|
||||
1. Go to the `Organization Settings` page via the menu in the lower right corner:
|
||||
|
||||
<MdxImage
|
||||
src={MenuItem}
|
||||
alt="Where to find the Menu Item for Organization Settings"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
@@ -49,7 +97,7 @@ src={MenuItem}
|
||||
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:
|
||||
3. In the modal, add the Name, Email and Role of the organization member you want to invite:
|
||||
|
||||
<MdxImage
|
||||
src={IndvInvite}
|
||||
@@ -58,19 +106,20 @@ src={MenuItem}
|
||||
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>
|
||||
<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 organization members as `Admins`.
|
||||
</Note>
|
||||
|
||||
Formbricks sends an email to the organization member with an invitation link. The organization member can accept the invitation or create a new account by clicking on the link.
|
||||
|
||||
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 organization members in bulk
|
||||
|
||||
|
||||
### Invite team members in bulk
|
||||
|
||||
1. Go to the `Team Settings` page via the menu in the lower right corner:
|
||||
1. Go to the `Organization Settings` page via the menu in the lower right corner:
|
||||
|
||||
<MdxImage
|
||||
src={MenuItem}
|
||||
alt="Where to find the Menu Item for Team Settings"
|
||||
alt="Where to find the Menu Item for Organization Settings"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
@@ -84,7 +133,7 @@ Formbricks sends an email to the team member with an invitation link. The team m
|
||||
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:
|
||||
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 organization members you want to invite:
|
||||
|
||||
<MdxImage
|
||||
src={BulkInvite}
|
||||
@@ -93,8 +142,8 @@ Formbricks sends an email to the team member with an invitation link. The team m
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
4. Upload the filled .CSV file and invite the team members in bulk ✅
|
||||
4. Upload the filled .CSV file and invite the organization 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.
|
||||
Formbricks sends an email to each organization member in the CSV. The member can accept the invitation or create a new account by clicking on the link.
|
||||
|
||||
---
|
||||
|
||||
BIN
apps/docs/app/global/hidden-fields/filled-hidden-fields.webp
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
apps/docs/app/global/hidden-fields/hidden-field-responses.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
apps/docs/app/global/hidden-fields/hidden-fields.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
apps/docs/app/global/hidden-fields/input-hidden-fields.webp
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
79
apps/docs/app/global/hidden-fields/page.mdx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import FilledHiddenFields from "./filled-hidden-fields.webp";
|
||||
import HiddenFieldResponses from "./hidden-field-responses.webp";
|
||||
import HiddenFields from "./hidden-fields.webp";
|
||||
import InputHiddenFields from "./input-hidden-fields.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Hidden Fields",
|
||||
description: "Add hidden fields to your surveys to capture additional data without requiring user inputs!",
|
||||
};
|
||||
|
||||
# Hidden Fields
|
||||
|
||||
Hidden fields are a powerful feature in Formbricks that allows you to add data to a submission without asking the user to type it in. This feature is especially useful when you already have information about a user that you want to use in the analysis of the survey results (e.g. `payment plan` or `email`)
|
||||
|
||||
<Note>Hidden fields are now available in the Formbricks in-app and website surveys as well</Note>
|
||||
|
||||
## How to Add Hidden Fields
|
||||
|
||||
### Enable them in the Survey Builder
|
||||
|
||||
1. Edit the survey you want to add hidden fields to & switch to the Questions tab and scroll down to the bottom of the page. You will see a section called **Hidden Fields**. Make sure to enable it by toggling the switch.
|
||||
|
||||
<MdxImage
|
||||
src={HiddenFields}
|
||||
alt="Enable Hidden Fields"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2. Now click on it to add a new hidden field ID. You can add as many hidden fields as you want.
|
||||
|
||||
<MdxImage
|
||||
src={InputHiddenFields}
|
||||
alt="Add Hidden Fields"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
<MdxImage
|
||||
src={FilledHiddenFields}
|
||||
alt="Filled Hidden Fields"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Set Hidden Field
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Example Screen from which the User filled it">
|
||||
|
||||
```sh
|
||||
formbricks.track("my event", {
|
||||
hiddenFields: {
|
||||
screen: "landing_page",
|
||||
job: "Founder"
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
## View Hidden Fields in Responses
|
||||
|
||||
These hidden fields will now be visible in the responses tab just like other fields in the Summary as well as the Response Cards, and you can use them to filter and analyze your responses.
|
||||
|
||||
<MdxImage
|
||||
src={HiddenFieldResponses}
|
||||
alt="Hidden Field Responses"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **User Metadata**: You can add hidden fields to capture user metadata such as user ID, email, or any other user-specific information.
|
||||
- **Survey Metadata**: You can add hidden fields to capture other metadata, e.g. the screen from which the survey was filled, or any other app specific information.
|
||||
@@ -4,27 +4,22 @@ export const metadata = {
|
||||
"Open source software beats proprietary software in every aspect - except for value capture. We're investing in growing the value creation of our open source platform because it directly translates into business with large organisations.",
|
||||
};
|
||||
|
||||
#### Introduction
|
||||
#### Introduction
|
||||
|
||||
# Why is Formbricks open source?
|
||||
|
||||
A lot has been written on why open source software beats proprietary software in all aspects - except for value capture for the company investing into its development. While this definitely poses a challenge for a profit-oriented organisation, it's also an interesting opportunity: Due to the open nature of our platform, it's usage is significantly higher. Capturing a small part of the value our platform generates translates into a decently-sized business.
|
||||
|
||||
|
||||
| Advantage | Open Source Software | Proprietary Software |
|
||||
|----------------------|----------------------------------------------------|----------------------------------------------------------|
|
||||
| **Data Privacy** | Self-host for maximum control over data | Dependent on thrid party data processor. |
|
||||
| **Cost** | Often free or significantly lower cost. | Typically requires a purchase or subscription. |
|
||||
| **Customizability** | Code can be modified to meet specific needs. | Limited customization, restricted to developer's features.|
|
||||
| **Security**| Frequent community reviews identify vulnerabilities quickly. | Security updates depend on vendor's schedule and interest.|
|
||||
| **Flexibility**| Supports a wide range of applications and integrations. | Designed for specific environments and integrations. |
|
||||
| **Community Support**| Large, active communities offer free support and resources. | Paid customer support with limited community help. |
|
||||
| **Innovation** | Fosters rapid innovation through community contributions. | Innovations depend on vendor's vision and development team.|
|
||||
| **Licensing** | Permissive licenses allow broad usage and modification. | Strict licensing with limited redistribution rights. |
|
||||
| **Independence** | Not dependent on a single vendor or developer. | Vendor lock-in can limit future choices. |
|
||||
| **Transparency** | Full visibility into the code base and development. | Closed-source, code is hidden from users. |
|
||||
| **Interoperability**| Supports open standards, ensuring interoperability. | Often requires additional software or plugins for compatibility. |
|
||||
|
||||
|
||||
|
||||
|
||||
| Advantage | Open Source Software | Proprietary Software |
|
||||
| --------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------- |
|
||||
| **Data Privacy** | Self-host for maximum control over data | Dependent on thrid party data processor. |
|
||||
| **Cost** | Often free or significantly lower cost. | Typically requires a purchase or subscription. |
|
||||
| **Customizability** | Code can be modified to meet specific needs. | Limited customization, restricted to developer's features. |
|
||||
| **Security** | Frequent community reviews identify vulnerabilities quickly. | Security updates depend on vendor's schedule and interest. |
|
||||
| **Flexibility** | Supports a wide range of applications and integrations. | Designed for specific environments and integrations. |
|
||||
| **Community Support** | Large, active communities offer free support and resources. | Paid customer support with limited community help. |
|
||||
| **Innovation** | Fosters rapid innovation through community contributions. | Innovations depend on vendor's vision and development team. |
|
||||
| **Licensing** | Permissive licenses allow broad usage and modification. | Strict licensing with limited redistribution rights. |
|
||||
| **Independence** | Not dependent on a single vendor or developer. | Vendor lock-in can limit future choices. |
|
||||
| **Transparency** | Full visibility into the code base and development. | Closed-source, code is hidden from users. |
|
||||
| **Interoperability** | Supports open standards, ensuring interoperability. | Often requires additional software or plugins for compatibility. |
|
||||
|
||||
@@ -40,6 +40,10 @@ To prefill the questions of a survey, add query parameters to the survey URL. Th
|
||||
|
||||
<Note>Please make sure the answer is [URL encoded](https://www.urlencoder.org/).</Note>
|
||||
|
||||
## Prefilling Customisation
|
||||
|
||||
You can customize the prefilling behavior using the `skipPrefilled` parameter in the URL. If you want to skip the prefilled questions and show the next available question, you can add `skipPrefilled=true` to the URL. By default, the `skipPrefilled` parameter is set to `false`.
|
||||
|
||||
## Prefilling multiple values
|
||||
|
||||
Formbricks let's you prefill as many values as you want. You can combine multiple values in the URL using `&` so for example `name=Bernadette&age=18`. The order of the query parameters does not matter so you can always move around questions or add new ones without having to worry about the order of the query parameters.
|
||||
|
||||
@@ -4,7 +4,6 @@ import FilledHiddenFields from "./filled-hidden-fields.webp";
|
||||
import HiddenFieldResponses from "./hidden-field-responses.webp";
|
||||
import HiddenFields from "./hidden-fields.webp";
|
||||
import InputHiddenFields from "./input-hidden-fields.webp";
|
||||
import SettingsPage from "./settings.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Hidden Fields",
|
||||
@@ -21,16 +20,7 @@ Hidden fields are a powerful feature in Formbricks that allows you to add data t
|
||||
|
||||
### Enable them in the Survey Builder
|
||||
|
||||
1. Edit the survey you want to add hidden fields to & open it's settings, make sure it's selected as a **Link Survey**.
|
||||
|
||||
<MdxImage
|
||||
src={SettingsPage}
|
||||
alt="Select the Survey Type as Link Survey"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2. Switch to the Questions tab and scroll down to the bottom of the page. You will see a section called **Hidden Fields**. Make sure to enable it by toggling the switch.
|
||||
1. Edit the survey you want to add hidden fields to & switch to the Questions tab and scroll down to the bottom of the page. You will see a section called **Hidden Fields**. Make sure to enable it by toggling the switch.
|
||||
|
||||
<MdxImage
|
||||
src={HiddenFields}
|
||||
@@ -39,7 +29,7 @@ Hidden fields are a powerful feature in Formbricks that allows you to add data t
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
3. Now click on it to add a new hidden field ID. You can add as many hidden fields as you want.
|
||||
2. Now click on it to add a new hidden field ID. You can add as many hidden fields as you want.
|
||||
|
||||
<MdxImage
|
||||
src={InputHiddenFields}
|
||||
|
||||
|
Before Width: | Height: | Size: 39 KiB |
@@ -51,8 +51,8 @@ These variables are present inside your machine’s docker-compose file. Restart
|
||||
| INSTANCE_ID | Instance ID for Formbricks Cloud to be sent to Telemetry. | optional | |
|
||||
| INTERNAL_SECRET | Internal Secret (Currently we overwrite the value with a random value). | optional | |
|
||||
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b |
|
||||
| DEFAULT_TEAM_ID | Automatically assign new users to a specific team when joining | optional | |
|
||||
| DEFAULT_TEAM_ROLE | Role of the user in the default team. | optional | admin |
|
||||
| DEFAULT_ORGANIZATION_ID | Automatically assign new users to a specific organization when joining | optional | |
|
||||
| DEFAULT_ORGANIZATION_ROLE | Role of the user in the default organization. | optional | admin |
|
||||
| ONBOARDING_DISABLED | Disables onboarding for new users if set to 1 | optional | |
|
||||
| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | |
|
||||
| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
|
||||
|
||||
140
apps/docs/app/self-hosting/custom-ssl/page.mdx
Normal file
@@ -0,0 +1,140 @@
|
||||
export const metadata = {
|
||||
title: "Add Custom SSL Certificate to Formbricks",
|
||||
description: "Learn how to add a custom SSL certificate to your Formbricks self-hosted instance.",
|
||||
};
|
||||
|
||||
# Using Formbricks One-Click Setup with a Custom SSL Certificate
|
||||
|
||||
<Note>
|
||||
Formbricks One-Click setup already comes with a valid SSL certificate using Let's Encrypt. This guide is
|
||||
only if you already have a valid SSL certificate that you need to use due to company policy or other
|
||||
requirements.
|
||||
</Note>
|
||||
|
||||
## Introduction
|
||||
|
||||
While Formbricks' One-Click setup can automatically create a valid SSL certificate using Let's Encrypt, there are scenarios where a custom SSL certificate is necessary. This is particularly relevant for environments like intranets or other setups with specific certificate requirements, where an internal or custom certificate authority (CA) might be used.
|
||||
|
||||
### Step 1: Navigate to the Formbricks Folder
|
||||
|
||||
Navigate into the "formbricks" folder that contains all the files from the Formbricks One-Click setup.
|
||||
|
||||
```sh
|
||||
cd formbricks
|
||||
```
|
||||
|
||||
### Step 2: Create a Folder for SSL Certificates
|
||||
|
||||
Create a new folder named "certs" within the "formbricks" folder. Place your SSL certificate files (`fullchain.crt` and `cert.key`) in this directory.
|
||||
|
||||
```sh
|
||||
mkdir certs
|
||||
# Move your SSL certificate files to the certs folder
|
||||
mv /path/to/your/fullchain.crt certs/
|
||||
mv /path/to/your/cert.key certs/
|
||||
```
|
||||
|
||||
### Step 3: Understand SSL Certificate Files
|
||||
|
||||
For a custom SSL setup, you need the following files:
|
||||
|
||||
- **fullchain.crt**: This file contains your SSL certificate along with the entire certificate chain. The certificate chain includes intermediate certificates that link your SSL certificate to a trusted root certificate.
|
||||
- **cert.key**: This is your private key file. It is used to encrypt and decrypt data sent between your server and clients.
|
||||
|
||||
### Step 4: Update File Permissions
|
||||
|
||||
Ensure the directory and files have appropriate permissions:
|
||||
|
||||
```sh
|
||||
sudo chown root:root certs/*
|
||||
sudo chmod 600 certs/*
|
||||
```
|
||||
|
||||
### Step 5: Update `traefik.yaml`
|
||||
|
||||
Update your `traefik.yaml` file to define entry points for HTTP and HTTPS traffic and set up a provider for Traefik to use Docker and a file-based configuration.
|
||||
|
||||
```yaml
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: websecure
|
||||
scheme: https
|
||||
permanent: true
|
||||
websecure:
|
||||
address: ":443"
|
||||
|
||||
providers:
|
||||
docker:
|
||||
watch: true
|
||||
exposedByDefault: false
|
||||
file:
|
||||
directory: /etc/traefik/dynamic
|
||||
```
|
||||
|
||||
### Step 6: Create `certs-traefik.yaml`
|
||||
|
||||
Create a `certs-traefik.yaml` file that specifies the path to your custom SSL certificate and key.
|
||||
|
||||
```yaml
|
||||
tls:
|
||||
certificates:
|
||||
- certFile: /certs/fullchain.crt
|
||||
keyFile: /certs/cert.key
|
||||
```
|
||||
|
||||
### Step 7: Update `docker-compose.yml`
|
||||
|
||||
Update your `docker-compose.yml` file to enforce TLS and link to your custom SSL certificate. Here's an example configuration for both the Formbricks and Traefik services. The rest of the configuration should remain the same as the One-Click setup:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
formbricks:
|
||||
restart: always
|
||||
image: ghcr.io/formbricks/formbricks:latest
|
||||
depends_on:
|
||||
- postgres
|
||||
labels:
|
||||
- "traefik.enable=true" # Enable Traefik for this service
|
||||
- "traefik.http.routers.formbricks.rule=Host(`my-domain.com`)" # Use your actual domain or IP
|
||||
- "traefik.http.routers.formbricks.entrypoints=websecure" # Use the websecure entrypoint (port 443 with TLS)
|
||||
- "traefik.http.routers.formbricks.tls=true" # Enable TLS
|
||||
- "traefik.http.services.formbricks.loadbalancer.server.port=3000" # Forward traffic to Formbricks on port 3000
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- uploads:/home/nextjs/apps/web/uploads/
|
||||
<<: *environment
|
||||
|
||||
traefik:
|
||||
image: "traefik:v2.7"
|
||||
restart: always
|
||||
container_name: "traefik"
|
||||
depends_on:
|
||||
- formbricks
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./traefik.yaml:/traefik.yaml
|
||||
- ./acme.json:/acme.json
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./certs:/certs
|
||||
- ./certs-traefik.yaml:/etc/traefik/dynamic/certs-traefik.yaml
|
||||
```
|
||||
|
||||
### Summary
|
||||
|
||||
1. **Navigate to the Formbricks Folder**: Move into the "formbricks" directory.
|
||||
2. **Create a Folder for SSL Certificates**: Create a "certs" folder and place your `fullchain.crt` and `cert.key` files inside it.
|
||||
3. **Understand SSL Certificate Files**: Ensure you have the `fullchain.crt` and `cert.key` files.
|
||||
4. **Update File Permissions**: Ensure the certificate files have the correct permissions.
|
||||
5. **Update `traefik.yaml`**: Define entry points and remove certificate resolvers.
|
||||
6. **Create `certs-traefik.yaml`**: Specify the paths to your SSL certificate and key.
|
||||
7. **Update `docker-compose.yml`**: Configure Traefik labels to enforce TLS and mount the certificate directory.
|
||||
|
||||
This setup ensures that Formbricks uses your custom SSL certificate for secure communications, suitable for environments with special certificate requirements.
|
||||
@@ -12,7 +12,9 @@ export const metadata = {
|
||||
The Formbricks Core source code is licensed under AGPLv3 and available on GitHub. Additionally, we offer features for bigger organisations & enterprises for self-hostesr under a separate Enterprise License.
|
||||
|
||||
<Note>
|
||||
Want to present a proof of concept? Request a free 30-day Enterprise Edition trial by [filling out the form below.](#30-day-trial-license-request) No call needed or strings attached: Just give us 24h to set up the key and send it over 🤙
|
||||
Want to present a proof of concept? Request a free 30-day Enterprise Edition trial by [filling out the form
|
||||
below.](#30-day-trial-license-request) No call needed or strings attached: Just give us 24h to set up the
|
||||
key and send it over 🤙
|
||||
</Note>
|
||||
|
||||
## Enterprise Edition License
|
||||
@@ -21,23 +23,24 @@ Additional to the AGPLv3 licensed Formbricks core, the Formbricks repository con
|
||||
|
||||
### When do I need an Enterprise License?
|
||||
|
||||
| | Community Edition | Enterprise License |
|
||||
| -------------------------------------------------------- | ----------------- | -------------------- |
|
||||
| Self-host for commercial purposes | ✅ | No EE license needed |
|
||||
| | Community Edition | Enterprise License |
|
||||
| ----------------------------------------------------------- | ----------------- | -------------------- |
|
||||
| Self-host for commercial purposes | ✅ | No EE license needed |
|
||||
| Make changes to the code base (have to publish all changes) | ✅ | No EE license needed |
|
||||
| Unlimited responses | ✅ | No EE license needed |
|
||||
| Unlimited surveys | ✅ | No EE license needed |
|
||||
| Remove branding | ✅ | No EE license needed |
|
||||
| SSO | ✅ | No EE license needed |
|
||||
| Use any of the other 100 features | ✅ | No EE license needed |
|
||||
| Team access roles | ❌ | ✅ |
|
||||
| Multi-language surveys | ❌ | ✅ |
|
||||
| Advanced targeting / Segments | ❌ | ✅ |
|
||||
| Make code changes and **keep private** | ❌ | ✅ |
|
||||
| Unlimited responses | ✅ | No EE license needed |
|
||||
| Unlimited surveys | ✅ | No EE license needed |
|
||||
| Remove branding | ✅ | No EE license needed |
|
||||
| SSO | ✅ | No EE license needed |
|
||||
| Use any of the other 100 features | ✅ | No EE license needed |
|
||||
| Organization access roles | ❌ | ✅ |
|
||||
| Multi-language surveys | ❌ | ✅ |
|
||||
| Advanced targeting / Segments | ❌ | ✅ |
|
||||
| Make code changes and **keep private** | ❌ | ✅ |
|
||||
|
||||
Ready to get started with the Enterprise Edition? Fill out our form below and we'll reach out to you.
|
||||
|
||||
## 30-day Trial License Request
|
||||
|
||||
Many organisations want to do an internal test run with the Enterprise Edition. To make that really easy, we now offer a 30-day trial license. Just fill out the form below and we'll send you a license key within 24 hours (business days):
|
||||
|
||||
<div
|
||||
|
||||
@@ -6,7 +6,7 @@ import StepTwo from "./images/StepTwo.webp";
|
||||
export const metadata = {
|
||||
title: "Using Actions in Formbricks | Fine-tuning Session Moments",
|
||||
description:
|
||||
"Dive deep into how actions in Formbricks help products and teams to engage active sessions at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine public facing websites' targeting and generate richer, more detailed insights.",
|
||||
"Dive deep into how actions in Formbricks help products and organizations to engage active sessions at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine public facing websites' targeting and generate richer, more detailed insights.",
|
||||
};
|
||||
|
||||
#### Website Surveys
|
||||
|
||||
@@ -35,6 +35,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Advanced Targeting", href: "/app-surveys/advanced-targeting" },
|
||||
{ title: "Show Survey to % of users", href: "/global/show-survey-to-percent-of-users" }, // app and website
|
||||
{ title: "Recontact Options", href: "/app-surveys/recontact" },
|
||||
{ title: "Hidden Fields", href: "/global/hidden-fields" }, // global
|
||||
{ title: "Multi Language Surveys", href: "/global/multi-language-surveys" }, // global
|
||||
{ title: "User Metadata", href: "/global/metadata" }, // global
|
||||
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
|
||||
@@ -57,6 +58,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Actions & Targeting", href: "/website-surveys/actions-and-targeting" },
|
||||
{ title: "Show Survey to % of users", href: "/global/show-survey-to-percent-of-users" }, // app and website
|
||||
{ title: "Recontact Options", href: "/app-surveys/recontact" },
|
||||
{ title: "Hidden Fields", href: "/global/hidden-fields" }, // global
|
||||
{ title: "Multi Language Surveys", href: "/global/multi-language-surveys" }, // global
|
||||
{ title: "User Metadata", href: "/global/metadata" }, // global
|
||||
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
|
||||
@@ -107,6 +109,7 @@ export const navigation: Array<NavGroup> = [
|
||||
links: [
|
||||
{ title: "Overview", href: "/self-hosting/overview" },
|
||||
{ title: "One-Click Setup", href: "/self-hosting/one-click" },
|
||||
{ title: "Custom SSL Certificate", href: "/self-hosting/custom-ssl" },
|
||||
{ title: "Docker Setup", href: "/self-hosting/docker" },
|
||||
{ title: "Migration Guide", href: "/self-hosting/migration-guide" },
|
||||
{ title: "Configuration", href: "/self-hosting/configuration" },
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-mdx": "^3.0.1",
|
||||
"schema-dts": "^1.1.2",
|
||||
"sharp": "^0.33.3",
|
||||
"sharp": "^0.33.4",
|
||||
"shiki": "^0.14.7",
|
||||
"simple-functional-loader": "^1.2.1",
|
||||
"tailwindcss": "^3.4.3",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { redirect } from "next/navigation";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
|
||||
import { ToasterClient } from "@formbricks/ui/ToasterClient";
|
||||
@@ -22,9 +22,9 @@ const EnvLayout = async ({ children, params }) => {
|
||||
throw new AuthorizationError("Not authorized");
|
||||
}
|
||||
|
||||
const team = await getTeamByEnvironmentId(params.environmentId);
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
const organization = await getOrganizationByEnvironmentId(params.environmentId);
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const environment = await getEnvironment(params.environmentId);
|
||||
@@ -39,11 +39,11 @@ const EnvLayout = async ({ children, params }) => {
|
||||
<PosthogIdentify
|
||||
session={session}
|
||||
environmentId={params.environmentId}
|
||||
teamId={team.id}
|
||||
teamName={team.name}
|
||||
inAppSurveyBillingStatus={team.billing.features.inAppSurvey.status}
|
||||
linkSurveyBillingStatus={team.billing.features.linkSurvey.status}
|
||||
userTargetingBillingStatus={team.billing.features.userTargeting.status}
|
||||
organizationId={organization.id}
|
||||
organizationName={organization.name}
|
||||
inAppSurveyBillingStatus={organization.billing.features.inAppSurvey.status}
|
||||
linkSurveyBillingStatus={organization.billing.features.linkSurvey.status}
|
||||
userTargetingBillingStatus={organization.billing.features.userTargeting.status}
|
||||
/>
|
||||
<FormbricksClient session={session} />
|
||||
<ToasterClient />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys";
|
||||
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
@@ -18,6 +19,7 @@ interface AddressQuestionFormProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const AddressQuestionForm = ({
|
||||
@@ -28,6 +30,7 @@ export const AddressQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: AddressQuestionFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
|
||||
@@ -37,12 +40,14 @@ export const AddressQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
label={"Question*"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -52,12 +57,14 @@ export const AddressQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
label={"Description"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import { LogicEditor } from "./LogicEditor";
|
||||
@@ -8,6 +9,7 @@ interface AdvancedSettingsProps {
|
||||
questionIdx: number;
|
||||
localSurvey: TSurvey;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const AdvancedSettings = ({
|
||||
@@ -15,6 +17,7 @@ export const AdvancedSettings = ({
|
||||
questionIdx,
|
||||
localSurvey,
|
||||
updateQuestion,
|
||||
attributeClasses,
|
||||
}: AdvancedSettingsProps) => {
|
||||
return (
|
||||
<div>
|
||||
@@ -24,6 +27,7 @@ export const AdvancedSettings = ({
|
||||
updateQuestion={updateQuestion}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TProductStyling } from "@formbricks/types/product";
|
||||
import { TSurveyBackgroundBgType, TSurveyStyling } from "@formbricks/types/surveys";
|
||||
import { TSurveyStyling } from "@formbricks/types/surveys";
|
||||
import { Badge } from "@formbricks/ui/Badge";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@formbricks/ui/Form";
|
||||
import { Slider } from "@formbricks/ui/Slider";
|
||||
|
||||
import { SurveyBgSelectorTab } from "./SurveyBgSelectorTab";
|
||||
@@ -14,52 +16,24 @@ import { SurveyBgSelectorTab } from "./SurveyBgSelectorTab";
|
||||
interface BackgroundStylingCardProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
styling: TSurveyStyling | TProductStyling | null;
|
||||
setStyling: React.Dispatch<React.SetStateAction<TSurveyStyling | TProductStyling>>;
|
||||
colors: string[];
|
||||
isSettingsPage?: boolean;
|
||||
disabled?: boolean;
|
||||
environmentId: string;
|
||||
isUnsplashConfigured: boolean;
|
||||
form: UseFormReturn<TProductStyling | TSurveyStyling>;
|
||||
}
|
||||
|
||||
export const BackgroundStylingCard = ({
|
||||
open,
|
||||
setOpen,
|
||||
styling,
|
||||
setStyling,
|
||||
colors,
|
||||
isSettingsPage = false,
|
||||
disabled,
|
||||
environmentId,
|
||||
isUnsplashConfigured,
|
||||
form,
|
||||
}: BackgroundStylingCardProps) => {
|
||||
const { bgType, brightness } = styling?.background ?? {};
|
||||
|
||||
const handleBgChange = (color: string, type: TSurveyBackgroundBgType) => {
|
||||
const { background } = styling ?? {};
|
||||
|
||||
setStyling({
|
||||
...styling,
|
||||
background: {
|
||||
...background,
|
||||
bg: color,
|
||||
bgType: type,
|
||||
brightness: 100,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBrightnessChange = (percent: number) => {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
background: {
|
||||
...prev.background,
|
||||
brightness: percent,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
@@ -101,48 +75,66 @@ export const BackgroundStylingCard = ({
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
<div className="flex flex-col gap-3 p-3">
|
||||
{/* Background */}
|
||||
<div className="p-3">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Change background</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Pick a background from our library or upload your own.
|
||||
</p>
|
||||
</div>
|
||||
<SurveyBgSelectorTab
|
||||
styling={styling}
|
||||
handleBgChange={handleBgChange}
|
||||
colors={colors}
|
||||
bgType={bgType}
|
||||
environmentId={environmentId}
|
||||
isUnsplashConfigured={isUnsplashConfigured}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Overlay */}
|
||||
<div className="flex flex-col gap-4 p-3">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Background overlay</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Darken or lighten background of your choice.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="ml-2 flex flex-col justify-center">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col justify-center rounded-lg border bg-slate-50 p-6">
|
||||
<Slider
|
||||
value={[brightness ?? 100]}
|
||||
max={200}
|
||||
onValueChange={(value) => {
|
||||
handleBrightnessChange(value[0]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<hr className="pt-1 text-slate-600" />
|
||||
<div className="flex flex-col gap-6 p-6 pt-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="background"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div>
|
||||
<FormLabel>Change background</FormLabel>
|
||||
<FormDescription>Pick a background from our library or upload your own.</FormDescription>
|
||||
</div>
|
||||
|
||||
<FormControl>
|
||||
<SurveyBgSelectorTab
|
||||
bg={field.value?.bg ?? ""}
|
||||
handleBgChange={(bg: string, bgType: string) => {
|
||||
field.onChange({
|
||||
...field.value,
|
||||
bg,
|
||||
bgType,
|
||||
brightness: 100,
|
||||
});
|
||||
}}
|
||||
colors={colors}
|
||||
bgType={field.value?.bgType ?? "color"}
|
||||
environmentId={environmentId}
|
||||
isUnsplashConfigured={isUnsplashConfigured}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col justify-center ">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="background.brightness"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div>
|
||||
<FormLabel>Brightness</FormLabel>
|
||||
<FormDescription>Darken or lighten background of your choice.</FormDescription>
|
||||
</div>
|
||||
|
||||
<FormControl>
|
||||
<div className="rounded-lg border bg-slate-50 p-6">
|
||||
<Slider
|
||||
value={[field.value ?? 100]}
|
||||
max={200}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value[0]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
@@ -18,6 +19,7 @@ interface CTAQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const CTAQuestionForm = ({
|
||||
@@ -29,6 +31,7 @@ export const CTAQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: CTAQuestionFormProps): JSX.Element => {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
|
||||
@@ -37,12 +40,14 @@ export const CTAQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
label={"Question*"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div className="mt-3">
|
||||
@@ -87,6 +92,7 @@ export const CTAQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
label={`"Next" Button Label`}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
@@ -95,12 +101,14 @@ export const CTAQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
{questionIdx !== 0 && (
|
||||
<QuestionFormInput
|
||||
id="backButtonLabel"
|
||||
value={question.backButtonLabel}
|
||||
label={`"Back" Button Label`}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
@@ -109,6 +117,7 @@ export const CTAQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -130,21 +139,20 @@ export const CTAQuestionForm = ({
|
||||
)}
|
||||
|
||||
{!question.required && (
|
||||
<div className="mt-3 flex-1">
|
||||
<Label htmlFor="buttonLabel">Skip Button Label</Label>
|
||||
<div className="mt-2">
|
||||
<QuestionFormInput
|
||||
id="dismissButtonLabel"
|
||||
value={question.dismissButtonLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
placeholder={"skip"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<QuestionFormInput
|
||||
id="dismissButtonLabel"
|
||||
value={question.dismissButtonLabel}
|
||||
label={"Skip Button Label"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
placeholder={"skip"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
@@ -17,6 +18,7 @@ interface CalQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const CalQuestionForm = ({
|
||||
@@ -27,6 +29,7 @@ export const CalQuestionForm = ({
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
isInvalid,
|
||||
attributeClasses,
|
||||
}: CalQuestionFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -36,12 +39,14 @@ export const CalQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
label={"Question*"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
@@ -50,12 +55,14 @@ export const CalQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
label={"Description"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,157 +2,47 @@
|
||||
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import React, { useMemo } from "react";
|
||||
import React from "react";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
|
||||
import { TProduct, TProductStyling } from "@formbricks/types/product";
|
||||
import { TCardArrangementOptions } from "@formbricks/types/styling";
|
||||
import { TSurveyStyling, TSurveyType } from "@formbricks/types/surveys";
|
||||
import { Badge } from "@formbricks/ui/Badge";
|
||||
import { CardArrangementTabs } from "@formbricks/ui/CardArrangementTabs";
|
||||
import { ColorPicker } from "@formbricks/ui/ColorPicker";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { ColorSelector } from "@formbricks/ui/ColorSelector";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@formbricks/ui/Form";
|
||||
import { Slider } from "@formbricks/ui/Slider";
|
||||
import { CardArrangement, ColorSelectorWithLabel } from "@formbricks/ui/Styling";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
|
||||
type CardStylingSettingsProps = {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
styling: TSurveyStyling | TProductStyling | null;
|
||||
setStyling: React.Dispatch<React.SetStateAction<TSurveyStyling | TProductStyling>>;
|
||||
isSettingsPage?: boolean;
|
||||
surveyType?: TSurveyType;
|
||||
disabled?: boolean;
|
||||
localProduct: TProduct;
|
||||
product: TProduct;
|
||||
form: UseFormReturn<TProductStyling | TSurveyStyling>;
|
||||
};
|
||||
|
||||
export const CardStylingSettings = ({
|
||||
setStyling,
|
||||
styling,
|
||||
isSettingsPage = false,
|
||||
surveyType,
|
||||
disabled,
|
||||
open,
|
||||
localProduct,
|
||||
product,
|
||||
setOpen,
|
||||
form,
|
||||
}: CardStylingSettingsProps) => {
|
||||
const isAppSurvey = surveyType === "app" || surveyType === "website";
|
||||
const cardBgColor = styling?.cardBackgroundColor?.light || COLOR_DEFAULTS.cardBackgroundColor;
|
||||
const surveyTypeDerived = isAppSurvey ? "App / Website" : "Link";
|
||||
const isLogoVisible = !!product.logo?.url;
|
||||
|
||||
const isLogoHidden = styling?.isLogoHidden ?? false;
|
||||
|
||||
const isLogoVisible = !!localProduct.logo?.url;
|
||||
|
||||
const linkSurveyCardArrangement = styling?.cardArrangement?.linkSurveys ?? "straight";
|
||||
|
||||
const inAppSurveyCardArrangement = styling?.cardArrangement?.appSurveys ?? "straight";
|
||||
|
||||
const setCardBgColor = (color: string) => {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
cardBackgroundColor: {
|
||||
...(prev.cardBackgroundColor ?? {}),
|
||||
light: color,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const cardBorderColor = styling?.cardBorderColor?.light || COLOR_DEFAULTS.cardBorderColor;
|
||||
const setCardBorderColor = (color: string) => {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
cardBorderColor: {
|
||||
...(prev.cardBorderColor ?? {}),
|
||||
light: color,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const cardShadowColor = styling?.cardShadowColor?.light || COLOR_DEFAULTS.cardShadowColor;
|
||||
const setCardShadowColor = (color: string) => {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
cardShadowColor: {
|
||||
...(prev.cardShadowColor ?? {}),
|
||||
light: color,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const isHighlightBorderAllowed = !!styling?.highlightBorderColor;
|
||||
const setIsHighlightBorderAllowed = (open: boolean) => {
|
||||
if (!open) {
|
||||
const { highlightBorderColor, ...rest } = styling ?? {};
|
||||
|
||||
setStyling({
|
||||
...rest,
|
||||
});
|
||||
} else {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
highlightBorderColor: {
|
||||
...(prev.highlightBorderColor ?? {}),
|
||||
light: COLOR_DEFAULTS.highlightBorderColor,
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const highlightBorderColor = styling?.highlightBorderColor?.light || COLOR_DEFAULTS.highlightBorderColor;
|
||||
const setHighlightBorderColor = (color: string) => {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
highlightBorderColor: {
|
||||
...(prev.highlightBorderColor ?? {}),
|
||||
light: color,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const roundness = styling?.roundness ?? 8;
|
||||
const setRoundness = (value: number) => {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
roundness: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const setCardArrangement = (arrangement: TCardArrangementOptions, surveyType: TSurveyType) => {
|
||||
const newCardArrangement = {
|
||||
linkSurveys: linkSurveyCardArrangement,
|
||||
appSurveys: inAppSurveyCardArrangement,
|
||||
};
|
||||
|
||||
if (surveyType === "link") {
|
||||
newCardArrangement.linkSurveys = arrangement;
|
||||
} else if (surveyType === "app" || surveyType === "website") {
|
||||
newCardArrangement.appSurveys = arrangement;
|
||||
}
|
||||
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
cardArrangement: newCardArrangement,
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleProgressBarVisibility = (hideProgressBar: boolean) => {
|
||||
setStyling({
|
||||
...styling,
|
||||
hideProgressBar,
|
||||
});
|
||||
};
|
||||
|
||||
const toggleLogoVisibility = () => {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
isLogoHidden: !prev.isLogoHidden,
|
||||
}));
|
||||
};
|
||||
|
||||
const hideProgressBar = useMemo(() => {
|
||||
return styling?.hideProgressBar;
|
||||
}, [styling]);
|
||||
const linkCardArrangement = form.watch("cardArrangement.linkSurveys") ?? "simple";
|
||||
const appCardArrangement = form.watch("cardArrangement.appSurveys") ?? "simple";
|
||||
const roundness = form.watch("roundness") ?? 8;
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
@@ -194,107 +84,225 @@ export const CardStylingSettings = ({
|
||||
<hr className="py-1 text-slate-600" />
|
||||
|
||||
<div className="flex flex-col gap-6 p-6 pt-2">
|
||||
<div className="flex max-w-xs flex-col gap-4">
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Roundness</h3>
|
||||
<p className="text-xs text-slate-500">Change the border radius of the card and the inputs.</p>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center rounded-lg border bg-slate-50 p-6">
|
||||
<Slider value={[roundness]} max={22} onValueChange={(value) => setRoundness(value[0])} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ColorSelectorWithLabel
|
||||
label="Card background color"
|
||||
color={cardBgColor}
|
||||
setColor={setCardBgColor}
|
||||
description="Change the background color of the card."
|
||||
/>
|
||||
|
||||
<ColorSelectorWithLabel
|
||||
label="Card border color"
|
||||
color={cardBorderColor}
|
||||
setColor={setCardBorderColor}
|
||||
description="Change the border color of the card."
|
||||
/>
|
||||
|
||||
<ColorSelectorWithLabel
|
||||
label="Card shadow color"
|
||||
color={cardShadowColor}
|
||||
setColor={setCardShadowColor}
|
||||
description="Change the shadow color of the card."
|
||||
/>
|
||||
|
||||
<CardArrangement
|
||||
surveyType={isAppSurvey ? "app" : "link"}
|
||||
activeCardArrangement={isAppSurvey ? inAppSurveyCardArrangement : linkSurveyCardArrangement}
|
||||
setActiveCardArrangement={setCardArrangement}
|
||||
/>
|
||||
|
||||
<>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch
|
||||
id="hideProgressBar"
|
||||
checked={!!hideProgressBar}
|
||||
onCheckedChange={(checked) => toggleProgressBarVisibility(checked)}
|
||||
/>
|
||||
<Label htmlFor="hideProgressBar" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Hide progress bar</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Disable the visibility of survey progress.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{isLogoVisible && (!surveyType || surveyType === "link") && !isSettingsPage && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch id="isLogoHidden" checked={isLogoHidden} onCheckedChange={toggleLogoVisibility} />
|
||||
<Label htmlFor="isLogoHidden" className="cursor-pointer">
|
||||
<div className="ml-2 flex flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Hide logo</h3>
|
||||
<Badge text="Link Surveys" type="gray" size="normal" />
|
||||
</div>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Hides the logo in this specific survey
|
||||
</p>
|
||||
<div className="flex flex-col justify-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="roundness"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<div>
|
||||
<FormLabel>Roundness</FormLabel>
|
||||
<FormDescription>Change the border radius of the card and the inputs.</FormDescription>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!surveyType || isAppSurvey) && (
|
||||
<div className="flex max-w-xs flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch checked={isHighlightBorderAllowed} onCheckedChange={setIsHighlightBorderAllowed} />
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="whitespace-nowrap text-sm font-semibold text-slate-700">
|
||||
Add highlight border
|
||||
</h3>
|
||||
<Badge
|
||||
text="App & Website Surveys"
|
||||
type="gray"
|
||||
size="normal"
|
||||
className="whitespace-nowrap"
|
||||
<FormControl>
|
||||
<div className="rounded-lg border bg-slate-50 p-6">
|
||||
<Slider
|
||||
value={[roundness]}
|
||||
max={22}
|
||||
onValueChange={(value) => {
|
||||
form.setValue("roundness", value[0]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">Add an outer border to your survey card.</p>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cardBackgroundColor.light"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-4">
|
||||
<div>
|
||||
<FormLabel>Card background color</FormLabel>
|
||||
<FormDescription>Change the background color of the card.</FormDescription>
|
||||
</div>
|
||||
|
||||
{isHighlightBorderAllowed && (
|
||||
<ColorPicker
|
||||
color={highlightBorderColor}
|
||||
onChange={setHighlightBorderColor}
|
||||
containerClass="my-0"
|
||||
<FormControl>
|
||||
<ColorSelector
|
||||
color={field.value || COLOR_DEFAULTS.cardBackgroundColor}
|
||||
setColor={(color) => field.onChange(color)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
</>
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cardBorderColor.light"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-4">
|
||||
<div>
|
||||
<FormLabel>Card border color</FormLabel>
|
||||
<FormDescription>Change the border color of the card.</FormDescription>
|
||||
</div>
|
||||
|
||||
<FormControl>
|
||||
<ColorSelector
|
||||
color={field.value || COLOR_DEFAULTS.cardBorderColor}
|
||||
setColor={(color) => field.onChange(color)}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cardShadowColor.light"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-4">
|
||||
<div>
|
||||
<FormLabel>Card shadow color</FormLabel>
|
||||
<FormDescription>Change the shadow color of the card.</FormDescription>
|
||||
</div>
|
||||
|
||||
<FormControl>
|
||||
<ColorSelector
|
||||
color={field.value || COLOR_DEFAULTS.cardShadowColor}
|
||||
setColor={(color) => field.onChange(color)}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"cardArrangement"}
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<div>
|
||||
<FormLabel>Card Arrangement for {surveyTypeDerived} Surveys</FormLabel>
|
||||
|
||||
<FormDescription>
|
||||
How funky do you want your cards in {surveyTypeDerived} Surveys
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<CardArrangementTabs
|
||||
key={isAppSurvey ? "app" : "link"}
|
||||
surveyType={isAppSurvey ? "app" : "link"}
|
||||
activeCardArrangement={isAppSurvey ? appCardArrangement : linkCardArrangement}
|
||||
setActiveCardArrangement={(value, type) => {
|
||||
type === "app"
|
||||
? form.setValue("cardArrangement.appSurveys", value)
|
||||
: form.setValue("cardArrangement.linkSurveys", value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-1">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="hideProgressBar"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex w-full items-center gap-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
id="hideProgressBar"
|
||||
checked={!!field.value}
|
||||
onCheckedChange={(checked) => field.onChange(checked)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<div>
|
||||
<FormLabel>Hide progress bar</FormLabel>
|
||||
<FormDescription>Disable the visibility of survey progress.</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isLogoVisible && (!surveyType || surveyType === "link") && !isSettingsPage && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isLogoHidden"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex w-full items-center gap-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
id="isLogoHidden"
|
||||
checked={!!field.value}
|
||||
onCheckedChange={(checked) => field.onChange(checked)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<div>
|
||||
<FormLabel>
|
||||
Hide logo
|
||||
<Badge text="Link Surveys" type="gray" size="normal" />
|
||||
</FormLabel>
|
||||
<FormDescription>Hides the logo in this specific survey</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!surveyType || isAppSurvey) && (
|
||||
<div className="flex max-w-xs flex-col gap-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="highlightBorderColor"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex w-full flex-col gap-2 space-y-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<Switch
|
||||
id="highlightBorderColor"
|
||||
checked={!!field.value}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!checked) {
|
||||
field.onChange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
field.onChange({
|
||||
light: COLOR_DEFAULTS.highlightBorderColor,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<div>
|
||||
<FormLabel>Add highlight border</FormLabel>
|
||||
<FormDescription className="text-xs font-normal text-slate-500">
|
||||
Add an outer border to your survey card.
|
||||
</FormDescription>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!!field.value && (
|
||||
<FormControl>
|
||||
<ColorPicker
|
||||
color={field.value?.light ?? COLOR_DEFAULTS.highlightBorderColor}
|
||||
onChange={(color: string) =>
|
||||
field.onChange({
|
||||
...field.value,
|
||||
light: color,
|
||||
})
|
||||
}
|
||||
containerClass="my-0"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
@@ -15,6 +16,7 @@ interface ConsentQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const ConsentQuestionForm = ({
|
||||
@@ -25,6 +27,7 @@ export const ConsentQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: ConsentQuestionFormProps): JSX.Element => {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
|
||||
@@ -32,6 +35,7 @@ export const ConsentQuestionForm = ({
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
label="Question*"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
@@ -39,6 +43,7 @@ export const ConsentQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div className="mt-3">
|
||||
@@ -61,7 +66,7 @@ export const ConsentQuestionForm = ({
|
||||
|
||||
<QuestionFormInput
|
||||
id="label"
|
||||
label="Checkbox Label"
|
||||
label="Checkbox Label*"
|
||||
placeholder="I agree to the terms and conditions"
|
||||
value={question.label}
|
||||
localSurvey={localSurvey}
|
||||
@@ -70,6 +75,7 @@ export const ConsentQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
@@ -17,6 +18,7 @@ interface IDateQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
const dateOptions = [
|
||||
@@ -42,6 +44,7 @@ export const DateQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: IDateQuestionFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -51,12 +54,14 @@ export const DateQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
label={"Question*"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
@@ -65,12 +70,14 @@ export const DateQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
label={"Description"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
@@ -19,6 +20,7 @@ interface EditThankYouCardProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const EditThankYouCard = ({
|
||||
@@ -29,6 +31,7 @@ export const EditThankYouCard = ({
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: EditThankYouCardProps) => {
|
||||
// const [open, setOpen] = useState(false);
|
||||
let open = activeQuestionId == "end";
|
||||
@@ -109,7 +112,7 @@ export const EditThankYouCard = ({
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
label="Headline"
|
||||
label="Note*"
|
||||
value={localSurvey?.thankYouCard?.headline}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={localSurvey.questions.length}
|
||||
@@ -117,17 +120,20 @@ export const EditThankYouCard = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={localSurvey.thankYouCard.subheader}
|
||||
label={"Description"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={localSurvey.questions.length}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
@@ -170,6 +176,7 @@ export const EditThankYouCard = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useState } from "react";
|
||||
|
||||
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { FileInput } from "@formbricks/ui/FileInput";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
@@ -20,6 +21,7 @@ interface EditWelcomeCardProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const EditWelcomeCard = ({
|
||||
@@ -30,6 +32,7 @@ export const EditWelcomeCard = ({
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: EditWelcomeCardProps) => {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
const path = usePathname();
|
||||
@@ -123,13 +126,14 @@ export const EditWelcomeCard = ({
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={localSurvey.welcomeCard.headline}
|
||||
label="Headline"
|
||||
label="Note*"
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={-1}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
@@ -164,6 +168,8 @@ export const EditWelcomeCard = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
label={`"Next" Button Label`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,8 @@ import { toast } from "react-hot-toast";
|
||||
|
||||
import { extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { createI18nString } from "@formbricks/lib/i18n/utils";
|
||||
import { useGetBillingInfo } from "@formbricks/lib/team/hooks/useGetBillingInfo";
|
||||
import { useGetBillingInfo } from "@formbricks/lib/organization/hooks/useGetBillingInfo";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/common";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey, TSurveyFileUploadQuestion } from "@formbricks/types/surveys";
|
||||
@@ -25,6 +26,7 @@ interface FileUploadFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const FileUploadQuestionForm = ({
|
||||
@@ -36,6 +38,7 @@ export const FileUploadQuestionForm = ({
|
||||
product,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: FileUploadFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const [extension, setExtension] = useState("");
|
||||
@@ -43,7 +46,7 @@ export const FileUploadQuestionForm = ({
|
||||
billingInfo,
|
||||
error: billingInfoError,
|
||||
isLoading: billingInfoLoading,
|
||||
} = useGetBillingInfo(product?.teamId ?? "");
|
||||
} = useGetBillingInfo(product?.organizationId ?? "");
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
@@ -115,12 +118,14 @@ export const FileUploadQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
label={"Question*"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
@@ -129,12 +134,14 @@ export const FileUploadQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
label={"Description"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,74 +3,50 @@
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon, SparklesIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
|
||||
import { mixColor } from "@formbricks/lib/utils";
|
||||
import { mixColor } from "@formbricks/lib/utils/colors";
|
||||
import { TProductStyling } from "@formbricks/types/product";
|
||||
import { TSurveyStyling } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { ColorSelectorWithLabel } from "@formbricks/ui/Styling";
|
||||
import { ColorSelector } from "@formbricks/ui/ColorSelector";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@formbricks/ui/Form";
|
||||
|
||||
type FormStylingSettingsProps = {
|
||||
styling: TSurveyStyling | TProductStyling | null;
|
||||
setStyling: React.Dispatch<React.SetStateAction<TSurveyStyling | TProductStyling>>;
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isSettingsPage?: boolean;
|
||||
disabled?: boolean;
|
||||
form: UseFormReturn<TProductStyling | TSurveyStyling>;
|
||||
};
|
||||
|
||||
export const FormStylingSettings = ({
|
||||
styling,
|
||||
setStyling,
|
||||
open,
|
||||
isSettingsPage = false,
|
||||
disabled = false,
|
||||
setOpen,
|
||||
form,
|
||||
}: FormStylingSettingsProps) => {
|
||||
const brandColor = styling?.brandColor?.light || COLOR_DEFAULTS.brandColor;
|
||||
const setBrandColor = (color: string) => {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
brandColor: {
|
||||
...(prev.brandColor ?? {}),
|
||||
light: color,
|
||||
},
|
||||
}));
|
||||
};
|
||||
const brandColor = form.watch("brandColor.light") || COLOR_DEFAULTS.brandColor;
|
||||
const background = form.watch("background");
|
||||
const highlightBorderColor = form.watch("highlightBorderColor");
|
||||
|
||||
const questionColor = styling?.questionColor?.light || COLOR_DEFAULTS.questionColor;
|
||||
const setQuestionColor = (color: string) => {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
questionColor: {
|
||||
...(prev.questionColor ?? {}),
|
||||
light: color,
|
||||
},
|
||||
}));
|
||||
const setQuestionColor = (color: string) => form.setValue("questionColor.light", color);
|
||||
const setInputColor = (color: string) => form.setValue("inputColor.light", color);
|
||||
const setInputBorderColor = (color: string) => form.setValue("inputBorderColor.light", color);
|
||||
const setCardBackgroundColor = (color: string) => form.setValue("cardBackgroundColor.light", color);
|
||||
const setCardBorderColor = (color: string) => form.setValue("cardBorderColor.light", color);
|
||||
const setCardShadowColor = (color: string) => form.setValue("cardShadowColor.light", color);
|
||||
const setBackgroundColor = (color: string) => {
|
||||
form.setValue("background", {
|
||||
bg: color,
|
||||
bgType: "color",
|
||||
});
|
||||
};
|
||||
|
||||
const inputColor = styling?.inputColor?.light || COLOR_DEFAULTS.inputColor;
|
||||
const setInputColor = (color: string) => {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
inputColor: {
|
||||
...(prev.inputColor ?? {}),
|
||||
light: color,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const inputBorderColor = styling?.inputBorderColor?.light || COLOR_DEFAULTS.inputBorderColor;
|
||||
const setInputBorderColor = (color: string) => {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
inputBorderColor: {
|
||||
...(prev.inputBorderColor ?? {}),
|
||||
light: color,
|
||||
},
|
||||
}));
|
||||
const setHighlightBorderColor = (color: string) => {
|
||||
form.setValue("highlightBorderColor", { light: mixColor(color, "#ffffff", 0.25) });
|
||||
};
|
||||
|
||||
const suggestColors = () => {
|
||||
@@ -79,42 +55,16 @@ export const FormStylingSettings = ({
|
||||
setInputColor(mixColor(brandColor, "#ffffff", 0.92));
|
||||
setInputBorderColor(mixColor(brandColor, "#ffffff", 0.6));
|
||||
|
||||
// card background, border and shadow colors
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
cardBackgroundColor: {
|
||||
...(prev.cardBackgroundColor ?? {}),
|
||||
light: mixColor(brandColor, "#ffffff", 0.97),
|
||||
},
|
||||
cardBorderColor: {
|
||||
...(prev.cardBorderColor ?? {}),
|
||||
light: mixColor(brandColor, "#ffffff", 0.8),
|
||||
},
|
||||
cardShadowColor: {
|
||||
...(prev.cardShadowColor ?? {}),
|
||||
light: brandColor,
|
||||
},
|
||||
}));
|
||||
setCardBackgroundColor(mixColor(brandColor, "#ffffff", 0.97));
|
||||
setCardBorderColor(mixColor(brandColor, "#ffffff", 0.8));
|
||||
setCardShadowColor(brandColor);
|
||||
|
||||
if (!styling?.background || styling?.background?.bgType === "color") {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
background: {
|
||||
...(prev.background ?? {}),
|
||||
bg: mixColor(brandColor, "#ffffff", 0.855),
|
||||
bgType: "color",
|
||||
},
|
||||
}));
|
||||
if (!background || background?.bgType === "color") {
|
||||
setBackgroundColor(mixColor(brandColor, "#ffffff", 0.855));
|
||||
}
|
||||
|
||||
if (styling?.highlightBorderColor) {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
highlightBorderColor: {
|
||||
...(prev.highlightBorderColor ?? {}),
|
||||
light: mixColor(brandColor, "#ffffff", 0.25),
|
||||
},
|
||||
}));
|
||||
if (!highlightBorderColor) {
|
||||
setHighlightBorderColor(brandColor);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -159,14 +109,28 @@ export const FormStylingSettings = ({
|
||||
|
||||
<div className="flex flex-col gap-6 p-6 pt-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<ColorSelectorWithLabel
|
||||
label="Brand color"
|
||||
color={brandColor}
|
||||
setColor={setBrandColor}
|
||||
description="Change the brand color of the survey"
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="brandColor.light"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-4">
|
||||
<div>
|
||||
<FormLabel>Brand color</FormLabel>
|
||||
<FormDescription>Change the brand color of the survey.</FormDescription>
|
||||
</div>
|
||||
|
||||
<FormControl>
|
||||
<ColorSelector
|
||||
color={field.value || COLOR_DEFAULTS.brandColor}
|
||||
setColor={(color) => field.onChange(color)}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
EndIcon={SparklesIcon}
|
||||
@@ -176,25 +140,64 @@ export const FormStylingSettings = ({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ColorSelectorWithLabel
|
||||
label="Text color"
|
||||
color={questionColor}
|
||||
setColor={setQuestionColor}
|
||||
description="Change the text color of the questions, descriptions and answer options."
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="questionColor.light"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-4">
|
||||
<div>
|
||||
<FormLabel>Question color</FormLabel>
|
||||
<FormDescription>Change the question color of the survey.</FormDescription>
|
||||
</div>
|
||||
|
||||
<FormControl>
|
||||
<ColorSelector
|
||||
color={field.value || COLOR_DEFAULTS.questionColor}
|
||||
setColor={(color) => field.onChange(color)}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<ColorSelectorWithLabel
|
||||
label="Input color"
|
||||
color={inputColor}
|
||||
setColor={setInputColor}
|
||||
description="Change the background color of the input fields."
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="inputColor.light"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-4">
|
||||
<div>
|
||||
<FormLabel>Input color</FormLabel>
|
||||
<FormDescription>Change the background color of the input fields.</FormDescription>
|
||||
</div>
|
||||
|
||||
<FormControl>
|
||||
<ColorSelector
|
||||
color={field.value || COLOR_DEFAULTS.inputColor}
|
||||
setColor={(color: string) => field.onChange(color)}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<ColorSelectorWithLabel
|
||||
label="Input border color"
|
||||
color={inputBorderColor}
|
||||
setColor={setInputBorderColor}
|
||||
description="Change the border color of the input fields."
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="inputBorderColor.light"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-4">
|
||||
<div>
|
||||
<FormLabel>Input border color</FormLabel>
|
||||
<FormDescription>Change the border color of the input fields.</FormDescription>
|
||||
</div>
|
||||
|
||||
<FormControl>
|
||||
<ColorSelector
|
||||
color={field.value || COLOR_DEFAULTS.inputBorderColor}
|
||||
setColor={(color: string) => field.onChange(color)}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
|
||||
@@ -94,18 +94,18 @@ export const HiddenFieldsCard = ({
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-6">
|
||||
<div className="flex gap-2">
|
||||
{localSurvey.hiddenFields?.fieldIds && localSurvey.hiddenFields?.fieldIds?.length > 0 ? (
|
||||
localSurvey.hiddenFields?.fieldIds?.map((question) => {
|
||||
localSurvey.hiddenFields?.fieldIds?.map((fieldId) => {
|
||||
return (
|
||||
<Tag
|
||||
key={question}
|
||||
key={fieldId}
|
||||
onDelete={() => {
|
||||
updateSurvey({
|
||||
enabled: true,
|
||||
fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== question),
|
||||
fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== fieldId),
|
||||
});
|
||||
}}
|
||||
tagId={question}
|
||||
tagName={question}
|
||||
tagId={fieldId}
|
||||
tagName={fieldId}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -11,7 +11,8 @@ import { toast } from "react-hot-toast";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
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 {
|
||||
TSurvey,
|
||||
TSurveyLogic,
|
||||
@@ -36,6 +37,7 @@ interface LogicEditorProps {
|
||||
questionIdx: number;
|
||||
question: TSurveyQuestion;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
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>("");
|
||||
localSurvey = useMemo(() => {
|
||||
return checkForRecallInHeadline(localSurvey, "default");
|
||||
}, [localSurvey]);
|
||||
return replaceHeadlineRecall(localSurvey, "default", attributeClasses);
|
||||
}, [localSurvey, attributeClasses]);
|
||||
|
||||
const questionValues = useMemo(() => {
|
||||
if ("choices" in question) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { createI18nString, extractLanguageCodes, getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
@@ -21,6 +22,7 @@ interface MatrixQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const MatrixQuestionForm = ({
|
||||
@@ -31,6 +33,7 @@ export const MatrixQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: MatrixQuestionFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const languageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -104,12 +107,14 @@ export const MatrixQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
label={"Question*"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
@@ -118,12 +123,14 @@ export const MatrixQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
label={"Description"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -158,6 +165,7 @@ export const MatrixQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
key={`row-${index}`}
|
||||
id={`row-${index}`}
|
||||
label={""}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
value={question.rows[index]}
|
||||
@@ -168,6 +176,7 @@ export const MatrixQuestionForm = ({
|
||||
isInvalid={
|
||||
isInvalid && !isLabelValidForAllLanguages(question.rows[index], localSurvey.languages)
|
||||
}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
{question.rows.length > 2 && (
|
||||
<TrashIcon
|
||||
@@ -199,6 +208,7 @@ export const MatrixQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
key={`column-${index}`}
|
||||
id={`column-${index}`}
|
||||
label={""}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
value={question.columns[index]}
|
||||
@@ -209,6 +219,7 @@ export const MatrixQuestionForm = ({
|
||||
isInvalid={
|
||||
isInvalid && !isLabelValidForAllLanguages(question.columns[index], localSurvey.languages)
|
||||
}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
{question.columns.length > 2 && (
|
||||
<TrashIcon
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import {
|
||||
TI18nString,
|
||||
TShuffleOption,
|
||||
@@ -31,6 +32,7 @@ interface OpenQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const MultipleChoiceQuestionForm = ({
|
||||
@@ -41,6 +43,7 @@ export const MultipleChoiceQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: OpenQuestionFormProps): JSX.Element => {
|
||||
const lastChoiceRef = useRef<HTMLInputElement>(null);
|
||||
const [isNew, setIsNew] = useState(true);
|
||||
@@ -184,12 +187,14 @@ export const MultipleChoiceQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
label={"Question*"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -199,12 +204,14 @@ export const MultipleChoiceQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
label={"Description"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -237,7 +244,7 @@ export const MultipleChoiceQuestionForm = ({
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="choices">Options</Label>
|
||||
<Label htmlFor="choices">Options*</Label>
|
||||
<div className="mt-2" id="choices">
|
||||
<DndContext
|
||||
onDragEnd={(event) => {
|
||||
@@ -283,6 +290,7 @@ export const MultipleChoiceQuestionForm = ({
|
||||
question={question}
|
||||
updateQuestion={updateQuestion}
|
||||
surveyLanguageCodes={surveyLanguageCodes}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyNPSQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
@@ -17,6 +18,7 @@ interface NPSQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const NPSQuestionForm = ({
|
||||
@@ -28,6 +30,7 @@ export const NPSQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: NPSQuestionFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -36,12 +39,14 @@ export const NPSQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
label={"Question*"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -51,12 +56,14 @@ export const NPSQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
label={"Description"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -93,24 +100,28 @@ export const NPSQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="lowerLabel"
|
||||
value={question.lowerLabel}
|
||||
label={"Lower Label"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<QuestionFormInput
|
||||
id="upperLabel"
|
||||
value={question.upperLabel}
|
||||
label={"Upper Label"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,6 +131,7 @@ export const NPSQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
label={`"Next" Button Label`}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
@@ -128,6 +140,7 @@ export const NPSQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyOpenTextQuestion,
|
||||
@@ -39,6 +40,7 @@ interface OpenQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const OpenQuestionForm = ({
|
||||
@@ -49,6 +51,7 @@ export const OpenQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: OpenQuestionFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const defaultPlaceholder = getPlaceholderByInputType(question.inputType ?? "text");
|
||||
@@ -73,6 +76,8 @@ export const OpenQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
label={"Question*"}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -88,6 +93,8 @@ export const OpenQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
label={"Description"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -131,6 +138,8 @@ export const OpenQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
label={"Placeholder"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyPictureSelectionQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { FileInput } from "@formbricks/ui/FileInput";
|
||||
@@ -20,6 +21,7 @@ interface PictureSelectionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const PictureSelectionForm = ({
|
||||
@@ -30,6 +32,7 @@ export const PictureSelectionForm = ({
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
isInvalid,
|
||||
attributeClasses,
|
||||
}: PictureSelectionFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const environmentId = localSurvey.environmentId;
|
||||
@@ -39,6 +42,7 @@ export const PictureSelectionForm = ({
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
label={"Question*"}
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
@@ -46,6 +50,7 @@ export const PictureSelectionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
@@ -54,12 +59,14 @@ export const PictureSelectionForm = ({
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
label={"Description"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { getTSurveyQuestionTypeName } from "@/app/lib/questions";
|
||||
import { QUESTIONS_ICON_MAP, getTSurveyQuestionTypeName } from "@/app/lib/questions";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import {
|
||||
ArrowUpFromLineIcon,
|
||||
CalendarDaysIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
Grid3X3Icon,
|
||||
GripIcon,
|
||||
HomeIcon,
|
||||
ImageIcon,
|
||||
ListIcon,
|
||||
MessageSquareTextIcon,
|
||||
MousePointerClickIcon,
|
||||
PhoneIcon,
|
||||
PresentationIcon,
|
||||
Rows3Icon,
|
||||
StarIcon,
|
||||
} from "lucide-react";
|
||||
import { ChevronDownIcon, ChevronRightIcon, GripIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TI18nString, TSurvey, TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
@@ -44,7 +28,7 @@ import { MultipleChoiceQuestionForm } from "./MultipleChoiceQuestionForm";
|
||||
import { NPSQuestionForm } from "./NPSQuestionForm";
|
||||
import { OpenQuestionForm } from "./OpenQuestionForm";
|
||||
import { PictureSelectionForm } from "./PictureSelectionForm";
|
||||
import { QuestionDropdown } from "./QuestionMenu";
|
||||
import { QuestionMenu } from "./QuestionMenu";
|
||||
import { RatingQuestionForm } from "./RatingQuestionForm";
|
||||
|
||||
interface QuestionCardProps {
|
||||
@@ -62,6 +46,8 @@ interface QuestionCardProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
addQuestion: (question: any, index?: number) => void;
|
||||
}
|
||||
|
||||
export const QuestionCard = ({
|
||||
@@ -79,6 +65,8 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
isInvalid,
|
||||
attributeClasses,
|
||||
addQuestion,
|
||||
}: QuestionCardProps) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: question.id,
|
||||
@@ -151,7 +139,8 @@ export const QuestionCard = ({
|
||||
"flex flex-row rounded-lg bg-white transition-all duration-300 ease-in-out"
|
||||
)}
|
||||
ref={setNodeRef}
|
||||
style={style}>
|
||||
style={style}
|
||||
id={question.id}>
|
||||
<div
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
@@ -183,43 +172,25 @@ export const QuestionCard = ({
|
||||
<div>
|
||||
<div className="inline-flex">
|
||||
<div className="-ml-0.5 mr-3 h-6 min-w-[1.5rem] text-slate-400">
|
||||
{question.type === TSurveyQuestionType.FileUpload ? (
|
||||
<ArrowUpFromLineIcon className="h-5 w-5" />
|
||||
) : question.type === TSurveyQuestionType.OpenText ? (
|
||||
<MessageSquareTextIcon className="h-5 w-5" />
|
||||
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
|
||||
<Rows3Icon className="h-5 w-5" />
|
||||
) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
|
||||
<ListIcon className="h-5 w-5" />
|
||||
) : question.type === TSurveyQuestionType.NPS ? (
|
||||
<PresentationIcon className="h-5 w-5" />
|
||||
) : question.type === TSurveyQuestionType.CTA ? (
|
||||
<MousePointerClickIcon className="h-5 w-5" />
|
||||
) : question.type === TSurveyQuestionType.Rating ? (
|
||||
<StarIcon className="h-5 w-5" />
|
||||
) : question.type === TSurveyQuestionType.Consent ? (
|
||||
<CheckIcon className="h-5 w-5" />
|
||||
) : question.type === TSurveyQuestionType.PictureSelection ? (
|
||||
<ImageIcon className="h-5 w-5" />
|
||||
) : question.type === TSurveyQuestionType.Date ? (
|
||||
<CalendarDaysIcon className="h-5 w-5" />
|
||||
) : question.type === TSurveyQuestionType.Cal ? (
|
||||
<PhoneIcon className="h-5 w-5" />
|
||||
) : question.type === TSurveyQuestionType.Matrix ? (
|
||||
<Grid3X3Icon className="h-5 w-5" />
|
||||
) : question.type === TSurveyQuestionType.Address ? (
|
||||
<HomeIcon className="h-5 w-5" />
|
||||
) : null}
|
||||
{QUESTIONS_ICON_MAP[question.type]}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold">
|
||||
{recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
]
|
||||
{recallToHeadline(
|
||||
question.headline,
|
||||
localSurvey,
|
||||
true,
|
||||
selectedLanguageCode,
|
||||
attributeClasses
|
||||
)[selectedLanguageCode]
|
||||
? formatTextWithSlashes(
|
||||
recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
] ?? ""
|
||||
recallToHeadline(
|
||||
question.headline,
|
||||
localSurvey,
|
||||
true,
|
||||
selectedLanguageCode,
|
||||
attributeClasses
|
||||
)[selectedLanguageCode] ?? ""
|
||||
)
|
||||
: getTSurveyQuestionTypeName(question.type)}
|
||||
</p>
|
||||
@@ -230,12 +201,16 @@ export const QuestionCard = ({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<QuestionDropdown
|
||||
<QuestionMenu
|
||||
questionIdx={questionIdx}
|
||||
lastQuestion={lastQuestion}
|
||||
duplicateQuestion={duplicateQuestion}
|
||||
deleteQuestion={deleteQuestion}
|
||||
moveQuestion={moveQuestion}
|
||||
question={question}
|
||||
product={product}
|
||||
updateQuestion={updateQuestion}
|
||||
addQuestion={addQuestion}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,6 +226,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
|
||||
<MultipleChoiceQuestionForm
|
||||
@@ -262,6 +238,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
|
||||
<MultipleChoiceQuestionForm
|
||||
@@ -273,6 +250,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.NPS ? (
|
||||
<NPSQuestionForm
|
||||
@@ -284,6 +262,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.CTA ? (
|
||||
<CTAQuestionForm
|
||||
@@ -295,6 +274,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Rating ? (
|
||||
<RatingQuestionForm
|
||||
@@ -306,6 +286,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Consent ? (
|
||||
<ConsentQuestionForm
|
||||
@@ -316,6 +297,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Date ? (
|
||||
<DateQuestionForm
|
||||
@@ -327,6 +309,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.PictureSelection ? (
|
||||
<PictureSelectionForm
|
||||
@@ -338,6 +321,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.FileUpload ? (
|
||||
<FileUploadQuestionForm
|
||||
@@ -350,6 +334,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Cal ? (
|
||||
<CalQuestionForm
|
||||
@@ -361,6 +346,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Matrix ? (
|
||||
<MatrixQuestionForm
|
||||
@@ -372,6 +358,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Address ? (
|
||||
<AddressQuestionForm
|
||||
@@ -383,6 +370,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : null}
|
||||
<div className="mt-4">
|
||||
@@ -405,6 +393,7 @@ export const QuestionCard = ({
|
||||
<QuestionFormInput
|
||||
id="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
label={`"Next" Button Label`}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
@@ -423,12 +412,14 @@ export const QuestionCard = ({
|
||||
if (questionIdx === localSurvey.questions.length - 1) return;
|
||||
updateEmptyNextButtonLabels(translatedNextButtonLabel);
|
||||
}}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
{questionIdx !== 0 && (
|
||||
<QuestionFormInput
|
||||
id="backButtonLabel"
|
||||
value={question.backButtonLabel}
|
||||
label={`"Back" Button Label`}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
@@ -437,6 +428,7 @@ export const QuestionCard = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -448,6 +440,7 @@ export const QuestionCard = ({
|
||||
<QuestionFormInput
|
||||
id="backButtonLabel"
|
||||
value={question.backButtonLabel}
|
||||
label={`"Back" Button Label`}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
@@ -456,6 +449,7 @@ export const QuestionCard = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -465,6 +459,7 @@ export const QuestionCard = ({
|
||||
questionIdx={questionIdx}
|
||||
localSurvey={localSurvey}
|
||||
updateQuestion={updateQuestion}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowDownIcon, ArrowUpIcon, CopyIcon, TrashIcon } from "lucide-react";
|
||||
import { QUESTIONS_ICON_MAP, QUESTIONS_NAME_MAP, getQuestionDefaults } from "@/app/lib/questions";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { ArrowDownIcon, ArrowUpIcon, CopyIcon, EllipsisIcon, TrashIcon } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { ConfirmationModal } from "@formbricks/ui/ConfirmationModal";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@formbricks/ui/DropdownMenu";
|
||||
|
||||
interface QuestionDropdownProps {
|
||||
questionIdx: number;
|
||||
@@ -8,39 +24,87 @@ interface QuestionDropdownProps {
|
||||
duplicateQuestion: (questionIdx: number) => void;
|
||||
deleteQuestion: (questionIdx: number) => void;
|
||||
moveQuestion: (questionIdx: number, up: boolean) => void;
|
||||
question: TSurveyQuestion;
|
||||
product: TProduct;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
addQuestion: (question: any, index?: number) => void;
|
||||
}
|
||||
|
||||
export const QuestionDropdown = ({
|
||||
export const QuestionMenu = ({
|
||||
questionIdx,
|
||||
lastQuestion,
|
||||
duplicateQuestion,
|
||||
deleteQuestion,
|
||||
moveQuestion,
|
||||
product,
|
||||
question,
|
||||
updateQuestion,
|
||||
addQuestion,
|
||||
}: QuestionDropdownProps) => {
|
||||
const [logicWarningModal, setLogicWarningModal] = useState(false);
|
||||
const [changeToType, setChangeToType] = useState(question.type);
|
||||
|
||||
const changeQuestionType = (type: TSurveyQuestionType) => {
|
||||
const { headline, required, subheader, imageUrl, videoUrl, buttonLabel, backButtonLabel } = question;
|
||||
|
||||
const questionDefaults = getQuestionDefaults(type, product);
|
||||
|
||||
// if going from single select to multi select or vice versa, we need to keep the choices as well
|
||||
|
||||
if (
|
||||
(type === TSurveyQuestionType.MultipleChoiceSingle &&
|
||||
question.type === TSurveyQuestionType.MultipleChoiceMulti) ||
|
||||
(type === TSurveyQuestionType.MultipleChoiceMulti &&
|
||||
question.type === TSurveyQuestionType.MultipleChoiceSingle)
|
||||
) {
|
||||
updateQuestion(questionIdx, {
|
||||
choices: question.choices,
|
||||
type,
|
||||
logic: undefined,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateQuestion(questionIdx, {
|
||||
...questionDefaults,
|
||||
type,
|
||||
headline,
|
||||
subheader,
|
||||
required,
|
||||
imageUrl,
|
||||
videoUrl,
|
||||
buttonLabel,
|
||||
backButtonLabel,
|
||||
logic: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const addQuestionBelow = (type: TSurveyQuestionType) => {
|
||||
const questionDefaults = getQuestionDefaults(type, product);
|
||||
|
||||
addQuestion(
|
||||
{
|
||||
...questionDefaults,
|
||||
type,
|
||||
id: createId(),
|
||||
required: true,
|
||||
},
|
||||
questionIdx + 1
|
||||
);
|
||||
|
||||
// scroll to the new question
|
||||
const section = document.getElementById(`${question.id}`);
|
||||
section?.scrollIntoView({ behavior: "smooth", block: "end", inline: "end" });
|
||||
};
|
||||
|
||||
const onConfirm = () => {
|
||||
changeQuestionType(changeToType);
|
||||
setLogicWarningModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
<ArrowUpIcon
|
||||
className={`h-4 cursor-pointer text-slate-500 hover:text-slate-600 ${
|
||||
questionIdx === 0 ? "opacity-50" : ""
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
if (questionIdx !== 0) {
|
||||
e.stopPropagation();
|
||||
moveQuestion(questionIdx, true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ArrowDownIcon
|
||||
className={`h-4 cursor-pointer text-slate-500 hover:text-slate-600 ${
|
||||
lastQuestion ? "opacity-50" : ""
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
if (!lastQuestion) {
|
||||
e.stopPropagation();
|
||||
moveQuestion(questionIdx, false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<CopyIcon
|
||||
className="h-4 cursor-pointer text-slate-500 hover:text-slate-600"
|
||||
onClick={(e) => {
|
||||
@@ -55,6 +119,114 @@ export const QuestionDropdown = ({
|
||||
deleteQuestion(questionIdx);
|
||||
}}
|
||||
/>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<EllipsisIcon className="h-4 w-4 text-slate-500 hover:text-slate-600" />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent>
|
||||
<div className="flex flex-col">
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<div className="cursor-pointer text-slate-500 hover:text-slate-600">
|
||||
<span className="text-xs text-slate-500">Change question type</span>
|
||||
</div>
|
||||
</DropdownMenuSubTrigger>
|
||||
|
||||
<DropdownMenuSubContent className="ml-4 border border-slate-200">
|
||||
{Object.entries(QUESTIONS_NAME_MAP).map(([type, name]) => {
|
||||
if (type === question.type) return null;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={type}
|
||||
className="min-h-8 cursor-pointer text-slate-500"
|
||||
onClick={() => {
|
||||
setChangeToType(type as TSurveyQuestionType);
|
||||
if (question.logic) {
|
||||
setLogicWarningModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
changeQuestionType(type as TSurveyQuestionType);
|
||||
}}>
|
||||
{QUESTIONS_ICON_MAP[type as TSurveyQuestionType]}
|
||||
<span className="ml-2">{name}</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<div className="cursor-pointer text-slate-500 hover:text-slate-600">
|
||||
<span className="text-xs text-slate-500">Add question below</span>
|
||||
</div>
|
||||
</DropdownMenuSubTrigger>
|
||||
|
||||
<DropdownMenuSubContent className="ml-4 border border-slate-200">
|
||||
{Object.entries(QUESTIONS_NAME_MAP).map(([type, name]) => {
|
||||
if (type === question.type) return null;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={type}
|
||||
className="min-h-8 cursor-pointer text-slate-500"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
addQuestionBelow(type as TSurveyQuestionType);
|
||||
}}>
|
||||
{QUESTIONS_ICON_MAP[type as TSurveyQuestionType]}
|
||||
<span className="ml-2">{name}</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuItem
|
||||
className={`flex min-h-8 cursor-pointer justify-between text-slate-500 hover:text-slate-600 ${
|
||||
questionIdx === 0 ? "opacity-50" : ""
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
if (questionIdx !== 0) {
|
||||
e.stopPropagation();
|
||||
moveQuestion(questionIdx, true);
|
||||
}
|
||||
}}
|
||||
disabled={questionIdx === 0}>
|
||||
<span className="text-xs text-slate-500">Move up</span>
|
||||
<ArrowUpIcon className="h-4 w-4" />
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className={`flex min-h-8 cursor-pointer justify-between text-slate-500 hover:text-slate-600 ${
|
||||
lastQuestion ? "opacity-50" : ""
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
if (!lastQuestion) {
|
||||
e.stopPropagation();
|
||||
moveQuestion(questionIdx, false);
|
||||
}
|
||||
}}
|
||||
disabled={lastQuestion}>
|
||||
<span className="text-xs text-slate-500">Move down</span>
|
||||
<ArrowDownIcon className="h-4 w-4" />
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<ConfirmationModal
|
||||
open={logicWarningModal}
|
||||
setOpen={setLogicWarningModal}
|
||||
title="Changing will cause logic errors"
|
||||
text="Changing the question type will remove the logic conditions from this question"
|
||||
buttonText="Change anyway"
|
||||
onConfirm={onConfirm}
|
||||
buttonVariant="darkCTA"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
@@ -18,6 +19,8 @@ interface QuestionsDraggableProps {
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
invalidQuestions: string[] | null;
|
||||
internalQuestionIdMap: Record<string, string>;
|
||||
attributeClasses: TAttributeClass[];
|
||||
addQuestion: (question: any, index?: number) => void;
|
||||
}
|
||||
|
||||
export const QuestionsDroppable = ({
|
||||
@@ -33,6 +36,8 @@ export const QuestionsDroppable = ({
|
||||
setSelectedLanguageCode,
|
||||
updateQuestion,
|
||||
internalQuestionIdMap,
|
||||
attributeClasses,
|
||||
addQuestion,
|
||||
}: QuestionsDraggableProps) => {
|
||||
return (
|
||||
<div className="group mb-5 grid w-full gap-5">
|
||||
@@ -54,6 +59,8 @@ export const QuestionsDroppable = ({
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
lastQuestion={questionIdx === localSurvey.questions.length - 1}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(question.id) : false}
|
||||
attributeClasses={attributeClasses}
|
||||
addQuestion={addQuestion}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { MultiLanguageCard } from "@formbricks/ee/multiLanguage/components/Multi
|
||||
import { extractLanguageCodes, getLocalizedValue, translateQuestion } from "@formbricks/lib/i18n/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
@@ -43,6 +44,7 @@ interface QuestionsViewProps {
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isMultiLanguageAllowed?: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const QuestionsView = ({
|
||||
@@ -57,6 +59,7 @@ export const QuestionsView = ({
|
||||
selectedLanguageCode,
|
||||
isMultiLanguageAllowed,
|
||||
isFormbricksCloud,
|
||||
attributeClasses,
|
||||
}: QuestionsViewProps) => {
|
||||
const internalQuestionIdMap = useMemo(() => {
|
||||
return localSurvey.questions.reduce((acc, question) => {
|
||||
@@ -145,17 +148,17 @@ export const QuestionsView = ({
|
||||
setbackButtonLabel(updatedAttributes.backButtonLabel);
|
||||
}
|
||||
}
|
||||
// If the value of buttonLabel is equal to {default:""}, then delete buttonLabel key
|
||||
if ("buttonLabel" in updatedAttributes) {
|
||||
const currentButtonLabel = updatedSurvey.questions[questionIdx].buttonLabel;
|
||||
if (
|
||||
currentButtonLabel &&
|
||||
Object.keys(currentButtonLabel).length === 1 &&
|
||||
currentButtonLabel["default"].trim() === ""
|
||||
) {
|
||||
delete updatedSurvey.questions[questionIdx].buttonLabel;
|
||||
const attributesToCheck = ["buttonLabel", "upperLabel", "lowerLabel"];
|
||||
|
||||
// If the value of buttonLabel, lowerLabel or upperLabel is equal to {default:""}, then delete buttonLabel key
|
||||
attributesToCheck.forEach((attribute) => {
|
||||
if (Object.keys(updatedAttributes).includes(attribute)) {
|
||||
const currentLabel = updatedSurvey.questions[questionIdx][attribute];
|
||||
if (currentLabel && Object.keys(currentLabel).length === 1 && currentLabel["default"].trim() === "") {
|
||||
delete updatedSurvey.questions[questionIdx][attribute];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
setLocalSurvey(updatedSurvey);
|
||||
validateSurveyQuestion(updatedSurvey.questions[questionIdx]);
|
||||
};
|
||||
@@ -213,14 +216,19 @@ export const QuestionsView = ({
|
||||
toast.success("Question duplicated.");
|
||||
};
|
||||
|
||||
const addQuestion = (question: any) => {
|
||||
const addQuestion = (question: any, index?: number) => {
|
||||
const updatedSurvey = { ...localSurvey };
|
||||
if (backButtonLabel) {
|
||||
question.backButtonLabel = backButtonLabel;
|
||||
}
|
||||
const languageSymbols = extractLanguageCodes(localSurvey.languages);
|
||||
const translatedQuestion = translateQuestion(question, languageSymbols);
|
||||
updatedSurvey.questions.push({ ...translatedQuestion, isDraft: true });
|
||||
|
||||
if (index) {
|
||||
updatedSurvey.questions.splice(index, 0, { ...translatedQuestion, isDraft: true });
|
||||
} else {
|
||||
updatedSurvey.questions.push({ ...translatedQuestion, isDraft: true });
|
||||
}
|
||||
|
||||
setLocalSurvey(updatedSurvey);
|
||||
setActiveQuestionId(question.id);
|
||||
@@ -339,6 +347,7 @@ export const QuestionsView = ({
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes("start") : false}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -356,6 +365,8 @@ export const QuestionsView = ({
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
invalidQuestions={invalidQuestions}
|
||||
internalQuestionIdMap={internalQuestionIdMap}
|
||||
attributeClasses={attributeClasses}
|
||||
addQuestion={addQuestion}
|
||||
/>
|
||||
</DndContext>
|
||||
|
||||
@@ -369,16 +380,15 @@ export const QuestionsView = ({
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes("end") : false}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
{localSurvey.type === "link" ? (
|
||||
<HiddenFieldsCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
activeQuestionId={activeQuestionId}
|
||||
/>
|
||||
) : null}
|
||||
<HiddenFieldsCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
activeQuestionId={activeQuestionId}
|
||||
/>
|
||||
|
||||
<MultiLanguageCard
|
||||
localSurvey={localSurvey}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { HashIcon, PlusIcon, SmileIcon, StarIcon, TrashIcon } from "lucide-react
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyRatingQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
@@ -18,6 +19,7 @@ interface RatingQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const RatingQuestionForm = ({
|
||||
@@ -28,6 +30,7 @@ export const RatingQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: RatingQuestionFormProps) => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -37,12 +40,14 @@ export const RatingQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
label={"Question*"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -52,12 +57,14 @@ export const RatingQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
label={"Description"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -129,12 +136,14 @@ export const RatingQuestionForm = ({
|
||||
id="lowerLabel"
|
||||
placeholder="Not good"
|
||||
value={question.lowerLabel}
|
||||
label={"Lower Label"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
@@ -142,12 +151,14 @@ export const RatingQuestionForm = ({
|
||||
id="upperLabel"
|
||||
placeholder="Very satisfied"
|
||||
value={question.upperLabel}
|
||||
label={"Upper Label"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,6 +169,7 @@ export const RatingQuestionForm = ({
|
||||
<QuestionFormInput
|
||||
id="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
label={`"Next" Button Label`}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
placeholder={"skip"}
|
||||
@@ -165,6 +177,7 @@ export const RatingQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import toast from "react-hot-toast";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { createI18nString } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
@@ -35,6 +36,7 @@ interface ChoiceProps {
|
||||
question: TSurveyMultipleChoiceQuestion;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyMultipleChoiceQuestion>) => void;
|
||||
surveyLanguageCodes: string[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const SelectQuestionChoice = ({
|
||||
@@ -54,6 +56,7 @@ export const SelectQuestionChoice = ({
|
||||
question,
|
||||
surveyLanguageCodes,
|
||||
updateQuestion,
|
||||
attributeClasses,
|
||||
}: ChoiceProps) => {
|
||||
const isDragDisabled = choice.id === "other";
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
||||
@@ -81,6 +84,7 @@ export const SelectQuestionChoice = ({
|
||||
key={choice.id}
|
||||
id={`choice-${choiceIdx}`}
|
||||
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
|
||||
label={""}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
value={choice.label}
|
||||
@@ -100,12 +104,14 @@ export const SelectQuestionChoice = ({
|
||||
isInvalid && !isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
|
||||
}
|
||||
className={`${choice.id === "other" ? "border border-dashed" : ""} mt-0`}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
{choice.id === "other" && (
|
||||
<QuestionFormInput
|
||||
id="otherOptionPlaceholder"
|
||||
localSurvey={localSurvey}
|
||||
placeholder={"Please specify"}
|
||||
label={""}
|
||||
questionIdx={questionIdx}
|
||||
value={
|
||||
question.otherOptionPlaceholder
|
||||
@@ -119,6 +125,7 @@ export const SelectQuestionChoice = ({
|
||||
isInvalid && !isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
|
||||
}
|
||||
className="border border-dashed"
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import { RotateCcwIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { UseFormReturn, useForm, useWatch } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TProduct, TProductStyling } from "@formbricks/types/product";
|
||||
import { TBaseStyling } from "@formbricks/types/styling";
|
||||
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys";
|
||||
import { AlertDialog } from "@formbricks/ui/AlertDialog";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormProvider,
|
||||
} from "@formbricks/ui/Form";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
|
||||
import { BackgroundStylingCard } from "./BackgroundStylingCard";
|
||||
@@ -39,9 +50,54 @@ export const StylingView = ({
|
||||
setLocalStylingChanges,
|
||||
isUnsplashConfigured,
|
||||
}: StylingViewProps) => {
|
||||
const [overwriteThemeStyling, setOverwriteThemeStyling] = useState(
|
||||
localSurvey?.styling?.overwriteThemeStyling ?? false
|
||||
);
|
||||
const stylingDefaults: TBaseStyling = useMemo(() => {
|
||||
let stylingDefaults: TBaseStyling;
|
||||
const isOverwriteEnabled = localSurvey.styling?.overwriteThemeStyling ?? false;
|
||||
|
||||
if (isOverwriteEnabled) {
|
||||
const { overwriteThemeStyling, ...baseSurveyStyles } = localSurvey.styling ?? {};
|
||||
stylingDefaults = baseSurveyStyles;
|
||||
} else {
|
||||
const { allowStyleOverwrite, ...baseProductStyles } = product.styling ?? {};
|
||||
stylingDefaults = baseProductStyles;
|
||||
}
|
||||
|
||||
return {
|
||||
brandColor: { light: stylingDefaults.brandColor?.light ?? COLOR_DEFAULTS.brandColor },
|
||||
questionColor: { light: stylingDefaults.questionColor?.light ?? COLOR_DEFAULTS.questionColor },
|
||||
inputColor: { light: stylingDefaults.inputColor?.light ?? COLOR_DEFAULTS.inputColor },
|
||||
inputBorderColor: { light: stylingDefaults.inputBorderColor?.light ?? COLOR_DEFAULTS.inputBorderColor },
|
||||
cardBackgroundColor: {
|
||||
light: stylingDefaults.cardBackgroundColor?.light ?? COLOR_DEFAULTS.cardBackgroundColor,
|
||||
},
|
||||
cardBorderColor: { light: stylingDefaults.cardBorderColor?.light ?? COLOR_DEFAULTS.cardBorderColor },
|
||||
cardShadowColor: { light: stylingDefaults.cardShadowColor?.light ?? COLOR_DEFAULTS.cardShadowColor },
|
||||
highlightBorderColor: stylingDefaults.highlightBorderColor?.light
|
||||
? {
|
||||
light: stylingDefaults.highlightBorderColor.light,
|
||||
}
|
||||
: undefined,
|
||||
isDarkModeEnabled: stylingDefaults.isDarkModeEnabled ?? false,
|
||||
roundness: stylingDefaults.roundness ?? 8,
|
||||
cardArrangement: stylingDefaults.cardArrangement ?? {
|
||||
linkSurveys: "simple",
|
||||
appSurveys: "simple",
|
||||
},
|
||||
background: stylingDefaults.background,
|
||||
hideProgressBar: stylingDefaults.hideProgressBar ?? false,
|
||||
isLogoHidden: stylingDefaults.isLogoHidden ?? false,
|
||||
};
|
||||
}, [localSurvey.styling, product.styling]);
|
||||
|
||||
const form = useForm<TSurveyStyling>({
|
||||
defaultValues: {
|
||||
...localSurvey.styling,
|
||||
...stylingDefaults,
|
||||
},
|
||||
});
|
||||
|
||||
const overwriteThemeStyling = form.watch("overwriteThemeStyling");
|
||||
const setOverwriteThemeStyling = (value: boolean) => form.setValue("overwriteThemeStyling", value);
|
||||
|
||||
const [formStylingOpen, setFormStylingOpen] = useState(false);
|
||||
const [cardStylingOpen, setCardStylingOpen] = useState(false);
|
||||
@@ -56,8 +112,13 @@ export const StylingView = ({
|
||||
...baseStyling,
|
||||
overwriteThemeStyling: true,
|
||||
});
|
||||
setConfirmResetStylingModalOpen(false);
|
||||
|
||||
form.reset({
|
||||
...baseStyling,
|
||||
overwriteThemeStyling: true,
|
||||
});
|
||||
|
||||
setConfirmResetStylingModalOpen(false);
|
||||
toast.success("Styling set to theme styles");
|
||||
};
|
||||
|
||||
@@ -69,14 +130,20 @@ export const StylingView = ({
|
||||
}
|
||||
}, [overwriteThemeStyling]);
|
||||
|
||||
const watchedValues = useWatch({
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (styling) {
|
||||
setLocalSurvey((prev) => ({
|
||||
...prev,
|
||||
styling,
|
||||
}));
|
||||
}
|
||||
}, [setLocalSurvey, styling]);
|
||||
// @ts-expect-error
|
||||
setLocalSurvey((prev) => ({
|
||||
...prev,
|
||||
styling: {
|
||||
...prev.styling,
|
||||
...watchedValues,
|
||||
},
|
||||
}));
|
||||
}, [watchedValues, setLocalSurvey]);
|
||||
|
||||
const defaultProductStyling = useMemo(() => {
|
||||
const { styling: productStyling } = product;
|
||||
@@ -129,79 +196,96 @@ export const StylingView = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-12 space-y-3 p-5">
|
||||
<div className="flex items-center gap-4 py-4">
|
||||
<Switch checked={overwriteThemeStyling} onCheckedChange={handleOverwriteToggle} />
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-base font-semibold text-slate-900">Add custom styles</h3>
|
||||
<p className="text-sm text-slate-800">Override the theme with individual styles for this survey.</p>
|
||||
</div>
|
||||
</div>
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<div className="mt-12 space-y-3 p-5">
|
||||
<div className="flex items-center gap-4 py-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="overwriteThemeStyling"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch checked={!!field.value} onCheckedChange={handleOverwriteToggle} />
|
||||
</FormControl>
|
||||
|
||||
<FormStylingSettings
|
||||
open={formStylingOpen}
|
||||
setOpen={setFormStylingOpen}
|
||||
styling={styling}
|
||||
setStyling={setStyling}
|
||||
disabled={!overwriteThemeStyling}
|
||||
/>
|
||||
<div>
|
||||
<FormLabel className="text-base font-semibold text-slate-900">
|
||||
Add custom styles
|
||||
</FormLabel>
|
||||
<FormDescription className="text-sm text-slate-800">
|
||||
Override the theme with individual styles for this survey.
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CardStylingSettings
|
||||
open={cardStylingOpen}
|
||||
setOpen={setCardStylingOpen}
|
||||
styling={styling}
|
||||
setStyling={setStyling}
|
||||
surveyType={localSurvey.type}
|
||||
disabled={!overwriteThemeStyling}
|
||||
localProduct={product}
|
||||
/>
|
||||
<FormStylingSettings
|
||||
open={formStylingOpen}
|
||||
setOpen={setFormStylingOpen}
|
||||
disabled={!overwriteThemeStyling}
|
||||
form={form as UseFormReturn<TProductStyling | TSurveyStyling>}
|
||||
/>
|
||||
|
||||
{localSurvey.type === "link" && (
|
||||
<BackgroundStylingCard
|
||||
open={stylingOpen}
|
||||
setOpen={setStylingOpen}
|
||||
styling={styling}
|
||||
setStyling={setStyling}
|
||||
environmentId={environment.id}
|
||||
colors={colors}
|
||||
disabled={!overwriteThemeStyling}
|
||||
isUnsplashConfigured={isUnsplashConfigured}
|
||||
/>
|
||||
)}
|
||||
<CardStylingSettings
|
||||
open={cardStylingOpen}
|
||||
setOpen={setCardStylingOpen}
|
||||
surveyType={localSurvey.type}
|
||||
disabled={!overwriteThemeStyling}
|
||||
product={product}
|
||||
form={form as UseFormReturn<TProductStyling | TSurveyStyling>}
|
||||
/>
|
||||
|
||||
<div className="mt-4 flex h-8 items-center justify-between">
|
||||
<div>
|
||||
{overwriteThemeStyling && (
|
||||
<Button
|
||||
variant="minimal"
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => setConfirmResetStylingModalOpen(true)}>
|
||||
Reset to theme styles
|
||||
<RotateCcwIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
{localSurvey.type === "link" && (
|
||||
<BackgroundStylingCard
|
||||
open={stylingOpen}
|
||||
setOpen={setStylingOpen}
|
||||
environmentId={environment.id}
|
||||
colors={colors}
|
||||
disabled={!overwriteThemeStyling}
|
||||
isUnsplashConfigured={isUnsplashConfigured}
|
||||
form={form as UseFormReturn<TProductStyling | TSurveyStyling>}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-slate-500">
|
||||
Adjust the theme in the{" "}
|
||||
<Link
|
||||
href={`/environments/${environment.id}/product/look`}
|
||||
target="_blank"
|
||||
className="font-semibold underline">
|
||||
Look & Feel
|
||||
</Link>{" "}
|
||||
settings
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<AlertDialog
|
||||
open={confirmResetStylingModalOpen}
|
||||
setOpen={setConfirmResetStylingModalOpen}
|
||||
headerText="Reset to theme styles"
|
||||
mainText="Are you sure you want to reset the styling to the theme styles? This will remove all custom styling."
|
||||
confirmBtnLabel="Confirm"
|
||||
onDecline={() => setConfirmResetStylingModalOpen(false)}
|
||||
onConfirm={onResetThemeStyling}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex h-8 items-center justify-between">
|
||||
<div>
|
||||
{overwriteThemeStyling && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="minimal"
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => setConfirmResetStylingModalOpen(true)}>
|
||||
Reset to theme styles
|
||||
<RotateCcwIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-slate-500">
|
||||
Adjust the theme in the{" "}
|
||||
<Link
|
||||
href={`/environments/${environment.id}/product/look`}
|
||||
target="_blank"
|
||||
className="font-semibold underline">
|
||||
Look & Feel
|
||||
</Link>{" "}
|
||||
settings
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<AlertDialog
|
||||
open={confirmResetStylingModalOpen}
|
||||
setOpen={setConfirmResetStylingModalOpen}
|
||||
headerText="Reset to theme styles"
|
||||
mainText="Are you sure you want to reset the styling to the theme styles? This will remove all custom styling."
|
||||
confirmBtnLabel="Confirm"
|
||||
onDecline={() => setConfirmResetStylingModalOpen(false)}
|
||||
onConfirm={onResetThemeStyling}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { TProductStyling } from "@formbricks/types/product";
|
||||
import { TSurveyStyling } from "@formbricks/types/surveys";
|
||||
import { TabBar } from "@formbricks/ui/TabBar";
|
||||
|
||||
import { AnimatedSurveyBg } from "./AnimatedSurveyBg";
|
||||
@@ -14,8 +12,8 @@ interface SurveyBgSelectorTabProps {
|
||||
colors: string[];
|
||||
bgType: string | null | undefined;
|
||||
environmentId: string;
|
||||
styling: TSurveyStyling | TProductStyling | null;
|
||||
isUnsplashConfigured: boolean;
|
||||
bg: string;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
@@ -26,29 +24,28 @@ const tabs = [
|
||||
];
|
||||
|
||||
export const SurveyBgSelectorTab = ({
|
||||
styling,
|
||||
handleBgChange,
|
||||
colors,
|
||||
bgType,
|
||||
bg,
|
||||
environmentId,
|
||||
isUnsplashConfigured,
|
||||
}: SurveyBgSelectorTabProps) => {
|
||||
const [activeTab, setActiveTab] = useState(bgType || "color");
|
||||
const bgUrl = styling?.background?.bg || "";
|
||||
|
||||
const [colorBackground, setColorBackground] = useState(bgUrl);
|
||||
const [animationBackground, setAnimationBackground] = useState(bgUrl);
|
||||
const [uploadBackground, setUploadBackground] = useState(bgUrl);
|
||||
const [colorBackground, setColorBackground] = useState(bg);
|
||||
const [animationBackground, setAnimationBackground] = useState(bg);
|
||||
const [uploadBackground, setUploadBackground] = useState(bg);
|
||||
|
||||
useEffect(() => {
|
||||
if (bgType === "color") {
|
||||
setColorBackground(bgUrl);
|
||||
setColorBackground(bg);
|
||||
setAnimationBackground("");
|
||||
setUploadBackground("");
|
||||
}
|
||||
|
||||
if (bgType === "animation") {
|
||||
setAnimationBackground(bgUrl);
|
||||
setAnimationBackground(bg);
|
||||
setColorBackground("");
|
||||
setUploadBackground("");
|
||||
}
|
||||
@@ -60,11 +57,11 @@ export const SurveyBgSelectorTab = ({
|
||||
}
|
||||
|
||||
if (bgType === "upload") {
|
||||
setUploadBackground(bgUrl);
|
||||
setUploadBackground(bg);
|
||||
setColorBackground("");
|
||||
setAnimationBackground("");
|
||||
}
|
||||
}, [bgUrl, bgType, isUnsplashConfigured]);
|
||||
}, [bg, bgType, isUnsplashConfigured]);
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeTab) {
|
||||
@@ -90,7 +87,7 @@ export const SurveyBgSelectorTab = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex flex-col items-center justify-center rounded-lg ">
|
||||
<div className="mt-4 flex flex-col items-center justify-center rounded-lg">
|
||||
<TabBar
|
||||
tabs={tabs.filter((tab) => tab.id !== "image" || isUnsplashConfigured)}
|
||||
activeId={activeTab}
|
||||
|
||||
@@ -143,7 +143,7 @@ export const SurveyEditor = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
<div className="relative z-0 flex flex-1 overflow-hidden">
|
||||
<main className="relative z-0 flex-1 overflow-y-auto focus:outline-none" ref={surveyEditorRef}>
|
||||
<main className="relative z-0 flex-1 overflow-y-auto focus:outline-none" ref={surveyEditorRef}>
|
||||
<QuestionsAudienceTabs
|
||||
activeId={activeView}
|
||||
setActiveId={setActiveView}
|
||||
@@ -163,6 +163,7 @@ export const SurveyEditor = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isMultiLanguageAllowed={isMultiLanguageAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TBaseFilter, TSegment, TSegmentCreateInput, TSegmentUpdateInput } from "@formbricks/types/segment";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { AlertDialog } from "@formbricks/ui/AlertDialog";
|
||||
import { BasicAddFilterModal } from "@formbricks/ui/BasicAddFilterModal";
|
||||
import { BasicSegmentEditor } from "@formbricks/ui/BasicSegmentEditor";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { BasicAddFilterModal } from "@formbricks/ui/Targeting/BasicAddFilterModal";
|
||||
import { BasicSegmentEditor } from "@formbricks/ui/Targeting/BasicSegmentEditor";
|
||||
import { LoadSegmentModal } from "@formbricks/ui/Targeting/LoadSegmentModal";
|
||||
import { SaveAsNewSegmentModal } from "@formbricks/ui/Targeting/SaveAsNewSegmentModal";
|
||||
import { SegmentTitle } from "@formbricks/ui/Targeting/SegmentTitle";
|
||||
import { TargetingIndicator } from "@formbricks/ui/Targeting/TargetingIndicator";
|
||||
import { LoadSegmentModal } from "@formbricks/ui/LoadSegmentModal";
|
||||
import { SaveAsNewSegmentModal } from "@formbricks/ui/SaveAsNewSegmentModal";
|
||||
import { SegmentTitle } from "@formbricks/ui/SegmentTitle";
|
||||
import { TargetingIndicator } from "@formbricks/ui/TargetingIndicator";
|
||||
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
|
||||
|
||||
import {
|
||||
|
||||
@@ -124,7 +124,11 @@ export const validationRules = {
|
||||
}
|
||||
|
||||
for (const field of fieldsToValidate) {
|
||||
if (question[field] && typeof question[field][defaultLanguageCode] !== "undefined") {
|
||||
if (
|
||||
question[field] &&
|
||||
typeof question[field][defaultLanguageCode] !== "undefined" &&
|
||||
question[field][defaultLanguageCode].trim() !== ""
|
||||
) {
|
||||
isValid = isValid && isLabelValidForAllLanguages(question[field], languages);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { IS_FORMBRICKS_CLOUD, SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSegments } from "@formbricks/lib/segment/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
||||
|
||||
import { SurveyEditor } from "./components/SurveyEditor";
|
||||
@@ -32,7 +32,7 @@ const Page = async ({ params }) => {
|
||||
actionClasses,
|
||||
attributeClasses,
|
||||
responseCount,
|
||||
team,
|
||||
organization,
|
||||
session,
|
||||
segments,
|
||||
] = await Promise.all([
|
||||
@@ -42,7 +42,7 @@ const Page = async ({ params }) => {
|
||||
getActionClasses(params.environmentId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
getResponseCountBySurveyId(params.surveyId),
|
||||
getTeamByEnvironmentId(params.environmentId),
|
||||
getOrganizationByEnvironmentId(params.environmentId),
|
||||
getServerSession(authOptions),
|
||||
getSegments(params.environmentId),
|
||||
]);
|
||||
@@ -51,16 +51,16 @@ const Page = async ({ params }) => {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isViewer } = getAccessFlags(currentUserMembership?.role);
|
||||
const isSurveyCreationDeletionDisabled = isViewer;
|
||||
|
||||
const isUserTargetingAllowed = await getAdvancedTargetingPermission(team);
|
||||
const isMultiLanguageAllowed = await getMultiLanguagePermission(team);
|
||||
const isUserTargetingAllowed = await getAdvancedTargetingPermission(organization);
|
||||
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
|
||||
|
||||
if (
|
||||
!survey ||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { createTeamAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import { createOrganizationAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import FormbricksLogo from "@/images/logo.svg";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -15,28 +15,28 @@ type FormValues = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const CreateFirstTeam = () => {
|
||||
export const CreateFirstOrganization = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { register, handleSubmit } = useForm<FormValues>();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [teamName, setTeamName] = useState("");
|
||||
const isTeamNameValid = teamName.trim() !== "";
|
||||
const [organizationName, setOrganizationName] = useState("");
|
||||
const isOrganizationNameValid = organizationName.trim() !== "";
|
||||
|
||||
const onCreateTeam = async (data: FormValues) => {
|
||||
const onCreateOrganization = async (data: FormValues) => {
|
||||
data.name = data.name.trim();
|
||||
if (!data.name) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const newTeam = await createTeamAction(data.name);
|
||||
const newOrganization = await createOrganizationAction(data.name);
|
||||
|
||||
toast.success("Team created successfully!");
|
||||
router.push(`/teams/${newTeam.id}`);
|
||||
toast.success("Organization created successfully!");
|
||||
router.push(`/organizations/${newOrganization.id}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error(`Unable to create team`);
|
||||
toast.error(`Unable to create organization`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -49,22 +49,22 @@ export const CreateFirstTeam = () => {
|
||||
<p className="text ml-4 text-2xl font-bold">Formbricks</p>
|
||||
</div>
|
||||
<div className="flex h-[calc(100%-12rem)] items-center justify-center border-red-800">
|
||||
<form onSubmit={handleSubmit(onCreateTeam)}>
|
||||
<form onSubmit={handleSubmit(onCreateOrganization)}>
|
||||
<div className="mb-2 flex w-full justify-between space-y-4 rounded-lg px-6">
|
||||
<div className="grid w-full gap-3">
|
||||
<h1 className="text text-3xl font-extrabold text-slate-800">
|
||||
Let's create a team <span className="text-primary-500">👇</span>
|
||||
Let's create an organization <span className="text-primary-500">👇</span>
|
||||
</h1>
|
||||
<p className="text text-md text-slate-700">
|
||||
We couldn't find a team for you. Please create one
|
||||
We couldn't find an organization for you. Please create one
|
||||
</p>
|
||||
<div>
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder="e.g. Power Puff Girls"
|
||||
{...register("name", { required: true })}
|
||||
value={teamName}
|
||||
onChange={(e) => setTeamName(e.target.value)}
|
||||
value={organizationName}
|
||||
onChange={(e) => setOrganizationName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,8 +75,8 @@ export const CreateFirstTeam = () => {
|
||||
variant="darkCTA"
|
||||
type="submit"
|
||||
loading={loading}
|
||||
disabled={!isTeamNameValid}>
|
||||
Create team
|
||||
disabled={!isOrganizationNameValid}>
|
||||
Create organization
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreateFirstTeam } from "@/app/(app)/create-first-team/components/CreateFirstTeam";
|
||||
import { CreateFirstOrganization } from "@/app/(app)/create-first-organization/components/CreateFirstOrganization";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
@@ -11,7 +11,7 @@ const Page = async () => {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
return <CreateFirstTeam />;
|
||||
return <CreateFirstOrganization />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -3,8 +3,8 @@
|
||||
import { TagIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
|
||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
|
||||
@@ -45,7 +45,7 @@ export const AttributeSettingsTab = async ({ attributeClass, setOpen }: Attribut
|
||||
<Label className="text-slate-600">Name</Label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="e.g. Product Team Info"
|
||||
placeholder="e.g. Product Organization Info"
|
||||
{...register("name", {
|
||||
disabled: attributeClass.type === "automatic" || attributeClass.type === "code" ? true : false,
|
||||
})}
|
||||
|
||||
@@ -8,7 +8,7 @@ const Loading = () => {
|
||||
return (
|
||||
<>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Attributes">
|
||||
<PageHeader pageTitle="People">
|
||||
<PeopleSecondaryNavigation activeId="attributes" loading />
|
||||
</PageHeader>
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ActivityTimeline } from "@/app/(app)/environments/[environmentId]/(peop
|
||||
import { getActionsByPersonId } from "@formbricks/lib/action/service";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
|
||||
export const ActivitySection = async ({
|
||||
environmentId,
|
||||
@@ -12,15 +12,15 @@ export const ActivitySection = async ({
|
||||
environmentId: string;
|
||||
personId: string;
|
||||
}) => {
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
// On Formbricks Cloud only render the timeline if the user targeting feature is booked
|
||||
const isUserTargetingEnabled = IS_FORMBRICKS_CLOUD
|
||||
? team.billing.features.userTargeting.status === "active"
|
||||
? organization.billing.features.userTargeting.status === "active"
|
||||
: true;
|
||||
|
||||
const [environment, actions] = await Promise.all([
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getAttributes } from "@formbricks/lib/attribute/service";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { getResponsesByPersonId } from "@formbricks/lib/response/service";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
|
||||
export const AttributesSection = async ({ personId }: { personId: string }) => {
|
||||
const [person, attributes] = await Promise.all([getPerson(personId), getAttributes(personId)]);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getResponsesByPersonId } from "@formbricks/lib/response/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
@@ -12,9 +13,15 @@ interface ResponseSectionProps {
|
||||
environment: TEnvironment;
|
||||
personId: string;
|
||||
environmentTags: TTag[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const ResponseSection = async ({ environment, personId, environmentTags }: ResponseSectionProps) => {
|
||||
export const ResponseSection = async ({
|
||||
environment,
|
||||
personId,
|
||||
environmentTags,
|
||||
attributeClasses,
|
||||
}: ResponseSectionProps) => {
|
||||
const responses = await getResponsesByPersonId(personId);
|
||||
const surveyIds = responses?.map((response) => response.surveyId) || [];
|
||||
const surveys: TSurvey[] = surveyIds.length === 0 ? [] : (await getSurveys(environment.id)) ?? [];
|
||||
@@ -34,6 +41,7 @@ export const ResponseSection = async ({ environment, personId, environmentTags }
|
||||
responses={responses}
|
||||
environment={environment}
|
||||
environmentTags={environmentTags}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ResponseFeed } from "@/app/(app)/environments/[environmentId]/(people)/
|
||||
import { ArrowDownUpIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
@@ -16,6 +17,7 @@ interface ResponseTimelineProps {
|
||||
responses: TResponse[];
|
||||
environment: TEnvironment;
|
||||
environmentTags: TTag[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const ResponseTimeline = ({
|
||||
@@ -24,6 +26,7 @@ export const ResponseTimeline = ({
|
||||
environment,
|
||||
responses,
|
||||
environmentTags,
|
||||
attributeClasses,
|
||||
}: ResponseTimelineProps) => {
|
||||
const [sortedResponses, setSortedResponses] = useState(responses);
|
||||
const toggleSortResponses = () => {
|
||||
@@ -53,6 +56,7 @@ export const ResponseTimeline = ({
|
||||
surveys={surveys}
|
||||
user={user}
|
||||
environmentTags={environmentTags}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useEffect, useState } from "react";
|
||||
|
||||
import { useMembershipRole } from "@formbricks/lib/membership/hooks/useMembershipRole";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
@@ -19,6 +20,7 @@ interface ResponseTimelineProps {
|
||||
responses: TResponse[];
|
||||
environment: TEnvironment;
|
||||
environmentTags: TTag[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const ResponseFeed = ({
|
||||
@@ -27,6 +29,7 @@ export const ResponseFeed = ({
|
||||
surveys,
|
||||
user,
|
||||
environmentTags,
|
||||
attributeClasses,
|
||||
}: ResponseTimelineProps) => {
|
||||
const [fetchedResponses, setFetchedResponses] = useState(responses);
|
||||
|
||||
@@ -59,6 +62,7 @@ export const ResponseFeed = ({
|
||||
environment={environment}
|
||||
deleteResponse={deleteResponse}
|
||||
updateResponse={updateResponse}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
@@ -74,6 +78,7 @@ const ResponseSurveyCard = ({
|
||||
environment,
|
||||
deleteResponse,
|
||||
updateResponse,
|
||||
attributeClasses,
|
||||
}: {
|
||||
response: TResponse;
|
||||
surveys: TSurvey[];
|
||||
@@ -82,6 +87,7 @@ const ResponseSurveyCard = ({
|
||||
environment: TEnvironment;
|
||||
deleteResponse: (responseId: string) => void;
|
||||
updateResponse: (responseId: string, response: TResponse) => void;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}) => {
|
||||
const survey = surveys.find((survey) => {
|
||||
return survey.id === response.surveyId;
|
||||
@@ -95,7 +101,7 @@ const ResponseSurveyCard = ({
|
||||
{survey && (
|
||||
<SingleResponseCard
|
||||
response={response}
|
||||
survey={checkForRecallInHeadline(survey, "default")}
|
||||
survey={replaceHeadlineRecall(survey, "default", attributeClasses)}
|
||||
user={user}
|
||||
pageType="people"
|
||||
environmentTags={environmentTags}
|
||||
|
||||
@@ -5,28 +5,31 @@ import { ResponseSection } from "@/app/(app)/environments/[environmentId]/(peopl
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { getAttributes } from "@formbricks/lib/attribute/service";
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
const Page = async ({ params }) => {
|
||||
const [environment, environmentTags, product, session, team, person, attributes] = await Promise.all([
|
||||
getEnvironment(params.environmentId),
|
||||
getTagsByEnvironmentId(params.environmentId),
|
||||
getProductByEnvironmentId(params.environmentId),
|
||||
getServerSession(authOptions),
|
||||
getTeamByEnvironmentId(params.environmentId),
|
||||
getPerson(params.personId),
|
||||
getAttributes(params.personId),
|
||||
]);
|
||||
const [environment, environmentTags, product, session, organization, person, attributes, attributeClasses] =
|
||||
await Promise.all([
|
||||
getEnvironment(params.environmentId),
|
||||
getTagsByEnvironmentId(params.environmentId),
|
||||
getProductByEnvironmentId(params.environmentId),
|
||||
getServerSession(authOptions),
|
||||
getOrganizationByEnvironmentId(params.environmentId),
|
||||
getPerson(params.personId),
|
||||
getAttributes(params.personId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
@@ -40,15 +43,15 @@ const Page = async ({ params }) => {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
if (!person) {
|
||||
throw new Error("Person not found");
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isViewer } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const getDeletePersonButton = () => {
|
||||
@@ -67,6 +70,7 @@ const Page = async ({ params }) => {
|
||||
environment={environment}
|
||||
personId={params.personId}
|
||||
environmentTags={environmentTags}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<ActivitySection environmentId={params.environmentId} personId={params.personId} />
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,11 @@ import { createSegmentAction } from "@formbricks/ee/advancedTargeting/lib/action
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TBaseFilter, TSegment, ZSegmentFilters } from "@formbricks/types/segment";
|
||||
import { BasicAddFilterModal } from "@formbricks/ui/BasicAddFilterModal";
|
||||
import { BasicSegmentEditor } from "@formbricks/ui/BasicSegmentEditor";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Modal } from "@formbricks/ui/Modal";
|
||||
import { BasicAddFilterModal } from "@formbricks/ui/Targeting/BasicAddFilterModal";
|
||||
import { BasicSegmentEditor } from "@formbricks/ui/Targeting/BasicSegmentEditor";
|
||||
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
|
||||
|
||||
type TCreateSegmentModalProps = {
|
||||
|
||||
@@ -9,11 +9,11 @@ import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { isAdvancedSegment } from "@formbricks/lib/segment/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TBaseFilter, TSegment, TSegmentWithSurveyNames, ZSegmentFilters } from "@formbricks/types/segment";
|
||||
import { BasicAddFilterModal } from "@formbricks/ui/BasicAddFilterModal";
|
||||
import { BasicSegmentEditor } from "@formbricks/ui/BasicSegmentEditor";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { ConfirmDeleteSegmentModal } from "@formbricks/ui/ConfirmDeleteSegmentModal";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { BasicAddFilterModal } from "@formbricks/ui/Targeting/BasicAddFilterModal";
|
||||
import { BasicSegmentEditor } from "@formbricks/ui/Targeting/BasicSegmentEditor";
|
||||
import { ConfirmDeleteSegmentModal } from "@formbricks/ui/Targeting/ConfirmDeleteSegmentModal";
|
||||
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
|
||||
|
||||
import { deleteBasicSegmentAction, updateBasicSegmentAction } from "../actions";
|
||||
|
||||
@@ -8,7 +8,7 @@ const Loading = () => {
|
||||
return (
|
||||
<>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Segments">
|
||||
<PageHeader pageTitle="People">
|
||||
<PeopleSecondaryNavigation activeId="segments" loading />
|
||||
</PageHeader>
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
|
||||
@@ -9,29 +9,29 @@ import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getSegments } from "@formbricks/lib/segment/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
const Page = async ({ params }) => {
|
||||
const [environment, segments, attributeClasses, actionClassesFromServer, team] = await Promise.all([
|
||||
const [environment, segments, attributeClasses, actionClassesFromServer, organization] = await Promise.all([
|
||||
getEnvironment(params.environmentId),
|
||||
getSegments(params.environmentId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
getActionClasses(params.environmentId),
|
||||
getTeamByEnvironmentId(params.environmentId),
|
||||
getOrganizationByEnvironmentId(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const isAdvancedTargetingAllowed = await getAdvancedTargetingPermission(team);
|
||||
const isAdvancedTargetingAllowed = await getAdvancedTargetingPermission(organization);
|
||||
|
||||
if (!segments) {
|
||||
throw new Error("Failed to fetch segments");
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use server";
|
||||
|
||||
import { Team } from "@prisma/client";
|
||||
import { Organization } from "@prisma/client";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { SHORT_URL_BASE, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
import { createOrganization, getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { createProduct } from "@formbricks/lib/product/service";
|
||||
import { createShortUrl } from "@formbricks/lib/shortUrl/service";
|
||||
import { createTeam, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { updateUser } from "@formbricks/lib/user/service";
|
||||
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
|
||||
@@ -27,20 +27,20 @@ export const createShortUrlAction = async (url: string) => {
|
||||
return fullShortUrl;
|
||||
};
|
||||
|
||||
export const createTeamAction = async (teamName: string): Promise<Team> => {
|
||||
export const createOrganizationAction = async (organizationName: string): Promise<Organization> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const newTeam = await createTeam({
|
||||
name: teamName,
|
||||
const newOrganization = await createOrganization({
|
||||
name: organizationName,
|
||||
});
|
||||
|
||||
await createMembership(newTeam.id, session.user.id, {
|
||||
await createMembership(newOrganization.id, session.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const product = await createProduct(newTeam.id, {
|
||||
const product = await createProduct(newOrganization.id, {
|
||||
name: "My Product",
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ export const createTeamAction = async (teamName: string): Promise<Team> => {
|
||||
notificationSettings: updatedNotificationSettings,
|
||||
});
|
||||
|
||||
return newTeam;
|
||||
return newOrganization;
|
||||
};
|
||||
|
||||
export const createProductAction = async (environmentId: string, productName: string) => {
|
||||
@@ -69,10 +69,10 @@ export const createProductAction = async (environmentId: string, productName: st
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
if (!team) throw new ResourceNotFoundError("Team from environment", environmentId);
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
if (!organization) throw new ResourceNotFoundError("Organization from environment", environmentId);
|
||||
|
||||
const product = await createProduct(team.id, {
|
||||
const product = await createProduct(organization.id, {
|
||||
name: productName,
|
||||
});
|
||||
const updatedNotificationSettings = {
|
||||
|
||||
@@ -11,8 +11,8 @@ import { canUserUpdateActionClass, verifyUserRoleAccess } from "@formbricks/lib/
|
||||
import { createActionClass, deleteActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getSurveysByActionClassId } from "@formbricks/lib/survey/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { TActionClassInput } from "@formbricks/types/actionClasses";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
|
||||
@@ -20,10 +20,10 @@ export const deleteActionClassAction = async (environmentId, actionClassId: stri
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
|
||||
@@ -43,10 +43,10 @@ export const updateActionClassAction = async (
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
|
||||
@@ -72,10 +72,10 @@ export const getActionCountInLastHourAction = async (actionClassId: string, envi
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
|
||||
@@ -88,10 +88,10 @@ export const getActionCountInLast24HoursAction = async (actionClassId: string, e
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
|
||||
@@ -104,10 +104,10 @@ export const getActionCountInLast7DaysAction = async (actionClassId: string, env
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
|
||||
@@ -123,10 +123,10 @@ export const getActiveInactiveSurveysAction = async (
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
|
||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TActionClass } from "@formbricks/types/actionClasses";
|
||||
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
|
||||
@@ -1,37 +1,42 @@
|
||||
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<span className="sr-only">Edit</span>
|
||||
<div className="col-span-4 pl-6 ">User Actions</div>
|
||||
<div className="col-span-2 text-center">Created</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div key={index} className="m-2 grid h-16 grid-cols-6 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-4 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-6 w-6 flex-shrink-0 animate-pulse rounded-full bg-slate-200 text-slate-500"></div>
|
||||
<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>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Actions" />
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="grid h-12 grid-cols-6 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
|
||||
<span className="sr-only">Edit</span>
|
||||
<div className="col-span-4 pl-6 ">User Actions</div>
|
||||
<div className="col-span-2 text-center">Created</div>
|
||||
</div>
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="m-2 grid h-16 grid-cols-6 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||
<div className="col-span-4 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-6 w-6 flex-shrink-0 animate-pulse rounded-full bg-slate-200 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 className="col-span-2 my-auto flex justify-center whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="h-4 w-28 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="m-28 h-4 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="text-center"></div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</PageContentWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Metadata } from "next";
|
||||
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
@@ -15,18 +15,18 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
const Page = async ({ params }) => {
|
||||
const [actionClasses, team] = await Promise.all([
|
||||
const [actionClasses, organization] = await Promise.all([
|
||||
getActionClasses(params.environmentId),
|
||||
getTeamByEnvironmentId(params.environmentId),
|
||||
getOrganizationByEnvironmentId(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
// On Formbricks Cloud only render the timeline if the user targeting feature is booked
|
||||
const isUserTargetingEnabled = IS_FORMBRICKS_CLOUD
|
||||
? team.billing.features.userTargeting.status === "active"
|
||||
? organization.billing.features.userTargeting.status === "active"
|
||||
: true;
|
||||
|
||||
const renderAddActionButton = () => (
|
||||
|
||||
@@ -55,7 +55,7 @@ export const AddProductModal = ({ environmentId, open, setOpen }: AddProductModa
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-medium text-slate-700">Add Product</div>
|
||||
<div className="text-sm text-slate-500">Create a new product for your team.</div>
|
||||
<div className="text-sm text-slate-500">Create a new product for your organization.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,12 @@ import type { Session } from "next-auth";
|
||||
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import {
|
||||
getOrganizationByEnvironmentId,
|
||||
getOrganizationsByUserId,
|
||||
} from "@formbricks/lib/organization/service";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
import { getTeamByEnvironmentId, getTeamsByUserId } from "@formbricks/lib/team/service";
|
||||
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
|
||||
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
||||
|
||||
@@ -17,25 +20,25 @@ interface EnvironmentLayoutProps {
|
||||
}
|
||||
|
||||
export const EnvironmentLayout = async ({ environmentId, session, children }: EnvironmentLayoutProps) => {
|
||||
const [environment, teams, team] = await Promise.all([
|
||||
const [environment, organizations, organization] = await Promise.all([
|
||||
getEnvironment(environmentId),
|
||||
getTeamsByUserId(session.user.id),
|
||||
getTeamByEnvironmentId(environmentId),
|
||||
getOrganizationsByUserId(session.user.id),
|
||||
getOrganizationByEnvironmentId(environmentId),
|
||||
]);
|
||||
|
||||
if (!team || !environment) {
|
||||
if (!organization || !environment) {
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
|
||||
const [products, environments] = await Promise.all([
|
||||
getProducts(team.id),
|
||||
getProducts(organization.id),
|
||||
getEnvironments(environment.productId),
|
||||
]);
|
||||
|
||||
if (!products || !environments || !teams) {
|
||||
if (!products || !environments || !organizations) {
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen min-h-screen flex-col overflow-hidden">
|
||||
@@ -43,8 +46,8 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
||||
<div className="flex h-full">
|
||||
<MainNavigation
|
||||
environment={environment}
|
||||
team={team}
|
||||
teams={teams}
|
||||
organization={organization}
|
||||
organizations={organizations}
|
||||
products={products}
|
||||
session={session}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
|
||||
@@ -29,14 +29,14 @@ import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { capitalizeFirstLetter, truncate } from "@formbricks/lib/strings";
|
||||
import { capitalizeFirstLetter, truncate } from "@formbricks/lib/utils/strings";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TMembershipRole } from "@formbricks/types/memberships";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TTeam } from "@formbricks/types/teams";
|
||||
import { ProfileAvatar } from "@formbricks/ui/Avatars";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { CreateTeamModal } from "@formbricks/ui/CreateTeamModal";
|
||||
import { CreateOrganizationModal } from "@formbricks/ui/CreateOrganizationModal";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -55,9 +55,9 @@ import { AddProductModal } from "./AddProductModal";
|
||||
|
||||
interface NavigationProps {
|
||||
environment: TEnvironment;
|
||||
teams: TTeam[];
|
||||
organizations: TOrganization[];
|
||||
session: Session;
|
||||
team: TTeam;
|
||||
organization: TOrganization;
|
||||
products: TProduct[];
|
||||
isFormbricksCloud: boolean;
|
||||
membershipRole?: TMembershipRole;
|
||||
@@ -65,8 +65,8 @@ interface NavigationProps {
|
||||
|
||||
export const MainNavigation = ({
|
||||
environment,
|
||||
teams,
|
||||
team,
|
||||
organizations,
|
||||
organization,
|
||||
session,
|
||||
products,
|
||||
isFormbricksCloud,
|
||||
@@ -75,10 +75,10 @@ export const MainNavigation = ({
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const [currentTeamName, setCurrentTeamName] = useState("");
|
||||
const [currentTeamId, setCurrentTeamId] = useState("");
|
||||
const [currentOrganizationName, setCurrentOrganizationName] = useState("");
|
||||
const [currentOrganizationId, setCurrentOrganizationId] = useState("");
|
||||
const [showAddProductModal, setShowAddProductModal] = useState(false);
|
||||
const [showCreateTeamModal, setShowCreateTeamModal] = useState(false);
|
||||
const [showCreateOrganizationModal, setShowCreateOrganizationModal] = useState(false);
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
const [isTextVisible, setIsTextVisible] = useState(true);
|
||||
|
||||
@@ -105,15 +105,15 @@ export const MainNavigation = ({
|
||||
}, [isCollapsed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (team && team.name !== "") {
|
||||
setCurrentTeamName(team.name);
|
||||
setCurrentTeamId(team.id);
|
||||
if (organization && organization.name !== "") {
|
||||
setCurrentOrganizationName(organization.name);
|
||||
setCurrentOrganizationId(organization.id);
|
||||
}
|
||||
}, [team]);
|
||||
}, [organization]);
|
||||
|
||||
const sortedTeams = useMemo(() => {
|
||||
return [...teams].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [teams]);
|
||||
const sortedOrganizations = useMemo(() => {
|
||||
return [...organizations].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [organizations]);
|
||||
|
||||
const sortedProducts = useMemo(() => {
|
||||
return [...products].sort((a, b) => a.name.localeCompare(b.name));
|
||||
@@ -123,8 +123,8 @@ export const MainNavigation = ({
|
||||
router.push(`/products/${productId}/`);
|
||||
};
|
||||
|
||||
const handleEnvironmentChangeByTeam = (teamId: string) => {
|
||||
router.push(`/teams/${teamId}/`);
|
||||
const handleEnvironmentChangeByOrganization = (organizationId: string) => {
|
||||
router.push(`/organizations/${organizationId}/`);
|
||||
};
|
||||
|
||||
const mainNavigation = useMemo(
|
||||
@@ -164,7 +164,7 @@ export const MainNavigation = ({
|
||||
href: `/environments/${environment.id}/product/general`,
|
||||
icon: Cog,
|
||||
isActive: pathname?.includes("/product"),
|
||||
isHidden: false,
|
||||
isHidden: isViewer,
|
||||
},
|
||||
],
|
||||
[environment.id, pathname, isViewer]
|
||||
@@ -177,7 +177,7 @@ export const MainNavigation = ({
|
||||
icon: UserCircleIcon,
|
||||
},
|
||||
{
|
||||
label: "Team",
|
||||
label: "Organization",
|
||||
href: `/environments/${environment.id}/settings/members`,
|
||||
icon: UsersIcon,
|
||||
},
|
||||
@@ -363,7 +363,9 @@ export const MainNavigation = ({
|
||||
<span>{truncate(session?.user?.email, 30)}</span>
|
||||
)}
|
||||
</p>
|
||||
<p className={cn("text-sm text-slate-500")}>{capitalizeFirstLetter(team?.name)}</p>
|
||||
<p className={cn("text-sm text-slate-500")}>
|
||||
{capitalizeFirstLetter(organization?.name)}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRightIcon className={cn("h-5 w-5 text-slate-700 hover:text-slate-500")} />
|
||||
</>
|
||||
@@ -410,13 +412,13 @@ export const MainNavigation = ({
|
||||
Logout
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* Team Switch */}
|
||||
{/* Organization Switch */}
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="rounded-lg">
|
||||
<div>
|
||||
<p>{currentTeamName}</p>
|
||||
<p className="block text-xs text-slate-500">Switch team</p>
|
||||
<p>{currentOrganizationName}</p>
|
||||
<p className="block text-xs text-slate-500">Switch organization</p>
|
||||
</div>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
@@ -425,21 +427,25 @@ export const MainNavigation = ({
|
||||
sideOffset={10}
|
||||
alignOffset={5}>
|
||||
<DropdownMenuRadioGroup
|
||||
value={currentTeamId}
|
||||
onValueChange={(teamId) => handleEnvironmentChangeByTeam(teamId)}>
|
||||
{sortedTeams.map((team) => (
|
||||
value={currentOrganizationId}
|
||||
onValueChange={(organizationId) =>
|
||||
handleEnvironmentChangeByOrganization(organizationId)
|
||||
}>
|
||||
{sortedOrganizations.map((organization) => (
|
||||
<DropdownMenuRadioItem
|
||||
value={team.id}
|
||||
value={organization.id}
|
||||
className="cursor-pointer rounded-lg"
|
||||
key={team.id}>
|
||||
{team.name}
|
||||
key={organization.id}>
|
||||
{organization.name}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setShowCreateTeamModal(true)} className="rounded-lg">
|
||||
<DropdownMenuItem
|
||||
onClick={() => setShowCreateOrganizationModal(true)}
|
||||
className="rounded-lg">
|
||||
<PlusIcon className="mr-2 h-4 w-4" />
|
||||
<span>Create new team</span>
|
||||
<span>Create new organization</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
@@ -450,7 +456,10 @@ export const MainNavigation = ({
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
<CreateTeamModal open={showCreateTeamModal} setOpen={(val) => setShowCreateTeamModal(val)} />
|
||||
<CreateOrganizationModal
|
||||
open={showCreateOrganizationModal}
|
||||
setOpen={(val) => setShowCreateOrganizationModal(val)}
|
||||
/>
|
||||
<AddProductModal
|
||||
open={showAddProductModal}
|
||||
setOpen={(val) => setShowAddProductModal(val)}
|
||||
|
||||
@@ -5,15 +5,15 @@ import { usePostHog } from "posthog-js/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { env } from "@formbricks/lib/env";
|
||||
import { TSubscriptionStatus } from "@formbricks/types/teams";
|
||||
import { TSubscriptionStatus } from "@formbricks/types/organizations";
|
||||
|
||||
const posthogEnabled = env.NEXT_PUBLIC_POSTHOG_API_KEY && env.NEXT_PUBLIC_POSTHOG_API_HOST;
|
||||
|
||||
interface PosthogIdentifyProps {
|
||||
session: Session;
|
||||
environmentId?: string;
|
||||
teamId?: string;
|
||||
teamName?: string;
|
||||
organizationId?: string;
|
||||
organizationName?: string;
|
||||
inAppSurveyBillingStatus?: TSubscriptionStatus;
|
||||
linkSurveyBillingStatus?: TSubscriptionStatus;
|
||||
userTargetingBillingStatus?: TSubscriptionStatus;
|
||||
@@ -22,8 +22,8 @@ interface PosthogIdentifyProps {
|
||||
export const PosthogIdentify = ({
|
||||
session,
|
||||
environmentId,
|
||||
teamId,
|
||||
teamName,
|
||||
organizationId,
|
||||
organizationName,
|
||||
inAppSurveyBillingStatus,
|
||||
linkSurveyBillingStatus,
|
||||
userTargetingBillingStatus,
|
||||
@@ -41,9 +41,9 @@ export const PosthogIdentify = ({
|
||||
if (environmentId) {
|
||||
posthog.group("environment", environmentId, { name: environmentId });
|
||||
}
|
||||
if (teamId) {
|
||||
posthog.group("team", teamId, {
|
||||
name: teamName,
|
||||
if (organizationId) {
|
||||
posthog.group("organization", organizationId, {
|
||||
name: organizationName,
|
||||
inAppSurveyBillingStatus,
|
||||
linkSurveyBillingStatus,
|
||||
userTargetingBillingStatus,
|
||||
@@ -54,8 +54,8 @@ export const PosthogIdentify = ({
|
||||
posthog,
|
||||
session.user,
|
||||
environmentId,
|
||||
teamId,
|
||||
teamName,
|
||||
organizationId,
|
||||
organizationName,
|
||||
inAppSurveyBillingStatus,
|
||||
linkSurveyBillingStatus,
|
||||
userTargetingBillingStatus,
|
||||
|
||||
@@ -11,7 +11,8 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationAirtable,
|
||||
@@ -38,6 +39,7 @@ type AddIntegrationModalProps = {
|
||||
airtableArray: TIntegrationItem[];
|
||||
surveys: TSurvey[];
|
||||
airtableIntegration: TIntegrationAirtable;
|
||||
attributeClasses: TAttributeClass[];
|
||||
} & EditModeProps;
|
||||
|
||||
export type IntegrationModalInputs = {
|
||||
@@ -65,6 +67,7 @@ export const AddIntegrationModal = ({
|
||||
airtableIntegration,
|
||||
isEditMode,
|
||||
defaultData,
|
||||
attributeClasses,
|
||||
}: AddIntegrationModalProps) => {
|
||||
const router = useRouter();
|
||||
const [tables, setTables] = useState<TIntegrationAirtableTables["tables"]>([]);
|
||||
@@ -282,32 +285,36 @@ export const AddIntegrationModal = ({
|
||||
<Label htmlFor="Surveys">Questions</Label>
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{checkForRecallInHeadline(selectedSurvey, "default")?.questions.map((question) => (
|
||||
<Controller
|
||||
key={question.id}
|
||||
control={control}
|
||||
name={"questions"}
|
||||
render={({ field }) => (
|
||||
<div className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={field.value?.includes(question.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([...field.value, question.id])
|
||||
: field.onChange(field.value?.filter((value) => value !== question.id));
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">{getLocalizedValue(question.headline, "default")}</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map(
|
||||
(question) => (
|
||||
<Controller
|
||||
key={question.id}
|
||||
control={control}
|
||||
name={"questions"}
|
||||
render={({ field }) => (
|
||||
<div className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={field.value?.includes(question.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([...field.value, question.id])
|
||||
: field.onChange(field.value?.filter((value) => value !== question.id));
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">
|
||||
{getLocalizedValue(question.headline, "default")}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { authorize } from "@/app/(app)/environments/[environmentId]/integrations
|
||||
import airtableLogo from "@/images/airtableLogo.svg";
|
||||
import { useState } from "react";
|
||||
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
@@ -19,6 +20,7 @@ interface AirtableWrapperProps {
|
||||
environment: TEnvironment;
|
||||
isEnabled: boolean;
|
||||
webAppUrl: string;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const AirtableWrapper = ({
|
||||
@@ -29,6 +31,7 @@ export const AirtableWrapper = ({
|
||||
environment,
|
||||
isEnabled,
|
||||
webAppUrl,
|
||||
attributeClasses,
|
||||
}: AirtableWrapperProps) => {
|
||||
const [isConnected, setIsConnected] = useState(
|
||||
airtableIntegration ? airtableIntegration.config?.key : false
|
||||
@@ -50,6 +53,7 @@ export const AirtableWrapper = ({
|
||||
airtableIntegration={airtableIntegration}
|
||||
setIsConnected={setIsConnected}
|
||||
surveys={surveys}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : (
|
||||
<ConnectIntegration
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
@@ -24,12 +25,21 @@ interface ManageIntegrationProps {
|
||||
setIsConnected: (data: boolean) => void;
|
||||
surveys: TSurvey[];
|
||||
airtableArray: TIntegrationItem[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
const tableHeaders = ["Survey", "Table Name", "Questions", "Updated At"];
|
||||
|
||||
export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||
const { airtableIntegration, environment, environmentId, setIsConnected, surveys, airtableArray } = props;
|
||||
const {
|
||||
airtableIntegration,
|
||||
environment,
|
||||
environmentId,
|
||||
setIsConnected,
|
||||
surveys,
|
||||
airtableArray,
|
||||
attributeClasses,
|
||||
} = props;
|
||||
const [isDeleting, setisDeleting] = useState(false);
|
||||
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
|
||||
const [defaultValues, setDefaultValues] = useState<(IntegrationModalInputs & { index: number }) | null>(
|
||||
@@ -142,6 +152,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||
environmentId={environmentId}
|
||||
surveys={surveys}
|
||||
airtableIntegration={airtableIntegration}
|
||||
attributeClasses={attributeClasses}
|
||||
{...data}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
||||
|
||||
import { getAirtableTables } from "@formbricks/lib/airtable/service";
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
@@ -14,10 +15,11 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
const Page = async ({ params }) => {
|
||||
const isEnabled = !!AIRTABLE_CLIENT_ID;
|
||||
const [surveys, integrations, environment] = await Promise.all([
|
||||
const [surveys, integrations, environment, attributeClasses] = await Promise.all([
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrations(params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
]);
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
@@ -49,6 +51,7 @@ const Page = async ({ params }) => {
|
||||
surveys={surveys}
|
||||
environment={environment}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
</PageContentWrapper>
|
||||
|
||||
@@ -12,7 +12,8 @@ import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
TIntegrationGoogleSheetsConfigData,
|
||||
@@ -33,6 +34,7 @@ interface AddIntegrationModalProps {
|
||||
setOpen: (v: boolean) => void;
|
||||
googleSheetIntegration: TIntegrationGoogleSheets;
|
||||
selectedIntegration?: (TIntegrationGoogleSheetsConfigData & { index: number }) | null;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const AddIntegrationModal = ({
|
||||
@@ -42,6 +44,7 @@ export const AddIntegrationModal = ({
|
||||
setOpen,
|
||||
googleSheetIntegration,
|
||||
selectedIntegration,
|
||||
attributeClasses,
|
||||
}: AddIntegrationModalProps) => {
|
||||
const integrationData = {
|
||||
spreadsheetId: "",
|
||||
@@ -216,25 +219,27 @@ export const AddIntegrationModal = ({
|
||||
<Label htmlFor="Surveys">Questions</Label>
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{checkForRecallInHeadline(selectedSurvey, "default")?.questions.map((question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={selectedQuestions.includes(question.id)}
|
||||
onCheckedChange={() => {
|
||||
handleCheckboxChange(question.id);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2 w-[30rem] truncate">
|
||||
{getLocalizedValue(question.headline, "default")}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map(
|
||||
(question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={selectedQuestions.includes(question.id)}
|
||||
onCheckedChange={() => {
|
||||
handleCheckboxChange(question.id);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2 w-[30rem] truncate">
|
||||
{getLocalizedValue(question.headline, "default")}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { authorize } from "@/app/(app)/environments/[environmentId]/integrations
|
||||
import googleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||
import { useState } from "react";
|
||||
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
@@ -21,6 +22,7 @@ interface GoogleSheetWrapperProps {
|
||||
surveys: TSurvey[];
|
||||
googleSheetIntegration?: TIntegrationGoogleSheets;
|
||||
webAppUrl: string;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const GoogleSheetWrapper = ({
|
||||
@@ -29,6 +31,7 @@ export const GoogleSheetWrapper = ({
|
||||
surveys,
|
||||
googleSheetIntegration,
|
||||
webAppUrl,
|
||||
attributeClasses,
|
||||
}: GoogleSheetWrapperProps) => {
|
||||
const [isConnected, setIsConnected] = useState(
|
||||
googleSheetIntegration ? googleSheetIntegration.config?.key : false
|
||||
@@ -57,6 +60,7 @@ export const GoogleSheetWrapper = ({
|
||||
setOpen={setModalOpen}
|
||||
googleSheetIntegration={googleSheetIntegration}
|
||||
selectedIntegration={selectedIntegration}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<ManageIntegration
|
||||
environment={environment}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import {
|
||||
GOOGLE_SHEETS_CLIENT_ID,
|
||||
GOOGLE_SHEETS_CLIENT_SECRET,
|
||||
@@ -17,10 +18,11 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
const Page = async ({ params }) => {
|
||||
const isEnabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
|
||||
const [surveys, integrations, environment] = await Promise.all([
|
||||
const [surveys, integrations, environment, attributeClasses] = await Promise.all([
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrations(params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
]);
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
@@ -45,6 +47,7 @@ const Page = async ({ params }) => {
|
||||
surveys={surveys}
|
||||
googleSheetIntegration={googleSheetIntegration}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
</PageContentWrapper>
|
||||
|
||||
@@ -14,7 +14,8 @@ import toast from "react-hot-toast";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
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 { TIntegrationInput } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationNotion,
|
||||
@@ -35,6 +36,7 @@ interface AddIntegrationModalProps {
|
||||
notionIntegration: TIntegrationNotion;
|
||||
databases: TIntegrationNotionDatabase[];
|
||||
selectedIntegration: (TIntegrationNotionConfigData & { index: number }) | null;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const AddIntegrationModal = ({
|
||||
@@ -45,6 +47,7 @@ export const AddIntegrationModal = ({
|
||||
notionIntegration,
|
||||
databases,
|
||||
selectedIntegration,
|
||||
attributeClasses,
|
||||
}: AddIntegrationModalProps) => {
|
||||
const { handleSubmit } = useForm();
|
||||
const [selectedDatabase, setSelectedDatabase] = useState<TIntegrationNotionDatabase | null>();
|
||||
@@ -109,7 +112,7 @@ export const AddIntegrationModal = ({
|
||||
|
||||
const questionItems = useMemo(() => {
|
||||
const questions = selectedSurvey
|
||||
? checkForRecallInHeadline(selectedSurvey, "default")?.questions.map((q) => ({
|
||||
? replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map((q) => ({
|
||||
id: q.id,
|
||||
name: getLocalizedValue(q.headline, "default"),
|
||||
type: q.type,
|
||||
@@ -260,11 +263,14 @@ export const AddIntegrationModal = ({
|
||||
</>
|
||||
);
|
||||
case ERRORS.MAPPING:
|
||||
const question = questionTypes.find((qt) => qt.id === ques.type);
|
||||
if (!question) return null;
|
||||
return (
|
||||
<>
|
||||
- <i>"{ques.name}"</i> of type{" "}
|
||||
<b>{questionTypes.find((qt) => qt.id === ques.type)?.label}</b> can't be mapped to the
|
||||
column <i>"{col.name}"</i> of type <b>{col.type}</b>
|
||||
- <i>"{ques.name}"</i> of type <b>{question.label}</b> can't be mapped to the
|
||||
column <i>"{col.name}"</i> of type <b>{col.type}</b>. Instead use column of type{" "}
|
||||
{""}
|
||||
<b>{TYPE_MAPPING[question.id].join(" ,")}.</b>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/inte
|
||||
import notionLogo from "@/images/notion.png";
|
||||
import { useState } from "react";
|
||||
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TIntegrationNotion,
|
||||
@@ -23,6 +24,7 @@ interface NotionWrapperProps {
|
||||
webAppUrl: string;
|
||||
surveys: TSurvey[];
|
||||
databasesArray: TIntegrationNotionDatabase[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const NotionWrapper = ({
|
||||
@@ -32,6 +34,7 @@ export const NotionWrapper = ({
|
||||
webAppUrl,
|
||||
surveys,
|
||||
databasesArray,
|
||||
attributeClasses,
|
||||
}: NotionWrapperProps) => {
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState(
|
||||
@@ -61,6 +64,7 @@ export const NotionWrapper = ({
|
||||
notionIntegration={notionIntegration}
|
||||
databases={databasesArray}
|
||||
selectedIntegration={selectedIntegration}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<ManageIntegration
|
||||
environment={environment}
|
||||
|
||||
@@ -22,6 +22,10 @@ export const TYPE_MAPPING = {
|
||||
[TSurveyQuestionType.Rating]: ["number"],
|
||||
[TSurveyQuestionType.PictureSelection]: ["url"],
|
||||
[TSurveyQuestionType.FileUpload]: ["url"],
|
||||
[TSurveyQuestionType.Date]: ["date"],
|
||||
[TSurveyQuestionType.Address]: ["rich_text"],
|
||||
[TSurveyQuestionType.Matrix]: ["rich_text"],
|
||||
[TSurveyQuestionType.Cal]: ["checkbox"],
|
||||
};
|
||||
|
||||
export const UNSUPPORTED_TYPES_BY_NOTION = [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper";
|
||||
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import {
|
||||
NOTION_AUTH_URL,
|
||||
NOTION_OAUTH_CLIENT_ID,
|
||||
@@ -23,10 +24,11 @@ const Page = async ({ params }) => {
|
||||
NOTION_AUTH_URL &&
|
||||
NOTION_REDIRECT_URI
|
||||
);
|
||||
const [surveys, notionIntegration, environment] = await Promise.all([
|
||||
const [surveys, notionIntegration, environment, attributeClasses] = await Promise.all([
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrationByType(params.environmentId, "notion"),
|
||||
getEnvironment(params.environmentId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!environment) {
|
||||
@@ -49,6 +51,7 @@ const Page = async ({ params }) => {
|
||||
notionIntegration={notionIntegration as TIntegrationNotion}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
databasesArray={databasesArray}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
|
||||
@@ -13,9 +13,9 @@ import Image from "next/image";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getWebhookCountBySource } from "@formbricks/lib/webhook/service";
|
||||
import { TIntegrationType } from "@formbricks/types/integration";
|
||||
import { Card } from "@formbricks/ui/Card";
|
||||
@@ -29,7 +29,7 @@ const Page = async ({ params }) => {
|
||||
const [
|
||||
environment,
|
||||
integrations,
|
||||
team,
|
||||
organization,
|
||||
session,
|
||||
userWebhookCount,
|
||||
zapierWebhookCount,
|
||||
@@ -38,7 +38,7 @@ const Page = async ({ params }) => {
|
||||
] = await Promise.all([
|
||||
getEnvironment(environmentId),
|
||||
getIntegrations(environmentId),
|
||||
getTeamByEnvironmentId(params.environmentId),
|
||||
getOrganizationByEnvironmentId(params.environmentId),
|
||||
getServerSession(authOptions),
|
||||
getWebhookCountBySource(environmentId, "user"),
|
||||
getWebhookCountBySource(environmentId, "zapier"),
|
||||
@@ -52,11 +52,11 @@ const Page = async ({ params }) => {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isViewer } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const isGoogleSheetsIntegrationConnected = isIntegrationConnected("googleSheets");
|
||||
|
||||
@@ -6,7 +6,8 @@ import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationSlack,
|
||||
@@ -28,6 +29,7 @@ interface AddChannelMappingModalProps {
|
||||
slackIntegration: TIntegrationSlack;
|
||||
channels: TIntegrationItem[];
|
||||
selectedIntegration?: (TIntegrationSlackConfigData & { index: number }) | null;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const AddChannelMappingModal = ({
|
||||
@@ -38,6 +40,7 @@ export const AddChannelMappingModal = ({
|
||||
channels,
|
||||
slackIntegration,
|
||||
selectedIntegration,
|
||||
attributeClasses,
|
||||
}: AddChannelMappingModalProps) => {
|
||||
const { handleSubmit } = useForm();
|
||||
|
||||
@@ -223,23 +226,25 @@ export const AddChannelMappingModal = ({
|
||||
<Label htmlFor="Surveys">Questions</Label>
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{checkForRecallInHeadline(selectedSurvey, "default")?.questions?.map((question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={selectedQuestions.includes(question.id)}
|
||||
onCheckedChange={() => {
|
||||
handleCheckboxChange(question.id);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">{getLocalizedValue(question.headline, "default")}</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions?.map(
|
||||
(question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={selectedQuestions.includes(question.id)}
|
||||
onCheckedChange={() => {
|
||||
handleCheckboxChange(question.id);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">{getLocalizedValue(question.headline, "default")}</span>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { authorize } from "@/app/(app)/environments/[environmentId]/integrations
|
||||
import slackLogo from "@/images/slacklogo.png";
|
||||
import { useState } from "react";
|
||||
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
|
||||
@@ -20,6 +21,7 @@ interface SlackWrapperProps {
|
||||
channelsArray: TIntegrationItem[];
|
||||
slackIntegration?: TIntegrationSlack;
|
||||
webAppUrl: string;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const SlackWrapper = ({
|
||||
@@ -29,6 +31,7 @@ export const SlackWrapper = ({
|
||||
channelsArray,
|
||||
slackIntegration,
|
||||
webAppUrl,
|
||||
attributeClasses,
|
||||
}: SlackWrapperProps) => {
|
||||
const [isConnected, setIsConnected] = useState(slackIntegration ? slackIntegration.config?.key : false);
|
||||
const [slackChannels, setSlackChannels] = useState(channelsArray);
|
||||
@@ -60,6 +63,7 @@ export const SlackWrapper = ({
|
||||
channels={slackChannels}
|
||||
slackIntegration={slackIntegration}
|
||||
selectedIntegration={selectedIntegration}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<ManageIntegration
|
||||
environment={environment}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
|
||||
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrationByType } from "@formbricks/lib/integration/service";
|
||||
@@ -14,10 +15,11 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
const Page = async ({ params }) => {
|
||||
const isEnabled = !!(SLACK_CLIENT_ID && SLACK_CLIENT_SECRET);
|
||||
|
||||
const [surveys, slackIntegration, environment] = await Promise.all([
|
||||
const [surveys, slackIntegration, environment, attributeClasses] = await Promise.all([
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrationByType(params.environmentId, "slack"),
|
||||
getEnvironment(params.environmentId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!environment) {
|
||||
@@ -41,6 +43,7 @@ const Page = async ({ params }) => {
|
||||
surveys={surveys}
|
||||
slackIntegration={slackIntegration as TIntegrationSlack}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
</PageContentWrapper>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
|
||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { TWebhook } from "@formbricks/types/webhooks";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
|
||||
import { timeSinceConditionally } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { TWebhook } from "@formbricks/types/webhooks";
|
||||
import { Badge } from "@formbricks/ui/Badge";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { redirect } from "next/navigation";
|
||||
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { ToasterClient } from "@formbricks/ui/ToasterClient";
|
||||
|
||||
@@ -22,9 +22,9 @@ const EnvLayout = async ({ children, params }) => {
|
||||
throw new AuthorizationError("Not authorized");
|
||||
}
|
||||
|
||||
const team = await getTeamByEnvironmentId(params.environmentId);
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
const organization = await getOrganizationByEnvironmentId(params.environmentId);
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -33,11 +33,11 @@ const EnvLayout = async ({ children, params }) => {
|
||||
<PosthogIdentify
|
||||
session={session}
|
||||
environmentId={params.environmentId}
|
||||
teamId={team.id}
|
||||
teamName={team.name}
|
||||
inAppSurveyBillingStatus={team.billing.features.inAppSurvey.status}
|
||||
linkSurveyBillingStatus={team.billing.features.linkSurvey.status}
|
||||
userTargetingBillingStatus={team.billing.features.userTargeting.status}
|
||||
organizationId={organization.id}
|
||||
organizationName={organization.name}
|
||||
inAppSurveyBillingStatus={organization.billing.features.inAppSurvey.status}
|
||||
linkSurveyBillingStatus={organization.billing.features.linkSurvey.status}
|
||||
userTargetingBillingStatus={organization.billing.features.userTargeting.status}
|
||||
/>
|
||||
<FormbricksClient session={session} />
|
||||
<ToasterClient />
|
||||
|
||||