Submission PDF Export Developer Guide - edmozley/freeitsm GitHub Wiki
Shipped in 2.2.0, asked for by a user who needed an audit trail: "the ability to download forms as PDFs and printing to maintain an audit trail." A form you can file, not just a row you can read.
The operator's own logo, the form title, the submission number, who submitted it and when, the approval trail, and every answer โ including answers to questions since removed from the form.
The approval block is the point of the document. A list of answers with no decision on it is a form, not a record.
Generated client-side with jsPDF, vendored at assets/js/vendor/jspdf.umd.min.js. Nothing is uploaded and no server-side PDF tooling is required โ see Offline and Air-Gapped Installs.
assets/js/form-pdf.js is the only place that decides what a record looks like. It is used by:
| Where | How |
|---|---|
| a submission's detail panel | Export as PDF |
| a row in the submissions list | the download icon, without opening anything |
| a collection | single, bundle, or separate, across several forms |
๐ Why it is a shared module. All of it began inside forms/submissions.php, which shows the submissions of one form. A collection shows several and needs identical documents; a second copy would have drifted the first time a field type changed. forms/submissions.php keeps the function names as one-line delegates so its call sites are unchanged.
form ({ title, fields }) instead of closing over a page-global formData. On a collection page every row may belong to a different form, so "the form" can no longer be a property of the page.
doc.addImage(logo, 'PNG', margin, y, w, h, 'brandlogo', 'FAST');
// ^alias ^deflateBoth parts are load-bearing. An uncompressed branding PNG took a one-page document to 1.45 MB. The alias is what makes bundles affordable โ without it jsPDF re-embeds the same picture per record. Measured: 59 KB for one submission, 68 KB for four, about 3 KB per extra record instead of 56 KB. loadBrandLogo() also caches the Image promise so eighty records do not fetch it eighty times.
A logo that fails to load resolves to null and is skipped. A missing logo must never cost somebody their record.
Software Request - Ed Mozley - 19.09.2026.pdf
The date uses the reader's own preference, so a US install files it as 09.19.2026 without the export knowing anything about locales.
- ๐
fmtDate, notfmtNaiveDate.submitted_dateis a real instant stamped by the server and must convert into the viewer's zone โ the opposite of a rota day. Both helpers exist and picking wrongly is silently wrong for everyone outside UTC. See Timezones and Time Handling. โ ๏ธ A date template is not a filename. Three of the nine shipped templates contain a slash, which no filesystem accepts, so separators become dots rather than being stripped. A trailing dot or space is trimmed because Windows refuses those, and the whole name is capped at 180 characters to leave room for the extension and the(1)a browser adds on a collision.- Verified against all nine date preferences plus a submitter name made entirely of illegal characters.
datetime goes the other way โ FormLogic.formatDateValue(), naive, never shifted. Two kinds of date in one document.
Neither option needs a zip library.
-
Bundle โ one document,
addPage()between records. -
Separate โ a run of
doc.save()with a 150 ms beat between them. Fired back to back, browsers drop all but the first few: the downloads are queued by the page, not by the click. A confirm appears above ten files, because a browser asks once before accepting a run of them.
- ๐ด
brandingLogoUrl()undefined โincludes/branding.phpwas never required. A PHP fatal is served as HTTP 200 with the page truncated mid-<script>, so every bit of JavaScript died: empty table, dead button, no error anywhere. - ๐ด The selection reset belongs at the TOP of the redraw. It was at the foot, and the redraw returns early when the list is empty โ so filtering down to nothing left the bar offering to export rows that had gone.
โ ๏ธ The selection bar is shown with a class, not thehiddenattribute. An element carrying its owndisplayrule ignoreshiddenentirely.โ ๏ธ Measuring any of this in a headless browser: the modal and the buttons animate, andgetBoundingClientRectincludes transforms whilegetComputedStylereturns interpolated values mid-transition. Disable transitions before measuring; a timer will lie on a slower run.