• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CBIIT / crdc-datahub-ui / 20439620979

22 Dec 2025 05:43PM UTC coverage: 78.972% (+0.8%) from 78.178%
20439620979

push

github

web-flow
Merge pull request #924 from CBIIT/3.5.0

3.5.0 Release

5158 of 5669 branches covered (90.99%)

Branch coverage included in aggregate %.

806 of 844 new or added lines in 46 files covered. (95.5%)

9 existing lines in 5 files now uncovered.

30666 of 39694 relevant lines covered (77.26%)

231.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

8.5
/src/components/ExportRequestButton/pdf/Generate.ts
1
import { jsPDF as JsPDF } from "jspdf";
1✔
2

3
import Logo from "../../../assets/header/Logo.jpg";
1✔
4
import env from "../../../env";
1✔
5
import { FormatDate, formatFullStudyName } from "../../../utils";
1✔
6

7
import {
1✔
8
  COLOR_HR,
9
  COLOR_FIELD_BASE,
10
  COLOR_EMAIL,
11
  COLOR_BASE,
12
  COLOR_HYPERLINK,
13
  COLOR_DOC_FOOTER,
14
} from "./Colors";
15
import fonts from "./Fonts";
1✔
16
import { loadFont } from "./utils";
1✔
17

18
/**
19
 * Describes the minimal padding from the edges of the document.
20
 *
21
 * @note No content should be placed from the document edge to this margin.
22
 */
23
const BASE_MARGIN = 20;
1✔
24

25
/**
26
 * Describes the margin around the content of the document.
27
 *
28
 * @note The design spec suggests it should be closer to +50p,
29
 * but this is a compromise to ensure the content fits within the page.
30
 * @note Any horizontal lines bypass this constraint.
31
 */
32
const CONTENT_MARGIN = BASE_MARGIN + 25;
1✔
33

34
/**
35
 * Describes the drawing width of horizontal lines.
36
 */
37
const HR_WIDTH = 0.8;
1✔
38

39
/**
40
 * Writes the PDF header to the document.
41
 *
42
 * @param doc The jsPDF instance to add the header to.
43
 * @param request The submission request to generate the header from.
44
 * @returns {number} The current Y position after writing the header including padding.
45
 */
46
const writeHeader = (doc: JsPDF, request: Application): number => {
1✔
47
  const { applicant, questionnaireData, status, submittedDate } = request;
×
48
  const { study } = questionnaireData || {};
×
49
  const formattedSubmittedDate =
×
50
    status !== "In Progress" ? FormatDate(submittedDate, "YYYY-MM-DD") : "N/A";
×
51

52
  const RIGHT_EDGE = doc.internal.pageSize.width - BASE_MARGIN;
×
53
  let y = BASE_MARGIN;
×
54

55
  doc.addImage(Logo, "JPEG", BASE_MARGIN, y, 278, 56);
×
56
  y += 63;
×
57

58
  // Submitter name
59
  doc.setDrawColor(...COLOR_HR);
×
60
  doc.setLineWidth(HR_WIDTH);
×
61
  doc.line(BASE_MARGIN, (y += 15), RIGHT_EDGE, y);
×
62
  doc.setFont("Nunito", "normal", 800);
×
63
  doc.setTextColor(...COLOR_FIELD_BASE);
×
64
  doc.setFontSize(10);
×
65
  doc.text("SUBMITTER'S NAME", CONTENT_MARGIN, (y += 15));
×
66
  doc.setFont("Nunito", "normal", 700);
×
67
  doc.setFontSize(14);
×
68
  doc.setTextColor(...COLOR_EMAIL);
×
69
  doc.text(applicant.applicantName, CONTENT_MARGIN + 85, (y += 1));
×
70
  doc.line(BASE_MARGIN, (y += 10), RIGHT_EDGE, y);
×
71
  y += 24;
×
72

73
  // Study name and abbreviation
74
  doc.setFont("Nunito", "normal", 600);
×
75
  doc.setTextColor(...COLOR_BASE);
×
76
  doc.setFontSize(10);
×
77
  doc.text("STUDY NAME", CONTENT_MARGIN, y);
×
78

79
  const formattedName = formatFullStudyName(study?.name, study?.abbreviation);
×
80
  const maxNameWidth = RIGHT_EDGE - CONTENT_MARGIN - 85;
×
81
  doc.setFont("Nunito", "normal", 400);
×
82
  doc.setFontSize(12);
×
83
  doc.setTextColor(...COLOR_FIELD_BASE);
×
84
  doc.text(formattedName, CONTENT_MARGIN + 85, y, {
×
85
    // NOTE – Take the full width of the page and subtract the margins/offsets
86
    maxWidth: maxNameWidth,
×
87
  });
×
88

89
  const computedNameLines = doc.splitTextToSize(formattedName, maxNameWidth);
×
NEW
90
  y += (computedNameLines?.length || 1) * 10 + 8;
×
91

92
  // Submitted Date
93
  doc.setFont("Nunito", "normal", 600);
×
94
  doc.setTextColor(...COLOR_BASE);
×
95
  doc.setFontSize(10);
×
96
  doc.text("SUBMITTED DATE", CONTENT_MARGIN, y);
×
97

98
  doc.setFont("Nunito", "normal", 400);
×
99
  doc.setFontSize(12);
×
100
  doc.setTextColor(...COLOR_FIELD_BASE);
×
101
  doc.text(formattedSubmittedDate, CONTENT_MARGIN + 85, y);
×
102
  y += 18;
×
103

104
  // Status
105
  doc.setFont("Nunito", "normal", 600);
×
106
  doc.setTextColor(...COLOR_BASE);
×
107
  doc.setFontSize(10);
×
108
  doc.text("STATUS", CONTENT_MARGIN, y);
×
109

110
  doc.setFont("Nunito", "normal", 400);
×
111
  doc.setFontSize(12);
×
112
  doc.setTextColor(...COLOR_FIELD_BASE);
×
113
  doc.text(status, CONTENT_MARGIN + 85, y);
×
114
  y += 24;
×
115

116
  // Click to view (text)
117
  doc.setFont("Nunito", "normal", 400);
×
118
  doc.setFontSize(12);
×
119
  doc.setTextColor(...COLOR_FIELD_BASE);
×
120
  doc.text("Click here to view the Submission Request Form", CONTENT_MARGIN, y, {
×
121
    align: "left",
×
122
  });
×
123

124
  // Click to view (overlapping link)
125
  doc.setTextColor(...COLOR_HYPERLINK);
×
126
  doc.textWithLink("here", CONTENT_MARGIN + 22, y, {
×
127
    url: `${env.VITE_NIH_REDIRECT_URL}/submission-request/${request._id}`,
×
128
    align: "left",
×
129
  });
×
130

131
  doc.setDrawColor(...COLOR_HYPERLINK);
×
132
  doc.setLineWidth(HR_WIDTH);
×
133
  doc.line(CONTENT_MARGIN + 22, (y += 2), CONTENT_MARGIN + 40, y);
×
134

135
  doc.setDrawColor(...COLOR_HR);
×
136
  doc.line(BASE_MARGIN, (y += 22), RIGHT_EDGE, y);
×
137

138
  return y + 30;
×
139
};
×
140

