mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-06 13:51:30 -05:00
chore: Don't force Project Onboarding for each project (#6299)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
This commit is contained in:
committed by
GitHub
parent
b1f78e7bf2
commit
0aaaaa54ee
+133
-2
@@ -9,8 +9,12 @@ import {
|
||||
} from "@/lib/organization/service";
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||
import {
|
||||
getOrganizationProjectsLimit,
|
||||
getRoleManagementPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamsByOrganizationId } from "@/modules/ee/teams/team-list/lib/team";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import type { Session } from "next-auth";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
@@ -49,10 +53,14 @@ vi.mock("@/lib/membership/utils", () => ({
|
||||
}));
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getOrganizationProjectsLimit: vi.fn(),
|
||||
getRoleManagementPermission: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/ee/teams/lib/roles", () => ({
|
||||
getProjectPermissionByUserId: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/ee/teams/team-list/lib/team", () => ({
|
||||
getTeamsByOrganizationId: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
@@ -71,7 +79,13 @@ vi.mock("@/lib/constants", () => ({
|
||||
|
||||
// Mock components
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/components/MainNavigation", () => ({
|
||||
MainNavigation: () => <div data-testid="main-navigation">MainNavigation</div>,
|
||||
MainNavigation: ({ organizationTeams, canDoRoleManagement }: any) => (
|
||||
<div data-testid="main-navigation">
|
||||
MainNavigation
|
||||
<div data-testid="organization-teams">{JSON.stringify(organizationTeams || [])}</div>
|
||||
<div data-testid="can-do-role-management">{canDoRoleManagement?.toString() || "false"}</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/components/TopControlBar", () => ({
|
||||
TopControlBar: () => <div data-testid="top-control-bar">TopControlBar</div>,
|
||||
@@ -156,6 +170,17 @@ const mockProjectPermission = {
|
||||
role: "admin",
|
||||
} as any;
|
||||
|
||||
const mockOrganizationTeams = [
|
||||
{
|
||||
id: "team-1",
|
||||
name: "Development Team",
|
||||
},
|
||||
{
|
||||
id: "team-2",
|
||||
name: "Marketing Team",
|
||||
},
|
||||
];
|
||||
|
||||
const mockSession: Session = {
|
||||
user: {
|
||||
id: "user-1",
|
||||
@@ -176,6 +201,8 @@ describe("EnvironmentLayout", () => {
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(500);
|
||||
vi.mocked(getOrganizationProjectsLimit).mockResolvedValue(null as any);
|
||||
vi.mocked(getProjectPermissionByUserId).mockResolvedValue(mockProjectPermission);
|
||||
vi.mocked(getTeamsByOrganizationId).mockResolvedValue(mockOrganizationTeams);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(true);
|
||||
mockIsDevelopment = false;
|
||||
mockIsFormbricksCloud = false;
|
||||
});
|
||||
@@ -288,6 +315,110 @@ describe("EnvironmentLayout", () => {
|
||||
expect(screen.getByTestId("downgrade-banner")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("passes canDoRoleManagement props to MainNavigation", async () => {
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: false,
|
||||
isPendingDowngrade: false,
|
||||
features: { isMultiOrgEnabled: false },
|
||||
lastChecked: new Date(),
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { EnvironmentLayout } = await import(
|
||||
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
|
||||
);
|
||||
render(
|
||||
await EnvironmentLayout({
|
||||
environmentId: "env-1",
|
||||
session: mockSession,
|
||||
children: <div>Child Content</div>,
|
||||
})
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("can-do-role-management")).toHaveTextContent("true");
|
||||
expect(vi.mocked(getRoleManagementPermission)).toHaveBeenCalledWith(mockOrganization.billing.plan);
|
||||
});
|
||||
|
||||
test("handles empty organizationTeams array", async () => {
|
||||
vi.mocked(getTeamsByOrganizationId).mockResolvedValue([]);
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: false,
|
||||
isPendingDowngrade: false,
|
||||
features: { isMultiOrgEnabled: false },
|
||||
lastChecked: new Date(),
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { EnvironmentLayout } = await import(
|
||||
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
|
||||
);
|
||||
render(
|
||||
await EnvironmentLayout({
|
||||
environmentId: "env-1",
|
||||
session: mockSession,
|
||||
children: <div>Child Content</div>,
|
||||
})
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("organization-teams")).toHaveTextContent("[]");
|
||||
});
|
||||
|
||||
test("handles null organizationTeams", async () => {
|
||||
vi.mocked(getTeamsByOrganizationId).mockResolvedValue(null);
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: false,
|
||||
isPendingDowngrade: false,
|
||||
features: { isMultiOrgEnabled: false },
|
||||
lastChecked: new Date(),
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { EnvironmentLayout } = await import(
|
||||
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
|
||||
);
|
||||
render(
|
||||
await EnvironmentLayout({
|
||||
environmentId: "env-1",
|
||||
session: mockSession,
|
||||
children: <div>Child Content</div>,
|
||||
})
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("organization-teams")).toHaveTextContent("[]");
|
||||
});
|
||||
|
||||
test("handles canDoRoleManagement false", async () => {
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(false);
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: false,
|
||||
isPendingDowngrade: false,
|
||||
features: { isMultiOrgEnabled: false },
|
||||
lastChecked: new Date(),
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { EnvironmentLayout } = await import(
|
||||
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
|
||||
);
|
||||
render(
|
||||
await EnvironmentLayout({
|
||||
environmentId: "env-1",
|
||||
session: mockSession,
|
||||
children: <div>Child Content</div>,
|
||||
})
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("can-do-role-management")).toHaveTextContent("false");
|
||||
});
|
||||
|
||||
test("throws error if user not found", async () => {
|
||||
vi.mocked(getUser).mockResolvedValue(null);
|
||||
vi.resetModules();
|
||||
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/license";
|
||||
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||
import {
|
||||
getOrganizationProjectsLimit,
|
||||
getRoleManagementPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
||||
import { LimitsReachedBanner } from "@/modules/ui/components/limits-reached-banner";
|
||||
@@ -48,9 +51,10 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
|
||||
const [projects, environments] = await Promise.all([
|
||||
const [projects, environments, canDoRoleManagement] = await Promise.all([
|
||||
getUserProjects(user.id, organization.id),
|
||||
getEnvironments(environment.projectId),
|
||||
getRoleManagementPermission(organization.billing.plan),
|
||||
]);
|
||||
|
||||
if (!projects || !environments || !organizations) {
|
||||
@@ -117,6 +121,7 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
||||
membershipRole={membershipRole}
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
isLicenseActive={active}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
/>
|
||||
<div id="mainContent" className="flex-1 overflow-y-auto bg-slate-50">
|
||||
<TopControlBar
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||
import { TOrganizationTeam } from "@/modules/ee/teams/team-list/types/team";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
@@ -52,9 +53,19 @@ vi.mock("@/modules/organization/components/CreateOrganizationModal", () => ({
|
||||
open ? <div data-testid="create-org-modal">Create Org Modal</div> : null,
|
||||
}));
|
||||
vi.mock("@/modules/projects/components/project-switcher", () => ({
|
||||
ProjectSwitcher: ({ isCollapsed }: { isCollapsed: boolean }) => (
|
||||
ProjectSwitcher: ({
|
||||
isCollapsed,
|
||||
organizationTeams,
|
||||
canDoRoleManagement,
|
||||
}: {
|
||||
isCollapsed: boolean;
|
||||
organizationTeams: TOrganizationTeam[];
|
||||
canDoRoleManagement: boolean;
|
||||
}) => (
|
||||
<div data-testid="project-switcher" data-collapsed={isCollapsed}>
|
||||
Project Switcher
|
||||
<div data-testid="organization-teams-count">{organizationTeams?.length || 0}</div>
|
||||
<div data-testid="can-do-role-management">{canDoRoleManagement.toString()}</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -146,6 +157,7 @@ const defaultProps = {
|
||||
membershipRole: "owner" as const,
|
||||
organizationProjectsLimit: 5,
|
||||
isLicenseActive: true,
|
||||
canDoRoleManagement: true,
|
||||
};
|
||||
|
||||
describe("MainNavigation", () => {
|
||||
@@ -334,4 +346,23 @@ describe("MainNavigation", () => {
|
||||
});
|
||||
expect(screen.queryByText("common.license")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("passes canDoRoleManagement props to ProjectSwitcher", () => {
|
||||
render(<MainNavigation {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("organization-teams-count")).toHaveTextContent("0");
|
||||
expect(screen.getByTestId("can-do-role-management")).toHaveTextContent("true");
|
||||
});
|
||||
|
||||
test("handles no organizationTeams", () => {
|
||||
render(<MainNavigation {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("organization-teams-count")).toHaveTextContent("0");
|
||||
});
|
||||
|
||||
test("handles canDoRoleManagement false", () => {
|
||||
render(<MainNavigation {...defaultProps} canDoRoleManagement={false} />);
|
||||
|
||||
expect(screen.getByTestId("can-do-role-management")).toHaveTextContent("false");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,6 +66,7 @@ interface NavigationProps {
|
||||
membershipRole?: TOrganizationRole;
|
||||
organizationProjectsLimit: number;
|
||||
isLicenseActive: boolean;
|
||||
canDoRoleManagement: boolean;
|
||||
}
|
||||
|
||||
export const MainNavigation = ({
|
||||
@@ -80,6 +81,7 @@ export const MainNavigation = ({
|
||||
organizationProjectsLimit,
|
||||
isLicenseActive,
|
||||
isDevelopment,
|
||||
canDoRoleManagement,
|
||||
}: NavigationProps) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -323,6 +325,7 @@ export const MainNavigation = ({
|
||||
isTextVisible={isTextVisible}
|
||||
organization={organization}
|
||||
organizationProjectsLimit={organizationProjectsLimit}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
"copy_code": "Code kopieren",
|
||||
"copy_link": "Link kopieren",
|
||||
"create_new_organization": "Neue Organisation erstellen",
|
||||
"create_project": "Projekt erstellen",
|
||||
"create_segment": "Segment erstellen",
|
||||
"create_survey": "Umfrage erstellen",
|
||||
"created": "Erstellt",
|
||||
@@ -303,8 +304,10 @@
|
||||
"product_manager": "Produktmanager",
|
||||
"profile": "Profil",
|
||||
"project_configuration": "Projektkonfiguration",
|
||||
"project_creation_description": "Organisieren Sie Umfragen in Projekten für eine bessere Zugriffskontrolle.",
|
||||
"project_id": "Projekt-ID",
|
||||
"project_name": "Projektname",
|
||||
"project_name_placeholder": "z.B. Formbricks",
|
||||
"project_not_found": "Projekt nicht gefunden",
|
||||
"project_permission_not_found": "Projekt-Berechtigung nicht gefunden",
|
||||
"projects": "Projekte",
|
||||
@@ -336,6 +339,7 @@
|
||||
"select": "Auswählen",
|
||||
"select_all": "Alles auswählen",
|
||||
"select_survey": "Umfrage auswählen",
|
||||
"select_teams": "Teams auswählen",
|
||||
"selected": "Ausgewählt",
|
||||
"selected_questions": "Ausgewählte Fragen",
|
||||
"selection": "Auswahl",
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
"copy_code": "Copy code",
|
||||
"copy_link": "Copy Link",
|
||||
"create_new_organization": "Create new organization",
|
||||
"create_project": "Create project",
|
||||
"create_segment": "Create segment",
|
||||
"create_survey": "Create survey",
|
||||
"created": "Created",
|
||||
@@ -303,8 +304,10 @@
|
||||
"product_manager": "Product Manager",
|
||||
"profile": "Profile",
|
||||
"project_configuration": "Project's Configuration",
|
||||
"project_creation_description": "Organize surveys in projects for better access control.",
|
||||
"project_id": "Project ID",
|
||||
"project_name": "Project Name",
|
||||
"project_name_placeholder": "e.g. Formbricks",
|
||||
"project_not_found": "Project not found",
|
||||
"project_permission_not_found": "Project permission not found",
|
||||
"projects": "Projects",
|
||||
@@ -336,6 +339,7 @@
|
||||
"select": "Select",
|
||||
"select_all": "Select all",
|
||||
"select_survey": "Select Survey",
|
||||
"select_teams": "Select teams",
|
||||
"selected": "Selected",
|
||||
"selected_questions": "Selected questions",
|
||||
"selection": "Selection",
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
"copy_code": "Copier le code",
|
||||
"copy_link": "Copier le lien",
|
||||
"create_new_organization": "Créer une nouvelle organisation",
|
||||
"create_project": "Créer un projet",
|
||||
"create_segment": "Créer un segment",
|
||||
"create_survey": "Créer un sondage",
|
||||
"created": "Créé",
|
||||
@@ -303,8 +304,10 @@
|
||||
"product_manager": "Chef de produit",
|
||||
"profile": "Profil",
|
||||
"project_configuration": "Configuration du projet",
|
||||
"project_creation_description": "Organisez les enquêtes en projets pour un meilleur contrôle d'accès.",
|
||||
"project_id": "ID de projet",
|
||||
"project_name": "Nom du projet",
|
||||
"project_name_placeholder": "p.ex. Formbricks",
|
||||
"project_not_found": "Projet non trouvé",
|
||||
"project_permission_not_found": "Autorisation de projet non trouvée",
|
||||
"projects": "Projets",
|
||||
@@ -336,6 +339,7 @@
|
||||
"select": "Sélectionner",
|
||||
"select_all": "Sélectionner tout",
|
||||
"select_survey": "Sélectionner l'enquête",
|
||||
"select_teams": "Sélectionner les équipes",
|
||||
"selected": "Sélectionné",
|
||||
"selected_questions": "Questions sélectionnées",
|
||||
"selection": "Sélection",
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
"copy_code": "Copiar código",
|
||||
"copy_link": "Copiar Link",
|
||||
"create_new_organization": "Criar nova organização",
|
||||
"create_project": "Criar projeto",
|
||||
"create_segment": "Criar segmento",
|
||||
"create_survey": "Criar pesquisa",
|
||||
"created": "Criado",
|
||||
@@ -303,8 +304,10 @@
|
||||
"product_manager": "Gerente de Produto",
|
||||
"profile": "Perfil",
|
||||
"project_configuration": "Configuração do Projeto",
|
||||
"project_creation_description": "Organize pesquisas em projetos para melhor controle de acesso.",
|
||||
"project_id": "ID do Projeto",
|
||||
"project_name": "Nome do Projeto",
|
||||
"project_name_placeholder": "por exemplo, Formbricks",
|
||||
"project_not_found": "Projeto não encontrado",
|
||||
"project_permission_not_found": "Permissão do projeto não encontrada",
|
||||
"projects": "Projetos",
|
||||
@@ -336,6 +339,7 @@
|
||||
"select": "Selecionar",
|
||||
"select_all": "Selecionar tudo",
|
||||
"select_survey": "Selecionar Pesquisa",
|
||||
"select_teams": "Selecionar times",
|
||||
"selected": "Selecionado",
|
||||
"selected_questions": "Perguntas selecionadas",
|
||||
"selection": "seleção",
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
"copy_code": "Copiar código",
|
||||
"copy_link": "Copiar Link",
|
||||
"create_new_organization": "Criar nova organização",
|
||||
"create_project": "Criar projeto",
|
||||
"create_segment": "Criar segmento",
|
||||
"create_survey": "Criar inquérito",
|
||||
"created": "Criado",
|
||||
@@ -303,8 +304,10 @@
|
||||
"product_manager": "Gestor de Produto",
|
||||
"profile": "Perfil",
|
||||
"project_configuration": "Configuração do Projeto",
|
||||
"project_creation_description": "Organize questionários em projetos para um melhor controlo de acesso.",
|
||||
"project_id": "ID do Projeto",
|
||||
"project_name": "Nome do Projeto",
|
||||
"project_name_placeholder": "por exemplo, Formbricks",
|
||||
"project_not_found": "Projeto não encontrado",
|
||||
"project_permission_not_found": "Permissão do projeto não encontrada",
|
||||
"projects": "Projetos",
|
||||
@@ -336,6 +339,7 @@
|
||||
"select": "Selecionar",
|
||||
"select_all": "Selecionar tudo",
|
||||
"select_survey": "Selecionar Inquérito",
|
||||
"select_teams": "Selecionar equipas",
|
||||
"selected": "Selecionado",
|
||||
"selected_questions": "Perguntas selecionadas",
|
||||
"selection": "Seleção",
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
"copy_code": "複製程式碼",
|
||||
"copy_link": "複製連結",
|
||||
"create_new_organization": "建立新組織",
|
||||
"create_project": "建立專案",
|
||||
"create_segment": "建立區隔",
|
||||
"create_survey": "建立問卷",
|
||||
"created": "已建立",
|
||||
@@ -303,8 +304,10 @@
|
||||
"product_manager": "產品經理",
|
||||
"profile": "個人資料",
|
||||
"project_configuration": "專案組態",
|
||||
"project_creation_description": "組織調查 在 專案中以便更好地存取控制。",
|
||||
"project_id": "專案 ID",
|
||||
"project_name": "專案名稱",
|
||||
"project_name_placeholder": "例如 Formbricks",
|
||||
"project_not_found": "找不到專案",
|
||||
"project_permission_not_found": "找不到專案權限",
|
||||
"projects": "專案",
|
||||
@@ -336,6 +339,7 @@
|
||||
"select": "選擇",
|
||||
"select_all": "全選",
|
||||
"select_survey": "選擇問卷",
|
||||
"select_teams": "選擇 團隊",
|
||||
"selected": "已選取",
|
||||
"selected_questions": "選取的問題",
|
||||
"selection": "選取",
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { getTeamsByOrganizationIdAction } from "@/modules/projects/settings/actions";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { CreateProjectModal } from "./index";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/actions", () => ({
|
||||
createProjectAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/projects/settings/actions", () => ({
|
||||
getTeamsByOrganizationIdAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/helper", () => ({
|
||||
getFormattedErrorMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockPush = vi.fn();
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock UI components
|
||||
vi.mock("@/modules/ui/components/dialog", () => ({
|
||||
Dialog: ({ open, onOpenChange, children }: any) =>
|
||||
open ? (
|
||||
<div data-testid="dialog" onClick={() => onOpenChange(false)}>
|
||||
{children}
|
||||
</div>
|
||||
) : null,
|
||||
DialogContent: ({ children }: any) => <div data-testid="dialog-content">{children}</div>,
|
||||
DialogHeader: ({ children }: any) => <div data-testid="dialog-header">{children}</div>,
|
||||
DialogTitle: ({ children }: any) => <h2 data-testid="dialog-title">{children}</h2>,
|
||||
DialogDescription: ({ children }: any) => <p data-testid="dialog-description">{children}</p>,
|
||||
DialogBody: ({ children }: any) => <div data-testid="dialog-body">{children}</div>,
|
||||
DialogFooter: ({ children }: any) => <div data-testid="dialog-footer">{children}</div>,
|
||||
}));
|
||||
|
||||
// Create a mutable form mock that can be modified per test
|
||||
let currentFormMock: any;
|
||||
|
||||
const createFormMock = (options: { shouldCallOnSubmit?: boolean; isSubmitting?: boolean } = {}) => ({
|
||||
handleSubmit: vi.fn((onSubmit) => (e: any) => {
|
||||
e.preventDefault();
|
||||
if (options.shouldCallOnSubmit !== false) {
|
||||
onSubmit({ name: "Test Project", teamIds: [] });
|
||||
}
|
||||
}),
|
||||
formState: { isSubmitting: options.isSubmitting || false },
|
||||
reset: vi.fn(),
|
||||
watch: vi.fn(),
|
||||
getValues: vi.fn(() => ({ teamIds: [] })),
|
||||
setValue: vi.fn(),
|
||||
control: {},
|
||||
});
|
||||
|
||||
vi.mock("react-hook-form", () => ({
|
||||
useForm: () => currentFormMock,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/form", () => ({
|
||||
FormProvider: ({ children }: any) => <div data-testid="form-provider">{children}</div>,
|
||||
FormField: ({ render, name }: any) => {
|
||||
const field = {
|
||||
value: name === "name" ? "Test Project" : [],
|
||||
onChange: vi.fn(),
|
||||
};
|
||||
return render({ field, fieldState: {} });
|
||||
},
|
||||
FormItem: ({ children }: any) => <div data-testid="form-item">{children}</div>,
|
||||
FormLabel: ({ children }: any) => <label data-testid="form-label">{children}</label>,
|
||||
FormControl: ({ children }: any) => <div data-testid="form-control">{children}</div>,
|
||||
FormError: ({ children }: any) => <span data-testid="form-error">{children}</span>,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/input", () => ({
|
||||
Input: (props: any) => <input data-testid="input" {...props} />,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/multi-select", () => ({
|
||||
MultiSelect: ({ value, options, onChange, placeholder }: any) => (
|
||||
<select
|
||||
data-testid="multi-select"
|
||||
data-placeholder={placeholder}
|
||||
multiple
|
||||
value={value}
|
||||
onChange={(e) => onChange(Array.from(e.target.selectedOptions, (option) => option.value))}>
|
||||
{options.map((option: any) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: ({ children, onClick, type, loading, variant, ...props }: any) => (
|
||||
<button
|
||||
data-testid={`button-${type || "button"}`}
|
||||
data-variant={variant}
|
||||
data-loading={loading}
|
||||
onClick={onClick}
|
||||
type={type}
|
||||
{...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("CreateProjectModal", () => {
|
||||
const mockOrganizationTeams = [
|
||||
{ id: "team-1", name: "Development Team" },
|
||||
{ id: "team-2", name: "Marketing Team" },
|
||||
];
|
||||
|
||||
const mockProject = {
|
||||
id: "project-123",
|
||||
name: "Test Project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
organizationId: "org-123",
|
||||
recontactDays: 7,
|
||||
inAppSurveyBranding: true,
|
||||
linkSurveyBranding: true,
|
||||
config: { channel: "website" as const, industry: "saas" as const },
|
||||
placement: "bottomRight" as const,
|
||||
clickOutsideClose: true,
|
||||
darkOverlay: false,
|
||||
brandColor: "#000000",
|
||||
highlightBorderColor: "#000000",
|
||||
styling: { allowStyleOverwrite: true },
|
||||
logo: null,
|
||||
languages: [],
|
||||
environments: [
|
||||
{
|
||||
id: "env-123",
|
||||
type: "production" as const,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "project-123",
|
||||
appSetupCompleted: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
open: true,
|
||||
setOpen: vi.fn(),
|
||||
organizationId: "org-123",
|
||||
canDoRoleManagement: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Reset form mock to default
|
||||
currentFormMock = createFormMock();
|
||||
|
||||
// Mock successful teams fetch
|
||||
vi.mocked(getTeamsByOrganizationIdAction).mockResolvedValue({
|
||||
data: mockOrganizationTeams,
|
||||
} as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders modal when open is true", async () => {
|
||||
render(<CreateProjectModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("dialog")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dialog-title")).toHaveTextContent("common.create_project");
|
||||
expect(screen.getByTestId("dialog-description")).toHaveTextContent("common.project_creation_description");
|
||||
expect(screen.getByTestId("input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not render when open is false", () => {
|
||||
render(<CreateProjectModal {...defaultProps} open={false} />);
|
||||
|
||||
expect(screen.queryByTestId("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("fetches organization teams on mount", async () => {
|
||||
render(<CreateProjectModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getTeamsByOrganizationIdAction).toHaveBeenCalledWith({
|
||||
organizationId: "org-123",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("shows team selection when canDoRoleManagement is true and teams exist", async () => {
|
||||
render(<CreateProjectModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("multi-select")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const multiSelect = screen.getByTestId("multi-select");
|
||||
expect(multiSelect).toHaveAttribute("data-placeholder", "common.select_teams");
|
||||
|
||||
const options = screen.getAllByRole("option");
|
||||
expect(options).toHaveLength(2);
|
||||
expect(options[0]).toHaveTextContent("Development Team");
|
||||
expect(options[1]).toHaveTextContent("Marketing Team");
|
||||
});
|
||||
|
||||
test("hides team selection when canDoRoleManagement is false", async () => {
|
||||
render(<CreateProjectModal {...defaultProps} canDoRoleManagement={false} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("multi-select")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("hides team selection when no teams exist", async () => {
|
||||
vi.mocked(getTeamsByOrganizationIdAction).mockResolvedValue({
|
||||
data: [],
|
||||
} as any);
|
||||
|
||||
render(<CreateProjectModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("multi-select")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("handles teams fetch error", async () => {
|
||||
const errorMessage = "Failed to fetch teams";
|
||||
vi.mocked(getTeamsByOrganizationIdAction).mockResolvedValue({
|
||||
serverError: "Failed to fetch teams",
|
||||
} as any);
|
||||
vi.mocked(getFormattedErrorMessage).mockReturnValue(errorMessage);
|
||||
|
||||
render(<CreateProjectModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(errorMessage);
|
||||
});
|
||||
});
|
||||
|
||||
test("submits form with project name only when no teams selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(createProjectAction).mockResolvedValue({ data: mockProject } as any);
|
||||
|
||||
render(<CreateProjectModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const submitButton = screen.getByTestId("button-submit");
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createProjectAction).toHaveBeenCalledWith({
|
||||
organizationId: "org-123",
|
||||
data: {
|
||||
name: "Test Project",
|
||||
teamIds: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("submits form with selected teams", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Update form mock to return teams
|
||||
currentFormMock = createFormMock();
|
||||
currentFormMock.handleSubmit = vi.fn((onSubmit) => (e: any) => {
|
||||
e.preventDefault();
|
||||
onSubmit({ name: "Test Project", teamIds: ["team-1", "team-2"] });
|
||||
});
|
||||
|
||||
vi.mocked(createProjectAction).mockResolvedValue({ data: mockProject } as any);
|
||||
|
||||
render(<CreateProjectModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("input")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("multi-select")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const submitButton = screen.getByTestId("button-submit");
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createProjectAction).toHaveBeenCalledWith({
|
||||
organizationId: "org-123",
|
||||
data: {
|
||||
name: "Test Project",
|
||||
teamIds: ["team-1", "team-2"],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("shows success message and redirects on successful creation", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(createProjectAction).mockResolvedValue({ data: mockProject } as any);
|
||||
|
||||
render(<CreateProjectModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const submitButton = screen.getByTestId("button-submit");
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createProjectAction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith("Project created successfully");
|
||||
expect(defaultProps.setOpen).toHaveBeenCalledWith(false);
|
||||
expect(mockPush).toHaveBeenCalledWith("/environments/env-123/surveys");
|
||||
});
|
||||
});
|
||||
|
||||
test("shows error message on creation failure", async () => {
|
||||
const user = userEvent.setup();
|
||||
const errorMessage = "Project creation failed";
|
||||
|
||||
vi.mocked(createProjectAction).mockResolvedValue({
|
||||
serverError: "Creation failed",
|
||||
} as any);
|
||||
vi.mocked(getFormattedErrorMessage).mockReturnValue(errorMessage);
|
||||
|
||||
render(<CreateProjectModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const submitButton = screen.getByTestId("button-submit");
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(errorMessage);
|
||||
});
|
||||
});
|
||||
|
||||
test("closes modal and resets form when cancel button clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<CreateProjectModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const cancelButton = screen.getByTestId("button-button");
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(defaultProps.setOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test("shows loading state during form submission", async () => {
|
||||
// Mock loading state
|
||||
currentFormMock = createFormMock({ isSubmitting: true });
|
||||
|
||||
vi.mocked(createProjectAction).mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve({ data: mockProject }), 100))
|
||||
);
|
||||
|
||||
render(<CreateProjectModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Check that loading state is shown
|
||||
const submitButton = screen.getByTestId("button-submit");
|
||||
expect(submitButton).toHaveAttribute("data-loading", "true");
|
||||
});
|
||||
|
||||
test("handles form validation errors", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock form with validation error - don't call onSubmit
|
||||
currentFormMock = createFormMock({ shouldCallOnSubmit: false });
|
||||
|
||||
render(<CreateProjectModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const submitButton = screen.getByTestId("button-submit");
|
||||
await user.click(submitButton);
|
||||
|
||||
// Form should not submit if validation fails
|
||||
expect(createProjectAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("closes modal when dialog background is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<CreateProjectModal {...defaultProps} />);
|
||||
|
||||
const dialog = screen.getByTestId("dialog");
|
||||
await user.click(dialog);
|
||||
|
||||
expect(defaultProps.setOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
"use client";
|
||||
|
||||
import { TOrganizationTeam } from "@/app/(app)/(onboarding)/types/onboarding";
|
||||
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { getTeamsByOrganizationIdAction } from "@/modules/projects/settings/actions";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/modules/ui/components/dialog";
|
||||
import {
|
||||
FormControl,
|
||||
FormError,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormProvider,
|
||||
} from "@/modules/ui/components/form";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { MultiSelect } from "@/modules/ui/components/multi-select";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { z } from "zod";
|
||||
import { ZProject } from "@formbricks/types/project";
|
||||
|
||||
const ZCreateProjectForm = z.object({
|
||||
name: ZProject.shape.name,
|
||||
teamIds: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
type TCreateProjectForm = z.infer<typeof ZCreateProjectForm>;
|
||||
|
||||
interface CreateProjectModalProps {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
organizationId: string;
|
||||
canDoRoleManagement: boolean;
|
||||
}
|
||||
|
||||
export const CreateProjectModal = ({
|
||||
open,
|
||||
setOpen,
|
||||
organizationId,
|
||||
canDoRoleManagement,
|
||||
}: CreateProjectModalProps) => {
|
||||
const { t } = useTranslate();
|
||||
const router = useRouter();
|
||||
|
||||
const [organizationTeams, setOrganizationTeams] = useState<TOrganizationTeam[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOrganizationTeams = async () => {
|
||||
const response = await getTeamsByOrganizationIdAction({ organizationId });
|
||||
if (response?.data) {
|
||||
setOrganizationTeams(response.data);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(response);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
fetchOrganizationTeams();
|
||||
}, [organizationId]);
|
||||
|
||||
const form = useForm<TCreateProjectForm>({
|
||||
resolver: zodResolver(ZCreateProjectForm),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
teamIds: [],
|
||||
},
|
||||
});
|
||||
|
||||
const { isSubmitting } = form.formState;
|
||||
|
||||
const organizationTeamsOptions = organizationTeams.map((team) => ({
|
||||
label: team.name,
|
||||
value: team.id,
|
||||
}));
|
||||
|
||||
const onSubmit = async (data: TCreateProjectForm) => {
|
||||
const createProjectResponse = await createProjectAction({
|
||||
organizationId,
|
||||
data: {
|
||||
name: data.name,
|
||||
teamIds: data.teamIds || [],
|
||||
},
|
||||
});
|
||||
|
||||
if (createProjectResponse?.data) {
|
||||
// Get production environment
|
||||
const productionEnvironment = createProjectResponse.data.environments.find(
|
||||
(environment) => environment.type === "production"
|
||||
);
|
||||
|
||||
if (productionEnvironment) {
|
||||
toast.success("Project created successfully");
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
// Redirect to the new project's surveys page
|
||||
router.push(`/environments/${productionEnvironment.id}/surveys`);
|
||||
}
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createProjectResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("common.create_project")}</DialogTitle>
|
||||
<DialogDescription>{t("common.project_creation_description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<DialogBody className="relative z-20 space-y-4 overflow-visible">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("common.project_name")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder={t("common.project_name_placeholder")} autoFocus />
|
||||
</FormControl>
|
||||
{error?.message && <FormError className="text-left">{error.message}</FormError>}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{canDoRoleManagement && organizationTeams.length > 0 && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="teamIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("common.team")}</FormLabel>
|
||||
<FormControl>
|
||||
<MultiSelect
|
||||
value={field.value || []}
|
||||
options={organizationTeamsOptions}
|
||||
onChange={(teamIds) => field.onChange(teamIds)}
|
||||
placeholder={t("common.select_teams")}
|
||||
/>
|
||||
</FormControl>
|
||||
{error?.message && <FormError className="text-left">{error.message}</FormError>}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</DialogBody>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={handleClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
{t("common.create_project")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TOrganizationTeam } from "@/modules/ee/teams/team-list/types/team";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
@@ -49,6 +50,20 @@ vi.mock("@/modules/projects/components/project-limit-modal", () => ({
|
||||
) : null,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/projects/components/create-project-modal", () => ({
|
||||
CreateProjectModal: ({ open, setOpen, organizationId, organizationTeams, canDoRoleManagement }: any) =>
|
||||
open ? (
|
||||
<div data-testid="create-project-modal">
|
||||
<button onClick={() => setOpen(false)} data-testid="close-create-modal">
|
||||
Close Create Modal
|
||||
</button>
|
||||
<div data-testid="modal-organization-id">{organizationId}</div>
|
||||
<div data-testid="modal-organization-teams">{organizationTeams?.length || 0}</div>
|
||||
<div data-testid="modal-can-do-role-management">{canDoRoleManagement.toString()}</div>
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
describe("ProjectSwitcher", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
@@ -66,21 +81,34 @@ describe("ProjectSwitcher", () => {
|
||||
} as TProject;
|
||||
const projects: TProject[] = [project, { ...project, id: "proj2", name: "Project 2" }];
|
||||
|
||||
const mockOrganizationTeams: TOrganizationTeam[] = [
|
||||
{
|
||||
id: "team-1",
|
||||
name: "Development Team",
|
||||
},
|
||||
{
|
||||
id: "team-2",
|
||||
name: "Marketing Team",
|
||||
},
|
||||
];
|
||||
|
||||
const defaultProps = {
|
||||
isCollapsed: false,
|
||||
isTextVisible: false,
|
||||
organization,
|
||||
project,
|
||||
projects,
|
||||
organizationProjectsLimit: 5,
|
||||
isFormbricksCloud: false,
|
||||
isLicenseActive: false,
|
||||
environmentId: "env1",
|
||||
isOwnerOrManager: true,
|
||||
organizationTeams: mockOrganizationTeams,
|
||||
canDoRoleManagement: true,
|
||||
};
|
||||
|
||||
test("renders dropdown and project name", () => {
|
||||
render(
|
||||
<ProjectSwitcher
|
||||
isCollapsed={false}
|
||||
isTextVisible={false}
|
||||
organization={organization}
|
||||
project={project}
|
||||
projects={projects}
|
||||
organizationProjectsLimit={2}
|
||||
isFormbricksCloud={false}
|
||||
isLicenseActive={false}
|
||||
environmentId="env1"
|
||||
isOwnerOrManager={true}
|
||||
/>
|
||||
);
|
||||
render(<ProjectSwitcher {...defaultProps} />);
|
||||
expect(screen.getByTestId("dropdown-menu")).toBeInTheDocument();
|
||||
expect(screen.getByTitle("Project 1")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dropdown-trigger")).toBeInTheDocument();
|
||||
@@ -90,40 +118,14 @@ describe("ProjectSwitcher", () => {
|
||||
});
|
||||
|
||||
test("opens ProjectLimitModal when project limit reached and add project is clicked", async () => {
|
||||
render(
|
||||
<ProjectSwitcher
|
||||
isCollapsed={false}
|
||||
isTextVisible={false}
|
||||
organization={organization}
|
||||
project={project}
|
||||
projects={projects}
|
||||
organizationProjectsLimit={2}
|
||||
isFormbricksCloud={false}
|
||||
isLicenseActive={false}
|
||||
environmentId="env1"
|
||||
isOwnerOrManager={true}
|
||||
/>
|
||||
);
|
||||
render(<ProjectSwitcher {...defaultProps} organizationProjectsLimit={2} />);
|
||||
const addButton = screen.getByText("common.add_project");
|
||||
await userEvent.click(addButton);
|
||||
expect(screen.getByTestId("project-limit-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("closes ProjectLimitModal when close button is clicked", async () => {
|
||||
render(
|
||||
<ProjectSwitcher
|
||||
isCollapsed={false}
|
||||
isTextVisible={false}
|
||||
organization={organization}
|
||||
project={project}
|
||||
projects={projects}
|
||||
organizationProjectsLimit={2}
|
||||
isFormbricksCloud={false}
|
||||
isLicenseActive={false}
|
||||
environmentId="env1"
|
||||
isOwnerOrManager={true}
|
||||
/>
|
||||
);
|
||||
render(<ProjectSwitcher {...defaultProps} organizationProjectsLimit={2} />);
|
||||
const addButton = screen.getByText("common.add_project");
|
||||
await userEvent.click(addButton);
|
||||
const closeButton = screen.getByTestId("close-modal");
|
||||
@@ -132,20 +134,7 @@ describe("ProjectSwitcher", () => {
|
||||
});
|
||||
|
||||
test("renders correct modal buttons and project limit", async () => {
|
||||
render(
|
||||
<ProjectSwitcher
|
||||
isCollapsed={false}
|
||||
isTextVisible={false}
|
||||
organization={organization}
|
||||
project={project}
|
||||
projects={projects}
|
||||
organizationProjectsLimit={2}
|
||||
isFormbricksCloud={true}
|
||||
isLicenseActive={false}
|
||||
environmentId="env1"
|
||||
isOwnerOrManager={true}
|
||||
/>
|
||||
);
|
||||
render(<ProjectSwitcher {...defaultProps} organizationProjectsLimit={2} isFormbricksCloud={true} />);
|
||||
const addButton = screen.getByText("common.add_project");
|
||||
await userEvent.click(addButton);
|
||||
expect(screen.getByTestId("modal-buttons")).toHaveTextContent(
|
||||
@@ -154,24 +143,28 @@ describe("ProjectSwitcher", () => {
|
||||
expect(screen.getByTestId("modal-project-limit")).toHaveTextContent("2");
|
||||
});
|
||||
|
||||
test("handleAddProject navigates if under limit", async () => {
|
||||
render(
|
||||
<ProjectSwitcher
|
||||
isCollapsed={false}
|
||||
isTextVisible={false}
|
||||
organization={organization}
|
||||
project={project}
|
||||
projects={projects.slice(0, 1)}
|
||||
organizationProjectsLimit={2}
|
||||
isFormbricksCloud={false}
|
||||
isLicenseActive={false}
|
||||
environmentId="env1"
|
||||
isOwnerOrManager={true}
|
||||
/>
|
||||
);
|
||||
test("opens CreateProjectModal if projects exist and under limit", async () => {
|
||||
render(<ProjectSwitcher {...defaultProps} projects={[project]} organizationProjectsLimit={5} />);
|
||||
const addButton = screen.getByText("common.add_project");
|
||||
await userEvent.click(addButton);
|
||||
expect(mockPush).toHaveBeenCalled();
|
||||
expect(mockPush).toHaveBeenCalledWith("/organizations/org1/projects/new/mode");
|
||||
expect(screen.getByTestId("create-project-modal")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("modal-organization-id")).toHaveTextContent("org1");
|
||||
expect(screen.getByTestId("modal-can-do-role-management")).toHaveTextContent("true");
|
||||
});
|
||||
|
||||
test("closes CreateProjectModal when close button is clicked", async () => {
|
||||
render(<ProjectSwitcher {...defaultProps} projects={[project]} organizationProjectsLimit={5} />);
|
||||
const addButton = screen.getByText("common.add_project");
|
||||
await userEvent.click(addButton);
|
||||
const closeButton = screen.getByTestId("close-create-modal");
|
||||
await userEvent.click(closeButton);
|
||||
expect(screen.queryByTestId("create-project-modal")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("passes correct props to CreateProjectModal", async () => {
|
||||
render(<ProjectSwitcher {...defaultProps} projects={[project]} canDoRoleManagement={false} />);
|
||||
const addButton = screen.getByText("common.add_project");
|
||||
await userEvent.click(addButton);
|
||||
expect(screen.getByTestId("modal-can-do-role-management")).toHaveTextContent("false");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { cn } from "@/lib/cn";
|
||||
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
||||
import { CreateProjectModal } from "@/modules/projects/components/create-project-modal";
|
||||
import { ProjectLimitModal } from "@/modules/projects/components/project-limit-modal";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -31,6 +32,7 @@ interface ProjectSwitcherProps {
|
||||
isLicenseActive: boolean;
|
||||
environmentId: string;
|
||||
isOwnerOrManager: boolean;
|
||||
canDoRoleManagement: boolean;
|
||||
}
|
||||
|
||||
export const ProjectSwitcher = ({
|
||||
@@ -44,8 +46,10 @@ export const ProjectSwitcher = ({
|
||||
isLicenseActive,
|
||||
environmentId,
|
||||
isOwnerOrManager,
|
||||
canDoRoleManagement,
|
||||
}: ProjectSwitcherProps) => {
|
||||
const [openLimitModal, setOpenLimitModal] = useState(false);
|
||||
const [openCreateProjectModal, setOpenCreateProjectModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -55,12 +59,13 @@ export const ProjectSwitcher = ({
|
||||
router.push(`/projects/${projectId}/`);
|
||||
};
|
||||
|
||||
const handleAddProject = (organizationId: string) => {
|
||||
const handleAddProject = () => {
|
||||
if (projects.length >= organizationProjectsLimit) {
|
||||
setOpenLimitModal(true);
|
||||
return;
|
||||
}
|
||||
router.push(`/organizations/${organizationId}/projects/new/mode`);
|
||||
|
||||
setOpenCreateProjectModal(true);
|
||||
};
|
||||
|
||||
const LimitModalButtons = (): [ModalButton, ModalButton] => {
|
||||
@@ -202,9 +207,7 @@ export const ProjectSwitcher = ({
|
||||
{isOwnerOrManager && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleAddProject(organization.id)}
|
||||
icon={<PlusIcon className="mr-2 h-4 w-4" />}>
|
||||
<DropdownMenuItem onClick={handleAddProject} icon={<PlusIcon className="mr-2 h-4 w-4" />}>
|
||||
<span>{t("common.add_project")}</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
@@ -219,6 +222,14 @@ export const ProjectSwitcher = ({
|
||||
projectLimit={organizationProjectsLimit}
|
||||
/>
|
||||
)}
|
||||
{openCreateProjectModal && (
|
||||
<CreateProjectModal
|
||||
open={openCreateProjectModal}
|
||||
setOpen={setOpenCreateProjectModal}
|
||||
organizationId={organization.id}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { getProject } from "@/lib/project/service";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
@@ -79,3 +80,24 @@ export const updateProjectAction = authenticatedActionClient.schema(ZUpdateProje
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const ZGetTeamsByOrganizationIdAction = z.object({
|
||||
organizationId: ZId,
|
||||
});
|
||||
|
||||
export const getTeamsByOrganizationIdAction = authenticatedActionClient
|
||||
.schema(ZGetTeamsByOrganizationIdAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
const teams = await getTeamsByOrganizationId(parsedInput.organizationId);
|
||||
return teams;
|
||||
});
|
||||
|
||||
@@ -119,16 +119,9 @@ export const createProject = async (
|
||||
|
||||
return updatedProject;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Prisma.PrismaClientKnownRequestError &&
|
||||
error.code === PrismaErrorType.UniqueConstraintViolation
|
||||
) {
|
||||
throw new InvalidInputError("A project with this name already exists in your organization");
|
||||
}
|
||||
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (error.code === PrismaErrorType.UniqueConstraintViolation) {
|
||||
throw new InvalidInputError("A project with this name already exists in this organization");
|
||||
throw new InvalidInputError("A project with this name already exists in your organization");
|
||||
}
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user