feat: move team privacy settings to dedicated settings page (#21590)
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user