Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<Layout xmlns="http://soap.sforce.com/2006/04/metadata">
<excludeButtons>AssignRecordLabel</excludeButtons>
<excludeButtons>ChangeOwnerOne</excludeButtons>
<excludeButtons>ChangeRecordType</excludeButtons>
<excludeButtons>Clone</excludeButtons>
<excludeButtons>OmniChannelRouteWork</excludeButtons>
<excludeButtons>PrintableView</excludeButtons>
<excludeButtons>RecordShareHierarchy</excludeButtons>
<excludeButtons>Share</excludeButtons>
<excludeButtons>Submit</excludeButtons>
<layoutSections>
<customLabel>true</customLabel>
<detailHeading>true</detailHeading>
Expand Down Expand Up @@ -28,10 +37,6 @@
<behavior>Edit</behavior>
<field>Priority__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>RequestedBy__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>OwnerId</field>
Expand Down Expand Up @@ -82,45 +87,45 @@
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>CorrelationId__c</field>
<field>RequestJSON__c</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>RequestJSON__c</field>
<field>RequestJSON03__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>RequestJSON02__c</field>
<field>RequestJSON05__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>RequestJSON03__c</field>
<field>RequestJSON07__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>RequestJSON04__c</field>
<field>RequestJSON09__c</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>RequestJSON05__c</field>
<field>CorrelationId__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>RequestJSON06__c</field>
<field>RequestJSON02__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>RequestJSON07__c</field>
<field>RequestJSON04__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>RequestJSON08__c</field>
<field>RequestJSON06__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>RequestJSON09__c</field>
<field>RequestJSON08__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
Expand Down Expand Up @@ -193,13 +198,20 @@
<layoutColumns>
<layoutItems>
<behavior>Readonly</behavior>
<field>CreatedById</field>
<field>RequestedBy__c</field>
</layoutItems>
<layoutItems>
<behavior>Readonly</behavior>
<field>Created_Date__c</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
<emptySpace>true</emptySpace>
</layoutItems>
<layoutItems>
<behavior>Readonly</behavior>
<field>LastModifiedById</field>
<field>Last_Modified_Date__c</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsTopToBottom</style>
Expand All @@ -220,6 +232,19 @@
<fields>OutputFormat__c</fields>
<fields>Template__c</fields>
</miniLayout>
<platformActionList>
<actionListContext>Record</actionListContext>
<platformActionListItems>
<actionName>Edit</actionName>
<actionType>StandardButton</actionType>
<sortOrder>0</sortOrder>
</platformActionListItems>
<platformActionListItems>
<actionName>Delete</actionName>
<actionType>StandardButton</actionType>
<sortOrder>1</sortOrder>
</platformActionListItems>
</platformActionList>
<relatedLists>
<relatedList>RelatedFileList</relatedList>
</relatedLists>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@
<label>Template Name</label>
<type>Text</type>
</nameField>
<actionOverrides>
<actionName>View</actionName>
<comment>Action override created by Lightning App Builder during activation.</comment>
<content>Docgen_Template_Record_Page</content>
<formFactor>Small</formFactor>
<skipRecordTypeSelect>false</skipRecordTypeSelect>
<type>Flexipage</type>
</actionOverrides>
<actionOverrides>
<actionName>View</actionName>
<comment>Action override created by Lightning App Builder during activation.</comment>
<content>Docgen_Template_Record_Page</content>
<formFactor>Large</formFactor>
<skipRecordTypeSelect>false</skipRecordTypeSelect>
<type>Flexipage</type>
</actionOverrides>
<deploymentStatus>Deployed</deploymentStatus>
<sharingModel>ReadWrite</sharingModel>
<description>Configuration for document generation templates. Each template references a DOCX file in Salesforce Files and defines how data is fetched and merged.</description>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Created_Date__c</fullName>
<description>Displays the date and time when the generated document record was created.</description>
<externalId>false</externalId>
<formula>CreatedDate</formula>
<formulaTreatBlanksAs>BlankAsZero</formulaTreatBlanksAs>
<label>Created Date</label>
<required>false</required>
<trackTrending>false</trackTrending>
<type>DateTime</type>
</CustomField>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Last_Modified_Date__c</fullName>
<description>Displays the date and time when the generated document record was last modified.</description>
<externalId>false</externalId>
<formula>LastModifiedDate</formula>
<formulaTreatBlanksAs>BlankAsZero</formulaTreatBlanksAs>
<label>Last Modified Date</label>
<required>false</required>
<trackTrending>false</trackTrending>
<type>DateTime</type>
</CustomField>
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,16 @@
<field>Generated_Document__c.ScheduledRetryTime__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>false</editable>
<field>Generated_Document__c.Created_Date__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>false</editable>
<field>Generated_Document__c.Last_Modified_Date__c</field>
<readable>true</readable>
</fieldPermissions>

