Auto-copy invite link on creation and replace Copy button with inline icon
- Invite link is now automatically copied to clipboard when created - "Copied" badge appears next to the share link header for 2s - Standalone "Copy link" button replaced with a small copy icon next to the link - Icon toggles to a green checkmark while "copied" feedback is shown Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import { companiesApi } from "../api/companies";
|
|||||||
import { accessApi } from "../api/access";
|
import { accessApi } from "../api/access";
|
||||||
import { queryKeys } from "../lib/queryKeys";
|
import { queryKeys } from "../lib/queryKeys";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Settings } from "lucide-react";
|
import { Settings, Check, Copy } from "lucide-react";
|
||||||
import { CompanyPatternIcon } from "../components/CompanyPatternIcon";
|
import { CompanyPatternIcon } from "../components/CompanyPatternIcon";
|
||||||
import { Field, ToggleField, HintIcon } from "../components/agent-config-primitives";
|
import { Field, ToggleField, HintIcon } from "../components/agent-config-primitives";
|
||||||
|
|
||||||
@@ -30,6 +30,7 @@ export function CompanySettings() {
|
|||||||
|
|
||||||
const [inviteLink, setInviteLink] = useState<string | null>(null);
|
const [inviteLink, setInviteLink] = useState<string | null>(null);
|
||||||
const [inviteError, setInviteError] = useState<string | null>(null);
|
const [inviteError, setInviteError] = useState<string | null>(null);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const generalDirty =
|
const generalDirty =
|
||||||
!!selectedCompany &&
|
!!selectedCompany &&
|
||||||
@@ -61,13 +62,18 @@ export function CompanySettings() {
|
|||||||
allowedJoinTypes: "both",
|
allowedJoinTypes: "both",
|
||||||
expiresInHours: 72,
|
expiresInHours: 72,
|
||||||
}),
|
}),
|
||||||
onSuccess: (invite) => {
|
onSuccess: async (invite) => {
|
||||||
setInviteError(null);
|
setInviteError(null);
|
||||||
const base = window.location.origin.replace(/\/+$/, "");
|
const base = window.location.origin.replace(/\/+$/, "");
|
||||||
const absoluteUrl = invite.inviteUrl.startsWith("http")
|
const absoluteUrl = invite.inviteUrl.startsWith("http")
|
||||||
? invite.inviteUrl
|
? invite.inviteUrl
|
||||||
: `${base}${invite.inviteUrl}`;
|
: `${base}${invite.inviteUrl}`;
|
||||||
setInviteLink(absoluteUrl);
|
setInviteLink(absoluteUrl);
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(absoluteUrl);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch { /* clipboard may not be available */ }
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId!) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId!) });
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
@@ -247,27 +253,38 @@ export function CompanySettings() {
|
|||||||
<span className="text-xs text-muted-foreground">Generate a link to invite humans or agents to this company.</span>
|
<span className="text-xs text-muted-foreground">Generate a link to invite humans or agents to this company.</span>
|
||||||
<HintIcon text="Invite links expire after 72 hours and allow both human and agent joins." />
|
<HintIcon text="Invite links expire after 72 hours and allow both human and agent joins." />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<Button size="sm" onClick={() => inviteMutation.mutate()} disabled={inviteMutation.isPending}>
|
||||||
<Button size="sm" onClick={() => inviteMutation.mutate()} disabled={inviteMutation.isPending}>
|
{inviteMutation.isPending ? "Creating..." : "Create invite link"}
|
||||||
{inviteMutation.isPending ? "Creating..." : "Create invite link"}
|
</Button>
|
||||||
</Button>
|
|
||||||
{inviteLink && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={async () => {
|
|
||||||
await navigator.clipboard.writeText(inviteLink);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Copy link
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{inviteError && <p className="text-sm text-destructive">{inviteError}</p>}
|
{inviteError && <p className="text-sm text-destructive">{inviteError}</p>}
|
||||||
{inviteLink && (
|
{inviteLink && (
|
||||||
<div className="rounded-md border border-border bg-muted/30 p-2">
|
<div className="rounded-md border border-border bg-muted/30 p-2">
|
||||||
<div className="text-xs text-muted-foreground">Share link</div>
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="mt-1 break-all font-mono text-xs">{inviteLink}</div>
|
<div className="text-xs text-muted-foreground">Share link</div>
|
||||||
|
{copied && (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-green-600">
|
||||||
|
<Check className="h-3 w-3" />
|
||||||
|
Copied
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 flex items-center gap-1.5">
|
||||||
|
<div className="flex-1 break-all font-mono text-xs">{inviteLink}</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="shrink-0 rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(inviteLink);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch { /* clipboard may not be available */ }
|
||||||
|
}}
|
||||||
|
title="Copy link"
|
||||||
|
>
|
||||||
|
{copied ? <Check className="h-3.5 w-3.5 text-green-600" /> : <Copy className="h-3.5 w-3.5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user