Compare commits

..

28 Commits

Author SHA1 Message Date
flō
24036b0f24 fix typo 2023-09-11 17:03:14 +10:00
Lucas Smith
975d52a07e Merge pull request #362 from documenso/fix/hide-user-selection
fix: hide popover when user selects a recipient
2023-09-11 12:27:50 +10:00
Ephraim Atta-Duncan
f8a193c0f8 refactor: replace whole implementation with a state 2023-09-09 10:56:45 +00:00
Ephraim Atta-Duncan
9186cb4d7b fix: hide popover when user selects a recipients 2023-09-09 10:42:03 +00:00
Mythie
933076fa3f fix: update devcontainer 2023-09-09 15:49:40 +10:00
Mythie
abc91f7eac fix: update devcontainer 2023-09-09 15:44:10 +10:00
Mythie
35acf05997 feat: add devcontainer 2023-09-09 04:38:37 +00:00
Lucas Smith
ff957a2f82 Merge pull request #353 from documenso/feat/disable-sign
feat: disable signing and editing for completed documents
2023-09-06 20:53:23 +10:00
Ephraim Atta-Duncan
6640f0496a feat: disable signing and editing for completed documents 2023-09-06 10:40:45 +00:00
Lucas Smith
de3ebe16ee Merge pull request #349 from documenso/feat/marketing-mobile-nav
feat: update marketing mobile nav
2023-09-05 18:20:53 +10:00
David Nguyen
84a2d3baf6 feat: update marketing mobile menu 2023-09-05 18:08:29 +10:00
Lucas Smith
74180defd1 Merge pull request #339 from G3root/feat-api-error
feat: add alert banner for errors in sigin page
2023-09-05 15:36:55 +10:00
Lucas Smith
aeeaaf0d8d Merge pull request #340 from G3root/fix-username-updateable
fix: user name not updatable
2023-09-05 15:33:19 +10:00
Lucas Smith
2b84293c4e Merge pull request #341 from documenso/feat/add-email-field
feat: add email field to document sign page
2023-09-05 13:25:29 +10:00
Lucas Smith
b38ef6c0a7 Merge pull request #346 from documenso/chore/remove-console-log-warn
chore: removed console logs and warn
2023-09-05 13:23:57 +10:00
Mythie
17af4d25bd fix: actually make timeouts clear 2023-09-05 11:33:49 +10:00
Mythie
6e095921e6 fix: tidy up code 2023-09-05 11:29:23 +10:00
nafees nazik
150c42b246 fix: value 2023-09-04 22:24:42 +05:30
Ephraim Atta-Duncan
b3291c65bc chore: remove console.log 2023-09-02 22:20:57 +00:00
Ephraim Atta-Duncan
4b849e286c feat: add missing email field to document sign page 2023-09-02 22:08:19 +00:00
nafees nazik
7bcc26a987 fix: user name not updatable 2023-09-02 12:11:07 +05:30
nafees nazik
692722d32e revert: fix: component style 2023-09-02 11:55:44 +05:30
Nafees Nazik
e4f06d8e30 Merge branch 'feat/refresh' into feat-api-error 2023-09-02 11:53:18 +05:30
nafees nazik
c799380787 chore: add comments 2023-09-02 11:51:21 +05:30
nafees nazik
5540fcf0d2 fix: use toast 2023-09-02 11:46:12 +05:30
nafees nazik
d9da09c1e7 fix: typo 2023-09-01 16:25:49 +05:30
nafees nazik
fe90aa3b7b feat: add api error 2023-09-01 16:25:27 +05:30
nafees nazik
0c680e0111 fix: component style 2023-09-01 16:25:00 +05:30
14 changed files with 208 additions and 18 deletions

View File

@@ -0,0 +1,20 @@
{
"name": "Documenso",
"image": "mcr.microsoft.com/devcontainers/base:bullseye",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "latest",
"enableNonRootDocker": "true",
"moby": "true"
},
"ghcr.io/devcontainers/features/node:1": {}
},
"onCreateCommand": "./.devcontainer/on-create.sh",
"forwardPorts": [
3000,
54320,
9000,
2500,
1100
]
}

