mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-22 14:10:45 -06:00
Compare commits
79 Commits
fix-user-i
...
fix/emove-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74337df278 | ||
|
|
3f16291137 | ||
|
|
a5958d5653 | ||
|
|
fdbdf8207a | ||
|
|
630e5489ec | ||
|
|
36943bb786 | ||
|
|
e1bbb0a10f | ||
|
|
27da540846 | ||
|
|
7d7f6ed04a | ||
|
|
ff01bc342d | ||
|
|
cd8b40b569 | ||
|
|
31c742f7a8 | ||
|
|
d6a7a2c21f | ||
|
|
499ecab691 | ||
|
|
df06540f1b | ||
|
|
a32b213ca5 | ||
|
|
6120f992a4 | ||
|
|
389a551a69 | ||
|
|
8ddbdc0e1e | ||
|
|
302c6a90c0 | ||
|
|
18e597d8a3 | ||
|
|
81d717ccff | ||
|
|
2e979c7323 | ||
|
|
4dfd15d6dd | ||
|
|
5b9bf3ff43 | ||
|
|
d2f7485098 | ||
|
|
f8fee1fba7 | ||
|
|
19249ca00f | ||
|
|
01e5700340 | ||
|
|
ff2f7660a6 | ||
|
|
2bc05e2b4a | ||
|
|
137c6447b7 | ||
|
|
ebc8f0c917 | ||
|
|
5a8d10b5b4 | ||
|
|
875815fb62 | ||
|
|
cdf526e130 | ||
|
|
b685032b34 | ||
|
|
a171f9cb00 | ||
|
|
c452f05ec2 | ||
|
|
93d91f80f2 | ||
|
|
7b764c8427 | ||
|
|
016289c8cb | ||
|
|
93a9575389 | ||
|
|
9e265adf14 | ||
|
|
eb08a0ed14 | ||
|
|
c533f37983 | ||
|
|
ca4f8385e4 | ||
|
|
3eb9aa74ed | ||
|
|
637b51464c | ||
|
|
fd9585a66e | ||
|
|
49ecbcb0c9 | ||
|
|
1132bdd66a | ||
|
|
c7d6ed9ea3 | ||
|
|
782528f169 | ||
|
|
104c78275f | ||
|
|
d9d88f7175 | ||
|
|
bf7e24cf11 | ||
|
|
c8aba01db3 | ||
|
|
a896c7e46e | ||
|
|
8018ec14a2 | ||
|
|
9c3208c860 | ||
|
|
e1063964cf | ||
|
|
38568738cc | ||
|
|
15b8358b14 | ||
|
|
2173cb2610 | ||
|
|
87b925d622 | ||
|
|
885b06cc26 | ||
|
|
adb6a5f41e | ||
|
|
3b815e22e3 | ||
|
|
4d4a5c0e64 | ||
|
|
0e89293974 | ||
|
|
c306911b3a | ||
|
|
4f276f0095 | ||
|
|
81fc97c7e9 | ||
|
|
785c5a59c6 | ||
|
|
25ecfaa883 | ||
|
|
38e2c019fa | ||
|
|
15878a4ac5 | ||
|
|
9802536ded |
12
.env.example
12
.env.example
@@ -117,7 +117,7 @@ IMPRINT_URL=
|
|||||||
IMPRINT_ADDRESS=
|
IMPRINT_ADDRESS=
|
||||||
|
|
||||||
# Configure Turnstile in signup flow
|
# Configure Turnstile in signup flow
|
||||||
# NEXT_PUBLIC_TURNSTILE_SITE_KEY=
|
# TURNSTILE_SITE_KEY=
|
||||||
# TURNSTILE_SECRET_KEY=
|
# TURNSTILE_SECRET_KEY=
|
||||||
|
|
||||||
# Configure Github Login
|
# Configure Github Login
|
||||||
@@ -155,9 +155,8 @@ STRIPE_SECRET_KEY=
|
|||||||
STRIPE_WEBHOOK_SECRET=
|
STRIPE_WEBHOOK_SECRET=
|
||||||
|
|
||||||
# Configure Formbricks usage within Formbricks
|
# Configure Formbricks usage within Formbricks
|
||||||
NEXT_PUBLIC_FORMBRICKS_API_HOST=
|
FORMBRICKS_API_HOST=
|
||||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=
|
FORMBRICKS_ENVIRONMENT_ID=
|
||||||
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID=
|
|
||||||
|
|
||||||
# Oauth credentials for Google sheet integration
|
# Oauth credentials for Google sheet integration
|
||||||
GOOGLE_SHEETS_CLIENT_ID=
|
GOOGLE_SHEETS_CLIENT_ID=
|
||||||
@@ -220,3 +219,8 @@ UNKEY_ROOT_KEY=
|
|||||||
# PROMETHEUS_ENABLED=
|
# PROMETHEUS_ENABLED=
|
||||||
# PROMETHEUS_EXPORTER_PORT=
|
# PROMETHEUS_EXPORTER_PORT=
|
||||||
|
|
||||||
|
# The SENTRY_DSN is used for error tracking and performance monitoring with Sentry.
|
||||||
|
# SENTRY_DSN=
|
||||||
|
# The SENTRY_AUTH_TOKEN variable is picked up by the Sentry Build Plugin.
|
||||||
|
# It's used automatically by Sentry during the build for authentication when uploading source maps.
|
||||||
|
# SENTRY_AUTH_TOKEN=
|
||||||
|
|||||||
12
.github/actions/cache-build-web/action.yml
vendored
12
.github/actions/cache-build-web/action.yml
vendored
@@ -8,6 +8,14 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: "0"
|
default: "0"
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
turbo_token:
|
||||||
|
description: "Turborepo token"
|
||||||
|
required: false
|
||||||
|
turbo_team:
|
||||||
|
description: "Turborepo team"
|
||||||
|
required: false
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
@@ -62,6 +70,8 @@ runs:
|
|||||||
|
|
||||||
- run: |
|
- run: |
|
||||||
pnpm build --filter=@formbricks/web...
|
pnpm build --filter=@formbricks/web...
|
||||||
|
|
||||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
TURBO_TOKEN: ${{ inputs.turbo_token }}
|
||||||
|
TURBO_TEAM: ${{ inputs.turbo_team }}
|
||||||
|
|||||||
4
.github/workflows/build-web.yml
vendored
4
.github/workflows/build-web.yml
vendored
@@ -4,7 +4,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build Formbricks-web
|
name: Build Formbricks-web
|
||||||
@@ -25,3 +25,5 @@ jobs:
|
|||||||
id: cache-build-web
|
id: cache-build-web
|
||||||
with:
|
with:
|
||||||
e2e_testing_mode: "0"
|
e2e_testing_mode: "0"
|
||||||
|
turbo_token: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
turbo_team: ${{ vars.TURBO_TEAM }}
|
||||||
|
|||||||
167
.github/workflows/docker-build-validation.yml
vendored
Normal file
167
.github/workflows/docker-build-validation.yml
vendored
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
name: Docker Build Validation
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
merge_group:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-docker-build:
|
||||||
|
name: Validate Docker Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# Add PostgreSQL service container
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: pgvector/pgvector:pg17
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: test
|
||||||
|
POSTGRES_PASSWORD: test
|
||||||
|
POSTGRES_DB: formbricks
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
# Health check to ensure PostgreSQL is ready before using it
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build Docker Image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./apps/web/Dockerfile
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: formbricks-test:${{ github.sha }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
secrets: |
|
||||||
|
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
||||||
|
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||||
|
|
||||||
|
- name: Verify PostgreSQL Connection
|
||||||
|
run: |
|
||||||
|
echo "Verifying PostgreSQL connection..."
|
||||||
|
# Install PostgreSQL client to test connection
|
||||||
|
sudo apt-get update && sudo apt-get install -y postgresql-client
|
||||||
|
|
||||||
|
# Test connection using psql
|
||||||
|
PGPASSWORD=test psql -h localhost -U test -d formbricks -c "\dt" || echo "Failed to connect to PostgreSQL"
|
||||||
|
|
||||||
|
# Show network configuration
|
||||||
|
echo "Network configuration:"
|
||||||
|
ip addr show
|
||||||
|
netstat -tulpn | grep 5432 || echo "No process listening on port 5432"
|
||||||
|
|
||||||
|
- name: Test Docker Image with Health Check
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "🧪 Testing if the Docker image starts correctly..."
|
||||||
|
|
||||||
|
# Add extra docker run args to support host.docker.internal on Linux
|
||||||
|
DOCKER_RUN_ARGS="--add-host=host.docker.internal:host-gateway"
|
||||||
|
|
||||||
|
# Start the container with host.docker.internal pointing to the host
|
||||||
|
docker run --name formbricks-test \
|
||||||
|
$DOCKER_RUN_ARGS \
|
||||||
|
-p 3000:3000 \
|
||||||
|
-e DATABASE_URL="postgresql://test:test@host.docker.internal:5432/formbricks" \
|
||||||
|
-e ENCRYPTION_KEY="${{ secrets.DUMMY_ENCRYPTION_KEY }}" \
|
||||||
|
-d formbricks-test:${{ github.sha }}
|
||||||
|
|
||||||
|
# Give it more time to start up
|
||||||
|
echo "Waiting 45 seconds for application to start..."
|
||||||
|
sleep 45
|
||||||
|
|
||||||
|
# Check if the container is running
|
||||||
|
if [ "$(docker inspect -f '{{.State.Running}}' formbricks-test)" != "true" ]; then
|
||||||
|
echo "❌ Container failed to start properly!"
|
||||||
|
docker logs formbricks-test
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "✅ Container started successfully!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try connecting to PostgreSQL from inside the container
|
||||||
|
echo "Testing PostgreSQL connection from inside container..."
|
||||||
|
docker exec formbricks-test sh -c 'apt-get update && apt-get install -y postgresql-client && PGPASSWORD=test psql -h host.docker.internal -U test -d formbricks -c "\dt" || echo "Failed to connect to PostgreSQL from container"'
|
||||||
|
|
||||||
|
# Try to access the health endpoint
|
||||||
|
echo "🏥 Testing /health endpoint..."
|
||||||
|
MAX_RETRIES=10
|
||||||
|
RETRY_COUNT=0
|
||||||
|
HEALTH_CHECK_SUCCESS=false
|
||||||
|
|
||||||
|
set +e # Disable exit on error to allow for retries
|
||||||
|
|
||||||
|
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
||||||
|
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||||
|
echo "Attempt $RETRY_COUNT of $MAX_RETRIES..."
|
||||||
|
|
||||||
|
# Show container logs before each attempt to help debugging
|
||||||
|
if [ $RETRY_COUNT -gt 1 ]; then
|
||||||
|
echo "📋 Current container logs:"
|
||||||
|
docker logs --tail 20 formbricks-test
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get detailed curl output for debugging
|
||||||
|
HTTP_OUTPUT=$(curl -v -s -m 30 http://localhost:3000/health 2>&1)
|
||||||
|
CURL_EXIT_CODE=$?
|
||||||
|
|
||||||
|
echo "Curl exit code: $CURL_EXIT_CODE"
|
||||||
|
echo "Curl output: $HTTP_OUTPUT"
|
||||||
|
|
||||||
|
if [ $CURL_EXIT_CODE -eq 0 ]; then
|
||||||
|
STATUS_CODE=$(echo "$HTTP_OUTPUT" | grep -oP "HTTP/\d(\.\d)? \K\d+")
|
||||||
|
echo "Status code detected: $STATUS_CODE"
|
||||||
|
|
||||||
|
if [ "$STATUS_CODE" = "200" ]; then
|
||||||
|
echo "✅ Health check successful!"
|
||||||
|
HEALTH_CHECK_SUCCESS=true
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "❌ Health check returned non-200 status code: $STATUS_CODE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "❌ Curl command failed with exit code: $CURL_EXIT_CODE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Waiting 15 seconds before next attempt..."
|
||||||
|
sleep 15
|
||||||
|
done
|
||||||
|
|
||||||
|
# Show full container logs for debugging
|
||||||
|
echo "📋 Full container logs:"
|
||||||
|
docker logs formbricks-test
|
||||||
|
|
||||||
|
# Clean up the container
|
||||||
|
echo "🧹 Cleaning up..."
|
||||||
|
docker rm -f formbricks-test
|
||||||
|
|
||||||
|
# Exit with failure if health check did not succeed
|
||||||
|
if [ "$HEALTH_CHECK_SUCCESS" != "true" ]; then
|
||||||
|
echo "❌ Health check failed after $MAX_RETRIES attempts"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✨ Docker validation complete - all checks passed!"
|
||||||
2
.github/workflows/e2e.yml
vendored
2
.github/workflows/e2e.yml
vendored
@@ -16,6 +16,8 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
TELEMETRY_DISABLED: 1
|
TELEMETRY_DISABLED: 1
|
||||||
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
|
|||||||
@@ -82,8 +82,6 @@ jobs:
|
|||||||
secrets: |
|
secrets: |
|
||||||
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
||||||
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|
||||||
# Sign the resulting Docker image digest except on PRs.
|
# Sign the resulting Docker image digest except on PRs.
|
||||||
# This will only write to the public Rekor transparency log when the Docker
|
# This will only write to the public Rekor transparency log when the Docker
|
||||||
|
|||||||
2
.github/workflows/release-docker-github.yml
vendored
2
.github/workflows/release-docker-github.yml
vendored
@@ -102,8 +102,6 @@ jobs:
|
|||||||
secrets: |
|
secrets: |
|
||||||
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
||||||
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|
||||||
# Sign the resulting Docker image digest except on PRs.
|
# Sign the resulting Docker image digest except on PRs.
|
||||||
# This will only write to the public Rekor transparency log when the Docker
|
# This will only write to the public Rekor transparency log when the Docker
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -72,3 +72,4 @@ infra/terraform/.terraform/
|
|||||||
# IntelliJ IDEA
|
# IntelliJ IDEA
|
||||||
/.idea/
|
/.idea/
|
||||||
/*.iml
|
/*.iml
|
||||||
|
packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.xcworkspace/xcuserdata
|
||||||
|
|||||||
13
.vscode/settings.json
vendored
13
.vscode/settings.json
vendored
@@ -1,4 +1,15 @@
|
|||||||
{
|
{
|
||||||
|
"github.copilot.chat.codeGeneration.instructions": [
|
||||||
|
{
|
||||||
|
"text": "When generating tests, always use vitest and use the `test` function instead of `it`."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||||
|
"sonarlint.connectedMode.project": {
|
||||||
|
"connectionId": "formbricks",
|
||||||
|
"projectKey": "formbricks_formbricks"
|
||||||
|
},
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"typescript.tsdk": "node_modules/typescript/lib"
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"typescript.updateImportsOnFileMove.enabled": "always"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"expo-status-bar": "2.0.1",
|
"expo-status-bar": "2.0.1",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "0.78.2",
|
"react-native": "0.76.6",
|
||||||
"react-native-webview": "13.12.5"
|
"react-native-webview": "13.12.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const secondaryNavigation = [
|
|||||||
|
|
||||||
export function Sidebar(): React.JSX.Element {
|
export function Sidebar(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-grow flex-col overflow-y-auto bg-cyan-700 pb-4 pt-5">
|
<div className="flex grow flex-col overflow-y-auto bg-cyan-700 pt-5 pb-4">
|
||||||
<nav
|
<nav
|
||||||
className="mt-5 flex flex-1 flex-col divide-y divide-cyan-800 overflow-y-auto"
|
className="mt-5 flex flex-1 flex-col divide-y divide-cyan-800 overflow-y-auto"
|
||||||
aria-label="Sidebar">
|
aria-label="Sidebar">
|
||||||
@@ -38,10 +38,10 @@ export function Sidebar(): React.JSX.Element {
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
item.current ? "bg-cyan-800 text-white" : "text-cyan-100 hover:bg-cyan-600 hover:text-white",
|
item.current ? "bg-cyan-800 text-white" : "text-cyan-100 hover:bg-cyan-600 hover:text-white",
|
||||||
"group flex items-center rounded-md px-2 py-2 text-sm font-medium leading-6"
|
"group flex items-center rounded-md px-2 py-2 text-sm leading-6 font-medium"
|
||||||
)}
|
)}
|
||||||
aria-current={item.current ? "page" : undefined}>
|
aria-current={item.current ? "page" : undefined}>
|
||||||
<item.icon className="mr-4 h-6 w-6 flex-shrink-0 text-cyan-200" aria-hidden="true" />
|
<item.icon className="mr-4 h-6 w-6 shrink-0 text-cyan-200" aria-hidden="true" />
|
||||||
{item.name}
|
{item.name}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
@@ -52,7 +52,7 @@ export function Sidebar(): React.JSX.Element {
|
|||||||
<a
|
<a
|
||||||
key={item.name}
|
key={item.name}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="group flex items-center rounded-md px-2 py-2 text-sm font-medium leading-6 text-cyan-100 hover:bg-cyan-600 hover:text-white">
|
className="group flex items-center rounded-md px-2 py-2 text-sm leading-6 font-medium text-cyan-100 hover:bg-cyan-600 hover:text-white">
|
||||||
<item.icon className="mr-4 h-6 w-6 text-cyan-200" aria-hidden="true" />
|
<item.icon className="mr-4 h-6 w-6 text-cyan-200" aria-hidden="true" />
|
||||||
{item.name}
|
{item.name}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,3 +1,23 @@
|
|||||||
@tailwind base;
|
@import 'tailwindcss';
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
@plugin '@tailwindcss/forms';
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
/*
|
||||||
|
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
||||||
|
so we've added these compatibility styles to make sure everything still
|
||||||
|
looks the same as it did with Tailwind CSS v3.
|
||||||
|
|
||||||
|
If we ever want to remove these styles, we need to add an explicit border
|
||||||
|
color utility to any element that depends on these defaults.
|
||||||
|
*/
|
||||||
|
@layer base {
|
||||||
|
*,
|
||||||
|
::after,
|
||||||
|
::before,
|
||||||
|
::backdrop,
|
||||||
|
::file-selector-button {
|
||||||
|
border-color: var(--color-gray-200, currentcolor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,12 +13,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formbricks/js": "workspace:*",
|
"@formbricks/js": "workspace:*",
|
||||||
"@tailwindcss/forms": "0.5.9",
|
"@tailwindcss/forms": "0.5.9",
|
||||||
|
"@tailwindcss/postcss": "4.1.3",
|
||||||
"lucide-react": "0.486.0",
|
"lucide-react": "0.486.0",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
"postcss": "8.5.3",
|
"postcss": "8.5.3",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.1.0",
|
||||||
"tailwindcss": "3.4.16"
|
"tailwindcss": "4.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@formbricks/config-typescript": "workspace:*",
|
"@formbricks/config-typescript": "workspace:*",
|
||||||
|
|||||||
@@ -96,10 +96,10 @@ export default function AppPage(): React.JSX.Element {
|
|||||||
<p className="text-slate-700 dark:text-slate-300">
|
<p className="text-slate-700 dark:text-slate-300">
|
||||||
Copy the environment ID of your Formbricks app to the env variable in /apps/demo/.env
|
Copy the environment ID of your Formbricks app to the env variable in /apps/demo/.env
|
||||||
</p>
|
</p>
|
||||||
<Image src={fbsetup} alt="fb setup" className="mt-4 rounded" priority />
|
<Image src={fbsetup} alt="fb setup" className="mt-4 rounded-xs" priority />
|
||||||
|
|
||||||
<div className="mt-4 flex-col items-start text-sm text-slate-700 sm:flex sm:items-center sm:text-base dark:text-slate-300">
|
<div className="mt-4 flex-col items-start text-sm text-slate-700 sm:flex sm:items-center sm:text-base dark:text-slate-300">
|
||||||
<p className="mb-1 sm:mb-0 sm:mr-2">You're connected with env:</p>
|
<p className="mb-1 sm:mr-2 sm:mb-0">You're connected with env:</p>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<strong className="w-32 truncate sm:w-auto">
|
<strong className="w-32 truncate sm:w-auto">
|
||||||
{process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID}
|
{process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
"@tailwindcss/postcss": {},
|
||||||
autoprefixer: {},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: [
|
|
||||||
"./app/**/*.{js,ts,jsx,tsx}",
|
|
||||||
"./pages/**/*.{js,ts,jsx,tsx}",
|
|
||||||
"./components/**/*.{js,ts,jsx,tsx}",
|
|
||||||
],
|
|
||||||
darkMode: "class",
|
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [require("@tailwindcss/forms")],
|
|
||||||
};
|
|
||||||
@@ -27,14 +27,14 @@
|
|||||||
"@storybook/react": "8.6.12",
|
"@storybook/react": "8.6.12",
|
||||||
"@storybook/react-vite": "8.6.12",
|
"@storybook/react-vite": "8.6.12",
|
||||||
"@storybook/test": "8.6.12",
|
"@storybook/test": "8.6.12",
|
||||||
"@typescript-eslint/eslint-plugin": "8.29.0",
|
"@typescript-eslint/eslint-plugin": "8.29.1",
|
||||||
"@typescript-eslint/parser": "8.29.0",
|
"@typescript-eslint/parser": "8.29.1",
|
||||||
"@vitejs/plugin-react": "4.3.4",
|
"@vitejs/plugin-react": "4.3.4",
|
||||||
"esbuild": "0.25.2",
|
"esbuild": "0.25.2",
|
||||||
"eslint-plugin-storybook": "0.12.0",
|
"eslint-plugin-storybook": "0.12.0",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"storybook": "8.6.12",
|
"storybook": "8.6.12",
|
||||||
"tsup": "8.4.0",
|
"tsup": "8.4.0",
|
||||||
"vite": "6.2.4"
|
"vite": "6.2.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ["@formbricks/eslint-config/legacy-next.js"],
|
extends: ["@formbricks/eslint-config/legacy-next.js"],
|
||||||
|
ignorePatterns: ["**/package.json", "**/tsconfig.json"],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["lib/messages/**/*.json"],
|
||||||
|
plugins: ["i18n-json"],
|
||||||
|
rules: {
|
||||||
|
"i18n-json/identical-keys": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
filePath: require("path").join(__dirname, "messages", "en-US.json"),
|
||||||
|
checkExtraKeys: false,
|
||||||
|
checkMissingKeys: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
2
apps/web/.gitignore
vendored
2
apps/web/.gitignore
vendored
@@ -50,4 +50,4 @@ uploads/
|
|||||||
.sentryclirc
|
.sentryclirc
|
||||||
|
|
||||||
# SAML Preloaded Connections
|
# SAML Preloaded Connections
|
||||||
saml-connection/
|
saml-connection/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS base
|
FROM node:22-alpine3.21 AS base
|
||||||
|
|
||||||
#
|
#
|
||||||
## step 1: Prune monorepo
|
## step 1: Prune monorepo
|
||||||
@@ -22,7 +22,7 @@ RUN npm install -g corepack@latest
|
|||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
# Install necessary build tools and compilers
|
# Install necessary build tools and compilers
|
||||||
RUN apk update && apk add --no-cache g++ cmake make gcc python3 openssl-dev jq
|
RUN apk update && apk add --no-cache cmake g++ gcc jq make openssl-dev python3
|
||||||
|
|
||||||
# BuildKit secret handling without hardcoded fallback values
|
# BuildKit secret handling without hardcoded fallback values
|
||||||
# This approach relies entirely on secrets passed from GitHub Actions
|
# This approach relies entirely on secrets passed from GitHub Actions
|
||||||
@@ -40,8 +40,6 @@ RUN echo '#!/bin/sh' > /tmp/read-secrets.sh && \
|
|||||||
echo 'exec "$@"' >> /tmp/read-secrets.sh && \
|
echo 'exec "$@"' >> /tmp/read-secrets.sh && \
|
||||||
chmod +x /tmp/read-secrets.sh
|
chmod +x /tmp/read-secrets.sh
|
||||||
|
|
||||||
ARG SENTRY_AUTH_TOKEN
|
|
||||||
|
|
||||||
# Increase Node.js memory limit as a regular build argument
|
# Increase Node.js memory limit as a regular build argument
|
||||||
ARG NODE_OPTIONS="--max_old_space_size=4096"
|
ARG NODE_OPTIONS="--max_old_space_size=4096"
|
||||||
ENV NODE_OPTIONS=${NODE_OPTIONS}
|
ENV NODE_OPTIONS=${NODE_OPTIONS}
|
||||||
@@ -83,35 +81,65 @@ RUN corepack enable
|
|||||||
RUN apk add --no-cache curl \
|
RUN apk add --no-cache curl \
|
||||||
&& apk add --no-cache supercronic \
|
&& apk add --no-cache supercronic \
|
||||||
# && addgroup --system --gid 1001 nodejs \
|
# && addgroup --system --gid 1001 nodejs \
|
||||||
&& adduser --system --uid 1001 nextjs
|
&& addgroup -S nextjs \
|
||||||
|
&& adduser -S -u 1001 -G nextjs nextjs
|
||||||
|
|
||||||
WORKDIR /home/nextjs
|
WORKDIR /home/nextjs
|
||||||
|
|
||||||
|
# Ensure no write permissions are assigned to the copied resources
|
||||||
|
COPY --from=installer /app/apps/web/.next/standalone ./
|
||||||
|
RUN chown -R nextjs:nextjs ./ && chmod -R 755 ./
|
||||||
|
|
||||||
COPY --from=installer /app/apps/web/next.config.mjs .
|
COPY --from=installer /app/apps/web/next.config.mjs .
|
||||||
|
RUN chmod 644 ./next.config.mjs
|
||||||
|
|
||||||
COPY --from=installer /app/apps/web/package.json .
|
COPY --from=installer /app/apps/web/package.json .
|
||||||
# Leverage output traces to reduce image size
|
RUN chmod 644 ./package.json
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/standalone ./
|
COPY --from=installer /app/apps/web/.next/static ./apps/web/.next/static
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/static ./apps/web/.next/static
|
RUN chown -R nextjs:nextjs ./apps/web/.next/static && chmod -R 755 ./apps/web/.next/static
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/package.json ./packages/database/package.json
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migration ./packages/database/migration
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/src ./packages/database/src
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/node_modules ./packages/database/node_modules
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
|
|
||||||
|
|
||||||
# Copy Prisma-specific generated files
|
COPY --from=installer /app/apps/web/public ./apps/web/public
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
RUN chown -R nextjs:nextjs ./apps/web/public && chmod -R 755 ./apps/web/public
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/.prisma ./node_modules/.prisma
|
|
||||||
|
COPY --from=installer /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
||||||
|
RUN chown nextjs:nextjs ./packages/database/schema.prisma && chmod 644 ./packages/database/schema.prisma
|
||||||
|
|
||||||
|
COPY --from=installer /app/packages/database/package.json ./packages/database/package.json
|
||||||
|
RUN chown nextjs:nextjs ./packages/database/package.json && chmod 644 ./packages/database/package.json
|
||||||
|
|
||||||
|
COPY --from=installer /app/packages/database/migration ./packages/database/migration
|
||||||
|
RUN chown -R nextjs:nextjs ./packages/database/migration && chmod -R 755 ./packages/database/migration
|
||||||
|
|
||||||
|
COPY --from=installer /app/packages/database/src ./packages/database/src
|
||||||
|
RUN chown -R nextjs:nextjs ./packages/database/src && chmod -R 755 ./packages/database/src
|
||||||
|
|
||||||
|
COPY --from=installer /app/packages/database/node_modules ./packages/database/node_modules
|
||||||
|
RUN chown -R nextjs:nextjs ./packages/database/node_modules && chmod -R 755 ./packages/database/node_modules
|
||||||
|
|
||||||
|
COPY --from=installer /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
|
||||||
|
RUN chown -R nextjs:nextjs ./packages/database/node_modules/@formbricks/logger/dist && chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist
|
||||||
|
|
||||||
|
COPY --from=installer /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
||||||
|
RUN chown -R nextjs:nextjs ./node_modules/@prisma/client && chmod -R 755 ./node_modules/@prisma/client
|
||||||
|
|
||||||
|
COPY --from=installer /app/node_modules/.prisma ./node_modules/.prisma
|
||||||
|
RUN chown -R nextjs:nextjs ./node_modules/.prisma && chmod -R 755 ./node_modules/.prisma
|
||||||
|
|
||||||
|
COPY --from=installer /prisma_version.txt .
|
||||||
|
RUN chown nextjs:nextjs ./prisma_version.txt && chmod 644 ./prisma_version.txt
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /prisma_version.txt .
|
|
||||||
COPY /docker/cronjobs /app/docker/cronjobs
|
COPY /docker/cronjobs /app/docker/cronjobs
|
||||||
|
RUN chmod -R 755 /app/docker/cronjobs
|
||||||
|
|
||||||
# Copy required dependencies
|
|
||||||
COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2
|
COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2
|
||||||
|
RUN chmod -R 755 ./node_modules/@paralleldrive/cuid2
|
||||||
|
|
||||||
COPY --from=installer /app/node_modules/@noble/hashes ./node_modules/@noble/hashes
|
COPY --from=installer /app/node_modules/@noble/hashes ./node_modules/@noble/hashes
|
||||||
|
RUN chmod -R 755 ./node_modules/@noble/hashes
|
||||||
|
|
||||||
COPY --from=installer /app/node_modules/zod ./node_modules/zod
|
COPY --from=installer /app/node_modules/zod ./node_modules/zod
|
||||||
|
RUN chmod -R 755 ./node_modules/zod
|
||||||
|
|
||||||
RUN npm install -g tsx typescript prisma pino-pretty
|
RUN npm install -g tsx typescript prisma pino-pretty
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { ArrowRight } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TProjectConfigChannel } from "@formbricks/types/project";
|
import { TProjectConfigChannel } from "@formbricks/types/project";
|
||||||
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
|
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
|
||||||
|
import { WEBAPP_URL } from "@/lib/constants";
|
||||||
|
import { getEnvironment } from "@/lib/environment/service";
|
||||||
|
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
|
||||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
|
||||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
|
||||||
|
|
||||||
interface ConnectPageProps {
|
interface ConnectPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -44,7 +44,7 @@ const Page = async (props: ConnectPageProps) => {
|
|||||||
channel={channel}
|
channel={channel}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={`/environments/${environment.id}`}>
|
<Link href={`/environments/${environment.id}`}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
import { AuthorizationError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
const OnboardingLayout = async (props) => {
|
const OnboardingLayout = async (props) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { replaceQuestionPresetPlaceholders } from "@formbricks/lib/utils/templates";
|
import { replaceQuestionPresetPlaceholders } from "@/lib/utils/templates";
|
||||||
import { TProject } from "@formbricks/types/project";
|
import { TProject } from "@formbricks/types/project";
|
||||||
import { TXMTemplate } from "@formbricks/types/templates";
|
import { TXMTemplate } from "@formbricks/types/templates";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
|
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
|
||||||
|
import { getEnvironment } from "@/lib/environment/service";
|
||||||
|
import { getProjectByEnvironmentId, getUserProjects } from "@/lib/project/service";
|
||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -7,9 +10,6 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
|
||||||
import { getProjectByEnvironmentId, getUserProjects } from "@formbricks/lib/project/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
|
|
||||||
interface XMTemplatePageProps {
|
interface XMTemplatePageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -49,7 +49,7 @@ const Page = async (props: XMTemplatePageProps) => {
|
|||||||
<XMTemplateList project={project} user={user} environmentId={environment.id} />
|
<XMTemplateList project={project} user={user} environmentId={environment.id} />
|
||||||
{projects.length >= 2 && (
|
{projects.length >= 2 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={`/environments/${environment.id}/surveys`}>
|
<Link href={`/environments/${environment.id}/surveys`}>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { TOrganizationTeam } from "@/app/(app)/(onboarding)/types/onboarding";
|
import { TOrganizationTeam } from "@/app/(app)/(onboarding)/types/onboarding";
|
||||||
|
import { cache } from "@/lib/cache";
|
||||||
import { teamCache } from "@/lib/cache/team";
|
import { teamCache } from "@/lib/cache/team";
|
||||||
|
import { validateInputs } from "@/lib/utils/validate";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { cache } from "@formbricks/lib/cache";
|
|
||||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { formbricksLogout } from "@/app/lib/formbricks";
|
import { formbricksLogout } from "@/app/lib/formbricks";
|
||||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
import FBLogo from "@/images/formbricks-wordmark.svg";
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
|
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
||||||
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
||||||
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
||||||
import {
|
import {
|
||||||
@@ -24,8 +26,6 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
|
||||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ export const LandingSidebar = ({
|
|||||||
{/* Dropdown Items */}
|
{/* Dropdown Items */}
|
||||||
|
|
||||||
{dropdownNavigation.map((link) => (
|
{dropdownNavigation.map((link) => (
|
||||||
<Link href={link.href} target={link.target} className="flex w-full items-center">
|
<Link id={link.href} href={link.href} target={link.target} className="flex w-full items-center">
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<link.icon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
<link.icon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
||||||
{link.label}
|
{link.label}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { getEnvironments } from "@/lib/environment/service";
|
||||||
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
|
import { getUserProjects } from "@/lib/project/service";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import { getEnvironments } from "@formbricks/lib/environment/service";
|
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
|
||||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
|
||||||
|
|
||||||
const LandingLayout = async (props) => {
|
const LandingLayout = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
|
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
|
||||||
|
import { getOrganizationsByUserId } from "@/lib/organization/service";
|
||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import { getOrganizationsByUserId } from "@formbricks/lib/organization/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
|
import { canUserAccessOrganization } from "@/lib/organization/auth";
|
||||||
|
import { getOrganization } from "@/lib/organization/service";
|
||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import "@testing-library/jest-dom/vitest";
|
import "@testing-library/jest-dom/vitest";
|
||||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
|
|
||||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import ProjectOnboardingLayout from "./layout";
|
import ProjectOnboardingLayout from "./layout";
|
||||||
|
|
||||||
// Mock all the modules and functions that this layout uses:
|
// Mock all the modules and functions that this layout uses:
|
||||||
|
|
||||||
vi.mock("@formbricks/lib/constants", () => ({
|
vi.mock("@/lib/constants", () => ({
|
||||||
IS_FORMBRICKS_CLOUD: false,
|
IS_FORMBRICKS_CLOUD: false,
|
||||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||||
POSTHOG_HOST: "mock-posthog-host",
|
POSTHOG_HOST: "mock-posthog-host",
|
||||||
@@ -42,13 +42,13 @@ vi.mock("next-auth", () => ({
|
|||||||
vi.mock("next/navigation", () => ({
|
vi.mock("next/navigation", () => ({
|
||||||
redirect: vi.fn(),
|
redirect: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@formbricks/lib/organization/auth", () => ({
|
vi.mock("@/lib/organization/auth", () => ({
|
||||||
canUserAccessOrganization: vi.fn(),
|
canUserAccessOrganization: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@formbricks/lib/organization/service", () => ({
|
vi.mock("@/lib/organization/service", () => ({
|
||||||
getOrganization: vi.fn(),
|
getOrganization: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@formbricks/lib/user/service", () => ({
|
vi.mock("@/lib/user/service", () => ({
|
||||||
getUser: vi.fn(),
|
getUser: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@/tolgee/server", () => ({
|
vi.mock("@/tolgee/server", () => ({
|
||||||
@@ -71,7 +71,7 @@ describe("ProjectOnboardingLayout", () => {
|
|||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("redirects to /auth/login if there is no session", async () => {
|
test("redirects to /auth/login if there is no session", async () => {
|
||||||
// Mock no session
|
// Mock no session
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ describe("ProjectOnboardingLayout", () => {
|
|||||||
expect(layoutElement).toBeUndefined();
|
expect(layoutElement).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws an error if user does not exist", async () => {
|
test("throws an error if user does not exist", async () => {
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({
|
vi.mocked(getServerSession).mockResolvedValueOnce({
|
||||||
user: { id: "user-123" },
|
user: { id: "user-123" },
|
||||||
});
|
});
|
||||||
@@ -99,7 +99,7 @@ describe("ProjectOnboardingLayout", () => {
|
|||||||
).rejects.toThrow("common.user_not_found");
|
).rejects.toThrow("common.user_not_found");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws AuthorizationError if user cannot access organization", async () => {
|
test("throws AuthorizationError if user cannot access organization", async () => {
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
||||||
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(false);
|
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(false);
|
||||||
@@ -112,7 +112,7 @@ describe("ProjectOnboardingLayout", () => {
|
|||||||
).rejects.toThrow("common.not_authorized");
|
).rejects.toThrow("common.not_authorized");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws an error if organization does not exist", async () => {
|
test("throws an error if organization does not exist", async () => {
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
||||||
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(true);
|
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(true);
|
||||||
@@ -126,7 +126,7 @@ describe("ProjectOnboardingLayout", () => {
|
|||||||
).rejects.toThrow("common.organization_not_found");
|
).rejects.toThrow("common.organization_not_found");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders child content plus PosthogIdentify & ToasterClient if everything is valid", async () => {
|
test("renders child content plus PosthogIdentify & ToasterClient if everything is valid", async () => {
|
||||||
// Provide valid data
|
// Provide valid data
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", name: "Test User" } as TUser);
|
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", name: "Test User" } as TUser);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
|
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
|
||||||
|
import { IS_POSTHOG_CONFIGURED } from "@/lib/constants";
|
||||||
|
import { canUserAccessOrganization } from "@/lib/organization/auth";
|
||||||
|
import { getOrganization } from "@/lib/organization/service";
|
||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { IS_POSTHOG_CONFIGURED } from "@formbricks/lib/constants";
|
|
||||||
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
|
|
||||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
import { AuthorizationError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
const ProjectOnboardingLayout = async (props) => {
|
const ProjectOnboardingLayout = async (props) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||||
|
import { getUserProjects } from "@/lib/project/service";
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
@@ -6,7 +7,6 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
|
import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
|
||||||
|
|
||||||
interface ChannelPageProps {
|
interface ChannelPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -50,7 +50,7 @@ const Page = async (props: ChannelPageProps) => {
|
|||||||
<OnboardingOptionsContainer options={channelOptions} />
|
<OnboardingOptionsContainer options={channelOptions} />
|
||||||
{projects.length >= 1 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
|
import { getOrganization } from "@/lib/organization/service";
|
||||||
|
import { getOrganizationProjectsCount } from "@/lib/project/service";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
|
||||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
|
||||||
import { getOrganizationProjectsCount } from "@formbricks/lib/project/service";
|
|
||||||
|
|
||||||
const OnboardingLayout = async (props) => {
|
const OnboardingLayout = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||||
|
import { getUserProjects } from "@/lib/project/service";
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
@@ -6,7 +7,6 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
|
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
|
||||||
|
|
||||||
interface ModePageProps {
|
interface ModePageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -47,7 +47,7 @@ const Page = async (props: ModePageProps) => {
|
|||||||
<OnboardingOptionsContainer options={channelOptions} />
|
<OnboardingOptionsContainer options={channelOptions} />
|
||||||
{projects.length >= 1 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||||
import { previewSurvey } from "@/app/lib/templates";
|
import { previewSurvey } from "@/app/lib/templates";
|
||||||
|
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@/lib/localStorage";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
|
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
|
||||||
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
|
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
|
||||||
@@ -26,7 +27,6 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
|
|
||||||
import {
|
import {
|
||||||
TProjectConfigChannel,
|
TProjectConfigChannel,
|
||||||
TProjectConfigIndustry,
|
TProjectConfigIndustry,
|
||||||
@@ -225,7 +225,7 @@ export const ProjectSettings = ({
|
|||||||
alt="Logo"
|
alt="Logo"
|
||||||
width={256}
|
width={256}
|
||||||
height={56}
|
height={56}
|
||||||
className="absolute left-2 top-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
|
className="absolute top-2 left-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className="text-sm text-slate-400">{t("common.preview")}</p>
|
<p className="text-sm text-slate-400">{t("common.preview")}</p>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
||||||
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
|
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
|
||||||
|
import { DEFAULT_BRAND_COLOR } from "@/lib/constants";
|
||||||
|
import { getUserProjects } from "@/lib/project/service";
|
||||||
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -8,8 +10,6 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
|
|
||||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
|
||||||
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
||||||
|
|
||||||
interface ProjectSettingsPageProps {
|
interface ProjectSettingsPageProps {
|
||||||
@@ -65,7 +65,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
|||||||
/>
|
/>
|
||||||
{projects.length >= 1 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
|
|||||||
@@ -1,191 +1,120 @@
|
|||||||
import "@testing-library/jest-dom/vitest";
|
import { getEnvironment } from "@/lib/environment/service";
|
||||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||||
import { getServerSession } from "next-auth";
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
|
import { Session } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import React from "react";
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
|
||||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
|
||||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import SurveyEditorEnvironmentLayout from "./layout";
|
import SurveyEditorEnvironmentLayout from "./layout";
|
||||||
|
|
||||||
// mock all dependencies
|
// Mock sub-components to render identifiable elements
|
||||||
|
vi.mock("@/modules/ui/components/environmentId-base-layout", () => ({
|
||||||
vi.mock("@formbricks/lib/constants", () => ({
|
EnvironmentIdBaseLayout: ({ children, environmentId }: any) => (
|
||||||
IS_FORMBRICKS_CLOUD: false,
|
<div data-testid="EnvironmentIdBaseLayout">
|
||||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
{environmentId}
|
||||||
POSTHOG_HOST: "mock-posthog-host",
|
{children}
|
||||||
IS_POSTHOG_CONFIGURED: true,
|
</div>
|
||||||
ENCRYPTION_KEY: "mock-encryption-key",
|
),
|
||||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
}));
|
||||||
GITHUB_ID: "mock-github-id",
|
vi.mock("@/modules/ui/components/dev-environment-banner", () => ({
|
||||||
GITHUB_SECRET: "test-githubID",
|
DevEnvironmentBanner: ({ environment }: any) => (
|
||||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
<div data-testid="DevEnvironmentBanner">{environment.id}</div>
|
||||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
),
|
||||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
|
||||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
|
||||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
|
||||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
|
||||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
|
||||||
OIDC_ISSUER: "test-oidc-issuer",
|
|
||||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
|
||||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
|
||||||
WEBAPP_URL: "test-webapp-url",
|
|
||||||
IS_PRODUCTION: false,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("next-auth", () => ({
|
// Mocks for dependencies
|
||||||
getServerSession: vi.fn(),
|
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||||
|
environmentIdLayoutChecks: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/environment/service", () => ({
|
||||||
|
getEnvironment: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("next/navigation", () => ({
|
vi.mock("next/navigation", () => ({
|
||||||
redirect: vi.fn(),
|
redirect: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@formbricks/lib/environment/auth", () => ({
|
|
||||||
hasUserEnvironmentAccess: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@formbricks/lib/environment/service", () => ({
|
|
||||||
getEnvironment: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@formbricks/lib/organization/service", () => ({
|
|
||||||
getOrganizationByEnvironmentId: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@formbricks/lib/user/service", () => ({
|
|
||||||
getUser: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@/tolgee/server", () => ({
|
|
||||||
getTranslate: vi.fn(() => {
|
|
||||||
return (key: string) => key; // trivial translator returning the key
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// mock child components rendered by the layout:
|
|
||||||
vi.mock("@/app/(app)/components/FormbricksClient", () => ({
|
|
||||||
FormbricksClient: () => <div data-testid="formbricks-client" />,
|
|
||||||
}));
|
|
||||||
vi.mock("@/app/(app)/environments/[environmentId]/components/PosthogIdentify", () => ({
|
|
||||||
PosthogIdentify: () => <div data-testid="posthog-identify" />,
|
|
||||||
}));
|
|
||||||
vi.mock("@/modules/ui/components/toaster-client", () => ({
|
|
||||||
ToasterClient: () => <div data-testid="mock-toaster" />,
|
|
||||||
}));
|
|
||||||
vi.mock("@/modules/ui/components/dev-environment-banner", () => ({
|
|
||||||
DevEnvironmentBanner: ({ environment }: { environment: TEnvironment }) => (
|
|
||||||
<div data-testid="dev-environment-banner">{environment?.id || "no-env"}</div>
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({
|
|
||||||
ResponseFilterProvider: ({ children }: { children: React.ReactNode }) => (
|
|
||||||
<div data-testid="mock-response-filter-provider">{children}</div>
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("SurveyEditorEnvironmentLayout", () => {
|
describe("SurveyEditorEnvironmentLayout", () => {
|
||||||
beforeEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("redirects to /auth/login if there is no session", async () => {
|
test("renders successfully when environment is found", async () => {
|
||||||
// Mock no session
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
t: ((key: string) => key) as any, // Mock translation function, we don't need to implement it for the test
|
||||||
|
session: { user: { id: "user1" } } as Session,
|
||||||
|
user: { id: "user1", email: "user1@example.com" } as TUser,
|
||||||
|
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||||
|
});
|
||||||
|
vi.mocked(getEnvironment).mockResolvedValueOnce({ id: "env1" } as TEnvironment);
|
||||||
|
|
||||||
const layoutElement = await SurveyEditorEnvironmentLayout({
|
const result = await SurveyEditorEnvironmentLayout({
|
||||||
params: { environmentId: "env-123" },
|
params: Promise.resolve({ environmentId: "env1" }),
|
||||||
children: <div data-testid="child-content">Hello!</div>,
|
children: <div data-testid="child">Survey Editor Content</div>,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(redirect).toHaveBeenCalledWith("/auth/login");
|
render(result);
|
||||||
// No JSX is returned after redirect
|
|
||||||
expect(layoutElement).toBeUndefined();
|
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveTextContent("env1");
|
||||||
|
expect(screen.getByTestId("DevEnvironmentBanner")).toHaveTextContent("env1");
|
||||||
|
expect(screen.getByTestId("child")).toHaveTextContent("Survey Editor Content");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error if user does not exist in DB", async () => {
|
test("throws an error when environment is not found", async () => {
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
vi.mocked(getUser).mockResolvedValueOnce(null); // user not found
|
t: ((key: string) => key) as any,
|
||||||
|
session: { user: { id: "user1" } } as Session,
|
||||||
await expect(
|
user: { id: "user1", email: "user1@example.com" } as TUser,
|
||||||
SurveyEditorEnvironmentLayout({
|
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||||
params: { environmentId: "env-123" },
|
});
|
||||||
children: <div data-testid="child-content">Hello!</div>,
|
|
||||||
})
|
|
||||||
).rejects.toThrow("common.user_not_found");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws AuthorizationError if user does not have environment access", async () => {
|
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", email: "test@example.com" } as TUser);
|
|
||||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(false);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
SurveyEditorEnvironmentLayout({
|
|
||||||
params: { environmentId: "env-123" },
|
|
||||||
children: <div>Child</div>,
|
|
||||||
})
|
|
||||||
).rejects.toThrow(AuthorizationError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws if no organization is found", async () => {
|
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
|
||||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
|
|
||||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(null);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
SurveyEditorEnvironmentLayout({
|
|
||||||
params: { environmentId: "env-123" },
|
|
||||||
children: <div data-testid="child-content">Hello from children!</div>,
|
|
||||||
})
|
|
||||||
).rejects.toThrow("common.organization_not_found");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws if no environment is found", async () => {
|
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
|
||||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
|
|
||||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce({ id: "org-999" } as TOrganization);
|
|
||||||
vi.mocked(getEnvironment).mockResolvedValueOnce(null);
|
vi.mocked(getEnvironment).mockResolvedValueOnce(null);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
SurveyEditorEnvironmentLayout({
|
SurveyEditorEnvironmentLayout({
|
||||||
params: { environmentId: "env-123" },
|
params: Promise.resolve({ environmentId: "env1" }),
|
||||||
children: <div>Child</div>,
|
children: <div>Content</div>,
|
||||||
})
|
})
|
||||||
).rejects.toThrow("common.environment_not_found");
|
).rejects.toThrow("common.environment_not_found");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders environment layout if everything is valid", async () => {
|
test("calls redirect when session is null", async () => {
|
||||||
// Provide all valid data
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
t: ((key: string) => key) as any,
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", email: "test@example.com" } as TUser);
|
session: undefined as unknown as Session,
|
||||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
|
user: undefined as unknown as TUser,
|
||||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce({ id: "org-999" } as TOrganization);
|
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||||
vi.mocked(getEnvironment).mockResolvedValueOnce({
|
});
|
||||||
id: "env-123",
|
vi.mocked(redirect).mockImplementationOnce(() => {
|
||||||
name: "My Test Environment",
|
throw new Error("Redirect called");
|
||||||
} as unknown as TEnvironment);
|
|
||||||
|
|
||||||
// Because it's an async server component, we typically wrap in act(...)
|
|
||||||
let layoutElement: React.ReactNode;
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
layoutElement = await SurveyEditorEnvironmentLayout({
|
|
||||||
params: { environmentId: "env-123" },
|
|
||||||
children: <div data-testid="child-content">Hello from children!</div>,
|
|
||||||
});
|
|
||||||
render(layoutElement);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Now confirm we got the child plus all the mocked sub-components
|
await expect(
|
||||||
expect(screen.getByTestId("child-content")).toHaveTextContent("Hello from children!");
|
SurveyEditorEnvironmentLayout({
|
||||||
expect(screen.getByTestId("posthog-identify")).toBeInTheDocument();
|
params: Promise.resolve({ environmentId: "env1" }),
|
||||||
expect(screen.getByTestId("formbricks-client")).toBeInTheDocument();
|
children: <div>Content</div>,
|
||||||
expect(screen.getByTestId("mock-toaster")).toBeInTheDocument();
|
})
|
||||||
expect(screen.getByTestId("mock-response-filter-provider")).toBeInTheDocument();
|
).rejects.toThrow("Redirect called");
|
||||||
expect(screen.getByTestId("dev-environment-banner")).toHaveTextContent("env-123");
|
});
|
||||||
|
|
||||||
|
test("throws error if user is null", async () => {
|
||||||
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
|
t: ((key: string) => key) as any,
|
||||||
|
session: { user: { id: "user1" } } as Session,
|
||||||
|
user: undefined as unknown as TUser,
|
||||||
|
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(redirect).mockImplementationOnce(() => {
|
||||||
|
throw new Error("Redirect called");
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
SurveyEditorEnvironmentLayout({
|
||||||
|
params: Promise.resolve({ environmentId: "env1" }),
|
||||||
|
children: <div>Content</div>,
|
||||||
|
})
|
||||||
|
).rejects.toThrow("common.user_not_found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,46 +1,24 @@
|
|||||||
import { FormbricksClient } from "@/app/(app)/components/FormbricksClient";
|
import { getEnvironment } from "@/lib/environment/service";
|
||||||
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
|
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||||
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
|
||||||
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
||||||
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
|
||||||
import { getServerSession } from "next-auth";
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { IS_POSTHOG_CONFIGURED } from "@formbricks/lib/constants";
|
|
||||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
|
||||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
|
||||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
|
||||||
|
|
||||||
const SurveyEditorEnvironmentLayout = async (props) => {
|
const SurveyEditorEnvironmentLayout = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
const t = await getTranslate();
|
const { t, session, user, organization } = await environmentIdLayoutChecks(params.environmentId);
|
||||||
const session = await getServerSession(authOptions);
|
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session) {
|
||||||
return redirect(`/auth/login`);
|
return redirect(`/auth/login`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await getUser(session.user.id);
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(t("common.user_not_found"));
|
throw new Error(t("common.user_not_found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAccess = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
|
|
||||||
if (!hasAccess) {
|
|
||||||
throw new AuthorizationError(t("common.not_authorized"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const organization = await getOrganizationByEnvironmentId(params.environmentId);
|
|
||||||
if (!organization) {
|
|
||||||
throw new Error(t("common.organization_not_found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const environment = await getEnvironment(params.environmentId);
|
const environment = await getEnvironment(params.environmentId);
|
||||||
|
|
||||||
if (!environment) {
|
if (!environment) {
|
||||||
@@ -48,23 +26,16 @@ const SurveyEditorEnvironmentLayout = async (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponseFilterProvider>
|
<EnvironmentIdBaseLayout
|
||||||
<PosthogIdentify
|
environmentId={params.environmentId}
|
||||||
session={session}
|
session={session}
|
||||||
user={user}
|
user={user}
|
||||||
environmentId={params.environmentId}
|
organization={organization}>
|
||||||
organizationId={organization.id}
|
|
||||||
organizationName={organization.name}
|
|
||||||
organizationBilling={organization.billing}
|
|
||||||
isPosthogEnabled={IS_POSTHOG_CONFIGURED}
|
|
||||||
/>
|
|
||||||
<FormbricksClient userId={user.id} email={user.email} />
|
|
||||||
<ToasterClient />
|
|
||||||
<div className="flex h-screen flex-col">
|
<div className="flex h-screen flex-col">
|
||||||
<DevEnvironmentBanner environment={environment} />
|
<DevEnvironmentBanner environment={environment} />
|
||||||
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</ResponseFilterProvider>
|
</EnvironmentIdBaseLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import formbricks from "@formbricks/js";
|
import formbricks from "@formbricks/js";
|
||||||
import { FormbricksClient } from "./FormbricksClient";
|
import { FormbricksClient } from "./FormbricksClient";
|
||||||
|
|
||||||
@@ -9,14 +9,6 @@ vi.mock("next/navigation", () => ({
|
|||||||
useSearchParams: () => new URLSearchParams("foo=bar"),
|
useSearchParams: () => new URLSearchParams("foo=bar"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock the environment variables.
|
|
||||||
vi.mock("@formbricks/lib/env", () => ({
|
|
||||||
env: {
|
|
||||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: "env-test",
|
|
||||||
NEXT_PUBLIC_FORMBRICKS_API_HOST: "https://api.test.com",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock the flag that enables Formbricks.
|
// Mock the flag that enables Formbricks.
|
||||||
vi.mock("@/app/lib/formbricks", () => ({
|
vi.mock("@/app/lib/formbricks", () => ({
|
||||||
formbricksEnabled: true,
|
formbricksEnabled: true,
|
||||||
@@ -34,17 +26,21 @@ vi.mock("@formbricks/js", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe("FormbricksClient", () => {
|
describe("FormbricksClient", () => {
|
||||||
afterEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("calls setup, setUserId, setEmail and registerRouteChange on mount when enabled", () => {
|
test("calls setup, setUserId, setEmail and registerRouteChange on mount when enabled", () => {
|
||||||
const mockSetup = vi.spyOn(formbricks, "setup");
|
const mockSetup = vi.spyOn(formbricks, "setup");
|
||||||
const mockSetUserId = vi.spyOn(formbricks, "setUserId");
|
const mockSetUserId = vi.spyOn(formbricks, "setUserId");
|
||||||
const mockSetEmail = vi.spyOn(formbricks, "setEmail");
|
const mockSetEmail = vi.spyOn(formbricks, "setEmail");
|
||||||
const mockRegisterRouteChange = vi.spyOn(formbricks, "registerRouteChange");
|
const mockRegisterRouteChange = vi.spyOn(formbricks, "registerRouteChange");
|
||||||
|
|
||||||
render(<FormbricksClient userId="user-123" email="test@example.com" />);
|
render(
|
||||||
|
<FormbricksClient
|
||||||
|
userId="user-123"
|
||||||
|
email="test@example.com"
|
||||||
|
formbricksEnvironmentId="env-test"
|
||||||
|
formbricksApiHost="https://api.test.com"
|
||||||
|
formbricksEnabled={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
// Expect the first effect to call setup and assign the provided user details.
|
// Expect the first effect to call setup and assign the provided user details.
|
||||||
expect(mockSetup).toHaveBeenCalledWith({
|
expect(mockSetup).toHaveBeenCalledWith({
|
||||||
@@ -64,7 +60,15 @@ describe("FormbricksClient", () => {
|
|||||||
const mockSetEmail = vi.spyOn(formbricks, "setEmail");
|
const mockSetEmail = vi.spyOn(formbricks, "setEmail");
|
||||||
const mockRegisterRouteChange = vi.spyOn(formbricks, "registerRouteChange");
|
const mockRegisterRouteChange = vi.spyOn(formbricks, "registerRouteChange");
|
||||||
|
|
||||||
render(<FormbricksClient userId="" email="test@example.com" />);
|
render(
|
||||||
|
<FormbricksClient
|
||||||
|
userId=""
|
||||||
|
email="test@example.com"
|
||||||
|
formbricksEnvironmentId="env-test"
|
||||||
|
formbricksApiHost="https://api.test.com"
|
||||||
|
formbricksEnabled={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
// Since userId is falsy, the first effect should not call setup or assign user details.
|
// Since userId is falsy, the first effect should not call setup or assign user details.
|
||||||
expect(mockSetup).not.toHaveBeenCalled();
|
expect(mockSetup).not.toHaveBeenCalled();
|
||||||
|
|||||||
@@ -1,32 +1,44 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { formbricksEnabled } from "@/app/lib/formbricks";
|
|
||||||
import { usePathname, useSearchParams } from "next/navigation";
|
import { usePathname, useSearchParams } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import formbricks from "@formbricks/js";
|
import formbricks from "@formbricks/js";
|
||||||
import { env } from "@formbricks/lib/env";
|
|
||||||
|
|
||||||
export const FormbricksClient = ({ userId, email }: { userId: string; email: string }) => {
|
interface FormbricksClientProps {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
formbricksEnvironmentId?: string;
|
||||||
|
formbricksApiHost?: string;
|
||||||
|
formbricksEnabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormbricksClient = ({
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
formbricksEnvironmentId,
|
||||||
|
formbricksApiHost,
|
||||||
|
formbricksEnabled,
|
||||||
|
}: FormbricksClientProps) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formbricksEnabled && userId) {
|
if (formbricksEnabled && userId) {
|
||||||
formbricks.setup({
|
formbricks.setup({
|
||||||
environmentId: env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID || "",
|
environmentId: formbricksEnvironmentId ?? "",
|
||||||
appUrl: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "",
|
appUrl: formbricksApiHost ?? "",
|
||||||
});
|
});
|
||||||
|
|
||||||
formbricks.setUserId(userId);
|
formbricks.setUserId(userId);
|
||||||
formbricks.setEmail(email);
|
formbricks.setEmail(email);
|
||||||
}
|
}
|
||||||
}, [userId, email]);
|
}, [userId, email, formbricksEnvironmentId, formbricksApiHost, formbricksEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formbricksEnabled) {
|
if (formbricksEnabled) {
|
||||||
formbricks.registerRouteChange();
|
formbricks.registerRouteChange();
|
||||||
}
|
}
|
||||||
}, [pathname, searchParams]);
|
}, [pathname, searchParams, formbricksEnabled]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
|
|
||||||
export const LoadingCard = ({
|
export const LoadingCard = ({
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { getOrganization } from "@/lib/organization/service";
|
||||||
|
import { getOrganizationProjectsCount } from "@/lib/project/service";
|
||||||
|
import { updateUser } from "@/lib/user/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import {
|
import {
|
||||||
@@ -8,9 +11,6 @@ import {
|
|||||||
} from "@/modules/ee/license-check/lib/utils";
|
} from "@/modules/ee/license-check/lib/utils";
|
||||||
import { createProject } from "@/modules/projects/settings/lib/project";
|
import { createProject } from "@/modules/projects/settings/lib/project";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
|
||||||
import { getOrganizationProjectsCount } from "@formbricks/lib/project/service";
|
|
||||||
import { updateUser } from "@formbricks/lib/user/service";
|
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||||
import { ZProjectUpdateInput } from "@formbricks/types/project";
|
import { ZProjectUpdateInput } from "@formbricks/types/project";
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { deleteActionClass, getActionClass, updateActionClass } from "@/lib/actionClass/service";
|
||||||
|
import { cache } from "@/lib/cache";
|
||||||
|
import { getSurveysByActionClassId } from "@/lib/survey/service";
|
||||||
import { actionClient, authenticatedActionClient } from "@/lib/utils/action-client";
|
import { actionClient, authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import { getOrganizationIdFromActionClassId, getProjectIdFromActionClassId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromActionClassId, getProjectIdFromActionClassId } from "@/lib/utils/helper";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
|
|
||||||
import { cache } from "@formbricks/lib/cache";
|
|
||||||
import { getSurveysByActionClassId } from "@formbricks/lib/survey/service";
|
|
||||||
import { ZActionClassInput } from "@formbricks/types/action-classes";
|
import { ZActionClassInput } from "@formbricks/types/action-classes";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
||||||
|
import { convertDateTimeStringShort } from "@/lib/time";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
|
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
||||||
import { createActionClassAction } from "@/modules/survey/editor/actions";
|
import { createActionClassAction } from "@/modules/survey/editor/actions";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { ErrorComponent } from "@/modules/ui/components/error-component";
|
import { ErrorComponent } from "@/modules/ui/components/error-component";
|
||||||
@@ -10,8 +12,6 @@ import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
|
|||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
|
||||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
|
||||||
import { TActionClass, TActionClassInput, TActionClassInputCode } from "@formbricks/types/action-classes";
|
import { TActionClass, TActionClassInput, TActionClassInputCode } from "@formbricks/types/action-classes";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { getActiveInactiveSurveysAction } from "../actions";
|
import { getActiveInactiveSurveysAction } from "../actions";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
||||||
import { timeSince } from "@formbricks/lib/time";
|
import { timeSince } from "@/lib/time";
|
||||||
import { TActionClass } from "@formbricks/types/action-classes";
|
import { TActionClass } from "@formbricks/types/action-classes";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export const ActionClassDataRow = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
<div className="col-span-2 my-auto text-center text-sm whitespace-nowrap text-slate-500">
|
||||||
{timeSince(actionClass.createdAt.toString(), locale)}
|
{timeSince(actionClass.createdAt.toString(), locale)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center"></div>
|
<div className="text-center"></div>
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import { ActionClassesTable } from "@/app/(app)/environments/[environmentId]/act
|
|||||||
import { ActionClassDataRow } from "@/app/(app)/environments/[environmentId]/actions/components/ActionRowData";
|
import { ActionClassDataRow } from "@/app/(app)/environments/[environmentId]/actions/components/ActionRowData";
|
||||||
import { ActionTableHeading } from "@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading";
|
import { ActionTableHeading } from "@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading";
|
||||||
import { AddActionModal } from "@/app/(app)/environments/[environmentId]/actions/components/AddActionModal";
|
import { AddActionModal } from "@/app/(app)/environments/[environmentId]/actions/components/AddActionModal";
|
||||||
|
import { getActionClasses } from "@/lib/actionClass/service";
|
||||||
|
import { getEnvironments } from "@/lib/environment/service";
|
||||||
|
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
|
||||||
import { getEnvironments } from "@formbricks/lib/environment/service";
|
|
||||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Actions",
|
title: "Actions",
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
||||||
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
|
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
|
||||||
|
import { IS_DEVELOPMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
|
import { getEnvironment, getEnvironments } from "@/lib/environment/service";
|
||||||
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
|
import {
|
||||||
|
getMonthlyActiveOrganizationPeopleCount,
|
||||||
|
getMonthlyOrganizationResponseCount,
|
||||||
|
getOrganizationByEnvironmentId,
|
||||||
|
getOrganizationsByUserId,
|
||||||
|
} from "@/lib/organization/service";
|
||||||
|
import { getUserProjects } from "@/lib/project/service";
|
||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import { getEnterpriseLicense, getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
import { getEnterpriseLicense, getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||||
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
||||||
@@ -7,18 +19,6 @@ import { LimitsReachedBanner } from "@/modules/ui/components/limits-reached-bann
|
|||||||
import { PendingDowngradeBanner } from "@/modules/ui/components/pending-downgrade-banner";
|
import { PendingDowngradeBanner } from "@/modules/ui/components/pending-downgrade-banner";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
|
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
|
||||||
import {
|
|
||||||
getMonthlyActiveOrganizationPeopleCount,
|
|
||||||
getMonthlyOrganizationResponseCount,
|
|
||||||
getOrganizationByEnvironmentId,
|
|
||||||
getOrganizationsByUserId,
|
|
||||||
} from "@formbricks/lib/organization/service";
|
|
||||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
|
|
||||||
interface EnvironmentLayoutProps {
|
interface EnvironmentLayoutProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
@@ -111,6 +111,7 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
|||||||
organizationProjectsLimit={organizationProjectsLimit}
|
organizationProjectsLimit={organizationProjectsLimit}
|
||||||
user={user}
|
user={user}
|
||||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||||
|
isDevelopment={IS_DEVELOPMENT}
|
||||||
membershipRole={membershipRole}
|
membershipRole={membershipRole}
|
||||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||||
isLicenseActive={active}
|
isLicenseActive={active}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@formbricks/lib/localStorage";
|
|
||||||
|
|
||||||
interface EnvironmentStorageHandlerProps {
|
interface EnvironmentStorageHandlerProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
import { Switch } from "@/modules/ui/components/switch";
|
import { Switch } from "@/modules/ui/components/switch";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
|
||||||
interface EnvironmentSwitchProps {
|
interface EnvironmentSwitchProps {
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { getLatestStableFbReleaseAction } from "@/app/(app)/environments/[enviro
|
|||||||
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
|
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
|
||||||
import { formbricksLogout } from "@/app/lib/formbricks";
|
import { formbricksLogout } from "@/app/lib/formbricks";
|
||||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
import FBLogo from "@/images/formbricks-wordmark.svg";
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
|
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
||||||
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
||||||
import { ProjectSwitcher } from "@/modules/projects/components/project-switcher";
|
import { ProjectSwitcher } from "@/modules/projects/components/project-switcher";
|
||||||
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
||||||
@@ -45,9 +48,6 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
|
||||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
@@ -63,6 +63,7 @@ interface NavigationProps {
|
|||||||
projects: TProject[];
|
projects: TProject[];
|
||||||
isMultiOrgEnabled: boolean;
|
isMultiOrgEnabled: boolean;
|
||||||
isFormbricksCloud: boolean;
|
isFormbricksCloud: boolean;
|
||||||
|
isDevelopment: boolean;
|
||||||
membershipRole?: TOrganizationRole;
|
membershipRole?: TOrganizationRole;
|
||||||
organizationProjectsLimit: number;
|
organizationProjectsLimit: number;
|
||||||
isLicenseActive: boolean;
|
isLicenseActive: boolean;
|
||||||
@@ -79,6 +80,7 @@ export const MainNavigation = ({
|
|||||||
isFormbricksCloud,
|
isFormbricksCloud,
|
||||||
organizationProjectsLimit,
|
organizationProjectsLimit,
|
||||||
isLicenseActive,
|
isLicenseActive,
|
||||||
|
isDevelopment,
|
||||||
}: NavigationProps) => {
|
}: NavigationProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -263,7 +265,7 @@ export const MainNavigation = ({
|
|||||||
size="icon"
|
size="icon"
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:outline-none focus:ring-0 focus:ring-transparent"
|
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:ring-0 focus:ring-transparent focus:outline-none"
|
||||||
)}>
|
)}>
|
||||||
{isCollapsed ? (
|
{isCollapsed ? (
|
||||||
<PanelLeftOpenIcon strokeWidth={1.5} />
|
<PanelLeftOpenIcon strokeWidth={1.5} />
|
||||||
@@ -296,7 +298,7 @@ export const MainNavigation = ({
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
{/* New Version Available */}
|
{/* New Version Available */}
|
||||||
{!isCollapsed && isOwnerOrManager && latestVersion && !isFormbricksCloud && (
|
{!isCollapsed && isOwnerOrManager && latestVersion && !isFormbricksCloud && !isDevelopment && (
|
||||||
<Link
|
<Link
|
||||||
href="https://github.com/formbricks/formbricks/releases"
|
href="https://github.com/formbricks/formbricks/releases"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { cn } from "@/lib/cn";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
|
||||||
|
|
||||||
interface NavigationLinkProps {
|
interface NavigationLinkProps {
|
||||||
href: string;
|
href: string;
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
// PosthogIdentify.test.tsx
|
|
||||||
import "@testing-library/jest-dom/vitest";
|
import "@testing-library/jest-dom/vitest";
|
||||||
import { cleanup, render } from "@testing-library/react";
|
import { cleanup, render } from "@testing-library/react";
|
||||||
import { Session } from "next-auth";
|
import { Session } from "next-auth";
|
||||||
import { usePostHog } from "posthog-js/react";
|
import { usePostHog } from "posthog-js/react";
|
||||||
import React from "react";
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import { PosthogIdentify } from "./PosthogIdentify";
|
import { PosthogIdentify } from "./PosthogIdentify";
|
||||||
@@ -20,7 +18,7 @@ describe("PosthogIdentify", () => {
|
|||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("identifies the user and sets groups when isPosthogEnabled is true", () => {
|
test("identifies the user and sets groups when isPosthogEnabled is true", () => {
|
||||||
const mockIdentify = vi.fn();
|
const mockIdentify = vi.fn();
|
||||||
const mockGroup = vi.fn();
|
const mockGroup = vi.fn();
|
||||||
|
|
||||||
@@ -74,7 +72,7 @@ describe("PosthogIdentify", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing if isPosthogEnabled is false", () => {
|
test("does nothing if isPosthogEnabled is false", () => {
|
||||||
const mockIdentify = vi.fn();
|
const mockIdentify = vi.fn();
|
||||||
const mockGroup = vi.fn();
|
const mockGroup = vi.fn();
|
||||||
|
|
||||||
@@ -97,7 +95,7 @@ describe("PosthogIdentify", () => {
|
|||||||
expect(mockGroup).not.toHaveBeenCalled();
|
expect(mockGroup).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing if session user is missing", () => {
|
test("does nothing if session user is missing", () => {
|
||||||
const mockIdentify = vi.fn();
|
const mockIdentify = vi.fn();
|
||||||
const mockGroup = vi.fn();
|
const mockGroup = vi.fn();
|
||||||
|
|
||||||
@@ -122,7 +120,7 @@ describe("PosthogIdentify", () => {
|
|||||||
expect(mockGroup).not.toHaveBeenCalled();
|
expect(mockGroup).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("identifies user but does not group if environmentId/organizationId not provided", () => {
|
test("identifies user but does not group if environmentId/organizationId not provided", () => {
|
||||||
const mockIdentify = vi.fn();
|
const mockIdentify = vi.fn();
|
||||||
const mockGroup = vi.fn();
|
const mockGroup = vi.fn();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { EnvironmentSwitch } from "@/app/(app)/environments/[environmentId]/components/EnvironmentSwitch";
|
import { EnvironmentSwitch } from "@/app/(app)/environments/[environmentId]/components/EnvironmentSwitch";
|
||||||
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
||||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -9,7 +10,6 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { BugIcon, CircleUserIcon, PlusIcon } from "lucide-react";
|
import { BugIcon, CircleUserIcon, PlusIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { AlertTriangleIcon, CheckIcon, RotateCcwIcon } from "lucide-react";
|
import { AlertTriangleIcon, CheckIcon, RotateCcwIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
|
||||||
interface WidgetStatusIndicatorProps {
|
interface WidgetStatusIndicatorProps {
|
||||||
@@ -53,7 +53,7 @@ export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProp
|
|||||||
<currentStatus.icon />
|
<currentStatus.icon />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
|
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
|
||||||
<p className="w-2/3 text-balance text-sm text-slate-600">{currentStatus.subtitle}</p>
|
<p className="w-2/3 text-sm text-balance text-slate-600">{currentStatus.subtitle}</p>
|
||||||
{status === "notImplemented" && (
|
{status === "notImplemented" && (
|
||||||
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
|
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
|
||||||
<RotateCcwIcon />
|
<RotateCcwIcon />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { createOrUpdateIntegration, deleteIntegration } from "@/lib/integration/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import {
|
import {
|
||||||
@@ -9,7 +10,6 @@ import {
|
|||||||
getProjectIdFromIntegrationId,
|
getProjectIdFromIntegrationId,
|
||||||
} from "@/lib/utils/helper";
|
} from "@/lib/utils/helper";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service";
|
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { ZIntegrationInput } from "@formbricks/types/integration";
|
import { ZIntegrationInput } from "@formbricks/types/integration";
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[envir
|
|||||||
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown";
|
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown";
|
||||||
import { fetchTables } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
import { fetchTables } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
||||||
import AirtableLogo from "@/images/airtableLogo.svg";
|
import AirtableLogo from "@/images/airtableLogo.svg";
|
||||||
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
|
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -23,8 +25,6 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
|
||||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
|
||||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import {
|
import {
|
||||||
TIntegrationAirtable,
|
TIntegrationAirtable,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
AddIntegrationModal,
|
AddIntegrationModal,
|
||||||
IntegrationModalInputs,
|
IntegrationModalInputs,
|
||||||
} from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal";
|
} from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal";
|
||||||
|
import { timeSince } from "@/lib/time";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
@@ -13,7 +14,6 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { Trash2Icon } from "lucide-react";
|
import { Trash2Icon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { timeSince } from "@formbricks/lib/time";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||||
|
import { getAirtableTables } from "@/lib/airtable/service";
|
||||||
|
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@/lib/constants";
|
||||||
|
import { getIntegrations } from "@/lib/integration/service";
|
||||||
|
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getAirtableTables } from "@formbricks/lib/airtable/service";
|
|
||||||
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
|
|
||||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
|
||||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
|
||||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { getSpreadsheetNameById } from "@/lib/googleSheet/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getSpreadsheetNameById } from "@formbricks/lib/googleSheet/service";
|
|
||||||
import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||||
|
|
||||||
const ZGetSpreadsheetNameByIdAction = z.object({
|
const ZGetSpreadsheetNameByIdAction = z.object({
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import {
|
|||||||
isValidGoogleSheetsUrl,
|
isValidGoogleSheetsUrl,
|
||||||
} from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util";
|
} from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util";
|
||||||
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||||
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
|
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Checkbox } from "@/modules/ui/components/checkbox";
|
import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||||
@@ -21,8 +23,6 @@ import Image from "next/image";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
|
||||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
|
||||||
import {
|
import {
|
||||||
TIntegrationGoogleSheets,
|
TIntegrationGoogleSheets,
|
||||||
TIntegrationGoogleSheetsConfigData,
|
TIntegrationGoogleSheetsConfigData,
|
||||||
@@ -255,7 +255,7 @@ export const AddIntegrationModal = ({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
||||||
<div className="mt-1 max-h-[15vh] overflow-y-auto overflow-x-hidden rounded-lg border border-slate-200">
|
<div className="mt-1 max-h-[15vh] overflow-x-hidden overflow-y-auto 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">
|
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||||
{replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((question) => (
|
{replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((question) => (
|
||||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||||
|
import { timeSince } from "@/lib/time";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
@@ -9,7 +10,6 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { Trash2Icon } from "lucide-react";
|
import { Trash2Icon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { timeSince } from "@formbricks/lib/time";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import {
|
import {
|
||||||
TIntegrationGoogleSheets,
|
TIntegrationGoogleSheets,
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||||
|
import {
|
||||||
|
GOOGLE_SHEETS_CLIENT_ID,
|
||||||
|
GOOGLE_SHEETS_CLIENT_SECRET,
|
||||||
|
GOOGLE_SHEETS_REDIRECT_URL,
|
||||||
|
WEBAPP_URL,
|
||||||
|
} from "@/lib/constants";
|
||||||
|
import { getIntegrations } from "@/lib/integration/service";
|
||||||
|
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import {
|
|
||||||
GOOGLE_SHEETS_CLIENT_ID,
|
|
||||||
GOOGLE_SHEETS_CLIENT_SECRET,
|
|
||||||
GOOGLE_SHEETS_REDIRECT_URL,
|
|
||||||
WEBAPP_URL,
|
|
||||||
} from "@formbricks/lib/constants";
|
|
||||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
|
||||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
|
||||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
|
import { cache } from "@/lib/cache";
|
||||||
|
import { surveyCache } from "@/lib/survey/cache";
|
||||||
|
import { selectSurvey } from "@/lib/survey/service";
|
||||||
|
import { transformPrismaSurvey } from "@/lib/survey/utils";
|
||||||
|
import { validateInputs } from "@/lib/utils/validate";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { cache } from "@formbricks/lib/cache";
|
|
||||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
|
||||||
import { selectSurvey } from "@formbricks/lib/survey/service";
|
|
||||||
import { transformPrismaSurvey } from "@formbricks/lib/survey/utils";
|
|
||||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
|
||||||
import { logger } from "@formbricks/logger";
|
import { logger } from "@formbricks/logger";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { cache } from "@/lib/cache";
|
||||||
import { webhookCache } from "@/lib/cache/webhook";
|
import { webhookCache } from "@/lib/cache/webhook";
|
||||||
|
import { validateInputs } from "@/lib/utils/validate";
|
||||||
import { Prisma, Webhook } from "@prisma/client";
|
import { Prisma, Webhook } from "@prisma/client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { cache } from "@formbricks/lib/cache";
|
|
||||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import {
|
|||||||
UNSUPPORTED_TYPES_BY_NOTION,
|
UNSUPPORTED_TYPES_BY_NOTION,
|
||||||
} from "@/app/(app)/environments/[environmentId]/integrations/notion/constants";
|
} from "@/app/(app)/environments/[environmentId]/integrations/notion/constants";
|
||||||
import NotionLogo from "@/images/notion.png";
|
import NotionLogo from "@/images/notion.png";
|
||||||
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
|
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||||
|
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||||
import { getQuestionTypes } from "@/modules/survey/lib/questions";
|
import { getQuestionTypes } from "@/modules/survey/lib/questions";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DropdownSelector } from "@/modules/ui/components/dropdown-selector";
|
import { DropdownSelector } from "@/modules/ui/components/dropdown-selector";
|
||||||
@@ -18,9 +21,6 @@ import Image from "next/image";
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
|
||||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
|
||||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
|
||||||
import { TIntegrationInput } from "@formbricks/types/integration";
|
import { TIntegrationInput } from "@formbricks/types/integration";
|
||||||
import {
|
import {
|
||||||
TIntegrationNotion,
|
TIntegrationNotion,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||||
|
import { timeSince } from "@/lib/time";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
@@ -10,7 +11,6 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { RefreshCcwIcon, Trash2Icon } from "lucide-react";
|
import { RefreshCcwIcon, Trash2Icon } from "lucide-react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { timeSince } from "@formbricks/lib/time";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion";
|
import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export const TYPE_MAPPING = {
|
|||||||
[TSurveyQuestionTypeEnum.Address]: ["rich_text"],
|
[TSurveyQuestionTypeEnum.Address]: ["rich_text"],
|
||||||
[TSurveyQuestionTypeEnum.Matrix]: ["rich_text"],
|
[TSurveyQuestionTypeEnum.Matrix]: ["rich_text"],
|
||||||
[TSurveyQuestionTypeEnum.Cal]: ["checkbox"],
|
[TSurveyQuestionTypeEnum.Cal]: ["checkbox"],
|
||||||
|
[TSurveyQuestionTypeEnum.ContactInfo]: ["rich_text"],
|
||||||
|
[TSurveyQuestionTypeEnum.Ranking]: ["rich_text"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UNSUPPORTED_TYPES_BY_NOTION = [
|
export const UNSUPPORTED_TYPES_BY_NOTION = [
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||||
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper";
|
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
|
||||||
import { getTranslate } from "@/tolgee/server";
|
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
import {
|
import {
|
||||||
NOTION_AUTH_URL,
|
NOTION_AUTH_URL,
|
||||||
NOTION_OAUTH_CLIENT_ID,
|
NOTION_OAUTH_CLIENT_ID,
|
||||||
NOTION_OAUTH_CLIENT_SECRET,
|
NOTION_OAUTH_CLIENT_SECRET,
|
||||||
NOTION_REDIRECT_URI,
|
NOTION_REDIRECT_URI,
|
||||||
WEBAPP_URL,
|
WEBAPP_URL,
|
||||||
} from "@formbricks/lib/constants";
|
} from "@/lib/constants";
|
||||||
import { getIntegrationByType } from "@formbricks/lib/integration/service";
|
import { getIntegrationByType } from "@/lib/integration/service";
|
||||||
import { getNotionDatabases } from "@formbricks/lib/notion/service";
|
import { getNotionDatabases } from "@/lib/notion/service";
|
||||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||||
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
|
import { getTranslate } from "@/tolgee/server";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
|
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import notionLogo from "@/images/notion.png";
|
|||||||
import SlackLogo from "@/images/slacklogo.png";
|
import SlackLogo from "@/images/slacklogo.png";
|
||||||
import WebhookLogo from "@/images/webhook.png";
|
import WebhookLogo from "@/images/webhook.png";
|
||||||
import ZapierLogo from "@/images/zapier-small.png";
|
import ZapierLogo from "@/images/zapier-small.png";
|
||||||
|
import { getIntegrations } from "@/lib/integration/service";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { Card } from "@/modules/ui/components/integration-card";
|
import { Card } from "@/modules/ui/components/integration-card";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
@@ -16,7 +17,6 @@ import { PageHeader } from "@/modules/ui/components/page-header";
|
|||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
|
||||||
import { TIntegrationType } from "@formbricks/types/integration";
|
import { TIntegrationType } from "@formbricks/types/integration";
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { getSlackChannels } from "@/lib/slack/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getSlackChannels } from "@formbricks/lib/slack/service";
|
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
|
|
||||||
const ZGetSlackChannelsAction = z.object({
|
const ZGetSlackChannelsAction = z.object({
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||||
import SlackLogo from "@/images/slacklogo.png";
|
import SlackLogo from "@/images/slacklogo.png";
|
||||||
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
|
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Checkbox } from "@/modules/ui/components/checkbox";
|
import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||||
@@ -15,8 +17,6 @@ import Link from "next/link";
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
|
||||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
|
||||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import {
|
import {
|
||||||
TIntegrationSlack,
|
TIntegrationSlack,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||||
|
import { timeSince } from "@/lib/time";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
@@ -10,7 +11,6 @@ import { T } from "@tolgee/react";
|
|||||||
import { Trash2Icon } from "lucide-react";
|
import { Trash2Icon } from "lucide-react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { timeSince } from "@formbricks/lib/time";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
|
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
|
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
|
||||||
|
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@/lib/constants";
|
||||||
|
import { getIntegrationByType } from "@/lib/integration/service";
|
||||||
|
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
|
|
||||||
import { getIntegrationByType } from "@formbricks/lib/integration/service";
|
|
||||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
|
||||||
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
|
|||||||
@@ -1,250 +1,156 @@
|
|||||||
import "@testing-library/jest-dom/vitest";
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||||
import { getServerSession } from "next-auth";
|
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
import React from "react";
|
import { Session } from "next-auth";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { redirect } from "next/navigation";
|
||||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
|
||||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
|
||||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
|
||||||
import { TMembership } from "@formbricks/types/memberships";
|
import { TMembership } from "@formbricks/types/memberships";
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
import { TProject } from "@formbricks/types/project";
|
import { TProject } from "@formbricks/types/project";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import EnvLayout from "./layout";
|
import EnvLayout from "./layout";
|
||||||
|
|
||||||
// mock all the dependencies
|
// Mock sub-components to render identifiable elements
|
||||||
|
vi.mock("@/app/(app)/environments/[environmentId]/components/EnvironmentLayout", () => ({
|
||||||
vi.mock("@formbricks/lib/constants", () => ({
|
EnvironmentLayout: ({ children }: any) => <div data-testid="EnvironmentLayout">{children}</div>,
|
||||||
IS_FORMBRICKS_CLOUD: false,
|
|
||||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
|
||||||
POSTHOG_HOST: "mock-posthog-host",
|
|
||||||
IS_POSTHOG_CONFIGURED: true,
|
|
||||||
ENCRYPTION_KEY: "mock-encryption-key",
|
|
||||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
|
||||||
GITHUB_ID: "mock-github-id",
|
|
||||||
GITHUB_SECRET: "test-githubID",
|
|
||||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
|
||||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
|
||||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
|
||||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
|
||||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
|
||||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
|
||||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
|
||||||
OIDC_ISSUER: "test-oidc-issuer",
|
|
||||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
|
||||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
|
||||||
WEBAPP_URL: "test-webapp-url",
|
|
||||||
IS_PRODUCTION: false,
|
|
||||||
}));
|
}));
|
||||||
|
vi.mock("@/modules/ui/components/environmentId-base-layout", () => ({
|
||||||
vi.mock("@/tolgee/server", () => ({
|
EnvironmentIdBaseLayout: ({ children, environmentId }: any) => (
|
||||||
getTranslate: vi.fn(() => {
|
<div data-testid="EnvironmentIdBaseLayout">
|
||||||
return (key: string) => {
|
{environmentId}
|
||||||
return key;
|
{children}
|
||||||
};
|
</div>
|
||||||
}),
|
),
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("next-auth", () => ({
|
|
||||||
getServerSession: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@formbricks/lib/environment/auth", () => ({
|
|
||||||
hasUserEnvironmentAccess: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@formbricks/lib/membership/service", () => ({
|
|
||||||
getMembershipByUserIdOrganizationId: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@formbricks/lib/organization/service", () => ({
|
|
||||||
getOrganizationByEnvironmentId: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@formbricks/lib/project/service", () => ({
|
|
||||||
getProjectByEnvironmentId: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@formbricks/lib/user/service", () => ({
|
|
||||||
getUser: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@formbricks/lib/aiModels", () => ({
|
|
||||||
llmModel: {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// mock all the components that are rendered in the layout
|
|
||||||
|
|
||||||
vi.mock("./components/PosthogIdentify", () => ({
|
|
||||||
PosthogIdentify: () => <div data-testid="posthog-identify" />,
|
|
||||||
}));
|
|
||||||
vi.mock("@/app/(app)/components/FormbricksClient", () => ({
|
|
||||||
FormbricksClient: () => <div data-testid="formbricks-client" />,
|
|
||||||
}));
|
}));
|
||||||
vi.mock("@/modules/ui/components/toaster-client", () => ({
|
vi.mock("@/modules/ui/components/toaster-client", () => ({
|
||||||
ToasterClient: () => <div data-testid="mock-toaster" />,
|
ToasterClient: () => <div data-testid="ToasterClient" />,
|
||||||
|
}));
|
||||||
|
vi.mock("../../components/FormbricksClient", () => ({
|
||||||
|
FormbricksClient: ({ userId, email }: any) => (
|
||||||
|
<div data-testid="FormbricksClient">
|
||||||
|
{userId}-{email}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
vi.mock("./components/EnvironmentStorageHandler", () => ({
|
vi.mock("./components/EnvironmentStorageHandler", () => ({
|
||||||
default: () => <div data-testid="mock-storage-handler" />,
|
default: ({ environmentId }: any) => <div data-testid="EnvironmentStorageHandler">{environmentId}</div>,
|
||||||
}));
|
}));
|
||||||
vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({
|
|
||||||
ResponseFilterProvider: ({ children }: { children: React.ReactNode }) => (
|
// Mocks for dependencies
|
||||||
<div data-testid="mock-response-filter-provider">{children}</div>
|
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||||
),
|
environmentIdLayoutChecks: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@/app/(app)/environments/[environmentId]/components/EnvironmentLayout", () => ({
|
vi.mock("@/lib/project/service", () => ({
|
||||||
EnvironmentLayout: ({ children }: { children: React.ReactNode }) => (
|
getProjectByEnvironmentId: vi.fn(),
|
||||||
<div data-testid="mock-environment-result">{children}</div>
|
}));
|
||||||
),
|
vi.mock("@/lib/membership/service", () => ({
|
||||||
|
getMembershipByUserIdOrganizationId: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("EnvLayout", () => {
|
describe("EnvLayout", () => {
|
||||||
beforeEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("redirects to /auth/login if there is no session", async () => {
|
test("renders successfully when all dependencies return valid data", async () => {
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
|
t: ((key: string) => key) as any, // Mock translation function, we don't need to implement it for the test
|
||||||
// Since it's an async server component, call EnvLayout yourself:
|
session: { user: { id: "user1" } } as Session,
|
||||||
const layoutElement = await EnvLayout({
|
user: { id: "user1", email: "user1@example.com" } as TUser,
|
||||||
params: Promise.resolve({ environmentId: "env-123" }),
|
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||||
children: <div data-testid="child-content">Hello!</div>,
|
|
||||||
});
|
});
|
||||||
|
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce({ id: "proj1" } as TProject);
|
||||||
|
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce({
|
||||||
|
id: "member1",
|
||||||
|
} as unknown as TMembership);
|
||||||
|
|
||||||
// Because we have no session, we expect a redirect to "/auth/login"
|
const result = await EnvLayout({
|
||||||
expect(redirect).toHaveBeenCalledWith("/auth/login");
|
params: Promise.resolve({ environmentId: "env1" }),
|
||||||
|
children: <div data-testid="child">Content</div>,
|
||||||
|
});
|
||||||
|
render(result);
|
||||||
|
|
||||||
// If your code calls redirect() early and returns no JSX,
|
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveTextContent("env1");
|
||||||
// layoutElement might be undefined or null.
|
expect(screen.getByTestId("EnvironmentStorageHandler")).toHaveTextContent("env1");
|
||||||
expect(layoutElement).toBeUndefined();
|
expect(screen.getByTestId("EnvironmentLayout")).toBeDefined();
|
||||||
|
expect(screen.getByTestId("child")).toHaveTextContent("Content");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("redirects to /auth/login if user does not exist in DB", async () => {
|
test("throws error if project is not found", async () => {
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
user: { id: "user-123" },
|
t: ((key: string) => key) as any,
|
||||||
|
session: { user: { id: "user1" } } as Session,
|
||||||
|
user: { id: "user1", email: "user1@example.com" } as TUser,
|
||||||
|
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mocked(getUser).mockResolvedValueOnce(null); // user not found
|
|
||||||
|
|
||||||
const layoutElement = await EnvLayout({
|
|
||||||
params: Promise.resolve({ environmentId: "env-123" }),
|
|
||||||
children: <div data-testid="child-content">Hello!</div>,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(redirect).toHaveBeenCalledWith("/auth/login");
|
|
||||||
expect(layoutElement).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws AuthorizationError if user does not have environment access", async () => {
|
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({
|
|
||||||
user: { id: "user-123" },
|
|
||||||
});
|
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({
|
|
||||||
id: "user-123",
|
|
||||||
email: "test@example.com",
|
|
||||||
} as TUser);
|
|
||||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(false);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
EnvLayout({
|
|
||||||
params: Promise.resolve({ environmentId: "env-123" }),
|
|
||||||
children: <div>Child</div>,
|
|
||||||
})
|
|
||||||
).rejects.toThrow(AuthorizationError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws if no organization is found", async () => {
|
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({
|
|
||||||
user: { id: "user-123" },
|
|
||||||
});
|
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({
|
|
||||||
id: "user-123",
|
|
||||||
email: "test@example.com",
|
|
||||||
} as TUser);
|
|
||||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
|
|
||||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(null);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
EnvLayout({
|
|
||||||
params: Promise.resolve({ environmentId: "env-123" }),
|
|
||||||
children: <div data-testid="child-content">Hello from children!</div>,
|
|
||||||
})
|
|
||||||
).rejects.toThrow("common.organization_not_found");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws if no project is found", async () => {
|
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({
|
|
||||||
user: { id: "user-123" },
|
|
||||||
});
|
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({
|
|
||||||
id: "user-123",
|
|
||||||
email: "test@example.com",
|
|
||||||
} as TUser);
|
|
||||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
|
|
||||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce({ id: "org-999" } as TOrganization);
|
|
||||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(null);
|
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(null);
|
||||||
|
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce({
|
||||||
|
id: "member1",
|
||||||
|
} as unknown as TMembership);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
EnvLayout({
|
EnvLayout({
|
||||||
params: Promise.resolve({ environmentId: "env-123" }),
|
params: Promise.resolve({ environmentId: "env1" }),
|
||||||
children: <div>Child</div>,
|
children: <div>Content</div>,
|
||||||
})
|
})
|
||||||
).rejects.toThrow("project_not_found");
|
).rejects.toThrow("common.project_not_found");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls notFound if membership is missing", async () => {
|
test("throws error if membership is not found", async () => {
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
user: { id: "user-123" },
|
t: ((key: string) => key) as any,
|
||||||
|
session: { user: { id: "user1" } } as Session,
|
||||||
|
user: { id: "user1", email: "user1@example.com" } as TUser,
|
||||||
|
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||||
});
|
});
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({
|
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce({ id: "proj1" } as TProject);
|
||||||
id: "user-123",
|
|
||||||
email: "test@example.com",
|
|
||||||
} as TUser);
|
|
||||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
|
|
||||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce({ id: "org-999" } as TOrganization);
|
|
||||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce({ id: "proj-111" } as TProject);
|
|
||||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(null);
|
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(null);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
EnvLayout({
|
EnvLayout({
|
||||||
params: Promise.resolve({ environmentId: "env-123" }),
|
params: Promise.resolve({ environmentId: "env1" }),
|
||||||
children: <div>Child</div>,
|
children: <div>Content</div>,
|
||||||
})
|
})
|
||||||
).rejects.toThrow("membership_not_found");
|
).rejects.toThrow("common.membership_not_found");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders environment layout if everything is valid", async () => {
|
test("calls redirect when session is null", async () => {
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
user: { id: "user-123" },
|
t: ((key: string) => key) as any,
|
||||||
|
session: undefined as unknown as Session,
|
||||||
|
user: undefined as unknown as TUser,
|
||||||
|
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||||
});
|
});
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({
|
vi.mocked(redirect).mockImplementationOnce(() => {
|
||||||
id: "user-123",
|
throw new Error("Redirect called");
|
||||||
email: "test@example.com",
|
|
||||||
} as TUser);
|
|
||||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
|
|
||||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce({ id: "org-999" } as TOrganization);
|
|
||||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce({ id: "proj-111" } as TProject);
|
|
||||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce({
|
|
||||||
id: "membership-123",
|
|
||||||
} as unknown as TMembership);
|
|
||||||
|
|
||||||
let layoutElement: React.ReactNode;
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
layoutElement = await EnvLayout({
|
|
||||||
params: Promise.resolve({ environmentId: "env-123" }),
|
|
||||||
children: <div data-testid="child-content">Hello from children!</div>,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Now render the fully resolved layout
|
|
||||||
render(layoutElement);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByTestId("child-content")).toHaveTextContent("Hello from children!");
|
await expect(
|
||||||
expect(screen.getByTestId("posthog-identify")).toBeInTheDocument();
|
EnvLayout({
|
||||||
expect(screen.getByTestId("formbricks-client")).toBeInTheDocument();
|
params: Promise.resolve({ environmentId: "env1" }),
|
||||||
expect(screen.getByTestId("mock-toaster")).toBeInTheDocument();
|
children: <div>Content</div>,
|
||||||
expect(screen.getByTestId("mock-storage-handler")).toBeInTheDocument();
|
})
|
||||||
expect(screen.getByTestId("mock-response-filter-provider")).toBeInTheDocument();
|
).rejects.toThrow("Redirect called");
|
||||||
expect(screen.getByTestId("mock-environment-result")).toBeInTheDocument();
|
});
|
||||||
|
|
||||||
|
test("throws error if user is null", async () => {
|
||||||
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
|
t: ((key: string) => key) as any,
|
||||||
|
session: { user: { id: "user1" } } as Session,
|
||||||
|
user: undefined as unknown as TUser,
|
||||||
|
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(redirect).mockImplementationOnce(() => {
|
||||||
|
throw new Error("Redirect called");
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
EnvLayout({
|
||||||
|
params: Promise.resolve({ environmentId: "env1" }),
|
||||||
|
children: <div>Content</div>,
|
||||||
|
})
|
||||||
|
).rejects.toThrow("common.user_not_found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,10 @@
|
|||||||
import { EnvironmentLayout } from "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout";
|
import { EnvironmentLayout } from "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout";
|
||||||
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||||
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout";
|
||||||
import { getServerSession } from "next-auth";
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { IS_POSTHOG_CONFIGURED } from "@formbricks/lib/constants";
|
|
||||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
|
||||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
|
||||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
|
||||||
import { FormbricksClient } from "../../components/FormbricksClient";
|
|
||||||
import EnvironmentStorageHandler from "./components/EnvironmentStorageHandler";
|
import EnvironmentStorageHandler from "./components/EnvironmentStorageHandler";
|
||||||
import { PosthogIdentify } from "./components/PosthogIdentify";
|
|
||||||
|
|
||||||
const EnvLayout = async (props: {
|
const EnvLayout = async (props: {
|
||||||
params: Promise<{ environmentId: string }>;
|
params: Promise<{ environmentId: string }>;
|
||||||
@@ -24,27 +14,16 @@ const EnvLayout = async (props: {
|
|||||||
|
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
const t = await getTranslate();
|
const { t, session, user, organization } = await environmentIdLayoutChecks(params.environmentId);
|
||||||
const session = await getServerSession(authOptions);
|
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session) {
|
||||||
return redirect(`/auth/login`);
|
return redirect(`/auth/login`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await getUser(session.user.id);
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return redirect(`/auth/login`);
|
throw new Error(t("common.user_not_found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAccess = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
|
|
||||||
if (!hasAccess) {
|
|
||||||
throw new AuthorizationError(t("common.not_authorized"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const organization = await getOrganizationByEnvironmentId(params.environmentId);
|
|
||||||
if (!organization) {
|
|
||||||
throw new Error(t("common.organization_not_found"));
|
|
||||||
}
|
|
||||||
const project = await getProjectByEnvironmentId(params.environmentId);
|
const project = await getProjectByEnvironmentId(params.environmentId);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error(t("common.project_not_found"));
|
throw new Error(t("common.project_not_found"));
|
||||||
@@ -57,23 +36,16 @@ const EnvLayout = async (props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponseFilterProvider>
|
<EnvironmentIdBaseLayout
|
||||||
<PosthogIdentify
|
environmentId={params.environmentId}
|
||||||
session={session}
|
session={session}
|
||||||
user={user}
|
user={user}
|
||||||
environmentId={params.environmentId}
|
organization={organization}>
|
||||||
organizationId={organization.id}
|
|
||||||
organizationName={organization.name}
|
|
||||||
organizationBilling={organization.billing}
|
|
||||||
isPosthogEnabled={IS_POSTHOG_CONFIGURED}
|
|
||||||
/>
|
|
||||||
<FormbricksClient userId={user.id} email={user.email} />
|
|
||||||
<ToasterClient />
|
|
||||||
<EnvironmentStorageHandler environmentId={params.environmentId} />
|
<EnvironmentStorageHandler environmentId={params.environmentId} />
|
||||||
<EnvironmentLayout environmentId={params.environmentId} session={session}>
|
<EnvironmentLayout environmentId={params.environmentId} session={session}>
|
||||||
{children}
|
{children}
|
||||||
</EnvironmentLayout>
|
</EnvironmentLayout>
|
||||||
</ResponseFilterProvider>
|
</EnvironmentIdBaseLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
|
||||||
|
|
||||||
const EnvironmentPage = async (props) => {
|
const EnvironmentPage = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||||
|
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
|
||||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
|
||||||
|
|
||||||
const AccountSettingsLayout = async (props) => {
|
const AccountSettingsLayout = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { updateUser } from "@/lib/user/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { updateUser } from "@formbricks/lib/user/service";
|
|
||||||
import { ZUserNotificationSettings } from "@formbricks/types/user";
|
import { ZUserNotificationSettings } from "@formbricks/types/user";
|
||||||
|
|
||||||
const ZUpdateNotificationSettingsAction = z.object({
|
const ZUpdateNotificationSettingsAction = z.object({
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
||||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||||
import { EditAlerts } from "./components/EditAlerts";
|
import { EditAlerts } from "./components/EditAlerts";
|
||||||
import { EditWeeklySummary } from "./components/EditWeeklySummary";
|
import { EditWeeklySummary } from "./components/EditWeeklySummary";
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { deleteFile } from "@/lib/storage/service";
|
||||||
|
import { getFileNameWithIdFromUrl } from "@/lib/storage/utils";
|
||||||
|
import { updateUser } from "@/lib/user/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { deleteFile } from "@formbricks/lib/storage/service";
|
|
||||||
import { getFileNameWithIdFromUrl } from "@formbricks/lib/storage/utils";
|
|
||||||
import { updateUser } from "@formbricks/lib/user/service";
|
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { ZUserUpdateInput } from "@formbricks/types/user";
|
import { ZUserUpdateInput } from "@formbricks/types/user";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { appLanguages } from "@/lib/i18n/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -23,7 +24,6 @@ import { ChevronDownIcon } from "lucide-react";
|
|||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { appLanguages } from "@formbricks/lib/i18n/utils";
|
|
||||||
import { TUser, ZUser } from "@formbricks/types/user";
|
import { TUser, ZUser } from "@formbricks/types/user";
|
||||||
import { updateUserAction } from "../actions";
|
import { updateUserAction } from "../actions";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
||||||
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
|
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
|
||||||
|
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
|
import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
|
||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils";
|
import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
@@ -7,9 +10,6 @@ import { PageHeader } from "@/modules/ui/components/page-header";
|
|||||||
import { SettingsId } from "@/modules/ui/components/settings-id";
|
import { SettingsId } from "@/modules/ui/components/settings-id";
|
||||||
import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
|
import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
import { getOrganizationsWhereUserIsSingleOwner } from "@formbricks/lib/organization/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { SettingsCard } from "../../components/SettingsCard";
|
import { SettingsCard } from "../../components/SettingsCard";
|
||||||
import { DeleteAccount } from "./components/DeleteAccount";
|
import { DeleteAccount } from "./components/DeleteAccount";
|
||||||
import { EditProfileAvatarForm } from "./components/EditProfileAvatarForm";
|
import { EditProfileAvatarForm } from "./components/EditProfileAvatarForm";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
import Loading from "@/modules/organization/settings/api-keys/loading";
|
import Loading from "@/modules/organization/settings/api-keys/loading";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
|
|
||||||
export default function LoadingPage() {
|
export default function LoadingPage() {
|
||||||
return <Loading isFormbricksCloud={IS_FORMBRICKS_CLOUD} />;
|
return <Loading isFormbricksCloud={IS_FORMBRICKS_CLOUD} />;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||||
|
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
|
|
||||||
const Loading = async () => {
|
const Loading = async () => {
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
|
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
|
||||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||||
|
|
||||||
interface OrganizationSettingsNavbarProps {
|
interface OrganizationSettingsNavbarProps {
|
||||||
@@ -22,7 +22,7 @@ export const OrganizationSettingsNavbar = ({
|
|||||||
loading,
|
loading,
|
||||||
}: OrganizationSettingsNavbarProps) => {
|
}: OrganizationSettingsNavbarProps) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { isMember } = getAccessFlags(membershipRole);
|
const { isMember, isOwner } = getAccessFlags(membershipRole);
|
||||||
const isPricingDisabled = isMember;
|
const isPricingDisabled = isMember;
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ export const OrganizationSettingsNavbar = ({
|
|||||||
label: t("common.api_keys"),
|
label: t("common.api_keys"),
|
||||||
href: `/environments/${environmentId}/settings/api-keys`,
|
href: `/environments/${environmentId}/settings/api-keys`,
|
||||||
current: pathname?.includes("/api-keys"),
|
current: pathname?.includes("/api-keys"),
|
||||||
|
hidden: !isOwner,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||||
|
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
|
|
||||||
const Loading = async () => {
|
const Loading = async () => {
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||||
|
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -8,7 +9,6 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { CheckIcon } from "lucide-react";
|
import { CheckIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
@@ -123,7 +123,7 @@ const Page = async (props) => {
|
|||||||
<div className="relative isolate mt-8 overflow-hidden rounded-lg bg-slate-900 px-3 pt-8 shadow-2xl sm:px-8 md:pt-12 lg:flex lg:gap-x-10 lg:px-12 lg:pt-0">
|
<div className="relative isolate mt-8 overflow-hidden rounded-lg bg-slate-900 px-3 pt-8 shadow-2xl sm:px-8 md:pt-12 lg:flex lg:gap-x-10 lg:px-12 lg:pt-0">
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 1024 1024"
|
viewBox="0 0 1024 1024"
|
||||||
className="absolute left-1/2 top-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
|
className="absolute top-1/2 left-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<circle
|
<circle
|
||||||
cx={512}
|
cx={512}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { deleteOrganization, updateOrganization } from "@/lib/organization/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { deleteOrganization, updateOrganization } from "@formbricks/lib/organization/service";
|
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||||
import { ZOrganizationUpdateInput } from "@formbricks/types/organizations";
|
import { ZOrganizationUpdateInput } from "@formbricks/types/organizations";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { deleteOrganizationAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
|
import { deleteOrganizationAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
|
||||||
|
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
||||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
@@ -9,7 +10,6 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Dispatch, SetStateAction, useState } from "react";
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@formbricks/lib/localStorage";
|
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
|
|
||||||
type DeleteOrganizationProps = {
|
type DeleteOrganizationProps = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { updateOrganizationNameAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
|
import { updateOrganizationNameAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
|
||||||
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -18,7 +19,6 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
|
||||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||||
import { TOrganization, ZOrganization } from "@formbricks/types/organizations";
|
import { TOrganization, ZOrganization } from "@formbricks/types/organizations";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { LoadingCard } from "@/app/(app)/components/LoadingCard";
|
import { LoadingCard } from "@/app/(app)/components/LoadingCard";
|
||||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||||
|
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
|
|
||||||
const Loading = async () => {
|
const Loading = async () => {
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import {
|
import {
|
||||||
getIsMultiOrgEnabled,
|
getIsMultiOrgEnabled,
|
||||||
getIsOrganizationAIReady,
|
getIsOrganizationAIReady,
|
||||||
@@ -6,12 +7,11 @@ import {
|
|||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import Page from "./page";
|
import Page from "./page";
|
||||||
|
|
||||||
vi.mock("@formbricks/lib/constants", () => ({
|
vi.mock("@/lib/constants", () => ({
|
||||||
IS_FORMBRICKS_CLOUD: false,
|
IS_FORMBRICKS_CLOUD: false,
|
||||||
IS_PRODUCTION: false,
|
IS_PRODUCTION: false,
|
||||||
FB_LOGO_URL: "https://example.com/mock-logo.png",
|
FB_LOGO_URL: "https://example.com/mock-logo.png",
|
||||||
@@ -33,19 +33,23 @@ vi.mock("@formbricks/lib/constants", () => ({
|
|||||||
WEBAPP_URL: "mock-webapp-url",
|
WEBAPP_URL: "mock-webapp-url",
|
||||||
SMTP_HOST: "mock-smtp-host",
|
SMTP_HOST: "mock-smtp-host",
|
||||||
SMTP_PORT: "mock-smtp-port",
|
SMTP_PORT: "mock-smtp-port",
|
||||||
AI_AZURE_LLM_RESSOURCE_NAME: "mock-azure-llm-resource-name",
|
AI_AZURE_LLM_RESSOURCE_NAME: "mock-ai-azure-llm-ressource-name",
|
||||||
AI_AZURE_LLM_API_KEY: "mock-azure-llm-api-key",
|
AI_AZURE_LLM_API_KEY: "mock-ai",
|
||||||
AI_AZURE_LLM_DEPLOYMENT_ID: "mock-azure-llm-deployment-id",
|
AI_AZURE_LLM_DEPLOYMENT_ID: "mock-ai-azure-llm-deployment-id",
|
||||||
AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: "mock-azure-embeddings-resource-name",
|
AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: "mock-ai-azure-embeddings-ressource-name",
|
||||||
AI_AZURE_EMBEDDINGS_API_KEY: "mock-azure-embeddings-api-key",
|
AI_AZURE_EMBEDDINGS_API_KEY: "mock-ai-azure-embeddings-api-key",
|
||||||
AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: "mock-azure-embeddings-deployment-id",
|
AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: "mock-ai-azure-embeddings-deployment-id",
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("next-auth", () => ({
|
||||||
|
getServerSession: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/tolgee/server", () => ({
|
vi.mock("@/tolgee/server", () => ({
|
||||||
getTranslate: vi.fn(),
|
getTranslate: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@formbricks/lib/user/service", () => ({
|
vi.mock("@/lib/user/service", () => ({
|
||||||
getUser: vi.fn(),
|
getUser: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -80,7 +84,7 @@ describe("Page", () => {
|
|||||||
vi.mocked(getWhiteLabelPermission).mockResolvedValue(true);
|
vi.mocked(getWhiteLabelPermission).mockResolvedValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the page with organization settings", async () => {
|
test("renders the page with organization settings", async () => {
|
||||||
const props = {
|
const props = {
|
||||||
params: Promise.resolve({ environmentId: "env-123" }),
|
params: Promise.resolve({ environmentId: "env-123" }),
|
||||||
};
|
};
|
||||||
@@ -90,7 +94,7 @@ describe("Page", () => {
|
|||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders if session user id empty", async () => {
|
test("renders if session user id empty", async () => {
|
||||||
mockEnvironmentAuth.session.user.id = "";
|
mockEnvironmentAuth.session.user.id = "";
|
||||||
|
|
||||||
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
|
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
|
||||||
@@ -104,7 +108,7 @@ describe("Page", () => {
|
|||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles getEnvironmentAuth error", async () => {
|
test("handles getEnvironmentAuth error", async () => {
|
||||||
vi.mocked(getEnvironmentAuth).mockRejectedValue(new Error("Authentication error"));
|
vi.mocked(getEnvironmentAuth).mockRejectedValue(new Error("Authentication error"));
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||||
import { AIToggle } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/components/AIToggle";
|
import { AIToggle } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/components/AIToggle";
|
||||||
|
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import {
|
import {
|
||||||
getIsMultiOrgEnabled,
|
getIsMultiOrgEnabled,
|
||||||
getIsOrganizationAIReady,
|
getIsOrganizationAIReady,
|
||||||
@@ -11,8 +13,6 @@ import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper
|
|||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { SettingsId } from "@/modules/ui/components/settings-id";
|
import { SettingsId } from "@/modules/ui/components/settings-id";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { SettingsCard } from "../../components/SettingsCard";
|
import { SettingsCard } from "../../components/SettingsCard";
|
||||||
import { DeleteOrganization } from "./components/DeleteOrganization";
|
import { DeleteOrganization } from "./components/DeleteOrganization";
|
||||||
import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm";
|
import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||||
|
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
|
||||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
|
||||||
|
|
||||||
const Layout = async (props) => {
|
const Layout = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
import { Badge } from "@/modules/ui/components/badge";
|
import { Badge } from "@/modules/ui/components/badge";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
|
||||||
|
|
||||||
export const SettingsCard = ({
|
export const SettingsCard = ({
|
||||||
title,
|
title,
|
||||||
@@ -31,7 +31,7 @@ export const SettingsCard = ({
|
|||||||
id={title}>
|
id={title}>
|
||||||
<div className="border-b border-slate-200 px-4 pb-4">
|
<div className="border-b border-slate-200 px-4 pb-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<h3 className="text-lg font-medium capitalize leading-6 text-slate-900">{title}</h3>
|
<h3 className="text-lg leading-6 font-medium text-slate-900 capitalize">{title}</h3>
|
||||||
<div className="ml-2">
|
<div className="ml-2">
|
||||||
{beta && <Badge size="normal" type="warning" text="Beta" />}
|
{beta && <Badge size="normal" type="warning" text="Beta" />}
|
||||||
{soon && (
|
{soon && (
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { generateInsightsForSurvey } from "@/app/api/(internal)/insights/lib/utils";
|
import { generateInsightsForSurvey } from "@/app/api/(internal)/insights/lib/utils";
|
||||||
|
import { getResponseCountBySurveyId, getResponses } from "@/lib/response/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getResponseCountBySurveyId, getResponses } from "@formbricks/lib/response/service";
|
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { ZResponseFilterCriteria } from "@formbricks/types/responses";
|
import { ZResponseFilterCriteria } from "@formbricks/types/responses";
|
||||||
import { getSurveySummary } from "./summary/lib/surveySummary";
|
import { getSurveySummary } from "./summary/lib/surveySummary";
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import {
|
|||||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
||||||
import { getFormattedFilters } from "@/app/lib/surveys/surveys";
|
import { getFormattedFilters } from "@/app/lib/surveys/surveys";
|
||||||
import { getResponseCountBySurveySharingKeyAction } from "@/app/share/[sharingKey]/actions";
|
import { getResponseCountBySurveySharingKeyAction } from "@/app/share/[sharingKey]/actions";
|
||||||
|
import { useIntervalWhenFocused } from "@/lib/utils/hooks/useIntervalWhenFocused";
|
||||||
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
|
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { InboxIcon, PresentationIcon } from "lucide-react";
|
import { InboxIcon, PresentationIcon } from "lucide-react";
|
||||||
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useIntervalWhenFocused } from "@formbricks/lib/utils/hooks/useIntervalWhenFocused";
|
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
|
||||||
interface SurveyAnalysisNavigationProps {
|
interface SurveyAnalysisNavigationProps {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
||||||
|
import { getSurvey } from "@/lib/survey/service";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
|
||||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: Promise<{ surveyId: string; environmentId: string }>;
|
params: Promise<{ surveyId: string; environmentId: string }>;
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import {
|
|||||||
getResponseCountBySurveySharingKeyAction,
|
getResponseCountBySurveySharingKeyAction,
|
||||||
getResponsesBySurveySharingKeyAction,
|
getResponsesBySurveySharingKeyAction,
|
||||||
} from "@/app/share/[sharingKey]/actions";
|
} from "@/app/share/[sharingKey]/actions";
|
||||||
|
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||||
import { useParams, useSearchParams } from "next/navigation";
|
import { useParams, useSearchParams } from "next/navigation";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TResponse } from "@formbricks/types/responses";
|
import { TResponse } from "@formbricks/types/responses";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { cn } from "@/lib/cn";
|
||||||
import { getCommonPinningStyles } from "@/modules/ui/components/data-table/lib/utils";
|
import { getCommonPinningStyles } from "@/modules/ui/components/data-table/lib/utils";
|
||||||
import { TableCell } from "@/modules/ui/components/table";
|
import { TableCell } from "@/modules/ui/components/table";
|
||||||
import { Cell, Row, flexRender } from "@tanstack/react-table";
|
import { Cell, Row, flexRender } from "@tanstack/react-table";
|
||||||
import { Maximize2Icon } from "lucide-react";
|
import { Maximize2Icon } from "lucide-react";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
|
||||||
import { TResponse, TResponseTableData } from "@formbricks/types/responses";
|
import { TResponse, TResponseTableData } from "@formbricks/types/responses";
|
||||||
|
|
||||||
interface ResponseTableCellProps {
|
interface ResponseTableCellProps {
|
||||||
@@ -36,7 +36,7 @@ export const ResponseTableCell = ({
|
|||||||
// Conditional rendering of maximize icon
|
// Conditional rendering of maximize icon
|
||||||
const renderMaximizeIcon = cell.column.id === "createdAt" && (
|
const renderMaximizeIcon = cell.column.id === "createdAt" && (
|
||||||
<div
|
<div
|
||||||
className="hidden flex-shrink-0 cursor-pointer items-center rounded-md border border-slate-200 bg-white p-2 hover:border-slate-300 group-hover:flex"
|
className="hidden flex-shrink-0 cursor-pointer items-center rounded-md border border-slate-200 bg-white p-2 group-hover:flex hover:border-slate-300"
|
||||||
onClick={handleCellClick}>
|
onClick={handleCellClick}>
|
||||||
<Maximize2Icon className="h-4 w-4" />
|
<Maximize2Icon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user