feat: Make Team Private

## What does this PR do?

Fixes https://github.com/calcom/cal.com/issues/8974

1) When user is admin

<img width="1440" alt="Screenshot 2023-07-03 at 6 45 50 PM" src="https://github.com/calcom/cal.com/assets/53316345/ce15158f-d278-4f1a-ba2e-8b63e4274793">

2) When user is not admin and team is private

<img width="1440" alt="Screenshot 2023-07-03 at 6 47 15 PM" src="https://github.com/calcom/cal.com/assets/53316345/ce23560e-690a-4c42-a76d-49691260aa4d">

3) 
<img width="1440" alt="Screenshot 2023-07-03 at 6 51 56 PM" src="https://github.com/calcom/cal.com/assets/53316345/13af38f8-5618-4dae-b359-b24dc91e4eb4">


## Type of change

<!-- Please delete bullets that are not relevant. -->

- New feature (non-breaking change which adds functionality)

## How should this be tested?

1) go to Team members page and turn on switch Make Team Private.

Now after making the team private only admin would be able to see all the members list in the settings. There will not be a button to Book a team member instead on the team page like before.


## Mandatory Tasks

- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
This commit is contained in:
Udit Takkar 2023-07-06 15:25:12 +05:30 committed by GitHub
parent 207e0aac1f
commit f755312ed7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 96 additions and 9 deletions

View File

@ -187,7 +187,7 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
{!isBioEmpty && (
<>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
className="text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: team.safeBio }}
/>
</>
@ -197,21 +197,26 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
<SubTeams />
) : (
<>
{(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />}
{(showMembers.isOn || !team.eventTypes.length) &&
(team.isPrivate ? (
<div className="w-full text-center">
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_team_members")}</h2>
</div>
) : (
<Team team={team} />
))}
{!showMembers.isOn && team.eventTypes.length > 0 && (
<div className="mx-auto max-w-3xl ">
<EventTypes />
{!team.hideBookATeamMember && (
{!(team.hideBookATeamMember || team.isPrivate) && (
<div>
<div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="border-subtle w-full border-t" />
</div>
<div className="relative flex justify-center">
<span className="dark:bg-darkgray-50 bg-subtle text-subtle dark:text-inverted px-2 text-sm">
{t("or")}
</span>
<span className="bg-subtle text-subtle px-2 text-sm">{t("or")}</span>
</div>
</div>

View File

@ -1003,6 +1003,9 @@
"user_impersonation_heading": "User Impersonation",
"user_impersonation_description": "Allows our support team to temporarily sign in as you to help us quickly resolve any issues you report to us.",
"team_impersonation_description": "Allows your team Owners/Admins to temporarily sign in as you.",
"make_team_private": "Make team private",
"make_team_private_description": "Your team members won't be able to see other team members when this is turned on.",
"you_cannot_see_team_members": "You cannot see all the team members of a private team.",
"allow_booker_to_select_duration": "Allow booker to select duration",
"impersonate_user_tip": "All uses of this feature is audited.",
"impersonating_user_warning": "Impersonating username \"{{user}}\".",

View File

@ -0,0 +1,60 @@
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { showToast, Switch } from "@calcom/ui";
const MakeTeamPrivateSwitch = ({
teamId,
isPrivate,
disabled,
}: {
teamId: number;
isPrivate: boolean;
disabled: boolean;
}) => {
const { t } = useLocale();
const utils = trpc.useContext();
const mutation = trpc.viewer.teams.update.useMutation({
onError: (err) => {
showToast(err.message, "error");
},
async onSuccess() {
await utils.viewer.teams.get.invalidate();
showToast(t("your_team_updated_successfully"), "success");
},
});
return (
<>
<div className="flex flex-col justify-between sm:flex-row">
<div>
<div className="flex flex-row items-center">
<h2
className={classNames(
"font-cal mb-0.5 text-sm font-semibold leading-6",
disabled ? "text-muted " : "text-emphasis "
)}>
{t("make_team_private")}
</h2>
</div>
<p className={classNames("text-sm leading-5 ", disabled ? "text-gray-300" : "text-default")}>
{t("make_team_private_description")}
</p>
</div>
<div className="mt-5 sm:mt-0 sm:self-center">
<Switch
disabled={disabled}
defaultChecked={isPrivate}
onCheckedChange={(isChecked) => {
mutation.mutate({ id: teamId, isPrivate: isChecked });
}}
/>
</div>
</div>
</>
);
};
export default MakeTeamPrivateSwitch;

View File

@ -12,6 +12,7 @@ import { Plus } from "@calcom/ui/components/icon";
import { getLayout } from "../../../settings/layouts/SettingsLayout";
import DisableTeamImpersonation from "../components/DisableTeamImpersonation";
import InviteLinkSettingsModal from "../components/InviteLinkSettingsModal";
import MakeTeamPrivateSwitch from "../components/MakeTeamPrivateSwitch";
import MemberInvitationModal from "../components/MemberInvitationModal";
import MemberListItem from "../components/MemberListItem";
import TeamInviteList from "../components/TeamInviteList";
@ -67,6 +68,7 @@ const MembersView = () => {
const router = useRouter();
const session = useSession();
const utils = trpc.useContext();
const teamId = Number(router.query.id);
@ -132,8 +134,13 @@ const MembersView = () => {
)}
</>
)}
<MembersList team={team} />
<hr className="border-subtle my-8" />
{((team?.isPrivate && isAdmin) || !team?.isPrivate) && (
<>
<MembersList team={team} />
<hr className="border-subtle my-8" />
</>
)}
{team && session.data && (
<DisableTeamImpersonation
@ -142,7 +149,13 @@ const MembersView = () => {
disabled={isInviteOpen}
/>
)}
<hr className="border-subtle my-8" />
{team && isAdmin && (
<>
<hr className="border-subtle my-8" />
<MakeTeamPrivateSwitch teamId={team.id} isPrivate={team.isPrivate} disabled={isInviteOpen} />
</>
)}
</div>
{showMemberInvitationModal && team && (
<MemberInvitationModal

View File

@ -24,6 +24,7 @@ export async function getTeamWithMembers(id?: number, slug?: string, userId?: nu
bio: true,
hideBranding: true,
hideBookATeamMember: true,
isPrivate: true,
metadata: true,
parent: {
select: {

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Team" ADD COLUMN "isPrivate" BOOLEAN NOT NULL DEFAULT false;

View File

@ -252,6 +252,7 @@ model Team {
appIconLogo String?
bio String?
hideBranding Boolean @default(false)
isPrivate Boolean @default(false)
hideBookATeamMember Boolean @default(false)
members Membership[]
eventTypes EventType[]

View File

@ -43,6 +43,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
logo: input.logo,
bio: input.bio,
hideBranding: input.hideBranding,
isPrivate: input.isPrivate,
hideBookATeamMember: input.hideBookATeamMember,
brandColor: input.brandColor,
darkBrandColor: input.darkBrandColor,

View File

@ -8,6 +8,7 @@ export const ZUpdateInputSchema = z.object({
slug: z.string().optional(),
hideBranding: z.boolean().optional(),
hideBookATeamMember: z.boolean().optional(),
isPrivate: z.boolean().optional(),
brandColor: z.string().optional(),
darkBrandColor: z.string().optional(),
theme: z.string().optional().nullable(),