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

CBIIT / crdc-datahub-ui / 17132131774

21 Aug 2025 03:52PM UTC coverage: 77.592% (+1.7%) from 75.941%
17132131774

Pull #806

github

web-flow
Merge 6b88b37d9 into c10ceac73
Pull Request #806: Submission Request Excel Import & Export CRDCDH-3033, CRDCDH-3045, CRDCDH-3063

4841 of 5322 branches covered (90.96%)

Branch coverage included in aggregate %.

3122 of 3394 new or added lines in 32 files covered. (91.99%)

7 existing lines in 3 files now uncovered.

28996 of 38287 relevant lines covered (75.73%)

1856.98 hits per line

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

91.97
/src/classes/Excel/SectionBase.ts
1
import type ExcelJS from "exceljs";
2
import { CellValue } from "exceljs";
3

4
import { Logger } from "@/utils";
1✔
5

6
/**
7
 * Represents the context for a section in the Excel worksheet.
8
 */
9
export type SectionCtxBase = {
10
  /**
11
   * The Excel workbook.
12
   */
13
  workbook: ExcelJS.Workbook;
14
  /**
15
   * The utility functions for the section.
16
   */
17
  u: {
18
    /**
19
     * Set the header for the worksheet.
20
     *
21
     * @param ws The worksheet to modify.
22
     * @param color The color to use for the header.
23
     * @returns void
24
     */
25
    header: (ws: ExcelJS.Worksheet, color?: string) => void;
26
  };
27
};
28

29
/**
30
 * Represents the definition of a column in the section.
31
 */
32
export type ColumnDef<K extends string> = Omit<Partial<ExcelJS.Column>, "key"> & {
33
  key: K;
34
  /**
35
   * An optional annotation for the column header.
36
   */
37
  annotation?: string;
38
};
39

40
/**
41
 * Represents the character limits for each column in the section.
42
 */
43
export type CharacterLimitsMap<K extends string> = Readonly<Partial<{ [key in K]: number }>>;
44

45
/**
46
 * Represents the configuration for a section in the Excel worksheet.
47
 */
48
export type SectionConfig<K extends string, D> = {
49
  id: string;
50
  deps: D;
51
  sheetName: string;
52
  columns: ColumnDef<K>[];
53
  headerColor?: string;
54
  characterLimits?: CharacterLimitsMap<K>;
55
};
56

57
/**
58
 * Represents a section in the Excel worksheet.
59
 */
60
export type Section = {
61
  /**
62
   * The unique identifier for the section.
63
   *
64
   * @example "B"
65
   */
66
  id: string;
67
  /**
68
   * Serialize the section data to the worksheet.
69
   *
70
   * @param ctx The section context.
71
   * @returns The created worksheet.
72
   */
73
  serialize: (ctx: SectionCtxBase) => Promise<ExcelJS.Worksheet>;
74
};
75

76
/**
77
 * Represents a section in the Excel worksheet.
78
 */