18
.devcontainer/on-create.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Start the database and mailserver
docker compose -f ./docker/compose-without-app.yml up -d
# Install dependencies
npm install
# Copy the env file
cp .env.example .env
# Source the env file, export the variables
set -a
source .env
set +a
# Run the migrations
npm run -w @documenso/prisma prisma:migrate-dev

3
.devcontainer/post-start.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
npm run dev

View File

@@ -12,7 +12,7 @@ tags:
Since we launched [Documenso 0.9 on Product Hunt](https://producthunt.com/products/documenso#documenso) last May, the team's been hard at work behind the scenes to ramp up development and design to deliver an excellent next version. Since we launched [Documenso 0.9 on Product Hunt](https://producthunt.com/products/documenso#documenso) last May, the team's been hard at work behind the scenes to ramp up development and design to deliver an excellent next version.
Last week, Lucas shared the reasoning how [why we're doing a rewrite](https://documenso.com/blog/why-were-doing-a-rewrite). Last week, Lucas shared the reasoning on [why we're doing a rewrite](https://documenso.com/blog/why-were-doing-a-rewrite).
Today, I'm pleased to share with you a preview of the next Documenso. Today, I'm pleased to share with you a preview of the next Documenso.

View File

@@ -22,6 +22,10 @@ export const MENU_NAVIGATION_LINKS = [
href: '/pricing', href: '/pricing',
text: 'Pricing', text: 'Pricing',
}, },
{
href: '/open',
text: 'Open',
},
{ {
href: 'https://status.documenso.com', href: 'https://status.documenso.com',
text: 'Status', text: 'Status',
@@ -59,7 +63,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
initial="initial" initial="initial"
animate="animate" animate="animate"
transition={{ transition={{
staggerChildren: 0.2, staggerChildren: 0.03,
}} }}
> >
{MENU_NAVIGATION_LINKS.map(({ href, text }) => ( {MENU_NAVIGATION_LINKS.map(({ href, text }) => (
@@ -75,6 +79,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
x: 0, x: 0,
transition: { transition: {
duration: 0.5, duration: 0.5,
ease: 'backInOut',
}, },
}, },
}} }}

View File

@@ -82,14 +82,14 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
<DropdownMenuContent className="w-52" align="start" forceMount> <DropdownMenuContent className="w-52" align="start" forceMount>
<DropdownMenuLabel>Action</DropdownMenuLabel> <DropdownMenuLabel>Action</DropdownMenuLabel>
<DropdownMenuItem disabled={!recipient} asChild> <DropdownMenuItem disabled={!recipient || isComplete} asChild>
<Link href={`/sign/${recipient?.token}`}> <Link href={`/sign/${recipient?.token}`}>
<Pencil className="mr-2 h-4 w-4" /> <Pencil className="mr-2 h-4 w-4" />
Sign Sign
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem disabled={!isOwner} asChild> <DropdownMenuItem disabled={!isOwner || isComplete} asChild>
<Link href={`/documents/${row.id}`}> <Link href={`/documents/${row.id}`}>
<Edit className="mr-2 h-4 w-4" /> <Edit className="mr-2 h-4 w-4" />
Edit Edit

View File

@@ -0,0 +1,96 @@
'use client';
import { useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Loader } from 'lucide-react';
import { Recipient } from '@documenso/prisma/client';
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredSigningContext } from './provider';
import { SigningFieldContainer } from './signing-field-container';
export type EmailFieldProps = {
field: FieldWithSignature;
recipient: Recipient;
};
export const EmailField = ({ field, recipient }: EmailFieldProps) => {
const router = useRouter();
const { toast } = useToast();
const { email: providedEmail } = useRequiredSigningContext();
const [isPending, startTransition] = useTransition();
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation();
const {
mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation();
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
const onSign = async () => {
try {
await signFieldWithToken({
token: recipient.token,
fieldId: field.id,
value: providedEmail ?? '',
isBase64: false,
});
startTransition(() => router.refresh());
} catch (err) {
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while signing the document.',
variant: 'destructive',
});
}
};
const onRemove = async () => {
try {
await removeSignedFieldWithToken({
token: recipient.token,
fieldId: field.id,
});
startTransition(() => router.refresh());
} catch (err) {
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while removing the signature.',
variant: 'destructive',
});
}
};
return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
{isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div>
)}
{!field.inserted && (
<p className="group-hover:text-primary text-muted-foreground text-lg duration-200">Email</p>
)}
{field.inserted && <p className="text-muted-foreground duration-200">{field.customText}</p>}
</SigningFieldContainer>
);
};

