Implement invitation renewal and optimize revalidation function
This commit adds a new function to renew team invitations and a central function for revalidating member page. It also removes redundant revalidations across different actions. The renew invitation function and UI elements are introduced including a new dialog for confirming the renewal action. Furthermore, function revalidateMemberPage() is added to abstract the revalidation path used multiple times in different functions. The code readability and maintainability have thus been improved.
This commit is contained in:
@@ -7,6 +7,7 @@ import { Ellipsis } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { DataTable } from '@kit/ui/data-table';
|
||||
import {
|
||||
@@ -22,6 +23,7 @@ import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { RoleBadge } from '../members/role-badge';
|
||||
import { DeleteInvitationDialog } from './delete-invitation-dialog';
|
||||
import { RenewInvitationDialog } from './renew-invitation-dialog';
|
||||
import { UpdateInvitationDialog } from './update-invitation-dialog';
|
||||
|
||||
type Invitations =
|
||||
@@ -107,6 +109,24 @@ function useGetColumns(permissions: {
|
||||
return new Date(row.original.created_at).toLocaleDateString();
|
||||
},
|
||||
},
|
||||
{
|
||||
header: t('expiresAtLabel'),
|
||||
cell: ({ row }) => {
|
||||
return new Date(row.original.expires_at).toLocaleDateString();
|
||||
},
|
||||
},
|
||||
{
|
||||
header: t('inviteStatus'),
|
||||
cell: ({ row }) => {
|
||||
const isExpired = getIsInviteExpired(row.original.expires_at);
|
||||
|
||||
if (isExpired) {
|
||||
return <Badge variant={'warning'}>{t('expired')}</Badge>;
|
||||
}
|
||||
|
||||
return <Badge variant={'success'}>{t('active')}</Badge>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
id: 'actions',
|
||||
@@ -131,6 +151,7 @@ function ActionsDropdown({
|
||||
}) {
|
||||
const [isDeletingInvite, setIsDeletingInvite] = useState(false);
|
||||
const [isUpdatingRole, setIsUpdatingRole] = useState(false);
|
||||
const [iRenewingInvite, setIsRenewingInvite] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -146,6 +167,12 @@ function ActionsDropdown({
|
||||
<DropdownMenuItem onClick={() => setIsUpdatingRole(true)}>
|
||||
<Trans i18nKey={'teams:updateInvitation'} />
|
||||
</DropdownMenuItem>
|
||||
|
||||
<If condition={getIsInviteExpired(invitation.expires_at)}>
|
||||
<DropdownMenuItem onClick={() => setIsRenewingInvite(true)}>
|
||||
<Trans i18nKey={'teams:renewInvitation'} />
|
||||
</DropdownMenuItem>
|
||||
</If>
|
||||
</If>
|
||||
|
||||
<If condition={permissions.canRemoveInvitation}>
|
||||
@@ -172,6 +199,24 @@ function ActionsDropdown({
|
||||
userRole={invitation.role}
|
||||
/>
|
||||
</If>
|
||||
|
||||
<If condition={iRenewingInvite}>
|
||||
<RenewInvitationDialog
|
||||
isOpen
|
||||
setIsOpen={setIsRenewingInvite}
|
||||
invitationId={invitation.id}
|
||||
email={invitation.email}
|
||||
/>
|
||||
</If>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getIsInviteExpired(isoExpiresAt: string) {
|
||||
const currentIsoTime = new Date().toISOString();
|
||||
|
||||
const isoExpiresAtDate = new Date(isoExpiresAt);
|
||||
const currentIsoTimeDate = new Date(currentIsoTime);
|
||||
|
||||
return isoExpiresAtDate < currentIsoTimeDate;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@kit/ui/alert-dialog';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { renewInvitationAction } from '../../server/actions/team-invitations-server-actions';
|
||||
|
||||
export const RenewInvitationDialog: React.FC<{
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
invitationId: number;
|
||||
email: string;
|
||||
}> = ({ isOpen, setIsOpen, invitationId, email }) => {
|
||||
return (
|
||||
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey="team:renewInvitation" />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans
|
||||
i18nKey="team:renewInvitationDialogDescription"
|
||||
values={{ email }}
|
||||
/>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<RenewInvitationForm
|
||||
setIsOpen={setIsOpen}
|
||||
invitationId={invitationId}
|
||||
/>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
function RenewInvitationForm({
|
||||
invitationId,
|
||||
setIsOpen,
|
||||
}: {
|
||||
invitationId: number;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}) {
|
||||
const [isSubmitting, startTransition] = useTransition();
|
||||
const [error, setError] = useState<boolean>();
|
||||
|
||||
const inInvitationRenewed = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await renewInvitationAction({ invitationId });
|
||||
|
||||
setIsOpen(false);
|
||||
} catch (e) {
|
||||
setError(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form action={inInvitationRenewed}>
|
||||
<div className={'flex flex-col space-y-6'}>
|
||||
<p className={'text-muted-foreground text-sm'}>
|
||||
<Trans i18nKey={'common:modalConfirmationQuestion'} />
|
||||
</p>
|
||||
|
||||
<If condition={error}>
|
||||
<RenewInvitationErrorAlert />
|
||||
</If>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<Button
|
||||
data-test={'confirm-renew-invitation'}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Trans i18nKey={'teams:renewInvitation'} />
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function RenewInvitationErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'teams:renewInvitationErrorTitle'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'teams:renewInvitationErrorDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export async function createInvitationsAction(params: {
|
||||
|
||||
await service.sendInvitations({ invitations, account: params.account });
|
||||
|
||||
revalidatePath('/home/[account]/members', 'page');
|
||||
revalidateMemberPage();
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
@@ -65,6 +65,8 @@ export async function deleteInvitationAction(
|
||||
|
||||
await service.deleteInvitation(invitation);
|
||||
|
||||
revalidateMemberPage();
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -80,6 +82,8 @@ export async function updateInvitationAction(
|
||||
|
||||
await service.updateInvitation(invitation);
|
||||
|
||||
revalidateMemberPage();
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -103,6 +107,21 @@ export async function acceptInvitationAction(data: FormData) {
|
||||
return redirect(nextPath);
|
||||
}
|
||||
|
||||
export async function renewInvitationAction(params: { invitationId: number }) {
|
||||
const client = getSupabaseServerActionClient();
|
||||
const { invitationId } = params;
|
||||
|
||||
await assertSession(client);
|
||||
|
||||
const service = new AccountInvitationsService(client);
|
||||
|
||||
await service.renewInvitation(invitationId);
|
||||
|
||||
revalidateMemberPage();
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function assertSession(client: SupabaseClient<Database>) {
|
||||
const { error, data } = await requireAuth(client);
|
||||
|
||||
@@ -112,3 +131,7 @@ async function assertSession(client: SupabaseClient<Database>) {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function revalidateMemberPage() {
|
||||
revalidatePath('/home/[account]/members', 'page');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { addDays, formatISO } from 'date-fns';
|
||||
import 'server-only';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -227,6 +228,35 @@ export class AccountInvitationsService {
|
||||
return data;
|
||||
}
|
||||
|
||||
async renewInvitation(invitationId: number) {
|
||||
Logger.info('Renewing invitation', {
|
||||
invitationId,
|
||||
name: this.namespace,
|
||||
});
|
||||
|
||||
const sevenDaysFromNow = formatISO(addDays(new Date(), 7));
|
||||
|
||||
const { data, error } = await this.client
|
||||
.from('invitations')
|
||||
.update({
|
||||
expires_at: sevenDaysFromNow,
|
||||
})
|
||||
.match({
|
||||
id: invitationId,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
Logger.info('Invitation successfully renewed', {
|
||||
invitationId,
|
||||
name: this.namespace,
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private async getUser() {
|
||||
const { data, error } = await requireAuth(this.client);
|
||||
|
||||
|
||||
@@ -481,20 +481,13 @@ export type Database = {
|
||||
};
|
||||
};
|
||||
Functions: {
|
||||
accept_invitation:
|
||||
| {
|
||||
Args: {
|
||||
invite_token: string;
|
||||
};
|
||||
Returns: undefined;
|
||||
}
|
||||
| {
|
||||
Args: {
|
||||
token: string;
|
||||
user_id: string;
|
||||
};
|
||||
Returns: undefined;
|
||||
};
|
||||
accept_invitation: {
|
||||
Args: {
|
||||
token: string;
|
||||
user_id: string;
|
||||
};
|
||||
Returns: undefined;
|
||||
};
|
||||
add_invitations_to_account: {
|
||||
Args: {
|
||||
account_slug: string;
|
||||
@@ -592,6 +585,7 @@ export type Database = {
|
||||
role: Database['public']['Enums']['account_role'];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
expires_at: string;
|
||||
inviter_name: string;
|
||||
inviter_email: string;
|
||||
}[];
|
||||
|
||||
Reference in New Issue
Block a user