From dd37caf32c48a49fbcc5da2ad4499f41fd69d295 Mon Sep 17 00:00:00 2001
From: "devin-ai-integration[bot]"
<158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Tue, 10 Jun 2025 22:23:28 +0200
Subject: [PATCH] feat: move team privacy settings to dedicated settings page
(#21590)
---
.../organization-invitation.e2e.ts | 26 +++++++--
.../organization/organization-privacy.e2e.ts | 56 ++++++++++++-------
apps/web/playwright/signup.e2e.ts | 2 +-
.../playwright/team/team-invitation.e2e.ts | 18 +++++-
apps/web/playwright/teams.e2e.ts | 2 +-
.../ee/teams/pages/team-members-view.tsx | 19 -------
.../ee/teams/pages/team-settings-view.tsx | 35 ++++++++++++
7 files changed, 109 insertions(+), 49 deletions(-)
diff --git a/apps/web/playwright/organization/organization-invitation.e2e.ts b/apps/web/playwright/organization/organization-invitation.e2e.ts
index 9fab1a31ea..6483a9f02a 100644
--- a/apps/web/playwright/organization/organization-invitation.e2e.ts
+++ b/apps/web/playwright/organization/organization-invitation.e2e.ts
@@ -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();
diff --git a/apps/web/playwright/organization/organization-privacy.e2e.ts b/apps/web/playwright/organization/organization-privacy.e2e.ts
index c94ea5eedd..0968461fb7 100644
--- a/apps/web/playwright/organization/organization-privacy.e2e.ts
+++ b/apps/web/playwright/organization/organization-privacy.e2e.ts
@@ -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();
});
});
diff --git a/apps/web/playwright/signup.e2e.ts b/apps/web/playwright/signup.e2e.ts
index 189ad760ef..cd69adae5e 100644
--- a/apps/web/playwright/signup.e2e.ts
+++ b/apps/web/playwright/signup.e2e.ts
@@ -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)
diff --git a/apps/web/playwright/team/team-invitation.e2e.ts b/apps/web/playwright/team/team-invitation.e2e.ts
index f32d580e32..86e3983fca 100644
--- a/apps/web/playwright/team/team-invitation.e2e.ts
+++ b/apps/web/playwright/team/team-invitation.e2e.ts
@@ -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();
diff --git a/apps/web/playwright/teams.e2e.ts b/apps/web/playwright/teams.e2e.ts
index 6f3eb9954e..aaeb1d90b7 100644
--- a/apps/web/playwright/teams.e2e.ts
+++ b/apps/web/playwright/teams.e2e.ts
@@ -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(),
diff --git a/packages/features/ee/teams/pages/team-members-view.tsx b/packages/features/ee/teams/pages/team-members-view.tsx
index 14a1cd183c..935a10c3b9 100644
--- a/packages/features/ee/teams/pages/team-members-view.tsx
+++ b/packages/features/ee/teams/pages/team-members-view.tsx
@@ -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 && (
-