Overview
An admin creates a Document Template, uploads a PDF or Word file, maps each form field in that file to a Salesforce field, and defines who signs it. Nothing here generates or sends a document — it produces the configuration records the runtime engine later consumes.
Three records hold that configuration:
|
Object |
Holds |
|---|---|
|
|
Template header: source object, file reference, eSign/OTP/reminder/vaulting settings, status |
|
|
One row per document form field → Salesforce field binding |
|
|
One row per signer: sequence, name/email field paths, SOQL query, assigned signature fields |
Supporting metadata:
|
Type |
Purpose |
|---|---|
|
|
Named child-object mappings for PDF templates; its |
|
|
Org-level licence flags: |
Architecture
┌───────────────────────────────────────────────────────────────────┐
│ Lightning App / Record Page │
│ │
│ createDocumentTemplate ──► documentTemplateDetail │
│ (4-step wizard) (edit + activate) │
│ │ │
│ cloneDocumentTemplate (quick action) │
│ │ │
│ documentTemplateMapping (mapping workspace) │
│ ┌──────────────────┬───────────────────────────────┐ │
│ │ VF iframe │ Tab: Mapping │ │
│ │ (PSPDFKit │ └ documentMappingField (×N) │ │
│ │ viewer) │ Tab: Signers │ │
│ │ ▲ │postMessage│ └ documentSigners │ │
│ │ │ ▼ │ └ documentSignerIndividual│ │
│ └──────────────────┴──────────└ documentSignerConfiguration
└───────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
PSPDFKitController AdminConsoleController DocumentMappingScreenController
│ │ │
└────────────────────┴────────────────────────┘
▼
Data (DML wrapper) · UtilityClass · DescribeCache
▼
DocumentTemplateTriggerHandler (before insert/update)
Key mechanism — the iframe bridge. The document is rendered by PSPDFKit inside a Visualforce page loaded in an <iframe>. LWC and VF cannot call each other directly, so they communicate via window.postMessage. The LWC listens; the VF page posts. See §7.
Apex — AdminConsoleController
public with sharing. Every SOQL/DML call goes through the Data wrapper, passing enforceSharing (from UtilityClass.getSharingSettings()) for the sharing / FLS / CRUD flags. Most write methods return a JSON-serialised UtilityClass.StatusWrapper (isSuccessful, message, recordId, recordDetails) — the caller must JSON.parse the result.
3.1 Read methods
|
Method |
Returns |
What it does |
|---|---|---|
|
|
|
All templates with the full settings field set, ordered |
|
|
|
Templates whose |
|
|
|
Global describe, keyed |
|
|
|
Source for the sender-address picklist. |
|
|
|
|
|
|
|
|
|
|
|
Delegates to |
3.2 Write methods
createTemplate(String sTemplate, Id contentDocumentId)
-
Rejects blank JSON or blank
contentDocumentId. -
Queries
ContentVersionfor the suppliedContentDocumentId. -
Deserialises
sTemplateinto aDocument_Template__c. -
Force-sets on the new record:
-
Content_Version_Id__c= the queried ContentVersion Id -
Update_Salesforce_data_on_signing__c=false -
Signing_Link_Expiry_Days__c=30 -
Status__c=ActiveifUtilityClass.activateTemplateOnCreation()elseInActive
-
-
Inserts the template, then inserts a
ContentDocumentLink(ShareType = 'V') joining the file to it.
Warning: step 2 uses
lstCv[0]with no bounds check. An empty result throws and surfaces as a generic failure wrapper.
editTemplate(String sTemplate) — deserialise → Data.modify → re-query the full field set → return it in recordDetails so the LWC can refresh without a second round trip. Used both for the full form save and for the Activate/Deactivate toggle, which sends only {Status__c, Id}.
deleteTemplate(Id recordId) — Data.remove on a single-element list.
updateContentVersionId(Id templateId, Id contentVersionId) — repoints a template at a new file version after re-upload. Throws AuraHandledException on blank input; it is the only write method here that throws rather than returning a wrapper.
3.3 Cloning
cloneTemplate(Id templateId, Map<String,Object> cloningConfigurations) → Id
Config keys, all supplied by cloneDocumentTemplate.js:
|
Key |
Effect |
|---|---|
|
|
Becomes |
|
|
Copy |
|
|
Copy |
|
|
Duplicate the attached file |
|
|
|
Flow: query the source template with an explicit ~25-field list → construct a new record copying those fields → Data.create → conditionally call the three private helpers → return the new Id.
Note:
Content_Version_Id__cis deliberately not copied during construction. It is set later bycloneDocumentFile, and only whencopyDocumentis on. A clone created withcopyDocument = falsetherefore has no file and will fail the mapping screen's file check (§4.1).
|
Private helper |
Behaviour |
|---|---|
|
|
Describes |
|
|
Identical pattern for |
|
|
Returns early if the source ContentVersion Id is blank. Reads it, inserts a new ContentVersion (creating a new ContentDocument), re-queries to obtain the new |
Warning — known defects here:
cloneDataFieldMappingsbuilds acreatableFieldslist that is never used;cloneDocumentFilenever uses itsoldTemplateIdparameter;cloneTemplatehas no failure handling, so a partial clone (template created, children not) is possible.
Apex — DocumentMappingScreenController
public with sharing. Same Data wrapper and enforceSharing pattern.
4.1 getScreenConfigurations(String sTemplateId)
The single bootstrap call for the mapping workspace. Returns bSuccess = false plus erroMessage (sic) on any of four validation failures:
|
Check |
Message shown |
|---|---|
|
Blank template Id |
"The provided Template Id is null or blank…" |
|
Template not found |
same message |
|
No file attached ( |
"This document template does not have a document file attached…" |
|
No |
"The template is missing a primary object…" |
On success:
|
Key |
Value |
|---|---|
|
|
|
|
|
|
|
|
Existing |
|
|
|
|
|
Used to build the VF iframe URL |
|
|
PDF: |
Warning:
getTemplateFile()base64-encodes the entireVersionDatablob purely to test whether a file exists — and the encoded string is then discarded, never added to the response. On a 2.5 MB file this burns heap for nothing. Replace with an Id-only query.
4.2 Describe and lookup helpers
|
Method |
Notes |
|---|---|
|
|
Returns one |
|
|
List form of the object picker (label/value only). Duplicates |
|
|
Describe → |
|
|
Describe → |
|
|
Sets |
|
|
Queries the data-mapping children. |
4.3 getChildMappingConfig(String templateId)
Queries all Child_Object_Mapping_Configurations__mdt rows, then filters in Apex: splits each row's Document_Template__c on ; and keeps rows whose list contains templateId. Filtering happens in memory because the field is a delimited text blob, not a relationship.
4.4 getSignerMappings(Id templateId)
Returns { Success: true, signers: [...] } ordered by Signer_Sequence__c ASC. Selected fields: Signer_Sequence__c, Template_Field_Name__c, Signer_Email_Field_API_Name__c, Signer_Name_Field_API_Name__c, Signer_Query__c.
4.5 upsertDocumentMapping(sFieldConfig, sObjectName, refrenceObjectName)
The most intricate method in the codebase. It takes the JSON of one Document_Data_Mapping__c, normalises the field path, derives the data type, and upserts.
Step 1 — the Today shortcut. If Salesforce_Field_API_Name__c equals Today (case-insensitive): set data type DATE, label Today's Date, Read_Only_On_Signing__c = true, upsert, return immediately.
Step 2 — dot-notation resolution (only when the API name contains . and Loop_Over_Child_Records__c is false). Splits on the first dot.
Child-mapping branch (Is_Child_Mapping__c = true):
-
Two segments (
Child.Field) → resolve againstrefrenceObjectName. -
Three segments (
Child.Lookup.Field) → resolve the intermediate object viaUtilityClass.getReferencedObject(), convert the lookup to relationship form (__c→__r,OwnerId→Owner) and rebuild the path.
Parent-lookup branch: the left segment carries a composite key shaped ObjectName::LookupFieldApiName, produced by documentMappingField.handleParentFieldChange. The method splits on ::, converts the lookup field to relationship form, and rewrites the API name as Lookup__r.Field. Without :: the object cannot be resolved and sObjectName becomes null, so the data type falls back to TEXT.
Step 3 — data type derivation. Calls getFieldType() unless the incoming type is already IMAGE or CONDITIONAL — those are user-declared markers set in the config modal and must survive.
Step 4 — writeability guard. If isFieldWriteable() is false, force Read_Only_On_Signing__c = true and Required_On_Signing__c = false. This is the safety net preventing a signer being asked to edit a formula or rollup field.
Step 5 — defaults.
|
Condition |
Default applied |
|---|---|
|
Blank API name |
|
|
|
|
|
Blank |
Falls back to |
|
Data type is |
|
Returns { bSuccess, recordId } or { bSuccess: false, errorMessage }.
4.6 upsurtSigner (List<String> lstSigner)
Accepts a list of JSON strings, each a serialised Document_Signer_Mapping__c. Deserialises each, bulk-upserts, returns a StatusWrapper JSON. Called from two places with different intent:
-
documentSigners.js— one signer record (create or edit). -
documentMappingField.js— one record per signer, rewritingTemplate_Field_Name__cso signature-field assignments persist.
Apex — PSPDFKitController
global with sharing. Backs the Visualforce page that hosts the PSPDFKit viewer inside the iframe. Its public properties are read by the VF markup; it is not a conventional LWC controller.
|
Member |
Type |
Purpose |
|---|---|---|
|
|
VF getter |
Primary entry point. Reads page params |
|
|
VF getter |
Alternative path keyed on page param |
|
|
|
Returns |
|
|
|
Reads |
All page parameters pass through escapeHtml4() and all queries go through Data.read with bind maps.
Apex — DocumentTemplateTriggerHandler
Single method, performSystemValidations(List<Document_Template__c>), intended for a before insert / before update context (it uses addError).
Fetches Features_Entitlement__c once for the batch, then per record:
|
Record state |
Action |
|---|---|
|
|
Force |
|
…and org lacks |
|
|
…and org lacks |
|
|
|
|
This is the server-side enforcement of the same rule the UI applies optimistically in documentTemplateDetail.handleChange.
The LWC ↔ Visualforce message contract
documentTemplateMapping registers window.addEventListener("message", …) in connectedCallback and builds the iframe URL as:
/apex/{pspdfKit_FramePageName}?context=Mapping&sFileId={contentVersionId}&sTemplateId={recordId}
Both pspdfKit_FramePageName and pspdfKit_VF_Origin come from custom labels exposed by the pSPDFKitLWC_Base class the component extends.
|
Message |
Direction |
Payload |
Handling |
|---|---|---|---|
|
|
VF → LWC |
Array of form-field descriptors found in the document |
|
|
|
VF → LWC |
Field name the user clicked in the viewer |
Scrolls the matching |
|
|
VF → LWC |
Base64 of the edited PDF |
Handled only in the dead |
LWC reference
8.1 createDocumentTemplate — creation wizard
Four-step wizard; currentStep is a string '1'–'4'.
|
Step |
Screen |
Getter |
|---|---|---|
|
1 |
Template details (name, action, source object, sender, folder, email templates, description) |
|
|
2 |
File upload |
|
|
3 |
OTP configuration |
|
|
4 |
Reminder configuration |
|
cardHeader and helpText are switch getters keyed on the step number.
Wires: getObjectInfo on Document_Template__c; getPicklistValues for Template_Group_Folder__c and Template_Action__c; getOrgWideEmails; getAvailableObjects.
|
Function |
Behaviour |
|---|---|
|
|
|
|
|
|
|
|
Routes by |
|
|
Captures |
|
|
Steps 1–3: |
|
|
|
|
|
Apex call; sets |
|
|
Keydown and paste filters for the numeric OTP/reminder inputs |
The flatten rule (repeated in documentTemplateDetail): a document going out for signature must keep its form fields interactive, so flattening is disabled. A generate-only document is flattened.
8.2 documentTemplateDetail — record-page editor
Accordion sections: templateDetails, otpDetails, advancedConfigurations, reminderDetails.
|
Member |
Behaviour |
|---|---|
|
|
Supplied by the parent record page component |
|
|
Sets |
|
|
|
|
|
|
|
|
Reads |
|
|
Checkbox |
|
|
|
|
|
Calls Apex, parses the JSON wrapper, fires an |
|
|
Normalises array-body vs |
8.3 cloneDocumentTemplate — quick action
Record quick action; extends NavigationMixin.
|
Member |
Behaviour |
|---|---|
|
|
Setter fires |
|
|
|
|
|
|
|
|
One per toggle; each writes a key on |
|
|
Guards on missing record Id and empty name → |
8.4 documentTemplateMapping — mapping workspace (parent)
Extends pSPDFKitLWC_Base. Layout is 8/12 iframe + 4/12 tabset (Mapping, Signers).
|
Function |
Behaviour |
|---|---|
|
|
Registers the pre-bound message listener, then |
|
|
Destructures the Apex response. Sets |
|
|
See §7 |
|
|
For each form field from the document, finds the matching |
|
|
Builds three structures from the signer records: |
|
|
Adds a newly saved loop field to |
|
|
Removes by matching both label and value, deletes the Id from the set, re-pushes to children |
|
|
Replaces |
8.5 documentMappingField — one field row + config modal
The busiest component. One instance per form field found in the document.
Public API
|
Member |
Purpose |
|---|---|
|
|
The field descriptor — copied to |
|
|
Context from the parent |
|
|
Freezes the dropdown list to avoid reactivity churn across hundreds of rows |
|
|
Called by the parent when the user clicks a field in the PDF viewer |
|
|
Refreshes |
Getters
|
Getter |
Returns |
|---|---|
|
|
Signature fields: is this label in |
|
|
|
|
|
|
|
|
Builds the status badge. Sequential overwrite, not accumulation — later matches replace earlier ones, so priority runs read-only → required-on-signing → required-to-generate → loop → signature → image → conditional → signing date, and only the last match renders. |
|
|
Whether to render the "mapped to" summary |
|
|
|
Selection handlers
|
Handler |
Behaviour |
|---|---|
|
|
If the chosen field is in |
|
|
Builds the composite path |
|
|
Child-object selection. PDF: resolve the child sObject from the custom metadata row. Word: the selected value is the object name. Then load that object's fields. |
|
|
Assemble two- and three-segment child paths |
|
|
Switch on |
Save and delete
handleMappingSaveAction():
-
Signature field →
upsurtSigner(createSignerConfig())and stop. -
validateForm()over.validSec1elements. -
Reject read-only and required-on-signing together.
-
Loop branch: set
Loop_Over_Child_Records__c, build the API name aschild.field, use the user's loop label, force typeTEXT, reject a duplicate loop label for the same sObject, and pull the WHERE clause from the nestedc-dynamic-query-creation-l-w-cwhen enabled. -
Stamp
Document_Template__candDocument_Field_Name__c, callupsertDocumentMapping, and on success emitupdateif this is a loop field.
deleteMapping() — signature fields delete from fieldSignerMapping and re-save all signers; other fields call deleteRecord(), reset every local variable, and emit loopdelete when applicable.
createSignerConfig() — inverts fieldSignerMapping into per-signer groups and returns an array of JSON strings shaped {Id, Template_Field_Name__c: "A;B;C"} — exactly what upsurtSigner expects.
parseFieldReference() — the reverse of the save logic, run when opening an existing mapping for edit. Splits the stored API name back into loop / lookup / child-lookup form, converts __r back to __c (and bare relationship names back to …Id), and reloads the dependent dropdowns.
8.6 documentSigners — signer list and SOQL builder
Renders the signer cards and hosts the add/edit modal containing a visual SOQL builder.
Wires
|
Wire |
Purpose |
|---|---|
|
|
Object picker |
|
|
Reactive — refires whenever the selected object changes. Contains the branching that rebuilds or repopulates WHERE state (add mode vs edit mode vs object-changed-in-edit-mode). |
|
|
Establishes |
Query construction
generatedQuery (getter) assembles:
SELECT Id, {emailField}, {nameField} FROM {objectApiName}
[WHERE cond1 AND cond2 …]
[ORDER BY {field} {ASC|DESC}]
[LIMIT n [OFFSET m]]
Value formatting is type-aware: numerics unquoted; booleans normalised to true/false; dates quoted unless they start with TODAY; everything else single-quoted with embedded quotes escaped. The literal ParentId is emitted unquoted — it is a runtime placeholder the generation engine substitutes with the actual record Id. If the admin hand-edits the query, queryEdited flips and the getter returns their text verbatim.
expressionOptionsByType maps each Salesforce data type to its legal operators: equality only for text/picklist/reference/id/email/phone/url; comparisons for numerics and dates; INCLUDES/EXCLUDES for multipicklist.
Edit-mode round trip — the subtlest flow in the component:
-
handleEditcaptures the signer, regex-extracts the object fromFROM\s+(\w+), and stores the record in_pendingSignerEdit. -
If the object differs from the current one, setting
objectApiNamerefires thegetsObjectFieldswire. -
When the wire returns,
repopulateFieldsAfterWire()runs — it needssObjectFieldspresent to normalise field API-name casing. -
Name and email fields are resolved: dot-notation paths are split, the relationship name is reversed to a lookup API name (
Account→AccountId,Obj__r→Obj__c), the parent object is found viareferenceTo, its fields are fetched, and the dot-notation is rebuilt canonically. -
parseQueryIntoState()regex-extractsORDER BY,LIMIT,OFFSETand each WHERE condition back intowhereConditions. The first row is locked (disableNamewhen the field isId,disableOperatoralways) to protect the parent-record binding.
|
Other function |
Behaviour |
|---|---|
|
|
|
|
|
If the chosen field is a |
|
|
|
|
|
Case-correct a field API name against a known field list |
|
|
Validates required fields, lookup sub-selections, and a fully-populated first WHERE row; then |
|
|
Reloads and labels signers |
|
|
Returns |
8.7 documentSignerIndividual — one signer card
|
Member |
Behaviour |
|---|---|
|
|
Set by the parent |
|
|
|
|
|
Runs once (guarded by |
|
Record display |
|
|
|
Emits |
|
|
Confirmation modal → |
|
|
Sets |
8.8 documentSignerConfiguration — read-only / mask field picker
A dual-listbox modal, reused for two purposes via the method property. Backed by DocumentSigningUtility, not by the controllers above.
|
Member |
Behaviour |
|---|---|
|
|
Inputs |
|
|
Returns the explanatory sentence for the active mode |
|
|
Calls |
|
|
Mask mode: every selected field is added to both the masked list and the read-only set — masking implies read-only. Read-only mode: the selection becomes the read-only set and |
|
|
Calls |
Warning: in read-only mode
maskedis set tonull, which wipes any existing masked-field configuration. This interaction needs a product decision before release.
Cross-cutting conventions
|
Convention |
Detail |
|---|---|
|
Delimited storage |
|
|
PDF vs Word |
|
|
Field-path encoding |
|
|
Error surfacing |
Every LWC has a near-identical |