diff --git a/static/apiWrapper.js b/static/apiWrapper.js index 3e9fa0c..2270af3 100644 --- a/static/apiWrapper.js +++ b/static/apiWrapper.js @@ -1,109 +1,175 @@ -_wrapperVersion = "1.0.0" -_minApiVersion = "1.0.0" -_maxApiVersion = "1.0.0" +_wrapperVersion = '1.0.0'; +_minApiVersion = '1.0.0'; +_maxApiVersion = '1.0.0'; + +_defaultTTL = 60000; _apiConfig = { - "basePath": "/api/v1/" + basePath: '/api/v1/' +}; + +if (!window.localStorage) { + console.warn('Local Storage is not available, some features may not work'); } // Generic driver functions let _api = { - "get": async function(path) { + get: async function (path) { const options = { - headers: new Headers({'content-type': 'application/json'}) - }; - const response = await fetch(_apiConfig.basePath + path, options) + headers: new Headers({ 'content-type': 'application/json' }) + }; + const response = await fetch(_apiConfig.basePath + path, options); // Handle the response if (!response.ok) { - _testPageFail(response.statusText) - return + _testPageFail(response.statusText); + return; } - const result = await response.json() + const result = await response.json(); // Handle the result, was json valid? if (!result) { - _testPageFail("Invalid JSON response") - return - } - - return result; - }, - "post": async function(path, data) { - const options = { - method: 'POST', - headers: new Headers({'content-type': 'application/json'}), - body: JSON.stringify(data) - }; - const response = await fetch(_apiConfig.basePath + path, options) - // Handle the response - if (!response.ok) { - _testPageFail(response.statusText) - return - } - const result = await response.json() - // Handle the result, was json valid? - if (!result) { - _testPageFail("Invalid JSON response") - return + _testPageFail('Invalid JSON response'); + return; } return result; }, - "getAsync": function(path, callback) { + post: async function (path, data) { 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)) + method: 'POST', + headers: new Headers({ 'content-type': 'application/json' }), + body: JSON.stringify(data) + }; + const response = await fetch(_apiConfig.basePath + path, options); + // Handle the response + if (!response.ok) { + _testPageFail(response.statusText); + return; + } + const result = await response.json(); + // Handle the result, was json valid? + if (!result) { + _testPageFail('Invalid JSON response'); + return; + } + + 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)); + } +}; + +function getApiDescriptionByTable(tableName) { + const keyDesc = `desc:${tableName}`; + const keyTime = `${keyDesc}:time`; + const keyTTL = `${keyDesc}:ttl`; + + // Retrieve cached data + const description = JSON.parse(localStorage.getItem(keyDesc)); + const timeCreated = parseInt(localStorage.getItem(keyTime)); + const ttl = parseInt(localStorage.getItem(keyTTL)); + + // Check if valid cached data exists + if (description && timeCreated && ttl) { + const currentTime = Date.now(); + const age = currentTime - parseInt(timeCreated, 10); + if (age < parseInt(ttl, 10)) { + // Return cached data immediately + return Promise.resolve(description); + } else { + console.warn('Cached description expired; fetching new data'); + // Fetch new data, update cache, and return it + return fetchAndUpdateCache(tableName); + } + } else { + console.warn('No cached description; fetching from server'); + // Fetch data, update cache, and return it + return fetchAndUpdateCache(tableName); + } + + function fetchAndUpdateCache(tableName) { + return _api + .get(`${tableName}/describe`) + .then((data) => { + if (data) { + // Update local storage with new data + localStorage.setItem(keyDesc, JSON.stringify(data)); + localStorage.setItem(keyTime, Date.now().toString()); + localStorage.setItem(keyTTL, '60000'); // 60 seconds TTL + } + return data; // Return the fetched data + }) + .catch((error) => { + console.error('Failed to fetch description:', error); + // Fallback to cached data if available (even if expired) + return description || null; + }); } } - - function returnTableDataByTableName(tableName) { - return _api.get(tableName) + return _api.get(tableName); } function returnTableDataByTableNameWithSearch(tableName, search) { - return _api.get(tableName + "?search=" + search) - } + return _api.get(tableName + '?search=' + search); +} function returnTableDataByTableNameAsync(tableName, callback) { - _api.getAsync(tableName, callback) + _api.getAsync(tableName, callback); } async function getCountByTable(tableName) { - let result = await(_api.get(tableName + "?count")) - if(typeof result !== 'number') { - _testPageWarn("Count was not a number, was: " + result) - console.warn("Count was not a number, was: " + result) - return -1 + // Stored in `data:count:${tableName}` + let result = await _api.get(tableName + '?count=true'); + if (typeof result !== 'number') { + _testPageWarn('Count was not a number, was: ' + result); + console.warn('Count was not a number, was: ' + result); + return -1; } - return result + return result; } function getRowsByTableAndColumnList(tableName, columnList) { - //return _api.get(tableName + '/rows/' + columnList.join(',')) - return undefined + //return _api.get(tableName + '/rows/' + columnList.join(',')) + return undefined; } function _testPageFail(reason) { - document.getElementById("heroStatus").classList.remove("is-success") - document.getElementById("heroStatus").classList.add("is-danger") + document.getElementById('heroStatus').classList.remove('is-success'); + document.getElementById('heroStatus').classList.add('is-danger'); - document.getElementById("heroExplainer").innerHTML = "API Wrapper Test Failed, reason: " + reason + document.getElementById('heroExplainer').innerHTML = 'API Wrapper Test Failed, reason: ' + reason; } function _testPageWarn(reason) { - document.getElementById("heroStatus").classList.remove("is-success") - document.getElementById("heroStatus").classList.add("is-warning") + document.getElementById('heroStatus').classList.remove('is-success'); + document.getElementById('heroStatus').classList.add('is-warning'); - document.getElementById("heroExplainer").innerHTML = "API Wrapper Test Warning, reason: " + reason + document.getElementById('heroExplainer').innerHTML = 'API Wrapper Test Warning, reason: ' + reason; } function getServerVersion() { - return _api.get('version') + return _api.get('version'); } function createEntry(tableName, data) { - return _api.post(tableName, data) + invalidateCache(tableName); + return _api.post(tableName, data); } +function invalidateCache(tableName) { + const keyDesc = `desc:${tableName}`; + const keyTime = `${keyDesc}:time`; + const keyTTL = `${keyDesc}:ttl`; + + localStorage.removeItem(keyDesc); + localStorage.removeItem(keyTime); + localStorage.removeItem(keyTTL); +} diff --git a/static/pageDriver.js b/static/pageDriver.js index b835ba8..6b6dfa4 100644 --- a/static/pageDriver.js +++ b/static/pageDriver.js @@ -1,55 +1,55 @@ -_pageDriverVersion = "1.0.1"; +_pageDriverVersion = '1.0.1'; // Handle color for icon svg with id="logo" based on the current theme -const logo = document.getElementById("logo"); -if(logo) { - logo.style.fill = getComputedStyle(document.documentElement).getPropertyValue("--bulma-text"); +const logo = document.getElementById('logo'); +if (logo) { + logo.style.fill = getComputedStyle(document.documentElement).getPropertyValue('--bulma-text'); } -if(_wrapperVersion === undefined) { - console.error("API Wrapper not found; Please include the API Wrapper before including the Page Driver"); +if (_wrapperVersion === undefined) { + console.error('API Wrapper not found; Please include the API Wrapper before including the Page Driver'); exit(); } else { - console.log("API Wrapper found; Page Driver is ready to use"); + console.log('API Wrapper found; Page Driver is ready to use'); } // Find all tables on the page which have data-dataSource attribute -var tables = document.querySelectorAll("table[data-dataSource]"); +var tables = document.querySelectorAll('table[data-dataSource]'); //var tables = [] -// Get all single values with data-dataSource, data-dataCol and data-dataAction -var singleValues = document.querySelectorAll("span[data-dataSource]"); +// Get all single values with data-dataSource, data-dataCol and data-dataAction +var singleValues = document.querySelectorAll('span[data-dataSource]'); // Find all search fields with data-searchTargetId -var searchFields = document.querySelectorAll("input[data-searchTargetId]"); +var searchFields = document.querySelectorAll('input[data-searchTargetId]'); // Find all modalForms -var modalForms = document.querySelectorAll("form[data-targetTable]"); +var modalForms = document.querySelectorAll('form[data-targetTable]'); // Iterate over all tables -tables.forEach(async table => { - console.log("Table found: ", table); +tables.forEach(async (table) => { + console.log('Table found: ', table); // Get THEAD and TBODY elements - const thead = table.querySelector("thead"); - const tbody = table.querySelector("tbody"); + const thead = table.querySelector('thead'); + const tbody = table.querySelector('tbody'); // get index per column - const columns = thead.querySelectorAll("th"); + const columns = thead.querySelectorAll('th'); const columnIndices = []; columns.forEach((column, index) => { - columnIndices[column.getAttribute("data-dataCol")] = index; + columnIndices[column.getAttribute('data-dataCol')] = index; }); // All required cols let requiredCols = []; - columns.forEach(column => { - requiredCols.push(column.getAttribute("data-dataCol")); + columns.forEach((column) => { + requiredCols.push(column.getAttribute('data-dataCol')); }); - console.log("Required columns: ", requiredCols); + 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")) + let result = await returnTableDataByTableName(table.getAttribute('data-dataSource')); // for (resultIndex in result) { // const row = result[resultIndex]; // const tr = document.createElement("tr"); @@ -62,122 +62,192 @@ tables.forEach(async table => { // } writeDataToTable(table, result); - console.log("Column indices: ", columnIndices); + console.log('Column indices: ', columnIndices); }); -console.info("Processing single values"); +console.info('Processing single values'); console.info(singleValues); // Iterate over all single values -singleValues.forEach(async singleValue => { +singleValues.forEach(async (singleValue) => { writeSingelton(singleValue); }); async function writeSingelton(element) { - const table = element.getAttribute("data-dataSource"); - console.log("Table: ", table, " Action: ", element.getAttribute("data-dataAction"), " Element: ", element); - switch(element.getAttribute("data-dataAction")) { - case "COUNT": { - console.log("Count action found"); - element.innerHTML = (await getCountByTable(table)) + const table = element.getAttribute('data-dataSource'); + console.log('Table: ', table, ' Action: ', element.getAttribute('data-dataAction'), ' Element: ', element); + switch (element.getAttribute('data-dataAction')) { + case 'COUNT': { + console.log('Count action found'); + element.innerHTML = await getCountByTable(table); break; } - case "SPECIAL": { - if(table == "version") { - element.innerHTML = (await getServerVersion())["version"]; - break + case 'SPECIAL': { + if (table == 'version') { + element.innerHTML = (await getServerVersion())['version']; + break; } - } default: { - console.error("Unknown action found: ", element.getAttribute("data-dataAction")); + console.error('Unknown action found: ', element.getAttribute('data-dataAction')); break; } } - element.classList.remove("is-skeleton"); - + element.classList.remove('is-skeleton'); } - // Attach listeners to search fields -searchFields.forEach(searchField => { - searchField.addEventListener("input", async function() { - console.log("Search field changed: ", searchField); - const targetId = searchField.getAttribute("data-searchTargetId"); - const target = document.getElementById(targetId); - const table = target.getAttribute("data-dataSource"); - 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); - console.log("Result: ", result); - clearTable(target); - writeDataToTable(target, result); +searchFields.forEach((searchField) => { + // Apply restrictions to search field (min, max, chars, etc) + + getApiDescriptionByTable(document.getElementById(searchField.getAttribute('data-searchTargetId')).getAttribute('data-dataSource')).then((desc) => { + desc = desc['GET']['keys']['search']; + var rules = desc['rules']; + rules.forEach((rule) => { + switch (rule['name']) { + case 'min': { + searchField.setAttribute('minlength', rule['args']['limit']); + break; + } + case 'max': { + searchField.setAttribute('maxlength', rule['args']['limit']); + break; + } + } + }); + }); + searchField.addEventListener('input', async function () { + console.log('Search field changed: ', searchField); + if (searchField.checkValidity() == false) { + console.log('Invalid input'); + searchField.classList.add('is-danger'); + return; + } else { + searchField.classList.remove('is-danger'); + const targetId = searchField.getAttribute('data-searchTargetId'); + const target = document.getElementById(targetId); + const table = target.getAttribute('data-dataSource'); + 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); + } }); }); // Attach listeners to modal forms -modalForms.forEach(modalForm => { - modalForm.addEventListener("submit", async function(event) { +modalForms.forEach((modalForm) => { + // Add validation to form by using API description (everything is assumed POST for now) + modalForm.addEventListener('input', async function (event) { + if (event.target.checkValidity() == false) { + modalForm.querySelector("input[type='submit']").setAttribute('disabled', true); + event.target.classList.add('is-danger'); + return; + } else { + modalForm.querySelector("input[type='submit']").removeAttribute('disabled'); + event.target.classList.remove('is-danger'); + } + }); + getApiDescriptionByTable(modalForm.getAttribute('data-targetTable')).then((desc) => { + console.log('Description: ', desc); + const keys = desc['POST']['keys']; + // Apply resitrictions and types to form fields + for (key in keys) { + const field = modalForm.querySelector("input[name='" + key + "']"); + if (field) { + const rules = keys[key]['rules']; + const flags = keys[key]['flags']; + console.log('Field: ', field, ' Rules: ', rules, ' Flags: ', flags); + rules.forEach((rule) => { + switch (rule['name']) { + case 'min': { + field.setAttribute('minlength', rule['args']['limit']); + break; + } + case 'max': { + field.setAttribute('maxlength', rule['args']['limit']); + break; + } + case 'pattern': { + field.setAttribute('pattern', rule['args']['regex'].substring(1, rule['args']['regex'].length - 1)); + //field.setAttribute("pattern", "^[\\+]?[\\(]?[0-9]{3}[\\)]?[\\-\\s\\.]?[0-9]{3}[\\-\\s\\.]?[0-9]{4,9}$"); + break; + } + case 'type': { + //field.setAttribute("type", rule["args"]["type"]); + console.log('Type: ', rule['args']['type']); + break; + } + } + }); + if (flags) { + flags['presence'] == 'required' ? field.setAttribute('required', true) : field.removeAttribute('required'); + } + } + } + console.log('Keys: ', keys); + }); + + modalForm.addEventListener('submit', async function (event) { event.preventDefault(); // Check what button submitted the form and if it has data-actionBtn = save // If not, close modal const pressedBtn = event.submitter; - if(pressedBtn.getAttribute("data-actionBtn") != "save") { - modalForm.closest(".modal").classList.remove('is-active'); + if (pressedBtn.getAttribute('data-actionBtn') != 'save') { + modalForm.closest('.modal').classList.remove('is-active'); return; } - // Find .entryPhase and hide it - const entryPhase = modalForm.querySelector(".entryPhase"); - const loadPhase = modalForm.querySelector(".loadPhase"); - if(entryPhase) { - entryPhase.classList.add("is-hidden"); + const entryPhase = modalForm.querySelector('.entryPhase'); + const loadPhase = modalForm.querySelector('.loadPhase'); + if (entryPhase) { + entryPhase.classList.add('is-hidden'); } - if(loadPhase) { - loadPhase.classList.remove("is-hidden"); + if (loadPhase) { + loadPhase.classList.remove('is-hidden'); } - console.log("Form submitted: ", modalForm); - const table = modalForm.getAttribute("data-targetTable"); + console.log('Form submitted: ', modalForm); + const table = modalForm.getAttribute('data-targetTable'); const data = new FormData(modalForm); // Convert to JSON object let jsonData = {}; data.forEach((value, key) => { jsonData[key] = value; }); - console.log("JSON Data: ", jsonData); + console.log('JSON Data: ', jsonData); let resp = await createEntry(table, jsonData); - console.log("Response: ", resp); - if(resp["status"] == "CREATED") { - console.log("Entry created successfully"); - modalForm.closest(".modal").classList.remove('is-active'); + console.log('Response: ', resp); + if (resp['status'] == 'CREATED') { + console.log('Entry created successfully'); + modalForm.closest('.modal').classList.remove('is-active'); modalForm.reset(); // Hide loadPhase - if(loadPhase) { - loadPhase.classList.add("is-hidden"); + if (loadPhase) { + loadPhase.classList.add('is-hidden'); } // Show entryPhase - if(entryPhase) { - entryPhase.classList.remove("is-hidden"); + if (entryPhase) { + entryPhase.classList.remove('is-hidden'); } } else { // Hide loadPhase - if(loadPhase) { - loadPhase.classList.add("is-hidden"); + if (loadPhase) { + loadPhase.classList.add('is-hidden'); } // Show entryPhase - if(entryPhase) { - entryPhase.classList.remove("is-hidden"); + if (entryPhase) { + entryPhase.classList.remove('is-hidden'); } // TODO: Show error message } // const target = document.getElementById(table); // writeDataToTable(target, result); - - // Find all tables with data-searchTargetId set to table setTimeout(() => { @@ -191,14 +261,14 @@ 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) { + if (searchField && searchField.value != '') { const value = searchField.value; - const dbTable = table.getAttribute("data-dataSource"); + const dbTable = table.getAttribute('data-dataSource'); const result = await returnTableDataByTableNameWithSearch(dbTable, value); clearTable(table); writeDataToTable(table, result); } else { - const result = await returnTableDataByTableName(table.getAttribute("data-dataSource")); + const result = await returnTableDataByTableName(table.getAttribute('data-dataSource')); clearTable(table); writeDataToTable(table, result); } @@ -206,48 +276,47 @@ async function refreshTable(table) { async function refreshTableByName(name) { const dirtyTables = document.querySelectorAll("table[data-dataSource='" + name + "']"); - for(dirty of dirtyTables) { + for (dirty of dirtyTables) { refreshTable(dirty); } } async function updateSingeltonsByTableName(name) { const dirtySingles = document.querySelectorAll("span[data-dataSource='" + name + "']"); - for(dirty of dirtySingles) { + for (dirty of dirtySingles) { writeSingelton(dirty); } } - function clearTable(table) { - const tbody = table.querySelector("tbody"); - tbody.innerHTML = ""; + const tbody = table.querySelector('tbody'); + tbody.innerHTML = ''; } function writeDataToTable(table, data) { - console.log("Writing data to table: ", table, data); + console.log('Writing data to table: ', table, data); // Get THEAD and TBODY elements - const thead = table.querySelector("thead"); - const tbody = table.querySelector("tbody"); + const thead = table.querySelector('thead'); + const tbody = table.querySelector('tbody'); // get index per column - const columns = thead.querySelectorAll("th"); + const columns = thead.querySelectorAll('th'); const columnIndices = []; columns.forEach((column, index) => { - columnIndices[column.getAttribute("data-dataCol")] = index; + columnIndices[column.getAttribute('data-dataCol')] = index; }); // All required cols let requiredCols = []; - columns.forEach(column => { - requiredCols.push(column.getAttribute("data-dataCol")); + columns.forEach((column) => { + requiredCols.push(column.getAttribute('data-dataCol')); }); for (resultIndex in data) { const row = data[resultIndex]; - const tr = document.createElement("tr"); - requiredCols.forEach(column => { - const td = document.createElement("td"); + const tr = document.createElement('tr'); + requiredCols.forEach((column) => { + const td = document.createElement('td'); td.innerHTML = row[column]; tr.appendChild(td); }); @@ -255,47 +324,46 @@ function writeDataToTable(table, data) { } } - -// Handle modal +// Handle modal document.addEventListener('DOMContentLoaded', () => { // Functions to open and close a modal function openModal($el) { - $el.classList.add('is-active'); + $el.classList.add('is-active'); } - + function closeModal($el) { - $el.classList.remove('is-active'); + $el.classList.remove('is-active'); } - + function closeAllModals() { - (document.querySelectorAll('.modal') || []).forEach(($modal) => { - closeModal($modal); - }); + (document.querySelectorAll('.modal') || []).forEach(($modal) => { + closeModal($modal); + }); } - + // Add a click event on buttons to open a specific modal (document.querySelectorAll('.js-modal-trigger') || []).forEach(($trigger) => { - const modal = $trigger.dataset.target; - const $target = document.getElementById(modal); - - $trigger.addEventListener('click', () => { - openModal($target); - }); + const modal = $trigger.dataset.target; + const $target = document.getElementById(modal); + + $trigger.addEventListener('click', () => { + openModal($target); + }); }); - + // Add a click event on various child elements to close the parent modal - (document.querySelectorAll('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(($close) => { - const $target = $close.closest('.modal'); - - $close.addEventListener('click', () => { - closeModal($target); - }); + (document.querySelectorAll('.modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(($close) => { + const $target = $close.closest('.modal'); + + $close.addEventListener('click', () => { + closeModal($target); + }); }); - + // Add a keyboard event to close all modals document.addEventListener('keydown', (event) => { - if(event.key === "Escape") { - closeAllModals(); - } + if (event.key === 'Escape') { + closeAllModals(); + } }); - }); \ No newline at end of file +}); diff --git a/views/test.eta b/views/test.eta index c1aed77..15bc8c6 100644 --- a/views/test.eta +++ b/views/test.eta @@ -35,67 +35,68 @@ + - +

Alarm Kontakte