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 && ( - - )} - - {team && team.id && (isAdmin || isOrgAdminOrOwner) && ( - - )} )} diff --git a/packages/features/ee/teams/pages/team-settings-view.tsx b/packages/features/ee/teams/pages/team-settings-view.tsx index 120a62dfc7..617cb1f65a 100644 --- a/packages/features/ee/teams/pages/team-settings-view.tsx +++ b/packages/features/ee/teams/pages/team-settings-view.tsx @@ -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 ( + <> +
+ {team && session.data && ( + + )} + + {team && team.id && (isAdmin || isOrgAdminOrOwner) && ( + + )} +
+ + ); +}; + const TeamSettingsViewWrapper = () => { const router = useRouter(); const params = useParamsWithFallback(); @@ -174,6 +208,7 @@ const TeamSettingsViewWrapper = () => { return ( <> +