mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-12 19:03:42 -05:00
Compare commits
37 Commits
revert/rem
...
feat/unify
| 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 |
9
.codex/environments/environment.toml
Normal file
9
.codex/environments/environment.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
|
||||
version = 1
|
||||
name = "formbricks"
|
||||
|
||||
[setup]
|
||||
script = '''
|
||||
pnpm install
|
||||
pnpm dev:setup
|
||||
'''
|
||||
48
.env.example
48
.env.example
@@ -38,15 +38,6 @@ LOG_LEVEL=info
|
||||
|
||||
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 #
|
||||
################
|
||||
@@ -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_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_AUTH_DISABLED=1
|
||||
|
||||
@@ -141,6 +138,31 @@ AZUREAD_CLIENT_ID=
|
||||
AZUREAD_CLIENT_SECRET=
|
||||
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
|
||||
# OIDC_CLIENT_ID=
|
||||
# OIDC_CLIENT_SECRET=
|
||||
@@ -194,6 +216,14 @@ ENTERPRISE_LICENSE_KEY=
|
||||
# Ignore Rate Limiting across the Formbricks app
|
||||
# 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)
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
||||
# OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -45,7 +45,7 @@ yarn-error.log*
|
||||
.direnv
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
**/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
26
README.md
26
README.md
@@ -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.
|
||||
|
||||
If you opt for self-hosting Formbricks, here are a few options to consider:
|
||||
|
||||
#### Docker
|
||||
|
||||
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
|
||||
|
||||
### 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.
|
||||
|
||||
<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 { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TWorkspaceConfigChannel } from "@formbricks/types/workspace";
|
||||
import { TProjectConfigChannel } from "@formbricks/types/project";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
||||
@@ -14,7 +14,7 @@ interface ConnectWithFormbricksProps {
|
||||
environment: TEnvironment;
|
||||
publicDomain: string;
|
||||
appSetupCompleted: boolean;
|
||||
channel: TWorkspaceConfigChannel;
|
||||
channel: TProjectConfigChannel;
|
||||
}
|
||||
|
||||
export const ConnectWithFormbricks = ({
|
||||
|
||||
@@ -5,7 +5,7 @@ import "prismjs/themes/prism.css";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TWorkspaceConfigChannel } from "@formbricks/types/workspace";
|
||||
import { TProjectConfigChannel } from "@formbricks/types/project";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { CodeBlock } from "@/modules/ui/components/code-block";
|
||||
import { Html5Icon, NpmIcon } from "@/modules/ui/components/icons";
|
||||
@@ -19,7 +19,7 @@ const tabs = [
|
||||
interface OnboardingSetupInstructionsProps {
|
||||
environmentId: string;
|
||||
publicDomain: string;
|
||||
channel: TWorkspaceConfigChannel;
|
||||
channel: TProjectConfigChannel;
|
||||
appSetupCompleted: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
|
||||
import { getEnvironment } from "@/lib/environment/service";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getWorkspaceByEnvironmentId } from "@/lib/workspace/service";
|
||||
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Header } from "@/modules/ui/components/header";
|
||||
@@ -24,12 +24,12 @@ const Page = async (props: ConnectPageProps) => {
|
||||
throw new ResourceNotFoundError(t("common.environment"), params.environmentId);
|
||||
}
|
||||
|
||||
const workspace = await getWorkspaceByEnvironmentId(environment.id);
|
||||
if (!workspace) {
|
||||
throw new Error(t("common.workspace_not_found"));
|
||||
const project = await getProjectByEnvironmentId(environment.id);
|
||||
if (!project) {
|
||||
throw new ResourceNotFoundError(t("common.workspace"), null);
|
||||
}
|
||||
|
||||
const channel = workspace.config.channel || null;
|
||||
const channel = project.config.channel || null;
|
||||
|
||||
const publicDomain = getPublicDomain();
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurveyCreateInput } from "@formbricks/types/surveys/types";
|
||||
import { TXMTemplate } from "@formbricks/types/templates";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { TWorkspace } from "@formbricks/types/workspace";
|
||||
import { replacePresetPlaceholders } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils";
|
||||
import { getXMTemplates } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates";
|
||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||
@@ -16,12 +16,12 @@ import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { createSurveyAction } from "@/modules/survey/components/template-list/actions";
|
||||
|
||||
interface XMTemplateListProps {
|
||||
workspace: TWorkspace;
|
||||
project: TProject;
|
||||
user: TUser;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export const XMTemplateList = ({ workspace, user, environmentId }: XMTemplateListProps) => {
|
||||
export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListProps) => {
|
||||
const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null);
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
@@ -48,7 +48,7 @@ export const XMTemplateList = ({ workspace, user, environmentId }: XMTemplateLis
|
||||
const handleTemplateClick = (templateIdx: number) => {
|
||||
setActiveTemplateId(templateIdx);
|
||||
const template = getXMTemplates(t)[templateIdx];
|
||||
const newTemplate = replacePresetPlaceholders(template, workspace);
|
||||
const newTemplate = replacePresetPlaceholders(template, project);
|
||||
createSurvey(newTemplate);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||
import { TXMTemplate } from "@formbricks/types/templates";
|
||||
import { TWorkspace } from "@formbricks/types/workspace";
|
||||
import { replacePresetPlaceholders } from "./utils";
|
||||
|
||||
// Mock data
|
||||
const mockWorkspace: TWorkspace = {
|
||||
id: "workspace1",
|
||||
const mockProject: TProject = {
|
||||
id: "project1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Test Workspace",
|
||||
name: "Test Project",
|
||||
organizationId: "org1",
|
||||
styling: {
|
||||
allowStyleOverwrite: true,
|
||||
@@ -32,7 +32,7 @@ const mockWorkspace: TWorkspace = {
|
||||
logo: null,
|
||||
};
|
||||
const mockTemplate: TXMTemplate = {
|
||||
name: "$[workspaceName] Survey",
|
||||
name: "$[projectName] Survey",
|
||||
blocks: [
|
||||
{
|
||||
id: "block1",
|
||||
@@ -42,7 +42,7 @@ const mockTemplate: TXMTemplate = {
|
||||
id: "q1",
|
||||
type: "openText" as TSurveyElementTypeEnum.OpenText,
|
||||
inputType: "text" as const,
|
||||
headline: { default: "$[workspaceName] Question" },
|
||||
headline: { default: "$[projectName] Question" },
|
||||
subheader: { default: "" },
|
||||
required: false,
|
||||
placeholder: { default: "" },
|
||||
@@ -70,19 +70,19 @@ describe("replacePresetPlaceholders", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("replaces workspaceName placeholder in template name", () => {
|
||||
const result = replacePresetPlaceholders(mockTemplate, mockWorkspace);
|
||||
expect(result.name).toBe("Test Workspace Survey");
|
||||
test("replaces projectName placeholder in template name", () => {
|
||||
const result = replacePresetPlaceholders(mockTemplate, mockProject);
|
||||
expect(result.name).toBe("Test Project Survey");
|
||||
});
|
||||
|
||||
test("replaces workspaceName placeholder in element headline", () => {
|
||||
const result = replacePresetPlaceholders(mockTemplate, mockWorkspace);
|
||||
expect(result.blocks[0].elements[0].headline.default).toBe("Test Workspace Question");
|
||||
test("replaces projectName placeholder in element headline", () => {
|
||||
const result = replacePresetPlaceholders(mockTemplate, mockProject);
|
||||
expect(result.blocks[0].elements[0].headline.default).toBe("Test Project Question");
|
||||
});
|
||||
|
||||
test("returns a new object without mutating the original template", () => {
|
||||
const originalTemplate = structuredClone(mockTemplate);
|
||||
const result = replacePresetPlaceholders(mockTemplate, mockWorkspace);
|
||||
const result = replacePresetPlaceholders(mockTemplate, mockProject);
|
||||
expect(result).not.toBe(mockTemplate);
|
||||
expect(mockTemplate).toEqual(originalTemplate);
|
||||
});
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
|
||||
import { TXMTemplate } from "@formbricks/types/templates";
|
||||
import { TWorkspace } from "@formbricks/types/workspace";
|
||||
import { replaceElementPresetPlaceholders } from "@/lib/utils/templates";
|
||||
|
||||
// replace all occurences of workspaceName with the actual workspace name in the current template
|
||||
export const replacePresetPlaceholders = (template: TXMTemplate, workspace: TWorkspace): TXMTemplate => {
|
||||
// replace all occurences of projectName with the actual project name in the current template
|
||||
export const replacePresetPlaceholders = (template: TXMTemplate, project: TProject): TXMTemplate => {
|
||||
const survey = structuredClone(template);
|
||||
|
||||
const modifiedBlocks = survey.blocks.map((block: TSurveyBlock) => ({
|
||||
...block,
|
||||
elements: block.elements.map((element) => replaceElementPresetPlaceholders(element, workspace)),
|
||||
elements: block.elements.map((element) => replaceElementPresetPlaceholders(element, project)),
|
||||
}));
|
||||
|
||||
return { ...survey, name: survey.name.replace("$[workspaceName]", workspace.name), blocks: modifiedBlocks };
|
||||
return { ...survey, name: survey.name.replace("$[projectName]", project.name), blocks: modifiedBlocks };
|
||||
};
|
||||
|
||||
@@ -4,9 +4,9 @@ import Link from "next/link";
|
||||
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
|
||||
import { getEnvironment } from "@/lib/environment/service";
|
||||
import { getProjectByEnvironmentId, getUserProjects } from "@/lib/project/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { getUserWorkspaces, getWorkspaceByEnvironmentId } from "@/lib/workspace/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -37,18 +37,18 @@ const Page = async (props: XMTemplatePageProps) => {
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
|
||||
|
||||
const workspace = await getWorkspaceByEnvironmentId(environment.id);
|
||||
if (!workspace) {
|
||||
throw new Error(t("common.workspace_not_found"));
|
||||
const project = await getProjectByEnvironmentId(environment.id);
|
||||
if (!project) {
|
||||
throw new ResourceNotFoundError(t("common.workspace"), null);
|
||||
}
|
||||
|
||||
const workspaces = await getUserWorkspaces(session.user.id, organizationId);
|
||||
const projects = await getUserProjects(session.user.id, organizationId);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
<Header title={t("environments.xm-templates.headline")} />
|
||||
<XMTemplateList workspace={workspace} user={user} environmentId={environment.id} />
|
||||
{workspaces.length >= 2 && (
|
||||
<XMTemplateList project={project} user={user} environmentId={environment.id} />
|
||||
{projects.length >= 2 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getServerSession } from "next-auth";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getEnvironments } from "@/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getUserWorkspaces } from "@/lib/workspace/service";
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
|
||||
const LandingLayout = async (props: {
|
||||
@@ -24,11 +24,11 @@ const LandingLayout = async (props: {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const workspaces = await getUserWorkspaces(session.user.id, params.organizationId);
|
||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
||||
|
||||
if (workspaces.length !== 0) {
|
||||
const firstWorkspace = workspaces[0];
|
||||
const environments = await getEnvironments(firstWorkspace.id);
|
||||
if (projects.length !== 0) {
|
||||
const firstProject = projects[0];
|
||||
const environments = await getEnvironments(firstProject.id);
|
||||
const prodEnvironment = environments.find((e) => e.type === "production");
|
||||
|
||||
if (prodEnvironment) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
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 { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
@@ -26,7 +26,8 @@ const Page = async (props: { params: Promise<{ organizationId: string }> }) => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
|
||||
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 (
|
||||
<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 h-full flex-col">
|
||||
<div className="p-6">
|
||||
{/* we only need to render organization breadcrumb on this page, organizations/workspaces are lazy-loaded */}
|
||||
<WorkspaceAndOrgSwitch
|
||||
{/* we only need to render organization breadcrumb on this page, organizations/projects are lazy-loaded */}
|
||||
<ProjectAndOrgSwitch
|
||||
currentOrganizationId={organization.id}
|
||||
currentOrganizationName={organization.name}
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
organizationWorkspacesLimit={0}
|
||||
organizationProjectsLimit={0}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
isLicenseActive={false}
|
||||
isOwnerOrManager={false}
|
||||
isAccessControlAllowed={false}
|
||||
isMember={isMember}
|
||||
isBilling={isBilling}
|
||||
isMembershipPending={isMembershipPending}
|
||||
environments={[]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getTranslate } from "@/lingodotdev/server";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
||||
|
||||
const WorkspaceOnboardingLayout = async (props: {
|
||||
const ProjectOnboardingLayout = async (props: {
|
||||
params: Promise<{ organizationId: string }>;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
@@ -47,4 +47,4 @@ const WorkspaceOnboardingLayout = async (props: {
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceOnboardingLayout;
|
||||
export default ProjectOnboardingLayout;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||
import { getUserWorkspaces } from "@/lib/workspace/service";
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -39,7 +39,7 @@ const Page = async (props: ChannelPageProps) => {
|
||||
},
|
||||
];
|
||||
|
||||
const workspaces = await getUserWorkspaces(session.user.id, params.organizationId);
|
||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
@@ -48,7 +48,7 @@ const Page = async (props: ChannelPageProps) => {
|
||||
subtitle={t("organizations.workspaces.new.channel.channel_select_subtitle")}
|
||||
/>
|
||||
<OnboardingOptionsContainer options={channelOptions} />
|
||||
{workspaces.length >= 1 && (
|
||||
{projects.length >= 1 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
|
||||
@@ -4,10 +4,10 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { getOrganizationWorkspacesCount } from "@/lib/workspace/service";
|
||||
import { getOrganizationProjectsCount } from "@/lib/project/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getOrganizationWorkspacesLimit } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||
|
||||
const OnboardingLayout = async (props: {
|
||||
params: Promise<{ organizationId: string }>;
|
||||
@@ -32,12 +32,12 @@ const OnboardingLayout = async (props: {
|
||||
throw new ResourceNotFoundError(t("common.organization"), params.organizationId);
|
||||
}
|
||||
|
||||
const [organizationWorkspacesLimit, organizationWorkspacesCount] = await Promise.all([
|
||||
getOrganizationWorkspacesLimit(organization.id),
|
||||
getOrganizationWorkspacesCount(organization.id),
|
||||
const [organizationProjectsLimit, organizationProjectsCount] = await Promise.all([
|
||||
getOrganizationProjectsLimit(organization.id),
|
||||
getOrganizationProjectsCount(organization.id),
|
||||
]);
|
||||
|
||||
if (organizationWorkspacesCount >= organizationWorkspacesLimit) {
|
||||
if (organizationProjectsCount >= organizationProjectsLimit) {
|
||||
return redirect(`/`);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||
import { getUserWorkspaces } from "@/lib/workspace/service";
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -39,13 +39,13 @@ const Page = async (props: ModePageProps) => {
|
||||
},
|
||||
];
|
||||
|
||||
const workspaces = await getUserWorkspaces(session.user.id, params.organizationId);
|
||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
<Header title={t("organizations.workspaces.new.mode.what_are_you_here_for")} />
|
||||
<OnboardingOptionsContainer options={channelOptions} />
|
||||
{workspaces.length >= 1 && (
|
||||
{projects.length >= 1 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
|
||||
@@ -8,19 +8,19 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
TWorkspaceConfigChannel,
|
||||
TWorkspaceConfigIndustry,
|
||||
TWorkspaceMode,
|
||||
TWorkspaceUpdateInput,
|
||||
ZWorkspaceUpdateInput,
|
||||
} from "@formbricks/types/workspace";
|
||||
import { createWorkspaceAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
TProjectConfigChannel,
|
||||
TProjectConfigIndustry,
|
||||
TProjectMode,
|
||||
TProjectUpdateInput,
|
||||
ZProjectUpdateInput,
|
||||
} from "@formbricks/types/project";
|
||||
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import { previewSurvey } from "@/app/lib/templates";
|
||||
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@/lib/localStorage";
|
||||
import { buildStylingFromBrandColor } from "@/lib/styling/constants";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
|
||||
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
|
||||
import { TOrganizationTeam } from "@/modules/ee/teams/workspace-teams/types/team";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { ColorPicker } from "@/modules/ui/components/color-picker";
|
||||
import {
|
||||
@@ -36,34 +36,34 @@ import { Input } from "@/modules/ui/components/input";
|
||||
import { MultiSelect } from "@/modules/ui/components/multi-select";
|
||||
import { SurveyInline } from "@/modules/ui/components/survey";
|
||||
|
||||
interface WorkspaceSettingsProps {
|
||||
interface ProjectSettingsProps {
|
||||
organizationId: string;
|
||||
workspaceMode: TWorkspaceMode;
|
||||
channel: TWorkspaceConfigChannel;
|
||||
industry: TWorkspaceConfigIndustry;
|
||||
projectMode: TProjectMode;
|
||||
channel: TProjectConfigChannel;
|
||||
industry: TProjectConfigIndustry;
|
||||
defaultBrandColor: string;
|
||||
organizationTeams: TOrganizationTeam[];
|
||||
isAccessControlAllowed: boolean;
|
||||
userWorkspacesCount: number;
|
||||
userProjectsCount: number;
|
||||
publicDomain: string;
|
||||
}
|
||||
|
||||
export const WorkspaceSettings = ({
|
||||
export const ProjectSettings = ({
|
||||
organizationId,
|
||||
workspaceMode,
|
||||
projectMode,
|
||||
channel,
|
||||
industry,
|
||||
defaultBrandColor,
|
||||
organizationTeams,
|
||||
isAccessControlAllowed = false,
|
||||
userWorkspacesCount,
|
||||
userProjectsCount,
|
||||
publicDomain,
|
||||
}: WorkspaceSettingsProps) => {
|
||||
}: ProjectSettingsProps) => {
|
||||
const [createTeamModalOpen, setCreateTeamModalOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const addWorkspace = async (data: TWorkspaceUpdateInput) => {
|
||||
const addProject = async (data: TProjectUpdateInput) => {
|
||||
try {
|
||||
// Build the full styling from the chosen brand color so all derived
|
||||
// colours (question, button, input, option, progress, etc.) are persisted.
|
||||
@@ -71,7 +71,7 @@ export const WorkspaceSettings = ({
|
||||
// back to STYLE_DEFAULTS computed from the default brand (#64748b).
|
||||
const fullStyling = buildStylingFromBrandColor(data.styling?.brandColor?.light);
|
||||
|
||||
const createWorkspaceResponse = await createWorkspaceAction({
|
||||
const createProjectResponse = await createProjectAction({
|
||||
organizationId,
|
||||
data: {
|
||||
...data,
|
||||
@@ -81,14 +81,14 @@ export const WorkspaceSettings = ({
|
||||
},
|
||||
});
|
||||
|
||||
if (createWorkspaceResponse?.data) {
|
||||
if (createProjectResponse?.data) {
|
||||
// get production environment
|
||||
const productionEnvironment = createWorkspaceResponse.data.environments.find(
|
||||
(environment: { type: string }) => environment.type === "production"
|
||||
const productionEnvironment = createProjectResponse.data.environments.find(
|
||||
(environment) => environment.type === "production"
|
||||
);
|
||||
if (productionEnvironment) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -96,11 +96,11 @@ export const WorkspaceSettings = ({
|
||||
router.push(`/environments/${productionEnvironment?.id}/connect`);
|
||||
} else if (channel === "link") {
|
||||
router.push(`/environments/${productionEnvironment?.id}/surveys`);
|
||||
} else if (workspaceMode === "cx") {
|
||||
} else if (projectMode === "cx") {
|
||||
router.push(`/environments/${productionEnvironment?.id}/xm-templates`);
|
||||
}
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createWorkspaceResponse);
|
||||
const errorMessage = getFormattedErrorMessage(createProjectResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -109,15 +109,15 @@ export const WorkspaceSettings = ({
|
||||
}
|
||||
};
|
||||
|
||||
const form = useForm<TWorkspaceUpdateInput>({
|
||||
const form = useForm<TProjectUpdateInput>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
styling: { allowStyleOverwrite: true, brandColor: { light: defaultBrandColor } },
|
||||
teamIds: [],
|
||||
},
|
||||
resolver: zodResolver(ZWorkspaceUpdateInput),
|
||||
resolver: zodResolver(ZProjectUpdateInput),
|
||||
});
|
||||
const workspaceName = form.watch("name");
|
||||
const projectName = form.watch("name");
|
||||
const logoUrl = form.watch("logo.url");
|
||||
const brandColor = form.watch("styling.brandColor.light") ?? defaultBrandColor;
|
||||
const previewStyling = useMemo(() => buildStylingFromBrandColor(brandColor), [brandColor]);
|
||||
@@ -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="flex w-1/2 flex-col space-y-4">
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(addWorkspace)} className="w-full space-y-4">
|
||||
<form onSubmit={form.handleSubmit(addProject)} className="w-full space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="styling.brandColor.light"
|
||||
@@ -184,7 +184,7 @@ export const WorkspaceSettings = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
{isAccessControlAllowed && userWorkspacesCount > 0 && (
|
||||
{isAccessControlAllowed && userProjectsCount > 0 && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="teamIds"
|
||||
@@ -242,7 +242,7 @@ export const WorkspaceSettings = ({
|
||||
<SurveyInline
|
||||
appUrl={publicDomain}
|
||||
isPreviewMode={true}
|
||||
survey={previewSurvey(workspaceName || t("common.my_product"), t)}
|
||||
survey={previewSurvey(projectName || t("common.my_product"), t)}
|
||||
styling={previewStyling}
|
||||
isBrandingEnabled={false}
|
||||
languageCode="default"
|
||||
@@ -2,34 +2,30 @@ import { XIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import {
|
||||
TWorkspaceConfigChannel,
|
||||
TWorkspaceConfigIndustry,
|
||||
TWorkspaceMode,
|
||||
} from "@formbricks/types/workspace";
|
||||
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
||||
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
||||
import { WorkspaceSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/settings/components/WorkspaceSettings";
|
||||
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/settings/components/ProjectSettings";
|
||||
import { DEFAULT_BRAND_COLOR } from "@/lib/constants";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getUserWorkspaces } from "@/lib/workspace/service";
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Header } from "@/modules/ui/components/header";
|
||||
|
||||
interface WorkspaceSettingsPageProps {
|
||||
interface ProjectSettingsPageProps {
|
||||
params: Promise<{
|
||||
organizationId: string;
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
channel?: TWorkspaceConfigChannel;
|
||||
industry?: TWorkspaceConfigIndustry;
|
||||
mode?: TWorkspaceMode;
|
||||
channel?: TProjectConfigChannel;
|
||||
industry?: TProjectConfigIndustry;
|
||||
mode?: TProjectMode;
|
||||
}>;
|
||||
}
|
||||
|
||||
const Page = async (props: WorkspaceSettingsPageProps) => {
|
||||
const Page = async (props: ProjectSettingsPageProps) => {
|
||||
const searchParams = await props.searchParams;
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
@@ -43,7 +39,7 @@ const Page = async (props: WorkspaceSettingsPageProps) => {
|
||||
const channel = searchParams.channel ?? null;
|
||||
const industry = searchParams.industry ?? null;
|
||||
const mode = searchParams.mode ?? "surveys";
|
||||
const workspaces = await getUserWorkspaces(session.user.id, params.organizationId);
|
||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
||||
|
||||
const organizationTeams = await getTeamsByOrganizationId(params.organizationId);
|
||||
|
||||
@@ -61,18 +57,18 @@ const Page = async (props: WorkspaceSettingsPageProps) => {
|
||||
title={t("organizations.workspaces.new.settings.workspace_settings_title")}
|
||||
subtitle={t("organizations.workspaces.new.settings.workspace_settings_subtitle")}
|
||||
/>
|
||||
<WorkspaceSettings
|
||||
<ProjectSettings
|
||||
organizationId={params.organizationId}
|
||||
workspaceMode={mode}
|
||||
projectMode={mode}
|
||||
channel={channel}
|
||||
industry={industry}
|
||||
defaultBrandColor={DEFAULT_BRAND_COLOR}
|
||||
organizationTeams={organizationTeams}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
userWorkspacesCount={workspaces.length}
|
||||
userProjectsCount={projects.length}
|
||||
publicDomain={publicDomain}
|
||||
/>
|
||||
{workspaces.length >= 1 && (
|
||||
{projects.length >= 1 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
|
||||
@@ -0,0 +1,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 { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { getOrganizationIdFromSurveyId, getWorkspaceIdFromSurveyId } from "@/lib/utils/helper";
|
||||
import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper";
|
||||
import { getSurveySummary } from "./summary/lib/surveySummary";
|
||||
|
||||
export const revalidateSurveyIdPath = async (environmentId: string, surveyId: string) => {
|
||||
@@ -36,9 +36,9 @@ export const getResponsesAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "read",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -70,9 +70,9 @@ export const getSurveySummaryAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "read",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -98,9 +98,9 @@ export const getResponseCountAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "read",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -126,9 +126,9 @@ export const getDisplaysWithContactAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "read",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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 { getResponsesDownloadUrlAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
|
||||
import { downloadResponsesFile } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/utils";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { deleteResponseAction } from "@/modules/analysis/components/SingleResponseCard/actions";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import {
|
||||
@@ -201,7 +202,13 @@ export const ResponseTable = ({
|
||||
};
|
||||
|
||||
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
|
||||
@@ -8,7 +8,7 @@ import { getSurvey, updateSurvey } from "@/lib/survey/service";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { convertToCsv } from "@/lib/utils/file-conversion";
|
||||
import { getOrganizationIdFromSurveyId, getWorkspaceIdFromSurveyId } from "@/lib/utils/helper";
|
||||
import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import { generatePersonalLinks } from "@/modules/ee/contacts/lib/contacts";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
@@ -35,9 +35,9 @@ export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "read",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -64,13 +64,13 @@ export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient
|
||||
|
||||
const ZResetSurveyAction = z.object({
|
||||
surveyId: ZId,
|
||||
workspaceId: ZId,
|
||||
projectId: ZId,
|
||||
});
|
||||
|
||||
export const resetSurveyAction = authenticatedActionClient.inputSchema(ZResetSurveyAction).action(
|
||||
withAuditLogging("updated", "survey", async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
|
||||
const workspaceId = await getWorkspaceIdFromSurveyId(parsedInput.surveyId);
|
||||
const projectId = await getProjectIdFromSurveyId(parsedInput.surveyId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -81,9 +81,9 @@ export const resetSurveyAction = authenticatedActionClient.inputSchema(ZResetSur
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
workspaceId,
|
||||
projectId,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -125,9 +125,9 @@ export const getEmailHtmlAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -160,8 +160,8 @@ export const generatePersonalLinksAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
@@ -234,8 +234,8 @@ export const updateSingleUseLinksAction = authenticatedActionClient
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
@@ -64,7 +64,7 @@ export const SurveyAnalysisCTA = ({
|
||||
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
|
||||
const { workspace } = useEnvironment();
|
||||
const { project } = useEnvironment();
|
||||
const { refreshSingleUseId } = useSingleUseId(survey, isReadOnly);
|
||||
|
||||
const appSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
|
||||
@@ -128,7 +128,7 @@ export const SurveyAnalysisCTA = ({
|
||||
setIsResetting(true);
|
||||
const result = await resetSurveyAction({
|
||||
surveyId: survey.id,
|
||||
workspaceId: workspace.id,
|
||||
projectId: project.id,
|
||||
});
|
||||
if (result?.data) {
|
||||
toast.success(
|
||||
@@ -212,7 +212,7 @@ export const SurveyAnalysisCTA = ({
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isReadOnly={isReadOnly}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
workspaceCustomScripts={workspace.customHeadScripts}
|
||||
projectCustomScripts={project.customHeadScripts}
|
||||
/>
|
||||
)}
|
||||
<SuccessMessage environment={environment} survey={survey} />
|
||||
@@ -53,7 +53,7 @@ interface ShareSurveyModalProps {
|
||||
isFormbricksCloud: boolean;
|
||||
isReadOnly: boolean;
|
||||
isStorageConfigured: boolean;
|
||||
workspaceCustomScripts?: string | null;
|
||||
projectCustomScripts?: string | null;
|
||||
}
|
||||
|
||||
export const ShareSurveyModal = ({
|
||||
@@ -68,7 +68,7 @@ export const ShareSurveyModal = ({
|
||||
isFormbricksCloud,
|
||||
isReadOnly,
|
||||
isStorageConfigured,
|
||||
workspaceCustomScripts,
|
||||
projectCustomScripts,
|
||||
}: ShareSurveyModalProps) => {
|
||||
const environmentId = survey.environmentId;
|
||||
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"),
|
||||
description: t("environments.surveys.share.custom_html.description"),
|
||||
componentType: CustomHtmlTab,
|
||||
componentProps: { workspaceCustomScripts, isReadOnly },
|
||||
componentProps: { projectCustomScripts, isReadOnly },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -216,7 +216,7 @@ export const ShareSurveyModal = ({
|
||||
isFormbricksCloud,
|
||||
email,
|
||||
isStorageConfigured,
|
||||
workspaceCustomScripts,
|
||||
projectCustomScripts,
|
||||
]);
|
||||
|
||||
const getDefaultActiveId = useCallback(() => {
|
||||
@@ -88,7 +88,7 @@ const DisplayCriteriaItem = ({ icon, title, titleSuffix, description }: DisplayC
|
||||
|
||||
export const AppTab = () => {
|
||||
const { t } = useTranslation();
|
||||
const { environment, workspace } = useEnvironment();
|
||||
const { environment, project } = useEnvironment();
|
||||
const { survey } = useSurvey();
|
||||
|
||||
const documentationLinks = useMemo(() => createDocumentationLinks(t), [t]);
|
||||
@@ -98,8 +98,8 @@ export const AppTab = () => {
|
||||
if (survey.recontactDays !== null) {
|
||||
return formatRecontactDaysString(survey.recontactDays, t);
|
||||
}
|
||||
if (workspace.recontactDays !== null) {
|
||||
return formatRecontactDaysString(workspace.recontactDays, t);
|
||||
if (project.recontactDays !== null) {
|
||||
return formatRecontactDaysString(project.recontactDays, t);
|
||||
}
|
||||
return t("environments.surveys.summary.in_app.display_criteria.time_based_always");
|
||||
};
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import { TabToggle } from "@/modules/ui/components/tab-toggle";
|
||||
|
||||
interface CustomHtmlTabProps {
|
||||
workspaceCustomScripts: string | null | undefined;
|
||||
projectCustomScripts: string | null | undefined;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ interface CustomHtmlFormData {
|
||||
customHeadScriptsMode: TSurvey["customHeadScriptsMode"];
|
||||
}
|
||||
|
||||
export const CustomHtmlTab = ({ workspaceCustomScripts, isReadOnly }: CustomHtmlTabProps) => {
|
||||
export const CustomHtmlTab = ({ projectCustomScripts, isReadOnly }: CustomHtmlTabProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { survey } = useSurvey();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -101,18 +101,18 @@ export const CustomHtmlTab = ({ workspaceCustomScripts, isReadOnly }: CustomHtml
|
||||
</div>
|
||||
|
||||
{/* Workspace Scripts Preview */}
|
||||
{workspaceCustomScripts && (
|
||||
{projectCustomScripts && (
|
||||
<div className={scriptsMode === "replace" ? "opacity-50" : ""}>
|
||||
<FormLabel>{t("environments.surveys.share.custom_html.workspace_scripts_label")}</FormLabel>
|
||||
<div className="mt-2 max-h-32 overflow-auto rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||
<pre className="whitespace-pre-wrap font-mono text-xs text-slate-600">
|
||||
{workspaceCustomScripts}
|
||||
{projectCustomScripts}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!workspaceCustomScripts && (
|
||||
{!projectCustomScripts && (
|
||||
<div className="rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||
<p className="text-sm text-slate-500">
|
||||
{t("environments.surveys.share.custom_html.no_workspace_scripts")}
|
||||
@@ -163,6 +163,7 @@ export const PersonalLinksTab = ({
|
||||
<UpgradePrompt
|
||||
title={t("environments.surveys.share.personal_links.upgrade_prompt_title")}
|
||||
description={t("environments.surveys.share.personal_links.upgrade_prompt_description")}
|
||||
feature="personal_links"
|
||||
buttons={[
|
||||
{
|
||||
text: isFormbricksCloud ? t("common.upgrade_plan") : t("common.request_trial_license"),
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { getStyling } from "@/lib/utils/styling";
|
||||
import { getWorkspaceByEnvironmentId } from "@/lib/workspace/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getPreviewEmailTemplateHtml } from "@/modules/email/components/preview-email-template";
|
||||
|
||||
@@ -12,12 +12,12 @@ export const getEmailTemplateHtml = async (surveyId: string, locale: string) =>
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError(t("common.survey"), surveyId);
|
||||
}
|
||||
const workspace = await getWorkspaceByEnvironmentId(survey.environmentId);
|
||||
if (!workspace) {
|
||||
throw new Error("Workspace not found");
|
||||
const project = await getProjectByEnvironmentId(survey.environmentId);
|
||||
if (!project) {
|
||||
throw new ResourceNotFoundError(t("common.workspace"), null);
|
||||
}
|
||||
|
||||
const styling = getStyling(workspace, survey);
|
||||
const styling = getStyling(project, survey);
|
||||
const surveyUrl = getPublicDomain() + "/s/" + survey.id;
|
||||
const html = await getPreviewEmailTemplateHtml(survey, surveyUrl, styling, locale, t);
|
||||
const doctype =
|
||||
@@ -2,10 +2,10 @@ import { Prisma } from "@prisma/client";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TLanguage } from "@formbricks/types/project";
|
||||
import { TResponseFilterCriteria } from "@formbricks/types/responses";
|
||||
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
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 { getDisplayCountBySurveyId } from "@/lib/display/service";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
@@ -99,7 +99,7 @@ const mockBaseSurvey: TSurvey = {
|
||||
createdBy: "user_123",
|
||||
isSingleResponsePerEmailEnabled: false,
|
||||
isVerifyEmailEnabled: false,
|
||||
workspaceOverwrites: null,
|
||||
projectOverwrites: null,
|
||||
showLanguageSwitch: false,
|
||||
isBackButtonHidden: false,
|
||||
followUps: [],
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user