mirror of
https://github.com/documenso/documenso.git
synced 2025-11-18 18:51:37 +10:00
feat: add envelope editor
This commit is contained in:
@ -51,6 +51,7 @@ export function FieldToolTip({ children, color, className = '', field }: FieldTo
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
id="field-tooltip"
|
||||
className={cn('pointer-events-none absolute')}
|
||||
style={{
|
||||
top: `${coords.y}px`,
|
||||
|
||||
23
packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx
Normal file
23
packages/ui/components/pdf-viewer/pdf-viewer-konva-lazy.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
|
||||
import { type PDFDocumentProxy } from 'pdfjs-dist';
|
||||
|
||||
export type LoadedPDFDocument = PDFDocumentProxy;
|
||||
|
||||
export type PDFViewerProps = {
|
||||
className?: string;
|
||||
onDocumentLoad?: () => void;
|
||||
[key: string]: unknown;
|
||||
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'onPageClick'>;
|
||||
|
||||
const EnvelopePdfViewer = lazy(async () => import('./pdf-viewer-konva'));
|
||||
|
||||
export const PDFViewerKonvaLazy = (props: PDFViewerProps) => {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading client component...</div>}>
|
||||
<EnvelopePdfViewer {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default PDFViewerKonvaLazy;
|
||||
173
packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx
Normal file
173
packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import Konva from 'konva';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { type PDFDocumentProxy } from 'pdfjs-dist';
|
||||
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
|
||||
|
||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
export type LoadedPDFDocument = PDFDocumentProxy;
|
||||
|
||||
/**
|
||||
* This imports the worker from the `pdfjs-dist` package.
|
||||
*/
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.js',
|
||||
import.meta.url,
|
||||
).toString();
|
||||
|
||||
const PDFLoader = () => (
|
||||
<>
|
||||
<Loader className="text-documenso h-12 w-12 animate-spin" />
|
||||
|
||||
<p className="text-muted-foreground mt-4">
|
||||
<Trans>Loading document...</Trans>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
export type PdfViewerKonvaProps = {
|
||||
className?: string;
|
||||
onDocumentLoad?: () => void;
|
||||
customPageRenderer?: React.FunctionComponent;
|
||||
[key: string]: unknown;
|
||||
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'onPageClick'>;
|
||||
|
||||
export const PdfViewerKonva = ({
|
||||
className,
|
||||
onDocumentLoad,
|
||||
customPageRenderer,
|
||||
...props
|
||||
}: PdfViewerKonvaProps) => {
|
||||
const $el = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { getPdfBuffer, currentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||
|
||||
const [width, setWidth] = useState(0);
|
||||
const [numPages, setNumPages] = useState(0);
|
||||
const [pdfError, setPdfError] = useState(false);
|
||||
|
||||
const envelopeItemFile = useMemo(() => {
|
||||
const data = getPdfBuffer(currentEnvelopeItem?.documentDataId || '');
|
||||
|
||||
if (!data || data.status !== 'loaded') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
data: new Uint8Array(data.file),
|
||||
};
|
||||
}, [currentEnvelopeItem?.documentDataId, getPdfBuffer]);
|
||||
|
||||
const onDocumentLoaded = useCallback(
|
||||
(doc: PDFDocumentProxy) => {
|
||||
setNumPages(doc.numPages);
|
||||
},
|
||||
[onDocumentLoad],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if ($el.current) {
|
||||
const $current = $el.current;
|
||||
|
||||
const { width } = $current.getBoundingClientRect();
|
||||
|
||||
setWidth(width);
|
||||
|
||||
const onResize = () => {
|
||||
const { width } = $current.getBoundingClientRect();
|
||||
setWidth(width);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={$el} className={cn('w-[800px] overflow-hidden', className)} {...props}>
|
||||
{envelopeItemFile && Konva ? (
|
||||
<PDFDocument
|
||||
file={envelopeItemFile}
|
||||
className={cn('w-full overflow-hidden rounded', {
|
||||
'h-[80vh] max-h-[60rem]': numPages === 0,
|
||||
})}
|
||||
onLoadSuccess={(d) => onDocumentLoaded(d)}
|
||||
// Uploading a invalid document causes an error which doesn't appear to be handled by the `error` prop.
|
||||
// Therefore we add some additional custom error handling.
|
||||
onSourceError={() => {
|
||||
setPdfError(true);
|
||||
}}
|
||||
externalLinkTarget="_blank"
|
||||
loading={
|
||||
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
|
||||
{pdfError ? (
|
||||
<div className="text-muted-foreground text-center">
|
||||
<p>
|
||||
<Trans>Something went wrong while loading the document.</Trans>
|
||||
</p>
|
||||
<p className="mt-1 text-sm">
|
||||
<Trans>Please try again or contact our support.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<PDFLoader />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
error={
|
||||
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
|
||||
<div className="text-muted-foreground text-center">
|
||||
<p>
|
||||
<Trans>Something went wrong while loading the document.</Trans>
|
||||
</p>
|
||||
<p className="mt-1 text-sm">
|
||||
<Trans>Please try again or contact our support.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{Array(numPages)
|
||||
.fill(null)
|
||||
.map((_, i) => (
|
||||
<div key={i} className="last:-mb-2">
|
||||
<div className="border-border overflow-hidden rounded border will-change-transform">
|
||||
<PDFPage
|
||||
pageNumber={i + 1}
|
||||
width={width}
|
||||
renderAnnotationLayer={false}
|
||||
renderTextLayer={false}
|
||||
loading={() => ''}
|
||||
renderMode={customPageRenderer ? 'custom' : 'canvas'}
|
||||
customRenderer={customPageRenderer}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground/80 my-2 text-center text-[11px]">
|
||||
<Trans>
|
||||
Page {i + 1} of {numPages}
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</PDFDocument>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-[80vh] max-h-[60rem] w-full flex-col items-center justify-center overflow-hidden rounded',
|
||||
)}
|
||||
>
|
||||
<PDFLoader />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PdfViewerKonva;
|
||||
@ -10,7 +10,7 @@ import { Card, CardContent } from '../primitives/card';
|
||||
export type SigningCardProps = {
|
||||
className?: string;
|
||||
name: string;
|
||||
signature?: Signature;
|
||||
signature?: Pick<Signature, 'signatureImageAsBase64' | 'typedSignature'>;
|
||||
signingCelebrationImage?: string;
|
||||
};
|
||||
|
||||
@ -154,7 +154,7 @@ export const SigningCard3D = ({
|
||||
|
||||
type SigningCardContentProps = {
|
||||
name: string;
|
||||
signature?: Signature;
|
||||
signature?: Pick<Signature, 'signatureImageAsBase64' | 'typedSignature'>;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
|
||||
@ -7,16 +7,22 @@ export type RecipientColorMap = Record<number, RecipientColorStyles>;
|
||||
|
||||
export type RecipientColorStyles = {
|
||||
base: string;
|
||||
baseRing: string;
|
||||
baseRingHover: string;
|
||||
fieldItem: string;
|
||||
fieldItemInitials: string;
|
||||
comboxBoxTrigger: string;
|
||||
comboxBoxItem: string;
|
||||
};
|
||||
|
||||
export const DEFAULT_RECT_BACKGROUND = 'rgba(255, 255, 255, 0.95)';
|
||||
|
||||
// !: values of the declared variable to do all the background, border and shadow styles.
|
||||
export const RECIPIENT_COLOR_STYLES = {
|
||||
readOnly: {
|
||||
base: 'ring-neutral-400',
|
||||
baseRing: 'rgba(176, 176, 176, 1)',
|
||||
baseRingHover: 'rgba(176, 176, 176, 1)',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: '',
|
||||
comboxBoxTrigger:
|
||||
@ -26,6 +32,8 @@ export const RECIPIENT_COLOR_STYLES = {
|
||||
|
||||
green: {
|
||||
base: 'ring-recipient-green hover:bg-recipient-green/30',
|
||||
baseRing: 'rgba(122, 195, 85, 1)',
|
||||
baseRingHover: 'rgba(122, 195, 85, 0.3)',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-green',
|
||||
comboxBoxTrigger:
|
||||
@ -35,6 +43,8 @@ export const RECIPIENT_COLOR_STYLES = {
|
||||
|
||||
blue: {
|
||||
base: 'ring-recipient-blue hover:bg-recipient-blue/30',
|
||||
baseRing: 'rgba(56, 123, 199, 1)',
|
||||
baseRingHover: 'rgba(56, 123, 199, 0.3)',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-blue',
|
||||
comboxBoxTrigger:
|
||||
@ -44,6 +54,8 @@ export const RECIPIENT_COLOR_STYLES = {
|
||||
|
||||
purple: {
|
||||
base: 'ring-recipient-purple hover:bg-recipient-purple/30',
|
||||
baseRing: 'rgba(151, 71, 255, 1)',
|
||||
baseRingHover: 'rgba(151, 71, 255, 0.3)',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-purple',
|
||||
comboxBoxTrigger:
|
||||
@ -53,6 +65,8 @@ export const RECIPIENT_COLOR_STYLES = {
|
||||
|
||||
orange: {
|
||||
base: 'ring-recipient-orange hover:bg-recipient-orange/30',
|
||||
baseRing: 'rgba(246, 159, 30, 1)',
|
||||
baseRingHover: 'rgba(246, 159, 30, 0.3)',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-orange',
|
||||
comboxBoxTrigger:
|
||||
@ -62,6 +76,8 @@ export const RECIPIENT_COLOR_STYLES = {
|
||||
|
||||
yellow: {
|
||||
base: 'ring-recipient-yellow hover:bg-recipient-yellow/30',
|
||||
baseRing: 'rgba(219, 186, 0, 1)',
|
||||
baseRingHover: 'rgba(219, 186, 0, 0.3)',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-yellow',
|
||||
comboxBoxTrigger:
|
||||
@ -71,6 +87,8 @@ export const RECIPIENT_COLOR_STYLES = {
|
||||
|
||||
pink: {
|
||||
base: 'ring-recipient-pink hover:bg-recipient-pink/30',
|
||||
baseRing: 'rgba(217, 74, 186, 1)',
|
||||
baseRingHover: 'rgba(217, 74, 186, 0.3)',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-pink',
|
||||
comboxBoxTrigger:
|
||||
@ -79,7 +97,7 @@ export const RECIPIENT_COLOR_STYLES = {
|
||||
},
|
||||
} satisfies Record<string, RecipientColorStyles>;
|
||||
|
||||
export type CombinedStylesKey = keyof typeof RECIPIENT_COLOR_STYLES;
|
||||
export type TRecipientColor = keyof typeof RECIPIENT_COLOR_STYLES;
|
||||
|
||||
export const AVAILABLE_RECIPIENT_COLORS = [
|
||||
'green',
|
||||
@ -88,7 +106,7 @@ export const AVAILABLE_RECIPIENT_COLORS = [
|
||||
'orange',
|
||||
'yellow',
|
||||
'pink',
|
||||
] satisfies CombinedStylesKey[];
|
||||
] satisfies TRecipientColor[];
|
||||
|
||||
export const useRecipientColors = (index: number) => {
|
||||
const key = AVAILABLE_RECIPIENT_COLORS[index % AVAILABLE_RECIPIENT_COLORS.length];
|
||||
|
||||
@ -1,27 +1,25 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
export type CardProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
spotlight?: boolean;
|
||||
gradient?: boolean;
|
||||
degrees?: number;
|
||||
|
||||
/**
|
||||
* Not sure if this is needed, but added a toggle so it defaults to true since that was how it was before.
|
||||
*
|
||||
* This is required to be set false if you want drag drop to work within this element.
|
||||
*/
|
||||
backdropBlur?: boolean;
|
||||
};
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
||||
({ className, children, gradient = false, spotlight = false, degrees = 120, ...props }, ref) => {
|
||||
const mouseX = useMotionValue(0);
|
||||
const mouseY = useMotionValue(0);
|
||||
|
||||
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
|
||||
const { left, top } = currentTarget.getBoundingClientRect();
|
||||
|
||||
mouseX.set(clientX - left);
|
||||
mouseY.set(clientY - top);
|
||||
};
|
||||
|
||||
(
|
||||
{ className, children, gradient = false, degrees = 120, backdropBlur = true, ...props },
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
@ -32,8 +30,9 @@ const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
'bg-background text-foreground group relative rounded-lg border-2 backdrop-blur-[2px]',
|
||||
'bg-background text-foreground group relative rounded-lg border-2',
|
||||
{
|
||||
'backdrop-blur-[2px]': backdropBlur,
|
||||
'gradient-border-mask before:pointer-events-none before:absolute before:-inset-[2px] before:rounded-lg before:p-[2px] before:[background:linear-gradient(var(--card-gradient-degrees),theme(colors.primary.DEFAULT/50%)_5%,theme(colors.border/80%)_30%)]':
|
||||
gradient,
|
||||
'dark:gradient-border-mask before:pointer-events-none before:absolute before:-inset-[2px] before:rounded-lg before:p-[2px] before:[background:linear-gradient(var(--card-gradient-degrees),theme(colors.primary.DEFAULT/70%)_5%,theme(colors.border/80%)_30%)]':
|
||||
@ -44,23 +43,8 @@ const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
||||
},
|
||||
className,
|
||||
)}
|
||||
onMouseMove={handleMouseMove}
|
||||
{...props}
|
||||
>
|
||||
{spotlight && (
|
||||
<motion.div
|
||||
className="pointer-events-none absolute -inset-[2px] rounded-lg opacity-0 transition duration-300 group-hover:opacity-100"
|
||||
style={{
|
||||
background: useMotionTemplate`
|
||||
radial-gradient(
|
||||
300px circle at ${mouseX}px ${mouseY}px,
|
||||
hsl(var(--primary) / 7%),
|
||||
transparent 80%
|
||||
)
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -26,9 +26,11 @@ import { Card, CardContent } from './card';
|
||||
|
||||
export type DocumentDropzoneProps = {
|
||||
className?: string;
|
||||
allowMultiple?: boolean;
|
||||
disabled?: boolean;
|
||||
disabledHeading?: MessageDescriptor;
|
||||
disabledMessage?: MessageDescriptor;
|
||||
onDrop?: (_file: File) => void | Promise<void>;
|
||||
onDrop?: (_file: File[]) => void | Promise<void>;
|
||||
onDropRejected?: () => void | Promise<void>;
|
||||
type?: 'document' | 'template';
|
||||
[key: string]: unknown;
|
||||
@ -36,9 +38,11 @@ export type DocumentDropzoneProps = {
|
||||
|
||||
export const DocumentDropzone = ({
|
||||
className,
|
||||
allowMultiple,
|
||||
onDrop,
|
||||
onDropRejected,
|
||||
disabled,
|
||||
disabledHeading,
|
||||
disabledMessage = msg`You cannot upload documents at this time.`,
|
||||
type = 'document',
|
||||
...props
|
||||
@ -51,11 +55,11 @@ export const DocumentDropzone = ({
|
||||
accept: {
|
||||
'application/pdf': ['.pdf'],
|
||||
},
|
||||
multiple: false,
|
||||
multiple: allowMultiple,
|
||||
disabled,
|
||||
onDrop: ([acceptedFile]) => {
|
||||
if (acceptedFile && onDrop) {
|
||||
void onDrop(acceptedFile);
|
||||
onDrop: (acceptedFiles) => {
|
||||
if (acceptedFiles.length > 0 && onDrop) {
|
||||
void onDrop(acceptedFiles);
|
||||
}
|
||||
},
|
||||
onDropRejected: () => {
|
||||
@ -67,7 +71,9 @@ export const DocumentDropzone = ({
|
||||
});
|
||||
|
||||
const heading = {
|
||||
document: disabled ? msg`You have reached your document limit.` : msg`Add a document`,
|
||||
document: disabled
|
||||
? disabledHeading || msg`You have reached your document limit.`
|
||||
: msg`Add a document`,
|
||||
template: msg`Upload Template Document`,
|
||||
};
|
||||
|
||||
@ -153,7 +159,7 @@ export const DocumentDropzone = ({
|
||||
|
||||
<input {...getInputProps()} />
|
||||
|
||||
<p className="text-foreground mt-8 font-medium">{_(heading[type])}</p>
|
||||
<p className="text-foreground mt-6 font-medium">{_(heading[type])}</p>
|
||||
|
||||
<p className="text-muted-foreground/80 mt-1 text-center text-sm">
|
||||
{_(disabled ? disabledMessage : msg`Drag & drop your PDF here.`)}
|
||||
|
||||
@ -19,7 +19,6 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { prop, sortBy } from 'remeda';
|
||||
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
|
||||
@ -551,29 +550,6 @@ export const AddFieldsFormPartial = ({
|
||||
return recipientsByRole;
|
||||
}, [recipients]);
|
||||
|
||||
const recipientsByRoleToDisplay = useMemo(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][])
|
||||
.filter(
|
||||
([role]) =>
|
||||
role !== RecipientRole.CC &&
|
||||
role !== RecipientRole.VIEWER &&
|
||||
role !== RecipientRole.ASSISTANT,
|
||||
)
|
||||
.map(
|
||||
([role, roleRecipients]) =>
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
[
|
||||
role,
|
||||
sortBy(
|
||||
roleRecipients,
|
||||
[(r) => r.signingOrder || Number.MAX_SAFE_INTEGER, 'asc'],
|
||||
[prop('id'), 'asc'],
|
||||
),
|
||||
] as [RecipientRole, Recipient[]],
|
||||
);
|
||||
}, [recipientsByRole]);
|
||||
|
||||
const handleAdvancedSettings = () => {
|
||||
setShowAdvancedSettings((prev) => !prev);
|
||||
};
|
||||
|
||||
@ -10,11 +10,11 @@ import {
|
||||
ZDocumentAccessAuthTypesSchema,
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
} from '@documenso/trpc/server/document-router/schema';
|
||||
} from '@documenso/lib/types/document-meta';
|
||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||
|
||||
export const ZAddSettingsFormSchema = z.object({
|
||||
title: z
|
||||
|
||||
@ -2,14 +2,17 @@ export const numberFormatValues = [
|
||||
{
|
||||
label: '123,456,789.00',
|
||||
value: '123,456,789.00',
|
||||
regex: /^(?:\d{1,3}(?:,\d{3})*|\d+)(?:\.\d{1,2})?$/,
|
||||
},
|
||||
{
|
||||
label: '123.456.789,00',
|
||||
value: '123.456.789,00',
|
||||
regex: /^(?:\d{1,3}(?:\.\d{3})*|\d+)(?:,\d{1,2})?$/,
|
||||
},
|
||||
{
|
||||
label: '123456,789.00',
|
||||
value: '123456,789.00',
|
||||
regex: /^(?:\d+)(?:,\d{1,3}(?:\.\d{1,2})?)?$/,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -102,7 +102,7 @@ export const NumberFieldAdvancedSettings = ({
|
||||
<Trans>Number format</Trans>
|
||||
</Label>
|
||||
<Select
|
||||
value={fieldState.numberFormat}
|
||||
value={fieldState.numberFormat ?? ''}
|
||||
onValueChange={(val) => handleInput('numberFormat', val)}
|
||||
>
|
||||
<SelectTrigger className="text-muted-foreground bg-background mt-2 w-full">
|
||||
@ -199,7 +199,7 @@ export const NumberFieldAdvancedSettings = ({
|
||||
id="minValue"
|
||||
className="bg-background mt-2"
|
||||
placeholder="E.g. 0"
|
||||
value={fieldState.minValue}
|
||||
value={fieldState.minValue ?? ''}
|
||||
onChange={(e) => handleInput('minValue', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
@ -211,7 +211,7 @@ export const NumberFieldAdvancedSettings = ({
|
||||
id="maxValue"
|
||||
className="bg-background mt-2"
|
||||
placeholder="E.g. 100"
|
||||
value={fieldState.maxValue}
|
||||
value={fieldState.maxValue ?? ''}
|
||||
onChange={(e) => handleInput('maxValue', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -20,9 +20,9 @@ export type DocumentDropzoneProps = {
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
disabledMessage?: MessageDescriptor;
|
||||
onDrop?: (_file: File) => void | Promise<void>;
|
||||
onDrop?: (_files: File[]) => void | Promise<void>;
|
||||
onDropRejected?: () => void | Promise<void>;
|
||||
type?: 'document' | 'template';
|
||||
type?: 'document' | 'template' | 'envelope';
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
@ -48,11 +48,12 @@ export const DocumentDropzone = ({
|
||||
accept: {
|
||||
'application/pdf': ['.pdf'],
|
||||
},
|
||||
multiple: false,
|
||||
multiple: type === 'envelope',
|
||||
disabled,
|
||||
onDrop: ([acceptedFile]) => {
|
||||
if (acceptedFile && onDrop) {
|
||||
void onDrop(acceptedFile);
|
||||
maxFiles: 10, // Todo: Envelopes - Use claims. And also update other places where this is used.
|
||||
onDrop: (acceptedFiles) => {
|
||||
if (acceptedFiles.length > 0 && onDrop) {
|
||||
void onDrop(acceptedFiles);
|
||||
}
|
||||
},
|
||||
onDropRejected: () => {
|
||||
@ -66,6 +67,7 @@ export const DocumentDropzone = ({
|
||||
const heading = {
|
||||
document: msg`Upload Document`,
|
||||
template: msg`Upload Template Document`,
|
||||
envelope: msg`Envelope (beta)`,
|
||||
};
|
||||
|
||||
if (disabled && IS_BILLING_ENABLED()) {
|
||||
|
||||
@ -48,7 +48,7 @@ const PDFLoader = () => (
|
||||
|
||||
export type PDFViewerProps = {
|
||||
className?: string;
|
||||
documentData: DocumentData;
|
||||
documentData: Pick<DocumentData, 'type' | 'data'>;
|
||||
onDocumentLoad?: (_doc: LoadedPDFDocument) => void;
|
||||
onPageClick?: OnPDFViewerPageClick;
|
||||
[key: string]: unknown;
|
||||
|
||||
@ -21,6 +21,7 @@ export interface RecipientSelectorProps {
|
||||
selectedRecipient: Recipient | null;
|
||||
onSelectedRecipientChange: (recipient: Recipient) => void;
|
||||
recipients: Recipient[];
|
||||
align?: 'center' | 'end' | 'start';
|
||||
}
|
||||
|
||||
export const RecipientSelector = ({
|
||||
@ -28,6 +29,7 @@ export const RecipientSelector = ({
|
||||
selectedRecipient,
|
||||
onSelectedRecipientChange,
|
||||
recipients,
|
||||
align = 'start',
|
||||
}: RecipientSelectorProps) => {
|
||||
const { _ } = useLingui();
|
||||
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
||||
@ -83,7 +85,7 @@ export const RecipientSelector = ({
|
||||
recipients.findIndex((r) => r.id === selectedRecipient?.id),
|
||||
0,
|
||||
),
|
||||
).base,
|
||||
).comboxBoxTrigger,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@ -101,8 +103,8 @@ export const RecipientSelector = ({
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command value={selectedRecipient?.email}>
|
||||
<PopoverContent className="p-0" align={align}>
|
||||
<Command value={selectedRecipient ? selectedRecipient.id.toString() : undefined}>
|
||||
<CommandInput />
|
||||
|
||||
<CommandEmpty>
|
||||
@ -149,7 +151,7 @@ export const RecipientSelector = ({
|
||||
>
|
||||
<span
|
||||
className={cn('text-foreground/70 truncate', {
|
||||
'text-foreground/80': recipient === selectedRecipient,
|
||||
'text-foreground/80': recipient.id === selectedRecipient?.id,
|
||||
})}
|
||||
>
|
||||
{recipient.name && (
|
||||
@ -164,10 +166,10 @@ export const RecipientSelector = ({
|
||||
<div className="ml-auto flex items-center justify-center">
|
||||
{recipient.sendStatus !== SendStatus.SENT ? (
|
||||
<Check
|
||||
aria-hidden={recipient !== selectedRecipient}
|
||||
aria-hidden={recipient.id !== selectedRecipient?.id}
|
||||
className={cn('h-4 w-4 flex-shrink-0', {
|
||||
'opacity-0': recipient !== selectedRecipient,
|
||||
'opacity-100': recipient === selectedRecipient,
|
||||
'opacity-0': recipient.id !== selectedRecipient?.id,
|
||||
'opacity-100': recipient.id === selectedRecipient?.id,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@ -19,11 +19,11 @@ import {
|
||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import type { TDocumentMetaDateFormat } from '@documenso/lib/types/document-meta';
|
||||
import type { TTemplate } from '@documenso/lib/types/template';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TDocumentMetaDateFormat } from '@documenso/trpc/server/document-router/schema';
|
||||
import {
|
||||
DocumentGlobalAuthAccessSelect,
|
||||
DocumentGlobalAuthAccessTooltip,
|
||||
@ -216,7 +216,12 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} maxLength={255} onBlur={handleAutoSave} />
|
||||
<Input
|
||||
className="bg-background"
|
||||
{...field}
|
||||
maxLength={255}
|
||||
onBlur={handleAutoSave}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -624,7 +629,12 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} maxLength={255} onBlur={handleAutoSave} />
|
||||
<Input
|
||||
className="bg-background"
|
||||
{...field}
|
||||
maxLength={255}
|
||||
onBlur={handleAutoSave}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
@ -715,7 +725,12 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} maxLength={255} onBlur={handleAutoSave} />
|
||||
<Input
|
||||
className="bg-background"
|
||||
{...field}
|
||||
maxLength={255}
|
||||
onBlur={handleAutoSave}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@ -12,11 +12,11 @@ import {
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
} from '@documenso/trpc/server/document-router/schema';
|
||||
} from '@documenso/lib/types/document-meta';
|
||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||
|
||||
export const ZAddTemplateSettingsFormSchema = z.object({
|
||||
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
||||
|
||||
Reference in New Issue
Block a user