diff --git a/src/routes/api/v1/index.ts b/src/routes/api/v1/index.ts
index b9ced10..4a6fc5d 100644
--- a/src/routes/api/v1/index.ts
+++ b/src/routes/api/v1/index.ts
@@ -12,16 +12,17 @@ import alertContactsRoute_schema from './alertContacts_schema.js';
// Router base is '/api/v1'
const Router = express.Router({ strict: false });
-// All empty strings are null values.
+// All empty strings are null values (body)
Router.use('*', function (req, res, next) {
for (let key in req.body) {
if (req.body[key] === '') {
- req.body[key] = null;
+ req.body[key] = undefined;
}
}
next();
});
+
// All api routes lowercase! Yea I know but when strict: true it matters.
Router.route('/alertcontacts').get(alertContactsRoute.get).post(alertContactsRoute.post).patch(alertContactsRoute.patch).delete(alertContactsRoute.del);
Router.route('/alertcontacts/describe').get(alertContactsRoute_schema);
diff --git a/static/apiWrapper.js b/static/apiWrapper.js
index 2270af3..713ac87 100644
--- a/static/apiWrapper.js
+++ b/static/apiWrapper.js
@@ -21,12 +21,18 @@ let _api = {
const response = await fetch(_apiConfig.basePath + path, options);
// Handle the response
if (!response.ok) {
+ console.error('Failed to fetch:', response.statusText);
_testPageFail(response.statusText);
return;
}
const result = await response.json();
// Handle the result, was json valid?
if (!result) {
+ // Is it a number instead?
+ if (typeof result === 'number') {
+ return result;
+ }
+ console.error('Invalid JSON response');
_testPageFail('Invalid JSON response');
return;
}
@@ -53,15 +59,6 @@ let _api = {
}
return result;
- },
- getAsync: function (path, callback) {
- const options = {
- headers: new Headers({ 'content-type': 'application/json' })
- };
- fetch(_apiConfig.basePath + path, options)
- .then((response) => response.json())
- .then((data) => callback(data))
- .catch((error) => _testPageFail(error));
}
};
@@ -113,21 +110,37 @@ function getApiDescriptionByTable(tableName) {
}
}
-function returnTableDataByTableName(tableName) {
- return _api.get(tableName);
+function returnTableDataByTableName(tableName, search="", orderBy="asc", sort="", take=-1, skip=0) {
+ var orderBy = orderBy.toLowerCase();
+ if(orderBy == "") {
+ orderBy = "asc";
+ }
+ var baseString = tableName + "?order=" + orderBy;
+ if(sort && sort.length > 0) {
+ baseString += "&sort=" + sort;
+ }
+ if(take > 0) {
+ baseString += "&take=" + take;
+ }
+ if(skip > 0) {
+ baseString += "&skip=" + skip;
+ }
+
+ if (search && search.length > 0) {
+ return _api.get(baseString + '&search=' + search);
+ } else {
+ return _api.get(baseString);
+ }
}
-function returnTableDataByTableNameWithSearch(tableName, search) {
- return _api.get(tableName + '?search=' + search);
-}
-
-function returnTableDataByTableNameAsync(tableName, callback) {
- _api.getAsync(tableName, callback);
-}
-
-async function getCountByTable(tableName) {
+async function getCountByTable(tableName, search="") {
+ let baseString = tableName + '?count=true';
+ if (search && search.length > 0) {
+ baseString += '&search=' + search;
+ }
// Stored in `data:count:${tableName}`
- let result = await _api.get(tableName + '?count=true');
+ let result = await _api.get(baseString);
+ console.debug('Count result:', result);
if (typeof result !== 'number') {
_testPageWarn('Count was not a number, was: ' + result);
console.warn('Count was not a number, was: ' + result);
@@ -136,10 +149,6 @@ async function getCountByTable(tableName) {
return result;
}
-function getRowsByTableAndColumnList(tableName, columnList) {
- //return _api.get(tableName + '/rows/' + columnList.join(','))
- return undefined;
-}
function _testPageFail(reason) {
document.getElementById('heroStatus').classList.remove('is-success');
diff --git a/static/pageDriver.js b/static/pageDriver.js
index 6b6dfa4..7618922 100644
--- a/static/pageDriver.js
+++ b/static/pageDriver.js
@@ -26,45 +26,6 @@ var searchFields = document.querySelectorAll('input[data-searchTargetId]');
// Find all modalForms
var modalForms = document.querySelectorAll('form[data-targetTable]');
-// Iterate over all tables
-tables.forEach(async (table) => {
- console.log('Table found: ', table);
- // Get THEAD and TBODY elements
- const thead = table.querySelector('thead');
- const tbody = table.querySelector('tbody');
-
- // get index per column
- const columns = thead.querySelectorAll('th');
- const columnIndices = [];
- columns.forEach((column, index) => {
- columnIndices[column.getAttribute('data-dataCol')] = index;
- });
-
- // All required cols
- let requiredCols = [];
- columns.forEach((column) => {
- requiredCols.push(column.getAttribute('data-dataCol'));
- });
- console.log('Required columns: ', requiredCols);
-
- // Get data from API
- //let result = getRowsByTableAndColumnList(table.getAttribute("data-dataSource"), requiredCols);
- let result = await returnTableDataByTableName(table.getAttribute('data-dataSource'));
- // for (resultIndex in result) {
- // const row = result[resultIndex];
- // const tr = document.createElement("tr");
- // requiredCols.forEach(column => {
- // const td = document.createElement("td");
- // td.innerHTML = row[column];
- // tr.appendChild(td);
- // });
- // tbody.appendChild(tr);
- // }
- writeDataToTable(table, result);
-
- console.log('Column indices: ', columnIndices);
-});
-
console.info('Processing single values');
console.info(singleValues);
@@ -73,6 +34,42 @@ singleValues.forEach(async (singleValue) => {
writeSingelton(singleValue);
});
+// Iterate over all tables
+tables.forEach(async (table) => {
+ // Get THs and attach onClick event to sort
+ const ths = table.querySelectorAll('th');
+ ths.forEach((th) => {
+ th.style.cursor = 'pointer';
+ th.style.userSelect = 'none';
+ th.addEventListener('click', async function () {
+ const table = th.closest('table');
+ const order = th.getAttribute('data-order');
+ // Clear all other order attributes
+ ths.forEach((th) => {
+ if (th != this) {
+ th.removeAttribute('data-order');
+ th.classList.remove("bi-caret-up-fill")
+ th.classList.remove("bi-caret-down-fill")
+ }
+ });
+ if (order == 'ASC') {
+ th.setAttribute('data-order', 'DESC');
+ th.classList.add("bi-caret-down-fill")
+ th.classList.remove("bi-caret-up-fill")
+ } else {
+ th.setAttribute('data-order', 'ASC');
+ th.classList.add("bi-caret-up-fill")
+ th.classList.remove("bi-caret-down-fill")
+ }
+ refreshTable(table);
+ });
+ });
+ refreshTable(table);
+
+});
+
+
+
async function writeSingelton(element) {
const table = element.getAttribute('data-dataSource');
console.log('Table: ', table, ' Action: ', element.getAttribute('data-dataAction'), ' Element: ', element);
@@ -131,9 +128,6 @@ searchFields.forEach((searchField) => {
const column = target.getAttribute('data-dataCol');
const value = searchField.value;
console.log('Searching for ', value, ' in ', table, ' column ', column);
- //const result = await returnTableDataByTableNameWithSearch(table, value);
- //clearTable(target);
- //writeDataToTable(target, result);
refreshTableByName(table);
}
});
@@ -152,6 +146,8 @@ modalForms.forEach((modalForm) => {
event.target.classList.remove('is-danger');
}
});
+
+
getApiDescriptionByTable(modalForm.getAttribute('data-targetTable')).then((desc) => {
console.log('Description: ', desc);
const keys = desc['POST']['keys'];
@@ -246,9 +242,6 @@ modalForms.forEach((modalForm) => {
// TODO: Show error message
}
- // const target = document.getElementById(table);
- // writeDataToTable(target, result);
-
// Find all tables with data-searchTargetId set to table
setTimeout(() => {
refreshTableByName(table);
@@ -261,19 +254,73 @@ modalForms.forEach((modalForm) => {
async function refreshTable(table) {
// Refresh a table while keeping (optionally set) search value
const searchField = document.querySelector("input[data-searchTargetId='" + table.id + "']");
- if (searchField && searchField.value != '') {
+ // Get state of order and sort
+ const ths = table.querySelectorAll('th');
+ const columnIndices = [];
+ ths.forEach((th, index) => {
+ columnIndices[th.getAttribute('data-dataCol')] = index;
+ });
+ let order = '';
+ let column = '';
+ ths.forEach((th) => {
+ if (th.hasAttribute('data-order')) {
+ order = th.getAttribute('data-order');
+ column = th.getAttribute('data-dataCol');
+ }
+ });
+ console.log('Order: ', order, ' Column: ', column);
+
+ const maxLinesPerPage = table.getAttribute('data-pageSize');
+ let currentPage = table.getAttribute('data-currentPage');
+ if(currentPage == null) {
+ table.setAttribute('data-currentPage', 1);
+ currentPage = 1;
+ }
+
+ const start = (currentPage - 1) * maxLinesPerPage;
+ const end = start + maxLinesPerPage;
+
+ let paginationPassOnPre = {
+ 'start': start,
+ 'end': end,
+ 'currentPage': currentPage,
+ 'maxLinesPerPage': maxLinesPerPage
+ };
+
+ if (searchField) {
const value = searchField.value;
const dbTable = table.getAttribute('data-dataSource');
- const result = await returnTableDataByTableNameWithSearch(dbTable, value);
+ const result = await returnTableDataByTableName(dbTable, value, order, column, take= maxLinesPerPage, skip= start);
+ const totalResultCount = await getCountByTable(dbTable, value);
+ paginationPassOnPre['dataLength'] = totalResultCount;
+ var magMiddl = managePaginationMiddleware(result, paginationPassOnPre);
+ var data = magMiddl[0];
+ var paginationPassOn = magMiddl[1];
clearTable(table);
- writeDataToTable(table, result);
+ writeDataToTable(table, data, paginationPassOn);
} else {
- const result = await returnTableDataByTableName(table.getAttribute('data-dataSource'));
+ const result = await returnTableDataByTableName(table.getAttribute('data-dataSource'), undefined, order, column, take= maxLinesPerPage, skip= start);
+ const resultCount = await getCountByTable(table.getAttribute('data-dataSource'));
+ paginationPassOnPre['dataLength'] = resultCount;
+ var magMiddl = managePaginationMiddleware(result, paginationPassOnPre);
+ var data = magMiddl[0];
+ var paginationPassOn = magMiddl[1];
clearTable(table);
- writeDataToTable(table, result);
+ writeDataToTable(table, data, paginationPassOn);
}
}
+function managePaginationMiddleware(data, paginationPassOnPre) {
+ const maxLinesPerPage = paginationPassOnPre['maxLinesPerPage'];
+
+ const dataLength = paginationPassOnPre['dataLength'];
+ const maxPages = Math.ceil(dataLength / maxLinesPerPage);
+ paginationPassOn = paginationPassOnPre;
+ paginationPassOn['maxPages'] = maxPages;
+ // paginationPassOn['dataLength'] = dataLength;
+ return [data, paginationPassOn];
+}
+
async function refreshTableByName(name) {
const dirtyTables = document.querySelectorAll("table[data-dataSource='" + name + "']");
for (dirty of dirtyTables) {
@@ -293,7 +340,10 @@ function clearTable(table) {
tbody.innerHTML = '';
}
-function writeDataToTable(table, data) {
+function writeDataToTable(table, data, paginationPassOn) {
+ if(data == undefined || data == null || data.length == 0) {
+ return;
+ }
console.log('Writing data to table: ', table, data);
// Get THEAD and TBODY elements
const thead = table.querySelector('thead');
@@ -312,18 +362,106 @@ function writeDataToTable(table, data) {
requiredCols.push(column.getAttribute('data-dataCol'));
});
+ // Get paginationPassOn
+ const start = paginationPassOn['start'];
+ const end = paginationPassOn['end'];
+ const currentPage = paginationPassOn['currentPage'];
+ const maxLinesPerPage = paginationPassOn['maxLinesPerPage'];
+ const maxPages = paginationPassOn['maxPages'];
+ const dataLength = paginationPassOn['dataLength'];
+
+
+ // Find nav with class pagination and data-targetTable="table.id"
+ const paginationElement = document.querySelector("nav.pagination[data-targetTable='" + table.id + "']");
+ const paginationList = paginationElement.querySelector('ul.pagination-list');
+ console.log('Data length: ', dataLength, ' Max pages: ', maxPages);
+
+ if(maxPages > 1) {
+ // Clear pagination list
+ paginationList.innerHTML = '';
+
+ for (let i = 1; i <= maxPages; i++) {
+ const li = document.createElement('li');
+ li.innerHTML = '';
+ if(i == currentPage) {
+ li.querySelector('a').classList.add('is-current');
+ }
+ paginationList.appendChild(li);
+ }
+
+
+ // Remove unused pages, only leave first, last, current and 2 neighbors
+ let pages = paginationList.querySelectorAll('li');
+ let friends = []
+ // Always add first and last
+ friends.push(0);
+ friends.push(pages.length - 1);
+ friends.push(currentPage-1);
+
+ // Add direct neighbors
+ // friends.push(currentPage - 2);
+ friends.push(currentPage);
+ friends.push(currentPage - 2);
+
+
+ // Deduplicate friends
+ friends = [...new Set(friends)];
+ // Sort friends
+ friends.sort((a, b) => a - b);
+ // Parse friends (string to int)
+ friends = friends.map((x) => parseInt(x));
+
+ console.log('Friends: ', friends, ' Pages: ', pages.length, ' Current: ', currentPage);
+
+
+ // Remove everyone who is not a friend
+ for(let i = 0; i < pages.length; i++) {
+ if(friends.includes(i)) {
+ continue;
+ }
+ pages[i].remove();
+ }
+
+ // Find all gaps (step size bigger then 1) and add an ellipsis in between the two numbers
+ let last = 0;
+ for(let i = 0; i < friends.length; i++) {
+ if(friends[i] - last > 1) {
+ const li = document.createElement('li');
+ li.innerHTML = '';
+ paginationList.insertBefore(li, pages[friends[i]]);
+ }
+ last = friends[i];
+ }
+
+
+ // Append on click event to all pagination links
+ paginationList.querySelectorAll('a').forEach((link) => {
+ link.addEventListener('click', async function() {
+ const page = link.getAttribute('data-page');
+ table.setAttribute('data-currentPage', page);
+ refreshTable(table);
+ });
+ });
+
+ paginationElement.classList.remove('is-hidden');
+ } else {
+ paginationElement.classList.add('is-hidden');
+ }
+
+
for (resultIndex in data) {
const row = data[resultIndex];
const tr = document.createElement('tr');
requiredCols.forEach((column) => {
const td = document.createElement('td');
- td.innerHTML = row[column];
+ td.innerText = row[column];
tr.appendChild(td);
});
tbody.appendChild(tr);
}
}
+
// Handle modal
document.addEventListener('DOMContentLoaded', () => {
// Functions to open and close a modal
diff --git a/views/test.eta b/views/test.eta
index 15bc8c6..fb4e2e5 100644
--- a/views/test.eta
+++ b/views/test.eta
@@ -101,7 +101,7 @@