mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
Six delete dialogs ask for a name typed exactly, as z.literal(name, {
errorMap: () => ({ message }) }). zod 4 took errorMap out of a schema's
params — it is `error` now — and ignores what it does not know, so since
the version sweep a wrong entry has shown zod's default, `Invalid input:
expected "delete acme"`, instead of "You must enter 'delete acme' to
proceed". The types caught it: the object literal no longer matched, and the
failed overload left the field's value typed as any literal, which the
input would not take.
The six build the field with createConfirmationSchema(name, message), which
spells it the zod 4 way. confirmation.test.ts checks the exact text passes
and near misses fail with the given message; with the errorMap spelling put
back, the second case fails on zod's default. zod 3 kept the message, so
this is the behaviour from before the sweep.
Co-authored-by: Hanzo Dev <[email protected]>
193 lines
5.3 KiB
TypeScript
193 lines
5.3 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { msg } from '@lingui/core/macro';
|
|
import { useLingui } from '@lingui/react';
|
|
import { Trans } from '@lingui/react/macro';
|
|
import type { ApiToken } from '@prisma/client';
|
|
import { useForm } from 'react-hook-form';
|
|
import { z } from 'zod';
|
|
|
|
import { createConfirmationSchema } from '@hanzo/esign-lib/types/confirmation';
|
|
import { useZapMutation } from '@hanzo/esign-trpc/zap/react';
|
|
import { Button } from '@hanzo/esign-ui/primitives/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from '@hanzo/esign-ui/primitives/dialog';
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from '@hanzo/esign-ui/primitives/form/form';
|
|
import { Input } from '@hanzo/esign-ui/primitives/input';
|
|
import { useToast } from '@hanzo/esign-ui/primitives/use-toast';
|
|
|
|
import { useCurrentTeam } from '~/providers/team';
|
|
|
|
export type TokenDeleteDialogProps = {
|
|
token: Pick<ApiToken, 'id' | 'name'>;
|
|
onDelete?: () => void;
|
|
children?: React.ReactNode;
|
|
};
|
|
|
|
export default function TokenDeleteDialog({ token, onDelete, children }: TokenDeleteDialogProps) {
|
|
const { _ } = useLingui();
|
|
const { toast } = useToast();
|
|
|
|
const team = useCurrentTeam();
|
|
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
const deleteMessage = _(msg`delete ${token.name}`);
|
|
|
|
const ZTokenDeleteDialogSchema = z.object({
|
|
tokenName: createConfirmationSchema(
|
|
deleteMessage,
|
|
_(msg`You must enter '${deleteMessage}' to proceed`),
|
|
),
|
|
});
|
|
|
|
type TDeleteTokenByIdMutationSchema = z.infer<typeof ZTokenDeleteDialogSchema>;
|
|
|
|
const { mutateAsync: deleteTokenMutation } = useZapMutation<void, { id: number; teamId: number }>(
|
|
'apiToken.delete',
|
|
{
|
|
onSuccess() {
|
|
onDelete?.();
|
|
},
|
|
},
|
|
);
|
|
|
|
const form = useForm<TDeleteTokenByIdMutationSchema>({
|
|
resolver: zodResolver(ZTokenDeleteDialogSchema),
|
|
values: {
|
|
tokenName: '',
|
|
},
|
|
});
|
|
|
|
const onSubmit = async () => {
|
|
try {
|
|
await deleteTokenMutation({
|
|
id: token.id,
|
|
teamId: team.id,
|
|
});
|
|
|
|
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>
|
|
);
|
|
}
|