Skip to content

Commit c687edc

Browse files
authored
Enable header row display in tables (#241)
Allows tables to have a table header row (THEAD) that contains values from the following list: No headers Base26 representation of the column index The selected header row from the source file The target header titles from the domain object (target). There are obvious contraints on their use (e.g. can't use target mode until mapping done, can't use source until a sheet is selected with a header range).
1 parent 5fd5cb9 commit c687edc

13 files changed

Lines changed: 326 additions & 40 deletions

File tree

lib/importer/assets/js/selectable_table.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -697,11 +697,16 @@ window.addEventListener("load", function() {
697697
for (var cell of selection.rightCells) { cell.node.classList.add(CellSelectedRightClassName); }
698698
selection.focus.node.scrollIntoViewIfNeeded(false);
699699

700+
const headerRowCellCount = table.querySelectorAll('th[scope="col"]').length
701+
const headerRowCount = table.querySelectorAll('thead tr').length
702+
const headerRowOffset = headerRowCellCount > 0 ? headerRowCount : 0;
703+
704+
700705
// If we have HTML elements defined on the page (with specific names) then they will be used
701706
// to store the top left row/column and bottom right row/column.
702-
if (tlRowTarget) { tlRowTarget.value = selection.startCell.row; }
707+
if (tlRowTarget) { tlRowTarget.value = selection.startCell.row - headerRowOffset; }
703708
if (tlColTarget) { tlColTarget.value = selection.startCell.col; }
704-
if (brRowTarget) { brRowTarget.value = selection.endCell.row; }
709+
if (brRowTarget) { brRowTarget.value = selection.endCell.row - headerRowOffset; }
705710
if (brColTarget) { brColTarget.value = selection.endCell.col; }
706711
}
707712

lib/importer/nunjucks/importer/macros/footer_selector.njk

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@
55
{% set rows = importerGetTrailingRows(data, count) %}
66
{% set caption = importerGetTableCaption(data, "Last", count) %}
77

8-
{{ importerRangeSelector(rows, caption) }}
8+
{% set tableHeaders = importerHeaderRowDisplay(data, "source") %}
9+
10+
{{ importerRangeSelector(rows, caption, tableHeaders) }}
911
{% endmacro %}

lib/importer/nunjucks/importer/macros/header_selector.njk

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
{% macro importerHeaderSelector(data, start, count=10) %}
55
{% set rows = importerGetRows(data, start, count) %}
66
{% set caption = importerGetTableCaption(data, "First", count) %}
7+
{% set tableHeaders = importerHeaderRowDisplay(data, "index") %}
78

8-
{{ importerRangeSelector(rows, caption) }}
9+
{{ importerRangeSelector(rows, caption, tableHeaders) }}
910
{% endmacro %}

lib/importer/nunjucks/importer/macros/range_selector.njk

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
{% macro importerRangeSelector(rows, caption) %}
2+
{% macro importerRangeSelector(rows, caption, tableHeaders=[]) %}
33

44
<div>
55
<input type="hidden" name="importer:selection:TLRow" id="importer:selection:TLRow"/>
@@ -14,11 +14,21 @@
1414
role="grid" aria-multiselectable="true"
1515
{% if caption %}
1616
aria-labelledby="tablecaption"
17-
{% endif %}
17+
{% endif %}
1818
>
1919
{% if caption %}
2020
<caption id="tablecaption" class="govuk-table__caption govuk-table__caption--m">{{caption}}</caption>
2121
{% endif %}
22+
23+
{% if tableHeaders %}
24+
<thead>
25+
<tr>
26+
{% for h in tableHeaders %}
27+
<th scope="col">{{ h }}</th>
28+
{% endfor %}
29+
</tr>
30+
</thead>
31+
{% endif %}
2232
<tbody role="rowgroup">
2333
{% for row in rows %}
2434
<tr role="row">

lib/importer/nunjucks/importer/macros/sheet_selector.njk

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
</div>
6161
{% else %}
6262
{% set caption = importerGetTableCaption(data, "First", 10, sheet.name) %}
63-
{{ importerTableView(sheet.data, caption=caption, hideHeader=true) }}
63+
{{ importerTableView(data, sheet.data, caption=caption, headerMode="none") }}
6464
{% endif %}
6565
</div>
6666
{% endfor %}

lib/importer/nunjucks/importer/macros/table_view.njk

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11

2-
{% macro importerTableView(data, caption=false, hideHeader=false ) %}
2+
{% macro importerTableView(session, data, caption=false, headerMode="none" ) %}
33
{% set headers = data.headers %}
44
{% set rows = data.rows %}
55
{% set moreRowsAvailable = data.extraRecordCount > 0 %}
66
{% set moreRowsCount = data.extraRecordCount %}
77

8+
{% set tableHeaders = importerHeaderRowDisplay(session, headerMode) %}
9+
810
<table class="selectable govuk-body" data-persist-selection="true">
911
{% if caption %}
1012
<caption class="govuk-table__caption govuk-table__caption--m">{{caption}}</caption>
1113
{% endif %}
12-
{% if not hideHeader %}
14+
{% if tableHeaders %}
1315
<thead>
16+
1417
<tr>
15-
{% for h in headers %}
18+
{% for h in tableHeaders %}
1619
<th>{{h}}</th>
1720
{% endfor %}
1821
</tr>

lib/importer/src/backend.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ exports.SessionPerformMappingJob = (sid, range, mapping, includeErrorRow = false
550550
const result = attrType(inputVal);
551551

552552
result.warnings.forEach((text) => {
553-
rowWarnings.push({ row: rowIdx, field: attr, message: text});
553+
rowWarnings.push({ row: rowIdx, field: attr, message: text });
554554
});
555555

556556
if (result.valid) {
@@ -575,7 +575,7 @@ exports.SessionPerformMappingJob = (sid, range, mapping, includeErrorRow = false
575575
records.push(mappedRecord);
576576
}
577577
} else {
578-
rowWarnings.push({row: rowIdx, message: "Row is empty"})
578+
rowWarnings.push({ row: rowIdx, message: "Row is empty" })
579579
}
580580

581581
if (rowWarnings.length > 0) {

lib/importer/src/functions.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
const sheets_lib = require("./sheets.js");
3+
const session_lib = require("./session.js");
34

45
const IMPORTER_SESSION_KEY = "importer.session";
56
const IMPORTER_ERROR_KEY = "importer.error";
@@ -162,6 +163,11 @@ const importerMappedData = (data) => {
162163
};
163164
}
164165

166+
const importerHeaderRowDisplay = (data, mode) => {
167+
const session = data[IMPORTER_SESSION_KEY];
168+
return session_lib.HeaderRowDisplay(session, mode)
169+
}
170+
165171
//--------------------------------------------------------------------
166172
// Helper functions that can be used on the review page to show
167173
// information about the data that has been mapped.
@@ -235,6 +241,7 @@ module.exports = {
235241
importerGetTrailingRows,
236242
importerGetTableCaption,
237243
importerMappedData,
244+
importerHeaderRowDisplay,
238245
data_sum,
239246
data_avg
240247
}

lib/importer/src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ exports.Initialise = (config, router, prototypeKit) => {
241241

242242
// Ensure the session is persisted. Currently in session, eventually another way
243243
request.session.data[IMPORTER_SESSION_KEY] = session;
244+
244245
redirectOnwards(request, response);
245246
},
246247
);
@@ -531,7 +532,7 @@ const getIntOrDefault = (key, dflt = 0) => {
531532
// then we will return no range.
532533
const getSelectionFromRequest = (request, session, optional = false) => {
533534
let defaultVal = optional ? -1 : 0;
534-
let defaultMaxCol = optional ? -1 : sheets_lib.GetTotalColumns(session);
535+
let defaultMaxCol = optional ? -1 : (sheets_lib.GetTotalColumns(session) - 1);
535536

536537
let tlRow = getIntOrDefault(
537538
request.body["importer:selection:TLRow"],

lib/importer/src/session.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const base26 = require("./base26.js");
12
const crypto = require("crypto");
23
const sheets_lib = require("./sheets.js");
34
const backend = require("./backend");
@@ -17,6 +18,13 @@ class Session {
1718
}
1819
}
1920

21+
class Range {
22+
constructor(start, end) {
23+
this.start = start
24+
this.end = end
25+
}
26+
}
27+
2028
exports.CreateSession = (config, request) => {
2129
const createResponse = {
2230
id: "",
@@ -67,3 +75,130 @@ var validateUpload = (file) => {
6775

6876
return undefined;
6977
};
78+
79+
/*
80+
* Returns a header row for display, based on the requested mode and the data
81+
* currently available in the state.
82+
*
83+
* The various display modes are:
84+
* - none - return nothing, no display required
85+
* - index - returns the index of the column in base26.
86+
* - source - returns the header names as they are defined in the source file
87+
* - target - returns the header names as they are in the domain object after mapping
88+
*/
89+
exports.HeaderRowDisplay = (session, displayMode) => {
90+
var mode = displayMode.toLowerCase();
91+
92+
// User has selected 'none' mode, which means we don't return any headers as
93+
// the user does not want to show them. No pre-requisites.
94+
// eslint-disable-next-line no-unused-vars
95+
const noneMode = (_range) => {
96+
return null
97+
}
98+
99+
// Index mode, shows the index as base26 where the values are taken from the range.
100+
// Requires a selected sheet
101+
const indexMode = (range) => {
102+
if (!session.sheet || session.sheet == "") {
103+
console.warn("HeaderRowDisplay: No sheet selected so header display mode is 'none'")
104+
return null
105+
}
106+
107+
const headers = new Array();
108+
for (var i = range.start; i <= range.end; i++) {
109+
headers.push(base26.toBase26(i + 1))
110+
}
111+
112+
return headers
113+
}
114+
115+
// Using the header values selected by the user, use those instead of column indices.
116+
// Requires a selected sheet and a headerRange
117+
const sourceMode = (range) => {
118+
if (!session.sheet || session.sheet == "" || !session.headerRange) {
119+
console.warn("HeaderRowDisplay: No sheet selected so header display mode is 'none'")
120+
return null
121+
}
122+
123+
if (!Object.prototype.hasOwnProperty.call(session.headerRange, "start") ||
124+
!Object.prototype.hasOwnProperty.call(session.headerRange, "end")) {
125+
console.warn("HeaderRowDisplay: No header range available for source headers")
126+
return null
127+
}
128+
129+
const rows = sheets_lib.GetRows(session, session.headerRange.start.row, session.headerRange.end.row + 1)[0];
130+
const headers = new Array();
131+
132+
for (var i = range.start; i <= range.end; i++) {
133+
headers.push(rows[i]?.value ?? "")
134+
}
135+
136+
return headers;
137+
}
138+
139+
// Use the header values from the mapped domain object, so the target column headings and the user's data
140+
// for that column.
141+
// Requires a selected sheet and a mapping
142+
// eslint-disable-next-line no-unused-vars
143+
const targetMode = (_range) => {
144+
if (!session.sheet || session.sheet == "") {
145+
console.warn("HeaderRowDisplay: No sheet selected so header display mode is 'none'")
146+
return null
147+
}
148+
149+
if (!session.mapping) {
150+
console.warn("HeaderRowDisplay: No mapping available so header display mode is 'none'")
151+
return null
152+
}
153+
154+
return noneMode;
155+
}
156+
157+
var range = calculateHeaderRange(session)
158+
159+
// Unless the mode is 'none' then a sheet is required to return headers for display
160+
if (mode != "none" && (!session.sheet || session.sheet == "")) {
161+
console.warn("HeaderRowDisplay: No sheet selected so header display mode is 'none'")
162+
return null
163+
}
164+
165+
switch (mode) {
166+
// Index as base26 name
167+
case "index":
168+
return indexMode(range);
169+
// Header names as selected by user
170+
case "source":
171+
return sourceMode(range);
172+
// Header names as the fieldnames for the domain object
173+
case "target":
174+
return targetMode(range);
175+
}
176+
177+
// If 'none' is specified, or the value provided isn't supported...
178+
return noneMode(range);
179+
}
180+
181+
// Calculate the range, either from the currently selected columns, or from the number
182+
// of items in a row (making the assumption that they're even).
183+
const calculateHeaderRange = (session) => {
184+
if (Object.prototype.hasOwnProperty.call(session, "headerRange") &&
185+
Object.prototype.hasOwnProperty.call(session.headerRange, "start") &&
186+
Object.prototype.hasOwnProperty.call(session.headerRange, "end")) {
187+
return new Range(session.headerRange.start.column, session.headerRange.end.column)
188+
}
189+
190+
if (!session.sheet || session.sheet == "") {
191+
console.warn("HeaderRowDisplay: No sheet selected so finding mode from rows is not possible")
192+
return null
193+
}
194+
195+
// With no specified headers, try and default to the first row. This may already be set
196+
// earlier in the flow, but we add this defensively.
197+
const r = sheets_lib.GetRows(session)
198+
if (r) {
199+
return new Range(0, r[0].length)
200+
}
201+
202+
console.warn("HeaderRowDisplay: No rows available when determining number of columns")
203+
return null
204+
}

0 commit comments

Comments
 (0)