Compare commits

..

1 Commits

Author SHA1 Message Date
Matti Nannt
fa9b5a480c chore: add codex environment config 2026-03-25 11:25:55 +01:00
500 changed files with 5407 additions and 8461 deletions

View File

@@ -0,0 +1,9 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "formbricks"
[setup]
script = '''
pnpm install
pnpm dev:setup
'''

View File

@@ -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 #
################

View File

@@ -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 = ({

View File

@@ -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;
}

View File

@@ -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) {
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();

View File

@@ -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);
};

View File

@@ -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);
});

View File

@@ -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 };
};

View File

@@ -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) {
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"

View File

@@ -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) {

View File

@@ -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";
@@ -34,12 +34,12 @@ 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}

View File

@@ -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;

View File

@@ -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"

View File

@@ -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(`/`);
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -7,29 +7,29 @@ import {
OperationNotAllowedError,
ResourceNotFoundError,
} from "@formbricks/types/errors";
import { ZWorkspaceUpdateInput } from "@formbricks/types/workspace";
import { ZProjectUpdateInput } from "@formbricks/types/project";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getOrganization } from "@/lib/organization/service";
import { getOrganizationProjectsCount } from "@/lib/project/service";
import { updateUser } from "@/lib/user/service";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { getOrganizationWorkspacesCount } from "@/lib/workspace/service";
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
import {
getAccessControlPermission,
getOrganizationWorkspacesLimit,
getOrganizationProjectsLimit,
} from "@/modules/ee/license-check/lib/utils";
import { createWorkspace } from "@/modules/workspaces/settings/lib/workspace";
import { createProject } from "@/modules/projects/settings/lib/project";
import { getOrganizationsByUserId } from "./lib/organization";
import { getWorkspacesByUserId } from "./lib/workspace";
import { getProjectsByUserId } from "./lib/project";
const ZCreateWorkspaceAction = z.object({
const ZCreateProjectAction = z.object({
organizationId: ZId,
data: ZWorkspaceUpdateInput,
data: ZProjectUpdateInput,
});
export const createWorkspaceAction = authenticatedActionClient.inputSchema(ZCreateWorkspaceAction).action(
withAuditLogging("created", "workspace", async ({ ctx, parsedInput }) => {
export const createProjectAction = authenticatedActionClient.inputSchema(ZCreateProjectAction).action(
withAuditLogging("created", "project", async ({ ctx, parsedInput }) => {
const { user } = ctx;
const organizationId = parsedInput.organizationId;
@@ -40,7 +40,7 @@ export const createWorkspaceAction = authenticatedActionClient.inputSchema(ZCrea
access: [
{
data: parsedInput.data,
schema: ZWorkspaceUpdateInput,
schema: ZProjectUpdateInput,
type: "organization",
roles: ["owner", "manager"],
},
@@ -53,10 +53,10 @@ export const createWorkspaceAction = authenticatedActionClient.inputSchema(ZCrea
throw new ResourceNotFoundError("Organization", organizationId);
}
const organizationWorkspacesLimit = await getOrganizationWorkspacesLimit(organization.id);
const organizationWorkspacesCount = await getOrganizationWorkspacesCount(organization.id);
const organizationProjectsLimit = await getOrganizationProjectsLimit(organization.id);
const organizationProjectsCount = await getOrganizationProjectsCount(organization.id);
if (organizationWorkspacesCount >= organizationWorkspacesLimit) {
if (organizationProjectsCount >= organizationProjectsLimit) {
throw new OperationNotAllowedError("Organization workspace limit reached");
}
@@ -68,7 +68,7 @@ export const createWorkspaceAction = authenticatedActionClient.inputSchema(ZCrea
}
}
const workspace = await createWorkspace(parsedInput.organizationId, parsedInput.data);
const project = await createProject(parsedInput.organizationId, parsedInput.data);
const updatedNotificationSettings = {
...user.notificationSettings,
alert: {
@@ -81,9 +81,9 @@ export const createWorkspaceAction = authenticatedActionClient.inputSchema(ZCrea
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.workspaceId = workspace.id;
ctx.auditLoggingCtx.newObject = workspace;
return workspace;
ctx.auditLoggingCtx.projectId = project.id;
ctx.auditLoggingCtx.newObject = project;
return project;
})
);
@@ -112,16 +112,16 @@ export const getOrganizationsForSwitcherAction = authenticatedActionClient
return await getOrganizationsByUserId(ctx.user.id);
});
const ZGetWorkspacesForSwitcherAction = z.object({
const ZGetProjectsForSwitcherAction = z.object({
organizationId: ZId, // Changed from environmentId to avoid extra query
});
/**
* Fetches projects list for switcher dropdown.
* Called on-demand when user opens the workspace switcher.
* Called on-demand when user opens the project switcher.
*/
export const getWorkspacesForSwitcherAction = authenticatedActionClient
.inputSchema(ZGetWorkspacesForSwitcherAction)
export const getProjectsForSwitcherAction = authenticatedActionClient
.inputSchema(ZGetProjectsForSwitcherAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
@@ -134,11 +134,11 @@ export const getWorkspacesForSwitcherAction = authenticatedActionClient
],
});
// Need membership for getWorkspacesByUserId (1 DB query)
// Need membership for getProjectsByUserId (1 DB query)
const membership = await getMembershipByUserIdOrganizationId(ctx.user.id, parsedInput.organizationId);
if (!membership) {
throw new AuthorizationError("Membership not found");
}
return await getWorkspacesByUserId(ctx.user.id, membership);
return await getProjectsByUserId(ctx.user.id, membership);
});

View File

@@ -5,7 +5,7 @@ import { IS_DEVELOPMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getPublicDomain } from "@/lib/getPublicUrl";
import { getAccessFlags } from "@/lib/membership/utils";
import { getTranslate } from "@/lingodotdev/server";
import { getOrganizationWorkspacesLimit } from "@/modules/ee/license-check/lib/utils";
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
import { TEnvironmentLayoutData } from "@/modules/environments/types/environment-auth";
import { LimitsReachedBanner } from "@/modules/ui/components/limits-reached-banner";
import { PendingDowngradeBanner } from "@/modules/ui/components/pending-downgrade-banner";
@@ -25,10 +25,10 @@ export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLay
environment,
organization,
membership,
workspace, // Current workspace details
environments, // All workspace environments (for environment switcher)
project, // Current project details
environments, // All project environments (for environment switcher)
isAccessControlAllowed,
workspacePermission,
projectPermission,
license,
responseCount,
} = layoutData;
@@ -38,11 +38,11 @@ export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLay
const { features, lastChecked, isPendingDowngrade, active, status } = license;
const isMultiOrgEnabled = features?.isMultiOrgEnabled ?? false;
const organizationWorkspacesLimit = await getOrganizationWorkspacesLimit(organization.id);
const organizationProjectsLimit = await getOrganizationProjectsLimit(organization.id);
const isOwnerOrManager = isOwner || isManager;
// Validate that workspace permission exists for members
if (isMember && !workspacePermission) {
// Validate that project permission exists for members
if (isMember && !projectPermission) {
throw new ResourceNotFoundError(t("common.workspace"), null);
}
@@ -70,7 +70,7 @@ export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLay
environment={environment}
organization={organization}
user={user}
workspace={{ id: workspace.id, name: workspace.name }}
project={{ id: project.id, name: project.name }}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
isDevelopment={IS_DEVELOPMENT}
membershipRole={membership.role}
@@ -80,9 +80,9 @@ export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLay
<TopControlBar
environments={environments}
currentOrganizationId={organization.id}
currentWorkspaceId={workspace.id}
currentProjectId={project.id}
isMultiOrgEnabled={isMultiOrgEnabled}
organizationWorkspacesLimit={organizationWorkspacesLimit}
organizationProjectsLimit={organizationProjectsLimit}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
isLicenseActive={active}
isOwnerOrManager={isOwnerOrManager}

View File

@@ -29,6 +29,7 @@ import { cn } from "@/lib/cn";
import { getAccessFlags } from "@/lib/membership/utils";
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
import { TrialAlert } from "@/modules/ee/billing/components/trial-alert";
import { getLatestStableFbReleaseAction } from "@/modules/projects/settings/(setup)/app-connection/actions";
import { ProfileAvatar } from "@/modules/ui/components/avatars";
import { Button } from "@/modules/ui/components/button";
import {
@@ -37,14 +38,13 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/modules/ui/components/dropdown-menu";
import { getLatestStableFbReleaseAction } from "@/modules/workspaces/settings/(setup)/app-connection/actions";
import packageJson from "../../../../../package.json";
interface NavigationProps {
environment: TEnvironment;
user: TUser;
organization: TOrganization;
workspace: { id: string; name: string };
project: { id: string; name: string };
isFormbricksCloud: boolean;
isDevelopment: boolean;
membershipRole?: TOrganizationRole;
@@ -55,7 +55,7 @@ export const MainNavigation = ({
environment,
organization,
user,
workspace,
project,
membershipRole,
isFormbricksCloud,
isDevelopment,
@@ -92,7 +92,7 @@ export const MainNavigation = ({
}, [isCollapsed]);
useEffect(() => {
// Auto collapse workspace navbar on org and account settings
// Auto collapse project navbar on org and account settings
if (pathname?.includes("/settings")) {
setIsCollapsed(true);
}
@@ -186,7 +186,7 @@ export const MainNavigation = ({
return (
<>
{workspace && (
{project && (
<aside
className={cn(
"z-40 flex flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100",

View File

@@ -1,13 +1,13 @@
import Link from "next/link";
import { ReactNode } from "react";
interface WorkspaceNavItemProps {
interface ProjectNavItemProps {
href: string;
children: ReactNode;
isActive: boolean;
}
export const WorkspaceNavItem = ({ href, children, isActive }: WorkspaceNavItemProps) => {
export const ProjectNavItem = ({ href, children, isActive }: ProjectNavItemProps) => {
const activeClass = "bg-slate-50 font-semibold";
const inactiveClass = "hover:bg-slate-50";

View File

@@ -2,16 +2,16 @@
import { TEnvironment } from "@formbricks/types/environment";
import { TOrganizationRole } from "@formbricks/types/memberships";
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 { useEnvironment } from "@/app/(app)/environments/[environmentId]/context/environment-context";
import { getAccessFlags } from "@/lib/membership/utils";
interface TopControlBarProps {
environments: TEnvironment[];
currentOrganizationId: string;
currentWorkspaceId: string;
currentProjectId: string;
isMultiOrgEnabled: boolean;
organizationWorkspacesLimit: number;
organizationProjectsLimit: number;
isFormbricksCloud: boolean;
isLicenseActive: boolean;
isOwnerOrManager: boolean;
@@ -22,9 +22,9 @@ interface TopControlBarProps {
export const TopControlBar = ({
environments,
currentOrganizationId,
currentWorkspaceId,
currentProjectId,
isMultiOrgEnabled,
organizationWorkspacesLimit,
organizationProjectsLimit,
isFormbricksCloud,
isLicenseActive,
isOwnerOrManager,
@@ -38,13 +38,13 @@ export const TopControlBar = ({
<div
className="flex h-14 w-full items-center justify-between bg-slate-50 px-6"
data-testid="fb__global-top-control-bar">
<WorkspaceAndOrgSwitch
<ProjectAndOrgSwitch
currentEnvironmentId={environment.id}
environments={environments}
currentOrganizationId={currentOrganizationId}
currentWorkspaceId={currentWorkspaceId}
currentProjectId={currentProjectId}
isMultiOrgEnabled={isMultiOrgEnabled}
organizationWorkspacesLimit={organizationWorkspacesLimit}
organizationProjectsLimit={organizationProjectsLimit}
isFormbricksCloud={isFormbricksCloud}
isLicenseActive={isLicenseActive}
isOwnerOrManager={isOwnerOrManager}

View File

@@ -138,12 +138,6 @@ export const OrganizationBreadcrumb = ({
label: t("common.members_and_teams"),
href: `/environments/${currentEnvironmentId}/settings/teams`,
},
{
id: "feedback-record-directories",
label: t("environments.settings.feedback_record_directories.nav_label"),
href: `/environments/${currentEnvironmentId}/settings/feedback-record-directories`,
hidden: isMember,
},
{
id: "api-keys",
label: t("common.api_keys"),

View File

@@ -2,18 +2,18 @@
import { EnvironmentBreadcrumb } from "@/app/(app)/environments/[environmentId]/components/environment-breadcrumb";
import { OrganizationBreadcrumb } from "@/app/(app)/environments/[environmentId]/components/organization-breadcrumb";
import { WorkspaceBreadcrumb } from "@/app/(app)/environments/[environmentId]/components/workspace-breadcrumb";
import { ProjectBreadcrumb } from "@/app/(app)/environments/[environmentId]/components/project-breadcrumb";
import { Breadcrumb, BreadcrumbList } from "@/modules/ui/components/breadcrumb";
interface WorkspaceAndOrgSwitchProps {
interface ProjectAndOrgSwitchProps {
currentOrganizationId: string;
currentOrganizationName?: string; // Optional: for pages without context
currentWorkspaceId?: string;
currentWorkspaceName?: string; // Optional: for pages without context
currentProjectId?: string;
currentProjectName?: string; // Optional: for pages without context
currentEnvironmentId?: string;
environments: { id: string; type: string }[];
isMultiOrgEnabled: boolean;
organizationWorkspacesLimit: number;
organizationProjectsLimit: number;
isFormbricksCloud: boolean;
isLicenseActive: boolean;
isOwnerOrManager: boolean;
@@ -21,21 +21,21 @@ interface WorkspaceAndOrgSwitchProps {
isAccessControlAllowed: boolean;
}
export const WorkspaceAndOrgSwitch = ({
export const ProjectAndOrgSwitch = ({
currentOrganizationId,
currentOrganizationName,
currentWorkspaceId,
currentWorkspaceName,
currentProjectId,
currentProjectName,
currentEnvironmentId,
environments,
isMultiOrgEnabled,
organizationWorkspacesLimit,
organizationProjectsLimit,
isFormbricksCloud,
isLicenseActive,
isOwnerOrManager,
isAccessControlAllowed,
isMember,
}: WorkspaceAndOrgSwitchProps) => {
}: ProjectAndOrgSwitchProps) => {
const currentEnvironment = environments.find((env) => env.id === currentEnvironmentId);
const showEnvironmentBreadcrumb = currentEnvironment?.type === "development";
@@ -51,14 +51,14 @@ export const WorkspaceAndOrgSwitch = ({
isMember={isMember}
isOwnerOrManager={isOwnerOrManager}
/>
{currentWorkspaceId && currentEnvironmentId && (
<WorkspaceBreadcrumb
currentWorkspaceId={currentWorkspaceId}
currentWorkspaceName={currentWorkspaceName}
{currentProjectId && currentEnvironmentId && (
<ProjectBreadcrumb
currentProjectId={currentProjectId}
currentProjectName={currentProjectName}
currentOrganizationId={currentOrganizationId}
currentEnvironmentId={currentEnvironmentId}
isOwnerOrManager={isOwnerOrManager}
organizationWorkspacesLimit={organizationWorkspacesLimit}
organizationProjectsLimit={organizationProjectsLimit}
isFormbricksCloud={isFormbricksCloud}
isLicenseActive={isLicenseActive}
isAccessControlAllowed={isAccessControlAllowed}

View File

@@ -6,8 +6,10 @@ import { usePathname, useRouter } from "next/navigation";
import { useEffect, useState, useTransition } from "react";
import { useTranslation } from "react-i18next";
import { logger } from "@formbricks/logger";
import { getWorkspacesForSwitcherAction } from "@/app/(app)/environments/[environmentId]/actions";
import { getProjectsForSwitcherAction } from "@/app/(app)/environments/[environmentId]/actions";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { CreateProjectModal } from "@/modules/projects/components/create-project-modal";
import { ProjectLimitModal } from "@/modules/projects/components/project-limit-modal";
import { BreadcrumbItem } from "@/modules/ui/components/breadcrumb";
import {
DropdownMenu,
@@ -18,15 +20,13 @@ import {
DropdownMenuTrigger,
} from "@/modules/ui/components/dropdown-menu";
import { ModalButton } from "@/modules/ui/components/upgrade-prompt";
import { CreateWorkspaceModal } from "@/modules/workspaces/components/create-workspace-modal";
import { WorkspaceLimitModal } from "@/modules/workspaces/components/workspace-limit-modal";
import { useWorkspace } from "../context/environment-context";
import { useProject } from "../context/environment-context";
interface WorkspaceBreadcrumbProps {
currentWorkspaceId: string;
currentWorkspaceName?: string; // Optional: pass directly if context not available
interface ProjectBreadcrumbProps {
currentProjectId: string;
currentProjectName?: string; // Optional: pass directly if context not available
isOwnerOrManager: boolean;
organizationWorkspacesLimit: number;
organizationProjectsLimit: number;
isFormbricksCloud: boolean;
isLicenseActive: boolean;
currentOrganizationId: string;
@@ -35,7 +35,7 @@ interface WorkspaceBreadcrumbProps {
isEnvironmentBreadcrumbVisible: boolean;
}
const isActiveWorkspaceSetting = (pathname: string, settingId: string): boolean => {
const isActiveProjectSetting = (pathname: string, settingId: string): boolean => {
// Match /workspace/{settingId} or /workspace/{settingId}/... but exclude settings paths
if (pathname.includes("/settings/")) {
return false;
@@ -45,59 +45,59 @@ const isActiveWorkspaceSetting = (pathname: string, settingId: string): boolean
return pattern.test(pathname);
};
export const WorkspaceBreadcrumb = ({
currentWorkspaceId,
currentWorkspaceName,
export const ProjectBreadcrumb = ({
currentProjectId,
currentProjectName,
isOwnerOrManager,
organizationWorkspacesLimit,
organizationProjectsLimit,
isFormbricksCloud,
isLicenseActive,
currentOrganizationId,
currentEnvironmentId,
isAccessControlAllowed,
isEnvironmentBreadcrumbVisible,
}: WorkspaceBreadcrumbProps) => {
}: ProjectBreadcrumbProps) => {
const { t } = useTranslation();
const [isWorkspaceDropdownOpen, setIsWorkspaceDropdownOpen] = useState(false);
const [openCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useState(false);
const [isProjectDropdownOpen, setIsProjectDropdownOpen] = useState(false);
const [openCreateProjectModal, setOpenCreateProjectModal] = useState(false);
const [openLimitModal, setOpenLimitModal] = useState(false);
const router = useRouter();
const [isLoadingWorkspaces, setIsLoadingWorkspaces] = useState(false);
const [workspaces, setWorkspaces] = useState<{ id: string; name: string }[]>([]);
const [isLoadingProjects, setIsLoadingProjects] = useState(false);
const [projects, setProjects] = useState<{ id: string; name: string }[]>([]);
const [loadError, setLoadError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
const pathname = usePathname();
// Get current workspace name from context OR prop
// Get current project name from context OR prop
// Context is preferred, but prop is fallback for pages without EnvironmentContextWrapper
const { workspace: currentWorkspace } = useWorkspace();
const workspaceName = currentWorkspace?.name || currentWorkspaceName || "";
const { project: currentProject } = useProject();
const projectName = currentProject?.name || currentProjectName || "";
// Lazy-load workspaces when dropdown opens
// Lazy-load projects when dropdown opens
useEffect(() => {
// Only fetch when dropdown opened for first time (and no error state)
if (isWorkspaceDropdownOpen && workspaces.length === 0 && !isLoadingWorkspaces && !loadError) {
setIsLoadingWorkspaces(true);
if (isProjectDropdownOpen && projects.length === 0 && !isLoadingProjects && !loadError) {
setIsLoadingProjects(true);
setLoadError(null); // Clear any previous errors
getWorkspacesForSwitcherAction({ organizationId: currentOrganizationId }).then((result) => {
getProjectsForSwitcherAction({ organizationId: currentOrganizationId }).then((result) => {
if (result?.data) {
// Sort workspaces by name
// Sort projects by name
const sorted = [...result.data].sort((a, b) => a.name.localeCompare(b.name));
setWorkspaces(sorted);
setProjects(sorted);
} else {
// Handle server errors or validation errors
const errorMessage = getFormattedErrorMessage(result);
const error = new Error(errorMessage);
logger.error(error, "Failed to load workspaces");
logger.error(error, "Failed to load projects");
Sentry.captureException(error);
setLoadError(errorMessage || t("common.failed_to_load_workspaces"));
}
setIsLoadingWorkspaces(false);
setIsLoadingProjects(false);
});
}
}, [isWorkspaceDropdownOpen, currentOrganizationId, workspaces.length, isLoadingWorkspaces, loadError, t]);
}, [isProjectDropdownOpen, currentOrganizationId, projects.length, isLoadingProjects, loadError, t]);
const workspaceSettings = [
const projectSettings = [
{
id: "general",
label: t("common.general"),
@@ -135,29 +135,29 @@ export const WorkspaceBreadcrumb = ({
},
];
if (!currentWorkspace) {
const errorMessage = `Workspace not found for workspace id: ${currentWorkspaceId}`;
if (!currentProject) {
const errorMessage = `Workspace not found for workspace id: ${currentProjectId}`;
logger.error(errorMessage);
Sentry.captureException(new Error(errorMessage));
return;
}
const handleWorkspaceChange = (workspaceId: string) => {
if (workspaceId === currentWorkspaceId) return;
const handleProjectChange = (projectId: string) => {
if (projectId === currentProjectId) return;
startTransition(() => {
router.push(`/workspaces/${workspaceId}/`);
router.push(`/workspaces/${projectId}/`);
});
};
const handleAddWorkspace = () => {
if (workspaces.length >= organizationWorkspacesLimit) {
const handleAddProject = () => {
if (projects.length >= organizationProjectsLimit) {
setOpenLimitModal(true);
return;
}
setOpenCreateWorkspaceModal(true);
setOpenCreateProjectModal(true);
};
const handleWorkspaceSettingsNavigation = (settingId: string) => {
const handleProjectSettingsNavigation = (settingId: string) => {
startTransition(() => {
router.push(`/environments/${currentEnvironmentId}/workspace/${settingId}`);
});
@@ -191,17 +191,17 @@ export const WorkspaceBreadcrumb = ({
];
};
return (
<BreadcrumbItem isActive={isWorkspaceDropdownOpen}>
<DropdownMenu onOpenChange={setIsWorkspaceDropdownOpen}>
<BreadcrumbItem isActive={isProjectDropdownOpen}>
<DropdownMenu onOpenChange={setIsProjectDropdownOpen}>
<DropdownMenuTrigger
className="flex cursor-pointer items-center gap-1 outline-none"
id="workspaceDropdownTrigger"
id="projectDropdownTrigger"
asChild>
<div className="flex items-center gap-1">
<HotelIcon className="h-3 w-3" strokeWidth={1.5} />
<span>{workspaceName}</span>
<span>{projectName}</span>
{isPending && <Loader2 className="h-3 w-3 animate-spin" strokeWidth={1.5} />}
{isEnvironmentBreadcrumbVisible && !isWorkspaceDropdownOpen ? (
{isEnvironmentBreadcrumbVisible && !isProjectDropdownOpen ? (
<ChevronRightIcon className="h-3 w-3" strokeWidth={1.5} />
) : (
<ChevronDownIcon className="h-3 w-3" strokeWidth={1.5} />
@@ -214,32 +214,32 @@ export const WorkspaceBreadcrumb = ({
<HotelIcon className="mr-2 inline h-4 w-4" strokeWidth={1.5} />
{t("common.choose_workspace")}
</div>
{isLoadingWorkspaces && (
{isLoadingProjects && (
<div className="flex items-center justify-center py-2">
<Loader2 className="h-4 w-4 animate-spin" />
</div>
)}
{!isLoadingWorkspaces && loadError && (
{!isLoadingProjects && loadError && (
<div className="px-2 py-4">
<p className="mb-2 text-sm text-red-600">{loadError}</p>
<button
onClick={() => {
setLoadError(null);
setWorkspaces([]);
setProjects([]);
}}
className="text-xs text-slate-600 underline hover:text-slate-800">
{t("common.try_again")}
</button>
</div>
)}
{!isLoadingWorkspaces && !loadError && (
{!isLoadingProjects && !loadError && (
<>
<DropdownMenuGroup className="max-h-[300px] overflow-y-auto">
{workspaces.map((proj) => (
{projects.map((proj) => (
<DropdownMenuCheckboxItem
key={proj.id}
checked={proj.id === currentWorkspaceId}
onClick={() => handleWorkspaceChange(proj.id)}
checked={proj.id === currentProjectId}
onClick={() => handleProjectChange(proj.id)}
className="cursor-pointer">
<div className="flex items-center gap-2">
<span>{proj.name}</span>
@@ -249,7 +249,7 @@ export const WorkspaceBreadcrumb = ({
</DropdownMenuGroup>
{isOwnerOrManager && (
<DropdownMenuCheckboxItem
onClick={handleAddWorkspace}
onClick={handleAddProject}
className="w-full cursor-pointer justify-between">
<span>{t("common.add_new_workspace")}</span>
<PlusIcon className="ml-2 h-4 w-4" strokeWidth={1.5} />
@@ -263,11 +263,11 @@ export const WorkspaceBreadcrumb = ({
<CogIcon className="mr-2 inline h-4 w-4" strokeWidth={1.5} />
{t("common.workspace_configuration")}
</div>
{workspaceSettings.map((setting) => (
{projectSettings.map((setting) => (
<DropdownMenuCheckboxItem
key={setting.id}
checked={isActiveWorkspaceSetting(pathname, setting.id)}
onClick={() => handleWorkspaceSettingsNavigation(setting.id)}
checked={isActiveProjectSetting(pathname, setting.id)}
onClick={() => handleProjectSettingsNavigation(setting.id)}
className="cursor-pointer">
{setting.label}
</DropdownMenuCheckboxItem>
@@ -277,17 +277,17 @@ export const WorkspaceBreadcrumb = ({
</DropdownMenu>
{/* Modals */}
{openLimitModal && (
<WorkspaceLimitModal
<ProjectLimitModal
open={openLimitModal}
setOpen={setOpenLimitModal}
buttons={LimitModalButtons()}
workspaceLimit={organizationWorkspacesLimit}
projectLimit={organizationProjectsLimit}
/>
)}
{openCreateWorkspaceModal && (
<CreateWorkspaceModal
open={openCreateWorkspaceModal}
setOpen={setOpenCreateWorkspaceModal}
{openCreateProjectModal && (
<CreateProjectModal
open={openCreateProjectModal}
setOpen={setOpenCreateProjectModal}
organizationId={currentOrganizationId}
isAccessControlAllowed={isAccessControlAllowed}
/>

View File

@@ -3,11 +3,11 @@
import { createContext, useContext, useMemo } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TOrganization } from "@formbricks/types/organizations";
import { TWorkspace } from "@formbricks/types/workspace";
import { TProject } from "@formbricks/types/project";
export interface EnvironmentContextType {
environment: TEnvironment;
workspace: TWorkspace;
project: TProject;
organization: TOrganization;
organizationId: string;
}
@@ -22,12 +22,12 @@ export const useEnvironment = () => {
return context;
};
export const useWorkspace = () => {
export const useProject = () => {
const context = useContext(EnvironmentContext);
if (!context) {
return { workspace: null };
return { project: null };
}
return { workspace: context.workspace };
return { project: context.project };
};
export const useOrganization = () => {
@@ -41,25 +41,25 @@ export const useOrganization = () => {
// Client wrapper component to be used in server components
interface EnvironmentContextWrapperProps {
environment: TEnvironment;
workspace: TWorkspace;
project: TProject;
organization: TOrganization;
children: React.ReactNode;
}
export const EnvironmentContextWrapper = ({
environment,
workspace,
project,
organization,
children,
}: EnvironmentContextWrapperProps) => {
const environmentContextValue = useMemo(
() => ({
environment,
workspace,
project,
organization,
organizationId: workspace.organizationId,
organizationId: project.organizationId,
}),
[environment, workspace, organization]
[environment, project, organization]
);
return (

View File

@@ -27,7 +27,7 @@ const EnvLayout = async (props: {
<EnvironmentStorageHandler environmentId={params.environmentId} />
<EnvironmentContextWrapper
environment={layoutData.environment}
workspace={layoutData.workspace}
project={layoutData.project}
organization={layoutData.organization}>
<EnvironmentLayout layoutData={layoutData}>{children}</EnvironmentLayout>
</EnvironmentContextWrapper>

View File

@@ -3,18 +3,18 @@ import { describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { DatabaseError } from "@formbricks/types/errors";
import { TMembership } from "@formbricks/types/memberships";
import { getWorkspacesByUserId } from "./workspace";
import { getProjectsByUserId } from "./project";
vi.mock("@formbricks/database", () => ({
prisma: {
workspace: {
project: {
findMany: vi.fn(),
},
},
}));
describe("Workspace", () => {
describe("getUserWorkspaces", () => {
describe("Project", () => {
describe("getUserProjects", () => {
const mockAdminMembership: TMembership = {
role: "manager",
organizationId: "org1",
@@ -29,17 +29,17 @@ describe("Workspace", () => {
accepted: true,
};
test("should return workspaces for admin role", async () => {
const mockWorkspaces = [
{ id: "workspace1", name: "Workspace 1" },
{ id: "workspace2", name: "Workspace 2" },
test("should return projects for admin role", async () => {
const mockProjects = [
{ id: "project1", name: "Project 1" },
{ id: "project2", name: "Project 2" },
];
vi.mocked(prisma.workspace.findMany).mockResolvedValue(mockWorkspaces as any);
vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects as any);
const result = await getWorkspacesByUserId("user1", mockAdminMembership);
const result = await getProjectsByUserId("user1", mockAdminMembership);
expect(prisma.workspace.findMany).toHaveBeenCalledWith({
expect(prisma.project.findMany).toHaveBeenCalledWith({
where: {
organizationId: "org1",
},
@@ -48,20 +48,20 @@ describe("Workspace", () => {
name: true,
},
});
expect(result).toEqual(mockWorkspaces);
expect(result).toEqual(mockProjects);
});
test("should return workspaces for member role with team restrictions", async () => {
const mockWorkspaces = [{ id: "workspace1", name: "Workspace 1" }];
test("should return projects for member role with team restrictions", async () => {
const mockProjects = [{ id: "project1", name: "Project 1" }];
vi.mocked(prisma.workspace.findMany).mockResolvedValue(mockWorkspaces as any);
vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects as any);
const result = await getWorkspacesByUserId("user1", mockMemberMembership);
const result = await getProjectsByUserId("user1", mockMemberMembership);
expect(prisma.workspace.findMany).toHaveBeenCalledWith({
expect(prisma.project.findMany).toHaveBeenCalledWith({
where: {
organizationId: "org1",
workspaceTeams: {
projectTeams: {
some: {
team: {
teamUsers: {
@@ -78,13 +78,13 @@ describe("Workspace", () => {
name: true,
},
});
expect(result).toEqual(mockWorkspaces);
expect(result).toEqual(mockProjects);
});
test("should return empty array when no workspaces found", async () => {
vi.mocked(prisma.workspace.findMany).mockResolvedValue([]);
test("should return empty array when no projects found", async () => {
vi.mocked(prisma.project.findMany).mockResolvedValue([]);
const result = await getWorkspacesByUserId("user1", mockAdminMembership);
const result = await getProjectsByUserId("user1", mockAdminMembership);
expect(result).toEqual([]);
});
@@ -95,27 +95,27 @@ describe("Workspace", () => {
clientVersion: "5.0.0",
});
vi.mocked(prisma.workspace.findMany).mockRejectedValue(prismaError);
vi.mocked(prisma.project.findMany).mockRejectedValue(prismaError);
await expect(getWorkspacesByUserId("user1", mockAdminMembership)).rejects.toThrow(
await expect(getProjectsByUserId("user1", mockAdminMembership)).rejects.toThrow(
new DatabaseError("Database error")
);
});
test("should re-throw unknown errors", async () => {
const unknownError = new Error("Unknown error");
vi.mocked(prisma.workspace.findMany).mockRejectedValue(unknownError);
vi.mocked(prisma.project.findMany).mockRejectedValue(unknownError);
await expect(getWorkspacesByUserId("user1", mockAdminMembership)).rejects.toThrow(unknownError);
await expect(getProjectsByUserId("user1", mockAdminMembership)).rejects.toThrow(unknownError);
});
test("should validate inputs correctly", async () => {
await expect(getWorkspacesByUserId(123 as any, mockAdminMembership)).rejects.toThrow();
await expect(getProjectsByUserId(123 as any, mockAdminMembership)).rejects.toThrow();
});
test("should validate membership input correctly", async () => {
const invalidMembership = {} as TMembership;
await expect(getWorkspacesByUserId("user1", invalidMembership)).rejects.toThrow();
await expect(getProjectsByUserId("user1", invalidMembership)).rejects.toThrow();
});
test("should handle owner role like manager", async () => {
@@ -126,12 +126,12 @@ describe("Workspace", () => {
accepted: true,
};
const mockWorkspaces = [{ id: "workspace1", name: "Workspace 1" }];
vi.mocked(prisma.workspace.findMany).mockResolvedValue(mockWorkspaces as any);
const mockProjects = [{ id: "project1", name: "Project 1" }];
vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects as any);
const result = await getWorkspacesByUserId("user1", mockOwnerMembership);
const result = await getProjectsByUserId("user1", mockOwnerMembership);
expect(prisma.workspace.findMany).toHaveBeenCalledWith({
expect(prisma.project.findMany).toHaveBeenCalledWith({
where: {
organizationId: "org1",
},
@@ -140,7 +140,7 @@ describe("Workspace", () => {
name: true,
},
});
expect(result).toEqual(mockWorkspaces);
expect(result).toEqual(mockProjects);
});
});
});

View File

@@ -6,15 +6,15 @@ import { DatabaseError } from "@formbricks/types/errors";
import { TMembership, ZMembership } from "@formbricks/types/memberships";
import { validateInputs } from "@/lib/utils/validate";
export const getWorkspacesByUserId = reactCache(
export const getProjectsByUserId = reactCache(
async (userId: string, orgMembership: TMembership): Promise<{ id: string; name: string }[]> => {
validateInputs([userId, ZString], [orgMembership, ZMembership]);
let workspaceWhereClause: Prisma.WorkspaceWhereInput = {};
let projectWhereClause: Prisma.ProjectWhereInput = {};
if (orgMembership.role === "member") {
workspaceWhereClause = {
workspaceTeams: {
projectWhereClause = {
projectTeams: {
some: {
team: {
teamUsers: {
@@ -29,17 +29,17 @@ export const getWorkspacesByUserId = reactCache(
}
try {
const workspaces = await prisma.workspace.findMany({
const projects = await prisma.project.findMany({
where: {
organizationId: orgMembership.organizationId,
...workspaceWhereClause,
...projectWhereClause,
},
select: {
id: true,
name: true,
},
});
return workspaces;
return projects;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);

View File

@@ -1,7 +1,7 @@
import { getServerSession } from "next-auth";
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { getWorkspaceByEnvironmentId } from "@/lib/workspace/service";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { getTranslate } from "@/lingodotdev/server";
import { authOptions } from "@/modules/auth/lib/authOptions";
@@ -14,9 +14,9 @@ const AccountSettingsLayout = async (props: {
const { children } = props;
const t = await getTranslate();
const [organization, workspace, session] = await Promise.all([
const [organization, project, session] = await Promise.all([
getOrganizationByEnvironmentId(params.environmentId),
getWorkspaceByEnvironmentId(params.environmentId),
getProjectByEnvironmentId(params.environmentId),
getServerSession(authOptions),
]);
@@ -24,7 +24,7 @@ const AccountSettingsLayout = async (props: {
throw new ResourceNotFoundError(t("common.organization"), null);
}
if (!workspace) {
if (!project) {
throw new ResourceNotFoundError(t("common.workspace"), null);
}

View File

@@ -40,7 +40,7 @@ export const EditAlerts = ({
{t("environments.settings.notifications.auto_subscribe_to_new_surveys")}
</p>
<NotificationSwitch
surveyOrWorkspaceOrOrganizationId={membership.organization.id}
surveyOrProjectOrOrganizationId={membership.organization.id}
notificationSettings={user.notificationSettings!}
notificationType={"unsubscribedOrganizationIds"}
autoDisableNotificationType={autoDisableNotificationType}
@@ -66,13 +66,13 @@ export const EditAlerts = ({
</TooltipProvider>
</div>
{membership.organization.workspaces.some((workspace) =>
workspace.environments.some((environment) => environment.surveys.length > 0)
{membership.organization.projects.some((project) =>
project.environments.some((environment) => environment.surveys.length > 0)
) ? (
<div className="grid-cols-8 space-y-1 p-2">
{membership.organization.workspaces.map((workspace) => (
<div key={workspace.id}>
{workspace.environments.map((environment) => (
{membership.organization.projects.map((project) => (
<div key={project.id}>
{project.environments.map((environment) => (
<div key={environment.id}>
{environment.surveys.map((survey) => (
<div
@@ -80,11 +80,11 @@ export const EditAlerts = ({
key={survey.name}>
<div className="col-span-2 text-left">
<div className="font-medium text-slate-900">{survey.name}</div>
<div className="text-xs text-slate-400">{workspace.name}</div>
<div className="text-xs text-slate-400">{project.name}</div>
</div>
<div className="col-span-1 text-center">
<NotificationSwitch
surveyOrWorkspaceOrOrganizationId={survey.id}
surveyOrProjectOrOrganizationId={survey.id}
notificationSettings={user.notificationSettings!}
notificationType={"alert"}
autoDisableNotificationType={autoDisableNotificationType}

View File

@@ -10,7 +10,7 @@ import { Switch } from "@/modules/ui/components/switch";
import { updateNotificationSettingsAction } from "../actions";
interface NotificationSwitchProps {
surveyOrWorkspaceOrOrganizationId: string;
surveyOrProjectOrOrganizationId: string;
notificationSettings: TUserNotificationSettings;
notificationType: "alert" | "unsubscribedOrganizationIds";
autoDisableNotificationType?: string;
@@ -18,7 +18,7 @@ interface NotificationSwitchProps {
}
export const NotificationSwitch = ({
surveyOrWorkspaceOrOrganizationId,
surveyOrProjectOrOrganizationId,
notificationSettings,
notificationType,
autoDisableNotificationType,
@@ -29,8 +29,8 @@ export const NotificationSwitch = ({
const router = useRouter();
const isChecked =
notificationType === "unsubscribedOrganizationIds"
? !notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrWorkspaceOrOrganizationId)
: notificationSettings[notificationType]?.[surveyOrWorkspaceOrOrganizationId] === true;
? !notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrProjectOrOrganizationId)
: notificationSettings[notificationType]?.[surveyOrProjectOrOrganizationId] === true;
const handleSwitchChange = async () => {
setIsLoading(true);
@@ -38,21 +38,21 @@ export const NotificationSwitch = ({
let updatedNotificationSettings = { ...notificationSettings };
if (notificationType === "unsubscribedOrganizationIds") {
const unsubscribedOrganizationIds = updatedNotificationSettings.unsubscribedOrganizationIds ?? [];
if (unsubscribedOrganizationIds.includes(surveyOrWorkspaceOrOrganizationId)) {
if (unsubscribedOrganizationIds.includes(surveyOrProjectOrOrganizationId)) {
updatedNotificationSettings.unsubscribedOrganizationIds = unsubscribedOrganizationIds.filter(
(id) => id !== surveyOrWorkspaceOrOrganizationId
(id) => id !== surveyOrProjectOrOrganizationId
);
} else {
updatedNotificationSettings.unsubscribedOrganizationIds = [
...unsubscribedOrganizationIds,
surveyOrWorkspaceOrOrganizationId,
surveyOrProjectOrOrganizationId,
];
}
} else {
updatedNotificationSettings[notificationType] = {
...updatedNotificationSettings[notificationType],
[surveyOrWorkspaceOrOrganizationId]:
!updatedNotificationSettings[notificationType]?.[surveyOrWorkspaceOrOrganizationId],
[surveyOrProjectOrOrganizationId]:
!updatedNotificationSettings[notificationType]?.[surveyOrProjectOrOrganizationId],
};
}
@@ -76,12 +76,12 @@ export const NotificationSwitch = ({
useEffect(() => {
if (
autoDisableNotificationType &&
autoDisableNotificationElementId === surveyOrWorkspaceOrOrganizationId &&
autoDisableNotificationElementId === surveyOrProjectOrOrganizationId &&
isChecked
) {
switch (notificationType) {
case "alert":
if (notificationSettings[notificationType]?.[surveyOrWorkspaceOrOrganizationId] === true) {
if (notificationSettings[notificationType]?.[surveyOrProjectOrOrganizationId] === true) {
handleSwitchChange();
toast.success(
t(
@@ -95,9 +95,7 @@ export const NotificationSwitch = ({
break;
case "unsubscribedOrganizationIds":
if (
!notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrWorkspaceOrOrganizationId)
) {
if (!notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrProjectOrOrganizationId)) {
handleSwitchChange();
toast.success(
t(

View File

@@ -22,9 +22,9 @@ const setCompleteNotificationSettings = (
unsubscribedOrganizationIds: notificationSettings.unsubscribedOrganizationIds || [],
};
for (const membership of memberships) {
for (const workspace of membership.organization.workspaces) {
for (const project of membership.organization.projects) {
// set default values for alerts
for (const environment of workspace.environments) {
for (const environment of project.environments) {
for (const survey of environment.surveys) {
newNotificationSettings.alert[survey.id] =
(notificationSettings as unknown as Record<string, Record<string, boolean>>)[survey.id]
@@ -47,17 +47,17 @@ const getMemberships = async (userId: string): Promise<Membership[]> => {
},
OR: [
{
// Fetch all workspaces if user role is owner or manager
// Fetch all projects if user role is owner or manager
role: {
in: ["owner", "manager"],
},
},
{
// Filter workspaces based on team membership if user is not owner or manager
// Filter projects based on team membership if user is not owner or manager
organization: {
workspaces: {
projects: {
some: {
workspaceTeams: {
projectTeams: {
some: {
team: {
teamUsers: {
@@ -79,12 +79,12 @@ const getMemberships = async (userId: string): Promise<Membership[]> => {
select: {
id: true,
name: true,
workspaces: {
projects: {
// Apply conditional filtering based on user's role
where: {
OR: [
{
// Fetch all workspaces if user is owner or manager
// Fetch all projects if user is owner or manager
organization: {
memberships: {
some: {
@@ -97,8 +97,8 @@ const getMemberships = async (userId: string): Promise<Membership[]> => {
},
},
{
// Only include workspaces accessible through teams if user is not owner or manager
workspaceTeams: {
// Only include projects accessible through teams if user is not owner or manager
projectTeams: {
some: {
team: {
teamUsers: {

View File

@@ -4,7 +4,7 @@ export interface Membership {
organization: {
id: string;
name: string;
workspaces: {
projects: {
id: string;
name: string;
environments: {

View File

@@ -40,13 +40,6 @@ export const OrganizationSettingsNavbar = ({
href: `/environments/${environmentId}/settings/teams`,
current: pathname?.includes("/teams"),
},
{
id: "feedback-record-directories",
label: t("environments.settings.feedback_record_directories.nav_label"),
href: `/environments/${environmentId}/settings/feedback-record-directories`,
current: pathname?.includes("/feedback-record-directories"),
hidden: isMember,
},
{
id: "api-keys",
label: t("common.api_keys"),

View File

@@ -14,7 +14,7 @@ interface SurveyWithSlug {
environment: {
id: string;
type: "production" | "development";
workspace: {
project: {
id: string;
name: string;
};
@@ -40,7 +40,7 @@ export const PrettyUrlsTable = ({ surveys }: PrettyUrlsTableProps) => {
},
{
label: t("environments.settings.domain.workspace"),
key: "workspace",
key: "project",
},
{
label: t("environments.settings.domain.pretty_url"),
@@ -81,7 +81,7 @@ export const PrettyUrlsTable = ({ surveys }: PrettyUrlsTableProps) => {
{survey.name}
</Link>
</TableCell>
<TableCell>{survey.environment.workspace.name}</TableCell>
<TableCell>{survey.environment.project.name}</TableCell>
<TableCell>
<IdBadge id={survey.slug ?? ""} />
</TableCell>

View File

@@ -25,8 +25,8 @@ const getFeatureDefinitions = (t: TFunction): TFeatureDefinition[] => {
"https://formbricks.com/docs/self-hosting/advanced/enterprise-features/contact-management-segments",
},
{
key: "workspaces",
labelKey: t("environments.settings.enterprise.license_feature_workspaces"),
key: "projects",
labelKey: t("environments.settings.enterprise.license_feature_projects"),
docsUrl: "https://formbricks.com/docs/self-hosting/advanced/license",
},
{

View File

@@ -1 +0,0 @@
export { FeedbackRecordDirectoriesPage as default } from "@/modules/ee/feedback-record-directory/page";

View File

@@ -1,7 +1,7 @@
import { getServerSession } from "next-auth";
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { getWorkspaceByEnvironmentId } from "@/lib/workspace/service";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { getTranslate } from "@/lingodotdev/server";
import { authOptions } from "@/modules/auth/lib/authOptions";
@@ -11,9 +11,9 @@ const Layout = async (props: { params: Promise<{ environmentId: string }>; child
const { children } = props;
const t = await getTranslate();
const [organization, workspace, session] = await Promise.all([
const [organization, project, session] = await Promise.all([
getOrganizationByEnvironmentId(params.environmentId),
getWorkspaceByEnvironmentId(params.environmentId),
getProjectByEnvironmentId(params.environmentId),
getServerSession(authOptions),
]);
@@ -21,7 +21,7 @@ const Layout = async (props: { params: Promise<{ environmentId: string }>; child
throw new ResourceNotFoundError(t("common.organization"), null);
}
if (!workspace) {
if (!project) {
throw new ResourceNotFoundError(t("common.workspace"), null);
}

View File

@@ -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),
},
],
});

View File

@@ -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",
},
],

View File

@@ -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} />

View File

@@ -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(() => {

View File

@@ -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");
};

View File

@@ -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")}

View File

@@ -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) {
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 =

View File

@@ -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: [],

View File

@@ -9,7 +9,7 @@ import { getSurvey } from "@/lib/survey/service";
import { getTagsByEnvironmentId } from "@/lib/tag/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 { getIsQuotasEnabled } from "@/modules/ee/license-check/lib/utils";
import { getQuotas } from "@/modules/ee/quotas/lib/quotas";
import { getOrganizationBilling } from "@/modules/survey/lib/survey";
@@ -32,9 +32,9 @@ export const getResponsesDownloadUrlAction = 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 getSurveyFilterDataAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "workspaceTeam",
type: "projectTeam",
minPermission: "read",
workspaceId: await getWorkspaceIdFromSurveyId(parsedInput.surveyId),
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
},
],
});

View File

@@ -1,3 +1,3 @@
import { AppConnectionLoading } from "@/modules/workspaces/settings/(setup)/app-connection/loading";
import { AppConnectionLoading } from "@/modules/projects/settings/(setup)/app-connection/loading";
export default AppConnectionLoading;

View File

@@ -1,3 +1,3 @@
import { AppConnectionPage } from "@/modules/workspaces/settings/(setup)/app-connection/page";
import { AppConnectionPage } from "@/modules/projects/settings/(setup)/app-connection/page";
export default AppConnectionPage;

View File

@@ -1,3 +1,3 @@
import { GeneralSettingsLoading } from "@/modules/workspaces/settings/general/loading";
import { GeneralSettingsLoading } from "@/modules/projects/settings/general/loading";
export default GeneralSettingsLoading;

View File

@@ -1,3 +1,3 @@
import { GeneralSettingsPage } from "@/modules/workspaces/settings/general/page";
import { GeneralSettingsPage } from "@/modules/projects/settings/general/page";
export default GeneralSettingsPage;

View File

@@ -9,8 +9,8 @@ import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-clie
import {
getOrganizationIdFromEnvironmentId,
getOrganizationIdFromIntegrationId,
getWorkspaceIdFromEnvironmentId,
getWorkspaceIdFromIntegrationId,
getProjectIdFromEnvironmentId,
getProjectIdFromIntegrationId,
} from "@/lib/utils/helper";
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
@@ -34,9 +34,9 @@ export const createOrUpdateIntegrationAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "workspaceTeam",
type: "projectTeam",
minPermission: "readWrite",
workspaceId: await getWorkspaceIdFromEnvironmentId(parsedInput.environmentId),
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
},
],
});
@@ -66,8 +66,8 @@ export const deleteIntegrationAction = authenticatedActionClient.inputSchema(ZDe
roles: ["owner", "manager"],
},
{
type: "workspaceTeam",
workspaceId: await getWorkspaceIdFromIntegrationId(parsedInput.integrationId),
type: "projectTeam",
projectId: await getProjectIdFromIntegrationId(parsedInput.integrationId),
minPermission: "readWrite",
},
],

View File

@@ -10,7 +10,7 @@ import { getSpreadsheetNameById, validateGoogleSheetsConnection } from "@/lib/go
import { getIntegrationByType } from "@/lib/integration/service";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { getOrganizationIdFromEnvironmentId, getWorkspaceIdFromEnvironmentId } from "@/lib/utils/helper";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
const ZValidateGoogleSheetsConnectionAction = z.object({
environmentId: ZId,
@@ -28,8 +28,8 @@ export const validateGoogleSheetsConnectionAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "workspaceTeam",
workspaceId: await getWorkspaceIdFromEnvironmentId(parsedInput.environmentId),
type: "projectTeam",
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
minPermission: "readWrite",
},
],
@@ -62,8 +62,8 @@ export const getSpreadsheetNameByIdAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "workspaceTeam",
workspaceId: await getWorkspaceIdFromEnvironmentId(parsedInput.environmentId),
type: "projectTeam",
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
minPermission: "readWrite",
},
],

View File

@@ -16,10 +16,10 @@ import ZapierLogo from "@/images/zapier-small.png";
import { getIntegrations } from "@/lib/integration/service";
import { getTranslate } from "@/lingodotdev/server";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation";
import { Card } from "@/modules/ui/components/integration-card";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { WorkspaceConfigNavigation } from "@/modules/workspaces/settings/components/workspace-config-navigation";
const getStatusText = (count: number, t: TFunction, type: string) => {
if (count === 1) return `1 ${type}`;
@@ -210,7 +210,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.workspace_configuration")}>
<WorkspaceConfigNavigation environmentId={params.environmentId} activeId="integrations" />
<ProjectConfigNavigation environmentId={params.environmentId} activeId="integrations" />
</PageHeader>
<div className="grid grid-cols-3 place-content-stretch gap-4 lg:grid-cols-3">
{integrationCards.map((card) => (

View File

@@ -5,7 +5,7 @@ import { ZId } from "@formbricks/types/common";
import { getSlackChannels } from "@/lib/slack/service";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { getOrganizationIdFromEnvironmentId, getWorkspaceIdFromEnvironmentId } from "@/lib/utils/helper";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
const ZGetSlackChannelsAction = z.object({
environmentId: ZId,
@@ -23,8 +23,8 @@ export const getSlackChannelsAction = authenticatedActionClient
roles: ["owner", "manager"],
},
{
type: "workspaceTeam",
workspaceId: await getWorkspaceIdFromEnvironmentId(parsedInput.environmentId),
type: "projectTeam",
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
minPermission: "readWrite",
},
],

View File

@@ -1,3 +1,3 @@
import { LanguagesLoading } from "@/modules/workspaces/settings/languages/loading";
import { LanguagesLoading } from "@/modules/projects/settings/languages/loading";
export default LanguagesLoading;

View File

@@ -1,3 +1,3 @@
import { LanguagesPage } from "@/modules/workspaces/settings/languages/page";
import { LanguagesPage } from "@/modules/projects/settings/languages/page";
export default LanguagesPage;

View File

@@ -1,4 +1,4 @@
import { WorkspaceSettingsLayout, metadata } from "@/modules/workspaces/settings/layout";
import { ProjectSettingsLayout, metadata } from "@/modules/projects/settings/layout";
export { metadata };
export default WorkspaceSettingsLayout;
export default ProjectSettingsLayout;

View File

@@ -1,3 +1,3 @@
import { WorkspaceLookSettingsLoading } from "@/modules/workspaces/settings/look/loading";
import { ProjectLookSettingsLoading } from "@/modules/projects/settings/look/loading";
export default WorkspaceLookSettingsLoading;
export default ProjectLookSettingsLoading;

View File

@@ -1,3 +1,3 @@
import { WorkspaceLookSettingsPage } from "@/modules/workspaces/settings/look/page";
import { ProjectLookSettingsPage } from "@/modules/projects/settings/look/page";
export default WorkspaceLookSettingsPage;
export default ProjectLookSettingsPage;

View File

@@ -1,3 +1,3 @@
import { WorkspaceSettingsPage } from "@/modules/workspaces/settings/page";
import { ProjectSettingsPage } from "@/modules/projects/settings/page";
export default WorkspaceSettingsPage;
export default ProjectSettingsPage;

View File

@@ -1,3 +1,3 @@
import { TagsLoading } from "@/modules/workspaces/settings/tags/loading";
import { TagsLoading } from "@/modules/projects/settings/tags/loading";
export default TagsLoading;

View File

@@ -1,3 +1,3 @@
import { TagsPage } from "@/modules/workspaces/settings/tags/page";
import { TagsPage } from "@/modules/projects/settings/tags/page";
export default TagsPage;

View File

@@ -1,3 +1,3 @@
import { WorkspaceTeams } from "@/modules/ee/teams/workspace-teams/page";
import { ProjectTeams } from "@/modules/ee/teams/project-teams/page";
export default WorkspaceTeams;
export default ProjectTeams;

View File

@@ -6,7 +6,7 @@ import { hasOrganizationAccess } from "@/lib/auth";
import { getEnvironments } from "@/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils";
import { getUserWorkspaces } from "@/lib/workspace/service";
import { getUserProjects } from "@/lib/project/service";
import { authOptions } from "@/modules/auth/lib/authOptions";
export const GET = async (_: Request, context: { params: Promise<{ organizationId: string }> }) => {
@@ -22,14 +22,14 @@ export const GET = async (_: Request, context: { params: Promise<{ organizationI
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organizationId);
const { isBilling } = getAccessFlags(currentUserMembership?.role);
// redirect to first workspace's production environment
const workspaces = await getUserWorkspaces(session.user.id, organizationId);
if (workspaces.length === 0) {
// redirect to first project's production environment
const projects = await getUserProjects(session.user.id, organizationId);
if (projects.length === 0) {
return redirect(`/organizations/${organizationId}/landing`);
}
const firstWorkspace = workspaces[0];
const environments = await getEnvironments(firstWorkspace.id);
const firstProject = projects[0];
const environments = await getEnvironments(firstProject.id);
const prodEnvironment = environments.find((e) => e.type === "production");
if (!prodEnvironment) return notFound();

View File

@@ -3,22 +3,22 @@ import { notFound, redirect } from "next/navigation";
import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors";
import { hasOrganizationAccess } from "@/lib/auth";
import { getEnvironments } from "@/lib/environment/service";
import { getWorkspace } from "@/lib/workspace/service";
import { getProject } from "@/lib/project/service";
import { authOptions } from "@/modules/auth/lib/authOptions";
export const GET = async (_: Request, context: { params: Promise<{ workspaceId: string }> }) => {
export const GET = async (_: Request, context: { params: Promise<{ projectId: string }> }) => {
const params = await context?.params;
const workspaceId = params.workspaceId;
if (!workspaceId) return notFound();
const projectId = params.projectId;
if (!projectId) return notFound();
// check auth
const session = await getServerSession(authOptions);
if (!session) throw new AuthenticationError("Not authenticated");
const workspace = await getWorkspace(workspaceId);
if (!workspace) return notFound();
const hasAccess = await hasOrganizationAccess(session.user.id, workspace.organizationId);
const project = await getProject(projectId);
if (!project) return notFound();
const hasAccess = await hasOrganizationAccess(session.user.id, project.organizationId);
if (!hasAccess) throw new AuthorizationError("Unauthorized");
// redirect to workspace's production environment
const environments = await getEnvironments(workspace.id);
// redirect to project's production environment
const environments = await getEnvironments(project.id);
const prodEnvironment = environments.find((e) => e.type === "production");
if (!prodEnvironment) return notFound();
return redirect(`/environments/${prodEnvironment.id}/`);

View File

@@ -155,7 +155,7 @@ export const mockSurvey: TSurvey = {
isSingleResponsePerEmailEnabled: false,
isBackButtonHidden: false,
recaptcha: null,
workspaceOverwrites: null,
projectOverwrites: null,
styling: null,
surveyClosedMessage: null,
singleUse: {

View File

@@ -20,7 +20,7 @@ vi.mock("@formbricks/database", () => ({
},
user: { count: vi.fn() },
team: { count: vi.fn() },
workspace: { count: vi.fn() },
project: { count: vi.fn() },
survey: { count: vi.fn() },
response: {
count: vi.fn(),
@@ -94,7 +94,7 @@ describe("sendTelemetryEvents", () => {
organizationCount: BigInt(1),
userCount: BigInt(5),
teamCount: BigInt(2),
workspaceCount: BigInt(3),
projectCount: BigInt(3),
surveyCount: BigInt(10),
inProgressSurveyCount: BigInt(4),
completedSurveyCount: BigInt(6),

View File

@@ -148,7 +148,7 @@ const sendTelemetry = async (lastSent: number) => {
organizationCount: bigint;
userCount: bigint;
teamCount: bigint;
workspaceCount: bigint;
projectCount: bigint;
surveyCount: bigint;
inProgressSurveyCount: bigint;
completedSurveyCount: bigint;
@@ -165,7 +165,7 @@ const sendTelemetry = async (lastSent: number) => {
(SELECT COUNT(*) FROM "Organization") as "organizationCount",
(SELECT COUNT(*) FROM "User") as "userCount",
(SELECT COUNT(*) FROM "Team") as "teamCount",
(SELECT COUNT(*) FROM "Workspace") as "workspaceCount",
(SELECT COUNT(*) FROM "Project") as "projectCount",
(SELECT COUNT(*) FROM "Survey") as "surveyCount",
(SELECT COUNT(*) FROM "Survey" WHERE status = 'inProgress') as "inProgressSurveyCount",
(SELECT COUNT(*) FROM "Survey" WHERE status = 'completed') as "completedSurveyCount",
@@ -186,7 +186,7 @@ const sendTelemetry = async (lastSent: number) => {
const organizationCount = Number(counts.organizationCount);
const userCount = Number(counts.userCount);
const teamCount = Number(counts.teamCount);
const workspaceCount = Number(counts.workspaceCount);
const projectCount = Number(counts.projectCount);
const surveyCount = Number(counts.surveyCount);
const inProgressSurveyCount = Number(counts.inProgressSurveyCount);
const completedSurveyCount = Number(counts.completedSurveyCount);
@@ -222,7 +222,7 @@ const sendTelemetry = async (lastSent: number) => {
organizationCount,
userCount,
teamCount,
workspaceCount,
projectCount,
surveyCount,
inProgressSurveyCount,
completedSurveyCount,

View File

@@ -168,7 +168,7 @@ export const POST = async (request: Request) => {
memberships: {
some: {
organization: {
workspaces: {
projects: {
some: {
environments: {
some: { id: environmentId },
@@ -192,9 +192,9 @@ export const POST = async (request: Request) => {
teamUsers: {
some: {
team: {
workspaceTeams: {
projectTeams: {
some: {
workspace: {
project: {
environments: {
some: {
id: environmentId,

View File

@@ -29,9 +29,9 @@ describe("getApiKeyWithPermissions", () => {
createdAt: new Date(),
updatedAt: new Date(),
type: "development" as const,
workspaceId: "workspace-1",
projectId: "project-1",
appSetupCompleted: true,
workspace: { id: "workspace-1", name: "Workspace 1" },
project: { id: "project-1", name: "Project 1" },
},
},
],
@@ -60,22 +60,22 @@ describe("hasPermission", () => {
environmentId: "env-1",
permission: "manage",
environmentType: "development",
workspaceId: "workspace-1",
workspaceName: "Workspace 1",
projectId: "project-1",
projectName: "Project 1",
},
{
environmentId: "env-2",
permission: "write",
environmentType: "production",
workspaceId: "workspace-2",
workspaceName: "Workspace 2",
projectId: "project-2",
projectName: "Project 2",
},
{
environmentId: "env-3",
permission: "read",
environmentType: "development",
workspaceId: "workspace-3",
workspaceName: "Workspace 3",
projectId: "project-3",
projectName: "Project 3",
},
];
@@ -125,9 +125,9 @@ describe("authenticateRequest", () => {
createdAt: new Date(),
updatedAt: new Date(),
type: "development" as const,
workspaceId: "workspace-1",
projectId: "project-1",
appSetupCompleted: true,
workspace: { id: "workspace-1", name: "Workspace 1" },
project: { id: "project-1", name: "Project 1" },
},
},
],
@@ -143,8 +143,8 @@ describe("authenticateRequest", () => {
environmentId: "env-1",
permission: "manage",
environmentType: "development",
workspaceId: "workspace-1",
workspaceName: "Workspace 1",
projectId: "project-1",
projectName: "Project 1",
},
],
apiKeyId: "api-key-id",

View File

@@ -22,8 +22,8 @@ export const authenticateRequest = async (request: NextRequest): Promise<TAuthen
environmentId: env.environmentId,
environmentType: env.environment.type,
permission: env.permission,
workspaceId: env.workspaceId ?? env.environment.workspaceId,
workspaceName: env.workspace?.name ?? env.environment.workspace.name,
projectId: env.environment.projectId,
projectName: env.environment.project.name,
})),
apiKeyId: apiKeyData.id,
organizationId: apiKeyData.organizationId,

View File

@@ -30,8 +30,8 @@ const mockEnvironmentData = {
id: environmentId,
type: "production",
appSetupCompleted: true,
workspace: {
id: "workspace-123",
project: {
id: "project-123",
recontactDays: 30,
clickOutsideClose: true,
overlay: "none",
@@ -73,7 +73,7 @@ const mockEnvironmentData = {
triggers: [],
displayPercentage: null,
delay: 0,
workspaceOverwrites: null,
projectOverwrites: null,
},
],
};
@@ -97,8 +97,8 @@ describe("getEnvironmentStateData", () => {
id: environmentId,
type: "production",
appSetupCompleted: true,
workspace: {
id: "workspace-123",
project: {
id: "project-123",
recontactDays: 30,
clickOutsideClose: true,
overlay: "none",
@@ -117,7 +117,7 @@ describe("getEnvironmentStateData", () => {
id: true,
type: true,
appSetupCompleted: true,
workspace: expect.any(Object),
project: expect.any(Object),
actionClasses: expect.any(Object),
surveys: expect.any(Object),
}),
@@ -131,10 +131,10 @@ describe("getEnvironmentStateData", () => {
await expect(getEnvironmentStateData(environmentId)).rejects.toThrow("environment");
});
test("should throw ResourceNotFoundError when workspace is not found", async () => {
test("should throw ResourceNotFoundError when project is not found", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
...mockEnvironmentData,
workspace: null,
project: null,
} as never);
await expect(getEnvironmentStateData(environmentId)).rejects.toThrow(ResourceNotFoundError);
@@ -201,9 +201,9 @@ describe("getEnvironmentStateData", () => {
expect(result.surveys).toHaveLength(2);
});
test("should correctly map workspace properties to environment.workspace", async () => {
const customWorkspace = {
...mockEnvironmentData.workspace,
test("should correctly map project properties to environment.project", async () => {
const customProject = {
...mockEnvironmentData.project,
recontactDays: 14,
clickOutsideClose: false,
overlay: "dark",
@@ -214,13 +214,13 @@ describe("getEnvironmentStateData", () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
...mockEnvironmentData,
workspace: customWorkspace,
project: customProject,
} as never);
const result = await getEnvironmentStateData(environmentId);
expect(result.environment.workspace).toEqual({
id: "workspace-123",
expect(result.environment.project).toEqual({
id: "project-123",
recontactDays: 14,
clickOutsideClose: false,
overlay: "dark",

View File

@@ -6,8 +6,8 @@ import { ZId } from "@formbricks/types/common";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import {
TJsEnvironmentStateActionClass,
TJsEnvironmentStateProject,
TJsEnvironmentStateSurvey,
TJsEnvironmentStateWorkspace,
} from "@formbricks/types/js";
import { validateInputs } from "@/lib/utils/validate";
import { resolveStorageUrlsInObject } from "@/modules/storage/utils";
@@ -23,7 +23,7 @@ export interface EnvironmentStateData {
id: string;
type: string;
appSetupCompleted: boolean;
workspace: TJsEnvironmentStateWorkspace;
project: TJsEnvironmentStateProject;
};
surveys: TJsEnvironmentStateSurvey[];
actionClasses: TJsEnvironmentStateActionClass[];
@@ -45,8 +45,8 @@ export const getEnvironmentStateData = async (environmentId: string): Promise<En
id: true,
type: true,
appSetupCompleted: true,
// Workspace data (optimized select)
workspace: {
// Project data (optimized select)
project: {
select: {
id: true,
recontactDays: true,
@@ -97,7 +97,7 @@ export const getEnvironmentStateData = async (environmentId: string): Promise<En
alias: true,
createdAt: true,
updatedAt: true,
workspaceId: true,
projectId: true,
},
},
},
@@ -132,7 +132,7 @@ export const getEnvironmentStateData = async (environmentId: string): Promise<En
},
displayPercentage: true,
delay: true,
workspaceOverwrites: true,
projectOverwrites: true,
},
},
},
@@ -142,8 +142,8 @@ export const getEnvironmentStateData = async (environmentId: string): Promise<En
throw new ResourceNotFoundError("environment", environmentId);
}
if (!environmentData.workspace) {
throw new ResourceNotFoundError("workspace", null);
if (!environmentData.project) {
throw new ResourceNotFoundError("project", null);
}
// Transform surveys using existing utility
@@ -156,14 +156,14 @@ export const getEnvironmentStateData = async (environmentId: string): Promise<En
id: environmentData.id,
type: environmentData.type,
appSetupCompleted: environmentData.appSetupCompleted,
workspace: {
id: environmentData.workspace.id,
recontactDays: environmentData.workspace.recontactDays,
clickOutsideClose: environmentData.workspace.clickOutsideClose,
overlay: environmentData.workspace.overlay,
placement: environmentData.workspace.placement,
inAppSurveyBranding: environmentData.workspace.inAppSurveyBranding,
styling: resolveStorageUrlsInObject(environmentData.workspace.styling),
project: {
id: environmentData.project.id,
recontactDays: environmentData.project.recontactDays,
clickOutsideClose: environmentData.project.clickOutsideClose,
overlay: environmentData.project.overlay,
placement: environmentData.project.placement,
inAppSurveyBranding: environmentData.project.inAppSurveyBranding,
styling: resolveStorageUrlsInObject(environmentData.project.styling),
},
},
surveys: resolveStorageUrlsInObject(transformedSurveys),

View File

@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { TActionClass } from "@formbricks/types/action-classes";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { TJsEnvironmentState, TJsEnvironmentStateWorkspace } from "@formbricks/types/js";
import { TJsEnvironmentState, TJsEnvironmentStateProject } from "@formbricks/types/js";
import { TOrganization } from "@formbricks/types/organizations";
import { TSurvey } from "@formbricks/types/surveys/types";
import { cache } from "@/lib/cache";
@@ -49,8 +49,8 @@ vi.mock("@formbricks/cache", () => ({
const environmentId = "test-environment-id";
const mockWorkspace: TJsEnvironmentStateWorkspace = {
id: "test-workspace-id",
const mockProject: TJsEnvironmentStateProject = {
id: "test-project-id",
recontactDays: 30,
inAppSurveyBranding: true,
placement: "bottomRight",
@@ -69,7 +69,7 @@ const mockOrganization: TOrganization = {
billing: {
stripeCustomerId: null,
limits: {
workspaces: 1,
projects: 1,
monthly: {
responses: 100,
},
@@ -94,7 +94,7 @@ const mockSurveys: TSurvey[] = [
isBackButtonHidden: false,
isSingleResponsePerEmailEnabled: false,
isVerifyEmailEnabled: false,
workspaceOverwrites: null,
projectOverwrites: null,
showLanguageSwitch: false,
questions: [],
displayOption: "displayOnce",
@@ -137,7 +137,11 @@ const mockEnvironmentStateData: EnvironmentStateData = {
id: environmentId,
type: "production",
appSetupCompleted: true,
workspace: mockWorkspace,
project: mockProject,
},
organization: {
id: mockOrganization.id,
billing: mockOrganization.billing,
},
surveys: mockSurveys,
actionClasses: mockActionClasses,
@@ -161,19 +165,11 @@ describe("getEnvironmentState", () => {
test("should return the correct environment state", async () => {
const result = await getEnvironmentState(environmentId);
// Backwards compat: response includes `project` alongside `workspace`,
// and each survey includes `projectOverwrites` alongside `workspaceOverwrites`
const surveysWithLegacy = mockSurveys.map((s) => ({
...s,
projectOverwrites: (s as Record<string, unknown>).workspaceOverwrites ?? null,
}));
const expectedData = {
const expectedData: TJsEnvironmentState["data"] = {
recaptchaSiteKey: "mock_recaptcha_site_key",
surveys: surveysWithLegacy,
surveys: mockSurveys,
actionClasses: mockActionClasses,
workspace: mockWorkspace,
project: mockWorkspace,
project: mockProject,
};
expect(result.data).toEqual(expectedData);
@@ -193,8 +189,8 @@ describe("getEnvironmentState", () => {
await expect(getEnvironmentState(environmentId)).rejects.toThrow(ResourceNotFoundError);
});
test("should throw ResourceNotFoundError if workspace not found", async () => {
vi.mocked(getEnvironmentStateData).mockRejectedValue(new ResourceNotFoundError("workspace", null));
test("should throw ResourceNotFoundError if project not found", async () => {
vi.mocked(getEnvironmentStateData).mockRejectedValue(new ResourceNotFoundError("project", null));
await expect(getEnvironmentState(environmentId)).rejects.toThrow(ResourceNotFoundError);
});
@@ -280,12 +276,7 @@ describe("getEnvironmentState", () => {
const result = await getEnvironmentState(environmentId);
// Backwards compat: each survey includes `projectOverwrites`
const expectedSurveys = mixedSurveys.map((s) => ({
...s,
projectOverwrites: (s as Record<string, unknown>).workspaceOverwrites ?? null,
}));
expect(result.data.surveys).toEqual(expectedSurveys);
expect(result.data.surveys).toEqual(mixedSurveys);
});
test("should handle empty surveys array", async () => {

View File

@@ -2,10 +2,6 @@ import "server-only";
import { createCacheKey } from "@formbricks/cache";
import { prisma } from "@formbricks/database";
import { TJsEnvironmentState } from "@formbricks/types/js";
import {
addLegacyProjectOverwritesToList,
addLegacyProjectToEnvironmentState,
} from "@/app/lib/api/api-backwards-compat";
import { cache } from "@/lib/cache";
import { IS_RECAPTCHA_CONFIGURED, RECAPTCHA_SITE_KEY } from "@/lib/constants";
import { getEnvironmentStateData } from "./data";
@@ -17,7 +13,7 @@ import { getEnvironmentStateData } from "./data";
*
* @param environmentId - The environment ID to fetch state for
* @returns The environment state
* @throws ResourceNotFoundError if environment, organization, or workspace not found
* @throws ResourceNotFoundError if environment, organization, or project not found
*/
export const getEnvironmentState = async (
environmentId: string
@@ -37,14 +33,12 @@ export const getEnvironmentState = async (
}
// Build the response data
// Backwards compat: include `project` alongside `workspace`, and
// `projectOverwrites` alongside `workspaceOverwrites` in each survey
const data = addLegacyProjectToEnvironmentState({
surveys: addLegacyProjectOverwritesToList(surveys),
const data: TJsEnvironmentState["data"] = {
surveys,
actionClasses,
workspace: environment.workspace,
project: environment.project,
...(IS_RECAPTCHA_CONFIGURED ? { recaptchaSiteKey: RECAPTCHA_SITE_KEY } : {}),
} as TJsEnvironmentState["data"]);
};
return { data };
},

View File

@@ -21,9 +21,9 @@ const apiKeySelect = {
type: true,
createdAt: true,
updatedAt: true,
workspaceId: true,
projectId: true,
appSetupCompleted: true,
workspace: {
project: {
select: {
id: true,
name: true,
@@ -49,9 +49,9 @@ type ApiKeyData = {
type: string;
createdAt: Date;
updatedAt: Date;
workspaceId: string;
projectId: string;
appSetupCompleted: boolean;
workspace: {
project: {
id: string;
name: string;
};
@@ -124,13 +124,10 @@ const buildEnvironmentResponse = (apiKeyData: ApiKeyData) => {
createdAt: env.createdAt,
updatedAt: env.updatedAt,
appSetupCompleted: env.appSetupCompleted,
workspace: {
id: env.workspaceId,
name: env.workspace.name,
project: {
id: env.projectId,
name: env.project.name,
},
// Backwards compat: old consumers expect project fields
projectId: env.workspaceId,
projectName: env.workspace.name,
});
};

View File

@@ -27,7 +27,7 @@ const mockOrganization = {
updatedAt: new Date(),
billing: {
stripeCustomerId: null,
limits: { workspaces: 3, monthly: { responses: null } },
limits: { projects: 3, monthly: { responses: null } },
usageCycleAnchor: new Date(),
} as TOrganizationBilling, // Default no limit
} as unknown as Organization;

View File

@@ -52,8 +52,8 @@ describe("checkAuth", () => {
environmentId: "env-123",
permission: "read",
environmentType: "development",
workspaceId: "workspace-1",
workspaceName: "Workspace 1",
projectId: "project-1",
projectName: "Project 1",
},
],
apiKeyId: "hashed-key",
@@ -84,8 +84,8 @@ describe("checkAuth", () => {
environmentId: "env-123",
permission: "write",
environmentType: "development",
workspaceId: "workspace-1",
workspaceName: "Workspace 1",
projectId: "project-1",
projectName: "Project 1",
},
],
apiKeyId: "hashed-key",

View File

@@ -4,10 +4,6 @@ import { ZSurveyUpdateInput } from "@formbricks/types/surveys/types";
import { handleErrorResponse } from "@/app/api/v1/auth";
import { deleteSurvey } from "@/app/api/v1/management/surveys/[surveyId]/lib/surveys";
import { checkFeaturePermissions } from "@/app/api/v1/management/surveys/lib/utils";
import {
addLegacyProjectOverwrites,
normaliseProjectOverwritesToWorkspace,
} from "@/app/lib/api/api-backwards-compat";
import { responses } from "@/app/lib/api/response";
import {
transformBlocksToQuestions,
@@ -61,21 +57,17 @@ export const GET = withV1ApiWrapper({
if (shouldTransformToQuestions) {
return {
response: responses.successResponse(
addLegacyProjectOverwrites(
resolveStorageUrlsInObject({
...result.survey,
questions: transformBlocksToQuestions(result.survey.blocks, result.survey.endings),
blocks: [],
})
)
resolveStorageUrlsInObject({
...result.survey,
questions: transformBlocksToQuestions(result.survey.blocks, result.survey.endings),
blocks: [],
})
),
};
}
return {
response: responses.successResponse(
addLegacyProjectOverwrites(resolveStorageUrlsInObject(result.survey))
),
response: responses.successResponse(resolveStorageUrlsInObject(result.survey)),
};
} catch (error) {
return {
@@ -167,9 +159,6 @@ export const PUT = withV1ApiWrapper({
};
}
// Backwards compat: accept projectOverwrites as alias for workspaceOverwrites
surveyUpdate = normaliseProjectOverwritesToWorkspace(surveyUpdate);
const validateResult = validateSurveyInput({ ...surveyUpdate, updateOnly: true });
if (!validateResult.ok) {
return {
@@ -222,16 +211,12 @@ export const PUT = withV1ApiWrapper({
};
return {
response: responses.successResponse(
addLegacyProjectOverwrites(resolveStorageUrlsInObject(surveyWithQuestions))
),
response: responses.successResponse(resolveStorageUrlsInObject(surveyWithQuestions)),
};
}
return {
response: responses.successResponse(
addLegacyProjectOverwrites(resolveStorageUrlsInObject(updatedSurvey))
),
response: responses.successResponse(resolveStorageUrlsInObject(updatedSurvey)),
};
} catch (error) {
return {

View File

@@ -42,7 +42,7 @@ const mockOrganization: TOrganization = {
billing: {
stripeCustomerId: null,
limits: {
workspaces: 3,
projects: 3,
monthly: {
responses: 1500,
},
@@ -79,7 +79,7 @@ const mockLanguage: TSurveyCreateInputWithEnvironmentId["languages"][number] = {
code: "en",
alias: "English",
createdAt: new Date(),
workspaceId: "mockWorkspaceId",
projectId: "mockProjectId",
updatedAt: new Date(),
},
default: true,

View File

@@ -2,11 +2,6 @@ import { logger } from "@formbricks/logger";
import { DatabaseError } from "@formbricks/types/errors";
import { ZSurveyCreateInputWithEnvironmentId } from "@formbricks/types/surveys/types";
import { checkFeaturePermissions } from "@/app/api/v1/management/surveys/lib/utils";
import {
addLegacyProjectOverwrites,
addLegacyProjectOverwritesToList,
normaliseProjectOverwritesToWorkspace,
} from "@/app/lib/api/api-backwards-compat";
import { responses } from "@/app/lib/api/response";
import {
transformBlocksToQuestions,
@@ -58,9 +53,7 @@ export const GET = withV1ApiWrapper({
});
return {
response: responses.successResponse(
addLegacyProjectOverwritesToList(resolveStorageUrlsInObject(surveysWithQuestions))
),
response: responses.successResponse(resolveStorageUrlsInObject(surveysWithQuestions)),
};
} catch (error) {
if (error instanceof DatabaseError) {
@@ -90,9 +83,6 @@ export const POST = withV1ApiWrapper({
};
}
// Backwards compat: accept projectOverwrites as alias for workspaceOverwrites
surveyInput = normaliseProjectOverwritesToWorkspace(surveyInput);
const inputValidation = ZSurveyCreateInputWithEnvironmentId.safeParse(surveyInput);
if (!inputValidation.success) {
@@ -157,12 +147,12 @@ export const POST = withV1ApiWrapper({
};
return {
response: responses.successResponse(addLegacyProjectOverwrites(surveyWithQuestions)),
response: responses.successResponse(surveyWithQuestions),
};
}
return {
response: responses.successResponse(addLegacyProjectOverwrites(survey)),
response: responses.successResponse(survey),
};
} catch (error) {
if (error instanceof DatabaseError) {

View File

@@ -22,7 +22,7 @@ describe("getOrganizationBillingByEnvironmentId", () => {
const mockBillingData: TOrganizationBilling = {
limits: {
monthly: { responses: 0 },
workspaces: 3,
projects: 3,
},
usageCycleAnchor: new Date(),
stripeCustomerId: "mock-stripe-customer-id",
@@ -34,7 +34,7 @@ describe("getOrganizationBillingByEnvironmentId", () => {
expect(result).toEqual(mockBillingData);
expect(prisma.organization.findFirst).toHaveBeenCalledWith({
where: {
workspaces: {
projects: {
some: {
environments: {
some: {

View File

@@ -8,7 +8,7 @@ export const getOrganizationBillingByEnvironmentId = reactCache(
try {
const organization = await prisma.organization.findFirst({
where: {
workspaces: {
projects: {
some: {
environments: {
some: {

View File

@@ -86,7 +86,7 @@ const mockSurvey: TSurvey = {
isBackButtonHidden: false,
isSingleResponsePerEmailEnabled: false,
isVerifyEmailEnabled: false,
workspaceOverwrites: null,
projectOverwrites: null,
showLanguageSwitch: false,
blocks: [],
isCaptureIpEnabled: false,
@@ -106,7 +106,7 @@ const mockResponseInput: TResponseInputV2 = {
const mockBillingData: TOrganizationBilling = {
limits: {
monthly: { responses: 0 },
workspaces: 3,
projects: 3,
},
usageCycleAnchor: new Date(),
stripeCustomerId: "mock-stripe-customer-id",

View File

@@ -0,0 +1,3 @@
import { DELETE, GET, POST, PUT } from "@/modules/api/v2/organizations/[organizationId]/project-teams/route";
export { GET, POST, PUT, DELETE };

View File

@@ -1,8 +0,0 @@
import {
DELETE,
GET,
POST,
PUT,
} from "@/modules/api/v2/organizations/[organizationId]/workspace-teams/route";
export { GET, POST, PUT, DELETE };

View File

@@ -2,7 +2,7 @@ import { ApiKeyPermission, EnvironmentType } from "@prisma/client";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
import { getOrganizationIdFromProjectId } from "@/lib/utils/helper";
import { getEnvironment } from "@/lib/utils/services";
import { requireSessionWorkspaceAccess, requireV3WorkspaceAccess } from "./auth";
@@ -16,7 +16,7 @@ vi.mock("@formbricks/logger", () => ({
}));
vi.mock("@/lib/utils/helper", () => ({
getOrganizationIdFromWorkspaceId: vi.fn(),
getOrganizationIdFromProjectId: vi.fn(),
}));
vi.mock("@/lib/utils/services", () => ({
@@ -79,9 +79,9 @@ describe("requireSessionWorkspaceAccess", () => {
test("returns 403 when user has no access to workspace", async () => {
vi.mocked(getEnvironment).mockResolvedValueOnce({
id: "env_abc",
workspaceId: "proj_abc",
projectId: "proj_abc",
} as any);
vi.mocked(getOrganizationIdFromWorkspaceId).mockResolvedValueOnce("org_1");
vi.mocked(getOrganizationIdFromProjectId).mockResolvedValueOnce("org_1");
vi.mocked(checkAuthorizationUpdated).mockRejectedValueOnce(new AuthorizationError("Not authorized"));
const result = await requireSessionWorkspaceAccess(
{ user: { id: "user_1" }, expires: "" } as any,
@@ -99,7 +99,7 @@ describe("requireSessionWorkspaceAccess", () => {
organizationId: "org_1",
access: [
{ type: "organization", roles: ["owner", "manager"] },
{ type: "workspaceTeam", workspaceId: "proj_abc", minPermission: "read" },
{ type: "projectTeam", projectId: "proj_abc", minPermission: "read" },
],
});
});
@@ -107,9 +107,9 @@ describe("requireSessionWorkspaceAccess", () => {
test("returns workspace context when session is valid and user has access", async () => {
vi.mocked(getEnvironment).mockResolvedValueOnce({
id: "env_abc",
workspaceId: "proj_abc",
projectId: "proj_abc",
} as any);
vi.mocked(getOrganizationIdFromWorkspaceId).mockResolvedValueOnce("org_1");
vi.mocked(getOrganizationIdFromProjectId).mockResolvedValueOnce("org_1");
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(undefined as any);
const result = await requireSessionWorkspaceAccess(
{ user: { id: "user_1" }, expires: "" } as any,
@@ -120,7 +120,7 @@ describe("requireSessionWorkspaceAccess", () => {
expect(result).not.toBeInstanceOf(Response);
expect(result).toEqual({
environmentId: "env_abc",
workspaceId: "proj_abc",
projectId: "proj_abc",
organizationId: "org_1",
});
expect(checkAuthorizationUpdated).toHaveBeenCalledWith({
@@ -128,7 +128,7 @@ describe("requireSessionWorkspaceAccess", () => {
organizationId: "org_1",
access: [
{ type: "organization", roles: ["owner", "manager"] },
{ type: "workspaceTeam", workspaceId: "proj_abc", minPermission: "readWrite" },
{ type: "projectTeam", projectId: "proj_abc", minPermission: "readWrite" },
],
});
});
@@ -145,8 +145,8 @@ function envPerm(environmentId: string, permission: ApiKeyPermission = ApiKeyPer
return {
environmentId,
environmentType: EnvironmentType.development,
workspaceId: "proj_k",
workspaceName: "K",
projectId: "proj_k",
projectName: "K",
permission,
};
}
@@ -155,9 +155,9 @@ describe("requireV3WorkspaceAccess", () => {
beforeEach(() => {
vi.mocked(getEnvironment).mockResolvedValue({
id: "env_k",
workspaceId: "proj_k",
projectId: "proj_k",
} as any);
vi.mocked(getOrganizationIdFromWorkspaceId).mockResolvedValue("org_k");
vi.mocked(getOrganizationIdFromProjectId).mockResolvedValue("org_k");
});
test("401 when authentication is null", async () => {
@@ -168,9 +168,9 @@ describe("requireV3WorkspaceAccess", () => {
test("delegates to session flow when user is present", async () => {
vi.mocked(getEnvironment).mockResolvedValueOnce({
id: "env_s",
workspaceId: "proj_s",
projectId: "proj_s",
} as any);
vi.mocked(getOrganizationIdFromWorkspaceId).mockResolvedValueOnce("org_s");
vi.mocked(getOrganizationIdFromProjectId).mockResolvedValueOnce("org_s");
vi.mocked(checkAuthorizationUpdated).mockResolvedValueOnce(undefined as any);
const r = await requireV3WorkspaceAccess(
{ user: { id: "user_1" }, expires: "" } as any,
@@ -180,7 +180,7 @@ describe("requireV3WorkspaceAccess", () => {
);
expect(r).toEqual({
environmentId: "env_s",
workspaceId: "proj_s",
projectId: "proj_s",
organizationId: "org_s",
});
});
@@ -193,7 +193,7 @@ describe("requireV3WorkspaceAccess", () => {
const r = await requireV3WorkspaceAccess(auth as any, "ws_a", "read", requestId);
expect(r).toEqual({
environmentId: "ws_a",
workspaceId: "proj_k",
projectId: "proj_k",
organizationId: "org_k",
});
expect(getEnvironment).toHaveBeenCalledWith("ws_a");
@@ -207,7 +207,7 @@ describe("requireV3WorkspaceAccess", () => {
const r = await requireV3WorkspaceAccess(auth as any, "ws_b", "read", requestId);
expect(r).toEqual({
environmentId: "ws_b",
workspaceId: "proj_k",
projectId: "proj_k",
organizationId: "org_k",
});
});
@@ -252,7 +252,7 @@ describe("requireV3WorkspaceAccess", () => {
const r = await requireV3WorkspaceAccess(auth as any, "ws_m", "manage", requestId);
expect(r).toEqual({
environmentId: "ws_m",
workspaceId: "proj_k",
projectId: "proj_k",
organizationId: "org_k",
});
});

View File

@@ -6,7 +6,7 @@ import { logger } from "@formbricks/logger";
import type { TAuthenticationApiKey } from "@formbricks/types/auth";
import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import type { TTeamPermission } from "@/modules/ee/teams/workspace-teams/types/team";
import type { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
import { problemForbidden, problemUnauthorized } from "./response";
import type { TV3Authentication } from "./types";
import { type V3WorkspaceContext, resolveV3WorkspaceContext } from "./workspace-context";
@@ -30,7 +30,7 @@ function apiKeyPermissionAllows(permission: ApiKeyPermission, minPermission: TTe
/**
* Require session and workspace access. workspaceId is resolved via the V3 workspace-context layer.
* Returns a Response (401 or 403) on failure, or the resolved workspace context on success so callers
* use internal IDs (environmentId, workspaceId, organizationId) without resolving again.
* use internal IDs (environmentId, projectId, organizationId) without resolving again.
* We use 403 (not 404) when the workspace is not found to avoid leaking resource existence.
*/
export async function requireSessionWorkspaceAccess(
@@ -52,16 +52,16 @@ export async function requireSessionWorkspaceAccess(
const log = logger.withContext({ requestId, workspaceId });
try {
// Resolve workspaceId → environmentId, workspaceId, organizationId (single place to change when Workspace exists).
// Resolve workspaceId → environmentId, projectId, organizationId (single place to change when Workspace exists).
const context = await resolveV3WorkspaceContext(workspaceId);
// Org + workspace-team access; we use internal IDs from context.
// Org + project-team access; we use internal IDs from context.
await checkAuthorizationUpdated({
userId,
organizationId: context.organizationId,
access: [
{ type: "organization", roles: ["owner", "manager"] },
{ type: "workspaceTeam", workspaceId: context.workspaceId, minPermission },
{ type: "projectTeam", projectId: context.projectId, minPermission },
],
});

View File

@@ -1,11 +1,11 @@
import { describe, expect, test, vi } from "vitest";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
import { getOrganizationIdFromProjectId } from "@/lib/utils/helper";
import { getEnvironment } from "@/lib/utils/services";
import { resolveV3WorkspaceContext } from "./workspace-context";
vi.mock("@/lib/utils/helper", () => ({
getOrganizationIdFromWorkspaceId: vi.fn(),
getOrganizationIdFromProjectId: vi.fn(),
}));
vi.mock("@/lib/utils/services", () => ({
@@ -13,26 +13,26 @@ vi.mock("@/lib/utils/services", () => ({
}));
describe("resolveV3WorkspaceContext", () => {
test("returns environmentId, workspaceId and organizationId when workspace exists (today: workspaceId === environmentId)", async () => {
test("returns environmentId, projectId and organizationId when workspace exists (today: workspaceId === environmentId)", async () => {
vi.mocked(getEnvironment).mockResolvedValueOnce({
id: "env_abc",
workspaceId: "proj_xyz",
projectId: "proj_xyz",
} as any);
vi.mocked(getOrganizationIdFromWorkspaceId).mockResolvedValueOnce("org_123");
vi.mocked(getOrganizationIdFromProjectId).mockResolvedValueOnce("org_123");
const result = await resolveV3WorkspaceContext("env_abc");
expect(result).toEqual({
environmentId: "env_abc",
workspaceId: "proj_xyz",
projectId: "proj_xyz",
organizationId: "org_123",
});
expect(getEnvironment).toHaveBeenCalledWith("env_abc");
expect(getOrganizationIdFromWorkspaceId).toHaveBeenCalledWith("proj_xyz");
expect(getOrganizationIdFromProjectId).toHaveBeenCalledWith("proj_xyz");
});
test("throws when workspace (environment) does not exist", async () => {
vi.mocked(getEnvironment).mockResolvedValueOnce(null);
await expect(resolveV3WorkspaceContext("env_nonexistent")).rejects.toThrow(ResourceNotFoundError);
expect(getEnvironment).toHaveBeenCalledWith("env_nonexistent");
expect(getOrganizationIdFromWorkspaceId).not.toHaveBeenCalled();
expect(getOrganizationIdFromProjectId).not.toHaveBeenCalled();
});
});

View File

@@ -9,7 +9,7 @@
* (and derive environmentId or equivalent from it). Change only this file.
*/
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
import { getOrganizationIdFromProjectId } from "@/lib/utils/helper";
import { getEnvironment } from "@/lib/utils/services";
/**
@@ -19,14 +19,14 @@ import { getEnvironment } from "@/lib/utils/services";
export type V3WorkspaceContext = {
/** Environment ID — the container for surveys today. Replaced by workspace when Environment is deprecated. */
environmentId: string;
/** Workspace ID used for workspaceTeam auth. */
workspaceId: string;
/** Project ID used for projectTeam auth. */
projectId: string;
/** Organization ID used for org-level auth. */
organizationId: string;
};
/**
* Resolves a V3 API workspaceId to internal environmentId, workspaceId, and organizationId.
* Resolves a V3 API workspaceId to internal environmentId, projectId, and organizationId.
* Today: workspaceId is treated as environmentId (workspace = container for surveys = Environment).
*
* @throws ResourceNotFoundError if the workspace (environment) does not exist.
@@ -38,13 +38,13 @@ export async function resolveV3WorkspaceContext(workspaceId: string): Promise<V3
throw new ResourceNotFoundError("environment", workspaceId);
}
// Derive org for auth; workspace comes from the environment.
const organizationId = await getOrganizationIdFromWorkspaceId(environment.workspaceId);
// Derive org for auth; project comes from the environment.
const organizationId = await getOrganizationIdFromProjectId(environment.projectId);
// We looked up by workspaceId (as environment id), so the resolved environment id is workspaceId.
return {
environmentId: workspaceId,
workspaceId: environment.workspaceId,
projectId: environment.projectId,
organizationId,
};
}

View File

@@ -81,8 +81,8 @@ const apiKeyAuth = {
{
environmentId: validWorkspaceId,
environmentType: EnvironmentType.development,
workspaceId: "proj_1",
workspaceName: "P",
projectId: "proj_1",
projectName: "P",
permission: ApiKeyPermission.read,
},
],
@@ -112,13 +112,13 @@ describe("GET /api/v3/surveys", () => {
}
return {
environmentId: workspaceId,
workspaceId: p.workspaceId,
projectId: p.projectId,
organizationId: auth.organizationId,
};
}
return {
environmentId: resolvedEnvironmentId,
workspaceId: "proj_1",
projectId: "proj_1",
organizationId: "org_1",
};
});
@@ -194,8 +194,8 @@ describe("GET /api/v3/surveys", () => {
{
environmentId: "claa1111111111111111111111",
environmentType: EnvironmentType.development,
workspaceId: "proj_x",
workspaceName: "X",
projectId: "proj_x",
projectName: "X",
permission: ApiKeyPermission.read,
},
],

View File

@@ -1,65 +0,0 @@
/**
* Backwards compatibility layer for the project → workspace rename across APIs.
*
* Provides utilities to normalise legacy `project*` field names to their `workspace*`
* equivalents in request bodies and enrich responses with legacy fields so existing
* integrations keep working.
*/
// ---------------------------------------------------------------------------
// Input transformation: accept `projectOverwrites` as an alias for `workspaceOverwrites`
// ---------------------------------------------------------------------------
/**
* Normalise a survey request body so that `projectOverwrites` is mapped to `workspaceOverwrites`.
* If both are provided, `workspaceOverwrites` takes precedence.
*/
export const normaliseProjectOverwritesToWorkspace = <T extends Record<string, unknown>>(input: T): T => {
if ("projectOverwrites" in input && !("workspaceOverwrites" in input)) {
const { projectOverwrites, ...rest } = input;
return { ...rest, workspaceOverwrites: projectOverwrites } as unknown as T;
}
// Drop stale projectOverwrites if workspaceOverwrites is already present
if ("projectOverwrites" in input && "workspaceOverwrites" in input) {
const { projectOverwrites: _, ...rest } = input;
return rest as unknown as T;
}
return input;
};
// ---------------------------------------------------------------------------
// Output transformation: include legacy `projectOverwrites` alongside `workspaceOverwrites`
// ---------------------------------------------------------------------------
/**
* Add `projectOverwrites` to a survey response object, mirroring `workspaceOverwrites`.
*/
export const addLegacyProjectOverwrites = <T extends Record<string, unknown>>(survey: T): T => {
if ("workspaceOverwrites" in survey) {
return { ...survey, projectOverwrites: survey.workspaceOverwrites };
}
return survey;
};
/**
* Add `projectOverwrites` to each survey in a list response.
*/
export const addLegacyProjectOverwritesToList = <T extends Record<string, unknown>>(surveys: T[]): T[] =>
surveys.map(addLegacyProjectOverwrites);
// ---------------------------------------------------------------------------
// Environment state output: include legacy `project` key alongside `workspace`
// ---------------------------------------------------------------------------
/**
* Enrich an environment state data object to include legacy `project` key
* alongside `workspace` so old SDK consumers still see the field they expect.
*/
export const addLegacyProjectToEnvironmentState = <T extends Record<string, unknown>>(data: T): T => {
if ("workspace" in data && !("project" in data)) {
return { ...data, project: data.workspace };
}
return data;
};

View File

@@ -1,10 +1,10 @@
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach, describe, expect, test } from "vitest";
import { TLanguage } from "@formbricks/types/project";
import { type TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types";
import { TTag } from "@formbricks/types/tags";
import { TLanguage } from "@formbricks/types/workspace";
import {
DateRange,
SelectedFilterValue,

View File

@@ -1647,7 +1647,7 @@ const identifyCustomerGoals = (t: TFunction): TTemplate => {
elements: [
buildMultipleChoiceElement({
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
headline: "What's your primary goal for using $[workspaceName]?",
headline: "What's your primary goal for using $[projectName]?",
required: true,
shuffleOption: "none",
choices: [
@@ -4815,7 +4815,7 @@ export const customSurveyTemplate = (t: TFunction): TTemplate => {
};
};
export const previewSurvey = (workspaceName: string, t: TFunction): TSurvey => {
export const previewSurvey = (projectName: string, t: TFunction): TSurvey => {
return {
id: "cltxxaa6x0000g8hacxdxejeu",
createdAt: new Date(),
@@ -4878,7 +4878,7 @@ export const previewSurvey = (workspaceName: string, t: TFunction): TSurvey => {
id: "lbdxozwikh838yc6a8vbwuju",
range: 5,
scale: "star",
headline: t("templates.preview_survey_question_1_headline", { workspaceName }),
headline: t("templates.preview_survey_question_1_headline", { projectName }),
required: true,
subheader: t("templates.preview_survey_question_1_subheader"),
lowerLabel: t("templates.preview_survey_question_1_lower_label"),
@@ -4914,7 +4914,7 @@ export const previewSurvey = (workspaceName: string, t: TFunction): TSurvey => {
autoComplete: 50,
isVerifyEmailEnabled: false,
isSingleResponsePerEmailEnabled: false,
workspaceOverwrites: null,
projectOverwrites: null,
surveyClosedMessage: null,
singleUse: {
enabled: false,

View File

@@ -7,8 +7,8 @@ import { getIsFreshInstance } from "@/lib/instance/service";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils";
import { getOrganizationsByUserId } from "@/lib/organization/service";
import { getUserProjectEnvironmentsByOrganizationIds } from "@/lib/project/service";
import { getUser } from "@/lib/user/service";
import { getUserWorkspaceEnvironmentsByOrganizationIds } from "@/lib/workspace/service";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { ClientLogout } from "@/modules/ui/components/client-logout";
@@ -35,13 +35,13 @@ const Page = async () => {
return redirect("/setup/organization/create");
}
const workspacesByOrg = await getUserWorkspaceEnvironmentsByOrganizationIds(
const projectsByOrg = await getUserProjectEnvironmentsByOrganizationIds(
userOrganizations.map((org) => org.id),
user.id
);
// Flatten all environments from all workspaces across all organizations
const allEnvironments = workspacesByOrg.flatMap((workspace) => workspace.environments);
// Flatten all environments from all projects across all organizations
const allEnvironments = projectsByOrg.flatMap((project) => project.environments);
// Find first production environment and collect all other environment IDs in one pass
const { firstProductionEnvironmentId, otherEnvironmentIds } = allEnvironments.reduce(

View File

@@ -91,7 +91,6 @@ checksums:
common/action: c92af0bdf1698b0d10cf5b28d2ad4945
common/actions: c46571856723b03262fd33f511116298
common/actions_description: 8e35b1538d1006fa8470183310ad21ef
common/active: 3e1ec025c4a50830bbb9ad57a176630a
common/active_surveys: 95bf0fd5d2bf62cdd010b7cf66795ed7
common/activity: 1948763de8e531483a798b68195e297e
common/add: 87c4a663507f2bcbbf79934af8164e13
@@ -113,7 +112,6 @@ checksums:
common/app: 77e32ac4e5a1e01bc9a6a15fdfef9bf8
common/app_survey: f076d131d20bfdadb35fba29c8275232
common/apply_filters: 6543c1e80038b3da0f4a42848d08d4d1
common/archived: cf5127ecfd7e43a35466a1ba5fe16450
common/are_you_sure: 6d5cd13628a7887711fd0c29f1123652
common/attributes: 86d0ae6fea0fbb119722ed3841f8385a
common/back: f541015a827e37cb3b1234e56bc2aa3c
@@ -459,7 +457,7 @@ checksums:
common/you: db2a4a796b70cc1430d1b21f6ffb6dcb
common/you_are_downgraded_to_the_community_edition: e3ae56502ff787109cae0997519f628e
common/you_are_not_authorized_to_perform_this_action: 1b3255ab740582ddff016a399f8bf302
common/you_have_reached_your_limit_of_workspace_limit: 506a6ee315d9754da7ea26929bc40f52
common/you_have_reached_your_limit_of_workspace_limit: 54d754c3267036742f23fb05fd3fcc45
common/you_have_reached_your_monthly_response_limit_of: 3824db23ecc3dcd2b1787b98ccfdd5f9
common/you_will_be_downgraded_to_the_community_edition_on_date: bff35b54c13e2c205dc4c19056261cc0
common/your_license_has_expired_please_renew: 3f21ae4a7deab351b143b407ece58254
@@ -1027,6 +1025,7 @@ checksums:
environments/settings/enterprise/license_feature_access_control: bdc5ce7e88ad724d4abd3e8a07a9de5d
environments/settings/enterprise/license_feature_audit_logs: e93f59c176cfc8460d2bd56551ed78b8
environments/settings/enterprise/license_feature_contacts: fd76522bc82324ac914e124cdf9935b0
environments/settings/enterprise/license_feature_projects: 8ba082a84aa35cf851af1cf874b853e2
environments/settings/enterprise/license_feature_quotas: e6afead11b5b8ae627885ce2b84a548f
environments/settings/enterprise/license_feature_remove_branding: a5c71d43cd3ed25e6e48bca64e8ffc9f
environments/settings/enterprise/license_feature_saml: 86b76024524fc585b2c3950126ef6f62
@@ -1034,7 +1033,6 @@ checksums:
environments/settings/enterprise/license_feature_sso: 8c029b7dd2cb3aa1393d2814aba6cd7b
environments/settings/enterprise/license_feature_two_factor_auth: bc68ddd9c3c82225ef641f097e0940db
environments/settings/enterprise/license_feature_whitelabel: 81e9ec1d4230419f4230e6f5a318497c
environments/settings/enterprise/license_feature_workspaces: 8ba082a84aa35cf851af1cf874b853e2
environments/settings/enterprise/license_features_table_access: 550606d4a12bdf108c1b12b925ca1b3a
environments/settings/enterprise/license_features_table_description: d6260830d0703f5a2c9ed59c9da462e3
environments/settings/enterprise/license_features_table_disabled: 0889a3dfd914a7ef638611796b17bf72
@@ -1072,32 +1070,6 @@ checksums:
environments/settings/enterprise/sso: 95e98e279bb89233d63549b202bd9112
environments/settings/enterprise/teams: 21ab78abcba0f16c3029741563f789ea
environments/settings/enterprise/unlock_the_full_power_of_formbricks_free_for_30_days: 104d07b63a42911c9673ceb08a4dbd43
environments/settings/feedback_record_directories/archive: fa813ab3074103e5daad07462af25789
environments/settings/feedback_record_directories/archive_directory: 1114a8e1c5c5af3d30a174b28582f424
environments/settings/feedback_record_directories/archive_not_allowed: 3ffe3336572a633406858887de60a470
environments/settings/feedback_record_directories/are_you_sure_you_want_to_archive: d249e6e8bc0345835a13f70856eb1c30
environments/settings/feedback_record_directories/assign_workspaces_description: 6c3f0bbf3bd7744bb313f4cd7886e184
environments/settings/feedback_record_directories/create_feedback_directory: c178dd6dbd702398df3ac08a9fa43324
environments/settings/feedback_record_directories/description: 8f56b169cb38d8c7b2697bf3a3ed7a61
environments/settings/feedback_record_directories/directory_archived_successfully: fba5b99ced59d0546c8f2241c092a5dd
environments/settings/feedback_record_directories/directory_created_successfully: 5db20153b840d91842543f71cdd91043
environments/settings/feedback_record_directories/directory_id: 933a1376d7d8a8dc41ded90ef1c0f619
environments/settings/feedback_record_directories/directory_name: 353de006e16451bf64da469a81fbe451
environments/settings/feedback_record_directories/directory_settings_description: 895d890b4292effa5ade45fe7164990f
environments/settings/feedback_record_directories/directory_settings_title: c32af860e3254dea9dfaeefc7cd92d49
environments/settings/feedback_record_directories/directory_unarchived_successfully: 08d56e260decc62fe664b50ab774b728
environments/settings/feedback_record_directories/directory_updated_successfully: 638cb6c92f535328d809274cf2be4d7d
environments/settings/feedback_record_directories/empty_state: 665593dcb7cfa081a3e719677d0f6b0d
environments/settings/feedback_record_directories/enter_directory_name: a1c950988199bb4c4e014dcf430cce41
environments/settings/feedback_record_directories/error_directory_name_duplicate: 349d650f562cff96b084787126323ca2
environments/settings/feedback_record_directories/error_directory_name_required: 0f42d7292979006a1069063ab213b8e3
environments/settings/feedback_record_directories/error_directory_workspaces_invalid_org: 477b5c1a466c4194668544ffd42ec9bf
environments/settings/feedback_record_directories/nav_label: cf9a57b3cbac0f04b98e06fb693e986e
environments/settings/feedback_record_directories/no_access: cc3385cd01a11e3949003a2cc6fb5b31
environments/settings/feedback_record_directories/select_workspaces_placeholder: 7d8c8f5910b264525f73bd32107765db
environments/settings/feedback_record_directories/show_archived: c4c1c3bbddc1bb1540c079b589a2d3de
environments/settings/feedback_record_directories/title: e3d425c27f80162f29ce094e31a3fd8f
environments/settings/feedback_record_directories/unarchive: 671fc7e9d7c8cb4d182a25a46551c168
environments/settings/general/bulk_invite_warning_description: e8737a2fbd5ff353db5580d17b4b5a37
environments/settings/general/cannot_delete_only_organization: 833cc6848b28f2694a4552b4de91a6ba
environments/settings/general/cannot_leave_only_organization: dd8463262e4299fef7ad73512225c55b
@@ -2090,8 +2062,8 @@ checksums:
environments/workspace/general/custom_scripts_updated_successfully: eabe8e6ededa86342d59093fe308c681
environments/workspace/general/custom_scripts_warning: 5faa0f284d48110918a5e8a467e2bcb8
environments/workspace/general/delete_workspace: 3badbc0f4b49644986fc19d8b2d8f317
environments/workspace/general/delete_workspace_confirmation: 2ab40f39fc52289953828ec078c8c36d
environments/workspace/general/delete_workspace_name_includes_surveys_responses_people_and_more: 7cc851b70a3081cf5bcbb862486e90a5
environments/workspace/general/delete_workspace_confirmation: 54a4ee78867537e0244c7170453cdb3f
environments/workspace/general/delete_workspace_name_includes_surveys_responses_people_and_more: 1b6c0597fddc5b6604e3a204402ed35e
environments/workspace/general/delete_workspace_settings_description: 411ef100f167fc8fca64e833b6c0d030
environments/workspace/general/error_saving_workspace_information: e7b8022785619ef34de1fb1630b3c476
environments/workspace/general/only_owners_or_managers_can_delete_workspaces: 58da180cd2610210302d85a9896d80bd
@@ -2276,8 +2248,8 @@ checksums:
organizations/workspaces/new/settings/create_new_team: 3f4d388a473f979d4bd366c3ef08e425
organizations/workspaces/new/settings/team_description: 534ba98e256646074a03949daf85665d
organizations/workspaces/new/settings/workspace_creation_failed: 5c6a0e087d481255a1b201c0287d0221
organizations/workspaces/new/settings/workspace_name: 855614bbf862fd6a9f6a3e76943f92d4
organizations/workspaces/new/settings/workspace_name_description: 3dc82d99f2ac9789c5c92b64a4addc0a
organizations/workspaces/new/settings/workspace_name: 7002a32bc4ce3343ede2076b30649ff7
organizations/workspaces/new/settings/workspace_name_description: 0885c4cd8cc8836e74b6ccdb5d25b43d
organizations/workspaces/new/settings/workspace_settings_subtitle: 83a72b3e9c08f2936524fcd11b29421b
organizations/workspaces/new/settings/workspace_settings_title: a4ec3549507071e1f7d3c834b019fcce
s/check_inbox_or_spam: c48ac1f7b76052881bb3b6d10615152d
@@ -2353,10 +2325,10 @@ checksums:
templates/book_interview: 1cc9c72d1c088b28e5dfa5ec7d7b78c4
templates/build_product_roadmap_description: 6ca163ed3b0095cedcbc11822a0d502a
templates/build_product_roadmap_name: 8c216b183c3539c0340ce87465a391cc
templates/build_product_roadmap_question_1_headline: 2f6f6ea1b3651fabba19a436e9b8417b
templates/build_product_roadmap_question_1_headline: 8875cb9cdb0dd7e1f4c471ac0f096e01
templates/build_product_roadmap_question_1_lower_label: 83c1f45c6c471dbab0277948397a1af3
templates/build_product_roadmap_question_1_upper_label: c24f08a4a3b5d3fbfb37eb948d594087
templates/build_product_roadmap_question_2_headline: 570b1bd272200749e7e43d75802100ce
templates/build_product_roadmap_question_2_headline: 123c5e407108afe5f5e179aa1f26f012
templates/build_product_roadmap_question_2_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
templates/card_abandonment_survey: 705c3dfcc7f6de3a445aaefe0d68c43f
templates/card_abandonment_survey_description: a3db29212b51402a7659a76248299798
@@ -2389,10 +2361,10 @@ checksums:
templates/card_abandonment_survey_question_8_headline: ef1289130df46b80d43119380095b579
templates/career_development_survey_description: 00e92f566813b67301934bf24956f2d8
templates/career_development_survey_name: d92b724f8c749bb9c859a14db164b257
templates/career_development_survey_question_1_headline: f03cc6bec0bd2c7b1560fed0b0d57f9d
templates/career_development_survey_question_1_headline: 663cb1f5a371e4c1e1e579d9722acc05
templates/career_development_survey_question_1_lower_label: 586eedbc7b53319775e42c7cd4cef4de
templates/career_development_survey_question_1_upper_label: a04ddfea06817ade6c56cff126cfca80
templates/career_development_survey_question_2_headline: 33fcb2c577264b09f39fe09f1ef1b4a9
templates/career_development_survey_question_2_headline: 0890709df1c9d6fcb003845fe3014f16
templates/career_development_survey_question_2_lower_label: 586eedbc7b53319775e42c7cd4cef4de
templates/career_development_survey_question_2_upper_label: a04ddfea06817ade6c56cff126cfca80
templates/career_development_survey_question_3_headline: 47d01a8841cdfbe076cf9eb4e14083ed
@@ -2418,7 +2390,7 @@ checksums:
templates/career_development_survey_question_6_headline: 88d2a87cbf2ec21882798890990c2225
templates/career_development_survey_question_6_subheader: b9b478e967930358b0c74324a7c18fc8
templates/cess_survey_name: dd706043a56d66f2895cad743935c5b4
templates/cess_survey_question_1_headline: 70115a7960746a05acef03f815652fc3
templates/cess_survey_question_1_headline: 1f75deab2d159eb8c33c37cc51ef4b3e
templates/cess_survey_question_1_lower_label: 586eedbc7b53319775e42c7cd4cef4de
templates/cess_survey_question_1_upper_label: a04ddfea06817ade6c56cff126cfca80
templates/cess_survey_question_2_headline: b05b561f04753ae81febee18e08ce2dd
@@ -2445,7 +2417,7 @@ checksums:
templates/churn_survey_question_1_headline: 899d5501fa89e0ca8cff72b050a14420
templates/churn_survey_question_1_subheader: 1861f78f2e78068dc80980efdce9282a
templates/churn_survey_question_2_button_label: 76a8497d7b546628b03bb81d5c1ce995
templates/churn_survey_question_2_headline: f24be65a7ea59614a3d6923520c374f4
templates/churn_survey_question_2_headline: 17d3e7e2ce62af5ef9332c0d208f9172
templates/churn_survey_question_3_button_label: 43834ccf20c1c7cd49382468abe2edce
templates/churn_survey_question_3_headline: 76444078de5c30666ff65f453f60b420
templates/churn_survey_question_3_html: b14d1ca18d779f49fc64015b36f1d1ad
@@ -2484,7 +2456,7 @@ checksums:
templates/csat_name: f216066cef52693bbaa842a3305377c7
templates/csat_question_10_headline: b6a9ca9c6c20dced146d817c9a1e9be7
templates/csat_question_10_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
templates/csat_question_1_headline: c3b9d9d9904c41c1f43c75ef8194ffda
templates/csat_question_1_headline: bd4894e95695ce5bc9fc5d326c79bc90
templates/csat_question_1_lower_label: 54d464343c0bc17231fd51aa2d73623f
templates/csat_question_1_upper_label: 9f000f63949d875ae628fc354a2a7f6a
templates/csat_question_2_choice_1: a0cf57bc571c95c43924a3c641d1355e
@@ -2492,7 +2464,7 @@ checksums:
templates/csat_question_2_choice_3: a7c58d9b8afdaefadeb1f5fdf4d5ad3f
templates/csat_question_2_choice_4: d09723c4bc1d85d99c2a9248ed0d4578
templates/csat_question_2_choice_5: a89ca2602a3322e89adf17b3349e03ab
templates/csat_question_2_headline: 0bb37bda01ec0a7ceb7ee527e9944e73
templates/csat_question_2_headline: 4e6dc882f742536accbd36e9a301ab57
templates/csat_question_2_subheader: c1fe8f70be7d41a4daa6c9a9ef60eafa
templates/csat_question_3_choice_1: 095b4b4aaf4aee8a77da3bf707a3b6e1
templates/csat_question_3_choice_10: a9886a5199df1d581aa9005a123af060
@@ -2504,28 +2476,28 @@ checksums:
templates/csat_question_3_choice_7: db68abbd863987837097aac843c8b6ad
templates/csat_question_3_choice_8: 6abbde1174a09d7b9d61de382f7f9677
templates/csat_question_3_choice_9: 4c3429e68c6fbd9a8808285838054e0c
templates/csat_question_3_headline: 9da4c07c08de5655bc91b90a0ea06d37
templates/csat_question_3_headline: 8c6ffa4d4c0fef480bf22f61dbea5ce6
templates/csat_question_3_subheader: ed43eb9723166fd6a8d8e44356def97e
templates/csat_question_4_choice_1: 3af1ce39d4f133a26c4272c8245ded07
templates/csat_question_4_choice_2: 78f2c2b121ffbb4633452f7da4e9c338
templates/csat_question_4_choice_3: 315f0d4c1497f69ef252d86e486fc715
templates/csat_question_4_choice_4: 3e123a754f7c5062b253814ca6fa7a1a
templates/csat_question_4_choice_5: 2a0c59ef15817ff02fecb0544b144562
templates/csat_question_4_headline: 80192356fb9c83d82bb245bed3d50779
templates/csat_question_4_headline: e082eacf184ec07a306977d36df3ec2c
templates/csat_question_4_subheader: aa206f483bcb547d10599b1e564cc813
templates/csat_question_5_choice_1: 2f6dd5a4f63df441b73a1e91ff1fb1f7
templates/csat_question_5_choice_2: 0036f3b944c7a367567c35dadea42ae6
templates/csat_question_5_choice_3: 8c99a10632579680c30ca19951c115cc
templates/csat_question_5_choice_4: 60e3f3b75e9082779eebff3a12892c78
templates/csat_question_5_choice_5: 2c079befacaca59ab5941c5e5f655ad5
templates/csat_question_5_headline: 7ed7fd916b071e76a00001bf5ca492fe
templates/csat_question_5_headline: 11a8884bd3ec93a32bb73caf550fd2a3
templates/csat_question_5_subheader: aa206f483bcb547d10599b1e564cc813
templates/csat_question_6_choice_1: 16372294108f169683263c353c4071dc
templates/csat_question_6_choice_2: 4adfac25f3b96d2f767c22c165623d35
templates/csat_question_6_choice_3: 8ebb872222898ed8785e5a9d77c5a2c3
templates/csat_question_6_choice_4: a4b641cd9783e7a4c33b5c75d42134ca
templates/csat_question_6_choice_5: ee43b88abd26e897ad4a612361c2ae2b
templates/csat_question_6_headline: ddfda6f1ff03f2b74e82304b5c26fa6c
templates/csat_question_6_headline: 699bcc628559419dde25786c58459b11
templates/csat_question_6_subheader: c1fe8f70be7d41a4daa6c9a9ef60eafa
templates/csat_question_7_choice_1: a6b5a70cac6cc35275b3e1bc3582361a
templates/csat_question_7_choice_2: fe4f1b5fe2fac2fceb75bfcb721d0250
@@ -2539,17 +2511,17 @@ checksums:
templates/csat_question_8_choice_3: 50fd311f267683de79c4c0c871be81c4
templates/csat_question_8_choice_4: 073b66551a87590137d8df50f7534869
templates/csat_question_8_choice_5: a94c895f9ea70203cddbf7d783c06fcc
templates/csat_question_8_headline: 5707d6be8ef4cb97a04b4b2b8a0ba5fa
templates/csat_question_8_headline: 8e95e1360583899191632a8be2764571
templates/csat_question_8_subheader: c1fe8f70be7d41a4daa6c9a9ef60eafa
templates/csat_question_9_choice_1: 77d668acd2d8fd6dd1ccc8960380dfb8
templates/csat_question_9_choice_2: 9f000f63949d875ae628fc354a2a7f6a
templates/csat_question_9_choice_3: d50b9b06372ff4e681253558a1b1925c
templates/csat_question_9_choice_4: a5672052779a848e42aa0723de7aff84
templates/csat_question_9_choice_5: d9de99c54e06922698076a00c7ae29e1
templates/csat_question_9_headline: 9e5f014cd5b49e54c578dc0965bd2249
templates/csat_question_9_headline: ba151502dd2ac4c8bbffc16fdc3a4703
templates/csat_question_9_subheader: aa206f483bcb547d10599b1e564cc813
templates/csat_survey_name: c9b8f32fdd57511fb63ecb74bfb5095b
templates/csat_survey_question_1_headline: c3db899931adf315522075ba5886f883
templates/csat_survey_name: ceed42acb13f9443cfde57b8ba264435
templates/csat_survey_question_1_headline: e16ad1b490d58ecbce5d2bcae8758e01
templates/csat_survey_question_1_lower_label: 04dc1812c9072b2f32c7b307a2b46e42
templates/csat_survey_question_1_upper_label: c24f08a4a3b5d3fbfb37eb948d594087
templates/csat_survey_question_2_headline: ac0e2dcc8108dd5dd339043958c98464
@@ -2563,7 +2535,7 @@ checksums:
templates/custom_survey_question_1_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
templates/customer_effort_score_description: 3427623c1f486fe2f3c9de17ef1ab05d
templates/customer_effort_score_name: 4f9aa509c57b7d0eb5773cd6fd62d308
templates/customer_effort_score_question_1_headline: 70115a7960746a05acef03f815652fc3
templates/customer_effort_score_question_1_headline: 1f75deab2d159eb8c33c37cc51ef4b3e
templates/customer_effort_score_question_1_lower_label: 586eedbc7b53319775e42c7cd4cef4de
templates/customer_effort_score_question_1_upper_label: a04ddfea06817ade6c56cff126cfca80
templates/customer_effort_score_question_2_headline: b05b561f04753ae81febee18e08ce2dd
@@ -2587,14 +2559,14 @@ checksums:
templates/earned_advocacy_score_name: 6f18c3f805a1f1875935784c7efa8639
templates/earned_advocacy_score_question_1_choice_1: ec580fd11a45779b039466f1e35eed2a
templates/earned_advocacy_score_question_1_choice_2: 8c708225830b06df2d1141c536f2a0d6
templates/earned_advocacy_score_question_1_headline: 57d2a966902d8cf06e5f3d21632ed2db
templates/earned_advocacy_score_question_1_headline: d282a886c3d0728917873dd0f2c111b3
templates/earned_advocacy_score_question_2_headline: f1044ad7510b0c1b6671730d277e51fb
templates/earned_advocacy_score_question_2_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
templates/earned_advocacy_score_question_3_headline: 2c036ea73e811371f7b9fdce1324da56
templates/earned_advocacy_score_question_3_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
templates/earned_advocacy_score_question_4_choice_1: ec580fd11a45779b039466f1e35eed2a
templates/earned_advocacy_score_question_4_choice_2: 8c708225830b06df2d1141c536f2a0d6
templates/earned_advocacy_score_question_4_headline: 7f723a7ab350cf1488f69980315575d6
templates/earned_advocacy_score_question_4_headline: 3363abab5ef272cc4bf3892808d01aba
templates/earned_advocacy_score_question_5_headline: 7aeb3b3873bd940d9a7a055fa67cdfb9
templates/earned_advocacy_score_question_5_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
templates/employee_satisfaction_description: 1b329b7c83094e0fdc4750dc0d1952ad
@@ -2643,7 +2615,7 @@ checksums:
templates/evaluate_a_product_idea_description: 734295caa08aac718e9ee01a99c3debe
templates/evaluate_a_product_idea_name: b0d8039556d686b83dfcd455092b9d9c
templates/evaluate_a_product_idea_question_1_button_label: ad840f586c880820e8b52b6860eb57fb
templates/evaluate_a_product_idea_question_1_headline: 5339ee5efbf33619b0e1e300083c8e1e
templates/evaluate_a_product_idea_question_1_headline: 7e2aab8a2b62564a5273c82dd92f9f8f
templates/evaluate_a_product_idea_question_1_html: bc0dcb887591e018dfeeb65a3a5c4bb9
templates/evaluate_a_product_idea_question_2_headline: 10a50778c4559554336e7289a48d021c
templates/evaluate_a_product_idea_question_2_lower_label: c2f05d3610d8879ae503a61d49e32e80
@@ -2726,7 +2698,7 @@ checksums:
templates/identify_sign_up_barriers_question_1_button_label: 080fd22c580f56ffdcea6c3d60448b84
templates/identify_sign_up_barriers_question_1_headline: c8c247363daf4697e1939aaf8dc5770c
templates/identify_sign_up_barriers_question_1_html: 51029ae64c19101af608684b6f429eb8
templates/identify_sign_up_barriers_question_2_headline: 019e6339441974088020d483c9313429
templates/identify_sign_up_barriers_question_2_headline: f768ea3053b07f6bbcba977f714ec3da
templates/identify_sign_up_barriers_question_2_lower_label: d9de99c54e06922698076a00c7ae29e1
templates/identify_sign_up_barriers_question_2_upper_label: 9f000f63949d875ae628fc354a2a7f6a
templates/identify_sign_up_barriers_question_3_choice_1_label: 136bb71047ef7aa7306b3624ef383ca0
@@ -2734,8 +2706,8 @@ checksums:
templates/identify_sign_up_barriers_question_3_choice_3_label: 969b81566c6be1a27e9848834b70946a
templates/identify_sign_up_barriers_question_3_choice_4_label: e2d3c2ebd342ea84dab7e0e9f2ea1156
templates/identify_sign_up_barriers_question_3_choice_5_label: d4bc66df5a8d842dfca0a176ccddf71e
templates/identify_sign_up_barriers_question_3_headline: ad950843e1ee338201beb63fcdb27164
templates/identify_sign_up_barriers_question_4_headline: 57ab48526001692d6c68a02607ec1e4a
templates/identify_sign_up_barriers_question_3_headline: eb2e0db2c4f9650fc72b931d47aebc58
templates/identify_sign_up_barriers_question_4_headline: d9652977772912124e858228dc8c3034
templates/identify_sign_up_barriers_question_4_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
templates/identify_sign_up_barriers_question_5_headline: b97e7e8403f4b4662e72a76e32aa3f47
templates/identify_sign_up_barriers_question_5_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
@@ -2754,7 +2726,7 @@ checksums:
templates/identify_upsell_opportunities_question_1_choice_2: 143d16c1d19d3579a5586cfb6b637d8f
templates/identify_upsell_opportunities_question_1_choice_3: da9bf0f1948918d93a8044dacd0af86c
templates/identify_upsell_opportunities_question_1_choice_4: 32cfb0ac99e5128103deb1cea876b089
templates/identify_upsell_opportunities_question_1_headline: c6d0abd4ea9c45397959a54e178731f7
templates/identify_upsell_opportunities_question_1_headline: 1e1aa49f39aca7873d05bfcc81fa77b7
templates/improve_activation_rate_description: 9587721eb5a40c4b8312e41a1f9fae19
templates/improve_activation_rate_name: ab2908e5166d4addb19093f3cb542835
templates/improve_activation_rate_question_1_choice_1: 148839adc1e7cee224a765cea67b9f22
@@ -2762,10 +2734,10 @@ checksums:
templates/improve_activation_rate_question_1_choice_3: e26f3bc7d47c851e0a7988fa72e45850
templates/improve_activation_rate_question_1_choice_4: 6dcd4ed2668c72ac1f57b4b0276ebb02
templates/improve_activation_rate_question_1_choice_5: d4bc66df5a8d842dfca0a176ccddf71e
templates/improve_activation_rate_question_1_headline: 170031a5a88bd67ecf63beb8d99e3252
templates/improve_activation_rate_question_2_headline: 31641f19cfe0b3e7cbe58ec1c4142bcd
templates/improve_activation_rate_question_1_headline: 22b5234b65cd8be0691a44d66a598150
templates/improve_activation_rate_question_2_headline: b52a2e4fe00ef7ef0376cba959a4310c
templates/improve_activation_rate_question_2_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
templates/improve_activation_rate_question_3_headline: 3868576109abecaa60d54f2650ff84c4
templates/improve_activation_rate_question_3_headline: a0b70d840e8a9fba18070cade4ff88a1
templates/improve_activation_rate_question_3_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
templates/improve_activation_rate_question_4_headline: 83c2d5428b5cdd6db19e5681f1e08007
templates/improve_activation_rate_question_4_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
@@ -2794,7 +2766,7 @@ checksums:
templates/improve_trial_conversion_question_1_headline: 6d0a492446be0250f04850de383fec54
templates/improve_trial_conversion_question_1_subheader: 67c7047ba2365d461df14dbed3f9506d
templates/improve_trial_conversion_question_2_button_label: 89ddbcf710eba274963494f312bdc8a9
templates/improve_trial_conversion_question_2_headline: 54324cd652667183dd3cf647ba72dd07
templates/improve_trial_conversion_question_2_headline: 05dd4820f60b9d267a9affc7e662f029
templates/improve_trial_conversion_question_4_button_label: d94a6a11cfdf4ebde4c5332e585e2e96
templates/improve_trial_conversion_question_4_headline: 9b07341f65574c4165086ec107cebb45
templates/improve_trial_conversion_question_4_html: 8ce95691eeeae7ad61c4d2f867b918ca
@@ -2810,7 +2782,7 @@ checksums:
templates/integration_setup_survey_question_1_upper_label: b88eaddaea17a4f285209c2529a9b8f8
templates/integration_setup_survey_question_2_headline: 7f98f8b8d4876c7e9e933d2bf874f6a6
templates/integration_setup_survey_question_2_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
templates/integration_setup_survey_question_3_headline: 65562a516eec7939484b3262a156fc63
templates/integration_setup_survey_question_3_headline: 0c16233b11661b4b2ebe046d17baba95
templates/integration_setup_survey_question_3_subheader: fd5ef11f1f5263f03f65c41caebe4c3b
templates/interview_prompt_description: d4e9f8cdaf01be900d6a3dc3a1d9e9b7
templates/interview_prompt_name: 9c55f5051d07df6b53f5fa0a95195235
@@ -2821,23 +2793,23 @@ checksums:
templates/long_term_retention_check_in_name: f1e97a69fbfbb0ebd3f740cdefa55f2f
templates/long_term_retention_check_in_question_10_headline: 9567068886cfea40a7044705ec7ffb06
templates/long_term_retention_check_in_question_10_placeholder: cc69cd219a32056c973e155b3ef9469d
templates/long_term_retention_check_in_question_1_headline: 688d3793cd3af11d59eb59bb3387aaea
templates/long_term_retention_check_in_question_1_headline: aff2f4abe332ba02b04f24b0fee0c0ad
templates/long_term_retention_check_in_question_1_lower_label: a6a66fe3d042b1ca404952096a7d3ef7
templates/long_term_retention_check_in_question_1_upper_label: a3a49eb9cc86972bce6dc41a107f472d
templates/long_term_retention_check_in_question_2_headline: ecc851d4c1077d1e04a315f38a1c9f34
templates/long_term_retention_check_in_question_2_headline: 8ffadebbe7626554a31f625cd0ce09d5
templates/long_term_retention_check_in_question_2_placeholder: 0c87033285f92b09a261977368fd080e
templates/long_term_retention_check_in_question_3_choice_1: 341ff316a339b106a178f0b8d362951b
templates/long_term_retention_check_in_question_3_choice_2: e3762261a5c81ffcdd00e19af5204ef8
templates/long_term_retention_check_in_question_3_choice_3: bbb93f54e33f2269ae0002b1576e6753
templates/long_term_retention_check_in_question_3_choice_4: ce27f1aeacccc542a174c4b2bce022b0
templates/long_term_retention_check_in_question_3_choice_5: 4e7493a16fd18152de9fe4e55086d437
templates/long_term_retention_check_in_question_3_headline: c720b000a0204b37fe460c9d5d22dbb2
templates/long_term_retention_check_in_question_4_headline: 03169477b5e208cb73f5132137a90247
templates/long_term_retention_check_in_question_3_headline: dc814600d2efed1976fbbe63230f19cf
templates/long_term_retention_check_in_question_4_headline: 642bf1125d9c0f5b4c241989d2392191
templates/long_term_retention_check_in_question_4_lower_label: 20b69a5ffcae1d4f52366870b349cf91
templates/long_term_retention_check_in_question_4_upper_label: 48f47b5f80ca0ce8ff7395081d4bb9e1
templates/long_term_retention_check_in_question_5_headline: 0ef86c4a1fcdcf8850a63cfa4d31e32d
templates/long_term_retention_check_in_question_5_headline: b1a0d267548ebced5594eb3c24e99e22
templates/long_term_retention_check_in_question_5_placeholder: d9ab7189d17443c03f728f146d5ff92e
templates/long_term_retention_check_in_question_6_headline: feb791cd9a69245b51dc1758993d797a
templates/long_term_retention_check_in_question_6_headline: 89245aac6cbe8f6ac7ce5dea84556ca7
templates/long_term_retention_check_in_question_6_lower_label: 54d464343c0bc17231fd51aa2d73623f
templates/long_term_retention_check_in_question_6_upper_label: 9f000f63949d875ae628fc354a2a7f6a
templates/long_term_retention_check_in_question_7_choice_1: ec3be31f7b93f97bff2b89252fca96be
@@ -2846,7 +2818,7 @@ checksums:
templates/long_term_retention_check_in_question_7_choice_4: 54f1b084598783779d84f332b7cb1e77
templates/long_term_retention_check_in_question_7_choice_5: e40d9aa9eda5560d8496b806e7a7e832
templates/long_term_retention_check_in_question_7_headline: 3136f67531f1cfbf6e9f8a890472c6a1
templates/long_term_retention_check_in_question_8_headline: 6a11ceb473c16b76fce93c45a32cef4c
templates/long_term_retention_check_in_question_8_headline: 934ea59cbe6b692daecfe462564dbfe2
templates/long_term_retention_check_in_question_8_placeholder: 863ad0c2bd362ab0c92c3018bdf7090c
templates/long_term_retention_check_in_question_9_headline: f69032a93cee667d66f83a0d608eb8ef
templates/long_term_retention_check_in_question_9_lower_label: 6dce68b71925d2b0b229f08498109e5e
@@ -2865,8 +2837,8 @@ checksums:
templates/market_site_clarity_question_1_choice_1: f1e807ab1684b329d0ffd670239ea37b
templates/market_site_clarity_question_1_choice_2: bdb8b4861dfc77d43ff9028c9c651a97
templates/market_site_clarity_question_1_choice_3: 91a0ee10fef5e56fd0adcefe5f1fcacf
templates/market_site_clarity_question_1_headline: 1b18b1375c77f5c4b89ef9e5b9ebc7d8
templates/market_site_clarity_question_2_headline: cd4f558bd7a7c881be65651f72ba03ce
templates/market_site_clarity_question_1_headline: 8502f3b9799198999ecab126bde938c0
templates/market_site_clarity_question_2_headline: 092eefd876d5d9460abadfe2109bb1bf
templates/market_site_clarity_question_3_button_label: d2eeb44560b2dd61fbc2b9aca2de8363
templates/market_site_clarity_question_3_headline: d6afccb3f411c48039ac15f3b3dcef14
templates/matrix: ff18f26a7020ec89bef25eee6aa800f8
@@ -2911,12 +2883,12 @@ checksums:
templates/nps_description: a0967386c2e53a3df5a165ff16a8fbda
templates/nps_lower_label: d9de99c54e06922698076a00c7ae29e1
templates/nps_name: ee9c686b6f62eeb53535c2ea91867775
templates/nps_question_1_headline: feb791cd9a69245b51dc1758993d797a
templates/nps_question_1_headline: 89245aac6cbe8f6ac7ce5dea84556ca7
templates/nps_question_1_lower_label: 54d464343c0bc17231fd51aa2d73623f
templates/nps_question_1_upper_label: 9f000f63949d875ae628fc354a2a7f6a
templates/nps_question_2_headline: 10665a2282eac80f6cb34d1ba9b1be2d
templates/nps_survey_name: 94ef35c2e6ed7061f8a533c9e61b155b
templates/nps_survey_question_1_headline: feb791cd9a69245b51dc1758993d797a
templates/nps_survey_question_1_headline: 89245aac6cbe8f6ac7ce5dea84556ca7
templates/nps_survey_question_1_lower_label: d9de99c54e06922698076a00c7ae29e1
templates/nps_survey_question_1_upper_label: 77d668acd2d8fd6dd1ccc8960380dfb8
templates/nps_survey_question_2_headline: b64138e12a7edcd1998e156f825a3471
@@ -2950,7 +2922,7 @@ checksums:
templates/preview_survey_ending_card_description: 6a98bdb1b083e437bb71d8b6c27b0e3a
templates/preview_survey_ending_card_headline: 3512326ea9ad9a8a6c420a1e69d4d93f
templates/preview_survey_name: 82503c2dba9f071c93860e5ce9584e14
templates/preview_survey_question_1_headline: b1b00f576c7b23e94e14094235799c2a
templates/preview_survey_question_1_headline: 91622f99f8018adefb3ed0921bc1ad7a
templates/preview_survey_question_1_lower_label: ff4681be0a94185111459994fe58478c
templates/preview_survey_question_1_subheader: c4d49637dd08a33a8271cd08b1bdbbe1
templates/preview_survey_question_1_upper_label: ac7de1981be78aff08b2133569fd3b95
@@ -2974,16 +2946,16 @@ checksums:
templates/prioritize_features_question_2_choice_2: 1e7326a57e8d91ed25a34a98bdc86c81
templates/prioritize_features_question_2_choice_3: 6913aa70491e150337f39bf146692ba2
templates/prioritize_features_question_2_headline: c4525eca2da7061b16b88b7a515266f1
templates/prioritize_features_question_3_headline: 46e936563314555c717356e359543a4b
templates/prioritize_features_question_3_headline: 17b35f21d32560f745997b78fcebd91a
templates/prioritize_features_question_3_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
templates/product_market_fit_short_description: d14c8e7f4eb7c98919de171457d10a31
templates/product_market_fit_short_name: 132d722134fb937204775988ddfd0b34
templates/product_market_fit_short_question_1_choice_1: 074b2a608d4bba5706b5c55dae249edf
templates/product_market_fit_short_question_1_choice_2: c054cdb1df479db6172b110551f59a98
templates/product_market_fit_short_question_1_choice_3: 33eca9f8f318485c076c9199048902ed
templates/product_market_fit_short_question_1_headline: 89ea8c1a467a83a3be0630613b32b2b8
templates/product_market_fit_short_question_1_headline: e672209cf1d47ba0523b06e1111e9179
templates/product_market_fit_short_question_1_subheader: b9b478e967930358b0c74324a7c18fc8
templates/product_market_fit_short_question_2_headline: 13799c7e571d8d167cc87fe48c40c9f7
templates/product_market_fit_short_question_2_headline: 015de4634fbeca165e399a526ac0992f
templates/product_market_fit_short_question_2_subheader: 51276effa2d7ddf4d1df7dfad45d9747
templates/product_market_fit_superhuman: 48b1b2db74562dea0d00483b29942346
templates/product_market_fit_superhuman_description: d14c8e7f4eb7c98919de171457d10a31
@@ -2993,7 +2965,7 @@ checksums:
templates/product_market_fit_superhuman_question_2_choice_1: 074b2a608d4bba5706b5c55dae249edf
templates/product_market_fit_superhuman_question_2_choice_2: c054cdb1df479db6172b110551f59a98
templates/product_market_fit_superhuman_question_2_choice_3: 33eca9f8f318485c076c9199048902ed
templates/product_market_fit_superhuman_question_2_headline: 89ea8c1a467a83a3be0630613b32b2b8
templates/product_market_fit_superhuman_question_2_headline: e672209cf1d47ba0523b06e1111e9179
templates/product_market_fit_superhuman_question_2_subheader: b9b478e967930358b0c74324a7c18fc8
templates/product_market_fit_superhuman_question_3_choice_1: da5bfc0a0de1d49c23fa6757dcd0f7b1
templates/product_market_fit_superhuman_question_3_choice_2: 4f4d5a5772566ea830ca2ba90ee80480
@@ -3002,9 +2974,9 @@ checksums:
templates/product_market_fit_superhuman_question_3_choice_5: e6a66473cd54c4a1958969481b8af6f7
templates/product_market_fit_superhuman_question_3_headline: bfba21665f10be39b957c68dac002dc9
templates/product_market_fit_superhuman_question_3_subheader: b9b478e967930358b0c74324a7c18fc8
templates/product_market_fit_superhuman_question_4_headline: ca9869d478a37bef84d8ad4789b7d6bc
templates/product_market_fit_superhuman_question_5_headline: 56faebca0e8a32aee4c2eec262980af2
templates/product_market_fit_superhuman_question_6_headline: 13799c7e571d8d167cc87fe48c40c9f7
templates/product_market_fit_superhuman_question_4_headline: a7c8b5b3eb52e410705135f1a7b6ec11
templates/product_market_fit_superhuman_question_5_headline: 65be01997ff636390de3e6fc21dba44b
templates/product_market_fit_superhuman_question_6_headline: 015de4634fbeca165e399a526ac0992f
templates/product_market_fit_superhuman_question_6_subheader: 51276effa2d7ddf4d1df7dfad45d9747
templates/professional_development_growth_survey_description: 9fb0c943037258435fdb1686cc4fec51
templates/professional_development_growth_survey_name: b7c77f82f48269619ad45fef47c94cb8
@@ -3075,7 +3047,7 @@ checksums:
templates/recognition_and_reward_survey_question_4_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
templates/review_prompt_description: 21f19ec762c1319c261764d9cceb9564
templates/review_prompt_name: beb7a913fa25579c937fe5f7570cf4b8
templates/review_prompt_question_1_headline: c66aeba9d97ff9496365e7b58def8372
templates/review_prompt_question_1_headline: 6b15d118037b729138c2214cfef49a68
templates/review_prompt_question_1_lower_label: ff4681be0a94185111459994fe58478c
templates/review_prompt_question_1_upper_label: a3a49eb9cc86972bce6dc41a107f472d
templates/review_prompt_question_2_button_label: 98490793678c51afc8b61ff405185bf1
@@ -3118,7 +3090,7 @@ checksums:
templates/site_abandonment_survey_question_8_headline: 9e82d6f51788351c7e2c8f73be66d005
templates/site_abandonment_survey_question_9_headline: ef1289130df46b80d43119380095b579
templates/smileys_survey_name: 6ef64e8182e7820efa53a2d1c81eb912
templates/smileys_survey_question_1_headline: c66aeba9d97ff9496365e7b58def8372
templates/smileys_survey_question_1_headline: 6b15d118037b729138c2214cfef49a68
templates/smileys_survey_question_1_lower_label: ff4681be0a94185111459994fe58478c
templates/smileys_survey_question_1_upper_label: a3a49eb9cc86972bce6dc41a107f472d
templates/smileys_survey_question_2_button_label: 98490793678c51afc8b61ff405185bf1
@@ -3128,8 +3100,8 @@ checksums:
templates/smileys_survey_question_3_headline: a76ad381785e46dbc0b1cb45662fa0be
templates/smileys_survey_question_3_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
templates/smileys_survey_question_3_subheader: 43098f46dc68b7594522e9d94ba0b51f
templates/star_rating_survey_name: 70bfbf5773bdbc6a662d963af2e352d8
templates/star_rating_survey_question_1_headline: c66aeba9d97ff9496365e7b58def8372
templates/star_rating_survey_name: a81021d9fb79ed14a7212274c0970a5f
templates/star_rating_survey_question_1_headline: 6b15d118037b729138c2214cfef49a68
templates/star_rating_survey_question_1_lower_label: 04dc1812c9072b2f32c7b307a2b46e42
templates/star_rating_survey_question_1_upper_label: c24f08a4a3b5d3fbfb37eb948d594087
templates/star_rating_survey_question_2_button_label: 98490793678c51afc8b61ff405185bf1
@@ -3162,7 +3134,7 @@ checksums:
templates/uncover_strengths_and_weaknesses_question_1_choice_3: e68c41a832ff03592d6955e30cbe5a86
templates/uncover_strengths_and_weaknesses_question_1_choice_4: 8e7f41cc79df15a3f9e5c285d46129b7
templates/uncover_strengths_and_weaknesses_question_1_choice_5: 79acaa6cd481262bea4e743a422529d2
templates/uncover_strengths_and_weaknesses_question_1_headline: 84643dbb23275a6928986b16f4af0a64
templates/uncover_strengths_and_weaknesses_question_1_headline: 997d9175bda65bfe83925cccae37b959
templates/uncover_strengths_and_weaknesses_question_2_choice_1: 1563fcb5ddb5037b0709ccd3dd384a92
templates/uncover_strengths_and_weaknesses_question_2_choice_2: 40607a68c0281d374337ae692e580835
templates/uncover_strengths_and_weaknesses_question_2_choice_3: ce27f1aeacccc542a174c4b2bce022b0
@@ -3178,8 +3150,8 @@ checksums:
templates/understand_low_engagement_question_1_choice_3: 6dcd4ed2668c72ac1f57b4b0276ebb02
templates/understand_low_engagement_question_1_choice_4: 1735e52f2db8627a8b0b4f293a570b78
templates/understand_low_engagement_question_1_choice_5: 79acaa6cd481262bea4e743a422529d2
templates/understand_low_engagement_question_1_headline: a367267c8e6da898a0b8b8dee80b4358
templates/understand_low_engagement_question_2_headline: dae498aeefa0718c527e7bc46f4ad724
templates/understand_low_engagement_question_1_headline: 5cba0c6c68c586c7a68dfc1483d4d27d
templates/understand_low_engagement_question_2_headline: 4ccc6e00439ccce991bccffd2c6431b2
templates/understand_low_engagement_question_2_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
templates/understand_low_engagement_question_3_headline: ebb675aba5e32a27fdf6a11060fae929
templates/understand_low_engagement_question_3_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee

View File

@@ -40,8 +40,6 @@ export const GITHUB_ID = env.GITHUB_ID;
export const GITHUB_SECRET = env.GITHUB_SECRET;
export const GOOGLE_CLIENT_ID = env.GOOGLE_CLIENT_ID;
export const GOOGLE_CLIENT_SECRET = env.GOOGLE_CLIENT_SECRET;
export const HUB_API_URL = env.HUB_API_URL;
export const HUB_API_KEY = env.HUB_API_KEY;
export const AZUREAD_CLIENT_ID = env.AZUREAD_CLIENT_ID;
export const AZUREAD_CLIENT_SECRET = env.AZUREAD_CLIENT_SECRET;

View File

@@ -54,6 +54,6 @@ export const mockEnvironment: TEnvironment = {
createdAt: new Date(),
updatedAt: new Date(),
type: "production",
workspaceId: mockId,
projectId: mockId,
appSetupCompleted: false,
};

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