fix: adding team members from organization tab that alredy exist (#11689)

* fix: adding team members from organization tab that alredy exist

* changed organizations.listOtherTeamMembers from useQuery to useInfiniteQuery

* undo yarn.lock

* fix: invalidate the organizations.getMembers query on removeMember and inviteMember Mutation

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
This commit is contained in:
Somay Chauhan 2023-12-07 15:09:23 +05:30 committed by GitHub
parent e1ac6f5454
commit 75eaed1c4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 69 additions and 39 deletions

View File

@ -18,7 +18,7 @@ import {
import { ExternalLink, MoreHorizontal } from "@calcom/ui/components/icon"; import { ExternalLink, MoreHorizontal } from "@calcom/ui/components/icon";
interface Props { interface Props {
member: RouterOutputs["viewer"]["organizations"]["listOtherTeamMembers"][number]; member: RouterOutputs["viewer"]["organizations"]["listOtherTeamMembers"]["rows"][number];
} }
export default function MemberListItem(props: Props) { export default function MemberListItem(props: Props) {

View File

@ -1,7 +1,7 @@
// import { debounce } from "lodash"; // import { debounce } from "lodash";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState, useEffect } from "react"; import { useState } from "react";
import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal"; import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -16,20 +16,21 @@ import { getLayout } from "../../../../settings/layouts/SettingsLayout";
import MakeTeamPrivateSwitch from "../../../teams/components/MakeTeamPrivateSwitch"; import MakeTeamPrivateSwitch from "../../../teams/components/MakeTeamPrivateSwitch";
import MemberListItem from "../components/MemberListItem"; import MemberListItem from "../components/MemberListItem";
type Members = RouterOutputs["viewer"]["organizations"]["listOtherTeamMembers"]; type Members = RouterOutputs["viewer"]["organizations"]["listOtherTeamMembers"]["rows"];
type Team = RouterOutputs["viewer"]["organizations"]["getOtherTeam"]; type Team = RouterOutputs["viewer"]["organizations"]["getOtherTeam"];
interface MembersListProps { interface MembersListProps {
members: Members | undefined; members: Members | undefined;
team: Team | undefined; team: Team | undefined;
offset: number; fetchNextPage: () => void;
setOffset: (offset: number) => void; hasNextPage: boolean | undefined;
displayLoadMore: boolean; isFetchingNextPage: boolean | undefined;
} }
function MembersList(props: MembersListProps) { function MembersList(props: MembersListProps) {
const { t } = useLocale(); const { t } = useLocale();
const { displayLoadMore, members, team } = props; const { hasNextPage, members = [], team, fetchNextPage, isFetchingNextPage } = props;
return ( return (
<div className="flex flex-col gap-y-3"> <div className="flex flex-col gap-y-3">
{members?.length && team ? ( {members?.length && team ? (
@ -44,13 +45,15 @@ function MembersList(props: MembersListProps) {
<p className="text-default text-sm font-bold">{t("no_members_found")}</p> <p className="text-default text-sm font-bold">{t("no_members_found")}</p>
</div> </div>
)} )}
{displayLoadMore && ( <div className="text-default p-4 text-center">
<button <Button
className="text-primary-500 hover:text-primary-600" color="minimal"
onClick={() => props.setOffset(props.offset + 1)}> loading={isFetchingNextPage}
{t("load_more")} disabled={!hasNextPage}
</button> onClick={() => fetchNextPage()}>
)} {hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
</div> </div>
); );
} }
@ -62,11 +65,9 @@ const MembersView = () => {
const teamId = Number(params.id); const teamId = Number(params.id);
const session = useSession(); const session = useSession();
const utils = trpc.useContext(); const utils = trpc.useContext();
const [offset, setOffset] = useState<number>(1);
// const [query, setQuery] = useState<string | undefined>(""); // const [query, setQuery] = useState<string | undefined>("");
// const [queryToFetch, setQueryToFetch] = useState<string | undefined>(""); // const [queryToFetch, setQueryToFetch] = useState<string | undefined>("");
const [loadMore, setLoadMore] = useState<boolean>(true); const limit = 20;
const limit = 100;
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState<boolean>(false); const [showMemberInvitationModal, setShowMemberInvitationModal] = useState<boolean>(false);
const [members, setMembers] = useState<Members>([]); const [members, setMembers] = useState<Members>([]);
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, {
@ -91,26 +92,25 @@ const MembersView = () => {
enabled: !Number.isNaN(teamId), enabled: !Number.isNaN(teamId),
} }
); );
const { data: membersFetch, isLoading: isLoadingMembers } =
trpc.viewer.organizations.listOtherTeamMembers.useQuery( const { fetchNextPage, isFetchingNextPage, hasNextPage } =
{ teamId, limit, offset: (offset - 1) * limit }, trpc.viewer.organizations.listOtherTeamMembers.useInfiniteQuery(
{ teamId, limit },
{ {
onSuccess: (data) => {
const flatData = data?.pages?.flatMap((page) => page.rows) as Members;
setMembers(flatData);
},
enabled: !Number.isNaN(teamId), enabled: !Number.isNaN(teamId),
onError: () => { onError: () => {
router.push("/settings"); router.push("/settings");
}, },
getNextPageParam: (lastPage) => lastPage.nextCursor,
keepPreviousData: true,
} }
); );
useEffect(() => { const isLoading = isTeamLoading || isOrgListLoading;
if (membersFetch) {
setLoadMore(membersFetch.length >= limit);
setMembers((m) => m.concat(membersFetch));
}
}, [membersFetch]);
const isLoading = isTeamLoading || isLoadingMembers || isOrgListLoading;
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({ const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({
onSuccess: () => { onSuccess: () => {
utils.viewer.organizations.listOtherTeams.invalidate(); utils.viewer.organizations.listOtherTeams.invalidate();
@ -162,9 +162,9 @@ const MembersView = () => {
<MembersList <MembersList
members={members} members={members}
team={team} team={team}
setOffset={setOffset} fetchNextPage={fetchNextPage}
offset={offset} hasNextPage={hasNextPage}
displayLoadMore={loadMore} isFetchingNextPage={isFetchingNextPage}
/> />
</> </>

View File

@ -67,6 +67,7 @@ export default function MemberListItem(props: Props) {
await utils.viewer.teams.get.invalidate(); await utils.viewer.teams.get.invalidate();
await utils.viewer.eventTypes.invalidate(); await utils.viewer.eventTypes.invalidate();
await utils.viewer.organizations.listMembers.invalidate(); await utils.viewer.organizations.listMembers.invalidate();
await utils.viewer.organizations.getMembers.invalidate();
showToast(t("success"), "success"); showToast(t("success"), "success");
}, },
async onError(err) { async onError(err) {

View File

@ -206,6 +206,7 @@ const MembersView = () => {
{ {
onSuccess: async (data) => { onSuccess: async (data) => {
await utils.viewer.teams.get.invalidate(); await utils.viewer.teams.get.invalidate();
await utils.viewer.organizations.getMembers.invalidate();
setShowMemberInvitationModal(false); setShowMemberInvitationModal(false);
if (Array.isArray(data.usernameOrEmail)) { if (Array.isArray(data.usernameOrEmail)) {

View File

@ -50,6 +50,24 @@ export const getMembersHandler = async ({ input, ctx }: CreateOptions) => {
}, },
}, },
}); });
if (teamIdToExclude && teamQuery?.members) {
const excludedteamUsers = await prisma.team.findUnique({
where: {
id: teamIdToExclude,
},
select: {
members: {
select: {
userId: true,
},
},
},
});
const excludedUserIds = excludedteamUsers?.members.map((item) => item.userId) ?? [];
teamQuery.members = teamQuery?.members.filter((member) => !excludedUserIds.includes(member.userId));
}
return teamQuery?.members || []; return teamQuery?.members || [];
}; };

View File

@ -8,8 +8,9 @@ import type { TrpcSessionUser } from "../../../trpc";
export const ZListOtherTeamMembersSchema = z.object({ export const ZListOtherTeamMembersSchema = z.object({
teamId: z.number(), teamId: z.number(),
query: z.string().optional(), query: z.string().optional(),
limit: z.number().optional(), limit: z.number(),
offset: z.number().optional(), offset: z.number().optional(),
cursor: z.number().nullish(), // <-- "cursor" needs to exist when using useInfiniteQuery, but can be any type
}); });
export type TListOtherTeamMembersSchema = z.infer<typeof ZListOtherTeamMembersSchema>; export type TListOtherTeamMembersSchema = z.infer<typeof ZListOtherTeamMembersSchema>;
@ -25,11 +26,12 @@ export const listOtherTeamMembers = async ({ input }: ListOptions) => {
const whereConditional: Prisma.MembershipWhereInput = { const whereConditional: Prisma.MembershipWhereInput = {
teamId: input.teamId, teamId: input.teamId,
}; };
const { limit = 20 } = input; // const { limit = 20 } = input;
let { offset = 0 } = input; // let { offset = 0 } = input;
const { cursor, limit } = input;
if (input.query) { if (input.query) {
offset = 0;
whereConditional.user = { whereConditional.user = {
OR: [ OR: [
{ {
@ -73,11 +75,19 @@ export const listOtherTeamMembers = async ({ input }: ListOptions) => {
}, },
distinct: ["userId"], distinct: ["userId"],
orderBy: { role: "desc" }, orderBy: { role: "desc" },
take: limit, cursor: cursor ? { id: cursor } : undefined,
skip: offset, take: limit + 1, // We take +1 as itll be used for the next cursor
}); });
let nextCursor: typeof cursor | undefined = undefined;
if (members && members.length > limit) {
const nextItem = members.pop();
nextCursor = nextItem?.id || null;
}
return members; return {
rows: members || [],
nextCursor,
};
}; };
export default listOtherTeamMembers; export default listOtherTeamMembers;