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]>
246 lines
7.4 KiB
TypeScript
246 lines
7.4 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 { useForm } from 'react-hook-form';
|
|
import { useNavigate } from 'react-router';
|
|
import { z } from 'zod';
|
|
|
|
import { useCurrentOrganisation } from '@hanzo/esign-lib/client-only/providers/organisation';
|
|
import { useSession } from '@hanzo/esign-lib/client-only/providers/session';
|
|
import { AppError } from '@hanzo/esign-lib/errors/app-error';
|
|
import { createConfirmationSchema } from '@hanzo/esign-lib/types/confirmation';
|
|
import type { ZDeleteTeamRequestSchema } from '@hanzo/esign-trpc/server/team-router/delete-team.types';
|
|
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 {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@hanzo/esign-ui/primitives/select';
|
|
import type { Toast } from '@hanzo/esign-ui/primitives/use-toast';
|
|
import { useToast } from '@hanzo/esign-ui/primitives/use-toast';
|
|
|
|
export type TeamDeleteDialogProps = {
|
|
teamId: number;
|
|
teamName: string;
|
|
redirectTo?: string;
|
|
trigger?: React.ReactNode;
|
|
};
|
|
|
|
export const TeamDeleteDialog = ({
|
|
trigger,
|
|
teamId,
|
|
teamName,
|
|
redirectTo,
|
|
}: TeamDeleteDialogProps) => {
|
|
const navigate = useNavigate();
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const { _ } = useLingui();
|
|
const { toast } = useToast();
|
|
const { refreshSession } = useSession();
|
|
|
|
const currentOrganisation = useCurrentOrganisation();
|
|
|
|
const deleteMessage = _(msg`delete ${teamName}`);
|
|
|
|
const filteredTeams = currentOrganisation.teams.filter((team) => team.id !== teamId);
|
|
|
|
const ZDeleteTeamFormSchema = z.object({
|
|
teamName: createConfirmationSchema(
|
|
deleteMessage,
|
|
_(msg`You must enter '${deleteMessage}' to proceed`),
|
|
),
|
|
transferTeamId: z.string().optional(),
|
|
});
|
|
|
|
const form = useForm({
|
|
resolver: zodResolver(ZDeleteTeamFormSchema),
|
|
defaultValues: {
|
|
teamName: '',
|
|
transferTeamId: undefined,
|
|
},
|
|
});
|
|
|
|
const { mutateAsync: deleteTeam } = useZapMutation<
|
|
void,
|
|
z.infer<typeof ZDeleteTeamRequestSchema>
|
|
>('team.delete');
|
|
|
|
const onFormSubmit = async (data: z.infer<typeof ZDeleteTeamFormSchema>) => {
|
|
try {
|
|
const transferTeamId = data.transferTeamId ? parseInt(data.transferTeamId, 10) : undefined;
|
|
|
|
await deleteTeam({ teamId, transferTeamId: transferTeamId || undefined });
|
|
|
|
await refreshSession();
|
|
|
|
toast({
|
|
title: _(msg`Success`),
|
|
description: _(msg`Your team has been successfully deleted.`),
|
|
duration: 5000,
|
|
});
|
|
|
|
if (redirectTo) {
|
|
await navigate(redirectTo);
|
|
}
|
|
|
|
setOpen(false);
|
|
} catch (err) {
|
|
const error = AppError.parseError(err);
|
|
|
|
let toastError: Toast = {
|
|
title: _(msg`An unknown error occurred`),
|
|
description: _(
|
|
msg`We encountered an unknown error while attempting to delete this team. Please try again later.`,
|
|
),
|
|
variant: 'destructive',
|
|
duration: 10000,
|
|
};
|
|
|
|
if (error.code === 'resource_missing') {
|
|
toastError = {
|
|
title: _(msg`Unable to delete team`),
|
|
description: _(
|
|
msg`Something went wrong while updating the team billing subscription, please contact support.`,
|
|
),
|
|
variant: 'destructive',
|
|
duration: 15000,
|
|
};
|
|
}
|
|
|
|
toast(toastError);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
form.reset();
|
|
}
|
|
}, [open, form]);
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
|
|
<DialogTrigger asChild>
|
|
{trigger ?? (
|
|
<Button variant="destructive">
|
|
<Trans>Delete</Trans>
|
|
</Button>
|
|
)}
|
|
</DialogTrigger>
|
|
|
|
<DialogContent position="center">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
<Trans>Are you sure you wish to delete this team?</Trans>
|
|
</DialogTitle>
|
|
|
|
<DialogDescription className="mt-4">
|
|
<Trans>
|
|
Please note that you will lose access to all documents associated with this team & all
|
|
the members will be removed and notified
|
|
</Trans>
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
|
<fieldset
|
|
className="flex h-full flex-col space-y-4"
|
|
disabled={form.formState.isSubmitting}
|
|
>
|
|
<FormField
|
|
control={form.control}
|
|
name="teamName"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
<Trans>
|
|
Confirm by typing <span className="text-destructive">{deleteMessage}</span>
|
|
</Trans>
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input className="bg-background" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
{filteredTeams.length > 0 && (
|
|
<FormField
|
|
control={form.control}
|
|
name="transferTeamId"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
<Trans>Transfer documents to a different team</Trans>
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Select {...field} onValueChange={field.onChange}>
|
|
<SelectTrigger>
|
|
<SelectValue
|
|
placeholder={_(msg`Don't transfer (Delete all documents)`)}
|
|
/>
|
|
</SelectTrigger>
|
|
|
|
<SelectContent>
|
|
<SelectItem value="-1">
|
|
<Trans>Don't transfer (Delete all documents)</Trans>
|
|
</SelectItem>
|
|
|
|
{filteredTeams.map((team) => (
|
|
<SelectItem key={team.id} value={team.id.toString()}>
|
|
{team.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
|
|
<Trans>Cancel</Trans>
|
|
</Button>
|
|
|
|
<Button type="submit" variant="destructive" loading={form.formState.isSubmitting}>
|
|
<Trans>Delete</Trans>
|
|
</Button>
|
|
</DialogFooter>
|
|
</fieldset>
|
|
</form>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|