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