This commit is contained in:
David Nguyen
2025-01-31 14:09:02 +11:00
parent f7a98180d7
commit d7d0fca501
146 changed files with 1250 additions and 1263 deletions

View File

@ -1,21 +0,0 @@
import { redirect, useParams } from 'react-router';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { useOptionalCurrentTeam } from '~/providers/team';
import { DocumentEditPageView } from './document-edit-page-view';
export default function DocumentEditPage() {
const { id: documentId } = useParams();
const team = useOptionalCurrentTeam();
const documentRootPath = formatDocumentsPath(team?.url);
if (!documentId || Number.isNaN(documentId)) {
redirect(documentRootPath);
}
return <DocumentEditPageView documentId={Number(documentId)} />;
}

View File

@ -1,21 +0,0 @@
import { redirect, useParams } from 'react-router';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { useOptionalCurrentTeam } from '~/providers/team';
import { DocumentPageView } from './document-page-view';
export default function DocumentPage() {
const { id: documentId } = useParams();
const team = useOptionalCurrentTeam();
const documentRootPath = formatDocumentsPath(team?.url);
if (!documentId || Number.isNaN(documentId)) {
redirect(documentRootPath);
}
return <DocumentPageView documentId={Number(documentId)} />;
}

View File

@ -1,21 +0,0 @@
import { redirect, useParams } from 'react-router';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { useOptionalCurrentTeam } from '~/providers/team';
import { DocumentLogsPageView } from './document-logs-page-view';
export default function DocumentsLogsPage() {
const { id: documentId } = useParams();
const team = useOptionalCurrentTeam();
const documentRootPath = formatDocumentsPath(team?.url);
if (!documentId || Number.isNaN(documentId)) {
redirect(documentRootPath);
}
return <DocumentLogsPageView documentId={Number(documentId)} />;
}

View File

@ -25,8 +25,7 @@ export const DocumentSearch = ({ initialValue = '' }: { initialValue?: string })
params.delete('search');
}
// Todo: Test
void navigate(`/documents?${params.toString()}`);
void navigate(`?${params.toString()}`);
},
[searchParams],
);

View File

@ -20,7 +20,7 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { LeaveTeamDialog } from '../dialogs/leave-team-dialog';
import { TeamLeaveDialog } from '~/components/dialogs/team-leave-dialog';
export const CurrentUserTeamsDataTable = () => {
const { _, i18n } = useLingui();
@ -99,7 +99,7 @@ export const CurrentUserTeamsDataTable = () => {
</Button>
)}
<LeaveTeamDialog
<TeamLeaveDialog
teamId={row.original.id}
teamName={row.original.name}
teamAvatarImageId={row.original.avatarImageId}

View File

