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.pull/9963/head
parent
207e0aac1f
commit
f755312ed7
|
@ -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>
|
||||
|
||||
|
|
|
@ -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}}\".",
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Team" ADD COLUMN "isPrivate" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -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[]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
Loading…
Reference in New Issue