View File

@@ -14,6 +14,7 @@ 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 { DateField } from './date-field'; import { DateField } from './date-field';
import { EmailField } from './email-field';
import { SigningForm } from './form'; import { SigningForm } from './form';
import { NameField } from './name-field'; import { NameField } from './name-field';
import { SigningProvider } from './provider'; import { SigningProvider } from './provider';
@@ -87,6 +88,9 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
.with(FieldType.DATE, () => ( .with(FieldType.DATE, () => (
<DateField key={field.id} field={field} recipient={recipient} /> <DateField key={field.id} field={field} recipient={recipient} />
)) ))
.with(FieldType.EMAIL, () => (
<EmailField key={field.id} field={field} recipient={recipient} />
))
.otherwise(() => null), .otherwise(() => null),
)} )}
</ElementVisible> </ElementVisible>

View File

@@ -44,7 +44,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
} = useForm<TProfileFormSchema>({ } = useForm<TProfileFormSchema>({
values: { values: {
name: user.name ?? '', name: user.name ?? '',
signature: '', signature: user.signature || '',
}, },
resolver: zodResolver(ZProfileFormSchema), resolver: zodResolver(ZProfileFormSchema),
}); });

View File

@@ -1,5 +1,9 @@
'use client'; 'use client';
import { useEffect } from 'react';
import { useSearchParams } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { signIn } from 'next-auth/react'; import { signIn } from 'next-auth/react';
@@ -7,12 +11,20 @@ import { useForm } from 'react-hook-form';
import { FcGoogle } from 'react-icons/fc'; import { FcGoogle } from 'react-icons/fc';
import { z } from 'zod'; import { z } from 'zod';
import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label'; import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
const ErrorMessages = {
[ErrorCode.CREDENTIALS_NOT_FOUND]: 'The email or password provided is incorrect',
[ErrorCode.INCORRECT_EMAIL_PASSWORD]: 'The email or password provided is incorrect',
[ErrorCode.USER_MISSING_PASSWORD]:
'This account appears to be using a social login method, please sign in using that method',
};
export const ZSignInFormSchema = z.object({ export const ZSignInFormSchema = z.object({
email: z.string().email().min(1), email: z.string().email().min(1),
password: z.string().min(6).max(72), password: z.string().min(6).max(72),
@@ -26,6 +38,7 @@ export type SignInFormProps = {
export const SignInForm = ({ className }: SignInFormProps) => { export const SignInForm = ({ className }: SignInFormProps) => {
const { toast } = useToast(); const { toast } = useToast();
const searchParams = useSearchParams();
const { const {
register, register,
@@ -39,6 +52,27 @@ export const SignInForm = ({ className }: SignInFormProps) => {
resolver: zodResolver(ZSignInFormSchema), resolver: zodResolver(ZSignInFormSchema),
}); });
const errorCode = searchParams?.get('error');
useEffect(() => {
let timeout: NodeJS.Timeout | null = null;
if (isErrorCode(errorCode)) {
timeout = setTimeout(() => {
toast({
variant: 'destructive',
description: ErrorMessages[errorCode] ?? 'An unknown error occurred',
});
}, 0);
}
return () => {
if (timeout) {
clearTimeout(timeout);
}
};
}, [errorCode, toast]);
const onFormSubmit = async ({ email, password }: TSignInFormSchema) => { const onFormSubmit = async ({ email, password }: TSignInFormSchema) => {
try { try {
await signIn('credentials', { await signIn('credentials', {

View File

@@ -2,7 +2,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"build": "turbo run build", "build": "turbo run build",
"dev": "turbo run dev --filter=@documenso/{web,marketing}", "dev": "turbo run dev --filter=@documenso/web --filter=@documenso/marketing",
"start": "cd apps && cd web && next start", "start": "cd apps && cd web && next start",
"lint": "turbo run lint", "lint": "turbo run lint",
"format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"", "format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"",

View File

@@ -7,7 +7,7 @@ import GoogleProvider, { GoogleProfile } from 'next-auth/providers/google';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { getUserByEmail } from '../server-only/user/get-user-by-email'; import { getUserByEmail } from '../server-only/user/get-user-by-email';
import { ErrorCodes } from './error-codes'; import { ErrorCode } from './error-codes';
export const NEXT_AUTH_OPTIONS: AuthOptions = { export const NEXT_AUTH_OPTIONS: AuthOptions = {
adapter: PrismaAdapter(prisma), adapter: PrismaAdapter(prisma),
@@ -24,23 +24,23 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
}, },
authorize: async (credentials, _req) => { authorize: async (credentials, _req) => {
if (!credentials) { if (!credentials) {
throw new Error(ErrorCodes.CredentialsNotFound); throw new Error(ErrorCode.CREDENTIALS_NOT_FOUND);
} }
const { email, password } = credentials; const { email, password } = credentials;
const user = await getUserByEmail({ email }).catch(() => { const user = await getUserByEmail({ email }).catch(() => {
throw new Error(ErrorCodes.IncorrectEmailPassword); throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD);
}); });
if (!user.password) { if (!user.password) {
throw new Error(ErrorCodes.UserMissingPassword); throw new Error(ErrorCode.USER_MISSING_PASSWORD);
} }
const isPasswordsSame = await compare(password, user.password); const isPasswordsSame = await compare(password, user.password);
if (!isPasswordsSame) { if (!isPasswordsSame) {
throw new Error(ErrorCodes.IncorrectEmailPassword); throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD);
} }
return { return {

View File

@@ -1,5 +1,11 @@
export const ErrorCodes = { export const isErrorCode = (code: unknown): code is ErrorCode => {
IncorrectEmailPassword: 'incorrect-email-password', return typeof code === 'string' && code in ErrorCode;
UserMissingPassword: 'missing-password', };
CredentialsNotFound: 'credentials-not-found',
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
export const ErrorCode = {
INCORRECT_EMAIL_PASSWORD: 'INCORRECT_EMAIL_PASSWORD',
USER_MISSING_PASSWORD: 'USER_MISSING_PASSWORD',
CREDENTIALS_NOT_FOUND: 'CREDENTIALS_NOT_FOUND',
} as const; } as const;

View File

@@ -102,6 +102,7 @@ export const AddFieldsFormPartial = ({
const [selectedField, setSelectedField] = useState<FieldType | null>(null); const [selectedField, setSelectedField] = useState<FieldType | null>(null);
const [selectedSigner, setSelectedSigner] = useState<Recipient | null>(null); const [selectedSigner, setSelectedSigner] = useState<Recipient | null>(null);
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT; const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT;
@@ -314,7 +315,7 @@ export const AddFieldsFormPartial = ({
))} ))}
{!hideRecipients && ( {!hideRecipients && (
<Popover> <Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
type="button" type="button"
@@ -324,7 +325,7 @@ export const AddFieldsFormPartial = ({
> >
{selectedSigner?.email && ( {selectedSigner?.email && (
<span className="flex-1 truncate text-left"> <span className="flex-1 truncate text-left">
{selectedSigner?.email} ({selectedSigner?.email}) {selectedSigner?.name} ({selectedSigner?.email})
</span> </span>
)} )}
@@ -348,7 +349,10 @@ export const AddFieldsFormPartial = ({
className={cn({ className={cn({
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT, 'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
})} })}
onSelect={() => setSelectedSigner(recipient)} onSelect={() => {
setSelectedSigner(recipient);
setShowRecipientsSelector(false);
}}
> >
{recipient.sendStatus !== SendStatus.SENT ? ( {recipient.sendStatus !== SendStatus.SENT ? (
<Check <Check