feat: move team privacy settings to dedicated settings page (#21590)

This commit is contained in:
devin-ai-integration[bot]
2025-06-10 22:23:28 +02:00
committed by GitHub
parent 856d223535
commit dd37caf32c
7 changed files with 109 additions and 49 deletions
@@ -105,7 +105,7 @@ test.describe("Organization", () => {
const { team: org } = await orgOwner.getOrgMembership();
await test.step("By email", async () => {
await page.goto(`/settings/teams/${team.id}/members`);
await page.goto(`/settings/teams/${team.id}/settings`);
const invitedUserEmail = users.trackEmail({ username: "rick", domain: "domain.com" });
// '-domain' because the email doesn't match orgAutoAcceptEmail
const usernameDerivedFromEmail = `${invitedUserEmail.split("@")[0]}-domain`;
@@ -168,7 +168,7 @@ test.describe("Organization", () => {
});
await test.step("By invite link", async () => {
await page.goto(`/settings/teams/${team.id}/members`);
await page.goto(`/settings/teams/${team.id}/settings`);
const inviteLink = await copyInviteLink(page, true);
const email = users.trackEmail({ username: "rick", domain: "domain.com" });
// '-domain' because the email doesn't match orgAutoAcceptEmail
@@ -339,7 +339,7 @@ test.describe("Organization", () => {
await orgOwner.apiLogin();
await test.step("By email", async () => {
await page.goto(`/settings/teams/${team.id}/members`);
await page.goto(`/settings/teams/${team.id}/settings`);
const invitedUserEmail = users.trackEmail({ username: "rick", domain: "example.com" });
const usernameDerivedFromEmail = invitedUserEmail.split("@")[0];
await inviteAnEmail(page, invitedUserEmail, true);
@@ -400,7 +400,7 @@ test.describe("Organization", () => {
});
await test.step("By invite link", async () => {
await page.goto(`/settings/teams/${team.id}/members`);
await page.goto(`/settings/teams/${team.id}/settings`);
const inviteLink = await copyInviteLink(page, true);
const email = users.trackEmail({ username: "rick", domain: "example.com" });
@@ -447,7 +447,7 @@ test.describe("Organization", () => {
const { team } = await orgOwner.getFirstTeamMembership();
await orgOwner.apiLogin();
await page.goto(`/settings/teams/${team.id}/members`);
await page.goto(`/settings/teams/${team.id}/settings`);
const invitedUserEmail = users.trackEmail({ username: "rick", domain: "example.com" });
await inviteAnEmail(page, invitedUserEmail, true);
@@ -540,6 +540,13 @@ export async function signupFromEmailInviteLink({
async function inviteAnEmail(page: Page, invitedUserEmail: string, teamPage?: boolean) {
if (teamPage) {
const url = page.url();
const teamIdMatch = url.match(/\/settings\/teams\/(\d+)/);
if (teamIdMatch && teamIdMatch[1]) {
await page.goto(`/settings/teams/${teamIdMatch[1]}/members`);
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(500); // Add a small delay to ensure UI is fully loaded
}
await page.getByTestId("new-member-button").click();
} else {
await page.getByTestId("new-organization-member-button").click();
@@ -597,8 +604,8 @@ async function expectUserToBeAMemberOfTeam({
}) {
// Check newly invited member is not pending anymore
await page.goto(`/settings/teams/${teamId}/members`);
await page.reload();
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(1000); // Add a small delay to ensure UI is fully loaded
expect(
(
await page
@@ -623,6 +630,13 @@ function assertInviteLink(inviteLink: string | null | undefined): asserts invite
async function copyInviteLink(page: Page, teamPage?: boolean) {
if (teamPage) {
const url = page.url();
const teamIdMatch = url.match(/\/settings\/teams\/(\d+)/);
if (teamIdMatch && teamIdMatch[1]) {
await page.goto(`/settings/teams/${teamIdMatch[1]}/members`);
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(500); // Add a small delay to ensure UI is fully loaded
}
await page.getByTestId("new-member-button").click();
} else {
await page.getByTestId("new-organization-member-button").click();
@@ -93,14 +93,13 @@ test.describe("Organization - Privacy", () => {
const teamId = membership.team.id;
// Update team to be private
await page.goto(`/settings/teams/${teamId}/members`);
await page.goto(`/settings/teams/${teamId}/settings`);
await page.waitForLoadState("domcontentloaded");
const togglePrivateSwitch = await page.getByTestId("make-team-private-check");
await togglePrivateSwitch.click();
// As admin/owner we can see the user list
const tableLocator = await page.getByTestId("team-member-list-container");
await expect(tableLocator).toBeVisible();
// As admin/owner we can see the privacy settings
await expect(page.getByTestId("make-team-private-check")).toBeVisible();
const memberUser = await prisma.membership.findFirst({
where: {
@@ -121,16 +120,25 @@ test.describe("Organization - Privacy", () => {
const memberOfTeam = await users.set(memberUser?.user.email);
await memberOfTeam.apiLogin();
await page.goto(`/settings/teams/${teamId}/members`);
await page.goto(`/settings/teams/${teamId}/settings`);
await page.waitForLoadState("domcontentloaded");
// As a user we can not see the user list when a team is private
// As a user we can not see the privacy settings when a team is private
await expect(page.getByTestId("make-team-private-check")).toBeHidden();
await page.goto(`/settings/teams/${teamId}/members`);
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(500); // Add a small delay to ensure UI is fully loaded
// As a user we can not see the member list when a team is private
const hiddenTableLocator = await page.getByTestId("team-member-list-container");
await expect(hiddenTableLocator).toBeHidden();
});
test(`Private Org - Public Team\n
1) All team members can see members in team \n
2) Team Admin/Owner can see members in team`, async ({ page, users, orgs }) => {
2) Privacy settings are hidden to non-admin members \n
3) Admin/Owner can see members in team \n
4) Only Team Admin/Owner can see privacy settings`, async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
@@ -154,18 +162,9 @@ test.describe("Organization - Privacy", () => {
}
);
await owner.apiLogin();
const membership = await owner.getFirstTeamMembership();
const teamId = membership.team.id;
// Update team to be private
await page.goto(`/settings/teams/${teamId}/members`);
await page.waitForLoadState("domcontentloaded");
// As admin/owner we can see the user list
const tableLocator = await page.getByTestId("team-member-list-container");
await expect(tableLocator).toBeVisible();
const memberUser = await prisma.membership.findFirst({
where: {
teamId,
@@ -185,11 +184,30 @@ test.describe("Organization - Privacy", () => {
const memberOfTeam = await users.set(memberUser?.user.email);
await memberOfTeam.apiLogin();
// 1) All team members can see members in team
await page.goto(`/settings/teams/${teamId}/members`);
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(500);
const memberTableLocator = await page.getByTestId("team-member-list-container");
await expect(memberTableLocator).toBeVisible();
// As a user we can not see the user list when a team is private
const hiddenTableLocator = await page.getByTestId("team-member-list-container");
await expect(hiddenTableLocator).toBeVisible();
// 2) Privacy settings are hidden to non-admin members
await page.goto(`/settings/teams/${teamId}/settings`);
await page.waitForLoadState("domcontentloaded");
await expect(page.getByTestId("make-team-private-check")).toBeHidden();
await owner.apiLogin();
// 3) Admin/Owner can see members in team
await page.goto(`/settings/teams/${teamId}/members`);
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(500);
const adminTableLocator = await page.getByTestId("team-member-list-container");
await expect(adminTableLocator).toBeVisible();
// 4) Only Team Admin/Owner can see privacy settings
await page.goto(`/settings/teams/${teamId}/settings`);
await page.waitForLoadState("domcontentloaded");
await expect(page.getByTestId("make-team-private-check")).toBeVisible();
});
});
+1 -1
View File
@@ -312,7 +312,7 @@ test.describe("Email Signup Flow Test", async () => {
const teamOwner = await users.create(undefined, { hasTeam: true });
const { team } = await teamOwner.getFirstTeamMembership();
await teamOwner.apiLogin();
await page.goto(`/settings/teams/${team.id}/members`);
await page.goto(`/settings/teams/${team.id}/settings`);
await test.step("Invite User to team", async () => {
// TODO: This invite logic should live in a fixture - its used in team and orgs invites (Duplicated from team/org invites)
@@ -21,13 +21,16 @@ test.describe("Team", () => {
const teamOwner = await users.create(undefined, { hasTeam: true });
const { team } = await teamOwner.getFirstTeamMembership();
await teamOwner.apiLogin();
await page.goto(`/settings/teams/${team.id}/members`);
await page.goto(`/settings/teams/${team.id}/settings`);
await test.step("To the team by email (external user)", async () => {
const invitedUserEmail = users.trackEmail({
username: "rick",
domain: `domain-${Date.now()}.com`,
});
await page.goto(`/settings/teams/${team.id}/members`);
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(500); // Add a small delay to ensure UI is fully loaded
await page.getByTestId("new-member-button").click();
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
await page.getByText(t("send_invite")).click();
@@ -66,7 +69,7 @@ test.describe("Team", () => {
// Check newly invited member is not pending anymore
await page.bringToFront();
await page.goto(`/settings/teams/${team.id}/members`);
await page.goto(`/settings/teams/${team.id}/settings`);
await expect(
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
).toHaveCount(0);
@@ -78,6 +81,9 @@ test.describe("Team", () => {
password: "P4ssw0rd!",
});
await page.goto(`/settings/teams/${team.id}/members`);
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(500); // Add a small delay to ensure UI is fully loaded
await page.getByTestId("new-member-button").click();
const inviteLink = await getInviteLink(page);
@@ -102,13 +108,16 @@ test.describe("Team", () => {
const teamOwner = await users.create({ name: `team-owner-${Date.now()}` }, { hasTeam: true });
const { team } = await teamOwner.getFirstTeamMembership();
await teamOwner.apiLogin();
await page.goto(`/settings/teams/${team.id}/members`);
await page.goto(`/settings/teams/${team.id}/settings`);
await test.step("To the organization by email (internal user)", async () => {
const invitedUserEmail = users.trackEmail({
username: "rick",
domain: `example.com`,
});
await page.goto(`/settings/teams/${team.id}/members`);
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(500); // Add a small delay to ensure UI is fully loaded
await page.getByTestId("new-member-button").click();
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
await page.getByText(t("send_invite")).click();
@@ -153,7 +162,10 @@ test.describe("Team", () => {
const { team } = await teamOwner.getFirstTeamMembership();
await teamOwner.apiLogin();
await page.goto(`/settings/teams/${team.id}/settings`);
await page.goto(`/settings/teams/${team.id}/members`);
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(500); // Add a small delay to ensure UI is fully loaded
await page.getByTestId("new-member-button").click();
await page.locator('input[name="inviteUser"]').fill(invitedMember.email);
await page.getByText(t("send_invite")).click();
+1 -1
View File
@@ -215,7 +215,7 @@ test.describe("Teams - NonOrg", () => {
const { team } = await owner.getFirstTeamMembership();
// Mark team as private
await page.goto(`/settings/teams/${team.id}/members`);
await page.goto(`/settings/teams/${team.id}/settings`);
await Promise.all([
page.click("[data-testid=make-team-private-check]"),
expect(page.locator(`[data-testid=make-team-private-check][data-state="checked"]`)).toBeVisible(),
@@ -9,9 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { trpc } from "@calcom/trpc/react";
import DisableTeamImpersonation from "../components/DisableTeamImpersonation";
import InviteLinkSettingsModal from "../components/InviteLinkSettingsModal";
import MakeTeamPrivateSwitch from "../components/MakeTeamPrivateSwitch";
import { MemberInvitationModalWithoutMembers } from "../components/MemberInvitationModal";
import MemberList from "../components/MemberList";
import TeamInviteList from "../components/TeamInviteList";
@@ -112,23 +110,6 @@ const MembersView = () => {
}}
/>
)}
{team && session.data && (
<DisableTeamImpersonation
teamId={team.id}
memberId={session.data.user.id}
disabled={isInviteOpen}
/>
)}
{team && team.id && (isAdmin || isOrgAdminOrOwner) && (
<MakeTeamPrivateSwitch
isOrg={false}
teamId={team.id}
isPrivate={team.isPrivate ?? false}
disabled={isInviteOpen}
/>
)}
</div>
</>
)}
@@ -1,5 +1,6 @@
"use client";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useForm, Controller } from "react-hook-form";
@@ -21,7 +22,9 @@ import { SettingsToggle } from "@calcom/ui/components/form";
import { CheckboxField } from "@calcom/ui/components/form";
import { showToast } from "@calcom/ui/components/toast";
import DisableTeamImpersonation from "../components/DisableTeamImpersonation";
import { default as InternalNotePresetsView } from "../components/InternalNotePresetsView";
import MakeTeamPrivateSwitch from "../components/MakeTeamPrivateSwitch";
import RoundRobinResetInterval from "../components/RoundRobinResetInterval";
type ProfileViewProps = { team: RouterOutputs["viewer"]["teams"]["get"] };
@@ -143,6 +146,37 @@ const BookingLimitsView = ({ team }: ProfileViewProps) => {
);
};
const PrivacySettingsView = ({ team }: ProfileViewProps) => {
const session = useSession();
const isAdmin = team && checkAdminOrOwner(team.membership.role);
const isOrgAdminOrOwner = checkAdminOrOwner(session?.data?.user.org?.role);
const isInviteOpen = !team?.membership.accepted;
const { t } = useLocale();
return (
<>
<div className="mt-6">
{team && session.data && (
<DisableTeamImpersonation
teamId={team.id}
memberId={session.data.user.id}
disabled={isInviteOpen}
/>
)}
{team && team.id && (isAdmin || isOrgAdminOrOwner) && (
<MakeTeamPrivateSwitch
isOrg={false}
teamId={team.id}
isPrivate={team.isPrivate ?? false}
disabled={isInviteOpen}
/>
)}
</div>
</>
);
};
const TeamSettingsViewWrapper = () => {
const router = useRouter();
const params = useParamsWithFallback();
@@ -174,6 +208,7 @@ const TeamSettingsViewWrapper = () => {
return (
<>
<BookingLimitsView team={team} />
<PrivacySettingsView team={team} />
<InternalNotePresetsView team={team} />
<RoundRobinResetInterval team={team} />
</>