mirror of
https://github.com/documenso/documenso.git
synced 2026-07-24 00:43:40 +10:00
Merge branch 'main' into update-documents-avatar
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -36,6 +36,16 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {
|
|||||||
|
|
||||||
const { mutateAsync: createDocument } = trpc.document.createDocument.useMutation();
|
const { mutateAsync: createDocument } = trpc.document.createDocument.useMutation();
|
||||||
|
|
||||||
|
const disabledMessage = useMemo(() => {
|
||||||
|
if (remaining.documents === 0) {
|
||||||
|
return 'You have reached your document limit.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session?.user.emailVerified) {
|
||||||
|
return 'Verify your email to upload documents.';
|
||||||
|
}
|
||||||
|
}, [remaining.documents, session?.user.emailVerified]);
|
||||||
|
|
||||||
const onFileDrop = async (file: File) => {
|
const onFileDrop = async (file: File) => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -91,6 +101,7 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {
|
|||||||
<DocumentDropzone
|
<DocumentDropzone
|
||||||
className="min-h-[40vh]"
|
className="min-h-[40vh]"
|
||||||
disabled={remaining.documents === 0 || !session?.user.emailVerified}
|
disabled={remaining.documents === 0 || !session?.user.emailVerified}
|
||||||
|
disabledMessage={disabledMessage}
|
||||||
onDrop={onFileDrop}
|
onDrop={onFileDrop}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { DocumentDownloadButton } from '@documenso/ui/components/document/docume
|
|||||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||||
|
|
||||||
|
import { truncateTitle } from '~/helpers/truncate-title';
|
||||||
|
|
||||||
export type CompletedSigningPageProps = {
|
export type CompletedSigningPageProps = {
|
||||||
params: {
|
params: {
|
||||||
token?: string;
|
token?: string;
|
||||||
@@ -36,6 +38,8 @@ export default async function CompletedSigningPage({
|
|||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const truncatedTitle = truncateTitle(document.title);
|
||||||
|
|
||||||
const { documentData } = document;
|
const { documentData } = document;
|
||||||
|
|
||||||
const [fields, recipient] = await Promise.all([
|
const [fields, recipient] = await Promise.all([
|
||||||
@@ -89,7 +93,7 @@ export default async function CompletedSigningPage({
|
|||||||
|
|
||||||
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
||||||
You have signed
|
You have signed
|
||||||
<span className="mt-1.5 block">"{document.title}"</span>
|
<span className="mt-1.5 block">"{truncatedTitle}"</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{match({ status: document.status, deletedAt: document.deletedAt })
|
{match({ status: document.status, deletedAt: document.deletedAt })
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import { Card, CardContent } from '@documenso/ui/primitives/card';
|
|||||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
|
|
||||||
|
import { truncateTitle } from '~/helpers/truncate-title';
|
||||||
|
|
||||||
import { DateField } from './date-field';
|
import { DateField } from './date-field';
|
||||||
import { EmailField } from './email-field';
|
import { EmailField } from './email-field';
|
||||||
import { SigningForm } from './form';
|
import { SigningForm } from './form';
|
||||||
@@ -51,6 +53,8 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const truncatedTitle = truncateTitle(document.title);
|
||||||
|
|
||||||
const { documentData } = document;
|
const { documentData } = document;
|
||||||
|
|
||||||
const { user } = await getServerComponentSession();
|
const { user } = await getServerComponentSession();
|
||||||
@@ -82,7 +86,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
>
|
>
|
||||||
<div className="mx-auto w-full max-w-screen-xl">
|
<div className="mx-auto w-full max-w-screen-xl">
|
||||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
||||||
{document.title}
|
{truncatedTitle}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="mt-2.5 flex items-center gap-x-6">
|
<div className="mt-2.5 flex items-center gap-x-6">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Document, Field } from '@documenso/prisma/client';
|
import type { Document, Field } from '@documenso/prisma/client';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
|
||||||
|
import { truncateTitle } from '~/helpers/truncate-title';
|
||||||
|
|
||||||
export type SignDialogProps = {
|
export type SignDialogProps = {
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
document: Document;
|
document: Document;
|
||||||
@@ -23,7 +25,7 @@ export const SignDialog = ({
|
|||||||
onSignatureComplete,
|
onSignatureComplete,
|
||||||
}: SignDialogProps) => {
|
}: SignDialogProps) => {
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
const truncatedTitle = truncateTitle(document.title);
|
||||||
const isComplete = fields.every((field) => field.inserted);
|
const isComplete = fields.every((field) => field.inserted);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,7 +45,7 @@ export const SignDialog = ({
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-xl font-semibold text-neutral-800">Sign Document</div>
|
<div className="text-xl font-semibold text-neutral-800">Sign Document</div>
|
||||||
<div className="text-muted-foreground mx-auto w-4/5 py-2 text-center">
|
<div className="text-muted-foreground mx-auto w-4/5 py-2 text-center">
|
||||||
You are about to finish signing "{document.title}". Are you sure?
|
You are about to finish signing "{truncatedTitle}". Are you sure?
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
|||||||
import {
|
import {
|
||||||
DOCUMENTS_PAGE_SHORTCUT,
|
DOCUMENTS_PAGE_SHORTCUT,
|
||||||
SETTINGS_PAGE_SHORTCUT,
|
SETTINGS_PAGE_SHORTCUT,
|
||||||
|
TEMPLATES_PAGE_SHORTCUT,
|
||||||
} from '@documenso/lib/constants/keyboard-shortcuts';
|
} from '@documenso/lib/constants/keyboard-shortcuts';
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import {
|
import {
|
||||||
@@ -39,6 +40,14 @@ const DOCUMENTS_PAGES = [
|
|||||||
{ label: 'Inbox documents', path: '/documents?status=INBOX' },
|
{ label: 'Inbox documents', path: '/documents?status=INBOX' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const TEMPLATES_PAGES = [
|
||||||
|
{
|
||||||
|
label: 'All templates',
|
||||||
|
path: '/templates',
|
||||||
|
shortcut: TEMPLATES_PAGE_SHORTCUT.replace('+', ''),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const SETTINGS_PAGES = [
|
const SETTINGS_PAGES = [
|
||||||
{
|
{
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
@@ -125,10 +134,12 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
|
|
||||||
const goToSettings = useCallback(() => push(SETTINGS_PAGES[0].path), [push]);
|
const goToSettings = useCallback(() => push(SETTINGS_PAGES[0].path), [push]);
|
||||||
const goToDocuments = useCallback(() => push(DOCUMENTS_PAGES[0].path), [push]);
|
const goToDocuments = useCallback(() => push(DOCUMENTS_PAGES[0].path), [push]);
|
||||||
|
const goToTemplates = useCallback(() => push(TEMPLATES_PAGES[0].path), [push]);
|
||||||
|
|
||||||
useHotkeys(['ctrl+k', 'meta+k'], toggleOpen);
|
useHotkeys(['ctrl+k', 'meta+k'], toggleOpen);
|
||||||
useHotkeys(SETTINGS_PAGE_SHORTCUT, goToSettings);
|
useHotkeys(SETTINGS_PAGE_SHORTCUT, goToSettings);
|
||||||
useHotkeys(DOCUMENTS_PAGE_SHORTCUT, goToDocuments);
|
useHotkeys(DOCUMENTS_PAGE_SHORTCUT, goToDocuments);
|
||||||
|
useHotkeys(TEMPLATES_PAGE_SHORTCUT, goToTemplates);
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
// Escape goes to previous page
|
// Escape goes to previous page
|
||||||
@@ -175,6 +186,9 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
<CommandGroup heading="Documents">
|
<CommandGroup heading="Documents">
|
||||||
<Commands push={push} pages={DOCUMENTS_PAGES} />
|
<Commands push={push} pages={DOCUMENTS_PAGES} />
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
<CommandGroup heading="Templates">
|
||||||
|
<Commands push={push} pages={TEMPLATES_PAGES} />
|
||||||
|
</CommandGroup>
|
||||||
<CommandGroup heading="Settings">
|
<CommandGroup heading="Settings">
|
||||||
<Commands push={push} pages={SETTINGS_PAGES} />
|
<Commands push={push} pages={SETTINGS_PAGES} />
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const Header = ({ className, user, ...props }: HeaderProps) => {
|
|||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-[1000] flex h-16 w-full items-center border-b border-b-transparent backdrop-blur duration-200',
|
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-[50] flex h-16 w-full items-center border-b border-b-transparent backdrop-blur duration-200',
|
||||||
scrollY > 5 && 'border-b-border',
|
scrollY > 5 && 'border-b-border',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export const truncateTitle = (title: string, maxLength: number = 16) => {
|
||||||
|
if (title.length <= maxLength) {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = title.slice(0, maxLength / 2);
|
||||||
|
const end = title.slice(-maxLength / 2);
|
||||||
|
|
||||||
|
return `${start}.....${end}`;
|
||||||
|
};
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export const SETTINGS_PAGE_SHORTCUT = 'N+S';
|
export const SETTINGS_PAGE_SHORTCUT = 'N+S';
|
||||||
export const DOCUMENTS_PAGE_SHORTCUT = 'N+D';
|
export const DOCUMENTS_PAGE_SHORTCUT = 'N+D';
|
||||||
|
export const TEMPLATES_PAGE_SHORTCUT = 'N+T';
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const DialogPortal = ({
|
|||||||
}: DialogPrimitive.DialogPortalProps & { position?: 'start' | 'end' | 'center' }) => (
|
}: DialogPrimitive.DialogPortalProps & { position?: 'start' | 'end' | 'center' }) => (
|
||||||
<DialogPrimitive.Portal {...props}>
|
<DialogPrimitive.Portal {...props}>
|
||||||
<div
|
<div
|
||||||
className={cn('fixed inset-0 z-[9999] flex justify-center sm:items-center', {
|
className={cn('fixed inset-0 z-[1000] flex justify-center sm:items-center', {
|
||||||
'items-start': position === 'start',
|
'items-start': position === 'start',
|
||||||
'items-end': position === 'end',
|
'items-end': position === 'end',
|
||||||
'items-center': position === 'center',
|
'items-center': position === 'center',
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ const DocumentDescription = {
|
|||||||
export type DocumentDropzoneProps = {
|
export type DocumentDropzoneProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
disabledMessage?: string;
|
||||||
onDrop?: (_file: File) => void | Promise<void>;
|
onDrop?: (_file: File) => void | Promise<void>;
|
||||||
type?: 'document' | 'template';
|
type?: 'document' | 'template';
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
@@ -96,6 +97,7 @@ export const DocumentDropzone = ({
|
|||||||
className,
|
className,
|
||||||
onDrop,
|
onDrop,
|
||||||
disabled,
|
disabled,
|
||||||
|
disabledMessage = 'You cannot upload documents at this time.',
|
||||||
type = 'document',
|
type = 'document',
|
||||||
...props
|
...props
|
||||||
}: DocumentDropzoneProps) => {
|
}: DocumentDropzoneProps) => {
|
||||||
@@ -172,7 +174,9 @@ export const DocumentDropzone = ({
|
|||||||
{DocumentDescription[type].headline}
|
{DocumentDescription[type].headline}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground/80 mt-1 text-sm ">Drag & drop your document here.</p>
|
<p className="text-muted-foreground/80 mt-1 text-sm">
|
||||||
|
{disabled ? disabledMessage : 'Drag & drop your document here.'}
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const AddTitleFormPartial = ({
|
|||||||
|
|
||||||
<Input
|
<Input
|
||||||
id="title"
|
id="title"
|
||||||
className="bg-background mt-2"
|
className="bg-background my-2"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...register('title', { required: "Title can't be empty" })}
|
{...register('title', { required: "Title can't be empty" })}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const SelectContent = React.forwardRef<
|
|||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-popover text-popover-foreground animate-in fade-in-80 relative z-50 min-w-[8rem] overflow-hidden rounded-md border shadow-md',
|
'bg-popover text-popover-foreground animate-in fade-in-80 relative z-[1001] min-w-[8rem] overflow-hidden rounded-md border shadow-md',
|
||||||
position === 'popper' && 'translate-y-1',
|
position === 'popper' && 'translate-y-1',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const ToastViewport = React.forwardRef<
|
|||||||
<ToastPrimitives.Viewport
|
<ToastPrimitives.Viewport
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed top-0 z-[9999] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
|
'fixed top-0 z-[1001] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
Reference in New Issue
Block a user