Compare commits

..

37 Commits

Author SHA1 Message Date
Johannes f356e86729 vibed settings unification 2026-04-12 20:48:07 +02:00
Manuel Delgado 1a02f91afd fix(api): return 409 Conflict instead of 500 when creating user with duplicate email (#7675)
Co-authored-by: Tiago Farto <tiago@formbricks.com>
2026-04-10 14:28:17 +00:00
Tiago cc22ccb22d chore: Harden SSO account linking for existing email-based accounts (#7702) 2026-04-10 14:19:21 +00:00
Tiago 12763f0ef6 fix: Dutch translations for link survey footer (Privacy Policy, Imprint, Report Survey) (#7707) 2026-04-10 13:42:15 +00:00
Dhruwang Jariwala d39e3ee638 feat: offline support for link surveys (#7694)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
Co-authored-by: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
Co-authored-by: Johannes <johannes@formbricks.com>
2026-04-10 11:27:48 +00:00
dingdyan d85242a86b fix: handle internal server error toast behavior in create organization (#7662)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-04-10 11:13:10 +00:00
Bhagya Amarasinghe ef53065abc feat: support GKE Envoy ingress split with numeric ports and service annotations (#7704) 2026-04-10 09:22:19 +00:00
Dhruwang Jariwala 805c1c6874 fix: (duplicate) server error toast handling (#7701) 2026-04-10 09:22:16 +00:00
Niels Kaspers 01687e8907 fix: add TERMS_URL support to survey link footers (#7670) 2026-04-10 09:21:11 +00:00
Johannes 31d455002d feat: unifiy nav auth behaviour (#7635)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Johannes <jobenjada@users.noreply.github.com>
2026-04-09 14:26:14 +00:00
Johannes d96304d86d fix: make navigation more user-friendly (#7599)
Co-authored-by: Tiago Farto <tiago@formbricks.com>
2026-04-09 08:03:24 +00:00
Bhagya Amarasinghe 1064f68435 fix: support OTEL host config for envoy telemetry (#7692) 2026-04-09 07:25:52 +00:00
Anshuman Pandey 3d16e859c6 feat: custom posthog events (#7647) 2026-04-09 05:34:01 +00:00
Salim B af198c5632 docs: remove spurious left-overs (#7690) 2026-04-08 16:11:30 +00:00
Bhagya Amarasinghe a43ed2b25c feat: add envoy gateway helm bundle (#7686) 2026-04-08 07:34:47 +00:00
Tiago 87bcad2b20 feat: Supporting different AI providers within Formbricks (#7611)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-04-06 05:45:12 +00:00
Anshuman Pandey b5eaa4c7fd fix: merge epic/improve-telemetry into main (#7666) 2026-04-03 10:12:51 +00:00
Tiago 995c03bc01 chore: Revoke all active sessions after password reset (#7628) 2026-04-03 06:10:28 +00:00
Johannes b4395a48c5 fix: multi-lang toggle covering arabic text (#7657)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-04-02 13:09:16 +00:00
Johannes 461e3893fe fix: 7549 multilang button overflow (#7656)
Co-authored-by: Niels Kaspers <kaspersniels@gmail.com>
2026-04-02 12:53:57 +00:00
Tiago 735a9f84ec fix: harden api error reporting for v2/v1 Sentry observability (#7633) 2026-04-02 12:08:44 +00:00
Dhruwang Jariwala 8cb8d734cf fix: prevent language switch from breaking survey orientation and resetting language on auto-save (#7654)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 12:08:12 +00:00
Anshuman Pandey 44d5530b48 fix: adds formbricks instance on window (#7630) 2026-04-02 07:26:48 +00:00
Matti Nannt a314eb391e chore: add Codex environment config (#7589) 2026-04-02 07:24:02 +00:00
Matti Nannt 6c34c316d0 docs: remove non-official self-hosting options from README.md 2026-04-01 14:16:47 +02:00
Matti Nannt 4f26278f16 docs: add German README summary (#7641) 2026-04-01 11:04:15 +02:00
Tiago b975e7fa2e feat: Make password reset links single-use and revocable (#7627) 2026-04-01 07:12:37 +00:00
Johannes 6c3052f9e4 fix: correct CSAT template option order for question 2 (#7636)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Johannes <jobenjada@users.noreply.github.com>
2026-04-01 07:11:27 +00:00
Dhruwang Jariwala 5bb8119ebf feat: split AI toggle into smart tools and data analysis settings (#7563) 2026-03-31 11:23:51 +00:00
Johannes 02411277d4 revert: remove fake-door workflows experiment (#7392) (#7631)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Johannes <jobenjada@users.noreply.github.com>
2026-03-31 10:47:33 +00:00
Dhruwang Jariwala 4cfb8c6d7b fix: resolve language code case mismatch in link survey rendering (#7624)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 11:34:20 +00:00
Anshuman Pandey e74a51a5ff fix: sync segment state after auto-save to prevent stale reference on publish (#7619) 2026-03-30 06:51:44 +00:00
Dhruwang Jariwala 29cc6a10fe fix: prevent auto-save from overwriting survey status during publish (#7618) 2026-03-30 06:34:20 +00:00
Bhagya Amarasinghe 01f765e969 fix: migrate auth sessions to database-backed storage (#7594) 2026-03-27 07:15:06 +00:00
Anshuman Pandey 9366960f18 feat: adds support for internal webhook urls (#7577) 2026-03-27 07:04:14 +00:00
IllimarR 697dc9cc99 feat: add Estonian language support for surveys (#7574)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-03-27 06:12:40 +00:00
Dhruwang Jariwala 83bc272ed2 fix: prevent duplicate hobby subscriptions from race condition (#7597)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 15:50:52 +00:00
858 changed files with 23443 additions and 12789 deletions
+9
View File
@@ -0,0 +1,9 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "formbricks"
[setup]
script = '''
pnpm install
pnpm dev:setup
'''
+39 -9
View File
@@ -38,15 +38,6 @@ LOG_LEVEL=info
DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=public' 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
################ ################
# MAIL SETUP # # MAIL SETUP #
################ ################
@@ -103,6 +94,12 @@ EMAIL_VERIFICATION_DISABLED=1
# Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too. # Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too.
PASSWORD_RESET_DISABLED=1 PASSWORD_RESET_DISABLED=1
# Password reset token lifetime in minutes. Must be between 5 and 120 if set.
# PASSWORD_RESET_TOKEN_LIFETIME_MINUTES=30
# Development-only helper: log the password reset link to the server console instead of sending reset emails.
# DEBUG_SHOW_RESET_LINK=1
# Email login. Disable the ability for users to login with email. # Email login. Disable the ability for users to login with email.
# EMAIL_AUTH_DISABLED=1 # EMAIL_AUTH_DISABLED=1
@@ -141,6 +138,31 @@ AZUREAD_CLIENT_ID=
AZUREAD_CLIENT_SECRET= AZUREAD_CLIENT_SECRET=
AZUREAD_TENANT_ID= 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, 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 Vertex AI credentials
# AI_GCP_PROJECT=
# AI_GCP_LOCATION=
# AI_GCP_CREDENTIALS_JSON=
# AI_GCP_APPLICATION_CREDENTIALS=
# Amazon Bedrock credentials
# AI_AWS_REGION=
# AI_AWS_ACCESS_KEY_ID=
# AI_AWS_SECRET_ACCESS_KEY=
# AI_AWS_SESSION_TOKEN=
# Azure AI / Microsoft Foundry credentials
# AI_AZURE_BASE_URL=
# AI_AZURE_RESOURCE_NAME=
# AI_AZURE_API_KEY=
# AI_AZURE_API_VERSION=v1
# OpenID Connect (OIDC) configuration # OpenID Connect (OIDC) configuration
# OIDC_CLIENT_ID= # OIDC_CLIENT_ID=
# OIDC_CLIENT_SECRET= # OIDC_CLIENT_SECRET=
@@ -194,6 +216,14 @@ ENTERPRISE_LICENSE_KEY=
# Ignore Rate Limiting across the Formbricks app # Ignore Rate Limiting across the Formbricks app
# RATE_LIMITING_DISABLED=1 # RATE_LIMITING_DISABLED=1
# Disable telemetry reporting (usage stats sent to Formbricks). Ignored when an EE license is active.
# TELEMETRY_DISABLED=1
# Allow webhook URLs to point to internal/private network addresses (e.g. localhost, 192.168.x.x)
# WARNING: Only enable this if you understand the SSRF risks. Useful for self-hosted instances
# that need to send webhooks to internal services.
# DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS=1
# OpenTelemetry OTLP endpoint (base URL, exporters append /v1/traces and /v1/metrics) # OpenTelemetry OTLP endpoint (base URL, exporters append /v1/traces and /v1/metrics)
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
# OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf # OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
+1 -1
View File
@@ -45,7 +45,7 @@ yarn-error.log*
.direnv .direnv
# Playwright # Playwright
/test-results/ **/test-results/
/playwright-report/ /playwright-report/
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
+1 -25
View File
@@ -127,34 +127,10 @@ Formbricks has a hosted cloud offering with a generous free plan to get you up a
Formbricks is available Open-Source under AGPLv3 license. You can host Formbricks on your own servers using Docker without a subscription. Formbricks is available Open-Source under AGPLv3 license. You can host Formbricks on your own servers using Docker without a subscription.
If you opt for self-hosting Formbricks, here are a few options to consider:
#### Docker #### Docker
To get started with self-hosting with Docker, take a look at our [self-hosting docs](https://formbricks.com/docs/self-hosting/deployment). To get started with self-hosting with Docker, take a look at our [self-hosting docs](https://formbricks.com/docs/self-hosting/deployment).
#### Community-managed One Click Hosting
##### Railway
You can deploy Formbricks on [Railway](https://railway.app) using the button below.
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/PPDzCd)
##### RepoCloud
Or you can also deploy Formbricks on [RepoCloud](https://repocloud.io) using the button below.
[![Deploy on RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploy.png)](https://repocloud.io/details/?app_id=254)
##### Zeabur
Or you can also deploy Formbricks on [Zeabur](https://zeabur.com) using the button below.
[![Deploy to Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/G4TUJL)
<a id="development"></a>
## 👨‍💻 Development ## 👨‍💻 Development
### Prerequisites ### Prerequisites
@@ -247,4 +223,4 @@ We currently do not offer Formbricks white-labeled. That means that we don't sel
The Enterprise Edition allows us to fund the development of Formbricks sustainably. It guarantees that the free and open-source surveying infrastructure we're building will be around for decades to come. The Enterprise Edition allows us to fund the development of Formbricks sustainably. It guarantees that the free and open-source surveying infrastructure we're building will be around for decades to come.
<p align="right"><a href="#top">🔼 Back to top</a></p> <a id="readme-de"></a>
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TEnvironment } from "@formbricks/types/environment"; import { TEnvironment } from "@formbricks/types/environment";
import { TWorkspaceConfigChannel } from "@formbricks/types/workspace"; import { TProjectConfigChannel } from "@formbricks/types/project";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions"; import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
@@ -14,7 +14,7 @@ interface ConnectWithFormbricksProps {
environment: TEnvironment; environment: TEnvironment;
publicDomain: string; publicDomain: string;
appSetupCompleted: boolean; appSetupCompleted: boolean;
channel: TWorkspaceConfigChannel; channel: TProjectConfigChannel;
} }
export const ConnectWithFormbricks = ({ export const ConnectWithFormbricks = ({
@@ -5,7 +5,7 @@ import "prismjs/themes/prism.css";
import { useState } from "react"; import { useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useTranslation } from "react-i18next"; 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 { Button } from "@/modules/ui/components/button";
import { CodeBlock } from "@/modules/ui/components/code-block"; import { CodeBlock } from "@/modules/ui/components/code-block";
import { Html5Icon, NpmIcon } from "@/modules/ui/components/icons"; import { Html5Icon, NpmIcon } from "@/modules/ui/components/icons";
@@ -19,7 +19,7 @@ const tabs = [
interface OnboardingSetupInstructionsProps { interface OnboardingSetupInstructionsProps {
environmentId: string; environmentId: string;
publicDomain: string; publicDomain: string;
channel: TWorkspaceConfigChannel; channel: TProjectConfigChannel;
appSetupCompleted: boolean; appSetupCompleted: boolean;
} }
@@ -4,7 +4,7 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks"; import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
import { getEnvironment } from "@/lib/environment/service"; import { getEnvironment } from "@/lib/environment/service";
import { getPublicDomain } from "@/lib/getPublicUrl"; import { getPublicDomain } from "@/lib/getPublicUrl";
import { getWorkspaceByEnvironmentId } from "@/lib/workspace/service"; import { getProjectByEnvironmentId } from "@/lib/project/service";
import { getTranslate } from "@/lingodotdev/server"; import { getTranslate } from "@/lingodotdev/server";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header"; import { Header } from "@/modules/ui/components/header";
@@ -24,12 +24,12 @@ const Page = async (props: ConnectPageProps) => {
throw new ResourceNotFoundError(t("common.environment"), params.environmentId); throw new ResourceNotFoundError(t("common.environment"), params.environmentId);
} }
const workspace = await getWorkspaceByEnvironmentId(environment.id); const project = await getProjectByEnvironmentId(environment.id);
if (!workspace) { if (!project) {
throw new Error(t("common.workspace_not_found")); throw new ResourceNotFoundError(t("common.workspace"), null);
} }
const channel = workspace.config.channel || null; const channel = project.config.channel || null;
const publicDomain = getPublicDomain(); const publicDomain = getPublicDomain();
@@ -5,10 +5,10 @@ import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TProject } from "@formbricks/types/project";
import { TSurveyCreateInput } from "@formbricks/types/surveys/types"; import { TSurveyCreateInput } from "@formbricks/types/surveys/types";
import { TXMTemplate } from "@formbricks/types/templates"; import { TXMTemplate } from "@formbricks/types/templates";
import { TUser } from "@formbricks/types/user"; 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 { 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 { getXMTemplates } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates";
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer"; import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
@@ -16,12 +16,12 @@ import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { createSurveyAction } from "@/modules/survey/components/template-list/actions"; import { createSurveyAction } from "@/modules/survey/components/template-list/actions";
interface XMTemplateListProps { interface XMTemplateListProps {
workspace: TWorkspace; project: TProject;
user: TUser; user: TUser;
environmentId: string; environmentId: string;
} }
export const XMTemplateList = ({ workspace, user, environmentId }: XMTemplateListProps) => { export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListProps) => {
const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null); const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
@@ -48,7 +48,7 @@ export const XMTemplateList = ({ workspace, user, environmentId }: XMTemplateLis
const handleTemplateClick = (templateIdx: number) => { const handleTemplateClick = (templateIdx: number) => {
setActiveTemplateId(templateIdx); setActiveTemplateId(templateIdx);
const template = getXMTemplates(t)[templateIdx]; const template = getXMTemplates(t)[templateIdx];
const newTemplate = replacePresetPlaceholders(template, workspace); const newTemplate = replacePresetPlaceholders(template, project);
createSurvey(newTemplate); createSurvey(newTemplate);
}; };
@@ -1,17 +1,17 @@
import "@testing-library/jest-dom/vitest"; import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react"; import { cleanup } from "@testing-library/react";
import { afterEach, describe, expect, test } from "vitest"; import { afterEach, describe, expect, test } from "vitest";
import { TProject } from "@formbricks/types/project";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants"; import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
import { TXMTemplate } from "@formbricks/types/templates"; import { TXMTemplate } from "@formbricks/types/templates";
import { TWorkspace } from "@formbricks/types/workspace";
import { replacePresetPlaceholders } from "./utils"; import { replacePresetPlaceholders } from "./utils";
// Mock data // Mock data
const mockWorkspace: TWorkspace = { const mockProject: TProject = {
id: "workspace1", id: "project1",
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
name: "Test Workspace", name: "Test Project",
organizationId: "org1", organizationId: "org1",
styling: { styling: {
allowStyleOverwrite: true, allowStyleOverwrite: true,
@@ -32,7 +32,7 @@ const mockWorkspace: TWorkspace = {
logo: null, logo: null,
}; };
const mockTemplate: TXMTemplate = { const mockTemplate: TXMTemplate = {
name: "$[workspaceName] Survey", name: "$[projectName] Survey",
blocks: [ blocks: [
{ {
id: "block1", id: "block1",
@@ -42,7 +42,7 @@ const mockTemplate: TXMTemplate = {
id: "q1", id: "q1",
type: "openText" as TSurveyElementTypeEnum.OpenText, type: "openText" as TSurveyElementTypeEnum.OpenText,
inputType: "text" as const, inputType: "text" as const,
headline: { default: "$[workspaceName] Question" }, headline: { default: "$[projectName] Question" },
subheader: { default: "" }, subheader: { default: "" },
required: false, required: false,
placeholder: { default: "" }, placeholder: { default: "" },
@@ -70,19 +70,19 @@ describe("replacePresetPlaceholders", () => {
cleanup(); cleanup();
}); });
test("replaces workspaceName placeholder in template name", () => { test("replaces projectName placeholder in template name", () => {
const result = replacePresetPlaceholders(mockTemplate, mockWorkspace); const result = replacePresetPlaceholders(mockTemplate, mockProject);
expect(result.name).toBe("Test Workspace Survey"); expect(result.name).toBe("Test Project Survey");
}); });
test("replaces workspaceName placeholder in element headline", () => { test("replaces projectName placeholder in element headline", () => {
const result = replacePresetPlaceholders(mockTemplate, mockWorkspace); const result = replacePresetPlaceholders(mockTemplate, mockProject);
expect(result.blocks[0].elements[0].headline.default).toBe("Test Workspace Question"); expect(result.blocks[0].elements[0].headline.default).toBe("Test Project Question");
}); });
test("returns a new object without mutating the original template", () => { test("returns a new object without mutating the original template", () => {
const originalTemplate = structuredClone(mockTemplate); const originalTemplate = structuredClone(mockTemplate);
const result = replacePresetPlaceholders(mockTemplate, mockWorkspace); const result = replacePresetPlaceholders(mockTemplate, mockProject);
expect(result).not.toBe(mockTemplate); expect(result).not.toBe(mockTemplate);
expect(mockTemplate).toEqual(originalTemplate); expect(mockTemplate).toEqual(originalTemplate);
}); });
@@ -1,16 +1,16 @@
import { TProject } from "@formbricks/types/project";
import { TSurveyBlock } from "@formbricks/types/surveys/blocks"; import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
import { TXMTemplate } from "@formbricks/types/templates"; import { TXMTemplate } from "@formbricks/types/templates";
import { TWorkspace } from "@formbricks/types/workspace";
import { replaceElementPresetPlaceholders } from "@/lib/utils/templates"; import { replaceElementPresetPlaceholders } from "@/lib/utils/templates";
// replace all occurences of workspaceName with the actual workspace name in the current template // replace all occurences of projectName with the actual project name in the current template
export const replacePresetPlaceholders = (template: TXMTemplate, workspace: TWorkspace): TXMTemplate => { export const replacePresetPlaceholders = (template: TXMTemplate, project: TProject): TXMTemplate => {
const survey = structuredClone(template); const survey = structuredClone(template);
const modifiedBlocks = survey.blocks.map((block: TSurveyBlock) => ({ const modifiedBlocks = survey.blocks.map((block: TSurveyBlock) => ({
...block, ...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 };
}; };
@@ -4,9 +4,9 @@ import Link from "next/link";
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors"; import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList"; import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
import { getEnvironment } from "@/lib/environment/service"; import { getEnvironment } from "@/lib/environment/service";
import { getProjectByEnvironmentId, getUserProjects } from "@/lib/project/service";
import { getUser } from "@/lib/user/service"; import { getUser } from "@/lib/user/service";
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper"; import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
import { getUserWorkspaces, getWorkspaceByEnvironmentId } from "@/lib/workspace/service";
import { getTranslate } from "@/lingodotdev/server"; import { getTranslate } from "@/lingodotdev/server";
import { authOptions } from "@/modules/auth/lib/authOptions"; import { authOptions } from "@/modules/auth/lib/authOptions";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
@@ -37,18 +37,18 @@ const Page = async (props: XMTemplatePageProps) => {
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id); const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
const workspace = await getWorkspaceByEnvironmentId(environment.id); const project = await getProjectByEnvironmentId(environment.id);
if (!workspace) { if (!project) {
throw new Error(t("common.workspace_not_found")); throw new ResourceNotFoundError(t("common.workspace"), null);
} }
const workspaces = await getUserWorkspaces(session.user.id, organizationId); const projects = await getUserProjects(session.user.id, organizationId);
return ( return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12"> <div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header title={t("environments.xm-templates.headline")} /> <Header title={t("environments.xm-templates.headline")} />
<XMTemplateList workspace={workspace} user={user} environmentId={environment.id} /> <XMTemplateList project={project} user={user} environmentId={environment.id} />
{workspaces.length >= 2 && ( {projects.length >= 2 && (
<Button <Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700" className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost" variant="ghost"
@@ -2,7 +2,7 @@ import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { getEnvironments } from "@/lib/environment/service"; import { getEnvironments } from "@/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/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"; import { authOptions } from "@/modules/auth/lib/authOptions";
const LandingLayout = async (props: { const LandingLayout = async (props: {
@@ -24,11 +24,11 @@ const LandingLayout = async (props: {
return notFound(); return notFound();
} }
const workspaces = await getUserWorkspaces(session.user.id, params.organizationId); const projects = await getUserProjects(session.user.id, params.organizationId);
if (workspaces.length !== 0) { if (projects.length !== 0) {
const firstWorkspace = workspaces[0]; const firstProject = projects[0];
const environments = await getEnvironments(firstWorkspace.id); const environments = await getEnvironments(firstProject.id);
const prodEnvironment = environments.find((e) => e.type === "production"); const prodEnvironment = environments.find((e) => e.type === "production");
if (prodEnvironment) { if (prodEnvironment) {
@@ -1,6 +1,6 @@
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar"; import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
import { WorkspaceAndOrgSwitch } from "@/app/(app)/environments/[environmentId]/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 { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils"; import { getAccessFlags } from "@/lib/membership/utils";
@@ -26,7 +26,8 @@ const Page = async (props: { params: Promise<{ organizationId: string }> }) => {
const isMultiOrgEnabled = await getIsMultiOrgEnabled(); const isMultiOrgEnabled = await getIsMultiOrgEnabled();
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id); const membership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
const { isMember } = getAccessFlags(membership?.role); const { isBilling } = getAccessFlags(membership?.role);
const isMembershipPending = membership?.role === undefined;
return ( return (
<div className="flex min-h-full min-w-full flex-row"> <div className="flex min-h-full min-w-full flex-row">
@@ -34,17 +35,18 @@ const Page = async (props: { params: Promise<{ organizationId: string }> }) => {
<div className="flex-1"> <div className="flex-1">
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<div className="p-6"> <div className="p-6">
{/* we only need to render organization breadcrumb on this page, organizations/workspaces are lazy-loaded */} {/* we only need to render organization breadcrumb on this page, organizations/projects are lazy-loaded */}
<WorkspaceAndOrgSwitch <ProjectAndOrgSwitch
currentOrganizationId={organization.id} currentOrganizationId={organization.id}
currentOrganizationName={organization.name} currentOrganizationName={organization.name}
isMultiOrgEnabled={isMultiOrgEnabled} isMultiOrgEnabled={isMultiOrgEnabled}
organizationWorkspacesLimit={0} organizationProjectsLimit={0}
isFormbricksCloud={IS_FORMBRICKS_CLOUD} isFormbricksCloud={IS_FORMBRICKS_CLOUD}
isLicenseActive={false} isLicenseActive={false}
isOwnerOrManager={false} isOwnerOrManager={false}
isAccessControlAllowed={false} isAccessControlAllowed={false}
isMember={isMember} isBilling={isBilling}
isMembershipPending={isMembershipPending}
environments={[]} environments={[]}
/> />
</div> </div>
@@ -8,7 +8,7 @@ import { getTranslate } from "@/lingodotdev/server";
import { authOptions } from "@/modules/auth/lib/authOptions"; import { authOptions } from "@/modules/auth/lib/authOptions";
import { ToasterClient } from "@/modules/ui/components/toaster-client"; import { ToasterClient } from "@/modules/ui/components/toaster-client";
const WorkspaceOnboardingLayout = async (props: { const ProjectOnboardingLayout = async (props: {
params: Promise<{ organizationId: string }>; params: Promise<{ organizationId: string }>;
children: React.ReactNode; children: React.ReactNode;
}) => { }) => {
@@ -47,4 +47,4 @@ const WorkspaceOnboardingLayout = async (props: {
); );
}; };
export default WorkspaceOnboardingLayout; export default ProjectOnboardingLayout;
@@ -2,7 +2,7 @@ import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer"; 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 { getTranslate } from "@/lingodotdev/server";
import { getOrganizationAuth } from "@/modules/organization/lib/utils"; import { getOrganizationAuth } from "@/modules/organization/lib/utils";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
@@ -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 ( return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12"> <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")} subtitle={t("organizations.workspaces.new.channel.channel_select_subtitle")}
/> />
<OnboardingOptionsContainer options={channelOptions} /> <OnboardingOptionsContainer options={channelOptions} />
{workspaces.length >= 1 && ( {projects.length >= 1 && (
<Button <Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700" className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost" variant="ghost"
@@ -4,10 +4,10 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils"; import { getAccessFlags } from "@/lib/membership/utils";
import { getOrganization } from "@/lib/organization/service"; import { getOrganization } from "@/lib/organization/service";
import { getOrganizationWorkspacesCount } from "@/lib/workspace/service"; import { getOrganizationProjectsCount } from "@/lib/project/service";
import { getTranslate } from "@/lingodotdev/server"; import { getTranslate } from "@/lingodotdev/server";
import { authOptions } from "@/modules/auth/lib/authOptions"; 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: { const OnboardingLayout = async (props: {
params: Promise<{ organizationId: string }>; params: Promise<{ organizationId: string }>;
@@ -32,12 +32,12 @@ const OnboardingLayout = async (props: {
throw new ResourceNotFoundError(t("common.organization"), params.organizationId); throw new ResourceNotFoundError(t("common.organization"), params.organizationId);
} }
const [organizationWorkspacesLimit, organizationWorkspacesCount] = await Promise.all([ const [organizationProjectsLimit, organizationProjectsCount] = await Promise.all([
getOrganizationWorkspacesLimit(organization.id), getOrganizationProjectsLimit(organization.id),
getOrganizationWorkspacesCount(organization.id), getOrganizationProjectsCount(organization.id),
]); ]);
if (organizationWorkspacesCount >= organizationWorkspacesLimit) { if (organizationProjectsCount >= organizationProjectsLimit) {
return redirect(`/`); return redirect(`/`);
} }
@@ -2,7 +2,7 @@ import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer"; 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 { getTranslate } from "@/lingodotdev/server";
import { getOrganizationAuth } from "@/modules/organization/lib/utils"; import { getOrganizationAuth } from "@/modules/organization/lib/utils";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
@@ -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 ( return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12"> <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")} /> <Header title={t("organizations.workspaces.new.mode.what_are_you_here_for")} />
<OnboardingOptionsContainer options={channelOptions} /> <OnboardingOptionsContainer options={channelOptions} />
{workspaces.length >= 1 && ( {projects.length >= 1 && (
<Button <Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700" className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost" variant="ghost"
@@ -8,19 +8,19 @@ import { useForm } from "react-hook-form";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
TWorkspaceConfigChannel, TProjectConfigChannel,
TWorkspaceConfigIndustry, TProjectConfigIndustry,
TWorkspaceMode, TProjectMode,
TWorkspaceUpdateInput, TProjectUpdateInput,
ZWorkspaceUpdateInput, ZProjectUpdateInput,
} from "@formbricks/types/workspace"; } from "@formbricks/types/project";
import { createWorkspaceAction } from "@/app/(app)/environments/[environmentId]/actions"; import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
import { previewSurvey } from "@/app/lib/templates"; import { previewSurvey } from "@/app/lib/templates";
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@/lib/localStorage"; import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@/lib/localStorage";
import { buildStylingFromBrandColor } from "@/lib/styling/constants"; import { buildStylingFromBrandColor } from "@/lib/styling/constants";
import { getFormattedErrorMessage } from "@/lib/utils/helper"; 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 { 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 { Button } from "@/modules/ui/components/button";
import { ColorPicker } from "@/modules/ui/components/color-picker"; import { ColorPicker } from "@/modules/ui/components/color-picker";
import { import {
@@ -36,34 +36,34 @@ import { Input } from "@/modules/ui/components/input";
import { MultiSelect } from "@/modules/ui/components/multi-select"; import { MultiSelect } from "@/modules/ui/components/multi-select";
import { SurveyInline } from "@/modules/ui/components/survey"; import { SurveyInline } from "@/modules/ui/components/survey";
interface WorkspaceSettingsProps { interface ProjectSettingsProps {
organizationId: string; organizationId: string;
workspaceMode: TWorkspaceMode; projectMode: TProjectMode;
channel: TWorkspaceConfigChannel; channel: TProjectConfigChannel;
industry: TWorkspaceConfigIndustry; industry: TProjectConfigIndustry;
defaultBrandColor: string; defaultBrandColor: string;
organizationTeams: TOrganizationTeam[]; organizationTeams: TOrganizationTeam[];
isAccessControlAllowed: boolean; isAccessControlAllowed: boolean;
userWorkspacesCount: number; userProjectsCount: number;
publicDomain: string; publicDomain: string;
} }
export const WorkspaceSettings = ({ export const ProjectSettings = ({
organizationId, organizationId,
workspaceMode, projectMode,
channel, channel,
industry, industry,
defaultBrandColor, defaultBrandColor,
organizationTeams, organizationTeams,
isAccessControlAllowed = false, isAccessControlAllowed = false,
userWorkspacesCount, userProjectsCount,
publicDomain, publicDomain,
}: WorkspaceSettingsProps) => { }: ProjectSettingsProps) => {
const [createTeamModalOpen, setCreateTeamModalOpen] = useState(false); const [createTeamModalOpen, setCreateTeamModalOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const addWorkspace = async (data: TWorkspaceUpdateInput) => { const addProject = async (data: TProjectUpdateInput) => {
try { try {
// Build the full styling from the chosen brand color so all derived // Build the full styling from the chosen brand color so all derived
// colours (question, button, input, option, progress, etc.) are persisted. // 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). // back to STYLE_DEFAULTS computed from the default brand (#64748b).
const fullStyling = buildStylingFromBrandColor(data.styling?.brandColor?.light); const fullStyling = buildStylingFromBrandColor(data.styling?.brandColor?.light);
const createWorkspaceResponse = await createWorkspaceAction({ const createProjectResponse = await createProjectAction({
organizationId, organizationId,
data: { data: {
...data, ...data,
@@ -81,14 +81,14 @@ export const WorkspaceSettings = ({
}, },
}); });
if (createWorkspaceResponse?.data) { if (createProjectResponse?.data) {
// get production environment // get production environment
const productionEnvironment = createWorkspaceResponse.data.environments.find( const productionEnvironment = createProjectResponse.data.environments.find(
(environment: { type: string }) => environment.type === "production" (environment) => environment.type === "production"
); );
if (productionEnvironment) { if (productionEnvironment) {
if (globalThis.window !== undefined) { if (globalThis.window !== undefined) {
// Remove filters when creating a new workspace // Rmove filters when creating a new project
localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS); localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
} }
} }
@@ -96,11 +96,11 @@ export const WorkspaceSettings = ({
router.push(`/environments/${productionEnvironment?.id}/connect`); router.push(`/environments/${productionEnvironment?.id}/connect`);
} else if (channel === "link") { } else if (channel === "link") {
router.push(`/environments/${productionEnvironment?.id}/surveys`); router.push(`/environments/${productionEnvironment?.id}/surveys`);
} else if (workspaceMode === "cx") { } else if (projectMode === "cx") {
router.push(`/environments/${productionEnvironment?.id}/xm-templates`); router.push(`/environments/${productionEnvironment?.id}/xm-templates`);
} }
} else { } else {
const errorMessage = getFormattedErrorMessage(createWorkspaceResponse); const errorMessage = getFormattedErrorMessage(createProjectResponse);
toast.error(errorMessage); toast.error(errorMessage);
} }
} catch (error) { } catch (error) {
@@ -109,15 +109,15 @@ export const WorkspaceSettings = ({
} }
}; };
const form = useForm<TWorkspaceUpdateInput>({ const form = useForm<TProjectUpdateInput>({
defaultValues: { defaultValues: {
name: "", name: "",
styling: { allowStyleOverwrite: true, brandColor: { light: defaultBrandColor } }, styling: { allowStyleOverwrite: true, brandColor: { light: defaultBrandColor } },
teamIds: [], teamIds: [],
}, },
resolver: zodResolver(ZWorkspaceUpdateInput), resolver: zodResolver(ZProjectUpdateInput),
}); });
const workspaceName = form.watch("name"); const projectName = form.watch("name");
const logoUrl = form.watch("logo.url"); const logoUrl = form.watch("logo.url");
const brandColor = form.watch("styling.brandColor.light") ?? defaultBrandColor; const brandColor = form.watch("styling.brandColor.light") ?? defaultBrandColor;
const previewStyling = useMemo(() => buildStylingFromBrandColor(brandColor), [brandColor]); const previewStyling = useMemo(() => buildStylingFromBrandColor(brandColor), [brandColor]);
@@ -132,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="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"> <div className="flex w-1/2 flex-col space-y-4">
<FormProvider {...form}> <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 <FormField
control={form.control} control={form.control}
name="styling.brandColor.light" name="styling.brandColor.light"
@@ -184,7 +184,7 @@ export const WorkspaceSettings = ({
)} )}
/> />
{isAccessControlAllowed && userWorkspacesCount > 0 && ( {isAccessControlAllowed && userProjectsCount > 0 && (
<FormField <FormField
control={form.control} control={form.control}
name="teamIds" name="teamIds"
@@ -242,7 +242,7 @@ export const WorkspaceSettings = ({
<SurveyInline <SurveyInline
appUrl={publicDomain} appUrl={publicDomain}
isPreviewMode={true} isPreviewMode={true}
survey={previewSurvey(workspaceName || t("common.my_product"), t)} survey={previewSurvey(projectName || t("common.my_product"), t)}
styling={previewStyling} styling={previewStyling}
isBrandingEnabled={false} isBrandingEnabled={false}
languageCode="default" languageCode="default"
@@ -2,34 +2,30 @@ import { XIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { ResourceNotFoundError } from "@formbricks/types/errors"; import { ResourceNotFoundError } from "@formbricks/types/errors";
import { import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
TWorkspaceConfigChannel,
TWorkspaceConfigIndustry,
TWorkspaceMode,
} from "@formbricks/types/workspace";
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding"; 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 { DEFAULT_BRAND_COLOR } from "@/lib/constants";
import { getPublicDomain } from "@/lib/getPublicUrl"; import { getPublicDomain } from "@/lib/getPublicUrl";
import { getUserWorkspaces } from "@/lib/workspace/service"; import { getUserProjects } from "@/lib/project/service";
import { getTranslate } from "@/lingodotdev/server"; import { getTranslate } from "@/lingodotdev/server";
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils"; import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
import { getOrganizationAuth } from "@/modules/organization/lib/utils"; import { getOrganizationAuth } from "@/modules/organization/lib/utils";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header"; import { Header } from "@/modules/ui/components/header";
interface WorkspaceSettingsPageProps { interface ProjectSettingsPageProps {
params: Promise<{ params: Promise<{
organizationId: string; organizationId: string;
}>; }>;
searchParams: Promise<{ searchParams: Promise<{
channel?: TWorkspaceConfigChannel; channel?: TProjectConfigChannel;
industry?: TWorkspaceConfigIndustry; industry?: TProjectConfigIndustry;
mode?: TWorkspaceMode; mode?: TProjectMode;
}>; }>;
} }
const Page = async (props: WorkspaceSettingsPageProps) => { const Page = async (props: ProjectSettingsPageProps) => {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const params = await props.params; const params = await props.params;
const t = await getTranslate(); const t = await getTranslate();
@@ -43,7 +39,7 @@ const Page = async (props: WorkspaceSettingsPageProps) => {
const channel = searchParams.channel ?? null; const channel = searchParams.channel ?? null;
const industry = searchParams.industry ?? null; const industry = searchParams.industry ?? null;
const mode = searchParams.mode ?? "surveys"; 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); const organizationTeams = await getTeamsByOrganizationId(params.organizationId);
@@ -61,18 +57,18 @@ const Page = async (props: WorkspaceSettingsPageProps) => {
title={t("organizations.workspaces.new.settings.workspace_settings_title")} title={t("organizations.workspaces.new.settings.workspace_settings_title")}
subtitle={t("organizations.workspaces.new.settings.workspace_settings_subtitle")} subtitle={t("organizations.workspaces.new.settings.workspace_settings_subtitle")}
/> />
<WorkspaceSettings <ProjectSettings
organizationId={params.organizationId} organizationId={params.organizationId}
workspaceMode={mode} projectMode={mode}
channel={channel} channel={channel}
industry={industry} industry={industry}
defaultBrandColor={DEFAULT_BRAND_COLOR} defaultBrandColor={DEFAULT_BRAND_COLOR}
organizationTeams={organizationTeams} organizationTeams={organizationTeams}
isAccessControlAllowed={isAccessControlAllowed} isAccessControlAllowed={isAccessControlAllowed}
userWorkspacesCount={workspaces.length} userProjectsCount={projects.length}
publicDomain={publicDomain} publicDomain={publicDomain}
/> />
{workspaces.length >= 1 && ( {projects.length >= 1 && (
<Button <Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700" className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost" variant="ghost"
@@ -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;
@@ -8,7 +8,7 @@ import { getDisplaysBySurveyIdWithContact } from "@/lib/display/service";
import { getResponseCountBySurveyId, getResponses } from "@/lib/response/service"; import { getResponseCountBySurveyId, getResponses } from "@/lib/response/service";
import { authenticatedActionClient } from "@/lib/utils/action-client"; import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware"; 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"; import { getSurveySummary } from "./summary/lib/surveySummary";
export const revalidateSurveyIdPath = async (environmentId: string, surveyId: string) => { export const revalidateSurveyIdPath = async (environmentId: string, surveyId: string) => {
@@ -36,9 +36,9 @@ export const getResponsesAction = authenticatedActionClient
roles: ["owner", "manager"], roles: ["owner", "manager"],
}, },
{ {
type: "workspaceTeam", type: "projectTeam",
minPermission: "read", minPermission: "read",
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId), projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
}, },
], ],
}); });
@@ -70,9 +70,9 @@ export const getSurveySummaryAction = authenticatedActionClient
roles: ["owner", "manager"], roles: ["owner", "manager"],
}, },
{ {
type: "workspaceTeam", type: "projectTeam",
minPermission: "read", minPermission: "read",
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId), projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
}, },
], ],
}); });
@@ -98,9 +98,9 @@ export const getResponseCountAction = authenticatedActionClient
roles: ["owner", "manager"], roles: ["owner", "manager"],
}, },
{ {
type: "workspaceTeam", type: "projectTeam",
minPermission: "read", minPermission: "read",
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId), projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
}, },
], ],
}); });
@@ -126,9 +126,9 @@ export const getDisplaysWithContactAction = authenticatedActionClient
roles: ["owner", "manager"], roles: ["owner", "manager"],
}, },
{ {
type: "workspaceTeam", type: "projectTeam",
minPermission: "read", minPermission: "read",
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId), projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
}, },
], ],
}); });
@@ -29,6 +29,7 @@ import { ResponseTableCell } from "@/app/(app)/environments/[environmentId]/surv
import { generateResponseTableColumns } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns"; import { generateResponseTableColumns } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns";
import { getResponsesDownloadUrlAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions"; import { getResponsesDownloadUrlAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
import { downloadResponsesFile } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/utils"; 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 { deleteResponseAction } from "@/modules/analysis/components/SingleResponseCard/actions";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { import {
@@ -201,7 +202,13 @@ export const ResponseTable = ({
}; };
const deleteResponse = async (responseId: string, params?: { decrementQuotas?: boolean }) => { const deleteResponse = async (responseId: string, params?: { decrementQuotas?: boolean }) => {
await deleteResponseAction({ responseId, decrementQuotas: params?.decrementQuotas ?? false }); const result = await deleteResponseAction({
responseId,
decrementQuotas: params?.decrementQuotas ?? false,
});
if (result?.serverError) {
throw new Error(getFormattedErrorMessage(result));
}
}; };
// Handle downloading selected responses // Handle downloading selected responses
@@ -8,7 +8,7 @@ import { getSurvey, updateSurvey } from "@/lib/survey/service";
import { authenticatedActionClient } from "@/lib/utils/action-client"; import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { convertToCsv } from "@/lib/utils/file-conversion"; 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 { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
import { generatePersonalLinks } from "@/modules/ee/contacts/lib/contacts"; import { generatePersonalLinks } from "@/modules/ee/contacts/lib/contacts";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
@@ -35,9 +35,9 @@ export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient
roles: ["owner", "manager"], roles: ["owner", "manager"],
}, },
{ {
type: "workspaceTeam", type: "projectTeam",
minPermission: "read", minPermission: "read",
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId), projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
}, },
], ],
}); });
@@ -64,13 +64,13 @@ export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient
const ZResetSurveyAction = z.object({ const ZResetSurveyAction = z.object({
surveyId: ZId, surveyId: ZId,
workspaceId: ZId, projectId: ZId,
}); });
export const resetSurveyAction = authenticatedActionClient.inputSchema(ZResetSurveyAction).action( export const resetSurveyAction = authenticatedActionClient.inputSchema(ZResetSurveyAction).action(
withAuditLogging("updated", "survey", async ({ ctx, parsedInput }) => { withAuditLogging("updated", "survey", async ({ ctx, parsedInput }) => {
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId); const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
const workspaceId = await getWorkspaceIdFromSurveyId(parsedInput.surveyId); const projectId = await getProjectIdFromSurveyId(parsedInput.surveyId);
await checkAuthorizationUpdated({ await checkAuthorizationUpdated({
userId: ctx.user.id, userId: ctx.user.id,
@@ -81,9 +81,9 @@ export const resetSurveyAction = authenticatedActionClient.inputSchema(ZResetSur
roles: ["owner", "manager"], roles: ["owner", "manager"],
}, },
{ {
type: "workspaceTeam", type: "projectTeam",
minPermission: "readWrite", minPermission: "readWrite",
workspaceId, projectId,
}, },
], ],
}); });
@@ -125,9 +125,9 @@ export const getEmailHtmlAction = authenticatedActionClient
roles: ["owner", "manager"], roles: ["owner", "manager"],
}, },
{ {
type: "workspaceTeam", type: "projectTeam",
minPermission: "readWrite", minPermission: "readWrite",
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId), projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
}, },
], ],
}); });
@@ -160,8 +160,8 @@ export const generatePersonalLinksAction = authenticatedActionClient
roles: ["owner", "manager"], roles: ["owner", "manager"],
}, },
{ {
type: "workspaceTeam", type: "projectTeam",
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId), projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
minPermission: "readWrite", minPermission: "readWrite",
}, },
], ],
@@ -234,8 +234,8 @@ export const updateSingleUseLinksAction = authenticatedActionClient
roles: ["owner", "manager"], roles: ["owner", "manager"],
}, },
{ {
type: "workspaceTeam", type: "projectTeam",
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId), projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
minPermission: "readWrite", minPermission: "readWrite",
}, },
], ],
@@ -64,7 +64,7 @@ export const SurveyAnalysisCTA = ({
const [isResetModalOpen, setIsResetModalOpen] = useState(false); const [isResetModalOpen, setIsResetModalOpen] = useState(false);
const [isResetting, setIsResetting] = useState(false); const [isResetting, setIsResetting] = useState(false);
const { workspace } = useEnvironment(); const { project } = useEnvironment();
const { refreshSingleUseId } = useSingleUseId(survey, isReadOnly); const { refreshSingleUseId } = useSingleUseId(survey, isReadOnly);
const appSetupCompleted = survey.type === "app" && environment.appSetupCompleted; const appSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
@@ -128,7 +128,7 @@ export const SurveyAnalysisCTA = ({
setIsResetting(true); setIsResetting(true);
const result = await resetSurveyAction({ const result = await resetSurveyAction({
surveyId: survey.id, surveyId: survey.id,
workspaceId: workspace.id, projectId: project.id,
}); });
if (result?.data) { if (result?.data) {
toast.success( toast.success(
@@ -212,7 +212,7 @@ export const SurveyAnalysisCTA = ({
isFormbricksCloud={isFormbricksCloud} isFormbricksCloud={isFormbricksCloud}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
isStorageConfigured={isStorageConfigured} isStorageConfigured={isStorageConfigured}
workspaceCustomScripts={workspace.customHeadScripts} projectCustomScripts={project.customHeadScripts}
/> />
)} )}
<SuccessMessage environment={environment} survey={survey} /> <SuccessMessage environment={environment} survey={survey} />
@@ -53,7 +53,7 @@ interface ShareSurveyModalProps {
isFormbricksCloud: boolean; isFormbricksCloud: boolean;
isReadOnly: boolean; isReadOnly: boolean;
isStorageConfigured: boolean; isStorageConfigured: boolean;
workspaceCustomScripts?: string | null; projectCustomScripts?: string | null;
} }
export const ShareSurveyModal = ({ export const ShareSurveyModal = ({
@@ -68,7 +68,7 @@ export const ShareSurveyModal = ({
isFormbricksCloud, isFormbricksCloud,
isReadOnly, isReadOnly,
isStorageConfigured, isStorageConfigured,
workspaceCustomScripts, projectCustomScripts,
}: ShareSurveyModalProps) => { }: ShareSurveyModalProps) => {
const environmentId = survey.environmentId; const environmentId = survey.environmentId;
const [surveyUrl, setSurveyUrl] = useState<string>(getSurveyUrl(survey, publicDomain, "default")); const [surveyUrl, setSurveyUrl] = useState<string>(getSurveyUrl(survey, publicDomain, "default"));
@@ -193,7 +193,7 @@ export const ShareSurveyModal = ({
title: t("environments.surveys.share.custom_html.nav_title"), title: t("environments.surveys.share.custom_html.nav_title"),
description: t("environments.surveys.share.custom_html.description"), description: t("environments.surveys.share.custom_html.description"),
componentType: CustomHtmlTab, componentType: CustomHtmlTab,
componentProps: { workspaceCustomScripts, isReadOnly }, componentProps: { projectCustomScripts, isReadOnly },
}, },
]; ];
@@ -216,7 +216,7 @@ export const ShareSurveyModal = ({
isFormbricksCloud, isFormbricksCloud,
email, email,
isStorageConfigured, isStorageConfigured,
workspaceCustomScripts, projectCustomScripts,
]); ]);
const getDefaultActiveId = useCallback(() => { const getDefaultActiveId = useCallback(() => {
@@ -88,7 +88,7 @@ const DisplayCriteriaItem = ({ icon, title, titleSuffix, description }: DisplayC
export const AppTab = () => { export const AppTab = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { environment, workspace } = useEnvironment(); const { environment, project } = useEnvironment();
const { survey } = useSurvey(); const { survey } = useSurvey();
const documentationLinks = useMemo(() => createDocumentationLinks(t), [t]); const documentationLinks = useMemo(() => createDocumentationLinks(t), [t]);
@@ -98,8 +98,8 @@ export const AppTab = () => {
if (survey.recontactDays !== null) { if (survey.recontactDays !== null) {
return formatRecontactDaysString(survey.recontactDays, t); return formatRecontactDaysString(survey.recontactDays, t);
} }
if (workspace.recontactDays !== null) { if (project.recontactDays !== null) {
return formatRecontactDaysString(workspace.recontactDays, t); return formatRecontactDaysString(project.recontactDays, t);
} }
return t("environments.surveys.summary.in_app.display_criteria.time_based_always"); return t("environments.surveys.summary.in_app.display_criteria.time_based_always");
}; };
@@ -22,7 +22,7 @@ import {
import { TabToggle } from "@/modules/ui/components/tab-toggle"; import { TabToggle } from "@/modules/ui/components/tab-toggle";
interface CustomHtmlTabProps { interface CustomHtmlTabProps {
workspaceCustomScripts: string | null | undefined; projectCustomScripts: string | null | undefined;
isReadOnly: boolean; isReadOnly: boolean;
} }
@@ -31,7 +31,7 @@ interface CustomHtmlFormData {
customHeadScriptsMode: TSurvey["customHeadScriptsMode"]; customHeadScriptsMode: TSurvey["customHeadScriptsMode"];
} }
export const CustomHtmlTab = ({ workspaceCustomScripts, isReadOnly }: CustomHtmlTabProps) => { export const CustomHtmlTab = ({ projectCustomScripts, isReadOnly }: CustomHtmlTabProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { survey } = useSurvey(); const { survey } = useSurvey();
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
@@ -101,18 +101,18 @@ export const CustomHtmlTab = ({ workspaceCustomScripts, isReadOnly }: CustomHtml
</div> </div>
{/* Workspace Scripts Preview */} {/* Workspace Scripts Preview */}
{workspaceCustomScripts && ( {projectCustomScripts && (
<div className={scriptsMode === "replace" ? "opacity-50" : ""}> <div className={scriptsMode === "replace" ? "opacity-50" : ""}>
<FormLabel>{t("environments.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"> <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"> <pre className="whitespace-pre-wrap font-mono text-xs text-slate-600">
{workspaceCustomScripts} {projectCustomScripts}
</pre> </pre>
</div> </div>
</div> </div>
)} )}
{!workspaceCustomScripts && ( {!projectCustomScripts && (
<div className="rounded-md border border-slate-200 bg-slate-50 p-3"> <div className="rounded-md border border-slate-200 bg-slate-50 p-3">
<p className="text-sm text-slate-500"> <p className="text-sm text-slate-500">
{t("environments.surveys.share.custom_html.no_workspace_scripts")} {t("environments.surveys.share.custom_html.no_workspace_scripts")}
@@ -163,6 +163,7 @@ export const PersonalLinksTab = ({
<UpgradePrompt <UpgradePrompt
title={t("environments.surveys.share.personal_links.upgrade_prompt_title")} title={t("environments.surveys.share.personal_links.upgrade_prompt_title")}
description={t("environments.surveys.share.personal_links.upgrade_prompt_description")} description={t("environments.surveys.share.personal_links.upgrade_prompt_description")}
feature="personal_links"
buttons={[ buttons={[
{ {
text: isFormbricksCloud ? t("common.upgrade_plan") : t("common.request_trial_license"), text: isFormbricksCloud ? t("common.upgrade_plan") : t("common.request_trial_license"),
@@ -1,8 +1,8 @@
import { ResourceNotFoundError } from "@formbricks/types/errors"; import { ResourceNotFoundError } from "@formbricks/types/errors";
import { getPublicDomain } from "@/lib/getPublicUrl"; import { getPublicDomain } from "@/lib/getPublicUrl";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { getSurvey } from "@/lib/survey/service"; import { getSurvey } from "@/lib/survey/service";
import { getStyling } from "@/lib/utils/styling"; import { getStyling } from "@/lib/utils/styling";
import { getWorkspaceByEnvironmentId } from "@/lib/workspace/service";
import { getTranslate } from "@/lingodotdev/server"; import { getTranslate } from "@/lingodotdev/server";
import { getPreviewEmailTemplateHtml } from "@/modules/email/components/preview-email-template"; import { getPreviewEmailTemplateHtml } from "@/modules/email/components/preview-email-template";
@@ -12,12 +12,12 @@ export const getEmailTemplateHtml = async (surveyId: string, locale: string) =>
if (!survey) { if (!survey) {
throw new ResourceNotFoundError(t("common.survey"), surveyId); throw new ResourceNotFoundError(t("common.survey"), surveyId);
} }
const workspace = await getWorkspaceByEnvironmentId(survey.environmentId); const project = await getProjectByEnvironmentId(survey.environmentId);
if (!workspace) { if (!project) {
throw new Error("Workspace not found"); throw new ResourceNotFoundError(t("common.workspace"), null);
} }
const styling = getStyling(workspace, survey); const styling = getStyling(project, survey);
const surveyUrl = getPublicDomain() + "/s/" + survey.id; const surveyUrl = getPublicDomain() + "/s/" + survey.id;
const html = await getPreviewEmailTemplateHtml(survey, surveyUrl, styling, locale, t); const html = await getPreviewEmailTemplateHtml(survey, surveyUrl, styling, locale, t);
const doctype = const doctype =
@@ -2,10 +2,10 @@ import { Prisma } from "@prisma/client";
import { beforeEach, describe, expect, test, vi } from "vitest"; import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database"; import { prisma } from "@formbricks/database";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TLanguage } from "@formbricks/types/project";
import { TResponseFilterCriteria } from "@formbricks/types/responses"; import { TResponseFilterCriteria } from "@formbricks/types/responses";
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements"; import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys/types"; import { TSurvey, TSurveySummary } from "@formbricks/types/surveys/types";
import { TLanguage } from "@formbricks/types/workspace";
import { getQuotasSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/survey"; import { getQuotasSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/survey";
import { getDisplayCountBySurveyId } from "@/lib/display/service"; import { getDisplayCountBySurveyId } from "@/lib/display/service";
import { getLocalizedValue } from "@/lib/i18n/utils"; import { getLocalizedValue } from "@/lib/i18n/utils";
@@ -99,7 +99,7 @@ const mockBaseSurvey: TSurvey = {
createdBy: "user_123", createdBy: "user_123",
isSingleResponsePerEmailEnabled: false, isSingleResponsePerEmailEnabled: false,
isVerifyEmailEnabled: false, isVerifyEmailEnabled: false,
workspaceOverwrites: null, projectOverwrites: null,
showLanguageSwitch: false, showLanguageSwitch: false,
isBackButtonHidden: false, isBackButtonHidden: false,
followUps: [], followUps: [],

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