@ -15,7 +15,8 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { CreateTeamCheckoutDialog } from '../dialogs/create-team-checkout-dialog';
import { TeamCheckoutCreateDialog } from '~/components/dialogs/team-checkout-create-dialog';
import { PendingUserTeamsDataTableActions } from './pending-user-teams-data-table-actions';
export const PendingUserTeamsDataTable = () => {
@ -139,7 +140,7 @@ export const PendingUserTeamsDataTable = () => {
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
<CreateTeamCheckoutDialog
<TeamCheckoutCreateDialog
pendingTeamId={checkoutPendingTeamId}
onClose={() => setCheckoutPendingTeamId(null)}
/>

View File

@ -26,8 +26,8 @@ import {
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { DeleteTeamMemberDialog } from '../dialogs/delete-team-member-dialog';
import { UpdateTeamMemberDialog } from '../dialogs/update-team-member-dialog';
import { DeleteTeamMemberDialog } from '../../dialogs/team-member-delete-dialog';
import { UpdateTeamMemberDialog } from '../../dialogs/team-member-update-dialog';
export type TeamMembersDataTableProps = {
currentUserTeamRole: TeamMemberRole;

View File

@ -0,0 +1,155 @@
import { useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { authClient } from '@documenso/auth/client';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useAuth } from '~/providers/auth';
export type AccountDeleteDialogProps = {
className?: string;
};
export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) => {
const { user } = useAuth();
const { _ } = useLingui();
const { toast } = useToast();
const hasTwoFactorAuthentication = user.twoFactorEnabled;
const [enteredEmail, setEnteredEmail] = useState<string>('');
const { mutateAsync: deleteAccount, isPending: isDeletingAccount } =
trpc.profile.deleteAccount.useMutation();
const onDeleteAccount = async () => {
try {
await deleteAccount();
toast({
title: _(msg`Account deleted`),
description: _(msg`Your account has been deleted successfully.`),
duration: 5000,
});
return await authClient.signOut();
} catch (err) {
toast({
title: _(msg`An unknown error occurred`),
variant: 'destructive',
description: _(
msg`We encountered an unknown error while attempting to delete your account. Please try again later.`,
),
});
}
};
return (
<div className={className}>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div>
<AlertTitle>
<Trans>Delete Account</Trans>
</AlertTitle>
<AlertDescription className="mr-2">
<Trans>
Delete your account and all its contents, including completed documents. This action
is irreversible and will cancel your subscription, so proceed with caution.
</Trans>
</AlertDescription>
</div>
<div className="flex-shrink-0">
<Dialog onOpenChange={() => setEnteredEmail('')}>
<DialogTrigger asChild>
<Button variant="destructive">
<Trans>Delete Account</Trans>
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="space-y-4">
<DialogTitle>
<Trans>Delete Account</Trans>
</DialogTitle>
<Alert variant="destructive">
<AlertDescription className="selection:bg-red-100">
<Trans>This action is not reversible. Please be certain.</Trans>
</AlertDescription>
</Alert>
{hasTwoFactorAuthentication && (
<Alert variant="destructive">
<AlertDescription className="selection:bg-red-100">
<Trans>Disable Two Factor Authentication before deleting your account.</Trans>
</AlertDescription>
</Alert>
)}
<DialogDescription>
<Trans>
Documenso will delete{' '}
<span className="font-semibold">all of your documents</span>, along with all of
your completed documents, signatures, and all other resources belonging to your
Account.
</Trans>
</DialogDescription>
</DialogHeader>
{!hasTwoFactorAuthentication && (
<div>
<Label>
<Trans>
Please type{' '}
<span className="text-muted-foreground font-semibold">{user.email}</span> to
confirm.
</Trans>
</Label>
<Input
type="text"
className="mt-2"
aria-label="Confirm Email"
value={enteredEmail}
onChange={(e) => setEnteredEmail(e.target.value)}
/>
</div>
)}
<DialogFooter>
<Button
onClick={onDeleteAccount}
loading={isDeletingAccount}
variant="destructive"
disabled={hasTwoFactorAuthentication || enteredEmail !== user.email}
>
{isDeletingAccount ? _(msg`Deleting account...`) : _(msg`Confirm Deletion`)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</Alert>
</div>
);
};

View File

@ -16,24 +16,26 @@ import {
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
type DocumentDuplicateDialogProps = {
id: number;
open: boolean;
onOpenChange: (_open: boolean) => void;
team?: Pick<Team, 'id' | 'url'>;
};
export const DocumentDuplicateDialog = ({
id,
open,
onOpenChange,
team,
}: DocumentDuplicateDialogProps) => {
const navigate = useNavigate();
const { toast } = useToast();
const { _ } = useLingui();
const team = useOptionalCurrentTeam();
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
documentId: id,
});

View File

@ -36,6 +36,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { StackAvatar } from '~/components/(dashboard)/avatar/stack-avatar';
import { useAuth } from '~/providers/auth';
import { useOptionalCurrentTeam } from '~/providers/team';
const FORM_ID = 'resend-email';
@ -44,7 +45,6 @@ export type DocumentResendDialogProps = {
team: Pick<Team, 'id' | 'url'> | null;
};
recipients: Recipient[];
team?: Pick<Team, 'id' | 'url'>;
};
export const ZResendDocumentFormSchema = z.object({
@ -55,8 +55,9 @@ export const ZResendDocumentFormSchema = z.object({
export type TResendDocumentFormSchema = z.infer<typeof ZResendDocumentFormSchema>;
export const DocumentResendDialog = ({ document, recipients, team }: DocumentResendDialogProps) => {
export const DocumentResendDialog = ({ document, recipients }: DocumentResendDialogProps) => {
const { user } = useAuth();
const team = useOptionalCurrentTeam();
const { toast } = useToast();
const { _ } = useLingui();

View File

@ -20,18 +20,18 @@ import {
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type CreateTeamCheckoutDialogProps = {
export type TeamCheckoutCreateDialogProps = {
pendingTeamId: number | null;
onClose: () => void;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
const MotionCard = motion(Card);
export const CreateTeamCheckoutDialog = ({
export const TeamCheckoutCreateDialog = ({
pendingTeamId,
onClose,
...props
}: CreateTeamCheckoutDialogProps) => {
}: TeamCheckoutCreateDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();

View File

@ -35,7 +35,7 @@ import {
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type CreateTeamDialogProps = {
export type TeamCreateDialogProps = {
trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
@ -46,7 +46,7 @@ const ZCreateTeamFormSchema = ZCreateTeamMutationSchema.pick({
type TCreateTeamFormSchema = z.infer<typeof ZCreateTeamFormSchema>;
export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) => {
export const TeamCreateDialog = ({ trigger, ...props }: TeamCreateDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();

View File

@ -30,13 +30,13 @@ import { Input } from '@documenso/ui/primitives/input';
import type { Toast } from '@documenso/ui/primitives/use-toast';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DeleteTeamDialogProps = {
export type TeamDeleteDialogProps = {
teamId: number;
teamName: string;
trigger?: React.ReactNode;
};
export const DeleteTeamDialog = ({ trigger, teamId, teamName }: DeleteTeamDialogProps) => {
export const TeamDeleteDialog = ({ trigger, teamId, teamName }: TeamDeleteDialogProps) => {
const navigate = useNavigate();
const [open, setOpen] = useState(false);

View File

@ -32,7 +32,7 @@ import {
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type AddTeamEmailDialogProps = {
export type TeamEmailAddDialogProps = {
teamId: number;
trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
@ -44,7 +44,7 @@ const ZCreateTeamEmailFormSchema = ZCreateTeamEmailVerificationMutationSchema.pi
type TCreateTeamEmailFormSchema = z.infer<typeof ZCreateTeamEmailFormSchema>;
export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDialogProps) => {
export const TeamEmailAddDialog = ({ teamId, trigger, ...props }: TeamEmailAddDialogProps) => {
const [open, setOpen] = useState(false);
const { _ } = useLingui();

View File

@ -21,7 +21,7 @@ import {
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type RemoveTeamEmailDialogProps = {
export type TeamEmailDeleteDialogProps = {
trigger?: React.ReactNode;
teamName: string;
team: Prisma.TeamGetPayload<{
@ -38,7 +38,7 @@ export type RemoveTeamEmailDialogProps = {
}>;
};
export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEmailDialogProps) => {
export const TeamEmailDeleteDialog = ({ trigger, teamName, team }: TeamEmailDeleteDialogProps) => {
const [open, setOpen] = useState(false);
const { _ } = useLingui();

View File

@ -30,7 +30,7 @@ import {
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type UpdateTeamEmailDialogProps = {
export type TeamEmailUpdateDialogProps = {
teamEmail: TeamEmail;
trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
@ -41,11 +41,11 @@ const ZUpdateTeamEmailFormSchema = z.object({
type TUpdateTeamEmailFormSchema = z.infer<typeof ZUpdateTeamEmailFormSchema>;
export const UpdateTeamEmailDialog = ({
export const TeamEmailUpdateDialog = ({
teamEmail,
trigger,
...props
}: UpdateTeamEmailDialogProps) => {
}: TeamEmailUpdateDialogProps) => {
const [open, setOpen] = useState(false);
const { _ } = useLingui();

View File

@ -21,7 +21,7 @@ import {
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type LeaveTeamDialogProps = {
export type TeamLeaveDialogProps = {
teamId: number;
teamName: string;
teamAvatarImageId?: string | null;
@ -29,13 +29,13 @@ export type LeaveTeamDialogProps = {
trigger?: React.ReactNode;
};
export const LeaveTeamDialog = ({
export const TeamLeaveDialog = ({
trigger,
teamId,
teamName,
teamAvatarImageId,
role,
}: LeaveTeamDialogProps) => {
}: TeamLeaveDialogProps) => {
const [open, setOpen] = useState(false);
const { _ } = useLingui();

View File

@ -18,7 +18,7 @@ import {
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DeleteTeamMemberDialogProps = {
export type TeamMemberDeleteDialogProps = {
teamId: number;
teamName: string;
teamMemberId: number;
@ -27,14 +27,14 @@ export type DeleteTeamMemberDialogProps = {
trigger?: React.ReactNode;
};
export const DeleteTeamMemberDialog = ({
export const TeamMemberDeleteDialog = ({
trigger,
teamId,
teamName,
teamMemberId,
teamMemberName,
teamMemberEmail,
}: DeleteTeamMemberDialogProps) => {
}: TeamMemberDeleteDialogProps) => {
const [open, setOpen] = useState(false);
const { _ } = useLingui();

View File

@ -45,7 +45,7 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type InviteTeamMembersDialogProps = {
export type TeamMemberInviteDialogProps = {
currentUserTeamRole: TeamMemberRole;
teamId: number;
trigger?: React.ReactNode;
@ -94,12 +94,12 @@ const ZImportTeamMemberSchema = z.array(
}),
);
export const InviteTeamMembersDialog = ({
export const TeamMemberInviteDialog = ({
currentUserTeamRole,
teamId,
trigger,
...props
}: InviteTeamMembersDialogProps) => {
}: TeamMemberInviteDialogProps) => {
const [open, setOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const [invitationType, setInvitationType] = useState<TabTypes>('INDIVIDUAL');

View File

@ -38,7 +38,7 @@ import {
} from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type UpdateTeamMemberDialogProps = {
export type TeamMemberUpdateDialogProps = {
currentUserTeamRole: TeamMemberRole;
trigger?: React.ReactNode;
teamId: number;
@ -53,7 +53,7 @@ const ZUpdateTeamMemberFormSchema = z.object({
type ZUpdateTeamMemberSchema = z.infer<typeof ZUpdateTeamMemberFormSchema>;
export const UpdateTeamMemberDialog = ({
export const TeamMemberUpdateDialog = ({
currentUserTeamRole,
trigger,
teamId,
@ -61,7 +61,7 @@ export const UpdateTeamMemberDialog = ({
teamMemberName,
teamMemberRole,
...props
}: UpdateTeamMemberDialogProps) => {
}: TeamMemberUpdateDialogProps) => {
const [open, setOpen] = useState(false);
const { _ } = useLingui();

View File

@ -38,19 +38,19 @@ import {
} from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type TransferTeamDialogProps = {
export type TeamTransferDialogProps = {
teamId: number;
teamName: string;
ownerUserId: number;
trigger?: React.ReactNode;
};
export const TransferTeamDialog = ({
export const TeamTransferDialog = ({
trigger,
teamId,
teamName,
ownerUserId,
}: TransferTeamDialogProps) => {
}: TeamTransferDialogProps) => {
const [open, setOpen] = useState(false);
const { _ } = useLingui();

View File

@ -8,7 +8,6 @@ import { z } from 'zod';
import { downloadFile } from '@documenso/lib/client-only/download-file';
import { AppError } from '@documenso/lib/errors/app-error';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
@ -145,7 +144,7 @@ export const ViewRecoveryCodesDialog = () => {
<Alert variant="destructive">
<AlertDescription>
{match(AppError.parseError(error).message)
.with(ErrorCode.INCORRECT_TWO_FACTOR_CODE, () => (
.with('INCORRECT_TWO_FACTOR_CODE', () => (
<Trans>Invalid code. Please try again.</Trans>
))
.otherwise(() => (

View File

@ -3,14 +3,14 @@ import { useMemo } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
// Todo
// import { ErrorCode, useDropzone } from 'react-dropzone';
import { ErrorCode, useDropzone } from 'react-dropzone';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { z } from 'zod';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError } from '@documenso/lib/errors/app-error';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { base64 } from '@documenso/lib/universal/base64';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
@ -40,9 +40,9 @@ export type AvatarImageFormProps = {
};
export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
const { user } = useAuth();
const { _ } = useLingui();
const { toast } = useToast();
const { user } = useAuth();
const team = useOptionalCurrentTeam();
@ -67,31 +67,31 @@ export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
resolver: zodResolver(ZAvatarImageFormSchema),
});
// const { getRootProps, getInputProps } = useDropzone({
// maxSize: 1024 * 1024,
// accept: {
// 'image/*': ['.png', '.jpg', '.jpeg'],
// },
// multiple: false,
// onDropAccepted: ([file]) => {
// void file.arrayBuffer().then((buffer) => {
// const contents = base64.encode(new Uint8Array(buffer));
const { getRootProps, getInputProps } = useDropzone({
maxSize: 1024 * 1024,
accept: {
'image/*': ['.png', '.jpg', '.jpeg'],
},
multiple: false,
onDropAccepted: ([file]) => {
void file.arrayBuffer().then((buffer) => {
const contents = base64.encode(new Uint8Array(buffer));
// form.setValue('bytes', contents);
// void form.handleSubmit(onFormSubmit)();
// });
// },
// onDropRejected: ([file]) => {
// form.setError('bytes', {
// type: 'onChange',
// message: match(file.errors[0].code)
// .with(ErrorCode.FileTooLarge, () => _(msg`Uploaded file is too large`))
// .with(ErrorCode.FileTooSmall, () => _(msg`Uploaded file is too small`))
// .with(ErrorCode.FileInvalidType, () => _(msg`Uploaded file not an allowed file type`))
// .otherwise(() => _(msg`An unknown error occurred`)),
// });
// },
// });
form.setValue('bytes', contents);
void form.handleSubmit(onFormSubmit)();
});
},
onDropRejected: ([file]) => {
form.setError('bytes', {
type: 'onChange',
message: match(file.errors[0].code)
.with(ErrorCode.FileTooLarge, () => _(msg`Uploaded file is too large`))
.with(ErrorCode.FileTooSmall, () => _(msg`Uploaded file is too small`))
.with(ErrorCode.FileInvalidType, () => _(msg`Uploaded file not an allowed file type`))
.otherwise(() => _(msg`An unknown error occurred`)),
});
},
});
const onFormSubmit = async (data: TAvatarImageFormSchema) => {
try {
@ -106,7 +106,8 @@ export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
duration: 5000,
});
// router.refresh(); // Todo
// Todo
// router.refresh();
} catch (err) {
const error = AppError.parseError(err);
@ -143,7 +144,11 @@ export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
<div className="flex items-center gap-8">
<div className="relative">
<Avatar className="h-16 w-16 border-2 border-solid">
{avatarImageId && <AvatarImage src={formatAvatarUrl(avatarImageId)} />}
{avatarImageId && (
<AvatarImage
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${avatarImageId}`}
/>
)}
<AvatarFallback className="text-sm text-gray-400">
{initials}
</AvatarFallback>
@ -165,12 +170,12 @@ export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
type="button"
variant="secondary"
size="sm"
// {...getRootProps()}
{...getRootProps()}
loading={form.formState.isSubmitting}
disabled={form.formState.isSubmitting}
>
<Trans>Upload Avatar</Trans>
{/* <input {...getInputProps()} /> */}
<input {...getInputProps()} />
</Button>
</div>
</FormControl>

View File

@ -6,8 +6,8 @@ import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { z } from 'zod';
import { authClient } from '@documenso/auth/client';
import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@ -55,11 +55,9 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
const isSubmitting = form.formState.isSubmitting;
const { mutateAsync: updatePassword } = trpc.profile.updatePassword.useMutation();
const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => {
try {
await updatePassword({
await authClient.updatePassword({
currentPassword,
password,
});

View File

@ -88,12 +88,12 @@ export const ClaimPublicProfileDialogForm = ({
} catch (err) {
const error = AppError.parseError(err);
if (error.code === AppErrorCode.PROFILE_URL_TAKEN) {
if (error.code === 'PROFILE_URL_TAKEN') {
form.setError('url', {
type: 'manual',
message: _(msg`This username is already taken`),
});
} else if (error.code === AppErrorCode.PREMIUM_PROFILE_URL) {
} else if (error.code === 'PREMIUM_PROFILE_URL') {
form.setError('url', {
type: 'manual',
message: error.message,

View File

@ -11,7 +11,7 @@ import { useForm } from 'react-hook-form';
import type { z } from 'zod';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { AppError } from '@documenso/lib/errors/app-error';
import { formatUserProfilePath } from '@documenso/lib/utils/public-profiles';
import {
MAX_PROFILE_BIO_LENGTH,
@ -88,8 +88,8 @@ export const PublicProfileForm = ({
const error = AppError.parseError(err);
switch (error.code) {
case AppErrorCode.PREMIUM_PROFILE_URL:
case AppErrorCode.PROFILE_URL_TAKEN:
case 'PREMIUM_PROFILE_URL':
case 'PROFILE_URL_TAKEN':
form.setError('url', {
type: 'manual',
message: error.message,

View File

@ -15,7 +15,6 @@ import { z } from 'zod';
import { authClient } from '@documenso/auth/client';
import { AuthenticationErrorCode } from '@documenso/auth/server/lib/errors/error-codes';
import { AppError } from '@documenso/lib/errors/app-error';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { trpc } from '@documenso/trpc/react';
import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils';
@ -46,8 +45,6 @@ const CommonErrorMessages = {
[AuthenticationErrorCode.AccountDisabled]: msg`This account has been disabled. Please contact support.`,
};
const TwoFactorEnabledErrorCode = ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS;
const LOGIN_REDIRECT_PATH = '/documents';
export const ZSignInFormSchema = z.object({
@ -90,7 +87,7 @@ export const SignInForm = ({
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
const callbackUrl = useMemo(() => {
const redirectUrl = useMemo(() => {
// Handle SSR
if (typeof window === 'undefined') {
return LOGIN_REDIRECT_PATH;
@ -161,15 +158,16 @@ export const SignInForm = ({
const credential = await startAuthentication(options);
const result = await authClient.passkey.signIn({
await authClient.passkey.signIn({
credential: JSON.stringify(credential),
csrfToken: sessionId,
redirectUrl,
// callbackUrl,
// redirect: false,
});
// Todo: Can't use navigate because of embed?
window.location.href = callbackUrl;
// window.location.href = callbackUrl;
} catch (err) {
setIsPasskeyLoading(false);
@ -208,17 +206,18 @@ export const SignInForm = ({
password,
totpCode,
backupCode,
redirectUrl,
// callbackUrl,
// redirect: false,
});
window.location.href = callbackUrl;
// window.location.href = callbackUrl; // Todo: Handle redirect.
} catch (err) {
console.log(err);
const error = AppError.parseError(err);
if (error.code === TwoFactorEnabledErrorCode) {
if (error.code === 'TWO_FACTOR_MISSING_CREDENTIALS') {
setIsTwoFactorAuthenticationDialogOpen(true);
return;
}
@ -257,12 +256,7 @@ export const SignInForm = ({
const onSignInWithGoogleClick = async () => {
try {
// await signIn('google', {
// callbackUrl,
// });
const result = await authClient.google.signIn();
console.log(result);
await authClient.google.signIn(); // Todo: Handle redirect.
} catch (err) {
toast({
title: _(msg`An unknown error occurred`),

View File

@ -12,9 +12,9 @@ import { Link, useNavigate, useSearchParams } from 'react-router';
import { z } from 'zod';
import communityCardsImage from '@documenso/assets/images/community-cards.png';
import { authClient } from '@documenso/auth/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@ -71,8 +71,8 @@ export const signupErrorMessages: Record<string, MessageDescriptor> = {
SIGNUP_DISABLED: msg`Signups are disabled.`,
[AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
[AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
[AppErrorCode.PROFILE_URL_TAKEN]: msg`This username has already been taken`,
[AppErrorCode.PREMIUM_PROFILE_URL]: msg`Only subscribers can have a username shorter than 6 characters`,
PROFILE_URL_TAKEN: msg`This username has already been taken`,
PREMIUM_PROFILE_URL: msg`Only subscribers can have a username shorter than 6 characters`,
};
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
@ -93,7 +93,7 @@ export const SignUpForm = ({
const { _ } = useLingui();
const { toast } = useToast();
// const analytics = useAnalytics();
// const analytics = useAnalytics(); // Todo
const navigate = useNavigate();
const [searchParams] = useSearchParams();
@ -120,11 +120,9 @@ export const SignUpForm = ({
const name = form.watch('name');
const url = form.watch('url');
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormSchema) => {
try {
await signup({ name, email, password, signature, url });
await authClient.emailPassword.signUp({ name, email, password, signature, url });
void navigate(`/unverified-account`);
@ -146,10 +144,7 @@ export const SignUpForm = ({
const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
if (
error.code === AppErrorCode.PROFILE_URL_TAKEN ||
error.code === AppErrorCode.PREMIUM_PROFILE_URL
) {
if (error.code === 'PROFILE_URL_TAKEN' || error.code === 'PREMIUM_PROFILE_URL') {
form.setError('url', {
type: 'manual',
message: _(errorMessage),
@ -175,8 +170,7 @@ export const SignUpForm = ({
const onSignUpWithGoogleClick = async () => {
try {
await new Promise((resolve) => setTimeout(resolve, 2000));
// await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
await authClient.google.signIn();
} catch (err) {
toast({
title: _(msg`An unknown error occurred`),

View File

@ -1,169 +0,0 @@
'use client';
import { useEffect, useMemo, useState, useTransition } from 'react';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { ChevronDownIcon as CaretSortIcon, Loader } from 'lucide-react';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Input } from '@documenso/ui/primitives/input';
export type SigningVolume = {
id: number;
name: string;
signingVolume: number;
createdAt: Date;
planId: string;
};
type LeaderboardTableProps = {
signingVolume: SigningVolume[];
totalPages: number;
perPage: number;
page: number;
sortBy: 'name' | 'createdAt' | 'signingVolume';
sortOrder: 'asc' | 'desc';
};
export const LeaderboardTable = ({
signingVolume,
totalPages,
perPage,
page,
sortBy,
sortOrder,
}: LeaderboardTableProps) => {
const { _, i18n } = useLingui();
const [isPending, startTransition] = useTransition();
const updateSearchParams = useUpdateSearchParams();
const [searchString, setSearchString] = useState('');
const debouncedSearchString = useDebouncedValue(searchString, 1000);
const columns = useMemo(() => {
return [
{
header: () => (
<div
className="flex cursor-pointer items-center"
onClick={() => handleColumnSort('name')}
>
{_(msg`Name`)}
<CaretSortIcon className="ml-2 h-4 w-4" />
</div>
),
accessorKey: 'name',
cell: ({ row }) => {
return (
<div>
<a
className="text-primary underline"
href={`https://dashboard.stripe.com/subscriptions/${row.original.planId}`}
target="_blank"
>
{row.getValue('name')}
</a>
</div>
);
},
size: 250,
},
{
header: () => (
<div
className="flex cursor-pointer items-center"
onClick={() => handleColumnSort('signingVolume')}
>
{_(msg`Signing Volume`)}
<CaretSortIcon className="ml-2 h-4 w-4" />
</div>
),
accessorKey: 'signingVolume',
cell: ({ row }) => <div>{Number(row.getValue('signingVolume'))}</div>,
},
{
header: () => {
return (
<div
className="flex cursor-pointer items-center"
onClick={() => handleColumnSort('createdAt')}
>
{_(msg`Created`)}
<CaretSortIcon className="ml-2 h-4 w-4" />
</div>
);
},
accessorKey: 'createdAt',
cell: ({ row }) => i18n.date(row.original.createdAt),
},
] satisfies DataTableColumnDef<SigningVolume>[];
}, [sortOrder]);
useEffect(() => {
startTransition(() => {
updateSearchParams({
search: debouncedSearchString,
page: 1,
perPage,
sortBy,
sortOrder,
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearchString]);
const onPaginationChange = (page: number, perPage: number) => {
startTransition(() => {
updateSearchParams({
page,
perPage,
});
});
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchString(e.target.value);
};
const handleColumnSort = (column: 'name' | 'createdAt' | 'signingVolume') => {
startTransition(() => {
updateSearchParams({
sortBy: column,
sortOrder: sortBy === column && sortOrder === 'asc' ? 'desc' : 'asc',
});
});
};
return (
<div className="relative">
<Input
className="my-6 flex flex-row gap-4"
type="text"
placeholder={_(msg`Search by name or email`)}
value={searchString}
onChange={handleChange}
/>
<DataTable
columns={columns}
data={signingVolume}
perPage={perPage}
currentPage={page}
totalPages={totalPages}
onPaginationChange={onPaginationChange}
>
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
{isPending && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
</div>
)}
</div>
);
};

View File

@ -1,25 +0,0 @@
'use server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
import { getSigningVolume } from '@documenso/lib/server-only/admin/get-signing-volume';
type SearchOptions = {
search: string;
page: number;
perPage: number;
sortBy: 'name' | 'createdAt' | 'signingVolume';
sortOrder: 'asc' | 'desc';
};
export async function search({ search, page, perPage, sortBy, sortOrder }: SearchOptions) {
const { user } = await getRequiredServerComponentSession();
if (!isAdmin(user)) {
throw new Error('Unauthorized');
}
const results = await getSigningVolume({ search, page, perPage, sortBy, sortOrder });
return results;
}

View File

@ -1,48 +0,0 @@
import { Trans } from '@lingui/macro';
import { LeaderboardTable } from './data-table-leaderboard';
import { search } from './fetch-leaderboard.actions';
type AdminLeaderboardProps = {
searchParams?: {
search?: string;
page?: number;
perPage?: number;
sortBy?: 'name' | 'createdAt' | 'signingVolume';
sortOrder?: 'asc' | 'desc';
};
};
export default async function LeaderboardPage({ searchParams = {} }: AdminLeaderboardProps) {
const page = Number(searchParams.page) || 1;
const perPage = Number(searchParams.perPage) || 10;
const searchString = searchParams.search || '';
const sortBy = searchParams.sortBy || 'signingVolume';
const sortOrder = searchParams.sortOrder || 'desc';
const { leaderboard: signingVolume, totalPages } = await search({
search: searchString,
page,
perPage,
sortBy,
sortOrder,
});
return (
<div>
<h2 className="text-4xl font-semibold">
<Trans>Signing Volume</Trans>
</h2>
<div className="mt-8">
<LeaderboardTable
signingVolume={signingVolume}
totalPages={totalPages}
page={page}
perPage={perPage}
sortBy={sortBy}
sortOrder={sortOrder}
/>
</div>
</div>
);
}

View File

@ -9,13 +9,15 @@ import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DownloadAuditLogButtonProps = {
export type DocumentAuditLogDownloadButtonProps = {
className?: string;
teamId?: number;
documentId: number;
};
export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => {
export const DocumentAuditLogDownloadButton = ({
className,
documentId,
}: DocumentAuditLogDownloadButtonProps) => {
const { toast } = useToast();
const { _ } = useLingui();

View File

@ -1,5 +1,3 @@
'use client';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DocumentStatus } from '@prisma/client';
@ -10,19 +8,17 @@ import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DownloadCertificateButtonProps = {
export type DocumentCertificateDownloadButtonProps = {
className?: string;
documentId: number;
documentStatus: DocumentStatus;
teamId?: number;
};
export const DownloadCertificateButton = ({
export const DocumentCertificateDownloadButton = ({
className,
documentId,
documentStatus,
teamId,
}: DownloadCertificateButtonProps) => {
}: DocumentCertificateDownloadButtonProps) => {
const { toast } = useToast();
const { _ } = useLingui();

View File

@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
import { useNavigate, useSearchParams } from 'react-router';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
import {
@ -29,7 +30,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
export type EditDocumentFormProps = {
export type DocumentEditFormProps = {
className?: string;
initialDocument: TDocument;
documentRootPath: string;
@ -39,17 +40,18 @@ export type EditDocumentFormProps = {
type EditDocumentStep = 'settings' | 'signers' | 'fields' | 'subject';
const EditDocumentSteps: EditDocumentStep[] = ['settings', 'signers', 'fields', 'subject'];
export const EditDocumentForm = ({
export const DocumentEditForm = ({
className,
initialDocument,
documentRootPath,
isDocumentEnterprise,
}: EditDocumentFormProps) => {
}: DocumentEditFormProps) => {
const { toast } = useToast();
const { _ } = useLingui();
const router = useRouter();
const searchParams = useSearchParams();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const team = useOptionalCurrentTeam();
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
@ -194,9 +196,6 @@ export const EditDocumentForm = ({
},
});
// Router refresh is here to clear the router cache for when navigating to /documents.
router.refresh();
setStep('signers');
} catch (err) {
console.error(err);
@ -227,9 +226,6 @@ export const EditDocumentForm = ({
}),
]);
// Router refresh is here to clear the router cache for when navigating to /documents.
router.refresh();
setStep('fields');
} catch (err) {
console.error(err);
@ -265,9 +261,6 @@ export const EditDocumentForm = ({
}
}
// Router refresh is here to clear the router cache for when navigating to /documents.
router.refresh();
setStep('subject');
} catch (err) {
console.error(err);
@ -301,7 +294,7 @@ export const EditDocumentForm = ({
duration: 5000,
});
router.push(documentRootPath);
void navigate(documentRootPath);
return;
}
@ -312,7 +305,7 @@ export const EditDocumentForm = ({
duration: 5000,
});
} else {
router.push(`${documentRootPath}/${document.id}`);
void navigate(`${documentRootPath}/${document.id}`);
}
} catch (err) {
console.error(err);

View File

@ -3,7 +3,7 @@ import { useLingui } from '@lingui/react';
import type { Document, Recipient, Team, User } from '@prisma/client';
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { Link } from 'react-router';
import { match } from 'ts-pattern';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
@ -12,25 +12,23 @@ import { trpc as trpcClient } from '@documenso/trpc/client';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useAuth } from '~/providers/auth';
export type DocumentPageViewButtonProps = {
document: Document & {
user: Pick<User, 'id' | 'name' | 'email'>;
recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null;
};
team?: Pick<Team, 'id' | 'url'>;
};
export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
const { data: session } = useSession();
const { user } = useAuth();
const { toast } = useToast();
const { _ } = useLingui();
if (!session) {
return null;
}
const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
const recipient = document.recipients.find((recipient) => recipient.email === user.email);
const isRecipient = !!recipient;
const isPending = document.status === DocumentStatus.PENDING;
@ -77,7 +75,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
})
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
<Button className="w-full" asChild>
<Link href={`/sign/${recipient?.token}`}>
<Link to={`/sign/${recipient?.token}`}>
{match(role)
.with(RecipientRole.SIGNER, () => (
<>
@ -102,7 +100,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
))
.with({ isComplete: false }, () => (
<Button className="w-full" asChild>
<Link href={`${documentsPath}/${document.id}/edit`}>
<Link to={`${documentsPath}/${document.id}/edit`}>
<Trans>Edit</Trans>
</Link>
</Button>

View File

@ -3,7 +3,7 @@ import { useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DocumentStatus } from '@prisma/client';
import type { Document, Recipient, Team, TeamEmail, User } from '@prisma/client';
import type { Document, Recipient, Team, User } from '@prisma/client';
import {
Copy,
Download,
@ -14,7 +14,7 @@ import {
Share,
Trash2,
} from 'lucide-react';
import { useSession } from 'next-auth/react';
import { Link } from 'react-router';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
@ -29,11 +29,12 @@ import {
} from '@documenso/ui/primitives/dropdown-menu';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialog';
import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog';
import { DocumentResendDialog } from '~/components/dialogs/document-resend-dialog';
import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
import { DeleteDocumentDialog } from '../delete-document-dialog';
import { DuplicateDocumentDialog } from '../duplicate-document-dialog';
import { ResendDocumentActionItem } from '../resend-document-dialog';
import { useAuth } from '~/providers/auth';
import { useOptionalCurrentTeam } from '~/providers/team';
export type DocumentPageViewDropdownProps = {
document: Document & {
@ -41,24 +42,21 @@ export type DocumentPageViewDropdownProps = {
recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null;
};
team?: Pick<Team, 'id' | 'url'> & { teamEmail: TeamEmail | null };
};
export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDropdownProps) => {
const { data: session } = useSession();
export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownProps) => {
const { user } = useAuth();
const { toast } = useToast();
const { _ } = useLingui();
const team = useOptionalCurrentTeam();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
if (!session) {
return null;
}
const recipient = document.recipients.find((recipient) => recipient.email === user.email);
const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
const isOwner = document.user.id === session.user.id;
const isOwner = document.user.id === user.id;
const isDraft = document.status === DocumentStatus.DRAFT;
const isPending = document.status === DocumentStatus.PENDING;
const isDeleted = document.deletedAt !== null;
@ -112,7 +110,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
{(isOwner || isCurrentTeamDocument) && !isComplete && (
<DropdownMenuItem asChild>
<Link href={`${documentsPath}/${document.id}/edit`}>
<Link to={`${documentsPath}/${document.id}/edit`}>
<Edit className="mr-2 h-4 w-4" />
<Trans>Edit</Trans>
</Link>
@ -127,7 +125,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
)}
<DropdownMenuItem asChild>
<Link href={`${documentsPath}/${document.id}/logs`}>
<Link to={`${documentsPath}/${document.id}/logs`}>
<ScrollTextIcon className="mr-2 h-4 w-4" />
<Trans>Audit Log</Trans>
</Link>
@ -165,11 +163,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
/>
)}
<ResendDocumentActionItem
document={document}
recipients={nonSignedRecipients}
team={team}
/>
<DocumentResendDialog document={document} recipients={nonSignedRecipients} />
<DocumentShareButton
documentId={document.id}
@ -185,7 +179,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
/>
</DropdownMenuContent>
<DeleteDocumentDialog
<DocumentDeleteDialog
id={document.id}
status={document.status}
documentTitle={document.title}
@ -195,11 +189,10 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
/>
{isDuplicateDialogOpen && (
<DuplicateDocumentDialog
<DocumentDuplicateDialog
id={document.id}
open={isDuplicateDialogOpen}
onOpenChange={setDuplicateDialogOpen}
team={team}
/>
)}
</DropdownMenu>

View File

@ -1,3 +1,7 @@
'use client';
import Link from 'next/link';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';

View File

@ -1,5 +1,9 @@
'use client';
import { useMemo } from 'react';
import { useSearchParams } from 'next/navigation';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
@ -16,7 +20,7 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
export type DocumentLogsDataTableProps = {
export type DocumentLogsTableProps = {
documentId: number;
};
@ -25,7 +29,7 @@ const dateFormat: DateTimeFormatOptions = {
hourCycle: 'h12',
};
export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps) => {
export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
const { _, i18n } = useLingui();
const searchParams = useSearchParams();

View File

@ -1,159 +0,0 @@
import { Trans } from '@lingui/macro';
import type { Team, TeamEmail, TeamMemberRole } from '@prisma/client';
import { Link } from 'react-router';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
import { parseToIntegerArray } from '@documenso/lib/utils/params';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { trpc } from '@documenso/trpc/react';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { DocumentSearch } from '~/components/(dashboard)/document-search/document-search';
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
import { isPeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
import { DocumentUploadDropzone } from '~/components/document/document-upload';
import { DocumentStatus } from '~/components/formatter/document-status';
import { DocumentsTable } from '~/components/tables/documents-table';
import { DocumentsTableEmptyState } from '~/components/tables/documents-table-empty-state';
import { DocumentsTableSenderFilter } from '~/components/tables/documents-table-sender-filter';
import { useAuth } from '~/providers/auth';
export interface DocumentsPageViewProps {
searchParams?: {
status?: ExtendedDocumentStatus;
period?: PeriodSelectorValue;
page?: string;
perPage?: string;
senderIds?: string;
search?: string;
};
team?: Team & { teamEmail?: TeamEmail | null } & { currentTeamMember?: { role: TeamMemberRole } };
}
export const DocumentsPageView = ({ searchParams = {}, team }: DocumentsPageViewProps) => {
const { user } = useAuth();
const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
const page = Number(searchParams.page) || 1;
const perPage = Number(searchParams.perPage) || 20;
const senderIds = parseToIntegerArray(searchParams.senderIds ?? '');
const search = searchParams.search || '';
const currentTeam = team
? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email }
: undefined;
const currentTeamMemberRole = team?.currentTeamMember?.role;
// const results = await findDocuments({
// status,
// orderBy: {
// column: 'createdAt',
// direction: 'desc',
// },
// page,
// perPage,
// period,
// senderIds,
// query: search,
// });
const { data, isLoading, isLoadingError } = trpc.document.findDocuments.useQuery({
page,
perPage,
});
const getTabHref = (value: typeof status) => {
const params = new URLSearchParams(searchParams);
params.set('status', value);
if (params.has('page')) {
params.delete('page');
}
return `${formatDocumentsPath(team?.url)}?${params.toString()}`;
};
return (
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<DocumentUploadDropzone team={currentTeam} />
<div className="mt-12 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
<div className="flex flex-row items-center">
{team && (
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
{team.avatarImageId && (
<AvatarImage src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`} />
)}
<AvatarFallback className="text-xs text-gray-400">
{team.name.slice(0, 1)}
</AvatarFallback>
</Avatar>
)}
<h1 className="text-4xl font-semibold">
<Trans>Documents</Trans>
</h1>
</div>
<div className="-m-1 flex flex-wrap gap-x-4 gap-y-6 overflow-hidden p-1">
<Tabs value={status} className="overflow-x-auto">
<TabsList>
{[
ExtendedDocumentStatus.INBOX,
ExtendedDocumentStatus.PENDING,
ExtendedDocumentStatus.COMPLETED,
ExtendedDocumentStatus.DRAFT,
ExtendedDocumentStatus.ALL,
].map((value) => (
<TabsTrigger
key={value}
className="hover:text-foreground min-w-[60px]"
value={value}
asChild
>
<Link to={getTabHref(value)} preventScrollReset>
<DocumentStatus status={value} />
{value !== ExtendedDocumentStatus.ALL && (
<span className="ml-1 inline-block opacity-50">todo</span>
)}
</Link>
</TabsTrigger>
))}
</TabsList>
</Tabs>
{team && <DocumentsTableSenderFilter teamId={team.id} />}
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
<PeriodSelector />
</div>
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
<DocumentSearch initialValue={search} />
</div>
</div>
</div>
<div className="mt-8">
<div>
{data && data.count === 0 ? (
<DocumentsTableEmptyState status={status} />
) : (
<DocumentsTable
data={data}
isLoading={isLoading}
isLoadingError={isLoadingError}
showSenderColumn={team !== undefined}
team={currentTeam}
/>
)}
</div>
</div>
</div>
);
};

View File

@ -1,16 +0,0 @@
import { useSearchParams } from 'react-router';
export function meta() {
return [{ title: 'Documents' }];
}
export default function DocumentsPage() {
const [searchParams] = useSearchParams();
return (
<>
<div>hello</div>
{/* <DocumentsPageView searchParams={searchParams} /> */}
</>
);
}

View File

@ -5,6 +5,7 @@ import {
Scripts,
ScrollRestoration,
isRouteErrorResponse,
useLoaderData,
} from 'react-router';
import { TrpcProvider } from '@documenso/trpc/react';
@ -32,7 +33,17 @@ export const links: Route.LinksFunction = () => [
{ rel: 'stylesheet', href: stylesheet },
];
export function loader() {
return {
__ENV__: Object.fromEntries(
Object.entries(process.env).filter(([key]) => key.startsWith('NEXT_')),
),
};
}
export function Layout({ children }: { children: React.ReactNode }) {
const { __ENV__ } = useLoaderData<typeof loader>() || {};
return (
<html lang="en">
<head>
@ -45,6 +56,12 @@ export function Layout({ children }: { children: React.ReactNode }) {
{children}
<ScrollRestoration />
<Scripts />
<script
dangerouslySetInnerHTML={{
__html: `window.__ENV__ = ${JSON.stringify(__ENV__)}`,
}}
/>
</body>
</html>
);

View File

@ -22,6 +22,7 @@ import {
getUsersWithSubscriptionsCount,
} from '@documenso/lib/server-only/admin/get-users-stats';
import { getSignerConversionMonthly } from '@documenso/lib/server-only/user/get-signer-conversion';
import { env } from '@documenso/lib/utils/env';
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
import { AdminStatsSignerConversionChart } from '~/components/general/admin-stats-signer-conversion-chart';
@ -87,11 +88,7 @@ export default function AdminStatsPage({ loaderData }: Route.ComponentProps) {
value={usersWithSubscriptionsCount}
/>
<CardMetric
icon={FileCog}
title={_(msg`App Version`)}
value={`v${process.env.APP_VERSION}`}
/>
<CardMetric icon={FileCog} title={_(msg`App Version`)} value={`v${env('APP_VERSION')}`} />
</div>
<div className="mt-16 gap-8">

View File

@ -1,20 +1,20 @@
import { Plural, Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DocumentStatus } from '@prisma/client';
import type { Team, TeamEmail } from '@prisma/client';
import { TeamMemberRole } from '@prisma/client';
import { ChevronLeft, Clock9, Users2 } from 'lucide-react';
import { Link, redirect } from 'react-router';
import { match } from 'ts-pattern';
import { getRequiredSession } from '@documenso/auth/server/lib/utils/get-session';
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -28,24 +28,38 @@ import {
DocumentStatus as DocumentStatusComponent,
FRIENDLY_STATUS_MAP,
} from '~/components/formatter/document-status';
import { DocumentPageViewButton } from '~/components/pages/document/document-page-view-button';
import { DocumentPageViewDropdown } from '~/components/pages/document/document-page-view-dropdown';
import { DocumentPageViewInformation } from '~/components/pages/document/document-page-view-information';
import { DocumentPageViewRecentActivity } from '~/components/pages/document/document-page-view-recent-activity';
import { DocumentPageViewRecipients } from '~/components/pages/document/document-page-view-recipients';
import { useAuth } from '~/providers/auth';
import { DocumentPageViewButton } from './document-page-view-button';
import { DocumentPageViewDropdown } from './document-page-view-dropdown';
import { DocumentPageViewInformation } from './document-page-view-information';
import { DocumentPageViewRecentActivity } from './document-page-view-recent-activity';
import { DocumentPageViewRecipients } from './document-page-view-recipients';
import type { Route } from './+types/$id._index';
export type DocumentPageViewProps = {
documentId: number;
team?: Team & { teamEmail: TeamEmail | null } & { currentTeamMember: { role: TeamMemberRole } };
};
export async function loader({ request, params }: Route.LoaderArgs) {
const { id } = params;
export const DocumentPageView = async ({ documentId, team }: DocumentPageViewProps) => {
const { _ } = useLingui();
const { user } = await getRequiredSession(request);
// Todo: Get from parent loader, this is just for testing.
const team = await prisma.team.findFirst({
where: {
documents: {
some: {
id: Number(id),
},
},
},
});
const documentId = Number(id);
const documentRootPath = formatDocumentsPath(team?.url);
const { user } = await getRequiredServerComponentSession();
if (!documentId || Number.isNaN(documentId)) {
return redirect(documentRootPath);
}
const document = await getDocumentById({
documentId,
@ -54,7 +68,7 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
}).catch(() => null);
if (document?.teamId && !team?.url) {
redirect(documentRootPath);
return redirect(documentRootPath);
}
const documentVisibility = document?.visibility;
@ -73,19 +87,15 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
.otherwise(() => false);
}
const isDocumentHistoryEnabled = await getServerComponentFlag(
'app_document_page_view_history_sheet',
);
if (!document || !document.documentData || (team && !canAccessDocument)) {
redirect(documentRootPath);
return redirect(documentRootPath);
}
if (team && !canAccessDocument) {
redirect(documentRootPath);
return redirect(documentRootPath);
}
const { documentData, documentMeta } = document;
const { documentMeta } = document;
if (documentMeta?.password) {
const key = DOCUMENSO_ENCRYPTION_KEY;
@ -104,6 +114,7 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
documentMeta.password = securePassword;
}
// Todo: Get full document instead???
const [recipients, fields] = await Promise.all([
getRecipientsForDocument({
documentId,
@ -122,13 +133,30 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
recipients,
};
return {
document: documentWithRecipients,
documentRootPath,
fields,
};
}
export default function DocumentPage({ loaderData }: Route.ComponentProps) {
const { _ } = useLingui();
const { user } = useAuth();
const { document, documentRootPath, fields } = loaderData;
const { recipients, documentData, documentMeta } = document;
const isDocumentHistoryEnabled = false; // Todo: Was flag
return (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
{document.status === DocumentStatus.PENDING && (
<DocumentRecipientLinkCopyDialog recipients={recipients} />
)}
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
<Link to={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
<Trans>Documents</Trans>
</Link>
@ -207,7 +235,7 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
{_(FRIENDLY_STATUS_MAP[document.status].labelExtended)}
</h3>
<DocumentPageViewDropdown document={documentWithRecipients} team={team} />
<DocumentPageViewDropdown document={document} />
</div>
<p className="text-muted-foreground mt-2 px-4 text-sm">
@ -235,18 +263,15 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
</p>
<div className="mt-4 border-t px-4 pt-4">
<DocumentPageViewButton document={documentWithRecipients} team={team} />
<DocumentPageViewButton document={document} />
</div>
</section>
{/* Document information section. */}
<DocumentPageViewInformation document={documentWithRecipients} userId={user.id} />
<DocumentPageViewInformation document={document} userId={user.id} />
{/* Recipients section. */}
<DocumentPageViewRecipients
document={documentWithRecipients}
documentRootPath={documentRootPath}
/>
<DocumentPageViewRecipients document={document} documentRootPath={documentRootPath} />
{/* Recent activity section. */}
<DocumentPageViewRecentActivity documentId={document.id} userId={user.id} />
@ -255,4 +280,4 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
</div>
</div>
);
};
}

View File

@ -1,37 +1,49 @@
import { Plural, Trans } from '@lingui/macro';
import type { Team } from '@prisma/client';
import { TeamMemberRole } from '@prisma/client';
import { DocumentStatus as InternalDocumentStatus } from '@prisma/client';
import { ChevronLeft, Users2 } from 'lucide-react';
import { Link, redirect } from 'react-router';
import { match } from 'ts-pattern';
import { getRequiredSession } from '@documenso/auth/server/lib/utils/get-session';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
import { DocumentStatus } from '~/components/formatter/document-status';
import { DocumentEditForm } from '~/components/pages/document/document-edit-form';
import { EditDocumentForm } from '../edit-document';
import type { Route } from './+types/$id.edit';
export type DocumentEditPageViewProps = {
documentId: number;
team?: Team & { currentTeamMember: { role: TeamMemberRole } };
};
export async function loader({ request, params }: Route.LoaderArgs) {
const { id } = params;
const { user } = await getRequiredSession(request);
// Todo: Get from parent loader, this is just for testing.
const team = await prisma.team.findFirst({
where: {
documents: {
some: {
id: Number(id),
},
},
},
});
const documentId = Number(id);
export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPageViewProps) => {
const documentRootPath = formatDocumentsPath(team?.url);
if (!documentId || Number.isNaN(documentId)) {
redirect(documentRootPath);
return redirect(documentRootPath);
}
const { user } = await getRequiredServerComponentSession();
const document = await getDocumentWithDetailsById({
documentId,
userId: user.id,
@ -39,7 +51,7 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
}).catch(() => null);
if (document?.teamId && !team?.url) {
redirect(documentRootPath);
return redirect(documentRootPath);
}
const documentVisibility = document?.visibility;
@ -59,15 +71,15 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
}
if (!document) {
redirect(documentRootPath);
return redirect(documentRootPath);
}
if (team && !canAccessDocument) {
redirect(documentRootPath);
return redirect(documentRootPath);
}
if (document.status === InternalDocumentStatus.COMPLETED) {
redirect(`${documentRootPath}/${documentId}`);
return redirect(`${documentRootPath}/${documentId}`);
}
const { documentMeta, recipients } = document;
@ -94,9 +106,21 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
teamId: team?.id,
});
return {
document,
documentRootPath,
isDocumentEnterprise,
};
}
export default function DocumentEditPage({ loaderData }: Route.ComponentProps) {
const { document, documentRootPath, isDocumentEnterprise } = loaderData;
const { recipients } = document;
return (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
<Link to={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
<Trans>Documents</Trans>
</Link>
@ -128,7 +152,7 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
)}
</div>
<EditDocumentForm
<DocumentEditForm
className="mt-6"
initialDocument={document}
documentRootPath={documentRootPath}
@ -136,4 +160,4 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
/>
</div>
);
};
}

View File

@ -4,36 +4,50 @@ import { useLingui } from '@lingui/react';
import type { Recipient } from '@prisma/client';
import { ChevronLeft } from 'lucide-react';
import { DateTime } from 'luxon';
import { Link, redirect } from 'react-router';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getRequiredSession } from '@documenso/auth/server/lib/utils/get-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { Card } from '@documenso/ui/primitives/card';
import {
DocumentStatus as DocumentStatusComponent,
FRIENDLY_STATUS_MAP,
} from '~/components/formatter/document-status';
import { useOptionalCurrentTeam } from '~/providers/team';
import { DocumentAuditLogDownloadButton } from '~/components/pages/document/document-audit-log-download-button';
import { DocumentCertificateDownloadButton } from '~/components/pages/document/document-certificate-download-button';
import { DocumentLogsTable } from '~/components/tables/document-logs-table';
import { DocumentLogsDataTable } from './document-logs-data-table';
import { DownloadAuditLogButton } from './download-audit-log-button';
import { DownloadCertificateButton } from './download-certificate-button';
import type { Route } from './+types/$id.logs';
export type DocumentLogsPageViewProps = {
documentId: number;
};
export async function loader({ request, params }: Route.LoaderArgs) {
const { id } = params;
export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewProps) => {
const { _, i18n } = useLingui();
const { user } = await getRequiredSession(request);
const team = useOptionalCurrentTeam();
// Todo: Get from parent loader, this is just for testing.
const team = await prisma.team.findFirst({
where: {
documents: {
some: {
id: Number(id),
},
},
},
});
const documentId = Number(id);
const documentRootPath = formatDocumentsPath(team?.url);
const { user } = await getRequiredServerComponentSession();
if (!documentId || Number.isNaN(documentId)) {
return redirect(documentRootPath);
}
// Todo: Get detailed?
const [document, recipients] = await Promise.all([
getDocumentById({
documentId,
@ -48,9 +62,21 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
]);
if (!document || !document.documentData) {
redirect(documentRootPath);
return redirect(documentRootPath);
}
return {
document,
documentRootPath,
recipients,
};
}
export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps) {
const { document, documentRootPath, recipients } = loaderData;
const { _, i18n } = useLingui();
const documentInformation: { description: MessageDescriptor; value: string }[] = [
{
description: msg`Document title`,
@ -97,11 +123,10 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
return `[${recipient.role}] ${text}`;
};
return (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
<Link
href={`${documentRootPath}/${document.id}`}
to={`${documentRootPath}/${document.id}`}
className="flex items-center text-[#7AC455] hover:opacity-80"
>
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
@ -127,14 +152,13 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
</div>
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
<DownloadCertificateButton
<DocumentCertificateDownloadButton
className="mr-2"
documentId={document.id}
documentStatus={document.status}
teamId={team?.id}
/>
<DownloadAuditLogButton teamId={team?.id} documentId={document.id} />
<DocumentAuditLogDownloadButton documentId={document.id} />
</div>
</div>
@ -161,8 +185,8 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
</section>
<section className="mt-6">
<DocumentLogsDataTable documentId={document.id} />
<DocumentLogsTable documentId={document.id} />
</section>
</div>
);
};
}

View File

@ -0,0 +1,165 @@
import { Trans } from '@lingui/macro';
import { useSearchParams } from 'react-router';
import { Link } from 'react-router';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { parseToIntegerArray } from '@documenso/lib/utils/params';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { trpc } from '@documenso/trpc/react';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { DocumentSearch } from '~/components/(dashboard)/document-search/document-search';
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
import { isPeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
import { DocumentUploadDropzone } from '~/components/document/document-upload';
import { DocumentStatus } from '~/components/formatter/document-status';
import { UpcomingProfileClaimTeaser } from '~/components/general/upcoming-profile-claim-teaser';
import { DocumentsTable } from '~/components/tables/documents-table';
import { DocumentsTableEmptyState } from '~/components/tables/documents-table-empty-state';
import { DocumentsTableSenderFilter } from '~/components/tables/documents-table-sender-filter';
import { useAuth } from '~/providers/auth';
import { useOptionalCurrentTeam } from '~/providers/team';
export function meta() {
return [{ title: 'Documents' }];
}
// searchParams?: {
// status?: ExtendedDocumentStatus;
// period?: PeriodSelectorValue;
// page?: string;
// perPage?: string;
// senderIds?: string;
// search?: string;
// };
export default function DocumentsPage() {
const [searchParams] = useSearchParams();
const { user } = useAuth();
const team = useOptionalCurrentTeam();
const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
const page = Number(searchParams.page) || 1;
const perPage = Number(searchParams.perPage) || 20;
const senderIds = parseToIntegerArray(searchParams.senderIds ?? '');
const search = searchParams.search || '';
const currentTeam = team
? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email }
: undefined;
const currentTeamMemberRole = team?.currentTeamMember?.role;
// const results = await findDocuments({
// status,
// orderBy: {
// column: 'createdAt',
// direction: 'desc',
// },
// page,
// perPage,
// period,
// senderIds,
// query: search,
// });
const { data, isLoading, isLoadingError } = trpc.document.findDocuments.useQuery({
page,
perPage,
});
const getTabHref = (value: typeof status) => {
const params = new URLSearchParams(searchParams);
params.set('status', value);
if (params.has('page')) {
params.delete('page');
}
return `${formatDocumentsPath(team?.url)}?${params.toString()}`;
};
return (
<>
<UpcomingProfileClaimTeaser />
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<DocumentUploadDropzone team={currentTeam} />
<div className="mt-12 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
<div className="flex flex-row items-center">
{team && (
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
<AvatarFallback className="text-xs text-gray-400">
{team.name.slice(0, 1)}
</AvatarFallback>
</Avatar>
)}
<h1 className="text-4xl font-semibold">
<Trans>Documents</Trans>
</h1>
</div>
<div className="-m-1 flex flex-wrap gap-x-4 gap-y-6 overflow-hidden p-1">
<Tabs value={status} className="overflow-x-auto">
<TabsList>
{[
ExtendedDocumentStatus.INBOX,
ExtendedDocumentStatus.PENDING,
ExtendedDocumentStatus.COMPLETED,
ExtendedDocumentStatus.DRAFT,
ExtendedDocumentStatus.ALL,
].map((value) => (
<TabsTrigger
key={value}
className="hover:text-foreground min-w-[60px]"
value={value}
asChild
>
<Link to={getTabHref(value)} preventScrollReset>
<DocumentStatus status={value} />
{value !== ExtendedDocumentStatus.ALL && (
<span className="ml-1 inline-block opacity-50">todo</span>
)}
</Link>
</TabsTrigger>
))}
</TabsList>
</Tabs>
{team && <DocumentsTableSenderFilter teamId={team.id} />}
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
<PeriodSelector />
</div>
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
<DocumentSearch initialValue={search} />
</div>
</div>
</div>
<div className="mt-8">
<div>
{data && data.count === 0 ? (
<DocumentsTableEmptyState status={status} />
) : (
<DocumentsTable
data={data}
isLoading={isLoading}
isLoadingError={isLoadingError}
showSenderColumn={team !== undefined}
team={currentTeam}
/>
)}
</div>
</div>
</div>
</>
);
}

View File

@ -1,19 +0,0 @@
import { useSearchParams } from 'react-router';
import { UpcomingProfileClaimTeaser } from '~/components/general/upcoming-profile-claim-teaser';
import { DocumentsPageView } from '~/documents+/_documents-page-view';
export function meta() {
return [{ title: 'Documents' }];
}
export default function DocumentsPage() {
const [searchParams] = useSearchParams();
return (
<>
<UpcomingProfileClaimTeaser />
<DocumentsPageView searchParams={searchParams} />
</>
);
}

View File

@ -2,14 +2,11 @@ import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
import { AccountDeleteDialog } from '~/components/dialogs/account-delete-dialog';
import { AvatarImageForm } from '~/components/forms/avatar-image';
import { ProfileForm } from '~/components/forms/profile';
import type { Route } from './+types/profile';
// import { DeleteAccountDialog } from './settings/profile/delete-account-dialog';
export function meta(_args: Route.MetaArgs) {
export function meta() {
return [{ title: 'Profile' }];
}
@ -28,7 +25,7 @@ export default function SettingsProfile() {
<hr className="my-4 max-w-xl" />
{/* <DeleteAccountDialog className="max-w-xl" /> */}
<AccountDeleteDialog className="max-w-xl" />
</div>
);
}

View File

@ -6,8 +6,8 @@ import { trpc } from '@documenso/trpc/react';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
import { CreateTeamDialog } from '~/components/(teams)/dialogs/create-team-dialog';
import { UserSettingsTeamsPageDataTable } from '~/components/(teams)/tables/user-settings-teams-page-data-table';
import { TeamCreateDialog } from '~/components/dialogs/team-create-dialog';
import { TeamEmailUsage } from './team-email-usage';
import { TeamInvitations } from './team-invitations';
@ -23,7 +23,7 @@ export default function TeamsSettingsPage() {
title={_(msg`Teams`)}
subtitle={_(msg`Manage all teams you are currently associated with.`)}
>
<CreateTeamDialog />
<TeamCreateDialog />
</SettingsHeader>
<UserSettingsTeamsPageDataTable />

View File

@ -0,0 +1,65 @@
import { Outlet, replace } from 'react-router';
import { getRequiredSession } from '@documenso/auth/server/lib/utils/get-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
import { TrpcProvider } from '@documenso/trpc/react';
import { TeamProvider } from '~/providers/team';
import type { Route } from './+types/_layout';
export const loader = async ({ request, params }: Route.LoaderArgs) => {
// Todo: get user better from context or something
// Todo: get user better from context or something
const { user } = await getRequiredSession(request);
const [getTeamsPromise, getTeamPromise] = await Promise.allSettled([
getTeams({ userId: user.id }),
getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl }),
]);
console.log('1');
console.log({ userId: user.id, teamUrl: params.teamUrl });
console.log(getTeamPromise.status);
if (getTeamPromise.status === 'rejected') {
console.log('2');
return replace('/documents');
}
const team = getTeamPromise.value;
const teams = getTeamsPromise.status === 'fulfilled' ? getTeamsPromise.value : [];
const trpcHeaders = {
'x-team-Id': team.id.toString(),
};
return {
team,
teams,
trpcHeaders,
};
};
export default function Layout({ loaderData }: Route.ComponentProps) {
const { team, trpcHeaders } = loaderData;
return (
<TeamProvider team={team}>
<TrpcProvider headers={trpcHeaders}>
{/* Todo: Do this. */}
{/* {team.subscription && team.subscription.status !== SubscriptionStatus.ACTIVE && (
<LayoutBillingBanner
subscription={team.subscription}
teamId={team.id}
userRole={team.currentTeamMember.role}
/>
)} */}
<main className="mt-8 pb-8 md:mt-12 md:pb-12">
<Outlet />
</main>
</TrpcProvider>
</TeamProvider>
);
}

View File

@ -0,0 +1,5 @@
import DocumentPage, { loader } from '~/routes/_authenticated+/documents+/$id._index';
export { loader };
export default DocumentPage;

View File

@ -0,0 +1,5 @@
import DocumentEditPage, { loader } from '~/routes/_authenticated+/documents+/$id.edit';
export { loader };
export default DocumentEditPage;

View File

@ -0,0 +1,5 @@
import DocumentLogsPage, { loader } from '~/routes/_authenticated+/documents+/$id.logs';
export { loader };
export default DocumentLogsPage;

View File

@ -0,0 +1,5 @@
import DocumentsPage, { meta } from '~/routes/_authenticated+/documents+/_index';
export { meta };
export default DocumentsPage;

View File

@ -0,0 +1,5 @@
import DocumentsPage, { meta } from '~/routes/_authenticated+/documents+/_index';
export { meta };
export default DocumentsPage;

View File

@ -1,23 +1,24 @@
import { Trans } from '@lingui/macro';
import { Link, redirect } from 'react-router';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import {
IS_GOOGLE_SSO_ENABLED,
IS_OIDC_SSO_ENABLED,
OIDC_PROVIDER_LABEL,
} from '@documenso/lib/constants/auth';
import { env } from '@documenso/lib/utils/env';
import { SignInForm } from '~/components/forms/signin';
import type { Route } from './+types/signin';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
export function meta(_args: Route.MetaArgs) {
return [{ title: 'Sign In' }];
}
export async function loader({ request }: Route.LoaderArgs) {
const session = await getSession(request)
const session = await getSession(request);
if (session.isAuthenticated) {
return redirect('/documents');
@ -25,9 +26,7 @@ export async function loader({ request }: Route.LoaderArgs) {
}
export default function SignIn() {
// Todo
// const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
const NEXT_PUBLIC_DISABLE_SIGNUP = 'false';
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
return (
<div className="w-screen max-w-lg px-4">

View File

@ -1,19 +1,16 @@
import { redirect } from 'react-router';
import { IS_GOOGLE_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth';
import { env } from '@documenso/lib/utils/env';
import { SignUpForm } from '~/components/forms/signup';
import type { Route } from './+types/_unauth.signup';
export function meta(_args: Route.MetaArgs) {
export function meta() {
return [{ title: 'Sign Up' }];
}
export function loader() {
// Todo
// const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
const NEXT_PUBLIC_DISABLE_SIGNUP: string = 'false';
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
if (NEXT_PUBLIC_DISABLE_SIGNUP === 'true') {
return redirect('/signin');

View File

@ -7,7 +7,8 @@
"dev": "react-router dev",
"start": "cross-env NODE_ENV=production node dist/server/index.js",
"clean": "rimraf .react-router && rimraf node_modules",
"typecheck": "react-router typegen && tsc"
"typecheck": "react-router typegen && tsc",
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
},
"dependencies": {
"@epic-web/remember": "^1.1.0",
@ -48,4 +49,4 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
}
}
}

22
apps/remix/public/pdf.worker.min.js vendored Normal file

File diff suppressed because one or more lines are too long

10
apps/remix/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string;
// more env variables...
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@ -4,15 +4,18 @@ import autoprefixer from 'autoprefixer';
import serverAdapter from 'hono-react-router-adapter/vite';
import path from 'path';
import tailwindcss from 'tailwindcss';
import { defineConfig } from 'vite';
import { defineConfig, loadEnv } from 'vite';
import macrosPlugin from 'vite-plugin-babel-macros';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
envDir: path.join(__dirname, '../../'),
envPrefix: 'NEXT_',
envPrefix: '__DO_NOT_USE_OR_YOU_WILL_BE_FIRED__',
define: {
'process.env': {},
'process.env': {
...process.env,
...loadEnv('development', path.join(__dirname, '../../'), ''),
},
},
css: {
postcss: {
@ -20,6 +23,7 @@ export default defineConfig({
},
},
ssr: {
// , 'next/font/google' doesnot work
noExternal: ['react-dropzone', 'recharts'],
},
server: {
@ -36,7 +40,7 @@ export default defineConfig({
tsconfigPaths(),
],
// optimizeDeps: {
// exclude: ['@node-rs/bcrypt', '@node-rs/bcrypt-wasm32-wasi', 'react-dropzone', '@documenso/ui'], // Todo: Probably remove.
// exclude: ['next/font/google'], // Todo: Probably remove.
// force: true,
// },
});

View File

@ -10,7 +10,6 @@ import { z } from 'zod';
import { downloadFile } from '@documenso/lib/client-only/download-file';
import { AppError } from '@documenso/lib/errors/app-error';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
@ -147,7 +146,7 @@ export const ViewRecoveryCodesDialog = () => {
<Alert variant="destructive">
<AlertDescription>
{match(AppError.parseError(error).message)
.with(ErrorCode.INCORRECT_TWO_FACTOR_CODE, () => (
.with('INCORRECT_TWO_FACTOR_CODE', () => (
<Trans>Invalid code. Please try again.</Trans>
))
.otherwise(() => (

View File

@ -76,10 +76,10 @@ export const ZSignUpFormV2Schema = z
export const signupErrorMessages: Record<string, MessageDescriptor> = {
SIGNUP_DISABLED: msg`Signups are disabled.`,
PROFILE_URL_TAKEN: msg`This username has already been taken`,
PREMIUM_PROFILE_URL: msg`Only subscribers can have a username shorter than 6 characters`,
[AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
[AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
[AppErrorCode.PROFILE_URL_TAKEN]: msg`This username has already been taken`,
[AppErrorCode.PREMIUM_PROFILE_URL]: msg`Only subscribers can have a username shorter than 6 characters`,
};
export type TSignUpFormV2Schema = z.infer<typeof ZSignUpFormV2Schema>;