УЛУЧШЕНИЕ - NODEDC LAUNCHER: полные ссылки инвайтов
This commit is contained in:
parent
bd1575d18a
commit
821a4009f0
|
|
@ -2904,7 +2904,7 @@ code {
|
||||||
|
|
||||||
.invite-link-cell {
|
.invite-link-cell {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.45rem;
|
gap: 0.45rem;
|
||||||
}
|
}
|
||||||
|
|
@ -2917,6 +2917,14 @@ code {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invite-link-cell__state {
|
||||||
|
color: var(--accent-lime);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-helper-note {
|
.admin-helper-note {
|
||||||
max-width: 38rem;
|
max-width: 38rem;
|
||||||
margin: 0.22rem 0 0;
|
margin: 0.22rem 0 0;
|
||||||
|
|
|
||||||
|
|
@ -2090,6 +2090,7 @@ function InvitesSection({
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [role, setRole] = useState<ClientMembershipRole>("member");
|
const [role, setRole] = useState<ClientMembershipRole>("member");
|
||||||
const [deleteInviteId, setDeleteInviteId] = useState<string | null>(null);
|
const [deleteInviteId, setDeleteInviteId] = useState<string | null>(null);
|
||||||
|
const [copiedInviteId, setCopiedInviteId] = useState<string | null>(null);
|
||||||
const invites = data.invites.filter((invite) => invite.clientId === clientId);
|
const invites = data.invites.filter((invite) => invite.clientId === clientId);
|
||||||
const deletingInvite = invites.find((invite) => invite.id === deleteInviteId) ?? null;
|
const deletingInvite = invites.find((invite) => invite.id === deleteInviteId) ?? null;
|
||||||
const actor = getUser(data, actorUserId);
|
const actor = getUser(data, actorUserId);
|
||||||
|
|
@ -2102,6 +2103,26 @@ function InvitesSection({
|
||||||
setRole("member");
|
setRole("member");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCopyInvite(invite: Invite) {
|
||||||
|
const inviteUrl = buildInviteUrl(invite.token);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await copyToClipboard(inviteUrl);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCopiedInviteId(invite.id);
|
||||||
|
|
||||||
|
if (invite.status === "created") {
|
||||||
|
onUpdateInvite(invite.id, { status: "sent" });
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
setCopiedInviteId((currentInviteId) => (currentInviteId === invite.id ? null : currentInviteId));
|
||||||
|
}, 1800);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="invites-layout invites-layout--catalog">
|
<div className="invites-layout invites-layout--catalog">
|
||||||
<GlassSurface className="invite-form invite-form--compact">
|
<GlassSurface className="invite-form invite-form--compact">
|
||||||
|
|
@ -2149,7 +2170,11 @@ function InvitesSection({
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{invites.map((invite) => (
|
{invites.map((invite) => {
|
||||||
|
const inviteUrl = buildInviteUrl(invite.token);
|
||||||
|
const isCopied = copiedInviteId === invite.id;
|
||||||
|
|
||||||
|
return (
|
||||||
<tr key={invite.id}>
|
<tr key={invite.id}>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
|
|
@ -2180,12 +2205,13 @@ function InvitesSection({
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="invite-link-cell">
|
<div className="invite-link-cell">
|
||||||
<code>{`/invite/${invite.token}`}</code>
|
<code title={inviteUrl}>{inviteUrl}</code>
|
||||||
|
{isCopied ? <span className="invite-link-cell__state">Скопировано</span> : null}
|
||||||
<IconButton
|
<IconButton
|
||||||
label={`Скопировать инвайт ${invite.email}`}
|
label={isCopied ? `Инвайт ${invite.email} скопирован` : `Скопировать инвайт ${invite.email}`}
|
||||||
className="admin-circle-action services-admin-table__edit"
|
className="admin-circle-action services-admin-table__edit"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void navigator.clipboard?.writeText(`/invite/${invite.token}`)}
|
onClick={() => void handleCopyInvite(invite)}
|
||||||
>
|
>
|
||||||
<Copy size={14} />
|
<Copy size={14} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
@ -2211,7 +2237,8 @@ function InvitesSection({
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</GlassSurface>
|
</GlassSurface>
|
||||||
|
|
@ -2233,6 +2260,38 @@ function InvitesSection({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildInviteUrl(token: string) {
|
||||||
|
if (typeof window === "undefined") return `/invite/${token}`;
|
||||||
|
|
||||||
|
return new URL(`/invite/${token}`, window.location.origin).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyToClipboard(value: string) {
|
||||||
|
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof document === "undefined") {
|
||||||
|
throw new Error("Clipboard API is not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = document.createElement("textarea");
|
||||||
|
input.value = value;
|
||||||
|
input.setAttribute("readonly", "");
|
||||||
|
input.style.position = "fixed";
|
||||||
|
input.style.opacity = "0";
|
||||||
|
document.body.append(input);
|
||||||
|
input.select();
|
||||||
|
|
||||||
|
const copied = document.execCommand("copy");
|
||||||
|
input.remove();
|
||||||
|
|
||||||
|
if (!copied) {
|
||||||
|
throw new Error("Fallback copy failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function SyncSection({
|
function SyncSection({
|
||||||
data,
|
data,
|
||||||
clientId,
|
clientId,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue