feat: add copy invite link to invitation action menu (#360)

* +copy invite link to clipboard from invite action menu

* -remove log to console for copy link action

* Refactor copy invite link feature

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
This commit is contained in:
Peter Shcherbakov 2025-02-26 21:28:44 +03:00 committed by GitHub
parent 54d27af76a
commit 7fc1a782a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 95 additions and 2 deletions

View File

@ -5,8 +5,10 @@ import { modals } from "@mantine/modals";
import { import {
useResendInvitationMutation, useResendInvitationMutation,
useRevokeInvitationMutation, useRevokeInvitationMutation,
useGetInviteLink,
} from "@/features/workspace/queries/workspace-query.ts"; } from "@/features/workspace/queries/workspace-query.ts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { notifications } from "@mantine/notifications";
interface Props { interface Props {
invitationId: string; invitationId: string;
@ -15,6 +17,17 @@ export default function InviteActionMenu({ invitationId }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const resendInvitationMutation = useResendInvitationMutation(); const resendInvitationMutation = useResendInvitationMutation();
const revokeInvitationMutation = useRevokeInvitationMutation(); const revokeInvitationMutation = useRevokeInvitationMutation();
const { data: inviteLink, error, } = useGetInviteLink(invitationId);
const onCopyLink = async () => {
if (error) {
notifications.show({ message: error.message, color: "red" })
} else {
navigator.clipboard.writeText(inviteLink.inviteLink)
notifications.show({ message: "Invite link copied to clipboard!"})
}
}
const onResend = async () => { const onResend = async () => {
await resendInvitationMutation.mutateAsync({ invitationId }); await resendInvitationMutation.mutateAsync({ invitationId });
@ -58,6 +71,8 @@ export default function InviteActionMenu({ invitationId }: Props) {
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Item onClick={onResend}>{t("Resend invitation")}</Menu.Item> <Menu.Item onClick={onResend}>{t("Resend invitation")}</Menu.Item>
<Menu.Item onClick={onCopyLink}>Copy invite link</Menu.Item>
<Menu.Item onClick={onResend}>Resend invitation</Menu.Item>
<Menu.Divider /> <Menu.Divider />
<Menu.Item <Menu.Item
c="red" c="red"

View File

@ -14,6 +14,7 @@ import {
resendInvitation, resendInvitation,
revokeInvitation, revokeInvitation,
getWorkspace, getWorkspace,
getInviteLink,
getWorkspacePublicData, getWorkspacePublicData,
} from "@/features/workspace/services/workspace-service"; } from "@/features/workspace/services/workspace-service";
import { IPagination, QueryParams } from "@/lib/types.ts"; import { IPagination, QueryParams } from "@/lib/types.ts";
@ -21,6 +22,7 @@ import { notifications } from "@mantine/notifications";
import { import {
ICreateInvite, ICreateInvite,
IInvitation, IInvitation,
IInvitationLink,
IWorkspace, IWorkspace,
} from "@/features/workspace/types/workspace.types.ts"; } from "@/features/workspace/types/workspace.types.ts";
import { IUser } from "@/features/user/types/user.types.ts"; import { IUser } from "@/features/user/types/user.types.ts";
@ -80,6 +82,15 @@ export function useWorkspaceInvitationsQuery(
}); });
} }
export function useGetInviteLink(
invitationId: string
): UseQueryResult<IInvitationLink,Error> {
return useQuery({
queryKey:["inviteLink",invitationId],
queryFn: () => getInviteLink({ invitationId }),
})
}
export function useCreateInvitationMutation() { export function useCreateInvitationMutation() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();

View File

@ -5,6 +5,7 @@ import {
IInvitation, IInvitation,
IWorkspace, IWorkspace,
IAcceptInvite, IAcceptInvite,
IInvitationLink,
} from "../types/workspace.types"; } from "../types/workspace.types";
import { IPagination, QueryParams } from "@/lib/types.ts"; import { IPagination, QueryParams } from "@/lib/types.ts";
@ -53,6 +54,13 @@ export async function acceptInvitation(data: IAcceptInvite): Promise<void> {
await api.post<void>("/workspace/invites/accept", data); await api.post<void>("/workspace/invites/accept", data);
} }
export async function getInviteLink(data: {
invitationId: string;
}): Promise<IInvitationLink> {
const req = await api.post("/workspace/invites/link", data);
return req.data;
}
export async function resendInvitation(data: { export async function resendInvitation(data: {
invitationId: string; invitationId: string;
}): Promise<void> { }): Promise<void> {

View File

@ -28,6 +28,10 @@ export interface IInvitation {
createdAt: Date; createdAt: Date;
} }
export interface IInvitationLink {
inviteLink: string;
}
export interface IAcceptInvite { export interface IAcceptInvite {
invitationId: string; invitationId: string;
name: string; name: string;

View File

@ -237,4 +237,30 @@ export class WorkspaceController {
secure: this.environmentService.isHttps(), secure: this.environmentService.isHttps(),
}); });
} }
@HttpCode(HttpStatus.OK)
@Post('invites/link')
async getInviteLink(
@Body() inviteDto: InvitationIdDto,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
if (this.environmentService.isCloud()) {
throw new ForbiddenException();
}
const ability = this.workspaceAbility.createForUser(user, workspace);
if (
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Member)
) {
throw new ForbiddenException();
}
const inviteLink =
await this.workspaceInvitationService.getInvitationLinkById(
inviteDto.invitationId,
workspace.id,
);
return { inviteLink };
}
} }

View File

@ -71,6 +71,21 @@ export class WorkspaceInvitationService {
return invitation; return invitation;
} }
async getInvitationTokenById(invitationId: string, workspaceId: string) {
const invitation = await this.db
.selectFrom('workspaceInvitations')
.select(['token'])
.where('id', '=', invitationId)
.where('workspaceId', '=', workspaceId)
.executeTakeFirst();
if (!invitation) {
throw new NotFoundException('Invitation not found');
}
return invitation;
}
async createInvitation( async createInvitation(
inviteUserDto: InviteUserDto, inviteUserDto: InviteUserDto,
workspaceId: string, workspaceId: string,
@ -256,7 +271,6 @@ export class WorkspaceInvitationService {
invitationId: string, invitationId: string,
workspaceId: string, workspaceId: string,
): Promise<void> { ): Promise<void> {
//
const invitation = await this.db const invitation = await this.db
.selectFrom('workspaceInvitations') .selectFrom('workspaceInvitations')
.selectAll() .selectAll()
@ -292,13 +306,28 @@ export class WorkspaceInvitationService {
.execute(); .execute();
} }
async getInvitationLinkById(
invitationId: string,
workspaceId: string,
): Promise<string> {
const token = await this.getInvitationTokenById(invitationId, workspaceId);
return this.buildInviteLink(invitationId, token.token);
}
async buildInviteLink(
invitationId: string,
inviteToken: string,
): Promise<string> {
return `${this.environmentService.getAppUrl()}/invites/${invitationId}?token=${inviteToken}`;
}
async sendInvitationMail( async sendInvitationMail(
invitationId: string, invitationId: string,
inviteeEmail: string, inviteeEmail: string,
inviteToken: string, inviteToken: string,
invitedByName: string, invitedByName: string,
): Promise<void> { ): Promise<void> {
const inviteLink = `${this.environmentService.getAppUrl()}/invites/${invitationId}?token=${inviteToken}`; const inviteLink = await this.buildInviteLink(invitationId, inviteToken);
const emailTemplate = InvitationEmail({ const emailTemplate = InvitationEmail({
inviteLink, inviteLink,