mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-03 03:14:34 -05:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f356e86729 | |||
| 1a02f91afd | |||
| cc22ccb22d | |||
| 12763f0ef6 | |||
| d39e3ee638 | |||
| d85242a86b | |||
| ef53065abc | |||
| 805c1c6874 | |||
| 01687e8907 | |||
| 31d455002d | |||
| d96304d86d | |||
| 1064f68435 | |||
| 3d16e859c6 | |||
| af198c5632 | |||
| a43ed2b25c | |||
| 87bcad2b20 | |||
| b5eaa4c7fd | |||
| 995c03bc01 | |||
| b4395a48c5 | |||
| 461e3893fe | |||
| 735a9f84ec | |||
| 8cb8d734cf | |||
| 44d5530b48 | |||
| a314eb391e | |||
| 6c34c316d0 | |||
| 4f26278f16 | |||
| b975e7fa2e | |||
| 6c3052f9e4 | |||
| 5bb8119ebf | |||
| 02411277d4 | |||
| 4cfb8c6d7b | |||
| e74a51a5ff | |||
| 29cc6a10fe | |||
| 01f765e969 | |||
| 9366960f18 | |||
| 697dc9cc99 | |||
| 83bc272ed2 |
@@ -0,0 +1,9 @@
|
|||||||
|
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
|
||||||
|
version = 1
|
||||||
|
name = "formbricks"
|
||||||
|
|
||||||
|
[setup]
|
||||||
|
script = '''
|
||||||
|
pnpm install
|
||||||
|
pnpm dev:setup
|
||||||
|
'''
|
||||||
+39
-9
@@ -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
@@ -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/
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|
||||||
[](https://railway.app/new/template/PPDzCd)
|
|
||||||
|
|
||||||
##### RepoCloud
|
|
||||||
|
|
||||||
Or you can also deploy Formbricks on [RepoCloud](https://repocloud.io) using the button below.
|
|
||||||
|
|
||||||
[](https://repocloud.io/details/?app_id=254)
|
|
||||||
|
|
||||||
##### Zeabur
|
|
||||||
|
|
||||||
Or you can also deploy Formbricks on [Zeabur](https://zeabur.com) using the button below.
|
|
||||||
|
|
||||||
[](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>
|
||||||
|
|||||||
+2
-2
@@ -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 = ({
|
||||||
|
|||||||
+2
-2
@@ -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();
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+13
-13
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-5
@@ -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;
|
||||||
|
|||||||
+3
-3
@@ -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"
|
||||||
|
|||||||
+6
-6
@@ -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(`/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -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"
|
||||||
|
|||||||
+31
-31
@@ -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"
|
||||||
+13
-17
@@ -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;
|
||||||
+9
-9
@@ -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),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
+8
-1
@@ -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
|
||||||
+13
-13
@@ -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",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
+3
-3
@@ -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} />
|
||||||
+4
-4
@@ -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(() => {
|
||||||
+3
-3
@@ -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");
|
||||||
};
|
};
|
||||||
+5
-5
@@ -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")}
|
||||||
+1
@@ -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"),
|
||||||
+5
-5
@@ -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
-2
@@ -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
Reference in New Issue
Block a user