Compare commits
132 Commits
v2.4.0
...
person-tab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec149d7fce | ||
|
|
f5e349a91f | ||
|
|
6b64367d99 | ||
|
|
fe9746ba67 | ||
|
|
e4009d5951 | ||
|
|
b1ed61c247 | ||
|
|
10255aa102 | ||
|
|
774c6f19a5 | ||
|
|
ebf35ea582 | ||
|
|
f13efc954e | ||
|
|
9ee052a229 | ||
|
|
152fbede90 | ||
|
|
c9b8ffa9ef | ||
|
|
04e16d44a1 | ||
|
|
29131f93c2 | ||
|
|
1e2fe7b066 | ||
|
|
426a0a3847 | ||
|
|
80ef504bef | ||
|
|
6ab83e25d3 | ||
|
|
688dc25990 | ||
|
|
c53b58c64f | ||
|
|
61d18edb5d | ||
|
|
07b5dfe28a | ||
|
|
f55cad0121 | ||
|
|
494299cd89 | ||
|
|
5cc071e5a8 | ||
|
|
0532f2744b | ||
|
|
43ea26a33a | ||
|
|
ec54e40a8b | ||
|
|
4b508f02e3 | ||
|
|
eec7e1b62a | ||
|
|
39e87eb8d3 | ||
|
|
780115ffb8 | ||
|
|
c7c4ba6e49 | ||
|
|
b9a7edf1f5 | ||
|
|
86bf2accc9 | ||
|
|
954c435404 | ||
|
|
538c1bd809 | ||
|
|
e0767881f2 | ||
|
|
7d0cbad326 | ||
|
|
0ce7703ab8 | ||
|
|
4362fdf35a | ||
|
|
4bcca2daf4 | ||
|
|
a1d83ac7b9 | ||
|
|
492729baf3 | ||
|
|
4003d21826 | ||
|
|
b10d398728 | ||
|
|
198df84b89 | ||
|
|
211fc22b2a | ||
|
|
4a0a5c9591 | ||
|
|
8f51afe198 | ||
|
|
40b6642ef0 | ||
|
|
0076cbaf54 | ||
|
|
14e0d57091 | ||
|
|
3e79ec9a61 | ||
|
|
42cd7d3b77 | ||
|
|
6f4c65c178 | ||
|
|
863424ffd7 | ||
|
|
c35cfbd170 | ||
|
|
22425726d1 | ||
|
|
94e8c1da68 | ||
|
|
55c305c569 | ||
|
|
ad028947e0 | ||
|
|
212e0753c8 | ||
|
|
711dc83f5c | ||
|
|
6314eeda0a | ||
|
|
cf783ea480 | ||
|
|
899fbef948 | ||
|
|
4a6d7952a7 | ||
|
|
5443226e27 | ||
|
|
89ffe99dcc | ||
|
|
ede306b88e | ||
|
|
0acc49c57d | ||
|
|
2bbeb040c2 | ||
|
|
5689c36b12 | ||
|
|
f4a367d2de | ||
|
|
afe042ecfc | ||
|
|
c108cd4780 | ||
|
|
d84146fd88 | ||
|
|
30e6316e16 | ||
|
|
6835e585b0 | ||
|
|
49d4f43652 | ||
|
|
70dd9c7724 | ||
|
|
f386e47efa | ||
|
|
ec16159497 | ||
|
|
5ba2959eb4 | ||
|
|
e9c5b00628 | ||
|
|
8e1f43eb8b | ||
|
|
3788293bc0 | ||
|
|
89a2e26f25 | ||
|
|
2c453bd491 | ||
|
|
0aebf234f9 | ||
|
|
3e25ef4b5a | ||
|
|
3dc3edb83e | ||
|
|
4ebe144191 | ||
|
|
49cd06a9b4 | ||
|
|
e0208da0ac | ||
|
|
1f41770060 | ||
|
|
3eff06281c | ||
|
|
5848dfb4f3 | ||
|
|
89f27adce5 | ||
|
|
bbab7fa672 | ||
|
|
fb149796fa | ||
|
|
3939013415 | ||
|
|
7fe18e99c3 | ||
|
|
6ddfd29be8 | ||
|
|
46efad94db | ||
|
|
b7ea073204 | ||
|
|
0d9c90ceeb | ||
|
|
3ba23e1787 | ||
|
|
e365718556 | ||
|
|
d65a49a7e7 | ||
|
|
7960aaf5d5 | ||
|
|
32b3a7d1d0 | ||
|
|
53fb976fb6 | ||
|
|
fffe71aa7e | ||
|
|
75b0a3a407 | ||
|
|
158689672a | ||
|
|
24e43dd1a2 | ||
|
|
b34366aaf7 | ||
|
|
2856c8d125 | ||
|
|
4ac1e1d798 | ||
|
|
b15d23035c | ||
|
|
8b2ea63ccb | ||
|
|
b0c65c76e6 | ||
|
|
c65c1af023 | ||
|
|
f10bd9c0d8 | ||
|
|
a6ac78294b | ||
|
|
04c9ead19d | ||
|
|
3c3798ee98 | ||
|
|
8df722ab02 | ||
|
|
dbe5ca60cd |
25
.env.example
@@ -11,33 +11,26 @@ WEBAPP_URL=http://localhost:3000
|
||||
# Required for next-auth. Should be the same as WEBAPP_URL
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# Set this if you want to have a shorter link for surveys
|
||||
SHORT_URL_BASE=
|
||||
|
||||
# Encryption keys
|
||||
# Please set both for now, we will change this in the future
|
||||
|
||||
# You can use: `openssl rand -hex 32` to generate one
|
||||
ENCRYPTION_KEY=
|
||||
|
||||
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
||||
# You can use: `openssl rand -hex 32` to generate a secure one
|
||||
NEXTAUTH_SECRET=
|
||||
|
||||
# API Secret for running cron jobs. (mandatory)
|
||||
# You can use: `openssl rand -hex 32` to generate a secure one
|
||||
CRON_SECRET=
|
||||
|
||||
##############
|
||||
# DATABASE #
|
||||
##############
|
||||
|
||||
DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=public'
|
||||
|
||||
###############
|
||||
# NEXT AUTH #
|
||||
###############
|
||||
|
||||
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
||||
# You can use: `openssl rand -hex 32` to generate a secure one
|
||||
NEXTAUTH_SECRET=RANDOM_STRING
|
||||
|
||||
# API Secret for running cron jobs. (mandatory)
|
||||
# You can use: `openssl rand -hex 32` to generate a secure one
|
||||
CRON_SECRET=RANDOM_STRING
|
||||
|
||||
################
|
||||
# MAIL SETUP #
|
||||
################
|
||||
@@ -77,6 +70,8 @@ S3_BUCKET_NAME=
|
||||
# Configure a third party S3 compatible storage service endpoint like StorJ leave empty if you use Amazon S3
|
||||
# e.g., https://gateway.storjshare.io
|
||||
S3_ENDPOINT_URL=
|
||||
# Force path style for S3 compatible storage (0 for disabled, 1 for enabled)
|
||||
S3_FORCE_PATH_STYLE=0
|
||||
|
||||
#####################
|
||||
# Disable Features #
|
||||
|
||||
33
.github/ISSUE_TEMPLATE/oss-gg-hack-submission.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: oss.gg hack submission 🕹️
|
||||
description: "Submit your contribution for the for the oss.gg hackathon"
|
||||
title: "[oss.gg hackathon]"
|
||||
labels: 🕹️ oss.gg, player submission
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: contribution-name
|
||||
attributes:
|
||||
label: What side quest or challenge are you solving?
|
||||
description: Add the name of the side quest or challenge.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: points
|
||||
attributes:
|
||||
label: Points
|
||||
description: How many points are assigned to this contribution?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: What's the task your performed?
|
||||
validations:
|
||||
- type: textarea
|
||||
id: proof
|
||||
attributes:
|
||||
label: Provide proof that you've completed the task
|
||||
description: Screenshots, loom recordings, links to the content you shared or interacted with.
|
||||
validations:
|
||||
required: true
|
||||
4
.github/actions/cache-build-web/action.yml
vendored
@@ -53,10 +53,12 @@ runs:
|
||||
run: cp .env.example .env
|
||||
shell: bash
|
||||
|
||||
- name: Fill ENCRYPTION_KE, ENTERPRISE_LICENSE_KEY and E2E_TESTING in .env
|
||||
- name: Fill ENCRYPTION_KEY, ENTERPRISE_LICENSE_KEY and E2E_TESTING in .env
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/ENTERPRISE_LICENSE_KEY=.*/ENTERPRISE_LICENSE_KEY=${RANDOM_KEY}/" .env
|
||||
echo "E2E_TESTING=${{ inputs.e2e_testing_mode }}" >> .env
|
||||
shell: bash
|
||||
|
||||
50
.github/workflows/e2e.yml
vendored
@@ -2,6 +2,8 @@ name: E2E Tests
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
env:
|
||||
TELEMETRY_DISABLED: 1
|
||||
jobs:
|
||||
build:
|
||||
name: Run E2E Tests
|
||||
@@ -25,29 +27,35 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
|
||||
- name: Build & Cache Web Binaries
|
||||
uses: ./.github/actions/cache-build-web
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
e2e_testing_mode: "1"
|
||||
|
||||
- name: Check if pnpm is installed
|
||||
id: pnpm-check
|
||||
run: |
|
||||
if pnpm --version; then
|
||||
echo "pnpm is installed."
|
||||
echo "PNPM_INSTALLED=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "pnpm is not installed."
|
||||
echo "PNPM_INSTALLED=false" >> $GITHUB_ENV
|
||||
fi
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install pnpm
|
||||
if: env.PNPM_INSTALLED == 'false'
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
if: env.PNPM_INSTALLED == 'false'
|
||||
run: pnpm install
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
shell: bash
|
||||
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
shell: bash
|
||||
|
||||
- name: Fill ENCRYPTION_KEY, ENTERPRISE_LICENSE_KEY and E2E_TESTING in .env
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/ENTERPRISE_LICENSE_KEY=.*/ENTERPRISE_LICENSE_KEY=${RANDOM_KEY}/" .env
|
||||
echo "E2E_TESTING=1" >> .env
|
||||
shell: bash
|
||||
|
||||
- name: Build App
|
||||
run: |
|
||||
pnpm build --filter=@formbricks/web...
|
||||
|
||||
- name: Apply Prisma Migrations
|
||||
run: |
|
||||
@@ -70,15 +78,7 @@ jobs:
|
||||
sleep 10
|
||||
done
|
||||
|
||||
- name: Cache Playwright
|
||||
uses: actions/cache@v3
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install Playwright
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: pnpm exec playwright install --with-deps
|
||||
|
||||
- name: Run E2E Tests
|
||||
|
||||
8
.github/workflows/lint.yml
vendored
@@ -25,10 +25,12 @@ jobs:
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Generate Random ENCRYPTION_KEY and fill in .env
|
||||
- name: Generate Random ENCRYPTION_KEY, CRON_SECRET & NEXTAUTH_SECRET and fill in .env
|
||||
run: |
|
||||
ENCRYPTION_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${ENCRYPTION_KEY}/" .env
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
name: Docker for Data Migrations
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
@@ -12,7 +7,6 @@ on:
|
||||
- "v*"
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: formbricks/data-migrations
|
||||
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/formbricks?schema=public"
|
||||
@@ -23,8 +17,6 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
# This is used to complete the identity challenge
|
||||
# with sigstore/fulcio when running outside of PRs.
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
@@ -50,6 +42,7 @@ jobs:
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=raw,value=${{ github.ref_name }}
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
@@ -66,3 +59,4 @@ jobs:
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
run: |
|
||||
cosign sign --yes ghcr.io/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
|
||||
cosign sign --yes ghcr.io/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
8
.github/workflows/test.yml
vendored
@@ -25,10 +25,12 @@ jobs:
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Generate Random ENCRYPTION_KEY and fill in .env
|
||||
- name: Generate Random ENCRYPTION_KEY, CRON_SECRET & NEXTAUTH_SECRET and fill in .env
|
||||
run: |
|
||||
ENCRYPTION_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${ENCRYPTION_KEY}/" .env
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
2
.gitignore
vendored
@@ -56,5 +56,5 @@ Zone.Identifier
|
||||
packages/lib/uploads
|
||||
|
||||
# Vite Timestamps
|
||||
vite.config.*.timestamp-*
|
||||
*vite.config.*.timestamp-*
|
||||
|
||||
|
||||
@@ -144,6 +144,12 @@ Or you can also deploy Formbricks on [RepoCloud](https://repocloud.io) using the
|
||||
|
||||
[](https://repocloud.io/details/?app_id=254)
|
||||
|
||||
##### Zeabur
|
||||
|
||||
Or you can also deploy Formbricks on [Zeabur](https://zeabur.com) using the button below.
|
||||
|
||||
[](https://zeabur.com/templates/G4TUJL)
|
||||
|
||||
<a id="development"></a>
|
||||
|
||||
## 👨💻 Development
|
||||
|
||||
2
apps/demo-react-native/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
EXPO_PUBLIC_API_HOST=http://192.168.178.20:3000
|
||||
EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=clzr04nkd000bcdl110j0ijyq
|
||||
7
apps/demo-react-native/.eslintrc.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ["@formbricks/eslint-config/react.js"],
|
||||
parserOptions: {
|
||||
project: "tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
};
|
||||
35
apps/demo-react-native/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
|
||||
# Native
|
||||
*.orig.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
|
||||
# Metro
|
||||
.metro-health-check*
|
||||
|
||||
# debug
|
||||
npm-debug.*
|
||||
yarn-debug.*
|
||||
yarn-error.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
0
apps/demo-react-native/.npmrc
Normal file
34
apps/demo-react-native/app.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "react-native-demo",
|
||||
"slug": "react-native-demo",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "light",
|
||||
"splash": {
|
||||
"image": "./assets/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"jsEngine": "hermes",
|
||||
"assetBundlePatterns": ["**/*"],
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"infoPlist": {
|
||||
"NSCameraUsageDescription": "Take pictures for certain activities.",
|
||||
"NSPhotoLibraryUsageDescription": "Select pictures for certain activities.",
|
||||
"NSMicrophoneUsageDescription": "Need microphone access for recording videos."
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
apps/demo-react-native/assets/adaptive-icon.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/demo-react-native/assets/favicon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
apps/demo-react-native/assets/icon.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
apps/demo-react-native/assets/splash.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
6
apps/demo-react-native/babel.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = function babel(api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ["babel-preset-expo"],
|
||||
};
|
||||
};
|
||||
7
apps/demo-react-native/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { registerRootComponent } from "expo";
|
||||
import { LogBox } from "react-native";
|
||||
import App from "./src/app";
|
||||
|
||||
registerRootComponent(App);
|
||||
|
||||
LogBox.ignoreAllLogs();
|
||||
21
apps/demo-react-native/metro.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// Learn more https://docs.expo.io/guides/customizing-metro
|
||||
const path = require("node:path");
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
|
||||
// Find the workspace root, this can be replaced with `find-yarn-workspace-root`
|
||||
const workspaceRoot = path.resolve(__dirname, "../..");
|
||||
const projectRoot = __dirname;
|
||||
|
||||
const config = getDefaultConfig(projectRoot);
|
||||
|
||||
// 1. Watch all files within the monorepo
|
||||
config.watchFolders = [workspaceRoot];
|
||||
// 2. Let Metro know where to resolve packages, and in what order
|
||||
config.resolver.nodeModulesPaths = [
|
||||
path.resolve(projectRoot, "node_modules"),
|
||||
path.resolve(workspaceRoot, "node_modules"),
|
||||
];
|
||||
// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
|
||||
config.resolver.disableHierarchicalLookup = true;
|
||||
|
||||
module.exports = config;
|
||||
28
apps/demo-react-native/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@formbricks/demo-react-native",
|
||||
"version": "1.0.0",
|
||||
"main": "./index.js",
|
||||
"scripts": {
|
||||
"dev": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web",
|
||||
"eject": "expo eject",
|
||||
"clean": "rimraf .turbo node_modules .expo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formbricks/js": "workspace:*",
|
||||
"@formbricks/react-native": "workspace:*",
|
||||
"expo": "^51.0.26",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
"react": "^18.2.0",
|
||||
"react-native": "^0.74.4",
|
||||
"react-native-webview": "13.8.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@types/react": "~18.2.79",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
52
apps/demo-react-native/src/app.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import { Button, LogBox, StyleSheet, Text, View } from "react-native";
|
||||
import Formbricks, { track } from "@formbricks/react-native";
|
||||
|
||||
LogBox.ignoreAllLogs();
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
if (!process.env.EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID) {
|
||||
throw new Error("EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID is required");
|
||||
}
|
||||
|
||||
if (!process.env.EXPO_PUBLIC_API_HOST) {
|
||||
throw new Error("EXPO_PUBLIC_API_HOST is required");
|
||||
}
|
||||
|
||||
const config = {
|
||||
environmentId: process.env.EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
|
||||
apiHost: process.env.EXPO_PUBLIC_API_HOST,
|
||||
userId: "random-user-id",
|
||||
attributes: {
|
||||
language: "en",
|
||||
testAttr: "attr-test",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>Formbricks React Native SDK Demo</Text>
|
||||
|
||||
<Button
|
||||
title="Trigger Code Action"
|
||||
onPress={() => {
|
||||
track("code").catch((error: unknown) => {
|
||||
// eslint-disable-next-line no-console -- logging is allowed in demo apps
|
||||
console.error("Error tracking event:", error);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<StatusBar style="auto" />
|
||||
<Formbricks initConfig={config} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#fff",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
});
|
||||
6
apps/demo-react-native/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ const AppPage = ({}) => {
|
||||
return (
|
||||
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
|
||||
<div className="flex flex-col justify-between md:flex-row">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col items-center gap-2 sm:flex-row">
|
||||
<SurveySwitch value="app" formbricks={formbricks} />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
|
||||
|
||||
@@ -58,7 +58,7 @@ const AppPage = ({}) => {
|
||||
return (
|
||||
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
|
||||
<div className="flex flex-col items-center justify-between md:flex-row">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col items-center gap-2 sm:flex-row">
|
||||
<SurveySwitch value="website" formbricks={formbricks} />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
|
||||
@@ -128,6 +128,7 @@ const AppPage = ({}) => {
|
||||
}}>
|
||||
Reset
|
||||
</button>
|
||||
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300">
|
||||
If you made a change in Formbricks app and it does not seem to work, hit 'Reset' and
|
||||
try again.
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 15 KiB |
@@ -8,9 +8,9 @@ import RideHailing from "./ride-hailing.webp";
|
||||
import UpsellMiro from "./upsell-miro.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Advanced Targeting in Surveys | Formbricks",
|
||||
title: "Advanced Targeting for In-app Surveys | Formbricks",
|
||||
description:
|
||||
"Advanced Targeting allows you to show surveys to just the right group of people. You can target surveys based on user attributes, user events, metadata , literally anything! This helps you get more relevant feedback and make data-driven decisions. All of this without writing a single line of code.",
|
||||
"Advanced Targeting allows you to show surveys to just the right group of people. You can target surveys based on user attributes, user events, and metadata. This helps you get more relevant feedback and make data-driven decisions.",
|
||||
};
|
||||
|
||||
#### App Surveys
|
||||
@@ -32,8 +32,7 @@ Advanced Targeting allows you to show surveys to the right group of people. You
|
||||
## How to setup Advanced Targeting
|
||||
|
||||
<Note>
|
||||
Advanced Targeting is available on the Pro plan! Don't worry, you just need to enter your credit card
|
||||
details to start the freemium plan.
|
||||
Advanced Targeting is available on the Pro plan!
|
||||
</Note>
|
||||
|
||||
1. On the Formbricks dashboard, click on **People** tab from the top navigation bar.
|
||||
@@ -72,25 +71,7 @@ Advanced Targeting allows you to show surveys to the right group of people. You
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
3. Target High Value users who have $100k+ in their bank account, own 20+ stocks, and have are an active user.
|
||||
|
||||
<MdxImage
|
||||
src={Hni}
|
||||
alt="Target Active High Net Worth Individuals"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
4. Target Germans on mobile phones who have regenerated chatGPT answers frequently in the last quarter and did so today.
|
||||
|
||||
<MdxImage
|
||||
src={GermansGpt}
|
||||
alt="Target Germans on Mobile Phones who have regenerated chatGPT answers frequently in the last quarter and did so today"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
5. Sneak Peak: How we at Formbricks automate inviting power users to chat with us
|
||||
3. Sneak Peak: How we at Formbricks automate inviting power users to chat with us
|
||||
|
||||
<MdxImage
|
||||
src={PowerUsers}
|
||||
|
||||
@@ -31,12 +31,18 @@ const libraries = [
|
||||
description: "Simply add us to your router change and sit back!",
|
||||
logo: logoVueJs,
|
||||
},
|
||||
{
|
||||
href: "#react-native",
|
||||
name: "React Native",
|
||||
description: "Easily integrate our SDK with your React Native app for seamless survey support!",
|
||||
logo: logoReactJs,
|
||||
},
|
||||
];
|
||||
|
||||
export const Libraries = () => {
|
||||
return (
|
||||
<div className="my-16 xl:max-w-none">
|
||||
<div className="not-prose mt-4 grid grid-cols-1 gap-x-6 gap-y-10 border-slate-900/5 sm:grid-cols-2 xl:max-w-none xl:grid-cols-3 dark:border-white/5">
|
||||
<div className="not-prose mt-4 grid grid-cols-1 gap-x-6 gap-y-10 border-slate-900/5 xl:max-w-none xl:grid-cols-2 2xl:grid-cols-3 dark:border-white/5">
|
||||
{libraries.map((library) => (
|
||||
<a
|
||||
key={library.name}
|
||||
|
||||
@@ -346,6 +346,66 @@ router.afterEach((to, from) => {
|
||||
|
||||
Refer to our [Example VueJs project](https://github.com/formbricks/examples/tree/main/vuejs) for more help! Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
|
||||
|
||||
## React Native
|
||||
|
||||
Install the Formbricks React Native SDK using one of the package managers, i.e., npm, pnpm, or yarn.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Install Formbricks JS library">
|
||||
```shell {{ title: 'npm' }}
|
||||
npm install @formbricks/react-native
|
||||
```
|
||||
```shell {{ title: 'pnpm' }}
|
||||
pnpm add @formbricks/react-native
|
||||
```
|
||||
```shell {{ title: 'yarn' }}
|
||||
yarn add @formbricks/react-native
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
Now, update your App.js/App.tsx file to initialize Formbricks:
|
||||
<Col>
|
||||
<CodeGroup title="src/App.js">
|
||||
|
||||
```js
|
||||
// other imports
|
||||
import Formbricks from "@formbricks/react-native";
|
||||
|
||||
const config = {
|
||||
environmentId: "<environment-id>",
|
||||
apiHost: "<api-host>",
|
||||
userId: "<user-id>",
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<>
|
||||
{/* Your app content */}
|
||||
<Formbricks initConfig={config} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
### Required customizations to be made
|
||||
|
||||
<Properties>
|
||||
<Property name="environment-id" type="string">
|
||||
Formbricks Environment ID.
|
||||
</Property>
|
||||
<Property name="api-host" type="string">
|
||||
URL of the hosted Formbricks instance.
|
||||
</Property>
|
||||
<Property name="userId" type="string">
|
||||
User ID of the user who has active session.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
---
|
||||
|
||||
## Validate your setup
|
||||
|
||||
Once you have completed the steps above, you can validate your setup by checking the **Setup Checklist** in the Settings. Your widget status indicator should go from this:
|
||||
|
||||
@@ -19,10 +19,12 @@ export const metadata = {
|
||||
|
||||
# Quickstart
|
||||
|
||||
App surveys have 6-10x better conversion rates than emailed out surveys. This tutorial explains how to run an app survey in your web app in 10 to 15 minutes. Let’s go!
|
||||
App surveys have 6-10x better conversion rates than emailed surveys. This tutorial explains how to run a survey in both your web app and mobile app (React Native) in just 10 to 15 minutes. Let’s go!
|
||||
|
||||
<Note>
|
||||
App Surveys are ideal for websites that **have a user authentication** system. If you are looking to run surveys on your public facing website, head over to the [Website Surveys Quickstart Guide](/website-surveys/quickstart).
|
||||
App Surveys are ideal for websites that **have a user authentication** system. If you are looking to run
|
||||
surveys on your public facing website, head over to the [Website Surveys Quickstart
|
||||
Guide](/website-surveys/quickstart).
|
||||
</Note>
|
||||
|
||||
1. **Create a free Formbricks Cloud account**: While you can [self-host](/self-hosting/deployment) Formbricks, but the quickest and easiest way to get started is with the free Cloud plan. Just [sign up here](https://app.formbricks.com/auth/signup) and you'll be guided to our onboarding like below:
|
||||
@@ -35,7 +37,7 @@ App surveys have 6-10x better conversion rates than emailed out surveys. This tu
|
||||
src={I1}
|
||||
alt="Choose website survey from survey type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2. **Connect your App/Website**: Once you get through a couple of onboarding steps, you’ll be asked to connect your app or website. This is where you’ll find the code snippet for both HTML as well as the npm package which you need to embed in your app:
|
||||
|
||||
@@ -79,27 +79,6 @@ Promise<{ id: string }, NetworkError | Error>
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
- Update Display
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Update Display">
|
||||
|
||||
```javascript {{ title: 'Update Display Method Call'}}
|
||||
await api.client.display.update(
|
||||
displayId: "<your-display-id>",
|
||||
{
|
||||
userId: "<your-user-id>", // optional
|
||||
responseId: "<your-response-id>", // optional
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
```javascript {{ title: 'Update Display Method Return Type' }}
|
||||
Promise<{ }, NetworkError | Error]>
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
## Responses
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@ import I3 from "./images/3-survey-logs-in-app-survey-popup.webp";
|
||||
export const metadata = {
|
||||
title: "Formbricks App Survey SDK",
|
||||
description:
|
||||
"An overview of all available methods & how to integrate Formbricks App Surveys for frontend developers in web applications. Learn the key methods, configuration settings, and best practices.",
|
||||
"Integrate Formbricks App Surveys into your web apps with the Formbricks JS SDK for App Surveys. Learn how to initialize Formbricks, set attributes, track actions, and troubleshoot common issues.",
|
||||
};
|
||||
|
||||
#### Developer Docs
|
||||
|
||||
# SDK: App Survey
|
||||
# SDK: Run Surveys Inside Your Web Apps
|
||||
|
||||
### Overview
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ cp .env.example .env
|
||||
4. Generate & set some secret values mandatory for the `ENCRYPTION_KEY`, `NEXTAUTH_SECRET` and `CRON_SECRET` in the .env file. You can use the following command to generate the random string of required length:
|
||||
|
||||
- For Linux
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="For Linux">
|
||||
|
||||
@@ -106,7 +107,8 @@ sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
- For Mac
|
||||
- For Mac
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="For Mac">
|
||||
|
||||
|
||||
@@ -10,11 +10,15 @@ export const metadata = {
|
||||
|
||||
Welcome to the Developer Docs section, your comprehensive resource for integrating and utilizing Formbricks SDKs &APIs, as well as contributing to our open source codebase. Here's what you can expect to find in this section:
|
||||
|
||||
### [SDK: App Survey](/developer-docs/app-survey-sdk)
|
||||
### [SDK: React Native Apps](/developer-docs/react-native-in-app-surveys)
|
||||
|
||||
The Formbricks React Native SDK for App Surveys is designed for React Native applications, enabling seamless integration of surveys within your mobile apps. Dive into the documentation to learn how to leverage the SDK for app surveys and engage with your users effectively.
|
||||
|
||||
### [SDK: Web Apps](/developer-docs/app-survey-sdk)
|
||||
|
||||
The Formbricks JS SDK tailored for App Surveys is designed for applications where users are logged in, allowing for targeted and identified interactions within Formbricks. This SDK is particularly useful for advanced user tracking, enabling deeper insights into user behavior. Learn how to seamlessly integrate Formbricks into your applications and harness valuable insights from your logged-in users.
|
||||
|
||||
### [SDK: Website Survey](/developer-docs/website-survey-sdk)
|
||||
### [SDK: Public Websites](/developer-docs/website-survey-sdk)
|
||||
|
||||
The Formbricks JS SDK for Website Surveys is ideal for public-facing websites without user authentication. It's recommended for pages with high traffic and no authentication walls, facilitating quick and efficient survey collection. Dive into the documentation to discover how to deploy surveys on your website and effectively engage with your audience.
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
export const metadata = {
|
||||
title: "React Native: Formbricks App SDK",
|
||||
description:
|
||||
"Integrate Formbricks App Surveys into your React Native apps with the Formbricks React Native SDK.",
|
||||
};
|
||||
|
||||
#### Developer Docs
|
||||
|
||||
# React Native: In App Surveys
|
||||
|
||||
### Overview
|
||||
|
||||
The Formbricks React Native SDK can be used for seamlessly integrating App Surveys into your React Native Apps. Here, w'll explore how to leverage the SDK for in app surveys. The SDK is [available on npm.](https://www.npmjs.com/package/@formbricks/react-native)
|
||||
|
||||
### Install
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="npm">
|
||||
|
||||
```js {{ title: 'npm' }}
|
||||
npm install @formbricks/react-native
|
||||
```
|
||||
|
||||
```js {{ title: 'yarn' }}
|
||||
yarn add @formbricks/react-native
|
||||
```
|
||||
|
||||
```js {{ title: 'pnpm' }}
|
||||
pnpm add @formbricks/react-native
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
## Methods
|
||||
|
||||
### Initialize Formbricks
|
||||
|
||||
In your React Native app, initialize the Formbricks React Native Client for app surveys where you pass the userId (creates a user if not existing in Formbricks) to attribute & target the user based on their actions.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Initialize Formbricks">
|
||||
|
||||
```javascript
|
||||
// other imports
|
||||
import Formbricks from "@formbricks/react-native";
|
||||
|
||||
const config = {
|
||||
environmentId: "<environment-id>",
|
||||
apiHost: "<api-host>",
|
||||
userId: "<user-id>",
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<>
|
||||
{/* Your app content */}
|
||||
<Formbricks initConfig={config} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
The moment you initialise Formbricks, your user will start seeing surveys that get triggered on simpler actions such as on New Session.
|
||||
|
||||
### Set Attribute
|
||||
|
||||
You can set custom attributes for the identified user. This can be helpful for segmenting users based on specific characteristics or properties. To learn how to set custom user attributes, please check out our [User Attributes Guide](/app-surveys/user-identification).
|
||||
|
||||
<Col>
|
||||
<CodeGroup>
|
||||
|
||||
```js
|
||||
formbricks.setAttribute("Plan", "Paid");
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
### Track Action
|
||||
|
||||
Track user actions to trigger surveys based on user interactions, such as button clicks or scrolling:
|
||||
|
||||
<Col>
|
||||
<CodeGroup>
|
||||
|
||||
```js
|
||||
formbricks.track("Clicked on Claim");
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
### Logout
|
||||
|
||||
To log out and deinitialize Formbricks, use the formbricks.logout() function. This action clears the current initialization configuration and erases stored frontend information, such as the surveys a user has viewed or completed. It's an important step when a user logs out of your application or when you want to reset Formbricks.
|
||||
|
||||
<Col>
|
||||
<CodeGroup>
|
||||
|
||||
```js
|
||||
formbricks.logout();
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
After calling formbricks.logout(), you'll need to reinitialize Formbricks before using any of its features again. Ensure that you properly reinitialize Formbricks to avoid unexpected errors or behavior in your application.
|
||||
|
||||
### Reset
|
||||
|
||||
Reset the current instance and fetch the latest surveys and state again:
|
||||
|
||||
<Col>
|
||||
<CodeGroup>
|
||||
|
||||
```js
|
||||
formbricks.reset();
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
@@ -39,7 +39,7 @@ We currently have the following Management API methods exposed and below is thei
|
||||
- [Me API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#79e08365-641d-4b2d-aea2-9a855e0438ec) - Retrieve Account Information
|
||||
- [People API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#cffc27a6-dafb-428f-8ea7-5165bedb911e) - List and Delete People
|
||||
- [Response API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#e544ec0d-8b30-4e33-8d35-2441cb40d676) - List, List by Survey, Update, and Delete Responses
|
||||
- [Survey API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#953189b2-37b5-4429-a7bd-f4d01ceae242) - List, Create, Update, and Delete Surveys
|
||||
- [Survey API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#953189b2-37b5-4429-a7bd-f4d01ceae242) - List, Create, Update, generate multiple suId and Delete Surveys
|
||||
- [Webhook API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#62e6ec65-021b-42a4-ac93-d1434b393c6c) - List, Create, and Delete Webhooks
|
||||
|
||||
## How to Generate an API key
|
||||
|
||||
@@ -4,12 +4,12 @@ import I1 from "./images/1-set-up-website-micro-survey-popup.webp";
|
||||
export const metadata = {
|
||||
title: "Formbricks Website Survey SDK",
|
||||
description:
|
||||
"An overview of all available methods & how to integrate Formbricks Website Surveys for frontend developers in public-facing web applications. Learn the key methods, configuration settings, and best practices.",
|
||||
"Run targeted pop-up surveys on your public websites with the Formbricks JS SDK for Website Surveys. Learn how to integrate the SDK, track user actions, and trigger surveys based on user interactions.",
|
||||
};
|
||||
|
||||
#### Developer Docs
|
||||
|
||||
# SDK: Website Survey
|
||||
# SDK: Run Surveys On Public Websites
|
||||
|
||||
### Overview
|
||||
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import StepOne from "./images/StepOne.webp";
|
||||
import StepThree from "./images/StepThree.webp";
|
||||
import StepTwo from "./images/StepTwo.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Custom Start & End Conditions for Surveys",
|
||||
description:
|
||||
"Optimize your survey management with custom Start & End Conditions in Formbricks. This feature allows you to control exactly when your survey is available for responses and when it should close, making it ideal for time-sensitive or number-of-response-limited surveys.",
|
||||
};
|
||||
|
||||
# Custom Start & End Conditions
|
||||
|
||||
Optimize your survey management with custom Start & End Conditions in Formbricks. This feature allows you to control exactly when your survey is available for responses and when it should close, making it ideal for time-sensitive or number-of-response-limited surveys.
|
||||
|
||||
Configure your surveys to open and close based on specific criteria. Here’s how to set up these conditions:
|
||||
|
||||
### **Opening a Survey**
|
||||
|
||||
**1. Schedule a Start Date:**
|
||||
|
||||
- **How to Set**: Open the Survey Editor, switch to the Settings tab. Scroll down to Response Options, Toggle the “Release Survey on Date”.
|
||||
|
||||
<MdxImage
|
||||
src={StepOne}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
- **Details**: Choose the date and time when the survey should become available to respondents. All times follow UTC timezone.
|
||||
- **Use Case**: This is useful for launching surveys in alignment with events, product releases, or specific marketing campaigns.
|
||||
|
||||
### **Closing a Survey**
|
||||
|
||||
**1. Limit by Number of Responses:**
|
||||
|
||||
- **How to Set**: Open the Survey Editor, switch to the Settings tab. Scroll down to Response Options, Toggle the “Close survey on response limit”.
|
||||
{" "}
|
||||
<MdxImage
|
||||
src={StepTwo}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
- **Details**: Set a specific number of responses after which the survey automatically closes.
|
||||
- **Use Case**: Perfect for limited offers, exclusive surveys, or when you need a precise sample size for statistical significance.
|
||||
|
||||
**2. Schedule an End Date:**
|
||||
|
||||
- **How to Set**: Open the Survey Editor, switch to the Settings tab. Scroll down to Response Options, Toggle the “Close survey on date”.
|
||||
|
||||
<MdxImage
|
||||
src={StepThree}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
- **Details**: Define a specific date and time for the survey to close. This also follows UTC timezone. - **Use
|
||||
Case**: Essential for surveys linked to time-bound events or studies where data collection needs to end at a specific
|
||||
point.
|
||||
|
||||
### **Summary**
|
||||
|
||||
Setting up Custom Start & End Conditions in Formbricks allows you to control the availability and duration of your surveys with precision. Whether you are conducting academic research, market analysis, or gathering event feedback, these settings help ensure that your data collection aligns perfectly with your objectives.
|
||||
|
||||
---
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
28
apps/docs/app/global/limit-submissions/page.mdx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import StepOne from "./images/StepOne.webp";
|
||||
import StepThree from "./images/StepThree.webp";
|
||||
import StepTwo from "./images/StepTwo.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Set a Maximum Number of Submissions for Surveys",
|
||||
description:
|
||||
"Limit the number of responses your survey can receive.",
|
||||
};
|
||||
|
||||
# Limit by Number of Submissions
|
||||
|
||||
Automatically close your survey after a specific number of responses with Formbricks. This feature is perfect for limited offers, exclusive surveys, or when you need a precise sample size for statistical significance.
|
||||
|
||||
- **How to**: Open the Survey Editor, switch to the Settings tab. Scroll down to Response Options, Toggle the “Close survey on response limit”.
|
||||
{" "}
|
||||
<MdxImage
|
||||
src={StepTwo}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
- **Details**: Set a specific number of responses after which the survey automatically closes.
|
||||
- **Use Case**: Perfect for limited offers, exclusive surveys, or when you need a precise sample size for statistical significance.
|
||||
|
||||
---
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 16 KiB |
51
apps/docs/app/global/schedule-start-end-dates/page.mdx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import StepOne from "./images/StepOne.webp";
|
||||
import StepThree from "./images/StepThree.webp";
|
||||
import StepTwo from "./images/StepTwo.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Schedule Start & End Dates for Surveys",
|
||||
description:
|
||||
"Automatically release and close surveys based on specific dates.",
|
||||
};
|
||||
|
||||
# Schedule Start & End Dates
|
||||
|
||||
Optimize your survey management with custom Start & End Conditions in Formbricks. This feature allows you to control exactly when your survey is available for responses and when it should close, making it ideal for time-sensitive or number-of-response-limited surveys.
|
||||
|
||||
Configure your surveys to open and close based on specific criteria. Here’s how to set up these conditions:
|
||||
|
||||
## **Schedule a Survey Release**
|
||||
|
||||
- **How to**: Open the Survey Editor, switch to the Settings tab. Scroll down to Response Options, Toggle the “Release Survey on Date”.
|
||||
|
||||
<MdxImage
|
||||
src={StepOne}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
- **Details**: Choose the date and time when the survey should become available to respondents. All times follow UTC timezone.
|
||||
- **Use Case**: This is useful for launching surveys in alignment with events, product releases, or specific marketing campaigns.
|
||||
|
||||
## **Automatically Closing a Survey**
|
||||
|
||||
- **How to**: Open the Survey Editor, switch to the Settings tab. Scroll down to Response Options, Toggle the “Close survey on date”.
|
||||
|
||||
<MdxImage
|
||||
src={StepThree}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
- **Details**: Define a specific date and time for the survey to close. This also follows UTC timezone. - **Use
|
||||
Case**: Essential for surveys linked to time-bound events or studies where data collection needs to end at a specific
|
||||
point.
|
||||
|
||||
### **Summary**
|
||||
|
||||
Setting up Start & End Dates in Formbricks allows you to control the availability and duration of your surveys with precision. Whether you are conducting academic research, market analysis, or gathering event feedback, these settings help ensure that your data collection aligns perfectly with your objectives.
|
||||
|
||||
---
|
||||
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -1,3 +1,16 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import EntraIDAppReg01 from "./images/entra_app_reg_01.png";
|
||||
import EntraIDAppReg02 from "./images/entra_app_reg_02.png";
|
||||
import EntraIDAppReg03 from "./images/entra_app_reg_03.png";
|
||||
import EntraIDAppReg04 from "./images/entra_app_reg_04.png";
|
||||
import EntraIDAppReg05 from "./images/entra_app_reg_05.png";
|
||||
import EntraIDAppReg06 from "./images/entra_app_reg_06.png";
|
||||
import EntraIDAppReg07 from "./images/entra_app_reg_07.png";
|
||||
import EntraIDAppReg08 from "./images/entra_app_reg_08.png";
|
||||
import EntraIDAppReg09 from "./images/entra_app_reg_09.png";
|
||||
import EntraIDAppReg10 from "./images/entra_app_reg_10.png";
|
||||
|
||||
export const metadata = {
|
||||
title: "Configure Formbricks with External auth providers",
|
||||
description:
|
||||
@@ -48,8 +61,6 @@ These variables are present inside your machine’s docker-compose file. Restart
|
||||
| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | |
|
||||
| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | |
|
||||
| TELEMETRY_DISABLED | Disables telemetry if set to 1. | optional | |
|
||||
| INSTANCE_ID | Instance ID for Formbricks Cloud to be sent to Telemetry. | optional | |
|
||||
| INTERNAL_SECRET | Internal Secret (Currently we overwrite the value with a random value). | optional | |
|
||||
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b |
|
||||
| DEFAULT_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 |
|
||||
@@ -135,26 +146,141 @@ GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||
- Navigate to your Docker setup directory where your `docker-compose.yml` file is located.
|
||||
- Run the following command to bring down your current Docker containers and then bring them back up with the updated environment configuration:
|
||||
|
||||
### Azure SSO OAuth
|
||||
### Microsoft Entra ID (Azure Active Directory) SSO OAuth
|
||||
|
||||
Have an Azure Active Directory (AAD) instance? Integrate it with your Formbricks instance to allow users to log in using their existing AAD credentials. This guide will walk you through the process of setting up Azure SSO for your Formbricks instance.
|
||||
Do you have a Microsoft Entra ID Tenant? Integrate it with your Formbricks instance to allow users to log in using their existing Microsoft credentials. This guide will walk you through the process of setting up an Application Registration for your Formbricks instance.
|
||||
|
||||
### Requirements
|
||||
#### Requirements
|
||||
|
||||
- An Azure Active Directory (AAD) instance.
|
||||
- A Microsoft Entra ID Tenant populated with users. [Create a tenant as per Microsoft's documentation](https://learn.microsoft.com/en-us/entra/fundamentals/create-new-tenant).
|
||||
- A Formbricks instance running and accessible.
|
||||
- The callback URI for your Formbricks instance: `{WEBAPP_URL}/api/auth/callback/azure-ad`
|
||||
|
||||
### Steps
|
||||
#### Creating an App Registration
|
||||
|
||||
1. Create a new Tenant in Azure Active Directory as per their [official documentation](https://learn.microsoft.com/en-us/entra/fundamentals/create-new-tenant).
|
||||
2. Add Users & Groups to your AAD instance.
|
||||
3. Now we need to fill the below environment variables in our Formbricks instance so get them from your AD configuration:
|
||||
- `AZUREAD_CLIENT_ID`
|
||||
- `AZUREAD_CLIENT_SECRET`
|
||||
- `AZUREAD_TENANT_ID`
|
||||
4. Update these environment variables in your `docker-compose.yml` or pass it like your other environment variables to the Formbricks container.
|
||||
5. Restart your Formbricks instance.
|
||||
6. You're all set! Users can now signup & log in using their AAD credentials.
|
||||
1. Login to the [Microsoft Entra admin center](https://entra.microsoft.com/).
|
||||
2. Go to **Applications** > **App registrations** in the left menu.
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg01}
|
||||
alt="App Registration Name Field"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
3. Click the **New registration** button at the top.
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg02}
|
||||
alt="App Registration Name Field"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
4. Name your application something descriptive, such as `Formbricks SSO`.
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg03}
|
||||
alt="App Registration Name Field"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
5. If you have multiple tenants/organizations, choose the appropriate **Supported account types** option. Otherwise, leave the default option for _Single Tenant_.
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg04}
|
||||
alt="Supported Account Types List"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
6. Under **Redirect URI**, select **Web** for the platform and paste your Formbricks callback URI (see Requirements above).
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg05}
|
||||
alt="Redirect URI Field"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
7. Click **Register** to create the App registration. You will be redirected to your new app's _Overview_ page after it is created.
|
||||
|
||||
8. On the _Overview_ page, under **Essentials**:
|
||||
- Copy the entry for **Application (client) ID** to populate the `AZUREAD_CLIENT_ID` variable.
|
||||
- Copy the entry for **Directory (tenant) ID** to populate the `AZUREAD_TENANT_ID` variable.
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg06}
|
||||
alt="Client and Tenant ID Fields"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
9. From your App registration's _Overview_ page, go to **Manage** > **Certificates & secrets**.
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg07}
|
||||
alt="Certificates & secrets link"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
10. Make sure you have the **Client secrets** tab active, and click **New client secret**.
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg08}
|
||||
alt="New Client Secret Tab & Button"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
11. Enter a **Description**, set an **Expires** period, then click **Add**.
|
||||
|
||||
<Note>
|
||||
You will need to create a new client secret using these steps whenever your chosen expiry period ends.
|
||||
</Note>
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg09}
|
||||
alt="Description & Expires Fields"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
12. Copy the entry under **Value** to populate the `AZUREAD_CLIENT_SECRET` variable.
|
||||
|
||||
<Note>
|
||||
Microsoft will only show this value to you immediately after creation, and you will not be able to access it again. If you lose it, simply start from step 9 to create a new secret.
|
||||
</Note>
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg10}
|
||||
alt="Client Secret Value Field"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
13. Update these environment variables in your `docker-compose.yml` or pass it like your other environment variables to the Formbricks container.
|
||||
|
||||
<Note>
|
||||
You must wrap the `AZUREAD_CLIENT_SECRET` value in double quotes (e.g., `"THis~iS4faKe.53CreTvALu3"`) to prevent issues with special characters.
|
||||
</Note>
|
||||
|
||||
An example `.env` for Microsoft Entra ID in Formbricks would look like:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Formbricks Env for Microsoft Entra ID SSO">
|
||||
```yml {{ title: ".env" }}
|
||||
AZUREAD_CLIENT_ID=a25cadbd-f049-4690-ada3-56a163a72f4c
|
||||
AZUREAD_TENANT_ID=2746c29a-a3a6-4ea1-8762-37816d4b7885
|
||||
AZUREAD_CLIENT_SECRET="THis~iS4faKe.53CreTvALu3"
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
14. Restart your Formbricks instance.
|
||||
15. You're all set! Users can now sign up & log in using their Microsoft credentials associated with your Entra ID Tenant.
|
||||
|
||||
## OpenID Configuration
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export const metadata = {
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
The Formbricks Core source code is licensed under AGPLv3 and available on GitHub. Additionally, we offer features for bigger organisations & enterprises for self-hosters 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
|
||||
|
||||
@@ -8,6 +8,103 @@ export const metadata = {
|
||||
|
||||
# Migration Guide
|
||||
|
||||
## v2.5
|
||||
|
||||
Formbricks v2.5 allows you to visualize responses in a data table format. This release also includes a few bug fixes and performance improvements.
|
||||
|
||||
<Note>
|
||||
This release will fix the inconsistency of CTA and consent question values in case of skipping the question.
|
||||
The value will be set to empty string instead of "dismissed" in order to make it consistent with other
|
||||
questions.
|
||||
</Note>
|
||||
|
||||
### Steps to Migrate
|
||||
|
||||
This guide is for users who are self-hosting Formbricks using our one-click setup. If you are using a different setup, you might adjust the commands accordingly.
|
||||
|
||||
To run all these steps, please navigate to the `formbricks` folder where your `docker-compose.yml` file is located.
|
||||
|
||||
1. **Backup your Database**: This is a crucial step. Please make sure to backup your database before proceeding with the upgrade. You can use the following command to backup your database:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Backup Postgres">
|
||||
|
||||
```bash
|
||||
docker exec formbricks-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v2.5_$(date +%Y%m%d_%H%M%S).dump
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
<Note>
|
||||
If you run into “No such container”, use `docker ps` to find your container name, e.g.
|
||||
`formbricks_postgres_1`.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
If you prefer storing the backup as an `*.sql` file remove the `-Fc` (custom format) option. In case of a
|
||||
restore scenario you will need to use `psql` then with an empty `formbricks` database.
|
||||
</Note>
|
||||
|
||||
2. Pull the latest version of Formbricks:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the containers">
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. Stop the running Formbricks instance & remove the related containers:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the containers">
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
4. Restarting the containers with the latest version of Formbricks:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Restart the containers">
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
5. Now let's migrate the data to the latest schema:
|
||||
|
||||
<Note>To find your Docker Network name for your Postgres Database, find it using `docker network ls`</Note>
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/formbricks/data-migrations:latest && \
|
||||
docker run --rm \
|
||||
--network=formbricks_default \
|
||||
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
|
||||
-e UPGRADE_TO_VERSION="v2.5" \
|
||||
ghcr.io/formbricks/data-migrations:latest
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
The above command will migrate your data to the latest schema. This is a crucial step to migrate your existing data to the new structure. Only if the script runs successful, changes are made to the database. The script can safely run multiple times.
|
||||
|
||||
6. That's it! Once the migration is complete, you can **now access your Formbricks instance** at the same URL as before.
|
||||
|
||||
## v2.4
|
||||
|
||||
Formbricks v2.4 allows you to create multiple endings for your surveys and decide which ending the user should see based on logic jumps. This release also includes many bug fixes and performance improvements.
|
||||
@@ -91,12 +188,12 @@ docker compose up -d
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/formbricks/data-migrations:latest && \
|
||||
docker pull ghcr.io/formbricks/data-migrations:v2.4.0 && \
|
||||
docker run --rm \
|
||||
--network=formbricks_default \
|
||||
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
|
||||
-e UPGRADE_TO_VERSION="v2.4" \
|
||||
ghcr.io/formbricks/data-migrations:latest
|
||||
ghcr.io/formbricks/data-migrations:v2.4.0
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -191,12 +288,12 @@ docker compose up -d
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/formbricks/data-migrations:latest && \
|
||||
docker pull ghcr.io/formbricks/data-migrations:v2.3.0 && \
|
||||
docker run --rm \
|
||||
--network=formbricks_default \
|
||||
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
|
||||
-e UPGRADE_TO_VERSION="v2.3" \
|
||||
ghcr.io/formbricks/data-migrations:latest
|
||||
ghcr.io/formbricks/data-migrations:v2.3.0
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -286,12 +383,12 @@ docker compose up -d
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/formbricks/data-migrations:latest && \
|
||||
docker pull ghcr.io/formbricks/data-migrations:v2.2 && \
|
||||
docker run --rm \
|
||||
--network=formbricks_default \
|
||||
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
|
||||
-e UPGRADE_TO_VERSION="v2.2" \
|
||||
ghcr.io/formbricks/data-migrations:latest
|
||||
ghcr.io/formbricks/data-migrations:v2.2
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -390,12 +487,12 @@ docker compose up -d
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/formbricks/data-migrations:latest && \
|
||||
docker pull ghcr.io/formbricks/data-migrations:v2.1.0 && \
|
||||
docker run --rm \
|
||||
--network=formbricks_default \
|
||||
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
|
||||
-e UPGRADE_TO_VERSION="v2.1" \
|
||||
ghcr.io/formbricks/data-migrations:latest
|
||||
ghcr.io/formbricks/data-migrations:v2.1.0
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -504,12 +601,12 @@ docker compose up -d
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/formbricks/data-migrations:latest && \
|
||||
docker pull ghcr.io/formbricks/data-migrations:v2.0.3 && \
|
||||
docker run --rm \
|
||||
--network=formbricks_default \
|
||||
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
|
||||
-e UPGRADE_TO_VERSION="v2.0" \
|
||||
ghcr.io/formbricks/data-migrations:latest
|
||||
ghcr.io/formbricks/data-migrations:v2.0.3
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
@@ -50,9 +50,10 @@ The script will prompt you for the following information:
|
||||
<CodeGroup title="Docker GPG Keys Overwrite Prompt">
|
||||
|
||||
```bash
|
||||
🧱 Welcome to the Formbricks single instance installer
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 22.04.2 LTS server.
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
@@ -64,48 +65,21 @@ File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N)
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
2. **Email Address**: Provide your email address for SSL certificate registration with Let's Encrypt.
|
||||
2. **Domain Name**: You will be asked to enter the domain name where you want to host Formbricks. This domain will be used to generate an SSL certificate.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Email Prompt">
|
||||
|
||||
```bash
|
||||
🧱 Welcome to the Formbricks single instance installer
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 22.04.2 LTS server.
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
🚀 Testing your Docker installation.
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. Youre now ready to run your Formbricks instance!
|
||||
🚗 Installing Traefik...
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
💡 Please enter your email address for the SSL certificate:
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. **Domain Name**: Enter the domain name that Traefik will use to create the SSL certificate and forward requests to Formbricks. Please make sure that port 80 and 443 are open in your VM's Security Group to allow Traefik to create the SSL certificate.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Domain Name for SSL certificate Prompt">
|
||||
|
||||
```bash
|
||||
🧱 Welcome to the Formbricks single instance installer
|
||||
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 22.04.2 LTS server.
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Dockers official GPG key and setting up the stable repository.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
@@ -113,13 +87,186 @@ File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!
|
||||
🚗 Installing Traefik...
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. **HTTPS Certificate Prompt**: The script will ask if you want to create an HTTPS certificate for your domain. Enter Y to proceed. This is highly recommended for secure access to your Formbricks instance.
|
||||
|
||||
<Col>
|
||||
<CodeGroup>
|
||||
|
||||
```bash
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
🚀 Testing your Docker installation.
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):
|
||||
my.hosted.url.com
|
||||
🔗 Do you want us to set up an HTTPS certificate for you? [Y/n]
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
4. **DNS Setup Prompt**: Ensure that your domain's DNS is correctly configured and ports 80 and 443 are open. Confirm this by entering Y. This step is crucial for proper SSL certificate issuance and secure server access.
|
||||
|
||||
<Col>
|
||||
<CodeGroup>
|
||||
|
||||
```bash
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
🚀 Testing your Docker installation.
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):
|
||||
my.hosted.url.com
|
||||
🔗 Do you want us to set up an HTTPS certificate for you? [Y/n]
|
||||
Y
|
||||
🔗 Please make sure that the domain points to the server's IP address and that ports 80 & 443 are open in your server's firewall. Is everything set up? [Y/n]
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
5. **Email Address**: Provide an email address for SSL certificate registration. This email will be used for notifications regarding your SSL certificate from Let's Encrypt.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Email Prompt">
|
||||
|
||||
```bash
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
🚀 Testing your Docker installation.
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):
|
||||
my.hosted.url.com
|
||||
🔗 Do you want us to set up an HTTPS certificate for you? [Y/n]
|
||||
Y
|
||||
🔗 Please make sure that the domain points to the server's IP address and that ports 80 & 443 are open in your server's firewall. Is everything set up? [Y/n]
|
||||
Y
|
||||
💡 Please enter your email address for the SSL certificate:
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
6. **Enforce HTTPS (HSTS) Prompt**: Enforcing HTTPS with HSTS is a good security practice, as it ensures all communication with your server is encrypted. Enter Y to enable this setting.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Domain Name for SSL certificate Prompt">
|
||||
|
||||
```bash
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
🚀 Testing your Docker installation.
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):
|
||||
my.hosted.url.com
|
||||
🔗 Do you want us to set up an HTTPS certificate for you? [Y/n]
|
||||
Y
|
||||
🔗 Please make sure that the domain points to the server's IP address and that ports 80 & 443 are open in your server's firewall. Is everything set up? [Y/n]
|
||||
Y
|
||||
💡 Please enter your email address for the SSL certificate:
|
||||
docs@formbricks.com
|
||||
💡 Created traefik.yaml file with your provided email address.
|
||||
💡 Created acme.json file with correct permissions.
|
||||
🔗 Do you want to enforce HTTPS (HSTS)? [Y/n]
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
7. **Email Service Setup Prompt**: The script will ask if you want to set up the email service. Enter `Y` to proceed.(default is `N`). You can skip this step if you don't want to set up the email service. You will still be able to use Formbricks without setting up the email service.
|
||||
|
||||
<Col>
|
||||
<CodeGroup>
|
||||
|
||||
```bash
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
🚀 Testing your Docker installation.
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):
|
||||
my.hosted.url.com
|
||||
🔗 Do you want us to set up an HTTPS certificate for you? [Y/n]
|
||||
Y
|
||||
🔗 Please make sure that the domain points to the server's IP address and that ports 80 & 443 are open in your server's firewall. Is everything set up? [Y/n]
|
||||
Y
|
||||
💡 Please enter your email address for the SSL certificate:
|
||||
docs@formbricks.com
|
||||
🔗 Do you want to enforce HTTPS (HSTS)? [Y/n]
|
||||
Y
|
||||
🚗 Configuring Traefik...
|
||||
💡 Created traefik.yaml and traefik-dynamic.yaml file.
|
||||
💡 Created acme.json file with correct permissions.
|
||||
📧 Do you want to set up the email service? You will need SMTP credentials for the same! [y/N]
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -131,39 +278,56 @@ docs@formbricks.com
|
||||
<CodeGroup title="Successfully setup Formbricks on your Ubuntu machine">
|
||||
|
||||
```bash
|
||||
🧱 Welcome to the Formbricks single instance installer
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🛸 Fasten your seatbelts! Were setting up your Formbricks environment on your Ubuntu 22.04.2 LTS server.
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Dockers official GPG key and setting up the stable repository.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
🚀 Testing your Docker installation.
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. Youre now ready to run your Formbricks instance!
|
||||
🚗 Installing Traefik...
|
||||
🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
💡 Please enter your email address for the SSL certificate:
|
||||
docs@formbricks.com
|
||||
💡 Created traefik.yaml file with your provided email address.
|
||||
💡 Created acme.json file with correct permissions.
|
||||
🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):
|
||||
my.hosted.url.com
|
||||
🚙 Updating NEXTAUTH_SECRET in the Formbricks container...
|
||||
🔗 Do you want us to set up an HTTPS certificate for you? [Y/n]
|
||||
Y
|
||||
🔗 Please make sure that the domain points to the server's IP address and that ports 80 & 443 are open in your server's firewall. Is everything set up? [Y/n]
|
||||
Y
|
||||
💡 Please enter your email address for the SSL certificate:
|
||||
docs@formbricks.com
|
||||
🔗 Do you want to enforce HTTPS (HSTS)? [Y/n]
|
||||
Y
|
||||
🚗 Configuring Traefik...
|
||||
💡 Created traefik.yaml and traefik-dynamic.yaml file.
|
||||
💡 Created acme.json file with correct permissions.
|
||||
📧 Do you want to set up the email service? You will need SMTP credentials for the same! [y/N] N
|
||||
📥 Downloading docker-compose.yml from Formbricks GitHub repository...
|
||||
% Total % Received % Xferd Average Speed Time Time Time Current
|
||||
Dload Upload Total Spent Left Speed
|
||||
100 6632 100 6632 0 0 24280 0 --:--:-- --:--:-- --:--:-- 24382
|
||||
🚙 Updating docker-compose.yml with your custom inputs...
|
||||
🚗 NEXTAUTH_SECRET updated successfully!
|
||||
🚗 ENCRYPTION_KEY updated successfully!
|
||||
🚗 CRON_SECRET updated successfully!
|
||||
|
||||
[+] Running 4/4
|
||||
✔ Network formbricks_default Created 0.1s
|
||||
✔ Container formbricks-postgres-1 Started 0.5s
|
||||
✔ Container formbricks-formbricks-1 Started 0.7s
|
||||
✔ Container traefik Started 1.1s
|
||||
✔ Network formbricks_default Created 0.2s
|
||||
✔ Container formbricks-postgres-1 Started 1.0s
|
||||
✔ Container formbricks-formbricks-1 Started 1.6s
|
||||
✔ Container traefik Started 2.8s
|
||||
🔗 To edit more variables and deeper config, go to the formbricks/docker-compose.yml, edit the file, and restart the container!
|
||||
🚨 Make sure you have set up the DNS records as well as inbound rules for the domain name and IP address of this instance.
|
||||
|
||||
🎉 All done! Check the status of Formbricks & Traefik with 'cd formbricks && sudo docker compose ps.'
|
||||
🎉 All done! Please setup your Formbricks instance by visiting your domain at https://my.hosted.url.com. You can check the status of Formbricks & Traefik with 'cd formbricks && sudo docker compose ps.'
|
||||
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
@@ -15,6 +15,7 @@ const TopLevelNavItem = ({ href, children }: { href: string; children: React.Rea
|
||||
<li>
|
||||
<Link
|
||||
href={href}
|
||||
target="_blank"
|
||||
className="text-sm leading-5 text-slate-600 transition hover:text-slate-900 dark:text-slate-400 dark:hover:text-white">
|
||||
{children}
|
||||
</Link>
|
||||
|
||||
@@ -171,6 +171,13 @@ const NavigationGroup = ({
|
||||
|
||||
const isParentOpen = (title: string) => openGroups.includes(title);
|
||||
|
||||
const sortedLinks = group.links.map((link) => {
|
||||
if (link.children) {
|
||||
link.children.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
return link;
|
||||
});
|
||||
|
||||
return (
|
||||
<li className={clsx("relative mt-6", className)}>
|
||||
<motion.h2 layout="position" className="font-semibold text-slate-900 dark:text-white">
|
||||
@@ -185,7 +192,7 @@ const NavigationGroup = ({
|
||||
{isActiveGroup && <ActivePageMarker group={group} pathname={pathname || "/docs"} />}
|
||||
</AnimatePresence>
|
||||
<ul role="list" className="border-l border-transparent">
|
||||
{group.links.map((link) => (
|
||||
{sortedLinks.map((link) => (
|
||||
<motion.li key={link.title} layout="position" className="relative">
|
||||
{link.href ? (
|
||||
<NavLink
|
||||
|
||||
@@ -40,7 +40,8 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "User Metadata", href: "/global/metadata" }, // global
|
||||
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
|
||||
{ title: "Conditional Logic", href: "/global/conditional-logic" }, // global
|
||||
{ title: "Custom Start & End Conditions", href: "/global/custom-start-end-conditions" }, // global
|
||||
{ title: "Start & End Dates", href: "/global/custom-start-end-conditions" }, // global
|
||||
{ title: "Limit submissions", href: "/global/limit-submissions" }, // global
|
||||
{ title: "Recall Functionality", href: "/global/recall" }, // global
|
||||
{ title: "Partial Submissions", href: "/global/partial-submissions" }, // global
|
||||
],
|
||||
@@ -63,7 +64,8 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "User Metadata", href: "/global/metadata" }, // global
|
||||
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
|
||||
{ title: "Conditional Logic", href: "/global/conditional-logic" }, // global
|
||||
{ title: "Custom Start & End Conditions", href: "/global/custom-start-end-conditions" }, // global
|
||||
{ title: "Start & End Dates", href: "/global/custom-start-end-conditions" }, // global
|
||||
{ title: "Limit submissions", href: "/global/limit-submissions" }, // global
|
||||
{ title: "Recall Functionality", href: "/global/recall" }, // global
|
||||
{ title: "Partial Submissions", href: "/global/partial-submissions" }, // global
|
||||
],
|
||||
@@ -89,7 +91,8 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "User Metadata", href: "/global/metadata" },
|
||||
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
|
||||
{ title: "Conditional Logic", href: "/global/conditional-logic" },
|
||||
{ title: "Custom Start & End Conditions", href: "/global/custom-start-end-conditions" },
|
||||
{ title: "Start & End Dates", href: "/global/custom-start-end-conditions" },
|
||||
{ title: "Limit submissions", href: "/global/limit-submissions" }, // global
|
||||
{ title: "Recall Functionality", href: "/global/recall" },
|
||||
{ title: "Verify Email before Survey", href: "/link-surveys/verify-email-before-survey" },
|
||||
{ title: "PIN Protected Surveys", href: "/link-surveys/pin-protected-surveys" },
|
||||
@@ -136,8 +139,9 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Zapier", href: "/developer-docs/integrations/zapier" },
|
||||
],
|
||||
},
|
||||
{ title: "SDK: App Survey", href: "/developer-docs/app-survey-sdk" },
|
||||
{ title: "SDK: Website Survey", href: "/developer-docs/website-survey-sdk" },
|
||||
{ title: "SDK: Web Apps", href: "/developer-docs/app-survey-sdk" },
|
||||
{ title: "SDK: Public Websites", href: "/developer-docs/website-survey-sdk" },
|
||||
{ title: "SDK: React Native", href: "/developer-docs/react-native-in-app-surveys" },
|
||||
{ title: "SDK: Formbricks API", href: "/developer-docs/api-sdk" },
|
||||
{ title: "REST API", href: "/developer-docs/rest-api" },
|
||||
{ title: "Webhooks", href: "/developer-docs/webhooks" },
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import nextMDX from "@next/mdx";
|
||||
|
||||
import { recmaPlugins } from "./mdx/recma.mjs";
|
||||
import { rehypePlugins } from "./mdx/rehype.mjs";
|
||||
import { remarkPlugins } from "./mdx/remark.mjs";
|
||||
@@ -105,17 +104,18 @@ const nextConfig = {
|
||||
destination: "/app-surveys/user-identification",
|
||||
permanent: true,
|
||||
},
|
||||
// Global Features
|
||||
{
|
||||
source: "/global/custom-start-end-conditions",
|
||||
destination: "/global/schedule-start-end-dates",
|
||||
permanent: true,
|
||||
},
|
||||
// Integrations
|
||||
{
|
||||
source: "/integrations/:path",
|
||||
destination: "/developer-docs/integrations/:path",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/global/custom-styling",
|
||||
destination: "/global/overwrite-styling",
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -19,23 +19,23 @@
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^1.6.1",
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@storybook/addon-a11y": "^8.2.7",
|
||||
"@storybook/addon-essentials": "^8.2.7",
|
||||
"@storybook/addon-interactions": "^8.2.7",
|
||||
"@storybook/addon-links": "^8.2.7",
|
||||
"@storybook/addon-onboarding": "^8.2.7",
|
||||
"@storybook/blocks": "^8.2.7",
|
||||
"@storybook/react": "^8.2.7",
|
||||
"@storybook/react-vite": "^8.2.7",
|
||||
"@storybook/test": "^8.2.7",
|
||||
"@storybook/addon-a11y": "^8.2.9",
|
||||
"@storybook/addon-essentials": "^8.2.9",
|
||||
"@storybook/addon-interactions": "^8.2.9",
|
||||
"@storybook/addon-links": "^8.2.9",
|
||||
"@storybook/addon-onboarding": "^8.2.9",
|
||||
"@storybook/blocks": "^8.2.9",
|
||||
"@storybook/react": "^8.2.9",
|
||||
"@storybook/react-vite": "^8.2.9",
|
||||
"@storybook/test": "^8.2.9",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"esbuild": "^0.23.0",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"storybook": "^8.2.7",
|
||||
"tsup": "^8.2.3",
|
||||
"vite": "^5.3.5"
|
||||
"storybook": "^8.2.9",
|
||||
"tsup": "^8.2.4",
|
||||
"vite": "^5.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ RUN PRISMA_VERSION=$(cat prisma_version.txt) && npm install -g prisma@$PRISMA_VE
|
||||
|
||||
EXPOSE 3000
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
USER nextjs
|
||||
# USER nextjs
|
||||
|
||||
# Prepare volume for uploads
|
||||
RUN mkdir -p /home/nextjs/apps/web/uploads/
|
||||
|
||||
@@ -82,7 +82,6 @@ export const ConnectWithFormbricks = ({
|
||||
</div>
|
||||
<Button
|
||||
id="finishOnboarding"
|
||||
className="text-slate-400 hover:text-slate-700"
|
||||
variant={widgetSetupCompleted ? "primary" : "minimal"}
|
||||
onClick={handleFinishOnboarding}
|
||||
EndIcon={ArrowRight}>
|
||||
|
||||
@@ -36,7 +36,12 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
|
||||
|
||||
const handleInvite = async (data: TInviteOrganizationMemberDetails) => {
|
||||
try {
|
||||
await inviteOrganizationMemberAction(organization.id, data.email, "developer", data.inviteMessage);
|
||||
await inviteOrganizationMemberAction({
|
||||
organizationId: organization.id,
|
||||
email: data.email,
|
||||
role: "developer",
|
||||
inviteMessage: data.inviteMessage,
|
||||
});
|
||||
toast.success("Invite sent successful");
|
||||
await finishOnboarding();
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||
import { CircleUserRoundIcon, EarthIcon, LinkIcon, XIcon } from "lucide-react";
|
||||
import { GlobeIcon, GlobeLockIcon, LinkIcon, XIcon } from "lucide-react";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Header } from "@formbricks/ui/Header";
|
||||
@@ -15,14 +15,14 @@ const Page = async ({ params }: ChannelPageProps) => {
|
||||
{
|
||||
title: "Public website",
|
||||
description: "Run well-timed pop-up surveys.",
|
||||
icon: EarthIcon,
|
||||
icon: GlobeIcon,
|
||||
iconText: "Built for scale",
|
||||
href: `/organizations/${params.organizationId}/products/new/industry?channel=website`,
|
||||
},
|
||||
{
|
||||
title: "App with sign up",
|
||||
description: "Run highly-targeted micro-surveys.",
|
||||
icon: CircleUserRoundIcon,
|
||||
icon: GlobeLockIcon,
|
||||
iconText: "Enrich user profiles",
|
||||
href: `/organizations/${params.organizationId}/products/new/industry?channel=app`,
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { FORMBRICKS_PRODUCT_ID_LS, FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
|
||||
import { PREVIEW_SURVEY } from "@formbricks/lib/styling/constants";
|
||||
import {
|
||||
TProductConfigChannel,
|
||||
@@ -59,6 +60,14 @@ export const ProductSettings = ({
|
||||
const productionEnvironment = createProductResponse.data.environments.find(
|
||||
(environment) => environment.type === "production"
|
||||
);
|
||||
if (productionEnvironment) {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(FORMBRICKS_PRODUCT_ID_LS, productionEnvironment.productId);
|
||||
|
||||
// Rmove filters when creating a new product
|
||||
localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
|
||||
}
|
||||
}
|
||||
if (channel !== "link") {
|
||||
router.push(`/environments/${productionEnvironment?.id}/connect`);
|
||||
} else {
|
||||
|
||||
@@ -1,68 +1,54 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { sendInviteMemberEmail } from "@formbricks/email";
|
||||
import { hasOrganizationAuthority } from "@formbricks/lib/auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { INVITE_DISABLED } from "@formbricks/lib/constants";
|
||||
import { inviteUser } from "@formbricks/lib/invite/service";
|
||||
import { verifyUserRoleAccess } from "@formbricks/lib/organization/auth";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { AuthenticationError } from "@formbricks/types/errors";
|
||||
import { TMembershipRole } from "@formbricks/types/memberships";
|
||||
import { ZMembershipRole } from "@formbricks/types/memberships";
|
||||
|
||||
export const inviteOrganizationMemberAction = async (
|
||||
organizationId: string,
|
||||
email: string,
|
||||
role: TMembershipRole,
|
||||
inviteMessage: string
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
const ZInviteOrganizationMemberAction = z.object({
|
||||
organizationId: ZId,
|
||||
email: z.string(),
|
||||
role: ZMembershipRole,
|
||||
inviteMessage: z.string(),
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError("Not authenticated");
|
||||
}
|
||||
export const inviteOrganizationMemberAction = authenticatedActionClient
|
||||
.schema(ZInviteOrganizationMemberAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
if (INVITE_DISABLED) {
|
||||
throw new AuthenticationError("Invite disabled");
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
rules: ["membership", "create"],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
const invite = await inviteUser({
|
||||
organizationId: parsedInput.organizationId,
|
||||
invitee: {
|
||||
email: parsedInput.email,
|
||||
name: "",
|
||||
role: parsedInput.role,
|
||||
},
|
||||
});
|
||||
|
||||
const isUserAuthorized = await hasOrganizationAuthority(session.user.id, organizationId);
|
||||
if (invite) {
|
||||
await sendInviteMemberEmail(
|
||||
invite.id,
|
||||
parsedInput.email,
|
||||
ctx.user.name ?? "",
|
||||
"",
|
||||
true, // is onboarding invite
|
||||
parsedInput.inviteMessage
|
||||
);
|
||||
}
|
||||
|
||||
if (INVITE_DISABLED) {
|
||||
throw new AuthenticationError("Invite disabled");
|
||||
}
|
||||
|
||||
if (!isUserAuthorized) {
|
||||
throw new AuthenticationError("Not authorized");
|
||||
}
|
||||
|
||||
const { hasCreateOrUpdateMembersAccess } = await verifyUserRoleAccess(organizationId, session.user.id);
|
||||
if (!hasCreateOrUpdateMembersAccess) {
|
||||
throw new AuthenticationError("Not authorized");
|
||||
}
|
||||
|
||||
const invite = await inviteUser({
|
||||
organizationId,
|
||||
invitee: {
|
||||
email,
|
||||
name: "",
|
||||
role,
|
||||
},
|
||||
return invite;
|
||||
});
|
||||
|
||||
if (invite) {
|
||||
await sendInviteMemberEmail(
|
||||
invite.id,
|
||||
email,
|
||||
user.name ?? "",
|
||||
"",
|
||||
true, // is onboarding invite
|
||||
inviteMessage
|
||||
);
|
||||
}
|
||||
|
||||
return invite;
|
||||
};
|
||||
|
||||
@@ -1,208 +1,206 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { createActionClass } from "@formbricks/lib/actionClass/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { canUserAccessProduct } from "@formbricks/lib/product/auth";
|
||||
import { actionClient, authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { UNSPLASH_ACCESS_KEY, UNSPLASH_ALLOWED_DOMAINS } from "@formbricks/lib/constants";
|
||||
import {
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getOrganizationIdFromProductId,
|
||||
getOrganizationIdFromSegmentId,
|
||||
getOrganizationIdFromSurveyId,
|
||||
} from "@formbricks/lib/organization/utils";
|
||||
import { getProduct } from "@formbricks/lib/product/service";
|
||||
import {
|
||||
cloneSegment,
|
||||
createSegment,
|
||||
deleteSegment,
|
||||
getSegment,
|
||||
resetSegmentInSurvey,
|
||||
updateSegment,
|
||||
} from "@formbricks/lib/segment/service";
|
||||
import { canUserAccessSurvey, verifyUserRoleAccess } from "@formbricks/lib/survey/auth";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import {
|
||||
deleteSurvey,
|
||||
getSurvey,
|
||||
loadNewSegmentInSurvey,
|
||||
updateSurvey,
|
||||
} from "@formbricks/lib/survey/service";
|
||||
import { TActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TBaseFilters, TSegmentUpdateInput, ZSegmentFilters } from "@formbricks/types/segment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { loadNewSegmentInSurvey, updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { ZActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZBaseFilters, ZSegmentFilters, ZSegmentUpdateInput } from "@formbricks/types/segment";
|
||||
import { ZSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
export const surveyMutateAction = async (survey: TSurvey): Promise<TSurvey> => {
|
||||
return await updateSurvey(survey);
|
||||
};
|
||||
|
||||
export const updateSurveyAction = async (survey: TSurvey): Promise<TSurvey> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, survey.id);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(survey.environmentId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await updateSurvey(survey);
|
||||
};
|
||||
|
||||
export const deleteSurveyAction = async (surveyId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
const { hasDeleteAccess } = await verifyUserRoleAccess(survey!.environmentId, session.user.id);
|
||||
if (!hasDeleteAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
await deleteSurvey(surveyId);
|
||||
};
|
||||
|
||||
export const refetchProductAction = async (productId: string): Promise<TProduct | null> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessProduct(session.user.id, productId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const product = await getProduct(productId);
|
||||
return product;
|
||||
};
|
||||
|
||||
export const createBasicSegmentAction = async ({
|
||||
description,
|
||||
environmentId,
|
||||
filters,
|
||||
isPrivate,
|
||||
surveyId,
|
||||
title,
|
||||
}: {
|
||||
environmentId: string;
|
||||
surveyId: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
isPrivate: boolean;
|
||||
filters: TBaseFilters;
|
||||
}) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const parsedFilters = ZSegmentFilters.safeParse(filters);
|
||||
|
||||
if (!parsedFilters.success) {
|
||||
const errMsg =
|
||||
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
const segment = await createSegment({
|
||||
environmentId,
|
||||
surveyId,
|
||||
title,
|
||||
description: description || "",
|
||||
isPrivate,
|
||||
filters,
|
||||
export const updateSurveyAction = authenticatedActionClient
|
||||
.schema(ZSurvey)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.id),
|
||||
rules: ["survey", "update"],
|
||||
});
|
||||
return await updateSurvey(parsedInput);
|
||||
});
|
||||
surveyCache.revalidate({ id: surveyId });
|
||||
|
||||
return segment;
|
||||
};
|
||||
const ZRefetchProductAction = z.object({
|
||||
productId: ZId,
|
||||
});
|
||||
|
||||
export const updateBasicSegmentAction = async (
|
||||
environmentId: string,
|
||||
segmentId: string,
|
||||
data: TSegmentUpdateInput
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
export const refetchProductAction = authenticatedActionClient
|
||||
.schema(ZRefetchProductAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromProductId(parsedInput.productId),
|
||||
rules: ["product", "read"],
|
||||
});
|
||||
|
||||
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
return await getProduct(parsedInput.productId);
|
||||
});
|
||||
|
||||
const { filters } = data;
|
||||
if (filters) {
|
||||
const parsedFilters = ZSegmentFilters.safeParse(filters);
|
||||
const ZCreateBasicSegmentAction = z.object({
|
||||
description: z.string().optional(),
|
||||
environmentId: ZId,
|
||||
filters: ZBaseFilters,
|
||||
isPrivate: z.boolean(),
|
||||
surveyId: ZId,
|
||||
title: z.string(),
|
||||
});
|
||||
|
||||
export const createBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZCreateBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["segment", "create"],
|
||||
});
|
||||
|
||||
const parsedFilters = ZSegmentFilters.safeParse(parsedInput.filters);
|
||||
|
||||
if (!parsedFilters.success) {
|
||||
const errMsg =
|
||||
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
return await updateSegment(segmentId, data);
|
||||
};
|
||||
const segment = await createSegment({
|
||||
environmentId: parsedInput.environmentId,
|
||||
surveyId: parsedInput.surveyId,
|
||||
title: parsedInput.title,
|
||||
description: parsedInput.description || "",
|
||||
isPrivate: parsedInput.isPrivate,
|
||||
filters: parsedInput.filters,
|
||||
});
|
||||
surveyCache.revalidate({ id: parsedInput.surveyId });
|
||||
|
||||
export const loadNewBasicSegmentAction = async (surveyId: string, segmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await loadNewSegmentInSurvey(surveyId, segmentId);
|
||||
};
|
||||
|
||||
export const cloneBasicSegmentAction = async (segmentId: string, surveyId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
try {
|
||||
const clonedSegment = await cloneSegment(segmentId, surveyId);
|
||||
return clonedSegment;
|
||||
} catch (err: any) {
|
||||
throw new Error(err);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteBasicSegmentAction = async (environmentId: string, segmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const foundSegment = await getSegment(segmentId);
|
||||
|
||||
if (!foundSegment) {
|
||||
throw new Error(`Segment with id ${segmentId} not found`);
|
||||
}
|
||||
|
||||
return await deleteSegment(segmentId);
|
||||
};
|
||||
|
||||
export const resetBasicSegmentFiltersAction = async (surveyId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await resetSegmentInSurvey(surveyId);
|
||||
};
|
||||
|
||||
export const getImagesFromUnsplashAction = async (searchQuery: string, page: number = 1) => {
|
||||
if (!UNSPLASH_ACCESS_KEY) {
|
||||
throw new Error("Unsplash access key is not set");
|
||||
}
|
||||
const baseUrl = "https://api.unsplash.com/search/photos";
|
||||
const params = new URLSearchParams({
|
||||
query: searchQuery,
|
||||
client_id: UNSPLASH_ACCESS_KEY,
|
||||
orientation: "landscape",
|
||||
per_page: "9",
|
||||
page: page.toString(),
|
||||
return segment;
|
||||
});
|
||||
|
||||
try {
|
||||
const ZUpdateBasicSegmentAction = z.object({
|
||||
segmentId: ZId,
|
||||
data: ZSegmentUpdateInput,
|
||||
});
|
||||
|
||||
export const updateBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZUpdateBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
|
||||
rules: ["segment", "update"],
|
||||
});
|
||||
|
||||
const { filters } = parsedInput.data;
|
||||
if (filters) {
|
||||
const parsedFilters = ZSegmentFilters.safeParse(filters);
|
||||
|
||||
if (!parsedFilters.success) {
|
||||
const errMsg =
|
||||
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
return await updateSegment(parsedInput.segmentId, parsedInput.data);
|
||||
});
|
||||
|
||||
const ZLoadNewBasicSegmentAction = z.object({
|
||||
surveyId: ZId,
|
||||
segmentId: ZId,
|
||||
});
|
||||
|
||||
export const loadNewBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZLoadNewBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSegmentId(parsedInput.surveyId),
|
||||
rules: ["segment", "read"],
|
||||
});
|
||||
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["survey", "update"],
|
||||
});
|
||||
|
||||
return await loadNewSegmentInSurvey(parsedInput.surveyId, parsedInput.segmentId);
|
||||
});
|
||||
|
||||
const ZCloneBasicSegmentAction = z.object({
|
||||
segmentId: ZId,
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
export const cloneBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZCloneBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
|
||||
rules: ["segment", "create"],
|
||||
});
|
||||
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["survey", "read"],
|
||||
});
|
||||
|
||||
return await cloneSegment(parsedInput.segmentId, parsedInput.surveyId);
|
||||
});
|
||||
|
||||
const ZResetBasicSegmentFiltersAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
export const resetBasicSegmentFiltersAction = authenticatedActionClient
|
||||
.schema(ZResetBasicSegmentFiltersAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["segment", "update"],
|
||||
});
|
||||
|
||||
return await resetSegmentInSurvey(parsedInput.surveyId);
|
||||
});
|
||||
|
||||
const ZGetImagesFromUnsplashAction = z.object({
|
||||
searchQuery: z.string(),
|
||||
page: z.number().optional(),
|
||||
});
|
||||
|
||||
export const getImagesFromUnsplashAction = actionClient
|
||||
.schema(ZGetImagesFromUnsplashAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
if (!UNSPLASH_ACCESS_KEY) {
|
||||
throw new Error("Unsplash access key is not set");
|
||||
}
|
||||
const baseUrl = "https://api.unsplash.com/search/photos";
|
||||
const params = new URLSearchParams({
|
||||
query: parsedInput.searchQuery,
|
||||
client_id: UNSPLASH_ACCESS_KEY,
|
||||
orientation: "landscape",
|
||||
per_page: "9",
|
||||
page: (parsedInput.page || 1).toString(),
|
||||
});
|
||||
|
||||
const response = await fetch(`${baseUrl}?${params}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -227,14 +225,29 @@ export const getImagesFromUnsplashAction = async (searchQuery: string, page: num
|
||||
},
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error("Error getting images from Unsplash");
|
||||
});
|
||||
|
||||
const isValidUnsplashUrl = (url: string): boolean => {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl.protocol === "https:" && UNSPLASH_ALLOWED_DOMAINS.includes(parsedUrl.hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const triggerDownloadUnsplashImageAction = async (downloadUrl: string) => {
|
||||
try {
|
||||
const response = await fetch(`${downloadUrl}/?client_id=${UNSPLASH_ACCESS_KEY}`, {
|
||||
const ZTriggerDownloadUnsplashImageAction = z.object({
|
||||
downloadUrl: z.string().url(),
|
||||
});
|
||||
|
||||
export const triggerDownloadUnsplashImageAction = actionClient
|
||||
.schema(ZTriggerDownloadUnsplashImageAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
if (!isValidUnsplashUrl(parsedInput.downloadUrl)) {
|
||||
throw new Error("Invalid Unsplash URL");
|
||||
}
|
||||
|
||||
const response = await fetch(`${parsedInput.downloadUrl}/?client_id=${UNSPLASH_ACCESS_KEY}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
@@ -245,20 +258,20 @@ export const triggerDownloadUnsplashImageAction = async (downloadUrl: string) =>
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
throw new Error("Error downloading image from Unsplash");
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const createActionClassAction = async (environmentId: string, action: TActionClassInput) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZCreateActionClassAction = z.object({
|
||||
action: ZActionClassInput,
|
||||
});
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, action.environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const createActionClassAction = authenticatedActionClient
|
||||
.schema(ZCreateActionClassAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.action.environmentId),
|
||||
rules: ["actionClass", "create"],
|
||||
});
|
||||
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(environmentId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await createActionClass(action.environmentId, action);
|
||||
};
|
||||
return await createActionClass(parsedInput.action.environmentId, parsedInput.action);
|
||||
});
|
||||
|
||||
@@ -56,6 +56,7 @@ export const AddActionModal = ({
|
||||
return (
|
||||
<ModalWithTabs
|
||||
label="Add action"
|
||||
description="Capture a new action to trigger a survey on."
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
tabs={tabs}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { getQuestionDefaults, questionTypes, universalQuestionPresets } from "@/app/lib/questions";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import {
|
||||
getQuestionDefaults,
|
||||
questionTypes,
|
||||
universalQuestionPresets,
|
||||
} from "@formbricks/lib/utils/questions";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
|
||||
interface AddQuestionButtonProps {
|
||||
|
||||
@@ -8,7 +8,7 @@ interface ColorSurveyBgProps {
|
||||
}
|
||||
|
||||
export const ColorSurveyBg = ({ handleBgChange, colors, background }: ColorSurveyBgProps) => {
|
||||
const [color, setColor] = useState(background || "#ffff");
|
||||
const [color, setColor] = useState(background || "#FFFFFF");
|
||||
|
||||
const handleBg = (x: string) => {
|
||||
setColor(x);
|
||||
@@ -23,7 +23,7 @@ export const ColorSurveyBg = ({ handleBgChange, colors, background }: ColorSurve
|
||||
{colors.map((x) => {
|
||||
return (
|
||||
<div
|
||||
className={`h-16 w-16 cursor-pointer rounded-lg ${
|
||||
className={`h-16 w-16 cursor-pointer rounded-lg border border-slate-300 ${
|
||||
color === x ? "border-4 border-slate-500" : ""
|
||||
}`}
|
||||
key={x}
|
||||
|
||||
@@ -135,10 +135,14 @@ export const CreateNewActionTab = ({
|
||||
};
|
||||
}
|
||||
|
||||
const newActionClass: TActionClass = await createActionClassAction(
|
||||
environmentId,
|
||||
updatedAction as TActionClassInput
|
||||
);
|
||||
// const newActionClass: TActionClass =
|
||||
const createActionClassResposne = await createActionClassAction({
|
||||
action: updatedAction as TActionClassInput,
|
||||
});
|
||||
|
||||
if (!createActionClassResposne?.data) return;
|
||||
|
||||
const newActionClass = createActionClassResposne.data;
|
||||
if (setActionClasses) {
|
||||
setActionClasses((prevActionClasses: TActionClass[]) => [...prevActionClasses, newActionClass]);
|
||||
}
|
||||
@@ -167,14 +171,14 @@ export const CreateNewActionTab = ({
|
||||
<div>
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(submitHandler)}>
|
||||
<div className="max-h-[400px] w-full space-y-4 overflow-y-auto">
|
||||
<div className="max-h-[500px] w-full space-y-4 overflow-y-auto pr-4">
|
||||
<div className="w-3/5">
|
||||
<FormField
|
||||
name={`type`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<Label className="font-semibold">Type</Label>
|
||||
<Label className="font-semibold">Action Type</Label>
|
||||
<TabToggle
|
||||
id="type"
|
||||
options={[
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { GripIcon } from "lucide-react";
|
||||
import { GripIcon, Handshake, Undo2 } from "lucide-react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -129,7 +129,13 @@ export const EditEndingCard = ({
|
||||
"flex w-10 flex-col items-center justify-between rounded-l-lg border-b border-l border-t py-2 group-aria-expanded:rounded-bl-none",
|
||||
isInvalid ? "bg-red-400" : "bg-white group-hover:bg-slate-50"
|
||||
)}>
|
||||
<p className="mt-3">{endingCard.type === "endScreen" ? "🙏" : "↪️"}</p>
|
||||
<div className="mt-3 flex w-full justify-center">
|
||||
{endingCard.type === "endScreen" ? (
|
||||
<Handshake className="h-4 w-4" />
|
||||
) : (
|
||||
<Undo2 className="h-4 w-4 rotate-180" />
|
||||
)}
|
||||
</div>
|
||||
<button className="opacity-0 transition-all duration-300 hover:cursor-move group-hover:opacity-100">
|
||||
<GripIcon className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { Hand } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { LocalizedEditor } from "@formbricks/ee/multi-language/components/localized-editor";
|
||||
@@ -66,7 +67,7 @@ export const EditWelcomeCard = ({
|
||||
"flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none",
|
||||
isInvalid ? "bg-red-400" : "bg-white group-hover:bg-slate-50"
|
||||
)}>
|
||||
<p>✋</p>
|
||||
<Hand className="h-4 w-4" />
|
||||
</div>
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
@@ -109,7 +110,7 @@ export const EditWelcomeCard = ({
|
||||
<div className="mt-3 flex w-full items-center justify-center">
|
||||
<FileInput
|
||||
id="welcome-card-image"
|
||||
allowedFileExtensions={["png", "jpeg", "jpg"]}
|
||||
allowedFileExtensions={["png", "jpeg", "jpg", "webp"]}
|
||||
environmentId={environmentId}
|
||||
onFileUpload={(url: string[]) => {
|
||||
updateSurvey({ fileUrl: url[0] });
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
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 { useState } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { QUESTIONS_ICON_MAP, QUESTIONS_NAME_MAP, getQuestionDefaults } from "@formbricks/lib/utils/questions";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import {
|
||||
TSurvey,
|
||||
@@ -213,8 +213,6 @@ export const EditorCardMenu = ({
|
||||
|
||||
<DropdownMenuSubContent className="ml-4 border border-slate-200">
|
||||
{Object.entries(QUESTIONS_NAME_MAP).map(([type, name]) => {
|
||||
if (type === card.type) return null;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={type}
|
||||
|
||||
@@ -72,7 +72,7 @@ export const EndScreenForm = ({
|
||||
} else {
|
||||
updateSurvey({
|
||||
buttonLabel: { default: "Create your own Survey" },
|
||||
buttonLink: "https://formbricks.com/signup",
|
||||
buttonLink: "https://formbricks.com",
|
||||
});
|
||||
}
|
||||
setshowEndingCardCTA(!showEndingCardCTA);
|
||||
@@ -111,7 +111,7 @@ export const EndScreenForm = ({
|
||||
id="buttonLink"
|
||||
name="buttonLink"
|
||||
className="bg-white"
|
||||
placeholder="https://formbricks.com/signup"
|
||||
placeholder="https://formbricks.com"
|
||||
value={endingCard.buttonLink}
|
||||
onChange={(e) => updateSurvey({ buttonLink: e.target.value })}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { EyeOff } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TSurvey, TSurveyHiddenFields } from "@formbricks/types/surveys/types";
|
||||
import { validateId } from "@formbricks/types/surveys/validation";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
@@ -36,9 +38,26 @@ export const HiddenFieldsCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
const updateSurvey = (data: TSurveyHiddenFields) => {
|
||||
const updateSurvey = (data: TSurveyHiddenFields, currentFieldId?: string) => {
|
||||
const questions = [...localSurvey.questions];
|
||||
|
||||
// Remove recall info from question headlines
|
||||
if (currentFieldId) {
|
||||
questions.forEach((question) => {
|
||||
for (const [languageCode, headline] of Object.entries(question.headline)) {
|
||||
if (headline.includes(`recall:${currentFieldId}`)) {
|
||||
const recallInfo = extractRecallInfo(headline);
|
||||
if (recallInfo) {
|
||||
question.headline[languageCode] = headline.replace(recallInfo, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
questions,
|
||||
hiddenFields: {
|
||||
...localSurvey.hiddenFields,
|
||||
...data,
|
||||
@@ -53,7 +72,7 @@ export const HiddenFieldsCard = ({
|
||||
open ? "bg-slate-50" : "bg-white group-hover:bg-slate-50",
|
||||
"flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none"
|
||||
)}>
|
||||
<p>🥷</p>
|
||||
<EyeOff className="h-4 w-4" />
|
||||
</div>
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
@@ -93,10 +112,13 @@ export const HiddenFieldsCard = ({
|
||||
<Tag
|
||||
key={fieldId}
|
||||
onDelete={() => {
|
||||
updateSurvey({
|
||||
enabled: true,
|
||||
fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== fieldId),
|
||||
});
|
||||
updateSurvey(
|
||||
{
|
||||
enabled: true,
|
||||
fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== fieldId),
|
||||
},
|
||||
fieldId
|
||||
);
|
||||
}}
|
||||
tagId={fieldId}
|
||||
tagName={fieldId}
|
||||
|
||||
@@ -16,7 +16,7 @@ export const UploadImageSurveyBg = ({
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<FileInput
|
||||
id="survey-bg-file-input"
|
||||
allowedFileExtensions={["png", "jpeg", "jpg"]}
|
||||
allowedFileExtensions={["png", "jpeg", "jpg", "webp"]}
|
||||
environmentId={environmentId}
|
||||
onFileUpload={(url: string[]) => {
|
||||
if (url.length > 0) {
|
||||
|
||||
@@ -80,6 +80,7 @@ const conditions = {
|
||||
cal: ["skipped", "booked"],
|
||||
matrix: ["isCompletelySubmitted", "isPartiallySubmitted", "skipped"],
|
||||
address: ["submitted", "skipped"],
|
||||
ranking: ["submitted", "skipped"],
|
||||
};
|
||||
|
||||
export const LogicEditor = ({
|
||||
@@ -452,14 +453,13 @@ export const LogicEditor = ({
|
||||
<div className="mt-2 flex items-center space-x-2">
|
||||
<Button
|
||||
id="logicJumps"
|
||||
className="bg-slate-100 hover:bg-slate-50"
|
||||
type="button"
|
||||
name="logicJumps"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
StartIcon={SplitIcon}
|
||||
onClick={() => addLogic()}>
|
||||
Add Logic
|
||||
Add logic
|
||||
</Button>
|
||||
<TooltipProvider delayDuration={50}>
|
||||
<Tooltip>
|
||||
|
||||
@@ -5,8 +5,7 @@ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { createI18nString, extractLanguageCodes, getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import {
|
||||
TI18nString,
|
||||
@@ -19,7 +18,7 @@ import { Button } from "@formbricks/ui/Button";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
|
||||
import { SelectQuestionChoice } from "./SelectQuestionChoice";
|
||||
import { QuestionOptionChoice } from "./QuestionOptionChoice";
|
||||
|
||||
interface OpenQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -220,6 +219,7 @@ export const MultipleChoiceQuestionForm = ({
|
||||
<Label htmlFor="choices">Options*</Label>
|
||||
<div className="mt-2" id="choices">
|
||||
<DndContext
|
||||
id="multi-choice-choices"
|
||||
onDragEnd={(event) => {
|
||||
const { active, over } = event;
|
||||
|
||||
@@ -245,7 +245,7 @@ export const MultipleChoiceQuestionForm = ({
|
||||
<div className="flex flex-col">
|
||||
{question.choices &&
|
||||
question.choices.map((choice, choiceIdx) => (
|
||||
<SelectQuestionChoice
|
||||
<QuestionOptionChoice
|
||||
key={choice.id}
|
||||
choice={choice}
|
||||
choiceIdx={choiceIdx}
|
||||
@@ -267,7 +267,7 @@ export const MultipleChoiceQuestionForm = ({
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<div className="mt-2 flex items-center justify-between space-x-2">
|
||||
{question.choices.filter((c) => c.id === "other").length === 0 && (
|
||||
<Button size="sm" variant="minimal" type="button" onClick={() => addOther()}>
|
||||
Add "Other"
|
||||
@@ -296,7 +296,7 @@ export const MultipleChoiceQuestionForm = ({
|
||||
onValueChange={(e: TShuffleOption) => {
|
||||
updateQuestion(questionIdx, { shuffleOption: e });
|
||||
}}>
|
||||
<SelectTrigger className="w-fit space-x-2 overflow-hidden border-0 font-semibold text-slate-600">
|
||||
<SelectTrigger className="w-fit space-x-2 overflow-hidden border-0 font-medium text-slate-600">
|
||||
<SelectValue placeholder="Select ordering" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -139,7 +139,7 @@ export const PictureSelectionForm = ({
|
||||
<div className="mt-3 flex w-full items-center justify-center">
|
||||
<FileInput
|
||||
id="choices-file-input"
|
||||
allowedFileExtensions={["png", "jpeg", "jpg"]}
|
||||
allowedFileExtensions={["png", "jpeg", "jpg", "webp"]}
|
||||
environmentId={environmentId}
|
||||
onFileUpload={handleFileInputChanges}
|
||||
fileUrl={question?.choices?.map((choice) => choice.imageUrl)}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { RankingQuestionForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/RankingQuestionForm";
|
||||
import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
|
||||
import { QUESTIONS_ICON_MAP, getTSurveyQuestionTypeEnumName } 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 { ChevronDownIcon, ChevronRightIcon, GripIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { QUESTIONS_ICON_MAP, getTSurveyQuestionTypeEnumName } from "@formbricks/lib/utils/questions";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
@@ -367,6 +368,18 @@ export const QuestionCard = ({
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.Ranking ? (
|
||||
<RankingQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={question}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : null}
|
||||
<div className="mt-4">
|
||||
<Collapsible.Root open={openAdvanced} onOpenChange={setOpenAdvanced} className="mt-5">
|
||||
|
||||
@@ -9,15 +9,14 @@ import {
|
||||
TSurvey,
|
||||
TSurveyLanguage,
|
||||
TSurveyMultipleChoiceQuestion,
|
||||
TSurveyQuestionChoice,
|
||||
TSurveyRankingQuestion,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { isLabelValidForAllLanguages } from "../lib/validation";
|
||||
|
||||
interface ChoiceProps {
|
||||
choice: {
|
||||
id: string;
|
||||
label: Record<string, string>;
|
||||
};
|
||||
choice: TSurveyQuestionChoice;
|
||||
choiceIdx: number;
|
||||
questionIdx: number;
|
||||
updateChoice: (choiceIdx: number, updatedAttributes: { label: TI18nString }) => void;
|
||||
@@ -28,13 +27,16 @@ interface ChoiceProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
surveyLanguages: TSurveyLanguage[];
|
||||
question: TSurveyMultipleChoiceQuestion;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyMultipleChoiceQuestion>) => void;
|
||||
question: TSurveyMultipleChoiceQuestion | TSurveyRankingQuestion;
|
||||
updateQuestion: (
|
||||
questionIdx: number,
|
||||
updatedAttributes: Partial<TSurveyMultipleChoiceQuestion> | Partial<TSurveyRankingQuestion>
|
||||
) => void;
|
||||
surveyLanguageCodes: string[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const SelectQuestionChoice = ({
|
||||
export const QuestionOptionChoice = ({
|
||||
addChoice,
|
||||
choice,
|
||||
choiceIdx,
|
||||
@@ -18,7 +18,7 @@ const tabs: Tab[] = [
|
||||
{
|
||||
id: "styling",
|
||||
label: "Styling",
|
||||
icon: <PaintbrushIcon />,
|
||||
icon: <PaintbrushIcon className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
@@ -46,7 +46,7 @@ export const QuestionsAudienceTabs = ({
|
||||
}, [isStylingTabVisible]);
|
||||
|
||||
return (
|
||||
<div className="fixed z-30 flex h-14 w-full items-center justify-center border bg-white md:w-1/2">
|
||||
<div className="fixed z-30 flex h-12 w-full items-center justify-center border-b bg-white md:w-1/2">
|
||||
<nav className="flex h-full items-center space-x-4" aria-label="Tabs">
|
||||
{tabsComputed.map((tab) => (
|
||||
<button
|
||||
@@ -55,9 +55,9 @@ export const QuestionsAudienceTabs = ({
|
||||
onClick={() => setActiveId(tab.id)}
|
||||
className={cn(
|
||||
tab.id === activeId
|
||||
? "border-brand-dark border-b-2 font-semibold text-slate-900"
|
||||
: "text-slate-500 hover:text-slate-700",
|
||||
"flex h-full items-center px-3 text-sm font-medium"
|
||||
? "border-brand-dark font-semibold text-slate-900"
|
||||
: "border-transparent text-slate-500 hover:text-slate-700",
|
||||
"flex h-full items-center border-b-2 px-3 text-sm font-medium"
|
||||
)}
|
||||
aria-current={tab.id === activeId ? "page" : undefined}>
|
||||
{tab.icon && <div className="mr-2 h-5 w-5">{tab.icon}</div>}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { createId } from "@paralleldrive/cuid2";
|
||||
import React, { SetStateAction, useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { MultiLanguageCard } from "@formbricks/ee/multi-language/components/multi-language-card";
|
||||
import { extractLanguageCodes, getLocalizedValue, translateQuestion } from "@formbricks/lib/i18n/utils";
|
||||
import { addMultiLanguageLabels, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { getDefaultEndingCard } from "@formbricks/lib/templates";
|
||||
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
@@ -209,15 +209,14 @@ export const QuestionsView = ({
|
||||
const activeQuestionIdTemp = activeQuestionId ?? localSurvey.questions[0].id;
|
||||
let updatedSurvey: TSurvey = { ...localSurvey };
|
||||
|
||||
// check if we are recalling from this question
|
||||
// check if we are recalling from this question for every language
|
||||
updatedSurvey.questions.forEach((question) => {
|
||||
if (question.headline[selectedLanguageCode].includes(`recall:${questionId}`)) {
|
||||
const recallInfo = extractRecallInfo(getLocalizedValue(question.headline, selectedLanguageCode));
|
||||
if (recallInfo) {
|
||||
question.headline[selectedLanguageCode] = question.headline[selectedLanguageCode].replace(
|
||||
recallInfo,
|
||||
""
|
||||
);
|
||||
for (const [languageCode, headline] of Object.entries(question.headline)) {
|
||||
if (headline.includes(`recall:${questionId}`)) {
|
||||
const recallInfo = extractRecallInfo(headline);
|
||||
if (recallInfo) {
|
||||
question.headline[languageCode] = headline.replace(recallInfo, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -258,18 +257,19 @@ export const QuestionsView = ({
|
||||
toast.success("Question duplicated.");
|
||||
};
|
||||
|
||||
const addQuestion = (question: any, index?: number) => {
|
||||
const addQuestion = (question: TSurveyQuestion, index?: number) => {
|
||||
const updatedSurvey = { ...localSurvey };
|
||||
if (backButtonLabel) {
|
||||
question.backButtonLabel = backButtonLabel;
|
||||
}
|
||||
|
||||
const languageSymbols = extractLanguageCodes(localSurvey.languages);
|
||||
const translatedQuestion = translateQuestion(question, languageSymbols);
|
||||
const updatedQuestion = addMultiLanguageLabels(question, languageSymbols);
|
||||
|
||||
if (index) {
|
||||
updatedSurvey.questions.splice(index, 0, { ...translatedQuestion, isDraft: true });
|
||||
updatedSurvey.questions.splice(index, 0, { ...updatedQuestion, isDraft: true });
|
||||
} else {
|
||||
updatedSurvey.questions.push({ ...translatedQuestion, isDraft: true });
|
||||
updatedSurvey.questions.push({ ...updatedQuestion, isDraft: true });
|
||||
}
|
||||
|
||||
setLocalSurvey(updatedSurvey);
|
||||
@@ -358,7 +358,7 @@ export const QuestionsView = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-16 w-full px-5 py-4">
|
||||
<div className="mt-12 w-full px-5 py-4">
|
||||
<div className="mb-5 flex w-full flex-col gap-5">
|
||||
<EditWelcomeCard
|
||||
localSurvey={localSurvey}
|
||||
@@ -372,7 +372,11 @@ export const QuestionsView = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DndContext sensors={sensors} onDragEnd={onQuestionCardDragEnd} collisionDetection={closestCorners}>
|
||||
<DndContext
|
||||
id="questions"
|
||||
sensors={sensors}
|
||||
onDragEnd={onQuestionCardDragEnd}
|
||||
collisionDetection={closestCorners}>
|
||||
<QuestionsDroppable
|
||||
localSurvey={localSurvey}
|
||||
product={product}
|
||||
@@ -395,7 +399,11 @@ export const QuestionsView = ({
|
||||
<AddQuestionButton addQuestion={addQuestion} product={product} />
|
||||
<div className="mt-5 flex flex-col gap-5">
|
||||
<hr className="border-t border-dashed" />
|
||||
<DndContext sensors={sensors} onDragEnd={onEndingCardDragEnd} collisionDetection={closestCorners}>
|
||||
<DndContext
|
||||
id="endings"
|
||||
sensors={sensors}
|
||||
onDragEnd={onEndingCardDragEnd}
|
||||
collisionDetection={closestCorners}>
|
||||
<SortableContext items={localSurvey.endings} strategy={verticalListSortingStrategy}>
|
||||
{localSurvey.endings.map((ending, index) => {
|
||||
return (
|
||||
@@ -433,6 +441,13 @@ export const QuestionsView = ({
|
||||
activeQuestionId={activeQuestionId}
|
||||
/>
|
||||
|
||||
{/* <SurveyVariablesCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
activeQuestionId={activeQuestionId}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
/> */}
|
||||
|
||||
<MultiLanguageCard
|
||||
localSurvey={localSurvey}
|
||||
product={product}
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
"use client";
|
||||
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import {
|
||||
TI18nString,
|
||||
TShuffleOption,
|
||||
TSurvey,
|
||||
TSurveyRankingQuestion,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
|
||||
import { QuestionOptionChoice } from "./QuestionOptionChoice";
|
||||
|
||||
interface RankingQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
question: TSurveyRankingQuestion;
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyRankingQuestion>) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const RankingQuestionForm = ({
|
||||
question,
|
||||
questionIdx,
|
||||
updateQuestion,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: RankingQuestionFormProps): JSX.Element => {
|
||||
const lastChoiceRef = useRef<HTMLInputElement>(null);
|
||||
const [isInvalidValue, setIsInvalidValue] = useState<string | null>(null);
|
||||
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
const surveyLanguages = localSurvey.languages ?? [];
|
||||
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: { label: TI18nString }) => {
|
||||
if (question.choices) {
|
||||
const newChoices = question.choices.map((choice, idx) => {
|
||||
if (idx !== choiceIdx) return choice;
|
||||
return { ...choice, ...updatedAttributes };
|
||||
});
|
||||
|
||||
updateQuestion(questionIdx, { choices: newChoices });
|
||||
}
|
||||
};
|
||||
|
||||
const addChoice = (choiceIdx: number) => {
|
||||
let newChoices = !question.choices ? [] : question.choices;
|
||||
|
||||
const newChoice = {
|
||||
id: createId(),
|
||||
label: createI18nString("", surveyLanguageCodes),
|
||||
};
|
||||
|
||||
updateQuestion(questionIdx, {
|
||||
choices: [...newChoices.slice(0, choiceIdx + 1), newChoice, ...newChoices.slice(choiceIdx + 1)],
|
||||
});
|
||||
};
|
||||
|
||||
const addOption = () => {
|
||||
const choices = !question.choices ? [] : question.choices;
|
||||
|
||||
const newChoice = {
|
||||
id: createId(),
|
||||
label: createI18nString("", surveyLanguageCodes),
|
||||
};
|
||||
|
||||
updateQuestion(questionIdx, { choices: [...choices, newChoice] });
|
||||
};
|
||||
|
||||
const deleteChoice = (choiceIdx: number) => {
|
||||
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
|
||||
const choiceValue = question.choices[choiceIdx].label[selectedLanguageCode];
|
||||
|
||||
if (isInvalidValue === choiceValue) {
|
||||
setIsInvalidValue(null);
|
||||
}
|
||||
|
||||
updateQuestion(questionIdx, { choices: newChoices });
|
||||
};
|
||||
|
||||
const shuffleOptionsTypes = {
|
||||
none: {
|
||||
id: "none",
|
||||
label: "Keep current order",
|
||||
show: true,
|
||||
},
|
||||
all: {
|
||||
id: "all",
|
||||
label: "Randomize all",
|
||||
show: question.choices.length > 0,
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (lastChoiceRef.current) {
|
||||
lastChoiceRef.current?.focus();
|
||||
}
|
||||
}, [question.choices?.length]);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
label={"Question*"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
label={"Description"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{question.subheader === undefined && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="minimal"
|
||||
className="mt-3"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
}}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="choices">Options*</Label>
|
||||
<div className="mt-2" id="choices">
|
||||
<DndContext
|
||||
id="ranking-choices"
|
||||
onDragEnd={(event) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!active || !over) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeIndex = question.choices.findIndex((choice) => choice.id === active.id);
|
||||
const overIndex = question.choices.findIndex((choice) => choice.id === over.id);
|
||||
|
||||
const newChoices = [...question.choices];
|
||||
|
||||
newChoices.splice(activeIndex, 1);
|
||||
newChoices.splice(overIndex, 0, question.choices[activeIndex]);
|
||||
|
||||
updateQuestion(questionIdx, { choices: newChoices });
|
||||
}}>
|
||||
<SortableContext items={question.choices} strategy={verticalListSortingStrategy}>
|
||||
<div className="flex flex-col">
|
||||
{question.choices &&
|
||||
question.choices.map((choice, choiceIdx) => (
|
||||
<QuestionOptionChoice
|
||||
key={choice.id}
|
||||
choice={choice}
|
||||
choiceIdx={choiceIdx}
|
||||
questionIdx={questionIdx}
|
||||
updateChoice={updateChoice}
|
||||
deleteChoice={deleteChoice}
|
||||
addChoice={addChoice}
|
||||
isInvalid={isInvalid}
|
||||
localSurvey={localSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
surveyLanguages={surveyLanguages}
|
||||
question={question}
|
||||
updateQuestion={updateQuestion}
|
||||
surveyLanguageCodes={surveyLanguageCodes}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
<div className="mt-2 flex flex-1 items-center justify-between gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
EndIcon={PlusIcon}
|
||||
type="button"
|
||||
onClick={() => addOption()}>
|
||||
Add option
|
||||
</Button>
|
||||
<Select
|
||||
defaultValue={question.shuffleOption}
|
||||
value={question.shuffleOption}
|
||||
onValueChange={(option: TShuffleOption) => {
|
||||
updateQuestion(questionIdx, { shuffleOption: option });
|
||||
}}>
|
||||
<SelectTrigger className="w-fit space-x-2 overflow-hidden border-0 font-medium text-slate-600">
|
||||
<SelectValue placeholder="Select ordering" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.values(shuffleOptionsTypes).map(
|
||||
(shuffleOptionsType) =>
|
||||
shuffleOptionsType.show && (
|
||||
<SelectItem
|
||||
key={shuffleOptionsType.id}
|
||||
value={shuffleOptionsType.id}
|
||||
title={shuffleOptionsType.label}>
|
||||
{shuffleOptionsType.label}
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
@@ -17,7 +16,7 @@ export const RedirectUrlForm = ({ endingCard, updateSurvey }: RedirectUrlFormPro
|
||||
id="redirectUrl"
|
||||
name="redirectUrl"
|
||||
className="bg-white"
|
||||
placeholder="https://formbricks.com/signup"
|
||||
placeholder="https://formbricks.com"
|
||||
value={endingCard.url}
|
||||
onChange={(e) => updateSurvey({ url: e.target.value })}
|
||||
/>
|
||||
|
||||
@@ -30,7 +30,10 @@ export const ResponseOptionsCard = ({
|
||||
const [closeOnDateToggle, setCloseOnDateToggle] = useState(false);
|
||||
useState;
|
||||
const [surveyClosedMessageToggle, setSurveyClosedMessageToggle] = useState(false);
|
||||
const [verifyEmailToggle, setVerifyEmailToggle] = useState(false);
|
||||
const [verifyEmailToggle, setVerifyEmailToggle] = useState(localSurvey.isVerifyEmailEnabled);
|
||||
const [isSingleResponsePerEmailEnabledToggle, setIsSingleResponsePerEmailToggle] = useState(
|
||||
localSurvey.isSingleResponsePerEmailEnabled
|
||||
);
|
||||
|
||||
const [surveyClosedMessage, setSurveyClosedMessage] = useState({
|
||||
heading: "Survey Completed",
|
||||
@@ -114,6 +117,14 @@ export const ResponseOptionsCard = ({
|
||||
setLocalSurvey({ ...localSurvey, isVerifyEmailEnabled: !localSurvey.isVerifyEmailEnabled });
|
||||
};
|
||||
|
||||
const handleSingleResponsePerEmailToggle = () => {
|
||||
setIsSingleResponsePerEmailToggle(!isSingleResponsePerEmailEnabledToggle);
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
isSingleResponsePerEmailEnabled: !localSurvey.isSingleResponsePerEmailEnabled,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRunOnDateChange = (date: Date) => {
|
||||
const equivalentDate = date?.getDate();
|
||||
date?.setUTCHours(0, 0, 0, 0);
|
||||
@@ -213,10 +224,6 @@ export const ResponseOptionsCard = ({
|
||||
setSingleUseEncryption(localSurvey.singleUse.isEncrypted);
|
||||
}
|
||||
|
||||
if (localSurvey.isVerifyEmailEnabled) {
|
||||
setVerifyEmailToggle(true);
|
||||
}
|
||||
|
||||
if (localSurvey.runOnDate) {
|
||||
setRunOnDate(localSurvey.runOnDate);
|
||||
setRunOnDateToggle(true);
|
||||
@@ -450,8 +457,17 @@ export const ResponseOptionsCard = ({
|
||||
onToggle={handleVerifyEmailToogle}
|
||||
title="Verify email before submission"
|
||||
description="Only let people with a real email respond."
|
||||
childBorder={true}
|
||||
/>
|
||||
childBorder={true}>
|
||||
<div className="m-1">
|
||||
<AdvancedOptionToggle
|
||||
htmlId="preventDoubleSubmission"
|
||||
isChecked={isSingleResponsePerEmailEnabledToggle}
|
||||
onToggle={handleSingleResponsePerEmailToggle}
|
||||
title="Prevent double submission"
|
||||
description={"Only allow 1 response per email address"}
|
||||
/>
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
<AdvancedOptionToggle
|
||||
htmlId="protectSurveyWithPin"
|
||||
isChecked={isPinProtectionEnabled}
|
||||
|
||||
@@ -68,9 +68,9 @@ export const SurveyEditor = ({
|
||||
const [localStylingChanges, setLocalStylingChanges] = useState<TSurveyStyling | null>(null);
|
||||
|
||||
const fetchLatestProduct = useCallback(async () => {
|
||||
const latestProduct = await refetchProductAction(localProduct.id);
|
||||
if (latestProduct) {
|
||||
setLocalProduct(latestProduct);
|
||||
const refetchProductResponse = await refetchProductAction({ productId: localProduct.id });
|
||||
if (refetchProductResponse?.data) {
|
||||
setLocalProduct(refetchProductResponse.data);
|
||||
}
|
||||
}, [localProduct.id]);
|
||||
|
||||
@@ -95,9 +95,9 @@ export const SurveyEditor = ({
|
||||
const listener = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
const fetchLatestProduct = async () => {
|
||||
const latestProduct = await refetchProductAction(localProduct.id);
|
||||
if (latestProduct) {
|
||||
setLocalProduct(latestProduct);
|
||||
const refetchProductResponse = await refetchProductAction({ productId: localProduct.id });
|
||||
if (refetchProductResponse?.data) {
|
||||
setLocalProduct(refetchProductResponse.data);
|
||||
}
|
||||
};
|
||||
fetchLatestProduct();
|
||||
@@ -206,7 +206,7 @@ export const SurveyEditor = ({
|
||||
)}
|
||||
</main>
|
||||
|
||||
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-200 bg-slate-100 py-6 shadow-inner md:flex md:flex-col">
|
||||
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-200 bg-slate-100 shadow-inner md:flex md:flex-col">
|
||||
<PreviewSurvey
|
||||
survey={localSurvey}
|
||||
questionId={activeQuestionId}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { createSegmentAction } from "@formbricks/ee/advanced-targeting/lib/actions";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { getLanguageLabel } from "@formbricks/lib/i18n/utils";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
@@ -85,6 +86,13 @@ export const SurveyMenuBar = ({
|
||||
};
|
||||
}, [localSurvey, survey]);
|
||||
|
||||
const clearSurveyLocalStorage = () => {
|
||||
if (typeof localStorage !== "undefined") {
|
||||
localStorage.removeItem(`${localSurvey.id}-columnOrder`);
|
||||
localStorage.removeItem(`${localSurvey.id}-columnVisibility`);
|
||||
}
|
||||
};
|
||||
|
||||
const containsEmptyTriggers = useMemo(() => {
|
||||
if (localSurvey.type === "link") return false;
|
||||
|
||||
@@ -132,7 +140,7 @@ export const SurveyMenuBar = ({
|
||||
title: localSurvey.id,
|
||||
});
|
||||
|
||||
return newSegment;
|
||||
return newSegment?.data;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -178,7 +186,9 @@ export const SurveyMenuBar = ({
|
||||
(invalidLanguage: string) => getLanguageLabel(invalidLanguage) ?? invalidLanguage
|
||||
);
|
||||
|
||||
toast.error(`${currentError.message} ${invalidLanguageLabels.join(", ")}`);
|
||||
const messageSplit = currentError.message.split("-fLang-")[0];
|
||||
|
||||
toast.error(`${messageSplit} ${invalidLanguageLabels.join(", ")}`);
|
||||
} else {
|
||||
toast.error(currentError.message);
|
||||
}
|
||||
@@ -223,13 +233,24 @@ export const SurveyMenuBar = ({
|
||||
}
|
||||
});
|
||||
|
||||
if (localSurvey.type !== "link" && !localSurvey.triggers?.length) {
|
||||
toast.error("Please set a survey trigger");
|
||||
setIsSurveySaving(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const segment = await handleSegmentUpdate();
|
||||
const updatedSurvey = await updateSurveyAction({ ...localSurvey, segment });
|
||||
clearSurveyLocalStorage();
|
||||
const updatedSurveyResponse = await updateSurveyAction({ ...localSurvey, segment });
|
||||
|
||||
setIsSurveySaving(false);
|
||||
setLocalSurvey(updatedSurvey);
|
||||
|
||||
toast.success("Changes saved.");
|
||||
if (updatedSurveyResponse?.data) {
|
||||
setLocalSurvey(updatedSurveyResponse.data);
|
||||
toast.success("Changes saved.");
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updatedSurveyResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -265,6 +286,7 @@ export const SurveyMenuBar = ({
|
||||
}
|
||||
const status = localSurvey.runOnDate ? "scheduled" : "inProgress";
|
||||
const segment = await handleSegmentUpdate();
|
||||
clearSurveyLocalStorage();
|
||||
|
||||
await updateSurveyAction({
|
||||
...localSurvey,
|
||||
@@ -281,10 +303,12 @@ export const SurveyMenuBar = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-b border-slate-200 bg-white px-5 py-3 sm:flex sm:items-center sm:justify-between">
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<div className="border-b border-slate-200 bg-white px-5 py-2.5 sm:flex sm:items-center sm:justify-between">
|
||||
<div className="flex h-full items-center space-x-2 whitespace-nowrap">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="h-full"
|
||||
StartIcon={ArrowLeftIcon}
|
||||
onClick={() => {
|
||||
handleBack();
|
||||
@@ -298,11 +322,11 @@ export const SurveyMenuBar = ({
|
||||
const updatedSurvey = { ...localSurvey, name: e.target.value };
|
||||
setLocalSurvey(updatedSurvey);
|
||||
}}
|
||||
className="w-72 border-white hover:border-slate-200"
|
||||
className="h-8 w-72 border-white py-0 hover:border-slate-200"
|
||||
/>
|
||||
</div>
|
||||
{responseCount > 0 && (
|
||||
<div className="ju flex items-center rounded-lg border border-amber-200 bg-amber-100 p-2 text-amber-700 shadow-sm lg:mx-auto">
|
||||
<div className="ju flex items-center rounded-lg border border-amber-200 bg-amber-100 p-1.5 text-amber-800 shadow-sm lg:mx-auto">
|
||||
<TooltipProvider delayDuration={50}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
@@ -313,7 +337,9 @@ export const SurveyMenuBar = ({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<p className="hidden pl-1 text-xs md:text-sm lg:block">{cautionText}</p>
|
||||
<p className="hidden text-ellipsis whitespace-nowrap pl-1.5 text-xs md:text-sm lg:block">
|
||||
{cautionText}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 flex sm:ml-4 sm:mt-0">
|
||||
@@ -327,6 +353,7 @@ export const SurveyMenuBar = ({
|
||||
<Button
|
||||
disabled={disableSave}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="mr-3"
|
||||
loading={isSurveySaving}
|
||||
onClick={() => handleSurveySave()}
|
||||
@@ -337,6 +364,7 @@ export const SurveyMenuBar = ({
|
||||
<Button
|
||||
disabled={disableSave}
|
||||
className="mr-3"
|
||||
size="sm"
|
||||
loading={isSurveySaving}
|
||||
onClick={() => handleSaveAndGoBack()}>
|
||||
Save & Close
|
||||
@@ -344,6 +372,7 @@ export const SurveyMenuBar = ({
|
||||
)}
|
||||
{localSurvey.status === "draft" && audiencePrompt && !isLinkSurvey && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setAudiencePrompt(false);
|
||||
setActiveId("settings");
|
||||
@@ -355,6 +384,7 @@ export const SurveyMenuBar = ({
|
||||
{/* Always display Publish button for link surveys for better CR */}
|
||||
{localSurvey.status === "draft" && (!audiencePrompt || isLinkSurvey) && (
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={isSurveySaving || containsEmptyTriggers}
|
||||
loading={isSurveyPublishing}
|
||||
onClick={handleSurveyPublish}>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { SurveyVariablesCardItem } from "./SurveyVariablesCardItem";
|
||||
|
||||
interface SurveyVariablesCardProps {
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: (survey: TSurvey) => void;
|
||||
activeQuestionId: string | null;
|
||||
setActiveQuestionId: (id: string | null) => void;
|
||||
}
|
||||
|
||||
const variablesCardId = `fb-variables-${Date.now()}`;
|
||||
|
||||
export const SurveyVariablesCard = ({
|
||||
localSurvey,
|
||||
setLocalSurvey,
|
||||
activeQuestionId,
|
||||
setActiveQuestionId,
|
||||
}: SurveyVariablesCardProps) => {
|
||||
const open = activeQuestionId === variablesCardId;
|
||||
|
||||
const setOpenState = (state: boolean) => {
|
||||
if (state) {
|
||||
setActiveQuestionId(variablesCardId);
|
||||
} else {
|
||||
setActiveQuestionId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(open ? "shadow-lg" : "shadow-md", "group z-10 flex flex-row rounded-lg bg-white")}>
|
||||
<div
|
||||
className={cn(
|
||||
open ? "bg-slate-50" : "bg-white group-hover:bg-slate-50",
|
||||
"flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none"
|
||||
)}>
|
||||
<p>🪣</p>
|
||||
</div>
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
onOpenChange={setOpenState}
|
||||
className="flex-1 rounded-r-lg border border-slate-200 transition-all duration-300 ease-in-out">
|
||||
<Collapsible.CollapsibleTrigger
|
||||
asChild
|
||||
className="flex cursor-pointer justify-between p-4 hover:bg-slate-50">
|
||||
<div>
|
||||
<div className="inline-flex">
|
||||
<div>
|
||||
<p className="text-sm font-semibold">Variables</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
{localSurvey.variables.length > 0 ? (
|
||||
localSurvey.variables.map((variable) => (
|
||||
<SurveyVariablesCardItem
|
||||
key={variable.id}
|
||||
mode="edit"
|
||||
variable={variable}
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="mt-2 text-sm italic text-slate-500">No variables yet. Add the first one below.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SurveyVariablesCardItem mode="create" localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,224 @@
|
||||
"use client";
|
||||
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TSurvey, TSurveyVariable } from "@formbricks/types/surveys/types";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { FormControl, FormField, FormItem, FormProvider } from "@formbricks/ui/Form";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
|
||||
|
||||
interface SurveyVariablesCardItemProps {
|
||||
variable?: TSurveyVariable;
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurvey>>;
|
||||
mode: "create" | "edit";
|
||||
}
|
||||
|
||||
export const SurveyVariablesCardItem = ({
|
||||
variable,
|
||||
localSurvey,
|
||||
setLocalSurvey,
|
||||
mode,
|
||||
}: SurveyVariablesCardItemProps) => {
|
||||
const form = useForm<TSurveyVariable>({
|
||||
defaultValues: variable ?? {
|
||||
id: createId(),
|
||||
name: "",
|
||||
type: "number",
|
||||
value: 0,
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const { errors } = form.formState;
|
||||
const isNameError = !!errors.name?.message;
|
||||
const variableType = form.watch("type");
|
||||
|
||||
const editSurveyVariable = useCallback(
|
||||
(data: TSurveyVariable) => {
|
||||
setLocalSurvey((prevSurvey) => {
|
||||
const updatedVariables = prevSurvey.variables.map((v) => (v.id === data.id ? data : v));
|
||||
return { ...prevSurvey, variables: updatedVariables };
|
||||
});
|
||||
},
|
||||
[setLocalSurvey]
|
||||
);
|
||||
|
||||
const createSurveyVariable = (data: TSurveyVariable) => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
variables: [...localSurvey.variables, data],
|
||||
});
|
||||
|
||||
form.reset({
|
||||
id: createId(),
|
||||
name: "",
|
||||
type: "number",
|
||||
value: 0,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === "create") {
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = form.watch(() => form.handleSubmit(editSurveyVariable)());
|
||||
return () => subscription.unsubscribe();
|
||||
}, [form, mode, editSurveyVariable]);
|
||||
|
||||
const onVaribleDelete = (variable: TSurveyVariable) => {
|
||||
const questions = [...localSurvey.questions];
|
||||
|
||||
// find if this variable is used in any question's recall and remove it for every language
|
||||
|
||||
questions.forEach((question) => {
|
||||
for (const [languageCode, headline] of Object.entries(question.headline)) {
|
||||
if (headline.includes(`recall:${variable.id}`)) {
|
||||
const recallInfo = extractRecallInfo(headline);
|
||||
if (recallInfo) {
|
||||
question.headline[languageCode] = headline.replace(recallInfo, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setLocalSurvey((prevSurvey) => {
|
||||
const updatedVariables = prevSurvey.variables.filter((v) => v.id !== variable.id);
|
||||
return { ...prevSurvey, variables: updatedVariables, questions };
|
||||
});
|
||||
};
|
||||
|
||||
if (mode === "edit" && !variable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
className="mt-5"
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
if (mode === "create") {
|
||||
createSurveyVariable(data);
|
||||
} else {
|
||||
editSurveyVariable(data);
|
||||
}
|
||||
})}>
|
||||
{mode === "create" && <Label htmlFor="headline">Add variable</Label>}
|
||||
|
||||
<div className="mt-2 flex w-full items-center gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
rules={{
|
||||
pattern: {
|
||||
value: /^[a-z0-9_]+$/,
|
||||
message: "Only lower case letters, numbers, and underscores are allowed.",
|
||||
},
|
||||
validate: (value) => {
|
||||
// if the variable name is already taken
|
||||
if (
|
||||
mode === "create" &&
|
||||
localSurvey.variables.find((variable) => variable.name === value)
|
||||
) {
|
||||
return "Variable name is already taken, please choose another.";
|
||||
}
|
||||
|
||||
if (mode === "edit" && variable && variable.name !== value) {
|
||||
if (localSurvey.variables.find((variable) => variable.name === value)) {
|
||||
return "Variable name is already taken, please choose another.";
|
||||
}
|
||||
}
|
||||
|
||||
// if it does not start with a letter
|
||||
if (!/^[a-z]/.test(value)) {
|
||||
return "Variable name must start with a letter.";
|
||||
}
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
isInvalid={isNameError}
|
||||
type="text"
|
||||
placeholder="Field name e.g, score, price"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
form.setValue("value", value === "number" ? 0 : "");
|
||||
field.onChange(value);
|
||||
}}>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue placeholder="Select type" className="text-sm" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"number"}>Number</SelectItem>
|
||||
<SelectItem value={"text"}>Text</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
|
||||
<p className="text-slate-600">=</p>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(variableType === "number" ? Number(e.target.value) : e.target.value);
|
||||
}}
|
||||
placeholder="Initial value"
|
||||
type={variableType === "number" ? "number" : "text"}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{mode === "create" && (
|
||||
<Button variant="secondary" type="submit" className="h-10 whitespace-nowrap">
|
||||
Add variable
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{mode === "edit" && variable && (
|
||||
<Button
|
||||
variant="minimal"
|
||||
type="button"
|
||||
size="sm"
|
||||
className="whitespace-nowrap"
|
||||
onClick={() => onVaribleDelete(variable)}>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isNameError && <p className="mt-1 text-sm text-red-500">{errors.name?.message}</p>}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { isAdvancedSegment } from "@formbricks/lib/segment/utils";
|
||||
@@ -81,34 +82,38 @@ export const TargetingCard = ({
|
||||
const handleCloneSegment = async () => {
|
||||
if (!segment) return;
|
||||
|
||||
try {
|
||||
const clonedSegment = await cloneBasicSegmentAction(segment.id, localSurvey.id);
|
||||
const cloneBasicSegmentResponse = await cloneBasicSegmentAction({
|
||||
segmentId: segment.id,
|
||||
surveyId: localSurvey.id,
|
||||
});
|
||||
|
||||
setSegment(clonedSegment);
|
||||
} catch (err) {
|
||||
toast.error(err.message);
|
||||
if (cloneBasicSegmentResponse?.data) {
|
||||
setSegment(cloneBasicSegmentResponse.data);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(cloneBasicSegmentResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadNewSegment = async (surveyId: string, segmentId: string) => {
|
||||
const updatedSurvey = await loadNewBasicSegmentAction(surveyId, segmentId);
|
||||
return updatedSurvey;
|
||||
const loadNewBasicSegmentResponse = await loadNewBasicSegmentAction({ surveyId, segmentId });
|
||||
return loadNewBasicSegmentResponse?.data as TSurvey;
|
||||
};
|
||||
|
||||
const handleSegmentUpdate = async (environmentId: string, segmentId: string, data: TSegmentUpdateInput) => {
|
||||
const updatedSegment = await updateBasicSegmentAction(environmentId, segmentId, data);
|
||||
return updatedSegment;
|
||||
const handleSegmentUpdate = async (segmentId: string, data: TSegmentUpdateInput) => {
|
||||
const updateBasicSegmentResponse = await updateBasicSegmentAction({ segmentId, data });
|
||||
return updateBasicSegmentResponse?.data as TSegment;
|
||||
};
|
||||
|
||||
const handleSegmentCreate = async (data: TSegmentCreateInput) => {
|
||||
const createdSegment = await createBasicSegmentAction(data);
|
||||
return createdSegment;
|
||||
return createdSegment?.data as TSegment;
|
||||
};
|
||||
|
||||
const handleSaveSegment = async (data: TSegmentUpdateInput) => {
|
||||
try {
|
||||
if (!segment) throw new Error("Invalid segment");
|
||||
await updateBasicSegmentAction(environmentId, segment?.id, data);
|
||||
await updateBasicSegmentAction({ segmentId: segment?.id, data });
|
||||
|
||||
router.refresh();
|
||||
toast.success("Segment saved successfully");
|
||||
@@ -122,7 +127,10 @@ export const TargetingCard = ({
|
||||
|
||||
const handleResetAllFilters = async () => {
|
||||
try {
|
||||
return await resetBasicSegmentFiltersAction(localSurvey.id);
|
||||
const resetBasicSegmentFiltersResponse = await resetBasicSegmentFiltersAction({
|
||||
surveyId: localSurvey.id,
|
||||
});
|
||||
return resetBasicSegmentFiltersResponse?.data;
|
||||
} catch (err) {
|
||||
toast.error("Error resetting filters");
|
||||
}
|
||||
@@ -361,7 +369,7 @@ export const TargetingCard = ({
|
||||
{isFormbricksCloud ? (
|
||||
<UpgradePlanNotice
|
||||
message="For advanced targeting, please"
|
||||
textForUrl="upgrade to the User Identification plan."
|
||||
textForUrl="upgrade to the Scale plan."
|
||||
url={`/environments/${environmentId}/settings/billing`}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -123,7 +123,13 @@ export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashS
|
||||
const fetchData = async (searchQuery: string, currentPage: number) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const imagesFromUnsplash = await getImagesFromUnsplashAction(searchQuery, currentPage);
|
||||
const getImagesFromUnsplashResponse = await getImagesFromUnsplashAction({
|
||||
searchQuery: searchQuery,
|
||||
page: currentPage,
|
||||
});
|
||||
if (!getImagesFromUnsplashResponse?.data) return;
|
||||
|
||||
const imagesFromUnsplash = getImagesFromUnsplashResponse.data;
|
||||
for (let i = 0; i < imagesFromUnsplash.length; i++) {
|
||||
const authorName = new URL(imagesFromUnsplash[i].urls.regularWithAttribution).searchParams.get(
|
||||
"authorName"
|
||||
@@ -163,7 +169,7 @@ export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashS
|
||||
try {
|
||||
handleBgChange(imageUrl, "image");
|
||||
if (downloadImageUrl) {
|
||||
await triggerDownloadUnsplashImageAction(downloadImageUrl);
|
||||
await triggerDownloadUnsplashImageAction({ downloadUrl: downloadImageUrl });
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
|
||||
@@ -44,7 +44,6 @@ const Page = async ({ params }) => {
|
||||
getServerSession(authOptions),
|
||||
getSegments(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
|
||||