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) {
|
||||
|
||||
Reference in New Issue
Block a user