Compare commits
22 Commits
v1.3.1-rc.
...
v1.3.1-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1191e1d9c3 | ||
|
|
9c1e1f50a8 | ||
|
|
efb9e9f3ec | ||
|
|
a7672545d7 | ||
|
|
1a10cd2ae1 | ||
|
|
204388888d | ||
|
|
0d977e783e | ||
|
|
0d15b80c2d | ||
|
|
4e9cce0df0 | ||
|
|
6f726565e8 | ||
|
|
b4b146ee49 | ||
|
|
67aebaac1a | ||
|
|
a593e045b5 | ||
|
|
1a73f3e007 | ||
|
|
ea0120abc8 | ||
|
|
b501ffdee9 | ||
|
|
31050d6b7b | ||
|
|
ed1998278a | ||
|
|
6a26ab4b2b | ||
|
|
b76d2cea3b | ||
|
|
b9282f11b0 | ||
|
|
38ad3a1922 |
56
.github/ISSUE_TEMPLATE/improvement.yml
vendored
56
.github/ISSUE_TEMPLATE/improvement.yml
vendored
@@ -1,35 +1,39 @@
|
||||
name: 'General Improvement'
|
||||
description: Suggest a minor enhancement or improvement for this project
|
||||
name: 'General Improvement Request'
|
||||
description: 'Suggest a minor enhancement or improvement for this project'
|
||||
title: '[Title for your improvement suggestion]'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please provide a clear and concise title for your improvement suggestion
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Improvement Description
|
||||
description: Describe the improvement you are suggesting in detail. Explain what specific aspect of the project it addresses or enhances.
|
||||
label: 'Describe the improvement you are suggesting in detail'
|
||||
description: 'Explain why this improvement would be beneficial. Share any context, pain points, or reasons for suggesting this change.'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Rationale
|
||||
description: Explain why this improvement would be beneficial. Share any context, pain points, or reasons for suggesting this change.
|
||||
- type: textarea
|
||||
label: 'Additional Information & Alternatives (optional)'
|
||||
description: 'Are there any additional context or information that might be relevant to the improvement suggestion.'
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: assignee
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: If you have a suggestion for how this improvement could be implemented, describe it here. Include any technical details, design suggestions, or other relevant information.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Alternatives (optional)
|
||||
description: Are there any alternative approaches to achieve the same improvement? Describe other ways to address the issue or enhance the project.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any additional context or information that might be relevant to the improvement suggestion.
|
||||
label: 'Do you want to work on this improvement?'
|
||||
multiple: false
|
||||
options:
|
||||
- 'No'
|
||||
- 'Yes'
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please check the boxes that apply to this improvement suggestion.
|
||||
label: 'Please check the boxes that apply to this improvement suggestion.'
|
||||
options:
|
||||
- label: I have searched the existing issues and improvement suggestions to avoid duplication.
|
||||
- label: I have provided a clear description of the improvement being suggested.
|
||||
- label: I have explained the rationale behind this improvement.
|
||||
- label: I have included any relevant technical details or design suggestions.
|
||||
- label: I understand that this is a suggestion and that there is no guarantee of implementation.
|
||||
- label: 'I have searched the existing issues and improvement suggestions to avoid duplication.'
|
||||
- label: 'I have provided a clear description of the improvement being suggested.'
|
||||
- label: 'I have explained the rationale behind this improvement.'
|
||||
- label: 'I have included any relevant technical details or design suggestions.'
|
||||
- label: 'I understand that this is a suggestion and that there is no guarantee of implementation.'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
>We are nominated for a Product Hunt Gold Kitty 😺✨ and appreciate any support: https://documen.so/kitty
|
||||
|
||||
<img src="https://github.com/documenso/documenso/assets/13398220/a643571f-0239-46a6-a73e-6bef38d1228b" alt="Documenso Logo">
|
||||
|
||||
<p align="center" style="margin-top: 20px">
|
||||
|
||||
115
apps/marketing/content/blog/linear-gh.mdx
Normal file
115
apps/marketing/content/blog/linear-gh.mdx
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: Moving from Linear to GitHub & LIVE Roadmap 2.0
|
||||
description: We are leaving linear and are going all in on GitHub. Here is how we do it.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-01-10
|
||||
Tags:
|
||||
- GitHub
|
||||
- Backlog
|
||||
- Roadmap
|
||||
---
|
||||
|
||||
# From Linear to GitHub
|
||||
|
||||
> TLDR; We are leaving Linear and are using only GitHub going forward. We no longer communicate feature timelines, only what we are working on and what's next.
|
||||
|
||||
If you follow us, you know we have been in full-on build mode. We are building, the community is building, it's great. Building is our daily business, so we think a lot about improving our approach to doing it.
|
||||
Our most recent approach is to reduce the number of tools and platforms we use. Every tool we use
|
||||
|
||||
- Reduces the average time you spend on the tool
|
||||
- Reduces your focus
|
||||
- Increases mental load to keep all points of interest in mind
|
||||
|
||||
We thought about where we spend the most time, and hardly surprising: it's GitHub. Not only do we spend a lot of time there, but we also WANT to spend a lot of time there because:
|
||||
|
||||
- It's where the community contributes, and we are all about community
|
||||
- It's where we show the world what we are working on
|
||||
|
||||
# The old structure
|
||||
|
||||
So far, we have been using Linear for our Backlog/ Task Management and synced issues we want to showcase or work on with the community via synclinear.com. Not only did we have our development issues there, but since
|
||||
we have our own resident founding designer, we created a proper design backlog to structure our design workflows.
|
||||
|
||||
# The new structure
|
||||
|
||||
We moved everything to GitHub once we realized our focus was already there. This has a few key benefits:
|
||||
|
||||
- Reducing dilution of attention and time: You can hang out on GitHub without risk of missing much
|
||||
- Putting different aspects of Documenso close to each other: Development, Design, Community
|
||||
- Keep long-term, niche, and very abstract issues out of the main repo so we don't get desensitized by large issue numbers
|
||||
|
||||
To achieve this, we created a few GitHub repositories to host issues, with the main repository remaining the central point of interest, especially for the community.
|
||||
|
||||
## 1. Main Repository - Day to day Issues and the shorter-term roadmap (LIVE Roadmap 2.0)
|
||||
|
||||
> [github.com/documenso/documenso](https://github.com/documenso/documenso)
|
||||
|
||||
Apart from the source code of the Documenso app and website, the main repo houses issues raised by the community and issues where we invite the community to participate.
|
||||
With the overhauling of our issue management, we are also updating our progress communication. While the software and product development process is highly complex,
|
||||
we try to give as much insight into what we do as possible. To that end, we went through 3 phases, three being what we do now.
|
||||
|
||||
1. **One extensive roadmap**: Initially we had one roadmap and were (very) slowly checking off boxes there (via a "Roadmap" milestone). While this is easy, it's also pretty imprecise and not practical as the project grows
|
||||
2. **Estimated releases per quarter**: To give better guidance, we tried communicating our goals for the quarter; a pretty big window we thought we could roughly "hit". While the idea of not being too detailed was good, it is tough to estimate when some significant things are done if you do a lot of minor/ other things in parallel,
|
||||
like working with the community and tuning things you go. Hitting time targets is tricky because there may be better things to do than sticking to that time target. This is always much easier to grasp for the people closely involved. The fallacy is to assume the thing you plan for exists in a vacuum.
|
||||
3. Since we do not want to limit ourselves in choosing the most effective course but still give some insight into what's going on and what's coming up, we updated the live roadmap [https://documen.so/live](https://documen.so/live). It now shows what we are currently working on and what we plan on doing next. We do not provide
|
||||
a specific timeline anymore since we couldn't even if we wanted to. Of course, we set our short-term goals based on what's best for the community. We give updates on the issues being worked on as well as possible.
|
||||
|
||||
## 2. Public Backlog - The longer-term roadmap
|
||||
|
||||
> [github.com/documenso/backlog](https://github.com/documenso/backlog)
|
||||
|
||||
The public backlog houses everything we want to build eventually. We do not provide a specific timeline of when that might happen. If we decide against something, it will be removed from the public backlog, as we consider this our long-term vision for Documenso. If you are interested in something on the roadmap, comment on the issue or post on Discord. This helps us gauge interest in specific features.
|
||||
**Issues in the public backlog are not** available to be worked on. For issues to work on, please check the main repository issues. The issues found here are scoped broader since they are not meant for immediate execution but rather give a sense of where Documenso is going and what we consider part of our domain.
|
||||
|
||||
## 3. Internal Backlog
|
||||
|
||||
> github.com/documenso/backlog-internal
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/gh1.png"
|
||||
width="1260"
|
||||
height="630"
|
||||
alt="GitHub: Development Board"
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
Our internal Kanban for development
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
This serves as the direct replacement for our Linear backlog. Here, we manage issues that are either too small or short-term for inclusion in the long-term roadmap, yet too specialized or fundamental to be integrated into the main repository. Our development Kanban board is implemented using a GitHub project.
|
||||
|
||||
## 4. Internal Design Backlog
|
||||
|
||||
> github.com/documenso/design-internal
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/gh2.png"
|
||||
width="1260"
|
||||
height="630"
|
||||
alt="GitHub: Design Board"
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
Our internal Kanban for design
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
This is the design equivalent of the internal backlog. The internal design backlog houses our design projects that include the exploration of new features, detailed UI designs, and improving the platform overall.
|
||||
It's similar to the Kanban board for the development backlog.
|
||||
|
||||
## 5. Public Design Repository
|
||||
|
||||
> [github.com/documenso/backlog-design](https://github.com/documenso/design)
|
||||
|
||||
While the internal design backlog also existed in Linear, the public design repository is new. Since designing in the open is tricky, we opted to publish the detailed design artifacts with the corresponding feature instead.
|
||||
We already have design.documenso.com housing our general design system. Here, we will publish the specifics of how we applied this to each feature. We will publish the first artifacts here soon, what may be in the cards can be found on the [LIVE Roadmap](https://documen.so/live).
|
||||
|
||||
Feel free to connect with us on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments! We're always here to help and would love to hear from you :)
|
||||
|
||||
Best from Hamburg\
|
||||
Timur
|
||||
BIN
apps/marketing/public/blog/gh1.png
Normal file
BIN
apps/marketing/public/blog/gh1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
BIN
apps/marketing/public/blog/gh2.png
Normal file
BIN
apps/marketing/public/blog/gh2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 175 KiB |
@@ -6,7 +6,7 @@ import { Download, Edit, Pencil } from 'lucide-react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||
@@ -55,28 +55,14 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
||||
const documentData = document?.documentData;
|
||||
|
||||
if (!documentData) {
|
||||
return;
|
||||
throw Error('No document available');
|
||||
}
|
||||
|
||||
const documentBytes = await getFile(documentData);
|
||||
|
||||
const blob = new Blob([documentBytes], {
|
||||
type: 'application/pdf',
|
||||
});
|
||||
|
||||
const link = window.document.createElement('a');
|
||||
const baseTitle = row.title.includes('.pdf') ? row.title.split('.pdf')[0] : row.title;
|
||||
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = baseTitle ? `${baseTitle}_signed.pdf` : 'document.pdf';
|
||||
|
||||
link.click();
|
||||
|
||||
window.URL.revokeObjectURL(link.href);
|
||||
} catch (error) {
|
||||
await downloadPDF({ documentData, fileName: row.title });
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
description: 'An error occurred while trying to download file.',
|
||||
description: 'An error occurred while downloading your document.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from '@documenso/ui/primitives/dropdown-menu';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { ResendDocumentActionItem } from './_action-items/resend-document';
|
||||
import { DeleteDocumentDialog } from './delete-document-dialog';
|
||||
@@ -44,6 +45,7 @@ export type DataTableActionDropdownProps = {
|
||||
|
||||
export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => {
|
||||
const { data: session } = useSession();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
|
||||
@@ -63,39 +65,33 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
||||
const isDocumentDeletable = isOwner;
|
||||
|
||||
const onDownloadClick = async () => {
|
||||
let document: DocumentWithData | null = null;
|
||||
try {
|
||||
let document: DocumentWithData | null = null;
|
||||
|
||||
if (!recipient) {
|
||||
document = await trpcClient.document.getDocumentById.query({
|
||||
id: row.id,
|
||||
});
|
||||
} else {
|
||||
document = await trpcClient.document.getDocumentByToken.query({
|
||||
token: recipient.token,
|
||||
if (!recipient) {
|
||||
document = await trpcClient.document.getDocumentById.query({
|
||||
id: row.id,
|
||||
});
|
||||
} else {
|
||||
document = await trpcClient.document.getDocumentByToken.query({
|
||||
token: recipient.token,
|
||||
});
|
||||
}
|
||||
|
||||
const documentData = document?.documentData;
|
||||
|
||||
if (!documentData) {
|
||||
return;
|
||||
}
|
||||
|
||||
await downloadPDF({ documentData, fileName: row.title });
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
description: 'An error occurred while downloading your document.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
const documentData = document?.documentData;
|
||||
|
||||
if (!documentData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const documentBytes = await getFile(documentData);
|
||||
|
||||
const blob = new Blob([documentBytes], {
|
||||
type: 'application/pdf',
|
||||
});
|
||||
|
||||
const link = window.document.createElement('a');
|
||||
const baseTitle = row.title.includes('.pdf') ? row.title.split('.pdf')[0] : row.title;
|
||||
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = baseTitle ? `${baseTitle}_signed.pdf` : 'document.pdf';
|
||||
|
||||
link.click();
|
||||
|
||||
window.URL.revokeObjectURL(link.href);
|
||||
};
|
||||
|
||||
const nonSignedRecipients = row.Recipient.filter((item) => item.signingStatus !== 'SIGNED');
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to
|
||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { StackAvatar } from './stack-avatar';
|
||||
@@ -19,6 +20,10 @@ export function AvatarWithRecipient({ recipient }: AvatarWithRecipientProps) {
|
||||
const { toast } = useToast();
|
||||
|
||||
const onRecipientClick = () => {
|
||||
if (!recipient.token) {
|
||||
return;
|
||||
}
|
||||
|
||||
void copy(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`).then(() => {
|
||||
toast({
|
||||
title: 'Copied to clipboard',
|
||||
@@ -28,19 +33,22 @@ export function AvatarWithRecipient({ recipient }: AvatarWithRecipientProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-1 flex cursor-pointer items-center gap-2" onClick={onRecipientClick}>
|
||||
<div
|
||||
className={cn('my-1 flex items-center gap-2', {
|
||||
'cursor-pointer hover:underline': recipient.token,
|
||||
})}
|
||||
role={recipient.token ? 'button' : undefined}
|
||||
title={recipient.token && 'Click to copy signing link for sending to recipient'}
|
||||
onClick={onRecipientClick}
|
||||
>
|
||||
<StackAvatar
|
||||
first={true}
|
||||
key={recipient.id}
|
||||
type={getRecipientType(recipient)}
|
||||
fallbackText={recipientAbbreviation(recipient)}
|
||||
/>
|
||||
<span
|
||||
className="text-muted-foreground text-sm hover:underline"
|
||||
title="Click to copy signing link for sending to recipient"
|
||||
>
|
||||
{recipient.email}
|
||||
</span>
|
||||
|
||||
<span className="text-muted-foreground text-sm">{recipient.email}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Loader, Monitor, Moon, Sun } from 'lucide-react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
SETTINGS_PAGE_SHORTCUT,
|
||||
TEMPLATES_PAGE_SHORTCUT,
|
||||
} from '@documenso/lib/constants/keyboard-shortcuts';
|
||||
import type { Document, Recipient } from '@documenso/prisma/client';
|
||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||
import {
|
||||
CommandDialog,
|
||||
@@ -65,6 +67,8 @@ export type CommandMenuProps = {
|
||||
|
||||
export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
||||
const { setTheme } = useTheme();
|
||||
const { data: session } = useSession();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(() => open ?? false);
|
||||
@@ -81,6 +85,17 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
||||
},
|
||||
);
|
||||
|
||||
const isOwner = useCallback(
|
||||
(document: Document) => document.userId === session?.user.id,
|
||||
[session?.user.id],
|
||||
);
|
||||
|
||||
const getSigningLink = useCallback(
|
||||
(recipients: Recipient[]) =>
|
||||
`/sign/${recipients.find((r) => r.email === session?.user.email)?.token}`,
|
||||
[session?.user.email],
|
||||
);
|
||||
|
||||
const searchResults = useMemo(() => {
|
||||
if (!searchDocumentsData) {
|
||||
return [];
|
||||
@@ -88,10 +103,10 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
||||
|
||||
return searchDocumentsData.map((document) => ({
|
||||
label: document.title,
|
||||
path: `/documents/${document.id}`,
|
||||
path: isOwner(document) ? `/documents/${document.id}` : getSigningLink(document.Recipient),
|
||||
value: [document.id, document.title, ...document.Recipient.map((r) => r.email)].join(' '),
|
||||
}));
|
||||
}, [searchDocumentsData]);
|
||||
}, [searchDocumentsData, isOwner, getSigningLink]);
|
||||
|
||||
const currentPage = pages[pages.length - 1];
|
||||
|
||||
|
||||
29
packages/lib/client-only/download-pdf.ts
Normal file
29
packages/lib/client-only/download-pdf.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { DocumentData } from '@documenso/prisma/client';
|
||||
|
||||
import { getFile } from '../universal/upload/get-file';
|
||||
|
||||
type DownloadPDFProps = {
|
||||
documentData: DocumentData;
|
||||
fileName?: string;
|
||||
};
|
||||
|
||||
export const downloadPDF = async ({ documentData, fileName }: DownloadPDFProps) => {
|
||||
const bytes = await getFile(documentData);
|
||||
|
||||
const blob = new Blob([bytes], {
|
||||
type: 'application/pdf',
|
||||
});
|
||||
|
||||
const link = window.document.createElement('a');
|
||||
|
||||
const [baseTitle] = fileName?.includes('.pdf')
|
||||
? fileName.split('.pdf')
|
||||
: [fileName ?? 'document'];
|
||||
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = `${baseTitle}_signed.pdf`;
|
||||
|
||||
link.click();
|
||||
|
||||
window.URL.revokeObjectURL(link.href);
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { SigningStatus } from '@documenso/prisma/client';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
|
||||
import type { FindResultSet } from '../../types/find-result-set';
|
||||
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
|
||||
|
||||
export type FindDocumentsOptions = {
|
||||
userId: number;
|
||||
@@ -173,8 +174,15 @@ export const findDocuments = async ({
|
||||
}),
|
||||
]);
|
||||
|
||||
const maskedData = data.map((document) =>
|
||||
maskRecipientTokensForDocument({
|
||||
document,
|
||||
user,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
data: maskedData,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
|
||||
@@ -5,14 +5,16 @@ import type { Prisma } from '@prisma/client';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type UpdateDocumentOptions = {
|
||||
documentId: number;
|
||||
data: Prisma.DocumentUpdateInput;
|
||||
userId: number;
|
||||
documentId: number;
|
||||
};
|
||||
|
||||
export const updateDocument = async ({ documentId, data }: UpdateDocumentOptions) => {
|
||||
export const updateDocument = async ({ documentId, userId, data }: UpdateDocumentOptions) => {
|
||||
return await prisma.document.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
userId,
|
||||
},
|
||||
data: {
|
||||
...data,
|
||||
|
||||
38
packages/lib/utils/mask-recipient-tokens-for-document.ts
Normal file
38
packages/lib/utils/mask-recipient-tokens-for-document.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import type { DocumentWithRecipients } from '@documenso/prisma/types/document-with-recipient';
|
||||
|
||||
export type MaskRecipientTokensForDocumentOptions<T extends DocumentWithRecipients> = {
|
||||
document: T;
|
||||
user?: User;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export const maskRecipientTokensForDocument = <T extends DocumentWithRecipients>({
|
||||
document,
|
||||
user,
|
||||
token,
|
||||
}: MaskRecipientTokensForDocumentOptions<T>) => {
|
||||
const maskedRecipients = document.Recipient.map((recipient) => {
|
||||
if (document.userId === user?.id) {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
if (recipient.email === user?.email) {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
if (recipient.token === token) {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
return {
|
||||
...recipient,
|
||||
token: '',
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...document,
|
||||
Recipient: maskedRecipients,
|
||||
};
|
||||
};
|
||||
@@ -18,6 +18,7 @@ export const seedDatabase = async () => {
|
||||
create: {
|
||||
name: 'Example User',
|
||||
email: 'example@documenso.com',
|
||||
emailVerified: new Date(),
|
||||
password: hashSync('password'),
|
||||
roles: [Role.USER],
|
||||
},
|
||||
@@ -31,6 +32,7 @@ export const seedDatabase = async () => {
|
||||
create: {
|
||||
name: 'Admin User',
|
||||
email: 'admin@documenso.com',
|
||||
emailVerified: new Date(),
|
||||
password: hashSync('password'),
|
||||
roles: [Role.USER, Role.ADMIN],
|
||||
},
|
||||
|
||||
@@ -5,11 +5,11 @@ import { useState } from 'react';
|
||||
|
||||
import { Download } from 'lucide-react';
|
||||
|
||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import type { DocumentData } from '@documenso/prisma/client';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { Button } from '../../primitives/button';
|
||||
import { useToast } from '../../primitives/use-toast';
|
||||
|
||||
export type DownloadButtonProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
disabled?: boolean;
|
||||
@@ -24,43 +24,29 @@ export const DocumentDownloadButton = ({
|
||||
disabled,
|
||||
...props
|
||||
}: DownloadButtonProps) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!documentData) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const bytes = await getFile(documentData);
|
||||
|
||||
const blob = new Blob([bytes], {
|
||||
type: 'application/pdf',
|
||||
await downloadPDF({ documentData, fileName }).then(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
const link = window.document.createElement('a');
|
||||
const baseTitle = fileName?.includes('.pdf') ? fileName.split('.pdf')[0] : fileName;
|
||||
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = baseTitle ? `${baseTitle}_signed.pdf` : 'document.pdf';
|
||||
|
||||
link.click();
|
||||
|
||||
window.URL.revokeObjectURL(link.href);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setIsLoading(false);
|
||||
|
||||
toast({
|
||||
title: 'Error',
|
||||
title: 'Something went wrong',
|
||||
description: 'An error occurred while downloading your document.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user