mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 17:21:41 +10:00
chore: merged main
This commit is contained in:
170
packages/ui/components/document/document-read-only-fields.tsx
Normal file
170
packages/ui/components/document/document-read-only-fields.tsx
Normal file
@ -0,0 +1,170 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { DocumentMeta, Field, Recipient, TemplateMeta } from '@prisma/client';
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { Clock, EyeOffIcon } from 'lucide-react';
|
||||
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document';
|
||||
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
|
||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
import { PopoverHover } from '@documenso/ui/primitives/popover';
|
||||
|
||||
import { getRecipientColorStyles } from '../../lib/recipient-colors';
|
||||
import { FieldContent } from '../../primitives/document-flow/field-content';
|
||||
|
||||
export type DocumentReadOnlyFieldsProps = {
|
||||
fields: DocumentField[];
|
||||
documentMeta?: DocumentMeta | TemplateMeta;
|
||||
|
||||
showFieldStatus?: boolean;
|
||||
|
||||
/**
|
||||
* Required if you want to show colors.
|
||||
*
|
||||
* Can't derive this from the fields because sometimes recipients don't have fields
|
||||
* yet.
|
||||
*/
|
||||
recipientIds?: number[];
|
||||
|
||||
/**
|
||||
* Whether to show the recipient tooltip.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
showRecipientTooltip?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to color code the recipient fields.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
showRecipientColors?: boolean;
|
||||
};
|
||||
|
||||
export const mapFieldsWithRecipients = (
|
||||
fields: Field[],
|
||||
recipients: Recipient[],
|
||||
): DocumentField[] => {
|
||||
return fields.map((field) => {
|
||||
const recipient = recipients.find((recipient) => recipient.id === field.recipientId) || {
|
||||
id: field.recipientId,
|
||||
name: 'Unknown',
|
||||
email: 'Unknown',
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
};
|
||||
|
||||
return { ...field, recipient, signature: null };
|
||||
});
|
||||
};
|
||||
|
||||
export const DocumentReadOnlyFields = ({
|
||||
documentMeta,
|
||||
fields,
|
||||
recipientIds = [],
|
||||
showFieldStatus = true,
|
||||
showRecipientTooltip = false,
|
||||
showRecipientColors = false,
|
||||
}: DocumentReadOnlyFieldsProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const [hiddenFieldIds, setHiddenFieldIds] = useState<Record<string, boolean>>({});
|
||||
|
||||
const handleHideField = (fieldId: string) => {
|
||||
setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true }));
|
||||
};
|
||||
|
||||
return (
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{fields.map(
|
||||
(field) =>
|
||||
!hiddenFieldIds[field.secondaryId] && (
|
||||
<FieldRootContainer
|
||||
field={field}
|
||||
key={field.id}
|
||||
color={
|
||||
showRecipientColors
|
||||
? getRecipientColorStyles(
|
||||
Math.max(
|
||||
recipientIds.findIndex((id) => id === field.recipientId),
|
||||
0,
|
||||
),
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{showRecipientTooltip && (
|
||||
<div className="absolute -right-3 -top-3">
|
||||
<PopoverHover
|
||||
trigger={
|
||||
<Avatar className="h-6 w-6 border-2 border-solid border-gray-200/50 transition-colors hover:border-gray-200">
|
||||
<AvatarFallback className="bg-neutral-50 text-xs text-gray-400">
|
||||
{extractInitials(field.recipient.name || field.recipient.email)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
}
|
||||
contentProps={{
|
||||
className: 'relative flex w-fit flex-col p-4 text-sm',
|
||||
}}
|
||||
>
|
||||
{showFieldStatus && (
|
||||
<Badge
|
||||
className="mx-auto mb-1 py-0.5"
|
||||
variant={
|
||||
field.recipient.signingStatus === SigningStatus.SIGNED
|
||||
? 'default'
|
||||
: 'secondary'
|
||||
}
|
||||
>
|
||||
{field.recipient.signingStatus === SigningStatus.SIGNED ? (
|
||||
<>
|
||||
<SignatureIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Signed</Trans>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Clock className="mr-1 h-3 w-3" />
|
||||
<Trans>Pending</Trans>
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<p className="text-center font-semibold">
|
||||
<span>
|
||||
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])} field
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-center text-xs">
|
||||
{field.recipient.name
|
||||
? `${field.recipient.name} (${field.recipient.email})`
|
||||
: field.recipient.email}{' '}
|
||||
</p>
|
||||
|
||||
<button
|
||||
className="absolute right-0 top-0 my-1 p-2 focus:outline-none focus-visible:ring-0"
|
||||
onClick={() => handleHideField(field.secondaryId)}
|
||||
title="Hide field"
|
||||
>
|
||||
<EyeOffIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</PopoverHover>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FieldContent field={field} documentMeta={documentMeta} />
|
||||
</FieldRootContainer>
|
||||
),
|
||||
)}
|
||||
</ElementVisible>
|
||||
);
|
||||
};
|
||||
@ -1,71 +1,18 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import type { Field } from '@prisma/client';
|
||||
import { type Field, FieldType } from '@prisma/client';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
|
||||
import type { RecipientColorStyles } from '../../lib/recipient-colors';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Card, CardContent } from '../../primitives/card';
|
||||
|
||||
export type FieldRootContainerProps = {
|
||||
field: Field;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export type FieldContainerPortalProps = {
|
||||
field: Field;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
cardClassName?: string;
|
||||
};
|
||||
|
||||
const getCardClassNames = (
|
||||
field: Field,
|
||||
parsedField: TFieldMetaSchema | null,
|
||||
isValidating: boolean,
|
||||
checkBoxOrRadio: boolean,
|
||||
cardClassName?: string,
|
||||
) => {
|
||||
const baseClasses =
|
||||
'field--FieldRootContainer field-card-container relative z-20 h-full w-full transition-all';
|
||||
|
||||
const insertedClasses =
|
||||
'bg-primary/20 border-primary ring-primary/20 ring-offset-primary/20 ring-2 ring-offset-2 dark:shadow-none';
|
||||
const nonRequiredClasses =
|
||||
'border-yellow-300 shadow-none ring-2 ring-yellow-100 ring-offset-2 ring-offset-yellow-100 dark:border-2';
|
||||
const validatingClasses = 'border-orange-300 ring-1 ring-orange-300';
|
||||
const requiredClasses =
|
||||
'border-red-500 shadow-none ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 hover:text-red-500';
|
||||
const requiredCheckboxRadioClasses = 'border-dashed border-red-500';
|
||||
|
||||
if (checkBoxOrRadio) {
|
||||
return cn(
|
||||
{
|
||||
[insertedClasses]: field.inserted,
|
||||
'ring-offset-yellow-200 border-dashed border-yellow-300 ring-2 ring-yellow-200 ring-offset-2 dark:shadow-none':
|
||||
!field.inserted && !parsedField?.required,
|
||||
'shadow-none': !field.inserted,
|
||||
[validatingClasses]: !field.inserted && isValidating,
|
||||
[requiredCheckboxRadioClasses]: !field.inserted && parsedField?.required,
|
||||
},
|
||||
cardClassName,
|
||||
);
|
||||
}
|
||||
|
||||
return cn(
|
||||
baseClasses,
|
||||
{
|
||||
[insertedClasses]: field.inserted,
|
||||
[nonRequiredClasses]: !field.inserted && !parsedField?.required,
|
||||
'shadow-none': !field.inserted && checkBoxOrRadio,
|
||||
[validatingClasses]: !field.inserted && isValidating,
|
||||
[requiredClasses]: !field.inserted && parsedField?.required && !checkBoxOrRadio,
|
||||
},
|
||||
cardClassName,
|
||||
);
|
||||
};
|
||||
|
||||
export function FieldContainerPortal({
|
||||
@ -76,13 +23,10 @@ export function FieldContainerPortal({
|
||||
const coords = useFieldPageCoords(field);
|
||||
|
||||
const isCheckboxOrRadioField = field.type === 'CHECKBOX' || field.type === 'RADIO';
|
||||
const isFieldSigned = field.inserted;
|
||||
|
||||
const style = {
|
||||
top: `${coords.y}px`,
|
||||
left: `${coords.x}px`,
|
||||
// height: `${coords.height}px`,
|
||||
// width: `${coords.width}px`,
|
||||
...(!isCheckboxOrRadioField && {
|
||||
height: `${coords.height}px`,
|
||||
width: `${coords.width}px`,
|
||||
@ -97,7 +41,14 @@ export function FieldContainerPortal({
|
||||
);
|
||||
}
|
||||
|
||||
export function FieldRootContainer({ field, children, cardClassName }: FieldContainerPortalProps) {
|
||||
export type FieldRootContainerProps = {
|
||||
field: Field;
|
||||
color?: RecipientColorStyles;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function FieldRootContainer({ field, children, color, className }: FieldRootContainerProps) {
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -121,33 +72,26 @@ export function FieldRootContainer({ field, children, cardClassName }: FieldCont
|
||||
};
|
||||
}, []);
|
||||
|
||||
const parsedField = useMemo(
|
||||
() => (field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null),
|
||||
[field.fieldMeta],
|
||||
);
|
||||
const isCheckboxOrRadio = useMemo(
|
||||
() => parsedField?.type === 'checkbox' || parsedField?.type === 'radio',
|
||||
[parsedField],
|
||||
);
|
||||
|
||||
const cardClassNames = useMemo(
|
||||
() => getCardClassNames(field, parsedField, isValidating, isCheckboxOrRadio, cardClassName),
|
||||
[field, parsedField, isValidating, isCheckboxOrRadio, cardClassName],
|
||||
);
|
||||
|
||||
return (
|
||||
<FieldContainerPortal field={field}>
|
||||
<Card
|
||||
<div
|
||||
id={`field-${field.id}`}
|
||||
ref={ref}
|
||||
data-field-type={field.type}
|
||||
data-inserted={field.inserted ? 'true' : 'false'}
|
||||
className={cardClassNames}
|
||||
className={cn(
|
||||
'field--FieldRootContainer field-card-container dark-mode-disabled group relative z-20 flex h-full w-full items-center rounded-[2px] bg-white/90 ring-2 ring-gray-200 transition-all',
|
||||
color?.base,
|
||||
{
|
||||
'px-2': field.type !== FieldType.SIGNATURE && field.type !== FieldType.FREE_SIGNATURE,
|
||||
'justify-center': !field.inserted,
|
||||
'ring-orange-300': isValidating && isFieldUnsignedAndRequired(field),
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<CardContent className="text-foreground hover:shadow-primary-foreground group flex h-full w-full flex-col items-center justify-center p-2">
|
||||
{children}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{children}
|
||||
</div>
|
||||
</FieldContainerPortal>
|
||||
);
|
||||
}
|
||||
|
||||
95
packages/ui/lib/recipient-colors.ts
Normal file
95
packages/ui/lib/recipient-colors.ts
Normal file
@ -0,0 +1,95 @@
|
||||
// !: We declare all of our classes here since TailwindCSS will remove any unused CSS classes,
|
||||
// !: therefore doing this at runtime is not possible without whitelisting a set of classnames.
|
||||
// !:
|
||||
// !: This will later be improved as we move to a CSS variable approach and rotate the lightness
|
||||
|
||||
export type RecipientColorMap = Record<number, RecipientColorStyles>;
|
||||
|
||||
export type RecipientColorStyles = {
|
||||
base: string;
|
||||
fieldItem: string;
|
||||
fieldItemInitials: string;
|
||||
comboxBoxTrigger: string;
|
||||
comboxBoxItem: string;
|
||||
};
|
||||
|
||||
// !: values of the declared variable to do all the background, border and shadow styles.
|
||||
export const RECIPIENT_COLOR_STYLES = {
|
||||
green: {
|
||||
base: 'ring-recipient-green hover:bg-recipient-green/30',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-green',
|
||||
comboxBoxTrigger:
|
||||
'ring-2 ring-recipient-green hover:bg-recipient-green/15 active:bg-recipient-green/15 shadow-[0_0_0_5px_hsl(var(--recipient-green)/10%),0_0_0_2px_hsl(var(--recipient-green)/60%),0_0_0_0.5px_hsl(var(--recipient-green))]',
|
||||
comboxBoxItem: 'hover:bg-recipient-green/15 active:bg-recipient-green/15',
|
||||
},
|
||||
|
||||
blue: {
|
||||
base: 'ring-recipient-blue hover:bg-recipient-blue/30',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-blue',
|
||||
comboxBoxTrigger:
|
||||
'ring-2 ring-recipient-blue hover:bg-recipient-blue/15 active:bg-recipient-blue/15 shadow-[0_0_0_5px_hsl(var(--recipient-blue)/10%),0_0_0_2px_hsl(var(--recipient-blue)/60%),0_0_0_0.5px_hsl(var(--recipient-blue))]',
|
||||
comboxBoxItem: 'ring-recipient-blue hover:bg-recipient-blue/15 active:bg-recipient-blue/15',
|
||||
},
|
||||
|
||||
purple: {
|
||||
base: 'ring-recipient-purple hover:bg-recipient-purple/30',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-purple',
|
||||
comboxBoxTrigger:
|
||||
'ring-2 ring-recipient-purple hover:bg-recipient-purple/15 active:bg-recipient-purple/15 shadow-[0_0_0_5px_hsl(var(--recipient-purple)/10%),0_0_0_2px_hsl(var(--recipient-purple)/60%),0_0_0_0.5px_hsl(var(--recipient-purple))]',
|
||||
comboxBoxItem: 'hover:bg-recipient-purple/15 active:bg-recipient-purple/15',
|
||||
},
|
||||
|
||||
orange: {
|
||||
base: 'ring-recipient-orange hover:bg-recipient-orange/30',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-orange',
|
||||
comboxBoxTrigger:
|
||||
'ring-2 ring-recipient-orange hover:bg-recipient-orange/15 active:bg-recipient-orange/15 shadow-[0_0_0_5px_hsl(var(--recipient-orange)/10%),0_0_0_2px_hsl(var(--recipient-orange)/60%),0_0_0_0.5px_hsl(var(--recipient-orange))]',
|
||||
comboxBoxItem: 'hover:bg-recipient-orange/15 active:bg-recipient-orange/15',
|
||||
},
|
||||
|
||||
yellow: {
|
||||
base: 'ring-recipient-yellow hover:bg-recipient-yellow/30',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-yellow',
|
||||
comboxBoxTrigger:
|
||||
'ring-2 ring-recipient-yellow hover:bg-recipient-yellow/15 active:bg-recipient-yellow/15 shadow-[0_0_0_5px_hsl(var(--recipient-yellow)/10%),0_0_0_2px_hsl(var(--recipient-yellow)/60%),0_0_0_0.5px_hsl(var(--recipient-yellow))]',
|
||||
comboxBoxItem: 'hover:bg-recipient-yellow/15 active:bg-recipient-yellow/15',
|
||||
},
|
||||
|
||||
pink: {
|
||||
base: 'ring-recipient-pink hover:bg-recipient-pink/30',
|
||||
fieldItem: 'group/field-item rounded-[2px]',
|
||||
fieldItemInitials: 'group-hover/field-item:bg-recipient-pink',
|
||||
comboxBoxTrigger:
|
||||
'ring-2 ring-recipient-pink hover:bg-recipient-pink/15 active:bg-recipient-pink/15 shadow-[0_0_0_5px_hsl(var(--recipient-pink)/10%),0_0_0_2px_hsl(var(--recipient-pink)/60%),0_0_0_0.5px_hsl(var(--recipient-pink',
|
||||
comboxBoxItem: 'hover:bg-recipient-pink/15 active:bg-recipient-pink/15',
|
||||
},
|
||||
} satisfies Record<string, RecipientColorStyles>;
|
||||
|
||||
export type CombinedStylesKey = keyof typeof RECIPIENT_COLOR_STYLES;
|
||||
|
||||
export const AVAILABLE_RECIPIENT_COLORS = [
|
||||
'green',
|
||||
'blue',
|
||||
'purple',
|
||||
'orange',
|
||||
'yellow',
|
||||
'pink',
|
||||
] satisfies CombinedStylesKey[];
|
||||
|
||||
export const useRecipientColors = (index: number) => {
|
||||
const key = AVAILABLE_RECIPIENT_COLORS[index % AVAILABLE_RECIPIENT_COLORS.length];
|
||||
|
||||
return RECIPIENT_COLOR_STYLES[key];
|
||||
};
|
||||
|
||||
export const getRecipientColorStyles = (index: number) => {
|
||||
// Disabling the rule since the hook doesn't do anything special and can
|
||||
// be used universally.
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return useRecipientColors(index);
|
||||
};
|
||||
@ -1,108 +0,0 @@
|
||||
// !: We declare all of our classes here since TailwindCSS will remove any unused CSS classes,
|
||||
// !: therefore doing this at runtime is not possible without whitelisting a set of classnames.
|
||||
// !:
|
||||
// !: This will later be improved as we move to a CSS variable approach and rotate the lightness
|
||||
// !: values of the declared variable to do all the background, border and shadow styles.
|
||||
export const SIGNER_COLOR_STYLES = {
|
||||
green: {
|
||||
default: {
|
||||
background: 'bg-[hsl(var(--signer-green))]',
|
||||
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-green)/10%),0_0_0_2px_hsl(var(--signer-green)/60%),0_0_0_0.5px_hsl(var(--signer-green))]',
|
||||
fieldItem:
|
||||
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-green))]/10 hover:to-[hsl(var(--signer-green))]/10',
|
||||
fieldItemInitials:
|
||||
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-green))]',
|
||||
comboxBoxItem:
|
||||
'hover:bg-[hsl(var(--signer-green)/15%)] active:bg-[hsl(var(--signer-green)/15%)]',
|
||||
},
|
||||
},
|
||||
|
||||
blue: {
|
||||
default: {
|
||||
background: 'bg-[hsl(var(--signer-blue))]',
|
||||
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-blue)/10%),0_0_0_2px_hsl(var(--signer-blue)/60%),0_0_0_0.5px_hsl(var(--signer-blue))]',
|
||||
fieldItem:
|
||||
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-blue))]/10 hover:to-[hsl(var(--signer-blue))]/10',
|
||||
fieldItemInitials:
|
||||
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-blue))]',
|
||||
comboxBoxItem:
|
||||
'hover:bg-[hsl(var(--signer-blue)/15%)] active:bg-[hsl(var(--signer-blue)/15%)]',
|
||||
},
|
||||
},
|
||||
|
||||
purple: {
|
||||
default: {
|
||||
background: 'bg-[hsl(var(--signer-purple))]',
|
||||
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-purple)/10%),0_0_0_2px_hsl(var(--signer-purple)/60%),0_0_0_0.5px_hsl(var(--signer-purple))]',
|
||||
fieldItem:
|
||||
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-purple))]/10 hover:to-[hsl(var(--signer-purple))]/10',
|
||||
fieldItemInitials:
|
||||
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-purple))]',
|
||||
comboxBoxItem:
|
||||
'hover:bg-[hsl(var(--signer-purple)/15%)] active:bg-[hsl(var(--signer-purple)/15%)]',
|
||||
},
|
||||
},
|
||||
|
||||
orange: {
|
||||
default: {
|
||||
background: 'bg-[hsl(var(--signer-orange))]',
|
||||
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-orange)/10%),0_0_0_2px_hsl(var(--signer-orange)/60%),0_0_0_0.5px_hsl(var(--signer-orange))]',
|
||||
fieldItem:
|
||||
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-orange))]/10 hover:to-[hsl(var(--signer-orange))]/10',
|
||||
fieldItemInitials:
|
||||
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-orange))]',
|
||||
comboxBoxItem:
|
||||
'hover:bg-[hsl(var(--signer-orange)/15%)] active:bg-[hsl(var(--signer-orange)/15%)]',
|
||||
},
|
||||
},
|
||||
|
||||
yellow: {
|
||||
default: {
|
||||
background: 'bg-[hsl(var(--signer-yellow))]',
|
||||
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-yellow)/10%),0_0_0_2px_hsl(var(--signer-yellow)/60%),0_0_0_0.5px_hsl(var(--signer-yellow))]',
|
||||
fieldItem:
|
||||
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-yellow))]/10 hover:to-[hsl(var(--signer-yellow))]/10',
|
||||
fieldItemInitials:
|
||||
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-yellow))]',
|
||||
comboxBoxItem:
|
||||
'hover:bg-[hsl(var(--signer-yellow)/15%)] active:bg-[hsl(var(--signer-yellow)/15%)]',
|
||||
},
|
||||
},
|
||||
|
||||
pink: {
|
||||
default: {
|
||||
background: 'bg-[hsl(var(--signer-pink))]',
|
||||
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-pink)/10%),0_0_0_2px_hsl(var(--signer-pink)/60%),0_0_0_0.5px_hsl(var(--signer-pink))]',
|
||||
fieldItem:
|
||||
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-pink))]/10 hover:to-[hsl(var(--signer-pink))]/10',
|
||||
fieldItemInitials:
|
||||
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-pink))]',
|
||||
comboxBoxItem:
|
||||
'hover:bg-[hsl(var(--signer-pink)/15%)] active:bg-[hsl(var(--signer-pink)/15%)]',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type CombinedStylesKey = keyof typeof SIGNER_COLOR_STYLES;
|
||||
|
||||
export const AVAILABLE_SIGNER_COLORS = [
|
||||
'green',
|
||||
'blue',
|
||||
'purple',
|
||||
'orange',
|
||||
'yellow',
|
||||
'pink',
|
||||
] satisfies CombinedStylesKey[];
|
||||
|
||||
export const useSignerColors = (index: number) => {
|
||||
const key = AVAILABLE_SIGNER_COLORS[index % AVAILABLE_SIGNER_COLORS.length];
|
||||
|
||||
return SIGNER_COLOR_STYLES[key];
|
||||
};
|
||||
|
||||
export const getSignerColorStyles = (index: number) => {
|
||||
// Disabling the rule since the hook doesn't do anything special and can
|
||||
// be used universally.
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return useSignerColors(index);
|
||||
};
|
||||
@ -3,9 +3,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
import { FieldType, RecipientRole, SendStatus } from '@prisma/client';
|
||||
import { FieldType, Prisma, RecipientRole, SendStatus } from '@prisma/client';
|
||||
import {
|
||||
CalendarDays,
|
||||
CheckSquare,
|
||||
@ -38,7 +37,7 @@ import {
|
||||
} from '@documenso/lib/utils/recipients';
|
||||
|
||||
import { FieldToolTip } from '../../components/field/field-tooltip';
|
||||
import { useSignerColors } from '../../lib/signer-colors';
|
||||
import { useRecipientColors } from '../../lib/recipient-colors';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Alert, AlertDescription } from '../alert';
|
||||
import { Card, CardContent } from '../card';
|
||||
@ -175,9 +174,10 @@ export const AddFieldsFormPartial = ({
|
||||
null,
|
||||
);
|
||||
const selectedSignerIndex = recipients.findIndex((r) => r.id === selectedSigner?.id);
|
||||
const selectedSignerStyles = useSignerColors(
|
||||
const selectedSignerStyles = useRecipientColors(
|
||||
selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
|
||||
);
|
||||
|
||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||
|
||||
const filterFieldsWithEmptyValues = (fields: typeof localFields, fieldType: string) =>
|
||||
@ -593,13 +593,12 @@ export const AddFieldsFormPartial = ({
|
||||
{selectedField && (
|
||||
<div
|
||||
className={cn(
|
||||
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200 [container-type:size]',
|
||||
selectedSignerStyles.default.base,
|
||||
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center rounded-[2px] bg-white ring-2 transition duration-200 [container-type:size]',
|
||||
selectedSignerStyles?.base,
|
||||
{
|
||||
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
|
||||
'dark:text-black/60': isFieldWithinBounds,
|
||||
},
|
||||
// selectedField === FieldType.SIGNATURE && fontCaveat.className,
|
||||
)}
|
||||
style={{
|
||||
top: coords.y,
|
||||
@ -646,7 +645,6 @@ export const AddFieldsFormPartial = ({
|
||||
setCurrentField(field);
|
||||
handleAdvancedSettings();
|
||||
}}
|
||||
hideRecipients={hideRecipients}
|
||||
hasErrors={!!hasFieldError}
|
||||
active={activeFieldId === field.formId}
|
||||
onFieldActivate={() => setActiveFieldId(field.formId)}
|
||||
@ -677,7 +675,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||
@ -702,7 +699,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||
@ -728,7 +724,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||
@ -754,7 +749,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
@ -780,7 +774,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
@ -806,7 +799,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
@ -832,7 +824,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
@ -858,7 +849,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
@ -884,7 +874,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
@ -911,7 +900,6 @@ export const AddFieldsFormPartial = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
|
||||
// selectedSignerStyles.borderClass,
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
|
||||
@ -25,6 +25,10 @@ import {
|
||||
DocumentGlobalAuthActionSelect,
|
||||
DocumentGlobalAuthActionTooltip,
|
||||
} from '@documenso/ui/components/document/document-global-auth-action-select';
|
||||
import {
|
||||
DocumentReadOnlyFields,
|
||||
mapFieldsWithRecipients,
|
||||
} from '@documenso/ui/components/document/document-read-only-fields';
|
||||
import {
|
||||
DocumentVisibilitySelect,
|
||||
DocumentVisibilityTooltip,
|
||||
@ -61,7 +65,6 @@ import {
|
||||
DocumentFlowFormContainerHeader,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { ShowFieldItem } from './show-field-item';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
|
||||
export type AddSettingsFormProps = {
|
||||
@ -173,10 +176,13 @@ export const AddSettingsFormPartial = ({
|
||||
/>
|
||||
|
||||
<DocumentFlowFormContainerContent>
|
||||
{isDocumentPdfLoaded &&
|
||||
fields.map((field, index) => (
|
||||
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
||||
))}
|
||||
{isDocumentPdfLoaded && (
|
||||
<DocumentReadOnlyFields
|
||||
showRecipientColors={true}
|
||||
recipientIds={recipients.map((recipient) => recipient.id)}
|
||||
fields={mapFieldsWithRecipients(fields, recipients)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<fieldset
|
||||
|
||||
@ -23,6 +23,10 @@ import { RecipientActionAuthSelect } from '@documenso/ui/components/recipient/re
|
||||
import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
import {
|
||||
DocumentReadOnlyFields,
|
||||
mapFieldsWithRecipients,
|
||||
} from '../../components/document/document-read-only-fields';
|
||||
import { Button } from '../button';
|
||||
import { Checkbox } from '../checkbox';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
|
||||
@ -40,7 +44,6 @@ import {
|
||||
DocumentFlowFormContainerHeader,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { ShowFieldItem } from './show-field-item';
|
||||
import { SigningOrderConfirmation } from './signing-order-confirmation';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
|
||||
@ -368,10 +371,13 @@ export const AddSignersFormPartial = ({
|
||||
description={documentFlow.description}
|
||||
/>
|
||||
<DocumentFlowFormContainerContent>
|
||||
{isDocumentPdfLoaded &&
|
||||
fields.map((field, index) => (
|
||||
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
||||
))}
|
||||
{isDocumentPdfLoaded && (
|
||||
<DocumentReadOnlyFields
|
||||
showRecipientColors={true}
|
||||
recipientIds={recipients.map((recipient) => recipient.id)}
|
||||
fields={mapFieldsWithRecipients(fields, recipients)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AnimateGenericFadeInOut motionKey={showAdvancedSettings ? 'Show' : 'Hide'}>
|
||||
<Form {...form}>
|
||||
|
||||
@ -16,6 +16,10 @@ import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
|
||||
import { CopyTextButton } from '../../components/common/copy-text-button';
|
||||
import { DocumentEmailCheckboxes } from '../../components/document/document-email-checkboxes';
|
||||
import {
|
||||
DocumentReadOnlyFields,
|
||||
mapFieldsWithRecipients,
|
||||
} from '../../components/document/document-read-only-fields';
|
||||
import { AvatarWithText } from '../avatar';
|
||||
import { FormErrorMessage } from '../form/form-error-message';
|
||||
import { Input } from '../input';
|
||||
@ -31,7 +35,6 @@ import {
|
||||
DocumentFlowFormContainerHeader,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { ShowFieldItem } from './show-field-item';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
|
||||
export type AddSubjectFormProps = {
|
||||
@ -103,10 +106,13 @@ export const AddSubjectFormPartial = ({
|
||||
/>
|
||||
<DocumentFlowFormContainerContent>
|
||||
<div className="flex flex-col">
|
||||
{isDocumentPdfLoaded &&
|
||||
fields.map((field, index) => (
|
||||
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
||||
))}
|
||||
{isDocumentPdfLoaded && (
|
||||
<DocumentReadOnlyFields
|
||||
showRecipientColors={true}
|
||||
recipientIds={recipients.map((recipient) => recipient.id)}
|
||||
fields={mapFieldsWithRecipients(fields, recipients)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tabs
|
||||
onValueChange={(value) =>
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import type { TCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
|
||||
import { FieldIcon } from '../field-icon';
|
||||
import type { TDocumentFlowFormSchema } from '../types';
|
||||
|
||||
type Field = TDocumentFlowFormSchema['fields'][0];
|
||||
|
||||
export type CheckboxFieldProps = {
|
||||
field: Field;
|
||||
};
|
||||
|
||||
export const CheckboxField = ({ field }: CheckboxFieldProps) => {
|
||||
let parsedFieldMeta: TCheckboxFieldMeta | undefined = undefined;
|
||||
|
||||
if (field.fieldMeta) {
|
||||
parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||
}
|
||||
|
||||
if (parsedFieldMeta && (!parsedFieldMeta.values || parsedFieldMeta.values.length === 0)) {
|
||||
return <FieldIcon fieldMeta={field.fieldMeta} type={field.type} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
{!parsedFieldMeta?.values ? (
|
||||
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
|
||||
) : (
|
||||
parsedFieldMeta.values.map((item: { value: string; checked: boolean }, index: number) => (
|
||||
<div key={index} className="flex items-center gap-x-1.5">
|
||||
<Checkbox
|
||||
className="dark:border-field-border h-3 w-3 bg-white"
|
||||
id={`checkbox-${index}`}
|
||||
checked={item.checked}
|
||||
/>
|
||||
<Label htmlFor={`checkbox-${index}`} className="text-xs font-normal text-black">
|
||||
{item.value}
|
||||
</Label>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,49 +0,0 @@
|
||||
import { ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import type { TRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
|
||||
|
||||
import { FieldIcon } from '../field-icon';
|
||||
import type { TDocumentFlowFormSchema } from '../types';
|
||||
|
||||
type Field = TDocumentFlowFormSchema['fields'][0];
|
||||
|
||||
export type RadioFieldProps = {
|
||||
field: Field;
|
||||
};
|
||||
|
||||
export const RadioField = ({ field }: RadioFieldProps) => {
|
||||
let parsedFieldMeta: TRadioFieldMeta | undefined = undefined;
|
||||
|
||||
if (field.fieldMeta) {
|
||||
parsedFieldMeta = ZRadioFieldMeta.parse(field.fieldMeta);
|
||||
}
|
||||
|
||||
if (parsedFieldMeta && (!parsedFieldMeta.values || parsedFieldMeta.values.length === 0)) {
|
||||
return <FieldIcon fieldMeta={field.fieldMeta} type={field.type} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{!parsedFieldMeta?.values ? (
|
||||
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
|
||||
) : (
|
||||
<RadioGroup className="gap-y-1">
|
||||
{parsedFieldMeta.values?.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-x-1.5">
|
||||
<RadioGroupItem
|
||||
className="dark:border-field-border pointer-events-none h-3 w-3"
|
||||
value={item.value}
|
||||
id={`option-${index}`}
|
||||
checked={item.checked}
|
||||
/>
|
||||
<Label htmlFor={`option-${index}`} className="text-xs font-normal text-black">
|
||||
{item.value}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
188
packages/ui/primitives/document-flow/field-content.tsx
Normal file
188
packages/ui/primitives/document-flow/field-content.tsx
Normal file
@ -0,0 +1,188 @@
|
||||
import { useLingui } from '@lingui/react';
|
||||
import type { DocumentMeta, Signature, TemplateMeta } from '@prisma/client';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import {
|
||||
DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
convertToLocalSystemFormat,
|
||||
} from '@documenso/lib/constants/date-formats';
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Checkbox } from '../checkbox';
|
||||
import { Label } from '../label';
|
||||
import { RadioGroup, RadioGroupItem } from '../radio-group';
|
||||
import { FRIENDLY_FIELD_TYPE } from './types';
|
||||
|
||||
type FieldIconProps = {
|
||||
/**
|
||||
* Loose field type since this is used for partial fields.
|
||||
*/
|
||||
field: {
|
||||
inserted?: boolean;
|
||||
customText?: string;
|
||||
type: FieldType;
|
||||
fieldMeta?: TFieldMetaSchema | null;
|
||||
signature?: Signature | null;
|
||||
};
|
||||
documentMeta?: DocumentMeta | TemplateMeta;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the content inside field containers prior to sealing.
|
||||
*/
|
||||
export const FieldContent = ({ field, documentMeta }: FieldIconProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { type, fieldMeta } = field;
|
||||
|
||||
// Only render checkbox if values exist, otherwise render the empty checkbox field content.
|
||||
if (
|
||||
field.type === FieldType.CHECKBOX &&
|
||||
field.fieldMeta?.type === 'checkbox' &&
|
||||
field.fieldMeta.values &&
|
||||
field.fieldMeta.values.length > 0
|
||||
) {
|
||||
let checkedValues: string[] = [];
|
||||
|
||||
try {
|
||||
checkedValues = fromCheckboxValue(field.customText ?? '');
|
||||
} catch (err) {
|
||||
// Do nothing.
|
||||
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1 py-0.5">
|
||||
{field.fieldMeta.values.map((item, index) => (
|
||||
<div key={index} className="flex items-center">
|
||||
<Checkbox
|
||||
className="h-3 w-3"
|
||||
id={`checkbox-${index}`}
|
||||
checked={checkedValues.includes(
|
||||
item.value === '' ? `empty-value-${index + 1}` : item.value, // I got no idea...
|
||||
)}
|
||||
/>
|
||||
|
||||
{item.value && (
|
||||
<Label
|
||||
htmlFor={`checkbox-${index}`}
|
||||
className="text-foreground ml-1.5 text-xs font-normal"
|
||||
>
|
||||
{item.value}
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Only render radio if values exist, otherwise render the empty radio field content.
|
||||
if (
|
||||
field.type === FieldType.RADIO &&
|
||||
field.fieldMeta?.type === 'radio' &&
|
||||
field.fieldMeta.values &&
|
||||
field.fieldMeta.values.length > 0
|
||||
) {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2 py-0.5">
|
||||
<RadioGroup className="gap-y-1">
|
||||
{field.fieldMeta.values.map((item, index) => (
|
||||
<div key={index} className="flex items-center">
|
||||
<RadioGroupItem
|
||||
className="pointer-events-none h-3 w-3"
|
||||
value={item.value}
|
||||
id={`option-${index}`}
|
||||
checked={item.value === field.customText}
|
||||
/>
|
||||
{item.value && (
|
||||
<Label
|
||||
htmlFor={`option-${index}`}
|
||||
className="text-foreground ml-1.5 text-xs font-normal"
|
||||
>
|
||||
{item.value}
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
field.type === FieldType.DROPDOWN &&
|
||||
field.fieldMeta?.type === 'dropdown' &&
|
||||
!field.inserted
|
||||
) {
|
||||
return (
|
||||
<div className="text-field-card-foreground flex flex-row items-center py-0.5 text-[clamp(0.07rem,25cqw,0.825rem)] text-sm">
|
||||
<p>Select</p>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
field.type === FieldType.SIGNATURE &&
|
||||
field.signature?.signatureImageAsBase64 &&
|
||||
field.inserted
|
||||
) {
|
||||
return (
|
||||
<img
|
||||
src={field.signature.signatureImageAsBase64}
|
||||
alt="Signature"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let textToDisplay = fieldMeta?.label || _(FRIENDLY_FIELD_TYPE[type]) || '';
|
||||
|
||||
const isSignatureField =
|
||||
field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE;
|
||||
|
||||
// Trim default labels.
|
||||
if (textToDisplay.length > 20) {
|
||||
textToDisplay = textToDisplay.substring(0, 20) + '...';
|
||||
}
|
||||
|
||||
if (field.inserted) {
|
||||
if (field.customText) {
|
||||
textToDisplay = field.customText;
|
||||
}
|
||||
|
||||
if (field.type === FieldType.DATE) {
|
||||
textToDisplay = convertToLocalSystemFormat(
|
||||
field.customText ?? '',
|
||||
documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
);
|
||||
}
|
||||
|
||||
if (isSignatureField && field.signature?.typedSignature) {
|
||||
textToDisplay = field.signature.typedSignature;
|
||||
}
|
||||
}
|
||||
|
||||
const textAlign = fieldMeta && 'textAlign' in fieldMeta ? fieldMeta.textAlign : 'left';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'text-field-card-foreground flex h-full w-full items-center justify-center gap-x-1.5 overflow-clip whitespace-nowrap text-center text-[clamp(0.07rem,25cqw,0.825rem)]',
|
||||
{
|
||||
// Using justify instead of align because we also vertically center the text.
|
||||
'justify-start': field.inserted && !isSignatureField && textAlign === 'left',
|
||||
'justify-end': field.inserted && !isSignatureField && textAlign === 'right',
|
||||
'font-signature text-[clamp(0.07rem,25cqw,1.125rem)]': isSignatureField,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{textToDisplay}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,72 +0,0 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import {
|
||||
CalendarDays,
|
||||
CheckSquare,
|
||||
ChevronDown,
|
||||
Contact,
|
||||
Disc,
|
||||
Hash,
|
||||
Mail,
|
||||
Type,
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
|
||||
import type { TFieldMetaSchema as FieldMetaType } from '@documenso/lib/types/field-meta';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
type FieldIconProps = {
|
||||
fieldMeta: FieldMetaType;
|
||||
type: FieldType;
|
||||
};
|
||||
|
||||
const fieldIcons = {
|
||||
[FieldType.INITIALS]: { icon: Contact, label: 'Initials' },
|
||||
[FieldType.EMAIL]: { icon: Mail, label: 'Email' },
|
||||
[FieldType.NAME]: { icon: User, label: 'Name' },
|
||||
[FieldType.DATE]: { icon: CalendarDays, label: 'Date' },
|
||||
[FieldType.TEXT]: { icon: Type, label: 'Text' },
|
||||
[FieldType.NUMBER]: { icon: Hash, label: 'Number' },
|
||||
[FieldType.RADIO]: { icon: Disc, label: 'Radio' },
|
||||
[FieldType.CHECKBOX]: { icon: CheckSquare, label: 'Checkbox' },
|
||||
[FieldType.DROPDOWN]: { icon: ChevronDown, label: 'Select' },
|
||||
};
|
||||
|
||||
export const FieldIcon = ({ fieldMeta, type }: FieldIconProps) => {
|
||||
if (type === 'SIGNATURE' || type === 'FREE_SIGNATURE') {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'text-field-card-foreground font-signature flex items-center justify-center gap-x-1 text-[clamp(0.575rem,25cqw,1.2rem)]',
|
||||
)}
|
||||
>
|
||||
<Trans>Signature</Trans>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const Icon = fieldIcons[type]?.icon;
|
||||
let label;
|
||||
|
||||
if (fieldMeta && (type === 'TEXT' || type === 'NUMBER')) {
|
||||
if (type === 'TEXT' && 'text' in fieldMeta && fieldMeta.text && !fieldMeta.label) {
|
||||
label =
|
||||
fieldMeta.text.length > 20 ? fieldMeta.text.substring(0, 20) + '...' : fieldMeta.text;
|
||||
} else if (fieldMeta.label) {
|
||||
label =
|
||||
fieldMeta.label.length > 20 ? fieldMeta.label.substring(0, 20) + '...' : fieldMeta.label;
|
||||
} else {
|
||||
label = fieldIcons[type]?.label;
|
||||
}
|
||||
} else {
|
||||
label = fieldIcons[type]?.label;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-field-card-foreground flex items-center justify-center gap-x-1.5 text-[clamp(0.425rem,25cqw,0.825rem)]">
|
||||
<Icon className="h-[clamp(0.625rem,20cqw,0.925rem)] w-[clamp(0.625rem,20cqw,0.925rem)]" />{' '}
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -229,11 +229,19 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
|
||||
return (
|
||||
<div ref={ref} className="flex h-full flex-col">
|
||||
<DocumentFlowFormContainerHeader title={title} description={description} />
|
||||
|
||||
<DocumentFlowFormContainerContent>
|
||||
{isDocumentPdfLoaded &&
|
||||
fields.map((field, index) => (
|
||||
fields.map((localField, index) => (
|
||||
<span key={index} className="opacity-75 active:pointer-events-none">
|
||||
<FieldItem key={index} field={field} disabled={true} />
|
||||
<FieldItem
|
||||
key={index}
|
||||
field={localField}
|
||||
disabled={true}
|
||||
fieldClassName={
|
||||
localField.formId === field.formId ? 'ring-red-400' : 'ring-neutral-200'
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
|
||||
|
||||
@ -4,23 +4,21 @@ import { FieldType } from '@prisma/client';
|
||||
import { CopyPlus, Settings2, Trash } from 'lucide-react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Rnd } from 'react-rnd';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
|
||||
import { useSignerColors } from '../../lib/signer-colors';
|
||||
import { useRecipientColors } from '../../lib/recipient-colors';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { CheckboxField } from './advanced-fields/checkbox';
|
||||
import { RadioField } from './advanced-fields/radio';
|
||||
import { FieldIcon } from './field-icon';
|
||||
import { FieldContent } from './field-content';
|
||||
import type { TDocumentFlowFormSchema } from './types';
|
||||
|
||||
type Field = TDocumentFlowFormSchema['fields'][0];
|
||||
|
||||
export type FieldItemProps = {
|
||||
field: Field;
|
||||
fieldClassName?: string;
|
||||
passive?: boolean;
|
||||
disabled?: boolean;
|
||||
minHeight?: number;
|
||||
@ -35,14 +33,17 @@ export type FieldItemProps = {
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
recipientIndex?: number;
|
||||
hideRecipients?: boolean;
|
||||
hasErrors?: boolean;
|
||||
active?: boolean;
|
||||
onFieldActivate?: () => void;
|
||||
onFieldDeactivate?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* The item when editing fields??
|
||||
*/
|
||||
export const FieldItem = ({
|
||||
fieldClassName,
|
||||
field,
|
||||
passive,
|
||||
disabled,
|
||||
@ -58,7 +59,6 @@ export const FieldItem = ({
|
||||
onBlur,
|
||||
onAdvancedSettings,
|
||||
recipientIndex = 0,
|
||||
hideRecipients = false,
|
||||
hasErrors,
|
||||
active,
|
||||
onFieldActivate,
|
||||
@ -73,7 +73,7 @@ export const FieldItem = ({
|
||||
const [settingsActive, setSettingsActive] = useState(false);
|
||||
const $el = useRef(null);
|
||||
|
||||
const signerStyles = useSignerColors(recipientIndex);
|
||||
const signerStyles = useRecipientColors(recipientIndex);
|
||||
|
||||
const advancedField = [
|
||||
'NUMBER',
|
||||
@ -209,7 +209,7 @@ export const FieldItem = ({
|
||||
return createPortal(
|
||||
<Rnd
|
||||
key={coords.pageX + coords.pageY + coords.pageHeight + coords.pageWidth}
|
||||
className={cn('group', {
|
||||
className={cn('dark-mode-disabled group', {
|
||||
'pointer-events-none': passive,
|
||||
'pointer-events-none cursor-not-allowed opacity-75': disabled,
|
||||
'z-50': active && !disabled,
|
||||
@ -260,11 +260,12 @@ export const FieldItem = ({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-full w-full items-center justify-center bg-white',
|
||||
!hasErrors && signerStyles.default.base,
|
||||
!hasErrors && signerStyles.default.fieldItem,
|
||||
'group/field-item relative flex h-full w-full items-center justify-center rounded-[2px] bg-white/90 px-2 ring-2 transition-colors',
|
||||
!hasErrors && signerStyles.base,
|
||||
!hasErrors && signerStyles.fieldItem,
|
||||
fieldClassName,
|
||||
{
|
||||
'rounded-lg border border-red-400 bg-red-400/20 shadow-[0_0_0_5px_theme(colors.red.500/10%),0_0_0_2px_theme(colors.red.500/40%),0_0_0_0.5px_theme(colors.red.500)]':
|
||||
'rounded-[2px] border bg-red-400/20 shadow-[0_0_0_5px_theme(colors.red.500/10%),0_0_0_2px_theme(colors.red.500/40%),0_0_0_0.5px_theme(colors.red.500)] ring-red-400':
|
||||
hasErrors,
|
||||
},
|
||||
!fixedSize && '[container-type:size]',
|
||||
@ -279,37 +280,31 @@ export const FieldItem = ({
|
||||
ref={$el}
|
||||
data-field-id={field.nativeId}
|
||||
>
|
||||
{match(field.type)
|
||||
.with('CHECKBOX', () => <CheckboxField field={field} />)
|
||||
.with('RADIO', () => <RadioField field={field} />)
|
||||
.otherwise(() => (
|
||||
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
|
||||
))}
|
||||
<FieldContent field={field} />
|
||||
|
||||
{!hideRecipients && (
|
||||
<div className="absolute -right-5 top-0 z-20 hidden h-full w-5 items-center justify-center group-hover:flex">
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-5 w-5 flex-col items-center justify-center rounded-r-md text-[0.5rem] font-bold text-white',
|
||||
signerStyles.default.fieldItemInitials,
|
||||
{
|
||||
'!opacity-50': disabled || passive,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{(field.signerEmail?.charAt(0)?.toUpperCase() ?? '') +
|
||||
(field.signerEmail?.charAt(1)?.toUpperCase() ?? '')}
|
||||
</div>
|
||||
{/* On hover, display recipient initials on side of field. */}
|
||||
<div className="absolute -right-5 top-0 z-20 hidden h-full w-5 items-center justify-center group-hover:flex">
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-5 w-5 flex-col items-center justify-center rounded-r-md text-[0.5rem] font-bold text-white opacity-0 transition duration-200 group-hover/field-item:opacity-100',
|
||||
signerStyles.fieldItemInitials,
|
||||
{
|
||||
'!opacity-50': disabled || passive,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{(field.signerEmail?.charAt(0)?.toUpperCase() ?? '') +
|
||||
(field.signerEmail?.charAt(1)?.toUpperCase() ?? '')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!disabled && settingsActive && (
|
||||
<div className="z-[60] mt-1 flex justify-center">
|
||||
<div className="dark:bg-background group flex items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5">
|
||||
<div className="absolute z-[60] mt-1 flex w-full items-center justify-center">
|
||||
<div className="group flex items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5">
|
||||
{advancedField && (
|
||||
<button
|
||||
className="dark:text-muted-foreground/50 dark:hover:text-muted-foreground dark:hover:bg-foreground/10 rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
onClick={onAdvancedSettings}
|
||||
onTouchEnd={onAdvancedSettings}
|
||||
>
|
||||
@ -318,7 +313,7 @@ export const FieldItem = ({
|
||||
)}
|
||||
|
||||
<button
|
||||
className="dark:text-muted-foreground/50 dark:hover:text-muted-foreground dark:hover:bg-foreground/10 rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
onClick={onDuplicate}
|
||||
onTouchEnd={onDuplicate}
|
||||
>
|
||||
@ -326,7 +321,7 @@ export const FieldItem = ({
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="dark:text-muted-foreground/50 dark:hover:text-muted-foreground dark:hover:bg-foreground/10 rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
onClick={onRemove}
|
||||
onTouchEnd={onRemove}
|
||||
>
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { FieldType, type Prisma } from '@prisma/client';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
||||
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Card, CardContent } from '../card';
|
||||
import { FRIENDLY_FIELD_TYPE } from './types';
|
||||
|
||||
export type ShowFieldItemProps = {
|
||||
field: Prisma.FieldGetPayload<null>;
|
||||
recipients: Prisma.RecipientGetPayload<null>[];
|
||||
};
|
||||
|
||||
export const ShowFieldItem = ({ field }: ShowFieldItemProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const coords = useFieldPageCoords(field);
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={cn('pointer-events-none absolute z-10 opacity-75')}
|
||||
style={{
|
||||
top: `${coords.y}px`,
|
||||
left: `${coords.x}px`,
|
||||
height: `${coords.height}px`,
|
||||
width: `${coords.width}px`,
|
||||
}}
|
||||
>
|
||||
<Card className={cn('bg-background h-full w-full [container-type:size]')}>
|
||||
<CardContent
|
||||
className={cn(
|
||||
'text-muted-foreground/50 flex h-full w-full flex-col items-center justify-center p-0 text-[clamp(0.575rem,1.8cqw,1.2rem)] leading-none',
|
||||
field.type === FieldType.SIGNATURE && 'font-signature',
|
||||
)}
|
||||
>
|
||||
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])}
|
||||
|
||||
{/* <p className="text-muted-foreground/50 w-full truncate text-center text-xs">
|
||||
{signerEmail}
|
||||
</p> */}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
@ -12,7 +12,12 @@ export const ElementVisible = ({ target, children }: ElementVisibleProps) => {
|
||||
const observer = new MutationObserver((_mutations) => {
|
||||
const $el = document.querySelector(target);
|
||||
|
||||
setVisible(!!$el);
|
||||
// Wait a fraction of a second to allow the scrollbar to load if it exists.
|
||||
// If we don't wait, then the elements on the first page will be
|
||||
// shifted across.
|
||||
setTimeout(() => {
|
||||
setVisible(!!$el);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
|
||||
@ -30,11 +30,12 @@ PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
type PopoverHoverProps = {
|
||||
trigger: React.ReactNode;
|
||||
side?: 'top' | 'bottom' | 'left' | 'right';
|
||||
children: React.ReactNode;
|
||||
contentProps?: React.ComponentPropsWithoutRef<typeof PopoverContent>;
|
||||
};
|
||||
|
||||
const PopoverHover = ({ trigger, children, contentProps }: PopoverHoverProps) => {
|
||||
const PopoverHover = ({ trigger, children, contentProps, side = 'top' }: PopoverHoverProps) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const isControlled = React.useRef(false);
|
||||
@ -79,7 +80,7 @@ const PopoverHover = ({ trigger, children, contentProps }: PopoverHoverProps) =>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
side="top"
|
||||
side={side}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
{...contentProps}
|
||||
|
||||
@ -9,7 +9,7 @@ import { sortBy } from 'remeda';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
|
||||
import { getSignerColorStyles } from '../lib/signer-colors';
|
||||
import { getRecipientColorStyles } from '../lib/recipient-colors';
|
||||
import { cn } from '../lib/utils';
|
||||
import { Button } from './button';
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from './command';
|
||||
@ -78,12 +78,12 @@ export const RecipientSelector = ({
|
||||
role="combobox"
|
||||
className={cn(
|
||||
'bg-background text-muted-foreground hover:text-foreground justify-between font-normal',
|
||||
getSignerColorStyles(
|
||||
getRecipientColorStyles(
|
||||
Math.max(
|
||||
recipients.findIndex((r) => r.id === selectedRecipient?.id),
|
||||
0,
|
||||
),
|
||||
).default.base,
|
||||
).base,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@ -131,12 +131,12 @@ export const RecipientSelector = ({
|
||||
key={recipient.id}
|
||||
className={cn(
|
||||
'px-2 last:mb-1 [&:not(:first-child)]:mt-1',
|
||||
getSignerColorStyles(
|
||||
getRecipientColorStyles(
|
||||
Math.max(
|
||||
recipients.findIndex((r) => r.id === recipient.id),
|
||||
0,
|
||||
),
|
||||
).default.comboxBoxItem,
|
||||
).comboxBoxItem,
|
||||
{
|
||||
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
|
||||
},
|
||||
|
||||
@ -53,7 +53,7 @@ import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/type
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { getSignerColorStyles, useSignerColors } from '../../lib/signer-colors';
|
||||
import { getRecipientColorStyles, useRecipientColors } from '../../lib/recipient-colors';
|
||||
import type { FieldFormType } from '../document-flow/add-fields';
|
||||
import { FieldAdvancedSettings } from '../document-flow/field-item-advanced-settings';
|
||||
import { Form } from '../form/form';
|
||||
@ -68,7 +68,6 @@ const DEFAULT_WIDTH_PX = MIN_WIDTH_PX * 2.5;
|
||||
|
||||
export type AddTemplateFieldsFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
hideRecipients?: boolean;
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
onSubmit: (_data: TAddTemplateFieldsFormSchema) => void;
|
||||
@ -77,7 +76,6 @@ export type AddTemplateFieldsFormProps = {
|
||||
|
||||
export const AddTemplateFieldsFormPartial = ({
|
||||
documentFlow,
|
||||
hideRecipients = false,
|
||||
recipients,
|
||||
fields,
|
||||
onSubmit,
|
||||
@ -136,7 +134,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
||||
|
||||
const selectedSignerIndex = recipients.findIndex((r) => r.id === selectedSigner?.id);
|
||||
const selectedSignerStyles = useSignerColors(
|
||||
const selectedSignerStyles = useRecipientColors(
|
||||
selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
|
||||
);
|
||||
|
||||
@ -505,8 +503,8 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
{selectedField && (
|
||||
<div
|
||||
className={cn(
|
||||
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200 [container-type:size]',
|
||||
selectedSignerStyles.default.base,
|
||||
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center rounded-[2px] bg-white ring-2 transition duration-200 [container-type:size]',
|
||||
selectedSignerStyles?.base,
|
||||
{
|
||||
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
|
||||
'dark:text-black/60': isFieldWithinBounds,
|
||||
@ -549,7 +547,6 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
setCurrentField(field);
|
||||
handleAdvancedSettings();
|
||||
}}
|
||||
hideRecipients={hideRecipients}
|
||||
active={activeFieldId === field.formId}
|
||||
onFieldActivate={() => setActiveFieldId(field.formId)}
|
||||
onFieldDeactivate={() => setActiveFieldId(null)}
|
||||
@ -557,99 +554,98 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
);
|
||||
})}
|
||||
|
||||
{!hideRecipients && (
|
||||
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
'bg-background text-muted-foreground hover:text-foreground mb-12 mt-2 justify-between font-normal',
|
||||
selectedSignerStyles.default.base,
|
||||
)}
|
||||
>
|
||||
{selectedSigner?.email && (
|
||||
<span className="flex-1 truncate text-left">
|
||||
{selectedSigner?.name} ({selectedSigner?.email})
|
||||
</span>
|
||||
)}
|
||||
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
'bg-background text-muted-foreground hover:text-foreground mb-12 mt-2 justify-between font-normal',
|
||||
selectedSignerStyles?.comboxBoxTrigger,
|
||||
)}
|
||||
>
|
||||
{selectedSigner?.email && (
|
||||
<span className="flex-1 truncate text-left">
|
||||
{selectedSigner?.name} ({selectedSigner?.email})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!selectedSigner?.email && (
|
||||
<span className="gradie flex-1 truncate text-left">
|
||||
{selectedSigner?.email}
|
||||
</span>
|
||||
)}
|
||||
{!selectedSigner?.email && (
|
||||
<span className="gradie flex-1 truncate text-left">
|
||||
{selectedSigner?.email}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command value={selectedSigner?.email}>
|
||||
<CommandInput />
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command value={selectedSigner?.email}>
|
||||
<CommandInput />
|
||||
|
||||
<CommandEmpty>
|
||||
<span className="text-muted-foreground inline-block px-4">
|
||||
<Trans>No recipient matching this description was found.</Trans>
|
||||
</span>
|
||||
</CommandEmpty>
|
||||
<CommandEmpty>
|
||||
<span className="text-muted-foreground inline-block px-4">
|
||||
<Trans>No recipient matching this description was found.</Trans>
|
||||
</span>
|
||||
</CommandEmpty>
|
||||
|
||||
{recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => (
|
||||
<CommandGroup key={roleIndex}>
|
||||
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
||||
{_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
|
||||
{/* Note: This is duplicated in `add-fields.tsx` */}
|
||||
{recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => (
|
||||
<CommandGroup key={roleIndex}>
|
||||
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
||||
{_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
|
||||
</div>
|
||||
|
||||
{roleRecipients.length === 0 && (
|
||||
<div
|
||||
key={`${role}-empty`}
|
||||
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
|
||||
>
|
||||
<Trans>No recipients with this role</Trans>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{roleRecipients.length === 0 && (
|
||||
<div
|
||||
key={`${role}-empty`}
|
||||
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
|
||||
{roleRecipients.map((recipient) => (
|
||||
<CommandItem
|
||||
key={recipient.id}
|
||||
className={cn(
|
||||
'px-2 last:mb-1 [&:not(:first-child)]:mt-1',
|
||||
getRecipientColorStyles(
|
||||
Math.max(
|
||||
recipients.findIndex((r) => r.id === recipient.id),
|
||||
0,
|
||||
),
|
||||
)?.comboxBoxItem,
|
||||
)}
|
||||
onSelect={() => {
|
||||
setSelectedSigner(recipient);
|
||||
setShowRecipientsSelector(false);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cn('text-foreground/70 truncate', {
|
||||
'text-foreground/80': recipient === selectedSigner,
|
||||
})}
|
||||
>
|
||||
<Trans>No recipients with this role</Trans>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{roleRecipients.map((recipient) => (
|
||||
<CommandItem
|
||||
key={recipient.id}
|
||||
className={cn(
|
||||
'px-2 last:mb-1 [&:not(:first-child)]:mt-1',
|
||||
getSignerColorStyles(
|
||||
Math.max(
|
||||
recipients.findIndex((r) => r.id === recipient.id),
|
||||
0,
|
||||
),
|
||||
).default.comboxBoxItem,
|
||||
{recipient.name && (
|
||||
<span title={`${recipient.name} (${recipient.email})`}>
|
||||
{recipient.name} ({recipient.email})
|
||||
</span>
|
||||
)}
|
||||
onSelect={() => {
|
||||
setSelectedSigner(recipient);
|
||||
setShowRecipientsSelector(false);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cn('text-foreground/70 truncate', {
|
||||
'text-foreground/80': recipient === selectedSigner,
|
||||
})}
|
||||
>
|
||||
{recipient.name && (
|
||||
<span title={`${recipient.name} (${recipient.email})`}>
|
||||
{recipient.name} ({recipient.email})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!recipient.name && (
|
||||
<span title={recipient.email}>{recipient.email}</span>
|
||||
)}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
{!recipient.name && (
|
||||
<span title={recipient.email}>{recipient.email}</span>
|
||||
)}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Form {...form}>
|
||||
<div className="-mx-2 flex-1 overflow-y-auto px-2">
|
||||
|
||||
@ -25,6 +25,10 @@ import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-messa
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { toast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import {
|
||||
DocumentReadOnlyFields,
|
||||
mapFieldsWithRecipients,
|
||||
} from '../../components/document/document-read-only-fields';
|
||||
import { Checkbox } from '../checkbox';
|
||||
import {
|
||||
DocumentFlowFormContainerActions,
|
||||
@ -33,7 +37,6 @@ import {
|
||||
DocumentFlowFormContainerHeader,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from '../document-flow/document-flow-root';
|
||||
import { ShowFieldItem } from '../document-flow/show-field-item';
|
||||
import { SigningOrderConfirmation } from '../document-flow/signing-order-confirmation';
|
||||
import type { DocumentFlowStep } from '../document-flow/types';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
|
||||
@ -391,10 +394,13 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
description={documentFlow.description}
|
||||
/>
|
||||
<DocumentFlowFormContainerContent>
|
||||
{isDocumentPdfLoaded &&
|
||||
fields.map((field, index) => (
|
||||
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
||||
))}
|
||||
{isDocumentPdfLoaded && (
|
||||
<DocumentReadOnlyFields
|
||||
showRecipientColors={true}
|
||||
recipientIds={recipients.map((recipient) => recipient.id)}
|
||||
fields={mapFieldsWithRecipients(fields, recipients)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AnimateGenericFadeInOut motionKey={showAdvancedSettings ? 'Show' : 'Hide'}>
|
||||
<Form {...form}>
|
||||
@ -500,6 +506,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
ref={provided.innerRef}
|
||||
className="flex w-full flex-col gap-y-2"
|
||||
>
|
||||
{/* todo */}
|
||||
{signers.map((signer, index) => (
|
||||
<Draggable
|
||||
key={`${signer.id}-${signer.signingOrder}`}
|
||||
|
||||
@ -50,6 +50,10 @@ import {
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
|
||||
import { DocumentEmailCheckboxes } from '../../components/document/document-email-checkboxes';
|
||||
import {
|
||||
DocumentReadOnlyFields,
|
||||
mapFieldsWithRecipients,
|
||||
} from '../../components/document/document-read-only-fields';
|
||||
import { DocumentSignatureSettingsTooltip } from '../../components/document/document-signature-settings-tooltip';
|
||||
import { Combobox } from '../combobox';
|
||||
import {
|
||||
@ -59,7 +63,6 @@ import {
|
||||
DocumentFlowFormContainerHeader,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from '../document-flow/document-flow-root';
|
||||
import { ShowFieldItem } from '../document-flow/show-field-item';
|
||||
import type { DocumentFlowStep } from '../document-flow/types';
|
||||
import { Input } from '../input';
|
||||
import { MultiSelectCombobox } from '../multi-select-combobox';
|
||||
@ -153,10 +156,13 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
/>
|
||||
|
||||
<DocumentFlowFormContainerContent>
|
||||
{isDocumentPdfLoaded &&
|
||||
fields.map((field, index) => (
|
||||
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
||||
))}
|
||||
{isDocumentPdfLoaded && (
|
||||
<DocumentReadOnlyFields
|
||||
showRecipientColors={true}
|
||||
recipientIds={recipients.map((recipient) => recipient.id)}
|
||||
fields={mapFieldsWithRecipients(fields, recipients)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<fieldset
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
:root,
|
||||
.dark-mode-disabled {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
@ -48,12 +49,12 @@
|
||||
|
||||
--gold: 47.9 95.8% 53.1%;
|
||||
|
||||
--signer-green: 100 48% 55%;
|
||||
--signer-blue: 212 56% 50%;
|
||||
--signer-purple: 266 100% 64%;
|
||||
--signer-orange: 36 92% 54%;
|
||||
--signer-yellow: 51 100% 43%;
|
||||
--signer-pink: 313 65% 57%;
|
||||
--recipient-green: 100 48% 55%;
|
||||
--recipient-blue: 212 56% 50%;
|
||||
--recipient-purple: 266 100% 64%;
|
||||
--recipient-orange: 36 92% 54%;
|
||||
--recipient-yellow: 51 100% 43%;
|
||||
--recipient-pink: 313 65% 57%;
|
||||
|
||||
/* Base - Neutral */
|
||||
--new-neutral-50: 0, 0%, 96%;
|
||||
@ -190,7 +191,9 @@
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: 'rlig' 1, 'calt' 1;
|
||||
font-feature-settings:
|
||||
'rlig' 1,
|
||||
'calt' 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,8 +213,12 @@
|
||||
}
|
||||
|
||||
.gradient-border-mask::before {
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
mask-composite: exclude;
|
||||
-webkit-mask-composite: xor;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user