<!-- Field Permissions for Docgen_Template__c (optional fields only) -->
<fieldPermissions>
Expand Down
7 changes: 4 additions & 3 deletions sfdx-project.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"packageDirectories": [
{
"versionName": "ver 0.3.6",
"versionNumber": "0.3.6.NEXT",
"versionName": "ver 0.3.7",
"versionNumber": "0.3.7.NEXT",
"path": "force-app/main",
"default": true,
"package": "docgen",
Expand All @@ -29,6 +29,7 @@
"docgen@0.3.3-1": "04tWS000002UXlNYAW",
"docgen@0.3.4-1": "04tWS000002UYmHYAW",
"docgen@0.3.5-1": "04tWS000002ZSczYAG",
"docgen@0.3.6-1": "04tWS000002agXRYAY"
"docgen@0.3.6-1": "04tWS000002agXRYAY",
"docgen@0.3.7-1": "04tWS000002bS7RYAU"
}
}
20 changes: 17 additions & 3 deletions src/convert/soffice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,15 +252,28 @@

// Execute LibreOffice conversion
const outputPath = path.join(jobWorkdir, 'input.pdf');
await this.executeLibreOffice(
const conversion = await this.executeLibreOffice(
inputPath,
jobWorkdir,
timeout,
correlationId
);

// Read PDF from output
const pdfBuffer = await fs.readFile(outputPath);
let pdfBuffer: Buffer;
try {
pdfBuffer = await fs.readFile(outputPath);
} catch (error: any) {

Check warning on line 266 in src/convert/soffice.ts

View workflow job for this annotation

GitHub Actions / Test & Build (20.x)

Unexpected any. Specify a different type
if (error?.code === 'ENOENT') {
const stderr = conversion.stderr ? ` stderr: ${conversion.stderr.trim()}` : '';
const stdout = conversion.stdout ? ` stdout: ${conversion.stdout.trim()}` : '';
throw new ConversionFailedError(
`LibreOffice did not produce PDF output.${stderr}${stdout}`,
{ correlationId }
);
}
throw error;
}
logger.debug(
{ correlationId, outputPath, size: pdfBuffer.length },
'Read PDF from output'
Expand Down Expand Up @@ -300,7 +313,7 @@
outputDir: string,
timeout: number,
correlationId: string
): Promise<void> {
): Promise<{ stdout: string; stderr: string }> {
// Create unique user profile directory to prevent lock conflicts
const userProfile = path.join(outputDir, '.libreoffice-profile');

Expand Down Expand Up @@ -341,7 +354,8 @@
{ correlationId, stdout: stdout ? stdout.trim() : '' },
'LibreOffice conversion completed'
);
return { stdout, stderr };
} catch (error: any) {

Check warning on line 358 in src/convert/soffice.ts

View workflow job for this annotation

GitHub Actions / Test & Build (20.x)

Unexpected any. Specify a different type
// Check if error is timeout
if (error.killed || error.signal === 'SIGTERM') {
logger.error(
Expand Down
102 changes: 101 additions & 1 deletion src/templates/docx-postprocess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ function addRowSuppressionMarkers(
if (fieldPaths.length === 0) {
return rowXml;
}
if (fieldPaths.some((fieldPath) => !isRootDataPath(data, fieldPath))) {
return rowXml;
}

const token = `__DOCGEN_ROW_${rowMarkers.length}__`;
rowMarkers.push({
Expand Down Expand Up @@ -195,13 +198,68 @@ function extractSimpleFieldPaths(xml: string): string[] {
return [...fields];
}

function extractWordText(xml: string): string {
const texts: string[] = [];
const matches = xml.matchAll(/<w:t\b[^>]*>([\s\S]*?)<\/w:t>/g);

for (const match of matches) {
texts.push(decodeXmlText(match[1]));
}

return texts.join('');
}

function decodeXmlText(text: string): string {
return text
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&amp;/g, '&');
}

function resolvePath(data: Record<string, unknown>, fieldPath: string): unknown {
return fieldPath.split('.').reduce<unknown>((current, part) => {
const directValue = fieldPath.split('.').reduce<unknown>((current, part) => {
if (current && typeof current === 'object' && part in current) {
return (current as Record<string, unknown>)[part];
}
return undefined;
}, data);

if (directValue !== undefined || fieldPath.includes('.')) {
return directValue;
}

const rootKeys = Object.keys(data);
if (rootKeys.length === 1) {
const rootValue = data[rootKeys[0]];
if (rootValue && typeof rootValue === 'object' && fieldPath in rootValue) {
return (rootValue as Record<string, unknown>)[fieldPath];
}
}

return undefined;
}

function isRootDataPath(data: Record<string, unknown>, fieldPath: string): boolean {
const firstPart = fieldPath.split('.')[0];
if (firstPart.startsWith('$')) {
return false;
}
if (firstPart in data) {
return true;
}
if (fieldPath.includes('.')) {
return false;
}

const rootKeys = Object.keys(data);
if (rootKeys.length !== 1) {
return false;
}

const rootValue = data[rootKeys[0]];
return Boolean(rootValue && typeof rootValue === 'object' && fieldPath in rootValue);
}

function isBlankValue(value: unknown): boolean {
Expand All @@ -225,6 +283,8 @@ async function postProcessXmlParts(zip: JSZip, context: DocxPostProcessContext):

let xml = await file.async('string');
xml = applyRowSuppression(xml, context.rowMarkers);
xml = removeEmptyTableCellParagraphs(xml);
xml = removeRowlessTables(xml);
xml = applyEditableControls(xml, context.controls);
zip.file(path, xml);
}
Expand Down Expand Up @@ -255,6 +315,46 @@ function applyRowSuppression(xml: string, rowMarkers: RowMarker[]): string {
return nextXml;
}

function removeEmptyTableCellParagraphs(xml: string): string {
return xml.replace(/<w:tc\b[\s\S]*?<\/w:tc>/g, (cellXml) => {
let fallbackParagraph = '';
const cleanedCellXml = cellXml.replace(/<w:p\b[\s\S]*?<\/w:p>/g, (paragraphXml) => {
if (!isRemovableEmptyParagraph(paragraphXml)) {
return paragraphXml;
}
fallbackParagraph ||= paragraphXml;
return '';
});

if (hasTableCellBlockContent(cleanedCellXml) || !fallbackParagraph) {
return cleanedCellXml;
}

return cleanedCellXml.replace('</w:tc>', `${fallbackParagraph}</w:tc>`);
});
}

function hasTableCellBlockContent(cellXml: string): boolean {
return /<w:(?:p|tbl|sdt|altChunk)\b/.test(cellXml);
}

function removeRowlessTables(xml: string): string {
return xml.replace(/<w:tbl\b[\s\S]*?<\/w:tbl>/g, (tableXml) =>
/<w:tr\b/.test(tableXml) ? tableXml : ''
);
}

function isRemovableEmptyParagraph(paragraphXml: string): boolean {
if (
paragraphXml.includes('__DOCGEN_CONTROL_') ||
/<w:(?:drawing|pict|object|sdt|fldSimple|br|tab)\b/.test(paragraphXml)
) {
return false;
}

return extractWordText(paragraphXml).trim() === '';
}

function applyEditableControls(xml: string, controls: ControlMarker[]): string {
let nextXml = xml;

Expand Down
Loading
Loading