Compare commits

..

1 Commits

Author SHA1 Message Date
Matti Nannt
623e82ff4d fix: Add rate limit to forgot password route
Add rate limiting to the `/auth/forgot-password` route.

* Import `loginLimiter` in `apps/web/app/api/v1/users/forgot-password/route.ts` and apply it to the `POST` function.
* Add `forgotPasswordLimiter` in `apps/web/app/middleware/bucket.ts` with the same limits as `loginLimiter`.
* Add `forgotPasswordRoute` function in `apps/web/app/middleware/endpointValidator.ts` to identify the `/auth/forgot-password` route.
* Update `apps/web/middleware.ts` to include `forgotPasswordLimiter` and `forgotPasswordRoute` in the rate limiting logic.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/formbricks/formbricks?shareId=XXXX-XXXX-XXXX-XXXX).
2024-10-28 21:36:30 +01:00
787 changed files with 10765 additions and 30574 deletions

View File

@@ -1,11 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
"access": "public",
"baseBranch": "main",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"ignore": ["@formbricks/demo", "@formbricks/web"],
"linked": [],
"updateInternalDependencies": "patch"
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@formbricks/demo", "@formbricks/web"]
}

View File

@@ -2,6 +2,11 @@
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/javascript-node-postgres
// Update the VARIANT arg in docker-compose.yml to pick a Node.js version
{
"name": "Node.js & PostgreSQL",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
@@ -11,18 +16,14 @@
}
},
"dockerComposeFile": "docker-compose.yml",
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or with the host.
"forwardPorts": [3000, 5432, 8025],
"name": "Node.js & PostgreSQL",
"postAttachCommand": "pnpm dev --filter=@formbricks/web... --filter=@formbricks/demo...",
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "cp .env.example .env && sed -i '/^ENCRYPTION_KEY=/c\\ENCRYPTION_KEY='$(openssl rand -hex 32) .env && sed -i '/^NEXTAUTH_SECRET=/c\\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env && sed -i '/^CRON_SECRET=/c\\CRON_SECRET='$(openssl rand -hex 32) .env && pnpm install && pnpm db:migrate:dev",
"postAttachCommand": "pnpm dev --filter=@formbricks/web... --filter=@formbricks/demo...",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node",
"service": "app",
"workspaceFolder": "/workspace"
"remoteUser": "node"
}

View File

@@ -157,7 +157,7 @@ ENTERPRISE_LICENSE_KEY=
# Insert an existing organization id or generate a valid CUID for a new one at https://www.getuniqueid.com/cuid (e.g. cjld2cjxh0000qzrmn831i7rn)
# (Role Management is an Enterprise feature)
# DEFAULT_ORGANIZATION_ID=
# DEFAULT_ORGANIZATION_ROLE=owner
# DEFAULT_ORGANIZATION_ROLE=admin
# Send new users to customer.io
# CUSTOMER_IO_API_KEY=

View File

@@ -1,25 +1,81 @@
name: Bug report
description: "Found a bug? Please fill out the sections below. \U0001F44D"
labels:
- bug
title: "[BUG]"
labels: bug
assignees: []
body:
- type: textarea
id: issue-summary
attributes:
label: Issue Summary
description: A summary of the issue. This needs to be a clear detailed-rich summary.
validations:
required: true
- type: textarea
id: other-information
attributes:
label: Other information (incl. screenshots, Formbricks version, steps to reproduce,...)
validations:
required: false
- type: dropdown
id: environment
attributes:
label: Your Environment
options:
- Formbricks Cloud (app.formbricks.com)
- Self-hosted Formbricks
- type: textarea
id: issue-summary
attributes:
label: Issue Summary
description: A summary of the issue. This needs to be a clear detailed-rich summary.
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to Reproduce
value: |
1. (for example) Went to ...
2. Clicked on...
3. ...
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: other-information
attributes:
label: Other information
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
validations:
required: false
- type: checkboxes
id: environment
attributes:
label: Environment
options:
- label: Formbricks Cloud (app.formbricks.com)
- label: Self-hosted Formbricks
- type: textarea
id: desktop-version
attributes:
label: Desktop (please complete the following information)
description: |
examples:
- **OS**: [e.g. iOS]
- **Browser**: [e.g. chrome, safari]
- **Version**: [e.g. 22]
value: |
- OS:
- Node:
- npm:
render: markdown
validations:
required: true
- type: markdown
id: nodejs-version
attributes:
value: |
#### Node.JS version
[e.g. v18.15.0]
- type: markdown
id: anything-else
attributes:
value: |
#### Anything else?
- Screen recording, console logs, network requests: You can make a recording with [Loom](https://www.loom.com).
- Anything else that you think could be an issue?

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions
url: https://github.com/formbricks/formbricks/discussions
about: Need help selfhosting or ask a general question about the project? Open a discussion
url: https://formbricks.com/discord
about: Ask a general question about the project on our Discord server

View File

@@ -1,7 +1,8 @@
name: Feature request
description: "Suggest an idea for this project \U0001F680"
labels:
- enhancement
title: "[FEATURE]"
labels: enhancement
assignees: []
body:
- type: textarea
id: problem-description
@@ -17,6 +18,13 @@ body:
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternate-solution-description
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
id: additional-context
attributes:
@@ -25,9 +33,15 @@ body:
validations:
required: false
- type: markdown
id: formbricks-info
attributes:
value: |
### Additional resources 🤓
### How we code at Formbricks 🤓
- Check out our [Contributor Docs](https://formbricks.com/docs/developer-docs/contributing/get-started)
- Follow Best Practices lined out in our [Contributor Docs](https://formbricks.com/docs/contributing/how-we-code)
- First time: Please read our [introductory blog post](https://formbricks.com/blog/join-the-formtribe)
- All UI components are in the package `formbricks/ui`
- Run `pnpm go` to find a demo app to test in-app surveys at `localhost:3002`
- Everything is type-safe.
- We use **chatGPT** to help refactor code.
- Anything unclear? [Ask in Discord](https://formbricks.com/discord)

View File

@@ -0,0 +1,33 @@
name: oss.gg hack submission 🕹️
description: "Submit your contribution for the for the oss.gg hackathon"
title: "[🕹️]"
labels: 🕹️ oss.gg, player submission, hacktoberfest
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

View File

@@ -4,7 +4,7 @@
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Fixes #(issue)
Fixes # (issue)
<!-- Please provide a screenshots or a loom video for visual changes to speed up reviews
Loom Video: https://www.loom.com/

View File

@@ -1,92 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '17 1 * * 1'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@@ -4,9 +4,9 @@ on:
workflow_dispatch:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs "At 00:00." (see https://crontab.guru)
- cron: "0 0 * * *"
# schedule:
# Runs At 00:00. (see https://crontab.guru)
# - cron: "0 0 * * *"
jobs:
cron-weeklySummary:
env:

View File

@@ -2,10 +2,5 @@ const baseConfig = require("./packages/config-prettier/prettier-preset");
module.exports = {
...baseConfig,
plugins: [
"@trivago/prettier-plugin-sort-imports",
"prettier-plugin-tailwindcss",
"prettier-plugin-sort-json",
],
jsonRecursiveSort: true,
plugins: ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],
};

18
.vscode/launch.json vendored
View File

@@ -1,21 +1,21 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch localhost:3002",
"reAttach": true,
"request": "launch",
"type": "firefox",
"request": "launch",
"reAttach": true,
"url": "http://localhost:3002/",
"webRoot": "${workspaceFolder}"
},
{
"name": "Attach",
"request": "attach",
"type": "firefox"
"type": "firefox",
"request": "attach"
}
],
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0"
]
}

View File

@@ -1,4 +1,4 @@
{
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.importModuleSpecifier": "non-relative"
}

View File

@@ -51,13 +51,13 @@ In the interest of responsibly managing vulnerabilities, please adhere to the fo
> Do not reveal the problem to others until it has been resolved.
1. **Send a Detailed Report**:
- Raise a security report on [Github](https://github.com/formbricks/formbricks/issues/new/choose) or send an email to [security@formbricks.com](mailto:security@formbricks.com).
- Address emails to [security@formbricks.com](mailto:security@formbricks.com).
- Include:
- Problem description.
- Detailed, reproducible steps, with screenshots where possible.
- Affected version(s).
- Known possible mitigations.
- Your preferred contact method.
- Your Discord username or preferred contact method.
2. **Acknowledgement of Receipt**:
- Our security team will acknowledge receipt and provide an initial response within 48 hours.
- Following verification of the vulnerability and the fix, a release plan will be formulated, with the fix deployed between 7 to 28 days, depending on the severity and complexity.

View File

@@ -1,32 +1,32 @@
{
"expo": {
"android": {
"adaptiveIcon": {
"backgroundColor": "#ffffff",
"foregroundImage": "./assets/adaptive-icon.png"
}
},
"assetBundlePatterns": ["**/*"],
"name": "react-native-demo",
"slug": "react-native-demo",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"ios": {
"infoPlist": {
"NSCameraUsageDescription": "Take pictures for certain activities.",
"NSMicrophoneUsageDescription": "Need microphone access for recording videos.",
"NSPhotoLibraryUsageDescription": "Select pictures for certain activities."
},
"supportsTablet": true
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"jsEngine": "hermes",
"name": "react-native-demo",
"orientation": "portrait",
"slug": "react-native-demo",
"splash": {
"backgroundColor": "#ffffff",
"image": "./assets/splash.png",
"resizeMode": "contain"
"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"
}
},
"userInterfaceStyle": "light",
"version": "1.0.0",
"web": {
"favicon": "./assets/favicon.png"
}

View File

@@ -1,6 +1,6 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
},
"extends": "expo/tsconfig.base"
}
}

View File

@@ -14,7 +14,7 @@
"@formbricks/js": "workspace:*",
"@formbricks/ui": "workspace:*",
"lucide-react": "0.452.0",
"next": "14.2.16",
"next": "14.2.15",
"react": "18.3.1",
"react-dom": "18.3.1"
},

View File

@@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
};
}

View File

