mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 07:43:16 +10:00
Rework: - Field styling to improve visibility - Field insertions, better alignment, centering and overflows ## Changes General changes: - Set default text alignment to left if no meta found - Reduce borders and rings around fields to allow smaller fields - Removed lots of redundant duplicated code surrounding field rendering - Make fields more consistent across viewing, editing and signing - Add more transparency to fields to allow users to see under fields - No more optional/required/etc colors when signing, required fields will be highlighted as orange when form is "validating" Highlighted internal changes: - Utilize native PDF fields to insert text, instead of drawing text - Change font auto scaling to only apply to when the height overflows AND no custom font is set ⚠️ Multiline changes: Multi line is enabled for a field under these conditions 1. Field content exceeds field width 2. Field includes a new line 3. Field type is TEXT ## [BEFORE] Field UI Signing  ## [AFTER] Field UI Signing  ## [BEFORE] Signing a checkbox   ## [AFTER] Signing a checkbox   ## [BEFORE] What a 2nd recipient sees once someone else signed a document  ## [AFTER] What a 2nd recipient sees once someone else signed a document  ## **[BEFORE]** Inserting fields  ## **[AFTER]** Inserting fields  ## Overflows, multilines and field alignments testing Debugging borders: - Red border = The original field placement without any modifications - Blue border = The available space to overflow ### Single line overflows and field alignments This is left aligned fields, overflow will always go to the end of the page and will not wrap  This is center aligned fields, the max width is the closest edge to the page * 2  This is right aligned text, the width will extend all the way to the left hand side of the page  ### Multiline line overflows and field alignments These are text fields that can be overflowed  Another example of left aligned text overflows with more text 
275 lines
9.0 KiB
TypeScript
275 lines
9.0 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { msg } from '@lingui/core/macro';
|
|
import { useLingui } from '@lingui/react';
|
|
import { Trans } from '@lingui/react/macro';
|
|
import { File as FileIcon, Upload, X } from 'lucide-react';
|
|
import { useForm } from 'react-hook-form';
|
|
import { z } from 'zod';
|
|
|
|
import { trpc } from '@documenso/trpc/react';
|
|
import { Button } from '@documenso/ui/primitives/button';
|
|
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from '@documenso/ui/primitives/dialog';
|
|
import { Form, FormControl, FormField, FormItem } from '@documenso/ui/primitives/form/form';
|
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
|
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
|
|
|
const ZBulkSendFormSchema = z.object({
|
|
file: z.instanceof(File),
|
|
sendImmediately: z.boolean().default(false),
|
|
});
|
|
|
|
type TBulkSendFormSchema = z.infer<typeof ZBulkSendFormSchema>;
|
|
|
|
export type TemplateBulkSendDialogProps = {
|
|
templateId: number;
|
|
recipients: Array<{ email: string; name?: string | null }>;
|
|
trigger?: React.ReactNode;
|
|
onSuccess?: () => void;
|
|
};
|
|
|
|
export const TemplateBulkSendDialog = ({
|
|
templateId,
|
|
recipients,
|
|
trigger,
|
|
onSuccess,
|
|
}: TemplateBulkSendDialogProps) => {
|
|
const { _ } = useLingui();
|
|
const { toast } = useToast();
|
|
|
|
const team = useOptionalCurrentTeam();
|
|
|
|
const form = useForm<TBulkSendFormSchema>({
|
|
resolver: zodResolver(ZBulkSendFormSchema),
|
|
defaultValues: {
|
|
sendImmediately: false,
|
|
},
|
|
});
|
|
|
|
const { mutateAsync: uploadBulkSend } = trpc.template.uploadBulkSend.useMutation();
|
|
|
|
const onDownloadTemplate = () => {
|
|
const headers = recipients.flatMap((_, index) => [
|
|
`recipient_${index + 1}_email`,
|
|
`recipient_${index + 1}_name`,
|
|
]);
|
|
|
|
const exampleRow = recipients.flatMap((recipient) => [recipient.email, recipient.name || '']);
|
|
|
|
const csv = [headers.join(','), exampleRow.join(',')].join('\n');
|
|
const blob = new Blob([csv], { type: 'text/csv' });
|
|
const url = window.URL.createObjectURL(blob);
|
|
|
|
const a = Object.assign(document.createElement('a'), {
|
|
href: url,
|
|
download: 'template.csv',
|
|
});
|
|
|
|
a.click();
|
|
|
|
window.URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const onSubmit = async (values: TBulkSendFormSchema) => {
|
|
try {
|
|
const csv = await values.file.text();
|
|
|
|
await uploadBulkSend({
|
|
templateId,
|
|
teamId: team?.id,
|
|
csv: csv,
|
|
sendImmediately: values.sendImmediately,
|
|
});
|
|
|
|
toast({
|
|
title: _(msg`Success`),
|
|
description: _(
|
|
msg`Your bulk send has been initiated. You will receive an email notification upon completion.`,
|
|
),
|
|
});
|
|
|
|
form.reset();
|
|
onSuccess?.();
|
|
} catch (err) {
|
|
console.error(err);
|
|
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to upload CSV. Please check the file format and try again.',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
{trigger ?? (
|
|
<Button variant="outline">
|
|
<Upload className="mr-2 h-4 w-4" />
|
|
<Trans>Bulk Send via CSV</Trans>
|
|
</Button>
|
|
)}
|
|
</DialogTrigger>
|
|
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
<Trans>Bulk Send Template via CSV</Trans>
|
|
</DialogTitle>
|
|
|
|
<DialogDescription>
|
|
<Trans>
|
|
Upload a CSV file to create multiple documents from this template. Each row represents
|
|
one document with its recipient details.
|
|
</Trans>
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-y-4">
|
|
<div className="bg-muted/70 rounded-lg border p-4">
|
|
<h3 className="text-sm font-medium">
|
|
<Trans>CSV Structure</Trans>
|
|
</h3>
|
|
|
|
<p className="text-muted-foreground mt-1 text-sm">
|
|
<Trans>
|
|
For each recipient, provide their email (required) and name (optional) in separate
|
|
columns. Download the template CSV below for the correct format.
|
|
</Trans>
|
|
</p>
|
|
|
|
<p className="mt-4 text-sm">
|
|
<Trans>Current recipients:</Trans>
|
|
</p>
|
|
|
|
<ul className="text-muted-foreground mt-2 list-inside list-disc text-sm">
|
|
{recipients.map((recipient, index) => (
|
|
<li key={index}>
|
|
{recipient.name ? `${recipient.name} (${recipient.email})` : recipient.email}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-y-2">
|
|
<Button onClick={onDownloadTemplate} variant="outline" type="button">
|
|
<Trans>Download Template CSV</Trans>
|
|
</Button>
|
|
|
|
<p className="text-muted-foreground text-xs">
|
|
<Trans>Pre-formatted CSV template with example data.</Trans>
|
|
</p>
|
|
</div>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="file"
|
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
{!value ? (
|
|
<Button asChild variant="outline" className="w-full">
|
|
<label className="cursor-pointer">
|
|
<input
|
|
type="file"
|
|
accept=".csv"
|
|
className="hidden"
|
|
onChange={(e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
onChange(file);
|
|
}
|
|
}}
|
|
disabled={form.formState.isSubmitting}
|
|
/>
|
|
<Upload className="mr-2 h-4 w-4" />
|
|
<Trans>Upload CSV</Trans>
|
|
</label>
|
|
</Button>
|
|
) : (
|
|
<div className="flex h-10 items-center rounded-md border px-3">
|
|
<div className="flex flex-1 items-center gap-2">
|
|
<FileIcon className="text-muted-foreground h-4 w-4" />
|
|
<span className="flex-1 truncate text-sm">{value.name}</span>
|
|
</div>
|
|
|
|
<Button
|
|
type="button"
|
|
variant="link"
|
|
className="text-destructive hover:text-destructive p-0 text-xs"
|
|
onClick={() => onChange(null)}
|
|
disabled={form.formState.isSubmitting}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
<span className="sr-only">
|
|
<Trans>Remove</Trans>
|
|
</span>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</FormControl>
|
|
|
|
{error && <p className="text-destructive text-sm">{error.message}</p>}
|
|
|
|
<p className="text-muted-foreground text-xs">
|
|
<Trans>
|
|
Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use
|
|
template defaults.
|
|
</Trans>
|
|
</p>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="sendImmediately"
|
|
render={({ field }) => (
|
|
<FormItem className="flex items-center space-x-2">
|
|
<FormControl>
|
|
<div className="flex items-center">
|
|
<Checkbox
|
|
id="send-immediately"
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
|
|
<label
|
|
htmlFor="send-immediately"
|
|
className="text-muted-foreground ml-2 flex items-center text-sm"
|
|
>
|
|
<Trans>Send documents to recipients immediately</Trans>
|
|
</label>
|
|
</div>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<DialogFooter className="mt-4">
|
|
<Button variant="secondary" onClick={() => form.reset()} type="button">
|
|
<Trans>Cancel</Trans>
|
|
</Button>
|
|
|
|
<Button type="submit" loading={form.formState.isSubmitting}>
|
|
<Trans>Upload and Process</Trans>
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|