ContactBridge

Log | Files | Refs | README

commit 5eaf61f779f6a465f9d39c47d7e2a2bb304c0a3d
parent 1b6f3f4c960548c29ff300ba1627d2bd0b100dfc
Author: William Lindholm <william_lindholm@outlook.com>
Date:   Thu,  9 Nov 2023 11:05:22 +0100

Updated ajax table to include debounce function.

Diffstat:
MWebAPI/namespaces/messages.py | 2+-
MWebInterface/routes.py | 22++++++----------------
Mdatabase/database.db | 0
Mdatabase/repository.py | 20+++++++++++---------
Mstatic/css/main.css | 11++++++++++-
Mstatic/html/tabletest.html | 80++++++++++++++++++++++++++++++++++++++++----------------------------------------
Astatic/js/ajaxtable.js | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dstatic/js/pagination.js | 106-------------------------------------------------------------------------------
Atemplates/ajaxtable.html | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtemplates/base.html | 4++--
Mtemplates/home.html | 24+-----------------------
Mtemplates/messages_template.html | 4++--
Mtemplates/table.html | 2+-
13 files changed, 231 insertions(+), 201 deletions(-)

diff --git a/WebAPI/namespaces/messages.py b/WebAPI/namespaces/messages.py @@ -58,7 +58,7 @@ class MessageList(Resource): if sort_order not in ['ASC', 'DESC']: messages_ns.abort(400, "sort_order must be either 'ASC' or 'DESC'") - valid_sort_fields = ['id', 'name', 'email', 'subject', 'timestamp', 'relevance'] + valid_sort_fields = ['id', 'name', 'email', 'subject', 'timestamp', 'relevance', 'content'] if sort_by not in valid_sort_fields: messages_ns.abort(400, f"Invalid sort_by field. Must be one of {valid_sort_fields}") diff --git a/WebInterface/routes.py b/WebInterface/routes.py @@ -15,22 +15,12 @@ def home(): if not session.get('logged_in'): return redirect(url_for('web_interface.login')) - messages = db.get_all_messages() - - formatted_messages = [ - { - "id": message[0], - "name": message[1], - "email": message[2], - "subject": message[3], - "message_content": message[4], - "timestamp": message[5], - "relevance": message[6] - } - for message in messages - ] - - return render_template('home.html', messages=formatted_messages, page_title="Messages") + page_number = request.args.get('page', 1, type=int) + + tables = TableFactory() + message_table = tables.get_message_table(page_number) + + return render_template('home.html', page_title="home", table=message_table) @web_interface.route('/integrations') diff --git a/database/database.db b/database/database.db Binary files differ. diff --git a/database/repository.py b/database/repository.py @@ -8,22 +8,23 @@ class DbRepository: def __init__(self): self.connection = sqlite3.connect(self.DB_PATH, check_same_thread=False) - self.cursor = self.connection.cursor() self.setup_database() def execute_query(self, query, parameters=(), expect_result=False): - self.cursor.execute(query, parameters) + with self.connection: + cursor = self.connection.cursor() + cursor.execute(query, parameters) - if expect_result: - return self.cursor.fetchall() - else: - self.connection.commit() + if expect_result: + result = cursor.fetchall() + cursor.close() + return result def setup_database(self): with open(os.path.join(self.CURRENT_DIR, 'db.sql'), 'r') as sql_file: sql_script = sql_file.read() - self.connection.executescript(sql_script) - self.connection.commit() + with self.connection: + self.connection.executescript(sql_script) def close_connection(self): - self.connection.close() + self.connection.close() +\ No newline at end of file diff --git a/static/css/main.css b/static/css/main.css @@ -108,7 +108,6 @@ h1.mobile { } table tr, table td { - padding: 0; margin: 0 !important; } @@ -119,3 +118,12 @@ table tr, table td { .pagination .active { background-color: var(--primary-color) !important; } + +.sortable-header { + cursor: pointer; + transition: all 0.3s ease; +} + +.sortable-header:hover { + text-decoration: underline; +} +\ No newline at end of file diff --git a/static/html/tabletest.html b/static/html/tabletest.html @@ -6,48 +6,48 @@ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css"> </head> <body> - <div class="container"> - <h2>Messages</h2> - - <div class="input-field"> - <input type="text" id="search-bar" placeholder="Search Messages..." onkeyup="searchData()"> - <div id="loading-indicator" style="display: none;">Loading...</div> - </div> - - <table class="highlight responsive-table"> - <thead id="table-headers"> - <tr> - <th data-field="id" data-order="asc">ID</th> - <th data-field="name" data-order="asc">Name</th> - <th data-field="email" data-order="asc">Email</th> - <th data-field="subject" data-order="asc">Subject</th> - <th data-field="timestamp" data-order="asc">Timestamp</th> - <th data-field="relevance" data-order="asc">Relevance</th> - </tr> - </thead> - <tbody id="table-body"> - <!-- Table data --> - </tbody> - </table> - - <ul class="pagination" id="pagination-container"> - <!-- Pagination --> - </ul> - - <select id="page-size" onchange="searchData('messages')"> - <option value="5">5</option> - <option value="10" selected>10</option> - <option value="25">25</option> - <option value="50">50</option> - </select> +<div class="container"> + <h2>Messages</h2> + <div class="input-field"> + <input type="text" id="search-bar" placeholder="Search Messages..." onkeyup="searchData()"> + <div id="loading-indicator" style="display: none;">Loading...</div> </div> - <!-- Materialize JS --> - <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script> - - <script>const API_ENDPOINT = '/api/messages/';</script> - - <script src="/static/js/pagination.js"></script> + <table class="highlight responsive-table"> + <thead id="table-headers"> + <tr> + <th data-field="id" data-order="asc">ID</th> + <th data-field="name" data-order="asc">Name</th> + <th data-field="email" data-order="asc">Email</th> + <th data-field="subject" data-order="asc">Subject</th> + <th data-field="timestamp" data-order="asc">Timestamp</th> + <th data-field="relevance" data-order="asc">Relevance</th> + </tr> + </thead> + <tbody id="table-body"> + <!-- Table data --> + </tbody> + </table> + + <ul class="pagination" id="pagination-container"> + <!-- Pagination --> + </ul> + + <select id="page-size" onchange="searchData('messages')"> + <option value="5">5</option> + <option value="10" selected>10</option> + <option value="25">25</option> + <option value="50">50</option> + </select> + +</div> + +<!-- Materialize JS --> +<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script> + +<script>const API_ENDPOINT = '/api/messages/';</script> + +<script src="/static/js/pagination.js"></script> </body> </html> \ No newline at end of file diff --git a/static/js/ajaxtable.js b/static/js/ajaxtable.js @@ -0,0 +1,98 @@ +// Function to fetch data and update the table +function fetchData(endpoint, queryParams) { + const url = new URL(endpoint, window.location.origin); + Object.keys(queryParams).forEach(key => url.searchParams.append(key, queryParams[key])); + + const xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.setRequestHeader('Accept', 'text/html'); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4 && xhr.status === 200) { + document.getElementById('table-body').innerHTML = xhr.responseText; + const totalPages = xhr.getResponseHeader('X-total-pages'); + const currentPage = xhr.getResponseHeader('X-current-page'); + updatePagination(parseInt(currentPage, 10), parseInt(totalPages, 10), queryParams.page_size, endpoint); + } + }; + xhr.send(); +} + +function searchData() { + const searchInput = document.getElementById('search-bar').value; + const pageSize = document.getElementById('page-size').value; + const queryParams = { + query: searchInput, + page_size: pageSize + }; + fetchData(API_ENDPOINT, queryParams); +} + +function updatePagination(currentPage, totalPages, pageSize) { + const container = document.getElementById('pagination-container'); + container.innerHTML = ''; // Clear existing pagination + + // create back button + const backLi = document.createElement('li'); + backLi.className = currentPage === 1 ? 'disabled' : 'waves-effect'; + const backA = document.createElement('a'); + const backIcon = document.createElement('i'); + backIcon.className = 'material-icons'; + backIcon.textContent = 'chevron_left'; + backA.appendChild(backIcon); + backA.onclick = function() { + if (currentPage > 1) { + fetchData(API_ENDPOINT, { page: currentPage - 1, page_size: pageSize }); + } + }; + backLi.appendChild(backA); + container.appendChild(backLi); + + // Create pagination buttons + for (let i = 1; i <= totalPages; i++) { + const li = document.createElement('li'); + li.className = i === currentPage ? 'active' : 'waves-effect'; + const a = document.createElement('a'); + a.textContent = i; + a.onclick = function() { + fetchData(API_ENDPOINT, { page: i, page_size: pageSize }); + }; + li.appendChild(a); + container.appendChild(li); + } + + // create forward button + const forwardLi = document.createElement('li'); + forwardLi.className = currentPage === totalPages ? 'disabled' : 'waves-effect'; + const forwardA = document.createElement('a'); + const forwardIcon = document.createElement('i'); + forwardIcon.className = 'material-icons'; + forwardIcon.textContent = 'chevron_right'; + forwardA.appendChild(forwardIcon); + forwardA.onclick = function() { + if (currentPage < totalPages) { + fetchData(API_ENDPOINT, { page: currentPage + 1, page_size: pageSize }); + } + }; + forwardLi.appendChild(forwardA); + container.appendChild(forwardLi); +} + +// Function to sort table by clicking on headers +function sortTableByHeader() { + document.querySelectorAll('#table-headers th').forEach(header => { + header.addEventListener('click', () => { + const sortField = header.getAttribute('data-field'); + const sortOrder = header.getAttribute('data-order'); + const searchInput = document.getElementById('search-bar').value; + const pageSize = document.getElementById('page-size').value || 10; + const queryParams = { + query: searchInput, + sort_by: sortField, + sort_order: sortOrder, + page_size: pageSize + }; + fetchData(API_ENDPOINT, queryParams); + header.setAttribute('data-order', sortOrder === 'asc' ? 'desc' : 'asc'); + }); + }); +} +\ No newline at end of file diff --git a/static/js/pagination.js b/static/js/pagination.js @@ -1,105 +0,0 @@ -// Function to fetch data and update the table -function fetchData(endpoint, queryParams) { - const url = new URL(endpoint, window.location.origin); - Object.keys(queryParams).forEach(key => url.searchParams.append(key, queryParams[key])); - - const xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - xhr.setRequestHeader('Accept', 'text/html'); - xhr.onreadystatechange = function() { - if (xhr.readyState === 4 && xhr.status === 200) { - document.getElementById('table-body').innerHTML = xhr.responseText; - const totalPages = xhr.getResponseHeader('X-total-pages'); - const currentPage = xhr.getResponseHeader('X-current-page'); - updatePagination(parseInt(currentPage, 10), parseInt(totalPages, 10), queryParams.page_size, endpoint); - } - }; - xhr.send(); -} - -// handle search input for messages or users -function searchData() { - const searchInput = document.getElementById('search-bar').value; - const pageSize = document.getElementById('page-size').value; - const queryParams = { - query: searchInput, - page_size: pageSize - }; - fetchData(API_ENDPOINT, queryParams); -} - -// Function to update pagination -function updatePagination(currentPage, totalPages, pageSize) { - const container = document.getElementById('pagination-container'); - container.innerHTML = ''; // Clear existing pagination - - // Create the 'Back' button - const backLi = document.createElement('li'); - backLi.className = currentPage === 1 ? 'disabled' : 'waves-effect'; - const backA = document.createElement('a'); - backA.href = '#!'; - backA.innerHTML = '&laquo;'; - backA.onclick = function() { - if (currentPage > 1) { - fetchData(API_ENDPOINT, { page: currentPage - 1, page_size: pageSize }); - } - }; - backLi.appendChild(backA); - container.appendChild(backLi); - - // Create the page number buttons - for (let i = 1; i <= totalPages; i++) { - const li = document.createElement('li'); - li.className = i === currentPage ? 'active' : 'waves-effect'; - const a = document.createElement('a'); - a.href = '#!'; - a.textContent = i; - a.onclick = function() { - fetchData(API_ENDPOINT, { page: i, page_size: pageSize }); - }; - li.appendChild(a); - container.appendChild(li); - } - - // Create the 'Forward' button - const forwardLi = document.createElement('li'); - forwardLi.className = currentPage === totalPages ? 'disabled' : 'waves-effect'; - const forwardA = document.createElement('a'); - forwardA.href = '#!'; - forwardA.innerHTML = '&raquo;'; - forwardA.onclick = function() { - if (currentPage < totalPages) { - fetchData(API_ENDPOINT, { page: currentPage + 1, page_size: pageSize }); - } - }; - forwardLi.appendChild(forwardA); - container.appendChild(forwardLi); -} - -// Function to sort table by clicking on headers -function sortTableByHeader(type) { - document.querySelectorAll('#table-headers th').forEach(header => { - header.addEventListener('click', () => { - const sortField = header.getAttribute('data-field'); - const sortOrder = header.getAttribute('data-order'); - const searchInput = document.getElementById('search-bar').value; - const pageSize = document.getElementById('page-size').value || 10; - const queryParams = { - query: searchInput, - sort_by: sortField, - sort_order: sortOrder, - page_size: pageSize - }; - fetchData(`/api/${type}`, queryParams); - // Toggle sort order for the next click - header.setAttribute('data-order', sortOrder === 'asc' ? 'desc' : 'asc'); - }); - }); -} - -// Initial fetch for messages and setup headers sorting -document.addEventListener('DOMContentLoaded', function() { - M.AutoInit(); // Initialize all Materialize components - fetchData(API_ENDPOINT, { page: 1, page_size: 10 }); - sortTableByHeader('messages'); -}); -\ No newline at end of file diff --git a/templates/ajaxtable.html b/templates/ajaxtable.html @@ -0,0 +1,57 @@ +<div class="row"> + <div class="input-field col s10"> + <input type="text" id="search-bar" placeholder="Search Messages..."> + </div> + <div class="input-field col s2"> + <select id="page-size" onchange="searchData()"> + <option value="5">5</option> + <option value="10" selected>10</option> + <option value="25">25</option> + <option value="50">50</option> + </select> + </div> +</div> + +<table class="highlight table-instance"> + <thead id="table-headers"> + <tr> + <th data-field="id" data-order="asc" class="sortable-header">ID <i class="material-icons tiny">unfold_more</i></th> + <th data-field="relevance" data-order="asc" class="sortable-header">Relevance <i class="material-icons tiny">unfold_more</i></th> + <th data-field="name" data-order="asc" class="sortable-header">Name <i class="material-icons tiny">unfold_more</i></th> + <th data-field="content" data-order="asc" class="sortable-header">Content <i class="material-icons tiny">unfold_more</i></th> + </tr> + </thead> + <tbody id="table-body"> + <!-- Table data --> + </tbody> +</table> + +<ul class="pagination" id="pagination-container"> + <!-- Pagination --> +</ul> + +<script> + + const API_ENDPOINT = '/api/messages/'; + + document.addEventListener('DOMContentLoaded', function() { + M.AutoInit(); + const debouncedSearchData = debounce(searchData, 500); + document.getElementById('search-bar').addEventListener('keyup', debouncedSearchData); + sortTableByHeader('id'); + + fetchData(API_ENDPOINT, { page: 1, page_size: 10 }); + sortTableByHeader('id'); + }); + + function debounce(func, delay) { + let debounceTimer; + return function() { + const context = this; + const args = arguments; + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => func.apply(context, args), delay); + }; + } + +</script> +\ No newline at end of file diff --git a/templates/base.html b/templates/base.html @@ -6,12 +6,12 @@ <link rel="stylesheet" href="{{ url_for('static', filename='css/materialize.min.css') }}"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> - <script src="{{ url_for('static', filename='js/materialize.min.js') }}"></script> + <script defer src="{{ url_for('static', filename='js/materialize.min.js') }}"></script> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@100&display=swap" rel="stylesheet"> <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}"> - <script src="{{ url_for('static', filename='js/pagination.js') }}"></script> + <script defer src="{{ url_for('static', filename='js/ajaxtable.js') }}"></script> </head> <body> diff --git a/templates/home.html b/templates/home.html @@ -1,27 +1,5 @@ {% extends 'base.html' %} {% block content %} -<br> -<table class="highlight"> - <thead> - <tr> - <th>Sender</th> - <th>Email</th> - <th>Timestamp</th> - <th>Relevance</th> - <th class="truncate">Message</th> - </tr> - </thead> - <tbody> - {% for message in messages %} - <tr> - <td>{{ message.name }}</td> - <td>{{ message.email }}</td> - <td>{{ message.timestamp }}</td> - <td>{{ message.relevance }}</td> - <td class="truncate-td">{{ message.message_content }}</td> - </tr> - {% endfor %} - </tbody> -</table> +{% include 'ajaxtable.html' %} {% endblock %} \ No newline at end of file diff --git a/templates/messages_template.html b/templates/messages_template.html @@ -1 +1 @@ -{% for message in messages %}<tr><td>{{ message.id }}</td><td>{{ message.name }}</td><td>{{ message.email }}</td><td>{{ message.subject }}</td><td>{{ message.timestamp }}</td><td>{{ message.relevance }}</td></tr>{% endfor %} -\ No newline at end of file +{% for message in messages %}<tr><td>{{ message.id }}</td><td>{{ message.relevance }}</td><td>{{ message.name }}</td><td>{{ message.content }}</td></tr>{% endfor %} +\ No newline at end of file diff --git a/templates/table.html b/templates/table.html @@ -12,7 +12,7 @@ {% endfor %} </tr> </thead> - <tbody> + <tbody id="table-body"> {% for row in table.rows %} <tr onclick="window.location.href='{{ table.row_click_url_template.format(row[0]) }}';"> {% for cell in row %}