mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-14 03:04:00 -05:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f356e86729 |
@@ -1 +0,0 @@
|
||||
{"sessionId":"f77248e2-8840-41c6-968b-c3b7d8a9e913","pid":49125,"acquiredAt":1776168010367}
|
||||
+8
-58
@@ -32,54 +32,12 @@ CRON_SECRET=
|
||||
# Set the minimum log level(debug, info, warn, error, fatal)
|
||||
LOG_LEVEL=info
|
||||
|
||||
# BullMQ workers require REDIS_URL (for example `redis://localhost:6379`) to be set.
|
||||
# BullMQ worker startup is enabled by default outside tests. Set to 0 to disable.
|
||||
# BULLMQ_WORKER_ENABLED=1
|
||||
# Set to 1 on web/API pods that only enqueue jobs while a separate BullMQ worker deployment consumes them.
|
||||
# BULLMQ_EXTERNAL_WORKER_ENABLED=0
|
||||
|
||||
# Number of BullMQ worker instances started per Formbricks server process.
|
||||
# BULLMQ_WORKER_COUNT=1
|
||||
|
||||
# Number of concurrent jobs each BullMQ worker can process.
|
||||
# BULLMQ_WORKER_CONCURRENCY=1
|
||||
|
||||
# Survey publish/close scheduling is configured with public build-time env vars because the editor UI
|
||||
# also needs to render the selected execution time and timezone.
|
||||
# NEXT_PUBLIC_SURVEY_SCHEDULING_TIME_ZONE=Europe/Berlin
|
||||
# NEXT_PUBLIC_SURVEY_SCHEDULING_LOCAL_HOUR=0
|
||||
# NEXT_PUBLIC_SURVEY_SCHEDULING_LOCAL_MINUTE=0
|
||||
|
||||
##############
|
||||
# DATABASE #
|
||||
##############
|
||||
|
||||
DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=public'
|
||||
|
||||
#################
|
||||
# HUB (DEV) #
|
||||
#################
|
||||
# The dev stack (pnpm db:up / pnpm go) runs Formbricks Hub on port 8080.
|
||||
# Set explicitly to avoid confusion; override as needed when using docker-compose.dev.yml.
|
||||
HUB_API_KEY=dev-api-key
|
||||
HUB_API_URL=http://localhost:8080
|
||||
HUB_DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
# Hub image tag used by docker-compose.dev.yml (hub + hub-migrate). Leave unset to use the
|
||||
# pinned default in the compose file; override here when testing a specific Hub release.
|
||||
# HUB_IMAGE_TAG=0.2.0
|
||||
|
||||
###########################
|
||||
# CUBE ANALYTICS (XM V5) #
|
||||
###########################
|
||||
# XM Suite v5 analysis features require Cube.js. The optional xm dev profile exposes Cube on port 4000.
|
||||
# Uncomment COMPOSE_PROFILES=xm to run the optional Cube analytics service.
|
||||
# COMPOSE_PROFILES=xm
|
||||
CUBEJS_API_URL=http://localhost:4000
|
||||
# Generate with: openssl rand -hex 32. `pnpm dev:setup` will create/preserve this automatically.
|
||||
CUBEJS_API_SECRET=
|
||||
CUBEJS_JWT_ISSUER=formbricks-web
|
||||
CUBEJS_JWT_AUDIENCE=formbricks-cube
|
||||
|
||||
################
|
||||
# MAIL SETUP #
|
||||
################
|
||||
@@ -182,17 +140,16 @@ AZUREAD_TENANT_ID=
|
||||
|
||||
# Configure Formbricks AI at the instance level
|
||||
# Set the provider used for AI features on this instance.
|
||||
# Accepted values for AI_PROVIDER: aws, google, azure
|
||||
# Set AI_MODEL to the provider-specific model or deployment name and configure the matching provider settings below.
|
||||
# AI_PROVIDER=google
|
||||
# Accepted values for AI_PROVIDER: aws, gcp, azure
|
||||
# Set AI_MODEL to the provider-specific model or deployment name and configure the matching credentials below.
|
||||
# AI_PROVIDER=gcp
|
||||
# AI_MODEL=gemini-2.5-flash
|
||||
|
||||
# Google Cloud settings for Gemini models
|
||||
# Credentials are optional when Application Default Credentials are available.
|
||||
# AI_GOOGLE_CLOUD_PROJECT=
|
||||
# AI_GOOGLE_CLOUD_LOCATION=
|
||||
# AI_GOOGLE_CLOUD_CREDENTIALS_JSON=
|
||||
# AI_GOOGLE_CLOUD_APPLICATION_CREDENTIALS=
|
||||
# Google Vertex AI credentials
|
||||
# AI_GCP_PROJECT=
|
||||
# AI_GCP_LOCATION=
|
||||
# AI_GCP_CREDENTIALS_JSON=
|
||||
# AI_GCP_APPLICATION_CREDENTIALS=
|
||||
|
||||
# Amazon Bedrock credentials
|
||||
# AI_AWS_REGION=
|
||||
@@ -311,13 +268,6 @@ REDIS_URL=redis://localhost:6379
|
||||
# If the ip should be added in the log or not. Default 0
|
||||
# AUDIT_LOG_GET_USER_IP=0
|
||||
|
||||
# Optional Cube.js database overrides. The official local docker-compose.dev.yml stack points Cube at the
|
||||
# local `postgres` service automatically; set these only when running Cube yourself or changing bundled defaults.
|
||||
# CUBEJS_DB_HOST=postgres
|
||||
# CUBEJS_DB_PORT=5432
|
||||
# CUBEJS_DB_NAME=postgres
|
||||
# CUBEJS_DB_USER=postgres
|
||||
# CUBEJS_DB_PASS=postgres
|
||||
|
||||
# Lingo.dev API key for translation generation
|
||||
LINGO_API_KEY=your_api_key_here
|
||||
|
||||
@@ -284,10 +284,6 @@ runs:
|
||||
database_url=${{ env.DUMMY_DATABASE_URL }}
|
||||
encryption_key=${{ env.DUMMY_ENCRYPTION_KEY }}
|
||||
redis_url=${{ env.DUMMY_REDIS_URL }}
|
||||
hub_api_url=${{ env.DUMMY_HUB_API_URL }}
|
||||
hub_api_key=${{ env.DUMMY_HUB_API_KEY }}
|
||||
cubejs_api_url=${{ env.DUMMY_CUBEJS_API_URL }}
|
||||
cubejs_api_secret=${{ env.DUMMY_CUBEJS_API_SECRET }}
|
||||
sentry_auth_token=${{ env.SENTRY_AUTH_TOKEN }}
|
||||
posthog_key=${{ env.POSTHOG_KEY }}
|
||||
env:
|
||||
@@ -295,10 +291,6 @@ runs:
|
||||
DUMMY_DATABASE_URL: ${{ env.DUMMY_DATABASE_URL }}
|
||||
DUMMY_ENCRYPTION_KEY: ${{ env.DUMMY_ENCRYPTION_KEY }}
|
||||
DUMMY_REDIS_URL: ${{ env.DUMMY_REDIS_URL }}
|
||||
DUMMY_HUB_API_URL: ${{ env.DUMMY_HUB_API_URL }}
|
||||
DUMMY_HUB_API_KEY: ${{ env.DUMMY_HUB_API_KEY }}
|
||||
DUMMY_CUBEJS_API_URL: ${{ env.DUMMY_CUBEJS_API_URL }}
|
||||
DUMMY_CUBEJS_API_SECRET: ${{ env.DUMMY_CUBEJS_API_SECRET }}
|
||||
SENTRY_AUTH_TOKEN: ${{ env.SENTRY_AUTH_TOKEN }}
|
||||
POSTHOG_KEY: ${{ env.POSTHOG_KEY }}
|
||||
|
||||
|
||||
@@ -57,14 +57,16 @@ runs:
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
|
||||
- name: Create .env
|
||||
run: pnpm dev:setup
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
shell: bash
|
||||
|
||||
- name: Fill E2E_TESTING in .env
|
||||
- name: Fill ENCRYPTION_KEY, ENTERPRISE_LICENSE_KEY and E2E_TESTING in .env
|
||||
env:
|
||||
E2E_TESTING_MODE: ${{ inputs.e2e_testing_mode }}
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
echo "E2E_TESTING=$E2E_TESTING_MODE" >> .env
|
||||
shell: bash
|
||||
|
||||
|
||||
@@ -91,9 +91,5 @@ jobs:
|
||||
DUMMY_DATABASE_URL: ${{ secrets.DUMMY_DATABASE_URL }}
|
||||
DUMMY_ENCRYPTION_KEY: ${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||
DUMMY_REDIS_URL: ${{ secrets.DUMMY_REDIS_URL }}
|
||||
DUMMY_HUB_API_URL: ${{ secrets.DUMMY_HUB_API_URL }}
|
||||
DUMMY_HUB_API_KEY: ${{ secrets.DUMMY_HUB_API_KEY }}
|
||||
DUMMY_CUBEJS_API_URL: ${{ secrets.DUMMY_CUBEJS_API_URL }}
|
||||
DUMMY_CUBEJS_API_SECRET: ${{ secrets.DUMMY_CUBEJS_API_SECRET }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
||||
|
||||
@@ -73,10 +73,6 @@ jobs:
|
||||
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
||||
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||
redis_url=redis://localhost:6379
|
||||
hub_api_url=http://localhost:4000
|
||||
hub_api_key=build-time-placeholder
|
||||
cubejs_api_url=http://localhost:4000
|
||||
cubejs_api_secret=build-time-placeholder
|
||||
|
||||
- name: Verify and Initialize PostgreSQL
|
||||
run: |
|
||||
|
||||
@@ -68,12 +68,16 @@ jobs:
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
shell: bash
|
||||
|
||||
- name: Create .env
|
||||
run: pnpm dev:setup
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
shell: bash
|
||||
|
||||
- name: Fill ENTERPRISE_LICENSE_KEY and E2E_TESTING in .env
|
||||
- name: Fill ENCRYPTION_KEY, ENTERPRISE_LICENSE_KEY and E2E_TESTING in .env
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/ENTERPRISE_LICENSE_KEY=.*/ENTERPRISE_LICENSE_KEY=${{ secrets.ENTERPRISE_LICENSE_KEY }}/" .env
|
||||
sed -i "s|REDIS_URL=.*|REDIS_URL=redis://localhost:6379|" .env
|
||||
echo "" >> .env
|
||||
|
||||
@@ -31,8 +31,15 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
- name: Create .env
|
||||
run: pnpm dev:setup
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Generate Random ENCRYPTION_KEY, CRON_SECRET & NEXTAUTH_SECRET and fill in .env
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
@@ -47,8 +47,4 @@ jobs:
|
||||
DUMMY_DATABASE_URL: ${{ secrets.DUMMY_DATABASE_URL }}
|
||||
DUMMY_ENCRYPTION_KEY: ${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||
DUMMY_REDIS_URL: ${{ secrets.DUMMY_REDIS_URL }}
|
||||
DUMMY_HUB_API_URL: ${{ secrets.DUMMY_HUB_API_URL }}
|
||||
DUMMY_HUB_API_KEY: ${{ secrets.DUMMY_HUB_API_KEY }}
|
||||
DUMMY_CUBEJS_API_URL: ${{ secrets.DUMMY_CUBEJS_API_URL }}
|
||||
DUMMY_CUBEJS_API_SECRET: ${{ secrets.DUMMY_CUBEJS_API_SECRET }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
@@ -105,8 +105,4 @@ jobs:
|
||||
DUMMY_DATABASE_URL: ${{ secrets.DUMMY_DATABASE_URL }}
|
||||
DUMMY_ENCRYPTION_KEY: ${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||
DUMMY_REDIS_URL: ${{ secrets.DUMMY_REDIS_URL }}
|
||||
DUMMY_HUB_API_URL: ${{ secrets.DUMMY_HUB_API_URL }}
|
||||
DUMMY_HUB_API_KEY: ${{ secrets.DUMMY_HUB_API_KEY }}
|
||||
DUMMY_CUBEJS_API_URL: ${{ secrets.DUMMY_CUBEJS_API_URL }}
|
||||
DUMMY_CUBEJS_API_SECRET: ${{ secrets.DUMMY_CUBEJS_API_SECRET }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
@@ -35,11 +35,15 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
- name: Create .env
|
||||
run: pnpm dev:setup
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Adjust CI-specific env values
|
||||
- name: Generate Random ENCRYPTION_KEY, CRON_SECRET & NEXTAUTH_SECRET and fill in .env
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s|REDIS_URL=.*|REDIS_URL=|" .env
|
||||
|
||||
- name: Run tests with coverage
|
||||
|
||||
@@ -32,11 +32,15 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
- name: Create .env
|
||||
run: pnpm dev:setup
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Adjust CI-specific env values
|
||||
- name: Generate Random ENCRYPTION_KEY, CRON_SECRET & NEXTAUTH_SECRET and fill in .env
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s|REDIS_URL=.*|REDIS_URL=|" .env
|
||||
|
||||
- name: Test
|
||||
|
||||
+1
-13
@@ -1,13 +1 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
if command -v pnpm >/dev/null 2>&1; then
|
||||
pnpm lint-staged
|
||||
elif command -v npm >/dev/null 2>&1; then
|
||||
npm exec --yes pnpm@10.32.1 lint-staged
|
||||
elif command -v corepack >/dev/null 2>&1; then
|
||||
corepack pnpm lint-staged
|
||||
else
|
||||
echo "Error: pnpm, npm, and corepack are unavailable in this Git hook PATH."
|
||||
echo "Install Node.js tooling or update your PATH, then retry the commit."
|
||||
exit 127
|
||||
fi
|
||||
pnpm lint-staged
|
||||
@@ -32,7 +32,6 @@ The `@formbricks/surveys` package is pre-compiled (Vite → UMD + ESM) and the b
|
||||
|
||||
TypeScript, React, and Prisma are the primary languages. Use the shared ESLint presets (`@formbricks/eslint-config`) and Prettier preset (110-char width, semicolons, double quotes, sorted import groups). Two-space indentation is standard; prefer `PascalCase` for React components and folders under `modules/`, `camelCase` for functions/variables, and `SCREAMING_SNAKE_CASE` only for constants. When adding mocks, place them inside `__mocks__` so import ordering stays stable.
|
||||
We are using SonarQube to identify code smells and security hotspots.
|
||||
Always mark React component props as `Readonly<>` (e.g., `({ children }: Readonly<MyProps>)`).
|
||||
|
||||
## Architecture & Patterns
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"eslint-plugin-react-refresh": "0.4.26",
|
||||
"eslint-plugin-storybook": "10.2.17",
|
||||
"storybook": "10.2.17",
|
||||
"vite": "7.3.2",
|
||||
"vite": "7.3.1",
|
||||
"@storybook/addon-docs": "10.2.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,10 +66,6 @@ RUN pnpm build --filter=@formbricks/database
|
||||
RUN --mount=type=secret,id=database_url \
|
||||
--mount=type=secret,id=encryption_key \
|
||||
--mount=type=secret,id=redis_url \
|
||||
--mount=type=secret,id=hub_api_url \
|
||||
--mount=type=secret,id=hub_api_key \
|
||||
--mount=type=secret,id=cubejs_api_url \
|
||||
--mount=type=secret,id=cubejs_api_secret \
|
||||
--mount=type=secret,id=sentry_auth_token \
|
||||
--mount=type=secret,id=posthog_key \
|
||||
/tmp/read-secrets.sh pnpm build --filter=@formbricks/web...
|
||||
|
||||
+13
-10
@@ -4,20 +4,21 @@ import { ArrowRight } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TWorkspaceConfigChannel } from "@formbricks/types/workspace";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProjectConfigChannel } from "@formbricks/types/project";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
||||
|
||||
interface ConnectWithFormbricksProps {
|
||||
workspaceId: string;
|
||||
environment: TEnvironment;
|
||||
publicDomain: string;
|
||||
appSetupCompleted: boolean;
|
||||
channel: TWorkspaceConfigChannel;
|
||||
channel: TProjectConfigChannel;
|
||||
}
|
||||
|
||||
export const ConnectWithFormbricks = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
publicDomain,
|
||||
appSetupCompleted,
|
||||
channel,
|
||||
@@ -25,7 +26,7 @@ export const ConnectWithFormbricks = ({
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const handleFinishOnboarding = async () => {
|
||||
router.push(`/workspaces/${workspaceId}/surveys`);
|
||||
router.push(`/environments/${environment.id}/surveys`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -47,7 +48,7 @@ export const ConnectWithFormbricks = ({
|
||||
<div className="flex w-full space-x-10">
|
||||
<div className="flex w-1/2 flex-col space-y-4">
|
||||
<OnboardingSetupInstructions
|
||||
workspaceId={workspaceId}
|
||||
environmentId={environment.id}
|
||||
publicDomain={publicDomain}
|
||||
channel={channel}
|
||||
appSetupCompleted={appSetupCompleted}
|
||||
@@ -60,9 +61,9 @@ export const ConnectWithFormbricks = ({
|
||||
)}>
|
||||
{appSetupCompleted ? (
|
||||
<div>
|
||||
<p className="text-3xl">{t("workspace.connect.congrats")}</p>
|
||||
<p className="text-3xl">{t("environments.connect.congrats")}</p>
|
||||
<p className="pt-4 text-sm font-medium text-slate-600">
|
||||
{t("workspace.connect.connection_successful_message")}
|
||||
{t("environments.connect.connection_successful_message")}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -72,7 +73,7 @@ export const ConnectWithFormbricks = ({
|
||||
<span className="relative inline-flex h-10 w-10 rounded-full bg-slate-500"></span>
|
||||
</span>
|
||||
<p className="pt-4 text-sm font-medium text-slate-600">
|
||||
{t("workspace.connect.waiting_for_your_signal")}
|
||||
{t("environments.connect.waiting_for_your_signal")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -82,7 +83,9 @@ export const ConnectWithFormbricks = ({
|
||||
id="finishOnboarding"
|
||||
variant={appSetupCompleted ? "default" : "ghost"}
|
||||
onClick={handleFinishOnboarding}>
|
||||
{appSetupCompleted ? t("workspace.connect.finish_onboarding") : t("workspace.connect.do_it_later")}
|
||||
{appSetupCompleted
|
||||
? t("environments.connect.finish_onboarding")
|
||||
: t("environments.connect.do_it_later")}
|
||||
<ArrowRight />
|
||||
</Button>
|
||||
</div>
|
||||
+19
-19
@@ -5,7 +5,7 @@ import "prismjs/themes/prism.css";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TWorkspaceConfigChannel } from "@formbricks/types/workspace";
|
||||
import { TProjectConfigChannel } from "@formbricks/types/project";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { CodeBlock } from "@/modules/ui/components/code-block";
|
||||
import { Html5Icon, NpmIcon } from "@/modules/ui/components/icons";
|
||||
@@ -17,14 +17,14 @@ const tabs = [
|
||||
];
|
||||
|
||||
interface OnboardingSetupInstructionsProps {
|
||||
workspaceId: string;
|
||||
environmentId: string;
|
||||
publicDomain: string;
|
||||
channel: TWorkspaceConfigChannel;
|
||||
channel: TProjectConfigChannel;
|
||||
appSetupCompleted: boolean;
|
||||
}
|
||||
|
||||
export const OnboardingSetupInstructions = ({
|
||||
workspaceId,
|
||||
environmentId,
|
||||
publicDomain,
|
||||
channel,
|
||||
appSetupCompleted,
|
||||
@@ -35,8 +35,8 @@ export const OnboardingSetupInstructions = ({
|
||||
<script type="text/javascript">
|
||||
!function(){
|
||||
var appUrl = "${publicDomain}";
|
||||
var workspaceId = "${workspaceId}";
|
||||
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({workspaceId:workspaceId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}();
|
||||
var environmentId = "${environmentId}";
|
||||
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({environmentId:environmentId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}();
|
||||
</script>
|
||||
<!-- END Formbricks Surveys -->
|
||||
`;
|
||||
@@ -45,46 +45,46 @@ export const OnboardingSetupInstructions = ({
|
||||
<script type="text/javascript">
|
||||
!function(){
|
||||
var appUrl = "${publicDomain}";
|
||||
var workspaceId = "${workspaceId}";
|
||||
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({workspaceId:workspaceId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}();
|
||||
var environmentId = "${environmentId}";
|
||||
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({environmentId:environmentId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}();
|
||||
</script>
|
||||
<!-- END Formbricks Surveys -->
|
||||
`;
|
||||
|
||||
const npmSnippetForAppSurveys = `
|
||||
import formbricks from "@formbricks/js";
|
||||
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
formbricks.setup({
|
||||
workspaceId: "${workspaceId}",
|
||||
environmentId: "${environmentId}",
|
||||
appUrl: "${publicDomain}",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function App() {
|
||||
// your own app
|
||||
}
|
||||
|
||||
|
||||
export default App;
|
||||
`;
|
||||
|
||||
const npmSnippetForWebsiteSurveys = `
|
||||
// other imports
|
||||
import formbricks from "@formbricks/js";
|
||||
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
formbricks.setup({
|
||||
workspaceId: "${workspaceId}",
|
||||
environmentId: "${environmentId}",
|
||||
appUrl: "${publicDomain}",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function App() {
|
||||
// your own app
|
||||
}
|
||||
|
||||
|
||||
export default App;
|
||||
|
||||
|
||||
`;
|
||||
|
||||
return (
|
||||
@@ -109,7 +109,7 @@ export const OnboardingSetupInstructions = ({
|
||||
yarn add @formbricks/js
|
||||
</CodeBlock>
|
||||
<p className="text-sm text-slate-700">
|
||||
{t("workspace.connect.import_formbricks_and_initialize_the_widget_in_your_component")}
|
||||
{t("environments.connect.import_formbricks_and_initialize_the_widget_in_your_component")}
|
||||
</p>
|
||||
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
|
||||
{channel === "app" ? npmSnippetForAppSurveys : npmSnippetForWebsiteSurveys}
|
||||
@@ -126,7 +126,7 @@ export const OnboardingSetupInstructions = ({
|
||||
) : activeTab === "html" ? (
|
||||
<div className="prose prose-slate">
|
||||
<p className="-mb-1 mt-6 text-sm text-slate-700">
|
||||
{t("workspace.connect.insert_this_code_into_the_head_tag_of_your_website")}
|
||||
{t("environments.connect.insert_this_code_into_the_head_tag_of_your_website")}
|
||||
</p>
|
||||
<div>
|
||||
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
|
||||
+17
-11
@@ -1,50 +1,56 @@
|
||||
import { XIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/workspaces/[workspaceId]/connect/components/ConnectWithFormbricks";
|
||||
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
|
||||
import { getEnvironment } from "@/lib/environment/service";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getWorkspace } from "@/lib/workspace/service";
|
||||
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Header } from "@/modules/ui/components/header";
|
||||
|
||||
interface ConnectPageProps {
|
||||
params: Promise<{
|
||||
workspaceId: string;
|
||||
environmentId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const Page = async (props: ConnectPageProps) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
const environment = await getEnvironment(params.environmentId);
|
||||
|
||||
const workspace = await getWorkspace(params.workspaceId);
|
||||
if (!workspace) {
|
||||
throw new ResourceNotFoundError(t("common.workspace"), params.workspaceId);
|
||||
if (!environment) {
|
||||
throw new ResourceNotFoundError(t("common.environment"), params.environmentId);
|
||||
}
|
||||
|
||||
const channel = workspace.config.channel || null;
|
||||
const project = await getProjectByEnvironmentId(environment.id);
|
||||
if (!project) {
|
||||
throw new ResourceNotFoundError(t("common.workspace"), null);
|
||||
}
|
||||
|
||||
const channel = project.config.channel || null;
|
||||
|
||||
const publicDomain = getPublicDomain();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full flex-col items-center justify-center py-10">
|
||||
<Header title={t("workspace.connect.headline")} subtitle={t("workspace.connect.subtitle")} />
|
||||
<Header title={t("environments.connect.headline")} subtitle={t("environments.connect.subtitle")} />
|
||||
<div className="space-y-4 text-center">
|
||||
<p className="text-4xl font-medium text-slate-800"></p>
|
||||
<p className="text-sm text-slate-500"></p>
|
||||
</div>
|
||||
<ConnectWithFormbricks
|
||||
workspaceId={params.workspaceId}
|
||||
environment={environment}
|
||||
publicDomain={publicDomain}
|
||||
appSetupCompleted={workspace.appSetupCompleted}
|
||||
appSetupCompleted={environment.appSetupCompleted}
|
||||
channel={channel}
|
||||
/>
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
asChild>
|
||||
<Link href={`/workspaces/${params.workspaceId}`}>
|
||||
<Link href={`/environments/${environment.id}`}>
|
||||
<XIcon className="h-7 w-7" strokeWidth={1.5} />
|
||||
</Link>
|
||||
</Button>
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { hasUserWorkspaceAccess } from "@/lib/workspace/auth";
|
||||
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
|
||||
const OnboardingLayout = async (props: {
|
||||
params: Promise<{ workspaceId: string }>;
|
||||
params: Promise<{ environmentId: string }>;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const params = await props.params;
|
||||
@@ -17,9 +17,9 @@ const OnboardingLayout = async (props: {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
|
||||
const isAuthorized = await hasUserWorkspaceAccess(session.user.id, params.workspaceId);
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
|
||||
if (!isAuthorized) {
|
||||
throw new AuthorizationError("User is not authorized to access this workspace");
|
||||
throw new AuthorizationError("User is not authorized to access this environment");
|
||||
}
|
||||
|
||||
return <div className="flex-1 bg-slate-50">{children}</div>;
|
||||
+21
-21
@@ -5,23 +5,23 @@ import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurveyCreateInput } from "@formbricks/types/surveys/types";
|
||||
import { TXMTemplate } from "@formbricks/types/templates";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { TWorkspace } from "@formbricks/types/workspace";
|
||||
import { replacePresetPlaceholders } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils";
|
||||
import { getXMTemplates } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates";
|
||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||
import { replacePresetPlaceholders } from "@/app/(app)/(onboarding)/workspaces/[workspaceId]/xm-templates/lib/utils";
|
||||
import { getXMTemplates } from "@/app/(app)/(onboarding)/workspaces/[workspaceId]/xm-templates/lib/xm-templates";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { createSurveyAction } from "@/modules/survey/components/template-list/actions";
|
||||
|
||||
interface XMTemplateListProps {
|
||||
workspace: TWorkspace;
|
||||
project: TProject;
|
||||
user: TUser;
|
||||
workspaceId: string;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export const XMTemplateList = ({ workspace, user, workspaceId }: XMTemplateListProps) => {
|
||||
export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListProps) => {
|
||||
const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null);
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
@@ -33,12 +33,12 @@ export const XMTemplateList = ({ workspace, user, workspaceId }: XMTemplateListP
|
||||
createdBy: user.id,
|
||||
};
|
||||
const createSurveyResponse = await createSurveyAction({
|
||||
workspaceId: workspaceId,
|
||||
environmentId: environmentId,
|
||||
surveyBody: augmentedTemplate,
|
||||
});
|
||||
|
||||
if (createSurveyResponse?.data) {
|
||||
router.push(`/workspaces/${workspaceId}/surveys/${createSurveyResponse.data.id}/edit?mode=cx`);
|
||||
router.push(`/environments/${environmentId}/surveys/${createSurveyResponse.data.id}/edit?mode=cx`);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createSurveyResponse);
|
||||
toast.error(errorMessage);
|
||||
@@ -48,49 +48,49 @@ export const XMTemplateList = ({ workspace, user, workspaceId }: XMTemplateListP
|
||||
const handleTemplateClick = (templateIdx: number) => {
|
||||
setActiveTemplateId(templateIdx);
|
||||
const template = getXMTemplates(t)[templateIdx];
|
||||
const newTemplate = replacePresetPlaceholders(template, workspace);
|
||||
const newTemplate = replacePresetPlaceholders(template, project);
|
||||
createSurvey(newTemplate);
|
||||
};
|
||||
|
||||
const XMTemplateOptions = [
|
||||
{
|
||||
title: t("workspace.xm-templates.nps"),
|
||||
description: t("workspace.xm-templates.nps_description"),
|
||||
title: t("environments.xm-templates.nps"),
|
||||
description: t("environments.xm-templates.nps_description"),
|
||||
icon: ShoppingCartIcon,
|
||||
onClick: () => handleTemplateClick(0),
|
||||
isLoading: activeTemplateId === 0,
|
||||
},
|
||||
{
|
||||
title: t("workspace.xm-templates.five_star_rating"),
|
||||
description: t("workspace.xm-templates.five_star_rating_description"),
|
||||
title: t("environments.xm-templates.five_star_rating"),
|
||||
description: t("environments.xm-templates.five_star_rating_description"),
|
||||
icon: StarIcon,
|
||||
onClick: () => handleTemplateClick(1),
|
||||
isLoading: activeTemplateId === 1,
|
||||
},
|
||||
{
|
||||
title: t("workspace.xm-templates.csat"),
|
||||
description: t("workspace.xm-templates.csat_description"),
|
||||
title: t("environments.xm-templates.csat"),
|
||||
description: t("environments.xm-templates.csat_description"),
|
||||
icon: ThumbsUpIcon,
|
||||
onClick: () => handleTemplateClick(2),
|
||||
isLoading: activeTemplateId === 2,
|
||||
},
|
||||
{
|
||||
title: t("workspace.xm-templates.ces"),
|
||||
description: t("workspace.xm-templates.ces_description"),
|
||||
title: t("environments.xm-templates.ces"),
|
||||
description: t("environments.xm-templates.ces_description"),
|
||||
icon: ActivityIcon,
|
||||
onClick: () => handleTemplateClick(3),
|
||||
isLoading: activeTemplateId === 3,
|
||||
},
|
||||
{
|
||||
title: t("workspace.xm-templates.smileys"),
|
||||
description: t("workspace.xm-templates.smileys_description"),
|
||||
title: t("environments.xm-templates.smileys"),
|
||||
description: t("environments.xm-templates.smileys_description"),
|
||||
icon: SmileIcon,
|
||||
onClick: () => handleTemplateClick(4),
|
||||
isLoading: activeTemplateId === 4,
|
||||
},
|
||||
{
|
||||
title: t("workspace.xm-templates.enps"),
|
||||
description: t("workspace.xm-templates.enps_description"),
|
||||
title: t("environments.xm-templates.enps"),
|
||||
description: t("environments.xm-templates.enps_description"),
|
||||
icon: UsersIcon,
|
||||
onClick: () => handleTemplateClick(5),
|
||||
isLoading: activeTemplateId === 5,
|
||||
+16
-16
@@ -1,17 +1,17 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||
import { TXMTemplate } from "@formbricks/types/templates";
|
||||
import { TWorkspace } from "@formbricks/types/workspace";
|
||||
import { replacePresetPlaceholders } from "./utils";
|
||||
|
||||
// Mock data
|
||||
const mockWorkspace: TWorkspace = {
|
||||
id: "workspace1",
|
||||
const mockProject: TProject = {
|
||||
id: "project1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Test Workspace",
|
||||
name: "Test Project",
|
||||
organizationId: "org1",
|
||||
styling: {
|
||||
allowStyleOverwrite: true,
|
||||
@@ -27,12 +27,12 @@ const mockWorkspace: TWorkspace = {
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
appSetupCompleted: false,
|
||||
environments: [],
|
||||
languages: [],
|
||||
logo: null,
|
||||
};
|
||||
const mockTemplate: TXMTemplate = {
|
||||
name: "$[workspaceName] Survey",
|
||||
name: "$[projectName] Survey",
|
||||
blocks: [
|
||||
{
|
||||
id: "block1",
|
||||
@@ -42,7 +42,7 @@ const mockTemplate: TXMTemplate = {
|
||||
id: "q1",
|
||||
type: "openText" as TSurveyElementTypeEnum.OpenText,
|
||||
inputType: "text" as const,
|
||||
headline: { default: "$[workspaceName] Question" },
|
||||
headline: { default: "$[projectName] Question" },
|
||||
subheader: { default: "" },
|
||||
required: false,
|
||||
placeholder: { default: "" },
|
||||
@@ -60,8 +60,8 @@ const mockTemplate: TXMTemplate = {
|
||||
],
|
||||
styling: {
|
||||
brandColor: { light: "#0000FF" },
|
||||
elementHeadlineColor: { light: "#00FF00" },
|
||||
inputBgColor: { light: "#FF0000" },
|
||||
questionColor: { light: "#00FF00" },
|
||||
inputColor: { light: "#FF0000" },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -70,19 +70,19 @@ describe("replacePresetPlaceholders", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("replaces workspaceName placeholder in template name", () => {
|
||||
const result = replacePresetPlaceholders(mockTemplate, mockWorkspace);
|
||||
expect(result.name).toBe("Test Workspace Survey");
|
||||
test("replaces projectName placeholder in template name", () => {
|
||||
const result = replacePresetPlaceholders(mockTemplate, mockProject);
|
||||
expect(result.name).toBe("Test Project Survey");
|
||||
});
|
||||
|
||||
test("replaces workspaceName placeholder in element headline", () => {
|
||||
const result = replacePresetPlaceholders(mockTemplate, mockWorkspace);
|
||||
expect(result.blocks[0].elements[0].headline.default).toBe("Test Workspace Question");
|
||||
test("replaces projectName placeholder in element headline", () => {
|
||||
const result = replacePresetPlaceholders(mockTemplate, mockProject);
|
||||
expect(result.blocks[0].elements[0].headline.default).toBe("Test Project Question");
|
||||
});
|
||||
|
||||
test("returns a new object without mutating the original template", () => {
|
||||
const originalTemplate = structuredClone(mockTemplate);
|
||||
const result = replacePresetPlaceholders(mockTemplate, mockWorkspace);
|
||||
const result = replacePresetPlaceholders(mockTemplate, mockProject);
|
||||
expect(result).not.toBe(mockTemplate);
|
||||
expect(mockTemplate).toEqual(originalTemplate);
|
||||
});
|
||||
+5
-5
@@ -1,16 +1,16 @@
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
|
||||
import { TXMTemplate } from "@formbricks/types/templates";
|
||||
import { TWorkspace } from "@formbricks/types/workspace";
|
||||
import { replaceElementPresetPlaceholders } from "@/lib/utils/templates";
|
||||
|
||||
// replace all occurences of workspaceName with the actual workspace name in the current template
|
||||
export const replacePresetPlaceholders = (template: TXMTemplate, workspace: TWorkspace): TXMTemplate => {
|
||||
// replace all occurences of projectName with the actual project name in the current template
|
||||
export const replacePresetPlaceholders = (template: TXMTemplate, project: TProject): TXMTemplate => {
|
||||
const survey = structuredClone(template);
|
||||
|
||||
const modifiedBlocks = survey.blocks.map((block: TSurveyBlock) => ({
|
||||
...block,
|
||||
elements: block.elements.map((element) => replaceElementPresetPlaceholders(element, workspace)),
|
||||
elements: block.elements.map((element) => replaceElementPresetPlaceholders(element, project)),
|
||||
}));
|
||||
|
||||
return { ...survey, name: survey.name.replace("$[workspaceName]", workspace.name), blocks: modifiedBlocks };
|
||||
return { ...survey, name: survey.name.replace("$[projectName]", project.name), blocks: modifiedBlocks };
|
||||
};
|
||||
+20
-13
@@ -2,9 +2,11 @@ import { XIcon } from "lucide-react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import Link from "next/link";
|
||||
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { XMTemplateList } from "@/app/(app)/(onboarding)/workspaces/[workspaceId]/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 { getUserWorkspaces, getWorkspace } from "@/lib/workspace/service";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -12,15 +14,15 @@ import { Header } from "@/modules/ui/components/header";
|
||||
|
||||
interface XMTemplatePageProps {
|
||||
params: Promise<{
|
||||
workspaceId: string;
|
||||
environmentId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const Page = async (props: XMTemplatePageProps) => {
|
||||
const params = await props.params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const environment = await getEnvironment(params.environmentId);
|
||||
const t = await getTranslate();
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError(t("common.not_authenticated"));
|
||||
}
|
||||
@@ -29,24 +31,29 @@ const Page = async (props: XMTemplatePageProps) => {
|
||||
if (!user) {
|
||||
throw new AuthenticationError(t("common.not_authenticated"));
|
||||
}
|
||||
|
||||
const workspace = await getWorkspace(params.workspaceId);
|
||||
if (!workspace) {
|
||||
throw new ResourceNotFoundError(t("common.workspace"), params.workspaceId);
|
||||
if (!environment) {
|
||||
throw new ResourceNotFoundError(t("common.environment"), params.environmentId);
|
||||
}
|
||||
|
||||
const workspaces = await getUserWorkspaces(session.user.id, workspace.organizationId);
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
|
||||
|
||||
const project = await getProjectByEnvironmentId(environment.id);
|
||||
if (!project) {
|
||||
throw new ResourceNotFoundError(t("common.workspace"), null);
|
||||
}
|
||||
|
||||
const projects = await getUserProjects(session.user.id, organizationId);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
<Header title={t("workspace.xm-templates.headline")} />
|
||||
<XMTemplateList workspace={workspace} user={user} workspaceId={params.workspaceId} />
|
||||
{workspaces.length >= 2 && (
|
||||
<Header title={t("environments.xm-templates.headline")} />
|
||||
<XMTemplateList project={project} user={user} environmentId={environment.id} />
|
||||
{projects.length >= 2 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
asChild>
|
||||
<Link href={`/workspaces/${params.workspaceId}/surveys`}>
|
||||
<Link href={`/environments/${environment.id}/surveys`}>
|
||||
<XIcon className="h-7 w-7" strokeWidth={1.5} />
|
||||
</Link>
|
||||
</Button>
|
||||
+31
-149
@@ -1,32 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ArrowUpRightIcon,
|
||||
Building2Icon,
|
||||
ChevronRightIcon,
|
||||
Loader2,
|
||||
LogOutIcon,
|
||||
PlusIcon,
|
||||
} from "lucide-react";
|
||||
import { ArrowUpRightIcon, ChevronRightIcon, LogOutIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useState, useTransition } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { getOrganizationsForSwitcherAction } from "@/app/(app)/workspaces/[workspaceId]/actions";
|
||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
||||
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/modules/ui/components/dropdown-menu";
|
||||
@@ -34,65 +22,14 @@ import {
|
||||
interface LandingSidebarProps {
|
||||
user: TUser;
|
||||
organization: TOrganization;
|
||||
isMultiOrgEnabled: boolean;
|
||||
}
|
||||
|
||||
export const LandingSidebar = ({ user, organization, isMultiOrgEnabled }: LandingSidebarProps) => {
|
||||
const [openCreateOrganizationModal, setOpenCreateOrganizationModal] = useState(false);
|
||||
const [organizations, setOrganizations] = useState<{ id: string; name: string }[]>([]);
|
||||
const [isLoadingOrganizations, setIsLoadingOrganizations] = useState(false);
|
||||
const [organizationLoadError, setOrganizationLoadError] = useState<string | null>(null);
|
||||
const [isOrgDropdownOpen, setIsOrgDropdownOpen] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const router = useRouter();
|
||||
export const LandingSidebar = ({ user, organization }: LandingSidebarProps) => {
|
||||
const [openCreateOrganizationModal, setOpenCreateOrganizationModal] = useState<boolean>(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { signOut: signOutWithAudit } = useSignOut({ id: user.id, email: user.email });
|
||||
|
||||
const loadOrganizations = useCallback(async () => {
|
||||
setIsLoadingOrganizations(true);
|
||||
setOrganizationLoadError(null);
|
||||
try {
|
||||
const result = await getOrganizationsForSwitcherAction({ organizationId: organization.id });
|
||||
if (result?.data) {
|
||||
const sorted = [...result.data].sort((a, b) => a.name.localeCompare(b.name));
|
||||
setOrganizations(sorted);
|
||||
} else {
|
||||
setOrganizationLoadError(
|
||||
getFormattedErrorMessage(result) || t("common.failed_to_load_organizations")
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
setOrganizationLoadError(t("common.failed_to_load_organizations"));
|
||||
} finally {
|
||||
setIsLoadingOrganizations(false);
|
||||
}
|
||||
}, [organization.id, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isOrgDropdownOpen &&
|
||||
organizations.length === 0 &&
|
||||
!isLoadingOrganizations &&
|
||||
!organizationLoadError
|
||||
) {
|
||||
loadOrganizations();
|
||||
}
|
||||
}, [
|
||||
isOrgDropdownOpen,
|
||||
organizations.length,
|
||||
isLoadingOrganizations,
|
||||
organizationLoadError,
|
||||
loadOrganizations,
|
||||
]);
|
||||
|
||||
const handleOrganizationChange = (orgId: string) => {
|
||||
startTransition(() => {
|
||||
setIsOrgDropdownOpen(false);
|
||||
router.push(`/organizations/${orgId}/`);
|
||||
});
|
||||
};
|
||||
|
||||
const dropdownNavigation = [
|
||||
{
|
||||
label: t("common.documentation"),
|
||||
@@ -102,109 +39,52 @@ export const LandingSidebar = ({ user, organization, isMultiOrgEnabled }: Landin
|
||||
},
|
||||
];
|
||||
|
||||
const switcherTriggerClasses =
|
||||
"w-full border-t px-3 py-3 text-left transition-colors duration-200 hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-500 focus-visible:ring-inset";
|
||||
const switcherIconClasses =
|
||||
"flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-slate-100 text-slate-600";
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
"z-40 flex w-sidebar-collapsed flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100"
|
||||
)}>
|
||||
<Image src={FBLogo} width={160} height={30} alt={t("workspace.formbricks_logo")} />
|
||||
<Image src={FBLogo} width={160} height={30} alt={t("environments.formbricks_logo")} />
|
||||
|
||||
<div className="flex flex-col">
|
||||
{/* Organization Switcher */}
|
||||
<DropdownMenu onOpenChange={setIsOrgDropdownOpen}>
|
||||
<DropdownMenuTrigger asChild className={switcherTriggerClasses}>
|
||||
<button type="button" className="flex w-full items-center gap-3">
|
||||
<span className={switcherIconClasses}>
|
||||
<Building2Icon className="h-4 w-4" strokeWidth={1.5} />
|
||||
</span>
|
||||
<div className="grow overflow-hidden">
|
||||
<p className="truncate text-sm font-bold text-slate-700">{organization.name}</p>
|
||||
<p className="text-sm text-slate-500">{t("common.organization")}</p>
|
||||
</div>
|
||||
{isPending && <Loader2 className="h-4 w-4 animate-spin text-slate-600" strokeWidth={1.5} />}
|
||||
<ChevronRightIcon className="h-4 w-4 shrink-0 text-slate-600" strokeWidth={1.5} />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="right" sideOffset={10} alignOffset={5} align="end">
|
||||
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
||||
<Building2Icon className="mr-2 inline h-4 w-4" strokeWidth={1.5} />
|
||||
{t("common.change_organization")}
|
||||
</div>
|
||||
{isLoadingOrganizations && (
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
{!isLoadingOrganizations && organizationLoadError && (
|
||||
<div className="px-2 py-4">
|
||||
<p className="mb-2 text-sm text-red-600">{organizationLoadError}</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setOrganizationLoadError(null);
|
||||
setOrganizations([]);
|
||||
}}
|
||||
className="text-xs text-slate-600 underline hover:text-slate-800">
|
||||
{t("common.try_again")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!isLoadingOrganizations && !organizationLoadError && (
|
||||
<>
|
||||
<DropdownMenuGroup className="max-h-[300px] overflow-y-auto">
|
||||
{organizations.map((org) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={org.id}
|
||||
checked={org.id === organization.id}
|
||||
onClick={() => handleOrganizationChange(org.id)}
|
||||
className="cursor-pointer">
|
||||
{org.name}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
{isMultiOrgEnabled && (
|
||||
<DropdownMenuCheckboxItem
|
||||
onClick={() => setOpenCreateOrganizationModal(true)}
|
||||
className="w-full cursor-pointer justify-between">
|
||||
<span>{t("common.create_new_organization")}</span>
|
||||
<PlusIcon className="ml-2 h-4 w-4" strokeWidth={1.5} />
|
||||
</DropdownMenuCheckboxItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* User Dropdown */}
|
||||
<div className="flex items-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
id="userDropdownTrigger"
|
||||
className={cn(switcherTriggerClasses, "rounded-br-xl")}>
|
||||
<button type="button" className="flex w-full items-center gap-3">
|
||||
<span className={switcherIconClasses}>
|
||||
<ProfileAvatar userId={user.id} />
|
||||
</span>
|
||||
className="w-full rounded-br-xl border-t p-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-none">
|
||||
<button
|
||||
type="button"
|
||||
className={cn("flex w-full cursor-pointer flex-row items-center gap-3 text-left")}
|
||||
aria-haspopup="menu">
|
||||
<ProfileAvatar userId={user.id} />
|
||||
<div className="grow overflow-hidden">
|
||||
<p
|
||||
title={user?.email}
|
||||
className="ph-no-capture -mb-0.5 truncate text-sm font-bold text-slate-700">
|
||||
className={cn(
|
||||
"ph-no-capture ph-no-capture -mb-0.5 truncate text-sm font-bold text-slate-700"
|
||||
)}>
|
||||
{user?.name ? <span>{user?.name}</span> : <span>{user?.email}</span>}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">{t("common.account")}</p>
|
||||
<p title={organization?.name} className="truncate text-sm text-slate-500">
|
||||
{organization?.name}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRightIcon className="h-4 w-4 shrink-0 text-slate-600" strokeWidth={1.5} />
|
||||
<ChevronRightIcon className={cn("h-5 w-5 shrink-0 text-slate-700 hover:text-slate-500")} />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent side="right" sideOffset={10} alignOffset={5} align="end">
|
||||
<DropdownMenuContent
|
||||
id="userDropdownInnerContentWrapper"
|
||||
side="right"
|
||||
sideOffset={10}
|
||||
alignOffset={5}
|
||||
align="end">
|
||||
{/* Dropdown Items */}
|
||||
|
||||
{dropdownNavigation.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
id={link.href}
|
||||
href={link.href}
|
||||
target={link.target}
|
||||
rel={link.target === "_blank" ? "noopener noreferrer" : undefined}
|
||||
@@ -215,6 +95,8 @@ export const LandingSidebar = ({ user, organization, isMultiOrgEnabled }: Landin
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{/* Logout */}
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await signOutWithAudit({
|
||||
@@ -223,6 +105,7 @@ export const LandingSidebar = ({ user, organization, isMultiOrgEnabled }: Landin
|
||||
organizationId: organization.id,
|
||||
redirect: true,
|
||||
callbackUrl: "/auth/login",
|
||||
clearEnvironmentId: true,
|
||||
});
|
||||
}}
|
||||
icon={<LogOutIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />}>
|
||||
@@ -231,7 +114,6 @@ export const LandingSidebar = ({ user, organization, isMultiOrgEnabled }: Landin
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<CreateOrganizationModal open={openCreateOrganizationModal} setOpen={setOpenCreateOrganizationModal} />
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getEnvironments } from "@/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getUserWorkspaces } from "@/lib/workspace/service";
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
|
||||
const LandingLayout = async (props: {
|
||||
@@ -23,11 +24,16 @@ const LandingLayout = async (props: {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const workspaces = await getUserWorkspaces(session.user.id, params.organizationId);
|
||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
||||
|
||||
if (workspaces.length !== 0) {
|
||||
const firstWorkspace = workspaces[0];
|
||||
return redirect(`/workspaces/${firstWorkspace.id}/`);
|
||||
if (projects.length !== 0) {
|
||||
const firstProject = projects[0];
|
||||
const environments = await getEnvironments(firstProject.id);
|
||||
const prodEnvironment = environments.find((e) => e.type === "production");
|
||||
|
||||
if (prodEnvironment) {
|
||||
return redirect(`/environments/${prodEnvironment.id}/`);
|
||||
}
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
|
||||
import { WorkspaceAndOrgSwitch } from "@/app/(app)/workspaces/[workspaceId]/components/workspace-and-org-switch";
|
||||
import { ProjectAndOrgSwitch } from "@/app/(app)/environments/[environmentId]/components/project-and-org-switch";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
@@ -25,25 +26,28 @@ const Page = async (props: { params: Promise<{ organizationId: string }> }) => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
|
||||
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
|
||||
const { isBilling } = getAccessFlags(membership?.role);
|
||||
const isMembershipPending = membership?.role === undefined;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-row">
|
||||
<LandingSidebar user={user} organization={organization} isMultiOrgEnabled={isMultiOrgEnabled} />
|
||||
<LandingSidebar user={user} organization={organization} />
|
||||
<div className="flex-1">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="p-6">
|
||||
{/* we only need to render organization breadcrumb on this page, organizations/workspaces are lazy-loaded */}
|
||||
<WorkspaceAndOrgSwitch
|
||||
{/* we only need to render organization breadcrumb on this page, organizations/projects are lazy-loaded */}
|
||||
<ProjectAndOrgSwitch
|
||||
currentOrganizationId={organization.id}
|
||||
currentOrganizationName={organization.name}
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
organizationWorkspacesLimit={0}
|
||||
organizationProjectsLimit={0}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
isLicenseActive={false}
|
||||
isOwnerOrManager={false}
|
||||
isAccessControlAllowed={false}
|
||||
isBilling={isBilling}
|
||||
isMembershipPending={isMembershipPending}
|
||||
environments={[]}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-12">
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getTranslate } from "@/lingodotdev/server";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
||||
|
||||
const WorkspaceOnboardingLayout = async (props: {
|
||||
const ProjectOnboardingLayout = async (props: {
|
||||
params: Promise<{ organizationId: string }>;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
@@ -47,4 +47,4 @@ const WorkspaceOnboardingLayout = async (props: {
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceOnboardingLayout;
|
||||
export default ProjectOnboardingLayout;
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||
import { getUserWorkspaces } from "@/lib/workspace/service";
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -39,7 +39,7 @@ const Page = async (props: ChannelPageProps) => {
|
||||
},
|
||||
];
|
||||
|
||||
const workspaces = await getUserWorkspaces(session.user.id, params.organizationId);
|
||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
@@ -48,7 +48,7 @@ const Page = async (props: ChannelPageProps) => {
|
||||
subtitle={t("organizations.workspaces.new.channel.channel_select_subtitle")}
|
||||
/>
|
||||
<OnboardingOptionsContainer options={channelOptions} />
|
||||
{workspaces.length >= 1 && (
|
||||
{projects.length >= 1 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
|
||||
+6
-6
@@ -4,10 +4,10 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { getOrganizationWorkspacesCount } from "@/lib/workspace/service";
|
||||
import { getOrganizationProjectsCount } from "@/lib/project/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getOrganizationWorkspacesLimit } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||
|
||||
const OnboardingLayout = async (props: {
|
||||
params: Promise<{ organizationId: string }>;
|
||||
@@ -32,12 +32,12 @@ const OnboardingLayout = async (props: {
|
||||
throw new ResourceNotFoundError(t("common.organization"), params.organizationId);
|
||||
}
|
||||
|
||||
const [organizationWorkspacesLimit, organizationWorkspacesCount] = await Promise.all([
|
||||
getOrganizationWorkspacesLimit(organization.id),
|
||||
getOrganizationWorkspacesCount(organization.id),
|
||||
const [organizationProjectsLimit, organizationProjectsCount] = await Promise.all([
|
||||
getOrganizationProjectsLimit(organization.id),
|
||||
getOrganizationProjectsCount(organization.id),
|
||||
]);
|
||||
|
||||
if (organizationWorkspacesCount >= organizationWorkspacesLimit) {
|
||||
if (organizationProjectsCount >= organizationProjectsLimit) {
|
||||
return redirect(`/`);
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||
import { getUserWorkspaces } from "@/lib/workspace/service";
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -39,13 +39,13 @@ const Page = async (props: ModePageProps) => {
|
||||
},
|
||||
];
|
||||
|
||||
const workspaces = await getUserWorkspaces(session.user.id, params.organizationId);
|
||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
<Header title={t("organizations.workspaces.new.mode.what_are_you_here_for")} />
|
||||
<OnboardingOptionsContainer options={channelOptions} />
|
||||
{workspaces.length >= 1 && (
|
||||
{projects.length >= 1 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ export const SelectPlanOnboarding = async ({ organizationId }: SelectPlanOnboard
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-8">
|
||||
<Header
|
||||
title={t("workspace.settings.billing.select_plan_header_title")}
|
||||
subtitle={t("workspace.settings.billing.select_plan_header_subtitle")}
|
||||
title={t("environments.settings.billing.select_plan_header_title")}
|
||||
subtitle={t("environments.settings.billing.select_plan_header_subtitle")}
|
||||
/>
|
||||
<SelectPlanCard nextUrl={nextUrl} organizationId={organizationId} />
|
||||
</div>
|
||||
|
||||
+40
-35
@@ -8,19 +8,19 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
TWorkspaceConfigChannel,
|
||||
TWorkspaceConfigIndustry,
|
||||
TWorkspaceMode,
|
||||
TWorkspaceUpdateInput,
|
||||
ZWorkspaceUpdateInput,
|
||||
} from "@formbricks/types/workspace";
|
||||
import { createWorkspaceAction } from "@/app/(app)/workspaces/[workspaceId]/actions";
|
||||
TProjectConfigChannel,
|
||||
TProjectConfigIndustry,
|
||||
TProjectMode,
|
||||
TProjectUpdateInput,
|
||||
ZProjectUpdateInput,
|
||||
} from "@formbricks/types/project";
|
||||
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import { previewSurvey } from "@/app/lib/templates";
|
||||
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@/lib/localStorage";
|
||||
import { buildStylingFromBrandColor } from "@/lib/styling/constants";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
|
||||
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
|
||||
import { TOrganizationTeam } from "@/modules/ee/teams/workspace-teams/types/team";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { ColorPicker } from "@/modules/ui/components/color-picker";
|
||||
import {
|
||||
@@ -36,34 +36,34 @@ import { Input } from "@/modules/ui/components/input";
|
||||
import { MultiSelect } from "@/modules/ui/components/multi-select";
|
||||
import { SurveyInline } from "@/modules/ui/components/survey";
|
||||
|
||||
interface WorkspaceSettingsProps {
|
||||
interface ProjectSettingsProps {
|
||||
organizationId: string;
|
||||
workspaceMode: TWorkspaceMode;
|
||||
channel: TWorkspaceConfigChannel;
|
||||
industry: TWorkspaceConfigIndustry;
|
||||
projectMode: TProjectMode;
|
||||
channel: TProjectConfigChannel;
|
||||
industry: TProjectConfigIndustry;
|
||||
defaultBrandColor: string;
|
||||
organizationTeams: TOrganizationTeam[];
|
||||
isAccessControlAllowed: boolean;
|
||||
userWorkspacesCount: number;
|
||||
userProjectsCount: number;
|
||||
publicDomain: string;
|
||||
}
|
||||
|
||||
export const WorkspaceSettings = ({
|
||||
export const ProjectSettings = ({
|
||||
organizationId,
|
||||
workspaceMode,
|
||||
projectMode,
|
||||
channel,
|
||||
industry,
|
||||
defaultBrandColor,
|
||||
organizationTeams,
|
||||
isAccessControlAllowed = false,
|
||||
userWorkspacesCount,
|
||||
userProjectsCount,
|
||||
publicDomain,
|
||||
}: WorkspaceSettingsProps) => {
|
||||
}: ProjectSettingsProps) => {
|
||||
const [createTeamModalOpen, setCreateTeamModalOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const addWorkspace = async (data: TWorkspaceUpdateInput) => {
|
||||
const addProject = async (data: TProjectUpdateInput) => {
|
||||
try {
|
||||
// Build the full styling from the chosen brand color so all derived
|
||||
// colours (question, button, input, option, progress, etc.) are persisted.
|
||||
@@ -71,7 +71,7 @@ export const WorkspaceSettings = ({
|
||||
// back to STYLE_DEFAULTS computed from the default brand (#64748b).
|
||||
const fullStyling = buildStylingFromBrandColor(data.styling?.brandColor?.light);
|
||||
|
||||
const createWorkspaceResponse = await createWorkspaceAction({
|
||||
const createProjectResponse = await createProjectAction({
|
||||
organizationId,
|
||||
data: {
|
||||
...data,
|
||||
@@ -81,21 +81,26 @@ export const WorkspaceSettings = ({
|
||||
},
|
||||
});
|
||||
|
||||
if (createWorkspaceResponse?.data) {
|
||||
if (globalThis.window !== undefined) {
|
||||
// Remove filters when creating a new workspace
|
||||
localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
|
||||
if (createProjectResponse?.data) {
|
||||
// get production environment
|
||||
const productionEnvironment = createProjectResponse.data.environments.find(
|
||||
(environment) => environment.type === "production"
|
||||
);
|
||||
if (productionEnvironment) {
|
||||
if (globalThis.window !== undefined) {
|
||||
// Rmove filters when creating a new project
|
||||
localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
|
||||
}
|
||||
}
|
||||
const workspaceId = createWorkspaceResponse.data.id;
|
||||
if (channel === "app" || channel === "website") {
|
||||
router.push(`/workspaces/${workspaceId}/connect`);
|
||||
router.push(`/environments/${productionEnvironment?.id}/connect`);
|
||||
} else if (channel === "link") {
|
||||
router.push(`/workspaces/${workspaceId}/surveys`);
|
||||
} else if (workspaceMode === "cx") {
|
||||
router.push(`/workspaces/${workspaceId}/xm-templates`);
|
||||
router.push(`/environments/${productionEnvironment?.id}/surveys`);
|
||||
} else if (projectMode === "cx") {
|
||||
router.push(`/environments/${productionEnvironment?.id}/xm-templates`);
|
||||
}
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createWorkspaceResponse);
|
||||
const errorMessage = getFormattedErrorMessage(createProjectResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -104,15 +109,15 @@ export const WorkspaceSettings = ({
|
||||
}
|
||||
};
|
||||
|
||||
const form = useForm<TWorkspaceUpdateInput>({
|
||||
const form = useForm<TProjectUpdateInput>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
styling: { allowStyleOverwrite: true, brandColor: { light: defaultBrandColor } },
|
||||
teamIds: [],
|
||||
},
|
||||
resolver: zodResolver(ZWorkspaceUpdateInput),
|
||||
resolver: zodResolver(ZProjectUpdateInput),
|
||||
});
|
||||
const workspaceName = form.watch("name");
|
||||
const projectName = form.watch("name");
|
||||
const logoUrl = form.watch("logo.url");
|
||||
const brandColor = form.watch("styling.brandColor.light") ?? defaultBrandColor;
|
||||
const previewStyling = useMemo(() => buildStylingFromBrandColor(brandColor), [brandColor]);
|
||||
@@ -127,7 +132,7 @@ export const WorkspaceSettings = ({
|
||||
<div className="mt-6 flex w-5/6 space-x-10 lg:w-2/3 2xl:w-1/2">
|
||||
<div className="flex w-1/2 flex-col space-y-4">
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(addWorkspace)} className="w-full space-y-4">
|
||||
<form onSubmit={form.handleSubmit(addProject)} className="w-full space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="styling.brandColor.light"
|
||||
@@ -179,7 +184,7 @@ export const WorkspaceSettings = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
{isAccessControlAllowed && userWorkspacesCount > 0 && (
|
||||
{isAccessControlAllowed && userProjectsCount > 0 && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="teamIds"
|
||||
@@ -237,7 +242,7 @@ export const WorkspaceSettings = ({
|
||||
<SurveyInline
|
||||
appUrl={publicDomain}
|
||||
isPreviewMode={true}
|
||||
survey={previewSurvey(workspaceName || t("common.my_product"), t)}
|
||||
survey={previewSurvey(projectName || t("common.my_product"), t)}
|
||||
styling={previewStyling}
|
||||
isBrandingEnabled={false}
|
||||
languageCode="default"
|
||||
+13
-17
@@ -2,34 +2,30 @@ import { XIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import {
|
||||
TWorkspaceConfigChannel,
|
||||
TWorkspaceConfigIndustry,
|
||||
TWorkspaceMode,
|
||||
} from "@formbricks/types/workspace";
|
||||
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
||||
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
||||
import { WorkspaceSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/settings/components/WorkspaceSettings";
|
||||
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/settings/components/ProjectSettings";
|
||||
import { DEFAULT_BRAND_COLOR } from "@/lib/constants";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getUserWorkspaces } from "@/lib/workspace/service";
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Header } from "@/modules/ui/components/header";
|
||||
|
||||
interface WorkspaceSettingsPageProps {
|
||||
interface ProjectSettingsPageProps {
|
||||
params: Promise<{
|
||||
organizationId: string;
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
channel?: TWorkspaceConfigChannel;
|
||||
industry?: TWorkspaceConfigIndustry;
|
||||
mode?: TWorkspaceMode;
|
||||
channel?: TProjectConfigChannel;
|
||||
industry?: TProjectConfigIndustry;
|
||||
mode?: TProjectMode;
|
||||
}>;
|
||||
}
|
||||
|
||||
const Page = async (props: WorkspaceSettingsPageProps) => {
|
||||
const Page = async (props: ProjectSettingsPageProps) => {
|
||||
const searchParams = await props.searchParams;
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
@@ -43,7 +39,7 @@ const Page = async (props: WorkspaceSettingsPageProps) => {
|
||||
const channel = searchParams.channel ?? null;
|
||||
const industry = searchParams.industry ?? null;
|
||||
const mode = searchParams.mode ?? "surveys";
|
||||
const workspaces = await getUserWorkspaces(session.user.id, params.organizationId);
|
||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
||||
|
||||
const organizationTeams = await getTeamsByOrganizationId(params.organizationId);
|
||||
|
||||
@@ -61,18 +57,18 @@ const Page = async (props: WorkspaceSettingsPageProps) => {
|
||||
title={t("organizations.workspaces.new.settings.workspace_settings_title")}
|
||||
subtitle={t("organizations.workspaces.new.settings.workspace_settings_subtitle")}
|
||||
/>
|
||||
<WorkspaceSettings
|
||||
<ProjectSettings
|
||||
organizationId={params.organizationId}
|
||||
workspaceMode={mode}
|
||||
projectMode={mode}
|
||||
channel={channel}
|
||||
industry={industry}
|
||||
defaultBrandColor={DEFAULT_BRAND_COLOR}
|
||||
organizationTeams={organizationTeams}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
userWorkspacesCount={workspaces.length}
|
||||
userProjectsCount={projects.length}
|
||||
publicDomain={publicDomain}
|
||||
/>
|
||||
{workspaces.length >= 1 && (
|
||||
{projects.length >= 1 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getEnvironment } from "@/lib/environment/service";
|
||||
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||
|
||||
const SurveyEditorEnvironmentLayout = async (props: {
|
||||
params: Promise<{ environmentId: string }>;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const params = await props.params;
|
||||
|
||||
const { children } = props;
|
||||
|
||||
const { t, session, user } = await environmentIdLayoutChecks(params.environmentId);
|
||||
|
||||
if (!session) {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new AuthenticationError(t("common.not_authenticated"));
|
||||
}
|
||||
|
||||
const environment = await getEnvironment(params.environmentId);
|
||||
|
||||
if (!environment) {
|
||||
throw new ResourceNotFoundError(t("common.environment"), params.environmentId);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SurveyEditorEnvironmentLayout;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getWorkspace } from "@/lib/workspace/service";
|
||||
import { workspaceIdLayoutChecks } from "@/modules/workspaces/lib/utils";
|
||||
|
||||
const SurveyEditorWorkspaceLayout = async (props: {
|
||||
params: Promise<{ workspaceId: string }>;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const params = await props.params;
|
||||
|
||||
const { children } = props;
|
||||
|
||||
const { t, session, user } = await workspaceIdLayoutChecks(params.workspaceId);
|
||||
|
||||
if (!session) {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new AuthenticationError(t("common.not_authenticated"));
|
||||
}
|
||||
|
||||
const workspace = await getWorkspace(params.workspaceId);
|
||||
|
||||
if (!workspace) {
|
||||
throw new ResourceNotFoundError(t("common.workspace"), params.workspaceId);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SurveyEditorWorkspaceLayout;
|
||||
@@ -6,12 +6,12 @@ import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Confetti } from "@/modules/ui/components/confetti";
|
||||
|
||||
const BILLING_CONFIRMATION_WORKSPACE_ID_KEY = "billingConfirmationWorkspaceId";
|
||||
const BILLING_CONFIRMATION_ENVIRONMENT_ID_KEY = "billingConfirmationEnvironmentId";
|
||||
|
||||
export const ConfirmationPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const [showConfetti, setShowConfetti] = useState(false);
|
||||
const [resolvedWorkspaceId, setResolvedWorkspaceId] = useState<string | null>(null);
|
||||
const [resolvedEnvironmentId, setResolvedEnvironmentId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setShowConfetti(true);
|
||||
@@ -20,9 +20,11 @@ export const ConfirmationPage = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const storedWorkspaceId = globalThis.window.sessionStorage.getItem(BILLING_CONFIRMATION_WORKSPACE_ID_KEY);
|
||||
if (storedWorkspaceId) {
|
||||
setResolvedWorkspaceId(storedWorkspaceId);
|
||||
const storedEnvironmentId = globalThis.window.sessionStorage.getItem(
|
||||
BILLING_CONFIRMATION_ENVIRONMENT_ID_KEY
|
||||
);
|
||||
if (storedEnvironmentId) {
|
||||
setResolvedEnvironmentId(storedEnvironmentId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -41,7 +43,9 @@ export const ConfirmationPage = () => {
|
||||
<Button asChild className="w-full justify-center">
|
||||
<Link
|
||||
href={
|
||||
resolvedWorkspaceId ? `/workspaces/${resolvedWorkspaceId}/settings/organization/billing` : "/"
|
||||
resolvedEnvironmentId
|
||||
? `/environments/${resolvedEnvironmentId}/settings/billing`
|
||||
: "/environments"
|
||||
}>
|
||||
{t("billing_confirmation.back_to_billing_overview")}
|
||||
</Link>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SettingsCard } from "@/app/(app)/workspaces/[workspaceId]/settings/components/SettingsCard";
|
||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
export const LoadingCard = ({
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { EnvironmentLayout } from "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getEnvironmentLayoutData } from "@/modules/environments/lib/utils";
|
||||
|
||||
const MainNavLayout = async (props: {
|
||||
params: Promise<{ environmentId: string }>;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const params = await props.params;
|
||||
const { children } = props;
|
||||
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
const layoutData = await getEnvironmentLayoutData(params.environmentId, session.user.id);
|
||||
|
||||
return <EnvironmentLayout layoutData={layoutData}>{children}</EnvironmentLayout>;
|
||||
};
|
||||
|
||||
export default MainNavLayout;
|
||||
+11
-11
@@ -8,11 +8,11 @@ import { getDisplaysBySurveyIdWithContact } from "@/lib/display/service";
|
||||
import { getResponseCountBySurveyId, getResponses } from "@/lib/response/service";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { getOrganizationIdFromSurveyId, getWorkspaceIdFromSurveyId } from "@/lib/utils/helper";
|
||||
import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper";
|
||||
import { getSurveySummary } from "./summary/lib/surveySummary";
|
||||
|
||||
export const revalidateSurveyIdPath = async (workspaceId: string, surveyId: string) => {
|
||||
revalidatePath(`/workspaces/${workspaceId}/surveys/${surveyId}`);
|
||||
export const revalidateSurveyIdPath = async (environmentId: string, surveyId: string) => {
|
||||
revalidatePath(`/environments/${environmentId}/surveys/${surveyId}`);
|
||||
};
|
||||
|
||||
const ZGetResponsesAction = z.object({
|
||||
@@ -36,9 +36,9 @@ export const getResponsesAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "read",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -70,9 +70,9 @@ export const getSurveySummaryAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "read",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -98,9 +98,9 @@ export const getResponseCountAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "read",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -126,9 +126,9 @@ export const getDisplaysWithContactAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "read",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
+9
-6
@@ -3,12 +3,15 @@
|
||||
import { Unplug } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
|
||||
export const EmptyAppSurveys = () => {
|
||||
interface TEmptyAppSurveysProps {
|
||||
environment: TEnvironment;
|
||||
}
|
||||
|
||||
export const EmptyAppSurveys = ({ environment }: TEmptyAppSurveysProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspace();
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center gap-8 bg-slate-100 py-12">
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-full border border-slate-200 bg-white">
|
||||
@@ -17,14 +20,14 @@ export const EmptyAppSurveys = () => {
|
||||
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-xl font-semibold text-slate-900">
|
||||
{t("workspace.surveys.summary.youre_not_plugged_in_yet")}
|
||||
{t("environments.surveys.summary.youre_not_plugged_in_yet")}
|
||||
</h1>
|
||||
|
||||
<p className="mt-2 text-sm text-slate-600">
|
||||
{t("workspace.surveys.summary.connect_your_website_or_app_with_formbricks_to_get_started")}
|
||||
{t("environments.surveys.summary.connect_your_website_or_app_with_formbricks_to_get_started")}
|
||||
</p>
|
||||
|
||||
<Link className="mt-2" href={`/workspaces/${workspace?.id}/settings/workspace/app-connection`}>
|
||||
<Link className="mt-2" href={`/environments/${environment.id}/workspace/app-connection`}>
|
||||
<Button size="sm" className="flex w-[120px] justify-center">
|
||||
{t("common.connect")}
|
||||
</Button>
|
||||
+10
-7
@@ -4,21 +4,24 @@ import { InboxIcon, PresentationIcon } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { revalidateSurveyIdPath } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/actions";
|
||||
import { revalidateSurveyIdPath } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
||||
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
|
||||
|
||||
interface SurveyAnalysisNavigationProps {
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
activeId: string;
|
||||
}
|
||||
|
||||
export const SurveyAnalysisNavigation = ({ survey, activeId }: SurveyAnalysisNavigationProps) => {
|
||||
export const SurveyAnalysisNavigation = ({
|
||||
environmentId,
|
||||
survey,
|
||||
activeId,
|
||||
}: SurveyAnalysisNavigationProps) => {
|
||||
const pathname = usePathname();
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspace();
|
||||
|
||||
const url = `/workspaces/${workspace?.id}/surveys/${survey.id}`;
|
||||
const url = `/environments/${environmentId}/surveys/${survey.id}`;
|
||||
|
||||
const navigation = [
|
||||
{
|
||||
@@ -28,7 +31,7 @@ export const SurveyAnalysisNavigation = ({ survey, activeId }: SurveyAnalysisNav
|
||||
href: `${url}/summary?referer=true`,
|
||||
current: pathname?.includes("/summary"),
|
||||
onClick: () => {
|
||||
revalidateSurveyIdPath(workspace?.id ?? "", survey.id);
|
||||
revalidateSurveyIdPath(environmentId, survey.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -38,7 +41,7 @@ export const SurveyAnalysisNavigation = ({ survey, activeId }: SurveyAnalysisNav
|
||||
href: `${url}/responses?referer=true`,
|
||||
current: pathname?.includes("/responses"),
|
||||
onClick: () => {
|
||||
revalidateSurveyIdPath(workspace?.id ?? "", survey.id);
|
||||
revalidateSurveyIdPath(environmentId, survey.id);
|
||||
},
|
||||
},
|
||||
];
|
||||
+2
-2
@@ -4,8 +4,8 @@ import React, { createContext, useCallback, useContext, useState } from "react";
|
||||
import {
|
||||
ElementOption,
|
||||
ElementOptions,
|
||||
} from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/components/ElementsComboBox";
|
||||
import { ElementFilterOptions } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/components/ResponseFilter";
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ElementsComboBox";
|
||||
import { ElementFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
|
||||
import { getTodayDate } from "@/app/lib/surveys/surveys";
|
||||
|
||||
export interface FilterValue {
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
import { Metadata } from "next";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { ResponseFilterProvider } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ surveyId: string; workspaceId: string }>;
|
||||
params: Promise<{ surveyId: string; environmentId: string }>;
|
||||
};
|
||||
|
||||
export const generateMetadata = async (props: Props): Promise<Metadata> => {
|
||||
@@ -19,7 +19,7 @@ export const generateMetadata = async (props: Props): Promise<Metadata> => {
|
||||
|
||||
if (session) {
|
||||
return {
|
||||
title: `${t("common.count_responses", { count: responseCount })} | ${t("workspace.surveys.summary.survey_results", { surveyName: survey?.name })}`,
|
||||
title: `${t("common.count_responses", { count: responseCount })} | ${t("environments.surveys.summary.survey_results", { surveyName: survey?.name })}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
+4
@@ -1,6 +1,7 @@
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
@@ -21,6 +22,7 @@ interface ResponseCardModalProps {
|
||||
selectedResponseId: string | null;
|
||||
setSelectedResponseId: (id: string | null) => void;
|
||||
survey: TSurvey;
|
||||
environment: TEnvironment;
|
||||
user?: TUser;
|
||||
environmentTags: TTag[];
|
||||
updateResponse: (responseId: string, updatedResponse: TResponse) => void;
|
||||
@@ -36,6 +38,7 @@ export const ResponseCardModal = ({
|
||||
selectedResponseId,
|
||||
setSelectedResponseId,
|
||||
survey,
|
||||
environment,
|
||||
user,
|
||||
environmentTags,
|
||||
updateResponse,
|
||||
@@ -107,6 +110,7 @@ export const ResponseCardModal = ({
|
||||
survey={survey}
|
||||
response={responses[currentIndex]}
|
||||
user={user}
|
||||
environment={environment}
|
||||
environmentTags={environmentTags}
|
||||
isReadOnly={isReadOnly}
|
||||
updateResponse={updateResponse}
|
||||
+7
-3
@@ -3,18 +3,20 @@
|
||||
import { TFunction } from "i18next";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurveyQuota } from "@formbricks/types/quota";
|
||||
import { TResponseDataValue, TResponseTableData, TResponseWithQuotas } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUser, TUserLocale } from "@formbricks/types/user";
|
||||
import { ResponseTable } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTable";
|
||||
import { ResponseTable } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTable";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
|
||||
interface ResponseDataViewProps {
|
||||
survey: TSurvey;
|
||||
responses: TResponseWithQuotas[];
|
||||
user?: TUser;
|
||||
environment: TEnvironment;
|
||||
environmentTags: TTag[];
|
||||
isReadOnly: boolean;
|
||||
fetchNextPage: () => void;
|
||||
@@ -94,8 +96,8 @@ const mapResponsesToTableData = (
|
||||
responseData: extractResponseData(response, survey),
|
||||
createdAt: response.createdAt,
|
||||
status: response.finished
|
||||
? t("workspace.surveys.responses.completed")
|
||||
: t("workspace.surveys.responses.not_completed"),
|
||||
? t("environments.surveys.responses.completed")
|
||||
: t("environments.surveys.responses.not_completed"),
|
||||
responseId: response.id,
|
||||
singleUseId: response.singleUseId,
|
||||
tags: response.tags,
|
||||
@@ -118,6 +120,7 @@ export const ResponseDataView: React.FC<ResponseDataViewProps> = ({
|
||||
survey,
|
||||
responses,
|
||||
user,
|
||||
environment,
|
||||
environmentTags,
|
||||
isReadOnly,
|
||||
fetchNextPage,
|
||||
@@ -145,6 +148,7 @@ export const ResponseDataView: React.FC<ResponseDataViewProps> = ({
|
||||
user={user}
|
||||
environmentTags={environmentTags}
|
||||
isReadOnly={isReadOnly}
|
||||
environment={environment}
|
||||
fetchNextPage={fetchNextPage}
|
||||
hasMore={hasMore}
|
||||
updateResponseList={updateResponseList}
|
||||
+8
-4
@@ -2,19 +2,21 @@
|
||||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurveyQuota } from "@formbricks/types/quota";
|
||||
import { TResponseWithQuotas } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUser, TUserLocale } from "@formbricks/types/user";
|
||||
import { getResponsesAction } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/actions";
|
||||
import { useResponseFilter } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import { ResponseDataView } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView";
|
||||
import { CustomFilter } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/components/CustomFilter";
|
||||
import { getResponsesAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import { ResponseDataView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseDataView";
|
||||
import { CustomFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter";
|
||||
import { getFormattedFilters } from "@/app/lib/surveys/surveys";
|
||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||
|
||||
interface ResponsePageProps {
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
surveyId: string;
|
||||
user?: TUser;
|
||||
@@ -28,6 +30,7 @@ interface ResponsePageProps {
|
||||
}
|
||||
|
||||
export const ResponsePage = ({
|
||||
environment,
|
||||
survey,
|
||||
surveyId,
|
||||
user,
|
||||
@@ -142,6 +145,7 @@ export const ResponsePage = ({
|
||||
survey={survey}
|
||||
responses={responses}
|
||||
user={user}
|
||||
environment={environment}
|
||||
environmentTags={environmentTags}
|
||||
isReadOnly={isReadOnly}
|
||||
fetchNextPage={fetchNextPage}
|
||||
+11
-7
@@ -18,16 +18,17 @@ import { VisibilityState, getCoreRowModel, useReactTable } from "@tanstack/react
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurveyQuota } from "@formbricks/types/quota";
|
||||
import { TResponseTableData, TResponseWithQuotas } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUser, TUserLocale } from "@formbricks/types/user";
|
||||
import { ResponseCardModal } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/responses/components/ResponseCardModal";
|
||||
import { ResponseTableCell } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell";
|
||||
import { generateResponseTableColumns } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns";
|
||||
import { getResponsesDownloadUrlAction } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/actions";
|
||||
import { downloadResponsesFile } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/utils";
|
||||
import { ResponseCardModal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseCardModal";
|
||||
import { ResponseTableCell } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableCell";
|
||||
import { generateResponseTableColumns } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns";
|
||||
import { getResponsesDownloadUrlAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
|
||||
import { downloadResponsesFile } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/utils";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { deleteResponseAction } from "@/modules/analysis/components/SingleResponseCard/actions";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -49,6 +50,7 @@ interface ResponseTableProps {
|
||||
data: TResponseTableData[];
|
||||
survey: TSurvey;
|
||||
responses: TResponseWithQuotas[] | null;
|
||||
environment: TEnvironment;
|
||||
user?: TUser;
|
||||
environmentTags: TTag[];
|
||||
isReadOnly: boolean;
|
||||
@@ -69,6 +71,7 @@ export const ResponseTable = ({
|
||||
survey,
|
||||
responses,
|
||||
user,
|
||||
environment,
|
||||
environmentTags,
|
||||
isReadOnly,
|
||||
fetchNextPage,
|
||||
@@ -220,11 +223,11 @@ export const ResponseTable = ({
|
||||
if (downloadResponse?.data) {
|
||||
downloadResponsesFile(downloadResponse.data.fileName, downloadResponse.data.fileContents, format);
|
||||
} else {
|
||||
toast.error(t("workspace.surveys.responses.error_downloading_responses"));
|
||||
toast.error(t("environments.surveys.responses.error_downloading_responses"));
|
||||
}
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
toast.error(t("workspace.surveys.responses.error_downloading_responses"));
|
||||
toast.error(t("environments.surveys.responses.error_downloading_responses"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -314,6 +317,7 @@ export const ResponseTable = ({
|
||||
survey={survey}
|
||||
responses={responses}
|
||||
user={user}
|
||||
environment={environment}
|
||||
environmentTags={environmentTags}
|
||||
isReadOnly={isReadOnly}
|
||||
updateResponse={updateResponse}
|
||||
+2
-2
@@ -293,7 +293,7 @@ export const generateResponseTableColumns = (
|
||||
<CircleHelpIcon className="h-3 w-3 text-slate-500" strokeWidth={1.5} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="space-x-1 font-normal">
|
||||
<span>{t("workspace.surveys.responses.how_to_identify_users")}</span>
|
||||
<span>{t("environments.surveys.responses.how_to_identify_users")}</span>
|
||||
<Link
|
||||
className="underline underline-offset-2 hover:text-slate-900"
|
||||
href="https://formbricks.com/docs/app-surveys/user-identification"
|
||||
@@ -317,7 +317,7 @@ export const generateResponseTableColumns = (
|
||||
|
||||
const singleUseIdColumn: ColumnDef<TResponseTableData> = {
|
||||
accessorKey: "singleUseId",
|
||||
header: () => <div className="gap-x-1.5">{t("workspace.surveys.responses.single_use_id")}</div>,
|
||||
header: () => <div className="gap-x-1.5">{t("environments.surveys.responses.single_use_id")}</div>,
|
||||
cell: ({ row }) => {
|
||||
return <p className="truncate text-slate-900">{row.original.singleUseId}</p>;
|
||||
},
|
||||
+31
-31
@@ -20,23 +20,23 @@ import {
|
||||
describe("utils", () => {
|
||||
const mockT = vi.fn((key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"workspace.surveys.responses.address_line_1": "Address Line 1",
|
||||
"workspace.surveys.responses.address_line_2": "Address Line 2",
|
||||
"workspace.surveys.responses.city": "City",
|
||||
"workspace.surveys.responses.state_region": "State/Region",
|
||||
"workspace.surveys.responses.zip_post_code": "ZIP/Post Code",
|
||||
"workspace.surveys.responses.country": "Country",
|
||||
"workspace.surveys.responses.first_name": "First Name",
|
||||
"workspace.surveys.responses.last_name": "Last Name",
|
||||
"workspace.surveys.responses.email": "Email",
|
||||
"workspace.surveys.responses.phone": "Phone",
|
||||
"workspace.surveys.responses.company": "Company",
|
||||
"environments.surveys.responses.address_line_1": "Address Line 1",
|
||||
"environments.surveys.responses.address_line_2": "Address Line 2",
|
||||
"environments.surveys.responses.city": "City",
|
||||
"environments.surveys.responses.state_region": "State/Region",
|
||||
"environments.surveys.responses.zip_post_code": "ZIP/Post Code",
|
||||
"environments.surveys.responses.country": "Country",
|
||||
"environments.surveys.responses.first_name": "First Name",
|
||||
"environments.surveys.responses.last_name": "Last Name",
|
||||
"environments.surveys.responses.email": "Email",
|
||||
"environments.surveys.responses.phone": "Phone",
|
||||
"environments.surveys.responses.company": "Company",
|
||||
"common.action": "Action",
|
||||
"workspace.surveys.responses.os": "OS",
|
||||
"workspace.surveys.responses.device": "Device",
|
||||
"workspace.surveys.responses.browser": "Browser",
|
||||
"environments.surveys.responses.os": "OS",
|
||||
"environments.surveys.responses.device": "Device",
|
||||
"environments.surveys.responses.browser": "Browser",
|
||||
"common.url": "URL",
|
||||
"workspace.surveys.responses.source": "Source",
|
||||
"environments.surveys.responses.source": "Source",
|
||||
};
|
||||
return translations[key] || key;
|
||||
}) as unknown as TFunction;
|
||||
@@ -45,37 +45,37 @@ describe("utils", () => {
|
||||
test("returns correct label for addressLine1", () => {
|
||||
const result = getAddressFieldLabel("addressLine1", mockT);
|
||||
expect(result).toBe("Address Line 1");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.address_line_1");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.address_line_1");
|
||||
});
|
||||
|
||||
test("returns correct label for addressLine2", () => {
|
||||
const result = getAddressFieldLabel("addressLine2", mockT);
|
||||
expect(result).toBe("Address Line 2");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.address_line_2");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.address_line_2");
|
||||
});
|
||||
|
||||
test("returns correct label for city", () => {
|
||||
const result = getAddressFieldLabel("city", mockT);
|
||||
expect(result).toBe("City");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.city");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.city");
|
||||
});
|
||||
|
||||
test("returns correct label for state", () => {
|
||||
const result = getAddressFieldLabel("state", mockT);
|
||||
expect(result).toBe("State/Region");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.state_region");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.state_region");
|
||||
});
|
||||
|
||||
test("returns correct label for zip", () => {
|
||||
const result = getAddressFieldLabel("zip", mockT);
|
||||
expect(result).toBe("ZIP/Post Code");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.zip_post_code");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.zip_post_code");
|
||||
});
|
||||
|
||||
test("returns correct label for country", () => {
|
||||
const result = getAddressFieldLabel("country", mockT);
|
||||
expect(result).toBe("Country");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.country");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.country");
|
||||
});
|
||||
|
||||
test("returns undefined for unknown field", () => {
|
||||
@@ -89,31 +89,31 @@ describe("utils", () => {
|
||||
test("returns correct label for firstName", () => {
|
||||
const result = getContactInfoFieldLabel("firstName", mockT);
|
||||
expect(result).toBe("First Name");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.first_name");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.first_name");
|
||||
});
|
||||
|
||||
test("returns correct label for lastName", () => {
|
||||
const result = getContactInfoFieldLabel("lastName", mockT);
|
||||
expect(result).toBe("Last Name");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.last_name");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.last_name");
|
||||
});
|
||||
|
||||
test("returns correct label for email", () => {
|
||||
const result = getContactInfoFieldLabel("email", mockT);
|
||||
expect(result).toBe("Email");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.email");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.email");
|
||||
});
|
||||
|
||||
test("returns correct label for phone", () => {
|
||||
const result = getContactInfoFieldLabel("phone", mockT);
|
||||
expect(result).toBe("Phone");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.phone");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.phone");
|
||||
});
|
||||
|
||||
test("returns correct label for company", () => {
|
||||
const result = getContactInfoFieldLabel("company", mockT);
|
||||
expect(result).toBe("Company");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.company");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.company");
|
||||
});
|
||||
|
||||
test("returns undefined for unknown field", () => {
|
||||
@@ -133,25 +133,25 @@ describe("utils", () => {
|
||||
test("returns correct label for country", () => {
|
||||
const result = getMetadataFieldLabel("country", mockT);
|
||||
expect(result).toBe("Country");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.country");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.country");
|
||||
});
|
||||
|
||||
test("returns correct label for os", () => {
|
||||
const result = getMetadataFieldLabel("os", mockT);
|
||||
expect(result).toBe("OS");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.os");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.os");
|
||||
});
|
||||
|
||||
test("returns correct label for device", () => {
|
||||
const result = getMetadataFieldLabel("device", mockT);
|
||||
expect(result).toBe("Device");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.device");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.device");
|
||||
});
|
||||
|
||||
test("returns correct label for browser", () => {
|
||||
const result = getMetadataFieldLabel("browser", mockT);
|
||||
expect(result).toBe("Browser");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.browser");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.browser");
|
||||
});
|
||||
|
||||
test("returns correct label for url", () => {
|
||||
@@ -163,7 +163,7 @@ describe("utils", () => {
|
||||
test("returns correct label for source", () => {
|
||||
const result = getMetadataFieldLabel("source", mockT);
|
||||
expect(result).toBe("Source");
|
||||
expect(mockT).toHaveBeenCalledWith("workspace.surveys.responses.source");
|
||||
expect(mockT).toHaveBeenCalledWith("environments.surveys.responses.source");
|
||||
});
|
||||
|
||||
test("returns capitalized label for unknown field", () => {
|
||||
+16
-16
@@ -13,17 +13,17 @@ import { TResponseMeta } from "@formbricks/types/responses";
|
||||
export const getAddressFieldLabel = (field: string, t: TFunction) => {
|
||||
switch (field) {
|
||||
case "addressLine1":
|
||||
return t("workspace.surveys.responses.address_line_1");
|
||||
return t("environments.surveys.responses.address_line_1");
|
||||
case "addressLine2":
|
||||
return t("workspace.surveys.responses.address_line_2");
|
||||
return t("environments.surveys.responses.address_line_2");
|
||||
case "city":
|
||||
return t("workspace.surveys.responses.city");
|
||||
return t("environments.surveys.responses.city");
|
||||
case "state":
|
||||
return t("workspace.surveys.responses.state_region");
|
||||
return t("environments.surveys.responses.state_region");
|
||||
case "zip":
|
||||
return t("workspace.surveys.responses.zip_post_code");
|
||||
return t("environments.surveys.responses.zip_post_code");
|
||||
case "country":
|
||||
return t("workspace.surveys.responses.country");
|
||||
return t("environments.surveys.responses.country");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -32,15 +32,15 @@ export const getAddressFieldLabel = (field: string, t: TFunction) => {
|
||||
export const getContactInfoFieldLabel = (field: string, t: TFunction) => {
|
||||
switch (field) {
|
||||
case "firstName":
|
||||
return t("workspace.surveys.responses.first_name");
|
||||
return t("environments.surveys.responses.first_name");
|
||||
case "lastName":
|
||||
return t("workspace.surveys.responses.last_name");
|
||||
return t("environments.surveys.responses.last_name");
|
||||
case "email":
|
||||
return t("workspace.surveys.responses.email");
|
||||
return t("environments.surveys.responses.email");
|
||||
case "phone":
|
||||
return t("workspace.surveys.responses.phone");
|
||||
return t("environments.surveys.responses.phone");
|
||||
case "company":
|
||||
return t("workspace.surveys.responses.company");
|
||||
return t("environments.surveys.responses.company");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -51,17 +51,17 @@ export const getMetadataFieldLabel = (label: string, t: TFunction) => {
|
||||
case "action":
|
||||
return t("common.action");
|
||||
case "country":
|
||||
return t("workspace.surveys.responses.country");
|
||||
return t("environments.surveys.responses.country");
|
||||
case "os":
|
||||
return t("workspace.surveys.responses.os");
|
||||
return t("environments.surveys.responses.os");
|
||||
case "device":
|
||||
return t("workspace.surveys.responses.device");
|
||||
return t("environments.surveys.responses.device");
|
||||
case "browser":
|
||||
return t("workspace.surveys.responses.browser");
|
||||
return t("environments.surveys.responses.browser");
|
||||
case "url":
|
||||
return t("common.url");
|
||||
case "source":
|
||||
return t("workspace.surveys.responses.source");
|
||||
return t("environments.surveys.responses.source");
|
||||
default:
|
||||
return capitalize(label);
|
||||
}
|
||||
+12
-10
@@ -1,32 +1,32 @@
|
||||
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { SurveyAnalysisNavigation } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation";
|
||||
import { ResponsePage } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
|
||||
import { SurveyAnalysisCTA } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
||||
import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation";
|
||||
import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
|
||||
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
||||
import { IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED, RESPONSES_PER_PAGE } from "@/lib/constants";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getResponseCountBySurveyId, getResponses } from "@/lib/response/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { getTagsByWorkspaceId } from "@/lib/tag/service";
|
||||
import { getTagsByEnvironmentId } from "@/lib/tag/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getSegments } from "@/modules/ee/contacts/segments/lib/segments";
|
||||
import { getIsContactsEnabled, getIsQuotasEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getQuotas } from "@/modules/ee/quotas/lib/quotas";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { getOrganizationBilling } from "@/modules/survey/lib/survey";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
|
||||
|
||||
const Page = async (props: { params: Promise<{ workspaceId: string; surveyId: string }> }) => {
|
||||
const Page = async (props: { params: Promise<{ environmentId: string; surveyId: string }> }) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
|
||||
const { session, organization, isReadOnly, workspace } = await getWorkspaceAuth(params.workspaceId);
|
||||
const { session, environment, organization, isReadOnly } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
const [survey, user, tags, isContactsEnabled, responseCount] = await Promise.all([
|
||||
getSurvey(params.surveyId),
|
||||
getUser(session.user.id),
|
||||
getTagsByWorkspaceId(workspace.id),
|
||||
getTagsByEnvironmentId(params.environmentId),
|
||||
getIsContactsEnabled(organization.id),
|
||||
getResponseCountBySurveyId(params.surveyId),
|
||||
]);
|
||||
@@ -43,7 +43,7 @@ const Page = async (props: { params: Promise<{ workspaceId: string; surveyId: st
|
||||
throw new ResourceNotFoundError(t("common.organization"), null);
|
||||
}
|
||||
|
||||
const segments = isContactsEnabled ? await getSegments(workspace.id) : [];
|
||||
const segments = isContactsEnabled ? await getSegments(params.environmentId) : [];
|
||||
|
||||
const publicDomain = getPublicDomain();
|
||||
|
||||
@@ -64,6 +64,7 @@ const Page = async (props: { params: Promise<{ workspaceId: string; surveyId: st
|
||||
pageTitle={survey.name}
|
||||
cta={
|
||||
<SurveyAnalysisCTA
|
||||
environment={environment}
|
||||
survey={survey}
|
||||
isReadOnly={isReadOnly}
|
||||
user={user}
|
||||
@@ -75,9 +76,10 @@ const Page = async (props: { params: Promise<{ workspaceId: string; surveyId: st
|
||||
isStorageConfigured={IS_STORAGE_CONFIGURED}
|
||||
/>
|
||||
}>
|
||||
<SurveyAnalysisNavigation survey={survey} activeId="responses" />
|
||||
<SurveyAnalysisNavigation environmentId={environment.id} survey={survey} activeId="responses" />
|
||||
</PageHeader>
|
||||
<ResponsePage
|
||||
environment={environment}
|
||||
survey={survey}
|
||||
surveyId={params.surveyId}
|
||||
environmentTags={tags}
|
||||
+18
-18
@@ -3,12 +3,12 @@
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { OperationNotAllowedError, ResourceNotFoundError, UnknownError } from "@formbricks/types/errors";
|
||||
import { getEmailTemplateHtml } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate";
|
||||
import { getEmailTemplateHtml } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate";
|
||||
import { getSurvey, updateSurvey } from "@/lib/survey/service";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { convertToCsv } from "@/lib/utils/file-conversion";
|
||||
import { getOrganizationIdFromSurveyId, getWorkspaceIdFromSurveyId } from "@/lib/utils/helper";
|
||||
import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import { generatePersonalLinks } from "@/modules/ee/contacts/lib/contacts";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
@@ -35,9 +35,9 @@ export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "read",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -56,7 +56,7 @@ export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient
|
||||
return await sendEmbedSurveyPreviewEmail(
|
||||
ctx.user.email,
|
||||
emailHtml,
|
||||
survey.workspaceId,
|
||||
survey.environmentId,
|
||||
ctx.user.locale,
|
||||
organizationLogoUrl || ""
|
||||
);
|
||||
@@ -64,13 +64,13 @@ export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient
|
||||
|
||||
const ZResetSurveyAction = z.object({
|
||||
surveyId: ZId,
|
||||
workspaceId: ZId,
|
||||
projectId: ZId,
|
||||
});
|
||||
|
||||
export const resetSurveyAction = authenticatedActionClient.inputSchema(ZResetSurveyAction).action(
|
||||
withAuditLogging("updated", "survey", async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
|
||||
const workspaceId = await getWorkspaceIdFromSurveyId(parsedInput.surveyId);
|
||||
const projectId = await getProjectIdFromSurveyId(parsedInput.surveyId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -81,9 +81,9 @@ export const resetSurveyAction = authenticatedActionClient.inputSchema(ZResetSur
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
workspaceId,
|
||||
projectId,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -125,9 +125,9 @@ export const getEmailHtmlAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -138,7 +138,7 @@ export const getEmailHtmlAction = authenticatedActionClient
|
||||
const ZGeneratePersonalLinksAction = z.object({
|
||||
surveyId: ZId,
|
||||
segmentId: ZId,
|
||||
workspaceId: ZId,
|
||||
environmentId: ZId,
|
||||
expirationDays: z.number().optional(),
|
||||
});
|
||||
|
||||
@@ -148,7 +148,7 @@ export const generatePersonalLinksAction = authenticatedActionClient
|
||||
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
|
||||
const isContactsEnabled = await getIsContactsEnabled(organizationId);
|
||||
if (!isContactsEnabled) {
|
||||
throw new OperationNotAllowedError("Contacts are not enabled for this workspace");
|
||||
throw new OperationNotAllowedError("Contacts are not enabled for this environment");
|
||||
}
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
@@ -160,8 +160,8 @@ export const generatePersonalLinksAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
@@ -217,7 +217,7 @@ export const generatePersonalLinksAction = authenticatedActionClient
|
||||
|
||||
const ZUpdateSingleUseLinksAction = z.object({
|
||||
surveyId: ZId,
|
||||
workspaceId: ZId,
|
||||
environmentId: ZId,
|
||||
isSingleUse: z.boolean(),
|
||||
isSingleUseEncryption: z.boolean(),
|
||||
});
|
||||
@@ -234,8 +234,8 @@ export const updateSingleUseLinksAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
+4
-5
@@ -4,7 +4,6 @@ import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyElementSummaryAddress } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { ArrayResponse } from "@/modules/ui/components/array-response";
|
||||
@@ -14,13 +13,13 @@ import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface AddressSummaryProps {
|
||||
elementSummary: TSurveyElementSummaryAddress;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const AddressSummary = ({ elementSummary, survey, locale }: AddressSummaryProps) => {
|
||||
export const AddressSummary = ({ elementSummary, environmentId, survey, locale }: AddressSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspace();
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
|
||||
@@ -33,7 +32,7 @@ export const AddressSummary = ({ elementSummary, survey, locale }: AddressSummar
|
||||
<div className="max-h-[62vh] w-full overflow-y-auto">
|
||||
{elementSummary.samples.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("workspace.surveys.summary.no_responses_found")} variant="simple" />
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
) : (
|
||||
elementSummary.samples.map((response) => {
|
||||
@@ -45,7 +44,7 @@ export const AddressSummary = ({ elementSummary, survey, locale }: AddressSummar
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/workspaces/${workspace?.id}/contacts/${response.contact.id}`}>
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
+2
-1
@@ -2,12 +2,13 @@
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyElementSummaryCal } from "@formbricks/types/surveys/types";
|
||||
import { convertFloatToNDecimal } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { convertFloatToNDecimal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface CalSummaryProps {
|
||||
elementSummary: TSurveyElementSummaryCal;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
+9
-5
@@ -4,7 +4,6 @@ import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyElementSummaryContactInfo } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { ArrayResponse } from "@/modules/ui/components/array-response";
|
||||
@@ -14,13 +13,18 @@ import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface ContactInfoSummaryProps {
|
||||
elementSummary: TSurveyElementSummaryContactInfo;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const ContactInfoSummary = ({ elementSummary, survey, locale }: ContactInfoSummaryProps) => {
|
||||
export const ContactInfoSummary = ({
|
||||
elementSummary,
|
||||
environmentId,
|
||||
survey,
|
||||
locale,
|
||||
}: ContactInfoSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspace();
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
|
||||
@@ -33,7 +37,7 @@ export const ContactInfoSummary = ({ elementSummary, survey, locale }: ContactIn
|
||||
<div className="max-h-[62vh] w-full overflow-y-auto">
|
||||
{elementSummary.samples.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("workspace.surveys.summary.no_responses_found")} variant="simple" />
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
) : (
|
||||
elementSummary.samples.map((response) => {
|
||||
@@ -45,7 +49,7 @@ export const ContactInfoSummary = ({ elementSummary, survey, locale }: ContactIn
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/workspaces/${workspace?.id}/contacts/${response.contact.id}`}>
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
+4
-5
@@ -5,7 +5,6 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyElementSummaryDate } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { formatStoredDateForDisplay } from "@/lib/utils/date-display";
|
||||
@@ -16,13 +15,13 @@ import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface DateElementSummary {
|
||||
elementSummary: TSurveyElementSummaryDate;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const DateElementSummary = ({ elementSummary, survey, locale }: DateElementSummary) => {
|
||||
export const DateElementSummary = ({ elementSummary, environmentId, survey, locale }: DateElementSummary) => {
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspace();
|
||||
const [visibleResponses, setVisibleResponses] = useState(10);
|
||||
|
||||
const handleLoadMore = () => {
|
||||
@@ -55,7 +54,7 @@ export const DateElementSummary = ({ elementSummary, survey, locale }: DateEleme
|
||||
<div className="max-h-[62vh] w-full overflow-y-auto">
|
||||
{elementSummary.samples.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("workspace.surveys.summary.no_responses_found")} variant="simple" />
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
) : (
|
||||
elementSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||
@@ -66,7 +65,7 @@ export const DateElementSummary = ({ elementSummary, survey, locale }: DateEleme
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/workspaces/${workspace?.id}/contacts/${response.contact.id}`}>
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
+2
-2
@@ -42,7 +42,7 @@ export const ElementSummaryHeader = ({
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
{elementType && <elementType.icon className="mr-2 h-4 w-4" />}
|
||||
{elementType ? elementType.label : t("workspace.surveys.summary.unknown_question_type")}{" "}
|
||||
{elementType ? elementType.label : t("environments.surveys.summary.unknown_question_type")}{" "}
|
||||
{t("common.question")}
|
||||
</div>
|
||||
{showResponses && (
|
||||
@@ -54,7 +54,7 @@ export const ElementSummaryHeader = ({
|
||||
{additionalInfo}
|
||||
{!elementSummary.element.required && (
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
{t("workspace.surveys.edit.optional")}
|
||||
{t("environments.surveys.edit.optional")}
|
||||
</div>
|
||||
)}
|
||||
<IdBadge id={elementSummary.element.id} />
|
||||
+9
-5
@@ -6,7 +6,6 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyElementSummaryFileUpload } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { getOriginalFileNameFromUrl } from "@/modules/storage/url-helpers";
|
||||
@@ -17,14 +16,19 @@ import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface FileUploadSummaryProps {
|
||||
elementSummary: TSurveyElementSummaryFileUpload;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const FileUploadSummary = ({ elementSummary, survey, locale }: FileUploadSummaryProps) => {
|
||||
export const FileUploadSummary = ({
|
||||
elementSummary,
|
||||
environmentId,
|
||||
survey,
|
||||
locale,
|
||||
}: FileUploadSummaryProps) => {
|
||||
const [visibleResponses, setVisibleResponses] = useState(10);
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspace();
|
||||
const handleLoadMore = () => {
|
||||
// Increase the number of visible responses by 10, not exceeding the total number of responses
|
||||
setVisibleResponses((prevVisibleResponses) =>
|
||||
@@ -44,7 +48,7 @@ export const FileUploadSummary = ({ elementSummary, survey, locale }: FileUpload
|
||||
<div className="max-h-[62vh] w-full overflow-y-auto">
|
||||
{elementSummary.files.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("workspace.surveys.summary.no_responses_found")} variant="simple" />
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
) : (
|
||||
elementSummary.files.slice(0, visibleResponses).map((response) => (
|
||||
@@ -55,7 +59,7 @@ export const FileUploadSummary = ({ elementSummary, survey, locale }: FileUpload
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/workspaces/${workspace?.id}/contacts/${response.contact.id}`}>
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
+5
-5
@@ -3,9 +3,9 @@
|
||||
import { InboxIcon, Link, MessageSquareTextIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurveyElementSummaryHiddenFields } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
@@ -13,14 +13,14 @@ import { Button } from "@/modules/ui/components/button";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
|
||||
interface HiddenFieldsSummaryProps {
|
||||
environment: TEnvironment;
|
||||
elementSummary: TSurveyElementSummaryHiddenFields;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const HiddenFieldsSummary = ({ elementSummary, locale }: HiddenFieldsSummaryProps) => {
|
||||
export const HiddenFieldsSummary = ({ environment, elementSummary, locale }: HiddenFieldsSummaryProps) => {
|
||||
const [visibleResponses, setVisibleResponses] = useState(10);
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspace();
|
||||
const handleLoadMore = () => {
|
||||
// Increase the number of visible responses by 10, not exceeding the total number of responses
|
||||
setVisibleResponses((prevVisibleResponses) =>
|
||||
@@ -53,7 +53,7 @@ export const HiddenFieldsSummary = ({ elementSummary, locale }: HiddenFieldsSumm
|
||||
</div>
|
||||
{elementSummary.samples.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("workspace.surveys.summary.no_responses_found")} variant="simple" />
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
) : (
|
||||
elementSummary.samples.slice(0, visibleResponses).map((response, idx) => (
|
||||
@@ -64,7 +64,7 @@ export const HiddenFieldsSummary = ({ elementSummary, locale }: HiddenFieldsSumm
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/workspaces/${workspace?.id}/contacts/${response.contact.id}`}>
|
||||
href={`/environments/${environment.id}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
+6
-6
@@ -7,7 +7,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveyElementSummaryMultipleChoice, TSurveyType } from "@formbricks/types/surveys/types";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { getChoiceIdByValue } from "@/lib/response/utils";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
@@ -19,6 +18,7 @@ import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface MultipleChoiceSummaryProps {
|
||||
elementSummary: TSurveyElementSummaryMultipleChoice;
|
||||
environmentId: string;
|
||||
surveyType: TSurveyType;
|
||||
survey: TSurvey;
|
||||
setFilter: (
|
||||
@@ -32,12 +32,12 @@ interface MultipleChoiceSummaryProps {
|
||||
|
||||
export const MultipleChoiceSummary = ({
|
||||
elementSummary,
|
||||
environmentId,
|
||||
surveyType,
|
||||
survey,
|
||||
setFilter,
|
||||
}: MultipleChoiceSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspace();
|
||||
const [visibleOtherResponses, setVisibleOtherResponses] = useState(10);
|
||||
const otherValue = elementSummary.element.choices.find((choice) => choice.id === "other")?.label.default;
|
||||
// sort by count and transform to array
|
||||
@@ -96,8 +96,8 @@ export const MultipleChoiceSummary = ({
|
||||
elementSummary.element.type,
|
||||
elementSummary.type === TSurveyElementTypeEnum.MultipleChoiceSingle ||
|
||||
otherValue === result.value
|
||||
? t("workspace.surveys.summary.includes_either")
|
||||
: t("workspace.surveys.summary.includes_all"),
|
||||
? t("environments.surveys.summary.includes_either")
|
||||
: t("environments.surveys.summary.includes_all"),
|
||||
[result.value]
|
||||
)
|
||||
}>
|
||||
@@ -125,7 +125,7 @@ export const MultipleChoiceSummary = ({
|
||||
<div className="mt-4 rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-2 content-center rounded-t-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-1 pl-6">
|
||||
{t("workspace.surveys.summary.other_values_found")}
|
||||
{t("environments.surveys.summary.other_values_found")}
|
||||
</div>
|
||||
<div className="col-span-1 pl-6">{surveyType === "app" && t("common.user")}</div>
|
||||
</div>
|
||||
@@ -143,7 +143,7 @@ export const MultipleChoiceSummary = ({
|
||||
<Link
|
||||
href={
|
||||
otherValue.contact.id
|
||||
? `/workspaces/${workspace?.id}/contacts/${otherValue.contact.id}`
|
||||
? `/environments/${environmentId}/contacts/${otherValue.contact.id}`
|
||||
: { pathname: null }
|
||||
}
|
||||
className="m-2 grid h-16 grid-cols-2 items-center rounded-lg text-sm hover:bg-slate-100">
|
||||
+14
-25
@@ -8,7 +8,7 @@ import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveyElementSummaryNps } from "@formbricks/types/surveys/types";
|
||||
import { HalfCircle, ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/modules/ui/components/tabs";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||
import { TooltipProvider } from "@/modules/ui/components/tooltip";
|
||||
import { convertFloatToNDecimal } from "../lib/utils";
|
||||
import { ClickableBarSegment } from "./ClickableBarSegment";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
@@ -39,20 +39,19 @@ const calculateNPSOpacity = (rating: number): number => {
|
||||
export const NPSSummary = ({ elementSummary, survey, setFilter }: NPSSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<"aggregated" | "individual">("aggregated");
|
||||
const promotersPercentage = convertFloatToNDecimal(elementSummary.promoters.percentage, 2);
|
||||
|
||||
const applyFilter = (group: string) => {
|
||||
const filters = {
|
||||
promoters: {
|
||||
comparison: t("workspace.surveys.summary.includes_either"),
|
||||
comparison: t("environments.surveys.summary.includes_either"),
|
||||
values: ["9", "10"],
|
||||
},
|
||||
passives: {
|
||||
comparison: t("workspace.surveys.summary.includes_either"),
|
||||
comparison: t("environments.surveys.summary.includes_either"),
|
||||
values: ["7", "8"],
|
||||
},
|
||||
detractors: {
|
||||
comparison: t("workspace.surveys.summary.is_less_than"),
|
||||
comparison: t("environments.surveys.summary.is_less_than"),
|
||||
values: "7",
|
||||
},
|
||||
dismissed: {
|
||||
@@ -82,23 +81,13 @@ export const NPSSummary = ({ elementSummary, survey, setFilter }: NPSSummaryProp
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
additionalInfo={
|
||||
<TooltipProvider delayDuration={150}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center space-x-2 rounded-lg bg-slate-100 p-2">
|
||||
<SatisfactionIndicator percentage={elementSummary.promoters.percentage} />
|
||||
<div>
|
||||
{t("workspace.surveys.summary.promoters")}: {promotersPercentage}%
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{t("workspace.surveys.summary.nps_promoters_tooltip", {
|
||||
percentage: promotersPercentage,
|
||||
})}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div className="flex items-center space-x-2 rounded-lg bg-slate-100 p-2">
|
||||
<SatisfactionIndicator percentage={elementSummary.promoters.percentage} />
|
||||
<div>
|
||||
{t("environments.surveys.summary.promoters")}:{" "}
|
||||
{convertFloatToNDecimal(elementSummary.promoters.percentage, 2)}%
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -106,10 +95,10 @@ export const NPSSummary = ({ elementSummary, survey, setFilter }: NPSSummaryProp
|
||||
<div className="flex justify-end px-4 md:px-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="aggregated" icon={<BarChartHorizontal className="h-4 w-4" />}>
|
||||
{t("workspace.surveys.summary.aggregated")}
|
||||
{t("environments.surveys.summary.aggregated")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="individual" icon={<BarChart className="h-4 w-4" />}>
|
||||
{t("workspace.surveys.summary.individual")}
|
||||
{t("environments.surveys.summary.individual")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
@@ -164,7 +153,7 @@ export const NPSSummary = ({ elementSummary, survey, setFilter }: NPSSummaryProp
|
||||
elementSummary.element.id,
|
||||
elementSummary.element.headline,
|
||||
elementSummary.element.type,
|
||||
t("workspace.surveys.summary.is_equal_to"),
|
||||
t("environments.surveys.summary.is_equal_to"),
|
||||
choice.rating.toString()
|
||||
)
|
||||
}>
|
||||
+4
-5
@@ -5,7 +5,6 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyElementSummaryOpenText } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { renderHyperlinkedContent } from "@/modules/analysis/utils";
|
||||
@@ -17,13 +16,13 @@ import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface OpenTextSummaryProps {
|
||||
elementSummary: TSurveyElementSummaryOpenText;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const OpenTextSummary = ({ elementSummary, survey, locale }: OpenTextSummaryProps) => {
|
||||
export const OpenTextSummary = ({ elementSummary, environmentId, survey, locale }: OpenTextSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspace();
|
||||
const [visibleResponses, setVisibleResponses] = useState(10);
|
||||
|
||||
const handleLoadMore = () => {
|
||||
@@ -39,7 +38,7 @@ export const OpenTextSummary = ({ elementSummary, survey, locale }: OpenTextSumm
|
||||
<div className="border-t border-slate-200"></div>
|
||||
{elementSummary.samples.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("workspace.surveys.summary.no_responses_found")} variant="simple" />
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[40vh] overflow-y-auto">
|
||||
@@ -58,7 +57,7 @@ export const OpenTextSummary = ({ elementSummary, survey, locale }: OpenTextSumm
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/workspaces/${workspace?.id}/contacts/${response.contact.id}`}>
|
||||
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
+2
-2
@@ -55,8 +55,8 @@ export const PictureChoiceSummary = ({ elementSummary, survey, setFilter }: Pict
|
||||
elementSummary.element.id,
|
||||
elementSummary.element.headline,
|
||||
elementSummary.element.type,
|
||||
t("workspace.surveys.summary.includes_all"),
|
||||
[`${t("workspace.surveys.edit.picture_idx", { idx: index + 1 })}`]
|
||||
t("environments.surveys.summary.includes_all"),
|
||||
[`${t("environments.surveys.edit.picture_idx", { idx: index + 1 })}`]
|
||||
)
|
||||
}>
|
||||
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">
|
||||
+1
-1
@@ -37,7 +37,7 @@ export const RankingSummary = ({ elementSummary, survey }: RankingSummaryProps)
|
||||
<span className="font-bold text-slate-600">
|
||||
#{convertFloatToNDecimal(result.avgRanking, 2)}
|
||||
</span>
|
||||
<span>{t("workspace.surveys.summary.average")}</span>
|
||||
<span>{t("environments.surveys.summary.average")}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
+42
-30
@@ -1,17 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { BarChart, BarChartHorizontal } from "lucide-react";
|
||||
import { type JSX, useState } from "react";
|
||||
import { BarChart, BarChartHorizontal, CircleSlash2, SmileIcon, StarIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyElementSummaryCes,
|
||||
TSurveyElementSummaryCsat,
|
||||
TSurveyElementSummaryRating,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { convertFloatToNDecimal } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { TSurvey, TSurveyElementSummaryRating } from "@formbricks/types/surveys/types";
|
||||
import { convertFloatToNDecimal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { RatingResponse } from "@/modules/ui/components/rating-response";
|
||||
@@ -20,14 +15,10 @@ import { TooltipProvider } from "@/modules/ui/components/tooltip";
|
||||
import { ClickableBarSegment } from "./ClickableBarSegment";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
import { RatingScaleLegend } from "./RatingScaleLegend";
|
||||
import { SatisfactionIndicator } from "./SatisfactionIndicator";
|
||||
|
||||
type RatingLikeElementSummary =
|
||||
| TSurveyElementSummaryCes
|
||||
| TSurveyElementSummaryCsat
|
||||
| TSurveyElementSummaryRating;
|
||||
|
||||
interface RatingLikeSummaryProps {
|
||||
elementSummary: RatingLikeElementSummary;
|
||||
interface RatingSummaryProps {
|
||||
elementSummary: TSurveyElementSummaryRating;
|
||||
survey: TSurvey;
|
||||
setFilter: (
|
||||
elementId: string,
|
||||
@@ -36,30 +27,51 @@ interface RatingLikeSummaryProps {
|
||||
filterValue: string,
|
||||
filterComboBoxValue?: string | string[]
|
||||
) => void;
|
||||
additionalInfo: JSX.Element;
|
||||
}
|
||||
|
||||
export const RatingLikeSummary = ({
|
||||
elementSummary,
|
||||
survey,
|
||||
setFilter,
|
||||
additionalInfo,
|
||||
}: RatingLikeSummaryProps) => {
|
||||
export const RatingSummary = ({ elementSummary, survey, setFilter }: RatingSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<"aggregated" | "individual">("aggregated");
|
||||
|
||||
const getIconBasedOnScale = useMemo(() => {
|
||||
const scale = elementSummary.element.scale;
|
||||
if (scale === "number") return <CircleSlash2 className="h-4 w-4" />;
|
||||
else if (scale === "star") return <StarIcon fill="rgb(250 204 21)" className="h-4 w-4 text-yellow-400" />;
|
||||
else if (scale === "smiley") return <SmileIcon className="h-4 w-4" />;
|
||||
}, [elementSummary]);
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} additionalInfo={additionalInfo} />
|
||||
<ElementSummaryHeader
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
additionalInfo={
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-2 rounded-lg bg-slate-100 p-2">
|
||||
{getIconBasedOnScale}
|
||||
<div>
|
||||
{t("environments.surveys.summary.overall")}: {elementSummary.average.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 rounded-lg bg-slate-100 p-2">
|
||||
<SatisfactionIndicator percentage={elementSummary.csat.satisfiedPercentage} />
|
||||
<div>
|
||||
CSAT: {elementSummary.csat.satisfiedPercentage}% {t("environments.surveys.summary.satisfied")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as "aggregated" | "individual")}>
|
||||
<div className="flex justify-end px-4 md:px-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="aggregated" icon={<BarChartHorizontal className="h-4 w-4" />}>
|
||||
{t("workspace.surveys.summary.aggregated")}
|
||||
{t("environments.surveys.summary.aggregated")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="individual" icon={<BarChart className="h-4 w-4" />}>
|
||||
{t("workspace.surveys.summary.individual")}
|
||||
{t("environments.surveys.summary.individual")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
@@ -68,7 +80,7 @@ export const RatingLikeSummary = ({
|
||||
<div className="px-4 pb-6 pt-4 md:px-6">
|
||||
{elementSummary.responseCount === 0 ? (
|
||||
<>
|
||||
<EmptyState text={t("workspace.surveys.summary.no_responses_found")} variant="simple" />
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
<RatingScaleLegend
|
||||
scale={elementSummary.element.scale}
|
||||
range={elementSummary.element.range}
|
||||
@@ -82,7 +94,7 @@ export const RatingLikeSummary = ({
|
||||
if (result.percentage === 0) return null;
|
||||
|
||||
const range = elementSummary.element.range;
|
||||
const opacity = 0.3 + (result.rating / range) * 0.7;
|
||||
const opacity = 0.3 + (result.rating / range) * 0.8;
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === elementSummary.choices.length - 1;
|
||||
|
||||
@@ -99,7 +111,7 @@ export const RatingLikeSummary = ({
|
||||
elementSummary.element.id,
|
||||
elementSummary.element.headline,
|
||||
elementSummary.element.type,
|
||||
t("workspace.surveys.summary.is_equal_to"),
|
||||
t("environments.surveys.summary.is_equal_to"),
|
||||
result.rating.toString()
|
||||
)
|
||||
}>
|
||||
@@ -164,7 +176,7 @@ export const RatingLikeSummary = ({
|
||||
elementSummary.element.id,
|
||||
elementSummary.element.headline,
|
||||
elementSummary.element.type,
|
||||
t("workspace.surveys.summary.is_equal_to"),
|
||||
t("environments.surveys.summary.is_equal_to"),
|
||||
result.rating.toString()
|
||||
)
|
||||
}>
|
||||
+11
-26
@@ -4,46 +4,31 @@ import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { useWorkspaceContext } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { Confetti } from "@/modules/ui/components/confetti";
|
||||
|
||||
interface SummaryMetadataProps {
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
export const SuccessMessage = ({ survey }: SummaryMetadataProps) => {
|
||||
export const SuccessMessage = ({ environment, survey }: SummaryMetadataProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspaceContext();
|
||||
const searchParams = useSearchParams();
|
||||
const [confetti, setConfetti] = useState(false);
|
||||
|
||||
const isAppSurvey = survey.type === "app";
|
||||
const appSetupCompleted = workspace.appSetupCompleted;
|
||||
const appSetupCompleted = environment.appSetupCompleted;
|
||||
|
||||
useEffect(() => {
|
||||
const publishSuccessParam = searchParams.get("success");
|
||||
const scheduledSuccessParam = searchParams.get("scheduled");
|
||||
|
||||
if (scheduledSuccessParam) {
|
||||
toast.success(t("workspace.surveys.summary.survey_scheduled_successfully"), {
|
||||
id: "survey-schedule-success-toast",
|
||||
duration: 5000,
|
||||
position: "bottom-right",
|
||||
});
|
||||
|
||||
const url = new URL(globalThis.location.href);
|
||||
url.searchParams.delete("scheduled");
|
||||
globalThis.history.replaceState({}, "", url.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (publishSuccessParam) {
|
||||
const newSurveyParam = searchParams?.get("success");
|
||||
if (newSurveyParam && survey && environment) {
|
||||
setConfetti(true);
|
||||
toast.success(
|
||||
isAppSurvey && !appSetupCompleted
|
||||
? t("workspace.surveys.summary.almost_there")
|
||||
: t("workspace.surveys.summary.congrats"),
|
||||
? t("environments.surveys.summary.almost_there")
|
||||
: t("environments.surveys.summary.congrats"),
|
||||
{
|
||||
id: "survey-publish-success-toast",
|
||||
icon: isAppSurvey && !appSetupCompleted ? "🤏" : "🎉",
|
||||
@@ -53,16 +38,16 @@ export const SuccessMessage = ({ survey }: SummaryMetadataProps) => {
|
||||
);
|
||||
|
||||
// Remove success param from url
|
||||
const url = new URL(globalThis.location.href);
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete("success");
|
||||
if (survey.type === "link") {
|
||||
// Add share param to url to open share embed modal
|
||||
url.searchParams.set("share", "true");
|
||||
}
|
||||
|
||||
globalThis.history.replaceState({}, "", url.toString());
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
}
|
||||
}, [appSetupCompleted, isAppSurvey, searchParams, survey.type, t]);
|
||||
}, [environment, isAppSurvey, searchParams, survey, appSetupCompleted, t]);
|
||||
|
||||
return <>{confetti && <Confetti />}</>;
|
||||
};
|
||||
+3
-3
@@ -33,14 +33,14 @@ export const SummaryDropOffs = ({ dropOff, survey }: SummaryDropOffsProps) => {
|
||||
<TimerIcon className="h-5 w-5" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={"top"}>
|
||||
<p className="text-center font-normal">{t("workspace.surveys.summary.ttc_tooltip")}</p>
|
||||
<p className="text-center font-normal">{t("environments.surveys.summary.ttc_tooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="px-4 text-right md:px-6">{t("workspace.surveys.summary.impressions")}</div>
|
||||
<div className="px-4 text-right md:px-6">{t("environments.surveys.summary.impressions")}</div>
|
||||
<div className="px-4 text-right md:mr-1 md:pl-6 md:pr-6">
|
||||
{t("workspace.surveys.summary.drop_offs")}
|
||||
{t("environments.surveys.summary.drop_offs")}
|
||||
</div>
|
||||
</div>
|
||||
{dropOff.map((quesDropOff) => (
|
||||
+6
-6
@@ -5,7 +5,6 @@ import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TDisplayWithContact } from "@formbricks/types/displays";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
|
||||
@@ -14,6 +13,7 @@ interface SummaryImpressionsProps {
|
||||
isLoading: boolean;
|
||||
hasMore: boolean;
|
||||
displaysError: string | null;
|
||||
environmentId: string;
|
||||
locale: TUserLocale;
|
||||
onLoadMore: () => void;
|
||||
onRetry: () => void;
|
||||
@@ -29,12 +29,12 @@ export const SummaryImpressions = ({
|
||||
isLoading,
|
||||
hasMore,
|
||||
displaysError,
|
||||
environmentId,
|
||||
locale,
|
||||
onLoadMore,
|
||||
onRetry,
|
||||
}: SummaryImpressionsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspace();
|
||||
|
||||
const renderContent = () => {
|
||||
if (displaysError) {
|
||||
@@ -57,7 +57,7 @@ export const SummaryImpressions = ({
|
||||
if (displays.length === 0) {
|
||||
return (
|
||||
<div className="p-8 text-center text-sm text-slate-500">
|
||||
{t("workspace.surveys.summary.no_identified_impressions")}
|
||||
{t("environments.surveys.summary.no_identified_impressions")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -66,7 +66,7 @@ export const SummaryImpressions = ({
|
||||
<>
|
||||
<div className="grid min-h-10 grid-cols-4 items-center border-b border-slate-200 bg-slate-100 text-sm font-semibold text-slate-600">
|
||||
<div className="col-span-2 px-4 md:px-6">{t("common.user")}</div>
|
||||
<div className="col-span-2 px-4 md:px-6">{t("workspace.contacts.survey_viewed_at")}</div>
|
||||
<div className="col-span-2 px-4 md:px-6">{t("environments.contacts.survey_viewed_at")}</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[62vh] overflow-y-auto">
|
||||
@@ -78,7 +78,7 @@ export const SummaryImpressions = ({
|
||||
{display.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture break-all text-slate-600 hover:underline"
|
||||
href={`/workspaces/${workspace?.id}/contacts/${display.contact.id}`}>
|
||||
href={`/environments/${environmentId}/contacts/${display.contact.id}`}>
|
||||
{getDisplayContactIdentifier(display)}
|
||||
</Link>
|
||||
) : (
|
||||
@@ -117,7 +117,7 @@ export const SummaryImpressions = ({
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="flex items-center gap-2 rounded-t-xl border-b border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
|
||||
<InfoIcon className="h-4 w-4 shrink-0" />
|
||||
<span>{t("workspace.surveys.summary.impressions_identified_only")}</span>
|
||||
<span>{t("environments.surveys.summary.impressions_identified_only")}</span>
|
||||
</div>
|
||||
{renderContent()}
|
||||
</div>
|
||||
+46
-51
@@ -2,35 +2,34 @@
|
||||
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspaceContext } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { EmptyAppSurveys } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
|
||||
import { EmptyAppSurveys } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
|
||||
import {
|
||||
SelectedFilterValue,
|
||||
useResponseFilter,
|
||||
} from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import { CESSummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/CESSummary";
|
||||
import { CSATSummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/CSATSummary";
|
||||
import { CTASummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary";
|
||||
import { CalSummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary";
|
||||
import { ConsentSummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary";
|
||||
import { ContactInfoSummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/ContactInfoSummary";
|
||||
import { DateElementSummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/DateElementSummary";
|
||||
import { FileUploadSummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary";
|
||||
import { HiddenFieldsSummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary";
|
||||
import { MatrixElementSummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/MatrixElementSummary";
|
||||
import { MultipleChoiceSummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary";
|
||||
import { NPSSummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary";
|
||||
import { OpenTextSummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary";
|
||||
import { PictureChoiceSummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary";
|
||||
import { RankingSummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/RankingSummary";
|
||||
import { RatingSummary } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary";
|
||||
import { constructToastMessage } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { OptionsType } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/components/ElementsComboBox";
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import { CTASummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary";
|
||||
import { CalSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary";
|
||||
import { ConsentSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary";
|
||||
import { ContactInfoSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ContactInfoSummary";
|
||||
import { DateElementSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateElementSummary";
|
||||
import { FileUploadSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary";
|
||||
import { HiddenFieldsSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary";
|
||||
import { MatrixElementSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixElementSummary";
|
||||
import { MultipleChoiceSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary";
|
||||
import { NPSSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary";
|
||||
import { OpenTextSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary";
|
||||
import { PictureChoiceSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary";
|
||||
import { RankingSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RankingSummary";
|
||||
import { RatingSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary";
|
||||
import { constructToastMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ElementsComboBox";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { SkeletonLoader } from "@/modules/ui/components/skeleton-loader";
|
||||
@@ -39,12 +38,12 @@ import { AddressSummary } from "./AddressSummary";
|
||||
interface SummaryListProps {
|
||||
summary: TSurveySummary["summary"];
|
||||
responseCount: number | null;
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryListProps) => {
|
||||
const { workspace } = useWorkspaceContext();
|
||||
export const SummaryList = ({ summary, environment, responseCount, survey, locale }: SummaryListProps) => {
|
||||
const { setSelectedFilter, selectedFilter } = useResponseFilter();
|
||||
const { t } = useTranslation();
|
||||
const setFilter = (
|
||||
@@ -76,7 +75,7 @@ export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryL
|
||||
filterValue: filterValue,
|
||||
},
|
||||
};
|
||||
toast.success(t("workspace.surveys.summary.filter_updated_successfully"), { duration: 5000 });
|
||||
toast.success(t("environments.surveys.summary.filter_updated_successfully"), { duration: 5000 });
|
||||
} else {
|
||||
// Add new filter
|
||||
filterObject.filter.push({
|
||||
@@ -88,7 +87,7 @@ export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryL
|
||||
});
|
||||
toast.success(
|
||||
constructToastMessage(elementType, filterValue, survey, elementId, t, filterComboBoxValue) ??
|
||||
t("workspace.surveys.summary.filter_added_successfully"),
|
||||
t("environments.surveys.summary.filter_added_successfully"),
|
||||
{ duration: 5000 }
|
||||
);
|
||||
}
|
||||
@@ -101,12 +100,12 @@ export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryL
|
||||
|
||||
return (
|
||||
<div className="mt-10 space-y-8">
|
||||
{survey.type === "app" && responseCount === 0 && !workspace.appSetupCompleted ? (
|
||||
<EmptyAppSurveys />
|
||||
{survey.type === "app" && responseCount === 0 && !environment.appSetupCompleted ? (
|
||||
<EmptyAppSurveys environment={environment} />
|
||||
) : summary.length === 0 ? (
|
||||
<SkeletonLoader type="summary" />
|
||||
) : responseCount === 0 ? (
|
||||
<EmptyState text={t("workspace.surveys.summary.no_responses_found")} />
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} />
|
||||
) : (
|
||||
summary.map((elementSummary) => {
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.OpenText) {
|
||||
@@ -114,6 +113,7 @@ export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryL
|
||||
<OpenTextSummary
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
locale={locale}
|
||||
/>
|
||||
@@ -127,6 +127,7 @@ export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryL
|
||||
<MultipleChoiceSummary
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
environmentId={environment.id}
|
||||
surveyType={survey.type}
|
||||
survey={survey}
|
||||
setFilter={setFilter}
|
||||
@@ -158,26 +159,6 @@ export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryL
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.CSAT) {
|
||||
return (
|
||||
<CSATSummary
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.CES) {
|
||||
return (
|
||||
<CESSummary
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.Consent) {
|
||||
return (
|
||||
<ConsentSummary
|
||||
@@ -203,6 +184,7 @@ export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryL
|
||||
<DateElementSummary
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
locale={locale}
|
||||
/>
|
||||
@@ -213,6 +195,7 @@ export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryL
|
||||
<FileUploadSummary
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
locale={locale}
|
||||
/>
|
||||
@@ -220,7 +203,12 @@ export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryL
|
||||
}
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.Cal) {
|
||||
return (
|
||||
<CalSummary key={elementSummary.element.id} elementSummary={elementSummary} survey={survey} />
|
||||
<CalSummary
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.Matrix) {
|
||||
@@ -238,6 +226,7 @@ export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryL
|
||||
<AddressSummary
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
locale={locale}
|
||||
/>
|
||||
@@ -254,7 +243,12 @@ export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryL
|
||||
}
|
||||
if (elementSummary.type === "hiddenField") {
|
||||
return (
|
||||
<HiddenFieldsSummary key={elementSummary.id} elementSummary={elementSummary} locale={locale} />
|
||||
<HiddenFieldsSummary
|
||||
key={elementSummary.id}
|
||||
elementSummary={elementSummary}
|
||||
environment={environment}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.ContactInfo) {
|
||||
@@ -262,6 +256,7 @@ export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryL
|
||||
<ContactInfoSummary
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
locale={locale}
|
||||
/>
|
||||
+14
-16
@@ -2,8 +2,8 @@
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { InteractiveCard } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/interactive-card";
|
||||
import { StatCard } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/stat-card";
|
||||
import { InteractiveCard } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/interactive-card";
|
||||
import { StatCard } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/stat-card";
|
||||
import { cn } from "@/modules/ui/lib/utils";
|
||||
|
||||
interface SummaryMetadataProps {
|
||||
@@ -68,48 +68,46 @@ export const SummaryMetadata = ({
|
||||
<InteractiveCard
|
||||
key="impressions"
|
||||
tab="impressions"
|
||||
label={t("workspace.surveys.summary.impressions")}
|
||||
label={t("environments.surveys.summary.impressions")}
|
||||
percentage={null}
|
||||
value={displayCount === 0 ? <span>-</span> : displayCount}
|
||||
tooltipText={t("workspace.surveys.summary.impressions_tooltip")}
|
||||
tooltipText={t("environments.surveys.summary.impressions_tooltip")}
|
||||
isLoading={isLoading}
|
||||
onClick={() => handleTabChange("impressions")}
|
||||
isActive={tab === "impressions"}
|
||||
/>
|
||||
<StatCard
|
||||
label={t("workspace.surveys.summary.starts")}
|
||||
label={t("environments.surveys.summary.starts")}
|
||||
percentage={Math.round(startsPercentage) > 100 ? null : Math.round(startsPercentage)}
|
||||
value={totalResponses === 0 ? <span>-</span> : totalResponses}
|
||||
tooltipText={t("workspace.surveys.summary.starts_tooltip")}
|
||||
tooltipText={t("environments.surveys.summary.starts_tooltip")}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<StatCard
|
||||
label={t("workspace.surveys.summary.completed")}
|
||||
label={t("environments.surveys.summary.completed")}
|
||||
percentage={Math.round(completedPercentage) > 100 ? null : Math.round(completedPercentage)}
|
||||
value={completedResponses === 0 ? <span>-</span> : completedResponses}
|
||||
tooltipText={t("workspace.surveys.summary.completed_tooltip")}
|
||||
tooltipText={t("environments.surveys.summary.completed_tooltip")}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<InteractiveCard
|
||||
key="dropOffs"
|
||||
tab="dropOffs"
|
||||
label={t("workspace.surveys.summary.drop_offs")}
|
||||
label={t("environments.surveys.summary.drop_offs")}
|
||||
percentage={dropOffPercentage}
|
||||
value={dropoffCountValue}
|
||||
tooltipText={t("workspace.surveys.summary.drop_offs_tooltip")}
|
||||
tooltipText={t("environments.surveys.summary.drop_offs_tooltip")}
|
||||
isLoading={isLoading}
|
||||
onClick={() => handleTabChange("dropOffs")}
|
||||
isActive={tab === "dropOffs"}
|
||||
/>
|
||||
|
||||
<StatCard
|
||||
label={t("workspace.surveys.summary.time_to_complete")}
|
||||
label={t("environments.surveys.summary.time_to_complete")}
|
||||
percentage={null}
|
||||
value={ttcAverage === 0 ? <span>-</span> : `${formatTime(ttcAverage)}`}
|
||||
tooltipText={t("workspace.surveys.summary.ttc_survey_tooltip", {
|
||||
defaultValue: "Average time to complete the survey.",
|
||||
})}
|
||||
tooltipText={t("environments.surveys.summary.ttc_tooltip")}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
@@ -117,10 +115,10 @@ export const SummaryMetadata = ({
|
||||
<InteractiveCard
|
||||
key="quotas"
|
||||
tab="quotas"
|
||||
label={t("workspace.surveys.summary.quotas_completed")}
|
||||
label={t("environments.surveys.summary.quotas_completed")}
|
||||
percentage={quotasCompletedPercentage}
|
||||
value={quotasCompleted}
|
||||
tooltipText={t("workspace.surveys.summary.quotas_completed_tooltip")}
|
||||
tooltipText={t("environments.surveys.summary.quotas_completed_tooltip")}
|
||||
isLoading={isLoading}
|
||||
onClick={() => handleTabChange("quotas")}
|
||||
isActive={tab === "quotas"}
|
||||
+11
-6
@@ -5,17 +5,18 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TDisplayWithContact } from "@formbricks/types/displays";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import {
|
||||
getDisplaysWithContactAction,
|
||||
getSurveySummaryAction,
|
||||
} from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/actions";
|
||||
import { useResponseFilter } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import ScrollToTop from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/ScrollToTop";
|
||||
import { SummaryDropOffs } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs";
|
||||
import { SummaryImpressions } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/SummaryImpressions";
|
||||
import { CustomFilter } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/components/CustomFilter";
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import ScrollToTop from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ScrollToTop";
|
||||
import { SummaryDropOffs } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs";
|
||||
import { SummaryImpressions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryImpressions";
|
||||
import { CustomFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter";
|
||||
import { getFormattedFilters } from "@/app/lib/surveys/surveys";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||
@@ -44,6 +45,7 @@ const defaultSurveySummary: TSurveySummary = {
|
||||
};
|
||||
|
||||
interface SummaryPageProps {
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
surveyId: string;
|
||||
locale: TUserLocale;
|
||||
@@ -52,6 +54,7 @@ interface SummaryPageProps {
|
||||
}
|
||||
|
||||
export const SummaryPage = ({
|
||||
environment,
|
||||
survey,
|
||||
surveyId,
|
||||
locale,
|
||||
@@ -193,6 +196,7 @@ export const SummaryPage = ({
|
||||
isLoading={isDisplaysLoading}
|
||||
hasMore={hasMoreDisplays}
|
||||
displaysError={displaysError}
|
||||
environmentId={environment.id}
|
||||
locale={locale}
|
||||
onLoadMore={handleLoadMoreDisplays}
|
||||
onRetry={loadInitialDisplays}
|
||||
@@ -208,6 +212,7 @@ export const SummaryPage = ({
|
||||
summary={surveySummary.summary}
|
||||
responseCount={surveySummary.meta.totalResponses}
|
||||
survey={surveyMemoized}
|
||||
environment={environment}
|
||||
locale={locale}
|
||||
/>
|
||||
</>
|
||||
+31
-26
@@ -5,17 +5,18 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { useWorkspaceContext } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { SuccessMessage } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage";
|
||||
import { ShareSurveyModal } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal";
|
||||
import { SurveyStatusDropdown } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/components/SurveyStatusDropdown";
|
||||
import { useEnvironment } from "@/app/(app)/environments/[environmentId]/context/environment-context";
|
||||
import { SuccessMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage";
|
||||
import { ShareSurveyModal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal";
|
||||
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { EditPublicSurveyAlertDialog } from "@/modules/survey/components/edit-public-survey-alert-dialog";
|
||||
import { useSingleUseId } from "@/modules/survey/hooks/useSingleUseId";
|
||||
import { copySurveyToOtherWorkspaceAction } from "@/modules/survey/list/actions";
|
||||
import { copySurveyToOtherEnvironmentAction } from "@/modules/survey/list/actions";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal";
|
||||
import { IconBar } from "@/modules/ui/components/iconbar";
|
||||
@@ -23,6 +24,7 @@ import { resetSurveyAction } from "../actions";
|
||||
|
||||
interface SurveyAnalysisCTAProps {
|
||||
survey: TSurvey;
|
||||
environment: TEnvironment;
|
||||
isReadOnly: boolean;
|
||||
user: TUser;
|
||||
publicDomain: string;
|
||||
@@ -40,6 +42,7 @@ interface ModalState {
|
||||
|
||||
export const SurveyAnalysisCTA = ({
|
||||
survey,
|
||||
environment,
|
||||
isReadOnly,
|
||||
user,
|
||||
publicDomain,
|
||||
@@ -61,10 +64,10 @@ export const SurveyAnalysisCTA = ({
|
||||
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
|
||||
const { workspace } = useWorkspaceContext();
|
||||
const { project } = useEnvironment();
|
||||
const { refreshSingleUseId } = useSingleUseId(survey, isReadOnly);
|
||||
|
||||
const appSetupCompleted = survey.type === "app" && workspace.appSetupCompleted;
|
||||
const appSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
|
||||
|
||||
useEffect(() => {
|
||||
setModalState((prev) => ({
|
||||
@@ -90,13 +93,13 @@ export const SurveyAnalysisCTA = ({
|
||||
|
||||
const duplicateSurveyAndRoute = async (surveyId: string) => {
|
||||
setLoading(true);
|
||||
const duplicatedSurveyResponse = await copySurveyToOtherWorkspaceAction({
|
||||
const duplicatedSurveyResponse = await copySurveyToOtherEnvironmentAction({
|
||||
surveyId: surveyId,
|
||||
targetWorkspaceId: workspace.id,
|
||||
targetEnvironmentId: environment.id,
|
||||
});
|
||||
if (duplicatedSurveyResponse?.data) {
|
||||
toast.success(t("workspace.surveys.survey_duplicated_successfully"));
|
||||
router.push(`/workspaces/${workspace?.id}/surveys/${duplicatedSurveyResponse.data.id}/edit`);
|
||||
toast.success(t("environments.surveys.survey_duplicated_successfully"));
|
||||
router.push(`/environments/${environment.id}/surveys/${duplicatedSurveyResponse.data.id}/edit`);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(duplicatedSurveyResponse);
|
||||
toast.error(errorMessage);
|
||||
@@ -125,11 +128,11 @@ export const SurveyAnalysisCTA = ({
|
||||
setIsResetting(true);
|
||||
const result = await resetSurveyAction({
|
||||
surveyId: survey.id,
|
||||
workspaceId: workspace.id,
|
||||
projectId: project.id,
|
||||
});
|
||||
if (result?.data) {
|
||||
toast.success(
|
||||
t("workspace.surveys.summary.survey_reset_successfully", {
|
||||
t("environments.surveys.summary.survey_reset_successfully", {
|
||||
responseCount: result.data.deletedResponsesCount,
|
||||
displayCount: result.data.deletedDisplaysCount,
|
||||
})
|
||||
@@ -146,8 +149,8 @@ export const SurveyAnalysisCTA = ({
|
||||
const iconActions = [
|
||||
{
|
||||
icon: BellRing,
|
||||
tooltip: t("workspace.surveys.summary.configure_alerts"),
|
||||
onClick: () => router.push(`/workspaces/${workspace?.id}/settings/account/notifications`),
|
||||
tooltip: t("environments.surveys.summary.configure_alerts"),
|
||||
onClick: () => router.push(`/environments/${survey.environmentId}/settings/notifications`),
|
||||
isVisible: !isReadOnly,
|
||||
},
|
||||
{
|
||||
@@ -161,7 +164,7 @@ export const SurveyAnalysisCTA = ({
|
||||
},
|
||||
{
|
||||
icon: ListRestart,
|
||||
tooltip: t("workspace.surveys.summary.reset_survey"),
|
||||
tooltip: t("environments.surveys.summary.reset_survey"),
|
||||
onClick: () => setIsResetModalOpen(true),
|
||||
isVisible: !isReadOnly,
|
||||
},
|
||||
@@ -171,7 +174,7 @@ export const SurveyAnalysisCTA = ({
|
||||
onClick: () => {
|
||||
responseCount > 0
|
||||
? setIsCautionDialogOpen(true)
|
||||
: router.push(`/workspaces/${workspace?.id}/surveys/${survey.id}/edit`);
|
||||
: router.push(`/environments/${environment.id}/surveys/${survey.id}/edit`);
|
||||
},
|
||||
isVisible: !isReadOnly,
|
||||
},
|
||||
@@ -180,7 +183,7 @@ export const SurveyAnalysisCTA = ({
|
||||
return (
|
||||
<div className="hidden justify-end gap-x-1.5 sm:flex">
|
||||
{!isReadOnly && (appSetupCompleted || survey.type === "link") && survey.status !== "draft" && (
|
||||
<SurveyStatusDropdown survey={survey} />
|
||||
<SurveyStatusDropdown environment={environment} survey={survey} />
|
||||
)}
|
||||
|
||||
<IconBar actions={iconActions} />
|
||||
@@ -188,7 +191,7 @@ export const SurveyAnalysisCTA = ({
|
||||
onClick={() => {
|
||||
setModalState((prev) => ({ ...prev, share: true }));
|
||||
}}>
|
||||
{t("workspace.surveys.summary.share_survey")}
|
||||
{t("environments.surveys.summary.share_survey")}
|
||||
</Button>
|
||||
|
||||
{user && (
|
||||
@@ -209,10 +212,10 @@ export const SurveyAnalysisCTA = ({
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isReadOnly={isReadOnly}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
workspaceCustomScripts={workspace.customHeadScripts}
|
||||
projectCustomScripts={project.customHeadScripts}
|
||||
/>
|
||||
)}
|
||||
<SuccessMessage survey={survey} />
|
||||
<SuccessMessage environment={environment} survey={survey} />
|
||||
|
||||
{responseCount > 0 && (
|
||||
<EditPublicSurveyAlertDialog
|
||||
@@ -220,8 +223,10 @@ export const SurveyAnalysisCTA = ({
|
||||
setOpen={setIsCautionDialogOpen}
|
||||
isLoading={loading}
|
||||
primaryButtonAction={() => duplicateSurveyAndRoute(survey.id)}
|
||||
primaryButtonText={t("workspace.surveys.edit.caution_edit_duplicate")}
|
||||
secondaryButtonAction={() => router.push(`/workspaces/${workspace?.id}/surveys/${survey.id}/edit`)}
|
||||
primaryButtonText={t("environments.surveys.edit.caution_edit_duplicate")}
|
||||
secondaryButtonAction={() =>
|
||||
router.push(`/environments/${environment.id}/surveys/${survey.id}/edit`)
|
||||
}
|
||||
secondaryButtonText={t("common.edit")}
|
||||
/>
|
||||
)}
|
||||
@@ -229,9 +234,9 @@ export const SurveyAnalysisCTA = ({
|
||||
<ConfirmationModal
|
||||
open={isResetModalOpen}
|
||||
setOpen={setIsResetModalOpen}
|
||||
title={t("workspace.surveys.summary.delete_all_existing_responses_and_displays")}
|
||||
body={t("workspace.surveys.summary.reset_survey_warning")}
|
||||
buttonText={t("workspace.surveys.summary.reset_survey")}
|
||||
title={t("environments.surveys.summary.delete_all_existing_responses_and_displays")}
|
||||
body={t("environments.surveys.summary.reset_survey_warning")}
|
||||
buttonText={t("environments.surveys.summary.reset_survey")}
|
||||
onConfirm={handleResetSurvey}
|
||||
buttonVariant="destructive"
|
||||
buttonLoading={isResetting}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
import { BaseCard } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/base-card";
|
||||
import { BaseCard } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/base-card";
|
||||
|
||||
interface InteractiveCardProps {
|
||||
tab: "dropOffs" | "quotas" | "impressions";
|
||||
+53
-52
@@ -17,23 +17,23 @@ import { useTranslation } from "react-i18next";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { AnonymousLinksTab } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab";
|
||||
import { AppTab } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab";
|
||||
import { CustomHtmlTab } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/custom-html-tab";
|
||||
import { DynamicPopupTab } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab";
|
||||
import { EmailTab } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab";
|
||||
import { LinkSettingsTab } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/link-settings-tab";
|
||||
import { PersonalLinksTab } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab";
|
||||
import { PrettyUrlTab } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/pretty-url-tab";
|
||||
import { QRCodeTab } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab";
|
||||
import { SocialMediaTab } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab";
|
||||
import { TabContainer } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container";
|
||||
import { WebsiteEmbedTab } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/website-embed-tab";
|
||||
import { AnonymousLinksTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab";
|
||||
import { AppTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab";
|
||||
import { CustomHtmlTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/custom-html-tab";
|
||||
import { DynamicPopupTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab";
|
||||
import { EmailTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab";
|
||||
import { LinkSettingsTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/link-settings-tab";
|
||||
import { PersonalLinksTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab";
|
||||
import { PrettyUrlTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/pretty-url-tab";
|
||||
import { QRCodeTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab";
|
||||
import { SocialMediaTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab";
|
||||
import { TabContainer } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container";
|
||||
import { WebsiteEmbedTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/website-embed-tab";
|
||||
import {
|
||||
LinkTabsType,
|
||||
ShareSettingsType,
|
||||
ShareViaType,
|
||||
} from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/types/share";
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/types/share";
|
||||
import { getSurveyUrl } from "@/modules/analysis/utils";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/modules/ui/components/dialog";
|
||||
import { ShareView } from "./shareEmbedModal/share-view";
|
||||
@@ -53,7 +53,7 @@ interface ShareSurveyModalProps {
|
||||
isFormbricksCloud: boolean;
|
||||
isReadOnly: boolean;
|
||||
isStorageConfigured: boolean;
|
||||
workspaceCustomScripts?: string | null;
|
||||
projectCustomScripts?: string | null;
|
||||
}
|
||||
|
||||
export const ShareSurveyModal = ({
|
||||
@@ -68,8 +68,9 @@ export const ShareSurveyModal = ({
|
||||
isFormbricksCloud,
|
||||
isReadOnly,
|
||||
isStorageConfigured,
|
||||
workspaceCustomScripts,
|
||||
projectCustomScripts,
|
||||
}: ShareSurveyModalProps) => {
|
||||
const environmentId = survey.environmentId;
|
||||
const [surveyUrl, setSurveyUrl] = useState<string>(getSurveyUrl(survey, publicDomain, "default"));
|
||||
const [showView, setShowView] = useState<ModalView>(modalView);
|
||||
const { email } = user;
|
||||
@@ -79,10 +80,10 @@ export const ShareSurveyModal = ({
|
||||
{
|
||||
id: ShareViaType.ANON_LINKS,
|
||||
type: LinkTabsType.SHARE_VIA,
|
||||
label: t("workspace.surveys.share.anonymous_links.nav_title"),
|
||||
label: t("environments.surveys.share.anonymous_links.nav_title"),
|
||||
icon: Link2Icon,
|
||||
title: t("workspace.surveys.share.anonymous_links.nav_title"),
|
||||
description: t("workspace.surveys.share.anonymous_links.description"),
|
||||
title: t("environments.surveys.share.anonymous_links.nav_title"),
|
||||
description: t("environments.surveys.share.anonymous_links.description"),
|
||||
componentType: AnonymousLinksTab,
|
||||
componentProps: {
|
||||
survey,
|
||||
@@ -96,13 +97,13 @@ export const ShareSurveyModal = ({
|
||||
{
|
||||
id: ShareViaType.PERSONAL_LINKS,
|
||||
type: LinkTabsType.SHARE_VIA,
|
||||
label: t("workspace.surveys.share.personal_links.nav_title"),
|
||||
label: t("environments.surveys.share.personal_links.nav_title"),
|
||||
icon: UserIcon,
|
||||
title: t("workspace.surveys.share.personal_links.nav_title"),
|
||||
description: t("workspace.surveys.share.personal_links.description"),
|
||||
title: t("environments.surveys.share.personal_links.nav_title"),
|
||||
description: t("environments.surveys.share.personal_links.description"),
|
||||
componentType: PersonalLinksTab,
|
||||
componentProps: {
|
||||
workspaceId: survey.workspaceId,
|
||||
environmentId,
|
||||
surveyId: survey.id,
|
||||
segments,
|
||||
isContactsEnabled,
|
||||
@@ -113,10 +114,10 @@ export const ShareSurveyModal = ({
|
||||
{
|
||||
id: ShareViaType.WEBSITE_EMBED,
|
||||
type: LinkTabsType.SHARE_VIA,
|
||||
label: t("workspace.surveys.share.embed_on_website.nav_title"),
|
||||
label: t("environments.surveys.share.embed_on_website.nav_title"),
|
||||
icon: Code2Icon,
|
||||
title: t("workspace.surveys.share.embed_on_website.nav_title"),
|
||||
description: t("workspace.surveys.share.embed_on_website.description"),
|
||||
title: t("environments.surveys.share.embed_on_website.nav_title"),
|
||||
description: t("environments.surveys.share.embed_on_website.description"),
|
||||
componentType: WebsiteEmbedTab,
|
||||
componentProps: { surveyUrl },
|
||||
disabled: survey.singleUse?.enabled,
|
||||
@@ -124,10 +125,10 @@ export const ShareSurveyModal = ({
|
||||
{
|
||||
id: ShareViaType.EMAIL,
|
||||
type: LinkTabsType.SHARE_VIA,
|
||||
label: t("workspace.surveys.share.send_email.nav_title"),
|
||||
label: t("environments.surveys.share.send_email.nav_title"),
|
||||
icon: MailIcon,
|
||||
title: t("workspace.surveys.share.send_email.nav_title"),
|
||||
description: t("workspace.surveys.share.send_email.description"),
|
||||
title: t("environments.surveys.share.send_email.nav_title"),
|
||||
description: t("environments.surveys.share.send_email.description"),
|
||||
componentType: EmailTab,
|
||||
componentProps: { surveyId: survey.id, email },
|
||||
disabled: survey.singleUse?.enabled,
|
||||
@@ -135,10 +136,10 @@ export const ShareSurveyModal = ({
|
||||
{
|
||||
id: ShareViaType.SOCIAL_MEDIA,
|
||||
type: LinkTabsType.SHARE_VIA,
|
||||
label: t("workspace.surveys.share.social_media.title"),
|
||||
label: t("environments.surveys.share.social_media.title"),
|
||||
icon: Share2Icon,
|
||||
title: t("workspace.surveys.share.social_media.title"),
|
||||
description: t("workspace.surveys.share.social_media.description"),
|
||||
title: t("environments.surveys.share.social_media.title"),
|
||||
description: t("environments.surveys.share.social_media.description"),
|
||||
componentType: SocialMediaTab,
|
||||
componentProps: { surveyUrl, surveyTitle: survey.name },
|
||||
disabled: survey.singleUse?.enabled,
|
||||
@@ -146,10 +147,10 @@ export const ShareSurveyModal = ({
|
||||
{
|
||||
id: ShareViaType.QR_CODE,
|
||||
type: LinkTabsType.SHARE_VIA,
|
||||
label: t("workspace.surveys.summary.qr_code"),
|
||||
label: t("environments.surveys.summary.qr_code"),
|
||||
icon: QrCodeIcon,
|
||||
title: t("workspace.surveys.summary.qr_code"),
|
||||
description: t("workspace.surveys.summary.qr_code_description"),
|
||||
title: t("environments.surveys.summary.qr_code"),
|
||||
description: t("environments.surveys.summary.qr_code_description"),
|
||||
componentType: QRCodeTab,
|
||||
componentProps: { surveyUrl },
|
||||
disabled: survey.singleUse?.enabled,
|
||||
@@ -157,42 +158,42 @@ export const ShareSurveyModal = ({
|
||||
{
|
||||
id: ShareViaType.DYNAMIC_POPUP,
|
||||
type: LinkTabsType.SHARE_VIA,
|
||||
label: t("workspace.surveys.share.dynamic_popup.nav_title"),
|
||||
label: t("environments.surveys.share.dynamic_popup.nav_title"),
|
||||
icon: SquareStack,
|
||||
title: t("workspace.surveys.share.dynamic_popup.nav_title"),
|
||||
description: t("workspace.surveys.share.dynamic_popup.description"),
|
||||
title: t("environments.surveys.share.dynamic_popup.nav_title"),
|
||||
description: t("environments.surveys.share.dynamic_popup.description"),
|
||||
componentType: DynamicPopupTab,
|
||||
componentProps: { surveyId: survey.id },
|
||||
componentProps: { environmentId, surveyId: survey.id },
|
||||
},
|
||||
{
|
||||
id: ShareSettingsType.LINK_SETTINGS,
|
||||
type: LinkTabsType.SHARE_SETTING,
|
||||
label: t("workspace.surveys.share.link_settings.title"),
|
||||
label: t("environments.surveys.share.link_settings.title"),
|
||||
icon: Settings,
|
||||
title: t("workspace.surveys.share.link_settings.title"),
|
||||
description: t("workspace.surveys.share.link_settings.description"),
|
||||
title: t("environments.surveys.share.link_settings.title"),
|
||||
description: t("environments.surveys.share.link_settings.description"),
|
||||
componentType: LinkSettingsTab,
|
||||
componentProps: { isReadOnly, locale: user.locale, isStorageConfigured },
|
||||
},
|
||||
{
|
||||
id: ShareSettingsType.PRETTY_URL,
|
||||
type: LinkTabsType.SHARE_SETTING,
|
||||
label: t("workspace.surveys.share.pretty_url.title"),
|
||||
label: t("environments.surveys.share.pretty_url.title"),
|
||||
icon: Link2Icon,
|
||||
title: t("workspace.surveys.share.pretty_url.title"),
|
||||
description: t("workspace.surveys.share.pretty_url.description"),
|
||||
title: t("environments.surveys.share.pretty_url.title"),
|
||||
description: t("environments.surveys.share.pretty_url.description"),
|
||||
componentType: PrettyUrlTab,
|
||||
componentProps: { publicDomain, isReadOnly },
|
||||
},
|
||||
{
|
||||
id: ShareSettingsType.CUSTOM_HTML,
|
||||
type: LinkTabsType.SHARE_SETTING,
|
||||
label: t("workspace.surveys.share.custom_html.nav_title"),
|
||||
label: t("environments.surveys.share.custom_html.nav_title"),
|
||||
icon: CodeIcon,
|
||||
title: t("workspace.surveys.share.custom_html.nav_title"),
|
||||
description: t("workspace.surveys.share.custom_html.description"),
|
||||
title: t("environments.surveys.share.custom_html.nav_title"),
|
||||
description: t("environments.surveys.share.custom_html.description"),
|
||||
componentType: CustomHtmlTab,
|
||||
componentProps: { workspaceCustomScripts, isReadOnly },
|
||||
componentProps: { projectCustomScripts, isReadOnly },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -209,13 +210,13 @@ export const ShareSurveyModal = ({
|
||||
user.locale,
|
||||
surveyUrl,
|
||||
isReadOnly,
|
||||
survey.workspaceId,
|
||||
environmentId,
|
||||
segments,
|
||||
isContactsEnabled,
|
||||
isFormbricksCloud,
|
||||
email,
|
||||
isStorageConfigured,
|
||||
workspaceCustomScripts,
|
||||
projectCustomScripts,
|
||||
]);
|
||||
|
||||
const getDefaultActiveId = useCallback(() => {
|
||||
@@ -283,8 +284,8 @@ export const ShareSurveyModal = ({
|
||||
return (
|
||||
<div className={`h-full w-full rounded-lg bg-slate-50 p-6`}>
|
||||
<TabContainer
|
||||
title={t("workspace.surveys.summary.in_app.title")}
|
||||
description={t("workspace.surveys.summary.in_app.description")}>
|
||||
title={t("environments.surveys.summary.in_app.title")}
|
||||
description={t("environments.surveys.summary.in_app.description")}>
|
||||
<AppTab />
|
||||
</TabContainer>
|
||||
</div>
|
||||
+24
-22
@@ -7,9 +7,9 @@ import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { updateSingleUseLinksAction } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/actions";
|
||||
import { DisableLinkModal } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/disable-link-modal";
|
||||
import { DocumentationLinks } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links";
|
||||
import { updateSingleUseLinksAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions";
|
||||
import { DisableLinkModal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/disable-link-modal";
|
||||
import { DocumentationLinks } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links";
|
||||
import { ShareSurveyLink } from "@/modules/analysis/components/ShareSurveyLink";
|
||||
import { generateSingleUseIdsAction } from "@/modules/survey/list/actions";
|
||||
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||
@@ -70,7 +70,7 @@ export const AnonymousLinksTab = ({
|
||||
try {
|
||||
const updatedSurveyResponse = await updateSingleUseLinksAction({
|
||||
surveyId: survey.id,
|
||||
workspaceId: survey.workspaceId,
|
||||
environmentId: survey.environmentId,
|
||||
isSingleUse,
|
||||
isSingleUseEncryption,
|
||||
});
|
||||
@@ -206,9 +206,9 @@ export const AnonymousLinksTab = ({
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error(t("workspace.surveys.share.anonymous_links.generate_links_error"));
|
||||
toast.error(t("environments.surveys.share.anonymous_links.generate_links_error"));
|
||||
} catch (error) {
|
||||
toast.error(t("workspace.surveys.share.anonymous_links.generate_links_error"));
|
||||
toast.error(t("environments.surveys.share.anonymous_links.generate_links_error"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -220,8 +220,8 @@ export const AnonymousLinksTab = ({
|
||||
htmlId="multi-use-link-switch"
|
||||
isChecked={isMultiUseLink}
|
||||
onToggle={handleMultiUseToggle}
|
||||
title={t("workspace.surveys.share.anonymous_links.multi_use_link")}
|
||||
description={t("workspace.surveys.share.anonymous_links.multi_use_link_description")}
|
||||
title={t("environments.surveys.share.anonymous_links.multi_use_link")}
|
||||
description={t("environments.surveys.share.anonymous_links.multi_use_link_description")}
|
||||
customContainerClass="pl-1 pr-0 py-0"
|
||||
childBorder>
|
||||
<div className="flex w-full flex-col gap-4 overflow-hidden bg-white p-4">
|
||||
@@ -237,10 +237,12 @@ export const AnonymousLinksTab = ({
|
||||
<div className="w-full">
|
||||
<Alert variant="info" size="default">
|
||||
<AlertTitle>
|
||||
{t("workspace.surveys.share.anonymous_links.multi_use_powers_other_channels_title")}
|
||||
{t("environments.surveys.share.anonymous_links.multi_use_powers_other_channels_title")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("workspace.surveys.share.anonymous_links.multi_use_powers_other_channels_description")}
|
||||
{t(
|
||||
"environments.surveys.share.anonymous_links.multi_use_powers_other_channels_description"
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
@@ -251,8 +253,8 @@ export const AnonymousLinksTab = ({
|
||||
htmlId="single-use-link-switch"
|
||||
isChecked={isSingleUseLink}
|
||||
onToggle={handleSingleUseToggle}
|
||||
title={t("workspace.surveys.share.anonymous_links.single_use_link")}
|
||||
description={t("workspace.surveys.share.anonymous_links.single_use_link_description")}
|
||||
title={t("environments.surveys.share.anonymous_links.single_use_link")}
|
||||
description={t("environments.surveys.share.anonymous_links.single_use_link_description")}
|
||||
customContainerClass="pl-1 pr-0 py-0"
|
||||
childBorder>
|
||||
<div className="flex w-full flex-col gap-4 bg-white p-4">
|
||||
@@ -260,8 +262,8 @@ export const AnonymousLinksTab = ({
|
||||
htmlId="single-use-encryption-switch"
|
||||
isChecked={singleUseEncryption}
|
||||
onToggle={handleSingleUseEncryptionToggle}
|
||||
title={t("workspace.surveys.share.anonymous_links.url_encryption_label")}
|
||||
description={t("workspace.surveys.share.anonymous_links.url_encryption_description")}
|
||||
title={t("environments.surveys.share.anonymous_links.url_encryption_label")}
|
||||
description={t("environments.surveys.share.anonymous_links.url_encryption_description")}
|
||||
customContainerClass="pl-1 pr-0 py-0"
|
||||
/>
|
||||
|
||||
@@ -269,10 +271,10 @@ export const AnonymousLinksTab = ({
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
<Alert variant="info" size="default">
|
||||
<AlertTitle>
|
||||
{t("workspace.surveys.share.anonymous_links.custom_single_use_id_title")}
|
||||
{t("environments.surveys.share.anonymous_links.custom_single_use_id_title")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("workspace.surveys.share.anonymous_links.custom_single_use_id_description")}
|
||||
{t("environments.surveys.share.anonymous_links.custom_single_use_id_description")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -298,7 +300,7 @@ export const AnonymousLinksTab = ({
|
||||
{singleUseEncryption && (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<h3 className="text-sm font-medium text-slate-900">
|
||||
{t("workspace.surveys.share.anonymous_links.number_of_links_label")}
|
||||
{t("environments.surveys.share.anonymous_links.number_of_links_label")}
|
||||
</h3>
|
||||
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
@@ -323,7 +325,7 @@ export const AnonymousLinksTab = ({
|
||||
</div>
|
||||
|
||||
<span className="text-sm text-slate-50">
|
||||
{t("workspace.surveys.share.anonymous_links.generate_and_download_links")}
|
||||
{t("environments.surveys.share.anonymous_links.generate_and_download_links")}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -337,19 +339,19 @@ export const AnonymousLinksTab = ({
|
||||
<DocumentationLinks
|
||||
links={[
|
||||
{
|
||||
title: t("workspace.surveys.share.anonymous_links.single_use_links"),
|
||||
title: t("environments.surveys.share.anonymous_links.single_use_links"),
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/single-use-links",
|
||||
},
|
||||
{
|
||||
title: t("workspace.surveys.share.anonymous_links.data_prefilling"),
|
||||
title: t("environments.surveys.share.anonymous_links.data_prefilling"),
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/data-prefilling",
|
||||
},
|
||||
{
|
||||
title: t("workspace.surveys.share.anonymous_links.source_tracking"),
|
||||
title: t("environments.surveys.share.anonymous_links.source_tracking"),
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/source-tracking",
|
||||
},
|
||||
{
|
||||
title: t("workspace.surveys.share.anonymous_links.custom_start_point"),
|
||||
title: t("environments.surveys.share.anonymous_links.custom_start_point"),
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/start-at-block",
|
||||
},
|
||||
]}
|
||||
+51
-48
@@ -13,8 +13,8 @@ import { ReactNode, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TActionClass } from "@formbricks/types/action-classes";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { useWorkspaceContext } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { useSurvey } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/context/survey-context";
|
||||
import { useEnvironment } from "@/app/(app)/environments/[environmentId]/context/environment-context";
|
||||
import { useSurvey } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context";
|
||||
import { Alert, AlertButton, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { H4, InlineSmall, Small } from "@/modules/ui/components/typography";
|
||||
import { DocumentationLinksSection } from "./documentation-links-section";
|
||||
@@ -22,41 +22,41 @@ import { DocumentationLinksSection } from "./documentation-links-section";
|
||||
const createDocumentationLinks = (t: ReturnType<typeof useTranslation>["t"]) => [
|
||||
{
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides#html",
|
||||
title: t("workspace.surveys.summary.in_app.html_embed"),
|
||||
title: t("environments.surveys.summary.in_app.html_embed"),
|
||||
},
|
||||
{
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides#react-js",
|
||||
title: t("workspace.surveys.summary.in_app.javascript_sdk"),
|
||||
title: t("environments.surveys.summary.in_app.javascript_sdk"),
|
||||
},
|
||||
{
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides#swift",
|
||||
title: t("workspace.surveys.summary.in_app.ios_sdk"),
|
||||
title: t("environments.surveys.summary.in_app.ios_sdk"),
|
||||
},
|
||||
{
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides#android",
|
||||
title: t("workspace.surveys.summary.in_app.kotlin_sdk"),
|
||||
title: t("environments.surveys.summary.in_app.kotlin_sdk"),
|
||||
},
|
||||
{
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides#react-native",
|
||||
title: t("workspace.surveys.summary.in_app.react_native_sdk"),
|
||||
title: t("environments.surveys.summary.in_app.react_native_sdk"),
|
||||
},
|
||||
];
|
||||
|
||||
const createNoCodeConfigType = (t: ReturnType<typeof useTranslation>["t"]) => ({
|
||||
click: t("workspace.actions.click"),
|
||||
pageView: t("workspace.actions.page_view"),
|
||||
exitIntent: t("workspace.actions.exit_intent"),
|
||||
fiftyPercentScroll: t("workspace.actions.fifty_percent_scroll"),
|
||||
pageDwell: t("workspace.actions.time_on_page"),
|
||||
click: t("environments.actions.click"),
|
||||
pageView: t("environments.actions.page_view"),
|
||||
exitIntent: t("environments.actions.exit_intent"),
|
||||
fiftyPercentScroll: t("environments.actions.fifty_percent_scroll"),
|
||||
pageDwell: t("environments.actions.time_on_page"),
|
||||
});
|
||||
|
||||
const formatRecontactDaysString = (days: number, t: ReturnType<typeof useTranslation>["t"]) => {
|
||||
if (days === 0) {
|
||||
return t("workspace.surveys.summary.in_app.display_criteria.time_based_always");
|
||||
return t("environments.surveys.summary.in_app.display_criteria.time_based_always");
|
||||
} else if (days === 1) {
|
||||
return `${days} ${t("workspace.surveys.summary.in_app.display_criteria.time_based_day")}`;
|
||||
return `${days} ${t("environments.surveys.summary.in_app.display_criteria.time_based_day")}`;
|
||||
} else {
|
||||
return `${days} ${t("workspace.surveys.summary.in_app.display_criteria.time_based_days")}`;
|
||||
return `${days} ${t("environments.surveys.summary.in_app.display_criteria.time_based_days")}`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,7 +88,7 @@ const DisplayCriteriaItem = ({ icon, title, titleSuffix, description }: DisplayC
|
||||
|
||||
export const AppTab = () => {
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspaceContext();
|
||||
const { environment, project } = useEnvironment();
|
||||
const { survey } = useSurvey();
|
||||
|
||||
const documentationLinks = useMemo(() => createDocumentationLinks(t), [t]);
|
||||
@@ -98,25 +98,25 @@ export const AppTab = () => {
|
||||
if (survey.recontactDays !== null) {
|
||||
return formatRecontactDaysString(survey.recontactDays, t);
|
||||
}
|
||||
if (workspace.recontactDays !== null) {
|
||||
return formatRecontactDaysString(workspace.recontactDays, t);
|
||||
if (project.recontactDays !== null) {
|
||||
return formatRecontactDaysString(project.recontactDays, t);
|
||||
}
|
||||
return t("workspace.surveys.summary.in_app.display_criteria.time_based_always");
|
||||
return t("environments.surveys.summary.in_app.display_criteria.time_based_always");
|
||||
};
|
||||
|
||||
const displayOption = () => {
|
||||
if (survey.displayOption === "displayOnce") {
|
||||
return t("workspace.surveys.edit.show_only_once");
|
||||
return t("environments.surveys.edit.show_only_once");
|
||||
} else if (survey.displayOption === "displayMultiple") {
|
||||
return t("workspace.surveys.edit.until_they_submit_a_response");
|
||||
return t("environments.surveys.edit.until_they_submit_a_response");
|
||||
} else if (survey.displayOption === "respondMultiple") {
|
||||
return t("workspace.surveys.edit.keep_showing_while_conditions_match");
|
||||
return t("environments.surveys.edit.keep_showing_while_conditions_match");
|
||||
} else if (survey.displayOption === "displaySome") {
|
||||
return t("workspace.surveys.edit.show_multiple_times");
|
||||
return t("environments.surveys.edit.show_multiple_times");
|
||||
}
|
||||
|
||||
// Default fallback for undefined or unexpected displayOption values
|
||||
return t("workspace.surveys.edit.show_only_once");
|
||||
return t("environments.surveys.edit.show_only_once");
|
||||
};
|
||||
|
||||
const getTriggerDescription = (
|
||||
@@ -124,7 +124,7 @@ export const AppTab = () => {
|
||||
noCodeConfigTypeParam: ReturnType<typeof createNoCodeConfigType>
|
||||
) => {
|
||||
if (actionClass.type === "code") {
|
||||
return `(${t("workspace.surveys.summary.in_app.display_criteria.code_trigger")})`;
|
||||
return `(${t("environments.surveys.summary.in_app.display_criteria.code_trigger")})`;
|
||||
} else {
|
||||
const configType = actionClass.noCodeConfig?.type;
|
||||
let configTypeLabel = "unknown";
|
||||
@@ -135,36 +135,36 @@ export const AppTab = () => {
|
||||
configTypeLabel = configType;
|
||||
}
|
||||
|
||||
return `(${t("workspace.surveys.summary.in_app.display_criteria.no_code_trigger")}, ${configTypeLabel})`;
|
||||
return `(${t("environments.surveys.summary.in_app.display_criteria.no_code_trigger")}, ${configTypeLabel})`;
|
||||
}
|
||||
};
|
||||
|
||||
const getSegmentTitle = (segment: TSegment | null) => {
|
||||
if (segment?.filters?.length && segment.filters.length > 0) {
|
||||
return segment.isPrivate
|
||||
? t("workspace.surveys.summary.in_app.display_criteria.targeted")
|
||||
? t("environments.surveys.summary.in_app.display_criteria.targeted")
|
||||
: segment.title;
|
||||
}
|
||||
return t("workspace.surveys.summary.in_app.display_criteria.everyone");
|
||||
return t("environments.surveys.summary.in_app.display_criteria.everyone");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-between space-y-6 pb-4">
|
||||
<div className="flex flex-col space-y-6">
|
||||
<Alert variant={workspace.appSetupCompleted ? "success" : "warning"} size="default">
|
||||
<Alert variant={environment.appSetupCompleted ? "success" : "warning"} size="default">
|
||||
<AlertTitle>
|
||||
{workspace.appSetupCompleted
|
||||
? t("workspace.surveys.summary.in_app.connection_title")
|
||||
: t("workspace.surveys.summary.in_app.no_connection_title")}
|
||||
{environment.appSetupCompleted
|
||||
? t("environments.surveys.summary.in_app.connection_title")
|
||||
: t("environments.surveys.summary.in_app.no_connection_title")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{workspace.appSetupCompleted
|
||||
? t("workspace.surveys.summary.in_app.connection_description")
|
||||
: t("workspace.surveys.summary.in_app.no_connection_description")}
|
||||
{environment.appSetupCompleted
|
||||
? t("environments.surveys.summary.in_app.connection_description")
|
||||
: t("environments.surveys.summary.in_app.no_connection_description")}
|
||||
</AlertDescription>
|
||||
{!workspace.appSetupCompleted && (
|
||||
{!environment.appSetupCompleted && (
|
||||
<AlertButton asChild>
|
||||
<Link href={`/workspaces/${workspace?.id}/settings/workspace/app-connection`}>
|
||||
<Link href={`/environments/${environment.id}/workspace/app-connection`}>
|
||||
{t("common.connect_formbricks")}
|
||||
</Link>
|
||||
</AlertButton>
|
||||
@@ -172,7 +172,7 @@ export const AppTab = () => {
|
||||
</Alert>
|
||||
|
||||
<div className="flex flex-col space-y-3">
|
||||
<H4>{t("workspace.surveys.summary.in_app.display_criteria")}</H4>
|
||||
<H4>{t("environments.surveys.summary.in_app.display_criteria")}</H4>
|
||||
<div
|
||||
className={
|
||||
"flex w-full flex-col space-y-4 rounded-xl border border-slate-200 bg-white p-3 text-left shadow-sm"
|
||||
@@ -182,15 +182,15 @@ export const AppTab = () => {
|
||||
title={waitTime()}
|
||||
titleSuffix={
|
||||
survey.recontactDays !== null
|
||||
? `(${t("workspace.surveys.summary.in_app.display_criteria.overwritten")})`
|
||||
? `(${t("environments.surveys.summary.in_app.display_criteria.overwritten")})`
|
||||
: undefined
|
||||
}
|
||||
description={t("workspace.surveys.summary.in_app.display_criteria.time_based_description")}
|
||||
description={t("environments.surveys.summary.in_app.display_criteria.time_based_description")}
|
||||
/>
|
||||
<DisplayCriteriaItem
|
||||
icon={<UsersIcon className="h-4 w-4" />}
|
||||
title={getSegmentTitle(survey.segment)}
|
||||
description={t("workspace.surveys.summary.in_app.display_criteria.audience_description")}
|
||||
description={t("environments.surveys.summary.in_app.display_criteria.audience_description")}
|
||||
/>
|
||||
{survey.triggers.map((trigger) => (
|
||||
<DisplayCriteriaItem
|
||||
@@ -204,31 +204,34 @@ export const AppTab = () => {
|
||||
}
|
||||
title={trigger.actionClass.name}
|
||||
titleSuffix={getTriggerDescription(trigger.actionClass, noCodeConfigType)}
|
||||
description={t("workspace.surveys.summary.in_app.display_criteria.trigger_description")}
|
||||
description={t("environments.surveys.summary.in_app.display_criteria.trigger_description")}
|
||||
/>
|
||||
))}
|
||||
{survey.displayPercentage !== null && survey.displayPercentage > 0 && (
|
||||
<DisplayCriteriaItem
|
||||
icon={<PercentIcon className="h-4 w-4" />}
|
||||
title={t("workspace.surveys.summary.in_app.display_criteria.randomizer", {
|
||||
percentage: survey.displayPercentage,
|
||||
})}
|
||||
description={t("workspace.surveys.summary.in_app.display_criteria.randomizer_description", {
|
||||
title={t("environments.surveys.summary.in_app.display_criteria.randomizer", {
|
||||
percentage: survey.displayPercentage,
|
||||
})}
|
||||
description={t(
|
||||
"environments.surveys.summary.in_app.display_criteria.randomizer_description",
|
||||
{
|
||||
percentage: survey.displayPercentage,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<DisplayCriteriaItem
|
||||
icon={<Repeat1Icon className="h-4 w-4" />}
|
||||
title={displayOption()}
|
||||
description={t("workspace.surveys.summary.in_app.display_criteria.recontact_description")}
|
||||
description={t("environments.surveys.summary.in_app.display_criteria.recontact_description")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DocumentationLinksSection
|
||||
title={t("workspace.surveys.summary.in_app.documentation_title")}
|
||||
title={t("environments.surveys.summary.in_app.documentation_title")}
|
||||
links={documentationLinks}
|
||||
/>
|
||||
</div>
|
||||
+20
-18
@@ -6,7 +6,7 @@ import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { useSurvey } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/context/survey-context";
|
||||
import { useSurvey } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { updateSurveyAction } from "@/modules/survey/editor/actions";
|
||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import { TabToggle } from "@/modules/ui/components/tab-toggle";
|
||||
|
||||
interface CustomHtmlTabProps {
|
||||
workspaceCustomScripts: string | null | undefined;
|
||||
projectCustomScripts: string | null | undefined;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ interface CustomHtmlFormData {
|
||||
customHeadScriptsMode: TSurvey["customHeadScriptsMode"];
|
||||
}
|
||||
|
||||
export const CustomHtmlTab = ({ workspaceCustomScripts, isReadOnly }: CustomHtmlTabProps) => {
|
||||
export const CustomHtmlTab = ({ projectCustomScripts, isReadOnly }: CustomHtmlTabProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { survey } = useSurvey();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -67,7 +67,7 @@ export const CustomHtmlTab = ({ workspaceCustomScripts, isReadOnly }: CustomHtml
|
||||
const result = await updateSurveyAction(updatedSurvey);
|
||||
|
||||
if (result?.data) {
|
||||
toast.success(t("workspace.surveys.share.custom_html.saved_successfully"));
|
||||
toast.success(t("environments.surveys.share.custom_html.saved_successfully"));
|
||||
reset(data);
|
||||
} else {
|
||||
toast.error(t("common.something_went_wrong_please_try_again"));
|
||||
@@ -82,12 +82,12 @@ export const CustomHtmlTab = ({ workspaceCustomScripts, isReadOnly }: CustomHtml
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
{/* Mode Toggle */}
|
||||
<div className="space-y-2">
|
||||
<FormLabel>{t("workspace.surveys.share.custom_html.script_mode")}</FormLabel>
|
||||
<FormLabel>{t("environments.surveys.share.custom_html.script_mode")}</FormLabel>
|
||||
<TabToggle
|
||||
id="custom-scripts-mode"
|
||||
options={[
|
||||
{ value: "add", label: t("workspace.surveys.share.custom_html.add_to_workspace") },
|
||||
{ value: "replace", label: t("workspace.surveys.share.custom_html.replace_workspace") },
|
||||
{ value: "add", label: t("environments.surveys.share.custom_html.add_to_workspace") },
|
||||
{ value: "replace", label: t("environments.surveys.share.custom_html.replace_workspace") },
|
||||
]}
|
||||
defaultSelected={scriptsMode ?? "add"}
|
||||
onChange={(value) => setValue("customHeadScriptsMode", value, { shouldDirty: true })}
|
||||
@@ -95,27 +95,27 @@ export const CustomHtmlTab = ({ workspaceCustomScripts, isReadOnly }: CustomHtml
|
||||
/>
|
||||
<p className="text-sm text-slate-500">
|
||||
{scriptsMode === "add"
|
||||
? t("workspace.surveys.share.custom_html.add_mode_description")
|
||||
: t("workspace.surveys.share.custom_html.replace_mode_description")}
|
||||
? t("environments.surveys.share.custom_html.add_mode_description")
|
||||
: t("environments.surveys.share.custom_html.replace_mode_description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Workspace Scripts Preview */}
|
||||
{workspaceCustomScripts && (
|
||||
{projectCustomScripts && (
|
||||
<div className={scriptsMode === "replace" ? "opacity-50" : ""}>
|
||||
<FormLabel>{t("workspace.surveys.share.custom_html.workspace_scripts_label")}</FormLabel>
|
||||
<FormLabel>{t("environments.surveys.share.custom_html.workspace_scripts_label")}</FormLabel>
|
||||
<div className="mt-2 max-h-32 overflow-auto rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||
<pre className="whitespace-pre-wrap font-mono text-xs text-slate-600">
|
||||
{workspaceCustomScripts}
|
||||
{projectCustomScripts}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!workspaceCustomScripts && (
|
||||
{!projectCustomScripts && (
|
||||
<div className="rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||
<p className="text-sm text-slate-500">
|
||||
{t("workspace.surveys.share.custom_html.no_workspace_scripts")}
|
||||
{t("environments.surveys.share.custom_html.no_workspace_scripts")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -126,14 +126,14 @@ export const CustomHtmlTab = ({ workspaceCustomScripts, isReadOnly }: CustomHtml
|
||||
name="customHeadScripts"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("workspace.surveys.share.custom_html.survey_scripts_label")}</FormLabel>
|
||||
<FormLabel>{t("environments.surveys.share.custom_html.survey_scripts_label")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("workspace.surveys.share.custom_html.survey_scripts_description")}
|
||||
{t("environments.surveys.share.custom_html.survey_scripts_description")}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<textarea
|
||||
rows={8}
|
||||
placeholder={t("workspace.surveys.share.custom_html.placeholder")}
|
||||
placeholder={t("environments.surveys.share.custom_html.placeholder")}
|
||||
className={cn(
|
||||
"flex w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:border-brand-dark focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
)}
|
||||
@@ -152,7 +152,9 @@ export const CustomHtmlTab = ({ workspaceCustomScripts, isReadOnly }: CustomHtml
|
||||
{/* Security Warning */}
|
||||
<Alert variant="warning" className="flex items-start gap-2">
|
||||
<AlertTriangleIcon className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<AlertDescription>{t("workspace.surveys.share.custom_html.security_warning")}</AlertDescription>
|
||||
<AlertDescription>
|
||||
{t("environments.surveys.share.custom_html.security_warning")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</form>
|
||||
</FormProvider>
|
||||
+8
-6
@@ -25,7 +25,7 @@ export const DisableLinkModal = ({ open, onOpenChange, type, onDisable }: Disabl
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-sm font-medium text-slate-900">
|
||||
{type === "multi-use"
|
||||
? t("workspace.surveys.share.anonymous_links.disable_multi_use_link_modal_title")
|
||||
? t("environments.surveys.share.anonymous_links.disable_multi_use_link_modal_title")
|
||||
: t("common.are_you_sure")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -33,18 +33,20 @@ export const DisableLinkModal = ({ open, onOpenChange, type, onDisable }: Disabl
|
||||
<DialogBody>
|
||||
{type === "multi-use" ? (
|
||||
<>
|
||||
<p>{t("workspace.surveys.share.anonymous_links.disable_multi_use_link_modal_description")}</p>
|
||||
<p>
|
||||
{t("environments.surveys.share.anonymous_links.disable_multi_use_link_modal_description")}
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
<p>
|
||||
{t(
|
||||
"workspace.surveys.share.anonymous_links.disable_multi_use_link_modal_description_subtext"
|
||||
"environments.surveys.share.anonymous_links.disable_multi_use_link_modal_description_subtext"
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p>{t("workspace.surveys.share.anonymous_links.disable_single_use_link_modal_description")}</p>
|
||||
<p>{t("environments.surveys.share.anonymous_links.disable_single_use_link_modal_description")}</p>
|
||||
)}
|
||||
</DialogBody>
|
||||
|
||||
@@ -57,8 +59,8 @@ export const DisableLinkModal = ({ open, onOpenChange, type, onDisable }: Disabl
|
||||
onOpenChange(false);
|
||||
}}>
|
||||
{type === "multi-use"
|
||||
? t("workspace.surveys.share.anonymous_links.disable_multi_use_link_modal_button")
|
||||
: t("workspace.surveys.share.anonymous_links.disable_single_use_link_modal_button")}
|
||||
? t("environments.surveys.share.anonymous_links.disable_multi_use_link_modal_button")
|
||||
: t("environments.surveys.share.anonymous_links.disable_single_use_link_modal_button")}
|
||||
</Button>
|
||||
|
||||
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user