mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-25 09:58:59 -06:00
feat: adds multiLanguageSurveys and accessControl license features (#6331)
This commit is contained in:
@@ -62,7 +62,7 @@ describe("ProjectSettings component", () => {
|
||||
industry: "ind",
|
||||
defaultBrandColor: "#fff",
|
||||
organizationTeams: [],
|
||||
canDoRoleManagement: false,
|
||||
isAccessControlAllowed: false,
|
||||
userProjectsCount: 0,
|
||||
} as any;
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ interface ProjectSettingsProps {
|
||||
industry: TProjectConfigIndustry;
|
||||
defaultBrandColor: string;
|
||||
organizationTeams: TOrganizationTeam[];
|
||||
canDoRoleManagement: boolean;
|
||||
isAccessControlAllowed: boolean;
|
||||
userProjectsCount: number;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export const ProjectSettings = ({
|
||||
industry,
|
||||
defaultBrandColor,
|
||||
organizationTeams,
|
||||
canDoRoleManagement = false,
|
||||
isAccessControlAllowed = false,
|
||||
userProjectsCount,
|
||||
}: ProjectSettingsProps) => {
|
||||
const [createTeamModalOpen, setCreateTeamModalOpen] = useState(false);
|
||||
@@ -174,7 +174,7 @@ export const ProjectSettings = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
{canDoRoleManagement && userProjectsCount > 0 && (
|
||||
{isAccessControlAllowed && userProjectsCount > 0 && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="teamIds"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
@@ -12,7 +12,7 @@ vi.mock("@/lib/constants", () => ({ DEFAULT_BRAND_COLOR: "#fff" }));
|
||||
// Mocks before component import
|
||||
vi.mock("@/app/(app)/(onboarding)/lib/onboarding", () => ({ getTeamsByOrganizationId: vi.fn() }));
|
||||
vi.mock("@/lib/project/service", () => ({ getUserProjects: vi.fn() }));
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({ getRoleManagementPermission: vi.fn() }));
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({ getAccessControlPermission: vi.fn() }));
|
||||
vi.mock("@/modules/organization/lib/utils", () => ({ getOrganizationAuth: vi.fn() }));
|
||||
vi.mock("@/tolgee/server", () => ({ getTranslate: () => Promise.resolve((key: string) => key) }));
|
||||
vi.mock("next/navigation", () => ({ redirect: vi.fn() }));
|
||||
@@ -61,7 +61,7 @@ describe("ProjectSettingsPage", () => {
|
||||
} as any);
|
||||
vi.mocked(getUserProjects).mockResolvedValueOnce([] as any);
|
||||
vi.mocked(getTeamsByOrganizationId).mockResolvedValueOnce(null as any);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValueOnce(false as any);
|
||||
vi.mocked(getAccessControlPermission).mockResolvedValueOnce(false as any);
|
||||
|
||||
await expect(Page({ params, searchParams })).rejects.toThrow("common.organization_teams_not_found");
|
||||
});
|
||||
@@ -73,7 +73,7 @@ describe("ProjectSettingsPage", () => {
|
||||
} as any);
|
||||
vi.mocked(getUserProjects).mockResolvedValueOnce([{ id: "p1" }] as any);
|
||||
vi.mocked(getTeamsByOrganizationId).mockResolvedValueOnce([{ id: "t1", name: "Team1" }] as any);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValueOnce(true as any);
|
||||
vi.mocked(getAccessControlPermission).mockResolvedValueOnce(true as any);
|
||||
|
||||
const element = await Page({ params, searchParams });
|
||||
render(element as React.ReactElement);
|
||||
@@ -96,7 +96,7 @@ describe("ProjectSettingsPage", () => {
|
||||
} as any);
|
||||
vi.mocked(getUserProjects).mockResolvedValueOnce([] as any);
|
||||
vi.mocked(getTeamsByOrganizationId).mockResolvedValueOnce([{ id: "t1", name: "Team1" }] as any);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValueOnce(true as any);
|
||||
vi.mocked(getAccessControlPermission).mockResolvedValueOnce(true as any);
|
||||
|
||||
const element = await Page({ params, searchParams });
|
||||
render(element as React.ReactElement);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboardin
|
||||
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
|
||||
import { DEFAULT_BRAND_COLOR } from "@/lib/constants";
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Header } from "@/modules/ui/components/header";
|
||||
@@ -41,7 +41,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
||||
|
||||
const organizationTeams = await getTeamsByOrganizationId(params.organizationId);
|
||||
|
||||
const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
|
||||
const isAccessControlAllowed = await getAccessControlPermission(organization.billing.plan);
|
||||
|
||||
if (!organizationTeams) {
|
||||
throw new Error(t("common.organization_teams_not_found"));
|
||||
@@ -60,7 +60,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
||||
industry={industry}
|
||||
defaultBrandColor={DEFAULT_BRAND_COLOR}
|
||||
organizationTeams={organizationTeams}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
userProjectsCount={projects.length}
|
||||
/>
|
||||
{projects.length >= 1 && (
|
||||
|
||||
@@ -8,8 +8,8 @@ import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-clie
|
||||
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import {
|
||||
getAccessControlPermission,
|
||||
getOrganizationProjectsLimit,
|
||||
getRoleManagementPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { createProject } from "@/modules/projects/settings/lib/project";
|
||||
import { z } from "zod";
|
||||
@@ -58,9 +58,9 @@ export const createProjectAction = authenticatedActionClient.schema(ZCreateProje
|
||||
}
|
||||
|
||||
if (parsedInput.data.teamIds && parsedInput.data.teamIds.length > 0) {
|
||||
const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
|
||||
const isAccessControlAllowed = await getAccessControlPermission(organization.billing.plan);
|
||||
|
||||
if (!canDoRoleManagement) {
|
||||
if (!isAccessControlAllowed) {
|
||||
throw new OperationNotAllowedError("You do not have permission to manage roles");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import {
|
||||
getAccessControlPermission,
|
||||
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";
|
||||
@@ -53,7 +53,7 @@ vi.mock("@/lib/membership/utils", () => ({
|
||||
}));
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getOrganizationProjectsLimit: vi.fn(),
|
||||
getRoleManagementPermission: vi.fn(),
|
||||
getAccessControlPermission: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/ee/teams/lib/roles", () => ({
|
||||
getProjectPermissionByUserId: vi.fn(),
|
||||
@@ -79,11 +79,11 @@ vi.mock("@/lib/constants", () => ({
|
||||
|
||||
// Mock components
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/components/MainNavigation", () => ({
|
||||
MainNavigation: ({ organizationTeams, canDoRoleManagement }: any) => (
|
||||
MainNavigation: ({ organizationTeams, isAccessControlAllowed }: 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 data-testid="is-access-control-allowed">{isAccessControlAllowed?.toString() || "false"}</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -202,7 +202,7 @@ describe("EnvironmentLayout", () => {
|
||||
vi.mocked(getOrganizationProjectsLimit).mockResolvedValue(null as any);
|
||||
vi.mocked(getProjectPermissionByUserId).mockResolvedValue(mockProjectPermission);
|
||||
vi.mocked(getTeamsByOrganizationId).mockResolvedValue(mockOrganizationTeams);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(true);
|
||||
vi.mocked(getAccessControlPermission).mockResolvedValue(true);
|
||||
mockIsDevelopment = false;
|
||||
mockIsFormbricksCloud = false;
|
||||
});
|
||||
@@ -315,7 +315,7 @@ describe("EnvironmentLayout", () => {
|
||||
expect(screen.getByTestId("downgrade-banner")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("passes canDoRoleManagement props to MainNavigation", async () => {
|
||||
test("passes isAccessControlAllowed props to MainNavigation", async () => {
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
@@ -337,8 +337,8 @@ describe("EnvironmentLayout", () => {
|
||||
})
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("can-do-role-management")).toHaveTextContent("true");
|
||||
expect(vi.mocked(getRoleManagementPermission)).toHaveBeenCalledWith(mockOrganization.billing.plan);
|
||||
expect(screen.getByTestId("is-access-control-allowed")).toHaveTextContent("true");
|
||||
expect(vi.mocked(getAccessControlPermission)).toHaveBeenCalledWith(mockOrganization.billing.plan);
|
||||
});
|
||||
|
||||
test("handles empty organizationTeams array", async () => {
|
||||
@@ -393,8 +393,8 @@ describe("EnvironmentLayout", () => {
|
||||
expect(screen.getByTestId("organization-teams")).toHaveTextContent("[]");
|
||||
});
|
||||
|
||||
test("handles canDoRoleManagement false", async () => {
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(false);
|
||||
test("handles isAccessControlAllowed false", async () => {
|
||||
vi.mocked(getAccessControlPermission).mockResolvedValue(false);
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
@@ -416,7 +416,7 @@ describe("EnvironmentLayout", () => {
|
||||
})
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("can-do-role-management")).toHaveTextContent("false");
|
||||
expect(screen.getByTestId("is-access-control-allowed")).toHaveTextContent("false");
|
||||
});
|
||||
|
||||
test("throws error if user not found", async () => {
|
||||
|
||||
@@ -14,8 +14,8 @@ import { getUserProjects } from "@/lib/project/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/license";
|
||||
import {
|
||||
getAccessControlPermission,
|
||||
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";
|
||||
@@ -51,10 +51,10 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
|
||||
const [projects, environments, canDoRoleManagement] = await Promise.all([
|
||||
const [projects, environments, isAccessControlAllowed] = await Promise.all([
|
||||
getUserProjects(user.id, organization.id),
|
||||
getEnvironments(environment.projectId),
|
||||
getRoleManagementPermission(organization.billing.plan),
|
||||
getAccessControlPermission(organization.billing.plan),
|
||||
]);
|
||||
|
||||
if (!projects || !environments || !organizations) {
|
||||
@@ -121,7 +121,7 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
||||
membershipRole={membershipRole}
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
isLicenseActive={active}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
/>
|
||||
<div id="mainContent" className="flex-1 overflow-y-auto bg-slate-50">
|
||||
<TopControlBar
|
||||
|
||||
@@ -56,16 +56,16 @@ vi.mock("@/modules/projects/components/project-switcher", () => ({
|
||||
ProjectSwitcher: ({
|
||||
isCollapsed,
|
||||
organizationTeams,
|
||||
canDoRoleManagement,
|
||||
isAccessControlAllowed,
|
||||
}: {
|
||||
isCollapsed: boolean;
|
||||
organizationTeams: TOrganizationTeam[];
|
||||
canDoRoleManagement: boolean;
|
||||
isAccessControlAllowed: 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 data-testid="is-access-control-allowed">{isAccessControlAllowed.toString()}</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -157,7 +157,7 @@ const defaultProps = {
|
||||
membershipRole: "owner" as const,
|
||||
organizationProjectsLimit: 5,
|
||||
isLicenseActive: true,
|
||||
canDoRoleManagement: true,
|
||||
isAccessControlAllowed: true,
|
||||
};
|
||||
|
||||
describe("MainNavigation", () => {
|
||||
@@ -347,11 +347,11 @@ describe("MainNavigation", () => {
|
||||
expect(screen.queryByText("common.license")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("passes canDoRoleManagement props to ProjectSwitcher", () => {
|
||||
test("passes isAccessControlAllowed props to ProjectSwitcher", () => {
|
||||
render(<MainNavigation {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("organization-teams-count")).toHaveTextContent("0");
|
||||
expect(screen.getByTestId("can-do-role-management")).toHaveTextContent("true");
|
||||
expect(screen.getByTestId("is-access-control-allowed")).toHaveTextContent("true");
|
||||
});
|
||||
|
||||
test("handles no organizationTeams", () => {
|
||||
@@ -360,9 +360,9 @@ describe("MainNavigation", () => {
|
||||
expect(screen.getByTestId("organization-teams-count")).toHaveTextContent("0");
|
||||
});
|
||||
|
||||
test("handles canDoRoleManagement false", () => {
|
||||
render(<MainNavigation {...defaultProps} canDoRoleManagement={false} />);
|
||||
test("handles isAccessControlAllowed false", () => {
|
||||
render(<MainNavigation {...defaultProps} isAccessControlAllowed={false} />);
|
||||
|
||||
expect(screen.getByTestId("can-do-role-management")).toHaveTextContent("false");
|
||||
expect(screen.getByTestId("is-access-control-allowed")).toHaveTextContent("false");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ interface NavigationProps {
|
||||
membershipRole?: TOrganizationRole;
|
||||
organizationProjectsLimit: number;
|
||||
isLicenseActive: boolean;
|
||||
canDoRoleManagement: boolean;
|
||||
isAccessControlAllowed: boolean;
|
||||
}
|
||||
|
||||
export const MainNavigation = ({
|
||||
@@ -81,7 +81,7 @@ export const MainNavigation = ({
|
||||
organizationProjectsLimit,
|
||||
isLicenseActive,
|
||||
isDevelopment,
|
||||
canDoRoleManagement,
|
||||
isAccessControlAllowed,
|
||||
}: NavigationProps) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -325,7 +325,7 @@ export const MainNavigation = ({
|
||||
isTextVisible={isTextVisible}
|
||||
organization={organization}
|
||||
organizationProjectsLimit={organizationProjectsLimit}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -102,6 +102,8 @@ describe("License Core Logic", () => {
|
||||
spamProtection: true,
|
||||
ai: false,
|
||||
auditLogs: true,
|
||||
multiLanguageSurveys: true,
|
||||
accessControl: true,
|
||||
};
|
||||
const mockFetchedLicenseDetails: TEnterpriseLicenseDetails = {
|
||||
status: "active",
|
||||
@@ -157,7 +159,6 @@ describe("License Core Logic", () => {
|
||||
active: true,
|
||||
features: mockFetchedLicenseDetails.features,
|
||||
lastChecked: expect.any(Date),
|
||||
version: 1,
|
||||
},
|
||||
expect.any(Number)
|
||||
);
|
||||
@@ -231,9 +232,10 @@ describe("License Core Logic", () => {
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
auditLogs: false,
|
||||
multiLanguageSurveys: false,
|
||||
accessControl: false,
|
||||
},
|
||||
lastChecked: expect.any(Date),
|
||||
version: 1,
|
||||
},
|
||||
expect.any(Number)
|
||||
);
|
||||
@@ -251,6 +253,8 @@ describe("License Core Logic", () => {
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
auditLogs: false,
|
||||
multiLanguageSurveys: false,
|
||||
accessControl: false,
|
||||
},
|
||||
lastChecked: expect.any(Date),
|
||||
isPendingDowngrade: false,
|
||||
@@ -278,6 +282,8 @@ describe("License Core Logic", () => {
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
auditLogs: false,
|
||||
multiLanguageSurveys: false,
|
||||
accessControl: false,
|
||||
};
|
||||
expect(mockCache.set).toHaveBeenCalledWith(
|
||||
expect.stringContaining("fb:license:"),
|
||||
@@ -285,7 +291,6 @@ describe("License Core Logic", () => {
|
||||
active: false,
|
||||
features: expectedFeatures,
|
||||
lastChecked: expect.any(Date),
|
||||
version: 1,
|
||||
},
|
||||
expect.any(Number)
|
||||
);
|
||||
|
||||
@@ -36,7 +36,6 @@ type TPreviousResult = {
|
||||
active: boolean;
|
||||
lastChecked: Date;
|
||||
features: TEnterpriseLicenseFeatures | null;
|
||||
version: number; // For cache versioning
|
||||
};
|
||||
|
||||
// Validation schemas
|
||||
@@ -52,6 +51,8 @@ const LicenseFeaturesSchema = z.object({
|
||||
saml: z.boolean(),
|
||||
spamProtection: z.boolean(),
|
||||
auditLogs: z.boolean(),
|
||||
multiLanguageSurveys: z.boolean(),
|
||||
accessControl: z.boolean(),
|
||||
});
|
||||
|
||||
const LicenseDetailsSchema = z.object({
|
||||
@@ -112,6 +113,8 @@ const DEFAULT_FEATURES: TEnterpriseLicenseFeatures = {
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
auditLogs: false,
|
||||
multiLanguageSurveys: false,
|
||||
accessControl: false,
|
||||
};
|
||||
|
||||
// Helper functions
|
||||
@@ -137,7 +140,6 @@ const getPreviousResult = async (): Promise<TPreviousResult> => {
|
||||
active: false,
|
||||
lastChecked: new Date(0),
|
||||
features: DEFAULT_FEATURES,
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,7 +160,6 @@ const getPreviousResult = async (): Promise<TPreviousResult> => {
|
||||
active: false,
|
||||
lastChecked: new Date(0),
|
||||
features: DEFAULT_FEATURES,
|
||||
version: 1,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -197,7 +198,6 @@ const trackApiError = (error: LicenseApiError) => {
|
||||
const validateFallback = (previousResult: TPreviousResult): boolean => {
|
||||
if (!previousResult.features) return false;
|
||||
if (previousResult.lastChecked.getTime() === new Date(0).getTime()) return false;
|
||||
if (previousResult.version !== 1) return false; // Add version check
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -224,7 +224,6 @@ const handleInitialFailure = async (currentTime: Date) => {
|
||||
active: false,
|
||||
features: DEFAULT_FEATURES,
|
||||
lastChecked: currentTime,
|
||||
version: 1,
|
||||
};
|
||||
await setPreviousResult(initialFailResult);
|
||||
return {
|
||||
@@ -370,7 +369,6 @@ export const getEnterpriseLicense = reactCache(
|
||||
active: liveLicenseDetails.status === "active",
|
||||
features: liveLicenseDetails.features,
|
||||
lastChecked: currentTime,
|
||||
version: 1,
|
||||
};
|
||||
await setPreviousResult(currentLicenseState);
|
||||
return {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Organization } from "@prisma/client";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import * as licenseModule from "./license";
|
||||
import {
|
||||
getAccessControlPermission,
|
||||
getBiggerUploadFileSizePermission,
|
||||
getIsContactsEnabled,
|
||||
getIsMultiOrgEnabled,
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
getMultiLanguagePermission,
|
||||
getOrganizationProjectsLimit,
|
||||
getRemoveBrandingPermission,
|
||||
getRoleManagementPermission,
|
||||
getWhiteLabelPermission,
|
||||
} from "./utils";
|
||||
|
||||
@@ -46,6 +46,8 @@ const defaultFeatures: TEnterpriseLicenseFeatures = {
|
||||
spamProtection: false,
|
||||
ai: false,
|
||||
auditLogs: false,
|
||||
multiLanguageSurveys: false,
|
||||
accessControl: false,
|
||||
};
|
||||
|
||||
const defaultLicense = {
|
||||
@@ -141,41 +143,59 @@ describe("License Utils", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRoleManagementPermission", () => {
|
||||
test("should return true if license active (self-hosted)", async () => {
|
||||
describe("getAccessControlPermission", () => {
|
||||
test("should return true if license active and accessControl feature enabled (self-hosted)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getRoleManagementPermission(mockOrganization.billing.plan);
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, accessControl: true },
|
||||
});
|
||||
const result = await getAccessControlPermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return true if license active and plan is SCALE (cloud)", async () => {
|
||||
test("should return true if license active, accessControl enabled and plan is SCALE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getRoleManagementPermission(constants.PROJECT_FEATURE_KEYS.SCALE);
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, accessControl: true },
|
||||
});
|
||||
const result = await getAccessControlPermission(constants.PROJECT_FEATURE_KEYS.SCALE);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return true if license active and plan is ENTERPRISE (cloud)", async () => {
|
||||
test("should return true if license active, accessControl enabled and plan is ENTERPRISE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getRoleManagementPermission(constants.PROJECT_FEATURE_KEYS.ENTERPRISE);
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, accessControl: true },
|
||||
});
|
||||
const result = await getAccessControlPermission(constants.PROJECT_FEATURE_KEYS.ENTERPRISE);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if license active and plan is not SCALE or ENTERPRISE (cloud)", async () => {
|
||||
test("should return false if license active, accessControl enabled but plan is not SCALE or ENTERPRISE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getRoleManagementPermission(constants.PROJECT_FEATURE_KEYS.STARTUP);
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, accessControl: true },
|
||||
});
|
||||
const result = await getAccessControlPermission(constants.PROJECT_FEATURE_KEYS.STARTUP);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("should return true if license active but accessControl feature disabled because of fallback", async () => {
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getAccessControlPermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if license is inactive", async () => {
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
active: false,
|
||||
});
|
||||
const result = await getRoleManagementPermission(mockOrganization.billing.plan);
|
||||
const result = await getAccessControlPermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -213,20 +233,52 @@ describe("License Utils", () => {
|
||||
});
|
||||
|
||||
describe("getMultiLanguagePermission", () => {
|
||||
test("should return true if license active (self-hosted)", async () => {
|
||||
test("should return true if license active and multiLanguageSurveys feature enabled (self-hosted)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, multiLanguageSurveys: true },
|
||||
});
|
||||
const result = await getMultiLanguagePermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return true if license active and plan is SCALE (cloud)", async () => {
|
||||
test("should return true if license active, multiLanguageSurveys enabled and plan is SCALE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, multiLanguageSurveys: true },
|
||||
});
|
||||
const result = await getMultiLanguagePermission(constants.PROJECT_FEATURE_KEYS.SCALE);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return true if license active, multiLanguageSurveys enabled and plan is ENTERPRISE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, multiLanguageSurveys: true },
|
||||
});
|
||||
const result = await getMultiLanguagePermission(constants.PROJECT_FEATURE_KEYS.ENTERPRISE);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if license active, multiLanguageSurveys enabled but plan is not SCALE or ENTERPRISE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, multiLanguageSurveys: true },
|
||||
});
|
||||
const result = await getMultiLanguagePermission(constants.PROJECT_FEATURE_KEYS.STARTUP);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("should return true if license active but multiLanguageSurveys feature disabled because of fallback", async () => {
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getMultiLanguagePermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if license is inactive", async () => {
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
|
||||
@@ -35,20 +35,6 @@ export const getWhiteLabelPermission = async (
|
||||
return getFeaturePermission(billingPlan, "whitelabel");
|
||||
};
|
||||
|
||||
export const getRoleManagementPermission = async (
|
||||
billingPlan: Organization["billing"]["plan"]
|
||||
): Promise<boolean> => {
|
||||
const license = await getEnterpriseLicense();
|
||||
|
||||
if (IS_FORMBRICKS_CLOUD)
|
||||
return (
|
||||
license.active &&
|
||||
(billingPlan === PROJECT_FEATURE_KEYS.SCALE || billingPlan === PROJECT_FEATURE_KEYS.ENTERPRISE)
|
||||
);
|
||||
else if (!IS_FORMBRICKS_CLOUD) return license.active;
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getBiggerUploadFileSizePermission = async (
|
||||
billingPlan: Organization["billing"]["plan"]
|
||||
): Promise<boolean> => {
|
||||
@@ -59,25 +45,16 @@ export const getBiggerUploadFileSizePermission = async (
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getMultiLanguagePermission = async (
|
||||
billingPlan: Organization["billing"]["plan"]
|
||||
): Promise<boolean> => {
|
||||
const license = await getEnterpriseLicense();
|
||||
|
||||
if (IS_FORMBRICKS_CLOUD)
|
||||
return (
|
||||
license.active &&
|
||||
(billingPlan === PROJECT_FEATURE_KEYS.SCALE || billingPlan === PROJECT_FEATURE_KEYS.ENTERPRISE)
|
||||
);
|
||||
else if (!IS_FORMBRICKS_CLOUD) return license.active;
|
||||
return false;
|
||||
};
|
||||
|
||||
// Helper function for simple boolean feature flags
|
||||
const getSpecificFeatureFlag = async (
|
||||
featureKey: keyof Pick<
|
||||
TEnterpriseLicenseFeatures,
|
||||
"isMultiOrgEnabled" | "contacts" | "twoFactorAuth" | "sso" | "auditLogs"
|
||||
| "isMultiOrgEnabled"
|
||||
| "contacts"
|
||||
| "twoFactorAuth"
|
||||
| "sso"
|
||||
| "auditLogs"
|
||||
| "multiLanguageSurveys"
|
||||
| "accessControl"
|
||||
>
|
||||
): Promise<boolean> => {
|
||||
const licenseFeatures = await getLicenseFeatures();
|
||||
@@ -133,6 +110,39 @@ export const getIsSpamProtectionEnabled = async (
|
||||
return license.active && !!license.features?.spamProtection;
|
||||
};
|
||||
|
||||
const featureFlagFallback = async (billingPlan: Organization["billing"]["plan"]): Promise<boolean> => {
|
||||
const license = await getEnterpriseLicense();
|
||||
if (IS_FORMBRICKS_CLOUD)
|
||||
return (
|
||||
license.active &&
|
||||
(billingPlan === PROJECT_FEATURE_KEYS.SCALE || billingPlan === PROJECT_FEATURE_KEYS.ENTERPRISE)
|
||||
);
|
||||
else if (!IS_FORMBRICKS_CLOUD) return license.active;
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getMultiLanguagePermission = async (
|
||||
billingPlan: Organization["billing"]["plan"]
|
||||
): Promise<boolean> => {
|
||||
const isEnabled = await getSpecificFeatureFlag("multiLanguageSurveys");
|
||||
// If the feature is enabled in the license, return true
|
||||
if (isEnabled) return true;
|
||||
|
||||
// If the feature is not enabled in the license, check the fallback(Backwards compatibility)
|
||||
return featureFlagFallback(billingPlan);
|
||||
};
|
||||
|
||||
export const getAccessControlPermission = async (
|
||||
billingPlan: Organization["billing"]["plan"]
|
||||
): Promise<boolean> => {
|
||||
const isEnabled = await getSpecificFeatureFlag("accessControl");
|
||||
// If the feature is enabled in the license, return true
|
||||
if (isEnabled) return true;
|
||||
|
||||
// If the feature is not enabled in the license, check the fallback(Backwards compatibility)
|
||||
return featureFlagFallback(billingPlan);
|
||||
};
|
||||
|
||||
export const getOrganizationProjectsLimit = async (
|
||||
limits: Organization["billing"]["limits"]
|
||||
): Promise<number> => {
|
||||
|
||||
@@ -16,6 +16,8 @@ const ZEnterpriseLicenseFeatures = z.object({
|
||||
spamProtection: z.boolean(),
|
||||
ai: z.boolean(),
|
||||
auditLogs: z.boolean(),
|
||||
multiLanguageSurveys: z.boolean(),
|
||||
accessControl: z.boolean(),
|
||||
});
|
||||
|
||||
export type TEnterpriseLicenseFeatures = z.infer<typeof ZEnterpriseLicenseFeatures>;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { updateInvite } from "@/modules/ee/role-management/lib/invite";
|
||||
import { updateMembership } from "@/modules/ee/role-management/lib/membership";
|
||||
import { ZInviteUpdateInput } from "@/modules/ee/role-management/types/invites";
|
||||
@@ -24,8 +24,8 @@ export const checkRoleManagementPermission = async (organizationId: string) => {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const isRoleManagementAllowed = await getRoleManagementPermission(organization.billing.plan);
|
||||
if (!isRoleManagementAllowed) {
|
||||
const isAccessControlAllowed = await getAccessControlPermission(organization.billing.plan);
|
||||
if (!isAccessControlAllowed) {
|
||||
throw new OperationNotAllowedError("Role management is not allowed for this organization");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,14 +18,14 @@ import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
|
||||
interface AddMemberRoleProps {
|
||||
control: Control<{ name: string; email: string; role: TOrganizationRole; teamIds: string[] }>;
|
||||
canDoRoleManagement: boolean;
|
||||
isAccessControlAllowed: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
membershipRole?: TOrganizationRole;
|
||||
}
|
||||
|
||||
export function AddMemberRole({
|
||||
control,
|
||||
canDoRoleManagement,
|
||||
isAccessControlAllowed,
|
||||
isFormbricksCloud,
|
||||
membershipRole,
|
||||
}: AddMemberRoleProps) {
|
||||
@@ -62,8 +62,8 @@ export function AddMemberRole({
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Label>{t("common.role_organization")}</Label>
|
||||
<Select
|
||||
defaultValue={canDoRoleManagement ? "member" : "owner"}
|
||||
disabled={!canDoRoleManagement}
|
||||
defaultValue={isAccessControlAllowed ? "member" : "owner"}
|
||||
disabled={!isAccessControlAllowed}
|
||||
onValueChange={(v) => {
|
||||
onChange(v as TOrganizationRole);
|
||||
}}
|
||||
|
||||
@@ -11,14 +11,20 @@ vi.mock("@tolgee/react", () => ({
|
||||
}));
|
||||
|
||||
// Create a wrapper component that provides the form context
|
||||
const FormWrapper = ({ children, defaultValues, membershipRole, canDoRoleManagement, isFormbricksCloud }) => {
|
||||
const FormWrapper = ({
|
||||
children,
|
||||
defaultValues,
|
||||
membershipRole,
|
||||
isAccessControlAllowed,
|
||||
isFormbricksCloud,
|
||||
}) => {
|
||||
const methods = useForm({ defaultValues });
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<AddMemberRole
|
||||
control={methods.control}
|
||||
membershipRole={membershipRole}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
/>
|
||||
{children}
|
||||
@@ -44,7 +50,7 @@ describe("AddMemberRole Component", () => {
|
||||
<FormWrapper
|
||||
defaultValues={defaultValues}
|
||||
membershipRole="owner"
|
||||
canDoRoleManagement={true}
|
||||
isAccessControlAllowed={true}
|
||||
isFormbricksCloud={true}>
|
||||
<div />
|
||||
</FormWrapper>
|
||||
@@ -59,7 +65,7 @@ describe("AddMemberRole Component", () => {
|
||||
<FormWrapper
|
||||
defaultValues={defaultValues}
|
||||
membershipRole="member"
|
||||
canDoRoleManagement={true}
|
||||
isAccessControlAllowed={true}
|
||||
isFormbricksCloud={true}>
|
||||
<div data-testid="child" />
|
||||
</FormWrapper>
|
||||
@@ -69,12 +75,12 @@ describe("AddMemberRole Component", () => {
|
||||
expect(screen.getByTestId("child")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("disables the role selector when canDoRoleManagement is false", () => {
|
||||
test("disables the role selector when isAccessControlAllowed is false", () => {
|
||||
render(
|
||||
<FormWrapper
|
||||
defaultValues={defaultValues}
|
||||
membershipRole="owner"
|
||||
canDoRoleManagement={false}
|
||||
isAccessControlAllowed={false}
|
||||
isFormbricksCloud={true}>
|
||||
<div />
|
||||
</FormWrapper>
|
||||
@@ -91,7 +97,7 @@ describe("AddMemberRole Component", () => {
|
||||
<FormWrapper
|
||||
defaultValues={defaultValues}
|
||||
membershipRole="owner"
|
||||
canDoRoleManagement={true}
|
||||
isAccessControlAllowed={true}
|
||||
isFormbricksCloud={true}>
|
||||
<div />
|
||||
</FormWrapper>
|
||||
|
||||
@@ -10,10 +10,10 @@ import { createUser, getUserByEmail, updateUser } from "@/modules/auth/lib/user"
|
||||
import { getIsValidInviteToken } from "@/modules/auth/signup/lib/invite";
|
||||
import { TOidcNameFields, TSamlNameFields } from "@/modules/auth/types/auth";
|
||||
import {
|
||||
getAccessControlPermission,
|
||||
getIsMultiOrgEnabled,
|
||||
getIsSamlSsoEnabled,
|
||||
getIsSsoEnabled,
|
||||
getRoleManagementPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { getFirstOrganization } from "@/modules/ee/sso/lib/organization";
|
||||
import { createDefaultTeamMembership, getOrganizationByTeamId } from "@/modules/ee/sso/lib/team";
|
||||
@@ -305,13 +305,13 @@ export const handleSsoCallback = async ({
|
||||
return false;
|
||||
}
|
||||
|
||||
const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
|
||||
if (!canDoRoleManagement && !callbackUrl) {
|
||||
const isAccessControlAllowed = await getAccessControlPermission(organization.billing.plan);
|
||||
if (!isAccessControlAllowed && !callbackUrl) {
|
||||
contextLogger.debug(
|
||||
{
|
||||
reason: "insufficient_role_permissions",
|
||||
organizationId: organization.id,
|
||||
canDoRoleManagement,
|
||||
isAccessControlAllowed,
|
||||
},
|
||||
"SSO callback rejected: insufficient role management permissions"
|
||||
);
|
||||
|
||||
@@ -5,10 +5,10 @@ import { createBrevoCustomer } from "@/modules/auth/lib/brevo";
|
||||
import { createUser, getUserByEmail, updateUser } from "@/modules/auth/lib/user";
|
||||
import type { TSamlNameFields } from "@/modules/auth/types/auth";
|
||||
import {
|
||||
getAccessControlPermission,
|
||||
getIsMultiOrgEnabled,
|
||||
getIsSamlSsoEnabled,
|
||||
getIsSsoEnabled,
|
||||
getRoleManagementPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { createDefaultTeamMembership, getOrganizationByTeamId } from "@/modules/ee/sso/lib/team";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
@@ -43,7 +43,7 @@ vi.mock("@/modules/auth/signup/lib/invite", () => ({
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getIsSamlSsoEnabled: vi.fn(),
|
||||
getIsSsoEnabled: vi.fn(),
|
||||
getRoleManagementPermission: vi.fn(),
|
||||
getAccessControlPermission: vi.fn(),
|
||||
getIsMultiOrgEnabled: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -310,7 +310,7 @@ describe("handleSsoCallback", () => {
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(getRoleManagementPermission).not.toHaveBeenCalled();
|
||||
expect(getAccessControlPermission).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should return true when organization exists but role management is not enabled", async () => {
|
||||
@@ -318,7 +318,7 @@ describe("handleSsoCallback", () => {
|
||||
vi.mocked(getUserByEmail).mockResolvedValue(null);
|
||||
vi.mocked(createUser).mockResolvedValue(mockCreatedUser());
|
||||
vi.mocked(getOrganizationByTeamId).mockResolvedValue(mockOrganization);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(false);
|
||||
vi.mocked(getAccessControlPermission).mockResolvedValue(false);
|
||||
|
||||
const result = await handleSsoCallback({
|
||||
user: mockUser,
|
||||
|
||||
@@ -12,7 +12,7 @@ interface TeamsViewProps {
|
||||
organizationId: string;
|
||||
membershipRole?: TOrganizationRole;
|
||||
currentUserId: string;
|
||||
canDoRoleManagement: boolean;
|
||||
isAccessControlAllowed: boolean;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const TeamsView = async ({
|
||||
organizationId,
|
||||
membershipRole,
|
||||
currentUserId,
|
||||
canDoRoleManagement,
|
||||
isAccessControlAllowed,
|
||||
environmentId,
|
||||
}: TeamsViewProps) => {
|
||||
const t = await getTranslate();
|
||||
@@ -52,7 +52,7 @@ export const TeamsView = async ({
|
||||
<SettingsCard
|
||||
title={t("environments.settings.teams.teams")}
|
||||
description={t("environments.settings.teams.teams_description")}>
|
||||
{canDoRoleManagement ? (
|
||||
{isAccessControlAllowed ? (
|
||||
<TeamsTable
|
||||
teams={teams}
|
||||
membershipRole={membershipRole}
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("EditMemberships", () => {
|
||||
organization: mockOrg,
|
||||
currentUserId: "user-1",
|
||||
role: "owner",
|
||||
canDoRoleManagement: true,
|
||||
isAccessControlAllowed: true,
|
||||
isUserManagementDisabledFromUi: false,
|
||||
});
|
||||
render(ui);
|
||||
@@ -81,18 +81,18 @@ describe("EditMemberships", () => {
|
||||
expect(props.organization.id).toBe("org-1");
|
||||
expect(props.currentUserId).toBe("user-1");
|
||||
expect(props.currentUserRole).toBe("owner");
|
||||
expect(props.canDoRoleManagement).toBe(true);
|
||||
expect(props.isAccessControlAllowed).toBe(true);
|
||||
expect(props.isUserManagementDisabledFromUi).toBe(false);
|
||||
expect(Array.isArray(props.invites)).toBe(true);
|
||||
expect(Array.isArray(props.members)).toBe(true);
|
||||
});
|
||||
|
||||
test("does not render role/actions columns if canDoRoleManagement or isUserManagementDisabledFromUi is false", async () => {
|
||||
test("does not render role/actions columns if isAccessControlAllowed or isUserManagementDisabledFromUi is false", async () => {
|
||||
const ui = await EditMemberships({
|
||||
organization: mockOrg,
|
||||
currentUserId: "user-1",
|
||||
role: "member",
|
||||
canDoRoleManagement: false,
|
||||
isAccessControlAllowed: false,
|
||||
isUserManagementDisabledFromUi: true,
|
||||
});
|
||||
render(ui);
|
||||
@@ -109,7 +109,7 @@ describe("EditMemberships", () => {
|
||||
organization: mockOrg,
|
||||
currentUserId: "user-1",
|
||||
role: undefined as any,
|
||||
canDoRoleManagement: true,
|
||||
isAccessControlAllowed: true,
|
||||
isUserManagementDisabledFromUi: false,
|
||||
});
|
||||
render(ui);
|
||||
|
||||
@@ -10,7 +10,7 @@ interface EditMembershipsProps {
|
||||
organization: TOrganization;
|
||||
currentUserId: string;
|
||||
role: TOrganizationRole;
|
||||
canDoRoleManagement: boolean;
|
||||
isAccessControlAllowed: boolean;
|
||||
isUserManagementDisabledFromUi: boolean;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export const EditMemberships = async ({
|
||||
organization,
|
||||
currentUserId,
|
||||
role,
|
||||
canDoRoleManagement,
|
||||
isAccessControlAllowed,
|
||||
isUserManagementDisabledFromUi,
|
||||
}: EditMembershipsProps) => {
|
||||
const members = await getMembershipByOrganizationId(organization.id);
|
||||
@@ -32,7 +32,9 @@ export const EditMemberships = async ({
|
||||
<div className="w-1/2 overflow-hidden">{t("common.full_name")}</div>
|
||||
<div className="w-1/2 overflow-hidden">{t("common.email")}</div>
|
||||
|
||||
{canDoRoleManagement && <div className="min-w-[100px] whitespace-nowrap">{t("common.role")}</div>}
|
||||
{isAccessControlAllowed && (
|
||||
<div className="min-w-[100px] whitespace-nowrap">{t("common.role")}</div>
|
||||
)}
|
||||
|
||||
<div className="min-w-[80px] whitespace-nowrap">{t("common.status")}</div>
|
||||
|
||||
@@ -48,7 +50,7 @@ export const EditMemberships = async ({
|
||||
invites={invites ?? []}
|
||||
members={members ?? []}
|
||||
currentUserRole={role}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
isUserManagementDisabledFromUi={isUserManagementDisabledFromUi}
|
||||
/>
|
||||
|
||||
@@ -77,7 +77,7 @@ describe("MembersInfo", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders member info and EditMembershipRole when canDoRoleManagement", () => {
|
||||
test("renders member info and EditMembershipRole when isAccessControlAllowed", () => {
|
||||
render(
|
||||
<MembersInfo
|
||||
organization={org}
|
||||
@@ -85,7 +85,7 @@ describe("MembersInfo", () => {
|
||||
invites={[]}
|
||||
currentUserRole="owner"
|
||||
currentUserId="user-1"
|
||||
canDoRoleManagement={true}
|
||||
isAccessControlAllowed={true}
|
||||
isFormbricksCloud={true}
|
||||
isUserManagementDisabledFromUi={false}
|
||||
/>
|
||||
@@ -105,7 +105,7 @@ describe("MembersInfo", () => {
|
||||
invites={[]}
|
||||
currentUserRole="owner"
|
||||
currentUserId="user-1"
|
||||
canDoRoleManagement={true}
|
||||
isAccessControlAllowed={true}
|
||||
isFormbricksCloud={true}
|
||||
isUserManagementDisabledFromUi={false}
|
||||
/>
|
||||
@@ -121,7 +121,7 @@ describe("MembersInfo", () => {
|
||||
invites={[invite]}
|
||||
currentUserRole="owner"
|
||||
currentUserId="user-1"
|
||||
canDoRoleManagement={true}
|
||||
isAccessControlAllowed={true}
|
||||
isFormbricksCloud={true}
|
||||
isUserManagementDisabledFromUi={false}
|
||||
/>
|
||||
@@ -139,7 +139,7 @@ describe("MembersInfo", () => {
|
||||
invites={[invite]}
|
||||
currentUserRole="owner"
|
||||
currentUserId="user-1"
|
||||
canDoRoleManagement={true}
|
||||
isAccessControlAllowed={true}
|
||||
isFormbricksCloud={true}
|
||||
isUserManagementDisabledFromUi={false}
|
||||
/>
|
||||
@@ -147,7 +147,7 @@ describe("MembersInfo", () => {
|
||||
expect(screen.getByTestId("expired-badge")).toHaveTextContent("Expired");
|
||||
});
|
||||
|
||||
test("does not render EditMembershipRole if canDoRoleManagement is false", () => {
|
||||
test("does not render EditMembershipRole if isAccessControlAllowed is false", () => {
|
||||
render(
|
||||
<MembersInfo
|
||||
organization={org}
|
||||
@@ -155,7 +155,7 @@ describe("MembersInfo", () => {
|
||||
invites={[]}
|
||||
currentUserRole="owner"
|
||||
currentUserId="user-1"
|
||||
canDoRoleManagement={false}
|
||||
isAccessControlAllowed={false}
|
||||
isFormbricksCloud={true}
|
||||
isUserManagementDisabledFromUi={false}
|
||||
/>
|
||||
@@ -171,7 +171,7 @@ describe("MembersInfo", () => {
|
||||
invites={[]}
|
||||
currentUserRole="owner"
|
||||
currentUserId="user-1"
|
||||
canDoRoleManagement={true}
|
||||
isAccessControlAllowed={true}
|
||||
isFormbricksCloud={true}
|
||||
isUserManagementDisabledFromUi={true}
|
||||
/>
|
||||
@@ -193,7 +193,7 @@ describe("MembersInfo", () => {
|
||||
invites={[invite]}
|
||||
currentUserRole="owner"
|
||||
currentUserId="user-1"
|
||||
canDoRoleManagement={true}
|
||||
isAccessControlAllowed={true}
|
||||
isFormbricksCloud={true}
|
||||
isUserManagementDisabledFromUi={false}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,7 @@ interface MembersInfoProps {
|
||||
invites: TInvite[];
|
||||
currentUserRole: TOrganizationRole;
|
||||
currentUserId: string;
|
||||
canDoRoleManagement: boolean;
|
||||
isAccessControlAllowed: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
isUserManagementDisabledFromUi: boolean;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export const MembersInfo = ({
|
||||
currentUserRole,
|
||||
members,
|
||||
currentUserId,
|
||||
canDoRoleManagement,
|
||||
isAccessControlAllowed,
|
||||
isFormbricksCloud,
|
||||
isUserManagementDisabledFromUi,
|
||||
}: MembersInfoProps) => {
|
||||
@@ -105,7 +105,7 @@ export const MembersInfo = ({
|
||||
<p className="w-full truncate"> {member.email}</p>
|
||||
</div>
|
||||
|
||||
{canDoRoleManagement && allMembers?.length > 0 && (
|
||||
{isAccessControlAllowed && allMembers?.length > 0 && (
|
||||
<div className="ph-no-capture min-w-[100px]">
|
||||
<EditMembershipRole
|
||||
currentUserRole={currentUserRole}
|
||||
|
||||
@@ -123,7 +123,7 @@ describe("OrganizationActions Component", () => {
|
||||
organization: { id: "org-123", name: "Test Org" } as TOrganization,
|
||||
teams: [{ id: "team-1", name: "Team 1" }],
|
||||
isInviteDisabled: false,
|
||||
canDoRoleManagement: true,
|
||||
isAccessControlAllowed: true,
|
||||
isFormbricksCloud: false,
|
||||
environmentId: "env-123",
|
||||
isMultiOrgEnabled: true,
|
||||
@@ -310,7 +310,7 @@ describe("OrganizationActions Component", () => {
|
||||
expect
|
||||
.objectContaining({
|
||||
environmentId: "env-123",
|
||||
canDoRoleManagement: true,
|
||||
isAccessControlAllowed: true,
|
||||
isFormbricksCloud: false,
|
||||
teams: expect.arrayContaining(defaultProps.teams),
|
||||
membershipRole: "owner",
|
||||
|
||||
@@ -31,7 +31,7 @@ interface OrganizationActionsProps {
|
||||
organization: TOrganization;
|
||||
teams: TOrganizationTeam[];
|
||||
isInviteDisabled: boolean;
|
||||
canDoRoleManagement: boolean;
|
||||
isAccessControlAllowed: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
environmentId: string;
|
||||
isMultiOrgEnabled: boolean;
|
||||
@@ -45,7 +45,7 @@ export const OrganizationActions = ({
|
||||
teams,
|
||||
isLeaveOrganizationDisabled,
|
||||
isInviteDisabled,
|
||||
canDoRoleManagement,
|
||||
isAccessControlAllowed,
|
||||
isFormbricksCloud,
|
||||
environmentId,
|
||||
isMultiOrgEnabled,
|
||||
@@ -154,7 +154,7 @@ export const OrganizationActions = ({
|
||||
setOpen={setInviteMemberModalOpen}
|
||||
onSubmit={handleAddMembers}
|
||||
membershipRole={membershipRole}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
environmentId={environmentId}
|
||||
teams={teams}
|
||||
|
||||
@@ -15,14 +15,14 @@ import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
interface BulkInviteTabProps {
|
||||
setOpen: (v: boolean) => void;
|
||||
onSubmit: (data: { name: string; email: string; role: TOrganizationRole }[]) => void;
|
||||
canDoRoleManagement: boolean;
|
||||
isAccessControlAllowed: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
}
|
||||
|
||||
export const BulkInviteTab = ({
|
||||
setOpen,
|
||||
onSubmit,
|
||||
canDoRoleManagement,
|
||||
isAccessControlAllowed,
|
||||
isFormbricksCloud,
|
||||
}: BulkInviteTabProps) => {
|
||||
const { t } = useTranslate();
|
||||
@@ -48,7 +48,7 @@ export const BulkInviteTab = ({
|
||||
},
|
||||
complete: (results: ParseResult<{ name: string; email: string; role: string }>) => {
|
||||
const members = results.data.map((csv) => {
|
||||
let orgRole = canDoRoleManagement ? csv.role.trim().toLowerCase() : "owner";
|
||||
let orgRole = isAccessControlAllowed ? csv.role.trim().toLowerCase() : "owner";
|
||||
if (!isFormbricksCloud) {
|
||||
orgRole = orgRole === "billing" ? "owner" : orgRole;
|
||||
}
|
||||
@@ -119,7 +119,7 @@ export const BulkInviteTab = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!canDoRoleManagement && (
|
||||
{!isAccessControlAllowed && (
|
||||
<Alert variant="default" className="mt-1.5 flex items-start bg-slate-50">
|
||||
<AlertDescription className="ml-2">
|
||||
<p className="text-sm">
|
||||
|
||||
@@ -35,7 +35,7 @@ const defaultProps = {
|
||||
{ id: "team-1", name: "Team 1" },
|
||||
{ id: "team-2", name: "Team 2" },
|
||||
],
|
||||
canDoRoleManagement: true,
|
||||
isAccessControlAllowed: true,
|
||||
isFormbricksCloud: true,
|
||||
environmentId: "env-1",
|
||||
membershipRole: "owner" as TOrganizationRole,
|
||||
@@ -85,21 +85,21 @@ describe("IndividualInviteTab", () => {
|
||||
});
|
||||
|
||||
test("shows member role info alert when role is member", async () => {
|
||||
render(<IndividualInviteTab {...defaultProps} canDoRoleManagement={true} />);
|
||||
render(<IndividualInviteTab {...defaultProps} isAccessControlAllowed={true} />);
|
||||
await userEvent.type(screen.getByLabelText("common.full_name"), "Test User");
|
||||
await userEvent.type(screen.getByLabelText("common.email"), "test@example.com");
|
||||
// Simulate selecting member role
|
||||
// Not needed as default is member if canDoRoleManagement is true
|
||||
// Not needed as default is member if isAccessControlAllowed is true
|
||||
expect(screen.getByText("environments.settings.teams.member_role_info_message")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows team select when canDoRoleManagement is true", () => {
|
||||
render(<IndividualInviteTab {...defaultProps} canDoRoleManagement={true} />);
|
||||
test("shows team select when isAccessControlAllowed is true", () => {
|
||||
render(<IndividualInviteTab {...defaultProps} isAccessControlAllowed={true} />);
|
||||
expect(screen.getByTestId("multi-select")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows upgrade alert when canDoRoleManagement is false", () => {
|
||||
render(<IndividualInviteTab {...defaultProps} canDoRoleManagement={false} />);
|
||||
test("shows upgrade alert when isAccessControlAllowed is false", () => {
|
||||
render(<IndividualInviteTab {...defaultProps} isAccessControlAllowed={false} />);
|
||||
expect(screen.getByText("environments.settings.teams.upgrade_plan_notice_message")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.start_free_trial")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ interface IndividualInviteTabProps {
|
||||
setOpen: (v: boolean) => void;
|
||||
onSubmit: (data: { name: string; email: string; role: TOrganizationRole }[]) => void;
|
||||
teams: TOrganizationTeam[];
|
||||
canDoRoleManagement: boolean;
|
||||
isAccessControlAllowed: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
environmentId: string;
|
||||
membershipRole?: TOrganizationRole;
|
||||
@@ -33,7 +33,7 @@ export const IndividualInviteTab = ({
|
||||
setOpen,
|
||||
onSubmit,
|
||||
teams,
|
||||
canDoRoleManagement,
|
||||
isAccessControlAllowed,
|
||||
isFormbricksCloud,
|
||||
environmentId,
|
||||
membershipRole,
|
||||
@@ -52,7 +52,7 @@ export const IndividualInviteTab = ({
|
||||
const form = useForm<TFormData>({
|
||||
resolver: zodResolver(ZFormSchema),
|
||||
defaultValues: {
|
||||
role: canDoRoleManagement ? "member" : "owner",
|
||||
role: isAccessControlAllowed ? "member" : "owner",
|
||||
teamIds: [],
|
||||
},
|
||||
});
|
||||
@@ -106,7 +106,7 @@ export const IndividualInviteTab = ({
|
||||
<div>
|
||||
<AddMemberRole
|
||||
control={control}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
membershipRole={membershipRole}
|
||||
/>
|
||||
@@ -117,7 +117,7 @@ export const IndividualInviteTab = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{canDoRoleManagement && (
|
||||
{isAccessControlAllowed && (
|
||||
<FormField
|
||||
control={control}
|
||||
name="teamIds"
|
||||
@@ -143,7 +143,7 @@ export const IndividualInviteTab = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{!canDoRoleManagement && (
|
||||
{!isAccessControlAllowed && (
|
||||
<Alert>
|
||||
<AlertDescription className="flex">
|
||||
{t("environments.settings.teams.upgrade_plan_notice_message")}
|
||||
|
||||
@@ -55,7 +55,7 @@ const defaultProps = {
|
||||
setOpen: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
teams: [],
|
||||
canDoRoleManagement: true,
|
||||
isAccessControlAllowed: true,
|
||||
isFormbricksCloud: true,
|
||||
environmentId: "env-1",
|
||||
membershipRole: "owner" as TOrganizationRole,
|
||||
|
||||
@@ -21,7 +21,7 @@ interface InviteMemberModalProps {
|
||||
setOpen: (v: boolean) => void;
|
||||
onSubmit: (data: { name: string; email: string; role: TOrganizationRole }[]) => void;
|
||||
teams: TOrganizationTeam[];
|
||||
canDoRoleManagement: boolean;
|
||||
isAccessControlAllowed: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
environmentId: string;
|
||||
membershipRole?: TOrganizationRole;
|
||||
@@ -32,7 +32,7 @@ export const InviteMemberModal = ({
|
||||
setOpen,
|
||||
onSubmit,
|
||||
teams,
|
||||
canDoRoleManagement,
|
||||
isAccessControlAllowed,
|
||||
isFormbricksCloud,
|
||||
environmentId,
|
||||
membershipRole,
|
||||
@@ -47,7 +47,7 @@ export const InviteMemberModal = ({
|
||||
setOpen={setOpen}
|
||||
environmentId={environmentId}
|
||||
onSubmit={onSubmit}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
teams={teams}
|
||||
membershipRole={membershipRole}
|
||||
@@ -57,7 +57,7 @@ export const InviteMemberModal = ({
|
||||
<BulkInviteTab
|
||||
setOpen={setOpen}
|
||||
onSubmit={onSubmit}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("MembersView", () => {
|
||||
organization: { id: "org-1", name: "Test Org" },
|
||||
currentUserId: "user-1",
|
||||
environmentId: "env-1",
|
||||
canDoRoleManagement: true,
|
||||
isAccessControlAllowed: true,
|
||||
isUserManagementDisabledFromUi: false,
|
||||
} as any;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ interface MembersViewProps {
|
||||
organization: TOrganization;
|
||||
currentUserId: string;
|
||||
environmentId: string;
|
||||
canDoRoleManagement: boolean;
|
||||
isAccessControlAllowed: boolean;
|
||||
isUserManagementDisabledFromUi: boolean;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export const MembersView = async ({
|
||||
organization,
|
||||
currentUserId,
|
||||
environmentId,
|
||||
canDoRoleManagement,
|
||||
isAccessControlAllowed,
|
||||
isUserManagementDisabledFromUi,
|
||||
}: MembersViewProps) => {
|
||||
const t = await getTranslate();
|
||||
@@ -47,7 +47,7 @@ export const MembersView = async ({
|
||||
|
||||
let teams: TOrganizationTeam[] = [];
|
||||
|
||||
if (canDoRoleManagement) {
|
||||
if (isAccessControlAllowed) {
|
||||
teams = (await getTeamsByOrganizationId(organization.id)) ?? [];
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export const MembersView = async ({
|
||||
role={membershipRole}
|
||||
isLeaveOrganizationDisabled={isLeaveOrganizationDisabled}
|
||||
isInviteDisabled={INVITE_DISABLED}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
environmentId={environmentId}
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
@@ -74,7 +74,7 @@ export const MembersView = async ({
|
||||
{membershipRole && (
|
||||
<Suspense fallback={<MembersLoading />}>
|
||||
<EditMemberships
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
organization={organization}
|
||||
currentUserId={currentUserId}
|
||||
role={membershipRole}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
@@ -20,7 +20,7 @@ vi.mock("@/lib/constants", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getRoleManagementPermission: vi.fn(),
|
||||
getAccessControlPermission: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/teams/team-list/components/teams-view", () => ({
|
||||
@@ -68,7 +68,7 @@ describe("TeamsPage", () => {
|
||||
currentUserMembership: mockMembership,
|
||||
organization: mockOrg,
|
||||
} as any);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(true);
|
||||
vi.mocked(getAccessControlPermission).mockResolvedValue(true);
|
||||
const props = { params: Promise.resolve(mockParams) };
|
||||
render(await TeamsPage(props));
|
||||
expect(screen.getByTestId("content-wrapper")).toBeInTheDocument();
|
||||
@@ -85,9 +85,9 @@ describe("TeamsPage", () => {
|
||||
currentUserMembership: mockMembership,
|
||||
organization: mockOrg,
|
||||
} as any);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(false);
|
||||
vi.mocked(getAccessControlPermission).mockResolvedValue(false);
|
||||
const props = { params: Promise.resolve(mockParams) };
|
||||
render(await TeamsPage(props));
|
||||
expect(getRoleManagementPermission).toHaveBeenCalledWith("free");
|
||||
expect(getAccessControlPermission).toHaveBeenCalledWith("free");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { IS_FORMBRICKS_CLOUD, USER_MANAGEMENT_MINIMUM_ROLE } from "@/lib/constants";
|
||||
import { getUserManagementAccess } from "@/lib/membership/utils";
|
||||
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { TeamsView } from "@/modules/ee/teams/team-list/components/teams-view";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { MembersView } from "@/modules/organization/settings/teams/components/members-view";
|
||||
@@ -15,7 +15,7 @@ export const TeamsPage = async (props) => {
|
||||
|
||||
const { session, currentUserMembership, organization } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
|
||||
const isAccessControlAllowed = await getAccessControlPermission(organization.billing.plan);
|
||||
const hasUserManagementAccess = getUserManagementAccess(
|
||||
currentUserMembership?.role,
|
||||
USER_MANAGEMENT_MINIMUM_ROLE
|
||||
@@ -36,14 +36,14 @@ export const TeamsPage = async (props) => {
|
||||
organization={organization}
|
||||
currentUserId={session.user.id}
|
||||
environmentId={params.environmentId}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
isUserManagementDisabledFromUi={!hasUserManagementAccess}
|
||||
/>
|
||||
<TeamsView
|
||||
organizationId={organization.id}
|
||||
membershipRole={currentUserMembership?.role}
|
||||
currentUserId={session.user.id}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
environmentId={params.environmentId}
|
||||
/>
|
||||
</PageContentWrapper>
|
||||
|
||||
@@ -169,7 +169,7 @@ describe("CreateProjectModal", () => {
|
||||
open: true,
|
||||
setOpen: vi.fn(),
|
||||
organizationId: "org-123",
|
||||
canDoRoleManagement: true,
|
||||
isAccessControlAllowed: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -214,7 +214,7 @@ describe("CreateProjectModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("shows team selection when canDoRoleManagement is true and teams exist", async () => {
|
||||
test("shows team selection when isAccessControlAllowed is true and teams exist", async () => {
|
||||
render(<CreateProjectModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -230,8 +230,8 @@ describe("CreateProjectModal", () => {
|
||||
expect(options[1]).toHaveTextContent("Marketing Team");
|
||||
});
|
||||
|
||||
test("hides team selection when canDoRoleManagement is false", async () => {
|
||||
render(<CreateProjectModal {...defaultProps} canDoRoleManagement={false} />);
|
||||
test("hides team selection when isAccessControlAllowed is false", async () => {
|
||||
render(<CreateProjectModal {...defaultProps} isAccessControlAllowed={false} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("multi-select")).not.toBeInTheDocument();
|
||||
|
||||
@@ -44,14 +44,14 @@ interface CreateProjectModalProps {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
organizationId: string;
|
||||
canDoRoleManagement: boolean;
|
||||
isAccessControlAllowed: boolean;
|
||||
}
|
||||
|
||||
export const CreateProjectModal = ({
|
||||
open,
|
||||
setOpen,
|
||||
organizationId,
|
||||
canDoRoleManagement,
|
||||
isAccessControlAllowed,
|
||||
}: CreateProjectModalProps) => {
|
||||
const { t } = useTranslate();
|
||||
const router = useRouter();
|
||||
@@ -144,7 +144,7 @@ export const CreateProjectModal = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
{canDoRoleManagement && organizationTeams.length > 0 && (
|
||||
{isAccessControlAllowed && organizationTeams.length > 0 && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="teamIds"
|
||||
|
||||
@@ -51,7 +51,7 @@ vi.mock("@/modules/projects/components/project-limit-modal", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/projects/components/create-project-modal", () => ({
|
||||
CreateProjectModal: ({ open, setOpen, organizationId, organizationTeams, canDoRoleManagement }: any) =>
|
||||
CreateProjectModal: ({ open, setOpen, organizationId, organizationTeams, isAccessControlAllowed }: any) =>
|
||||
open ? (
|
||||
<div data-testid="create-project-modal">
|
||||
<button onClick={() => setOpen(false)} data-testid="close-create-modal">
|
||||
@@ -59,7 +59,7 @@ vi.mock("@/modules/projects/components/create-project-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 data-testid="modal-is-access-control-allowed">{isAccessControlAllowed.toString()}</div>
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
@@ -104,7 +104,7 @@ describe("ProjectSwitcher", () => {
|
||||
environmentId: "env1",
|
||||
isOwnerOrManager: true,
|
||||
organizationTeams: mockOrganizationTeams,
|
||||
canDoRoleManagement: true,
|
||||
isAccessControlAllowed: true,
|
||||
};
|
||||
|
||||
test("renders dropdown and project name", () => {
|
||||
@@ -149,7 +149,7 @@ describe("ProjectSwitcher", () => {
|
||||
await userEvent.click(addButton);
|
||||
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");
|
||||
expect(screen.getByTestId("modal-is-access-control-allowed")).toHaveTextContent("true");
|
||||
});
|
||||
|
||||
test("closes CreateProjectModal when close button is clicked", async () => {
|
||||
@@ -162,9 +162,9 @@ describe("ProjectSwitcher", () => {
|
||||
});
|
||||
|
||||
test("passes correct props to CreateProjectModal", async () => {
|
||||
render(<ProjectSwitcher {...defaultProps} projects={[project]} canDoRoleManagement={false} />);
|
||||
render(<ProjectSwitcher {...defaultProps} projects={[project]} isAccessControlAllowed={false} />);
|
||||
const addButton = screen.getByText("common.add_project");
|
||||
await userEvent.click(addButton);
|
||||
expect(screen.getByTestId("modal-can-do-role-management")).toHaveTextContent("false");
|
||||
expect(screen.getByTestId("modal-is-access-control-allowed")).toHaveTextContent("false");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ interface ProjectSwitcherProps {
|
||||
isLicenseActive: boolean;
|
||||
environmentId: string;
|
||||
isOwnerOrManager: boolean;
|
||||
canDoRoleManagement: boolean;
|
||||
isAccessControlAllowed: boolean;
|
||||
}
|
||||
|
||||
export const ProjectSwitcher = ({
|
||||
@@ -46,7 +46,7 @@ export const ProjectSwitcher = ({
|
||||
isLicenseActive,
|
||||
environmentId,
|
||||
isOwnerOrManager,
|
||||
canDoRoleManagement,
|
||||
isAccessControlAllowed,
|
||||
}: ProjectSwitcherProps) => {
|
||||
const [openLimitModal, setOpenLimitModal] = useState(false);
|
||||
const [openCreateProjectModal, setOpenCreateProjectModal] = useState(false);
|
||||
@@ -227,7 +227,7 @@ export const ProjectSwitcher = ({
|
||||
open={openCreateProjectModal}
|
||||
setOpen={setOpenCreateProjectModal}
|
||||
organizationId={organization.id}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user