mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-21 03:03:25 -05:00
chore: remove environment layer and replace with workspace (Phase 12) (#7687)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
8073c0cc7b
commit
24b5425c88
-1
@@ -105,7 +105,6 @@ export const LandingSidebar = ({ user, organization }: LandingSidebarProps) => {
|
||||
organizationId: organization.id,
|
||||
redirect: true,
|
||||
callbackUrl: "/auth/login",
|
||||
clearEnvironmentId: true,
|
||||
});
|
||||
}}
|
||||
icon={<LogOutIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />}>
|
||||
|
||||
@@ -45,7 +45,6 @@ const Page = async (props: { params: Promise<{ organizationId: string }> }) => {
|
||||
isOwnerOrManager={false}
|
||||
isAccessControlAllowed={false}
|
||||
isMember={isMember}
|
||||
environments={[]}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-12">
|
||||
|
||||
+2
-2
@@ -36,7 +36,7 @@ export const OnboardingSetupInstructions = ({
|
||||
!function(){
|
||||
var appUrl = "${publicDomain}";
|
||||
var workspaceId = "${workspaceId}";
|
||||
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({environmentId:workspaceId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}();
|
||||
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({workspaceId:workspaceId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}();
|
||||
</script>
|
||||
<!-- END Formbricks Surveys -->
|
||||
`;
|
||||
@@ -46,7 +46,7 @@ export const OnboardingSetupInstructions = ({
|
||||
!function(){
|
||||
var appUrl = "${publicDomain}";
|
||||
var workspaceId = "${workspaceId}";
|
||||
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({environmentId:workspaceId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}();
|
||||
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({workspaceId:workspaceId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}();
|
||||
</script>
|
||||
<!-- END Formbricks Surveys -->
|
||||
`;
|
||||
|
||||
+2
-3
@@ -18,11 +18,10 @@ import { createSurveyAction } from "@/modules/survey/components/template-list/ac
|
||||
interface XMTemplateListProps {
|
||||
workspace: TWorkspace;
|
||||
user: TUser;
|
||||
environmentId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export const XMTemplateList = ({ workspace, user, environmentId, workspaceId }: XMTemplateListProps) => {
|
||||
export const XMTemplateList = ({ workspace, user, workspaceId }: XMTemplateListProps) => {
|
||||
const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null);
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
@@ -34,7 +33,7 @@ export const XMTemplateList = ({ workspace, user, environmentId, workspaceId }:
|
||||
createdBy: user.id,
|
||||
};
|
||||
const createSurveyResponse = await createSurveyAction({
|
||||
environmentId: environmentId,
|
||||
workspaceId: workspaceId,
|
||||
surveyBody: augmentedTemplate,
|
||||
});
|
||||
|
||||
|
||||
-1
@@ -28,7 +28,6 @@ const mockWorkspace: TWorkspace = {
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
appSetupCompleted: false,
|
||||
environments: [],
|
||||
languages: [],
|
||||
logo: null,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { getServerSession } from "next-auth";
|
||||
import Link from "next/link";
|
||||
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { XMTemplateList } from "@/app/(app)/(onboarding)/workspaces/[workspaceId]/xm-templates/components/XMTemplateList";
|
||||
import { getEnvironments } from "@/lib/environment/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getUserWorkspaces, getWorkspace } from "@/lib/workspace/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
@@ -36,23 +35,12 @@ const Page = async (props: XMTemplatePageProps) => {
|
||||
throw new ResourceNotFoundError(t("common.workspace"), params.workspaceId);
|
||||
}
|
||||
|
||||
const environments = await getEnvironments(params.workspaceId);
|
||||
const environment = environments[0];
|
||||
if (!environment) {
|
||||
throw new ResourceNotFoundError(t("common.environment"), null);
|
||||
}
|
||||
|
||||
const workspaces = await getUserWorkspaces(session.user.id, workspace.organizationId);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
<Header title={t("workspace.xm-templates.headline")} />
|
||||
<XMTemplateList
|
||||
workspace={workspace}
|
||||
user={user}
|
||||
environmentId={environment.id}
|
||||
workspaceId={params.workspaceId}
|
||||
/>
|
||||
<XMTemplateList workspace={workspace} user={user} workspaceId={params.workspaceId} />
|
||||
{workspaces.length >= 2 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
|
||||
@@ -27,12 +27,6 @@ const SurveyEditorWorkspaceLayout = async (props: {
|
||||
throw new ResourceNotFoundError(t("common.workspace"), params.workspaceId);
|
||||
}
|
||||
|
||||
const environment = workspace.environments[0];
|
||||
|
||||
if (!environment) {
|
||||
throw new ResourceNotFoundError(t("common.environment"), null);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
|
||||
const { isReadOnly, environment, isBilling, workspace } = await getWorkspaceAuth(params.workspaceId);
|
||||
const { isReadOnly, isBilling, workspace } = await getWorkspaceAuth(params.workspaceId);
|
||||
|
||||
const [
|
||||
integrations,
|
||||
@@ -62,7 +62,7 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
const isN8nIntegrationConnected = isIntegrationConnected("n8n");
|
||||
const isSlackIntegrationConnected = isIntegrationConnected("slack");
|
||||
|
||||
const appSetupCompleted = !!environment?.appSetupCompleted;
|
||||
const appSetupCompleted = !!workspace.appSetupCompleted;
|
||||
const integrationCards = [
|
||||
{
|
||||
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/zapier",
|
||||
|
||||
@@ -88,7 +88,7 @@ export const createWorkspaceAction = authenticatedActionClient.inputSchema(ZCrea
|
||||
);
|
||||
|
||||
const ZGetOrganizationsForSwitcherAction = z.object({
|
||||
organizationId: ZId, // Changed from environmentId to avoid extra query
|
||||
organizationId: ZId, // Changed from workspaceId to avoid extra query
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -113,7 +113,7 @@ export const getOrganizationsForSwitcherAction = authenticatedActionClient
|
||||
});
|
||||
|
||||
const ZGetWorkspacesForSwitcherAction = z.object({
|
||||
organizationId: ZId, // Changed from environmentId to avoid extra query
|
||||
organizationId: ZId, // Changed from workspaceId to avoid extra query
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
"use client";
|
||||
|
||||
// Environments are deprecated — only production environments exist now.
|
||||
// This component is kept as a no-op for any remaining references.
|
||||
export const EnvironmentSwitch = () => {
|
||||
return null;
|
||||
};
|
||||
@@ -332,7 +332,7 @@ export const MainNavigation = ({
|
||||
organizationId: organization.id,
|
||||
redirect: false,
|
||||
callbackUrl: loginUrl,
|
||||
clearEnvironmentId: true,
|
||||
clearWorkspaceId: true,
|
||||
});
|
||||
router.push(route?.url || loginUrl); // NOSONAR // We want to check for empty strings
|
||||
}}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { WorkspaceAndOrgSwitch } from "@/app/(app)/workspaces/[workspaceId]/components/workspace-and-org-switch";
|
||||
import { useEnvironment } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspaceContext } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
|
||||
interface TopControlBarProps {
|
||||
environments: TEnvironment[];
|
||||
currentOrganizationId: string;
|
||||
currentWorkspaceId: string;
|
||||
isMultiOrgEnabled: boolean;
|
||||
organizationWorkspacesLimit: number;
|
||||
isFormbricksCloud: boolean;
|
||||
@@ -20,9 +17,7 @@ interface TopControlBarProps {
|
||||
}
|
||||
|
||||
export const TopControlBar = ({
|
||||
environments,
|
||||
currentOrganizationId,
|
||||
currentWorkspaceId,
|
||||
isMultiOrgEnabled,
|
||||
organizationWorkspacesLimit,
|
||||
isFormbricksCloud,
|
||||
@@ -32,17 +27,15 @@ export const TopControlBar = ({
|
||||
membershipRole,
|
||||
}: TopControlBarProps) => {
|
||||
const { isMember } = getAccessFlags(membershipRole);
|
||||
const { environment } = useEnvironment();
|
||||
const { workspace } = useWorkspaceContext();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-14 w-full items-center justify-between bg-slate-50 px-6"
|
||||
data-testid="fb__global-top-control-bar">
|
||||
<WorkspaceAndOrgSwitch
|
||||
currentEnvironmentId={environment.id}
|
||||
environments={environments}
|
||||
currentWorkspaceId={workspace.id}
|
||||
currentOrganizationId={currentOrganizationId}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
organizationWorkspacesLimit={organizationWorkspacesLimit}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
import { AlertTriangleIcon, CheckIcon, RotateCcwIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
|
||||
interface WidgetStatusIndicatorProps {
|
||||
environment: TEnvironment;
|
||||
workspace: { appSetupCompleted: boolean };
|
||||
}
|
||||
|
||||
export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProps) => {
|
||||
export const WidgetStatusIndicator = ({ workspace }: WidgetStatusIndicatorProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const stati = {
|
||||
@@ -29,7 +28,7 @@ export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProp
|
||||
|
||||
let status: "notImplemented" | "running";
|
||||
|
||||
if (environment.appSetupCompleted) {
|
||||
if (workspace.appSetupCompleted) {
|
||||
status = "running";
|
||||
} else {
|
||||
status = "notImplemented";
|
||||
|
||||
+4
-7
@@ -6,16 +6,16 @@ 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 { 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";
|
||||
import { TWorkspaceLayoutData } from "@/modules/workspaces/types/workspace-auth";
|
||||
|
||||
interface EnvironmentLayoutProps {
|
||||
layoutData: TEnvironmentLayoutData;
|
||||
interface WorkspaceLayoutProps {
|
||||
layoutData: TWorkspaceLayoutData;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLayoutProps) => {
|
||||
export const WorkspaceLayout = async ({ layoutData, children }: WorkspaceLayoutProps) => {
|
||||
const t = await getTranslate();
|
||||
const publicDomain = getPublicDomain();
|
||||
|
||||
@@ -25,7 +25,6 @@ export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLay
|
||||
organization,
|
||||
membership,
|
||||
workspace, // Current workspace details
|
||||
environments, // All workspace environments (for environment switcher)
|
||||
isAccessControlAllowed,
|
||||
workspacePermission,
|
||||
license,
|
||||
@@ -71,9 +70,7 @@ export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLay
|
||||
/>
|
||||
<div id="mainContent" className="flex flex-1 flex-col overflow-hidden bg-slate-50">
|
||||
<TopControlBar
|
||||
environments={environments}
|
||||
currentOrganizationId={organization.id}
|
||||
currentWorkspaceId={workspace.id}
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
organizationWorkspacesLimit={organizationWorkspacesLimit}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
@@ -5,15 +5,14 @@ import { FORMBRICKS_ENVIRONMENT_ID_LS, FORMBRICKS_WORKSPACE_ID_LS } from "@/lib/
|
||||
|
||||
interface WorkspaceStorageHandlerProps {
|
||||
workspaceId: string;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
const WorkspaceStorageHandler = ({ workspaceId, environmentId }: WorkspaceStorageHandlerProps) => {
|
||||
const WorkspaceStorageHandler = ({ workspaceId }: WorkspaceStorageHandlerProps) => {
|
||||
useEffect(() => {
|
||||
localStorage.setItem(FORMBRICKS_WORKSPACE_ID_LS, workspaceId);
|
||||
// Keep environment ID in sync for backward compatibility with old SDK clients
|
||||
localStorage.setItem(FORMBRICKS_ENVIRONMENT_ID_LS, environmentId);
|
||||
}, [workspaceId, environmentId]);
|
||||
// Keep legacy environment ID in sync for backward compatibility with old SDK clients
|
||||
localStorage.setItem(FORMBRICKS_ENVIRONMENT_ID_LS, workspaceId);
|
||||
}, [workspaceId]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -25,13 +25,13 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/modules/ui/components/dropdown-menu";
|
||||
import { useOrganization, useWorkspace } from "../context/environment-context";
|
||||
import { useOrganization, useWorkspace } from "../context/workspace-context";
|
||||
|
||||
interface OrganizationBreadcrumbProps {
|
||||
currentOrganizationId: string;
|
||||
currentOrganizationName?: string; // Optional: pass directly if context not available
|
||||
isMultiOrgEnabled: boolean;
|
||||
currentEnvironmentId?: string;
|
||||
currentWorkspaceId?: string;
|
||||
isFormbricksCloud: boolean;
|
||||
isMember: boolean;
|
||||
isOwnerOrManager: boolean;
|
||||
@@ -52,7 +52,7 @@ export const OrganizationBreadcrumb = ({
|
||||
currentOrganizationId,
|
||||
currentOrganizationName,
|
||||
isMultiOrgEnabled,
|
||||
currentEnvironmentId,
|
||||
currentWorkspaceId,
|
||||
isFormbricksCloud,
|
||||
isMember,
|
||||
isOwnerOrManager,
|
||||
@@ -241,7 +241,7 @@ export const OrganizationBreadcrumb = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{currentEnvironmentId && (
|
||||
{currentWorkspaceId && (
|
||||
<div>
|
||||
{showOrganizationDropdown && <DropdownMenuSeparator />}
|
||||
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
||||
|
||||
@@ -9,8 +9,6 @@ interface WorkspaceAndOrgSwitchProps {
|
||||
currentOrganizationName?: string; // Optional: for pages without context
|
||||
currentWorkspaceId?: string;
|
||||
currentWorkspaceName?: string; // Optional: for pages without context
|
||||
currentEnvironmentId?: string;
|
||||
environments: { id: string; type: string }[];
|
||||
isMultiOrgEnabled: boolean;
|
||||
organizationWorkspacesLimit: number;
|
||||
isFormbricksCloud: boolean;
|
||||
@@ -25,7 +23,6 @@ export const WorkspaceAndOrgSwitch = ({
|
||||
currentOrganizationName,
|
||||
currentWorkspaceId,
|
||||
currentWorkspaceName,
|
||||
currentEnvironmentId,
|
||||
isMultiOrgEnabled,
|
||||
organizationWorkspacesLimit,
|
||||
isFormbricksCloud,
|
||||
@@ -40,13 +37,13 @@ export const WorkspaceAndOrgSwitch = ({
|
||||
<OrganizationBreadcrumb
|
||||
currentOrganizationId={currentOrganizationId}
|
||||
currentOrganizationName={currentOrganizationName}
|
||||
currentEnvironmentId={currentEnvironmentId}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isMember={isMember}
|
||||
isOwnerOrManager={isOwnerOrManager}
|
||||
/>
|
||||
{currentWorkspaceId && currentEnvironmentId && (
|
||||
{currentWorkspaceId && (
|
||||
<WorkspaceBreadcrumb
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
currentWorkspaceName={currentWorkspaceName}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
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 { useWorkspace } from "../context/workspace-context";
|
||||
|
||||
interface WorkspaceBreadcrumbProps {
|
||||
currentWorkspaceId: string;
|
||||
|
||||
+13
-20
@@ -1,29 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TWorkspace } from "@formbricks/types/workspace";
|
||||
|
||||
export interface EnvironmentContextType {
|
||||
environment: TEnvironment;
|
||||
export interface WorkspaceContextType {
|
||||
workspace: TWorkspace;
|
||||
organization: TOrganization;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
const EnvironmentContext = createContext<EnvironmentContextType | null>(null);
|
||||
const WorkspaceContext = createContext<WorkspaceContextType | null>(null);
|
||||
|
||||
export const useEnvironment = () => {
|
||||
const context = useContext(EnvironmentContext);
|
||||
export const useWorkspaceContext = () => {
|
||||
const context = useContext(WorkspaceContext);
|
||||
if (!context) {
|
||||
throw new Error("useEnvironment must be used within an EnvironmentProvider");
|
||||
throw new Error("useWorkspaceContext must be used within a WorkspaceContextWrapper");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useWorkspace = () => {
|
||||
const context = useContext(EnvironmentContext);
|
||||
const context = useContext(WorkspaceContext);
|
||||
if (!context) {
|
||||
return { workspace: null };
|
||||
}
|
||||
@@ -31,7 +29,7 @@ export const useWorkspace = () => {
|
||||
};
|
||||
|
||||
export const useOrganization = () => {
|
||||
const context = useContext(EnvironmentContext);
|
||||
const context = useContext(WorkspaceContext);
|
||||
if (!context) {
|
||||
return { organization: null };
|
||||
}
|
||||
@@ -39,30 +37,25 @@ export const useOrganization = () => {
|
||||
};
|
||||
|
||||
// Client wrapper component to be used in server components
|
||||
interface EnvironmentContextWrapperProps {
|
||||
environment: TEnvironment;
|
||||
interface WorkspaceContextWrapperProps {
|
||||
workspace: TWorkspace;
|
||||
organization: TOrganization;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const EnvironmentContextWrapper = ({
|
||||
environment,
|
||||
export const WorkspaceContextWrapper = ({
|
||||
workspace,
|
||||
organization,
|
||||
children,
|
||||
}: EnvironmentContextWrapperProps) => {
|
||||
const environmentContextValue = useMemo(
|
||||
}: WorkspaceContextWrapperProps) => {
|
||||
const workspaceContextValue = useMemo(
|
||||
() => ({
|
||||
environment,
|
||||
workspace,
|
||||
organization,
|
||||
organizationId: workspace.organizationId,
|
||||
}),
|
||||
[environment, workspace, organization]
|
||||
[workspace, organization]
|
||||
);
|
||||
|
||||
return (
|
||||
<EnvironmentContext.Provider value={environmentContextValue}>{children}</EnvironmentContext.Provider>
|
||||
);
|
||||
return <WorkspaceContext.Provider value={workspaceContextValue}>{children}</WorkspaceContext.Provider>;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { EnvironmentLayout } from "@/app/(app)/workspaces/[workspaceId]/components/EnvironmentLayout";
|
||||
import { EnvironmentContextWrapper } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { WorkspaceLayout as WorkspaceLayoutComponent } from "@/app/(app)/workspaces/[workspaceId]/components/WorkspaceLayout";
|
||||
import { WorkspaceContextWrapper } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getWorkspaceLayoutData } from "@/modules/workspaces/lib/utils";
|
||||
import WorkspaceStorageHandler from "./components/WorkspaceStorageHandler";
|
||||
@@ -22,13 +22,10 @@ const WorkspaceLayout = async (props: {
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkspaceStorageHandler workspaceId={params.workspaceId} environmentId={layoutData.environment.id} />
|
||||
<EnvironmentContextWrapper
|
||||
environment={layoutData.environment}
|
||||
workspace={layoutData.workspace}
|
||||
organization={layoutData.organization}>
|
||||
<EnvironmentLayout layoutData={layoutData}>{children}</EnvironmentLayout>
|
||||
</EnvironmentContextWrapper>
|
||||
<WorkspaceStorageHandler workspaceId={params.workspaceId} />
|
||||
<WorkspaceContextWrapper workspace={layoutData.workspace} organization={layoutData.organization}>
|
||||
<WorkspaceLayoutComponent layoutData={layoutData}>{children}</WorkspaceLayoutComponent>
|
||||
</WorkspaceContextWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-2
@@ -2,11 +2,10 @@
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
|
||||
|
||||
interface AccountSettingsNavbarProps {
|
||||
environmentId?: string;
|
||||
activeId: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
+19
-27
@@ -4,7 +4,7 @@ import { HelpCircleIcon, UsersIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||
import { Membership } from "../types";
|
||||
import { NotificationSwitch } from "./NotificationSwitch";
|
||||
@@ -12,7 +12,6 @@ import { NotificationSwitch } from "./NotificationSwitch";
|
||||
interface EditAlertsProps {
|
||||
memberships: Membership[];
|
||||
user: TUser;
|
||||
environmentId: string;
|
||||
autoDisableNotificationType: string;
|
||||
autoDisableNotificationElementId: string;
|
||||
}
|
||||
@@ -20,7 +19,6 @@ interface EditAlertsProps {
|
||||
export const EditAlerts = ({
|
||||
memberships,
|
||||
user,
|
||||
environmentId: _environmentId,
|
||||
autoDisableNotificationType,
|
||||
autoDisableNotificationElementId,
|
||||
}: EditAlertsProps) => {
|
||||
@@ -68,33 +66,27 @@ export const EditAlerts = ({
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
{membership.organization.workspaces.some((workspace) =>
|
||||
workspace.environments.some((environment) => environment.surveys.length > 0)
|
||||
) ? (
|
||||
{membership.organization.workspaces.some((workspace) => workspace.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) => (
|
||||
<div key={environment.id}>
|
||||
{environment.surveys.map((survey) => (
|
||||
<div
|
||||
className="grid h-auto w-full cursor-pointer grid-cols-3 place-content-center rounded-lg px-2 py-2 text-left text-sm text-slate-900 hover:bg-slate-50"
|
||||
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>
|
||||
<div className="col-span-1 text-center">
|
||||
<NotificationSwitch
|
||||
surveyOrWorkspaceOrOrganizationId={survey.id}
|
||||
notificationSettings={user.notificationSettings!}
|
||||
notificationType={"alert"}
|
||||
autoDisableNotificationType={autoDisableNotificationType}
|
||||
autoDisableNotificationElementId={autoDisableNotificationElementId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{workspace.surveys.map((survey) => (
|
||||
<div
|
||||
className="grid h-auto w-full cursor-pointer grid-cols-3 place-content-center rounded-lg px-2 py-2 text-left text-sm text-slate-900 hover:bg-slate-50"
|
||||
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>
|
||||
<div className="col-span-1 text-center">
|
||||
<NotificationSwitch
|
||||
surveyOrWorkspaceOrOrganizationId={survey.id}
|
||||
notificationSettings={user.notificationSettings!}
|
||||
notificationType={"alert"}
|
||||
autoDisableNotificationType={autoDisableNotificationType}
|
||||
autoDisableNotificationElementId={autoDisableNotificationElementId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+2
-6
@@ -1,14 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { SlackIcon } from "@/modules/ui/components/icons";
|
||||
|
||||
interface IntegrationsTipProps {
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export const IntegrationsTip = ({ environmentId: _environmentId }: IntegrationsTipProps) => {
|
||||
export const IntegrationsTip = () => {
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspace();
|
||||
return (
|
||||
|
||||
+10
-22
@@ -24,14 +24,12 @@ const setCompleteNotificationSettings = (
|
||||
for (const membership of memberships) {
|
||||
for (const workspace of membership.organization.workspaces) {
|
||||
// set default values for alerts
|
||||
for (const environment of workspace.environments) {
|
||||
for (const survey of environment.surveys) {
|
||||
newNotificationSettings.alert[survey.id] =
|
||||
(notificationSettings as unknown as Record<string, Record<string, boolean>>)[survey.id]
|
||||
?.responseFinished ||
|
||||
(notificationSettings.alert && notificationSettings.alert[survey.id]) ||
|
||||
false; // check for legacy notification settings w/o "alerts" key
|
||||
}
|
||||
for (const survey of workspace.surveys) {
|
||||
newNotificationSettings.alert[survey.id] =
|
||||
(notificationSettings as unknown as Record<string, Record<string, boolean>>)[survey.id]
|
||||
?.responseFinished ||
|
||||
(notificationSettings.alert && notificationSettings.alert[survey.id]) ||
|
||||
false; // check for legacy notification settings w/o "alerts" key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,18 +113,10 @@ const getMemberships = async (userId: string): Promise<Membership[]> => {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
environments: {
|
||||
where: {
|
||||
type: "production",
|
||||
},
|
||||
surveys: {
|
||||
select: {
|
||||
id: true,
|
||||
surveys: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -143,7 +133,6 @@ const Page = async (props: {
|
||||
searchParams: Promise<Record<string, string>>;
|
||||
}) => {
|
||||
const searchParams = await props.searchParams;
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
@@ -167,7 +156,7 @@ const Page = async (props: {
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("common.account_settings")}>
|
||||
<AccountSettingsNavbar environmentId={params.workspaceId} activeId="notifications" />
|
||||
<AccountSettingsNavbar activeId="notifications" />
|
||||
</PageHeader>
|
||||
<SettingsCard
|
||||
title={t("workspace.settings.notifications.email_alerts_surveys")}
|
||||
@@ -175,12 +164,11 @@ const Page = async (props: {
|
||||
<EditAlerts
|
||||
memberships={memberships}
|
||||
user={user}
|
||||
environmentId={params.workspaceId}
|
||||
autoDisableNotificationType={autoDisableNotificationType}
|
||||
autoDisableNotificationElementId={autoDisableNotificationElementId}
|
||||
/>
|
||||
</SettingsCard>
|
||||
<IntegrationsTip environmentId={params.workspaceId} />
|
||||
<IntegrationsTip />
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-5
@@ -7,12 +7,9 @@ export interface Membership {
|
||||
workspaces: {
|
||||
id: string;
|
||||
name: string;
|
||||
environments: {
|
||||
surveys: {
|
||||
id: string;
|
||||
surveys: {
|
||||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
name: string;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
|
||||
+2
-2
@@ -97,7 +97,7 @@ export const EditProfileDetailsForm = ({
|
||||
redirectUrl: "/email-change-without-verification-success",
|
||||
redirect: true,
|
||||
callbackUrl: "/email-change-without-verification-success",
|
||||
clearEnvironmentId: true,
|
||||
clearWorkspaceId: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -141,7 +141,7 @@ export const EditProfileDetailsForm = ({
|
||||
redirectUrl: "/auth/login",
|
||||
redirect: true,
|
||||
callbackUrl: "/auth/login",
|
||||
clearEnvironmentId: true,
|
||||
clearWorkspaceId: true,
|
||||
});
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(result);
|
||||
|
||||
@@ -20,8 +20,6 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
const { workspaceId } = params;
|
||||
|
||||
const { session } = await getWorkspaceAuth(params.workspaceId);
|
||||
|
||||
const organizationsWithSingleOwner = await getOrganizationsWhereUserIsSingleOwner(session.user.id);
|
||||
@@ -37,7 +35,7 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("common.account_settings")}>
|
||||
<AccountSettingsNavbar environmentId={workspaceId} activeId="profile" />
|
||||
<AccountSettingsNavbar activeId="profile" />
|
||||
</PageHeader>
|
||||
{user && (
|
||||
<div>
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
|
||||
|
||||
|
||||
+6
-22
@@ -11,13 +11,10 @@ interface SurveyWithSlug {
|
||||
name: string;
|
||||
slug: string | null;
|
||||
status: TSurveyStatus;
|
||||
environment: {
|
||||
workspace: {
|
||||
id: string;
|
||||
type: "production" | "development";
|
||||
workspace: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
name: string;
|
||||
organizationId: string;
|
||||
};
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -29,10 +26,6 @@ interface PrettyUrlsTableProps {
|
||||
export const PrettyUrlsTable = ({ surveys }: PrettyUrlsTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getEnvironmentBadgeColor = () => {
|
||||
return "bg-green-100 text-green-800";
|
||||
};
|
||||
|
||||
const tableHeaders = [
|
||||
{
|
||||
label: t("workspace.settings.domain.survey_name"),
|
||||
@@ -46,10 +39,6 @@ export const PrettyUrlsTable = ({ surveys }: PrettyUrlsTableProps) => {
|
||||
label: t("workspace.settings.domain.pretty_url"),
|
||||
key: "slug",
|
||||
},
|
||||
{
|
||||
label: t("common.environment"),
|
||||
key: "environment",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -67,7 +56,7 @@ export const PrettyUrlsTable = ({ surveys }: PrettyUrlsTableProps) => {
|
||||
<TableBody className="[&_tr:last-child]:border-b">
|
||||
{surveys.length === 0 && (
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableCell colSpan={4} className="text-center text-slate-500">
|
||||
<TableCell colSpan={3} className="text-center text-slate-500">
|
||||
{t("workspace.settings.domain.no_pretty_urls")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -76,20 +65,15 @@ export const PrettyUrlsTable = ({ surveys }: PrettyUrlsTableProps) => {
|
||||
<TableRow key={survey.id} className="border-slate-200 hover:bg-transparent">
|
||||
<TableCell className="font-medium">
|
||||
<Link
|
||||
href={`/workspaces/${survey.environment.workspace.id}/surveys/${survey.id}/summary`}
|
||||
href={`/workspaces/${survey.workspace.id}/surveys/${survey.id}/summary`}
|
||||
className="text-slate-900 hover:text-slate-700 hover:underline">
|
||||
{survey.name}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>{survey.environment.workspace.name}</TableCell>
|
||||
<TableCell>{survey.workspace.name}</TableCell>
|
||||
<TableCell>
|
||||
<IdBadge id={survey.slug ?? ""} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className={`rounded px-2 py-1 text-xs font-medium ${getEnvironmentBadgeColor()}`}>
|
||||
{t("common.production")}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -55,7 +55,7 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
<FaviconCustomizationSettings
|
||||
organization={organization}
|
||||
hasWhiteLabelPermission={hasWhiteLabelPermission}
|
||||
environmentId={params.workspaceId}
|
||||
workspaceId={params.workspaceId}
|
||||
isReadOnly={!isOwnerOrManager}
|
||||
isStorageConfigured={IS_STORAGE_CONFIGURED}
|
||||
/>
|
||||
|
||||
-1
@@ -23,7 +23,6 @@ import {
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
|
||||
interface EditOrganizationNameProps {
|
||||
environmentId: string;
|
||||
organization: TOrganization;
|
||||
membershipRole?: TOrganizationRole;
|
||||
}
|
||||
|
||||
+2
-6
@@ -53,16 +53,12 @@ const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||
<SettingsCard
|
||||
title={t("workspace.settings.general.organization_name")}
|
||||
description={t("workspace.settings.general.organization_name_description")}>
|
||||
<EditOrganizationNameForm
|
||||
organization={organization}
|
||||
environmentId={params.workspaceId}
|
||||
membershipRole={currentUserMembership?.role}
|
||||
/>
|
||||
<EditOrganizationNameForm organization={organization} membershipRole={currentUserMembership?.role} />
|
||||
</SettingsCard>
|
||||
<EmailCustomizationSettings
|
||||
organization={organization}
|
||||
hasWhiteLabelPermission={hasWhiteLabelPermission}
|
||||
environmentId={params.workspaceId}
|
||||
workspaceId={params.workspaceId}
|
||||
isReadOnly={!isOwnerOrManager}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
fbLogoUrl={FB_LOGO_URL}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
import { Unplug } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
|
||||
export const EmptyAppSurveys = () => {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { InboxIcon, PresentationIcon } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { revalidateSurveyIdPath } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/actions";
|
||||
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
|
||||
|
||||
|
||||
-4
@@ -1,7 +1,6 @@
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
@@ -22,7 +21,6 @@ interface ResponseCardModalProps {
|
||||
selectedResponseId: string | null;
|
||||
setSelectedResponseId: (id: string | null) => void;
|
||||
survey: TSurvey;
|
||||
environment: TEnvironment;
|
||||
user?: TUser;
|
||||
environmentTags: TTag[];
|
||||
updateResponse: (responseId: string, updatedResponse: TResponse) => void;
|
||||
@@ -38,7 +36,6 @@ export const ResponseCardModal = ({
|
||||
selectedResponseId,
|
||||
setSelectedResponseId,
|
||||
survey,
|
||||
environment,
|
||||
user,
|
||||
environmentTags,
|
||||
updateResponse,
|
||||
@@ -110,7 +107,6 @@ export const ResponseCardModal = ({
|
||||
survey={survey}
|
||||
response={responses[currentIndex]}
|
||||
user={user}
|
||||
environment={environment}
|
||||
environmentTags={environmentTags}
|
||||
isReadOnly={isReadOnly}
|
||||
updateResponse={updateResponse}
|
||||
|
||||
-4
@@ -3,7 +3,6 @@
|
||||
import { TFunction } from "i18next";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurveyQuota } from "@formbricks/types/quota";
|
||||
import { TResponseDataValue, TResponseTableData, TResponseWithQuotas } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
@@ -16,7 +15,6 @@ interface ResponseDataViewProps {
|
||||
survey: TSurvey;
|
||||
responses: TResponseWithQuotas[];
|
||||
user?: TUser;
|
||||
environment: TEnvironment;
|
||||
environmentTags: TTag[];
|
||||
isReadOnly: boolean;
|
||||
fetchNextPage: () => void;
|
||||
@@ -120,7 +118,6 @@ export const ResponseDataView: React.FC<ResponseDataViewProps> = ({
|
||||
survey,
|
||||
responses,
|
||||
user,
|
||||
environment,
|
||||
environmentTags,
|
||||
isReadOnly,
|
||||
fetchNextPage,
|
||||
@@ -148,7 +145,6 @@ export const ResponseDataView: React.FC<ResponseDataViewProps> = ({
|
||||
user={user}
|
||||
environmentTags={environmentTags}
|
||||
isReadOnly={isReadOnly}
|
||||
environment={environment}
|
||||
fetchNextPage={fetchNextPage}
|
||||
hasMore={hasMore}
|
||||
updateResponseList={updateResponseList}
|
||||
|
||||
-4
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurveyQuota } from "@formbricks/types/quota";
|
||||
import { TResponseWithQuotas } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
@@ -16,7 +15,6 @@ import { getFormattedFilters } from "@/app/lib/surveys/surveys";
|
||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||
|
||||
interface ResponsePageProps {
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
surveyId: string;
|
||||
user?: TUser;
|
||||
@@ -30,7 +28,6 @@ interface ResponsePageProps {
|
||||
}
|
||||
|
||||
export const ResponsePage = ({
|
||||
environment,
|
||||
survey,
|
||||
surveyId,
|
||||
user,
|
||||
@@ -145,7 +142,6 @@ export const ResponsePage = ({
|
||||
survey={survey}
|
||||
responses={responses}
|
||||
user={user}
|
||||
environment={environment}
|
||||
environmentTags={environmentTags}
|
||||
isReadOnly={isReadOnly}
|
||||
fetchNextPage={fetchNextPage}
|
||||
|
||||
-4
@@ -18,7 +18,6 @@ import { VisibilityState, getCoreRowModel, useReactTable } from "@tanstack/react
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurveyQuota } from "@formbricks/types/quota";
|
||||
import { TResponseTableData, TResponseWithQuotas } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
@@ -49,7 +48,6 @@ interface ResponseTableProps {
|
||||
data: TResponseTableData[];
|
||||
survey: TSurvey;
|
||||
responses: TResponseWithQuotas[] | null;
|
||||
environment: TEnvironment;
|
||||
user?: TUser;
|
||||
environmentTags: TTag[];
|
||||
isReadOnly: boolean;
|
||||
@@ -70,7 +68,6 @@ export const ResponseTable = ({
|
||||
survey,
|
||||
responses,
|
||||
user,
|
||||
environment,
|
||||
environmentTags,
|
||||
isReadOnly,
|
||||
fetchNextPage,
|
||||
@@ -310,7 +307,6 @@ export const ResponseTable = ({
|
||||
survey={survey}
|
||||
responses={responses}
|
||||
user={user}
|
||||
environment={environment}
|
||||
environmentTags={environmentTags}
|
||||
isReadOnly={isReadOnly}
|
||||
updateResponse={updateResponse}
|
||||
|
||||
+1
-5
@@ -21,9 +21,7 @@ const Page = async (props: { params: Promise<{ workspaceId: string; surveyId: st
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
|
||||
const { session, environment, organization, isReadOnly, workspace } = await getWorkspaceAuth(
|
||||
params.workspaceId
|
||||
);
|
||||
const { session, organization, isReadOnly, workspace } = await getWorkspaceAuth(params.workspaceId);
|
||||
|
||||
const [survey, user, tags, isContactsEnabled, responseCount] = await Promise.all([
|
||||
getSurvey(params.surveyId),
|
||||
@@ -66,7 +64,6 @@ const Page = async (props: { params: Promise<{ workspaceId: string; surveyId: st
|
||||
pageTitle={survey.name}
|
||||
cta={
|
||||
<SurveyAnalysisCTA
|
||||
environment={environment}
|
||||
survey={survey}
|
||||
isReadOnly={isReadOnly}
|
||||
user={user}
|
||||
@@ -81,7 +78,6 @@ const Page = async (props: { params: Promise<{ workspaceId: string; surveyId: st
|
||||
<SurveyAnalysisNavigation survey={survey} activeId="responses" />
|
||||
</PageHeader>
|
||||
<ResponsePage
|
||||
environment={environment}
|
||||
survey={survey}
|
||||
surveyId={params.surveyId}
|
||||
environmentTags={tags}
|
||||
|
||||
+3
-3
@@ -138,7 +138,7 @@ export const getEmailHtmlAction = authenticatedActionClient
|
||||
const ZGeneratePersonalLinksAction = z.object({
|
||||
surveyId: ZId,
|
||||
segmentId: ZId,
|
||||
environmentId: ZId,
|
||||
workspaceId: ZId,
|
||||
expirationDays: z.number().optional(),
|
||||
});
|
||||
|
||||
@@ -148,7 +148,7 @@ export const generatePersonalLinksAction = authenticatedActionClient
|
||||
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
|
||||
const isContactsEnabled = await getIsContactsEnabled(organizationId);
|
||||
if (!isContactsEnabled) {
|
||||
throw new OperationNotAllowedError("Contacts are not enabled for this environment");
|
||||
throw new OperationNotAllowedError("Contacts are not enabled for this workspace");
|
||||
}
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
@@ -217,7 +217,7 @@ export const generatePersonalLinksAction = authenticatedActionClient
|
||||
|
||||
const ZUpdateSingleUseLinksAction = z.object({
|
||||
surveyId: ZId,
|
||||
environmentId: ZId,
|
||||
workspaceId: ZId,
|
||||
isSingleUse: z.boolean(),
|
||||
isSingleUseEncryption: z.boolean(),
|
||||
});
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyElementSummaryAddress } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { ArrayResponse } from "@/modules/ui/components/array-response";
|
||||
|
||||
-1
@@ -8,7 +8,6 @@ import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface CalSummaryProps {
|
||||
elementSummary: TSurveyElementSummaryCal;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyElementSummaryContactInfo } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { ArrayResponse } from "@/modules/ui/components/array-response";
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyElementSummaryDate } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { formatStoredDateForDisplay } from "@/lib/utils/date-display";
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyElementSummaryFileUpload } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { getOriginalFileNameFromUrl } from "@/modules/storage/url-helpers";
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveyElementSummaryHiddenFields } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveyElementSummaryMultipleChoice, TSurveyType } from "@formbricks/types/surveys/types";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { getChoiceIdByValue } from "@/lib/response/utils";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyElementSummaryOpenText } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { renderHyperlinkedContent } from "@/modules/analysis/utils";
|
||||
|
||||
+6
-6
@@ -4,26 +4,26 @@ import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { useWorkspaceContext } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { Confetti } from "@/modules/ui/components/confetti";
|
||||
|
||||
interface SummaryMetadataProps {
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
export const SuccessMessage = ({ environment, survey }: SummaryMetadataProps) => {
|
||||
export const SuccessMessage = ({ survey }: SummaryMetadataProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspaceContext();
|
||||
const searchParams = useSearchParams();
|
||||
const [confetti, setConfetti] = useState(false);
|
||||
|
||||
const isAppSurvey = survey.type === "app";
|
||||
const appSetupCompleted = environment.appSetupCompleted;
|
||||
const appSetupCompleted = workspace.appSetupCompleted;
|
||||
|
||||
useEffect(() => {
|
||||
const newSurveyParam = searchParams?.get("success");
|
||||
if (newSurveyParam && survey && environment) {
|
||||
if (newSurveyParam && survey && workspace) {
|
||||
setConfetti(true);
|
||||
toast.success(
|
||||
isAppSurvey && !appSetupCompleted
|
||||
@@ -47,7 +47,7 @@ export const SuccessMessage = ({ environment, survey }: SummaryMetadataProps) =>
|
||||
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
}
|
||||
}, [environment, isAppSurvey, searchParams, survey, appSetupCompleted, t]);
|
||||
}, [workspace, isAppSurvey, searchParams, survey, appSetupCompleted, t]);
|
||||
|
||||
return <>{confetti && <Confetti />}</>;
|
||||
};
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TDisplayWithContact } from "@formbricks/types/displays";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
|
||||
|
||||
+6
-12
@@ -2,13 +2,12 @@
|
||||
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspaceContext } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { EmptyAppSurveys } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
|
||||
import {
|
||||
SelectedFilterValue,
|
||||
@@ -38,12 +37,12 @@ import { AddressSummary } from "./AddressSummary";
|
||||
interface SummaryListProps {
|
||||
summary: TSurveySummary["summary"];
|
||||
responseCount: number | null;
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const SummaryList = ({ summary, environment, responseCount, survey, locale }: SummaryListProps) => {
|
||||
export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryListProps) => {
|
||||
const { workspace } = useWorkspaceContext();
|
||||
const { setSelectedFilter, selectedFilter } = useResponseFilter();
|
||||
const { t } = useTranslation();
|
||||
const setFilter = (
|
||||
@@ -100,7 +99,7 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
|
||||
|
||||
return (
|
||||
<div className="mt-10 space-y-8">
|
||||
{survey.type === "app" && responseCount === 0 && !environment.appSetupCompleted ? (
|
||||
{survey.type === "app" && responseCount === 0 && !workspace.appSetupCompleted ? (
|
||||
<EmptyAppSurveys />
|
||||
) : summary.length === 0 ? (
|
||||
<SkeletonLoader type="summary" />
|
||||
@@ -199,12 +198,7 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
|
||||
}
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.Cal) {
|
||||
return (
|
||||
<CalSummary
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
/>
|
||||
<CalSummary key={elementSummary.element.id} elementSummary={elementSummary} survey={survey} />
|
||||
);
|
||||
}
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.Matrix) {
|
||||
|
||||
-4
@@ -5,7 +5,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TDisplayWithContact } from "@formbricks/types/displays";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import {
|
||||
@@ -45,7 +44,6 @@ const defaultSurveySummary: TSurveySummary = {
|
||||
};
|
||||
|
||||
interface SummaryPageProps {
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
surveyId: string;
|
||||
locale: TUserLocale;
|
||||
@@ -54,7 +52,6 @@ interface SummaryPageProps {
|
||||
}
|
||||
|
||||
export const SummaryPage = ({
|
||||
environment,
|
||||
survey,
|
||||
surveyId,
|
||||
locale,
|
||||
@@ -211,7 +208,6 @@ export const SummaryPage = ({
|
||||
summary={surveySummary.summary}
|
||||
responseCount={surveySummary.meta.totalResponses}
|
||||
survey={surveyMemoized}
|
||||
environment={environment}
|
||||
locale={locale}
|
||||
/>
|
||||
</>
|
||||
|
||||
+8
-11
@@ -5,18 +5,17 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { useEnvironment } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspaceContext } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { SuccessMessage } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage";
|
||||
import { ShareSurveyModal } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal";
|
||||
import { SurveyStatusDropdown } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/components/SurveyStatusDropdown";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { EditPublicSurveyAlertDialog } from "@/modules/survey/components/edit-public-survey-alert-dialog";
|
||||
import { useSingleUseId } from "@/modules/survey/hooks/useSingleUseId";
|
||||
import { copySurveyToOtherEnvironmentAction } from "@/modules/survey/list/actions";
|
||||
import { copySurveyToOtherWorkspaceAction } from "@/modules/survey/list/actions";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal";
|
||||
import { IconBar } from "@/modules/ui/components/iconbar";
|
||||
@@ -24,7 +23,6 @@ import { resetSurveyAction } from "../actions";
|
||||
|
||||
interface SurveyAnalysisCTAProps {
|
||||
survey: TSurvey;
|
||||
environment: TEnvironment;
|
||||
isReadOnly: boolean;
|
||||
user: TUser;
|
||||
publicDomain: string;
|
||||
@@ -42,7 +40,6 @@ interface ModalState {
|
||||
|
||||
export const SurveyAnalysisCTA = ({
|
||||
survey,
|
||||
environment,
|
||||
isReadOnly,
|
||||
user,
|
||||
publicDomain,
|
||||
@@ -64,10 +61,10 @@ export const SurveyAnalysisCTA = ({
|
||||
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
|
||||
const { workspace } = useEnvironment();
|
||||
const { workspace } = useWorkspaceContext();
|
||||
const { refreshSingleUseId } = useSingleUseId(survey, isReadOnly);
|
||||
|
||||
const appSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
|
||||
const appSetupCompleted = survey.type === "app" && workspace.appSetupCompleted;
|
||||
|
||||
useEffect(() => {
|
||||
setModalState((prev) => ({
|
||||
@@ -93,9 +90,9 @@ export const SurveyAnalysisCTA = ({
|
||||
|
||||
const duplicateSurveyAndRoute = async (surveyId: string) => {
|
||||
setLoading(true);
|
||||
const duplicatedSurveyResponse = await copySurveyToOtherEnvironmentAction({
|
||||
const duplicatedSurveyResponse = await copySurveyToOtherWorkspaceAction({
|
||||
surveyId: surveyId,
|
||||
targetEnvironmentId: environment.id,
|
||||
targetWorkspaceId: workspace.id,
|
||||
});
|
||||
if (duplicatedSurveyResponse?.data) {
|
||||
toast.success(t("workspace.surveys.survey_duplicated_successfully"));
|
||||
@@ -183,7 +180,7 @@ export const SurveyAnalysisCTA = ({
|
||||
return (
|
||||
<div className="hidden justify-end gap-x-1.5 sm:flex">
|
||||
{!isReadOnly && (appSetupCompleted || survey.type === "link") && survey.status !== "draft" && (
|
||||
<SurveyStatusDropdown environment={environment} survey={survey} />
|
||||
<SurveyStatusDropdown survey={survey} />
|
||||
)}
|
||||
|
||||
<IconBar actions={iconActions} />
|
||||
@@ -215,7 +212,7 @@ export const SurveyAnalysisCTA = ({
|
||||
workspaceCustomScripts={workspace.customHeadScripts}
|
||||
/>
|
||||
)}
|
||||
<SuccessMessage environment={environment} survey={survey} />
|
||||
<SuccessMessage survey={survey} />
|
||||
|
||||
{responseCount > 0 && (
|
||||
<EditPublicSurveyAlertDialog
|
||||
|
||||
+3
-4
@@ -70,7 +70,6 @@ export const ShareSurveyModal = ({
|
||||
isStorageConfigured,
|
||||
workspaceCustomScripts,
|
||||
}: ShareSurveyModalProps) => {
|
||||
const environmentId = survey.environmentId;
|
||||
const [surveyUrl, setSurveyUrl] = useState<string>(getSurveyUrl(survey, publicDomain, "default"));
|
||||
const [showView, setShowView] = useState<ModalView>(modalView);
|
||||
const { email } = user;
|
||||
@@ -103,7 +102,7 @@ export const ShareSurveyModal = ({
|
||||
description: t("workspace.surveys.share.personal_links.description"),
|
||||
componentType: PersonalLinksTab,
|
||||
componentProps: {
|
||||
environmentId,
|
||||
workspaceId: survey.workspaceId,
|
||||
surveyId: survey.id,
|
||||
segments,
|
||||
isContactsEnabled,
|
||||
@@ -163,7 +162,7 @@ export const ShareSurveyModal = ({
|
||||
title: t("workspace.surveys.share.dynamic_popup.nav_title"),
|
||||
description: t("workspace.surveys.share.dynamic_popup.description"),
|
||||
componentType: DynamicPopupTab,
|
||||
componentProps: { environmentId, surveyId: survey.id },
|
||||
componentProps: { surveyId: survey.id },
|
||||
},
|
||||
{
|
||||
id: ShareSettingsType.LINK_SETTINGS,
|
||||
@@ -210,7 +209,7 @@ export const ShareSurveyModal = ({
|
||||
user.locale,
|
||||
surveyUrl,
|
||||
isReadOnly,
|
||||
environmentId,
|
||||
survey.workspaceId,
|
||||
segments,
|
||||
isContactsEnabled,
|
||||
isFormbricksCloud,
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ export const AnonymousLinksTab = ({
|
||||
try {
|
||||
const updatedSurveyResponse = await updateSingleUseLinksAction({
|
||||
surveyId: survey.id,
|
||||
environmentId: survey.environmentId,
|
||||
workspaceId: survey.workspaceId,
|
||||
isSingleUse,
|
||||
isSingleUseEncryption,
|
||||
});
|
||||
|
||||
+6
-6
@@ -13,7 +13,7 @@ import { ReactNode, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TActionClass } from "@formbricks/types/action-classes";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { useEnvironment } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspaceContext } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { useSurvey } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/context/survey-context";
|
||||
import { Alert, AlertButton, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { H4, InlineSmall, Small } from "@/modules/ui/components/typography";
|
||||
@@ -88,7 +88,7 @@ const DisplayCriteriaItem = ({ icon, title, titleSuffix, description }: DisplayC
|
||||
|
||||
export const AppTab = () => {
|
||||
const { t } = useTranslation();
|
||||
const { environment, workspace } = useEnvironment();
|
||||
const { workspace } = useWorkspaceContext();
|
||||
const { survey } = useSurvey();
|
||||
|
||||
const documentationLinks = useMemo(() => createDocumentationLinks(t), [t]);
|
||||
@@ -151,18 +151,18 @@ export const AppTab = () => {
|
||||
return (
|
||||
<div className="flex flex-col justify-between space-y-6 pb-4">
|
||||
<div className="flex flex-col space-y-6">
|
||||
<Alert variant={environment.appSetupCompleted ? "success" : "warning"} size="default">
|
||||
<Alert variant={workspace.appSetupCompleted ? "success" : "warning"} size="default">
|
||||
<AlertTitle>
|
||||
{environment.appSetupCompleted
|
||||
{workspace.appSetupCompleted
|
||||
? t("workspace.surveys.summary.in_app.connection_title")
|
||||
: t("workspace.surveys.summary.in_app.no_connection_title")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{environment.appSetupCompleted
|
||||
{workspace.appSetupCompleted
|
||||
? t("workspace.surveys.summary.in_app.connection_description")
|
||||
: t("workspace.surveys.summary.in_app.no_connection_description")}
|
||||
</AlertDescription>
|
||||
{!environment.appSetupCompleted && (
|
||||
{!workspace.appSetupCompleted && (
|
||||
<AlertButton asChild>
|
||||
<Link href={`/workspaces/${workspace?.id}/app-connection`}>
|
||||
{t("common.connect_formbricks")}
|
||||
|
||||
+2
-3
@@ -2,16 +2,15 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { DocumentationLinks } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links";
|
||||
import { Alert, AlertButton, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
|
||||
interface DynamicPopupTabProps {
|
||||
environmentId: string;
|
||||
surveyId: string;
|
||||
}
|
||||
|
||||
export const DynamicPopupTab = ({ environmentId: _environmentId, surveyId }: DynamicPopupTabProps) => {
|
||||
export const DynamicPopupTab = ({ surveyId }: DynamicPopupTabProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspace();
|
||||
|
||||
|
||||
+1
-1
@@ -233,7 +233,7 @@ export const LinkSettingsTab = ({ isReadOnly, locale, isStorageConfigured }: Lin
|
||||
<FileInput
|
||||
id={`og-image-upload-${survey.id}`}
|
||||
allowedFileExtensions={["png", "jpeg", "jpg", "webp"]}
|
||||
environmentId={survey.environmentId}
|
||||
workspaceId={survey.workspaceId}
|
||||
onFileUpload={handleFileUpload}
|
||||
fileUrl={field.value}
|
||||
maxSizeInMB={5}
|
||||
|
||||
+4
-4
@@ -6,7 +6,7 @@ import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { DocumentationLinks } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -30,7 +30,7 @@ import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
|
||||
import { generatePersonalLinksAction } from "../../actions";
|
||||
|
||||
interface PersonalLinksTabProps {
|
||||
environmentId: string;
|
||||
workspaceId: string;
|
||||
surveyId: string;
|
||||
segments: TSegment[];
|
||||
isContactsEnabled: boolean;
|
||||
@@ -70,7 +70,7 @@ const RestrictedDatePicker = ({
|
||||
};
|
||||
|
||||
export const PersonalLinksTab = ({
|
||||
environmentId,
|
||||
workspaceId,
|
||||
segments,
|
||||
surveyId,
|
||||
isContactsEnabled,
|
||||
@@ -117,7 +117,7 @@ export const PersonalLinksTab = ({
|
||||
const result = await generatePersonalLinksAction({
|
||||
surveyId: surveyId,
|
||||
segmentId: selectedSegment,
|
||||
environmentId: environmentId,
|
||||
workspaceId: workspaceId,
|
||||
expirationDays: expiryDate
|
||||
? Math.max(1, Math.floor((expiryDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)))
|
||||
: undefined,
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/environment-context";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import {
|
||||
ShareSettingsType,
|
||||
ShareViaType,
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { getStyling } from "@/lib/utils/styling";
|
||||
import { getWorkspaceByEnvironmentId } from "@/lib/workspace/service";
|
||||
import { getWorkspace } from "@/lib/workspace/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getPreviewEmailTemplateHtml } from "@/modules/email/components/preview-email-template";
|
||||
|
||||
@@ -12,7 +12,7 @@ export const getEmailTemplateHtml = async (surveyId: string, locale: string) =>
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError(t("common.survey"), surveyId);
|
||||
}
|
||||
const workspace = await getWorkspaceByEnvironmentId(survey.environmentId);
|
||||
const workspace = await getWorkspace(survey.workspaceId);
|
||||
if (!workspace) {
|
||||
throw new ResourceNotFoundError(t("common.workspace"), null);
|
||||
}
|
||||
|
||||
+1
-5
@@ -21,9 +21,7 @@ const SurveyPage = async (props: { params: Promise<{ workspaceId: string; survey
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
|
||||
const { session, environment, isReadOnly, workspace, organization } = await getWorkspaceAuth(
|
||||
params.workspaceId
|
||||
);
|
||||
const { session, isReadOnly, workspace, organization } = await getWorkspaceAuth(params.workspaceId);
|
||||
|
||||
const surveyId = params.surveyId;
|
||||
|
||||
@@ -66,7 +64,6 @@ const SurveyPage = async (props: { params: Promise<{ workspaceId: string; survey
|
||||
pageTitle={survey.name}
|
||||
cta={
|
||||
<SurveyAnalysisCTA
|
||||
environment={environment}
|
||||
survey={survey}
|
||||
isReadOnly={isReadOnly}
|
||||
user={user}
|
||||
@@ -81,7 +78,6 @@ const SurveyPage = async (props: { params: Promise<{ workspaceId: string; survey
|
||||
<SurveyAnalysisNavigation survey={survey} activeId="summary" />
|
||||
</PageHeader>
|
||||
<SummaryPage
|
||||
environment={environment}
|
||||
survey={survey}
|
||||
surveyId={params.surveyId}
|
||||
locale={user.locale ?? DEFAULT_LOCALE}
|
||||
|
||||
+4
-8
@@ -3,8 +3,8 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { useWorkspaceContext } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { updateSurveyAction } from "@/modules/survey/editor/actions";
|
||||
import {
|
||||
@@ -17,16 +17,12 @@ import {
|
||||
import { SurveyStatusIndicator } from "@/modules/ui/components/survey-status-indicator";
|
||||
|
||||
interface SurveyStatusDropdownProps {
|
||||
environment: TEnvironment;
|
||||
updateLocalSurveyStatus?: (status: TSurvey["status"]) => void;
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
export const SurveyStatusDropdown = ({
|
||||
environment,
|
||||
updateLocalSurveyStatus,
|
||||
survey,
|
||||
}: SurveyStatusDropdownProps) => {
|
||||
export const SurveyStatusDropdown = ({ updateLocalSurveyStatus, survey }: SurveyStatusDropdownProps) => {
|
||||
const { workspace } = useWorkspaceContext();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -72,7 +68,7 @@ export const SurveyStatusDropdown = ({
|
||||
<SelectTrigger className="w-[170px] bg-white md:w-[200px]">
|
||||
<SelectValue>
|
||||
<div className="flex items-center">
|
||||
{(survey.type === "link" || environment.appSetupCompleted) && (
|
||||
{(survey.type === "link" || workspace.appSetupCompleted) && (
|
||||
<SurveyStatusIndicator status={survey.status} />
|
||||
)}
|
||||
<span className="ml-2 text-sm text-slate-700">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors";
|
||||
import { findWorkspaceByIdOrLegacyEnvId } from "@/lib/utils/resolve-client-id";
|
||||
import { hasUserWorkspaceAccess } from "@/lib/workspace/auth";
|
||||
import { getWorkspaceByEnvironmentId } from "@/lib/workspace/service";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
|
||||
export const GET = async (
|
||||
@@ -17,7 +17,7 @@ export const GET = async (
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthenticationError("Not authenticated");
|
||||
|
||||
const workspace = await getWorkspaceByEnvironmentId(environmentId);
|
||||
const workspace = await findWorkspaceByIdOrLegacyEnvId(environmentId);
|
||||
if (!workspace) return notFound();
|
||||
|
||||
const hasAccess = await hasUserWorkspaceAccess(session.user.id, workspace.id);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors";
|
||||
import { findWorkspaceByIdOrLegacyEnvId } from "@/lib/utils/resolve-client-id";
|
||||
import { hasUserWorkspaceAccess } from "@/lib/workspace/auth";
|
||||
import { getWorkspaceByEnvironmentId } from "@/lib/workspace/service";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
|
||||
export const GET = async (_: Request, context: { params: Promise<{ environmentId: string }> }) => {
|
||||
@@ -14,7 +14,7 @@ export const GET = async (_: Request, context: { params: Promise<{ environmentId
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthenticationError("Not authenticated");
|
||||
|
||||
const workspace = await getWorkspaceByEnvironmentId(environmentId);
|
||||
const workspace = await findWorkspaceByIdOrLegacyEnvId(environmentId);
|
||||
if (!workspace) return notFound();
|
||||
|
||||
const hasAccess = await hasUserWorkspaceAccess(session.user.id, workspace.id);
|
||||
|
||||
@@ -87,7 +87,7 @@ export const mockSurvey: TSurvey = {
|
||||
updatedAt: new Date(),
|
||||
name: "Start from scratch",
|
||||
type: "link",
|
||||
environmentId: "cm98djl8e000919hpzi6a80zp",
|
||||
workspaceId: "cm98djl8e000919hpzi6a80zp",
|
||||
createdBy: "cm98dg3xm000019hpubj39vfi",
|
||||
status: "inProgress",
|
||||
welcomeCard: {
|
||||
|
||||
@@ -58,7 +58,7 @@ const hiddenFieldId = "hidden1";
|
||||
const variableId = "var1";
|
||||
|
||||
const mockPipelineInput = {
|
||||
environmentId: "env1",
|
||||
workspaceId: "env1",
|
||||
surveyId: surveyId,
|
||||
response: {
|
||||
id: "response1",
|
||||
@@ -158,7 +158,7 @@ const mockSurvey = {
|
||||
updatedAt: new Date(),
|
||||
displayOption: "displayOnce",
|
||||
displayPercentage: null,
|
||||
environmentId: "env1",
|
||||
workspaceId: "env1",
|
||||
singleUse: null,
|
||||
surveyClosedMessage: null,
|
||||
pin: null,
|
||||
@@ -167,7 +167,7 @@ const mockSurvey = {
|
||||
const mockAirtableIntegration: TIntegrationAirtable = {
|
||||
id: "int_airtable",
|
||||
type: "airtable",
|
||||
environmentId: "env1",
|
||||
workspaceId: "env1",
|
||||
config: {
|
||||
key: { access_token: "airtable_key" } as TIntegrationAirtableCredential,
|
||||
data: [
|
||||
@@ -189,7 +189,7 @@ const mockAirtableIntegration: TIntegrationAirtable = {
|
||||
const mockGoogleSheetsIntegration: TIntegrationGoogleSheets = {
|
||||
id: "int_gsheets",
|
||||
type: "googleSheets",
|
||||
environmentId: "env1",
|
||||
workspaceId: "env1",
|
||||
config: {
|
||||
key: { refresh_token: "gsheet_key" } as TIntegrationGoogleSheetsCredential,
|
||||
data: [
|
||||
@@ -212,7 +212,7 @@ const mockGoogleSheetsIntegration: TIntegrationGoogleSheets = {
|
||||
const mockSlackIntegration: TIntegrationSlack = {
|
||||
id: "int_slack",
|
||||
type: "slack",
|
||||
environmentId: "env1",
|
||||
workspaceId: "env1",
|
||||
config: {
|
||||
key: { access_token: "slack_key", app_id: "A1" } as TIntegrationSlackCredential,
|
||||
data: [
|
||||
@@ -235,7 +235,7 @@ const mockSlackIntegration: TIntegrationSlack = {
|
||||
const mockNotionIntegration: TIntegrationNotion = {
|
||||
id: "int_notion",
|
||||
type: "notion",
|
||||
environmentId: "env1",
|
||||
workspaceId: "env1",
|
||||
config: {
|
||||
key: {
|
||||
access_token: "notion_key",
|
||||
|
||||
@@ -38,7 +38,6 @@ export const POST = async (request: Request) => {
|
||||
jsonInput,
|
||||
new Set(["contactAttributes", "variables", "data", "meta"])
|
||||
);
|
||||
|
||||
const inputValidation = ZPipelineInput.safeParse(convertedJsonInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ZResponse } from "@formbricks/types/responses";
|
||||
export const ZPipelineInput = z.object({
|
||||
event: ZWebhook.shape.triggers.element,
|
||||
response: ZResponse,
|
||||
environmentId: z.string(),
|
||||
workspaceId: z.string(),
|
||||
surveyId: z.string(),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { TAPIKeyEnvironmentPermission } from "@formbricks/types/auth";
|
||||
import { TAPIKeyWorkspacePermission } from "@formbricks/types/auth";
|
||||
import { getApiKeyWithPermissions } from "@/modules/organization/settings/api-keys/lib/api-key";
|
||||
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
|
||||
import { authenticateRequest } from "./auth";
|
||||
@@ -20,19 +20,10 @@ describe("getApiKeyWithPermissions", () => {
|
||||
createdBy: "user-id",
|
||||
lastUsedAt: null,
|
||||
label: "Test API Key",
|
||||
apiKeyEnvironments: [
|
||||
apiKeyWorkspaces: [
|
||||
{
|
||||
environmentId: "env-1",
|
||||
workspaceId: "workspace-1",
|
||||
permission: "manage" as const,
|
||||
environment: {
|
||||
id: "env-1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "production" as const,
|
||||
workspaceId: "workspace-1",
|
||||
appSetupCompleted: true,
|
||||
},
|
||||
workspace: { id: "workspace-1", name: "Workspace 1" },
|
||||
},
|
||||
],
|
||||
@@ -56,25 +47,19 @@ describe("getApiKeyWithPermissions", () => {
|
||||
});
|
||||
|
||||
describe("hasPermission", () => {
|
||||
const permissions: TAPIKeyEnvironmentPermission[] = [
|
||||
const permissions: TAPIKeyWorkspacePermission[] = [
|
||||
{
|
||||
environmentId: "env-1",
|
||||
permission: "manage",
|
||||
environmentType: "production",
|
||||
workspaceId: "workspace-1",
|
||||
workspaceName: "Workspace 1",
|
||||
},
|
||||
{
|
||||
environmentId: "env-2",
|
||||
permission: "write",
|
||||
environmentType: "production",
|
||||
workspaceId: "workspace-2",
|
||||
workspaceName: "Workspace 2",
|
||||
},
|
||||
{
|
||||
environmentId: "env-3",
|
||||
permission: "read",
|
||||
environmentType: "production",
|
||||
workspaceId: "workspace-3",
|
||||
workspaceName: "Workspace 3",
|
||||
},
|
||||
@@ -117,19 +102,10 @@ describe("authenticateRequest", () => {
|
||||
createdBy: "user-id",
|
||||
lastUsedAt: null,
|
||||
label: "Test API Key",
|
||||
apiKeyEnvironments: [
|
||||
apiKeyWorkspaces: [
|
||||
{
|
||||
environmentId: "env-1",
|
||||
workspaceId: "workspace-1",
|
||||
permission: "manage" as const,
|
||||
environment: {
|
||||
id: "env-1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "production" as const,
|
||||
workspaceId: "workspace-1",
|
||||
appSetupCompleted: true,
|
||||
},
|
||||
workspace: { id: "workspace-1", name: "Workspace 1" },
|
||||
},
|
||||
],
|
||||
@@ -140,11 +116,9 @@ describe("authenticateRequest", () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
type: "apiKey",
|
||||
environmentPermissions: [
|
||||
workspacePermissions: [
|
||||
{
|
||||
environmentId: "env-1",
|
||||
permission: "manage",
|
||||
environmentType: "production",
|
||||
workspaceId: "workspace-1",
|
||||
workspaceName: "Workspace 1",
|
||||
},
|
||||
@@ -173,7 +147,7 @@ describe("authenticateRequest", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("returns null when API key has no environment permissions", async () => {
|
||||
test("returns null when API key has no workspace permissions", async () => {
|
||||
const request = new NextRequest("http://localhost", {
|
||||
headers: { "x-api-key": "valid-api-key" },
|
||||
});
|
||||
@@ -186,7 +160,7 @@ describe("authenticateRequest", () => {
|
||||
createdBy: "user-id",
|
||||
lastUsedAt: null,
|
||||
label: "Test API Key",
|
||||
apiKeyEnvironments: [],
|
||||
apiKeyWorkspaces: [],
|
||||
};
|
||||
|
||||
vi.mocked(getApiKeyWithPermissions).mockResolvedValue(mockApiKeyData as any);
|
||||
|
||||
@@ -13,14 +13,11 @@ export const authenticateRequest = async (request: NextRequest): Promise<TAuthen
|
||||
if (!apiKeyData) return null;
|
||||
|
||||
// In the route handlers, we'll do more specific permission checks
|
||||
const environmentIds = apiKeyData.apiKeyEnvironments.map((env) => env.environmentId);
|
||||
if (environmentIds.length === 0) return null;
|
||||
if (apiKeyData.apiKeyWorkspaces.length === 0) return null;
|
||||
|
||||
const authentication: TAuthenticationApiKey = {
|
||||
type: "apiKey",
|
||||
environmentPermissions: apiKeyData.apiKeyEnvironments.map((env) => ({
|
||||
environmentId: env.environmentId,
|
||||
environmentType: env.environment.type,
|
||||
workspacePermissions: apiKeyData.apiKeyWorkspaces.map((env) => ({
|
||||
permission: env.permission,
|
||||
workspaceId: env.workspaceId,
|
||||
workspaceName: env.workspace.name,
|
||||
|
||||
@@ -31,7 +31,6 @@ vi.mock("./contact", () => ({
|
||||
getContactByUserId: vi.fn(),
|
||||
}));
|
||||
|
||||
const environmentId = "scgavd0rtce5xgahiresk4p0";
|
||||
const workspaceId = "cqiu9au22kgzgqjossdlp5qh";
|
||||
const surveyId = "kf4w7x11wut39ttl4j5v9ccg";
|
||||
const userId = "test-user-id";
|
||||
@@ -39,14 +38,12 @@ const contactId = "f9bufd72cffj19a7qj67z5fm";
|
||||
const displayId = "apbycx5war0mfsyztgpwb8wr";
|
||||
|
||||
const displayInput: TDisplayCreateInput = {
|
||||
environmentId,
|
||||
workspaceId,
|
||||
surveyId,
|
||||
userId,
|
||||
};
|
||||
|
||||
const displayInputWithoutUserId: TDisplayCreateInput = {
|
||||
environmentId,
|
||||
workspaceId,
|
||||
surveyId,
|
||||
};
|
||||
@@ -80,7 +77,7 @@ const mockDisplayWithoutContact = {
|
||||
const mockSurvey = {
|
||||
id: surveyId,
|
||||
name: "Test Survey",
|
||||
environmentId,
|
||||
workspaceId,
|
||||
} as any;
|
||||
|
||||
describe("createDisplay", () => {
|
||||
@@ -122,8 +119,7 @@ describe("createDisplay", () => {
|
||||
expect(getContactByUserId).toHaveBeenCalledWith(workspaceId, userId);
|
||||
expect(prisma.contact.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
environment: { connect: { id: environmentId } },
|
||||
workspace: { connect: { id: workspaceId } },
|
||||
workspaceId,
|
||||
attributes: {
|
||||
create: {
|
||||
attributeKey: {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getContactByUserId } from "./contact";
|
||||
export const createDisplay = async (displayInput: TDisplayCreateInput): Promise<{ id: string }> => {
|
||||
validateInputs([displayInput, ZDisplayCreateInput]);
|
||||
|
||||
const { environmentId, workspaceId, userId, surveyId } = displayInput;
|
||||
const { workspaceId, userId, surveyId } = displayInput;
|
||||
|
||||
try {
|
||||
let contact: { id: string } | null = null;
|
||||
@@ -17,8 +17,7 @@ export const createDisplay = async (displayInput: TDisplayCreateInput): Promise<
|
||||
if (!contact) {
|
||||
contact = await prisma.contact.create({
|
||||
data: {
|
||||
environment: { connect: { id: environmentId } },
|
||||
workspace: { connect: { id: workspaceId } },
|
||||
workspaceId,
|
||||
attributes: {
|
||||
create: {
|
||||
attributeKey: {
|
||||
|
||||
@@ -30,12 +30,11 @@ export const POST = withV1ApiWrapper({
|
||||
response: responses.notFoundResponse("Workspace", params.workspaceId),
|
||||
};
|
||||
}
|
||||
const { environmentId, workspaceId } = resolved;
|
||||
const { workspaceId } = resolved;
|
||||
|
||||
const jsonInput = await req.json();
|
||||
const inputValidation = ZDisplayCreateInput.safeParse({
|
||||
...jsonInput,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
|
||||
@@ -2,8 +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 { TJsWorkspaceState, TJsWorkspaceStateWorkspaceSetting } from "@formbricks/types/js";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TJsWorkspaceStateWorkspaceSetting } from "@formbricks/types/js";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { cache } from "@/lib/cache";
|
||||
import { WorkspaceStateData, getWorkspaceStateData } from "./data";
|
||||
@@ -11,6 +10,8 @@ import { getWorkspaceState } from "./environmentState";
|
||||
|
||||
vi.mock("server-only", () => ({}));
|
||||
|
||||
vi.mock("server-only", () => ({}));
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/cache", () => ({
|
||||
cache: {
|
||||
@@ -67,7 +68,6 @@ vi.mock("@formbricks/cache", () => ({
|
||||
const workspaceId = "test-workspace-id";
|
||||
|
||||
const mockWorkspace: TJsWorkspaceStateWorkspaceSetting = {
|
||||
id: workspaceId,
|
||||
recontactDays: 30,
|
||||
inAppSurveyBranding: true,
|
||||
placement: "bottomRight",
|
||||
@@ -84,7 +84,6 @@ const mockSurveys: TSurvey[] = [
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "App Survey In Progress",
|
||||
environmentId: workspaceId,
|
||||
type: "app",
|
||||
status: "inProgress",
|
||||
displayLimit: null,
|
||||
@@ -117,7 +116,7 @@ const mockSurveys: TSurvey[] = [
|
||||
} as unknown as TSurvey,
|
||||
];
|
||||
|
||||
const mockActionClasses: TActionClass[] = [
|
||||
const mockActionClasses = [
|
||||
{
|
||||
id: "action-1",
|
||||
createdAt: new Date(),
|
||||
@@ -129,7 +128,7 @@ const mockActionClasses: TActionClass[] = [
|
||||
environmentId: workspaceId,
|
||||
key: "action1",
|
||||
},
|
||||
];
|
||||
] as unknown as TActionClass[];
|
||||
|
||||
const mockWorkspaceStateData: WorkspaceStateData = {
|
||||
workspace: {
|
||||
|
||||
@@ -205,7 +205,6 @@ export const PUT = withV1ApiWrapper({
|
||||
// don't await to not block the response
|
||||
sendToPipeline({
|
||||
event: "responseUpdated",
|
||||
environmentId: survey.environmentId,
|
||||
workspaceId: survey.workspaceId,
|
||||
surveyId: survey.id,
|
||||
response: responseData,
|
||||
@@ -216,7 +215,6 @@ export const PUT = withV1ApiWrapper({
|
||||
// don't await to not block the response
|
||||
sendToPipeline({
|
||||
event: "responseFinished",
|
||||
environmentId: survey.environmentId,
|
||||
workspaceId: survey.workspaceId,
|
||||
surveyId: survey.id,
|
||||
response: responseData,
|
||||
|
||||
@@ -24,7 +24,7 @@ vi.mock("react", async () => {
|
||||
});
|
||||
|
||||
const mockContactId = "test-contact-id";
|
||||
const mockEnvironmentId = "test-env-id";
|
||||
const mockWorkspaceId = "test-env-id";
|
||||
const mockUserId = "test-user-id";
|
||||
|
||||
describe("Contact API Lib", () => {
|
||||
@@ -36,11 +36,11 @@ describe("Contact API Lib", () => {
|
||||
test("should return contact if found", async () => {
|
||||
const mockContactData = {
|
||||
id: mockContactId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: mockEnvironmentId,
|
||||
};
|
||||
vi.mocked(prisma.contact.findUnique).mockResolvedValue(mockContactData);
|
||||
vi.mocked(prisma.contact.findUnique).mockResolvedValue(mockContactData as any);
|
||||
|
||||
const contact = await getContact(mockContactId);
|
||||
|
||||
@@ -82,19 +82,17 @@ describe("Contact API Lib", () => {
|
||||
test("should return contact with formatted attributes if found", async () => {
|
||||
const mockContactData = {
|
||||
id: mockContactId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: mockEnvironmentId,
|
||||
attributes: [
|
||||
{ attributeKey: { key: "userId" }, value: mockUserId },
|
||||
{ attributeKey: { key: "email" }, value: "test@example.com" },
|
||||
],
|
||||
};
|
||||
vi.mocked(prisma.contact.findFirst).mockResolvedValue(
|
||||
mockContactData as Awaited<ReturnType<typeof prisma.contact.findFirst>>
|
||||
);
|
||||
vi.mocked(prisma.contact.findFirst).mockResolvedValue(mockContactData as any);
|
||||
|
||||
const contact = await getContactByUserId(mockEnvironmentId, mockUserId);
|
||||
const contact = await getContactByUserId(mockWorkspaceId, mockUserId);
|
||||
|
||||
expect(prisma.contact.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
@@ -102,7 +100,7 @@ describe("Contact API Lib", () => {
|
||||
some: {
|
||||
attributeKey: {
|
||||
key: "userId",
|
||||
workspaceId: mockEnvironmentId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
},
|
||||
value: mockUserId,
|
||||
},
|
||||
@@ -130,7 +128,7 @@ describe("Contact API Lib", () => {
|
||||
test("should return null if contact not found by userId", async () => {
|
||||
vi.mocked(prisma.contact.findFirst).mockResolvedValue(null);
|
||||
|
||||
const contact = await getContactByUserId(mockEnvironmentId, mockUserId);
|
||||
const contact = await getContactByUserId(mockWorkspaceId, mockUserId);
|
||||
|
||||
expect(prisma.contact.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
@@ -138,7 +136,7 @@ describe("Contact API Lib", () => {
|
||||
some: {
|
||||
attributeKey: {
|
||||
key: "userId",
|
||||
workspaceId: mockEnvironmentId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
},
|
||||
value: mockUserId,
|
||||
},
|
||||
|
||||
@@ -58,7 +58,6 @@ vi.mock("@/modules/ee/quotas/lib/evaluation-service", () => ({
|
||||
evaluateResponseQuotas: vi.fn(),
|
||||
}));
|
||||
|
||||
const environmentId = "test-environment-id";
|
||||
const workspaceId = "test-workspace-id";
|
||||
const surveyId = "test-survey-id";
|
||||
const organizationId = "test-organization-id";
|
||||
@@ -73,7 +72,6 @@ const mockOrganization = {
|
||||
};
|
||||
|
||||
const mockResponseInput: TResponseInput = {
|
||||
environmentId,
|
||||
workspaceId,
|
||||
surveyId,
|
||||
userId: null,
|
||||
|
||||
@@ -45,7 +45,6 @@ export const responseSelection = {
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
name: true,
|
||||
environmentId: true,
|
||||
workspaceId: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -73,11 +73,10 @@ export const POST = withV1ApiWrapper({
|
||||
response: responses.notFoundResponse("Workspace", params.workspaceId),
|
||||
};
|
||||
}
|
||||
const { environmentId, workspaceId } = resolved;
|
||||
const { workspaceId } = resolved;
|
||||
|
||||
const responseInputValidation = ZResponseInput.safeParse({
|
||||
...responseInput,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
@@ -190,7 +189,6 @@ export const POST = withV1ApiWrapper({
|
||||
|
||||
sendToPipeline({
|
||||
event: "responseCreated",
|
||||
environmentId: survey.environmentId,
|
||||
workspaceId,
|
||||
surveyId: responseData.surveyId,
|
||||
response: responseData,
|
||||
@@ -199,7 +197,6 @@ export const POST = withV1ApiWrapper({
|
||||
if (responseInput.finished) {
|
||||
sendToPipeline({
|
||||
event: "responseFinished",
|
||||
environmentId: survey.environmentId,
|
||||
workspaceId,
|
||||
surveyId: responseData.surveyId,
|
||||
response: responseData,
|
||||
|
||||
@@ -39,7 +39,7 @@ export const POST = withV1ApiWrapper({
|
||||
response: responses.notFoundResponse("Workspace", params.workspaceId),
|
||||
};
|
||||
}
|
||||
const { environmentId, workspaceId } = resolved;
|
||||
const { workspaceId } = resolved;
|
||||
|
||||
let jsonInput: TUploadPrivateFileRequest;
|
||||
|
||||
@@ -54,7 +54,6 @@ export const POST = withV1ApiWrapper({
|
||||
|
||||
const parsedInputResult = ZUploadPrivateFileRequest.safeParse({
|
||||
...jsonInput,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
@@ -88,7 +87,7 @@ export const POST = withV1ApiWrapper({
|
||||
|
||||
if (!organization) {
|
||||
return {
|
||||
response: responses.notFoundResponse("OrganizationByEnvironmentId", environmentId),
|
||||
response: responses.notFoundResponse("OrganizationByWorkspaceId", workspaceId),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const fetchAndAuthorizeActionClass = async (
|
||||
}
|
||||
|
||||
// Check if API key has permission to access this workspace with appropriate permissions
|
||||
if (!hasPermission(authentication.environmentPermissions, actionClass.workspaceId, method)) {
|
||||
if (!hasPermission(authentication.workspacePermissions, actionClass.workspaceId, method)) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export const PUT = withV1ApiWrapper({
|
||||
}
|
||||
|
||||
// Accept workspaceId as alternative to environmentId — resolve to production environment
|
||||
const resolved = await resolveBodyIds(actionClassUpdate, authentication.environmentPermissions, "PUT");
|
||||
const resolved = await resolveBodyIds(actionClassUpdate, authentication.workspacePermissions, "PUT");
|
||||
if (!resolved.ok) return { response: resolved.response };
|
||||
|
||||
const inputValidation = ZActionClassInput.safeParse(resolved.body);
|
||||
@@ -108,7 +108,7 @@ export const PUT = withV1ApiWrapper({
|
||||
|
||||
if (
|
||||
!resolved.alreadyAuthorized &&
|
||||
!hasPermission(authentication.environmentPermissions, inputValidation.data.workspaceId, "PUT")
|
||||
!hasPermission(authentication.workspacePermissions, inputValidation.data.workspaceId, "PUT")
|
||||
) {
|
||||
return { response: responses.unauthorizedResponse() };
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("getActionClasses", () => {
|
||||
type: "code" as const,
|
||||
key: "test-key-1" as string | null,
|
||||
noCodeConfig: {},
|
||||
environmentId: "env1",
|
||||
workspaceId: "ws1",
|
||||
},
|
||||
{
|
||||
id: "action2",
|
||||
@@ -35,7 +35,7 @@ describe("getActionClasses", () => {
|
||||
type: "noCode" as const,
|
||||
key: "test-key-2" as string | null,
|
||||
noCodeConfig: {},
|
||||
environmentId: "env2",
|
||||
workspaceId: "ws2",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ const selectActionClass = {
|
||||
type: true,
|
||||
key: true,
|
||||
noCodeConfig: true,
|
||||
environmentId: true,
|
||||
workspaceId: true,
|
||||
} satisfies Prisma.ActionClassSelect;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const GET = withV1ApiWrapper({
|
||||
|
||||
try {
|
||||
const workspaceIds = [
|
||||
...new Set(authentication.environmentPermissions.map((permission) => permission.workspaceId)),
|
||||
...new Set(authentication.workspacePermissions.map((permission) => permission.workspaceId)),
|
||||
];
|
||||
|
||||
const actionClasses = await getActionClasses(workspaceIds);
|
||||
@@ -53,8 +53,8 @@ export const POST = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
// Accept workspaceId as alternative to environmentId — resolve to production environment
|
||||
const resolved = await resolveBodyIds(actionClassInput, authentication.environmentPermissions, "POST");
|
||||
// Validate workspace-level permission
|
||||
const resolved = await resolveBodyIds(actionClassInput, authentication.workspacePermissions, "POST");
|
||||
if (!resolved.ok) return { response: resolved.response };
|
||||
|
||||
const inputValidation = ZActionClassInput.safeParse(resolved.body);
|
||||
@@ -70,7 +70,7 @@ export const POST = withV1ApiWrapper({
|
||||
|
||||
if (
|
||||
!resolved.alreadyAuthorized &&
|
||||
!hasPermission(authentication.environmentPermissions, inputValidation.data.workspaceId, "POST")
|
||||
!hasPermission(authentication.workspacePermissions, inputValidation.data.workspaceId, "POST")
|
||||
) {
|
||||
return { response: responses.unauthorizedResponse() };
|
||||
}
|
||||
|
||||
@@ -1,90 +1,54 @@
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TAPIKeyEnvironmentPermission } from "@formbricks/types/auth";
|
||||
import { TAPIKeyWorkspacePermission } from "@formbricks/types/auth";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { getWorkspaceIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { hasWorkspacePermission } from "@/modules/organization/settings/api-keys/lib/utils";
|
||||
import { findWorkspaceByIdOrLegacyEnvId } from "@/lib/utils/resolve-client-id";
|
||||
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
|
||||
|
||||
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
|
||||
/**
|
||||
* Resolves a workspaceId to its production environment's ID (simple wrapper for v1 routes).
|
||||
*/
|
||||
export const getProductionEnvironmentIdByWorkspaceId = async (
|
||||
workspaceId: string
|
||||
): Promise<string | null> => {
|
||||
const environment = await prisma.environment.findFirst({
|
||||
where: { workspaceId, type: "production" },
|
||||
select: { id: true },
|
||||
});
|
||||
return environment?.id ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a request body that contains either workspaceId or environmentId (but not both),
|
||||
* resolves the missing identifier so the returned body is guaranteed to contain both.
|
||||
* Given a request body that must contain workspaceId (or legacy environmentId),
|
||||
* validates workspace-level permission and returns the authorized body.
|
||||
*
|
||||
* Returns `{ body, alreadyAuthorized: true }` when workspace-level auth was used,
|
||||
* or `{ body, alreadyAuthorized: false }` when the caller still needs to check env-level permission.
|
||||
* Returns an error response if authorization or resolution fails.
|
||||
* Returns `{ body, alreadyAuthorized: true }` when workspace-level auth was used.
|
||||
* Returns an error response if authorization fails.
|
||||
*/
|
||||
export const resolveBodyIds = async <T extends Record<string, unknown>>(
|
||||
body: T,
|
||||
permissions: TAPIKeyEnvironmentPermission[],
|
||||
permissions: TAPIKeyWorkspacePermission[],
|
||||
method: HttpMethod
|
||||
): Promise<
|
||||
| { ok: true; body: T & { environmentId: string; workspaceId: string }; alreadyAuthorized: boolean }
|
||||
| { ok: true; body: T & { workspaceId: string }; alreadyAuthorized: boolean }
|
||||
| { ok: false; response: Response }
|
||||
> => {
|
||||
if (body.workspaceId && body.environmentId) {
|
||||
// Accept workspaceId or environmentId (legacy alias)
|
||||
const rawId = (body.workspaceId ?? body.environmentId) as string | undefined;
|
||||
|
||||
if (!rawId) {
|
||||
return {
|
||||
ok: false,
|
||||
response: responses.badRequestResponse("Provide either environmentId or workspaceId, not both"),
|
||||
response: responses.badRequestResponse("workspaceId must be provided"),
|
||||
};
|
||||
}
|
||||
|
||||
if (body.workspaceId && !body.environmentId) {
|
||||
if (typeof body.workspaceId !== "string") {
|
||||
return { ok: false, response: responses.badRequestResponse("workspaceId must be a string") };
|
||||
}
|
||||
const workspaceId = body.workspaceId;
|
||||
|
||||
if (!hasWorkspacePermission(permissions, workspaceId, method)) {
|
||||
return { ok: false, response: responses.unauthorizedResponse() };
|
||||
}
|
||||
|
||||
const resolvedEnvId = await getProductionEnvironmentIdByWorkspaceId(workspaceId);
|
||||
if (!resolvedEnvId) {
|
||||
return { ok: false, response: responses.notFoundResponse("Workspace", workspaceId) };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
body: { ...body, environmentId: resolvedEnvId, workspaceId },
|
||||
alreadyAuthorized: true,
|
||||
};
|
||||
if (typeof rawId !== "string") {
|
||||
return { ok: false, response: responses.badRequestResponse("workspaceId must be a string") };
|
||||
}
|
||||
|
||||
if (body.environmentId && !body.workspaceId) {
|
||||
if (typeof body.environmentId !== "string") {
|
||||
return { ok: false, response: responses.badRequestResponse("environmentId must be a string") };
|
||||
}
|
||||
// Resolve to canonical workspace id (handles legacy environment IDs)
|
||||
const workspace = await findWorkspaceByIdOrLegacyEnvId(rawId);
|
||||
if (!workspace) {
|
||||
return { ok: false, response: responses.notFoundResponse("Workspace", rawId) };
|
||||
}
|
||||
|
||||
let resolvedWorkspaceId: string;
|
||||
try {
|
||||
resolvedWorkspaceId = await getWorkspaceIdFromEnvironmentId(body.environmentId);
|
||||
} catch {
|
||||
return { ok: false, response: responses.notFoundResponse("Environment", body.environmentId) };
|
||||
}
|
||||
const workspaceId = workspace.id;
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
body: { ...body, workspaceId: resolvedWorkspaceId, environmentId: body.environmentId },
|
||||
alreadyAuthorized: false,
|
||||
};
|
||||
if (!hasPermission(permissions, workspaceId, method)) {
|
||||
return { ok: false, response: responses.unauthorizedResponse() };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
response: responses.badRequestResponse("Either environmentId or workspaceId must be provided"),
|
||||
ok: true,
|
||||
body: { ...body, workspaceId },
|
||||
alreadyAuthorized: true,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ const apiKeySelect = {
|
||||
id: true,
|
||||
organizationId: true,
|
||||
lastUsedAt: true,
|
||||
apiKeyEnvironments: {
|
||||
apiKeyWorkspaces: {
|
||||
select: {
|
||||
environment: {
|
||||
select: {
|
||||
@@ -42,7 +42,7 @@ type ApiKeyData = {
|
||||
hashedKey: string;
|
||||
organizationId: string;
|
||||
lastUsedAt: Date | null;
|
||||
apiKeyEnvironments: Array<{
|
||||
apiKeyWorkspaces: Array<{
|
||||
permission: string;
|
||||
environment: {
|
||||
id: string;
|
||||
@@ -117,7 +117,7 @@ const updateApiKeyUsage = async (apiKeyId: string) => {
|
||||
};
|
||||
|
||||
const buildEnvironmentResponse = (apiKeyData: ApiKeyData) => {
|
||||
const env = apiKeyData.apiKeyEnvironments[0].environment;
|
||||
const env = apiKeyData.apiKeyWorkspaces[0].environment;
|
||||
return Response.json({
|
||||
id: env.id,
|
||||
type: env.type,
|
||||
@@ -136,9 +136,9 @@ const buildEnvironmentResponse = (apiKeyData: ApiKeyData) => {
|
||||
|
||||
const isValidApiKeyEnvironment = (apiKeyData: ApiKeyData): boolean => {
|
||||
return (
|
||||
apiKeyData.apiKeyEnvironments.length === 1 &&
|
||||
apiKeyData.apiKeyWorkspaces.length === 1 &&
|
||||
ALLOWED_PERMISSIONS.includes(
|
||||
apiKeyData.apiKeyEnvironments[0].permission as (typeof ALLOWED_PERMISSIONS)[number]
|
||||
apiKeyData.apiKeyWorkspaces[0].permission as (typeof ALLOWED_PERMISSIONS)[number]
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ describe("updateResponseWithQuotaEvaluation", () => {
|
||||
name: "important",
|
||||
createdAt: new Date("2024-01-01"),
|
||||
updatedAt: new Date("2024-01-01"),
|
||||
environmentId: "env123",
|
||||
workspaceId: "workspace123",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ async function fetchAndAuthorizeResponse(
|
||||
return { error: responses.notFoundResponse("Survey", response.surveyId, true) };
|
||||
}
|
||||
|
||||
if (!hasPermission(authentication.environmentPermissions, survey.workspaceId, requiredPermission)) {
|
||||
if (!hasPermission(authentication.workspacePermissions, survey.workspaceId, requiredPermission)) {
|
||||
return { error: responses.unauthorizedResponse() };
|
||||
}
|
||||
|
||||
@@ -171,7 +171,6 @@ export const PUT = withV1ApiWrapper({
|
||||
|
||||
sendToPipeline({
|
||||
event: "responseUpdated",
|
||||
environmentId: result.survey.environmentId,
|
||||
workspaceId: result.survey.workspaceId,
|
||||
surveyId: result.survey.id,
|
||||
response: updated,
|
||||
@@ -180,7 +179,6 @@ export const PUT = withV1ApiWrapper({
|
||||
if (updated.finished) {
|
||||
sendToPipeline({
|
||||
event: "responseFinished",
|
||||
environmentId: result.survey.environmentId,
|
||||
workspaceId: result.survey.workspaceId,
|
||||
surveyId: result.survey.id,
|
||||
response: updated,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Organization, Prisma, Response as ResponsePrisma } from "@prisma/client";
|
||||
import { Prisma, Response as ResponsePrisma } from "@prisma/client";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
||||
import { TResponse, TResponseInput } from "@formbricks/types/responses";
|
||||
import { getResponseContact } from "@/lib/response/service";
|
||||
import { calculateTtcTotal } from "@/lib/response/utils";
|
||||
@@ -13,7 +12,6 @@ import { getContactByUserId } from "./contact";
|
||||
import { createResponse, getResponsesByWorkspaceIds } from "./response";
|
||||
|
||||
// Mock Data
|
||||
const environmentId = "test-environment-id";
|
||||
const workspaceId = "test-workspace-id";
|
||||
const mockUserId = "test-user-id";
|
||||
const surveyId = "test-survey-id";
|
||||
@@ -24,7 +22,6 @@ const mockOrganization = "test-organization-id";
|
||||
|
||||
const mockResponseInput: TResponseInput = {
|
||||
workspaceId,
|
||||
environmentId,
|
||||
surveyId,
|
||||
displayId,
|
||||
finished: true,
|
||||
|
||||
@@ -49,7 +49,6 @@ export const responseSelection = {
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
name: true,
|
||||
environmentId: true,
|
||||
workspaceId: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@ export const GET = withV1ApiWrapper({
|
||||
response: responses.notFoundResponse("Survey", surveyId, true),
|
||||
};
|
||||
}
|
||||
if (!hasPermission(authentication.environmentPermissions, survey.workspaceId, "GET")) {
|
||||
if (!hasPermission(authentication.workspacePermissions, survey.workspaceId, "GET")) {
|
||||
return {
|
||||
response: responses.unauthorizedResponse(),
|
||||
};
|
||||
@@ -42,7 +42,7 @@ export const GET = withV1ApiWrapper({
|
||||
allResponses.push(...surveyResponses);
|
||||
} else {
|
||||
const workspaceIds = [
|
||||
...new Set(authentication.environmentPermissions.map((permission) => permission.workspaceId)),
|
||||
...new Set(authentication.workspacePermissions.map((permission) => permission.workspaceId)),
|
||||
];
|
||||
const workspaceResponses = await getResponsesByWorkspaceIds(workspaceIds, limit, offset);
|
||||
allResponses.push(...workspaceResponses);
|
||||
@@ -101,7 +101,7 @@ export const POST = withV1ApiWrapper({
|
||||
}
|
||||
|
||||
// Accept workspaceId as alternative to environmentId — resolve to production environment
|
||||
const resolved = await resolveBodyIds(jsonInput, authentication.environmentPermissions, "POST");
|
||||
const resolved = await resolveBodyIds(jsonInput, authentication.workspacePermissions, "POST");
|
||||
if (!resolved.ok) return { response: resolved.response };
|
||||
|
||||
const inputValidation = ZResponseInput.safeParse(resolved.body);
|
||||
@@ -119,7 +119,7 @@ export const POST = withV1ApiWrapper({
|
||||
|
||||
if (
|
||||
!resolved.alreadyAuthorized &&
|
||||
!hasPermission(authentication.environmentPermissions, responseInput.workspaceId, "POST")
|
||||
!hasPermission(authentication.workspacePermissions, responseInput.workspaceId, "POST")
|
||||
) {
|
||||
return { response: responses.unauthorizedResponse() };
|
||||
}
|
||||
@@ -168,7 +168,6 @@ export const POST = withV1ApiWrapper({
|
||||
|
||||
sendToPipeline({
|
||||
event: "responseCreated",
|
||||
environmentId: surveyResult.survey.environmentId,
|
||||
workspaceId: surveyResult.survey.workspaceId,
|
||||
surveyId: response.surveyId,
|
||||
response: response,
|
||||
@@ -177,7 +176,6 @@ export const POST = withV1ApiWrapper({
|
||||
if (response.finished) {
|
||||
sendToPipeline({
|
||||
event: "responseFinished",
|
||||
environmentId: surveyResult.survey.environmentId,
|
||||
workspaceId: surveyResult.survey.workspaceId,
|
||||
surveyId: response.surveyId,
|
||||
response: response,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Session } from "next-auth";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { TAuthenticationApiKey } from "@formbricks/types/auth";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
|
||||
import { hasUserWorkspaceAccess } from "@/lib/workspace/auth";
|
||||
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
|
||||
import { checkAuth } from "./utils";
|
||||
|
||||
@@ -11,8 +11,8 @@ const mockBadRequestResponse = new Response("Bad Request", { status: 400 });
|
||||
const mockNotAuthenticatedResponse = new Response("Not authenticated", { status: 401 });
|
||||
const mockUnauthorizedResponse = new Response("Unauthorized", { status: 401 });
|
||||
|
||||
vi.mock("@/lib/environment/auth", () => ({
|
||||
hasUserEnvironmentAccess: vi.fn(),
|
||||
vi.mock("@/lib/workspace/auth", () => ({
|
||||
hasUserWorkspaceAccess: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/organization/settings/api-keys/lib/utils", () => ({
|
||||
@@ -28,17 +28,17 @@ vi.mock("@/app/lib/api/response", () => ({
|
||||
}));
|
||||
|
||||
describe("checkAuth", () => {
|
||||
const environmentId = "env-123";
|
||||
const workspaceId = "workspace-123";
|
||||
|
||||
test("returns notAuthenticatedResponse when authentication is null", async () => {
|
||||
const result = await checkAuth(null, environmentId);
|
||||
const result = await checkAuth(null, workspaceId);
|
||||
|
||||
expect(responses.notAuthenticatedResponse).toHaveBeenCalled();
|
||||
expect(result).toBe(mockNotAuthenticatedResponse);
|
||||
});
|
||||
|
||||
test("returns notAuthenticatedResponse when authentication is undefined", async () => {
|
||||
const result = await checkAuth(undefined as any, environmentId);
|
||||
const result = await checkAuth(undefined as any, workspaceId);
|
||||
|
||||
expect(responses.notAuthenticatedResponse).toHaveBeenCalled();
|
||||
expect(result).toBe(mockNotAuthenticatedResponse);
|
||||
@@ -47,12 +47,10 @@ describe("checkAuth", () => {
|
||||
test("returns unauthorizedResponse when API key authentication lacks POST permission", async () => {
|
||||
const mockAuthentication: TAuthenticationApiKey = {
|
||||
type: "apiKey",
|
||||
environmentPermissions: [
|
||||
workspacePermissions: [
|
||||
{
|
||||
environmentId: "env-123",
|
||||
permission: "read",
|
||||
environmentType: "production",
|
||||
workspaceId: "workspace-1",
|
||||
workspaceId: "workspace-123",
|
||||
workspaceName: "Workspace 1",
|
||||
},
|
||||
],
|
||||
@@ -65,11 +63,11 @@ describe("checkAuth", () => {
|
||||
|
||||
vi.mocked(hasPermission).mockReturnValue(false);
|
||||
|
||||
const result = await checkAuth(mockAuthentication, environmentId);
|
||||
const result = await checkAuth(mockAuthentication, workspaceId);
|
||||
|
||||
expect(hasPermission).toHaveBeenCalledWith(
|
||||
mockAuthentication.environmentPermissions,
|
||||
"workspace-1",
|
||||
mockAuthentication.workspacePermissions,
|
||||
"workspace-123",
|
||||
"POST"
|
||||
);
|
||||
expect(responses.unauthorizedResponse).toHaveBeenCalled();
|
||||
@@ -79,12 +77,10 @@ describe("checkAuth", () => {
|
||||
test("returns undefined when API key authentication has POST permission", async () => {
|
||||
const mockAuthentication: TAuthenticationApiKey = {
|
||||
type: "apiKey",
|
||||
environmentPermissions: [
|
||||
workspacePermissions: [
|
||||
{
|
||||
environmentId: "env-123",
|
||||
permission: "write",
|
||||
environmentType: "production",
|
||||
workspaceId: "workspace-1",
|
||||
workspaceId: "workspace-123",
|
||||
workspaceName: "Workspace 1",
|
||||
},
|
||||
],
|
||||
@@ -97,17 +93,17 @@ describe("checkAuth", () => {
|
||||
|
||||
vi.mocked(hasPermission).mockReturnValue(true);
|
||||
|
||||
const result = await checkAuth(mockAuthentication, environmentId);
|
||||
const result = await checkAuth(mockAuthentication, workspaceId);
|
||||
|
||||
expect(hasPermission).toHaveBeenCalledWith(
|
||||
mockAuthentication.environmentPermissions,
|
||||
"workspace-1",
|
||||
mockAuthentication.workspacePermissions,
|
||||
"workspace-123",
|
||||
"POST"
|
||||
);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returns unauthorizedResponse when session exists but user lacks environment access", async () => {
|
||||
test("returns unauthorizedResponse when session exists but user lacks workspace access", async () => {
|
||||
const mockSession: Session = {
|
||||
user: {
|
||||
id: "user-123",
|
||||
@@ -115,16 +111,16 @@ describe("checkAuth", () => {
|
||||
expires: "2024-12-31T23:59:59.999Z",
|
||||
};
|
||||
|
||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValue(false);
|
||||
vi.mocked(hasUserWorkspaceAccess).mockResolvedValue(false);
|
||||
|
||||
const result = await checkAuth(mockSession, environmentId);
|
||||
const result = await checkAuth(mockSession, workspaceId);
|
||||
|
||||
expect(hasUserEnvironmentAccess).toHaveBeenCalledWith("user-123", environmentId);
|
||||
expect(hasUserWorkspaceAccess).toHaveBeenCalledWith("user-123", workspaceId);
|
||||
expect(responses.unauthorizedResponse).toHaveBeenCalled();
|
||||
expect(result).toBe(mockUnauthorizedResponse);
|
||||
});
|
||||
|
||||
test("returns undefined when session exists and user has environment access", async () => {
|
||||
test("returns undefined when session exists and user has workspace access", async () => {
|
||||
const mockSession: Session = {
|
||||
user: {
|
||||
id: "user-123",
|
||||
@@ -132,18 +128,18 @@ describe("checkAuth", () => {
|
||||
expires: "2024-12-31T23:59:59.999Z",
|
||||
};
|
||||
|
||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValue(true);
|
||||
vi.mocked(hasUserWorkspaceAccess).mockResolvedValue(true);
|
||||
|
||||
const result = await checkAuth(mockSession, environmentId);
|
||||
const result = await checkAuth(mockSession, workspaceId);
|
||||
|
||||
expect(hasUserEnvironmentAccess).toHaveBeenCalledWith("user-123", environmentId);
|
||||
expect(hasUserWorkspaceAccess).toHaveBeenCalledWith("user-123", workspaceId);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returns notAuthenticatedResponse when authentication object is neither session nor API key", async () => {
|
||||
const invalidAuth = { someProperty: "value" } as any;
|
||||
|
||||
const result = await checkAuth(invalidAuth, environmentId);
|
||||
const result = await checkAuth(invalidAuth, workspaceId);
|
||||
|
||||
expect(responses.notAuthenticatedResponse).toHaveBeenCalled();
|
||||
expect(result).toBe(mockNotAuthenticatedResponse);
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { TApiV1Authentication } from "@/app/lib/api/with-api-logging";
|
||||
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
|
||||
import { hasUserWorkspaceAccess } from "@/lib/workspace/auth";
|
||||
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
|
||||
|
||||
export const checkAuth = async (authentication: TApiV1Authentication | undefined, environmentId: string) => {
|
||||
export const checkAuth = async (authentication: TApiV1Authentication | undefined, workspaceId: string) => {
|
||||
if (!authentication) {
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
|
||||
if ("user" in authentication) {
|
||||
const isUserAuthorized = await hasUserEnvironmentAccess(authentication.user.id, environmentId);
|
||||
const isUserAuthorized = await hasUserWorkspaceAccess(authentication.user.id, workspaceId);
|
||||
if (!isUserAuthorized) {
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
} else if ("apiKeyId" in authentication) {
|
||||
const perm = authentication.environmentPermissions.find((p) => p.environmentId === environmentId);
|
||||
if (!perm || !hasPermission(authentication.environmentPermissions, perm.workspaceId, "POST")) {
|
||||
if (!hasPermission(authentication.workspacePermissions, workspaceId, "POST")) {
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ZUploadPublicFileRequest } from "@formbricks/types/storage";
|
||||
import {
|
||||
getProductionEnvironmentIdByWorkspaceId,
|
||||
resolveBodyIds,
|
||||
} from "@/app/api/v1/management/lib/workspace-resolver";
|
||||
import { resolveBodyIds } from "@/app/api/v1/management/lib/workspace-resolver";
|
||||
import { checkAuth } from "@/app/api/v1/management/storage/lib/utils";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { getWorkspaceIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
|
||||
import { getSignedUrlForUpload } from "@/modules/storage/service";
|
||||
import { getErrorResponseFromStorageError } from "@/modules/storage/utils";
|
||||
@@ -31,33 +27,14 @@ export const POST = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
// Accept workspaceId as alternative to environmentId
|
||||
// Accept workspaceId
|
||||
if (authentication && "apiKeyId" in authentication) {
|
||||
// API key auth: resolveBodyIds handles resolution + permission check
|
||||
const resolved = await resolveBodyIds(storageInput, authentication.environmentPermissions, "POST");
|
||||
const resolved = await resolveBodyIds(storageInput, authentication.workspacePermissions, "POST");
|
||||
if (!resolved.ok) return { response: resolved.response };
|
||||
storageInput = resolved.body;
|
||||
} else if (storageInput.workspaceId && !storageInput.environmentId) {
|
||||
// Session auth with workspaceId only: resolve environmentId
|
||||
if (typeof storageInput.workspaceId !== "string") {
|
||||
return { response: responses.badRequestResponse("workspaceId must be a string") };
|
||||
}
|
||||
const envId = await getProductionEnvironmentIdByWorkspaceId(storageInput.workspaceId);
|
||||
if (!envId) {
|
||||
return { response: responses.notFoundResponse("Workspace", storageInput.workspaceId) };
|
||||
}
|
||||
storageInput = { ...storageInput, environmentId: envId };
|
||||
} else if (storageInput.environmentId && !storageInput.workspaceId) {
|
||||
// Session auth with environmentId only (current UI): resolve workspaceId
|
||||
if (typeof storageInput.environmentId !== "string") {
|
||||
return { response: responses.badRequestResponse("environmentId must be a string") };
|
||||
}
|
||||
try {
|
||||
const wsId = await getWorkspaceIdFromEnvironmentId(storageInput.environmentId);
|
||||
storageInput = { ...storageInput, workspaceId: wsId };
|
||||
} catch {
|
||||
return { response: responses.notFoundResponse("Environment", storageInput.environmentId) };
|
||||
}
|
||||
} else if (!storageInput.workspaceId) {
|
||||
return { response: responses.badRequestResponse("workspaceId must be provided") };
|
||||
}
|
||||
|
||||
const parsedInputResult = ZUploadPublicFileRequest.safeParse(storageInput);
|
||||
@@ -76,9 +53,9 @@ export const POST = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
const { fileName, fileType, environmentId, workspaceId } = parsedInputResult.data;
|
||||
const { fileName, fileType, workspaceId } = parsedInputResult.data;
|
||||
|
||||
const authResponse = await checkAuth(authentication, environmentId);
|
||||
const authResponse = await checkAuth(authentication, workspaceId);
|
||||
if (authResponse) {
|
||||
return {
|
||||
response: authResponse,
|
||||
|
||||
@@ -26,14 +26,14 @@ vi.mock("@formbricks/logger", () => ({
|
||||
}));
|
||||
|
||||
const surveyId = "clq5n7p1q0000m7z0h5p6g3r2";
|
||||
const environmentId = "clq5n7p1q0000m7z0h5p6g3r3";
|
||||
const workspaceId = "clq5n7p1q0000m7z0h5p6g3r3";
|
||||
const segmentId = "clq5n7p1q0000m7z0h5p6g3r4";
|
||||
const actionClassId1 = "clq5n7p1q0000m7z0h5p6g3r5";
|
||||
const actionClassId2 = "clq5n7p1q0000m7z0h5p6g3r6";
|
||||
|
||||
const mockDeletedSurveyAppPrivateSegment = {
|
||||
id: surveyId,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
type: "app",
|
||||
segment: { id: segmentId, isPrivate: true },
|
||||
triggers: [{ actionClass: { id: actionClassId1 } }, { actionClass: { id: actionClassId2 } }],
|
||||
@@ -41,7 +41,7 @@ const mockDeletedSurveyAppPrivateSegment = {
|
||||
|
||||
const mockDeletedSurveyLink = {
|
||||
id: surveyId,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
type: "link",
|
||||
segment: null,
|
||||
triggers: [],
|
||||
|
||||
@@ -30,7 +30,7 @@ const fetchAndAuthorizeSurvey = async (
|
||||
if (!survey) {
|
||||
return { error: responses.notFoundResponse("Survey", surveyId) };
|
||||
}
|
||||
if (!hasPermission(authentication.environmentPermissions, survey.workspaceId, requiredPermission)) {
|
||||
if (!hasPermission(authentication.workspacePermissions, survey.workspaceId, requiredPermission)) {
|
||||
return { error: responses.unauthorizedResponse() };
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export const GET = withV1ApiWrapper({
|
||||
response: responses.notFoundResponse("Survey", params.surveyId),
|
||||
};
|
||||
}
|
||||
if (!hasPermission(authentication.environmentPermissions, survey.workspaceId, "GET")) {
|
||||
if (!hasPermission(authentication.workspacePermissions, survey.workspaceId, "GET")) {
|
||||
return {
|
||||
response: responses.unauthorizedResponse(),
|
||||
};
|
||||
|
||||
@@ -28,8 +28,8 @@ vi.mock("react", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
const environmentId1 = "env1";
|
||||
const environmentId2 = "env2";
|
||||
const workspaceId1 = "env1";
|
||||
const workspaceId2 = "env2";
|
||||
const surveyId1 = "survey1";
|
||||
const surveyId2 = "survey2";
|
||||
const surveyId3 = "survey3";
|
||||
@@ -38,19 +38,19 @@ type PrismaSurvey = Awaited<ReturnType<typeof prisma.survey.findMany>>[number];
|
||||
|
||||
const mockSurveyPrisma1 = {
|
||||
id: surveyId1,
|
||||
environmentId: environmentId1,
|
||||
workspaceId: workspaceId1,
|
||||
name: "Survey 1",
|
||||
updatedAt: new Date(),
|
||||
} as unknown as PrismaSurvey;
|
||||
const mockSurveyPrisma2 = {
|
||||
id: surveyId2,
|
||||
environmentId: environmentId1,
|
||||
workspaceId: workspaceId1,
|
||||
name: "Survey 2",
|
||||
updatedAt: new Date(),
|
||||
} as unknown as PrismaSurvey;
|
||||
const mockSurveyPrisma3 = {
|
||||
id: surveyId3,
|
||||
environmentId: environmentId2,
|
||||
workspaceId: workspaceId2,
|
||||
name: "Survey 3",
|
||||
updatedAt: new Date(),
|
||||
} as unknown as PrismaSurvey;
|
||||
@@ -85,20 +85,20 @@ describe("getSurveys (Management API)", () => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test("should return surveys for a single environment ID with limit and offset", async () => {
|
||||
test("should return surveys for a single workspace ID with limit and offset", async () => {
|
||||
const limit = 1;
|
||||
const offset = 1;
|
||||
vi.mocked(prisma.survey.findMany).mockResolvedValue([mockSurveyPrisma2]);
|
||||
|
||||
const surveys = await getSurveys([environmentId1], limit, offset);
|
||||
const surveys = await getSurveys([workspaceId1], limit, offset);
|
||||
|
||||
expect(validateInputs).toHaveBeenCalledWith(
|
||||
[[environmentId1], expect.any(Object)],
|
||||
[[workspaceId1], expect.any(Object)],
|
||||
[limit, expect.any(Object)],
|
||||
[offset, expect.any(Object)]
|
||||
);
|
||||
expect(prisma.survey.findMany).toHaveBeenCalledWith({
|
||||
where: { workspaceId: { in: [environmentId1] } },
|
||||
where: { workspaceId: { in: [workspaceId1] } },
|
||||
select: selectSurvey,
|
||||
orderBy: { updatedAt: "desc" },
|
||||
take: limit,
|
||||
@@ -109,22 +109,22 @@ describe("getSurveys (Management API)", () => {
|
||||
expect(surveys).toEqual([mockSurveyTransformed2]);
|
||||
});
|
||||
|
||||
test("should return surveys for multiple environment IDs without limit and offset", async () => {
|
||||
test("should return surveys for multiple workspace IDs without limit and offset", async () => {
|
||||
vi.mocked(prisma.survey.findMany).mockResolvedValue([
|
||||
mockSurveyPrisma1,
|
||||
mockSurveyPrisma2,
|
||||
mockSurveyPrisma3,
|
||||
]);
|
||||
|
||||
const surveys = await getSurveys([environmentId1, environmentId2]);
|
||||
const surveys = await getSurveys([workspaceId1, workspaceId2]);
|
||||
|
||||
expect(validateInputs).toHaveBeenCalledWith(
|
||||
[[environmentId1, environmentId2], expect.any(Object)],
|
||||
[[workspaceId1, workspaceId2], expect.any(Object)],
|
||||
[undefined, expect.any(Object)],
|
||||
[undefined, expect.any(Object)]
|
||||
);
|
||||
expect(prisma.survey.findMany).toHaveBeenCalledWith({
|
||||
where: { workspaceId: { in: [environmentId1, environmentId2] } },
|
||||
where: { workspaceId: { in: [workspaceId1, workspaceId2] } },
|
||||
select: selectSurvey,
|
||||
orderBy: { updatedAt: "desc" },
|
||||
take: undefined,
|
||||
@@ -137,7 +137,7 @@ describe("getSurveys (Management API)", () => {
|
||||
test("should return an empty array if no surveys are found", async () => {
|
||||
vi.mocked(prisma.survey.findMany).mockResolvedValue([]);
|
||||
|
||||
const surveys = await getSurveys([environmentId1]);
|
||||
const surveys = await getSurveys([workspaceId1]);
|
||||
|
||||
expect(prisma.survey.findMany).toHaveBeenCalled();
|
||||
expect(transformPrismaSurvey).not.toHaveBeenCalled();
|
||||
@@ -151,7 +151,7 @@ describe("getSurveys (Management API)", () => {
|
||||
});
|
||||
vi.mocked(prisma.survey.findMany).mockRejectedValue(prismaError);
|
||||
|
||||
await expect(getSurveys([environmentId1])).rejects.toThrow(DatabaseError);
|
||||
await expect(getSurveys([workspaceId1])).rejects.toThrow(DatabaseError);
|
||||
expect(logger.error).toHaveBeenCalledWith(prismaError, "Error getting surveys");
|
||||
});
|
||||
|
||||
@@ -159,7 +159,7 @@ describe("getSurveys (Management API)", () => {
|
||||
const genericError = new Error("Something went wrong");
|
||||
vi.mocked(prisma.survey.findMany).mockRejectedValue(genericError);
|
||||
|
||||
await expect(getSurveys([environmentId1])).rejects.toThrow(genericError);
|
||||
await expect(getSurveys([workspaceId1])).rejects.toThrow(genericError);
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyCreateInputWithWorkspaceId,
|
||||
@@ -88,7 +89,7 @@ const mockLanguage: TSurveyCreateInputWithWorkspaceId["languages"][number] = {
|
||||
|
||||
const baseSurveyData: TSurveyCreateInputWithWorkspaceId = {
|
||||
name: "Test Survey",
|
||||
environmentId: "test-env",
|
||||
workspaceId: "mockWorkspaceId",
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
@@ -333,7 +334,7 @@ describe("checkFeaturePermissions", () => {
|
||||
elements: [
|
||||
{
|
||||
id: "cta1",
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
headline: { default: "CTA" },
|
||||
required: false,
|
||||
buttonExternal: true,
|
||||
@@ -345,7 +346,7 @@ describe("checkFeaturePermissions", () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = await checkFeaturePermissions(surveyData, mockOrganization);
|
||||
const result = await checkFeaturePermissions(surveyData as any, mockOrganization);
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result?.status).toBe(403);
|
||||
expect(responses.forbiddenResponse).toHaveBeenCalledWith(
|
||||
@@ -364,7 +365,7 @@ describe("checkFeaturePermissions", () => {
|
||||
elements: [
|
||||
{
|
||||
id: "cta1",
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
headline: { default: "CTA" },
|
||||
required: false,
|
||||
buttonExternal: true,
|
||||
@@ -384,7 +385,7 @@ describe("checkFeaturePermissions", () => {
|
||||
elements: [
|
||||
{
|
||||
id: "cta1",
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
headline: { default: "CTA" },
|
||||
required: false,
|
||||
buttonExternal: true,
|
||||
@@ -397,7 +398,7 @@ describe("checkFeaturePermissions", () => {
|
||||
],
|
||||
endings: [],
|
||||
} as unknown as TSurvey;
|
||||
const result = await checkFeaturePermissions(surveyData, mockOrganization, oldSurvey);
|
||||
const result = await checkFeaturePermissions(surveyData as any, mockOrganization, oldSurvey);
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result?.status).toBe(403);
|
||||
});
|
||||
@@ -413,7 +414,7 @@ describe("checkFeaturePermissions", () => {
|
||||
elements: [
|
||||
{
|
||||
id: "cta1",
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
headline: { default: "CTA" },
|
||||
required: false,
|
||||
buttonExternal: true,
|
||||
@@ -433,7 +434,7 @@ describe("checkFeaturePermissions", () => {
|
||||
elements: [
|
||||
{
|
||||
id: "cta1",
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
headline: { default: "CTA" },
|
||||
required: false,
|
||||
buttonExternal: true,
|
||||
@@ -446,7 +447,7 @@ describe("checkFeaturePermissions", () => {
|
||||
],
|
||||
endings: [],
|
||||
} as unknown as TSurvey;
|
||||
const result = await checkFeaturePermissions(surveyData, mockOrganization, oldSurvey);
|
||||
const result = await checkFeaturePermissions(surveyData as any, mockOrganization, oldSurvey);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
@@ -461,7 +462,7 @@ describe("checkFeaturePermissions", () => {
|
||||
elements: [
|
||||
{
|
||||
id: "cta1",
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
headline: { default: "CTA" },
|
||||
required: false,
|
||||
buttonExternal: true,
|
||||
@@ -473,7 +474,7 @@ describe("checkFeaturePermissions", () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = await checkFeaturePermissions(surveyData, mockOrganization);
|
||||
const result = await checkFeaturePermissions(surveyData as any, mockOrganization);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
@@ -488,7 +489,7 @@ describe("checkFeaturePermissions", () => {
|
||||
elements: [
|
||||
{
|
||||
id: "cta1",
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
headline: { default: "CTA" },
|
||||
required: false,
|
||||
buttonExternal: true,
|
||||
@@ -508,7 +509,7 @@ describe("checkFeaturePermissions", () => {
|
||||
elements: [
|
||||
{
|
||||
id: "cta1",
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
headline: { default: "CTA" },
|
||||
required: false,
|
||||
buttonExternal: false,
|
||||
@@ -521,7 +522,7 @@ describe("checkFeaturePermissions", () => {
|
||||
],
|
||||
endings: [],
|
||||
} as unknown as TSurvey;
|
||||
const result = await checkFeaturePermissions(surveyData, mockOrganization, oldSurvey);
|
||||
const result = await checkFeaturePermissions(surveyData as any, mockOrganization, oldSurvey);
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result?.status).toBe(403);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user