@@ -1,5 +1,5 @@
{
"exclude": ["node_modules"],
"extends": "@formbricks/config-typescript/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@@ -60,7 +60,7 @@ All you need to do is copy a `<script>` tag to your HTML head, and thats abou
</Property>
</Properties>
Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
Refer to our [Example HTML project](https://github.com/formbricks/examples/tree/main/html) for more help! Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
---
@@ -118,7 +118,7 @@ export default App;
</Property>
</Properties>
Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
Refer to our [Example ReactJs project](https://github.com/formbricks/examples/tree/main/reactjs) for more help! Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
---
@@ -200,6 +200,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</CodeGroup>
</Col>
Refer to our [Example NextJS App Directory project](https://github.com/formbricks/examples/tree/main/nextjs-app) for more help!
### Pages Directory
<Col>
@@ -237,6 +239,7 @@ export default function App({ Component, pageProps }: AppProps) {
</CodeGroup>
</Col>
Refer to our [Example NextJS Pages Directory project](https://github.com/formbricks/examples/tree/main/nextjs-pages) for more help!
### Required customizations to be made
@@ -329,7 +332,7 @@ router.afterEach((to, from) => {
</Property>
</Properties>
Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,7 +1,7 @@
import { MdxImage } from "@/components/MdxImage";
import AddQuestion from "./images/add-question.webp";
import EmailField from "./images/email-field.webp";
import EmbedImage from "./images/embed.webp";
import EmbedImage from "./images/embed.png";
import MessageField from "./images/message-field.webp";
import NameField from "./images/name-field.webp";
import QueryImage from "./images/query-form.webp";
@@ -159,8 +159,6 @@ After publishing the form, follow these steps to integrate it into your site:
2. **Embed the Code**
- Copy the provided code and paste it into your website where you want the form to appear.
<Note>Note: There is an options toggle button called "Embed Mode." When enabled, it updates the `src` to `"?embed=true"` and displays your survey in a minimalist design, removing padding and background for a cleaner look.</Note>
3. **Test the Integration**
- Check if the form displays correctly on your site.
- Submit a test entry to ensure everything works and notifications are received.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -39,13 +39,13 @@ To get this running, you'll need a bit of time. Here are the steps we're going t
1. To get started, create an account for the [Formbricks Cloud](https://app.formbricks.com/auth/signup).
2. In the Menu (top right) you see that you can toggle switch between a “Development” and a “Production” environment. These are two separate environments so your test data doesnt mess up the insights from prod. Switch to “Development”:
2. In the Menu (top right) you see that you can switch between a “Development” and a “Production” environment. These are two separate environments so your test data doesnt mess up the insights from prod. Switch to “Development”:
<MdxImage
src={SwitchToDev}
alt="switch to dev environment"
quality="100"
className="max-w-full rounded-lg sm:max-w-xs"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
3. Then, create a survey using the template “Docs Feedback”:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -115,7 +115,7 @@ Lastly, scroll down to “Recontact Options”. Here you have full freedom to de
src={RecontactOptions}
alt="Set recontact options"
quality="100"
className="max-w-full rounded-lg sm:max-w-2xl"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
### 7. Congrats! Youre ready to publish your survey 💃

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -1,178 +0,0 @@
import { MdxImage } from "@/components/MdxImage";
import SingleSelect from "./single-select.webp";
import Quiz from "./quiz.webp";
import Score from "./score.webp"
import AddLogic from "./conditional-logic.webp";
import WhenThen from "./when-then.webp";
import EndingLogic from "./ending-logic.webp";
import PassFail from "./pass-fail.webp";
export const metadata = {
title: "How to Create a Quiz Using Formbricks - Step-by-Step Guide",
description:
"Learn to leverage Formbricks to create Quizzes. Follow our detailed step-by-step guide to build quizzes with custom logic and multiple endings.",
};
# Creating a quiz with Formbricks - Step-by-step Guide
Welcome to this guide on creating engaging quizzes with Formbricks! Quizzes help you capture customer insights, explore user personalities, or simply add fun for your team. With Formbricks, you can personalize quizzes in minutes add scores, customize backgrounds, and more, all without any technical skills!
## What we'll build
By the end of this tutorial, you'll have created a simple trivia Quiz featuring:
1. Score calculations.
2. Multiple endings depending on the score.
## Setting up the form
First, make sure you have a Formbricks account. If not, you can create one [here](https://app.formbricks.com):
1. Head to the Surveys page and click on **New Survey**.
2. Select Start from Scratch to create a new form.
3. Go to the settings and select form type as **Link Survey**
4. In the form editor, click the three dots next to a question, then select Change Question Type and choose **Single-Select**.
<MdxImage
src={SingleSelect}
alt="Change Question type to Single-Select"
quality="100"
className="max-w-full rounded-lg sm:max-w-2xl"
/>
5. Add a welcoming statement to greet your users and explain the Quiz's purpose.
6. Personalize the greeting to make it inviting and encourage engagement.
**Note:** While were creating a Link Survey here, the process is similar for Web and App surveys.
## Adding the questions
Next, let's create a question for example with multiple options:
What country has the longest coastline in the world?
A) Canada
B) Japan
C) India
D) Nepal
<MdxImage
src={Quiz}
alt="Sample Question"
quality="100"
className="max-w-full rounded-lg sm:max-w-xl"
/>
## Calculate Score
Now that we have our question ready, lets add the logic to calculate scores.
1. Scroll down in the editor and click on variables.
2. Create a new variable named `score` with a default value of 0
<MdxImage
src={Score}
alt="Create Variable named Score image"
quality="100"
className="max-w-full rounded-lg sm:max-w-xl"
/>
3. Now go back to the question and expand the advanced settings by clicking on `> Show Advanced Settings`.
4. Under the conditional logic you should see the option to `Add Logic`. Click on that.
<MdxImage
src={AddLogic}
alt="Add Logic Button"
quality="100"
className="max-w-full rounded-lg sm:max-w-xl"
/>
5. Now you should see conditional logic. Customize the logic to match your needs for example:
**When** `question` equals `YOUR_ANSWER_HERE` **Then** `Calculate` `score` `Add +` `01`. So it should look something like this.
<MdxImage
src={WhenThen}
alt="When-Then Conditional Logic"
quality="100"
className="max-w-full rounded-lg sm:max-w-xl"
/>
6. Let's duplicate and customize these questions. Click on the duplicate icon at the top of the question.
7. Now edit the questions, options, and answers in the **conditional logic**
## Creating Multiple Endings Based on Scores
Once you have all the questions and the calculation logic in place, its time to customize the endings. Scroll down to the Ending Card section. We will create two cards for this quiz: one for when the user fails the quiz and another for when the user passes.
1. Customize the ending card.
2. Display the score by typing `@score`. ( You can address all the variables or questions by just typing @ ).
3. Add logic to the last question. ( this is necessary to redirect the user based on the score ). Kind of like this:
**When** `score` >= `03` **Then** `Jump to` `Pass`. So it should look something like this.
<MdxImage
src={EndingLogic}
alt="Conditional Logic for ending card"
quality="100"
className="max-w-full rounded-lg sm:max-w-xl"
/>
4. Ensure that the Fail card is positioned above the Pass card. This allows any condition that does not meet the criteria of being greater than or equal to 3 to jump to the Fail card.
<MdxImage
src={PassFail}
alt="Pass or Fail ending Cards"
quality="100"
className="max-w-full rounded-lg sm:max-w-xl"
/>
5. That's it! Now you can save and publish the quiz.
# Wrapping Up
Congratulations! Youve successfully created a Quiz with Formbricks. You can play around with the quiz that we just created [here](https://app.formbricks.com/s/cm2wwt3vu0001ir8o7ys0bezz).
A great quiz can serve as an excellent lead generator, a job fit checker, or just a fun icebreaker for your team. You now have the skills to build that! If you want to read more about building quizzes and how you can create a Job Fit Quiz check this article [here](https://www.harshbhat.me/blog/formbricks-quiz).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -8,10 +8,7 @@ import LinkWithQuestions from "./images/link-with-questions.webp";
import ListLinkedSurveys from "./images/list-linked-surveys.webp";
import SlackAuth from "./images/slack-auth.webp";
import SlackConnected from "./images/slack-connected.webp";
import AddSlackBot1 from "./images/add-slack-bot-1.webp";
import AddSlackBot2 from "./images/add-slack-bot-2.webp";
import AddSlackBot3 from "./images/add-slack-bot-3.webp";
import AddSlackBot4 from "./images/add-slack-bot-4.webp";
export const metadata = {
title: "Slack",
description:
@@ -72,34 +69,7 @@ The slack integration allows you to automatically send responses to a Slack chan
channel in the Slack workspace you integrated.
</Note>
5. In order to make your channel available in channel dropdown, you need to add formbricks integration bot to the channel you want to link. You can do this by going to channel settings -> Integrations -> Add apps -> Search for "Formbricks" -> Select the bot -> Add.
<MdxImage
src={AddSlackBot1}
alt="Click on three dot at top right of the channel"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<MdxImage
src={AddSlackBot2}
alt="Select Edit Settings"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<MdxImage
src={AddSlackBot3}
alt="Navigate to Integrations"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<MdxImage
src={AddSlackBot4}
alt="Add Formbricks Bot"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
6. Now click on the "Link channel" button to link a Slack channel with Formbricks and a modal will open up.
5. Now click on the "Link channel" button to link a Slack channel with Formbricks and a modal will open up.
<MdxImage
src={LinkSurveyWithChannel}
@@ -108,7 +78,7 @@ The slack integration allows you to automatically send responses to a Slack chan
className="max-w-full rounded-lg sm:max-w-3xl"
/>
7. Select the channel you want to link with Formbricks and the Survey. On doing so, you will be asked to select the questions' responses you want to feed in the Slack channel. Select the questions and click on the "Link Channel" button.
6. Select the channel you want to link with Formbricks and the Survey. On doing so, you will be asked to select the questions' responses you want to feed in the Slack channel. Select the questions and click on the "Link Channel" button.
<MdxImage
src={LinkWithQuestions}
@@ -117,7 +87,7 @@ The slack integration allows you to automatically send responses to a Slack chan
className="max-w-full rounded-lg sm:max-w-3xl"
/>
8. On submitting, the modal will close and you will see the linked Slack channel in the list of linked Slack channels.
7. On submitting, the modal will close and you will see the linked Slack channel in the list of linked Slack channels.
<MdxImage
src={ListLinkedSurveys}
@@ -154,7 +124,6 @@ Enabling the Slack Integration in a self-hosted environment requires a setup usi
4. Go to the **OAuth & Permissions** tab on the sidebar and add the following **Bot Token Scopes**:
- `channels:read`
- `groups:read`
- `chat:write`
- `chat:write.public`
- `chat:write.customize`

View File

@@ -13,91 +13,64 @@ export const metadata = {
# Organization Access Roles
Learn about the different organization-level and team-level roles and how they affect permissions in Formbricks.
## Memberships
Permissions in Formbricks are broadly handled using organization-level roles, which apply to all teams and projects in the organization. Users on a self-hosting and Enterprise plan, have access to team-level roles, which enable more granular permissions.
Assign different roles to organization members to grant them specific rights like creating surveys, viewing responses, or managing organization members.
<Note>
Access Roles is a feature of the **Enterprise Edition**. In the **Community Edition** and on the **Free**
and **Startup** plan in the Cloud you can invite unlimited organization members as `Owner`.
and **Startup** plan in the Cloud you can invite unlimited organization members as `Admins`.
</Note>
Here are the different access permissions, ranked from highest to lowest access
1. Owner
2. Manager
3. Billing
4. Member
### Organisational level
All users and their organization-level roles are listed in **Organization Settings > General**. Users can hold any of the following org-level roles:
- **Billing** users can manage payment and compliance details in the organization.
- **Org Members** can view most data in the organization and act in the products they are members of. They cannot join products on their own and need to be assigned.
- **Org Managers** have full management access to all teams and products. They can also manage the organization's membership. Org Managers can perform Team Admin actions without needing to join the team. They cannot change other organization settings.
- **Org Owners** have full access to the organization, its data, and settings. Org Owners can perform Team Admin actions without needing to join the team.
### Permissions at product level
- **read**: read access to all resources (except settings) in the product.
- **read & write**: read & write access to all resources (except settings) in the product.
- **manage**: read & write access to all resources including settings in the product.
### Team-level Roles
- **Team Contributors** can view and act on surveys and responses.
- **Team Admins** have additional permissions to manage their team's membership and products. These permissions are granted at the team-level, and don't apply to teams where they're not a Team Admin.
2. Admin
3. Developer
4. Editor
5. Viewer
For more information on user roles & permissions, see below:
| | Owner | Manager | Billing | Member |
| -------------------------------- | ----- | ------- | ------- | ------ |
| **Organization** | | | | |
| Update organization | ✅ | | ❌ | ❌ |
| Delete organization | ✅ | ❌ | ❌ | ❌ |
| Add new Member | ✅ | ✅ | ❌ | ❌ |
| Delete Member | ✅ | ✅ | ❌ | ❌ |
| Update Member Access | ✅ | ✅ | ❌ | ❌ |
| Update Billing | ✅ | ✅ | | ❌ |
| **Product** | | | | |
| Create Product | ✅ | ✅ | ❌ | ❌ |
| Update Product Name | ✅ | ✅ | ❌ | ✅\*\* |
| Update Product Recontact Options | ✅ | ✅ | | ✅\*\* |
| Update Look & Feel | ✅ | ✅ | | ✅\*\* |
| Update Survey Languages | ✅ | ✅ | | ✅\*\* |
| Delete Product | ✅ | ✅ | | ❌ |
| **Surveys** | | | | |
| Create New Survey | ✅ | ✅ | | ✅\* |
| Edit Survey | ✅ | ✅ | | ✅\* |
| Delete Survey | ✅ | ✅ | | ✅\* |
| View survey results | ✅ | ✅ | | ✅ |
| **Response** | | | | |
| Delete response | ✅ | ✅ | | ✅\* |
| Add tags on response | ✅ | ✅ | | ✅\* |
| Edit tags on response | ✅ | ✅ | | ✅\* |
| **Actions** | | | | |
| Create Action | ✅ | ✅ | | ✅\* |
| Update Action | ✅ | ✅ | | ✅\* |
| Delete Action | ✅ | ✅ | | ✅\* |
| **API Keys** | | | | |
| Create API key | ✅ | ✅ | | ✅\*\* |
| Update API key | ✅ | ✅ | | ✅\*\* |
| Delete API key | ✅ | ✅ | | ✅\*\* |
| **Tags** | | | | |
| Create tags | ✅ | ✅ | | ✅\* |
| Update tags | ✅ | ✅ | | ✅\* |
| Delete tags | ✅ | ✅ | | ✅\*\* |
| **People** | | | | |
| Delete Person | ✅ | ✅ | | ✅\* |
| **Integrations** | | | | |
| Manage Integrations | ✅ | ✅ | | ✅\* |
\* - for the read & write permissions team members
\*\* - for the manage permissions team members
| | Owner | Admin | Editor | Developer | Viewer |
| -------------------------------- | ----- | ----- | ------ | --------- | ------ |
| **Organization** | | | | | |
| Update organization | ✅ | | ❌ | ❌ | ❌ |
| Delete organization | ✅ | ❌ | ❌ | ❌ | ❌ |
| Add new Member | ✅ | ✅ | ❌ | ❌ | ❌ |
| Delete Member | ✅ | ✅ | ❌ | ❌ | ❌ |
| Update Member Access | ✅ | ✅ | ❌ | ❌ | ❌ |
| Update Billing | ✅ | ✅ | ❌ | | ❌ |
| **Product** | | | | | |
| Create Product | ✅ | ✅ | ❌ | ❌ | ❌ |
| Update Product Name | ✅ | ✅ | ✅ | ❌ | ❌ |
| Update Product Recontact Options | ✅ | ✅ | ✅ | | |
| Update Look & Feel | ✅ | ✅ | ✅ | | |
| Update Survey Languages | ✅ | ✅ | ✅ | | |
| Delete Product | ✅ | ✅ | ✅ | | ❌ |
| **Surveys** | | | | | |
| Create New Survey | ✅ | ✅ | ✅ | | ❌ |
| Edit Survey | ✅ | ✅ | ✅ | | ❌ |
| Delete Survey | ✅ | ✅ | ✅ | | ❌ |
| View survey results | ✅ | ✅ | ✅ | | ✅ |
| **Response** | | | | | |
| Delete response | ✅ | ✅ | ✅ | | ❌ |
| Add tags on response | ✅ | ✅ | ✅ | | ❌ |
| Edit tags on response | ✅ | ✅ | ✅ | | ❌ |
| **Actions** | | | | | |
| Create Action | ✅ | ✅ | ✅ | | ❌ |
| Update Action | ✅ | ✅ | ✅ | | ❌ |
| Delete Action | ✅ | ✅ | ✅ | | ❌ |
| **API Keys** | | | | | |
| Create API key | ✅ | ✅ | ✅ | | |
| Update API key | ✅ | ✅ | ✅ | | |
| Delete API key | ✅ | ✅ | ✅ | | |
| **Tags** | | | | | |
| Create tags | ✅ | ✅ | ✅ | | ❌ |
| Update tags | ✅ | ✅ | ✅ | | ❌ |
| Delete tags | ✅ | ✅ | ✅ | | |
| **People** | | | | | |
| Delete Person | ✅ | ✅ | ✅ | | ❌ |
| **Integrations** | | | | | |
| Manage Integrations | ✅ | ✅ | ✅ | | ❌ |
## Inviting organization members

View File

@@ -11,7 +11,6 @@ Matrix questions allow respondents to select a value for each option presented i
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/obqeey0574jig4lo2gqyv51e" />
## Elements
<MdxImage
src={Matrix}
alt="Overview of Matrix question type"
@@ -20,22 +19,18 @@ Matrix questions allow respondents to select a value for each option presented i
/>
### Title
Add a clear title to inform the respondent what information you are asking for.
### Description
Provide an optional description with further instructions.
### Rows
Define the options shown on the left side of the matrix. These represent the items for which users will select a value.
### Columns
Represent the range of values from 0 to X (right side of the screen). Users can choose any value, including 0, using radio buttons.
### Select ordering
- Keep current order: This will keep the order of options the same for all respondents.
- Randomize all: This will randomize the options for each respondent.
- Randomize all: This will randomize the options for each respondent.

View File

@@ -63,7 +63,7 @@ These variables are present inside your machines docker-compose file. Restart
| TELEMETRY_DISABLED | Disables telemetry if set to 1. | 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 | owner |
| DEFAULT_ORGANIZATION_ROLE | Role of the user in the default organization. | optional | admin |
| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | |
| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
| OIDC_CLIENT_SECRET | Secret for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |

View File

@@ -302,7 +302,6 @@ Enabling the Slack Integration in a self-hosted environment requires a setup usi
4. Go to the **OAuth & Permissions** tab on the sidebar and add the following **Bot Token Scopes**:
- `channels:read`
- `groups:read`
- `chat:write`
- `chat:write.public`
- `chat:write.customize`

View File

@@ -289,25 +289,6 @@ interface NavigationProps extends React.ComponentPropsWithoutRef<"nav"> {
export const Navigation = ({ isMobile, ...props }: NavigationProps) => {
const [activeGroup, setActiveGroup] = useState<NavGroup | null>(navigation[0]);
const [openGroups, setOpenGroups] = useState<string[]>([]);
const pathname = usePathname();
useEffect(() => {
// Check the current pathname and set the active group
navigation.forEach((group) => {
group.links.forEach((link) => {
if (link.href && pathname.startsWith(link.href)) {
setActiveGroup(group);
} else if (link.children) {
link.children.forEach((child) => {
if (pathname.startsWith(child.href)) {
setActiveGroup(group);
setOpenGroups([`${group.title}-${link.title}`]); // Ensure parent is open
}
});
}
});
});
}, [pathname]);
return (
<nav {...props}>

View File

@@ -19,7 +19,6 @@ export const navigation: Array<NavGroup> = [
{ title: "Docs Feedback", href: "/best-practices/docs-feedback" },
{ title: "Improve Email Content", href: "/best-practices/improve-email-content" },
{ title: "Contact Form", href: "/best-practices/contact-form" },
{ title: "Quiz Time", href: "/best-practices/quiz-time" },
],
},
],
@@ -126,7 +125,7 @@ export const navigation: Array<NavGroup> = [
{ title: "Zapier", href: "/developer-docs/integrations/zapier" },
],
},
{ title: "Organization and User Management", href: "/global/access-roles" },
{ title: "Access Roles", href: "/global/access-roles" },
{ title: "Styling Theme", href: "/global/styling-theme" },
],
},

View File

@@ -2,19 +2,9 @@ import { slugifyWithCounter } from "@sindresorhus/slugify";
import * as acorn from "acorn";
import { toString } from "mdast-util-to-string";
import { mdxAnnotations } from "mdx-annotations";
import { createCssVariablesTheme, createHighlighter } from "shiki";
import { getHighlighter, renderToHtml } from "shiki";
import { visit } from "unist-util-visit";
let highlighterPromise;
const supportedLanguages = ["javascript", "html", "shell", "tsx", "json", "yml", "ts"];
const myTheme = createCssVariablesTheme({
name: "css-variables",
variablePrefix: "--shiki-",
variableDefaults: {},
fontStyle: true,
});
const rehypeParseCodeBlocks = () => {
return (tree) => {
visit(tree, "element", (node, _nodeIndex, parentNode) => {
@@ -25,31 +15,29 @@ const rehypeParseCodeBlocks = () => {
};
};
let highlighter;
const getHighlighter = async () => {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
langs: supportedLanguages,
themes: [myTheme],
})
}
return highlighterPromise;
}
const rehypeShiki = () => {
return async (tree) => {
const highlighter = await getHighlighter();
highlighter = highlighter ?? (await getHighlighter({ theme: "css-variables" }));
visit(tree, "element", (node) => {
if (node.tagName === "pre" && node.children[0]?.tagName === "code") {
let codeNode = node.children[0];
let textNode = codeNode.children[0];
if (!codeNode || !textNode) return;
node.properties.code = textNode.value;
if (codeNode.properties.className && codeNode.properties.className.length > 0) {
let lang = codeNode.properties.className[0].replace("language-", "");
const code = highlighter.codeToHtml(textNode.value, { lang, theme: "css-variables" });
textNode.value = code;
if (node.properties.language) {
let tokens = highlighter.codeToThemedTokens(textNode.value, node.properties.language);
textNode.value = renderToHtml(tokens, {
elements: {
pre: ({ children }) => children,
code: ({ children }) => children,
line: ({ children }) => `<span>${children}</span>`,
},
});
}
}
});

View File

@@ -39,7 +39,7 @@
"lucide-react": "0.452.0",
"mdast-util-to-string": "4.0.0",
"mdx-annotations": "0.1.4",
"next": "14.2.16",
"next": "14.2.15",
"next-plausible": "3.12.2",
"next-seo": "6.6.0",
"next-sitemap": "4.2.3",
@@ -57,7 +57,7 @@
"remark-mdx": "3.0.1",
"schema-dts": "1.1.2",
"sharp": "0.33.5",
"shiki": "1.22.0",
"shiki": "0.14.7",
"simple-functional-loader": "1.2.1",
"tailwindcss": "3.4.13",
"unist-util-filter": "5.0.1",

View File

@@ -1,4 +1,7 @@
{
"extends": "@formbricks/config-typescript/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../../packages/types/*.d.ts"],
"exclude": ["../../.env", "node_modules"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
@@ -10,8 +13,5 @@
}
],
"strictNullChecks": true
},
"exclude": ["../../.env", "node_modules"],
"extends": "@formbricks/config-typescript/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../../packages/types/*.d.ts"]
}
}

View File

@@ -1,36 +1,35 @@
import { Meta } from "@storybook/blocks";
import Accessibility from "./assets/accessibility.png";
import AddonLibrary from "./assets/addon-library.png";
import Assets from "./assets/assets.png";
import Context from "./assets/context.png";
import Discord from "./assets/discord.svg";
import Docs from "./assets/docs.png";
import FigmaPlugin from "./assets/figma-plugin.png";
import Github from "./assets/github.svg";
import Share from "./assets/share.png";
import Styling from "./assets/styling.png";
import Testing from "./assets/testing.png";
import Theming from "./assets/theming.png";
import Tutorials from "./assets/tutorials.svg";
import Discord from "./assets/discord.svg";
import Youtube from "./assets/youtube.svg";
import Tutorials from "./assets/tutorials.svg";
import Styling from "./assets/styling.png";
import Context from "./assets/context.png";
import Assets from "./assets/assets.png";
import Docs from "./assets/docs.png";
import Share from "./assets/share.png";
import FigmaPlugin from "./assets/figma-plugin.png";
import Testing from "./assets/testing.png";
import Accessibility from "./assets/accessibility.png";
import Theming from "./assets/theming.png";
import AddonLibrary from "./assets/addon-library.png";
export const RightArrow = () => (
<svg
viewBox="0 0 14 14"
width="8px"
height="14px"
style={{
marginLeft: "4px",
display: "inline-block",
shapeRendering: "inherit",
verticalAlign: "middle",
fill: "currentColor",
"path fill": "currentColor",
}}>
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
</svg>
);
export const RightArrow = () => <svg
viewBox="0 0 14 14"
width="8px"
height="14px"
style={{
marginLeft: '4px',
display: 'inline-block',
shapeRendering: 'inherit',
verticalAlign: 'middle',
fill: 'currentColor',
'path fill': 'currentColor'
}}
>
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
</svg>
<Meta title="Configure your project" />
@@ -39,7 +38,6 @@ export const RightArrow = () => (
# Configure your project
Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community.
</div>
<div className="sb-section">
<div className="sb-section-item">
@@ -86,7 +84,6 @@ export const RightArrow = () => (
# Do more with Storybook
Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs.
</div>
<div className="sb-section">
@@ -206,7 +203,6 @@ export const RightArrow = () => (
target="_blank"
>Discover tutorials<RightArrow /></a>
</div>
</div>
<style>

View File

@@ -1,24 +1,24 @@
{
"compilerOptions": {
"allowImportingTsExtensions": true,
"isolatedModules": true,
"jsx": "react-jsx",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"noEmit": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"target": "ES2020",
"useDefineForClassFields": true
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]

View File

@@ -1,10 +1,10 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"skipLibCheck": true
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,7 +1,6 @@
"use client";
import { ArrowRight } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { cn } from "@formbricks/lib/cn";
@@ -23,8 +22,8 @@ export const ConnectWithFormbricks = ({
widgetSetupCompleted,
channel,
}: ConnectWithFormbricksProps) => {
const t = useTranslations();
const router = useRouter();
const handleFinishOnboarding = async () => {
if (!widgetSetupCompleted) {
router.push(`/environments/${environment.id}/connect/invite`);
@@ -65,10 +64,8 @@ export const ConnectWithFormbricks = ({
)}>
{widgetSetupCompleted ? (
<div>
<p className="text-3xl">{t("environments.connect.congrats")}</p>
<p className="pt-4 text-sm font-medium text-slate-600">
{t("environments.connect.connection_successful_message")}
</p>
<p className="text-3xl">Congrats!</p>
<p className="pt-4 text-sm font-medium text-slate-600">Well done! We&apos;re connected.</p>
</div>
) : (
<div className="flex animate-pulse flex-col items-center space-y-4">
@@ -76,9 +73,7 @@ export const ConnectWithFormbricks = ({
<span className="animate-ping-slow absolute inline-flex h-full w-full rounded-full bg-slate-400 opacity-75"></span>
<span className="relative inline-flex h-10 w-10 rounded-full bg-slate-500"></span>
</span>
<p className="pt-4 text-sm font-medium text-slate-600">
{t("environments.connect.waiting_for_your_signal")}
</p>
<p className="pt-4 text-sm font-medium text-slate-600">Waiting for your signal...</p>
</div>
)}
</div>
@@ -88,9 +83,7 @@ export const ConnectWithFormbricks = ({
variant={widgetSetupCompleted ? "primary" : "minimal"}
onClick={handleFinishOnboarding}
EndIcon={ArrowRight}>
{widgetSetupCompleted
? t("environments.connect.finish_onboarding")
: t("environments.connect.i_dont_know_how_to_do_it")}
{widgetSetupCompleted ? "Finish Onboarding" : "I don't know how to do it"}
</Button>
</div>
);

View File

@@ -2,7 +2,6 @@
import { inviteOrganizationMemberAction } from "@/app/(app)/(onboarding)/organizations/actions";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { FormProvider, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
@@ -25,11 +24,11 @@ type TInviteOrganizationMemberDetails = z.infer<typeof ZInviteOrganizationMember
export const InviteOrganizationMember = ({ organization, environmentId }: InviteOrganizationMemberProps) => {
const router = useRouter();
const t = useTranslations();
const form = useForm<TInviteOrganizationMemberDetails>({
defaultValues: {
email: "",
inviteMessage: t("environments.connect.invite.invite_message_content"),
inviteMessage: "I'm looking into Formbricks to run targeted surveys. Can you help me set it up? 🙏",
},
resolver: zodResolver(ZInviteOrganizationMemberDetails),
});
@@ -40,7 +39,7 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
await inviteOrganizationMemberAction({
organizationId: organization.id,
email: data.email,
role: "member",
role: "developer",
inviteMessage: data.inviteMessage,
});
toast.success("Invite sent successful");
@@ -64,7 +63,7 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
name="email"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<FormLabel>{t("common.email")}</FormLabel>
<FormLabel>Email</FormLabel>
<FormControl>
<div>
<Input
@@ -84,7 +83,7 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
name="inviteMessage"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<FormLabel>{t("environments.connect.invite.invite_message")}</FormLabel>
<FormLabel>Invite Message</FormLabel>
<FormControl>
<div>
<textarea
@@ -109,10 +108,10 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
e.preventDefault();
finishOnboarding();
}}>
{t("common.not_now")}
Not now
</Button>
<Button id="onboarding-inapp-invite-send-invite" type={"submit"} loading={isSubmitting}>
{t("common.invite")}
Invite
</Button>
</div>
</div>

View File

@@ -1,6 +1,5 @@
"use client";
import { useTranslations } from "next-intl";
import "prismjs/themes/prism.css";
import { useState } from "react";
import toast from "react-hot-toast";
@@ -28,7 +27,6 @@ export const OnboardingSetupInstructions = ({
channel,
widgetSetupCompleted,
}: OnboardingSetupInstructionsProps) => {
const t = useTranslations();
const [activeTab, setActiveTab] = useState(tabs[0].id);
const htmlSnippetForAppSurveys = `<!-- START Formbricks Surveys -->
<script type="text/javascript">
@@ -105,12 +103,12 @@ export const OnboardingSetupInstructions = ({
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="sh">
npm install @formbricks/js
</CodeBlock>
<p>{t("common.or")}</p>
<p>or</p>
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="sh">
yarn add @formbricks/js
</CodeBlock>
<p className="text-sm text-slate-700">
{t("environments.connect.import_formbricks_and_initialize_the_widget_in_your_component")}
Import Formbricks and initialize the widget in your Component (e.g. App.tsx):
</p>
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
{channel === "app" ? npmSnippetForAppSurveys : npmSnippetForWebsiteSurveys}
@@ -121,13 +119,13 @@ export const OnboardingSetupInstructions = ({
variant="secondary"
href={`https://formbricks.com/docs/${channel}-surveys/framework-guides`}
target="_blank">
{t("common.read_docs")}
Read docs
</Button>
</div>
) : activeTab === "html" ? (
<div className="prose prose-slate">
<p className="-mb-1 mt-6 text-sm text-slate-700">
{t("environments.connect.insert_this_code_into_the_head_tag_of_your_website")}
Insert this code into the &lt;head&gt; tag of your website:
</p>
<div>
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
@@ -143,16 +141,16 @@ export const OnboardingSetupInstructions = ({
navigator.clipboard.writeText(
channel === "app" ? htmlSnippetForAppSurveys : htmlSnippetForWebsiteSurveys
);
toast.success(t("common.copied_to_clipboard"));
toast.success("Copied to clipboard");
}}>
{t("common.copy_code")}
Copy code
</Button>
<Button
id="onboarding-inapp-connect-step-by-step-manual"
variant="secondary"
href={`https://formbricks.com/docs/${channel}-surveys/framework-guides#html`}
target="_blank">
{t("common.step_by_step_manual")}
Step by step manual
</Button>
</div>
</div>

View File

@@ -1,7 +1,6 @@
import { InviteOrganizationMember } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/InviteOrganizationMember";
import { XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { notFound, redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
@@ -16,7 +15,6 @@ interface InvitePageProps {
}
const Page = async ({ params }: InvitePageProps) => {
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
@@ -27,15 +25,15 @@ const Page = async ({ params }: InvitePageProps) => {
}
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
if (!membership || (membership.role !== "owner" && membership.role !== "manager")) {
if (!membership || (membership.role !== "owner" && membership.role !== "admin")) {
return notFound();
}
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center">
<Header
title={t("environments.connect.invite.headline")}
subtitle={t("environments.connect.invite.subtitle")}
title="Who is your favorite engineer?"
subtitle="Invite your tech-savvy co-worker to help with the setup."
/>
<div className="space-y-4 text-center">
<p className="text-4xl font-medium text-slate-800"></p>

View File

@@ -1,6 +1,5 @@
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
import { XIcon } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
@@ -14,23 +13,22 @@ interface ConnectPageProps {
}
const Page = async ({ params }: ConnectPageProps) => {
const t = await getTranslations();
const environment = await getEnvironment(params.environmentId);
if (!environment) {
throw new Error(t("common.environment_not_found"));
throw new Error("Environment not found");
}
const product = await getProductByEnvironmentId(environment.id);
if (!product) {
throw new Error(t("common.product_not_found"));
throw new Error("Product not found");
}
const channel = product.config.channel || null;
return (
<div className="flex min-h-full flex-col items-center justify-center py-10">
<Header title={t("environments.connect.headline")} subtitle={t("environments.connect.subtitle")} />
<Header title={`Let's connect your product with Formbricks`} subtitle="It takes less than 4 minutes." />
<div className="space-y-4 text-center">
<p className="text-4xl font-medium text-slate-800"></p>
<p className="text-sm text-slate-500"></p>

View File

@@ -1,19 +1,18 @@
"use client";
import { replacePresetPlaceholders } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils";
import { getXMTemplates } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates";
import { XMTemplates } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates";
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { createSurveyAction } from "@/modules/surveys/components/TemplateList/actions";
import { ActivityIcon, ShoppingCartIcon, UsersIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { ActivityIcon, ShoppingCartIcon, SmileIcon, StarIcon, ThumbsUpIcon, UsersIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
import { TProduct } from "@formbricks/types/product";
import { TSurveyCreateInput } from "@formbricks/types/surveys/types";
import { TXMTemplate } from "@formbricks/types/templates";
import { TUser } from "@formbricks/types/user";
import { createSurveyAction } from "@formbricks/ui/components/TemplateList/actions";
interface XMTemplateListProps {
product: TProduct;
@@ -23,7 +22,7 @@ interface XMTemplateListProps {
export const XMTemplateList = ({ product, user, environmentId }: XMTemplateListProps) => {
const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null);
const t = useTranslations();
const router = useRouter();
const createSurvey = async (activeTemplate: TXMTemplate) => {
@@ -47,50 +46,50 @@ export const XMTemplateList = ({ product, user, environmentId }: XMTemplateListP
const handleTemplateClick = (templateIdx) => {
setActiveTemplateId(templateIdx);
const template = getXMTemplates(user.locale)[templateIdx];
const template = XMTemplates[templateIdx];
const newTemplate = replacePresetPlaceholders(template, product);
createSurvey(newTemplate);
};
const XMTemplateOptions = [
{
title: t("environments.xm-templates.nps"),
description: t("environments.xm-templates.nps_description"),
title: "NPS",
description: "Implement proven best practices to understand WHY people buy.",
icon: ShoppingCartIcon,
onClick: () => handleTemplateClick(0),
isLoading: activeTemplateId === 0,
},
/* {
title: t("environments.xm-templates.five_star_rating"),
description: t("environments.xm-templates.five_star_rating_description"),
{
title: "5-Star Rating",
description: "Universal feedback solution to gauge overall satisfaction.",
icon: StarIcon,
onClick: () => handleTemplateClick(1),
isLoading: activeTemplateId === 1,
},
{
title: t("environments.xm-templates.csat"),
description: t("environments.xm-templates.csat_description"),
title: "CSAT",
description: "Implement best practices to measure customer satisfaction.",
icon: ThumbsUpIcon,
onClick: () => handleTemplateClick(2),
isLoading: activeTemplateId === 2,
}, */
},
{
title: t("environments.xm-templates.ces"),
description: t("environments.xm-templates.ces_description"),
title: "CES",
description: "Leverage every touchpoint to understand ease of customer interaction.",
icon: ActivityIcon,
onClick: () => handleTemplateClick(3),
isLoading: activeTemplateId === 3,
},
/* {
title: t("environments.xm-templates.smileys"),
description: t("environments.xm-templates.smileys_description"),
{
title: "Smileys",
description: "Use visual indicators to capture feedback across customer touchpoints.",
icon: SmileIcon,
onClick: () => handleTemplateClick(4),
isLoading: activeTemplateId === 4,
}, */
},
{
title: t("environments.xm-templates.enps"),
description: t("environments.xm-templates.enps_description"),
title: "eNPS",
description: "Universal feedback to understand employee engagement and satisfaction.",
icon: UsersIcon,
onClick: () => handleTemplateClick(5),
isLoading: activeTemplateId === 5,

View File

@@ -1,61 +1,42 @@
import { createId } from "@paralleldrive/cuid2";
import { getDefaultEndingCard, translate } from "@formbricks/lib/templates";
import { getDefaultEndingCard } from "@formbricks/lib/templates";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { TXMTemplate } from "@formbricks/types/templates";
function validateLocale(locale: string): boolean {
// Add logic to validate the locale, e.g., check against a list of supported locales
return typeof locale === "string" && locale.length > 0;
}
function logError(error: Error, context: string) {
console.error(`Error in ${context}:`, error);
}
export const getXMSurveyDefault = (locale: string): TXMTemplate => {
try {
if (!validateLocale(locale)) {
throw new Error("Invalid locale");
}
return {
name: "",
endings: [getDefaultEndingCard([], locale)],
questions: [],
styling: {
overwriteThemeStyling: true,
},
};
} catch (error) {
logError(error, "getXMSurveyDefault");
throw error; // Re-throw after logging
}
export const XMSurveyDefault: TXMTemplate = {
name: "",
endings: [getDefaultEndingCard([])],
questions: [],
styling: {
overwriteThemeStyling: true,
},
};
const NPSSurvey = (locale: string): TXMTemplate => {
const NPSSurvey = (): TXMTemplate => {
return {
...getXMSurveyDefault(locale),
name: translate("nps_survey_name", locale),
...XMSurveyDefault,
name: "NPS Survey",
questions: [
{
id: createId(),
type: TSurveyQuestionTypeEnum.NPS,
headline: { default: translate("nps_survey_question_1_headline", locale) },
headline: { default: "How likely are you to recommend {{productName}} to a friend or colleague?" },
required: true,
lowerLabel: { default: translate("nps_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("nps_survey_question_1_upper_label", locale) },
lowerLabel: { default: "Not at all likely" },
upperLabel: { default: "Extremely likely" },
isColorCodingEnabled: true,
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("nps_survey_question_2_headline", locale) },
headline: { default: "To help us improve, can you describe the reason(s) for your rating?" },
required: false,
inputType: "text",
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("nps_survey_question_3_headline", locale) },
headline: { default: "Any other comments, feedback, or concerns?" },
required: false,
inputType: "text",
},
@@ -63,12 +44,12 @@ const NPSSurvey = (locale: string): TXMTemplate => {
};
};
const StarRatingSurvey = (locale: string): TXMTemplate => {
const StarRatingSurvey = (): TXMTemplate => {
const reusableQuestionIds = [createId(), createId(), createId()];
return {
...getXMSurveyDefault(locale),
name: translate("star_rating_survey_name", locale),
...XMSurveyDefault,
name: "{{productName}}'s Rating Survey",
questions: [
{
id: reusableQuestionIds[0],
@@ -105,15 +86,15 @@ const StarRatingSurvey = (locale: string): TXMTemplate => {
],
range: 5,
scale: "number",
headline: { default: translate("star_rating_survey_question_1_headline", locale) },
headline: { default: "How do you like {{productName}}?" },
required: true,
lowerLabel: { default: translate("star_rating_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("star_rating_survey_question_1_upper_label", locale) },
lowerLabel: { default: "Extremely dissatisfied" },
upperLabel: { default: "Extremely satisfied" },
isColorCodingEnabled: false,
},
{
id: reusableQuestionIds[1],
html: { default: translate("star_rating_survey_question_2_html", locale) },
html: { default: '<p class="fb-editor-paragraph" dir="ltr"><span>This helps us a lot.</span></p>' },
type: TSurveyQuestionTypeEnum.CTA,
logic: [
{
@@ -136,15 +117,15 @@ const StarRatingSurvey = (locale: string): TXMTemplate => {
{
id: createId(),
objective: "jumpToQuestion",
target: getXMSurveyDefault(locale).endings[0].id,
target: XMSurveyDefault.endings[0].id,
},
],
},
],
headline: { default: translate("star_rating_survey_question_2_headline", locale) },
headline: { default: "Happy to hear 🙏 Please write a review for us!" },
required: true,
buttonUrl: "https://formbricks.com/github",
buttonLabel: { default: translate("star_rating_survey_question_2_button_label", locale) },
buttonLabel: { default: "Write review" },
buttonExternal: true,
},
{
@@ -161,12 +142,12 @@ const StarRatingSurvey = (locale: string): TXMTemplate => {
};
};
const CSATSurvey = (locale: string): TXMTemplate => {
const CSATSurvey = (): TXMTemplate => {
const reusableQuestionIds = [createId(), createId(), createId()];
return {
...getXMSurveyDefault(locale),
name: translate("csat_survey_name", locale),
...XMSurveyDefault,
name: "{{productName}} CSAT",
questions: [
{
id: reusableQuestionIds[0],
@@ -203,10 +184,10 @@ const CSATSurvey = (locale: string): TXMTemplate => {
],
range: 5,
scale: "smiley",
headline: { default: translate("csat_survey_question_1_headline", locale) },
headline: { default: "How satisfied are you with your {{productName}} experience?" },
required: true,
lowerLabel: { default: translate("csat_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("csat_survey_question_1_upper_label", locale) },
lowerLabel: { default: "Extremely dissatisfied" },
upperLabel: { default: "Extremely satisfied" },
isColorCodingEnabled: false,
},
{
@@ -233,62 +214,62 @@ const CSATSurvey = (locale: string): TXMTemplate => {
{
id: createId(),
objective: "jumpToQuestion",
target: getXMSurveyDefault(locale).endings[0].id,
target: XMSurveyDefault.endings[0].id,
},
],
},
],
headline: { default: translate("csat_survey_question_2_headline", locale) },
headline: { default: "Lovely! Is there anything we can do to improve your experience?" },
required: false,
placeholder: { default: translate("csat_survey_question_2_placeholder", locale) },
placeholder: { default: "Type your answer here..." },
inputType: "text",
},
{
id: reusableQuestionIds[2],
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("csat_survey_question_3_headline", locale) },
headline: { default: "Ugh, sorry! Is there anything we can do to improve your experience?" },
required: false,
placeholder: { default: translate("csat_survey_question_3_placeholder", locale) },
placeholder: { default: "Type your answer here..." },
inputType: "text",
},
],
};
};
const CESSurvey = (locale: string): TXMTemplate => {
const CESSurvey = (): TXMTemplate => {
return {
...getXMSurveyDefault(locale),
name: translate("cess_survey_name", locale),
...XMSurveyDefault,
name: "CES Survey",
questions: [
{
id: createId(),
type: TSurveyQuestionTypeEnum.Rating,
range: 5,
scale: "number",
headline: { default: translate("cess_survey_question_1_headline", locale) },
headline: { default: "{{productName}} makes it easy for me to [ADD GOAL]" },
required: true,
lowerLabel: { default: translate("cess_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("cess_survey_question_1_upper_label", locale) },
lowerLabel: { default: "Disagree strongly" },
upperLabel: { default: "Agree strongly" },
isColorCodingEnabled: false,
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("cess_survey_question_2_headline", locale) },
headline: { default: "Thanks! How could we make it easier for you to [ADD GOAL]?" },
required: true,
placeholder: { default: translate("cess_survey_question_2_placeholder", locale) },
placeholder: { default: "Type your answer here..." },
inputType: "text",
},
],
};
};
const SmileysRatingSurvey = (locale: string): TXMTemplate => {
const SmileysRatingSurvey = (): TXMTemplate => {
const reusableQuestionIds = [createId(), createId(), createId()];
return {
...getXMSurveyDefault(locale),
name: translate("smileys_survey_name", locale),
...XMSurveyDefault,
name: "Smileys Survey",
questions: [
{
id: reusableQuestionIds[0],
@@ -325,15 +306,15 @@ const SmileysRatingSurvey = (locale: string): TXMTemplate => {
],
range: 5,
scale: "smiley",
headline: { default: translate("smileys_survey_question_1_headline", locale) },
headline: { default: "How do you like {{productName}}?" },
required: true,
lowerLabel: { default: translate("smileys_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("smileys_survey_question_1_upper_label", locale) },
lowerLabel: { default: "Not good" },
upperLabel: { default: "Very satisfied" },
isColorCodingEnabled: false,
},
{
id: reusableQuestionIds[1],
html: { default: translate("smileys_survey_question_2_html", locale) },
html: { default: '<p class="fb-editor-paragraph" dir="ltr"><span>This helps us a lot.</span></p>' },
type: TSurveyQuestionTypeEnum.CTA,
logic: [
{
@@ -356,58 +337,58 @@ const SmileysRatingSurvey = (locale: string): TXMTemplate => {
{
id: createId(),
objective: "jumpToQuestion",
target: getXMSurveyDefault(locale).endings[0].id,
target: XMSurveyDefault.endings[0].id,
},
],
},
],
headline: { default: translate("smileys_survey_question_2_headline", locale) },
headline: { default: "Happy to hear 🙏 Please write a review for us!" },
required: true,
buttonUrl: "https://formbricks.com/github",
buttonLabel: { default: translate("smileys_survey_question_2_button_label", locale) },
buttonLabel: { default: "Write review" },
buttonExternal: true,
},
{
id: reusableQuestionIds[2],
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("smileys_survey_question_3_headline", locale) },
headline: { default: "Sorry to hear! What is ONE thing we can do better?" },
required: true,
subheader: { default: translate("smileys_survey_question_3_subheader", locale) },
buttonLabel: { default: translate("smileys_survey_question_3_button_label", locale) },
placeholder: { default: translate("smileys_survey_question_3_placeholder", locale) },
subheader: { default: "Help us improve your experience." },
buttonLabel: { default: "Send" },
placeholder: { default: "Type your answer here..." },
inputType: "text",
},
],
};
};
const eNPSSurvey = (locale: string): TXMTemplate => {
const eNPSSurvey = (): TXMTemplate => {
return {
...getXMSurveyDefault(locale),
name: translate("enps_survey_name", locale),
...XMSurveyDefault,
name: "eNPS Survey",
questions: [
{
id: createId(),
type: TSurveyQuestionTypeEnum.NPS,
headline: {
default: translate("enps_survey_question_1_headline", locale),
default: "How likely are you to recommend working at this company to a friend or colleague?",
},
required: false,
lowerLabel: { default: translate("enps_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("enps_survey_question_1_upper_label", locale) },
lowerLabel: { default: "Not at all likely" },
upperLabel: { default: "Extremely likely" },
isColorCodingEnabled: true,
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("enps_survey_question_2_headline", locale) },
headline: { default: "To help us improve, can you describe the reason(s) for your rating?" },
required: false,
inputType: "text",
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("enps_survey_question_3_headline", locale) },
headline: { default: "Any other comments, feedback, or concerns?" },
required: false,
inputType: "text",
},
@@ -415,21 +396,11 @@ const eNPSSurvey = (locale: string): TXMTemplate => {
};
};
export const getXMTemplates = (locale: string): TXMTemplate[] => {
try {
if (!validateLocale(locale)) {
throw new Error("Invalid locale");
}
return [
NPSSurvey(locale),
StarRatingSurvey(locale),
CSATSurvey(locale),
CESSurvey(locale),
SmileysRatingSurvey(locale),
eNPSSurvey(locale),
];
} catch (error) {
logError(error, "getXMTemplates");
return []; // Return an empty array or handle as needed
}
};
export const XMTemplates: TXMTemplate[] = [
NPSSurvey(),
StarRatingSurvey(),
CSATSurvey(),
CESSurvey(),
SmileysRatingSurvey(),
eNPSSurvey(),
];

View File

@@ -1,11 +1,10 @@
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
import { XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProductByEnvironmentId, getUserProducts } from "@formbricks/lib/product/service";
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
import { getProductByEnvironmentId, getProducts } from "@formbricks/lib/product/service";
import { getUser } from "@formbricks/lib/user/service";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
@@ -19,31 +18,32 @@ interface XMTemplatePageProps {
const Page = async ({ params }: XMTemplatePageProps) => {
const session = await getServerSession(authOptions);
const environment = await getEnvironment(params.environmentId);
const t = await getTranslations();
if (!session) {
throw new Error(t("common.session_not_found"));
throw new Error("Session not found");
}
const user = await getUser(session.user.id);
if (!user) {
throw new Error(t("common.user_not_found"));
throw new Error("User not found");
}
if (!environment) {
throw new Error(t("common.environment_not_found"));
throw new Error("Environment not found");
}
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
const product = await getProductByEnvironmentId(environment.id);
if (!product) {
throw new Error(t("common.product_not_found"));
throw new Error("Product not found");
}
const products = await getUserProducts(session.user.id, organizationId);
const products = await getProducts(organizationId);
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header title={t("environments.xm-templates.headline")} />
<Header title="What kind of feedback would you like to get?" />
<XMTemplateList product={product} user={user} environmentId={environment.id} />
{products.length >= 2 && (
<Button

View File

@@ -1,48 +0,0 @@
"use server";
import { TOrganizationTeam } from "@/app/(app)/(onboarding)/types/onboarding";
import { teamCache } from "@/lib/cache/team";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { ZId } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
export const getTeamsByOrganizationId = reactCache(
(organizationId: string): Promise<TOrganizationTeam[] | null> =>
cache(
async () => {
validateInputs([organizationId, ZId]);
try {
const teams = await prisma.team.findMany({
where: {
organizationId,
},
select: {
id: true,
name: true,
},
});
const productTeams = teams.map((team) => ({
id: team.id,
name: team.name,
}));
return productTeams;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getTeamsByOrganizationId-${organizationId}`],
{
tags: [teamCache.tag.byOrganizationId(organizationId)],
}
)()
);

View File

@@ -3,10 +3,10 @@ import { TProductConfigChannel } from "@formbricks/types/product";
export const getCustomHeadline = (channel?: TProductConfigChannel) => {
switch (channel) {
case "website":
return "organizations.products.new.settings.website_channel_headline";
return "Let's get the most out of your website traffic!";
case "app":
return "organizations.products.new.settings.app_channel_headline";
return "Let's research what your users need!";
default:
return "organizations.products.new.settings.link_channel_headline";
return "You maintain a product, how exciting!";
}
};

View File

@@ -1,185 +0,0 @@
"use client";
import { formbricksLogout } from "@/app/lib/formbricks";
import FBLogo from "@/images/formbricks-wordmark.svg";
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
import { ArrowUpRightIcon, ChevronRightIcon, LogOutIcon, PlusIcon } from "lucide-react";
import { signOut } from "next-auth/react";
import { useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import { AiOutlineDiscord } from "react-icons/ai";
import { cn } from "@formbricks/lib/cn";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TOrganization } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user";
import { ProfileAvatar } from "@formbricks/ui/components/Avatars";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@formbricks/ui/components/DropdownMenu";
interface LandingSidebarProps {
isMultiOrgEnabled: boolean;
user: TUser;
organization: TOrganization;
organizations: TOrganization[];
}
export const LandingSidebar = ({
isMultiOrgEnabled,
user,
organization,
organizations,
}: LandingSidebarProps) => {
const [openCreateOrganizationModal, setOpenCreateOrganizationModal] = useState<boolean>(false);
const t = useTranslations();
const router = useRouter();
const handleEnvironmentChangeByOrganization = (organizationId: string) => {
router.push(`/organizations/${organizationId}/`);
};
const dropdownNavigation = [
{
label: t("common.documentation"),
href: "https://formbricks.com/docs",
target: "_blank",
icon: ArrowUpRightIcon,
},
{
label: t("common.join_discord"),
href: "https://formbricks.com/discord",
target: "_blank",
icon: AiOutlineDiscord,
},
];
const currentOrganizationId = organization?.id;
const currentOrganizationName = capitalizeFirstLetter(organization?.name);
const sortedOrganizations = useMemo(() => {
return [...organizations].sort((a, b) => a.name.localeCompare(b.name));
}, [organizations]);
return (
<aside
className={cn(
"w-sidebar-collapsed z-40 flex flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100"
)}>
<Image src={FBLogo} width={160} height={30} alt={t("environments.formbricks_logo")} />
<div className="flex items-center">
<DropdownMenu>
<DropdownMenuTrigger
asChild
id="userDropdownTrigger"
className="w-full rounded-br-xl border-t py-4 pl-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-none">
<div tabIndex={0} className={cn("flex cursor-pointer flex-row items-center space-x-3")}>
<ProfileAvatar userId={user.id} imageUrl={user.imageUrl} />
<>
<div>
<p
title={user?.email}
className={cn(
"ph-no-capture ph-no-capture -mb-0.5 max-w-28 truncate text-sm font-bold text-slate-700"
)}>
{user?.name ? <span>{user?.name}</span> : <span>{user?.email}</span>}
</p>
<p
title={capitalizeFirstLetter(organization?.name)}
className="max-w-28 truncate text-sm text-slate-500">
{capitalizeFirstLetter(organization?.name)}
</p>
</div>
<ChevronRightIcon className={cn("h-5 w-5 text-slate-700 hover:text-slate-500")} />
</>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
id="userDropdownInnerContentWrapper"
side="right"
sideOffset={10}
alignOffset={5}
align="end">
{/* Dropdown Items */}
{dropdownNavigation.map((link) => (
<Link href={link.href} target={link.target} className="flex w-full items-center">
<DropdownMenuItem>
<link.icon className="mr-2 h-4 w-4" strokeWidth={1.5} />
{link.label}
</DropdownMenuItem>
</Link>
))}
{/* Logout */}
<DropdownMenuItem
onClick={async () => {
await signOut({ callbackUrl: "/auth/login" });
await formbricksLogout();
}}
icon={<LogOutIcon className="h-4 w-4" strokeWidth={1.5} />}>
{t("common.logout")}
</DropdownMenuItem>
{/* Organization Switch */}
{(isMultiOrgEnabled || organizations.length > 1) && (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="rounded-lg">
<div>
<p>{currentOrganizationName}</p>
<p className="block text-xs text-slate-500">{t("common.switch_organization")}</p>
</div>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent sideOffset={10} alignOffset={5}>
<DropdownMenuRadioGroup
value={currentOrganizationId}
onValueChange={(organizationId) =>
handleEnvironmentChangeByOrganization(organizationId)
}>
{sortedOrganizations.map((organization) => (
<DropdownMenuRadioItem
value={organization.id}
className="cursor-pointer rounded-lg"
key={organization.id}>
{organization.name}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
{isMultiOrgEnabled && (
<DropdownMenuItem
onClick={() => setOpenCreateOrganizationModal(true)}
icon={<PlusIcon className="mr-2 h-4 w-4" />}>
<span>{t("common.create_new_organization")}</span>
</DropdownMenuItem>
)}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
<CreateOrganizationModal open={openCreateOrganizationModal} setOpen={setOpenCreateOrganizationModal} />
</aside>
);
};

View File

@@ -1,35 +0,0 @@
import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getUserProducts } from "@formbricks/lib/product/service";
const LandingLayout = async ({ children, params }) => {
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
}
const membership = await getMembershipByUserIdOrganizationId(session.user.id, params.organizationId);
if (!membership) {
return notFound();
}
const products = await getUserProducts(session.user.id, params.organizationId);
if (products.length !== 0) {
const firstProduct = products[0];
const environments = await getEnvironments(firstProduct.id);
const prodEnvironment = environments.find((e) => e.type === "production");
if (prodEnvironment) {
return redirect(`/environments/${prodEnvironment.id}/`);
}
}
return <>{children}</>;
};
export default LandingLayout;

View File

@@ -1,50 +0,0 @@
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { notFound, redirect } from "next/navigation";
import { getEnterpriseLicense } from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { getOrganization, getOrganizationsByUserId } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
import { Header } from "@formbricks/ui/components/Header";
const Page = async ({ params }) => {
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
}
const user = await getUser(session.user.id);
if (!user) return notFound();
const organization = await getOrganization(params.organizationId);
if (!organization) return notFound();
const organizations = await getOrganizationsByUserId(session.user.id);
const { features } = await getEnterpriseLicense();
const isMultiOrgEnabled = features?.isMultiOrgEnabled ?? false;
return (
<div className="flex min-h-full min-w-full flex-row">
<LandingSidebar
user={user}
organization={organization}
isMultiOrgEnabled={isMultiOrgEnabled}
organizations={organizations}
/>
<div className="flex-1">
<div className="flex h-full flex-col items-center justify-center space-y-12">
<Header
title={t("organizations.landing.no_products_warning_title")}
subtitle={t("organizations.landing.no_products_warning_subtitle")}
/>
</div>
</div>
</div>
);
};
export default Page;

View File

@@ -1,8 +1,8 @@
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { notFound, redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
@@ -10,7 +10,6 @@ import { AuthorizationError } from "@formbricks/types/errors";
import { ToasterClient } from "@formbricks/ui/components/ToasterClient";
const ProductOnboardingLayout = async ({ children, params }) => {
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
@@ -18,7 +17,7 @@ const ProductOnboardingLayout = async ({ children, params }) => {
const user = await getUser(session.user.id);
if (!user) {
throw new Error(t("common.user_not_found"));
throw new Error("User not found");
}
const isAuthorized = await canUserAccessOrganization(session.user.id, params.organizationId);
@@ -26,9 +25,12 @@ const ProductOnboardingLayout = async ({ children, params }) => {
throw AuthorizationError;
}
const membership = await getMembershipByUserIdOrganizationId(session.user.id, params.organizationId);
if (!membership || membership.role === "viewer") return notFound();
const organization = await getOrganization(params.organizationId);
if (!organization) {
throw new Error(t("common.organization_not_found"));
throw new Error("Organization not found");
}
return (

View File

@@ -1,20 +0,0 @@
import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
const OnboardingLayout = async ({ children, params }) => {
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
}
const membership = await getMembershipByUserIdOrganizationId(session.user.id, params.organizationId);
const { isMember, isBilling } = getAccessFlags(membership?.role);
if (isMember || isBilling) return notFound();
return <>{children}</>;
};
export default OnboardingLayout;

View File

@@ -1,10 +1,6 @@
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { GlobeIcon, GlobeLockIcon, LinkIcon, XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { getUserProducts } from "@formbricks/lib/product/service";
import { getProducts } from "@formbricks/lib/product/service";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
@@ -15,44 +11,38 @@ interface ChannelPageProps {
}
const Page = async ({ params }: ChannelPageProps) => {
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
}
const t = await getTranslations();
const channelOptions = [
{
title: t("organizations.products.new.channel.public_website"),
description: t("organizations.products.new.channel.public_website_description"),
title: "Public website",
description: "Run well-timed pop-up surveys.",
icon: GlobeIcon,
iconText: t("organizations.products.new.channel.public_website_icon_text"),
iconText: "Built for scale",
href: `/organizations/${params.organizationId}/products/new/settings?channel=website`,
},
{
title: t("organizations.products.new.channel.app_with_sign_up"),
description: t("organizations.products.new.channel.app_with_sign_up_description"),
title: "App with sign up",
description: "Run highly-targeted micro-surveys.",
icon: GlobeLockIcon,
iconText: t("organizations.products.new.channel.app_with_sign_up_icon_text"),
iconText: "Enrich user profiles",
href: `/organizations/${params.organizationId}/products/new/settings?channel=app`,
},
{
channel: "link",
title: t("organizations.products.new.channel.link_and_email_surveys"),
description: t("organizations.products.new.channel.link_and_email_surveys_description"),
title: "Link & email surveys",
description: "Reach people anywhere online.",
icon: LinkIcon,
iconText: t("organizations.products.new.channel.link_and_email_surveys_icon_text"),
iconText: "Anywhere online",
href: `/organizations/${params.organizationId}/products/new/settings?channel=link`,
},
];
const products = await getUserProducts(session.user.id, params.organizationId);
const products = await getProducts(params.organizationId);
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header
title={t("organizations.products.new.channel.channel_select_title")}
subtitle={t("organizations.products.new.channel.channel_select_subtitle")}
title="Where do you mainly want to survey people?"
subtitle="Run surveys on public websites, in your app, or with shareable links & emails."
/>
<OnboardingOptionsContainer options={channelOptions} />
{products.length >= 1 && (

View File

@@ -0,0 +1,69 @@
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { HeartIcon, MonitorIcon, ShoppingCart, XIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { getProducts } from "@formbricks/lib/product/service";
import { TProductConfigChannel } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface IndustryPageProps {
params: {
organizationId: string;
};
searchParams: {
channel?: TProductConfigChannel;
};
}
const Page = async ({ params, searchParams }: IndustryPageProps) => {
const channel = searchParams.channel;
if (!channel) {
return notFound();
}
const products = await getProducts(params.organizationId);
const industryOptions = [
{
title: "E-Commerce",
description: "Understand why people buy.",
icon: ShoppingCart,
iconText: "B2B and B2C",
href: `/organizations/${params.organizationId}/products/new/settings?channel=${channel}&industry=eCommerce`,
},
{
title: "SaaS",
description: "Improve product-market fit.",
icon: MonitorIcon,
iconText: "Proven methods",
href: `/organizations/${params.organizationId}/products/new/settings?channel=${channel}&industry=saas`,
},
{
title: "Other",
description: "Listen to your customers.",
icon: HeartIcon,
iconText: "Customer insights",
href: `/organizations/${params.organizationId}/products/new/settings?channel=${channel}&industry=other`,
},
];
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header
title="Which industry do you work for?"
subtitle="Get started with battle-tested best practices."
/>
<OnboardingOptionsContainer options={industryOptions} />
{products.length >= 1 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="minimal"
href={"/"}>
<XIcon className="h-7 w-7" strokeWidth={1.5} />
</Button>
)}
</div>
);
};
export default Page;

View File

@@ -1,10 +1,6 @@
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { getUserProducts } from "@formbricks/lib/product/service";
import { getProducts } from "@formbricks/lib/product/service";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
@@ -15,32 +11,26 @@ interface ModePageProps {
}
const Page = async ({ params }: ModePageProps) => {
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
}
const t = await getTranslations();
const channelOptions = [
{
title: t("organizations.products.new.mode.formbricks_surveys"),
description: t("organizations.products.new.mode.formbricks_surveys_description"),
title: "Formbricks Surveys",
description: "Multi-purpose survey platform for web, app and email surveys.",
icon: ListTodoIcon,
href: `/organizations/${params.organizationId}/products/new/channel`,
},
{
title: t("organizations.products.new.mode.formbricks_cx"),
description: t("organizations.products.new.mode.formbricks_cx_description"),
title: "Formbricks CX",
description: "Surveys and reports to understand what your customers need.",
icon: HeartIcon,
href: `/organizations/${params.organizationId}/products/new/settings?mode=cx`,
},
];
const products = await getUserProducts(session.user.id, params.organizationId);
const products = await getProducts(params.organizationId);
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header title={t("organizations.products.new.mode.what_are_you_here_for")} />
<Header title="What are you here for?" />
<OnboardingOptionsContainer options={channelOptions} />
{products.length >= 1 && (
<Button

View File

@@ -1,18 +1,14 @@
"use client";
import { createProductAction } from "@/app/(app)/environments/[environmentId]/actions";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { TOrganizationTeam } from "@/modules/ee/teams/product-teams/types/teams";
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
import { getPreviewSurvey } from "@formbricks/lib/styling/constants";
import { PREVIEW_SURVEY } from "@formbricks/lib/styling/constants";
import {
TProductConfigChannel,
TProductConfigIndustry,
@@ -32,7 +28,6 @@ import {
FormProvider,
} from "@formbricks/ui/components/Form";
import { Input } from "@formbricks/ui/components/Input";
import { MultiSelect } from "@formbricks/ui/components/MultiSelect";
import { SurveyInline } from "@formbricks/ui/components/Survey";
interface ProductSettingsProps {
@@ -41,9 +36,6 @@ interface ProductSettingsProps {
channel: TProductConfigChannel;
industry: TProductConfigIndustry;
defaultBrandColor: string;
organizationTeams: TOrganizationTeam[];
canDoRoleManagement: boolean;
locale: string;
}
export const ProductSettings = ({
@@ -52,14 +44,9 @@ export const ProductSettings = ({
channel,
industry,
defaultBrandColor,
organizationTeams,
canDoRoleManagement = false,
locale,
}: ProductSettingsProps) => {
const [createTeamModalOpen, setCreateTeamModalOpen] = useState(false);
const router = useRouter();
const t = useTranslations();
const addProduct = async (data: TProductUpdateInput) => {
try {
const createProductResponse = await createProductAction({
@@ -67,7 +54,6 @@ export const ProductSettings = ({
data: {
...data,
config: { channel, industry },
teamIds: data.teamIds,
},
});
@@ -103,7 +89,6 @@ export const ProductSettings = ({
defaultValues: {
name: "",
styling: { allowStyleOverwrite: true, brandColor: { light: defaultBrandColor } },
teamIds: [],
},
resolver: zodResolver(ZProductUpdateInput),
});
@@ -111,11 +96,6 @@ export const ProductSettings = ({
const brandColor = form.watch("styling.brandColor.light") ?? defaultBrandColor;
const { isSubmitting } = form.formState;
const organizationTeamsOptions = organizationTeams.map((team) => ({
label: team.name,
value: team.id,
}));
return (
<div className="mt-6 flex w-5/6 space-x-10 lg:w-2/3 2xl:w-1/2">
<div className="flex w-1/2 flex-col space-y-4">
@@ -127,10 +107,8 @@ export const ProductSettings = ({
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<div>
<FormLabel>{t("organizations.products.new.settings.brand_color")}</FormLabel>
<FormDescription>
{t("organizations.products.new.settings.brand_color_description")}
</FormDescription>
<FormLabel>Brand color</FormLabel>
<FormDescription>Match the main color of surveys with your brand.</FormDescription>
</div>
<FormControl>
<div>
@@ -151,10 +129,8 @@ export const ProductSettings = ({
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<div>
<FormLabel>{t("organizations.products.new.settings.product_name")}</FormLabel>
<FormDescription>
{t("organizations.products.new.settings.product_name_description")}
</FormDescription>
<FormLabel>Product name</FormLabel>
<FormDescription>What is your product called?</FormDescription>
</div>
<FormControl>
<div>
@@ -172,42 +148,9 @@ export const ProductSettings = ({
)}
/>
{canDoRoleManagement && (
<FormField
control={form.control}
name="teamIds"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<div className="flex items-center justify-between">
<div>
<FormLabel>Teams</FormLabel>
<FormDescription>Who all can access this product?</FormDescription>
</div>
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setCreateTeamModalOpen(true)}>
{t("organizations.products.new.settings.create_new_team")}
</Button>
</div>
<FormControl>
<div>
<MultiSelect
value={field.value}
onChange={(teamIds) => field.onChange(teamIds)}
options={organizationTeamsOptions}
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
)}
<div className="flex w-full justify-end">
<Button loading={isSubmitting} type="submit" id="form-next-button">
{t("common.next")}
<Button loading={isSubmitting} type="submit">
Next
</Button>
</div>
</form>
@@ -224,10 +167,10 @@ export const ProductSettings = ({
className="absolute left-2 top-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
/>
)}
<p className="text-sm text-slate-400">{t("common.preview")}</p>
<div className="z-0 h-3/4 w-3/4">
<p className="text-sm text-slate-400">Preview</p>
<div className="h-3/4 w-3/4">
<SurveyInline
survey={getPreviewSurvey(locale)}
survey={PREVIEW_SURVEY}
styling={{ brandColor: { light: brandColor } }}
isBrandingEnabled={false}
languageCode="default"
@@ -236,14 +179,6 @@ export const ProductSettings = ({
/>
</div>
</div>
<CreateTeamModal
open={createTeamModalOpen}
setOpen={setCreateTeamModalOpen}
organizationId={organizationId}
onCreate={(teamId) => {
form.setValue("teamIds", [...(form.getValues("teamIds") || []), teamId]);
}}
/>
</div>
);
};

View File

@@ -1,16 +1,8 @@
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils";
import { ProductSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/components/ProductSettings";
import { XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { getRoleManagementPermission } from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { DEFAULT_BRAND_COLOR, DEFAULT_LOCALE } from "@formbricks/lib/constants";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getUserProducts } from "@formbricks/lib/product/service";
import { getUserLocale } from "@formbricks/lib/user/service";
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
import { getProducts } from "@formbricks/lib/product/service";
import { TProductConfigChannel, TProductConfigIndustry, TProductMode } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
@@ -27,45 +19,24 @@ interface ProductSettingsPageProps {
}
const Page = async ({ params, searchParams }: ProductSettingsPageProps) => {
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
}
const channel = searchParams.channel || null;
const industry = searchParams.industry || null;
const mode = searchParams.mode || "surveys";
const locale = session?.user.id ? await getUserLocale(session.user.id) : undefined;
const customHeadline = getCustomHeadline(channel);
const products = await getUserProducts(session.user.id, params.organizationId);
const organizationTeams = await getTeamsByOrganizationId(params.organizationId);
const organization = await getOrganization(params.organizationId);
if (!organization) {
throw new Error(t("common.organization_not_found"));
}
const canDoRoleManagement = await getRoleManagementPermission(organization);
if (!organizationTeams) {
throw new Error(t("common.organization_teams_not_found"));
}
const products = await getProducts(params.organizationId);
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
{channel === "link" || mode === "cx" ? (
<Header
title={t("organizations.products.new.settings.channel_settings_title")}
subtitle={t("organizations.products.new.settings.channel_settings_subtitle")}
title="Match your brand, get 2x more responses."
subtitle="When people recognize your brand, they are much more likely to start and complete responses."
/>
) : (
<Header
title={t(customHeadline)}
subtitle={t("organizations.products.new.settings.channel_settings_description")}
title={customHeadline}
subtitle="Get 2x more responses matching surveys with your brand and UI"
/>
)}
<ProductSettings
@@ -74,9 +45,6 @@ const Page = async ({ params, searchParams }: ProductSettingsPageProps) => {
channel={channel}
industry={industry}
defaultBrandColor={DEFAULT_BRAND_COLOR}
organizationTeams={organizationTeams}
canDoRoleManagement={canDoRoleManagement}
locale={locale ?? DEFAULT_LOCALE}
/>
{products.length >= 1 && (
<Button

View File

@@ -1,19 +1,19 @@
"use server";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { sendInviteMemberEmail } from "@/modules/email";
import { z } from "zod";
import { sendInviteMemberEmail } from "@formbricks/email";
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 { ZId } from "@formbricks/types/common";
import { AuthenticationError } from "@formbricks/types/errors";
import { ZOrganizationRole } from "@formbricks/types/memberships";
import { ZMembershipRole } from "@formbricks/types/memberships";
const ZInviteOrganizationMemberAction = z.object({
organizationId: ZId,
email: z.string(),
role: ZOrganizationRole,
role: ZMembershipRole,
inviteMessage: z.string(),
});
@@ -24,15 +24,10 @@ export const inviteOrganizationMemberAction = authenticatedActionClient
throw new AuthenticationError("Invite disabled");
}
await checkAuthorizationUpdated({
await checkAuthorization({
userId: ctx.user.id,
organizationId: parsedInput.organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
],
rules: ["membership", "create"],
});
const invite = await inviteUser({
@@ -51,8 +46,7 @@ export const inviteOrganizationMemberAction = authenticatedActionClient
ctx.user.name ?? "",
"",
true, // is onboarding invite
parsedInput.inviteMessage,
ctx.user.locale
parsedInput.inviteMessage
);
}

View File

@@ -1,8 +0,0 @@
import { z } from "zod";
export const ZOrganizationTeam = z.object({
id: z.string().cuid2(),
name: z.string(),
});
export type TOrganizationTeam = z.infer<typeof ZOrganizationTeam>;

View File

@@ -2,7 +2,6 @@ import { FormbricksClient } from "@/app/(app)/components/FormbricksClient";
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
@@ -14,7 +13,6 @@ import { DevEnvironmentBanner } from "@formbricks/ui/components/DevEnvironmentBa
import { ToasterClient } from "@formbricks/ui/components/ToasterClient";
const SurveyEditorEnvironmentLayout = async ({ children, params }) => {
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
@@ -22,23 +20,23 @@ const SurveyEditorEnvironmentLayout = async ({ children, params }) => {
const user = await getUser(session.user.id);
if (!user) {
throw new Error(t("common.user_not_found"));
throw new Error("User not found");
}
const hasAccess = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
if (!hasAccess) {
throw new AuthorizationError(t("common.not_authorized"));
throw new AuthorizationError("Not authorized");
}
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!organization) {
throw new Error(t("common.organization_not_found"));
throw new Error("Organization not found");
}
const environment = await getEnvironment(params.environmentId);
if (!environment) {
throw new Error(t("common.environment_not_found"));
throw new Error("Environment not found");
}
return (

View File

@@ -1,20 +1,16 @@
"use server";
import { actionClient, authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { z } from "zod";
import { createActionClass } from "@formbricks/lib/actionClass/service";
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,
getProductIdFromEnvironmentId,
getProductIdFromSegmentId,
getProductIdFromSurveyId,
} from "@/lib/utils/helper";
import { getSegment, getSurvey } from "@/lib/utils/services";
import { z } from "zod";
import { createActionClass } from "@formbricks/lib/actionClass/service";
import { UNSPLASH_ACCESS_KEY, UNSPLASH_ALLOWED_DOMAINS } from "@formbricks/lib/constants";
} from "@formbricks/lib/organization/utils";
import { getProduct } from "@formbricks/lib/product/service";
import {
cloneSegment,
@@ -32,22 +28,11 @@ import { ZSurvey } from "@formbricks/types/surveys/types";
export const updateSurveyAction = authenticatedActionClient
.schema(ZSurvey)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSurveyId(parsedInput.id),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "productTeam",
productId: await getProductIdFromSurveyId(parsedInput.id),
minPermission: "readWrite",
},
],
rules: ["survey", "update"],
});
return await updateSurvey(parsedInput);
});
@@ -58,20 +43,10 @@ const ZRefetchProductAction = z.object({
export const refetchProductAction = authenticatedActionClient
.schema(ZRefetchProductAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromProductId(parsedInput.productId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "productTeam",
minPermission: "readWrite",
productId: parsedInput.productId,
},
],
rules: ["product", "read"],
});
return await getProduct(parsedInput.productId);
@@ -89,30 +64,10 @@ const ZCreateBasicSegmentAction = z.object({
export const createBasicSegmentAction = authenticatedActionClient
.schema(ZCreateBasicSegmentAction)
.action(async ({ ctx, parsedInput }) => {
const surveyEnvironment = await getSurvey(parsedInput.surveyId);
if (!surveyEnvironment) {
throw new Error("Survey not found");
}
if (surveyEnvironment.environmentId !== parsedInput.environmentId) {
throw new Error("Survey and segment are not in the same environment");
}
await checkAuthorizationUpdated({
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(surveyEnvironment.environmentId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "productTeam",
minPermission: "readWrite",
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
},
],
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
rules: ["segment", "create"],
});
const parsedFilters = ZSegmentFilters.safeParse(parsedInput.filters);
@@ -144,22 +99,10 @@ const ZUpdateBasicSegmentAction = z.object({
export const updateBasicSegmentAction = authenticatedActionClient
.schema(ZUpdateBasicSegmentAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
access: [
{
schema: ZSegmentUpdateInput,
data: parsedInput.data,
type: "organization",
roles: ["owner", "manager"],
},
{
type: "productTeam",
minPermission: "readWrite",
productId: await getProductIdFromSegmentId(parsedInput.segmentId),
},
],
rules: ["segment", "update"],
});
const { filters } = parsedInput.data;
@@ -184,36 +127,16 @@ const ZLoadNewBasicSegmentAction = z.object({
export const loadNewBasicSegmentAction = authenticatedActionClient
.schema(ZLoadNewBasicSegmentAction)
.action(async ({ ctx, parsedInput }) => {
const surveyEnvironment = await getSurvey(parsedInput.surveyId);
const segmentEnvironment = await getSegment(parsedInput.segmentId);
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSegmentId(parsedInput.surveyId),
rules: ["segment", "read"],
});
if (!surveyEnvironment || !segmentEnvironment) {
if (!surveyEnvironment) {
throw new Error("Survey not found");
}
if (!segmentEnvironment) {
throw new Error("Segment not found");
}
}
if (surveyEnvironment.environmentId !== segmentEnvironment.environmentId) {
throw new Error("Segment and survey are not in the same environment");
}
await checkAuthorizationUpdated({
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "productTeam",
minPermission: "readWrite",
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
},
],
rules: ["survey", "update"],
});
return await loadNewSegmentInSurvey(parsedInput.surveyId, parsedInput.segmentId);
@@ -227,36 +150,16 @@ const ZCloneBasicSegmentAction = z.object({
export const cloneBasicSegmentAction = authenticatedActionClient
.schema(ZCloneBasicSegmentAction)
.action(async ({ ctx, parsedInput }) => {
const surveyEnvironment = await getSurvey(parsedInput.surveyId);
const segmentEnvironment = await getSegment(parsedInput.segmentId);
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
rules: ["segment", "create"],
});
if (!surveyEnvironment || !segmentEnvironment) {
if (!surveyEnvironment) {
throw new Error("Survey not found");
}
if (!segmentEnvironment) {
throw new Error("Segment not found");
}
}
if (surveyEnvironment.environmentId !== segmentEnvironment.environmentId) {
throw new Error("Segment and survey are not in the same environment");
}
await checkAuthorizationUpdated({
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "productTeam",
minPermission: "readWrite",
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
},
],
rules: ["survey", "read"],
});
return await cloneSegment(parsedInput.segmentId, parsedInput.surveyId);
@@ -269,20 +172,10 @@ const ZResetBasicSegmentFiltersAction = z.object({
export const resetBasicSegmentFiltersAction = authenticatedActionClient
.schema(ZResetBasicSegmentFiltersAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "productTeam",
minPermission: "readWrite",
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
},
],
rules: ["segment", "update"],
});
return await resetSegmentInSurvey(parsedInput.surveyId);
@@ -374,20 +267,10 @@ const ZCreateActionClassAction = z.object({
export const createActionClassAction = authenticatedActionClient
.schema(ZCreateActionClassAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.action.environmentId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "productTeam",
minPermission: "readWrite",
productId: await getProductIdFromEnvironmentId(parsedInput.action.environmentId),
},
],
rules: ["actionClass", "create"],
});
return await createActionClass(parsedInput.action.environmentId, parsedInput.action);

View File

@@ -1,6 +1,5 @@
"use client";
import { useTranslations } from "next-intl";
import { TActionClass } from "@formbricks/types/action-classes";
import { TSurvey } from "@formbricks/types/surveys/types";
import { ModalWithTabs } from "@formbricks/ui/components/ModalWithTabs";
@@ -13,7 +12,7 @@ interface AddActionModalProps {
environmentId: string;
actionClasses: TActionClass[];
setActionClasses: React.Dispatch<React.SetStateAction<TActionClass[]>>;
isReadOnly: boolean;
isViewer: boolean;
localSurvey: TSurvey;
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurvey>>;
}
@@ -25,13 +24,12 @@ export const AddActionModal = ({
setActionClasses,
localSurvey,
setLocalSurvey,
isReadOnly,
isViewer,
environmentId,
}: AddActionModalProps) => {
const t = useTranslations();
const tabs = [
{
title: t("environments.surveys.edit.select_saved_action"),
title: "Select saved action",
children: (
<SavedActionsTab
actionClasses={actionClasses}
@@ -42,13 +40,13 @@ export const AddActionModal = ({
),
},
{
title: t("environments.surveys.edit.capture_new_action"),
title: "Capture new action",
children: (
<CreateNewActionTab
actionClasses={actionClasses}
setActionClasses={setActionClasses}
setOpen={setOpen}
isReadOnly={isReadOnly}
isViewer={isViewer}
setLocalSurvey={setLocalSurvey}
environmentId={environmentId}
/>
@@ -57,8 +55,8 @@ export const AddActionModal = ({
];
return (
<ModalWithTabs
label={t("common.add_action")}
description={t("environments.surveys.edit.capture_a_new_action_to_trigger_a_survey_on")}
label="Add action"
description="Capture a new action to trigger a survey on."
open={open}
setOpen={setOpen}
tabs={tabs}

View File

@@ -1,7 +1,6 @@
"use client";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { TSurvey } from "@formbricks/types/surveys/types";
interface AddEndingCardButtonProps {
@@ -11,7 +10,6 @@ interface AddEndingCardButtonProps {
}
export const AddEndingCardButton = ({ localSurvey, addEndingCard }: AddEndingCardButtonProps) => {
const t = useTranslations();
return (
<div
className="group inline-flex rounded-lg border border-slate-300 bg-slate-50 hover:cursor-pointer hover:bg-white"
@@ -20,7 +18,7 @@ export const AddEndingCardButton = ({ localSurvey, addEndingCard }: AddEndingCar
<PlusIcon className="h-6 w-6 text-white" />
</div>
<div className="px-4 py-3 text-sm">
<p className="font-semibold">{t("environments.surveys.edit.add_ending")}</p>
<p className="font-semibold">Add ending</p>
</div>
</div>
);

View File

@@ -4,13 +4,12 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import * as Collapsible from "@radix-ui/react-collapsible";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import {
getCXQuestionTypes,
CXQuestionTypes,
getQuestionDefaults,
getQuestionTypes,
questionTypes,
universalQuestionPresets,
} from "@formbricks/lib/utils/questions";
import { TProduct } from "@formbricks/types/product";
@@ -19,16 +18,15 @@ interface AddQuestionButtonProps {
addQuestion: (question: any) => void;
product: TProduct;
isCxMode: boolean;
locale: string;
}
export const AddQuestionButton = ({ addQuestion, product, isCxMode, locale }: AddQuestionButtonProps) => {
const t = useTranslations();
export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestionButtonProps) => {
const [open, setOpen] = useState(false);
const [hoveredQuestionId, setHoveredQuestionId] = useState<string | null>(null);
const availableQuestionTypes = isCxMode ? getCXQuestionTypes(locale) : getQuestionTypes(locale);
const [parent] = useAutoAnimate();
const availableQuestionTypes = isCxMode ? CXQuestionTypes : questionTypes;
return (
<Collapsible.Root
open={open}
@@ -43,10 +41,8 @@ export const AddQuestionButton = ({ addQuestion, product, isCxMode, locale }: Ad
<PlusIcon className="h-5 w-5 text-white" />
</div>
<div className="px-4 py-3">
<p className="text-sm font-semibold">{t("environments.surveys.edit.add_question")}</p>
<p className="mt-1 text-xs text-slate-500">
{t("environments.surveys.edit.add_a_new_question_to_your_survey")}
</p>
<p className="text-sm font-semibold">Add question</p>
<p className="mt-1 text-xs text-slate-500">Add a new question to your survey</p>
</div>
</div>
</Collapsible.CollapsibleTrigger>
@@ -60,7 +56,7 @@ export const AddQuestionButton = ({ addQuestion, product, isCxMode, locale }: Ad
onClick={() => {
addQuestion({
...universalQuestionPresets,
...getQuestionDefaults(questionType.id, product, locale),
...getQuestionDefaults(questionType.id, product),
id: createId(),
type: questionType.id,
});

View File

@@ -1,15 +1,13 @@
"use client";
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Button } from "@formbricks/ui/components/Button";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { QuestionToggleTable } from "@formbricks/ui/components/QuestionToggleTable";
interface AddressQuestionFormProps {
@@ -22,7 +20,6 @@ interface AddressQuestionFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const AddressQuestionForm = ({
@@ -34,39 +31,38 @@ export const AddressQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: AddressQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
const t = useTranslations();
const fields = [
{
id: "addressLine1",
label: t("environments.surveys.edit.address_line_1"),
label: "Address Line 1",
...question.addressLine1,
},
{
id: "addressLine2",
label: t("environments.surveys.edit.address_line_2"),
label: "Address Line 2",
...question.addressLine2,
},
{
id: "city",
label: t("environments.surveys.edit.city"),
label: "City",
...question.city,
},
{
id: "state",
label: t("environments.surveys.edit.state"),
label: "State",
...question.state,
},
{
id: "zip",
label: t("environments.surveys.edit.zip"),
label: "Zip",
...question.zip,
},
{
id: "country",
label: t("environments.surveys.edit.country"),
label: "Country",
...question.country,
},
];
@@ -102,7 +98,7 @@ export const AddressQuestionForm = ({
<QuestionFormInput
id="headline"
value={question.headline}
label={t("environments.surveys.edit.question") + "*"}
label={"Question*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -110,7 +106,6 @@ export const AddressQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div ref={parent}>
@@ -120,7 +115,7 @@ export const AddressQuestionForm = ({
<QuestionFormInput
id="subheader"
value={question.subheader}
label={t("common.description")}
label={"Description"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -128,7 +123,6 @@ export const AddressQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -145,7 +139,7 @@ export const AddressQuestionForm = ({
});
}}>
<PlusIcon className="mr-1 h-4 w-4" />
{t("environments.surveys.edit.add_description")}
Add Description
</Button>
)}

View File

@@ -3,7 +3,6 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
import { TProductStyling } from "@formbricks/types/product";
@@ -34,7 +33,6 @@ export const BackgroundStylingCard = ({
isUnsplashConfigured,
form,
}: BackgroundStylingCardProps) => {
const t = useTranslations();
const [parent] = useAutoAnimate();
return (
@@ -67,12 +65,12 @@ export const BackgroundStylingCard = ({
<div className="flex flex-col">
<div className="flex items-center gap-2">
<p className={cn("font-semibold text-slate-800", isSettingsPage ? "text-sm" : "text-base")}>
{t("environments.surveys.edit.background_styling")}
Background Styling
</p>
{isSettingsPage && <Badge text={t("common.link_surveys")} type="gray" size="normal" />}
{isSettingsPage && <Badge text="Link Surveys" type="gray" size="normal" />}
</div>
<p className={cn("mt-1 text-slate-500", isSettingsPage ? "text-xs" : "text-sm")}>
{t("environments.surveys.edit.change_the_background_to_a_color_image_or_animation")}
Change the background to a color, image or animation.
</p>
</div>
</div>
@@ -86,10 +84,8 @@ export const BackgroundStylingCard = ({
render={({ field }) => (
<FormItem>
<div>
<FormLabel>{t("environments.surveys.edit.change_background")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.pick_a_background_from_our_library_or_upload_your_own")}
</FormDescription>
<FormLabel>Change background</FormLabel>
<FormDescription>Pick a background from our library or upload your own.</FormDescription>
</div>
<FormControl>
@@ -122,10 +118,8 @@ export const BackgroundStylingCard = ({
render={({ field }) => (
<FormItem>
<div>
<FormLabel>{t("environments.surveys.edit.brightness")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.darken_or_lighten_background_of_your_choice")}
</FormDescription>
<FormLabel>Brightness</FormLabel>
<FormDescription>Darken or lighten background of your choice.</FormDescription>
</div>
<FormControl>

View File

@@ -1,23 +1,21 @@
"use client";
import { LocalizedEditor } from "@/modules/ee/multi-language-surveys/components/localized-editor";
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { LocalizedEditor } from "@formbricks/ee/multi-language/components/localized-editor";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Input } from "@formbricks/ui/components/Input";
import { Label } from "@formbricks/ui/components/Label";
import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
const options = [
{
value: "internal",
label: "environments.surveys.edit.button_to_continue_in_survey",
label: "Button to continue in survey",
},
{ value: "external", label: "environments.surveys.edit.button_to_link_to_external_url" },
{ value: "external", label: "Button to link to external URL" },
];
interface CTAQuestionFormProps {
@@ -30,7 +28,6 @@ interface CTAQuestionFormProps {
setSelectedLanguageCode: (languageCode: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const CTAQuestionForm = ({
@@ -43,17 +40,15 @@ export const CTAQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: CTAQuestionFormProps): JSX.Element => {
const [firstRender, setFirstRender] = useState(true);
const t = useTranslations();
const [parent] = useAutoAnimate();
return (
<form ref={parent}>
<QuestionFormInput
id="headline"
value={question.headline}
label={t("environments.surveys.edit.question") + "*"}
label={"Question*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -61,11 +56,10 @@ export const CTAQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div className="mt-3">
<Label htmlFor="subheader">{t("common.description")}</Label>
<Label htmlFor="subheader">Description</Label>
<div className="mt-2">
<LocalizedEditor
id="subheader"
@@ -78,7 +72,6 @@ export const CTAQuestionForm = ({
firstRender={firstRender}
setFirstRender={setFirstRender}
questionIdx={questionIdx}
locale={locale}
/>
</div>
</div>
@@ -95,24 +88,23 @@ export const CTAQuestionForm = ({
<QuestionFormInput
id="buttonLabel"
value={question.buttonLabel}
label={t("environments.surveys.edit.next_button_label")}
label={`"Next" Button Label`}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
placeholder={lastQuestion ? t("common.finish") : t("common.next")}
placeholder={lastQuestion ? "Finish" : "Next"}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
{questionIdx !== 0 && (
<QuestionFormInput
id="backButtonLabel"
value={question.backButtonLabel}
label={t("environments.surveys.edit.back_button_label")}
label={`"Back" Button Label`}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
@@ -122,7 +114,6 @@ export const CTAQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
)}
</div>
@@ -130,7 +121,7 @@ export const CTAQuestionForm = ({
{question.buttonExternal && (
<div className="mt-3 flex-1">
<Label htmlFor="buttonLabel">{t("environments.surveys.edit.button_url")}</Label>
<Label htmlFor="buttonLabel">Button URL</Label>
<div className="mt-2">
<Input
id="buttonUrl"
@@ -148,7 +139,7 @@ export const CTAQuestionForm = ({
<QuestionFormInput
id="dismissButtonLabel"
value={question.dismissButtonLabel}
label={t("environments.surveys.edit.skip_button_label")}
label={"Skip Button Label"}
localSurvey={localSurvey}
questionIdx={questionIdx}
placeholder={"skip"}
@@ -157,7 +148,6 @@ export const CTAQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
)}

View File

@@ -1,15 +1,13 @@
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { AdvancedOptionToggle } from "@formbricks/ui/components/AdvancedOptionToggle";
import { Button } from "@formbricks/ui/components/Button";
import { Input } from "@formbricks/ui/components/Input";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
interface CalQuestionFormProps {
localSurvey: TSurvey;
@@ -21,7 +19,6 @@ interface CalQuestionFormProps {
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const CalQuestionForm = ({
@@ -33,11 +30,10 @@ export const CalQuestionForm = ({
setSelectedLanguageCode,
isInvalid,
attributeClasses,
locale,
}: CalQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const [isCalHostEnabled, setIsCalHostEnabled] = useState(!!question.calHost);
const t = useTranslations();
useEffect(() => {
if (!isCalHostEnabled) {
updateQuestion(questionIdx, { calHost: undefined });
@@ -53,7 +49,7 @@ export const CalQuestionForm = ({
<QuestionFormInput
id="headline"
value={question.headline}
label={t("environments.surveys.edit.question") + "*"}
label={"Question*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -61,7 +57,6 @@ export const CalQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div>
{question.subheader !== undefined && (
@@ -70,7 +65,7 @@ export const CalQuestionForm = ({
<QuestionFormInput
id="subheader"
value={question.subheader}
label={t("common.description")}
label={"Description"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -78,7 +73,6 @@ export const CalQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -96,12 +90,12 @@ export const CalQuestionForm = ({
}}>
{" "}
<PlusIcon className="mr-1 h-4 w-4" />
{t("environments.surveys.edit.add_description")}
Add Description
</Button>
)}
<div className="mt-3 flex flex-col gap-6">
<div className="flex flex-col gap-3">
<Label htmlFor="calUserName">{t("environments.surveys.edit.cal_username")}</Label>
<Label htmlFor="calUserName">Cal.com username or username/event</Label>
<div>
<Input
id="calUserName"
@@ -116,13 +110,13 @@ export const CalQuestionForm = ({
isChecked={isCalHostEnabled}
onToggle={(checked: boolean) => setIsCalHostEnabled(checked)}
htmlId="calHost"
description={t("environments.surveys.edit.needed_for_self_hosted_cal_com_instance")}
description="Needed for a self-hosted Cal.com instance"
childBorder
title={t("environments.surveys.edit.custom_hostname")}
title="Custom hostname"
customContainerClass="p-0">
<div className="p-4">
<div className="flex items-center gap-2">
<Label htmlFor="calHost">{t("environments.surveys.edit.hostname")}</Label>
<Label htmlFor="calHost">Hostname</Label>
<Input
id="calHost"
name="calHost"

View File

@@ -3,7 +3,6 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import React from "react";
import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
@@ -36,7 +35,6 @@ export const CardStylingSettings = ({
setOpen,
form,
}: CardStylingSettingsProps) => {
const t = useTranslations();
const isAppSurvey = surveyType === "app";
const surveyTypeDerived = isAppSurvey ? "App" : "Link";
const isLogoVisible = !!product.logo?.url;
@@ -73,10 +71,10 @@ export const CardStylingSettings = ({
<div>
<p className={cn("font-semibold text-slate-800", isSettingsPage ? "text-sm" : "text-base")}>
{t("environments.surveys.edit.card_styling")}
Card Styling
</p>
<p className={cn("mt-1 text-slate-500", isSettingsPage ? "text-xs" : "text-sm")}>
{t("environments.surveys.edit.style_the_survey_card")}
Style the survey card.
</p>
</div>
</div>
@@ -93,10 +91,8 @@ export const CardStylingSettings = ({
render={() => (
<FormItem>
<div>
<FormLabel>{t("environments.surveys.edit.roundness")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_border_radius_of_the_card_and_the_inputs")}
</FormDescription>
<FormLabel>Roundness</FormLabel>
<FormDescription>Change the border radius of the card and the inputs.</FormDescription>
</div>
<FormControl>
@@ -121,10 +117,8 @@ export const CardStylingSettings = ({
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>{t("environments.surveys.edit.card_background_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_background_color_of_the_card")}
</FormDescription>
<FormLabel>Card background color</FormLabel>
<FormDescription>Change the background color of the card.</FormDescription>
</div>
<FormControl>
@@ -144,10 +138,8 @@ export const CardStylingSettings = ({
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>{t("environments.surveys.edit.card_border_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_border_color_of_the_card")}
</FormDescription>
<FormLabel>Card border color</FormLabel>
<FormDescription>Change the border color of the card.</FormDescription>
</div>
<FormControl>
@@ -167,10 +159,8 @@ export const CardStylingSettings = ({
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>{t("environments.surveys.edit.card_shadow_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_shadow_color_of_the_card")}
</FormDescription>
<FormLabel>Card shadow color</FormLabel>
<FormDescription>Change the shadow color of the card.</FormDescription>
</div>
<FormControl>
@@ -190,18 +180,10 @@ export const CardStylingSettings = ({
render={() => (
<FormItem>
<div>
<FormLabel>
{t("environments.surveys.edit.card_arrangement_for_survey_type_derived", {
surveyTypeDerived: surveyTypeDerived,
})}
</FormLabel>
<FormLabel>Card Arrangement for {surveyTypeDerived} Surveys</FormLabel>
<FormDescription>
{t(
"environments.surveys.edit.how_funky_do_you_want_your_cards_in_survey_type_derived_surveys",
{
surveyTypeDerived: surveyTypeDerived,
}
)}
How funky do you want your cards in {surveyTypeDerived} Surveys
</FormDescription>
</div>
<FormControl>
@@ -234,10 +216,8 @@ export const CardStylingSettings = ({
</FormControl>
<div>
<FormLabel>{t("environments.surveys.edit.hide_progress_bar")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.disable_the_visibility_of_survey_progress")}
</FormDescription>
<FormLabel>Hide progress bar</FormLabel>
<FormDescription>Disable the visibility of survey progress.</FormDescription>
</div>
</FormItem>
)}
@@ -261,12 +241,10 @@ export const CardStylingSettings = ({
<div>
<FormLabel>
{t("environments.surveys.edit.hide_logo")}
<Badge text={t("common.link_surveys")} type="gray" size="normal" />
Hide logo
<Badge text="Link Surveys" type="gray" size="normal" />
</FormLabel>
<FormDescription>
{t("environments.surveys.edit.hide_the_logo_in_this_specific_survey")}
</FormDescription>
<FormDescription>Hides the logo in this specific survey</FormDescription>
</div>
</FormItem>
)}
@@ -301,9 +279,9 @@ export const CardStylingSettings = ({
</FormControl>
<div>
<FormLabel>{t("environments.surveys.edit.add_highlight_border")}</FormLabel>
<FormLabel>Add highlight border</FormLabel>
<FormDescription className="text-xs font-normal text-slate-500">
{t("environments.surveys.edit.add_highlight_border_description")}
Add an outer border to your survey card.
</FormDescription>
</div>
</div>

View File

@@ -14,7 +14,6 @@ import {
SplitIcon,
TrashIcon,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { duplicateLogicItem } from "@formbricks/lib/surveyLogic/utils";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
@@ -44,7 +43,6 @@ export function ConditionalLogic({
questionIdx,
updateQuestion,
}: ConditionalLogicProps) {
const t = useTranslations();
const transformedSurvey = useMemo(() => {
let modifiedSurvey = replaceHeadlineRecall(localSurvey, "default", attributeClasses);
modifiedSurvey = replaceEndingCardHeadlineRecall(modifiedSurvey, "default", attributeClasses);
@@ -119,7 +117,7 @@ export function ConditionalLogic({
return (
<div className="mt-4" ref={parent}>
<Label className="flex gap-2">
{t("environments.surveys.edit.conditional_logic")}
Conditional Logic
<SplitIcon className="h-4 w-4 rotate-90" />
</Label>
@@ -149,7 +147,7 @@ export function ConditionalLogic({
duplicateLogic(logicItemIdx);
}}
icon={<CopyIcon className="h-4 w-4" />}>
{t("common.duplicate")}
Duplicate
</DropdownMenuItem>
<DropdownMenuItem
disabled={logicItemIdx === 0}
@@ -157,7 +155,7 @@ export function ConditionalLogic({
moveLogic(logicItemIdx, logicItemIdx - 1);
}}
icon={<ArrowUpIcon className="h-4 w-4" />}>
{t("common.move_up")}
Move up
</DropdownMenuItem>
<DropdownMenuItem
disabled={logicItemIdx === (question.logic ?? []).length - 1}
@@ -165,14 +163,14 @@ export function ConditionalLogic({
moveLogic(logicItemIdx, logicItemIdx + 1);
}}
icon={<ArrowDownIcon className="h-4 w-4" />}>
{t("common.move_down")}
Move down
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
handleRemoveLogic(logicItemIdx);
}}
icon={<TrashIcon className="h-4 w-4" />}>
{t("common.remove")}
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -191,7 +189,7 @@ export function ConditionalLogic({
variant="secondary"
EndIcon={PlusIcon}
onClick={addLogic}>
{t("environments.surveys.edit.add_logic")}
Add logic
</Button>
</div>
</div>

View File

@@ -1,13 +1,11 @@
"use client";
import { LocalizedEditor } from "@/modules/ee/multi-language-surveys/components/localized-editor";
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { LocalizedEditor } from "@formbricks/ee/multi-language/components/localized-editor";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
interface ConsentQuestionFormProps {
localSurvey: TSurvey;
@@ -18,7 +16,6 @@ interface ConsentQuestionFormProps {
setSelectedLanguageCode: (languageCode: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const ConsentQuestionForm = ({
@@ -30,15 +27,14 @@ export const ConsentQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: ConsentQuestionFormProps): JSX.Element => {
const [firstRender, setFirstRender] = useState(true);
const t = useTranslations();
return (
<form>
<QuestionFormInput
id="headline"
label={t("environments.surveys.edit.question") + "*"}
label="Question*"
value={question.headline}
localSurvey={localSurvey}
questionIdx={questionIdx}
@@ -47,11 +43,10 @@ export const ConsentQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div className="mt-3">
<Label htmlFor="subheader">{t("common.description")}</Label>
<Label htmlFor="subheader">Description</Label>
<div className="mt-2">
<LocalizedEditor
id="subheader"
@@ -64,14 +59,13 @@ export const ConsentQuestionForm = ({
firstRender={firstRender}
setFirstRender={setFirstRender}
questionIdx={questionIdx}
locale={locale}
/>
</div>
</div>
<QuestionFormInput
id="label"
label={t("environments.surveys.edit.checkbox_label") + "*"}
label="Checkbox Label*"
placeholder="I agree to the terms and conditions"
value={question.label}
localSurvey={localSurvey}
@@ -81,7 +75,6 @@ export const ConsentQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</form>
);

View File

@@ -1,15 +1,13 @@
"use client";
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyContactInfoQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Button } from "@formbricks/ui/components/Button";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { QuestionToggleTable } from "@formbricks/ui/components/QuestionToggleTable";
interface ContactInfoQuestionFormProps {
@@ -22,7 +20,6 @@ interface ContactInfoQuestionFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const ContactInfoQuestionForm = ({
@@ -34,35 +31,33 @@ export const ContactInfoQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: ContactInfoQuestionFormProps): JSX.Element => {
const t = useTranslations();
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
const fields = [
{
id: "firstName",
label: t("environments.surveys.edit.first_name"),
label: "First Name",
...question.firstName,
},
{
id: "lastName",
label: t("environments.surveys.edit.last_name"),
label: "Last Name",
...question.lastName,
},
{
id: "email",
label: t("common.email"),
label: "Email",
...question.email,
},
{
id: "phone",
label: t("common.phone"),
label: "Phone",
...question.phone,
},
{
id: "company",
label: t("environments.surveys.edit.company"),
label: "Company",
...question.company,
},
];
@@ -92,7 +87,7 @@ export const ContactInfoQuestionForm = ({
<QuestionFormInput
id="headline"
value={question.headline}
label={t("environments.surveys.edit.question") + "*"}
label={"Question*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -100,7 +95,6 @@ export const ContactInfoQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div ref={parent}>
@@ -110,7 +104,7 @@ export const ContactInfoQuestionForm = ({
<QuestionFormInput
id="subheader"
value={question.subheader}
label={t("common.description")}
label={"Description"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -118,7 +112,6 @@ export const ContactInfoQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -135,7 +128,7 @@ export const ContactInfoQuestionForm = ({
});
}}>
<PlusIcon className="mr-1 h-4 w-4" />
{t("environments.surveys.edit.add_description")}
Add Description
</Button>
)}

View File

@@ -1,6 +1,5 @@
import { isValidCssSelector } from "@/app/lib/actionClass/actionClass";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { FormProvider, useForm } from "react-hook-form";
import toast from "react-hot-toast";
@@ -24,7 +23,7 @@ import { createActionClassAction } from "../actions";
interface CreateNewActionTabProps {
actionClasses: TActionClass[];
setActionClasses: React.Dispatch<React.SetStateAction<TActionClass[]>>;
isReadOnly: boolean;
isViewer: boolean;
setLocalSurvey?: React.Dispatch<React.SetStateAction<TSurvey>>;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
environmentId: string;
@@ -34,11 +33,10 @@ export const CreateNewActionTab = ({
actionClasses,
setActionClasses,
setOpen,
isReadOnly,
isViewer,
setLocalSurvey,
environmentId,
}: CreateNewActionTabProps) => {
const t = useTranslations();
const actionClassNames = useMemo(
() => actionClasses.map((actionClass) => actionClass.name),
[actionClasses]
@@ -65,7 +63,7 @@ export const CreateNewActionTab = ({
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["name"],
message: t("environments.actions.action_with_name_already_exists", { name: data.name }),
message: `Action with name ${data.name} already exists`,
});
}
})
@@ -87,16 +85,16 @@ export const CreateNewActionTab = ({
const submitHandler = async (data: TActionClassInput) => {
const { type } = data;
try {
if (isReadOnly) {
throw new Error(t("common.you_are_not_authorised_to_perform_this_action"));
if (isViewer) {
throw new Error("You are not authorised to perform this action.");
}
if (data.name && actionClassNames.includes(data.name)) {
throw new Error(t("environments.actions.action_with_name_already_exists", { name: data.name }));
throw new Error(`Action with name ${data.name} already exist`);
}
if (type === "code" && data.key && actionClassKeys.includes(data.key)) {
throw new Error(t("environments.actions.action_with_key_already_exists", { key: data.key }));
throw new Error(`Action with key ${data.key} already exist`);
}
if (
@@ -158,7 +156,7 @@ export const CreateNewActionTab = ({
reset();
resetAllStates();
toast.success(t("environments.actions.action_created_successfully"));
toast.success("Action created successfully");
} catch (e: any) {
toast.error(e.message);
}
@@ -180,12 +178,12 @@ export const CreateNewActionTab = ({
control={control}
render={({ field }) => (
<div>
<Label className="font-semibold">{t("environments.actions.action_type")}</Label>
<Label className="font-semibold">Action Type</Label>
<TabToggle
id="type"
options={[
{ value: "noCode", label: t("common.no_code") },
{ value: "code", label: t("common.code") },
{ value: "noCode", label: "No code" },
{ value: "code", label: "Code" },
]}
{...field}
defaultSelected={field.value}
@@ -202,16 +200,14 @@ export const CreateNewActionTab = ({
name="name"
render={({ field, fieldState: { error } }) => (
<FormItem>
<FormLabel htmlFor="actionNameInput">
{t("environments.actions.what_did_your_user_do")}
</FormLabel>
<FormLabel htmlFor="actionNameInput">What did your user do?</FormLabel>
<FormControl>
<Input
type="text"
id="actionNameInput"
{...field}
placeholder={t("environments.actions.eg_clicked_download")}
placeholder="E.g. Clicked Download"
isInvalid={!!error?.message}
/>
</FormControl>
@@ -227,14 +223,14 @@ export const CreateNewActionTab = ({
name="description"
render={({ field }) => (
<FormItem>
<FormLabel htmlFor="actionDescriptionInput">{t("common.description")}</FormLabel>
<FormLabel htmlFor="actionDescriptionInput">Description</FormLabel>
<FormControl>
<Input
type="text"
id="actionDescriptionInput"
{...field}
placeholder={t("environments.actions.eg_user_clicked_download_button")}
placeholder="User clicked Download Button"
value={field.value ?? ""}
/>
</FormControl>
@@ -247,18 +243,18 @@ export const CreateNewActionTab = ({
<hr className="border-slate-200" />
{watch("type") === "code" ? (
<CodeActionForm form={form} isReadOnly={isReadOnly} />
<CodeActionForm form={form} isEdit={false} />
) : (
<NoCodeActionForm form={form} isReadOnly={isReadOnly} />
<NoCodeActionForm form={form} />
)}
</div>
<div className="flex justify-end pt-6">
<div className="flex space-x-2">
<Button type="button" variant="minimal" onClick={resetAllStates}>
{t("common.cancel")}
Cancel
</Button>
<Button type="submit" loading={isSubmitting}>
{t("environments.actions.create_action")}
Create action
</Button>
</div>
</div>

View File

@@ -1,14 +1,12 @@
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Button } from "@formbricks/ui/components/Button";
import { Label } from "@formbricks/ui/components/Label";
import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
interface IDateQuestionFormProps {
localSurvey: TSurvey;
@@ -20,7 +18,6 @@ interface IDateQuestionFormProps {
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
const dateOptions = [
@@ -47,17 +44,15 @@ export const DateQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: IDateQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const t = useTranslations();
const [parent] = useAutoAnimate();
return (
<form>
<QuestionFormInput
id="headline"
value={question.headline}
label={t("environments.surveys.edit.question") + "*"}
label={"Question*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -65,7 +60,6 @@ export const DateQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div ref={parent}>
{question.subheader !== undefined && (
@@ -74,7 +68,7 @@ export const DateQuestionForm = ({
<QuestionFormInput
id="subheader"
value={question.subheader}
label={t("environments.surveys.edit.description")}
label={"Description"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -82,7 +76,6 @@ export const DateQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -100,13 +93,13 @@ export const DateQuestionForm = ({
});
}}>
<PlusIcon className="mr-1 h-4 w-4" />
{t("environments.surveys.edit.add_description")}
Add Description
</Button>
)}
</div>
<div className="mt-3">
<Label htmlFor="questionType">{t("environments.surveys.edit.date_format")}</Label>
<Label htmlFor="questionType">Date Format</Label>
<div className="mt-2 flex items-center">
<OptionsSwitch
options={dateOptions}

View File

@@ -9,7 +9,6 @@ import { CSS } from "@dnd-kit/utilities";
import { createId } from "@paralleldrive/cuid2";
import * as Collapsible from "@radix-ui/react-collapsible";
import { GripIcon, Handshake, Undo2 } from "lucide-react";
import { useTranslations } from "next-intl";
import { cn } from "@formbricks/lib/cn";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
@@ -20,7 +19,6 @@ import {
TSurveyQuestionId,
TSurveyRedirectUrlCard,
} from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch";
import { TooltipRenderer } from "@formbricks/ui/components/Tooltip";
@@ -37,7 +35,6 @@ interface EditEndingCardProps {
plan: TOrganizationBillingPlan;
addEndingCard: (index: number) => void;
isFormbricksCloud: boolean;
locale: TUserLocale;
}
export const EditEndingCard = ({
@@ -53,21 +50,16 @@ export const EditEndingCard = ({
plan,
addEndingCard,
isFormbricksCloud,
locale,
}: EditEndingCardProps) => {
const endingCard = localSurvey.endings[endingCardIndex];
const t = useTranslations();
const isRedirectToUrlDisabled = isFormbricksCloud
? plan === "free" && endingCard.type !== "redirectToUrl"
: false;
const endingCardTypes = [
{ value: "endScreen", label: t("environments.surveys.edit.ending_card") },
{
value: "redirectToUrl",
label: t("environments.surveys.edit.redirect_to_url"),
disabled: isRedirectToUrlDisabled,
},
{ value: "endScreen", label: "Ending card" },
{ value: "redirectToUrl", label: "Redirect to Url", disabled: isRedirectToUrlDisabled },
];
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
@@ -184,15 +176,12 @@ export const EditEndingCard = ({
attributeClasses
)[selectedLanguageCode]
)
: t("environments.surveys.edit.ending_card"))}
{endingCard.type === "redirectToUrl" &&
(endingCard.label || t("environments.surveys.edit.redirect_to_url"))}
: "Ending card")}
{endingCard.type === "redirectToUrl" && (endingCard.label || "Redirect to Url")}
</p>
{!open && (
<p className="mt-1 truncate text-xs text-slate-500">
{endingCard.type === "endScreen"
? t("environments.surveys.edit.ending_card")
: t("environments.surveys.edit.redirect_to_url")}
{endingCard.type === "endScreen" ? "Ending card" : "Redirect to Url"}
</p>
)}
</div>
@@ -210,7 +199,6 @@ export const EditEndingCard = ({
updateCard={() => {}}
addCard={addEndingCard}
cardType="ending"
locale={locale}
/>
</div>
</div>
@@ -218,7 +206,7 @@ export const EditEndingCard = ({
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "mt-3 pb-6"}`}>
<TooltipRenderer
shouldRender={endingCard.type === "endScreen" && isRedirectToUrlDisabled}
tooltipContent={t("environments.surveys.edit.redirect_to_url_not_available_on_free_plan")}
tooltipContent={"Redirect To Url is not available on free plan"}
triggerClass="w-full">
<OptionsSwitch
options={endingCardTypes}
@@ -245,7 +233,6 @@ export const EditEndingCard = ({
attributeClasses={attributeClasses}
updateSurvey={updateSurvey}
endingCard={endingCard}
locale={locale}
/>
)}
{endingCard.type === "redirectToUrl" && (

Some files were not shown because too many files have changed in this diff Show More