This commit is contained in:
David Nguyen
2025-02-05 00:57:00 +11:00
parent 540cc5bfc1
commit 1057ae6d2a
105 changed files with 379 additions and 357 deletions

View File

@ -3,6 +3,7 @@ import { useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import type { User } from '@prisma/client';
import { useNavigate } from 'react-router';
import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
@ -29,7 +30,7 @@ export type AdminUserDeleteDialogProps = {
export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const navigate = useNavigate();
const [email, setEmail] = useState('');
const { mutateAsync: deleteUser, isPending: isDeletingUser } =
@ -41,14 +42,13 @@ export const AdminUserDeleteDialog = ({ className, user }: AdminUserDeleteDialog
id: user.id,
});
await navigate('/admin/users');
toast({
title: _(msg`Account deleted`),
description: _(msg`The account has been deleted successfully.`),
duration: 5000,
});
// todo
// router.push('/admin/users');
} catch (err) {
const error = AppError.parseError(err);

View File

@ -24,6 +24,7 @@ type DocumentDeleteDialogProps = {
id: number;
open: boolean;
onOpenChange: (_open: boolean) => void;
onDelete?: () => Promise<void> | void;
status: DocumentStatus;
documentTitle: string;
teamId?: number;
@ -34,6 +35,7 @@ export const DocumentDeleteDialog = ({
id,
open,
onOpenChange,
onDelete,
status,
documentTitle,
canManageDocument,
@ -48,9 +50,7 @@ export const DocumentDeleteDialog = ({
const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
const { mutateAsync: deleteDocument, isPending } = trpcReact.document.deleteDocument.useMutation({
onSuccess: () => {
// todo
// router.refresh();
onSuccess: async () => {
void refreshLimits();
toast({
@ -59,8 +59,18 @@ export const DocumentDeleteDialog = ({
duration: 5000,
});
await onDelete?.();
onOpenChange(false);
},
onError: () => {
toast({
title: _(msg`Something went wrong`),
description: _(msg`This document could not be deleted at this time. Please try again.`),
variant: 'destructive',
duration: 7500,
});
},
});
useEffect(() => {
@ -70,19 +80,6 @@ export const DocumentDeleteDialog = ({
}
}, [open, status]);
const onDelete = async () => {
try {
await deleteDocument({ documentId: id });
} catch {
toast({
title: _(msg`Something went wrong`),
description: _(msg`This document could not be deleted at this time. Please try again.`),
variant: 'destructive',
duration: 7500,
});
}
};
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
setIsDeleteEnabled(event.target.value === _(deleteMessage));
@ -191,7 +188,7 @@ export const DocumentDeleteDialog = ({
<Button
type="button"
loading={isPending}
onClick={onDelete}
onClick={() => void deleteDocument({ documentId: id })}
disabled={!isDeleteEnabled && canManageDocument}
variant="destructive"
>

View File

@ -3,7 +3,7 @@ import { useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { trpc } from '@documenso/trpc/react';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
@ -40,14 +40,12 @@ export const DocumentMoveDialog = ({ documentId, open, onOpenChange }: DocumentM
const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({
onSuccess: () => {
// todo
// router.refresh();
toast({
title: _(msg`Document moved`),
description: _(msg`The document has been successfully moved to the selected team.`),
duration: 5000,
});
onOpenChange(false);
},
onError: (error) => {
@ -95,9 +93,7 @@ export const DocumentMoveDialog = ({ documentId, open, onOpenChange }: DocumentM
<div className="flex items-center gap-4">
<Avatar className="h-8 w-8">
{team.avatarImageId && (
<AvatarImage
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`}
/>
<AvatarImage src={formatAvatarUrl(team.avatarImageId)} />
)}
<AvatarFallback className="text-sm text-gray-400">

View File

@ -35,9 +35,10 @@ import {
} from '@documenso/ui/primitives/form/form';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { StackAvatar } from '~/components/(dashboard)/avatar/stack-avatar';
import { useOptionalCurrentTeam } from '~/providers/team';
import { StackAvatar } from '../general/stack-avatar';
const FORM_ID = 'resend-email';
export type DocumentResendDialogProps = {

View File

@ -17,15 +17,21 @@ type TemplateDeleteDialogProps = {
id: number;
open: boolean;
onOpenChange: (_open: boolean) => void;
onDelete?: () => Promise<void> | void;
};
export const TemplateDeleteDialog = ({ id, open, onOpenChange }: TemplateDeleteDialogProps) => {
export const TemplateDeleteDialog = ({
id,
open,
onOpenChange,
onDelete,
}: TemplateDeleteDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { mutateAsync: deleteTemplate, isPending } = trpcReact.template.deleteTemplate.useMutation({
onSuccess: () => {
// router.refresh(); // Todo
onSuccess: async () => {
await onDelete?.();
toast({
title: _(msg`Template deleted`),

View File

@ -30,8 +30,6 @@ export const TemplateDuplicateDialog = ({
const { mutateAsync: duplicateTemplate, isPending } =
trpcReact.template.duplicateTemplate.useMutation({
onSuccess: () => {
// router.refresh(); // Todo
toast({
title: _(msg`Template duplicated`),
description: _(msg`Your template has been duplicated successfully.`),

View File

@ -4,8 +4,8 @@ import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { match } from 'ts-pattern';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { trpc } from '@documenso/trpc/react';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
@ -30,23 +30,42 @@ type TemplateMoveDialogProps = {
templateId: number;
open: boolean;
onOpenChange: (_open: boolean) => void;
onMove?: ({
templateId,
teamUrl,
}: {
templateId: number;
teamUrl: string;
}) => Promise<void> | void;
};
export const TemplateMoveDialog = ({ templateId, open, onOpenChange }: TemplateMoveDialogProps) => {
export const TemplateMoveDialog = ({
templateId,
open,
onOpenChange,
onMove,
}: TemplateMoveDialogProps) => {
const { toast } = useToast();
const { _ } = useLingui();
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
const { mutateAsync: moveTemplate, isPending } = trpc.template.moveTemplateToTeam.useMutation({
onSuccess: () => {
// router.refresh(); // Todo
onSuccess: async () => {
const team = teams?.find((team) => team.id === selectedTeamId);
if (team) {
await onMove?.({ templateId, teamUrl: team.url });
}
toast({
title: _(msg`Template moved`),
description: _(msg`The template has been successfully moved to the selected team.`),
duration: 5000,
});
onOpenChange(false);
},
onError: (err) => {
@ -69,7 +88,7 @@ export const TemplateMoveDialog = ({ templateId, open, onOpenChange }: TemplateM
},
});
const onMove = async () => {
const handleOnMove = async () => {
if (!selectedTeamId) {
return;
}
@ -104,9 +123,7 @@ export const TemplateMoveDialog = ({ templateId, open, onOpenChange }: TemplateM
<div className="flex items-center gap-4">
<Avatar className="h-8 w-8">
{team.avatarImageId && (
<AvatarImage
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`}
/>
<AvatarImage src={formatAvatarUrl(team.avatarImageId)} />
)}
<AvatarFallback className="text-sm text-gray-400">
@ -126,7 +143,11 @@ export const TemplateMoveDialog = ({ templateId, open, onOpenChange }: TemplateM
<Button variant="secondary" onClick={() => onOpenChange(false)}>
<Trans>Cancel</Trans>
</Button>
<Button onClick={onMove} loading={isPending} disabled={!selectedTeamId || isPending}>
<Button
onClick={handleOnMove}
loading={isPending}
disabled={!selectedTeamId || isPending}
>
{isPending ? <Trans>Moving...</Trans> : <Trans>Move</Trans>}
</Button>
</DialogFooter>

View File

@ -0,0 +1,188 @@
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import type { ApiToken } from '@prisma/client';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type TokenDeleteDialogProps = {
teamId?: number;
token: Pick<ApiToken, 'id' | 'name'>;
onDelete?: () => void;
children?: React.ReactNode;
};
export default function TokenDeleteDialog({
teamId,
token,
onDelete,
children,
}: TokenDeleteDialogProps) {
const { _ } = useLingui();
const { toast } = useToast();
const [isOpen, setIsOpen] = useState(false);
const deleteMessage = _(msg`delete ${token.name}`);
const ZTokenDeleteDialogSchema = z.object({
tokenName: z.literal(deleteMessage, {
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
}),
});
type TDeleteTokenByIdMutationSchema = z.infer<typeof ZTokenDeleteDialogSchema>;
const { mutateAsync: deleteTokenMutation } = trpc.apiToken.deleteTokenById.useMutation({
onSuccess() {
onDelete?.();
},
});
const form = useForm<TDeleteTokenByIdMutationSchema>({
resolver: zodResolver(ZTokenDeleteDialogSchema),
values: {
tokenName: '',
},
});
const onSubmit = async () => {
try {
await deleteTokenMutation({
id: token.id,
teamId,
});
toast({
title: _(msg`Token deleted`),
description: _(msg`The token was deleted successfully.`),
duration: 5000,
});
setIsOpen(false);
} catch (error) {
toast({
title: _(msg`An unknown error occurred`),
description: _(
msg`We encountered an unknown error while attempting to delete this token. Please try again later.`,
),
variant: 'destructive',
duration: 5000,
});
}
};
useEffect(() => {
if (!isOpen) {
form.reset();
}
}, [isOpen, form]);
return (
<Dialog
open={isOpen}
onOpenChange={(value) => !form.formState.isSubmitting && setIsOpen(value)}
>
<DialogTrigger asChild={true}>
{children ?? (
<Button className="mr-4" variant="destructive">
<Trans>Delete</Trans>
</Button>
)}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans>Are you sure you want to delete this token?</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
Please note that this action is irreversible. Once confirmed, your token will be
permanently deleted.
</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<fieldset
className="flex h-full flex-col space-y-4"
disabled={form.formState.isSubmitting}
>
<FormField
control={form.control}
name="tokenName"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>
Confirm by typing:{' '}
<span className="font-sm text-destructive font-semibold">
{deleteMessage}
</span>
</Trans>
</FormLabel>
<FormControl>
<Input className="bg-background" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<div className="flex w-full flex-nowrap gap-4">
<Button
type="button"
variant="secondary"
className="flex-1"
onClick={() => setIsOpen(false)}
>
<Trans>Cancel</Trans>
</Button>
<Button
type="submit"
variant="destructive"
className="flex-1"
disabled={!form.formState.isValid}
loading={form.formState.isSubmitting}
>
<Trans>I'm sure! Delete it</Trans>
</Button>
</div>
</DialogFooter>
</fieldset>
</form>
</Form>
</DialogContent>
</Dialog>
);
}

View File

@ -88,8 +88,6 @@ export const WebhookCreateDialog = ({ trigger, ...props }: WebhookCreateDialogPr
});
form.reset();
// router.refresh(); // Todo
} catch (err) {
toast({
title: _(msg`Error`),

View File

@ -1,5 +1,3 @@
'use effect';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
@ -77,8 +75,6 @@ export const WebhookDeleteDialog = ({ webhook, children }: WebhookDeleteDialogPr
});
setOpen(false);
// router.refresh(); // Todo
} catch (error) {
toast({
title: _(msg`An unknown error occurred`),