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]>
172 lines
5.1 KiB
TypeScript
172 lines
5.1 KiB
TypeScript
import { useEffect } from 'react';
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { Trans } from '@lingui/react/macro';
|
|
import { useForm } from 'react-hook-form';
|
|
import { z } from 'zod';
|
|
|
|
import { AppError, AppErrorCode } from '@hanzo/esign-lib/errors/app-error';
|
|
import { createConfirmationSchema } from '@hanzo/esign-lib/types/confirmation';
|
|
import type { TFolderWithSubfolders } from '@hanzo/esign-trpc/server/folder-router/schema';
|
|
import { useZapMutation } from '@hanzo/esign-trpc/zap/react';
|
|
import { Alert, AlertDescription } from '@hanzo/esign-ui/primitives/alert';
|
|
import { Button } from '@hanzo/esign-ui/primitives/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
type DialogProps,
|
|
DialogTitle,
|
|
} 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';
|
|
|
|
export type FolderDeleteDialogProps = {
|
|
folder: TFolderWithSubfolders;
|
|
isOpen: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
} & Omit<DialogProps, 'children'>;
|
|
|
|
export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDeleteDialogProps) => {
|
|
const { t } = useLingui();
|
|
|
|
const { toast } = useToast();
|
|
const { mutateAsync: deleteFolder } = useZapMutation<void, unknown>('folder.deleteFolder');
|
|
|
|
const deleteMessage = t`delete ${folder.name}`;
|
|
|
|
const ZDeleteFolderFormSchema = z.object({
|
|
confirmText: createConfirmationSchema(
|
|
deleteMessage,
|
|
t`You must type '${deleteMessage}' to confirm`,
|
|
),
|
|
});
|
|
|
|
type TDeleteFolderFormSchema = z.infer<typeof ZDeleteFolderFormSchema>;
|
|
|
|
const form = useForm<TDeleteFolderFormSchema>({
|
|
resolver: zodResolver(ZDeleteFolderFormSchema),
|
|
defaultValues: {
|
|
confirmText: '',
|
|
},
|
|
});
|
|
|
|
const onFormSubmit = async () => {
|
|
try {
|
|
await deleteFolder({
|
|
folderId: folder.id,
|
|
});
|
|
|
|
onOpenChange(false);
|
|
|
|
toast({
|
|
title: t`Folder deleted successfully`,
|
|
});
|
|
} catch (err) {
|
|
const error = AppError.parseError(err);
|
|
|
|
if (error.code === AppErrorCode.NOT_FOUND) {
|
|
toast({
|
|
title: t`Folder not found`,
|
|
description: t`The folder you are trying to delete does not exist.`,
|
|
variant: 'destructive',
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
toast({
|
|
title: t`Failed to delete folder`,
|
|
description: t`An unknown error occurred while deleting the folder.`,
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) {
|
|
form.reset();
|
|
}
|
|
}, [isOpen]);
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
<Trans>Delete Folder</Trans>
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
<Trans>Are you sure you want to delete this folder?</Trans>
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{(folder._count.documents > 0 ||
|
|
folder._count.templates > 0 ||
|
|
folder._count.subfolders > 0) && (
|
|
<Alert variant="destructive">
|
|
<AlertDescription>
|
|
<Trans>
|
|
This folder contains multiple items. Deleting it will remove all subfolders and move
|
|
all nested documents and templates to the root folder.
|
|
</Trans>
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
|
<fieldset disabled={form.formState.isSubmitting} className="space-y-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="confirmText"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
<Trans>
|
|
Confirm by typing:{' '}
|
|
<span className="font-sm text-destructive font-semibold">
|
|
{deleteMessage}
|
|
</span>
|
|
</Trans>
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder={deleteMessage} {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<DialogFooter>
|
|
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
|
<Trans>Cancel</Trans>
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
type="submit"
|
|
disabled={!form.formState.isValid}
|
|
loading={form.formState.isSubmitting}
|
|
>
|
|
<Trans>Delete</Trans>
|
|
</Button>
|
|
</DialogFooter>
|
|
</fieldset>
|
|
</form>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|