79
export abstract class SectionBase<K extends string, D> implements Section {
1✔
80
  /**
81
   * The unique identifier for the section.
82
   * @example "B"
83
   */
84
  public readonly id: string;
68✔
85

86
  /**
87
   * The section dependencies.
88
   */
89
  protected readonly deps: D;
68✔
90

91
  /**
92
   * The name of the worksheet.
93
   * @example "Program and Study"
94
   */
95
  private readonly _sheetName: string;
68✔
96

97
  /**
98
   * The columns in the section.
99
   */
100
  private readonly _columns: ColumnDef<K>[];
68✔
101

102
  /**
103
   * The color of the header row.
104
   * @example "#D9EAD3"
105
   */
106
  private readonly _headerColor: string;
68✔
107

108
  /**
109
   * The character limits for each column in the section.
110
   */
111
  protected readonly CHARACTER_LIMITS: CharacterLimitsMap<K>;
68✔
112

113
  /**
114
   * Create a new section.
115
   *
116
   * @param cfg The section configuration.
117
   */
118
  constructor(cfg: SectionConfig<K, D>) {
68✔
119
    this.id = cfg.id;
68✔
120
    this.deps = cfg.deps;
68✔
121
    this._sheetName = cfg.sheetName;
68✔
122
    this._columns = cfg.columns;
68✔
123
    this._headerColor = cfg.headerColor || "#D9EAD3";
68!
124
    this.CHARACTER_LIMITS = Object.freeze({ ...(cfg.characterLimits ?? {}) });
68✔
125
  }
68✔
126

127
  /**
128
   * Serialize the section data to the worksheet.
129
   *
130
   * @param ctx The section context.
131
   * @returns The created worksheet.
132
   */
133
  public async serialize(ctx: SectionCtxBase): Promise<ExcelJS.Worksheet> {
68✔
134
    const ws = this.create(ctx);
68✔
135
    this.write(ctx, ws);
68✔
136
    this.applyValidation(ctx, ws);
68✔
137
    this.annotate(
68✔
138
      ws,
68✔
139
      this._columns
68✔
140
        .filter((c) => c.annotation)
68✔
141
        .reduce(
68✔
142
          (acc, col) => {
68✔
143
            acc[col.key] = col.annotation;
182✔
144
            return acc;
182✔
145
          },
182✔
146
          {} as Partial<Record<K, string>>
68✔
147
        )
68✔
148
    );
68✔
149

150
    return ws;
68✔
151
  }
68✔
152

153
  /**
154
   * Create worksheet, add columns, and style header.
155
   *
156
   * @param ctx The section context.
157
   * @returns The created worksheet.
158
   */
159
  protected create(ctx: SectionCtxBase): ExcelJS.Worksheet {
68✔
160
    const existing = ctx.workbook.worksheets.find((ws) => ws.name === this._sheetName);
68✔
161
    if (existing) {
68!
NEW
162
      ctx.workbook.removeWorksheet(existing.id);
×
NEW
163
    }
×
164

165
    const ws = ctx.workbook.addWorksheet(this._sheetName);
68✔
166
    ws.columns = this._columns;
68✔
167

168
    const color = (this._headerColor || "#D9EAD3").replace(/^#/, "").toUpperCase();
68!
169
    ctx.u.header(ws, color);
68✔
170

171
    ws.views = [{ state: "frozen", ySplit: 1 }];
68✔
172

173
    return ws;
68✔
174
  }
68✔
175

176
  /**
177
   * Write data to the worksheet.
178
   *
179
   * @param ctx The section context.
180
   * @param ws The worksheet.
181
   * @returns The created row.
182
   */
183
  protected abstract write(ctx: SectionCtxBase, ws: ExcelJS.Worksheet): ExcelJS.Row[];
184

185
  /**
186
   * Attach data validation to the data in the worksheet.
187
   *
188
   * @param ctx The section context.
189
   * @param ws The worksheet.
190
   * @param row The row to validate.
191
   */
192
  protected abstract applyValidation(
193
    ctx: SectionCtxBase,
194
    ws: ExcelJS.Worksheet
195
  ): void | Promise<void>;
196

197
  /**
198
   * Write annotations on the specified cells in the worksheet.
199
   *
200
   * @param ws The worksheet to annotate
201
   * @param annotations A map of Cell Key -> Note
202
   * @param rowNum The row to annotate. Default is the header row.
203
   */
204
  protected annotate(
68✔
205
    ws: ExcelJS.Worksheet,
68✔
206
    annotations: Partial<Record<K, string>>,
68✔
207
    rowNum = 1
68✔
208
  ): void {
68✔
209
    const row = ws.getRow(rowNum);
68✔
210
    this._columns.forEach((col, index) => {
68✔
211
      if (col.key in annotations) {
1,239✔
212
        const cell = row.getCell(index + 1);
182✔
213
        cell.note = {
182✔
214
          texts: [{ text: annotations[col.key] || "" }],
182!
215
          protection: { locked: "True", lockText: "True" },
182✔
216
        };
182✔
217
      }
182✔
218
    });
68✔
219
  }
68✔
220

221
  /**
222
   * Get the cells in row 2 of the worksheet.
223
   *
224
   * @note Follows the order of the defined columns.
225
   * @param ws The worksheet.
226
   * @param row The row number to get cells from.
227
   * @returns The cells in the specified row.
228
   */
229
  protected getRowCells(ws: ExcelJS.Worksheet, row = 2) {
68✔
230
    const r = ws.getRow(row);
64✔
231
    const cells = this._columns.map((col) => r.getCell(col.key)) || [];
64!
232

233
    return cells;
64✔
234
  }
64✔
235

236
  /**
237
   * Set the values for a specific row in the worksheet.
238
   *
239
   * @param ws The worksheet.
240
   * @param rowIndex The index of the row to update.
241
   * @param values The values to set.
242
   */
243
  protected setRowValues(
68✔
244
    ws: ExcelJS.Worksheet,
170✔
245
    rowIndex: number,
170✔
246
    values: Partial<Record<K, CellValue>>
170✔
247
  ) {
170✔
248
    const row = ws.getRow(rowIndex);
170✔
249
    this._columns.forEach((col, index) => {
170✔
250
      if (col.key in values) {
3,022✔
251
        row.getCell(index + 1).value = values[col.key] || null;
1,023✔
252
      }
1,023✔
253
    });
170✔
254
  }
170✔
255

256
  /**
257
   * Apply data validation to a specific column in the worksheet.
258
   *
259
   * @param ws The worksheet.
260
   * @param key The column key.
261
   * @param callback The callback to apply to each cell.
262
   * @param options The options for the cell range.
263
   */
264
  protected forEachCellInColumn(
68✔
265
    ws: ExcelJS.Worksheet,
410✔
266
    key: K,
410✔
267
    callback: (cell: ExcelJS.Cell, rowNumber: number) => void,
410✔
268
    options?: { startRow?: number; endRow?: number }
410✔
269
  ): void {
410✔
270
    const keyIsFound = this._columns?.find((c) => c.key === key);
410✔
271
    if (!keyIsFound) {
410!
NEW
272
      Logger.error(`SectionBase.tsx: Column with key "${key}" not found.`);
×
NEW
273
      return;
×
NEW
274
    }
×
275

276
    const { startRow = 2, endRow = 9999 } = options ?? {};
410✔
277
    const column = ws.getColumn(key as string);
410✔
278

279
    Array.from({ length: endRow - startRow + 1 }, (_, i) => i + startRow).forEach((rowNum) => {
410✔
280
      const row = ws.getRow(rowNum);
4,099,180✔
281
      const cell = row.getCell(column.number);
4,099,180✔
282
      callback(cell, rowNum);
4,099,180✔
283
    });
410✔
284
  }
410✔
285

286
  /**
287
   * Applies a standard character limit validation to the specified cell.
288
   *
289
   * @param cell The cell to apply the ruleset to
290
   * @param limit The character limit to apply
291
   */
292
  // eslint-disable-next-line class-methods-use-this
293
  protected applyTextLengthValidation(cell: ExcelJS.Cell, limit: number): void {
68✔
294
    cell.dataValidation = {
749,952✔
295
      type: "textLength",
749,952✔
296
      operator: "lessThan",
749,952✔
297
      showErrorMessage: true,
749,952✔
298
      error: `Must be less than ${limit} characters.`,
749,952✔
299
      allowBlank: false,
749,952✔
300
      formulae: [limit],
749,952✔
301
    };
749,952✔
302
  }
749,952✔
303
}
68✔
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

© 2025 Coveralls, Inc