mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-27 00:40:29 -06:00
Compare commits
4 Commits
v2.3.0
...
fix/radix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da86d95398 | ||
|
|
00d49b91b2 | ||
|
|
3276ce7844 | ||
|
|
93ce9580a0 |
@@ -176,10 +176,7 @@ ENTERPRISE_LICENSE_KEY=
|
||||
UNSPLASH_ACCESS_KEY=
|
||||
|
||||
# The below is used for Next Caching (uses In-Memory from Next Cache if not provided)
|
||||
# REDIS_URL=redis://localhost:6379
|
||||
# REDIS_URL:
|
||||
|
||||
# The below is used for Rate Limiting (uses In-Memory LRU Cache if not provided) (You can use a service like Webdis for this)
|
||||
# REDIS_HTTP_URL:
|
||||
|
||||
# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
|
||||
# CUSTOM_CACHE_DISABLED=1
|
||||
|
||||
58
.github/workflows/e2e.yml
vendored
58
.github/workflows/e2e.yml
vendored
@@ -7,20 +7,6 @@ jobs:
|
||||
name: Run E2E Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
env:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd="pg_isready -U testuser"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=5
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
@@ -30,46 +16,44 @@ jobs:
|
||||
with:
|
||||
e2e_testing_mode: "1"
|
||||
|
||||
- name: Check if pnpm is installed
|
||||
id: pnpm-check
|
||||
run: |
|
||||
if pnpm --version; then
|
||||
echo "pnpm is installed."
|
||||
echo "PNPM_INSTALLED=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "pnpm is not installed."
|
||||
echo "PNPM_INSTALLED=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Install pnpm
|
||||
if: env.PNPM_INSTALLED == 'false'
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Install dependencies
|
||||
if: env.PNPM_INSTALLED == 'false'
|
||||
run: pnpm install
|
||||
|
||||
- name: Apply Prisma Migrations
|
||||
- name: Start PostgreSQL
|
||||
run: |
|
||||
pnpm prisma migrate deploy
|
||||
cd packages/database && pnpm db:up &
|
||||
for attempt in {1..20}; do
|
||||
if nc -zv localhost 5432; then
|
||||
echo "Ready"
|
||||
break
|
||||
fi
|
||||
echo "Waiting..."
|
||||
sleep 5
|
||||
done
|
||||
pnpm db:migrate:dev
|
||||
- name: Serve packages for lazy loading
|
||||
run: |
|
||||
cd packages/surveys && pnpm serve &
|
||||
|
||||
- name: Run App
|
||||
run: |
|
||||
NODE_ENV=test pnpm start --filter=@formbricks/web &
|
||||
sleep 10 # Optional: gives some buffer for the app to start
|
||||
for attempt in {1..10}; do
|
||||
for attempt in {1..20}; do
|
||||
if [ $(curl -o /dev/null -s -w "%{http_code}" http://localhost:3000/health) -eq 200 ]; then
|
||||
echo "Application is ready."
|
||||
echo "Ready"
|
||||
break
|
||||
fi
|
||||
if [ $attempt -eq 10 ]; then
|
||||
echo "Application failed to start in time."
|
||||
exit 1
|
||||
fi
|
||||
echo "Still waiting for the application to be ready..."
|
||||
echo "Waiting..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
- name: Test Serve endpoints
|
||||
run: |
|
||||
curl -s http://localhost:3003
|
||||
|
||||
- name: Cache Playwright
|
||||
uses: actions/cache@v3
|
||||
id: playwright-cache
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"dependencies": {
|
||||
"@formbricks/js": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"lucide-react": "^0.397.0",
|
||||
"lucide-react": "^0.395.0",
|
||||
"next": "14.2.4",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
|
||||
@@ -140,7 +140,7 @@ const AppPage = ({}) => {
|
||||
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
<button className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
No-Code Action
|
||||
</button>
|
||||
</div>
|
||||
@@ -169,7 +169,7 @@ const AppPage = ({}) => {
|
||||
onClick={() => {
|
||||
formbricks.setAttribute("Plan", "Free");
|
||||
}}
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
Set Plan to 'Free'
|
||||
</button>
|
||||
</div>
|
||||
@@ -192,7 +192,7 @@ const AppPage = ({}) => {
|
||||
onClick={() => {
|
||||
formbricks.setAttribute("Plan", "Paid");
|
||||
}}
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
Set Plan to 'Paid'
|
||||
</button>
|
||||
</div>
|
||||
@@ -215,7 +215,7 @@ const AppPage = ({}) => {
|
||||
onClick={() => {
|
||||
formbricks.setEmail("test@web.com");
|
||||
}}
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
Set Email
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -29,8 +29,8 @@ We are so happy that you are interested in contributing to Formbricks 🤗 There
|
||||
- **Issues**: Spotted a bug? Has deployment gone wrong? Do you have user feedback? [Raise an issue](https://github.com/formbricks/formbricks/issues/new/choose) for the fastest response.
|
||||
- **Feature requests**: Raise an issue for these and tag it as an Enhancement. We love every idea. Please give us as much context on the why as possible.
|
||||
- **Creating a PR**: Please fork the repository, make your changes and create a new pull request if you want to make an update.
|
||||
- **E2E Tests**: Understand how we write E2E tests and make sure to write whenever you ship a feature [here](https://formbricks.notion.site/Formbricks-End-to-End-Tests-06dc830d71604deaa8da24714540f7ab?pvs=4).
|
||||
- **Introducing a new Question Type?**: Adding a new question type to our surveys? Follow this guide to make sure you’re on the right track [here](https://formbricks.notion.site/Guidelines-for-Implementing-a-New-Question-Type-9ac0d1c362714addb24b9abeb326d1c1?pvs=4).
|
||||
- **E2E Tests**: Understand how we write E2E tests and make sure to write whenever you ship a feature [here](https://www.notion.so/Formbricks-End-to-End-Tests-06dc830d71604deaa8da24714540f7ab?pvs=21).
|
||||
- **Introducing a new Question Type?**: Adding a new question type to our surveys? Follow this guide to make sure you’re on the right track [here](https://www.notion.so/Guidelines-for-Implementing-a-New-Question-Type-9ac0d1c362714addb24b9abeb326d1c1?pvs=21).
|
||||
- **How we Code at Formbricks**: View this Notion document and understand the coding practises we follow so that you can adhere to them for uniformity.
|
||||
- **How to create a service**: Services are our Database abstraction layer where we connect Prisma (our DB ORM) with logical methods that are reusable across the server. View this document to understand when & how to write them.
|
||||
- **Roadmap**: Our roadmap is open on GitHub tickets and some customer and community tickets on GitHub.
|
||||
@@ -56,7 +56,7 @@ We currently officially support the below methods to set up your development env
|
||||
|
||||
This will open a fully configured workspace in your browser with all the necessary dependencies already installed. Click the button below to open this project in Gitpod. For a detailed guide, visit the **Gitpod Setup Guide** section below.
|
||||
|
||||
[](https://gitpod.io/#https://github.com/formbricks/formbricks)
|
||||
[](https://gitpod.io/#https://Github.com/formbricks/formbricks)
|
||||
|
||||
### Github Codespaces
|
||||
|
||||
@@ -124,9 +124,8 @@ After clicking the one-click setup button, Gitpod will open a new tab or window.
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
- This is the Gitpod Authentication Page. It appears when you click the "Open in GitPod" button and Gitpod
|
||||
needs to authenticate your access to the workspace. Click on 'Continue With Github' to authorize your GitPod
|
||||
session.
|
||||
- This is the Gitpod Authentication Page. It appears when you click the "Open in GitPod" button and Gitpod needs
|
||||
to authenticate your access to the workspace. Click on 'Continue With Github' to authorize your GitPod session.
|
||||
|
||||
### 3. Creating a New Workspace
|
||||
|
||||
@@ -136,12 +135,11 @@ session.
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
- After authentication, Gitpod asks to create a new workspace for you. This page displays the configurations
|
||||
of your workspace. - You can use either choose either VS Code Browser or VS Code Desktop editor with the
|
||||
'Standard Class' for your workspace class. - If you opt for the VS Code Desktop, follow the following steps 1.
|
||||
Gitpod will prompt you to grant access to the VSCode app. Once approved, install the GitPod extension from the
|
||||
VSCode Marketplace and follow the prompts to authorize the integration. 2. Change the `WEBAPP_URL` and the
|
||||
`NEXTAUTH_URL` to `https://localhost:3000`
|
||||
- After authentication, Gitpod asks to create a new workspace for you. This page displays the configurations of
|
||||
your workspace. - You can use either choose either VS Code Browser or VS Code Desktop editor with the 'Standard
|
||||
Class' for your workspace class. - If you opt for the VS Code Desktop, follow the following steps 1. Gitpod will
|
||||
prompt you to grant access to the VSCode app. Once approved, install the GitPod extension from the VSCode Marketplace
|
||||
and follow the prompts to authorize the integration. 2. Change the `WEBAPP_URL` and the `NEXTAUTH_URL` to `https://localhost:3000`
|
||||
|
||||
### 4. Gitpod preparing the created Workspace
|
||||
|
||||
@@ -162,8 +160,8 @@ page while Gitpod sets up your development environment.
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
- Once the workspace is fully prepared, voila, it enters the running state. You can start working on your
|
||||
project in this environment.
|
||||
- Once the workspace is fully prepared, voila, it enters the running state. You can start working on your project
|
||||
in this environment.
|
||||
|
||||
### Ports and Services
|
||||
|
||||
|
||||
@@ -15,7 +15,15 @@ export const metadata = {
|
||||
|
||||
Formbricks offers two types of APIs: the **Public Client API** and the **Management API**. Each API serves a different purpose, has different authentication requirements, and provides access to different data and settings.
|
||||
|
||||
View our [API Documentation](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh) in more than 30 frameworks and languages.
|
||||
View our [API Documentation](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh) in more than 30 frameworks and languages. Or directly try out our APIs in Postman by clicking the button below:
|
||||
|
||||
<div className="max-w-full sm:max-w-3xl">
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://formbricks.postman.co/collection/11026000-927c954f-85a9-4f8f-b0ec-14191b903737?source=rip_html">
|
||||
<img alt="Run in Postman" src="https://run.pstmn.io/button.svg" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Public Client API
|
||||
|
||||
@@ -59,8 +67,7 @@ The API requests are authorized with a personal API key. This API key gives you
|
||||
/>
|
||||
|
||||
<Note>
|
||||
### Store API key safely! Anyone who has your API key has full control over your account. For security
|
||||
reasons, you cannot view the API key again.
|
||||
### Store API key safely! Anyone who has your API key has full control over your account. For security reasons, you cannot view the API key again.
|
||||
</Note>
|
||||
|
||||
### Test your API Key
|
||||
|
||||
BIN
apps/docs/app/global/api/add-api-key.webp
Normal file
BIN
apps/docs/app/global/api/add-api-key.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
apps/docs/app/global/api/api-key-secret.webp
Normal file
BIN
apps/docs/app/global/api/api-key-secret.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
139
apps/docs/app/global/api/page.mdx
Normal file
139
apps/docs/app/global/api/page.mdx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import AddApiKey from "./add-api-key.webp";
|
||||
import ApiKeySecret from "./api-key-secret.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Formbricks API Overview: Public Client & Management API Breakdown",
|
||||
description:
|
||||
"Formbricks provides a powerful API to manage your surveys, responses, users, displays, actions, attributes & webhooks programmatically. Get a detailed understanding of Formbricks' dual API offerings: the unauthenticated Public Client API optimized for client-side tasks and the secured Management API for advanced account operations. Choose the perfect fit for your integration needs and ensure robust data handling",
|
||||
};
|
||||
|
||||
#### API
|
||||
|
||||
# API Overview
|
||||
|
||||
Formbricks offers two types of APIs: the **Public Client API** and the **Management API**. Each API serves a different purpose, has different authentication requirements, and provides access to different data and settings.
|
||||
|
||||
View our [API Documentation](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh) in more than 30 frameworks and languages. Or directly try out our APIs in Postman by clicking the button below:
|
||||
|
||||
<div className="max-w-full sm:max-w-3xl">
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://formbricks.postman.co/collection/11026000-927c954f-85a9-4f8f-b0ec-14191b903737?source=rip_html">
|
||||
<img alt="Run in Postman" src="https://run.pstmn.io/button.svg" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Public Client API
|
||||
|
||||
The [Public Client API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#5c981d9e-5e7d-455d-9795-b9c45bc2f930) is designed for our SDKs and **does not require authentication**. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
|
||||
|
||||
We currently have the following Client API methods exposed and below is their documentation attached in Postman:
|
||||
|
||||
- [Actions API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#b8f3a10e-1642-4d82-a629-fef0a8c6c86c) - Create actions for a Person
|
||||
- [Displays API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#949272bf-daec-4d72-9b52-47af3d74a62c) - Mark Survey as Displayed or Update an existing Display by linking it with a Response for a Person
|
||||
- [People API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#ee3d2188-4253-4bca-9238-6b76455805a9) - Create & Update a Person (e.g. attributes, email, userId, etc)
|
||||
- [Responses API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#8c773032-536c-483c-a237-c7697347946e) - Create & Update a Response for a Survey
|
||||
|
||||
## Management API
|
||||
|
||||
The [Management API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#98fce5a1-1365-4125-8de1-acdb28206766) provides access to all data and settings that your account has access to in the Formbricks app. This API **requires a personal API Key** for authentication, which can be generated in the Settings section of the Formbricks app. Checkout the [API Key Setup](#how-to-generate-an-api-key) below to generate & manage API Keys.
|
||||
|
||||
We currently have the following Management API methods exposed and below is their documentation attached in Postman:
|
||||
|
||||
- [Action Class API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#81947f69-99fc-41c9-a184-f3260e02be48) - Create, List, and Delete Action Classes
|
||||
- [Attribute Class API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#31089010-d468-4a7c-943e-8ebe71b9a36e) - Create, List, and Delete Attribute Classes
|
||||
- [Me API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#79e08365-641d-4b2d-aea2-9a855e0438ec) - Retrieve Account Information
|
||||
- [People API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#cffc27a6-dafb-428f-8ea7-5165bedb911e) - List and Delete People
|
||||
- [Response API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#e544ec0d-8b30-4e33-8d35-2441cb40d676) - List, List by Survey, Update, and Delete Responses
|
||||
- [Survey API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#953189b2-37b5-4429-a7bd-f4d01ceae242) - List, Create, Update, and Delete Surveys
|
||||
- [Webhook API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#62e6ec65-021b-42a4-ac93-d1434b393c6c) - List, Create, and Delete Webhooks
|
||||
|
||||
## How to Generate an API key
|
||||
|
||||
The API requests are authorized with a personal API key. This API key gives you the same rights as if you were logged in at formbricks.com - **don't share it around!**
|
||||
|
||||
1. Go to your settings on [app.formbricks.com](https://app.formbricks.com).
|
||||
2. Go to page “API keys”
|
||||
<MdxImage src={AddApiKey} alt="Add API Key" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
|
||||
3. Create a key for the development or production environment.
|
||||
4. Copy the key immediately. You won’t be able to see it again.
|
||||
<MdxImage
|
||||
src={ApiKeySecret}
|
||||
alt="API Key Secret"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
<Note>
|
||||
### Store API key safely! Anyone who has your API key has full control over your account. For security reasons, you cannot view the API key again.
|
||||
</Note>
|
||||
|
||||
### Test your API Key
|
||||
|
||||
Hit the below request to verify that you are authenticated with your API Key and the server is responding.
|
||||
|
||||
## Get My Profile {{ tag: 'GET', label: '/api/v1/me' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Get the product details and environment type of your account.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-Api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### Delete a personal API key
|
||||
|
||||
1. Go to settings on [app.formbricks.com](https://app.formbricks.com/).
|
||||
2. Go to page “API keys”.
|
||||
3. Find the key you wish to revoke and select “Delete”.
|
||||
4. Your API key will stop working immediately.
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/me">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/me' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"id": "cll2m30r70004mx0huqkitgqv",
|
||||
"createdAt": "2023-08-08T18:04:59.922Z",
|
||||
"updatedAt": "2023-08-08T18:04:59.922Z",
|
||||
"type": "production",
|
||||
"product": {
|
||||
"id": "cll2m30r60003mx0hnemjfckr",
|
||||
"name": "My Product"
|
||||
},
|
||||
"appSetupCompleted": false,
|
||||
"websiteSetupCompleted": false,
|
||||
}
|
||||
```
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
Not authenticated
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
Can’t figure it out? Join our [Discord](https://discord.com/invite/3YFcABF2Ts) and we'd be glad to assist you!
|
||||
|
||||
---
|
||||
@@ -3,7 +3,7 @@ import { MdxImage } from "@/components/MdxImage";
|
||||
import PeopleView from "./people-view.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Effectively identify users in Formbricks link surveys",
|
||||
title: "Effective Identify Users in Formbricks Link Surveys",
|
||||
description:
|
||||
"Discover how to seamlessly connect responses from Formbricks link surveys to existing users in your database. Learn the intricacies of the userId URL parameter to enhance user tracking, profiling, and segmentation, ensuring more personalized interactions and data-driven decisions.",
|
||||
};
|
||||
|
||||
@@ -58,7 +58,6 @@ These variables are present inside your machine’s docker-compose file. Restart
|
||||
| OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have .well-known configured at this) | optional (required if OIDC auth is enabled) | |
|
||||
| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | RS256 |
|
||||
| OPENTELEMETRY_LISTENER_URL | URL for OpenTelemetry listener inside Formbricks. | optional | |
|
||||
| CUSTOM_CACHE_DISABLED | Disables custom cache handler if set to 1 (required for deployment on Vercel) | optional | |
|
||||
| `<add more>` | | | |
|
||||
| | | | |
|
||||
|
||||
@@ -176,10 +175,8 @@ An example configuration for a FusionAuth OpenID Connect in Formbricks would loo
|
||||
<Col>
|
||||
<CodeGroup title="Formbricks Env for FusionAuth OIDC">
|
||||
```yml {{ title: ".env" }}
|
||||
OIDC_CLIENT_ID=59cada54-56d4-4aa8-a5e7-5823bbe0e5b7
|
||||
OIDC_CLIENT_SECRET=4f4dwP0ZoOAqMW8fM9290A7uIS3E8Xg29xe1umhlB_s
|
||||
OIDC_ISSUER=http://localhost:9011
|
||||
OIDC_DISPLAY_NAME=FusionAuth OIDC_SIGNING_ALGORITHM=HS256
|
||||
OIDC_CLIENT_ID=59cada54-56d4-4aa8-a5e7-5823bbe0e5b7 OIDC_CLIENT_SECRET=4f4dwP0ZoOAqMW8fM9290A7uIS3E8Xg29xe1umhlB_s
|
||||
OIDC_ISSUER=http://localhost:9011 OIDC_DISPLAY_NAME=FusionAuth OIDC_SIGNING_ALGORITHM=HS256
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
@@ -8,106 +8,6 @@ export const metadata = {
|
||||
|
||||
# Migration Guide
|
||||
|
||||
## v2.3
|
||||
|
||||
Formbricks v2.3 includes new color options for rating questions, improved multi-language functionality for Chinese (Simplified & Traditional), and various bug fixes and performance improvements.
|
||||
|
||||
### Steps to Migrate
|
||||
|
||||
<Note>
|
||||
You only need to run the data migration if you have multi language surveys set up in the Chinese language
|
||||
(`cn`). If you don't have any surveys in Chinese, you can skip the data migration step.
|
||||
</Note>
|
||||
|
||||
This guide is for users who are self-hosting Formbricks using our one-click setup. If you are using a different setup, you might adjust the commands accordingly.
|
||||
|
||||
To run all these steps, please navigate to the `formbricks` folder where your `docker-compose.yml` file is located.
|
||||
|
||||
1. **Backup your Database**: This is a crucial step. Please make sure to backup your database before proceeding with the upgrade. You can use the following command to backup your database:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Backup Postgres">
|
||||
|
||||
```bash
|
||||
docker exec formbricks-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v2.3_$(date +%Y%m%d_%H%M%S).dump
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
<Note>
|
||||
If you run into “No such container”, use `docker ps` to find your container name, e.g.
|
||||
`formbricks_postgres_1`.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
If you prefer storing the backup as an `*.sql` file remove the `-Fc` (custom format) option. In case of a
|
||||
restore scenario you will need to use `psql` then with an empty `formbricks` database.
|
||||
</Note>
|
||||
|
||||
2. Pull the latest version of Formbricks:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the containers">
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. Stop the running Formbricks instance & remove the related containers:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the containers">
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
4. Restarting the containers with the latest version of Formbricks:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Restart the containers">
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
5. Now let's migrate the data to the latest schema:
|
||||
|
||||
<Note>To find your Docker Network name for your Postgres Database, find it using `docker network ls`</Note>
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/formbricks/data-migrations:latest && \
|
||||
docker run --rm \
|
||||
--network=formbricks_default \
|
||||
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
|
||||
-e UPGRADE_TO_VERSION="v2.3" \
|
||||
ghcr.io/formbricks/data-migrations:latest
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
The above command will migrate your data to the latest schema. This is a crucial step to migrate your existing data to the new structure. Only if the script runs successful, changes are made to the database. The script can safely run multiple times.
|
||||
|
||||
6. That's it! Once the migration is complete, you can **now access your Formbricks instance** at the same URL as before.
|
||||
|
||||
### Additional Updates
|
||||
|
||||
The feature to create short urls in Formbricks is now deprecated. Previously generated short urls will keep working for now but we recommend to use the long urls instead. Redirect support for short urls will be removed in a future release.
|
||||
|
||||
## v2.2
|
||||
|
||||
Formbricks v2.2 introduces XM research presets into your products with a brand new product onboarding. Our objective is to make user research “obviously easy”, industry by industry. And we're starting with Software-as-a-Service and E-Commerce.
|
||||
|
||||
@@ -26,7 +26,6 @@ Before you proceed, make sure you have the following:
|
||||
|
||||
- A **Linux Ubuntu** Virtual Machine deployed with SSH access.
|
||||
- An **A record** set up to connect a custom domain to your instance. Formbricks will **automatically create an SSL certificate** for your domain using Let's Encrypt.
|
||||
- **Port 80 and 443** open in your VM's Security Group to allow Traefik to create the SSL certificate.
|
||||
|
||||
## Start
|
||||
|
||||
@@ -92,7 +91,7 @@ File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N)
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. **Domain Name**: Enter the domain name that Traefik will use to create the SSL certificate and forward requests to Formbricks. Please make sure that port 80 and 443 are open in your VM's Security Group to allow Traefik to create the SSL certificate.
|
||||
3. **Domain Name**: Enter the domain name that Traefik will use to create the SSL certificate and forward requests to Formbricks.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Domain Name for SSL certificate Prompt">
|
||||
|
||||
@@ -47,7 +47,7 @@ export const SideNavigation = ({ pathname }) => {
|
||||
return (
|
||||
<li
|
||||
key={heading.text}
|
||||
className={`mb-4 ml-4 text-slate-900 dark:text-white ml-${heading.level === 2 ? 0 : heading.level === 3 ? 4 : 6}`}>
|
||||
className={`mb-4 ml-4 text-slate-900 dark:text-white ml-${heading.level === 2 ? 0 : heading.level === 3 ? 4 : 6}`}>
|
||||
<Link
|
||||
href={`#${heading.id}`}
|
||||
onClick={() => setSelectedId(heading.id)}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"@headlessui/react": "^2.1.1",
|
||||
"@headlessui/react": "^2.0.4",
|
||||
"@headlessui/tailwindcss": "^0.2.1",
|
||||
"@mapbox/rehype-prism": "^0.9.0",
|
||||
"@mdx-js/loader": "^3.0.1",
|
||||
@@ -33,10 +33,10 @@
|
||||
"clsx": "^2.1.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
"flexsearch": "^0.7.43",
|
||||
"framer-motion": "11.2.12",
|
||||
"framer-motion": "11.2.11",
|
||||
"lottie-web": "^5.12.2",
|
||||
"lucide": "^0.397.0",
|
||||
"lucide-react": "^0.397.0",
|
||||
"lucide": "^0.395.0",
|
||||
"lucide-react": "^0.395.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"mdx-annotations": "^0.1.4",
|
||||
"next": "14.2.4",
|
||||
@@ -62,7 +62,7 @@
|
||||
"tailwindcss": "^3.4.4",
|
||||
"unist-util-filter": "^5.0.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"zustand": "^4.5.4"
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
|
||||
@@ -65,7 +65,7 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
|
||||
value={field.value}
|
||||
onChange={(email) => field.onChange(email)}
|
||||
placeholder="engineering@acme.com"
|
||||
className="bg-white"
|
||||
className=" bg-white"
|
||||
/>
|
||||
{error?.message && <FormError className="text-left">{error.message}</FormError>}
|
||||
</div>
|
||||
|
||||
@@ -9,10 +9,9 @@ import { TProductConfigChannel } from "@formbricks/types/product";
|
||||
interface OnboardingSurveyProps {
|
||||
organizationId: string;
|
||||
channel: TProductConfigChannel;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export const OnboardingSurvey = ({ organizationId, channel, userId }: OnboardingSurveyProps) => {
|
||||
export const OnboardingSurvey = ({ organizationId, channel }: OnboardingSurveyProps) => {
|
||||
const [isIFrameVisible, setIsIFrameVisible] = useState(false);
|
||||
const [fadeout, setFadeout] = useState(false);
|
||||
const router = useRouter();
|
||||
@@ -45,7 +44,7 @@ export const OnboardingSurvey = ({ organizationId, channel, userId }: Onboarding
|
||||
<div className="relative h-[60vh] w-[50vh] overflow-auto">
|
||||
<iframe
|
||||
onLoad={() => setIsIFrameVisible(true)}
|
||||
src={`https://app.formbricks.com/s/clxcwr22p0cwlpvgekzdab2x5?embed=true&userId=${userId}`}
|
||||
src="https://app.formbricks.com/s/clxcwr22p0cwlpvgekzdab2x5?embed=true"
|
||||
className="absolute left-0 top-0 h-full w-full overflow-visible border-0"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { OnboardingSurvey } from "@/app/(app)/(onboarding)/organizations/[organizationId]/products/new/survey/components/OnboardingSurvey";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { notFound } from "next/navigation";
|
||||
import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
|
||||
|
||||
interface OnboardingSurveyPageProps {
|
||||
@@ -15,18 +13,11 @@ interface OnboardingSurveyPageProps {
|
||||
}
|
||||
|
||||
const Page = async ({ params, searchParams }: OnboardingSurveyPageProps) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
|
||||
const channel = searchParams.channel;
|
||||
const industry = searchParams.industry;
|
||||
if (!channel || !industry) return notFound();
|
||||
|
||||
return (
|
||||
<OnboardingSurvey organizationId={params.organizationId} channel={channel} userId={session.user.id} />
|
||||
);
|
||||
return <OnboardingSurvey organizationId={params.organizationId} channel={channel} />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -22,7 +22,7 @@ export const AddQuestionButton = ({ addQuestion, product }: AddQuestionButtonPro
|
||||
onOpenChange={setOpen}
|
||||
className={cn(
|
||||
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
|
||||
"group w-full space-y-2 rounded-lg border border-slate-300 bg-white transition-all duration-300 ease-in-out hover:scale-100 hover:cursor-pointer hover:bg-slate-50"
|
||||
"group w-full space-y-2 rounded-lg border border-slate-300 bg-white transition-all duration-300 ease-in-out hover:scale-100 hover:cursor-pointer hover:bg-slate-50"
|
||||
)}>
|
||||
<Collapsible.CollapsibleTrigger asChild className="group h-full w-full">
|
||||
<div className="inline-flex">
|
||||
@@ -35,7 +35,7 @@ export const AddQuestionButton = ({ addQuestion, product }: AddQuestionButtonPro
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="justify-left flex flex-col">
|
||||
<Collapsible.CollapsibleContent className="justify-left flex flex-col ">
|
||||
{/* <hr className="py-1 text-slate-600" /> */}
|
||||
{questionTypes.map((questionType) => (
|
||||
<button
|
||||
|
||||
@@ -41,7 +41,7 @@ export const BackgroundStylingCard = ({
|
||||
}}
|
||||
className={cn(
|
||||
open ? "" : "hover:bg-slate-50",
|
||||
"w-full space-y-2 rounded-lg border border-slate-300 bg-white"
|
||||
"w-full space-y-2 rounded-lg border border-slate-300 bg-white "
|
||||
)}>
|
||||
<Collapsible.CollapsibleTrigger
|
||||
asChild
|
||||
@@ -108,7 +108,7 @@ export const BackgroundStylingCard = ({
|
||||
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex flex-col justify-center ">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="background.brightness"
|
||||
|
||||
@@ -152,7 +152,6 @@ export const CreateNewActionTab = ({
|
||||
|
||||
reset();
|
||||
resetAllStates();
|
||||
toast.success("Action created successfully");
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export const EditThankYouCard = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
|
||||
open ? "scale-100 shadow-lg " : "scale-97 shadow-md",
|
||||
"group z-20 flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
|
||||
)}>
|
||||
<div
|
||||
|
||||
@@ -61,7 +61,7 @@ export const EditWelcomeCard = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
|
||||
open ? "scale-100 shadow-lg " : "scale-97 shadow-md",
|
||||
"group flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
|
||||
)}>
|
||||
<div
|
||||
|
||||
@@ -49,7 +49,7 @@ export const HiddenFieldsCard = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
|
||||
open ? "scale-100 shadow-lg " : "scale-97 shadow-md",
|
||||
"group z-10 flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
|
||||
)}>
|
||||
<div
|
||||
|
||||
@@ -79,7 +79,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment, produc
|
||||
description: "Run targeted surveys on public websites.",
|
||||
comingSoon: false,
|
||||
alert: !websiteSetupCompleted,
|
||||
hide: product.config.channel && product.config.channel !== "website",
|
||||
hide: product.config.channel !== "website",
|
||||
},
|
||||
{
|
||||
id: "app",
|
||||
@@ -88,7 +88,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment, produc
|
||||
description: "Embed a survey in your web app to collect responses with user identification.",
|
||||
comingSoon: false,
|
||||
alert: !appSetupCompleted,
|
||||
hide: product.config.channel && product.config.channel !== "app",
|
||||
hide: product.config.channel !== "app",
|
||||
},
|
||||
{
|
||||
id: "link",
|
||||
@@ -125,7 +125,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment, produc
|
||||
onOpenChange={setOpen}
|
||||
className={cn(
|
||||
open ? "" : "hover:bg-slate-50",
|
||||
"w-full space-y-2 rounded-lg border border-slate-300 bg-white"
|
||||
"w-full space-y-2 rounded-lg border border-slate-300 bg-white "
|
||||
)}>
|
||||
<Collapsible.CollapsibleTrigger
|
||||
asChild
|
||||
@@ -159,7 +159,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment, produc
|
||||
key={option.id}
|
||||
htmlFor={option.id}
|
||||
className={cn(
|
||||
"flex w-full items-center rounded-lg border bg-slate-50 p-4",
|
||||
"flex w-full items-center rounded-lg border bg-slate-50 p-4",
|
||||
option.comingSoon
|
||||
? "border-slate-200 bg-slate-50/50"
|
||||
: option.id === localSurvey.type
|
||||
@@ -170,10 +170,10 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment, produc
|
||||
<RadioGroupItem
|
||||
value={option.id}
|
||||
id={option.id}
|
||||
className="aria-checked:border-brand-dark mx-5 disabled:border-slate-400 aria-checked:border-2"
|
||||
className="aria-checked:border-brand-dark mx-5 disabled:border-slate-400 aria-checked:border-2"
|
||||
disabled={option.comingSoon}
|
||||
/>
|
||||
<div className="inline-flex items-center">
|
||||
<div className=" inline-flex items-center">
|
||||
<option.icon className="mr-4 h-8 w-8 text-slate-500" />
|
||||
<div>
|
||||
<div className="inline-flex items-center">
|
||||
@@ -192,7 +192,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment, produc
|
||||
{option.alert && (
|
||||
<div className="mt-2 flex items-center space-x-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-2">
|
||||
<AlertCircleIcon className="h-5 w-5 text-amber-500" />
|
||||
<div className="text-amber-800">
|
||||
<div className=" text-amber-800">
|
||||
<p className="text-xs font-semibold">
|
||||
Your {option.id} is not yet connected to Formbricks.
|
||||
</p>
|
||||
@@ -217,7 +217,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment, produc
|
||||
{promotedFeaturesString && (
|
||||
<div className="mt-2 flex items-center space-x-3 rounded-b-lg border border-slate-200 bg-slate-50/50 px-4 py-2">
|
||||
<AlertCircleIcon className="h-5 w-5 text-slate-500" />
|
||||
<div className="text-slate-500">
|
||||
<div className=" text-slate-500">
|
||||
<p className="text-xs">
|
||||
You can also use Formbricks to run {promotedFeaturesString} surveys. Create a new product for
|
||||
your {promotedFeaturesString} to use this feature.
|
||||
|
||||
@@ -285,13 +285,11 @@ export const LogicEditor = ({
|
||||
<div className="mt-2 space-y-3">
|
||||
{question?.logic?.map((logic, logicIdx) => (
|
||||
<div key={logicIdx} className="flex items-center space-x-2 space-y-1 text-xs xl:text-sm">
|
||||
<div>
|
||||
<CornerDownRightIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<CornerDownRightIcon className="h-4 w-4" />
|
||||
<p className="text-slate-800">If this answer</p>
|
||||
|
||||
<Select value={logic.condition} onValueChange={(e) => updateLogic(logicIdx, { condition: e })}>
|
||||
<SelectTrigger className="min-w-fit flex-1">
|
||||
<SelectTrigger className=" min-w-fit flex-1">
|
||||
<SelectValue placeholder="Select condition" className="text-xs lg:text-sm" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -369,7 +367,7 @@ export const LogicEditor = ({
|
||||
<Select
|
||||
value={logic.destination}
|
||||
onValueChange={(e) => updateLogic(logicIdx, { destination: e })}>
|
||||
<SelectTrigger className="w-fit overflow-hidden">
|
||||
<SelectTrigger className="w-fit overflow-hidden ">
|
||||
<SelectValue placeholder="Select question" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -393,12 +391,11 @@ export const LogicEditor = ({
|
||||
<SelectItem value="end">End of survey</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div>
|
||||
<TrashIcon
|
||||
className="h-4 w-4 cursor-pointer text-slate-400"
|
||||
onClick={() => deleteLogic(logicIdx)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TrashIcon
|
||||
className="h-4 w-4 cursor-pointer text-slate-400"
|
||||
onClick={() => deleteLogic(logicIdx)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex flex-wrap items-center space-x-2 py-1 text-sm">
|
||||
|
||||
@@ -4,7 +4,6 @@ import { PlusIcon } from "lucide-react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyNPSQuestion } from "@formbricks/types/surveys";
|
||||
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
|
||||
@@ -133,16 +132,6 @@ export const NPSQuestionForm = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AdvancedOptionToggle
|
||||
isChecked={question.isColorCodingEnabled}
|
||||
onToggle={() => updateQuestion(questionIdx, { isColorCodingEnabled: !question.isColorCodingEnabled })}
|
||||
htmlId="isColorCodingEnabled"
|
||||
title="Add color coding"
|
||||
description="Add red, orange and green color codes to the options."
|
||||
childBorder
|
||||
customContainerClass="p-0 mt-4"
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -136,7 +136,7 @@ export const QuestionCard = ({
|
||||
<div
|
||||
className={cn(
|
||||
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
|
||||
"flex w-full flex-row rounded-lg bg-white transition-all duration-300 ease-in-out"
|
||||
"flex flex-row rounded-lg bg-white transition-all duration-300 ease-in-out"
|
||||
)}
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
@@ -146,13 +146,13 @@ export const QuestionCard = ({
|
||||
{...attributes}
|
||||
className={cn(
|
||||
open ? "bg-slate-700" : "bg-slate-400",
|
||||
"top-0 w-[5%] rounded-l-lg p-2 text-center text-sm text-white hover:cursor-grab hover:bg-slate-600",
|
||||
isInvalid && "bg-red-400 hover:bg-red-600",
|
||||
"top-0 w-10 rounded-l-lg p-2 text-center text-sm text-white hover:cursor-grab hover:bg-slate-600",
|
||||
isInvalid && "bg-red-400 hover:bg-red-600",
|
||||
"flex flex-col items-center justify-between"
|
||||
)}>
|
||||
<span>{questionIdx + 1}</span>
|
||||
|
||||
<button className="opacity-0 hover:cursor-move group-hover:opacity-100">
|
||||
<button className="hidden hover:cursor-move group-hover:block">
|
||||
<GripIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -165,10 +165,10 @@ export const QuestionCard = ({
|
||||
setActiveQuestionId(null);
|
||||
}
|
||||
}}
|
||||
className="w-[95%] flex-1 rounded-r-lg border border-slate-200">
|
||||
className="flex-1 rounded-r-lg border border-slate-200">
|
||||
<Collapsible.CollapsibleTrigger
|
||||
asChild
|
||||
className={cn(open ? "" : " ", "flex cursor-pointer justify-between gap-4 p-4 hover:bg-slate-50")}>
|
||||
className={cn(open ? "" : " ", "flex cursor-pointer justify-between gap-4 p-4 hover:bg-slate-50")}>
|
||||
<div>
|
||||
<div className="flex grow">
|
||||
<div className="-ml-0.5 mr-3 h-6 min-w-[1.5rem] text-slate-400">
|
||||
|
||||
@@ -40,7 +40,7 @@ export const QuestionsDroppable = ({
|
||||
isFormbricksCloud,
|
||||
}: QuestionsDraggableProps) => {
|
||||
return (
|
||||
<div className="group mb-5 flex w-full flex-col gap-5">
|
||||
<div className="group mb-5 grid w-full gap-5">
|
||||
<SortableContext items={localSurvey.questions} strategy={verticalListSortingStrategy}>
|
||||
{localSurvey.questions.map((question, questionIdx) => (
|
||||
<QuestionCard
|
||||
|
||||
@@ -55,7 +55,7 @@ export const QuestionsAudienceTabs = ({
|
||||
onClick={() => setActiveId(tab.id)}
|
||||
className={cn(
|
||||
tab.id === activeId
|
||||
? "border-brand-dark border-b-2 font-semibold text-slate-900"
|
||||
? " border-brand-dark border-b-2 font-semibold text-slate-900"
|
||||
: "text-slate-500 hover:text-slate-700",
|
||||
"flex h-full items-center px-3 text-sm font-medium"
|
||||
)}
|
||||
|
||||
@@ -335,8 +335,8 @@ export const QuestionsView = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-16 w-full px-5 py-4">
|
||||
<div className="mb-5 flex w-full flex-col gap-5">
|
||||
<div className="mt-16 px-5 py-4">
|
||||
<div className="mb-5 flex flex-col gap-5">
|
||||
<EditWelcomeCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { HashIcon, PlusIcon, SmileIcon, StarIcon } from "lucide-react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyRatingQuestion } from "@formbricks/types/surveys";
|
||||
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
@@ -94,13 +93,7 @@ export const RatingQuestionForm = ({
|
||||
{ label: "Smiley", value: "smiley", icon: SmileIcon },
|
||||
]}
|
||||
defaultValue={question.scale || "number"}
|
||||
onSelect={(option) => {
|
||||
if (option.value === "star") {
|
||||
updateQuestion(questionIdx, { scale: option.value, isColorCodingEnabled: false });
|
||||
return;
|
||||
}
|
||||
updateQuestion(questionIdx, { scale: option.value });
|
||||
}}
|
||||
onSelect={(option) => updateQuestion(questionIdx, { scale: option.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,20 +168,6 @@ export const RatingQuestionForm = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{question.scale !== "star" && (
|
||||
<AdvancedOptionToggle
|
||||
isChecked={question.isColorCodingEnabled}
|
||||
onToggle={() =>
|
||||
updateQuestion(questionIdx, { isColorCodingEnabled: !question.isColorCodingEnabled })
|
||||
}
|
||||
htmlId="isColorCodingEnabled"
|
||||
title="Add color coding"
|
||||
description="Add red, orange and green color codes to the options."
|
||||
childBorder
|
||||
customContainerClass="p-0 mt-4"
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -315,7 +315,7 @@ export const ResponseOptionsCard = ({
|
||||
onOpenChange={setOpen}
|
||||
className={cn(
|
||||
open ? "" : "hover:bg-slate-50",
|
||||
"w-full space-y-2 rounded-lg border border-slate-300 bg-white"
|
||||
"w-full space-y-2 rounded-lg border border-slate-300 bg-white "
|
||||
)}>
|
||||
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
|
||||
<div className="inline-flex px-4 py-4">
|
||||
@@ -415,7 +415,7 @@ export const ResponseOptionsCard = ({
|
||||
description="Change the message visitors see when the survey is closed."
|
||||
childBorder={true}>
|
||||
<div className="flex w-full items-center space-x-1 p-4 pb-4">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<Label htmlFor="headline">Heading</Label>
|
||||
<Input
|
||||
autoFocus
|
||||
@@ -447,7 +447,7 @@ export const ResponseOptionsCard = ({
|
||||
description="Allow only 1 response per survey link."
|
||||
childBorder={true}>
|
||||
<div className="flex w-full items-center space-x-1 p-4 pb-4">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<div className="row mb-2 flex cursor-default items-center space-x-2">
|
||||
<Label htmlFor="howItWorks">How it works</Label>
|
||||
</div>
|
||||
@@ -487,7 +487,7 @@ export const ResponseOptionsCard = ({
|
||||
/>
|
||||
<Label htmlFor="headline">URL Encryption</Label>
|
||||
<div>
|
||||
<div className="mt-2 flex items-center space-x-1">
|
||||
<div className="mt-2 flex items-center space-x-1 ">
|
||||
<Switch
|
||||
id="encryption-switch"
|
||||
checked={singleUseEncryption}
|
||||
@@ -515,7 +515,7 @@ export const ResponseOptionsCard = ({
|
||||
description="Only let people with a real email respond."
|
||||
childBorder={true}>
|
||||
<div className="flex w-full items-center space-x-1 p-4 pb-4">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<Label htmlFor="howItWorks">How it works</Label>
|
||||
<p className="mb-4 mt-2 text-sm text-slate-500">
|
||||
Respondants will receive the survey link via email.
|
||||
|
||||
@@ -126,7 +126,7 @@ export const SurveyEditor = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="flex h-full flex-col">
|
||||
<SurveyMenuBar
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
localSurvey={localSurvey}
|
||||
@@ -141,9 +141,7 @@ export const SurveyEditor = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
<div className="relative z-0 flex flex-1 overflow-hidden">
|
||||
<main
|
||||
className="relative z-0 w-1/2 flex-1 overflow-y-auto focus:outline-none"
|
||||
ref={surveyEditorRef}>
|
||||
<main className="relative z-0 flex-1 overflow-y-auto focus:outline-none" ref={surveyEditorRef}>
|
||||
<QuestionsAudienceTabs
|
||||
activeId={activeView}
|
||||
setActiveId={setActiveView}
|
||||
|
||||
@@ -231,7 +231,7 @@ export const SurveyMenuBar = ({
|
||||
const updatedSurvey = { ...localSurvey, name: e.target.value };
|
||||
setLocalSurvey(updatedSurvey);
|
||||
}}
|
||||
className="w-72 border-white hover:border-slate-200"
|
||||
className="w-72 border-white hover:border-slate-200 "
|
||||
/>
|
||||
</div>
|
||||
{responseCount > 0 && (
|
||||
@@ -239,14 +239,16 @@ export const SurveyMenuBar = ({
|
||||
<TooltipProvider delayDuration={50}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<AlertTriangleIcon className="h-5 w-5 text-amber-400" />
|
||||
<AlertTriangleIcon className=" h-5 w-5 text-amber-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={"top"} className="lg:hidden">
|
||||
<p className="py-2 text-center text-xs text-slate-500 dark:text-slate-400">{cautionText}</p>
|
||||
<p className="py-2 text-center text-xs text-slate-500 dark:text-slate-400 ">
|
||||
{cautionText}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<p className="hidden pl-1 text-xs md:text-sm lg:block">{cautionText}</p>
|
||||
<p className=" hidden pl-1 text-xs md:text-sm lg:block">{cautionText}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 flex sm:ml-4 sm:mt-0">
|
||||
|
||||
@@ -275,7 +275,7 @@ export const WhenToSendCard = ({
|
||||
childBorder={true}>
|
||||
<label
|
||||
htmlFor="triggerDelay"
|
||||
className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-slate-700">
|
||||
Wait
|
||||
|
||||
@@ -44,7 +44,7 @@ export const TemplateContainerWithPreview = ({
|
||||
value={templateSearch ?? ""}
|
||||
onChange={(e) => setTemplateSearch(e.target.value)}
|
||||
placeholder={"Search..."}
|
||||
className="block rounded-md border border-slate-100 bg-white shadow-sm focus:border-slate-500 focus:outline-none focus:ring-0 sm:text-sm md:w-auto"
|
||||
className="block rounded-md border border-slate-100 bg-white shadow-sm focus:border-slate-500 focus:outline-none focus:ring-0 sm:text-sm md:w-auto "
|
||||
type="search"
|
||||
name="search"
|
||||
/>
|
||||
|
||||
@@ -71,23 +71,23 @@ export const AttributeActivityTab = ({ attributeClass }: EventActivityTabProps)
|
||||
<div className="col-span-1 space-y-3 rounded-lg border border-slate-100 bg-slate-50 p-2">
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">Created on</Label>
|
||||
<p className="text-xs text-slate-700">
|
||||
<p className=" text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(attributeClass.createdAt.toString())}
|
||||
</p>
|
||||
</div>{" "}
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">Last updated</Label>
|
||||
<p className="text-xs text-slate-700">
|
||||
<Label className=" text-xs font-normal text-slate-500">Last updated</Label>
|
||||
<p className=" text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(attributeClass.updatedAt.toString())}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="block text-xs font-normal text-slate-500">Type</Label>
|
||||
<div className="mt-1 flex items-center">
|
||||
<div className="mr-1.5 h-4 w-4 text-slate-600">
|
||||
<div className="mr-1.5 h-4 w-4 text-slate-600">
|
||||
<TagIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-700">{capitalizeFirstLetter(attributeClass.type)}</p>
|
||||
<p className="text-sm text-slate-700 ">{capitalizeFirstLetter(attributeClass.type)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -91,7 +91,7 @@ export const AttributeSettingsTab = async ({ attributeClass, setOpen }: Attribut
|
||||
) : (
|
||||
<>
|
||||
{" "}
|
||||
<ArchiveIcon className="mr-2 h-4 text-slate-600" />
|
||||
<ArchiveIcon className="mr-2 h-4 text-slate-600" />
|
||||
<span>Archive</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ export const AttributeTableHeading = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="grid h-12 grid-cols-5 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6">Name</div>
|
||||
<div className="col-span-3 pl-6 ">Name</div>
|
||||
<div className="hidden text-center sm:block">Created</div>
|
||||
<div className="hidden text-center sm:block">Last Updated</div>
|
||||
</div>
|
||||
|
||||
@@ -11,8 +11,8 @@ const Loading = () => {
|
||||
<PeopleSecondaryNavigation activeId="attributes" loading />
|
||||
</PageHeader>
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="grid h-12 grid-cols-5 content-center border-b text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6">Name</div>
|
||||
<div className="grid h-12 grid-cols-5 content-center border-b text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6 ">Name</div>
|
||||
<div className="text-center">Created</div>
|
||||
<div className="text-center">Last Updated</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Label } from "@formbricks/ui/Label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui/Popover";
|
||||
|
||||
export const ActivityItemIcon = ({ actionItem }: { actionItem: TAction }) => (
|
||||
<div className="h-12 w-12 rounded-full bg-white p-3 text-slate-500 duration-100 ease-in-out group-hover:scale-110 group-hover:text-slate-600">
|
||||
<div className="h-12 w-12 rounded-full bg-white p-3 text-slate-500 duration-100 ease-in-out group-hover:scale-110 group-hover:text-slate-600">
|
||||
<div>
|
||||
{actionItem.actionClass?.type === "code" && <CodeIcon className="h-5 w-5" />}
|
||||
{actionItem.actionClass?.type === "noCode" && <MousePointerClickIcon className="h-5 w-5" />}
|
||||
@@ -47,7 +47,7 @@ export const ActivityItemPopover = ({
|
||||
{actionItem && (
|
||||
<div>
|
||||
<Label className="font-normal text-slate-400">Action Label</Label>
|
||||
<p className="mb-2 text-sm font-medium text-slate-900">{actionItem.actionClass!.name}</p>
|
||||
<p className=" mb-2 text-sm font-medium text-slate-900">{actionItem.actionClass!.name}</p>
|
||||
<Label className="font-normal text-slate-400">Action Description</Label>
|
||||
<p className="text-sm font-medium text-slate-900">{actionItem.actionClass!.description}</p>
|
||||
<Label className="font-normal text-slate-400">Action Type</Label>
|
||||
|
||||
@@ -66,7 +66,7 @@ const Loading = () => {
|
||||
</div>
|
||||
</div>
|
||||
<section className="pb-24 pt-6">
|
||||
<div className="grid grid-cols-1 gap-x-8 md:grid-cols-4">
|
||||
<div className="grid grid-cols-1 gap-x-8 md:grid-cols-4">
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-bold text-slate-700">Attributes</h2>
|
||||
<div>
|
||||
@@ -105,14 +105,14 @@ const Loading = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group space-y-4 rounded-lg bg-white p-6">
|
||||
<div className="group space-y-4 rounded-lg bg-white p-6 ">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="h-12 w-12 flex-shrink-0 rounded-full bg-slate-100"></div>
|
||||
<div className="h-6 w-full rounded-full bg-slate-100"></div>
|
||||
<div className=" h-6 w-full rounded-full bg-slate-100"></div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="h-12 w-full rounded-full bg-slate-100"></div>
|
||||
<div className="flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100">
|
||||
<div className=" flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100">
|
||||
<span className="animate-pulse text-center">Loading user responses</span>
|
||||
</div>
|
||||
<div className="h-12 w-full rounded-full bg-slate-50/50"></div>
|
||||
|
||||
@@ -63,7 +63,7 @@ const Page = async ({ params }) => {
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={getPersonIdentifier(person, attributes)} cta={getDeletePersonButton()} />
|
||||
<section className="pb-24 pt-6">
|
||||
<div className="grid grid-cols-1 gap-x-8 md:grid-cols-4">
|
||||
<div className="grid grid-cols-1 gap-x-8 md:grid-cols-4">
|
||||
<AttributesSection personId={params.personId} />
|
||||
<ResponseSection
|
||||
environment={environment}
|
||||
|
||||
@@ -41,7 +41,7 @@ export const PeopleSecondaryNavigation = async ({
|
||||
label: "Attributes",
|
||||
href: `/environments/${environmentId}/attributes`,
|
||||
// hide attributes tab if it's being used in the loading state or if the product's channel is website or link
|
||||
hidden: loading || !!(currentProductChannel && currentProductChannel !== "app"),
|
||||
hidden: !!(!loading && currentProductChannel && currentProductChannel !== "app"),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export const PersonCard = async ({ person }: { person: TPerson }) => {
|
||||
href={`/environments/${person.environmentId}/people/${person.id}`}
|
||||
key={person.id}
|
||||
className="w-full">
|
||||
<div className="m-2 grid h-16 grid-cols-7 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||
<div className="m-2 grid h-16 grid-cols-7 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="ph-no-capture h-10 w-10 flex-shrink-0">
|
||||
|
||||
@@ -64,7 +64,7 @@ const Page = async ({
|
||||
) : (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="grid h-12 grid-cols-7 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6">User</div>
|
||||
<div className="col-span-3 pl-6 ">User</div>
|
||||
<div className="col-span-2 hidden text-center sm:block">User ID</div>
|
||||
<div className="col-span-2 hidden text-center sm:block">Email</div>
|
||||
</div>
|
||||
|
||||
@@ -35,13 +35,13 @@ export const SegmentActivityTab = ({ currentSegment }: SegmentActivityTabProps)
|
||||
<div className="col-span-1 space-y-3 rounded-lg border border-slate-100 bg-slate-50 p-2">
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">Created on</Label>
|
||||
<p className="text-xs text-slate-700">
|
||||
<p className=" text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(currentSegment.createdAt?.toString())}
|
||||
</p>
|
||||
</div>{" "}
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">Last updated</Label>
|
||||
<p className="text-xs text-slate-700">
|
||||
<p className=" text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(currentSegment.updatedAt?.toString())}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -121,20 +121,20 @@ export const EventActivityTab = ({
|
||||
<div className="col-span-1 space-y-3 rounded-lg border border-slate-100 bg-slate-50 p-2">
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">Created on</Label>
|
||||
<p className="text-xs text-slate-700">
|
||||
<p className=" text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(actionClass.createdAt?.toString())}
|
||||
</p>
|
||||
</div>{" "}
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">Last updated</Label>
|
||||
<p className="text-xs text-slate-700">
|
||||
<Label className=" text-xs font-normal text-slate-500">Last updated</Label>
|
||||
<p className=" text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(actionClass.updatedAt?.toString())}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="block text-xs font-normal text-slate-500">Type</Label>
|
||||
<div className="mt-1 flex items-center">
|
||||
<div className="mr-1.5 h-4 w-4 text-slate-600">
|
||||
<div className="mr-1.5 h-4 w-4 text-slate-600">
|
||||
{actionClass.type === "code" ? (
|
||||
<Code2Icon className="h-5 w-5" />
|
||||
) : actionClass.type === "noCode" ? (
|
||||
@@ -143,7 +143,7 @@ export const EventActivityTab = ({
|
||||
<SparklesIcon className="h-5 w-5" />
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-sm text-slate-700">{capitalizeFirstLetter(actionClass.type)}</p>
|
||||
<p className="text-sm text-slate-700 ">{capitalizeFirstLetter(actionClass.type)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ export const ActionTableHeading = () => {
|
||||
<>
|
||||
<div className="grid h-12 grid-cols-6 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
|
||||
<span className="sr-only">Edit</span>
|
||||
<div className="col-span-4 pl-6">User Actions</div>
|
||||
<div className="col-span-4 pl-6 ">User Actions</div>
|
||||
<div className="col-span-2 text-center">Created</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -9,7 +9,7 @@ const Loading = () => {
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="grid h-12 grid-cols-6 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
|
||||
<span className="sr-only">Edit</span>
|
||||
<div className="col-span-4 pl-6">User Actions</div>
|
||||
<div className="col-span-4 pl-6 ">User Actions</div>
|
||||
<div className="col-span-2 text-center">Created</div>
|
||||
</div>
|
||||
{[...Array(3)].map((_, index) => (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
||||
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
|
||||
import type { Session } from "next-auth";
|
||||
import { getEnterpriseLicense } from "@formbricks/ee/lib/service";
|
||||
import { getIsMultiOrgEnabled } from "@formbricks/ee/lib/service";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
@@ -15,7 +15,6 @@ import { getProducts } from "@formbricks/lib/product/service";
|
||||
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
|
||||
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
||||
import { LimitsReachedBanner } from "@formbricks/ui/LimitsReachedBanner";
|
||||
import { PendingDowngradeBanner } from "@formbricks/ui/PendingDowngradeBanner";
|
||||
|
||||
interface EnvironmentLayoutProps {
|
||||
environmentId: string;
|
||||
@@ -44,22 +43,15 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { features, lastChecked, isPendingDowngrade, active } = await getEnterpriseLicense();
|
||||
|
||||
const isMultiOrgEnabled = features?.isMultiOrgEnabled ?? false;
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
|
||||
const currentProductChannel =
|
||||
products.find((product) => product.id === environment.productId)?.config.channel ?? null;
|
||||
|
||||
let peopleCount = 0;
|
||||
let responseCount = 0;
|
||||
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
[peopleCount, responseCount] = await Promise.all([
|
||||
getMonthlyActiveOrganizationPeopleCount(organization.id),
|
||||
getMonthlyOrganizationResponseCount(organization.id),
|
||||
]);
|
||||
}
|
||||
const [peopleCount, responseCount] = await Promise.all([
|
||||
getMonthlyActiveOrganizationPeopleCount(organization.id),
|
||||
getMonthlyOrganizationResponseCount(organization.id),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen min-h-screen flex-col overflow-hidden">
|
||||
@@ -68,19 +60,11 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
||||
{IS_FORMBRICKS_CLOUD && (
|
||||
<LimitsReachedBanner
|
||||
organization={organization}
|
||||
environmentId={environment.id}
|
||||
peopleCount={peopleCount}
|
||||
responseCount={responseCount}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PendingDowngradeBanner
|
||||
lastChecked={lastChecked}
|
||||
isPendingDowngrade={isPendingDowngrade ?? false}
|
||||
active={active}
|
||||
environmentId={environment.id}
|
||||
/>
|
||||
|
||||
<div className="flex h-full">
|
||||
<MainNavigation
|
||||
environment={environment}
|
||||
|
||||
@@ -286,7 +286,7 @@ export const MainNavigation = ({
|
||||
<div>
|
||||
<p
|
||||
className={cn(
|
||||
"ph-no-capture ph-no-capture -mb-0.5 text-sm font-bold text-slate-700 transition-opacity duration-200",
|
||||
"ph-no-capture ph-no-capture -mb-0.5 text-sm font-bold text-slate-700 transition-opacity duration-200 ",
|
||||
isTextVisible ? "opacity-0" : "opacity-100"
|
||||
)}>
|
||||
{product.name}
|
||||
|
||||
@@ -3,16 +3,16 @@ export const NavbarLoading = () => {
|
||||
<div>
|
||||
<div className="flex justify-between space-x-4 px-4 py-2">
|
||||
<div className="flex">
|
||||
<div className="mx-2 h-8 w-8 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className="mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className="mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className="mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className="mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className="mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className=" mx-2 h-8 w-8 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="mx-2 h-8 w-8 animate-pulse rounded-full bg-slate-200"></div>
|
||||
<div className="mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className=" mx-2 h-8 w-8 animate-pulse rounded-full bg-slate-200"></div>
|
||||
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ export const UrlShortenerForm = ({ webAppUrl }: { webAppUrl: string }) => {
|
||||
<form onSubmit={handleSubmit(shortenUrl)}>
|
||||
<div className="w-full space-y-2 rounded-lg">
|
||||
<Label>Paste Survey Link</Label>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-3 ">
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder={`${webAppUrl}...`}
|
||||
@@ -92,9 +92,9 @@ export const UrlShortenerForm = ({ webAppUrl }: { webAppUrl: string }) => {
|
||||
{shortUrl && (
|
||||
<div className="w-full space-y-2 rounded-lg">
|
||||
<Label>Short Link</Label>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-3 ">
|
||||
<span
|
||||
className="h-10 w-full cursor-pointer rounded-md border border-slate-300 bg-slate-100 px-3 py-2 text-sm text-slate-700"
|
||||
className="h-10 w-full cursor-pointer rounded-md border border-slate-300 bg-slate-100 px-3 py-2 text-sm text-slate-700"
|
||||
onClick={() => {
|
||||
if (shortUrl) {
|
||||
copyShortUrlToClipboard();
|
||||
|
||||
@@ -291,7 +291,7 @@ export const AddIntegrationModal = ({
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="Surveys">Questions</Label>
|
||||
<div className="mt-1 max-h-[15vh] overflow-y-auto rounded-lg border border-slate-200">
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map(
|
||||
(question) => (
|
||||
|
||||
@@ -72,7 +72,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||
return (
|
||||
<div className="mt-6 flex w-full flex-col items-center justify-center p-6">
|
||||
<div className="flex w-full justify-end gap-x-6">
|
||||
<div className="flex items-center">
|
||||
<div className=" flex items-center">
|
||||
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
|
||||
<span
|
||||
className="cursor-pointer text-slate-500"
|
||||
|
||||
@@ -229,7 +229,7 @@ export const AddIntegrationModal = ({
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="Surveys">Questions</Label>
|
||||
<div className="mt-1 max-h-[15vh] overflow-y-auto rounded-lg border border-slate-200">
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map(
|
||||
(question) => (
|
||||
|
||||
@@ -98,7 +98,7 @@ export const ManageIntegration = ({
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="m-2 grid h-16 cursor-pointer grid-cols-8 content-center rounded-lg hover:bg-slate-100"
|
||||
className="m-2 grid h-16 cursor-pointer grid-cols-8 content-center rounded-lg hover:bg-slate-100"
|
||||
onClick={() => {
|
||||
editIntegration(index);
|
||||
}}>
|
||||
|
||||
@@ -15,9 +15,9 @@ const Loading = () => {
|
||||
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-12 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-4 text-center">Survey</div>
|
||||
<div className="col-span-4 text-center ">Survey</div>
|
||||
<div className="col-span-4 text-center">Google Sheet Name</div>
|
||||
<div className="col-span-2 text-center">Questions</div>
|
||||
<div className="col-span-2 text-center ">Questions</div>
|
||||
<div className="col-span-2 text-center">Updated At</div>
|
||||
</div>
|
||||
<div className="grid-cols-7">
|
||||
|
||||
@@ -15,7 +15,7 @@ const Loading = () => {
|
||||
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-2 text-center">Survey</div>
|
||||
<div className="col-span-2 text-center ">Survey</div>
|
||||
<div className="col-span-2 text-center">Database Name</div>
|
||||
<div className="col-span-2 text-center">Updated At</div>
|
||||
</div>
|
||||
|
||||
@@ -231,7 +231,7 @@ export const AddChannelMappingModal = ({
|
||||
<div>
|
||||
<div>
|
||||
<Label htmlFor="Surveys">Questions</Label>
|
||||
<div className="mt-1 max-h-[15vh] overflow-y-auto rounded-lg border border-slate-200">
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions?.map(
|
||||
(question) => (
|
||||
|
||||
@@ -75,13 +75,13 @@ export const WebhookOverviewTab = ({ webhook, surveys }: ActivityTabProps) => {
|
||||
<div className="col-span-1 space-y-3 rounded-lg border border-slate-100 bg-slate-50 p-2">
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">Created on</Label>
|
||||
<p className="text-xs text-slate-700">
|
||||
<p className=" text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(webhook.createdAt?.toString())}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">Last updated</Label>
|
||||
<p className="text-xs text-slate-700">
|
||||
<Label className=" text-xs font-normal text-slate-500">Last updated</Label>
|
||||
<p className=" text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(webhook.updatedAt?.toString())}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,10 @@ export const WebhookTableHeading = () => {
|
||||
<>
|
||||
<div className="grid h-12 grid-cols-12 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<span className="sr-only">Edit</span>
|
||||
<div className="col-span-3 pl-6">Webhook</div>
|
||||
<div className="col-span-3 pl-6 ">Webhook</div>
|
||||
<div className="col-span-1 text-center">Source</div>
|
||||
<div className="col-span-4 text-center">Surveys</div>
|
||||
<div className="col-span-2 text-center">Triggers</div>
|
||||
<div className="col-span-2 text-center ">Triggers</div>
|
||||
<div className="col-span-2 text-center">Updated</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -8,13 +8,13 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
const LoadingCard = ({ title, description, skeletonLines }) => {
|
||||
return (
|
||||
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 text-left shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 text-left shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="text-lg font-medium leading-6">{title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">{description}</p>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="rounded-lg px-4">
|
||||
<div className="rounded-lg px-4 ">
|
||||
{skeletonLines.map((line, index) => (
|
||||
<div key={index} className="mt-4">
|
||||
<div
|
||||
@@ -107,7 +107,7 @@ const Loading = () => {
|
||||
<div>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Configuration">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{navigation.map((navElem) => (
|
||||
<div
|
||||
@@ -127,7 +127,7 @@ const Loading = () => {
|
||||
<div className="justify-self-end"></div>
|
||||
</div>
|
||||
</PageHeader>
|
||||
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
|
||||
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
|
||||
{cards.map((card, index) => (
|
||||
<LoadingCard key={index} {...card} />
|
||||
))}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SetupInstructions } from "@/app/(app)/environments/[environmentId]/prod
|
||||
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getMultiLanguagePermission } from "@formbricks/ee/lib/service";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
@@ -61,7 +61,12 @@ const Page = async ({ params }) => {
|
||||
title="How to setup"
|
||||
description="Follow these steps to setup the Formbricks widget within your app"
|
||||
noPadding>
|
||||
<SetupInstructions type="app" environmentId={params.environmentId} webAppUrl={WEBAPP_URL} />
|
||||
<SetupInstructions
|
||||
environmentId={params.environmentId}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
type="app"
|
||||
/>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
</PageContentWrapper>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import packageJson from "@/package.json";
|
||||
import Link from "next/link";
|
||||
import "prismjs/themes/prism.css";
|
||||
import { useState } from "react";
|
||||
@@ -19,10 +20,16 @@ const tabs = [
|
||||
interface SetupInstructionsProps {
|
||||
environmentId: string;
|
||||
webAppUrl: string;
|
||||
isFormbricksCloud: boolean;
|
||||
type: "app" | "website";
|
||||
}
|
||||
|
||||
export const SetupInstructions = ({ environmentId, webAppUrl, type }: SetupInstructionsProps) => {
|
||||
export const SetupInstructions = ({
|
||||
environmentId,
|
||||
webAppUrl,
|
||||
isFormbricksCloud,
|
||||
type,
|
||||
}: SetupInstructionsProps) => {
|
||||
const [activeTab, setActiveTab] = useState(tabs[0].id);
|
||||
|
||||
return (
|
||||
@@ -166,6 +173,14 @@ if (typeof window !== "undefined") {
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
{!isFormbricksCloud && (
|
||||
<div>
|
||||
<hr className="my-3" />
|
||||
<p className="flex w-full justify-end text-sm text-slate-700">
|
||||
Formbricks version: {packageJson.version}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,13 +8,13 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
const LoadingCard = ({ title, description, skeletonLines }) => {
|
||||
return (
|
||||
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 text-left shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 text-left shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="text-lg font-medium leading-6">{title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">{description}</p>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="rounded-lg px-4">
|
||||
<div className="rounded-lg px-4 ">
|
||||
{skeletonLines.map((line, index) => (
|
||||
<div key={index} className="mt-4">
|
||||
<div
|
||||
@@ -107,7 +107,7 @@ const Loading = () => {
|
||||
<div>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Configuration">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{navigation.map((navElem) => (
|
||||
<div
|
||||
@@ -127,7 +127,7 @@ const Loading = () => {
|
||||
<div className="justify-self-end"></div>
|
||||
</div>
|
||||
</PageHeader>
|
||||
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
|
||||
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
|
||||
{cards.map((card, index) => (
|
||||
<LoadingCard key={index} {...card} />
|
||||
))}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SetupInstructions } from "@/app/(app)/environments/[environmentId]/prod
|
||||
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getMultiLanguagePermission } from "@formbricks/ee/lib/service";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
@@ -61,7 +61,12 @@ const Page = async ({ params }) => {
|
||||
title="How to setup"
|
||||
description="Follow these steps to setup the Formbricks widget within your website"
|
||||
noPadding>
|
||||
<SetupInstructions environmentId={params.environmentId} webAppUrl={WEBAPP_URL} type="website" />
|
||||
<SetupInstructions
|
||||
environmentId={params.environmentId}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
type="website"
|
||||
/>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
</PageContentWrapper>
|
||||
|
||||
@@ -90,7 +90,7 @@ export const EditAPIKeys = ({
|
||||
</div>
|
||||
<div className="grid-cols-9">
|
||||
{apiKeysLocal && apiKeysLocal.length === 0 ? (
|
||||
<div className="flex h-12 items-center justify-center whitespace-nowrap px-6 text-sm font-medium text-slate-400">
|
||||
<div className="flex h-12 items-center justify-center whitespace-nowrap px-6 text-sm font-medium text-slate-400 ">
|
||||
You don't have any API keys yet
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -9,12 +9,12 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
const LoadingCard = () => {
|
||||
return (
|
||||
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="h-6 w-full max-w-56 animate-pulse rounded-lg bg-gray-100 text-lg font-medium leading-6"></h3>
|
||||
<p className="mt-3 h-4 w-full max-w-80 animate-pulse rounded-lg bg-gray-100 text-sm text-slate-500"></p>
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="h-6 w-full max-w-56 animate-pulse rounded-lg bg-gray-100 text-lg font-medium leading-6"></h3>
|
||||
<p className="mt-3 h-4 w-full max-w-80 animate-pulse rounded-lg bg-gray-100 text-sm text-slate-500 "></p>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="rounded-lg px-4 pt-4">
|
||||
<div className="rounded-lg px-4 pt-4 ">
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-10 content-center rounded-t-lg bg-slate-100 px-6 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-4 sm:col-span-2">Label</div>
|
||||
@@ -26,7 +26,7 @@ const LoadingCard = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-start">
|
||||
<div className="mt-4 flex h-7 w-44 animate-pulse flex-col items-center justify-center rounded-md bg-black text-sm text-white">
|
||||
<div className="mt-4 flex h-7 w-44 animate-pulse flex-col items-center justify-center rounded-md bg-black text-sm text-white">
|
||||
Loading
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@ const Loading = () => {
|
||||
<div>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Configuration">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{navigation.map((navElem) => (
|
||||
<div
|
||||
@@ -111,7 +111,7 @@ const Loading = () => {
|
||||
<div className="justify-self-end"></div>
|
||||
</div>
|
||||
</PageHeader>
|
||||
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
|
||||
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
|
||||
|
||||
<LoadingCard />
|
||||
</PageContentWrapper>
|
||||
|
||||
@@ -9,12 +9,12 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
const LoadingCard = ({ title, description, skeletonLines }) => {
|
||||
return (
|
||||
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="text-lg font-medium leading-6">{title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">{description}</p>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="rounded-lg px-4 py-4 pb-0 pt-2">
|
||||
<div className="rounded-lg px-4 py-4 pb-0 pt-2 ">
|
||||
{skeletonLines.map((line, index) => (
|
||||
<div key={index} className="mt-4">
|
||||
<div className={`animate-pulse rounded-full bg-slate-200 ${line.classes}`}></div>
|
||||
@@ -100,7 +100,7 @@ const Loading = () => {
|
||||
<div>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Configuration">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{navigation.map((navElem) => (
|
||||
<div
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
|
||||
import packageJson from "@/package.json";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { getMultiLanguagePermission } from "@formbricks/ee/lib/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
@@ -75,12 +73,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
|
||||
description="Delete product with all surveys, responses, people, actions and attributes. This cannot be undone.">
|
||||
<DeleteProduct environmentId={params.environmentId} product={product} />
|
||||
</SettingsCard>
|
||||
<div>
|
||||
<SettingsId title="Product ID" id={product.id}></SettingsId>
|
||||
{!IS_FORMBRICKS_CLOUD && (
|
||||
<SettingsId title="Formbricks version" id={packageJson.version}></SettingsId>
|
||||
)}
|
||||
</div>
|
||||
<SettingsId title="Product" id={product.id}></SettingsId>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ export const EditFormbricksBranding = ({
|
||||
|
||||
return (
|
||||
<div className="w-full items-center space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<Switch
|
||||
id={`branding-${type}`}
|
||||
checked={isBrandingEnabled}
|
||||
|
||||
@@ -125,13 +125,7 @@ export const EditLogo = ({ product, environmentId, isViewer }: EditLogoProps) =>
|
||||
/>
|
||||
)}
|
||||
|
||||
<Input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg, image/png"
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<Input ref={fileInputRef} type="file" accept="image/*" className="hidden" onChange={handleFileChange} />
|
||||
|
||||
{isEditing && logoUrl && (
|
||||
<>
|
||||
|
||||
@@ -75,7 +75,7 @@ const Loading = () => {
|
||||
<div>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Configuration">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{navigation.map((navElem) => (
|
||||
<div
|
||||
@@ -178,7 +178,7 @@ const Loading = () => {
|
||||
<div className="flex cursor-not-allowed select-none">
|
||||
<RadioGroup>
|
||||
{placements.map((placement) => (
|
||||
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap ">
|
||||
<RadioGroupItem
|
||||
className="cursor-not-allowed select-none"
|
||||
id={placement.value}
|
||||
|
||||
@@ -81,26 +81,24 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
|
||||
<EditPlacementForm product={product} environmentId={params.environmentId} />
|
||||
</SettingsCard>
|
||||
)}
|
||||
<SettingsCard
|
||||
title="Formbricks Branding"
|
||||
description="We love your support but understand if you toggle it off.">
|
||||
<div className="space-y-4">
|
||||
{currentProductChannel !== "link" && (
|
||||
<SettingsCard
|
||||
title="Formbricks Branding"
|
||||
description="We love your support but understand if you toggle it off.">
|
||||
<EditFormbricksBranding
|
||||
type="linkSurvey"
|
||||
product={product}
|
||||
canRemoveBranding={canRemoveLinkBranding}
|
||||
environmentId={params.environmentId}
|
||||
/>
|
||||
{currentProductChannel !== "link" && (
|
||||
<EditFormbricksBranding
|
||||
type="inAppSurvey"
|
||||
product={product}
|
||||
canRemoveBranding={canRemoveInAppBranding}
|
||||
environmentId={params.environmentId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
<EditFormbricksBranding
|
||||
type="inAppSurvey"
|
||||
product={product}
|
||||
canRemoveBranding={canRemoveInAppBranding}
|
||||
environmentId={params.environmentId}
|
||||
/>
|
||||
</SettingsCard>
|
||||
)}
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const LoadingCard = ({ title, description, skeletonLines }) => {
|
||||
return (
|
||||
<div className="my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="text-lg font-medium leading-6">{title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">{description}</p>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@ const Loading = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 border-b border-slate-200">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{pages.map((navElem) => (
|
||||
<div
|
||||
|
||||
@@ -144,7 +144,7 @@ export const EditProfileAvatarForm = ({ session, environmentId }: EditProfileAva
|
||||
inputRef.current = e;
|
||||
}}
|
||||
className="hidden"
|
||||
accept="image/jpeg, image/png"
|
||||
accept="image/*"
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.files);
|
||||
form.handleSubmit(onSubmit)();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const LoadingCard = ({ title, description, skeletonLines }) => {
|
||||
return (
|
||||
<div className="my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="text-lg font-medium leading-6">{title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">{description}</p>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@ const Loading = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 border-b border-slate-200">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{pages.map((navElem) => (
|
||||
<div
|
||||
|
||||
@@ -9,7 +9,7 @@ const Loading = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 border-b border-slate-200">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{pages.map((navElem) => (
|
||||
<div
|
||||
@@ -22,8 +22,8 @@ const Loading = () => {
|
||||
<div className="justify-self-end"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-8 h-64 animate-pulse rounded-xl bg-slate-200"></div>
|
||||
<div className="my-8 h-96 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className=" my-8 h-64 animate-pulse rounded-xl bg-slate-200 "></div>
|
||||
<div className=" my-8 h-96 animate-pulse rounded-md bg-slate-200"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ const Loading = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 border-b border-slate-200">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{pages.map((navElem) => (
|
||||
<div
|
||||
@@ -22,8 +22,8 @@ const Loading = () => {
|
||||
<div className="justify-self-end"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-8 h-64 animate-pulse rounded-xl bg-slate-200"></div>
|
||||
<div className="my-8 h-96 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className=" my-8 h-64 animate-pulse rounded-xl bg-slate-200 "></div>
|
||||
<div className=" my-8 h-96 animate-pulse rounded-md bg-slate-200"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmen
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getEnterpriseLicense } from "@formbricks/ee/lib/service";
|
||||
import { getIsEnterpriseEdition } from "@formbricks/ee/lib/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
@@ -37,7 +37,7 @@ const Page = async ({ params }) => {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { active: isEnterpriseEdition } = await getEnterpriseLicense();
|
||||
const isEnterpriseEdition = await getIsEnterpriseEdition();
|
||||
|
||||
const paidFeatures = [
|
||||
{
|
||||
@@ -166,7 +166,7 @@ const Page = async ({ params }) => {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="my-6 text-sm text-slate-700">
|
||||
<p className="my-6 text-sm text-slate-700">
|
||||
No call needed, no strings attached: Request a free 30-day trial license to test all features
|
||||
by filling out this form:
|
||||
</p>
|
||||
@@ -176,7 +176,7 @@ const Page = async ({ params }) => {
|
||||
target="_blank">
|
||||
Request 30-day Trial License
|
||||
</Button>
|
||||
<p className="mt-2 text-xs text-slate-500">No credit card. No sales call. Just test it :)</p>
|
||||
<p className="mt-2 text-xs text-slate-500">No credit card. No sales call. Just test it :)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,11 +69,11 @@ export const BulkInviteTab = ({ setOpen, onSubmit, canDoRoleManagement }: BulkIn
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className="relative flex h-52 cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border border-slate-300 bg-slate-50 transition-colors hover:bg-slate-100"
|
||||
className="relative flex h-52 cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border border-slate-300 bg-slate-50 transition-colors hover:bg-slate-100"
|
||||
onClick={() => fileInputRef.current?.click()}>
|
||||
{csvFile ? (
|
||||
<XIcon
|
||||
className="absolute right-4 top-4 h-6 w-6 cursor-pointer text-neutral-500"
|
||||
className="absolute right-4 top-4 h-6 w-6 cursor-pointer text-neutral-500"
|
||||
onClick={removeFile}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -41,7 +41,7 @@ export const MembersInfo = async ({
|
||||
<div className="ph-no-capture col-span-5 flex flex-col justify-center break-all">
|
||||
<p>{member.name}</p>
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-5 flex flex-col justify-center break-all">
|
||||
<div className="ph-no-capture col-span-5 flex flex-col justify-center break-all">
|
||||
{member.email}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ export const OrganizationActions = ({
|
||||
} catch (err) {
|
||||
toast.error(`Error: ${err.message}`);
|
||||
}
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,14 +22,7 @@ export const IndividualInviteTab = ({
|
||||
isFormbricksCloud,
|
||||
environmentId,
|
||||
}: IndividualInviteTabProps) => {
|
||||
const {
|
||||
register,
|
||||
getValues,
|
||||
handleSubmit,
|
||||
reset,
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<{
|
||||
const { register, getValues, handleSubmit, reset, control } = useForm<{
|
||||
name: string;
|
||||
email: string;
|
||||
role: MembershipRole;
|
||||
@@ -38,13 +31,13 @@ export const IndividualInviteTab = ({
|
||||
const submitEventClass = async () => {
|
||||
const data = getValues();
|
||||
data.role = data.role || MembershipRole.Admin;
|
||||
await onSubmit([data]);
|
||||
onSubmit([data]);
|
||||
setOpen(false);
|
||||
reset();
|
||||
};
|
||||
return (
|
||||
<form onSubmit={handleSubmit(submitEventClass)}>
|
||||
<div className="flex justify-between rounded-lg">
|
||||
<div className="flex justify-between rounded-lg ">
|
||||
<div className="w-full space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="memberNameInput">Full Name</Label>
|
||||
@@ -93,7 +86,7 @@ export const IndividualInviteTab = ({
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="darkCTA" type="submit" size="sm" loading={isSubmitting}>
|
||||
<Button variant="darkCTA" type="submit" size="sm">
|
||||
Send Invitation
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,8 @@ import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
|
||||
const LoadingCard = ({ title, description, skeletonLines }) => {
|
||||
return (
|
||||
<div
|
||||
data-testid="members-loading-card"
|
||||
className="my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<div className="my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="text-lg font-medium leading-6">{title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">{description}</p>
|
||||
</div>
|
||||
@@ -52,7 +50,7 @@ const Loading = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 border-b border-slate-200">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{pages.map((navElem) => (
|
||||
<div
|
||||
|
||||
@@ -24,16 +24,16 @@ export const HiddenFieldsSummary = ({ environment, questionSummary }: HiddenFiel
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<div className={"align-center flex justify-between gap-4"}>
|
||||
<div className={"align-center flex justify-between gap-4 "}>
|
||||
<h3 className="pb-1 text-lg font-semibold text-slate-900 md:text-xl">{questionSummary.id}</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2 ">
|
||||
<MessageSquareTextIcon className="mr-2 h-4 w-4" />
|
||||
Hidden Field
|
||||
</div>
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2 ">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{questionSummary.responseCount} {questionSummary.responseCount === 1 ? "Response" : "Responses"}
|
||||
</div>
|
||||
@@ -48,7 +48,7 @@ export const HiddenFieldsSummary = ({ environment, questionSummary }: HiddenFiel
|
||||
{questionSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||
<div
|
||||
key={response.value}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
|
||||
<div className="pl-4 md:pl-6">
|
||||
{response.person ? (
|
||||
<Link
|
||||
|
||||
@@ -45,7 +45,7 @@ export const MatrixQuestionSummary = ({
|
||||
<tr>
|
||||
<th className="p-4 pb-3 pt-0 font-medium text-slate-400 dark:border-slate-600 dark:text-slate-200"></th>
|
||||
{columns.map((column) => (
|
||||
<th key={column} className="text-center font-medium">
|
||||
<th key={column} className="text-center font-medium ">
|
||||
<TooltipRenderer tooltipContent={getTooltipContent(column)} shouldRender={true}>
|
||||
<p className="max-w-40 overflow-hidden text-ellipsis whitespace-nowrap">{column}</p>
|
||||
</TooltipRenderer>
|
||||
@@ -56,7 +56,7 @@ export const MatrixQuestionSummary = ({
|
||||
<tbody>
|
||||
{questionSummary.data.map(({ rowLabel, columnPercentages }, rowIndex) => (
|
||||
<tr key={rowLabel}>
|
||||
<td className="max-w-60 overflow-hidden text-ellipsis whitespace-nowrap p-4">
|
||||
<td className=" max-w-60 overflow-hidden text-ellipsis whitespace-nowrap p-4">
|
||||
<TooltipRenderer tooltipContent={getTooltipContent(rowLabel)} shouldRender={true}>
|
||||
<p className="max-w-40 overflow-hidden text-ellipsis whitespace-nowrap">{rowLabel}</p>
|
||||
</TooltipRenderer>
|
||||
@@ -74,7 +74,7 @@ export const MatrixQuestionSummary = ({
|
||||
)}>
|
||||
<div
|
||||
style={{ backgroundColor: `rgba(0,196,184,${getOpacityLevel(percentage)})` }}
|
||||
className="hover:outline-brand-dark m-1 flex h-full w-40 cursor-default items-center justify-center rounded p-4 text-sm text-slate-950 hover:outline">
|
||||
className=" hover:outline-brand-dark m-1 flex h-full w-40 cursor-default items-center justify-center rounded p-4 text-sm text-slate-950 hover:outline">
|
||||
{percentage}
|
||||
</div>
|
||||
</TooltipRenderer>
|
||||
|
||||
@@ -75,8 +75,8 @@ export const MultipleChoiceSummary = ({
|
||||
{result.others && result.others.length > 0 && (
|
||||
<div className="mt-4 rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-2 content-center rounded-t-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-1 pl-6">Other values found</div>
|
||||
<div className="col-span-1 pl-6">{surveyType === "app" && "User"}</div>
|
||||
<div className="col-span-1 pl-6 ">Other values found</div>
|
||||
<div className="col-span-1 pl-6 ">{surveyType === "app" && "User"}</div>
|
||||
</div>
|
||||
{result.others
|
||||
.filter((otherValue) => otherValue.value !== "")
|
||||
|
||||
@@ -29,7 +29,7 @@ export const PictureChoiceSummary = ({
|
||||
{results.map((result) => (
|
||||
<div key={result.id}>
|
||||
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">
|
||||
<div className="mr-8 flex w-full justify-between space-x-1 sm:justify-normal">
|
||||
<div className="mr-8 flex w-full justify-between space-x-1 sm:justify-normal ">
|
||||
<div className="relative h-32 w-[220px]">
|
||||
<Image
|
||||
src={result.imageUrl}
|
||||
|
||||
@@ -42,7 +42,7 @@ export const QuestionSummaryHeader = ({
|
||||
|
||||
return (
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<div className={"align-center flex justify-between gap-4"}>
|
||||
<div className={"align-center flex justify-between gap-4 "}>
|
||||
<h3 className="pb-1 text-lg font-semibold text-slate-900 md:text-xl">
|
||||
{formatTextWithSlashes(
|
||||
recallToHeadline(questionSummary.question.headline, survey, true, "default", attributeClasses)[
|
||||
@@ -52,19 +52,19 @@ export const QuestionSummaryHeader = ({
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
{questionType && <questionType.icon className="mr-2 h-4 w-4" />}
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2 ">
|
||||
{questionType && <questionType.icon className="mr-2 h-4 w-4 " />}
|
||||
{questionType ? questionType.label : "Unknown Question Type"} Question
|
||||
</div>
|
||||
{showResponses && (
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<div className=" flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{`${questionSummary.responseCount} Responses`}
|
||||
</div>
|
||||
)}
|
||||
{insights}
|
||||
{!questionSummary.question.required && (
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">Optional</div>
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">Optional</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,6 @@ export const RatingSummary = ({ questionSummary, survey, attributeClasses }: Rat
|
||||
scale={questionSummary.question.scale}
|
||||
answer={result.rating}
|
||||
range={questionSummary.question.range}
|
||||
addColors={questionSummary.question.isColorCodingEnabled}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -70,19 +70,19 @@ export const ShareEmbedSurvey = ({ survey, open, setOpen, webAppUrl, user }: Sha
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleInitialPageButton}
|
||||
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-sm text-slate-500 hover:border-slate-200 md:p-8">
|
||||
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-sm text-slate-500 hover:border-slate-200 md:p-8">
|
||||
<Code2Icon className="h-6 w-6 text-slate-700" />
|
||||
Embed survey
|
||||
</button>
|
||||
<Link
|
||||
href={`/environments/${environmentId}//settings/notifications`}
|
||||
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-sm text-slate-500 hover:border-slate-200 md:p-8">
|
||||
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-sm text-slate-500 hover:border-slate-200 md:p-8">
|
||||
<BellRing className="h-6 w-6 text-slate-700" />
|
||||
Configure alerts
|
||||
</Link>
|
||||
<Link
|
||||
href={`/environments/${environmentId}/integrations`}
|
||||
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-sm text-slate-500 hover:border-slate-200 md:p-8">
|
||||
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-sm text-slate-500 hover:border-slate-200 md:p-8">
|
||||
<BlocksIcon className="h-6 w-6 text-slate-700" />
|
||||
Setup integrations
|
||||
</Link>
|
||||
|
||||
@@ -60,12 +60,12 @@ export const ShareSurveyResults = ({
|
||||
<Button
|
||||
type="submit"
|
||||
variant="secondary"
|
||||
className="text-center"
|
||||
className=" text-center"
|
||||
onClick={() => handleUnpublish()}>
|
||||
Unpublish
|
||||
</Button>
|
||||
|
||||
<Button variant="darkCTA" className="text-center" href={surveyUrl} target="_blank">
|
||||
<Button variant="darkCTA" className=" text-center" href={surveyUrl} target="_blank">
|
||||
View site
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const SummaryDropOffs = ({ dropOff }: SummaryDropOffsProps) => {
|
||||
{quesDropOff.ttc > 0 ? (quesDropOff.ttc / 1000).toFixed(2) + "s" : "N/A"}
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap text-center font-semibold">{quesDropOff.impressions}</div>
|
||||
<div className="pl-6 text-center md:px-6">
|
||||
<div className=" pl-6 text-center md:px-6">
|
||||
<span className="mr-1.5 font-semibold">{quesDropOff.dropOffCount}</span>
|
||||
<span>({Math.round(quesDropOff.dropOffPercentage)}%)</span>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,7 @@ export const EmailTab = ({ surveyId, email }: EmailTabProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-5 ">
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
{showEmbed ? (
|
||||
<Button
|
||||
|
||||
@@ -116,7 +116,7 @@ export const QuestionFilterComboBox = ({
|
||||
<div
|
||||
onClick={() => !disabled && !isDisabledComboBox && filterValue && setOpen(true)}
|
||||
className={clsx(
|
||||
"group flex items-center justify-between rounded-md rounded-l-none bg-white px-3 py-2 text-sm",
|
||||
"group flex items-center justify-between rounded-md rounded-l-none bg-white px-3 py-2 text-sm",
|
||||
disabled || isDisabledComboBox || !filterValue ? "opacity-50" : "cursor-pointer"
|
||||
)}>
|
||||
{filterComboBoxValue && filterComboBoxValue?.length > 0 ? (
|
||||
|
||||
@@ -201,10 +201,10 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-[300px] border-slate-200 bg-slate-100 p-6 sm:w-[400px] md:w-[750px] lg:w-[1000px]">
|
||||
className="w-[300px] border-slate-200 bg-slate-100 p-6 sm:w-[400px] md:w-[750px] lg:w-[1000px] ">
|
||||
<div className="mb-8 flex flex-wrap items-start justify-between">
|
||||
<p className="hidden text-lg font-bold text-black sm:block">Show all responses that match</p>
|
||||
<p className="block text-base text-slate-500 sm:hidden">Show all responses where...</p>
|
||||
<p className="block text-base text-slate-500 sm:hidden">Show all responses where...</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className="text-sm font-normal text-slate-600">Only completed</label>
|
||||
<Checkbox
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user