From fe8b13885cb02fd00db44463ece65e8e9e07741d Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Fri, 18 Aug 2023 18:10:17 -0300 Subject: [PATCH] fix: org admin has team member privileges [CAL-2338] (#10809) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Omar López --- apps/web/public/static/locales/en/common.json | 4 +- .../pages/components/OtherTeamsListing.tsx | 8 +-- .../settings/other-team-members-view.tsx | 63 +++++++++++-------- .../ee/teams/components/AddNewTeamMembers.tsx | 63 +++++++++++++------ .../ee/teams/pages/team-members-view.tsx | 13 +++- .../settings/layouts/SettingsLayout.tsx | 12 ++-- .../organizations/listOtherTeams.handler.ts | 1 + .../routers/viewer/teams/create.handler.ts | 9 ++- .../routers/viewer/teams/get.handler.ts | 5 +- .../inviteMember/inviteMember.handler.ts | 7 ++- .../viewer/teams/removeMember.handler.ts | 5 +- 11 files changed, 127 insertions(+), 63 deletions(-) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index d172f4df25..ebc27343d8 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -562,6 +562,7 @@ "leave": "Leave", "profile": "Profile", "my_team_url": "My team URL", + "my_teams": "My teams", "team_name": "Team Name", "your_team_name": "Your team name", "team_updated_successfully": "Team updated successfully", @@ -1998,7 +1999,7 @@ "kyc_verification": "KYC Verification", "organizations": "Organizations", "org_admin_other_teams": "Other teams", - "org_admin_other_teams_description": "Here you can see teams inside your organization but that you are not part of. You can add yourself to them if needed.", + "org_admin_other_teams_description": "Here you can see teams inside your organization that you are not part of. You can add yourself to them if needed.", "no_other_teams_found": "No other teams found", "no_other_teams_found_description": "There are no other teams in this organization.", "attendee_first_name_variable": "Attendee first name", @@ -2007,6 +2008,7 @@ "attendee_last_name_info": "The person booking's last name", "me": "Me", "verify_team_tooltip": "Verify your team to enable sending messages to attendees", + "member_removed": "Member removed", "my_availability": "My Availability", "team_availability": "Team Availability", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" diff --git a/packages/features/ee/organizations/pages/components/OtherTeamsListing.tsx b/packages/features/ee/organizations/pages/components/OtherTeamsListing.tsx index 153ec79155..824b2be8ec 100644 --- a/packages/features/ee/organizations/pages/components/OtherTeamsListing.tsx +++ b/packages/features/ee/organizations/pages/components/OtherTeamsListing.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useState } from "react"; import SkeletonLoaderTeamList from "@calcom/ee/teams/components/SkeletonloaderTeamList"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -12,14 +12,12 @@ export function OtherTeamsListing() { const [errorMessage, setErrorMessage] = useState(""); - const { data, isLoading } = trpc.viewer.organizations.listOtherTeams.useQuery(undefined, { + const { data: teams, isLoading } = trpc.viewer.organizations.listOtherTeams.useQuery(undefined, { onError: (e) => { setErrorMessage(e.message); }, }); - const teams = useMemo(() => data?.filter((m) => m.accepted) || [], [data]); - if (isLoading) { return ; } @@ -28,7 +26,7 @@ export function OtherTeamsListing() { <> {!!errorMessage && } - {teams.length > 0 ? ( + {teams && teams.length > 0 ? ( ) : ( { const router = useRouter(); const searchParams = useSearchParams(); const teamId = Number(searchParams.get("id")); - + const session = useSession(); const utils = trpc.useContext(); const [offset, setOffset] = useState(1); // const [query, setQuery] = useState(""); @@ -60,6 +63,9 @@ const MembersView = () => { const limit = 100; const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false); const [members, setMembers] = useState([]); + const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { + enabled: !!session.data?.user?.organizationId, + }); const { data: team, isLoading: isTeamLoading } = trpc.viewer.organizations.getOtherTeam.useQuery( { teamId }, { @@ -68,6 +74,16 @@ const MembersView = () => { }, } ); + const { data: orgMembersNotInThisTeam, isLoading: isOrgListLoading } = + trpc.viewer.organizations.getMembers.useQuery( + { + teamIdToExclude: teamId, + distinctUser: true, + }, + { + enabled: searchParams !== null, + } + ); const { data: membersFetch, isLoading: isLoadingMembers } = trpc.viewer.organizations.listOtherTeamMembers.useQuery( { teamId, limit, offset: (offset - 1) * limit }, @@ -89,38 +105,34 @@ const MembersView = () => { } }, [membersFetch]); - // useEffect(() => { - // if (queryToFetch !== "") { - // setMembers(membersFetch || []); - // setLoadMore(false); - // } - // }, [membersFetch, query]); - - const isLoading = isTeamLoading || isLoadingMembers; + const isLoading = isTeamLoading || isLoadingMembers || isOrgListLoading; const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation(); - // const debouncedFunction = debounce((query) => { - // setQueryToFetch(query); - // }, 500); + const isOrgAdminOrOwner = + currentOrg && + (currentOrg.user.role === MembershipRole.OWNER || currentOrg.user.role === MembershipRole.ADMIN); return ( <> setShowMemberInvitationModal(true)} - // data-testid="new-member-button"> - // {t("add")} - // - // } + CTA={ + isOrgAdminOrOwner ? ( + + ) : ( + <> + ) + } /> {!isLoading && ( <> @@ -157,6 +169,7 @@ const MembersView = () => { setShowMemberInvitationModal(false)} diff --git a/packages/features/ee/teams/components/AddNewTeamMembers.tsx b/packages/features/ee/teams/components/AddNewTeamMembers.tsx index 5222b1be74..f58076bca2 100644 --- a/packages/features/ee/teams/components/AddNewTeamMembers.tsx +++ b/packages/features/ee/teams/components/AddNewTeamMembers.tsx @@ -1,14 +1,15 @@ import { useSession } from "next-auth/react"; import { useRouter, useSearchParams } from "next/navigation"; import { useState } from "react"; -import { z } from "zod"; +import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; import InviteLinkSettingsModal from "@calcom/features/ee/teams/components/InviteLinkSettingsModal"; import MemberInvitationModal from "@calcom/features/ee/teams/components/MemberInvitationModal"; import { classNames } from "@calcom/lib"; import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants"; import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { MembershipRole } from "@calcom/prisma/enums"; import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import { @@ -22,10 +23,6 @@ import { } from "@calcom/ui"; import { ArrowRight, Plus, Trash2 } from "@calcom/ui/components/icon"; -const querySchema = z.object({ - id: z.string().transform((val) => parseInt(val)), -}); - type TeamMember = RouterOutputs["viewer"]["teams"]["get"]["members"][number]; type FormValues = { @@ -57,12 +54,22 @@ export const AddNewTeamMembersForm = ({ const router = useRouter(); const utils = trpc.useContext(); + const orgBranding = useOrgBranding(); const showDialog = searchParams?.get("inviteModal") === "true"; const [memberInviteModal, setMemberInviteModal] = useState(showDialog); const [inviteLinkSettingsModal, setInviteLinkSettingsModal] = useState(false); const { data: team, isLoading } = trpc.viewer.teams.get.useQuery({ teamId }, { enabled: !!teamId }); + const { data: orgMembersNotInThisTeam } = trpc.viewer.organizations.getMembers.useQuery( + { + teamIdToExclude: teamId, + distinctUser: true, + }, + { + enabled: orgBranding !== null, + } + ); const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation(); @@ -78,17 +85,19 @@ export const AddNewTeamMembersForm = ({ return ( <>
-
    - {defaultValues.members.map((member, index) => ( - - ))} -
+ {defaultValues.members.length > 0 && ( +
    + {defaultValues.members.map((member, index) => ( + + ))} +
+ )}
@@ -99,6 +108,7 @@ export const AddNewTeamMembersForm = ({ setMemberInviteModal(false)} @@ -162,14 +172,18 @@ export const AddNewTeamMembersForm = ({ )}
); @@ -198,18 +212,26 @@ const PendingMemberItem = (props: { member: TeamMember; index: number; teamId: n const { member, index, teamId } = props; const { t } = useLocale(); const utils = trpc.useContext(); + const session = useSession(); const bookerUrl = useBookerUrl(); + const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { + enabled: !!session.data?.user?.organizationId, + }); const removeMemberMutation = trpc.viewer.teams.removeMember.useMutation({ async onSuccess() { await utils.viewer.teams.get.invalidate(); await utils.viewer.eventTypes.invalidate(); - showToast("Member removed", "success"); + showToast(t("member_removed"), "success"); }, async onError(err) { showToast(err.message, "error"); }, }); + const isOrgAdminOrOwner = + currentOrg && + (currentOrg.user.role === MembershipRole.OWNER || currentOrg.user.role === MembershipRole.ADMIN); + return (
  • {member.name || member.email || t("team_member")}

    {/* Assume that the first member of the team is the creator */} - {index === 0 && {t("you")}} + {member.id === session.data?.user.id && {t("you")}} {!member.accepted && {t("pending")}} {member.role === "MEMBER" && {t("member")}} {member.role === "ADMIN" && {t("admin")}} @@ -241,7 +263,7 @@ const PendingMemberItem = (props: { member: TeamMember; index: number; teamId: n )} - {member.role !== "OWNER" && ( + {(member.role !== "OWNER" || isOrgAdminOrOwner) && (