141
/**
142
 * A function to write a footer across all pages in the PDF document.
143
 *
144
 * @param doc The jsPDF instance to add the footer to.
145
 * @returns {void}
146
 */
147
const writeFooters = (doc: JsPDF): void => {
1✔
148
  const pdfRightEdge = doc.internal.pageSize.width - BASE_MARGIN;
×
149
  const bottomMargin = doc.internal.pageSize.height - BASE_MARGIN;
×
150
  const pageCount: number = doc.internal.pages.filter((p) => p !== null).length;
×
151

152
  for (let pageNum = 0; pageNum < pageCount; pageNum += 1) {
×
153
    doc.setPage(pageNum + 1);
×
154

155
    // Draw the horizontal line 20px from the bottom
156
    doc.setDrawColor(...COLOR_HR);
×
157
    doc.setLineWidth(HR_WIDTH);
×
158
    doc.line(
×
159
      BASE_MARGIN,
×
160
      bottomMargin - 20,
×
161
      doc.internal.pageSize.width - BASE_MARGIN,
×
162
      bottomMargin - 20
×
163
    );
×
164

165
    // Write the page number
166
    doc.setFontSize(10);
×
167
    doc.setTextColor(...COLOR_DOC_FOOTER);
×
168
    doc.setFont("Nunito", "normal", 600);
×
169
    doc.text(`Page ${pageNum + 1} of ${pageCount}`, pdfRightEdge, bottomMargin, {
×
170
      align: "right",
×
171
    });
×
172
  }
×
173
};
×
174

175
/**
176
 * A function to generate a PDF document from a submission request.
177
 *
178
 * @note This function temporarily modifies the width of the print region to ensure the
179
 *  content fits within the page.
180
 * @param request The submission request to generate a PDF from.
181
 * @param printRegion The region to print to the PDF.
182
 * @returns {Promise<URL>} A promise that resolves when the PDF is generated.
183
 * @throws {Error} If the submission request is invalid.
184
 */
185
export const GenerateDocument = async (
1✔
186
  request: Application,
×
187
  printRegion: HTMLElement
×
188
): Promise<Blob> => {
×
189
  if (!request || !request?._id) {
×
190
    throw new Error("Invalid submission request provided.");
×
191
  }
×
192
  if (!printRegion || !(printRegion instanceof Element)) {
×
193
    throw new Error("Invalid print region provided.");
×
194
  }
×
195

196
  const doc = new JsPDF({
×
197
    orientation: "portrait",
×
198
    unit: "px",
×
199
    format: "letter",
×
200
  });
×
201

202
  doc.setProperties({
×
203
    title: `Submission Request ${request._id}`,
×
204
    subject: `PDF Export of Submission Request ${request._id}`,
×
205
    keywords: "CRDC, submission request, PDF export",
×
206
    author: "CRDC Submission Portal",
×
207
    creator: "crdc-datahub-ui",
×
208
  });
×
209

210
  await Promise.allSettled(fonts.map((font) => loadFont(doc, font)));
×
211

212
  return new Promise((resolve) => {
×
213
    const y = writeHeader(doc, request);
×
214

215
    // NOTE: This fixes the width of the form to prevent the form from being too wide in the PDF
216
    // html2canvas width/windowWidth props do not work as expected
217
    printRegion.style.width = "794px";
×
218

219
    doc.html(printRegion, {
×
220
      callback: (doc) => {
×
221
        printRegion.style.width = "";
×
222

223
        writeFooters(doc);
×
224
        resolve(doc.output("blob"));
×
225
      },
×
226
      html2canvas: {
×
227
        scale: 0.5,
×
228
        ignoreElements: (element: HTMLElement) => {
×
229
          if (element?.getAttribute("data-print") === "false") {
×
230
            return true;
×
231
          }
×
232

233
          return false;
×
234
        },
×
235
        logging: false,
×
236
      },
×
237
      autoPaging: "text",
×
238
      x: 0,
×
239
      y: y - 25,
×
240
      margin: [BASE_MARGIN, BASE_MARGIN + 10, 50, BASE_MARGIN + 10],
×
241
    });
×
242
  });
×
243
};
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc