From f3e07c5223d5f1efe9f483c3ec9d76c0648ae967 Mon Sep 17 00:00:00 2001 From: Bandhan Majumder <133476557+bandhan-majumder@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:45:20 +0530 Subject: [PATCH] chore(member-invite): early return for pending mutations while copying invite link (#28753) * chore(member-invite): early return for pending mutations while copying invite link * typo fix * make rabbit happy --------- Co-authored-by: Romit <85230081+romitg2@users.noreply.github.com> --- .../components/MemberInvitationModal.tsx | 80 +++++++++++-------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/apps/web/modules/ee/teams/components/MemberInvitationModal.tsx b/apps/web/modules/ee/teams/components/MemberInvitationModal.tsx index 6e3c00eb5050f3..11f765a6603d31 100644 --- a/apps/web/modules/ee/teams/components/MemberInvitationModal.tsx +++ b/apps/web/modules/ee/teams/components/MemberInvitationModal.tsx @@ -1,7 +1,7 @@ import { useSession } from "next-auth/react"; import posthog from "posthog-js"; import type { FormEvent } from "react"; -import { useMemo, useRef, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import TeamInviteFromOrg from "~/ee/organizations/components/TeamInviteFromOrg"; @@ -89,6 +89,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps) const [modalImportMode, setModalInputMode] = useState( canSeeOrganization ? "ORGANIZATION" : "INDIVIDUAL" ); + const [isCopying, setIsCopying] = useState(false); const createInviteMutation = trpc.viewer.teams.createInvite.useMutation({ async onSuccess() { @@ -193,6 +194,48 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps) const importRef = useRef(null); +const handleCopyInviteLink = useCallback(async () => { + if (isCopying || createInviteMutation.isPending) return; + + setIsCopying(true); + try { + // Required for Safari but also works on Chrome + // Credits to https://wolfgangrittner.dev/how-to-use-clipboard-api-in-firefox/ + if (typeof ClipboardItem !== "undefined") { + const inviteLinkClipboardItem = new ClipboardItem({ + //eslint-disable-next-line no-async-promise-executor + "text/plain": new Promise((resolve, reject) => { + // Instead of doing async work and then writing to clipboard, do async work in clipboard API itself + createInviteMutation.mutateAsync({ + teamId: props.teamId, + token: props.token, + }).then(({ inviteLink }) => { + resolve(new Blob([inviteLink], { type: "text/plain" })); + }) + .catch((err) => { + reject(err); + }); + }), + }); + await navigator.clipboard.write([inviteLinkClipboardItem]); + showToast(t("invite_link_copied"), "success"); + } else { + // Fallback for browsers that don't support ClipboardItem e.g. Firefox + const { inviteLink } = await createInviteMutation.mutateAsync({ + teamId: props.teamId, + token: props.token, + }); + await navigator.clipboard.writeText(inviteLink); + showToast(t("invite_link_copied"), "success"); + } + } catch (e) { + showToast(t("something_went_wrong_on_our_end"), "error"); + console.error(e); + } finally { + setIsCopying(false); + } + }, [isCopying, createInviteMutation, props.teamId, props.token, t]); + return ( { - // Instead of doing async work and then writing to clipboard, do async work in clipboard API itself - const { inviteLink } = await createInviteMutation.mutateAsync({ - teamId: props.teamId, - token: props.token, - }); - showToast(t("invite_link_copied"), "success"); - resolve(new Blob([inviteLink], { type: "text/plain" })); - }), - }); - await navigator.clipboard.write([inviteLinkClipbardItem]); - } else { - // Fallback for browsers that don't support ClipboardItem e.g. Firefox - const { inviteLink } = await createInviteMutation.mutateAsync({ - teamId: props.teamId, - token: props.token, - }); - await navigator.clipboard.writeText(inviteLink); - showToast(t("invite_link_copied"), "success"); - } - } catch (e) { - showToast(t("something_went_wrong_on_our_end"), "error"); - console.error(e); - } - }} + onClick={handleCopyInviteLink} + loading={isCopying || createInviteMutation.isPending} + disabled={isCopying || createInviteMutation.isPending} className={classNames("gap-2", props.token && "opacity-50")} StartIcon="link" data-testid="copy-invite-link-button">