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 = '' + i + ''; + 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 @@

Alarm Kontakte

- +
@@ -112,7 +112,13 @@ +
Pos
+
<%~ include("partials/footer.eta") %>