feat(web-admin): show copyable invite link after sending invite

After a platform admin sends an invite, the generated invite URL is
displayed inline with a one-click copy button so it can be shared via
any channel (email, WeChat, etc.). Link auto-dismisses when the invite
form is reopened.

Also adds i18n keys for invite link UI in en/zh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-07 05:37:30 -08:00
parent e31baa1f40
commit 6e50f4cc50
3 changed files with 42 additions and 3 deletions

View File

@ -225,6 +225,8 @@ export default function TenantDetailPage() {
const [showInviteForm, setShowInviteForm] = useState(false); const [showInviteForm, setShowInviteForm] = useState(false);
const [inviteEmail, setInviteEmail] = useState(''); const [inviteEmail, setInviteEmail] = useState('');
const [inviteRole, setInviteRole] = useState('viewer'); const [inviteRole, setInviteRole] = useState('viewer');
const [inviteLink, setInviteLink] = useState<string | null>(null);
const [linkCopied, setLinkCopied] = useState(false);
const [form, setForm] = useState<TenantFormData>({ const [form, setForm] = useState<TenantFormData>({
name: '', name: '',
plan: 'free', plan: 'free',
@ -297,12 +299,15 @@ export default function TenantDetailPage() {
const sendInviteMutation = useMutation({ const sendInviteMutation = useMutation({
mutationFn: (body: { email: string; role: string }) => mutationFn: (body: { email: string; role: string }) =>
apiClient(`/api/v1/admin/tenants/${id}/invites`, { method: 'POST', body }), apiClient<{ token: string }>(`/api/v1/admin/tenants/${id}/invites`, { method: 'POST', body }),
onSuccess: () => { onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: queryKeys.tenants.invites(id) }); queryClient.invalidateQueries({ queryKey: queryKeys.tenants.invites(id) });
setShowInviteForm(false); setShowInviteForm(false);
setInviteEmail(''); setInviteEmail('');
setInviteRole('viewer'); setInviteRole('viewer');
const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
setInviteLink(`${baseUrl}/invite/${data.token}`);
setLinkCopied(false);
}, },
}); });
@ -686,13 +691,39 @@ export default function TenantDetailPage() {
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">{t('detail.invitations')}</h2> <h2 className="text-lg font-semibold">{t('detail.invitations')}</h2>
<button <button
onClick={() => setShowInviteForm(!showInviteForm)} onClick={() => { setShowInviteForm(!showInviteForm); setInviteLink(null); }}
className="px-3 py-1 text-xs rounded-md bg-primary text-primary-foreground hover:opacity-90" className="px-3 py-1 text-xs rounded-md bg-primary text-primary-foreground hover:opacity-90"
> >
{t('detail.inviteUser')} {t('detail.inviteUser')}
</button> </button>
</div> </div>
{inviteLink && (
<div className="mb-4 p-4 border border-green-500/50 rounded-md bg-green-500/10 space-y-2">
<p className="text-sm font-medium text-green-700 dark:text-green-400">
{t('detail.inviteLinkReady')}
</p>
<div className="flex gap-2 items-center">
<input
readOnly
value={inviteLink}
className="flex-1 px-2 py-1 text-xs font-mono bg-background border rounded-md"
/>
<button
onClick={() => {
navigator.clipboard.writeText(inviteLink);
setLinkCopied(true);
setTimeout(() => setLinkCopied(false), 2000);
}}
className="px-3 py-1 text-xs rounded-md bg-primary text-primary-foreground hover:opacity-90 shrink-0"
>
{linkCopied ? t('detail.copied') : t('detail.copy')}
</button>
</div>
<p className="text-xs text-muted-foreground">{t('detail.inviteLinkHint')}</p>
</div>
)}
{showInviteForm && ( {showInviteForm && (
<div className="mb-4 p-4 border rounded-md bg-muted/30 space-y-3"> <div className="mb-4 p-4 border rounded-md bg-muted/30 space-y-3">
<div> <div>

View File

@ -66,6 +66,10 @@
"sendInvite": "Send Invite", "sendInvite": "Send Invite",
"sending": "Sending...", "sending": "Sending...",
"revoke": "Revoke", "revoke": "Revoke",
"inviteLinkReady": "Invitation link generated — share it with the user:",
"copy": "Copy",
"copied": "Copied!",
"inviteLinkHint": "This link is valid for 7 days. The user can use it to create their account.",
"suspendTenant": "Suspend Tenant", "suspendTenant": "Suspend Tenant",
"activateTenant": "Activate Tenant", "activateTenant": "Activate Tenant",
"viewAuditLog": "View Audit Log", "viewAuditLog": "View Audit Log",

View File

@ -66,6 +66,10 @@
"sendInvite": "发送邀请", "sendInvite": "发送邀请",
"sending": "正在发送...", "sending": "正在发送...",
"revoke": "撤销", "revoke": "撤销",
"inviteLinkReady": "邀请链接已生成,将其发送给用户:",
"copy": "复制",
"copied": "已复制!",
"inviteLinkHint": "此链接有效期 7 天,用户通过该链接创建账号后即可登录。",
"suspendTenant": "停用租户", "suspendTenant": "停用租户",
"activateTenant": "激活租户", "activateTenant": "激活租户",
"viewAuditLog": "查看审计日志", "viewAuditLog": "查看审计日志",