Add document approval workflow
Introduce a complete approval system for document publishing. Document versions can now require approval from selected reviewers before being published, with automatic publishing once all approvers have approved. - Add approval quorum and decision tables with backfill migration - Implement request approval, approve, and reject flows with electronic signature support for approve decisions - Add employee approvals page with dedicated tab and pending approvals view - Add changelog field to publish and request approval flows - Pre-select previous version's approvers in the publish dialog - Show quorum approvers in document list with 100 approver hard limit - Expose approval workflow through GraphQL, MCP, and CLI - Remove legacy default approvers feature entirely - Add comprehensive e2e test coverage for approval workflows Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -212,6 +212,7 @@ const (
|
||||
subjectConfirmEmail = "Confirm your email address"
|
||||
subjectPasswordReset = "Reset your password"
|
||||
subjectInvitation = "Invitation to join %s on Probo"
|
||||
subjectDocumentApproval = "Action Required – Please review and approve %s"
|
||||
subjectDocumentSigning = "Action Required – Please review and sign %s compliance documents"
|
||||
subjectDocumentExport = "Your document export is ready"
|
||||
subjectFrameworkExport = "Your framework export is ready"
|
||||
@@ -231,6 +232,8 @@ var (
|
||||
passwordResetTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/password-reset.txt.tmpl"))
|
||||
invitationHTMLTemplate = htmltemplate.Must(htmltemplate.ParseFS(Templates, "dist/invitation.html.tmpl"))
|
||||
invitationTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/invitation.txt.tmpl"))
|
||||
documentApprovalHTMLTemplate = htmltemplate.Must(htmltemplate.ParseFS(Templates, "dist/document-approval.html.tmpl"))
|
||||
documentApprovalTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/document-approval.txt.tmpl"))
|
||||
documentSigningHTMLTemplate = htmltemplate.Must(htmltemplate.ParseFS(Templates, "dist/document-signing.html.tmpl"))
|
||||
documentSigningTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/document-signing.txt.tmpl"))
|
||||
documentExportHTMLTemplate = htmltemplate.Must(htmltemplate.ParseFS(Templates, "dist/document-export.html.tmpl"))
|
||||
@@ -348,6 +351,39 @@ func (p *Presenter) RenderInvitation(ctx context.Context, invitationURLPath stri
|
||||
return fmt.Sprintf(subjectInvitation, organizationName), textBody, htmlBody, err
|
||||
}
|
||||
|
||||
func (p *Presenter) RenderDocumentApproval(
|
||||
ctx context.Context,
|
||||
approvalURLPath string,
|
||||
approvalURLQuery url.Values,
|
||||
organizationName string,
|
||||
documentName string,
|
||||
) (subject string, textBody string, htmlBody *string, err error) {
|
||||
vars, err := p.getCommonVariables(ctx)
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("cannot get common variables: %w", err)
|
||||
}
|
||||
|
||||
approvalURL := baseurl.MustParse(vars.BaseURL).
|
||||
AppendPath(approvalURLPath).
|
||||
WithQueryValues(approvalURLQuery).
|
||||
MustString()
|
||||
|
||||
data := struct {
|
||||
*CommonVariables
|
||||
ApprovalUrl string
|
||||
OrganizationName string
|
||||
DocumentName string
|
||||
}{
|
||||
CommonVariables: vars,
|
||||
ApprovalUrl: approvalURL,
|
||||
OrganizationName: organizationName,
|
||||
DocumentName: documentName,
|
||||
}
|
||||
|
||||
textBody, htmlBody, err = renderEmail(documentApprovalTextTemplate, documentApprovalHTMLTemplate, data)
|
||||
return fmt.Sprintf(subjectDocumentApproval, documentName), textBody, htmlBody, err
|
||||
}
|
||||
|
||||
func (p *Presenter) RenderDocumentSigning(
|
||||
ctx context.Context,
|
||||
signingURLPath string,
|
||||
@@ -478,7 +514,7 @@ func (p *Presenter) RenderMagicLink(ctx context.Context, magicLinkUrlPath string
|
||||
return fmt.Sprintf(subjectMagicLink, organizationName), textBody, htmlBody, err
|
||||
}
|
||||
|
||||
func (p *Presenter) RenderElectronicSignatureCertificate(ctx context.Context, signerName string, documentType string) (subject string, textBody string, htmlBody *string, err error) {
|
||||
func (p *Presenter) RenderElectronicSignatureCertificate(ctx context.Context, signerName string, documentName string) (subject string, textBody string, htmlBody *string, err error) {
|
||||
vars, err := p.getCommonVariables(ctx)
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("cannot get common variables: %w", err)
|
||||
@@ -487,15 +523,15 @@ func (p *Presenter) RenderElectronicSignatureCertificate(ctx context.Context, si
|
||||
data := struct {
|
||||
*CommonVariables
|
||||
SignerName string
|
||||
DocumentType string
|
||||
DocumentName string
|
||||
}{
|
||||
CommonVariables: vars,
|
||||
SignerName: signerName,
|
||||
DocumentType: documentType,
|
||||
DocumentName: documentName,
|
||||
}
|
||||
|
||||
textBody, htmlBody, err = renderEmail(electronicSignatureCertificateTextTemplate, electronicSignatureCertificateHTMLTemplate, data)
|
||||
return fmt.Sprintf(subjectElectronicSignatureCertificate, documentType), textBody, htmlBody, err
|
||||
return fmt.Sprintf(subjectElectronicSignatureCertificate, documentName), textBody, htmlBody, err
|
||||
}
|
||||
|
||||
func (p *Presenter) RenderMailingListSubscription(ctx context.Context, organizationName string, confirmURL string, unsubscribeURL string) (subject string, textBody string, htmlBody *string, err error) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as React from "react";
|
||||
import MailingListUpdates from "../src/MailingListUpdates";
|
||||
import ConfirmEmail from "../src/ConfirmEmail";
|
||||
import DocumentExport from "../src/DocumentExport";
|
||||
import DocumentApproval from "../src/DocumentApproval";
|
||||
import DocumentSigning from "../src/DocumentSigning";
|
||||
import FrameworkExport from "../src/FrameworkExport";
|
||||
import Invitation from "../src/Invitation";
|
||||
@@ -39,6 +40,10 @@ const templates: TemplateConfig[] = [
|
||||
name: "invitation",
|
||||
render: () => Invitation(),
|
||||
},
|
||||
{
|
||||
name: "document-approval",
|
||||
render: () => DocumentApproval(),
|
||||
},
|
||||
{
|
||||
name: "document-signing",
|
||||
render: () => DocumentSigning(),
|
||||
|
||||
36
packages/emails/src/DocumentApproval.tsx
Normal file
36
packages/emails/src/DocumentApproval.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Button, Section, Text } from '@react-email/components';
|
||||
import * as React from 'react';
|
||||
import EmailLayout, { bodyText, button, buttonContainer, footerText } from './components/EmailLayout';
|
||||
|
||||
export const DocumentApproval = () => {
|
||||
return (
|
||||
<EmailLayout subject={'Action Required – Please review and approve {{.DocumentName}}'}>
|
||||
<Text style={bodyText}>
|
||||
You're receiving this message because <strong>{'{{.OrganizationName}}'}</strong> has requested your approval on the document <strong>{'{{.DocumentName}}'}</strong>.
|
||||
</Text>
|
||||
<Text style={bodyText}>
|
||||
Please take a moment to review the document and approve or reject it by clicking the button below:
|
||||
</Text>
|
||||
|
||||
<Section style={buttonContainer}>
|
||||
<Button style={button} href={'{{.ApprovalUrl}}'}>
|
||||
Review and Approve
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
<Text style={bodyText}>
|
||||
If you have any questions, please contact your security team.
|
||||
</Text>
|
||||
|
||||
<Text style={footerText}>
|
||||
This process is managed securely by Probo, acting as the compliance partner on behalf of <strong>{'{{.OrganizationName}}'}</strong>.
|
||||
Thank you for your prompt attention to this matter.
|
||||
</Text>
|
||||
<Text style={footerText}>
|
||||
Best regards,
|
||||
</Text>
|
||||
</EmailLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentApproval;
|
||||
@@ -8,10 +8,10 @@ import EmailLayout, {
|
||||
export const ElectronicSignatureCertificate = () => {
|
||||
return (
|
||||
<EmailLayout
|
||||
subject={`Your signed ${"{{.DocumentType}}"} — Certificate of Completion`}
|
||||
subject={`Your signed ${"{{.DocumentName}}"} — Certificate of Completion`}
|
||||
>
|
||||
<Text style={bodyText}>
|
||||
Your <strong>{"{{.DocumentType}}"}</strong> has been signed
|
||||
Your <strong>{"{{.DocumentName}}"}</strong> has been signed
|
||||
electronically. A Certificate of Completion is attached to this email
|
||||
as a PDF document.
|
||||
</Text>
|
||||
|
||||
20
packages/emails/templates/document-approval.txt
Normal file
20
packages/emails/templates/document-approval.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
Probo
|
||||
|
||||
Hi {{.RecipientFullName}},
|
||||
|
||||
You're receiving this message because {{.OrganizationName}} has requested your approval on the document {{.DocumentName}}.
|
||||
|
||||
Please take a moment to review the document and approve or reject it by clicking the link below:
|
||||
|
||||
{{.ApprovalUrl}}
|
||||
|
||||
If you have any questions, please contact your security team.
|
||||
|
||||
This process is managed securely by Probo, acting as the compliance partner on behalf of {{.OrganizationName}}.
|
||||
|
||||
Thank you for your prompt attention to this matter.
|
||||
|
||||
Best regards,
|
||||
|
||||
{{.SenderCompanyHeadquarterAddress}}
|
||||
Powered By Probo
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Hi {{.RecipientFullName}},
|
||||
|
||||
Your {{.DocumentType}} has been signed electronically. A Certificate of Completion is attached to this email as a PDF document.
|
||||
Your {{.DocumentName}} has been signed electronically. A Certificate of Completion is attached to this email as a PDF document.
|
||||
|
||||
The certificate contains a complete record of the signing event, including the integrity seal, timestamp, and audit trail.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user