- Introduction of eveai-listview (to select objects) that is sortable, filterable, ...

- npm build does now also include building css files.
- Source javascript and css are now defined in the source directories (eveai_app or eveai_chat_client), and automatically built for use with nginx
- eveai.css is now split into several more manageable files (per control type)
This commit is contained in:
Josako
2025-07-11 15:25:28 +02:00
parent 42635a583c
commit acad28b623
92 changed files with 6339 additions and 5168 deletions

View File

@@ -0,0 +1,301 @@
<script type="module">
window.EveAI = window.EveAI || {};
window.EveAI.ListView = {
instances: {},
initialize: function(containerId, options = {}) {
const container = document.getElementById(containerId);
if (!container) {
console.error(`Container ${containerId} not found`);
return null;
}
// Default configuration
const config = {
data: [],
columns: [],
initialSort: [],
initialFilters: [],
filterableColumns: [],
selectable: true,
actions: [],
usePagination: true, // Nieuwe optie om paginering aan/uit te zetten
tableHeight: 700, // Standaard tabelhoogte als deze niet wordt gespecificeerd
...options
};
// Check if Tabulator is available
if (typeof window.Tabulator !== 'function') {
console.error('Tabulator not loaded (window.Tabulator missing).');
container.innerHTML = `<div class="alert alert-danger">
<strong>Error:</strong> Tabulator not loaded
</div>`;
return null;
}
try {
// Create Tabulator table
const tableContainer = document.createElement('div');
tableContainer.className = 'tabulator-container tabulator-list-view mt-3';
container.appendChild(tableContainer);
// Basisinstellingen voor tabel configuratie
const tableConfig = {
data: config.data,
columns: this._buildColumns(config.columns, config.selectable),
layout: "fitColumns",
// Conditionele paginatie of progressieve lading
...(config.usePagination ? {
pagination: "local",
paginationSize: 25,
paginationSizeSelector: [10, 25, 50, 100]
} : {
progressiveLoad: "scroll" // Gebruik progressieve lading voor grote datasets wanneer paginatie uitstaat
}),
// Gemeenschappelijke configuratie voor beide modi
movableColumns: true,
resizableRows: false, // Schakel dit uit om prestatieproblemen te voorkomen
initialSort: config.initialSort,
initialFilter: config.initialFilters,
selectable: 1, // Beperk tot maximaal 1 rij selectie
selectableRangeMode: "click",
selectableCheck: function() { return true; }, // Zorg ervoor dat alle cellen selecteerbaar zijn
rowClick: function(e, row) {
// Selecteer de rij bij klikken op willekeurige cel
if (config.selectable) {
console.log('Row clicked!', row.getData());
row.getTable().deselectRow();
row.select();
// Handmatig de rowSelectionChanged event aanroepen als extra maatregel
const instance = EveAI.ListView.instances[containerId];
if (instance) {
instance.selectedRow = row.getData();
EveAI.ListView._updateActionButtons(containerId);
}
}
},
cellClick: function(e, cell) {
// Gebruik ook cellClick om ervoor te zorgen dat klikken op een cel de rij selecteert
if (config.selectable && !e.target.matches('a, input, button, select, .tabulator-cell-editing')) {
console.log('Cell clicked!', cell.getData());
const row = cell.getRow();
row.getTable().deselectRow();
row.select();
// Handmatig de rowSelectionChanged event aanroepen als extra maatregel
const instance = EveAI.ListView.instances[containerId];
if (instance) {
instance.selectedRow = row.getData();
EveAI.ListView._updateActionButtons(containerId);
}
}
},
cellTap: function(e, cell) {
// Extra handler voor touch devices
if (config.selectable && !e.target.matches('a, input, button, select, .tabulator-cell-editing')) {
console.log('Cell tapped!', cell.getData());
const row = cell.getRow();
row.getTable().deselectRow();
row.select();
// Handmatig de rowSelectionChanged event aanroepen
const instance = EveAI.ListView.instances[containerId];
if (instance) {
instance.selectedRow = row.getData();
EveAI.ListView._updateActionButtons(containerId);
}
}
},
rowSelectionChanged: function(data, rows) {
console.log("Aantal geselecteerde rijen:", rows.length);
console.log("Geselecteerde rijen:", rows.map(r => r.getData()));
// Update de geselecteerde rij (enkelvoud)
EveAI.ListView.instances[containerId].selectedRow = rows.length > 0 ? rows[0].getData() : null;
EveAI.ListView._updateActionButtons(containerId);
},
rowFormatter: function(row) {
// Voeg cursor-style toe om aan te geven dat rijen klikbaar zijn
if (config.selectable) {
row.getElement().style.cursor = 'pointer';
}
},
placeholder: "No data available",
tooltips: false, // Schakel tooltips uit voor betere prestaties
responsiveLayout: false, // Schakel responsiveLayout uit om recursieve problemen te voorkomen
renderVerticalBuffer: 20, // Optimaliseer virtuele rendering
virtualDomBuffer: 80, // Optimaliseer virtuele DOM buffer
height: config.tableHeight // Gebruik de geconfigureerde tabel hoogte
};
// Maak Tabulator instantie met de geconfigureerde instellingen
const table = new Tabulator(tableContainer, tableConfig);
// Create action buttons
if (config.actions.length > 0) {
this._createActionButtons(container, config.actions, table);
}
// Store instance
this.instances[containerId] = {
table: table,
config: config,
selectedRow: null
};
// Registreer events met de Tabulator API voor betere compatibiliteit
table.on("rowClick", function(e, row){
console.log("Tabulator API: Row clicked!", row.getData());
if (config.selectable) {
// Stop event propagation om te voorkomen dat andere handlers interfereren
e.stopPropagation();
row.getTable().deselectRow();
row.select();
}
});
// Row selection event met de Tabulator API
table.on("rowSelectionChanged", function(data, rows){
console.log("Tabulator API: Aantal geselecteerde rijen:", rows.length);
console.log("Tabulator API: Geselecteerde rijen:", rows.map(r => r.getData()));
// Update de geselecteerde rij (enkelvoud)
EveAI.ListView.instances[containerId].selectedRow = rows.length > 0 ? rows[0].getData() : null;
EveAI.ListView._updateActionButtons(containerId);
});
// Opmerking: row selection event wordt nu afgehandeld via de Tabulator API
return table;
} catch (error) {
console.error('Error initializing ListView:', error);
container.innerHTML = `<div class="alert alert-danger">
<strong>Error loading data:</strong> ${error.message}
</div>`;
return null;
}
},
_buildColumns: function(columns, selectable) {
const tabulatorColumns = [];
// Add selection column if needed
{#if (selectable) {#}
{# tabulatorColumns.push({#}
{# formatter: "rowSelection", #}
{# titleFormatter: "rowSelection", #}
{# hozAlign: "center", #}
{# headerSort: false, #}
{# width: 60#}
{# });#}
{#}#}
// Add data columns
columns.forEach(col => {
// Maak een nieuwe schone kolom met alleen geldige Tabulator-eigenschappen
const column = {
title: col.title,
field: col.field,
headerSort: col.sortable !== false,
headerFilter: col.filterable !== false,
formatter: col.formatter,
width: col.width,
hozAlign: col.hozAlign,
vertAlign: col.vertAlign
};
// Zorg voor juiste numerieke sortering voor ID velden
if (col.field === 'id' || col.field.endsWith('_id') || col.type === 'number') {
column.sorter = 'number';
}
// Set appropriate header filter based on the data type
if (col.type === 'date') {
column.headerFilter = "input";
column.headerFilterParams = {type: "date"};
} else if (col.type === 'number') {
column.headerFilter = "number";
} else if (col.filterValues) {
column.headerFilter = "select";
column.headerFilterParams = {values: col.filterValues};
} else {
column.headerFilter = "input";
}
tabulatorColumns.push(column);
});
return tabulatorColumns;
},
_createActionButtons: function(container, actions, table) {
const actionSection = document.createElement('div');
actionSection.className = 'mt-3 d-flex justify-content-between';
const leftActions = document.createElement('div');
const rightActions = document.createElement('div');
actions.forEach(action => {
const button = document.createElement('button');
button.type = 'submit';
button.name = 'action';
button.value = action.value;
button.className = `btn ${action.class || 'btn-primary'} me-2`;
button.textContent = action.text;
button.disabled = action.requiresSelection !== false; // Disabled by default if requires selection
if (action.position === 'right') {
rightActions.appendChild(button);
} else {
leftActions.appendChild(button);
}
});
actionSection.appendChild(leftActions);
actionSection.appendChild(rightActions);
container.appendChild(actionSection);
},
_updateActionButtons: function(containerId) {
const instance = this.instances[containerId];
const container = document.getElementById(containerId);
const buttons = container.querySelectorAll('button[name="action"]');
buttons.forEach(button => {
// Enable/disable based on selection requirement
const action = instance.config.actions.find(a => a.value === button.value);
if (action && action.requiresSelection !== false) {
// Controleer of er een geselecteerde rij is
button.disabled = !instance.selectedRow;
}
});
// Update hidden input with selected row data
this._updateSelectedRowInput(containerId);
},
_updateSelectedRowInput: function(containerId) {
const instance = this.instances[containerId];
let hiddenInput = document.getElementById(`${containerId}-selected-row`);
if (!hiddenInput) {
hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'selected_row';
hiddenInput.id = `${containerId}-selected-row`;
document.getElementById(containerId).appendChild(hiddenInput);
}
if (instance.selectedRow) {
// Bewaar de geselecteerde rij-ID
hiddenInput.value = JSON.stringify({value: instance.selectedRow.id});
} else {
hiddenInput.value = '';
}
}
};
</script>

View File

@@ -1,22 +1,17 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="apple-touch-icon" sizes="76x76" href="{{url_for('static', filename='assets/img/apple-icon.png')}}">
<link rel="icon" type="image/png" href="{{url_for('static', filename='assets/img/favicon.png')}}">
<title>
{% block title %}{% endblock %}
</title>
<!-- Fonts and icons -->
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900|Roboto+Slab:400,700" />
<!-- Nucleo Icons -->
<link href="{{url_for('static', filename='assets/css/nucleo-icons.css" rel="stylesheet')}}" />
<link href="{{url_for('static', filename='assets/css/nucleo-svg.css" rel="stylesheet')}}" />
<!-- Font Awesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
<!-- Material Icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
<!-- CSS Files -->
<link id="pagestyle" href="{{url_for('static', filename='assets/css/material-kit-pro.css')}}" rel="stylesheet" />
<link id="pagestyle" href="{{url_for('static', filename='assets/css/eveai.css')}}" rel="stylesheet" />
</head>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="apple-touch-icon" sizes="76x76" href="{{url_for('static', filename='assets/img/apple-icon.png')}}">
<link rel="icon" type="image/png" href="{{url_for('static', filename='assets/img/favicon.png')}}">
<title>{% block title %}{% endblock %}</title>
<!-- Fonts blijven via CDN (sneller) -->
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900|Roboto+Slab:400,700" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
<!-- Gebundelde CSS (bevat nu al je CSS) -->
<link href="{{url_for('static', filename='dist/main.css')}}" rel="stylesheet" />
</head>

View File

@@ -1,27 +1,77 @@
{% extends 'base.html' %}
{% from 'macros.html' import render_selectable_table, render_pagination %}
{% block title %}Specialists{% endblock %}
{% block content_title %}Specialists{% endblock %}
{% block content_description %}View Specialists for Tenant{% endblock %}
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
{% block content %}
<div class="container">
<form method="POST" action="{{ url_for('interaction_bp.handle_specialist_selection') }}" id="specialistsForm">
{{ render_selectable_table(headers=["Specialist ID", "Name", "Type", "Type Version", "Active"], rows=rows, selectable=True, id="specialistsTable") }}
<div class="form-group mt-3 d-flex justify-content-between">
<div>
<button type="submit" name="action" value="edit_specialist" class="btn btn-primary" onclick="return validateTableSelection('specialistsForm')">Edit Specialist</button>
<button type="submit" name="action" value="execute_specialist" class="btn btn-primary" onclick="return validateTableSelection('specialistsForm')">Execute Specialist</button>
</div>
<button type="submit" name="action" value="create_specialist" class="btn btn-success">Register Specialist</button>
</div>
<form method="POST" action="{{ url_for('interaction_bp.handle_specialist_selection') }}" id="specialistsForm" onsubmit="return validateSpecialistSelection()">
<div id="specialists-list-view" class="tabulator-list-view"></div>
</form>
</div>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'interaction_bp.specialists') }}
{% endblock %}
<!-- Include the list view component -->
{% include 'eveai_list_view.html' %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Laad data met een kleine vertraging om de pagina eerst te laten renderen
setTimeout(() => {
// Initialize the list view met gepagineerde data en optimalisaties
const tabulatorTable = EveAI.ListView.initialize('specialists-list-view', {
data: {{ specialists_data | tojson }},
columns: {{ columns | tojson }},
initialSort: {{ initial_sort | tojson }},
actions: {{ actions | tojson }},
selectable: true,
usePagination: true, // Gebruik paginatie in plaats van progressieve lading
tableHeight: 800 // Hogere tabel voor specialists view
});
// Extra debug event listeners
if (tabulatorTable) {
console.log('Tabulator tabel succesvol geïnitialiseerd');
// Voeg een klik-event toe aan de hele tabel container voor een betere gebruikerservaring
document.querySelector('.tabulator-container').addEventListener('click', function(e) {
console.log('Tabulator container click event');
// De specifieke row click events worden afgehandeld door Tabulator zelf
});
} else {
console.error('Tabulator tabel kon niet worden geïnitialiseerd');
}
}, 50);
});
// Validation function for form submission
function validateSpecialistSelection() {
const selectedRow = document.getElementById('specialists-list-view-selected-row');
const actionButton = document.activeElement;
// Als de actie geen selectie vereist, ga dan altijd door
if (actionButton && actionButton.classList.contains('btn-success')) {
return true;
}
if (!selectedRow || !selectedRow.value) {
alert('Selecteer a.u.b. eerst een specialist.');
return false;
}
try {
const selection = JSON.parse(selectedRow.value);
// Controleer of er een specialist is geselecteerd
if (!selection.value) {
alert('Selecteer a.u.b. eerst een specialist.');
return false;
}
} catch (e) {
alert('Er is een fout opgetreden bij het verwerken van de selectie.');
return false;
}
return true;
}
</script>
{% endblock %}

View File

@@ -1,15 +1,14 @@
{#dist/main.js contains all used javascript libraries#}
<script src="{{url_for('static', filename='dist/main.js')}}"></script>
<script src="{{url_for('static', filename='assets/js/plugins/typedjs.js')}}"></script>
<script src="{{url_for('static', filename='assets/js/plugins/prism.min.js')}}"></script>
<script src="{{url_for('static', filename='assets/js/plugins/highlight.min.js')}}"></script>
<script src="{{url_for('static', filename='assets/js/plugins/parallax.min.js')}}"></script>
<script src="{{url_for('static', filename='assets/js/plugins/nouislider.min.js')}}"></script>
<script src="{{url_for('static', filename='assets/js/plugins/anime.min.js')}}"></script>
<script src="{{url_for('static', filename='assets/js/material-kit-pro.min.js')}}"></script>
<!-- Chat client bundle (alleen op chat paginas) -->
{% if 'chat' in request.endpoint or request.path.startswith('/chat') %}
<script src="{{url_for('static', filename='dist/chat-client.js')}}"></script>
{% endif %}
{% include 'eveai_json_editor.html' %}
{% include 'eveai_ordered_list_editor.html' %}
{% include 'eveai_list_view.html' %}
<script>
document.addEventListener('DOMContentLoaded', function() {