mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
fix(dialogs): give the delete confirmations their message back
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]>
This commit is contained in:
@@ -7,6 +7,7 @@ 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';
|
||||
@@ -46,9 +47,10 @@ export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDelet
|
||||
const deleteMessage = t`delete ${folder.name}`;
|
||||
|
||||
const ZDeleteFolderFormSchema = z.object({
|
||||
confirmText: z.literal(deleteMessage, {
|
||||
errorMap: () => ({ message: t`You must type '${deleteMessage}' to confirm` }),
|
||||
}),
|
||||
confirmText: createConfirmationSchema(
|
||||
deleteMessage,
|
||||
t`You must type '${deleteMessage}' to confirm`,
|
||||
),
|
||||
});
|
||||
|
||||
type TDeleteFolderFormSchema = z.infer<typeof ZDeleteFolderFormSchema>;
|
||||
@@ -134,7 +136,7 @@ export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDelet
|
||||
<FormLabel>
|
||||
<Trans>
|
||||
Confirm by typing:{' '}
|
||||
<span className="font-sm font-semibold text-destructive">
|
||||
<span className="font-sm text-destructive font-semibold">
|
||||
{deleteMessage}
|
||||
</span>
|
||||
</Trans>
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 { useZapMutation } from '@hanzo/esign-trpc/zap/react';
|
||||
import { Button } from '@hanzo/esign-ui/primitives/button';
|
||||
import {
|
||||
@@ -50,9 +51,10 @@ export const OrganisationDeleteDialog = ({ trigger }: OrganisationDeleteDialogPr
|
||||
const deleteMessage = _(msg`delete ${organisation.name}`);
|
||||
|
||||
const ZDeleteOrganisationFormSchema = z.object({
|
||||
organisationName: z.literal(deleteMessage, {
|
||||
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
|
||||
}),
|
||||
organisationName: createConfirmationSchema(
|
||||
deleteMessage,
|
||||
_(msg`You must enter '${deleteMessage}' to proceed`),
|
||||
),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useCurrentOrganisation } from '@hanzo/esign-lib/client-only/providers/organisation';
|
||||
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 {
|
||||
@@ -50,9 +51,10 @@ export const OrganisationEmailDomainDeleteDialog = ({
|
||||
const deleteMessage = t`delete ${emailDomain}`;
|
||||
|
||||
const ZDeleteEmailDomainFormSchema = z.object({
|
||||
confirmText: z.literal(deleteMessage, {
|
||||
errorMap: () => ({ message: t`You must type '${deleteMessage}' to confirm` }),
|
||||
}),
|
||||
confirmText: createConfirmationSchema(
|
||||
deleteMessage,
|
||||
t`You must type '${deleteMessage}' to confirm`,
|
||||
),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof ZDeleteEmailDomainFormSchema>>({
|
||||
|
||||
@@ -11,6 +11,7 @@ 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';
|
||||
@@ -69,9 +70,10 @@ export const TeamDeleteDialog = ({
|
||||
const filteredTeams = currentOrganisation.teams.filter((team) => team.id !== teamId);
|
||||
|
||||
const ZDeleteTeamFormSchema = z.object({
|
||||
teamName: z.literal(deleteMessage, {
|
||||
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
|
||||
}),
|
||||
teamName: createConfirmationSchema(
|
||||
deleteMessage,
|
||||
_(msg`You must enter '${deleteMessage}' to proceed`),
|
||||
),
|
||||
transferTeamId: z.string().optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 {
|
||||
@@ -49,21 +50,22 @@ export default function TokenDeleteDialog({ token, onDelete, children }: TokenDe
|
||||
const deleteMessage = _(msg`delete ${token.name}`);
|
||||
|
||||
const ZTokenDeleteDialogSchema = z.object({
|
||||
tokenName: z.literal(deleteMessage, {
|
||||
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
|
||||
}),
|
||||
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 { mutateAsync: deleteTokenMutation } = useZapMutation<void, { id: number; teamId: number }>(
|
||||
'apiToken.delete',
|
||||
{
|
||||
onSuccess() {
|
||||
onDelete?.();
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const form = useForm<TDeleteTokenByIdMutationSchema>({
|
||||
resolver: zodResolver(ZTokenDeleteDialogSchema),
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { Webhook } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createConfirmationSchema } from '@hanzo/esign-lib/types/confirmation';
|
||||
import type { TDeleteWebhookRequestSchema } from '@hanzo/esign-trpc/server/webhook-router/schema';
|
||||
import { useZapMutation } from '@hanzo/esign-trpc/zap/react';
|
||||
import { Button } from '@hanzo/esign-ui/primitives/button';
|
||||
@@ -50,9 +51,10 @@ export const WebhookDeleteDialog = ({ webhook, children }: WebhookDeleteDialogPr
|
||||
const deleteMessage = _(msg`delete ${webhook.webhookUrl}`);
|
||||
|
||||
const ZDeleteWebhookFormSchema = z.object({
|
||||
webhookUrl: z.literal(deleteMessage, {
|
||||
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
|
||||
}),
|
||||
webhookUrl: createConfirmationSchema(
|
||||
deleteMessage,
|
||||
_(msg`You must enter '${deleteMessage}' to proceed`),
|
||||
),
|
||||
});
|
||||
|
||||
type TDeleteWebhookFormSchema = z.infer<typeof ZDeleteWebhookFormSchema>;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Typing a name to confirm a delete.
|
||||
*
|
||||
* Run: npx tsx --test packages/lib/__tests__/confirmation.test.ts
|
||||
*/
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, test } from 'node:test';
|
||||
|
||||
import { createConfirmationSchema } from '../types/confirmation';
|
||||
|
||||
const message = "You must enter 'delete acme' to proceed";
|
||||
const schema = createConfirmationSchema('delete acme', message);
|
||||
|
||||
describe('createConfirmationSchema', () => {
|
||||
test('accepts the text, exactly', () => {
|
||||
assert.equal(schema.safeParse('delete acme').success, true);
|
||||
});
|
||||
|
||||
test('refuses anything else with the message it was given', () => {
|
||||
for (const typed of ['', 'delete', 'Delete Acme', 'delete acme ']) {
|
||||
const result = schema.safeParse(typed);
|
||||
|
||||
assert.equal(result.success, false, typed);
|
||||
assert.deepEqual(
|
||||
result.error?.issues.map((issue) => issue.message),
|
||||
[message],
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* A field the user fills with `expected`, exactly, to confirm a destructive
|
||||
* action. Anything else fails with `message`.
|
||||
*/
|
||||
export const createConfirmationSchema = (expected: string, message: string) =>
|
||||
z.literal(expected, { error: message });
|
||||
Reference in New Issue
Block a user