Compare commits

..

11 Commits

Author SHA1 Message Date
Catalin Pit
f0d2ab8700 fix: grammar mistake 2024-07-26 08:48:14 +03:00
Catalin Pit
deb2f1d255 fix: self-hosting docker 2024-07-26 08:47:24 +03:00
Catalin Pit
b172ac5834 fix: add db auth details 2024-07-24 11:54:29 +03:00
Catalin Pit
a6260fd18b fix: update env variables step 2024-07-24 11:01:39 +03:00
Chirag Chandrashekhar
994f6867f5 fix: API token deletion not reflected in cache until page reload (#1128)
Stops the API token copy card from continuing to appear when a newly created
token has been subsequently deleted.
2024-07-24 13:31:56 +10:00
Timur Ercan
c2374a9d65 Update standards-and-regulations.mdx
link to blog
2024-07-23 15:14:48 +02:00
Paul Koeck
7a1b9feee3 fix: typo in pricing table (#1239) 2024-07-23 13:08:55 +00:00
Timur Ercan
ddc704518f Chore/blog-basic-signing-and-roles (#1241)
blostpost basic roles
2024-07-23 15:08:15 +02:00
Ephraim Duncan
f647244e07 chore: update signup text (#1234)
Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
2024-07-23 08:24:21 +00:00
Ephraim Duncan
f8349bb927 chore: add new colors (#1224)
Adds the colors from our design system revamp into our current CSS vars to be used
at a later point.
2024-07-23 14:51:37 +10:00
David Nguyen
d6ec3f252a feat: add upload translation workflow 2024-07-23 13:08:43 +10:00
11 changed files with 309 additions and 34 deletions

View File

@@ -0,0 +1,58 @@
name: 'Extract and upload translations'
on:
workflow_dispatch:
workflow_call:
# Disabled until i18n PR is landed.
# push:
# branches: ['main']
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
extract_translations:
name: Extract and upload translations
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
- uses: ./.github/actions/node-install
- name: Extract and compile translations
run: |
npm run translate:extract
npm run translate:compile
- name: Check and commit any files created
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@documenso.com'
git add packages/lib/translations
git diff --staged --quiet --exit-code || (git commit -m "chore: extract translations" && git push)
- name: Compile translations
id: compile_translations
run: npm run translate:compile -- -- --strict
continue-on-error: true
- name: Upload missing translations
if: ${{ steps.compile_translations.outcome == 'failure' }}
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: true
download_translations: false
localization_branch_name: chore/translations
env:
# A numeric ID, found at https://crowdin.com/project/<projectName>/tools/api
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
# Visit https://crowdin.com/settings#api-key to create this token
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@@ -89,7 +89,7 @@ Please note that you must provide environment variables for connecting to the da
### Option 1: Production Docker Compose Setup
This setup includes a PostgreSQL database and the Documenso application. You will need to provide your own SMTP details using environment variables.
This setup includes a PostgreSQL database and the Documenso application.
<Steps>
@@ -103,12 +103,14 @@ Once downloaded, navigate to the directory containing the `compose.yml` file.
### Set Up Environment Variables
Create a `.env` file in the same directory as the `compose.yml` file.
Then add your SMTP details as well as the following environment variables:
Create a `.env` file in the same directory as the `compose.yml` file and fill in the following environment variables:
```bash
POSTGRES_USER="user"
POSTGRES_PASSWORD="changeme"
POSTGRES_DB=documenso
NEXTAUTH_SECRET="<your-secret>"
NEXT_PRIVATE_DATABASE_URL="postgres://<user>:<password>@<docker-network-or-ip:5432>/<db-name>"
NEXT_PRIVATE_ENCRYPTION_KEY="<your-key>"
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="<your-secondary-key>"
NEXT_PUBLIC_WEBAPP_URL="<your-url>"
@@ -126,12 +128,21 @@ The `cert.p12` file is required to sign and encrypt documents, so you must provi
```yaml
volumes:
- /path/to/your/keyfile.p12:/opt/documenso/cert.p12
# example
volumes:
- ../../apps/web/example/cert.p12:/opt/documenso/cert.p12
```
<Callout type="info">
Follow the instructions from the ["Signing Certificate"
section](developers/local-development/signing-certificate) to generate the `cert.p12` file.
</Callout>
After updating the volume binding, save the `compose.yml` file and run the following command to start the containers:
```bash
docker-compose --env-file ./.env -d up
docker-compose up -d
```
The command will start the PostgreSQL database and the Documenso application containers.

View File

@@ -10,6 +10,8 @@ import { Callout } from 'nextra/components';
signatures to ensure their authenticity, integrity, and confidentiality in the pharmaceutical, medical
device, and other FDA-regulated industries.
> Read more about 21 CFR Part 11 with Documenso here: https://documen.so/21-CFR-Part-11
### Main Requirements
- [x] Strong Identity Checks for each Signature

View File

@@ -0,0 +1,96 @@
---
title: Creating an Efficient Statement of Work Approval Process with Documenso
description: Submitting statements of work can be a drag on morale and project efficiency. Let's look at how to create a modern, low-friction workflow for this.
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2024-07-23
tags:
- Freelancer
- Statement of Work
- Productivity
---
<figure>
<MdxNextImage
src="/blog/sov.webp"
width="1400"
height="884"
alt="Working papers image"
/>
<figcaption className="text-center">Fine-tune your process using custom role for everyone involved.</figcaption>
</figure>
> TLDR; Statements of Work detail what needs to be done. Automate sending and approving them using Documenso and Zapier.
## What is a Statement of Work
A statement of work is a detailed document that outlines what needs to be done in a project. It covers the project's scope, objectives, and deliverables, laying out all the tasks, deadlines, and milestones. The statement of work also spells out whos responsible for what, ensuring everyones on the same page. Its a roadmap that keeps both clients and service providers aligned and ensures the project stays on track from start to finish.
In the context of freelance work, the statement of work is a document that outlines the details of a project between a freelancer and their client. It's a concrete work to be agreed upon and tracked after completion. The statement of work is created after the [proposal is accepted](https://documen.so/freelance-proposal) and the [contract signed](https://documen.so/freelance-contract).
## What does a good workflow look like?
### 1. Create the statement of work
The team at Zapier created a [excellent guide](https://zapier.com/blog/statement-of-work-template/), which goes into the statement of work. There is a short checklist:
- Project Context/ Current Scope
- Objectives for this piece of work
- Scope (tasks, activities, and limits)
- Requirements (e.g., technical and regulatory)
- Deliverables to be created
- Roles and Responsibilities
### 2. Get approval from subject matter experts (optional)
Since a statements of work can be very technical, having professionals from either side approve it first can be sensible. This can avoid double or unnecessary work and minimize the chances of misunderstandings. If this makes sense, it depends heavily on the scale of the project and the level of insight of the professional providing it.
### 3. Let the client sign off
Having the client sign off on the concrete work is the central step of the statement of work workflow. Assuming the documents content is correct, getting the go-ahead ensures everyone is aligned and clear on what should and will be worked on.
### 4. Inform other Stakeholders (optional)
Depending on the scale of the organizations working together, other people may need to be kept in the loop. This could be accounting on either side, project managers, or other interested parties.
## Fine-Tuning the Flow with Custom Roles
<figure>
<MdxNextImage src="/blog/roles.webp" width="1400" height="884" alt="Documenso Roles UI" />
<figcaption className="text-center">
Let's take a look at what it would look like with Documenso.
</figcaption>
</figure>
### 1. Creating the Document: Templates vs. Custom Document
[Creating a template](https://docs.documenso.com/users/templates) can make sense if you submit statements of work regularly. If you create a Documenso Template, you can add [dynamic text and number fields](https://docs.documenso.com/users/signing-documents/fields) to be filled out when using the template. Another approach to this is creating a template on a document service like Google Docs, filling out a new copy, and uploading the custom-created document to Documenso.
Different parts of this process can be automated using the [Zapier Documenso Integration](https://documen.so/zapier) as desired:
- Automatically sending out a template to be filled out and signed
- Creating a document in Documenso from a newly created document in Google Docs
- Triggering sending a document created from either template or automation
### 2. Approvals
Looping in subject matter experts can easily be done using the approver role. This role allows you to complete a document without blocking the signing. This means the client can sign a document, even if the approval is not yet in place, removing friction. However, having the approval denied will stop the document from being completed and let everyone know there are corrections to be made. A software version of this is possible, having the expert in a viewer role and marking the document as seen without the option to block.
### 3. Signers
Looping in the client is done by adding one or more signer roles. Signer roles require recipients to place at least one signature to fulfill their part in the flow.roles
### 4. BCC
You can add one or several BCC roles to inform interested parties, e.g., accounting or project managers. As the process finishes, BCC recipients receive a copy of the completed document (assuming it completes and is not blocked). Using BCC roles automates filling in everyone who is only interested in the outcome and wants to avoid involvement in the steps leading up to it.
### Conclusion
Streamlining your statement of work approval process with Documenso can significantly improve productivity and ease. Using templates, dynamic fields, and Zapier integrations, you can create a smooth, efficient workflow from start to finish. Adding roles for experts, signers, and BCC recipients tailors the process to fit your project's needs, ensuring everyone stays on the same page.
An efficient SOW process saves time and improves communication, letting you focus on delivering great work. We're excited to hear your thoughts and experiences—reach out on [Twitter / X](https://twitter.com/eltimuro) (DMs are open) or [Discord](https://documen.so/discord) if you have any questions, ideas, or comments.
Best from Hamburg\
Timur

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -128,7 +128,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
<div className="mt-8 flex w-full flex-col divide-y">
<p className="text-foreground py-4">Unlimited Documents per Month</p>
<p className="text-foreground py-4">API Accesss</p>
<p className="text-foreground py-4">API Access</p>
<p className="text-foreground py-4">Email and Discord Support</p>
<p className="text-foreground py-4">Premium Profile Name</p>
</div>
@@ -162,7 +162,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
<div className="mt-8 flex w-full flex-col divide-y">
<p className="text-foreground py-4">Unlimited Documents per Month</p>
<p className="text-foreground py-4">API Accesss</p>
<p className="text-foreground py-4">API Access</p>
<p className="text-foreground py-4">Email and Discord Support</p>
<p className="text-foreground py-4 font-medium">Team Inbox</p>
<p className="text-foreground py-4">5 Users Included</p>

View File

@@ -32,7 +32,7 @@ export default async function ApiTokensPage() {
<hr className="my-4" />
<ApiTokenForm className="max-w-xl" />
<ApiTokenForm className="max-w-xl" tokens={tokens} />
<hr className="mb-4 mt-8" />

View File

@@ -26,7 +26,7 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
const team = await getTeamByUrl({ userId: user.id, teamUrl });
let tokens: GetTeamTokensResponse | null = null;
let tokens: GetTeamTokensResponse | undefined = undefined;
try {
tokens = await getTeamTokens({ userId: user.id, teamId: team.id });
@@ -63,7 +63,7 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
<hr className="my-4" />
<ApiTokenForm className="max-w-xl" teamId={team.id} />
<ApiTokenForm className="max-w-xl" teamId={team.id} tokens={tokens} />
<hr className="mb-4 mt-8" />

View File

@@ -1,14 +1,16 @@
'use client';
import { useState } from 'react';
import { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import type { ApiToken } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import type { TCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
@@ -44,23 +46,37 @@ const ZCreateTokenFormSchema = ZCreateTokenMutationSchema.extend({
type TCreateTokenFormSchema = z.infer<typeof ZCreateTokenFormSchema>;
type NewlyCreatedToken = {
id: number;
token: string;
};
export type ApiTokenFormProps = {
className?: string;
teamId?: number;
tokens?: Pick<ApiToken, 'id'>[];
};
export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) => {
const router = useRouter();
const [isTransitionPending, startTransition] = useTransition();
const [, copy] = useCopyToClipboard();
const { toast } = useToast();
const [newlyCreatedToken, setNewlyCreatedToken] = useState('');
const [newlyCreatedToken, setNewlyCreatedToken] = useState<NewlyCreatedToken | null>();
const [noExpirationDate, setNoExpirationDate] = useState(false);
// This lets us hide the token from being copied if it has been deleted without
// resorting to a useEffect or any other fanciness. This comes at the cost of it
// taking slighly longer to appear since it will need to wait for the router.refresh()
// to finish updating.
const hasNewlyCreatedToken =
tokens?.find((token) => token.id === newlyCreatedToken?.id) !== undefined;
const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({
onSuccess(data) {
setNewlyCreatedToken(data.token);
setNewlyCreatedToken(data);
},
});
@@ -110,7 +126,7 @@ export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
form.reset();
router.refresh();
startTransition(() => router.refresh());
} catch (error) {
if (error instanceof TRPCClientError && error.data?.code === 'BAD_REQUEST') {
toast({
@@ -216,7 +232,7 @@ export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
type="submit"
className="hidden md:inline-flex"
disabled={!form.formState.isDirty}
loading={form.formState.isSubmitting}
loading={form.formState.isSubmitting || isTransitionPending}
>
Create token
</Button>
@@ -225,7 +241,7 @@ export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
<Button
type="submit"
disabled={!form.formState.isDirty}
loading={form.formState.isSubmitting}
loading={form.formState.isSubmitting || isTransitionPending}
>
Create token
</Button>
@@ -234,24 +250,33 @@ export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
</form>
</Form>
{newlyCreatedToken && (
<Card className="mt-8" gradient>
<CardContent className="p-4">
<p className="text-muted-foreground mt-2 text-sm">
Your token was created successfully! Make sure to copy it because you won't be able to
see it again!
</p>
<AnimatePresence initial={!hasNewlyCreatedToken}>
{newlyCreatedToken && hasNewlyCreatedToken && (
<motion.div
className="mt-8"
initial={{ opacity: 0, y: -40 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 40 }}
>
<Card gradient>
<CardContent className="p-4">
<p className="text-muted-foreground mt-2 text-sm">
Your token was created successfully! Make sure to copy it because you won't be
able to see it again!
</p>
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
{newlyCreatedToken}
</p>
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
{newlyCreatedToken.token}
</p>
<Button variant="outline" onClick={() => void copyToken(newlyCreatedToken)}>
Copy token
</Button>
</CardContent>
</Card>
)}
<Button variant="outline" onClick={() => void copyToken(newlyCreatedToken.token)}>
Copy token
</Button>
</CardContent>
</Card>
</motion.div>
)}
</AnimatePresence>
</div>
);
};

View File

@@ -211,7 +211,7 @@ export const SignUpFormV2 = ({
<div className="relative flex h-full w-full flex-col items-center justify-evenly">
<div className="bg-background rounded-2xl border px-4 py-1 text-sm font-medium">
User profiles are coming soon!
User profiles are here!
</div>
<AnimatePresence>

View File

@@ -53,6 +53,89 @@
--signer-orange: 36 92% 54%;
--signer-yellow: 51 100% 43%;
--signer-pink: 313 65% 57%;
/* Base - Neutral */
--new-neutral-50: 0, 0%, 96%;
--new-neutral-100: 0, 0%, 91%;
--new-neutral-200: 0, 0%, 82%;
--new-neutral-300: 0, 0%, 69%;
--new-neutral-400: 0, 0%, 53%;
--new-neutral-500: 0, 0%, 43%;
--new-neutral-600: 0, 0%, 36%;
--new-neutral-700: 0, 0%, 31%;
--new-neutral-800: 0, 0%, 27%;
--new-neutral-900: 0, 0%, 24%;
--new-neutral-950: 0, 0%, 9%;
/* Base - White */
--new-white-50: 0, 0%, 5%;
--new-white-60: 0, 0%, 6%;
--new-white-100: 0, 0%, 10%;
--new-white-200: 0, 0%, 20%;
--new-white-300: 0, 0%, 30%;
--new-white-400: 0, 0%, 40%;
--new-white-500: 0, 0%, 50%;
--new-white-600: 0, 0%, 60%;
--new-white-700: 0, 0%, 80%;
--new-white-800: 0, 0%, 90%;
--new-white-900: 0, 0%, 100%;
/* Primary - Green */
--new-primary-50: 98, 73%, 97%;
--new-primary-100: 95, 73%, 94%;
--new-primary-200: 94, 70%, 87%;
--new-primary-300: 95, 71%, 81%;
--new-primary-400: 95, 71%, 74%;
--new-primary-500: 95, 71%, 67%;
--new-primary-600: 95, 71%, 54%;
--new-primary-700: 95, 71%, 41%;
--new-primary-800: 95, 71%, 27%;
--new-primary-900: 95, 72%, 14%;
--new-primary-950: 95, 72%, 7%;
/* Secondary - Info */
--new-info-50: 210, 54%, 95%;
--new-info-100: 211, 57%, 90%;
--new-info-200: 212, 55%, 80%;
--new-info-300: 212, 56%, 70%;
--new-info-400: 212, 56%, 60%;
--new-info-500: 212, 56%, 50%;
--new-info-600: 212, 56%, 40%;
--new-info-700: 212, 56%, 30%;
--new-info-800: 212, 55%, 20%;
--new-info-900: 211, 57%, 10%;
--new-info-950: 214, 54%, 5%;
/* Secondary - Error */
--new-error-50: 4, 80%, 96%;
--new-error-100: 3, 78%, 91%;
--new-error-200: 3, 79%, 83%;
--new-error-300: 3, 79%, 74%;
--new-error-400: 3, 79%, 66%;
--new-error-500: 4, 79%, 57%;
--new-error-600: 3, 79%, 46%;
--new-error-700: 3, 79%, 34%;
--new-error-800: 3, 79%, 23%;
--new-error-900: 3, 79%, 11%;
--new-error-950: 3, 80%, 6%;
/* Secondary - Warning */
--new-warning-50: 39, 100%, 96%;
--new-warning-100: 40, 100%, 93%;
--new-warning-200: 39, 100%, 86%;
--new-warning-300: 39, 100%, 79%;
--new-warning-400: 39, 100%, 71%;
--new-warning-500: 39, 100%, 64%;
--new-warning-600: 39, 100%, 57%;
--new-warning-700: 39, 100%, 43%;
--new-warning-800: 39, 100%, 29%;
--new-warning-900: 39, 100%, 14%;
--new-warning-950: 38, 100%, 7%;
/* Surface */
--new-surface-black: 0, 0%, 0%;
--new-surface-white: 0, 0%, 91%;
}
.dark {