From 423af9cce94e1f56080be63b643cb8e951e93b5d Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 18 Mar 2025 14:56:43 +0100 Subject: [PATCH 01/45] Migrate datatables from client to server side --- assets/src/legacy/attributeTable.js | 389 +++++++++--------- .../lizmap/controllers/datatables.classic.php | 88 ++++ 2 files changed, 273 insertions(+), 204 deletions(-) create mode 100644 lizmap/modules/lizmap/controllers/datatables.classic.php diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index cc7cae563a..ffdcc8f1ec 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1324,25 +1324,23 @@ var lizAttributeTable = function() { if( cFeatures && cFeatures.length > 0 ){ // Format features for datatable - formatDatatableFeatures( + var ff = formatDatatableFeatures( cFeatures, isChild, hiddenFields, lConfig['selectedFeatures'], lConfig['id'], - parentLayerID - ).then((ff) => { - var foundFeatures = ff.foundFeatures; - var dataSet = ff.dataSet; - - // Datatable configuration - if ( $.fn.dataTable.isDataTable( aTable ) ) { - var oTable = $( aTable ).dataTable(); - oTable.fnClearTable(); - oTable.fnAddData( dataSet ); - } - lConfig['features'] = foundFeatures; - }); + parentLayerID); + var foundFeatures = ff.foundFeatures; + var dataSet = ff.dataSet; + + // Datatable configuration + if ( $.fn.dataTable.isDataTable( aTable ) ) { + var oTable = $( aTable ).dataTable(); + oTable.fnClearTable(); + oTable.fnAddData( dataSet ); + } + lConfig['features'] = foundFeatures; } if ( !cFeatures || cFeatures.length == 0 ){ @@ -1484,150 +1482,152 @@ var lizAttributeTable = function() { var firstDisplayedColIndex = cdc.firstDisplayedColIndex; // Format features for datatable - formatDatatableFeatures( + var ff = formatDatatableFeatures( atFeatures, isChild, hiddenFields, lConfig['selectedFeatures'], lConfig['id'], parentLayerID, - pivotReference - ).then((ff) => { - var foundFeatures = ff.foundFeatures; - var dataSet = ff.dataSet; - - // Fill in the features object - // only when necessary : object is empty or is not child or (is child and no full features list in the object) - var refillFeatures = false; - var dLen = lConfig['features'] ? Object.keys(lConfig['features']).length : 0; - if( dLen == 0 ){ - refillFeatures = true; - if( !isChild ){ - lConfig['featuresFullSet'] = true; - } + pivotReference); + var foundFeatures = ff.foundFeatures; + var dataSet = ff.dataSet; + + // Fill in the features object + // only when necessary : object is empty or is not child or (is child and no full features list in the object) + var refillFeatures = false; + var dLen = lConfig['features'] ? Object.keys(lConfig['features']).length : 0; + if( dLen == 0 ){ + refillFeatures = true; + if( !isChild ){ + lConfig['featuresFullSet'] = true; } - else{ - if( isChild ){ - if( !lConfig['featuresFullSet'] ){ - refillFeatures = true; - } - }else{ - lConfig['featuresFullSet'] = true; + } + else{ + if( isChild ){ + if( !lConfig['featuresFullSet'] ){ refillFeatures = true; } + }else{ + lConfig['featuresFullSet'] = true; + refillFeatures = true; } - if( refillFeatures ) { - lConfig['features'] = foundFeatures; - } + } + if( refillFeatures ) { + lConfig['features'] = foundFeatures; + } - lConfig['alias'] = cAliases; - // Datatable configuration - if ( $.fn.dataTable.isDataTable( aTable ) ) { - var oTable = $( aTable ).dataTable(); - oTable.fnClearTable(); - oTable.fnAddData( dataSet ); + lConfig['alias'] = cAliases; + // Datatable configuration + if ( $.fn.dataTable.isDataTable( aTable ) ) { + var oTable = $( aTable ).dataTable(); + oTable.fnClearTable(); + oTable.fnAddData( dataSet ); + } + else { + // Search while typing in text input + // Deactivate if too many items + var searchWhileTyping = true; + if( dataLength > 500000 ){ + searchWhileTyping = false; } - else { - // Search while typing in text input - // Deactivate if too many items - var searchWhileTyping = true; - if( dataLength > 500000 ){ - searchWhileTyping = false; - } - var myDom = '<ipl>'; - if( searchWhileTyping ) { - $('#attribute-layer-search-' + cleanName).on( 'keyup', function (e){ - var searchVal = this.value; - lizdelay(function(){ - oTable.fnFilter( searchVal ); - }, 500 ); - }); - }else{ - myDom = '<ipl>'; - } + var myDom = '<ipl>'; + if( searchWhileTyping ) { + $('#attribute-layer-search-' + cleanName).on( 'keyup', function (e){ + var searchVal = this.value; + lizdelay(function(){ + oTable.fnFilter( searchVal ); + }, 500 ); + }); + }else{ + myDom = '<ipl>'; + } - $( aTable ).dataTable( { - data: dataSet - ,columns: columns - ,initComplete: function(settings, json) { - const api = new $.fn.dataTable.Api(settings); - const tableId = api.table().node().id; - const featureType = tableId.split('attribute-layer-table-')[1]; - - // Trigger event telling attribute table is ready - lizMap.events.triggerEvent("attributeLayerContentReady", - { - 'featureType': featureType - } - ); - } - ,order: [[ firstDisplayedColIndex, "asc" ]] - ,language: { url:globalThis['lizUrls']["dataTableLanguage"] } - ,deferRender: true - ,createdRow: function ( row, data, dataIndex ) { - if ( $.inArray( data.DT_RowId.toString(), lConfig['selectedFeatures'] ) != -1 - ) { - $(row).addClass('selected'); - data.lizSelected = 'a'; + const datatablesUrl = globalThis['lizUrls'].wms.replace('service', 'datatables'); + const params = globalThis['lizUrls'].params; + params['layerId'] = lConfig.id; + + $( aTable ).dataTable( { + serverSide: true + ,ajax: datatablesUrl + '?' + new URLSearchParams(params).toString() + ,columns: columns + ,initComplete: function(settings, json) { + const api = new $.fn.dataTable.Api(settings); + const tableId = api.table().node().id; + const featureType = tableId.split('attribute-layer-table-')[1]; + + // Trigger event telling attribute table is ready + lizMap.events.triggerEvent("attributeLayerContentReady", + { + 'featureType': featureType } + ); + } + ,order: [[ firstDisplayedColIndex, "asc" ]] + ,language: { url:globalThis['lizUrls']["dataTableLanguage"] } + ,deferRender: true + ,createdRow: function ( row, data, dataIndex ) { + if ( $.inArray( data.DT_RowId.toString(), lConfig['selectedFeatures'] ) != -1 + ) { + $(row).addClass('selected'); + data.lizSelected = 'a'; } - ,drawCallback: function (settings) { - // rendering ok, find img with data-attr-thumbnail - const thumbnailColl = document.getElementsByClassName('data-attr-thumbnail'); - for(let thumbnail of thumbnailColl) { - thumbnail.setAttribute('src', lizUrls.media+'?repository='+lizUrls.params.repository+'&project='+lizUrls.params.project+'&path='+thumbnail.dataset.src); - } + } + ,drawCallback: function (settings) { + // rendering ok, find img with data-attr-thumbnail + const thumbnailColl = document.getElementsByClassName('data-attr-thumbnail'); + for(let thumbnail of thumbnailColl) { + thumbnail.setAttribute('src', lizUrls.media+'?repository='+lizUrls.params.repository+'&project='+lizUrls.params.project+'&path='+thumbnail.dataset.src); } - ,dom: myDom - ,pageLength: 50 - ,scrollY: '95%' - ,scrollX: '100%' - - } ); + } + ,dom: myDom + ,pageLength: 10 + ,scrollY: '95%' + ,scrollX: '100%' - var oTable = $( aTable ).dataTable(); + } ); - if( !searchWhileTyping ) - $('#attribute-layer-search-' + cleanName).hide(); + var oTable = $( aTable ).dataTable(); - // Bind button which clears top-left search input content - $('#attribute-layer-search-' + cleanName).next('.clear-layer-search').click(function(){ - $('#attribute-layer-search-' + cleanName).val('').focus().keyup(); - }); + if( !searchWhileTyping ) + $('#attribute-layer-search-' + cleanName).hide(); - // Unbind previous events on page - $( aTable ).on( 'page.dt', function() { - // unbind previous events - $(aTable +' tr').unbind('click'); - $(aTable +' tr td button').unbind('click'); - }); + // Bind button which clears top-left search input content + $('#attribute-layer-search-' + cleanName).next('.clear-layer-search').click(function(){ + $('#attribute-layer-search-' + cleanName).val('').focus().keyup(); + }); - // Bind events when drawing table - $( aTable ).on( 'draw.dt', function() { + // Unbind previous events on page + $( aTable ).on( 'page.dt', function() { + // unbind previous events + $(aTable +' tr').unbind('click'); + $(aTable +' tr td button').unbind('click'); + }); - $(aTable +' tr').unbind('click'); - $(aTable +' tr td button').unbind('click'); + // Bind events when drawing table + $( aTable ).on( 'draw.dt', function() { - // Bind event when users click anywhere on the table line to highlight - bindTableLineClick(aName, aTable); + $(aTable +' tr').unbind('click'); + $(aTable +' tr td button').unbind('click'); - // Refresh size - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); + // Bind event when users click anywhere on the table line to highlight + bindTableLineClick(aName, aTable); - refreshDatatableSize('#' + mycontainerId); + // Refresh size + var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - return false; + refreshDatatableSize('#' + mycontainerId); - }); - } + return false; - // Check editable features - if (canEdit || canDelete) { - lizMap.mainLizmap.edition.fetchEditableFeatures([lConfig.id]); - } + }); + } - }); + // Check editable features + if (canEdit || canDelete) { + lizMap.mainLizmap.edition.fetchEditableFeatures([lConfig.id]); + } } if ( !cFeatures || cFeatures.length == 0 ){ @@ -1666,10 +1666,33 @@ var lizAttributeTable = function() { const columns = []; let firstDisplayedColIndex = 0; // Column with selected status - columns.push( {"data": "lizSelected", "width": "25px", "searchable": false, "sortable": true, "visible": false} ); + columns.push({ + data: "lizSelected", + width: "25px", + searchable: false, + sortable: true, + visible: false + }); firstDisplayedColIndex+=1; - columns.push({ "data": "featureToolbar", "width": "25px", "searchable": false, "sortable": false}); + columns.push({ + data: "featureToolbar", + width: "25px", + searchable: false, + sortable: false, + render: (data, type, row, meta) => { + const layerId = config.layers[aName].id; + const fid = row['DT_RowId']; + + // TODO: handle pivotId, parentLayerID and isChild + let pivotId; + let isChild; + let parentLayerID; + return ` + + `; + } + }); firstDisplayedColIndex += 1; // Add column for each field @@ -1889,86 +1912,46 @@ var lizAttributeTable = function() { * @param layerId * @param parentLayerID * @param pivotId - * - * @returns {Promise} */ function formatDatatableFeatures(atFeatures, isChild, hiddenFields, selectedFeatures, layerId, parentLayerID, pivotId = null){ - const waitForIt = (delay) => { - return new Promise((resolve) => setTimeout(resolve, delay)); - } - const mapFeatures = (features, isChild, hiddenFields, selectedFeatures, layerId, parentLayerID, pivotId = null) => { - return new Promise((resolve) => { - const dataSet = []; - const foundFeatures = {}; + var dataSet = []; + var foundFeatures = {}; + atFeatures.forEach(function(feat) { + var line = {}; - features.forEach(function(feat) { - const line = {}; + // add feature to layer global data + var fid = feat.id.split('.')[1]; + foundFeatures[fid] = feat; - // add feature to layer global data - const fid = feat.id.split('.')[1]; - foundFeatures[fid] = feat; + // Add row ID + line['DT_RowId'] = fid; + line['lizSelected'] = 'z'; - // Add row ID - line['DT_RowId'] = fid; - line['lizSelected'] = 'z'; - - if( selectedFeatures && selectedFeatures.indexOf(fid) != -1 ) - line.lizSelected = 'a'; - line['featureToolbar'] = - `` + - ``; - - // Build table lines - for (var idx in feat.properties){ - if( (hiddenFields.indexOf(idx) > -1) ) - continue; - var prop = feat.properties[idx]; - if (typeof prop == 'string') { - prop = DOMPurify.sanitize(prop, { - ADD_ATTR: ['target'] - }); - } - line[idx] = prop; - } + if( selectedFeatures && $.inArray( fid, selectedFeatures ) != -1 ) + line.lizSelected = 'a'; + line['featureToolbar'] = ``; - dataSet.push( line ); - }); - - resolve({ - 'dataSet': dataSet, - 'foundFeatures': foundFeatures - }); - }); - } - return new Promise(async (resolve) => { - const step = 500; - let start = 0; - let end = start+step; - let found = -1; - const timeout = 10; - var dataSet = []; - var foundFeatures = {}; - do { - const features = atFeatures.slice(start, end); - found = features.length; - if (found != 0) { - const result = await mapFeatures(features, isChild, hiddenFields, selectedFeatures, layerId, parentLayerID, pivotId); - Object.assign(foundFeatures, result.foundFeatures); - dataSet = dataSet.concat(result.dataSet); - await waitForIt(timeout); - start = end; - end = start+step; + // Build table lines + for (var idx in feat.properties){ + if( ($.inArray(idx, hiddenFields) > -1) ) + continue; + var prop = feat.properties[idx]; + if (typeof prop == 'string') { + prop = DOMPurify.sanitize(prop, { + ADD_ATTR: ['target'] + }); } - } while (found !== 0); - resolve({ - 'dataSet': dataSet, - 'foundFeatures': foundFeatures - }); + line[idx] = prop; + } + + + dataSet.push( line ); }); + return { + 'dataSet': dataSet, + 'foundFeatures': foundFeatures + }; } /** @@ -3295,9 +3278,7 @@ var lizAttributeTable = function() { if(refAttributeLayerConf && refAttributeLayerConf[1]?.pivot == 'True'){ // check if pivot is in relations for both layers const validRelation = [nlayerId,mLayerId].every((layerId)=>{ - return config.relations[layerId] && config.relations[layerId].some((rlayer)=>{ - return rlayer.referencingLayer == pivotId - }) + return config.relations[layerId] && config.relations[layerId].filter((rlayer)=>{ return rlayer.referencingLayer == pivotId}).length == 1 }) if (validRelation) return pivotId; diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php new file mode 100644 index 0000000000..ed6806c955 --- /dev/null +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -0,0 +1,88 @@ +getResponse('binary'); + $rep->outputFileName = 'datatables.json'; + $rep->mimeType = 'application/json'; + + // Lizmap parameters + $repository = $this->param('repository'); + $project = $this->param('project'); + $layerId = $this->param('layerId'); + + // DataTables parameters + $DTStart = $this->param('start'); + $DTLength = $this->param('length'); + + $DTOrder = $this->param('order'); + $DTColumns = $this->param('columns'); + $DTOrderColumnIndex = $DTOrder[0]['column']; + $DTOrderColumnDirection = $DTOrder[0]['dir'] == 'desc' ? 'd' : ''; + $DTOrderColumnName = $DTColumns[$DTOrderColumnIndex]['data']; + + $DTSearch = $this->param('search'); + + $lproj = lizmap::getProject($repository.'~'.$project); + $layer = $lproj->getLayer($layerId); + $typeName = $layer->getWfsTypeName(); + + $wfsparams = array( + 'SERVICE' => 'WFS', + 'VERSION' => '1.0.0', + 'REQUEST' => 'GetFeature', + 'TYPENAME' => $typeName, + 'OUTPUTFORMAT' => 'GeoJSON', + 'GEOMETRYNAME' => 'none', + 'MAXFEATURES' => $DTLength, + 'SORTBY' => $DTOrderColumnName.' '.$DTOrderColumnDirection, + 'STARTINDEX' => $DTStart, + ); + + $wfsrequest = new WFSRequest($lproj, $wfsparams, lizmap::getServices()); + $wfsresponse = $wfsrequest->process(); + $featureData = $wfsresponse->getBodyAsString(); + $jsonFeatures = json_decode($featureData)->features; + $data = array(); + foreach ($jsonFeatures as $key => $feature) { + $dataObject = array_merge( + array( + 'DT_RowId' => (int) explode('.', $feature->id)[1], + 'lizSelected' => '', + 'featureToolbar' => '', + ), + (array) $feature->properties + ); + $data[] = $dataObject; + } + // jLog::log(var_export(, true), 'error'); + + $returnedData = array( + 'draw' => (int) $this->param('draw'), + 'recordsTotal' => 31, // TODO: get the total number of features + 'recordsFiltered' => 31, + 'data' => $data, + // 'error' => 'error', + ); + + $rep->content = json_encode($returnedData); + // $rep->content = $wfsresponse; + + return $rep; + } +} From 26d20ed10de6d7145f323cda8c371271bbba399a Mon Sep 17 00:00:00 2001 From: nboisteault Date: Thu, 20 Mar 2025 17:26:57 +0100 Subject: [PATCH 02/45] DT: handle `selected` class for lines in `createdRow` callback --- assets/src/legacy/attributeTable.js | 75 +++-------------------------- 1 file changed, 7 insertions(+), 68 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index ffdcc8f1ec..4d37ba6827 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1567,10 +1567,9 @@ var lizAttributeTable = function() { ,order: [[ firstDisplayedColIndex, "asc" ]] ,language: { url:globalThis['lizUrls']["dataTableLanguage"] } ,deferRender: true - ,createdRow: function ( row, data, dataIndex ) { - if ( $.inArray( data.DT_RowId.toString(), lConfig['selectedFeatures'] ) != -1 - ) { - $(row).addClass('selected'); + , createdRow: (row, data) => { + if ((lConfig['selectedFeatures'].includes(data.DT_RowId.toString()))) { + row.classList.add('selected'); data.lizSelected = 'a'; } } @@ -3019,68 +3018,6 @@ var lizAttributeTable = function() { } - /** - * - * @param featureType - * @param featureIds - */ - function redrawAttributeTableContent( featureType, featureIds ){ - // Loop through all datatables to get the one concerning this featureType - $('.attribute-table-table[id]').each(function(){ - var tableId = $(this).attr('id'); - var tableLayerName = $(this).parents('div.dataTables_wrapper:first').prev('input.attribute-table-hidden-layer').val() - - if ( tableLayerName - && $.fn.dataTable.isDataTable( $(this) ) - && lizMap.cleanName( featureType ) == tableLayerName - ){ - // Get selected feature ids if not given - if( !featureIds ){ - // Assure selectedFeatures property exists for the layer - if( !config.layers[featureType]['selectedFeatures'] ) - config.layers[featureType]['selectedFeatures'] = []; - var featureIds = config.layers[featureType]['selectedFeatures']; - } - - // Get Datatable api - var rTable = $(this).DataTable(); - var dTable = $(this).dataTable(); - - // Remove class selected for all the lines - rTable - .rows( $(this).find('tr.selected') ) - .every(function(){ - dTable.fnUpdate( 'z', this, 0, false, false ); - }) - //~ .draw() - .nodes() - .to$() - .removeClass( 'selected' ) - ; - - // Add class selected from featureIds - // And change lizSelected column value to a - if( featureIds.length > 0 ){ - - var rTable = $(this).DataTable(); - rTable.data().each( function(d){ - if( $.inArray( d.DT_RowId.toString(), featureIds ) != -1 ) - d.lizSelected = 'a'; - }); - rTable - .rows( function ( idx, data, node ) { - return data.lizSelected == 'a' ? true : false; - }) - .nodes() - .to$() - .addClass( 'selected' ) - ; - } - } - - }); - } - /** * * @param featureType @@ -3388,8 +3325,10 @@ var lizAttributeTable = function() { // Update attribute table tools updateAttributeTableTools( e.featureType ); - // Redraw attribute table content ( = add "selected" classes) - redrawAttributeTableContent( e.featureType, e.featureIds ); + // Redraw attribute table content + // `createdRow` callback handles "selected" classes + const table = new DataTable('table[id^=attribute-layer-table-]'); + table.draw('page'); // Update openlayers layer drawing if( e.updateDrawing ) From 8eb720588dc367e7d25ccb63bb19918107817b01 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Fri, 21 Mar 2025 19:10:43 +0100 Subject: [PATCH 03/45] Attribute table: handle moveSelectedToTop w/ serverSide --- assets/src/legacy/attributeTable.js | 45 ++++++++++--------- .../lizmap/controllers/datatables.classic.php | 41 ++++++++++++++--- 2 files changed, 58 insertions(+), 28 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 4d37ba6827..1388859f20 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -45,6 +45,7 @@ var lizAttributeTable = function() { var wfsTypenameMap = {}; var mediaLinkPrefix = globalThis['lizUrls'].media + '?' + new URLSearchParams(globalThis['lizUrls'].params); var startupFilter = false; + let moveSelectedToTop = false; if( !( typeof lizLayerFilter === 'undefined' ) ){ startupFilter = true; lizMap.lizmapLayerFilterActive = true; @@ -775,27 +776,19 @@ var lizAttributeTable = function() { ); // Bind click on "move selected to top" button - $('#attribute-layer-'+ cleanName + ' button.btn-moveselectedtotop-attributeTable') - .click(function(){ - var aTable = '#attribute-layer-table-' + $(this).val(); - var dTable = $( aTable ).DataTable(); - var previousOrder = dTable.order(); - previousOrder = $.grep(previousOrder, function(o){ - return o[0] != 0; - }); - var selectedOrder = [ [0, 'asc'] ]; - var newOrder = selectedOrder.concat(previousOrder); - dTable.order( newOrder ).draw(); - - // Scroll to top - $(aTable).parents('div.attribute-layer-content').scrollTop(0); - - return false; - }) - .hover( - function(){ $(this).addClass('btn-primary'); }, - function(){ $(this).removeClass('btn-primary'); } - ); + const moveSelectedToTopSelector = '#attribute-layer-' + cleanName + ' button.btn-moveselectedtotop-attributeTable'; + document.querySelector(moveSelectedToTopSelector).addEventListener('click', (e) => { + const dTableSelector = '#attribute-layer-table-' + e.currentTarget.value; + const dTable = new DataTable(dTableSelector); + moveSelectedToTop = true; + dTable.draw(); + moveSelectedToTop = false; + + // Scroll to top + document.querySelector(dTableSelector).parentElement.scroll({ + top: 0 + }) ; + }); // Bind click on filter button @@ -1550,7 +1543,15 @@ var lizAttributeTable = function() { $( aTable ).dataTable( { serverSide: true - ,ajax: datatablesUrl + '?' + new URLSearchParams(params).toString() + ,ajax: { + url: datatablesUrl + '?' + new URLSearchParams(params).toString(), + data: (d) => { + if (moveSelectedToTop) { + d.moveselectedtotop = true; + d.selectedfeatureids = lConfig['selectedFeatures'].join(); + } + } + } ,columns: columns ,initComplete: function(settings, json) { const api = new $.fn.dataTable.Api(settings); diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index ed6806c955..1de7b83ee9 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -25,6 +25,8 @@ public function index() $repository = $this->param('repository'); $project = $this->param('project'); $layerId = $this->param('layerId'); + $moveSelectedToTop = $this->param('moveselectedtotop'); + $selectedFeatureIDs = explode(',', $this->param('selectedfeatureids')); // DataTables parameters $DTStart = $this->param('start'); @@ -42,7 +44,9 @@ public function index() $layer = $lproj->getLayer($layerId); $typeName = $layer->getWfsTypeName(); - $wfsparams = array( + $jsonFeatures = array(); + + $wfsParamsData = array( 'SERVICE' => 'WFS', 'VERSION' => '1.0.0', 'REQUEST' => 'GetFeature', @@ -54,10 +58,38 @@ public function index() 'STARTINDEX' => $DTStart, ); - $wfsrequest = new WFSRequest($lproj, $wfsparams, lizmap::getServices()); + if ($moveSelectedToTop == 'true') { + $featureIds = array(); + foreach ($selectedFeatureIDs as $id) { + $featureIds[] = $typeName.'.'.$id; + } + + $wfsrequest = new WFSRequest( + $lproj, + array( + 'SERVICE' => 'WFS', + 'VERSION' => '1.0.0', + 'REQUEST' => 'GetFeature', + 'OUTPUTFORMAT' => 'GeoJSON', + 'GEOMETRYNAME' => 'none', + 'FEATUREID' => implode(',', $featureIds), + ), + lizmap::getServices() + ); + $wfsresponse = $wfsrequest->process(); + $featureData = $wfsresponse->getBodyAsString(); + $jsonFeatures = json_decode($featureData)->features; + + // Remove selected features from the list of features to get + $DTLength = $DTLength - count($jsonFeatures); + $wfsParamsData['MAXFEATURES'] = $DTLength; + $wfsParamsData['EXP_FILTER'] = '$id NOT IN ('.implode(' , ', $selectedFeatureIDs).')'; + } + + $wfsrequest = new WFSRequest($lproj, $wfsParamsData, lizmap::getServices()); $wfsresponse = $wfsrequest->process(); $featureData = $wfsresponse->getBodyAsString(); - $jsonFeatures = json_decode($featureData)->features; + $jsonFeatures = array_merge($jsonFeatures, json_decode($featureData)->features); $data = array(); foreach ($jsonFeatures as $key => $feature) { $dataObject = array_merge( @@ -70,18 +102,15 @@ public function index() ); $data[] = $dataObject; } - // jLog::log(var_export(, true), 'error'); $returnedData = array( 'draw' => (int) $this->param('draw'), 'recordsTotal' => 31, // TODO: get the total number of features 'recordsFiltered' => 31, 'data' => $data, - // 'error' => 'error', ); $rep->content = json_encode($returnedData); - // $rep->content = $wfsresponse; return $rep; } From e2d907401f2373224cee9470d7e53b6506990308 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 25 Mar 2025 18:24:19 +0100 Subject: [PATCH 04/45] Attribute table: handle filter w/ serverSide --- assets/src/legacy/attributeTable.js | 46 ++++++++----------- .../lizmap/controllers/datatables.classic.php | 10 ++++ 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 1388859f20..d1987749c1 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1325,22 +1325,10 @@ var lizAttributeTable = function() { lConfig['id'], parentLayerID); var foundFeatures = ff.foundFeatures; - var dataSet = ff.dataSet; - - // Datatable configuration - if ( $.fn.dataTable.isDataTable( aTable ) ) { - var oTable = $( aTable ).dataTable(); - oTable.fnClearTable(); - oTable.fnAddData( dataSet ); - } lConfig['features'] = foundFeatures; } if ( !cFeatures || cFeatures.length == 0 ){ - if ( $.fn.dataTable.isDataTable( aTable ) ) { - var oTable = $( aTable ).dataTable(); - oTable.fnClearTable(); - } $(aTable).hide(); $('#attribute-layer-'+ cleanName +' span.attribute-layer-msg').html( @@ -1512,12 +1500,7 @@ var lizAttributeTable = function() { lConfig['alias'] = cAliases; // Datatable configuration - if ( $.fn.dataTable.isDataTable( aTable ) ) { - var oTable = $( aTable ).dataTable(); - oTable.fnClearTable(); - oTable.fnAddData( dataSet ); - } - else { + if ( !$.fn.dataTable.isDataTable( aTable ) ) { // Search while typing in text input // Deactivate if too many items var searchWhileTyping = true; @@ -1546,10 +1529,23 @@ var lizAttributeTable = function() { ,ajax: { url: datatablesUrl + '?' + new URLSearchParams(params).toString(), data: (d) => { + // Handle selected features moved to top if (moveSelectedToTop) { d.moveselectedtotop = true; d.selectedfeatureids = lConfig['selectedFeatures'].join(); } + + // Handle filtered features + const filteredFeaturesIds = lConfig.filteredFeatures; + if (filteredFeaturesIds && filteredFeaturesIds.length > 0) { + d.filteredfeatureids = filteredFeaturesIds.join(); + } + + // Handle features filtered by their parent + const exp_filter = lConfig.request_params.exp_filter; + if (exp_filter) { + d.exp_filter = exp_filter; + } } } ,columns: columns @@ -1631,10 +1627,6 @@ var lizAttributeTable = function() { } if ( !cFeatures || cFeatures.length == 0 ){ - if ( $.fn.dataTable.isDataTable( aTable ) ) { - var oTable = $( aTable ).dataTable(); - oTable.fnClearTable(); - } $(aTable).hide(); $('#attribute-layer-'+ cleanName +' span.attribute-layer-msg').html( @@ -2326,6 +2318,10 @@ var lizAttributeTable = function() { applyEmptyLayerFilter( typeName, typeNamePile, typeNameFilter, typeNameDone, cascade ); } + // Refresh attributeTable + const table = new DataTable('#attribute-layer-table-' + lizMap.cleanName(typeName)); + table.draw(); + $('#layerActionUnfilter').toggle((lizMap.lizmapLayerFilterActive !== null)); } @@ -2720,12 +2716,6 @@ var lizAttributeTable = function() { lizMap.mainLizmap.state.layersAndGroupsCollection.getLayerByName(layerConfig.name).expressionFilter = null; } - // Refresh attributeTable - var opTable = '#attribute-layer-table-'+lizMap.cleanName( typeName ); - if( $( opTable ).length ){ - refreshLayerAttributeDatatable(typeName, opTable, cFeatures); - } - // And send event so that getFeatureInfo and getPrint use the updated layer filters lizMap.events.triggerEvent("layerFilterParamChanged", { diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index 1de7b83ee9..44fd3a11f9 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -27,6 +27,8 @@ public function index() $layerId = $this->param('layerId'); $moveSelectedToTop = $this->param('moveselectedtotop'); $selectedFeatureIDs = explode(',', $this->param('selectedfeatureids')); + $filteredFeatureIDs = explode(',', $this->param('filteredfeatureids')); + $expFilter = $this->param('exp_filter'); // DataTables parameters $DTStart = $this->param('start'); @@ -86,6 +88,14 @@ public function index() $wfsParamsData['EXP_FILTER'] = '$id NOT IN ('.implode(' , ', $selectedFeatureIDs).')'; } + if (count($filteredFeatureIDs) > 0) { + $wfsParamsData['EXP_FILTER'] = '$id IN ('.implode(' , ', $filteredFeatureIDs).')'; + } + + if ($expFilter) { + $wfsParamsData['EXP_FILTER'] = $expFilter; + } + $wfsrequest = new WFSRequest($lproj, $wfsParamsData, lizmap::getServices()); $wfsresponse = $wfsrequest->process(); $featureData = $wfsresponse->getBodyAsString(); From 3d22007b505ba9bbbfd673fe322cb9484d0dbc64 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Thu, 27 Mar 2025 18:11:40 +0100 Subject: [PATCH 05/45] AttributeTable: add `data-layerid` in s attributes to ease selection of them --- assets/src/legacy/attributeTable.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index d1987749c1..1a92ab40cc 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -644,7 +644,8 @@ var lizAttributeTable = function() { } html+= '
'; html+= ' '; - html+= '
'; + const classes = 'attribute-table-table table table-hover table-condensed table-striped order-column cell-border'; + html+= '
'; html+= ''; // attribute-layer-content @@ -1119,9 +1120,10 @@ var lizAttributeTable = function() { var cDiv = '
'; var tId = 'attribute-layer-table-' + lizMap.cleanName(parentLayerName) + '-' + lizMap.cleanName(childLayerName); var tClass = 'attribute-table-table table table-hover table-condensed table-striped cell-border child-of-' + lizMap.cleanName(parentLayerName); + const dataLayerId = relation.referencingLayer; cDiv+= ' '; cDiv+= ' '; - cDiv+= '
'; + cDiv+= '
'; cDiv+= '
'; childDiv.push(cDiv); From a6fc5453d20c24e94b03b855b69e782eef0b9217 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Thu, 27 Mar 2025 18:14:27 +0100 Subject: [PATCH 06/45] Handle 'selected' state for rows without querying server --- assets/src/legacy/attributeTable.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 1a92ab40cc..61b6b6d885 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -3318,10 +3318,21 @@ var lizAttributeTable = function() { // Update attribute table tools updateAttributeTableTools( e.featureType ); - // Redraw attribute table content - // `createdRow` callback handles "selected" classes - const table = new DataTable('table[id^=attribute-layer-table-]'); - table.draw('page'); + // Update selected features in the table + const layerId = config.attributeLayers[e.featureType].layerId; + const selectedFeatures = config.layers[e.featureType].selectedFeatures; + const table = new DataTable('table[data-layerid=' + layerId + ']'); + + table.rows().every(function (rowIdx) { + var data = this.data(); + if ((selectedFeatures.includes(data.DT_RowId.toString()))) { + this.row(rowIdx).node().classList.add('selected'); + data.lizSelected = 'a'; + } else { + this.row(rowIdx).node().classList.remove('selected'); + data.lizSelected = 'z'; + } + }); // Update openlayers layer drawing if( e.updateDrawing ) From 26ab40c72223113a5e92767e3ffc73333251f6ba Mon Sep 17 00:00:00 2001 From: nboisteault Date: Fri, 28 Mar 2025 15:29:18 +0100 Subject: [PATCH 07/45] Get total number of records with WFS `RESULTTYPE=hits` --- .../lizmap/controllers/datatables.classic.php | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index 44fd3a11f9..108d1cf8a9 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -26,8 +26,14 @@ public function index() $project = $this->param('project'); $layerId = $this->param('layerId'); $moveSelectedToTop = $this->param('moveselectedtotop'); - $selectedFeatureIDs = explode(',', $this->param('selectedfeatureids')); - $filteredFeatureIDs = explode(',', $this->param('filteredfeatureids')); + $selectedFeatureIDs = array(); + if ($this->param('selectedfeatureids')) { + $selectedFeatureIDs = explode(',', $this->param('selectedfeatureids')); + } + $filteredFeatureIDs = array(); + if ($this->param('filteredfeatureids')) { + $filteredFeatureIDs = explode(',', $this->param('filteredfeatureids')); + } $expFilter = $this->param('exp_filter'); // DataTables parameters @@ -48,6 +54,23 @@ public function index() $jsonFeatures = array(); + // Get total number of features + $hits = 0; + $wfsParamsHits = array( + 'SERVICE' => 'WFS', + 'VERSION' => '1.0.0', + 'REQUEST' => 'GetFeature', + 'TYPENAME' => $typeName, + 'RESULTTYPE' => 'hits', + ); + + $wfsrequest = new WFSRequest($lproj, $wfsParamsHits, lizmap::getServices()); + $wfsresponse = $wfsrequest->process(); + $hitsData = $wfsresponse->getBodyAsString(); + preg_match('/numberOfFeatures="([0-9]+)"/', $hitsData, $matches); + $hits = $matches[1]; + + // Get features $wfsParamsData = array( 'SERVICE' => 'WFS', 'VERSION' => '1.0.0', @@ -115,8 +138,8 @@ public function index() $returnedData = array( 'draw' => (int) $this->param('draw'), - 'recordsTotal' => 31, // TODO: get the total number of features - 'recordsFiltered' => 31, + 'recordsTotal' => $hits, + 'recordsFiltered' => $hits, // TODO: implement filtering 'data' => $data, ); From 68864d61fc91cc6f9ddfbc3fd5bf3dfa53a22190 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Fri, 28 Mar 2025 15:31:06 +0100 Subject: [PATCH 08/45] Don't request WFS GetFeature at startup to get data as DT does --- assets/src/legacy/attributeTable.js | 313 +++++++++++----------------- 1 file changed, 125 insertions(+), 188 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 61b6b6d885..5ef99ffd27 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -335,11 +335,8 @@ var lizAttributeTable = function() { wfsParams['SRSNAME'] = 'EPSG:4326'; } - const getFeatureRequest = lizMap.mainLizmap.wfs.getFeature(wfsParams); - - let fetchRequests = [getFeatureRequest]; - let namedRequests = {'getFeature': fetchRequests.length-1}; - + let fetchRequests = []; + let namedRequests = []; if (!(layerConfig?.['alias'] && layerConfig?.['types'])) { const describeFeatureTypeRequest = lizMap.mainLizmap.wfs.describeFeatureType({ @@ -351,7 +348,7 @@ var lizAttributeTable = function() { const allColumnsKeyValues = {}; - // Indexes 0 and 1 are use for getFeature and describeFeature requests + // Index 0 is used for describeFeature requests namedRequests['keyValues'] = fetchRequests.length+0; let responseOrder = fetchRequests.length+0; for (const fieldName in lizMap.keyValueConfig?.[layerName]) { @@ -378,7 +375,7 @@ var lizAttributeTable = function() { })); } } - if (forceEmptyTable) return buildLayerAttributeDatatable(layerName, tableSelector, [], layerConfig.aliases, layerConfig.types, allColumnsKeyValues, callBack); + // if (forceEmptyTable) return buildLayerAttributeDatatable(layerName, tableSelector, [], layerConfig.aliases, layerConfig.types, allColumnsKeyValues, callBack); document.body.style.cursor = 'progress'; Promise.all(fetchRequests).then(responses => { @@ -398,14 +395,14 @@ var lizAttributeTable = function() { } layerConfig['featureCrs'] = 'EPSG:4326'; - if (namedRequests?.['describeFeatureType']) { + if (namedRequests.hasOwnProperty('describeFeatureType') ) { const describeFeatureTypeResponse = responses[namedRequests['describeFeatureType']]; layerConfig['aliases'] = describeFeatureTypeResponse.aliases; layerConfig['types'] = describeFeatureTypeResponse.types; layerConfig['columns'] = describeFeatureTypeResponse.columns; } - buildLayerAttributeDatatable(layerName, tableSelector, responses[0].features, layerConfig.aliases, layerConfig.types, allColumnsKeyValues, callBack); + buildLayerAttributeDatatable(layerName, tableSelector, layerConfig.aliases, layerConfig.types, allColumnsKeyValues, callBack); document.body.style.cursor = 'default'; }).catch(() => { @@ -1350,13 +1347,12 @@ var lizAttributeTable = function() { * * @param aName * @param aTable - * @param cFeatures * @param cAliases * @param cTypes * @param allColumnsKeyValues * @param aCallback */ - function buildLayerAttributeDatatable(aName, aTable, cFeatures, cAliases, cTypes, allColumnsKeyValues, aCallback ) { + function buildLayerAttributeDatatable(aName, aTable, cAliases, cTypes, allColumnsKeyValues, aCallback ) { // Get config var lConfig = config.layers[aName]; @@ -1445,202 +1441,137 @@ var lizAttributeTable = function() { canDelete = true; } - cFeatures = typeof cFeatures !== 'undefined' ? cFeatures : null; - if( !cFeatures ){ - // features is an object, let's transform it to an array - // XXX IE compat: Object.values is not available on IE... - var features = config.layers[aName]['features']; - cFeatures = Object.keys(features).map(function (key) { - return features[key]; - }); - } - - var atFeatures = cFeatures; - var dataLength = atFeatures.length; - - if( cFeatures && cFeatures.length > 0 ){ - // Create columns for datatable - var cdc = createDatatableColumns(aName, atFeatures, hiddenFields, cAliases, cTypes, allColumnsKeyValues); - var columns = cdc.columns; - var firstDisplayedColIndex = cdc.firstDisplayedColIndex; - - // Format features for datatable - var ff = formatDatatableFeatures( - atFeatures, - isChild, - hiddenFields, - lConfig['selectedFeatures'], - lConfig['id'], - parentLayerID, - pivotReference); - var foundFeatures = ff.foundFeatures; - var dataSet = ff.dataSet; - - // Fill in the features object - // only when necessary : object is empty or is not child or (is child and no full features list in the object) - var refillFeatures = false; - var dLen = lConfig['features'] ? Object.keys(lConfig['features']).length : 0; - if( dLen == 0 ){ - refillFeatures = true; - if( !isChild ){ - lConfig['featuresFullSet'] = true; - } - } - else{ - if( isChild ){ - if( !lConfig['featuresFullSet'] ){ - refillFeatures = true; - } - }else{ - lConfig['featuresFullSet'] = true; - refillFeatures = true; - } - } - if( refillFeatures ) { - lConfig['features'] = foundFeatures; - } - - lConfig['alias'] = cAliases; - // Datatable configuration - if ( !$.fn.dataTable.isDataTable( aTable ) ) { - // Search while typing in text input - // Deactivate if too many items - var searchWhileTyping = true; - if( dataLength > 500000 ){ - searchWhileTyping = false; - } - - var myDom = '<ipl>'; - if( searchWhileTyping ) { - $('#attribute-layer-search-' + cleanName).on( 'keyup', function (e){ - var searchVal = this.value; - lizdelay(function(){ - oTable.fnFilter( searchVal ); - }, 500 ); - }); - }else{ - myDom = '<ipl>'; - } - - const datatablesUrl = globalThis['lizUrls'].wms.replace('service', 'datatables'); - const params = globalThis['lizUrls'].params; - params['layerId'] = lConfig.id; - - $( aTable ).dataTable( { - serverSide: true - ,ajax: { - url: datatablesUrl + '?' + new URLSearchParams(params).toString(), - data: (d) => { - // Handle selected features moved to top - if (moveSelectedToTop) { - d.moveselectedtotop = true; - d.selectedfeatureids = lConfig['selectedFeatures'].join(); - } + // Create columns for datatable + var cdc = createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues); + var columns = cdc.columns; + var firstDisplayedColIndex = cdc.firstDisplayedColIndex; + + lConfig['alias'] = cAliases; + // Datatable configuration + if ( !$.fn.dataTable.isDataTable( aTable ) ) { + // Search while typing in text input + // Deactivate if too many items + var searchWhileTyping = true; + + var myDom = '<ipl>'; + if( searchWhileTyping ) { + $('#attribute-layer-search-' + cleanName).on( 'keyup', function (e){ + var searchVal = this.value; + lizdelay(function(){ + oTable.fnFilter( searchVal ); + }, 500 ); + }); + }else{ + myDom = '<ipl>'; + } + + const datatablesUrl = globalThis['lizUrls'].wms.replace('service', 'datatables'); + const params = globalThis['lizUrls'].params; + params['layerId'] = lConfig.id; + + $( aTable ).dataTable( { + serverSide: true + ,ajax: { + url: datatablesUrl + '?' + new URLSearchParams(params).toString(), + type: 'POST', + data: (d) => { + // Handle selected features moved to top + if (moveSelectedToTop) { + d.moveselectedtotop = true; + d.selectedfeatureids = lConfig['selectedFeatures'].join(); + } - // Handle filtered features - const filteredFeaturesIds = lConfig.filteredFeatures; - if (filteredFeaturesIds && filteredFeaturesIds.length > 0) { - d.filteredfeatureids = filteredFeaturesIds.join(); - } + // Handle filtered features + const filteredFeaturesIds = lConfig.filteredFeatures; + if (filteredFeaturesIds && filteredFeaturesIds.length > 0) { + d.filteredfeatureids = filteredFeaturesIds.join(); + } - // Handle features filtered by their parent - const exp_filter = lConfig.request_params.exp_filter; - if (exp_filter) { - d.exp_filter = exp_filter; - } + // Handle features filtered by their parent + const exp_filter = lConfig.request_params.exp_filter; + if (exp_filter) { + d.exp_filter = exp_filter; } } - ,columns: columns - ,initComplete: function(settings, json) { - const api = new $.fn.dataTable.Api(settings); - const tableId = api.table().node().id; - const featureType = tableId.split('attribute-layer-table-')[1]; - - // Trigger event telling attribute table is ready - lizMap.events.triggerEvent("attributeLayerContentReady", - { - 'featureType': featureType - } - ); - } - ,order: [[ firstDisplayedColIndex, "asc" ]] - ,language: { url:globalThis['lizUrls']["dataTableLanguage"] } - ,deferRender: true - , createdRow: (row, data) => { - if ((lConfig['selectedFeatures'].includes(data.DT_RowId.toString()))) { - row.classList.add('selected'); - data.lizSelected = 'a'; + } + ,columns: columns + ,initComplete: function(settings, json) { + const api = new $.fn.dataTable.Api(settings); + const tableId = api.table().node().id; + const featureType = tableId.split('attribute-layer-table-')[1]; + + // Trigger event telling attribute table is ready + lizMap.events.triggerEvent("attributeLayerContentReady", + { + 'featureType': featureType } + ); + } + ,order: [[ firstDisplayedColIndex, "asc" ]] + ,language: { url:globalThis['lizUrls']["dataTableLanguage"] } + ,deferRender: true + , createdRow: (row, data) => { + if ((lConfig['selectedFeatures'].includes(data.DT_RowId.toString()))) { + row.classList.add('selected'); + data.lizSelected = 'a'; } - ,drawCallback: function (settings) { - // rendering ok, find img with data-attr-thumbnail - const thumbnailColl = document.getElementsByClassName('data-attr-thumbnail'); - for(let thumbnail of thumbnailColl) { - thumbnail.setAttribute('src', lizUrls.media+'?repository='+lizUrls.params.repository+'&project='+lizUrls.params.project+'&path='+thumbnail.dataset.src); - } + } + ,drawCallback: function (settings) { + // rendering ok, find img with data-attr-thumbnail + const thumbnailColl = document.getElementsByClassName('data-attr-thumbnail'); + for(let thumbnail of thumbnailColl) { + thumbnail.setAttribute('src', lizUrls.media+'?repository='+lizUrls.params.repository+'&project='+lizUrls.params.project+'&path='+thumbnail.dataset.src); } - ,dom: myDom - ,pageLength: 10 - ,scrollY: '95%' - ,scrollX: '100%' - - } ); + } + ,dom: myDom + ,pageLength: 50 + ,scrollY: '95%' + ,scrollX: '100%' + } ); - var oTable = $( aTable ).dataTable(); + var oTable = $( aTable ).dataTable(); - if( !searchWhileTyping ) - $('#attribute-layer-search-' + cleanName).hide(); + if( !searchWhileTyping ) + $('#attribute-layer-search-' + cleanName).hide(); - // Bind button which clears top-left search input content - $('#attribute-layer-search-' + cleanName).next('.clear-layer-search').click(function(){ - $('#attribute-layer-search-' + cleanName).val('').focus().keyup(); - }); - - // Unbind previous events on page - $( aTable ).on( 'page.dt', function() { - // unbind previous events - $(aTable +' tr').unbind('click'); - $(aTable +' tr td button').unbind('click'); - }); + // Bind button which clears top-left search input content + $('#attribute-layer-search-' + cleanName).next('.clear-layer-search').click(function(){ + $('#attribute-layer-search-' + cleanName).val('').focus().keyup(); + }); - // Bind events when drawing table - $( aTable ).on( 'draw.dt', function() { + // Unbind previous events on page + $( aTable ).on( 'page.dt', function() { + // unbind previous events + $(aTable +' tr').unbind('click'); + $(aTable +' tr td button').unbind('click'); + }); - $(aTable +' tr').unbind('click'); - $(aTable +' tr td button').unbind('click'); + // Bind events when drawing table + $( aTable ).on( 'draw.dt', function() { - // Bind event when users click anywhere on the table line to highlight - bindTableLineClick(aName, aTable); + $(aTable +' tr').unbind('click'); + $(aTable +' tr td button').unbind('click'); - // Refresh size - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); + // Bind event when users click anywhere on the table line to highlight + bindTableLineClick(aName, aTable); - refreshDatatableSize('#' + mycontainerId); + // Refresh size + var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - return false; + refreshDatatableSize('#' + mycontainerId); - }); - } + return false; - // Check editable features - if (canEdit || canDelete) { - lizMap.mainLizmap.edition.fetchEditableFeatures([lConfig.id]); - } + }); } - if ( !cFeatures || cFeatures.length == 0 ){ - $(aTable).hide(); - - $('#attribute-layer-'+ cleanName +' span.attribute-layer-msg').html( - lizDict['attributeLayers.toolbar.msg.data.nodata'] + ' ' + lizDict['attributeLayers.toolbar.msg.data.extent'] - ).addClass('failure'); - - } else { - $(aTable).show(); - refreshDatatableSize('#'+$('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id')) - + // Check editable features + if (canEdit || canDelete) { + lizMap.mainLizmap.edition.fetchEditableFeatures([lConfig.id]); } + refreshDatatableSize('#'+$('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id')) + if (aCallback) aCallback(aName,aTable); @@ -1656,7 +1587,7 @@ var lizAttributeTable = function() { * @param cTypes * @param allColumnsKeyValues */ - function createDatatableColumns(aName, atFeatures, hiddenFields, cAliases, cTypes, allColumnsKeyValues){ + function createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues){ const columns = []; let firstDisplayedColIndex = 0; // Column with selected status @@ -1690,7 +1621,7 @@ var lizAttributeTable = function() { firstDisplayedColIndex += 1; // Add column for each field - for (var columnName in atFeatures[0].properties){ + for (var columnName in cAliases){ // Do not add hidden fields if (hiddenFields.includes(columnName)){ continue; @@ -1736,8 +1667,14 @@ var lizAttributeTable = function() { let davConf = globalThis['lizUrls'].webDavUrl && globalThis['lizUrls']?.resourceUrlReplacement?.webdav && config.layers[aName]?.webDavFields && Array.isArray(config.layers[aName].webDavFields) && config.layers[aName].webDavFields.includes(columnName); colConf['render'] = function (data, type, row, meta) { // Replace media and URL with links - if (!data || !(typeof data === 'string')) + if (!data || !(typeof data === 'string')){ return data; + } + // Sanitize 'string' data + data = DOMPurify.sanitize(data, { + ADD_ATTR: ['target'] + }); + if (davConf) { // replace the root of the url if(data.startsWith(globalThis['lizUrls'].webDavUrl)){ From 8c1adb86ddd64102d6282db8788ba4e1d7dadf86 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Fri, 28 Mar 2025 15:32:00 +0100 Subject: [PATCH 09/45] e2e: refactor attributeTable tests TODO: assert DT requests are correct --- .../integration/attribute_table-ghaction.js | 52 ++++++------------- 1 file changed, 15 insertions(+), 37 deletions(-) diff --git a/tests/end2end/cypress/integration/attribute_table-ghaction.js b/tests/end2end/cypress/integration/attribute_table-ghaction.js index bee9e2eecb..db95ce87f3 100644 --- a/tests/end2end/cypress/integration/attribute_table-ghaction.js +++ b/tests/end2end/cypress/integration/attribute_table-ghaction.js @@ -35,6 +35,10 @@ describe('Attribute table', () => { }) }).as('getMap') + cy.intercept('POST', '*datatables*', (req) => { + req.alias = 'postToDataTables' + }) + cy.visit('/index.php/view/map/?repository=testsrepository&project=attribute_table') // Wait for map displayed @@ -49,12 +53,12 @@ describe('Attribute table', () => { cy.get('button[value="quartiers_shp"].btn-open-attribute-layer').click({ force: true }) // Wait for features - cy.wait('@postGetFeature').then((interception) => { + cy.wait('@postDescribeFeatureType').then((interception) => { expect(interception.request.body) .to.contain('SERVICE=WFS') - .to.contain('REQUEST=GetFeature') + .to.contain('REQUEST=DescribeFeatureType') .to.contain('TYPENAME=quartiers_shp') - .to.contain('OUTPUTFORMAT=GeoJSON') + .to.contain('OUTPUTFORMAT=JSON') }) cy.get('#attribute-layer-table-quartiers_shp_wrapper div.dataTables_scrollHead th').then(theaders => { @@ -70,12 +74,12 @@ describe('Attribute table', () => { cy.get('button[value="Les_quartiers_a_Montpellier"].btn-open-attribute-layer').click({ force: true }) // Wait for features - cy.wait('@postGetFeature').then((interception) => { + cy.wait('@postDescribeFeatureType').then((interception) => { expect(interception.request.body) .to.contain('SERVICE=WFS') - .to.contain('REQUEST=GetFeature') + .to.contain('REQUEST=DescribeFeatureType') .to.contain('TYPENAME=quartiers') - .to.contain('OUTPUTFORMAT=GeoJSON') + .to.contain('OUTPUTFORMAT=JSON') }) cy.get('#attribute-layer-table-Les_quartiers_a_Montpellier_wrapper div.dataTables_scrollHead th').then(theaders => { @@ -95,12 +99,12 @@ describe('Attribute table', () => { cy.get('button[value="Les_quartiers_a_Montpellier"].btn-open-attribute-layer').click({ force: true }) // Wait for features - cy.wait('@postGetFeature').then((interception) => { + cy.wait('@postDescribeFeatureType').then((interception) => { expect(interception.request.body) .to.contain('SERVICE=WFS') - .to.contain('REQUEST=GetFeature') + .to.contain('REQUEST=DescribeFeatureType') .to.contain('TYPENAME=quartiers') - .to.contain('OUTPUTFORMAT=GeoJSON') + .to.contain('OUTPUTFORMAT=JSON') }) // Check table lines @@ -340,20 +344,7 @@ describe('Attribute table', () => { cy.get('button[value="Les_quartiers_a_Montpellier"].btn-open-attribute-layer').click({ force: true }) // The content of the table has to be fetched again // Wait for features - cy.wait('@postGetFeature').then((interception) => { - expect(interception.request.body) - .to.contain('SERVICE=WFS') - .to.contain('REQUEST=GetFeature') - .to.contain('TYPENAME=quartiers') - .to.contain('OUTPUTFORMAT=GeoJSON') - .to.contain('EXP_FILTER=%24id+IN+%28+2+%2C+4+%2C+6+%29+') - expect(interception.response.body) - .to.have.property('type') - expect(interception.response.body.type).to.be.eq('FeatureCollection') - expect(interception.response.body) - .to.have.property('features') - expect(interception.response.body.features).to.have.length(3) - }) + // TODO assert datatables request is made // check that the layer is filtered cy.get('#attribute-layer-table-Les_quartiers_a_Montpellier tbody tr').should('have.length', 3) @@ -396,20 +387,7 @@ describe('Attribute table', () => { cy.get('button[value="quartiers_shp"].btn-open-attribute-layer').click({ force: true }) // Wait for features - cy.wait('@postGetFeature').then((interception) => { - expect(interception.request.body) - .to.contain('SERVICE=WFS') - .to.contain('REQUEST=GetFeature') - .to.contain('TYPENAME=quartiers_shp') - .to.contain('OUTPUTFORMAT=GeoJSON') - .to.not.contain('EXP_FILTER') - expect(interception.response.body) - .to.have.property('type') - expect(interception.response.body.type).to.be.eq('FeatureCollection') - expect(interception.response.body) - .to.have.property('features') - expect(interception.response.body.features).to.have.length(7) - }) + // TODO assert datatables request is made // Check table lines cy.get('#attribute-layer-table-quartiers_shp tbody tr').should('have.length', 7) From f6c6c4a871c1771a5163d3b5dd848824197d7ec6 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Mon, 31 Mar 2025 16:31:30 +0200 Subject: [PATCH 10/45] Handle children tables --- assets/src/legacy/attributeTable.js | 39 ++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 5ef99ffd27..e7aad09d87 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -121,6 +121,9 @@ var lizAttributeTable = function() { 'exp_filter': null, 'selection': null }; + // This parameter is used when a parent feature is highlighted + // It only filters children tables + config.layers[configLayerName]['line_filter'] = null; // Get existing filter if exists (via permalink) const layer = lizMap.mainLizmap.state.layersAndGroupsCollection.getLayerByName(layername); @@ -319,10 +322,6 @@ var lizAttributeTable = function() { GEOMETRYNAME: 'extent' }; - if(filter){ - wfsParams['EXP_FILTER'] = filter; - } - // Calculate bbox from map extent if needed if (config.options?.limitDataToBbox == 'True') { const mapExtent = lizMap.mainLizmap.map.getView().calculateExtent(); @@ -1247,7 +1246,16 @@ var lizAttributeTable = function() { if( relation.referencingLayer == childLayerConfig.id ){ filter = '"' + relation.referencingField + '" = ' + "'" + fp[relation.referencedField] + "'"; } - getDataAndFillAttributeTable(childLayerName, filter, childTableSelector, false); + + // Refresh datatable if it is already created + // Create if it is not + childLayerConfig.line_filter = filter; + if (DataTable.isDataTable(childTableSelector)) { + const childTable = new DataTable(childTableSelector); + childTable.draw(); + } else { + getDataAndFillAttributeTable(childLayerName, filter, childTableSelector, false); + } } } } @@ -1469,7 +1477,7 @@ var lizAttributeTable = function() { const params = globalThis['lizUrls'].params; params['layerId'] = lConfig.id; - $( aTable ).dataTable( { + $( aTable ).dataTable({ serverSide: true ,ajax: { url: datatablesUrl + '?' + new URLSearchParams(params).toString(), @@ -1488,10 +1496,23 @@ var lizAttributeTable = function() { } // Handle features filtered by their parent - const exp_filter = lConfig.request_params.exp_filter; - if (exp_filter) { - d.exp_filter = exp_filter; + if(isChild) { + if(lConfig.line_filter) { + d.exp_filter = lConfig.line_filter; + } + } else { + const exp_filter = lConfig.request_params.exp_filter; + if (exp_filter) { + d.exp_filter = exp_filter; + } + } + }, + dataSrc: function(json) { + // Copy received features to config + for (const feature of json.data) { + config.layers[aName]['features'][feature.DT_RowId] = {'properties' : feature}; } + return json.data; } } ,columns: columns From 98ad860041ed12252bd1d3788d1ff53ed48c41f4 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Mon, 31 Mar 2025 21:56:05 +0200 Subject: [PATCH 11/45] Return JSON features as they are cached in `lizMap.config` --- assets/src/legacy/attributeTable.js | 19 ++++++++++++++----- .../lizmap/controllers/datatables.classic.php | 16 +--------------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index e7aad09d87..d941945293 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1507,12 +1507,21 @@ var lizAttributeTable = function() { } } }, - dataSrc: function(json) { - // Copy received features to config - for (const feature of json.data) { - config.layers[aName]['features'][feature.DT_RowId] = {'properties' : feature}; + dataSrc: (json) => { + // Format data for DataTables + let formatedData = []; + for (const feature of json.data.features) { + const featID = feature.id.split('.').pop(); + formatedData.push(Object.assign({ + 'DT_RowId': featID, + 'lizSelected': '', + 'featureToolbar': '', + }, feature.properties)); + + // Copy received features to config + config.layers[aName]['features'][featID] = feature; } - return json.data; + return formatedData; } } ,columns: columns diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index 108d1cf8a9..722477fdc8 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -77,7 +77,6 @@ public function index() 'REQUEST' => 'GetFeature', 'TYPENAME' => $typeName, 'OUTPUTFORMAT' => 'GeoJSON', - 'GEOMETRYNAME' => 'none', 'MAXFEATURES' => $DTLength, 'SORTBY' => $DTOrderColumnName.' '.$DTOrderColumnDirection, 'STARTINDEX' => $DTStart, @@ -122,25 +121,12 @@ public function index() $wfsrequest = new WFSRequest($lproj, $wfsParamsData, lizmap::getServices()); $wfsresponse = $wfsrequest->process(); $featureData = $wfsresponse->getBodyAsString(); - $jsonFeatures = array_merge($jsonFeatures, json_decode($featureData)->features); - $data = array(); - foreach ($jsonFeatures as $key => $feature) { - $dataObject = array_merge( - array( - 'DT_RowId' => (int) explode('.', $feature->id)[1], - 'lizSelected' => '', - 'featureToolbar' => '', - ), - (array) $feature->properties - ); - $data[] = $dataObject; - } $returnedData = array( 'draw' => (int) $this->param('draw'), 'recordsTotal' => $hits, 'recordsFiltered' => $hits, // TODO: implement filtering - 'data' => $data, + 'data' => json_decode($featureData), ); $rep->content = json_encode($returnedData); From 037c9c52111b6832034ec0f4ee02bbdd8b2c8ba7 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 1 Apr 2025 11:07:51 +0200 Subject: [PATCH 12/45] Handle link/unlink in featureToolbars in attribute table --- assets/src/legacy/attributeTable.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index d941945293..acdc452593 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1400,6 +1400,7 @@ var lizAttributeTable = function() { // Pivot table ? var isPivot = false; + let pivotId; if( isChild && 'pivot' in config.attributeLayers[aName] && config.attributeLayers[aName]['pivot'] == 'True' @@ -1421,7 +1422,7 @@ var lizAttributeTable = function() { } if (parentLayerConfig && parentLayerConfig[1] && parentLayerConfig[1].cleanname && highlightedFeature) { var childLayerId = lConfig.id; - var pivotId = getPivotIdFromRelatedLayers(parentLayerID, childLayerId); + pivotId = getPivotIdFromRelatedLayers(parentLayerID, childLayerId); if (pivotId) { pivotReference = pivotId + ":" + highlightedFeature; } @@ -1450,7 +1451,7 @@ var lizAttributeTable = function() { } // Create columns for datatable - var cdc = createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues); + var cdc = createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues, isChild, pivotId, parentLayerID); var columns = cdc.columns; var firstDisplayedColIndex = cdc.firstDisplayedColIndex; @@ -1617,7 +1618,7 @@ var lizAttributeTable = function() { * @param cTypes * @param allColumnsKeyValues */ - function createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues){ + function createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues, isChild, pivotId, parentLayerID){ const columns = []; let firstDisplayedColIndex = 0; // Column with selected status @@ -1638,11 +1639,6 @@ var lizAttributeTable = function() { render: (data, type, row, meta) => { const layerId = config.layers[aName].id; const fid = row['DT_RowId']; - - // TODO: handle pivotId, parentLayerID and isChild - let pivotId; - let isChild; - let parentLayerID; return ` `; From 45970f2ab8fbb5c13806800b893f9602b919308d Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 1 Apr 2025 11:09:52 +0200 Subject: [PATCH 13/45] Return `recordsFiltered` for Datatables --- lizmap/modules/lizmap/controllers/datatables.classic.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index 722477fdc8..f94860d51e 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -69,6 +69,10 @@ public function index() $hitsData = $wfsresponse->getBodyAsString(); preg_match('/numberOfFeatures="([0-9]+)"/', $hitsData, $matches); $hits = $matches[1]; + $recordsFiltered = $hits; + if (count($filteredFeatureIDs) > 0) { + $recordsFiltered = count($filteredFeatureIDs); + } // Get features $wfsParamsData = array( @@ -125,7 +129,7 @@ public function index() $returnedData = array( 'draw' => (int) $this->param('draw'), 'recordsTotal' => $hits, - 'recordsFiltered' => $hits, // TODO: implement filtering + 'recordsFiltered' => $recordsFiltered, 'data' => json_decode($featureData), ); From f932b513974251fb27a1a82142b28e728dace7b3 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 1 Apr 2025 18:22:43 +0200 Subject: [PATCH 14/45] Redraw table if it already exists --- assets/src/legacy/attributeTable.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index acdc452593..804a4a2780 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1594,6 +1594,10 @@ var lizAttributeTable = function() { return false; }); + } else { + // Table already created, just redraw it + const table = new DataTable(aTable); + table.draw(); } // Check editable features From 9c4ea10059b9fdeccdc2325d3e9369769e07ebdf Mon Sep 17 00:00:00 2001 From: nboisteault Date: Wed, 2 Apr 2025 11:50:14 +0200 Subject: [PATCH 15/45] Handle children tables follow up --- assets/src/legacy/attributeTable.js | 40 ++++++++++++++++------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 804a4a2780..9f1bbeabc3 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -220,7 +220,7 @@ var lizAttributeTable = function() { const tableSelector = '#attribute-layer-table-' + cleanName; // Get data and fill attribute table - getDataAndFillAttributeTable(layerName, layerFilter, tableSelector, false); + getDataAndFillAttributeTable(layerName, layerFilter, false, tableSelector, false); const tabElement = document.getElementById('nav-tab-attribute-layer-' + cleanName); bootstrap.Tab.getOrCreateInstance(tabElement).show(); @@ -308,15 +308,27 @@ var lizAttributeTable = function() { * * @param layerName * @param filter + * @param isLinefilter * @param tableSelector * @param forceEmptyTable * @param callBack */ - function getDataAndFillAttributeTable(layerName, filter, tableSelector, forceEmptyTable, callBack){ + function getDataAndFillAttributeTable(layerName, filter, isLinefilter = false, tableSelector, forceEmptyTable, callBack){ let layerConfig = lizMap.config.layers[layerName]; const typeName = layerConfig?.shortname || layerConfig?.typename || layerConfig?.name; + if(filter){ + if (isLinefilter) { + layerConfig['line_filter'] = filter; + } else { + layerConfig['request_params']['filter'] = filter; + } + } else { + layerConfig['line_filter'] = '$id = -99999999'; + layerConfig['request_params']['filter'] = undefined; + } + const wfsParams = { TYPENAME: typeName, GEOMETRYNAME: 'extent' @@ -717,7 +729,7 @@ var lizAttributeTable = function() { const tableSelector = '#attribute-layer-table-'+cleanName; $('#attribute-layer-main-'+cleanName+' > div.attribute-layer-content').hide(); - getDataAndFillAttributeTable(lname, null, tableSelector, false, () => { + getDataAndFillAttributeTable(lname, null, false, tableSelector, false, () => { $('#attribute-layer-main-' + cleanName + ' > div.attribute-layer-content').show(); refreshDatatableSize('#attribute-layer-main-' + cleanName); }); @@ -1247,15 +1259,7 @@ var lizAttributeTable = function() { filter = '"' + relation.referencingField + '" = ' + "'" + fp[relation.referencedField] + "'"; } - // Refresh datatable if it is already created - // Create if it is not - childLayerConfig.line_filter = filter; - if (DataTable.isDataTable(childTableSelector)) { - const childTable = new DataTable(childTableSelector); - childTable.draw(); - } else { - getDataAndFillAttributeTable(childLayerName, filter, childTableSelector, false); - } + getDataAndFillAttributeTable(childLayerName, filter, true, childTableSelector, false); } } } @@ -1407,7 +1411,7 @@ var lizAttributeTable = function() { ){ isPivot = true; } - var pivotReference = null; + let pivotReference = null; // checks if the parent and child are related via pivot if (parentLayerID) { // means that the table is displayed as a child @@ -1451,7 +1455,7 @@ var lizAttributeTable = function() { } // Create columns for datatable - var cdc = createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues, isChild, pivotId, parentLayerID); + var cdc = createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues, isChild, pivotReference, parentLayerID); var columns = cdc.columns; var firstDisplayedColIndex = cdc.firstDisplayedColIndex; @@ -1622,7 +1626,7 @@ var lizAttributeTable = function() { * @param cTypes * @param allColumnsKeyValues */ - function createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues, isChild, pivotId, parentLayerID){ + function createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues, isChild, pivotReference, parentLayerID){ const columns = []; let firstDisplayedColIndex = 0; // Column with selected status @@ -1644,7 +1648,7 @@ var lizAttributeTable = function() { const layerId = config.layers[aName].id; const fid = row['DT_RowId']; return ` - + `; } }); @@ -1977,7 +1981,7 @@ var lizAttributeTable = function() { * @param forceEmptyTable */ function getEditionChildData( childLayerName, filter, childTable, forceEmptyTable = false ){ - getDataAndFillAttributeTable(childLayerName, filter, childTable, forceEmptyTable, () => { + getDataAndFillAttributeTable(childLayerName, filter, true, childTable, forceEmptyTable, () => { // Check edition capabilities var canCreateChildren = false; var canEdit = false; @@ -3080,7 +3084,7 @@ var lizAttributeTable = function() { // Else refresh main table with no filter else{ // If not pivot - getDataAndFillAttributeTable(featureType, null, zTable, false); + getDataAndFillAttributeTable(featureType, null, false, zTable, false); } } }); From af1ff66747c48c1dfa0a391a626de3eb4a6737a2 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 8 Apr 2025 15:22:30 +0200 Subject: [PATCH 16/45] Remove legacy code linked to `limitDataToBbox` parameter This parameter was set in the plugin by the `Limit fetched data to the current map extent and layer visibility` checkbox --- assets/src/legacy/attributeTable.js | 98 +---------------------------- assets/src/legacy/map.js | 5 -- assets/src/modules/SelectionTool.js | 6 -- 3 files changed, 2 insertions(+), 107 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 9f1bbeabc3..9066b2ed47 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -51,11 +51,6 @@ var lizAttributeTable = function() { lizMap.lizmapLayerFilterActive = true; } - var limitDataToBbox = false; - if ( 'limitDataToBbox' in config.options && config.options.limitDataToBbox == 'True'){ - limitDataToBbox = true; - } - if (!('attributeLayers' in config)) return -1; @@ -198,16 +193,6 @@ var lizAttributeTable = function() { .click(function(){ var cleanName = $(this).val(); - // Disable attribute table if limitDataToBbox and layer not visible in map - if(limitDataToBbox){ - let layer = lizMap.mainLizmap.map.getLayerByName(lizMap.getLayerNameByCleanName(cleanName)); - if( layer ) { - if(warnResolution(layer)){ - return false; - } - } - } - // Add Div if not already there const layerName = attributeLayersDic[cleanName]; if( !$('#nav-tab-attribute-layer-' + cleanName ).length ){ @@ -334,18 +319,6 @@ var lizAttributeTable = function() { GEOMETRYNAME: 'extent' }; - // Calculate bbox from map extent if needed - if (config.options?.limitDataToBbox == 'True') { - const mapExtent = lizMap.mainLizmap.map.getView().calculateExtent(); - const mapExtent4326 = lizMap.mainLizmap.transformExtent( - mapExtent, - lizMap.mainLizmap.map.getView().getProjection().getCode(), - 'EPSG:4326' - ); - wfsParams['BBOX'] = mapExtent4326; - wfsParams['SRSNAME'] = 'EPSG:4326'; - } - let fetchRequests = []; let namedRequests = []; @@ -442,18 +415,6 @@ var lizAttributeTable = function() { wfsParams['EXP_FILTER'] = filter; } - // Calculate bbox from map extent if needed - if (config.options?.limitDataToBbox == 'True') { - const mapExtent = lizMap.mainLizmap.map.getView().calculateExtent(); - const mapExtent4326 = lizMap.mainLizmap.transformExtent( - mapExtent, - lizMap.mainLizmap.map.getView().getProjection().getCode(), - 'EPSG:4326' - ); - wfsParams['BBOX'] = mapExtent4326; - wfsParams['SRSNAME'] = 'EPSG:4326'; - } - const getFeatureRequest = lizMap.mainLizmap.wfs.getFeature(wfsParams); Promise.all([getFeatureRequest]).then(responses => { refreshLayerAttributeDatatable(layerName, tableSelector, responses[0].features); @@ -597,16 +558,6 @@ var lizAttributeTable = function() { html+= ' '; } - // Refresh button (if limitDataToBbox is true) - if( limitDataToBbox - && config.layers[lname]['geometryType'] != 'none' - && config.layers[lname]['geometryType'] != 'unknown' - ){ - // Add button to refresh table - html+= ''; - - } - // Get children content var childHtml = getChildrenHtmlContent( lname ); var alc=''; @@ -705,43 +656,6 @@ var lizAttributeTable = function() { }); } - if(limitDataToBbox){ - $('#attribute-layer-'+ cleanName + ' button.btn-refresh-table') - .click(function(){ - // Reset button tooltip & style - $(this) - .attr('data-bs-toggle', 'tooltip') - .attr('data-bs-title', lizDict['attributeLayers.toolbar.btn.refresh.table.tooltip']) - .removeClass('btn-warning'); - - // Disable if the layer is not visible - let layer = lizMap.mainLizmap.map.getLayerByName(lizMap.getLayerNameByCleanName(cleanName)); - if( layer ) { - if(warnResolution(layer)){ - return false; - } - }else{ - // do nothing if no layer found - return false; - } - - // Refresh table - const tableSelector = '#attribute-layer-table-'+cleanName; - $('#attribute-layer-main-'+cleanName+' > div.attribute-layer-content').hide(); - - getDataAndFillAttributeTable(lname, null, false, tableSelector, false, () => { - $('#attribute-layer-main-' + cleanName + ' > div.attribute-layer-content').show(); - refreshDatatableSize('#attribute-layer-main-' + cleanName); - }); - - return false; - }) - .hover( - function(){ $(this).addClass('btn-primary'); }, - function(){ $(this).removeClass('btn-primary'); } - ); - } - if( childHtml ){ $('#attribute-layer-'+ cleanName + ' button.btn-toggle-children') .click(function(){ @@ -828,7 +742,7 @@ var lizAttributeTable = function() { eFormat = 'GML3'; var cleanName = $(this).parents('div.attribute-layer-main:first').attr('id').replace('attribute-layer-main-', ''); var eName = attributeLayersDic[ cleanName ]; - lizMap.exportVectorLayer( eName, eFormat, limitDataToBbox ); + lizMap.exportVectorLayer( eName, eFormat ); }); // Bind click on createFeature button @@ -2075,11 +1989,7 @@ var lizAttributeTable = function() { if( aName in config.attributeLayers ) atConfig = config.attributeLayers[aName]; - var limitDataToBbox = false; - if ( 'limitDataToBbox' in config.options && config.options.limitDataToBbox == 'True'){ - limitDataToBbox = true; - } - lizMap.getFeatureData(aName, aFilter, aFeatureID, aGeometryName, limitDataToBbox, null, null, aCallBack); + lizMap.getFeatureData(aName, aFilter, aFeatureID, aGeometryName, null, null, null, aCallBack); return true; } @@ -3218,10 +3128,6 @@ var lizAttributeTable = function() { GEOMETRYNAME: 'extent' }; wfsParams['EXP_FILTER'] = '"' + referencedPivotField + '" = ' + "'" + referencedFieldValue + "'"; - if (config.options?.limitDataToBbox == 'True') { - wfsParams['BBOX'] = lizMap.mainLizmap.map.getView().calculateExtent(); - wfsParams['SRSNAME'] = lizMap.mainLizmap.map.getView().getProjection().getCode(); - } const getFeatureRequest = lizMap.mainLizmap.wfs.getFeature(wfsParams); diff --git a/assets/src/legacy/map.js b/assets/src/legacy/map.js index 45aaf1562f..5acd3926f3 100644 --- a/assets/src/legacy/map.js +++ b/assets/src/legacy/map.js @@ -1111,11 +1111,6 @@ window.lizMap = function() { }; wfsParams['EXP_FILTER'] = '"' + config.relations.pivot[rLayerId][layerId] + '" = ' + "'" + feat.properties[relation.referencedField] + "'"; - // Calculate bbox - if (config.options?.limitDataToBbox == 'True') { - wfsParams['BBOX'] = lizMap.mainLizmap.map.getView().calculateExtent(); - wfsParams['SRSNAME'] = lizMap.mainLizmap.map.getView().getProjection().getCode(); - } preProcessRequest = lizMap.mainLizmap.wfs.getFeature(wfsParams); let ut = { diff --git a/assets/src/modules/SelectionTool.js b/assets/src/modules/SelectionTool.js index ca9d82c70a..4d410b58dd 100644 --- a/assets/src/modules/SelectionTool.js +++ b/assets/src/modules/SelectionTool.js @@ -492,12 +492,6 @@ export default class SelectionTool { EXP_FILTER: spatialFilter }; - // Apply limit to bounding box config - if (this._lizmap3.config?.limitDataToBbox === 'True') { - wfsParams['BBOX'] = this._map.getView().calculateExtent(); - wfsParams['SRSNAME'] = this._map.getView().getProjection().getCode(); - } - // Restrict to current geometry extent for performance // But not with 'disjoint' to get features if (this._geomOperator !== 'disjoint') { From 122c218b9375b47d465c44e307f37202186cf2f9 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 8 Apr 2025 15:27:07 +0200 Subject: [PATCH 17/45] Attribute table: add a toggle button to filter features in the current extent --- assets/src/images/svg/filter-square.svg | 4 ++ assets/src/legacy/attributeTable.js | 55 +++++++++++++------ .../lizmap/controllers/datatables.classic.php | 31 +++++++++++ .../en_US/dictionnary.UTF-8.properties | 4 +- lizmap/www/assets/css/map.css | 5 ++ 5 files changed, 80 insertions(+), 19 deletions(-) create mode 100644 assets/src/images/svg/filter-square.svg diff --git a/assets/src/images/svg/filter-square.svg b/assets/src/images/svg/filter-square.svg new file mode 100644 index 0000000000..3b58be62da --- /dev/null +++ b/assets/src/images/svg/filter-square.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 9066b2ed47..175fdb43ee 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -6,6 +6,7 @@ */ import DOMPurify from 'dompurify'; +import '../images/svg/filter-square.svg'; var lizAttributeTable = function() { @@ -248,18 +249,14 @@ var lizAttributeTable = function() { {'layers': attributeLayersDic} ); - // Map events - if (limitDataToBbox) { - lizMap.mainLizmap.map.on('moveend', () => { - let btitle = lizDict['attributeLayers.toolbar.btn.refresh.table.tooltip.changed']; - btitle += ' ' + lizDict['attributeLayers.toolbar.btn.refresh.table.tooltip']; - $('button.btn-refresh-table') - .attr('data-bs-toggle', 'tooltip') - .attr('data-bs-title', btitle) - .addClass('btn-warning') - .tooltip(); + // Redraw datatables when map is moved and their filter by extent button is active + lizMap.mainLizmap.map.on('moveend', () => { + document.querySelectorAll('.btn-filterbyextent-attributeTable.active').forEach((btn) => { + const layerId = btn.getAttribute('data-layerid'); + const dTable = new DataTable('table[data-layerid=' + layerId + ']'); + dTable.draw(); }); - } + }); // Bind click on tabs to resize datatable tables $('#attributeLayers-tabs li').click(function(){ @@ -525,6 +522,14 @@ var lizAttributeTable = function() { html+= ' '; } + // Filter data by extent button + html+= ` + `; + // Invert selection html += '' @@ -936,11 +941,17 @@ var lizAttributeTable = function() { { 'featureType': aName, 'updateDrawing': true} ); return false; - }) - .hover( - function(){ $(this).addClass('btn-primary'); }, - function(){ $(this).removeClass('btn-primary'); } - ); + }).hover( + function(){ $(this).addClass('btn-primary'); }, + function(){ $(this).removeClass('btn-primary'); } + ); + + // Bind click on btn-filterbyextent button + document.querySelector('#attribute-layer-'+ cleanName + ' button.btn-filterbyextent-attributeTable').addEventListener('click', (e) => { + const layerId = e.currentTarget.getAttribute('data-layerid'); + const dTable = new DataTable('table[data-layerid=' + layerId + ']'); + dTable.draw(); + }); } /** @@ -1425,10 +1436,22 @@ var lizAttributeTable = function() { d.exp_filter = exp_filter; } } + // Handle features filtered by extent + if (document.querySelector('.btn-filterbyextent-attributeTable.active[value="' + cleanName + '"]')) { + const olView = lizMap.mainLizmap.map.getView(); + const bbox = olView.calculateExtent().join(','); + d.bbox = bbox; + const projCode = olView.getProjection().getCode(); + d.srsname = projCode; + } }, dataSrc: (json) => { // Format data for DataTables let formatedData = []; + if (!json.data) { + return formatedData; + } + for (const feature of json.data.features) { const featID = feature.id.split('.').pop(); formatedData.push(Object.assign({ diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index f94860d51e..af03707974 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -36,6 +36,12 @@ public function index() } $expFilter = $this->param('exp_filter'); + $bbox = array(); + $srsName = $this->param('srsname'); + if ($this->param('bbox') && $srsName) { + $bbox = explode(',', $this->param('bbox')); + } + // DataTables parameters $DTStart = $this->param('start'); $DTLength = $this->param('length'); @@ -122,6 +128,31 @@ public function index() $wfsParamsData['EXP_FILTER'] = $expFilter; } + // Handle filter by extent + if (count($bbox) == 4) { + // Add parameters to get features in the bounding box (paginated) + $bboxString = implode(',', $bbox); + $wfsParamsData['BBOX'] = $bboxString; + $wfsParamsData['SRSNAME'] = $srsName; + + // Get total number of features in the bounding box + $wfsParamsFilterByExtentHits = array( + 'SERVICE' => 'WFS', + 'VERSION' => '1.0.0', + 'REQUEST' => 'GetFeature', + 'TYPENAME' => $typeName, + 'RESULTTYPE' => 'hits', + 'BBOX' => $bboxString, + 'SRSNAME' => $srsName, + ); + + $wfsrequest = new WFSRequest($lproj, $wfsParamsFilterByExtentHits, lizmap::getServices()); + $wfsresponse = $wfsrequest->process(); + $filterByExtentHitsData = $wfsresponse->getBodyAsString(); + preg_match('/numberOfFeatures="([0-9]+)"/', $filterByExtentHitsData, $matches); + $recordsFiltered = $matches[1]; + } + $wfsrequest = new WFSRequest($lproj, $wfsParamsData, lizmap::getServices()); $wfsresponse = $wfsrequest->process(); $featureData = $wfsresponse->getBodyAsString(); diff --git a/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties b/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties index 57dc404faf..28601005a8 100644 --- a/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties +++ b/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties @@ -111,9 +111,7 @@ attributeLayers.toolbar.btn.toggle.children.title=Toggle children tables attributeLayers.toolbar.btn.data.linkFeatures.title=Link selected features attributeLayers.toolbar.btn.data.hide.title=Hide attributeLayers.toolbar.btn.data.show.title=Show -attributeLayers.toolbar.btn.refresh.table.title=Refresh -attributeLayers.toolbar.btn.refresh.table.tooltip=Refresh the table data. -attributeLayers.toolbar.btn.refresh.table.tooltip.changed=The map extent has changed since the last time data were fetched. +attributeLayers.toolbar.btn.filterByExtent.title=Filter the table with the features in the current map extent attributeLayers.toolbar.msg.data.lines=lines returned attributeLayers.toolbar.msg.data.nodata=No result for this layer attributeLayers.toolbar.msg.data.extent=in the current extent diff --git a/lizmap/www/assets/css/map.css b/lizmap/www/assets/css/map.css index 5c65337d26..b95dd83ebf 100644 --- a/lizmap/www/assets/css/map.css +++ b/lizmap/www/assets/css/map.css @@ -1395,6 +1395,11 @@ SubDock overflow: auto; } +.btn-filterbyextent-attributeTable svg { + height: 14px; + width: 14px; +} + .attribute-layer-content { overflow:auto; height: 90%; From 193edfaf69db5c86e7756e6563b1dc276357a526 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 8 Apr 2025 17:07:42 +0200 Subject: [PATCH 18/45] e2e: test data filtered by extent in attribute table TODO: handle export --- .../playwright/attribute-table.spec.js | 48 ++++++++----------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/tests/end2end/playwright/attribute-table.spec.js b/tests/end2end/playwright/attribute-table.spec.js index 3bd1da7248..0b6dbe34ee 100644 --- a/tests/end2end/playwright/attribute-table.spec.js +++ b/tests/end2end/playwright/attribute-table.spec.js @@ -48,39 +48,29 @@ test.describe('Attribute table', () => { await expect(project.attributeTableWrapper(layerName).locator('div.dataTables_info')) .toContainText('Showing 651 to 700 of 700 entries'); }); -}); - -test.describe('Attribute table data restricted to map extent', () => { - test.beforeEach(async ({ page }) => { - await page.route('**/service/getProjectConfig*', async route => { - const response = await route.fetch(); - const json = await response.json(); - json.options['limitDataToBbox'] = 'True'; - await route.fulfill({ response, json }); - }); - const url = '/index.php/view/map/?repository=testsrepository&project=attribute_table'; - await gotoMap(url, page) - }); - test('Data restriction and refresh button behaviour', async ({ page }) => { + test('Data filtered by extent', async ({ page }) => { const project = new ProjectPage(page, 'attribute_table'); const layerName = 'Les_quartiers_a_Montpellier'; + const datatablesRequestPromise = page.waitForRequest(request => request.method() === 'POST' && request.postData()?.includes('draw') === true); await project.openAttributeTable(layerName); - - await expect(page.locator('.btn-refresh-table')).not.toHaveClass(/btn-warning/); - - const getMapPromise = page.waitForRequest(/GetMap/); - - await page.locator('lizmap-feature-toolbar:nth-child(1) > div:nth-child(1) > button:nth-child(3)').first().click(); - - await getMapPromise; - - await expect(page.locator('.btn-refresh-table')).toHaveClass(/btn-warning/); - - // Refresh - await page.locator('.btn-refresh-table').click(); - + await datatablesRequestPromise; + await expect(project.attributeTableHtml(layerName).locator('tbody tr')).toHaveCount(7); + await page.locator('.btn-filterbyextent-attributeTable').click(); + await expect(page.locator('.btn-filterbyextent-attributeTable')).toHaveClass(/active/); + await datatablesRequestPromise; + await expect(project.attributeTableHtml(layerName).locator('tbody tr')).toHaveCount(7); + + // Zoom and assert features are filtered by extent + await page.locator('.feature-zoom').first().click(); + await datatablesRequestPromise; await expect(project.attributeTableHtml(layerName).locator('tbody tr')).toHaveCount(5); + + // Unactivate filter by extent and assert all features are in the table + await page.locator('.btn-filterbyextent-attributeTable').click(); + await expect(page.locator('.btn-filterbyextent-attributeTable')).not.toHaveClass(/active/); + await datatablesRequestPromise; + await expect(project.attributeTableHtml(layerName).locator('tbody tr')).toHaveCount(7); }); }); @@ -224,4 +214,4 @@ test.describe('Layer export permissions ACL', () => { await expectParametersToContain('Export GeoJSON with BBOX', getFeatureRequest.postData() ?? '', expectedParameters); }) -}); +}); \ No newline at end of file From d04cd2599c0bc8a9c7838c0cdc85d0b863913e24 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Fri, 11 Apr 2025 16:14:09 +0200 Subject: [PATCH 19/45] Attribute table: install Datatables v 2.2.2 --- assets/src/legacy/attributeTable.js | 1 + .../app/responses/myHtmlMapResponse.class.php | 3 +- lizmap/www/assets/css/datatables.min.css | 19 + package-lock.json | 438 +++++++++++------- package.json | 1 + stylelint.config.mjs | 1 + 6 files changed, 293 insertions(+), 170 deletions(-) create mode 100644 lizmap/www/assets/css/datatables.min.css diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 175fdb43ee..e2de6692ec 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -5,6 +5,7 @@ * @license MPL-2.0 */ +import DataTable from 'datatables.net-bs5'; import DOMPurify from 'dompurify'; import '../images/svg/filter-square.svg'; diff --git a/lizmap/app/responses/myHtmlMapResponse.class.php b/lizmap/app/responses/myHtmlMapResponse.class.php index 6189cbadba..183ce247e1 100644 --- a/lizmap/app/responses/myHtmlMapResponse.class.php +++ b/lizmap/app/responses/myHtmlMapResponse.class.php @@ -35,9 +35,10 @@ public function __construct() $this->addAssets('jquery_ui'); $this->addAssets('bootstrap'); - $this->addAssets('datatables'); $this->addAssets('map'); + $this->addCSSLink($bp.'assets/css/datatables.min.css'); + $this->setBodyAttributes(array('data-proj4js-lib-path' => $bp.'assets/js/Proj4js/')); } diff --git a/lizmap/www/assets/css/datatables.min.css b/lizmap/www/assets/css/datatables.min.css new file mode 100644 index 0000000000..095585d8bb --- /dev/null +++ b/lizmap/www/assets/css/datatables.min.css @@ -0,0 +1,19 @@ +/* + * This combined file was created by the DataTables downloader builder: + * https://datatables.net/download + * + * To rebuild or modify this file with the latest versions of the included + * software please visit: + * https://datatables.net/download/#bs5/dt-2.2.2 + * + * Included libraries: + * DataTables 2.2.2 + */ + +:root{--dt-row-selected: 13, 110, 253;--dt-row-selected-text: 255, 255, 255;--dt-row-selected-link: 9, 10, 11;--dt-row-stripe: 0, 0, 0;--dt-row-hover: 0, 0, 0;--dt-column-ordering: 0, 0, 0;--dt-html-background: white}:root.dark{--dt-html-background: rgb(33, 37, 41)}table.dataTable td.dt-control{text-align:center;cursor:pointer}table.dataTable td.dt-control:before{display:inline-block;box-sizing:border-box;content:"";border-top:5px solid transparent;border-left:10px solid rgba(0, 0, 0, 0.5);border-bottom:5px solid transparent;border-right:0px solid transparent}table.dataTable tr.dt-hasChild td.dt-control:before{border-top:10px solid rgba(0, 0, 0, 0.5);border-left:5px solid transparent;border-bottom:0px solid transparent;border-right:5px solid transparent}table.dataTable tfoot:empty{display:none}html.dark table.dataTable td.dt-control:before,:root[data-bs-theme=dark] table.dataTable td.dt-control:before,:root[data-theme=dark] table.dataTable td.dt-control:before{border-left-color:rgba(255, 255, 255, 0.5)}html.dark table.dataTable tr.dt-hasChild td.dt-control:before,:root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before,:root[data-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before{border-top-color:rgba(255, 255, 255, 0.5);border-left-color:transparent}div.dt-scroll{width:100%}div.dt-scroll-body thead tr,div.dt-scroll-body tfoot tr{height:0}div.dt-scroll-body thead tr th,div.dt-scroll-body thead tr td,div.dt-scroll-body tfoot tr th,div.dt-scroll-body tfoot tr td{height:0 !important;padding-top:0px !important;padding-bottom:0px !important;border-top-width:0px !important;border-bottom-width:0px !important}div.dt-scroll-body thead tr th div.dt-scroll-sizing,div.dt-scroll-body thead tr td div.dt-scroll-sizing,div.dt-scroll-body tfoot tr th div.dt-scroll-sizing,div.dt-scroll-body tfoot tr td div.dt-scroll-sizing{height:0 !important;overflow:hidden !important}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:before{position:absolute;display:block;bottom:50%;content:"▲";content:"▲"/""}table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:after{position:absolute;display:block;top:50%;content:"▼";content:"▼"/""}table.dataTable thead>tr>th.dt-orderable-asc,table.dataTable thead>tr>th.dt-orderable-desc,table.dataTable thead>tr>th.dt-ordering-asc,table.dataTable thead>tr>th.dt-ordering-desc,table.dataTable thead>tr>td.dt-orderable-asc,table.dataTable thead>tr>td.dt-orderable-desc,table.dataTable thead>tr>td.dt-ordering-asc,table.dataTable thead>tr>td.dt-ordering-desc{position:relative;padding-right:30px}table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order,table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order{position:absolute;right:12px;top:0;bottom:0;width:12px}table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:after,table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:before,table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:after,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:before,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:after{left:0;opacity:.125;line-height:9px;font-size:.8em}table.dataTable thead>tr>th.dt-orderable-asc,table.dataTable thead>tr>th.dt-orderable-desc,table.dataTable thead>tr>td.dt-orderable-asc,table.dataTable thead>tr>td.dt-orderable-desc{cursor:pointer}table.dataTable thead>tr>th.dt-orderable-asc:hover,table.dataTable thead>tr>th.dt-orderable-desc:hover,table.dataTable thead>tr>td.dt-orderable-asc:hover,table.dataTable thead>tr>td.dt-orderable-desc:hover{outline:2px solid rgba(0, 0, 0, 0.05);outline-offset:-2px}table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:after{opacity:.6}table.dataTable thead>tr>th.sorting_desc_disabled span.dt-column-order:after,table.dataTable thead>tr>th.sorting_asc_disabled span.dt-column-order:before,table.dataTable thead>tr>td.sorting_desc_disabled span.dt-column-order:after,table.dataTable thead>tr>td.sorting_asc_disabled span.dt-column-order:before{display:none}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}div.dt-scroll-body>table.dataTable>thead>tr>th,div.dt-scroll-body>table.dataTable>thead>tr>td{overflow:hidden}:root.dark table.dataTable thead>tr>th.dt-orderable-asc:hover,:root.dark table.dataTable thead>tr>th.dt-orderable-desc:hover,:root.dark table.dataTable thead>tr>td.dt-orderable-asc:hover,:root.dark table.dataTable thead>tr>td.dt-orderable-desc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>th.dt-orderable-asc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>th.dt-orderable-desc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>td.dt-orderable-asc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>td.dt-orderable-desc:hover{outline:2px solid rgba(255, 255, 255, 0.05)}div.dt-processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-22px;text-align:center;padding:2px;z-index:10}div.dt-processing>div:last-child{position:relative;width:80px;height:15px;margin:1em auto}div.dt-processing>div:last-child>div{position:absolute;top:0;width:13px;height:13px;border-radius:50%;background:rgb(13, 110, 253);background:rgb(var(--dt-row-selected));animation-timing-function:cubic-bezier(0, 1, 1, 0)}div.dt-processing>div:last-child>div:nth-child(1){left:8px;animation:datatables-loader-1 .6s infinite}div.dt-processing>div:last-child>div:nth-child(2){left:8px;animation:datatables-loader-2 .6s infinite}div.dt-processing>div:last-child>div:nth-child(3){left:32px;animation:datatables-loader-2 .6s infinite}div.dt-processing>div:last-child>div:nth-child(4){left:56px;animation:datatables-loader-3 .6s infinite}@keyframes datatables-loader-1{0%{transform:scale(0)}100%{transform:scale(1)}}@keyframes datatables-loader-3{0%{transform:scale(1)}100%{transform:scale(0)}}@keyframes datatables-loader-2{0%{transform:translate(0, 0)}100%{transform:translate(24px, 0)}}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable th,table.dataTable td{box-sizing:border-box}table.dataTable th.dt-type-numeric,table.dataTable th.dt-type-date,table.dataTable td.dt-type-numeric,table.dataTable td.dt-type-date{text-align:right}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable th.dt-empty,table.dataTable td.dt-empty{text-align:center;vertical-align:top}table.dataTable thead th,table.dataTable thead td,table.dataTable tfoot th,table.dataTable tfoot td{text-align:left}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}/*! Bootstrap 5 integration for DataTables + * + * ©2020 SpryMedia Ltd, all rights reserved. + * License: MIT datatables.net/license/mit + */table.table.dataTable{clear:both;margin-bottom:0;max-width:none;border-spacing:0}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1)>*{box-shadow:none}table.table.dataTable>:not(caption)>*>*{background-color:var(--bs-table-bg)}table.table.dataTable>tbody>tr{background-color:transparent}table.table.dataTable>tbody>tr.selected>*{box-shadow:inset 0 0 0 9999px rgb(13, 110, 253);box-shadow:inset 0 0 0 9999px rgb(var(--dt-row-selected));color:rgb(255, 255, 255);color:rgb(var(--dt-row-selected-text))}table.table.dataTable>tbody>tr.selected a{color:rgb(9, 10, 11);color:rgb(var(--dt-row-selected-link))}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1)>*{box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-stripe), 0.05)}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1).selected>*{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.95);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95)}table.table.dataTable.table-hover>tbody>tr:hover>*{box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.075)}table.table.dataTable.table-hover>tbody>tr.selected:hover>*{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.975);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975)}div.dt-container div.dt-layout-start>*:not(:last-child){margin-right:1em}div.dt-container div.dt-layout-end>*:not(:first-child){margin-left:1em}div.dt-container div.dt-layout-full{width:100%}div.dt-container div.dt-layout-full>*:only-child{margin-left:auto;margin-right:auto}div.dt-container div.dt-layout-table>div{display:block !important}@media screen and (max-width: 767px){div.dt-container div.dt-layout-start>*:not(:last-child){margin-right:0}div.dt-container div.dt-layout-end>*:not(:first-child){margin-left:0}}div.dt-container div.dt-length label{font-weight:normal;text-align:left;white-space:nowrap}div.dt-container div.dt-length select{width:auto;display:inline-block;margin-right:.5em}div.dt-container div.dt-search{text-align:right}div.dt-container div.dt-search label{font-weight:normal;white-space:nowrap;text-align:left}div.dt-container div.dt-search input{margin-left:.5em;display:inline-block;width:auto}div.dt-container div.dt-paging{margin:0}div.dt-container div.dt-paging ul.pagination{margin:2px 0;flex-wrap:wrap}div.dt-container div.dt-row{position:relative}div.dt-scroll-head table.dataTable{margin-bottom:0 !important}div.dt-scroll-body{border-bottom-color:var(--bs-border-color);border-bottom-width:var(--bs-border-width);border-bottom-style:solid}div.dt-scroll-body>table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dt-scroll-body>table>tbody>tr:first-child{border-top-width:0}div.dt-scroll-body>table>thead>tr{border-width:0 !important}div.dt-scroll-body>table>tbody>tr:last-child>*{border-bottom:none}div.dt-scroll-foot>.dt-scroll-footInner{box-sizing:content-box}div.dt-scroll-foot>.dt-scroll-footInner>table{margin-top:0 !important;border-top:none}div.dt-scroll-foot>.dt-scroll-footInner>table>tfoot>tr:first-child{border-top-width:0 !important}@media screen and (max-width: 767px){div.dt-container div.dt-length,div.dt-container div.dt-search,div.dt-container div.dt-info,div.dt-container div.dt-paging{text-align:center}div.dt-container .row{--bs-gutter-y: 0.5rem}div.dt-container div.dt-paging ul.pagination{justify-content:center !important}}table.dataTable.table-sm>thead>tr th.dt-orderable-asc,table.dataTable.table-sm>thead>tr th.dt-orderable-desc,table.dataTable.table-sm>thead>tr th.dt-ordering-asc,table.dataTable.table-sm>thead>tr th.dt-ordering-desc,table.dataTable.table-sm>thead>tr td.dt-orderable-asc,table.dataTable.table-sm>thead>tr td.dt-orderable-desc,table.dataTable.table-sm>thead>tr td.dt-ordering-asc,table.dataTable.table-sm>thead>tr td.dt-ordering-desc{padding-right:20px}table.dataTable.table-sm>thead>tr th.dt-orderable-asc span.dt-column-order,table.dataTable.table-sm>thead>tr th.dt-orderable-desc span.dt-column-order,table.dataTable.table-sm>thead>tr th.dt-ordering-asc span.dt-column-order,table.dataTable.table-sm>thead>tr th.dt-ordering-desc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-orderable-asc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-orderable-desc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-ordering-asc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-ordering-desc span.dt-column-order{right:5px}div.dt-scroll-head table.table-bordered{border-bottom-width:0}div.table-responsive>div.dt-container>div.row{margin:0}div.table-responsive>div.dt-container>div.row>div[class^=col-]:first-child{padding-left:0}div.table-responsive>div.dt-container>div.row>div[class^=col-]:last-child{padding-right:0}:root[data-bs-theme=dark]{--dt-row-hover: 255, 255, 255;--dt-row-stripe: 255, 255, 255;--dt-column-ordering: 255, 255, 255} + + diff --git a/package-lock.json b/package-lock.json index 20f64b39d3..9c2bf2924b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "chai": "^6.0.x", "cypress": "<10.0.0", "cypress-file-upload": "^5.0.x", + "datatables.net-bs5": "^2.2.2", "dompurify": "^3.2.x", "eslint-plugin-jsdoc": "^60.2.x", "flatgeobuf": "~4.2.x", @@ -176,33 +177,33 @@ } }, "node_modules/@cacheable/memoize": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@cacheable/memoize/-/memoize-2.0.2.tgz", - "integrity": "sha512-wPrr7FUiq3Qt4yQyda2/NcOLTJCFcQSU3Am2adP+WLy+sz93/fKTokVTHmtz+rjp4PD7ee0AEOeRVNN6IvIfsg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@cacheable/memoize/-/memoize-2.0.3.tgz", + "integrity": "sha512-hl9wfQgpiydhQEIv7fkjEzTGE+tcosCXLKFDO707wYJ/78FVOlowb36djex5GdbSyeHnG62pomYLMuV/OT8Pbw==", "dev": true, "license": "MIT", "dependencies": { - "@cacheable/utils": "^2.0.2" + "@cacheable/utils": "^2.0.3" } }, "node_modules/@cacheable/memory": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.2.tgz", - "integrity": "sha512-sJTITLfeCI1rg7P3ssaGmQryq235EGT8dXGcx6oZwX5NRnKq9IE6lddlllcOl+oXW+yaeTRddCjo0xrfU6ZySA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.3.tgz", + "integrity": "sha512-R3UKy/CKOyb1LZG/VRCTMcpiMDyLH7SH3JrraRdK6kf3GweWCOU3sgvE13W3TiDRbxnDKylzKJvhUAvWl9LQOA==", "dev": true, "license": "MIT", "dependencies": { - "@cacheable/memoize": "^2.0.1", - "@cacheable/utils": "^2.0.2", + "@cacheable/memoize": "^2.0.3", + "@cacheable/utils": "^2.0.3", "@keyv/bigmap": "^1.0.2", "hookified": "^1.12.1", - "keyv": "^5.5.2" + "keyv": "^5.5.3" } }, "node_modules/@cacheable/memory/node_modules/keyv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.2.tgz", - "integrity": "sha512-TXcFHbmm/z7MGd1u9ASiCSfTS+ei6Z8B3a5JHzx3oPa/o7QzWVtPRpc4KGER5RR469IC+/nfg4U5YLIuDUua2g==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz", + "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==", "dev": true, "license": "MIT", "dependencies": { @@ -210,11 +211,24 @@ } }, "node_modules/@cacheable/utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.0.2.tgz", - "integrity": "sha512-JTFM3raFhVv8LH95T7YnZbf2YoE9wEtkPPStuRF9a6ExZ103hFvs+QyCuYJ6r0hA9wRtbzgZtwUCoDWxssZd4Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.1.0.tgz", + "integrity": "sha512-ZdxfOiaarMqMj+H7qwlt5EBKWaeGihSYVHdQv5lUsbn8MJJOTW82OIwirQ39U5tMZkNvy3bQE+ryzC+xTAb9/g==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "keyv": "^5.5.3" + } + }, + "node_modules/@cacheable/utils/node_modules/keyv": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz", + "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } }, "node_modules/@colors/colors": { "version": "1.5.0", @@ -424,17 +438,17 @@ } }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.58.0.tgz", - "integrity": "sha512-smMc5pDht/UVsCD3hhw/a/e/p8m0RdRYiluXToVfd+d4yaQQh7nn9bACjkk6nXJvat7EWPAxuFkMEFfrxeGa3Q==", + "version": "0.71.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.71.0.tgz", + "integrity": "sha512-2p9+dXWNQnp5Kq/V0XVWZiVAabzlX6rUW8vXXvtX8Yc1CkKgD93IPDEnv1sYZFkkS6HMvg6H0RMZfob/Co0YXA==", "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.8", - "@typescript-eslint/types": "^8.43.0", + "@typescript-eslint/types": "^8.46.0", "comment-parser": "1.4.1", "esquery": "^1.6.0", - "jsdoc-type-pratt-parser": "~5.4.0" + "jsdoc-type-pratt-parser": "~6.6.0" }, "engines": { "node": ">=20.11.0" @@ -502,20 +516,23 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", "dev": true, "license": "Apache-2.0", "peer": true, + "dependencies": { + "@eslint/core": "^0.16.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -552,9 +569,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.36.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", - "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", "dev": true, "license": "MIT", "engines": { @@ -576,14 +593,14 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.16.0", "levn": "^0.4.1" }, "engines": { @@ -817,9 +834,9 @@ } }, "node_modules/@jsonjoy.com/buffers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz", - "integrity": "sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.0.tgz", + "integrity": "sha512-6RX+W5a+ZUY/c/7J5s5jK9UinLfJo5oWKh84fb4X0yK2q4WXEWUWZWuEMjvCb1YNUQhEAhUfr5scEGOH7jC4YQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -851,16 +868,16 @@ } }, "node_modules/@jsonjoy.com/json-pack": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.14.0.tgz", - "integrity": "sha512-LpWbYgVnKzphN5S6uss4M25jJ/9+m6q6UJoeN6zTkK4xAGhKsiBRPVeF7OYMWonn5repMQbE5vieRXcMUrKDKw==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.19.0.tgz", + "integrity": "sha512-ed3bz2NTJOH+i/HoVOGDjI9FCIA1yW2xLFuB+7PABRJrs0Dj+SoUpHMhQgNe2xYZ3zTiT2jb6xp8VTvM1MBdcQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/buffers": "^1.2.0", "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.1", + "@jsonjoy.com/json-pointer": "^1.0.2", "@jsonjoy.com/util": "^1.9.0", "hyperdyperid": "^1.2.0", "thingies": "^2.5.0" @@ -1074,13 +1091,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.1.tgz", - "integrity": "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==", + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0.tgz", + "integrity": "sha512-Tzh95Twig7hUwwNe381/K3PggZBZblKUe2wv25oIpzWLr6Z0m4KgV1ZVIjnR6GM9ANEqjZD7XsZEa6JL/7YEgg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.55.1" + "playwright": "1.56.0" }, "bin": { "playwright": "cli.js" @@ -1263,9 +1280,9 @@ ] }, "node_modules/@rspack/cli": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@rspack/cli/-/cli-1.5.6.tgz", - "integrity": "sha512-8U/SJdbMdSpWB+FwztpRB5ZMdzF4n4k3PVC1yS6QL+OVpVHhfhRTq0S5fePM/R5qT5zgDj5/SPZeOj3+fQSJaw==", + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/@rspack/cli/-/cli-1.5.8.tgz", + "integrity": "sha512-CVqxLGTHBLGDJxYRlVNCtbWix+bXLIHxT11225wAXSyn/5/kJYWxJNNy42vjUNNGSP1Va/aI5lse/pCZjn3xNA==", "dev": true, "license": "MIT", "dependencies": { @@ -1658,9 +1675,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", "dev": true, "license": "MIT", "dependencies": { @@ -1754,13 +1771,12 @@ "license": "MIT" }, "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", + "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, @@ -1775,15 +1791,26 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz", + "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", "@types/node": "*", - "@types/send": "*" + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" } }, "node_modules/@types/sinonjs__fake-timers": { @@ -1839,9 +1866,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.1.tgz", - "integrity": "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz", + "integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==", "dev": true, "license": "MIT", "engines": { @@ -2569,9 +2596,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", - "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", + "version": "2.8.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.14.tgz", + "integrity": "sha512-GM9c0cWWR8Ga7//Ves/9KRgTS8nLausCkP3CGiFLrnwA2CDUluXgaQqvrULoR2Ujrd/mz/lkX87F5BHFsNr5sQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2741,9 +2768,9 @@ "license": "ISC" }, "node_modules/browserslist": { - "version": "4.26.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", - "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "dev": true, "funding": [ { @@ -2761,9 +2788,9 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.3", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, @@ -2881,23 +2908,24 @@ } }, "node_modules/cacheable": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.0.2.tgz", - "integrity": "sha512-dWjhLx8RWnPsAWVKwW/wI6OJpQ/hSVb1qS0NUif8TR9vRiSwci7Gey8x04kRU9iAF+Rnbtex5Kjjfg/aB5w8Pg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.1.0.tgz", + "integrity": "sha512-zzL1BxdnqwD69JRT0dihnawAcLkBMwAH+hZSKjUzeBbPedVhk3qYPjRw9VOMYWwt5xRih5xd8S+3kEdGohZm/g==", "dev": true, "license": "MIT", "dependencies": { - "@cacheable/memoize": "^2.0.2", - "@cacheable/memory": "^2.0.2", - "@cacheable/utils": "^2.0.2", + "@cacheable/memoize": "^2.0.3", + "@cacheable/memory": "^2.0.3", + "@cacheable/utils": "^2.1.0", "hookified": "^1.12.1", - "keyv": "^5.5.2" + "keyv": "^5.5.3", + "qified": "^0.5.0" } }, "node_modules/cacheable/node_modules/keyv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.2.tgz", - "integrity": "sha512-TXcFHbmm/z7MGd1u9ASiCSfTS+ei6Z8B3a5JHzx3oPa/o7QzWVtPRpc4KGER5RR469IC+/nfg4U5YLIuDUua2g==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz", + "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==", "dev": true, "license": "MIT", "dependencies": { @@ -2988,9 +3016,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001743", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", - "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", + "version": "1.0.30001749", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz", + "integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==", "dev": true, "funding": [ { @@ -3016,9 +3044,9 @@ "license": "Apache-2.0" }, "node_modules/chai": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.0.1.tgz", - "integrity": "sha512-/JOoU2//6p5vCXh00FpNgtlw0LjvhGttaWc+y7wpW9yjBm3ys0dI8tSKZxIOgNruz5J0RleccatSIC3uxEZP0g==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.0.tgz", + "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==", "dev": true, "license": "MIT", "engines": { @@ -3868,6 +3896,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/datatables.net": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-2.3.4.tgz", + "integrity": "sha512-fKuRlrBIdpAl2uIFgl9enKecHB41QmFd/2nN9LBbOvItV/JalAxLcyqdZXex7wX4ZXjnJQEnv6xeS9veOpKzSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-bs5": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/datatables.net-bs5/-/datatables.net-bs5-2.3.4.tgz", + "integrity": "sha512-OSoPWhNfiU71VjNP604uTmFRxiX32U7SCW0KRZ2X6z3ZYbIwjjoWcMEjjPWOH3uOqaI0OTDBgOgOs5G28VaJog==", + "dev": true, + "license": "MIT", + "dependencies": { + "datatables.net": "2.3.4", + "jquery": ">=1.7" + } + }, "node_modules/dayjs": { "version": "1.11.18", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", @@ -4240,9 +4289,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.222", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", - "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", + "version": "1.5.233", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.233.tgz", + "integrity": "sha512-iUdTQSf7EFXsDdQsp8MwJz5SVk4APEFqXU/S47OtQ0YLqacSwPXdZ5vRlMX3neb07Cy2vgioNuRnWUXFwuslkg==", "dev": true, "license": "ISC" }, @@ -4512,9 +4561,9 @@ } }, "node_modules/eslint": { - "version": "9.36.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", - "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", "peer": true, @@ -4522,11 +4571,11 @@ "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.36.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -4574,19 +4623,20 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "60.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-60.2.0.tgz", - "integrity": "sha512-VQNycH0EbjIvgdX6llnLidv7X/yQTJMoQl/L5bXAPasAlPZQFJQi7r5OLc29ushbeQJ7+BFf8UFt9JAQNBZhXg==", + "version": "60.8.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-60.8.3.tgz", + "integrity": "sha512-4191bTMvnd5WUtopCdzNhQchvv/MxtPD86ZGl3vem8Ibm22xJhKuIyClmgSxw+YERtorVc/NhG+bGjfFVa6+VQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@es-joy/jsdoccomment": "~0.58.0", + "@es-joy/jsdoccomment": "~0.71.0", "are-docs-informative": "^0.0.2", "comment-parser": "1.4.1", "debug": "^4.4.3", "escape-string-regexp": "^4.0.0", "espree": "^10.4.0", "esquery": "^1.6.0", + "html-entities": "^2.6.0", "object-deep-merge": "^1.0.5", "parse-imports-exports": "^0.2.4", "semver": "^7.7.2", @@ -5481,6 +5531,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/geotiff": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.1.3.tgz", @@ -5649,9 +5709,9 @@ } }, "node_modules/glob-to-regex.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.0.1.tgz", - "integrity": "sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6122,6 +6182,23 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -6661,14 +6738,15 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -7096,6 +7174,13 @@ "node": ">= 10.13.0" } }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/js-base64": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", @@ -7135,13 +7220,13 @@ "license": "MIT" }, "node_modules/jsdoc-type-pratt-parser": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-5.4.0.tgz", - "integrity": "sha512-F9GQ+F1ZU6qvSrZV8fNFpjDNf614YzR2eF6S0+XbDjAcUI28FSoXnYZFjQmb1kFx3rrJb5PnxUH3/Yti6fcM+g==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-6.6.0.tgz", + "integrity": "sha512-3hSD14nXx66Rspx1RMnz1Pj4JacrMBAsC0CrF9lZYO/Qsp5/oIr6KqujVUNhQu94B6mMip2ukki8MpEWZwyhKA==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.0.0" + "node": ">=20.0.0" } }, "node_modules/jsesc": { @@ -7387,13 +7472,17 @@ } }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loader-utils": { @@ -7594,9 +7683,9 @@ } }, "node_modules/memfs": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.43.0.tgz", - "integrity": "sha512-XCdhMy33sgxCwJ4JgjSasLGgOjFm9/i+IEhO03gPHroJeTBhM7ZQ1+v3j7di9nNKtcLGjVvKjHVOkTbVop/R/Q==", + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.49.0.tgz", + "integrity": "sha512-L9uC9vGuc4xFybbdOpRLoOAOq1YEBBsocCs5NVW32DfU+CZWWIn3OVF+lB8Gp4ttBVSMazwrTrjv8ussX/e3VQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -7607,9 +7696,6 @@ "tree-dump": "^1.0.3", "tslib": "^2.0.0" }, - "engines": { - "node": ">= 4.0.0" - }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" @@ -7854,9 +7940,9 @@ } }, "node_modules/mocha": { - "version": "11.7.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.2.tgz", - "integrity": "sha512-lkqVJPmqqG/w5jmmFtiRvtA2jkDyNVUcefFJKb2uyX4dekk8Okgqop3cgbFiaIvj8uCRJVTP5x9dfxGyXm2jvQ==", + "version": "11.7.4", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.4.tgz", + "integrity": "sha512-1jYAaY8x0kAZ0XszLWu14pzsf4KV740Gld4HXkhNTXwcHx4AUEDkPzgEHg9CM5dVcW+zv036tjpsEbLraPJj4w==", "dev": true, "license": "MIT", "dependencies": { @@ -7868,6 +7954,7 @@ "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", "minimatch": "^9.0.5", @@ -8118,9 +8205,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", "dev": true, "license": "MIT" }, @@ -8807,13 +8894,13 @@ } }, "node_modules/playwright": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz", - "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==", + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0.tgz", + "integrity": "sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.55.1" + "playwright-core": "1.56.0" }, "bin": { "playwright": "cli.js" @@ -8826,9 +8913,9 @@ } }, "node_modules/playwright-core": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz", - "integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==", + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0.tgz", + "integrity": "sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -8839,9 +8926,9 @@ } }, "node_modules/playwright-ctrf-json-reporter": { - "version": "0.0.23", - "resolved": "https://registry.npmjs.org/playwright-ctrf-json-reporter/-/playwright-ctrf-json-reporter-0.0.23.tgz", - "integrity": "sha512-49e1gSdq7Rytz9ji8kwgrrWZ0w39RNVDH5FlPiBeosdn12VTfWmhYRBcKpC2D29pa1ZdTxGhHdbB4UML2MNITg==", + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/playwright-ctrf-json-reporter/-/playwright-ctrf-json-reporter-0.0.24.tgz", + "integrity": "sha512-NLwmLA4aTd3GEhgJXYjIfwDFuhhEuz24hYpnuovE/EF/AY3xLqt3JYd4ylE5tbgW9bYwMVqpEsv9pL1oUcFIjQ==", "dev": true, "license": "MIT" }, @@ -9219,6 +9306,19 @@ "node": ">=6" } }, + "node_modules/qified": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.5.0.tgz", + "integrity": "sha512-Zj6Q/Vc/SQ+Fzc87N90jJUzBzxD7MVQ2ZvGyMmYtnl2u1a07CejAhvtk4ZwASos+SiHKCAIylyGHJKIek75QBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.12.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/qs": { "version": "6.10.4", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", @@ -9778,9 +9878,9 @@ "license": "ISC" }, "node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", "dependencies": { @@ -9856,9 +9956,9 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -10177,9 +10277,9 @@ } }, "node_modules/shpjs": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/shpjs/-/shpjs-6.1.0.tgz", - "integrity": "sha512-uaUpod7uIWetJK80yiiedZ3x4z9ZAPgDVT89N27+8F97Z8ZOqmu88P96I6CBC8N+YyERqdneZNT/wNFUEnzNpw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/shpjs/-/shpjs-6.2.0.tgz", + "integrity": "sha512-8cR/RKYHQepmVyBMtzZQ+1bnSbWrtLXS6aoEJmpUlOSHtSUddterebVxYmIWq2g9kOEX9jm2kjHiikyPX7cNQA==", "dev": true, "license": "MIT", "dependencies": { @@ -10967,9 +11067,9 @@ } }, "node_modules/stylelint-config-standard": { - "version": "39.0.0", - "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-39.0.0.tgz", - "integrity": "sha512-JabShWORb8Bmc1A47ZyJstran60P3yUdI1zWMpGYPeFiC6xzHXJMkpKAd8EjIhq3HPUplIWWMDJ/xu0AiPd+kA==", + "version": "39.0.1", + "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-39.0.1.tgz", + "integrity": "sha512-b7Fja59EYHRNOTa3aXiuWnhUWXFU2Nfg6h61bLfAb5GS5fX3LMUD0U5t4S8N/4tpHQg3Acs2UVPR9jy2l1g/3A==", "dev": true, "funding": [ { @@ -11010,13 +11110,13 @@ } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "6.1.14", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.14.tgz", - "integrity": "sha512-ExZSCSV9e7v/Zt7RzCbX57lY2dnPdxzU/h3UE6WJ6NtEMfwBd8jmi1n4otDEUfz+T/R+zxrFDpICFdjhD3H/zw==", + "version": "6.1.17", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.17.tgz", + "integrity": "sha512-Jzse4YoiUJBVYTwz5Bwl4h/2VQM7e2KK3MVAMlXzX9uamIHAH/TXUlRKU1AQGQOryQhN0EsmufiiF40G057YXA==", "dev": true, "license": "MIT", "dependencies": { - "cacheable": "^2.0.1", + "cacheable": "^2.0.3", "flatted": "^3.3.3", "hookified": "^1.12.0" } @@ -11572,9 +11672,9 @@ } }, "node_modules/tapable": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", - "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "license": "MIT", "engines": { @@ -12388,9 +12488,9 @@ "license": "Apache-2.0" }, "node_modules/webpack": { - "version": "5.102.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.0.tgz", - "integrity": "sha512-hUtqAR3ZLVEYDEABdBioQCIqSoguHbFn1K7WlPPWSuXmx0031BD73PSE35jKyftdSh4YLDoQNgK4pqBt5Q82MA==", + "version": "5.102.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", + "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12402,7 +12502,7 @@ "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.24.5", + "browserslist": "^4.26.3", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.3", "es-module-lexer": "^1.2.1", @@ -12414,8 +12514,8 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.2.3", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.4", "webpack-sources": "^3.3.3" @@ -12496,14 +12596,14 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.3.tgz", - "integrity": "sha512-5kA/PzpZzDz5mNOkcNLmU1UdjGeSSxd7rt1akWpI70jMNHLASiBPRaQZn0hgyhvhawfIwSnnLfDABIxL3ueyFg==", + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", + "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", "dev": true, "license": "MIT", "dependencies": { "colorette": "^2.0.10", - "memfs": "^4.6.0", + "memfs": "^4.43.1", "mime-types": "^3.0.1", "on-finished": "^2.4.1", "range-parser": "^1.2.1", diff --git a/package.json b/package.json index 0eb2b3df74..e198824d01 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "cypress": "<10.0.0", "cypress-file-upload": "^5.0.x", "dompurify": "^3.2.x", + "datatables.net-bs5": "^2.2.2", "eslint-plugin-jsdoc": "^60.2.x", "flatgeobuf": "~4.2.x", "jsts": "^2.11.x", diff --git a/stylelint.config.mjs b/stylelint.config.mjs index ca0f28030c..d21e1bed4b 100644 --- a/stylelint.config.mjs +++ b/stylelint.config.mjs @@ -24,6 +24,7 @@ export default { "lizmap/www/assets/css/dataTables.bootstrap.min.css", "lizmap/www/assets/css/jquery.dataTables.min.css", "lizmap/www/assets/css/responsive.dataTables.min.css", + "lizmap/www/assets/css/datatables.min.css", "lizmap/www/assets/jelix/**/*.css", "lizmap/www/assets/js/jquery/**/*.css", "tests/units/vendor/**/*.css", From 7ed653851415538fc3bfbd257c8b3441f1d3611c Mon Sep 17 00:00:00 2001 From: nboisteault Date: Fri, 11 Apr 2025 16:15:25 +0200 Subject: [PATCH 20/45] Attribute table: update code for Datatables v2.2.2 Legacy datatables is kept for admin part of Lizmap --- assets/src/legacy/attributeTable.js | 143 ++++++---------------------- assets/src/legacy/map.js | 4 +- lizmap/www/assets/css/map.css | 83 ---------------- 3 files changed, 34 insertions(+), 196 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index e2de6692ec..e3b4dcf71d 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -258,13 +258,6 @@ var lizAttributeTable = function() { dTable.draw(); }); }); - - // Bind click on tabs to resize datatable tables - $('#attributeLayers-tabs li').click(function(){ - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - refreshDatatableSize('#'+mycontainerId); - }); - } else { // Hide navbar menu $('#mapmenu li.attributeLayers').hide(); @@ -501,12 +494,6 @@ var lizAttributeTable = function() { // Action bar specific to the tab html+= '
'; - // Search input - html+= '
'; - html+= ' '; - html+= ' '; - html+= '
'; - // Selected searched lines button html+= ''; @@ -652,22 +639,12 @@ var lizAttributeTable = function() { $(tabContentId).remove(); //remove respective tab content }); - if( childHtml ){ - - // Bind adjust child columns when children tab visibility change - $('#attribute-layer-' + cleanName + ' div.attribute-layer-child-content ul li a[data-toggle="tab"]').on('shown.bs.tab', function (e) { - var target = $(e.target).attr("href") // activated tab - var dtable = $(target).find('table.dataTable'); - dtable.DataTable().tables().columns.adjust(); - }); - } - if( childHtml ){ $('#attribute-layer-'+ cleanName + ' button.btn-toggle-children') .click(function(){ var parentDir = $(this).parents('div.attribute-layer-main'); parentDir.find('div.attribute-layer-content').toggleClass('showChildren'); - parentDir.find('div.tabbable.attribute-layer-child-content').toggle(); + parentDir.find('div.attribute-layer-child-content').toggle(); // Refresh parent table size refreshDatatableSize('#attribute-layer-main-'+ cleanName); return false; @@ -683,8 +660,6 @@ var lizAttributeTable = function() { $('#attribute-layer-main-' + cleanName).toggleClass('reduced', !$(this).hasClass('btn-primary')); $('#attribute-table-panel-' + cleanName).toggleClass('visible', !$(this).hasClass('btn-primary')); $(this).toggleClass('btn-primary'); - - refreshDatatableSize('#attribute-layer-main-'+ cleanName); return false; }); } @@ -1387,28 +1362,12 @@ var lizAttributeTable = function() { lConfig['alias'] = cAliases; // Datatable configuration - if ( !$.fn.dataTable.isDataTable( aTable ) ) { - // Search while typing in text input - // Deactivate if too many items - var searchWhileTyping = true; - - var myDom = '<ipl>'; - if( searchWhileTyping ) { - $('#attribute-layer-search-' + cleanName).on( 'keyup', function (e){ - var searchVal = this.value; - lizdelay(function(){ - oTable.fnFilter( searchVal ); - }, 500 ); - }); - }else{ - myDom = '<ipl>'; - } - + if ( !DataTable.isDataTable( aTable ) ) { const datatablesUrl = globalThis['lizUrls'].wms.replace('service', 'datatables'); const params = globalThis['lizUrls'].params; params['layerId'] = lConfig.id; - $( aTable ).dataTable({ + const oTable = new DataTable(aTable, { serverSide: true ,ajax: { url: datatablesUrl + '?' + new URLSearchParams(params).toString(), @@ -1468,17 +1427,17 @@ var lizAttributeTable = function() { } } ,columns: columns - ,initComplete: function(settings, json) { - const api = new $.fn.dataTable.Api(settings); - const tableId = api.table().node().id; - const featureType = tableId.split('attribute-layer-table-')[1]; + ,initComplete: function(settings) { + // Refresh size of datatable after data has been loaded + refreshDatatableSize('#'+$('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id')); // Trigger event telling attribute table is ready - lizMap.events.triggerEvent("attributeLayerContentReady", - { - 'featureType': featureType - } - ); + const tableId = settings.api.table().node().id; + const featureType = tableId.split('attribute-layer-table-')[1]; + + lizMap.events.triggerEvent("attributeLayerContentReady",{ + 'featureType': featureType, + }); } ,order: [[ firstDisplayedColIndex, "asc" ]] ,language: { url:globalThis['lizUrls']["dataTableLanguage"] } @@ -1496,43 +1455,35 @@ var lizAttributeTable = function() { thumbnail.setAttribute('src', lizUrls.media+'?repository='+lizUrls.params.repository+'&project='+lizUrls.params.project+'&path='+thumbnail.dataset.src); } } - ,dom: myDom ,pageLength: 50 - ,scrollY: '95%' - ,scrollX: '100%' - } ); - - var oTable = $( aTable ).dataTable(); - - if( !searchWhileTyping ) - $('#attribute-layer-search-' + cleanName).hide(); - - // Bind button which clears top-left search input content - $('#attribute-layer-search-' + cleanName).next('.clear-layer-search').click(function(){ - $('#attribute-layer-search-' + cleanName).val('').focus().keyup(); + ,scrollY: '95%' // Used to init but refreshDatatableSize() does the job of setting the height + ,layout: { + topStart: null, + topEnd: null, + bottomStart: ['info', 'pageLength'], + bottomEnd: { + paging: { + firstLast: false + } + }, + } }); // Unbind previous events on page - $( aTable ).on( 'page.dt', function() { + oTable.on( 'page', function() { // unbind previous events $(aTable +' tr').unbind('click'); $(aTable +' tr td button').unbind('click'); }); // Bind events when drawing table - $( aTable ).on( 'draw.dt', function() { + oTable.on( 'draw', function() { $(aTable +' tr').unbind('click'); $(aTable +' tr td button').unbind('click'); // Bind event when users click anywhere on the table line to highlight bindTableLineClick(aName, aTable); - - // Refresh size - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - - refreshDatatableSize('#' + mycontainerId); - return false; }); @@ -1547,8 +1498,6 @@ var lizAttributeTable = function() { lizMap.mainLizmap.edition.fetchEditableFeatures([lConfig.id]); } - refreshDatatableSize('#'+$('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id')) - if (aCallback) aCallback(aName,aTable); @@ -2069,7 +2018,7 @@ var lizAttributeTable = function() { var tableLayerName = $(this).parents('div.dataTables_wrapper:first').prev('input.attribute-table-hidden-layer').val() // Get parent table for the feature type if ( tableLayerName - && $.fn.dataTable.isDataTable( $(this) ) + && DataTable.isDataTable( $(this) ) && lizMap.cleanName( featureType ) == tableLayerName ){ @@ -2932,7 +2881,7 @@ var lizAttributeTable = function() { var tableLayerName = $(this).parents('div.dataTables_wrapper:first').prev('input.attribute-table-hidden-layer').val() if ( tableLayerName - && $.fn.dataTable.isDataTable( $(this) ) + && DataTable.isDataTable( $(this) ) && lizMap.cleanName( featureType ) == tableLayerName ){ var zTable = '#' + tableId; @@ -3039,14 +2988,10 @@ var lizAttributeTable = function() { var h = $(container + ' div.attribute-layer-content').height() ? $(container + ' div.attribute-layer-content').height() : 0; h -= $(container + ' thead').height() ? $(container + ' thead').height() : 0; - h -= $(container + ' div.dataTables_paginate').height() ? $(container + ' div.dataTables_paginate').height() : 0; - h -= $(container + ' div.dataTables_filter').height() ? $(container + ' div.dataTables_filter').height() : 0; - h -= 20; - - dtable.parent('div.dataTables_scrollBody').height(h); + h -= $(container + ' div.dt-paging').height() ? $(container + ' div.dt-paging').height() : 0; + h -= 25; - // Width : adapt columns size - dtable.DataTable().tables().columns.adjust(); + dtable.parent('div.dt-scroll-body').height(h); } /** @@ -3525,35 +3470,10 @@ var lizAttributeTable = function() { } }, - bottomdocksizechanged: function(evt) { + bottomdocksizechanged: function() { var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); refreshDatatableSize('#'+mycontainerId); - }, - dockopened: function(evt) { - if($('#mapmenu li.attributeLayers').hasClass('active')){ - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - refreshDatatableSize('#'+mycontainerId); - } - }, - dockclosed: function(evt) { - if($('#mapmenu li.attributeLayers').hasClass('active')){ - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - refreshDatatableSize('#'+mycontainerId); - } - }, - rightdockopened: function(evt) { - if($('#mapmenu li.attributeLayers').hasClass('active')){ - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - refreshDatatableSize('#'+mycontainerId); - } - }, - rightdockclosed: function(evt) { - if($('#mapmenu li.attributeLayers').hasClass('active')){ - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - refreshDatatableSize('#'+mycontainerId); - } } - }); // lizMap.events.on end // Extend lizMap API @@ -3561,5 +3481,4 @@ var lizAttributeTable = function() { } // uicreated }); - }(); diff --git a/assets/src/legacy/map.js b/assets/src/legacy/map.js index 5acd3926f3..55810e0ca5 100644 --- a/assets/src/legacy/map.js +++ b/assets/src/legacy/map.js @@ -15,6 +15,8 @@ import WFS from '../modules/WFS.js'; import WMS from '../modules/WMS.js'; import { Utils } from '../modules/Utils.js'; +import DataTable from 'datatables.net-bs5'; + window.lizMap = function() { /** * PRIVATE Property: config @@ -1331,7 +1333,7 @@ window.lizMap = function() { } // Handle compact-tables/explode-tables behaviour - parentDiv.find('.lizmapPopupChildren .popupAllFeaturesCompact table').DataTable({ + new DataTable(parentDiv.find('.lizmapPopupChildren .popupAllFeaturesCompact table'),{ order: [[1, 'asc']], language: { url:globalThis['lizUrls']["dataTableLanguage"] } }); diff --git a/lizmap/www/assets/css/map.css b/lizmap/www/assets/css/map.css index b95dd83ebf..8d4491e332 100644 --- a/lizmap/www/assets/css/map.css +++ b/lizmap/www/assets/css/map.css @@ -1289,88 +1289,6 @@ SubDock font-size: 1em; } -#attributeLayers .tab-content .attribute-content div.dataTables_wrapper div.dataTables_paginate ul.pagination { - margin: 2px 0; - white-space: nowrap; -} - -#attributeLayers .tab-content .attribute-content .pagination { - display: inline-block; - padding-left: 0; - margin: 20px 0; - border-radius: 4px; -} - -#attributeLayers .tab-content .attribute-content .pagination > li { - display: inline; - box-sizing: border-box; - padding:0; - margin:0; - border:0; - border-radius:0; -} - -#attributeLayers .tab-content .attribute-content .pagination > li > a, -#attributeLayers .tab-content .attribute-content .pagination > li > span { - position: relative; - float: left; - padding: 6px 12px; - margin-left: -1px; - line-height: 1.4286; - color: #337ab7; - text-decoration: none; - background-color: #fff; - border: 1px solid #ddd; -} - -#attributeLayers .tab-content .attribute-content .pagination > li > a:focus, -#attributeLayers .tab-content .attribute-content .pagination > li > a:hover, -#attributeLayers .tab-content .attribute-content .pagination > li > span:focus, -#attributeLayers .tab-content .attribute-content .pagination > li > span:hover { - z-index: 3; - color: #23527c; - background-color: #eee; - border-color: #ddd; -} - -#attributeLayers .tab-content .attribute-content .pagination > .active > a, -#attributeLayers .tab-content .attribute-content .pagination > .active > a:focus, -#attributeLayers .tab-content .attribute-content .pagination > .active > a:hover, -#attributeLayers .tab-content .attribute-content .pagination > .active > span, -#attributeLayers .tab-content .attribute-content .pagination > .active > span:focus, -#attributeLayers .tab-content .attribute-content .pagination > .active > span:hover { - z-index: 2; - color: #fff; - cursor: default; - background-color: #337ab7; - border-color: #337ab7; -} - -#attributeLayers .tab-content .attribute-content .pagination > .disabled > a, -#attributeLayers .tab-content .attribute-content .pagination > .disabled > a:focus, -#attributeLayers .tab-content .attribute-content .pagination > .disabled > a:hover, -#attributeLayers .tab-content .attribute-content .pagination > .disabled > span, -#attributeLayers .tab-content .attribute-content .pagination > .disabled > span:focus, -#attributeLayers .tab-content .attribute-content .pagination > .disabled > span:hover { - color: #777; - cursor: not-allowed; - background-color: #fff; - border-color: #ddd; -} - -#attributeLayers .tab-content .attribute-content .pagination > li:first-child > a, -#attributeLayers .tab-content .attribute-content .pagination > li:first-child > span { - margin-left: 0; - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; -} - -#attributeLayers .tab-content .attribute-content .pagination > li:last-child > a, -#attributeLayers .tab-content .attribute-content .pagination > li:last-child > span { - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; -} - #attribute-summary.attribute-content.bottom-content{ overflow:auto; } @@ -1401,7 +1319,6 @@ SubDock } .attribute-layer-content { - overflow:auto; height: 90%; clear:right; } From 31641b5382f9802ad6a2252ffb6ca6b2e835b122 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 15 Apr 2025 10:31:49 +0200 Subject: [PATCH 21/45] Add Datataables searchBuilder --- assets/src/legacy/attributeTable.js | 27 +++++++++++- lizmap/www/assets/css/datatables.min.css | 13 +++++- lizmap/www/assets/css/map.css | 10 ++++- package-lock.json | 56 ++++++++++++++++++++++++ package.json | 3 ++ 5 files changed, 105 insertions(+), 4 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index e3b4dcf71d..adbdc16e67 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -6,6 +6,9 @@ */ import DataTable from 'datatables.net-bs5'; +import 'datatables.net-buttons-bs5'; +import DateTime from 'datatables.net-datetime'; +import 'datatables.net-searchbuilder-bs5'; import DOMPurify from 'dompurify'; import '../images/svg/filter-square.svg'; @@ -1469,6 +1472,28 @@ var lizAttributeTable = function() { } }); + // Add searchBuilder button + // Disable live search to avoid searching on each keystroke + // Only display columns that are sortable for search + const searchBuilderButton = new DataTable.Buttons(oTable, { + buttons: [ + { + extend: 'searchBuilder', + config: { + liveSearch: false, + columns: '.dt-orderable-asc' + } + } + ] + }); + + // Attach searchBuilder button to attribute-layer-action-bar + const actionBar = document.querySelector(aTable) + .closest('.attribute-layer-content') + .previousElementSibling; + + actionBar.insertAdjacentElement('afterbegin', searchBuilderButton.container()[0]); + // Unbind previous events on page oTable.on( 'page', function() { // unbind previous events @@ -1521,7 +1546,7 @@ var lizAttributeTable = function() { data: "lizSelected", width: "25px", searchable: false, - sortable: true, + sortable: false, visible: false }); firstDisplayedColIndex+=1; diff --git a/lizmap/www/assets/css/datatables.min.css b/lizmap/www/assets/css/datatables.min.css index 095585d8bb..721e5366a6 100644 --- a/lizmap/www/assets/css/datatables.min.css +++ b/lizmap/www/assets/css/datatables.min.css @@ -4,10 +4,10 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.2.2 + * https://datatables.net/download/#bs5/dt-2.2.2/b-3.2.2/date-1.5.5/sb-1.8.2 * * Included libraries: - * DataTables 2.2.2 + * DataTables 2.2.2, Buttons 3.2.2, DateTime 1.5.5, SearchBuilder 1.8.2 */ :root{--dt-row-selected: 13, 110, 253;--dt-row-selected-text: 255, 255, 255;--dt-row-selected-link: 9, 10, 11;--dt-row-stripe: 0, 0, 0;--dt-row-hover: 0, 0, 0;--dt-column-ordering: 0, 0, 0;--dt-html-background: white}:root.dark{--dt-html-background: rgb(33, 37, 41)}table.dataTable td.dt-control{text-align:center;cursor:pointer}table.dataTable td.dt-control:before{display:inline-block;box-sizing:border-box;content:"";border-top:5px solid transparent;border-left:10px solid rgba(0, 0, 0, 0.5);border-bottom:5px solid transparent;border-right:0px solid transparent}table.dataTable tr.dt-hasChild td.dt-control:before{border-top:10px solid rgba(0, 0, 0, 0.5);border-left:5px solid transparent;border-bottom:0px solid transparent;border-right:5px solid transparent}table.dataTable tfoot:empty{display:none}html.dark table.dataTable td.dt-control:before,:root[data-bs-theme=dark] table.dataTable td.dt-control:before,:root[data-theme=dark] table.dataTable td.dt-control:before{border-left-color:rgba(255, 255, 255, 0.5)}html.dark table.dataTable tr.dt-hasChild td.dt-control:before,:root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before,:root[data-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before{border-top-color:rgba(255, 255, 255, 0.5);border-left-color:transparent}div.dt-scroll{width:100%}div.dt-scroll-body thead tr,div.dt-scroll-body tfoot tr{height:0}div.dt-scroll-body thead tr th,div.dt-scroll-body thead tr td,div.dt-scroll-body tfoot tr th,div.dt-scroll-body tfoot tr td{height:0 !important;padding-top:0px !important;padding-bottom:0px !important;border-top-width:0px !important;border-bottom-width:0px !important}div.dt-scroll-body thead tr th div.dt-scroll-sizing,div.dt-scroll-body thead tr td div.dt-scroll-sizing,div.dt-scroll-body tfoot tr th div.dt-scroll-sizing,div.dt-scroll-body tfoot tr td div.dt-scroll-sizing{height:0 !important;overflow:hidden !important}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:before{position:absolute;display:block;bottom:50%;content:"▲";content:"▲"/""}table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:after{position:absolute;display:block;top:50%;content:"▼";content:"▼"/""}table.dataTable thead>tr>th.dt-orderable-asc,table.dataTable thead>tr>th.dt-orderable-desc,table.dataTable thead>tr>th.dt-ordering-asc,table.dataTable thead>tr>th.dt-ordering-desc,table.dataTable thead>tr>td.dt-orderable-asc,table.dataTable thead>tr>td.dt-orderable-desc,table.dataTable thead>tr>td.dt-ordering-asc,table.dataTable thead>tr>td.dt-ordering-desc{position:relative;padding-right:30px}table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order,table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order{position:absolute;right:12px;top:0;bottom:0;width:12px}table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:after,table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:before,table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:after,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:before,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:after{left:0;opacity:.125;line-height:9px;font-size:.8em}table.dataTable thead>tr>th.dt-orderable-asc,table.dataTable thead>tr>th.dt-orderable-desc,table.dataTable thead>tr>td.dt-orderable-asc,table.dataTable thead>tr>td.dt-orderable-desc{cursor:pointer}table.dataTable thead>tr>th.dt-orderable-asc:hover,table.dataTable thead>tr>th.dt-orderable-desc:hover,table.dataTable thead>tr>td.dt-orderable-asc:hover,table.dataTable thead>tr>td.dt-orderable-desc:hover{outline:2px solid rgba(0, 0, 0, 0.05);outline-offset:-2px}table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:after{opacity:.6}table.dataTable thead>tr>th.sorting_desc_disabled span.dt-column-order:after,table.dataTable thead>tr>th.sorting_asc_disabled span.dt-column-order:before,table.dataTable thead>tr>td.sorting_desc_disabled span.dt-column-order:after,table.dataTable thead>tr>td.sorting_asc_disabled span.dt-column-order:before{display:none}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}div.dt-scroll-body>table.dataTable>thead>tr>th,div.dt-scroll-body>table.dataTable>thead>tr>td{overflow:hidden}:root.dark table.dataTable thead>tr>th.dt-orderable-asc:hover,:root.dark table.dataTable thead>tr>th.dt-orderable-desc:hover,:root.dark table.dataTable thead>tr>td.dt-orderable-asc:hover,:root.dark table.dataTable thead>tr>td.dt-orderable-desc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>th.dt-orderable-asc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>th.dt-orderable-desc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>td.dt-orderable-asc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>td.dt-orderable-desc:hover{outline:2px solid rgba(255, 255, 255, 0.05)}div.dt-processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-22px;text-align:center;padding:2px;z-index:10}div.dt-processing>div:last-child{position:relative;width:80px;height:15px;margin:1em auto}div.dt-processing>div:last-child>div{position:absolute;top:0;width:13px;height:13px;border-radius:50%;background:rgb(13, 110, 253);background:rgb(var(--dt-row-selected));animation-timing-function:cubic-bezier(0, 1, 1, 0)}div.dt-processing>div:last-child>div:nth-child(1){left:8px;animation:datatables-loader-1 .6s infinite}div.dt-processing>div:last-child>div:nth-child(2){left:8px;animation:datatables-loader-2 .6s infinite}div.dt-processing>div:last-child>div:nth-child(3){left:32px;animation:datatables-loader-2 .6s infinite}div.dt-processing>div:last-child>div:nth-child(4){left:56px;animation:datatables-loader-3 .6s infinite}@keyframes datatables-loader-1{0%{transform:scale(0)}100%{transform:scale(1)}}@keyframes datatables-loader-3{0%{transform:scale(1)}100%{transform:scale(0)}}@keyframes datatables-loader-2{0%{transform:translate(0, 0)}100%{transform:translate(24px, 0)}}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable th,table.dataTable td{box-sizing:border-box}table.dataTable th.dt-type-numeric,table.dataTable th.dt-type-date,table.dataTable td.dt-type-numeric,table.dataTable td.dt-type-date{text-align:right}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable th.dt-empty,table.dataTable td.dt-empty{text-align:center;vertical-align:top}table.dataTable thead th,table.dataTable thead td,table.dataTable tfoot th,table.dataTable tfoot td{text-align:left}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}/*! Bootstrap 5 integration for DataTables @@ -17,3 +17,12 @@ */table.table.dataTable{clear:both;margin-bottom:0;max-width:none;border-spacing:0}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1)>*{box-shadow:none}table.table.dataTable>:not(caption)>*>*{background-color:var(--bs-table-bg)}table.table.dataTable>tbody>tr{background-color:transparent}table.table.dataTable>tbody>tr.selected>*{box-shadow:inset 0 0 0 9999px rgb(13, 110, 253);box-shadow:inset 0 0 0 9999px rgb(var(--dt-row-selected));color:rgb(255, 255, 255);color:rgb(var(--dt-row-selected-text))}table.table.dataTable>tbody>tr.selected a{color:rgb(9, 10, 11);color:rgb(var(--dt-row-selected-link))}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1)>*{box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-stripe), 0.05)}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1).selected>*{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.95);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95)}table.table.dataTable.table-hover>tbody>tr:hover>*{box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.075)}table.table.dataTable.table-hover>tbody>tr.selected:hover>*{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.975);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975)}div.dt-container div.dt-layout-start>*:not(:last-child){margin-right:1em}div.dt-container div.dt-layout-end>*:not(:first-child){margin-left:1em}div.dt-container div.dt-layout-full{width:100%}div.dt-container div.dt-layout-full>*:only-child{margin-left:auto;margin-right:auto}div.dt-container div.dt-layout-table>div{display:block !important}@media screen and (max-width: 767px){div.dt-container div.dt-layout-start>*:not(:last-child){margin-right:0}div.dt-container div.dt-layout-end>*:not(:first-child){margin-left:0}}div.dt-container div.dt-length label{font-weight:normal;text-align:left;white-space:nowrap}div.dt-container div.dt-length select{width:auto;display:inline-block;margin-right:.5em}div.dt-container div.dt-search{text-align:right}div.dt-container div.dt-search label{font-weight:normal;white-space:nowrap;text-align:left}div.dt-container div.dt-search input{margin-left:.5em;display:inline-block;width:auto}div.dt-container div.dt-paging{margin:0}div.dt-container div.dt-paging ul.pagination{margin:2px 0;flex-wrap:wrap}div.dt-container div.dt-row{position:relative}div.dt-scroll-head table.dataTable{margin-bottom:0 !important}div.dt-scroll-body{border-bottom-color:var(--bs-border-color);border-bottom-width:var(--bs-border-width);border-bottom-style:solid}div.dt-scroll-body>table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dt-scroll-body>table>tbody>tr:first-child{border-top-width:0}div.dt-scroll-body>table>thead>tr{border-width:0 !important}div.dt-scroll-body>table>tbody>tr:last-child>*{border-bottom:none}div.dt-scroll-foot>.dt-scroll-footInner{box-sizing:content-box}div.dt-scroll-foot>.dt-scroll-footInner>table{margin-top:0 !important;border-top:none}div.dt-scroll-foot>.dt-scroll-footInner>table>tfoot>tr:first-child{border-top-width:0 !important}@media screen and (max-width: 767px){div.dt-container div.dt-length,div.dt-container div.dt-search,div.dt-container div.dt-info,div.dt-container div.dt-paging{text-align:center}div.dt-container .row{--bs-gutter-y: 0.5rem}div.dt-container div.dt-paging ul.pagination{justify-content:center !important}}table.dataTable.table-sm>thead>tr th.dt-orderable-asc,table.dataTable.table-sm>thead>tr th.dt-orderable-desc,table.dataTable.table-sm>thead>tr th.dt-ordering-asc,table.dataTable.table-sm>thead>tr th.dt-ordering-desc,table.dataTable.table-sm>thead>tr td.dt-orderable-asc,table.dataTable.table-sm>thead>tr td.dt-orderable-desc,table.dataTable.table-sm>thead>tr td.dt-ordering-asc,table.dataTable.table-sm>thead>tr td.dt-ordering-desc{padding-right:20px}table.dataTable.table-sm>thead>tr th.dt-orderable-asc span.dt-column-order,table.dataTable.table-sm>thead>tr th.dt-orderable-desc span.dt-column-order,table.dataTable.table-sm>thead>tr th.dt-ordering-asc span.dt-column-order,table.dataTable.table-sm>thead>tr th.dt-ordering-desc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-orderable-asc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-orderable-desc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-ordering-asc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-ordering-desc span.dt-column-order{right:5px}div.dt-scroll-head table.table-bordered{border-bottom-width:0}div.table-responsive>div.dt-container>div.row{margin:0}div.table-responsive>div.dt-container>div.row>div[class^=col-]:first-child{padding-left:0}div.table-responsive>div.dt-container>div.row>div[class^=col-]:last-child{padding-right:0}:root[data-bs-theme=dark]{--dt-row-hover: 255, 255, 255;--dt-row-stripe: 255, 255, 255;--dt-column-ordering: 255, 255, 255} +@keyframes dtb-spinner{100%{transform:rotate(360deg)}}@-o-keyframes dtb-spinner{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes dtb-spinner{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dtb-spinner{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes dtb-spinner{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}div.dataTables_wrapper{position:relative}div.dt-buttons{position:initial}div.dt-buttons .dt-button{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border-radius:.75em;box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.8);text-align:center;z-index:2003;overflow:hidden}div.dt-button-info h2{padding:2rem 2rem 1rem 2rem;margin:0;font-weight:normal}div.dt-button-info>div{padding:1em 2em 2em 2em}div.dtb-popover-close{position:absolute;top:6px;right:6px;width:22px;height:22px;text-align:center;border-radius:3px;cursor:pointer;z-index:2003}button.dtb-hide-drop{display:none !important}div.dt-button-collection-title{text-align:center;padding:.3em .5em .5em;margin-left:.5em;margin-right:.5em;font-size:.9em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}div.dt-button-collection-title:empty{display:none}span.dt-button-spacer{display:inline-block;margin:.5em;white-space:nowrap}span.dt-button-spacer.bar{border-left:1px solid rgba(0, 0, 0, 0.3);vertical-align:middle;padding-left:.5em}span.dt-button-spacer.bar:empty{height:1em;width:1px;padding-left:0}div.dt-button-collection .dt-button-active{padding-right:3em}div.dt-button-collection .dt-button-active:after{position:absolute;top:50%;margin-top:-10px;right:1em;display:inline-block;content:"✓";color:inherit}div.dt-button-collection .dt-button-active.dt-button-split{padding-right:0}div.dt-button-collection .dt-button-active.dt-button-split:after{display:none}div.dt-button-collection .dt-button-active.dt-button-split>*:first-child{padding-right:3em}div.dt-button-collection .dt-button-active.dt-button-split>*:first-child:after{position:absolute;top:50%;margin-top:-10px;right:1em;display:inline-block;content:"✓";color:inherit}div.dt-button-collection .dt-button-active-a a{padding-right:3em}div.dt-button-collection .dt-button-active-a a:after{position:absolute;right:1em;display:inline-block;content:"✓";color:inherit}div.dt-button-collection span.dt-button-spacer{width:100%;font-size:.9em;text-align:center;margin:.5em 0}div.dt-button-collection span.dt-button-spacer:empty{height:0;width:100%}div.dt-button-collection span.dt-button-spacer.bar{border-left:none;border-bottom:1px solid rgba(0, 0, 0, 0.1);padding-left:0}@media print{table.dataTable tr>*{box-shadow:none !important}}div.dt-buttons div.btn-group{position:initial}div.dt-buttons span.dt-button-spacer.empty{margin:1px}div.dt-buttons span.dt-button-spacer.bar:empty{height:inherit}div.dt-buttons .btn.processing{color:rgba(0, 0, 0, 0.2)}div.dt-buttons .btn.processing:after{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;box-sizing:border-box;display:block;content:" ";border:2px solid rgb(40, 40, 40);border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:dtb-spinner 1500ms infinite linear;-o-animation:dtb-spinner 1500ms infinite linear;-ms-animation:dtb-spinner 1500ms infinite linear;-webkit-animation:dtb-spinner 1500ms infinite linear;-moz-animation:dtb-spinner 1500ms infinite linear}div.dropdown-menu.dt-button-collection{margin-top:4px;width:200px}div.dropdown-menu.dt-button-collection .dt-button{position:relative}div.dropdown-menu.dt-button-collection .dt-button.dropdown-toggle::after{position:absolute;right:12px;top:14px}div.dropdown-menu.dt-button-collection div.dt-button-split{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:flex-start;align-content:flex-start;align-items:stretch}div.dropdown-menu.dt-button-collection div.dt-button-split a:first-child{min-width:auto;flex:1 0 50px;padding-right:0}div.dropdown-menu.dt-button-collection div.dt-button-split button:last-child{min-width:33px;flex:0;background:transparent;border:none;line-height:1rem;color:var(--bs-dropdown-link-color);padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);overflow:visible}div.dropdown-menu.dt-button-collection div.dt-button-split button:last-child:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}div.dropdown-menu.dt-button-collection.fixed{position:fixed;display:block;top:50%;left:50%;margin-left:-75px;border-radius:5px;background-color:white;padding:.5em}div.dropdown-menu.dt-button-collection.fixed.two-column{margin-left:-200px}div.dropdown-menu.dt-button-collection.fixed.three-column{margin-left:-225px}div.dropdown-menu.dt-button-collection.fixed.four-column{margin-left:-300px}div.dropdown-menu.dt-button-collection.fixed.columns{margin-left:-409px}@media screen and (max-width: 1024px){div.dropdown-menu.dt-button-collection.fixed.columns{margin-left:-308px}}@media screen and (max-width: 640px){div.dropdown-menu.dt-button-collection.fixed.columns{margin-left:-203px}}@media screen and (max-width: 460px){div.dropdown-menu.dt-button-collection.fixed.columns{margin-left:-100px}}div.dropdown-menu.dt-button-collection.fixed>:last-child{max-height:100vh;overflow:auto}div.dropdown-menu.dt-button-collection.two-column>:last-child,div.dropdown-menu.dt-button-collection.three-column>:last-child,div.dropdown-menu.dt-button-collection.four-column>:last-child{display:block !important;column-gap:8px}div.dropdown-menu.dt-button-collection.two-column>:last-child>*,div.dropdown-menu.dt-button-collection.three-column>:last-child>*,div.dropdown-menu.dt-button-collection.four-column>:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dropdown-menu.dt-button-collection.two-column{width:400px}div.dropdown-menu.dt-button-collection.two-column>:last-child{padding-bottom:1px;column-count:2}div.dropdown-menu.dt-button-collection.three-column{width:450px}div.dropdown-menu.dt-button-collection.three-column>:last-child{padding-bottom:1px;column-count:3}div.dropdown-menu.dt-button-collection.four-column{width:600px}div.dropdown-menu.dt-button-collection.four-column>:last-child{padding-bottom:1px;column-count:4}div.dropdown-menu.dt-button-collection .dt-button{border-radius:0}div.dropdown-menu.dt-button-collection.columns{width:auto}div.dropdown-menu.dt-button-collection.columns>:last-child{display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:6px;width:818px;padding-bottom:1px}div.dropdown-menu.dt-button-collection.columns>:last-child .dt-button{min-width:200px;flex:0 1;margin:0}div.dropdown-menu.dt-button-collection.columns.dtb-b3>:last-child,div.dropdown-menu.dt-button-collection.columns.dtb-b2>:last-child,div.dropdown-menu.dt-button-collection.columns.dtb-b1>:last-child{justify-content:space-between}div.dropdown-menu.dt-button-collection.columns.dtb-b3 .dt-button{flex:1 1 32%}div.dropdown-menu.dt-button-collection.columns.dtb-b2 .dt-button{flex:1 1 48%}div.dropdown-menu.dt-button-collection.columns.dtb-b1 .dt-button{flex:1 1 100%}@media screen and (max-width: 1024px){div.dropdown-menu.dt-button-collection.columns>:last-child{width:612px}}@media screen and (max-width: 640px){div.dropdown-menu.dt-button-collection.columns>:last-child{width:406px}div.dropdown-menu.dt-button-collection.columns.dtb-b3 .dt-button{flex:0 1 32%}}@media screen and (max-width: 460px){div.dropdown-menu.dt-button-collection.columns>:last-child{width:200px}}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;z-index:999}@media screen and (max-width: 767px){div.dt-buttons{float:none;width:100%;text-align:center;margin-bottom:.5em}div.dt-buttons a.btn{float:none}}div.dt-button-info{background-color:var(--bs-body-bg);border:1px solid var(--bs-border-color-translucent)}:root[data-bs-theme=dark] div.dropdown-menu.dt-button-collection.fixed{background-color:var(--bs-body-bg);border:1px solid var(--bs-border-color-translucent)} + + +div.dt-datetime{position:absolute;background-color:white;z-index:2050;border:1px solid #ccc;box-shadow:0 5px 15px -5px rgba(0, 0, 0, 0.5);padding:6px 20px;width:275px;border-radius:5px}div.dt-datetime.inline{position:relative;box-shadow:none}div.dt-datetime div.dt-datetime-title{text-align:center;padding:5px 0px 3px}div.dt-datetime div.dt-datetime-buttons{text-align:center}div.dt-datetime div.dt-datetime-buttons a{display:inline-block;padding:0 .5em .5em .5em;margin:0;font-size:.9em}div.dt-datetime div.dt-datetime-buttons a:hover{text-decoration:underline}div.dt-datetime table{border-spacing:0;margin:12px 0;width:100%}div.dt-datetime table.dt-datetime-table-nospace{margin-top:-12px}div.dt-datetime table th{font-size:.8em;color:#777;font-weight:normal;width:14.285714286%;padding:0 0 4px 0;text-align:center}div.dt-datetime table td{font-size:.9em;color:#444;padding:0}div.dt-datetime table td.selectable{text-align:center;background:#f5f5f5}div.dt-datetime table td.selectable.disabled{color:#aaa;background:white}div.dt-datetime table td.selectable.disabled button:hover{color:#aaa;background:white}div.dt-datetime table td.selectable.now{background-color:#ddd}div.dt-datetime table td.selectable.now button{font-weight:bold}div.dt-datetime table td.selectable.selected button{background:#4e6ca3;color:white;border-radius:2px}div.dt-datetime table td.selectable button:hover{background:#ff8000;color:white;border-radius:2px}div.dt-datetime table td.dt-datetime-week{font-size:.7em}div.dt-datetime table button{width:100%;box-sizing:border-box;border:none;background:transparent;font-size:inherit;color:inherit;text-align:center;padding:4px 0;cursor:pointer;margin:0}div.dt-datetime table button span{display:inline-block;min-width:14px;text-align:right}div.dt-datetime table.weekNumber th{width:12.5%}div.dt-datetime div.dt-datetime-calendar table{margin-top:0}div.dt-datetime div.dt-datetime-label{position:relative;display:inline-block;height:30px;padding:5px 6px;border:1px solid transparent;box-sizing:border-box;cursor:pointer}div.dt-datetime div.dt-datetime-label:hover{border:1px solid #ddd;border-radius:2px;background-color:#f5f5f5}div.dt-datetime div.dt-datetime-label select{position:absolute;top:6px;left:0;cursor:pointer;opacity:0}div.dt-datetime.horizontal{width:550px}div.dt-datetime.horizontal div.dt-datetime-date,div.dt-datetime.horizontal div.dt-datetime-time{width:48%}div.dt-datetime.horizontal div.dt-datetime-time{margin-left:4%}div.dt-datetime div.dt-datetime-date{position:relative;float:left;width:100%}div.dt-datetime div.dt-datetime-time{position:relative;float:left;width:100%;text-align:center}div.dt-datetime div.dt-datetime-time>span{vertical-align:middle}div.dt-datetime div.dt-datetime-time th{text-align:left}div.dt-datetime div.dt-datetime-time div.dt-datetime-timeblock{display:inline-block;vertical-align:middle}div.dt-datetime div.dt-datetime-iconLeft,div.dt-datetime div.dt-datetime-iconRight{width:30px;height:30px;background-position:center;background-repeat:no-repeat;opacity:.3;overflow:hidden;box-sizing:border-box;border:1px solid transparent}div.dt-datetime div.dt-datetime-iconLeft:hover,div.dt-datetime div.dt-datetime-iconRight:hover{border:1px solid #ccc;border-radius:2px;background-color:#f0f0f0;opacity:.6}div.dt-datetime div.dt-datetime-iconLeft button,div.dt-datetime div.dt-datetime-iconRight button{border:none;background:transparent;text-indent:30px;height:100%;width:100%;cursor:pointer}div.dt-datetime div.dt-datetime-iconLeft{position:absolute;top:5px;left:5px}div.dt-datetime div.dt-datetime-iconLeft button{position:relative;z-index:1}div.dt-datetime div.dt-datetime-iconLeft:after{position:absolute;top:7px;left:10px;display:block;content:"";border-top:7px solid transparent;border-right:7px solid black;border-bottom:7px solid transparent}div.dt-datetime div.dt-datetime-iconRight{position:absolute;top:5px;right:5px}div.dt-datetime div.dt-datetime-iconRight button{position:relative;z-index:1}div.dt-datetime div.dt-datetime-iconRight:after{position:absolute;top:7px;left:12px;display:block;content:"";border-top:7px solid transparent;border-left:7px solid black;border-bottom:7px solid transparent}div.dt-datetime-error{clear:both;padding:0 1em;max-width:240px;font-size:11px;line-height:1.25em;text-align:center;color:#b11f1f}html.dark input.dt-datetime,:root[data-theme=dark] input.dt-datetime,:root[data-bs-theme=dark] input.dt-datetime{color-scheme:dark}html.dark div.dt-datetime,:root[data-theme=dark] div.dt-datetime,:root[data-bs-theme=dark] div.dt-datetime{border:1px solid #595b5e;background-color:#212529;box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.8)}html.dark div.dt-datetime table th,:root[data-theme=dark] div.dt-datetime table th,:root[data-bs-theme=dark] div.dt-datetime table th{color:#ccc}html.dark div.dt-datetime table td,:root[data-theme=dark] div.dt-datetime table td,:root[data-bs-theme=dark] div.dt-datetime table td{color:#eee}html.dark div.dt-datetime table td.selectable,:root[data-theme=dark] div.dt-datetime table td.selectable,:root[data-bs-theme=dark] div.dt-datetime table td.selectable{background:#373c41}html.dark div.dt-datetime table td.selectable.disabled,:root[data-theme=dark] div.dt-datetime table td.selectable.disabled,:root[data-bs-theme=dark] div.dt-datetime table td.selectable.disabled{color:#aaa;background:#171b1f}html.dark div.dt-datetime table td.selectable.disabled button:hover,:root[data-theme=dark] div.dt-datetime table td.selectable.disabled button:hover,:root[data-bs-theme=dark] div.dt-datetime table td.selectable.disabled button:hover{color:#aaa;background:#171b1f}html.dark div.dt-datetime table td.selectable.now,:root[data-theme=dark] div.dt-datetime table td.selectable.now,:root[data-bs-theme=dark] div.dt-datetime table td.selectable.now{background:#4b5055}html.dark div.dt-datetime table td.selectable.selected button,:root[data-theme=dark] div.dt-datetime table td.selectable.selected button,:root[data-bs-theme=dark] div.dt-datetime table td.selectable.selected button{background:#6ea8fe;color:black}html.dark div.dt-datetime table td.selectable button:hover,:root[data-theme=dark] div.dt-datetime table td.selectable button:hover,:root[data-bs-theme=dark] div.dt-datetime table td.selectable button:hover{background:#ff8000;color:black}html.dark div.dt-datetime div.dt-datetime-label:hover,:root[data-theme=dark] div.dt-datetime div.dt-datetime-label:hover,:root[data-bs-theme=dark] div.dt-datetime div.dt-datetime-label:hover{border:1px solid transparent;background-color:rgba(255, 255, 255, 0.1)}html.dark div.dt-datetime div.dt-datetime-iconLeft:hover,html.dark div.dt-datetime div.dt-datetime-iconRight:hover,html.dark div.dt-datetime div.dt-datetime-iconUp:hover,html.dark div.dt-datetime div.dt-datetime-iconDown:hover,:root[data-theme=dark] div.dt-datetime div.dt-datetime-iconLeft:hover,:root[data-theme=dark] div.dt-datetime div.dt-datetime-iconRight:hover,:root[data-theme=dark] div.dt-datetime div.dt-datetime-iconUp:hover,:root[data-theme=dark] div.dt-datetime div.dt-datetime-iconDown:hover,:root[data-bs-theme=dark] div.dt-datetime div.dt-datetime-iconLeft:hover,:root[data-bs-theme=dark] div.dt-datetime div.dt-datetime-iconRight:hover,:root[data-bs-theme=dark] div.dt-datetime div.dt-datetime-iconUp:hover,:root[data-bs-theme=dark] div.dt-datetime div.dt-datetime-iconDown:hover{border:1px solid transparent;background-color:rgba(255, 255, 255, 0.1)}html.dark div.dt-datetime div.dt-datetime-iconLeft:after,:root[data-theme=dark] div.dt-datetime div.dt-datetime-iconLeft:after,:root[data-bs-theme=dark] div.dt-datetime div.dt-datetime-iconLeft:after{border-right-color:white}html.dark div.dt-datetime div.dt-datetime-iconRight:after,:root[data-theme=dark] div.dt-datetime div.dt-datetime-iconRight:after,:root[data-bs-theme=dark] div.dt-datetime div.dt-datetime-iconRight:after{border-left-color:white}html.dark div.dt-datetime select,:root[data-theme=dark] div.dt-datetime select,:root[data-bs-theme=dark] div.dt-datetime select{color-scheme:dark}html.dark div.dt-datetime-error,:root[data-theme=dark] div.dt-datetime-error,:root[data-bs-theme=dark] div.dt-datetime-error{color:#b11f1f} + + +div.dt-button-collection{overflow:visible !important;z-index:2002 !important}div.dt-button-collection div.dtsb-searchBuilder{box-sizing:border-box;padding-left:1em !important;padding-right:1em !important}div.dt-button-collection.dtb-collection-closeable div.dtsb-titleRow{padding-right:40px}.dtsb-greyscale{border:1px solid #cecece !important}div.dtsb-logicContainer .dtsb-greyscale{border:none !important}div.dtsb-searchBuilder{justify-content:space-evenly;cursor:default;margin-bottom:1em;text-align:left;width:100%}div.dtsb-searchBuilder button.dtsb-button,div.dtsb-searchBuilder select{font-size:1em}div.dtsb-searchBuilder div.dtsb-titleRow{justify-content:space-evenly;margin-bottom:.5em}div.dtsb-searchBuilder div.dtsb-titleRow div.dtsb-title{display:inline-block;padding-top:14px}div.dtsb-searchBuilder div.dtsb-titleRow div.dtsb-title:empty{display:inline}div.dtsb-searchBuilder div.dtsb-titleRow button.dtsb-clearAll{float:right;margin-bottom:.8em}div.dtsb-searchBuilder div.dtsb-vertical .dtsb-value,div.dtsb-searchBuilder div.dtsb-vertical .dtsb-data,div.dtsb-searchBuilder div.dtsb-vertical .dtsb-condition{display:block}div.dtsb-searchBuilder div.dtsb-group{position:relative;clear:both;margin-bottom:.8em}div.dtsb-searchBuilder div.dtsb-group button.dtsb-search{float:right}div.dtsb-searchBuilder div.dtsb-group button.dtsb-clearGroup{margin:2px;text-align:center;padding:0}div.dtsb-searchBuilder div.dtsb-group div.dtsb-logicContainer{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-o-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);position:absolute;margin-top:.8em;margin-right:.8em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria{margin-bottom:.8em;display:flex;justify-content:start;flex-flow:row wrap}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria select.dtsb-dropDown,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria input.dtsb-input{padding:.4em;margin-right:.8em;min-width:5em;max-width:20em;color:inherit;font-size:1em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria select.dtsb-dropDown option.dtsb-notItalic,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria input.dtsb-input option.dtsb-notItalic{font-style:normal}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria select.dtsb-italic{font-style:italic}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-inputCont{flex:1;white-space:nowrap}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-inputCont span.dtsb-joiner{margin-right:.8em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-inputCont input.dtsb-value{width:33%}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-inputCont select,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-inputCont input{height:100%;box-sizing:border-box}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer{margin-left:auto;display:inline-block}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer button.dtsb-delete,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer button.dtsb-right,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer button.dtsb-left{margin-right:.8em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer button.dtsb-delete:last-child,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer button.dtsb-right:last-child,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer button.dtsb-left:last-child{margin-right:0}@media screen and (max-width: 550px){div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria{display:flex;flex-flow:none;flex-direction:column;justify-content:start;padding-right:calc(35px + .8em);margin-bottom:0px}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria:not(:first-child),div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria:not(:nth-child(2)),div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria:not(:last-child){padding-top:.8em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria:first-child,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria:nth-child(2),div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria:last-child{padding-top:0em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria select.dtsb-dropDown,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria input.dtsb-input{max-width:none;width:100%;margin-bottom:.8em;margin-right:.8em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-inputCont{margin-right:.8em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer{position:absolute;width:35px;display:flex;flex-wrap:wrap-reverse;right:0}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer button{margin-right:0px !important}}div.dtsb-searchBuilder div.dtsb-titleRow{height:40px}div.dtsb-searchBuilder div.dtsb-titleRow div.dtsb-title{padding-top:10px}div.dtsb-searchBuilder div.dtsb-group button.dtsb-clearGroup{margin-right:8px}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria .form-select{width:auto;display:inline-block;padding-right:30px !important}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria select.dtsb-condition{border-color:#28a745}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria select.dtsb-data{border-color:#dc3545}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria select.dtsb-value,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria input.dtsb-value{border-color:#007bff}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria .form-control{display:inline-block;font-size:1em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-logicContainer{border-radius:4px;display:flex;flex-direction:row;flex-wrap:wrap;justify-content:flex-start;align-content:flex-start;align-items:flex-start;margin-top:10px;overflow:hidden}div.dtsb-searchBuilder div.dtsb-group div.dtsb-logicContainer button.dtsb-logic{border:none;border-radius:0px;flex-grow:1;flex-shrink:0;flex-basis:3em;margin:0px;padding:.375rem .7rem}div.dtsb-searchBuilder div.dtsb-group div.dtsb-logicContainer button.dtsb-clearGroup{border:none;border-radius:0px;width:2em;margin:0px}div.dt-button-collection div.dtsb-searchBuilder{padding-left:10px;padding-right:10px} + + diff --git a/lizmap/www/assets/css/map.css b/lizmap/www/assets/css/map.css index 8d4491e332..b8030d9976 100644 --- a/lizmap/www/assets/css/map.css +++ b/lizmap/www/assets/css/map.css @@ -1304,7 +1304,7 @@ SubDock } .attribute-layer-action-bar{ - height:30px; + height:33px; border-bottom: 1px solid lightgrey; } @@ -1313,6 +1313,14 @@ SubDock overflow: auto; } +.attribute-layer-action-bar .dt-buttons{ + margin-right: 4px; +} + +.attribute-layer-action-bar .dt-buttons > .btn { + padding: 2px 6px; +} + .btn-filterbyextent-attributeTable svg { height: 14px; width: 14px; diff --git a/package-lock.json b/package-lock.json index 9c2bf2924b..1f8d7aed10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,9 @@ "cypress": "<10.0.0", "cypress-file-upload": "^5.0.x", "datatables.net-bs5": "^2.2.2", + "datatables.net-buttons-bs5": "^3.2.2", + "datatables.net-datetime": "^1.5.5", + "datatables.net-searchbuilder-bs5": "^1.8.2", "dompurify": "^3.2.x", "eslint-plugin-jsdoc": "^60.2.x", "flatgeobuf": "~4.2.x", @@ -3917,6 +3920,59 @@ "jquery": ">=1.7" } }, + "node_modules/datatables.net-buttons": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/datatables.net-buttons/-/datatables.net-buttons-3.2.5.tgz", + "integrity": "sha512-OSTl7evbfe0SMee11lyzu5iv/z8Yp05eh3s1QBte/FNqHcoXN8hlAVSSGpYgk5pj8zwHPYIu6fHeMEue4ARUNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "datatables.net": "^2", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-buttons-bs5": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/datatables.net-buttons-bs5/-/datatables.net-buttons-bs5-3.2.5.tgz", + "integrity": "sha512-3eT/Sd90x7imq9MRcKP9X3j70qg/u+OvtZSNWJEihRf1Mb/Sr8NexQw/Bag/ui6GJHa5dhUeFrOgBSKtEW70iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "datatables.net-bs5": "^2", + "datatables.net-buttons": "3.2.5", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-datetime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/datatables.net-datetime/-/datatables.net-datetime-1.6.0.tgz", + "integrity": "sha512-iOCaUv8MEuRMizBoCQlIWmI8xlNW5rJeohpxdE4RBd6oYz5Z01m4bca4zd26+EY4/4ONnVPPAPht40LKgNSkgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/datatables.net-searchbuilder": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/datatables.net-searchbuilder/-/datatables.net-searchbuilder-1.8.4.tgz", + "integrity": "sha512-rK+5sZU5wwphVRGqFB0ahJ75+yL5jBznnLrRKnGZ03fBDlhQo9TM9k8FKU1vK6NyWHj2fPes+mS/0s7LkP8cMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "datatables.net": "^2", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-searchbuilder-bs5": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/datatables.net-searchbuilder-bs5/-/datatables.net-searchbuilder-bs5-1.8.4.tgz", + "integrity": "sha512-9adKB0QZCl4rZ0TluwJxYWxqKCWBndL/ie7I2LGKTT4Qq02uYjkQi/xqueczGPf3HChohuSHARJO6wbtYcNxyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "datatables.net-bs5": "^2", + "datatables.net-searchbuilder": "1.8.4", + "jquery": ">=1.7" + } + }, "node_modules/dayjs": { "version": "1.11.18", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", diff --git a/package.json b/package.json index e198824d01..2b332a1492 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,9 @@ "cypress-file-upload": "^5.0.x", "dompurify": "^3.2.x", "datatables.net-bs5": "^2.2.2", + "datatables.net-buttons-bs5": "^3.2.2", + "datatables.net-datetime": "^1.5.5", + "datatables.net-searchbuilder-bs5": "^1.8.2", "eslint-plugin-jsdoc": "^60.2.x", "flatgeobuf": "~4.2.x", "jsts": "^2.11.x", From af35b66c517b40d2ec85dad9b49c1643ccdcb21b Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 15 Apr 2025 17:21:21 +0200 Subject: [PATCH 22/45] Add Datatables searchBuilder backend logic --- assets/src/legacy/attributeTable.js | 3 +- .../lizmap/controllers/datatables.classic.php | 160 ++++++++++++++---- 2 files changed, 130 insertions(+), 33 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index adbdc16e67..61b8896308 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1481,7 +1481,8 @@ var lizAttributeTable = function() { extend: 'searchBuilder', config: { liveSearch: false, - columns: '.dt-orderable-asc' + columns: '.dt-orderable-asc', + depthLimit: 1 } } ] diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index af03707974..2881e34a96 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -52,7 +52,10 @@ public function index() $DTOrderColumnDirection = $DTOrder[0]['dir'] == 'desc' ? 'd' : ''; $DTOrderColumnName = $DTColumns[$DTOrderColumnIndex]['data']; - $DTSearch = $this->param('search'); + $DTSearchBuilder = ''; + if ($this->param('searchBuilder')) { + $DTSearchBuilder = $this->param('searchBuilder'); + } $lproj = lizmap::getProject($repository.'~'.$project); $layer = $lproj->getLayer($layerId); @@ -60,17 +63,20 @@ public function index() $jsonFeatures = array(); - // Get total number of features - $hits = 0; - $wfsParamsHits = array( + $wfsParamsData = array( 'SERVICE' => 'WFS', 'VERSION' => '1.0.0', 'REQUEST' => 'GetFeature', 'TYPENAME' => $typeName, + ); + + // Get total number of features + $hits = 0; + $wfsParamsHits = array( 'RESULTTYPE' => 'hits', ); - $wfsrequest = new WFSRequest($lproj, $wfsParamsHits, lizmap::getServices()); + $wfsrequest = new WFSRequest($lproj, array_merge($wfsParamsData, $wfsParamsHits), lizmap::getServices()); $wfsresponse = $wfsrequest->process(); $hitsData = $wfsresponse->getBodyAsString(); preg_match('/numberOfFeatures="([0-9]+)"/', $hitsData, $matches); @@ -80,18 +86,6 @@ public function index() $recordsFiltered = count($filteredFeatureIDs); } - // Get features - $wfsParamsData = array( - 'SERVICE' => 'WFS', - 'VERSION' => '1.0.0', - 'REQUEST' => 'GetFeature', - 'TYPENAME' => $typeName, - 'OUTPUTFORMAT' => 'GeoJSON', - 'MAXFEATURES' => $DTLength, - 'SORTBY' => $DTOrderColumnName.' '.$DTOrderColumnDirection, - 'STARTINDEX' => $DTStart, - ); - if ($moveSelectedToTop == 'true') { $featureIds = array(); foreach ($selectedFeatureIDs as $id) { @@ -124,6 +118,108 @@ public function index() $wfsParamsData['EXP_FILTER'] = '$id IN ('.implode(' , ', $filteredFeatureIDs).')'; } + // Handle search made by searchBuilder + if ($DTSearchBuilder) { + foreach ($DTSearchBuilder['criteria'] as $criteria) { + $column = $criteria['data']; + $condition = $criteria['condition']; + $value = ''; + $value1 = isset($criteria['value1']) ? addslashes($criteria['value1']) : ''; + $value2 = isset($criteria['value2']) ? addslashes($criteria['value2']) : ''; + + // Map DataTables operators to QGIS Server operators + switch ($condition) { + case '=': + case '!=': + case '<': + case '<=': + case '>': + case '>=': + $qgisOperator = $condition; + if ($criteria['type'] == 'num') { + $value = $value1; + } else { + $value = '\''.$value1.'\''; + } + + break; + + case 'starts': + $qgisOperator = 'ILIKE'; + $value = '\''.$value1.'%\''; + + break; + + case '!starts': + $qgisOperator = 'NOT ILIKE'; + $value = '\''.$value1.'%\''; + + break; + + case 'contains': + $qgisOperator = 'ILIKE'; + $value = '\'%'.$value1.'%\''; + + break; + + case '!contains': + $qgisOperator = 'NOT ILIKE'; + $value = '\'%'.$value1.'%\''; + + break; + + case 'ends': + $qgisOperator = 'ILIKE'; + $value = '\'%'.$value1.'\''; + + break; + + case '!ends': + $qgisOperator = 'NOT ILIKE'; + $value = '\'%'.$value1.'\''; + + break; + + case 'null': + $qgisOperator = 'IS NULL'; + + break; + + case '!null': + $qgisOperator = 'IS NOT NULL'; + + break; + + case 'between': + $qgisOperator = 'BETWEEN'; + if ($criteria['type'] == 'num') { + $value = $value1.' AND '.$value2; + } else { + $value = '\''.$value1.'\' AND \''.$value2.'\''; + } + + break; + + case '!between': + $qgisOperator = 'NOT BETWEEN'; + if ($criteria['type'] == 'num') { + $value = $value1.' AND '.$value2; + } else { + $value = '\''.$value1.'\' AND \''.$value2.'\''; + } + + break; + } + // Append the filter to the exp_filter string + if (!empty($expFilter)) { + $logic = isset($DTSearchBuilder['logic']) ? $DTSearchBuilder['logic'] : 'AND'; + $expFilter .= " {$logic} "; + } + + $expFilter .= "{$column} {$qgisOperator} {$value}"; + } + } + if ($expFilter) { $wfsParamsData['EXP_FILTER'] = $expFilter; } @@ -134,29 +230,29 @@ public function index() $bboxString = implode(',', $bbox); $wfsParamsData['BBOX'] = $bboxString; $wfsParamsData['SRSNAME'] = $srsName; + } - // Get total number of features in the bounding box - $wfsParamsFilterByExtentHits = array( - 'SERVICE' => 'WFS', - 'VERSION' => '1.0.0', - 'REQUEST' => 'GetFeature', - 'TYPENAME' => $typeName, - 'RESULTTYPE' => 'hits', - 'BBOX' => $bboxString, - 'SRSNAME' => $srsName, - ); + $wfsParamsPaginated = array( + 'OUTPUTFORMAT' => 'GeoJSON', + 'MAXFEATURES' => $DTLength, + 'STARTINDEX' => $DTStart, + 'SORTBY' => $DTOrderColumnName.' '.$DTOrderColumnDirection, + ); - $wfsrequest = new WFSRequest($lproj, $wfsParamsFilterByExtentHits, lizmap::getServices()); + $wfsrequest = new WFSRequest($lproj, array_merge($wfsParamsData, $wfsParamsPaginated), lizmap::getServices()); + $wfsresponse = $wfsrequest->process(); + $featureData = $wfsresponse->getBodyAsString(); + + // Get hits when data is filtered + if ($expFilter || count($bbox) == 4) { + + $wfsrequest = new WFSRequest($lproj, array_merge($wfsParamsData, $wfsParamsHits), lizmap::getServices()); $wfsresponse = $wfsrequest->process(); $filterByExtentHitsData = $wfsresponse->getBodyAsString(); preg_match('/numberOfFeatures="([0-9]+)"/', $filterByExtentHitsData, $matches); $recordsFiltered = $matches[1]; } - $wfsrequest = new WFSRequest($lproj, $wfsParamsData, lizmap::getServices()); - $wfsresponse = $wfsrequest->process(); - $featureData = $wfsresponse->getBodyAsString(); - $returnedData = array( 'draw' => (int) $this->param('draw'), 'recordsTotal' => $hits, From c9b459202102b5c1a37481145dbdc6921613cedc Mon Sep 17 00:00:00 2001 From: nboisteault Date: Fri, 25 Apr 2025 17:05:20 +0200 Subject: [PATCH 23/45] Returns editableFeatures in datatables request --- assets/src/legacy/attributeTable.js | 85 ++++++++++--------- .../lizmap/classes/qgisVectorLayer.class.php | 19 ++++- .../lizmap/controllers/datatables.classic.php | 11 +++ 3 files changed, 73 insertions(+), 42 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 61b8896308..ae0d9d669d 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1415,12 +1415,24 @@ var lizAttributeTable = function() { return formatedData; } + // Get editable features + const editableFeatures = json.editableFeatures; + for (const feature of json.data.features) { - const featID = feature.id.split('.').pop(); + const featID = parseInt(feature.id.split('.').pop()); + let editionRestricted = ''; + if (editableFeatures.status === 'restricted') { + editionRestricted = 'edition-restricted="true"'; + if (editableFeatures.featuresids.includes(featID)) { + editionRestricted = 'edition-restricted="false"'; + } + } + const ftb = ``; + formatedData.push(Object.assign({ 'DT_RowId': featID, 'lizSelected': '', - 'featureToolbar': '', + 'featureToolbar': ftb, }, feature.properties)); // Copy received features to config @@ -1472,28 +1484,29 @@ var lizAttributeTable = function() { } }); - // Add searchBuilder button - // Disable live search to avoid searching on each keystroke - // Only display columns that are sortable for search - const searchBuilderButton = new DataTable.Buttons(oTable, { - buttons: [ - { - extend: 'searchBuilder', - config: { - liveSearch: false, - columns: '.dt-orderable-asc', - depthLimit: 1 - } - } - ] - }); - - // Attach searchBuilder button to attribute-layer-action-bar + // Attach searchBuilder button to attribute-layer-action-bar if exists const actionBar = document.querySelector(aTable) - .closest('.attribute-layer-content') - .previousElementSibling; - - actionBar.insertAdjacentElement('afterbegin', searchBuilderButton.container()[0]); + ?.closest('.attribute-layer-content') + ?.previousElementSibling; + + if (actionBar) { + // Add searchBuilder button + // Disable live search to avoid searching on each keystroke + // Only display columns that are sortable for search + const searchBuilderButton = new DataTable.Buttons(oTable, { + buttons: [ + { + extend: 'searchBuilder', + config: { + liveSearch: false, + columns: '.dt-orderable-asc', + depthLimit: 1 + } + } + ] + }); + actionBar.insertAdjacentElement('afterbegin', searchBuilderButton.container()[0]); + } // Unbind previous events on page oTable.on( 'page', function() { @@ -1519,11 +1532,6 @@ var lizAttributeTable = function() { table.draw(); } - // Check editable features - if (canEdit || canDelete) { - lizMap.mainLizmap.edition.fetchEditableFeatures([lConfig.id]); - } - if (aCallback) aCallback(aName,aTable); @@ -1557,13 +1565,13 @@ var lizAttributeTable = function() { width: "25px", searchable: false, sortable: false, - render: (data, type, row, meta) => { - const layerId = config.layers[aName].id; - const fid = row['DT_RowId']; - return ` - - `; - } + // render: (data, type, row, meta) => { + // const layerId = config.layers[aName].id; + // const fid = row['DT_RowId']; + // return ` + // + // `; + // } }); firstDisplayedColIndex += 1; @@ -1909,19 +1917,20 @@ var lizAttributeTable = function() { } // Bind events when drawing table - $( childTable ).one( 'draw.dt', function() { + const DTchildTable = new DataTable(childTable); + DTchildTable.one('draw', function() { if( canEdit ) { // Add property on lizmap-feature-toolbar to edit children feature linked to a parent feature const parentFeatId = $(childTable).parents('div.tab-pane.attribute-layer-child-content') .find('input.attribute-table-hidden-parent-feature-id').val(); - $(childTable).DataTable().cells().nodes() + DTchildTable.cells().nodes() .to$().children('lizmap-feature-toolbar').attr('parent-feature-id', parentFeatId); } if ( canCreateChildren ) { // Button to create feature linked to parent - const createHeader = $($(childTable).DataTable().column(1).header()); + const createHeader = $(DTchildTable.column(1).header()); if ( createHeader.find('button.attribute-layer-feature-create').length == 0 ) { createHeader .append(` '; // Unselect button - html+= ' '; + html+= ''; // 'Move selected to top' button - html+= ' '; + html+= ` + `; // Filter button : only if no filter applied at startup if( !startupFilter - && ( !lizMap.lizmapLayerFilterActive || lizMap.lizmapLayerFilterActive == lname ) - ){ - html+= ' '; + && ( !lizMap.lizmapLayerFilterActive || lizMap.lizmapLayerFilterActive == lname )){ + html+= ''; } // Filter data by extent button @@ -687,9 +692,7 @@ var lizAttributeTable = function() { document.querySelector(moveSelectedToTopSelector).addEventListener('click', (e) => { const dTableSelector = '#attribute-layer-table-' + e.currentTarget.value; const dTable = new DataTable(dTableSelector); - moveSelectedToTop = true; dTable.draw(); - moveSelectedToTop = false; // Scroll to top document.querySelector(dTableSelector).parentElement.scroll({ @@ -1379,9 +1382,8 @@ var lizAttributeTable = function() { type: 'POST', data: (d) => { // Handle selected features moved to top - if (moveSelectedToTop) { - d.moveselectedtotop = true; - d.selectedfeatureids = lConfig['selectedFeatures'].join(); + if (document.querySelector('.btn-moveselectedtotop-attributeTable.active[data-layerid="' + lConfig.id + '"]')) { + d.filteredfeatureids = lConfig['selectedFeatures'].join(); } // Handle filtered features @@ -3097,20 +3099,25 @@ var lizAttributeTable = function() { const selectedFeatures = config.layers[e.featureType].selectedFeatures; const table = new DataTable('table[data-layerid=' + layerId + ']'); - table.rows().every(function (rowIdx) { - var data = this.data(); - if ((selectedFeatures.includes(data.DT_RowId.toString()))) { - this.row(rowIdx).node().classList.add('selected'); - data.lizSelected = 'a'; - } else { - this.row(rowIdx).node().classList.remove('selected'); - data.lizSelected = 'z'; - } - }); + if (document.querySelector('.btn-moveselectedtotop-attributeTable.active[data-layerid="' + layerId + '"]')) { + table.draw(); + } else { + table.rows().every(function (rowIdx) { + var data = this.data(); + if ((selectedFeatures.includes(data.DT_RowId.toString()))) { + this.row(rowIdx).node().classList.add('selected'); + data.lizSelected = 'a'; + } else { + this.row(rowIdx).node().classList.remove('selected'); + data.lizSelected = 'z'; + } + }); + } // Update openlayers layer drawing - if( e.updateDrawing ) + if( e.updateDrawing ){ updateMapLayerSelection( e.featureType ); + } }, layerFilteredFeaturesChanged: function(e) { diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index 9c27acee99..88d204e8c5 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -24,11 +24,6 @@ public function index() $repository = $this->param('repository'); $project = $this->param('project'); $layerId = $this->param('layerId'); - $moveSelectedToTop = $this->param('moveselectedtotop'); - $selectedFeatureIDs = array(); - if ($this->param('selectedfeatureids')) { - $selectedFeatureIDs = explode(',', $this->param('selectedfeatureids')); - } $filteredFeatureIDs = array(); if ($this->param('filteredfeatureids')) { $filteredFeatureIDs = explode(',', $this->param('filteredfeatureids')); @@ -87,34 +82,6 @@ public function index() $recordsFiltered = count($filteredFeatureIDs); } - if ($moveSelectedToTop == 'true') { - $featureIds = array(); - foreach ($selectedFeatureIDs as $id) { - $featureIds[] = $typeName.'.'.$id; - } - - $wfsrequest = new WFSRequest( - $lproj, - array( - 'SERVICE' => 'WFS', - 'VERSION' => '1.0.0', - 'REQUEST' => 'GetFeature', - 'OUTPUTFORMAT' => 'GeoJSON', - 'GEOMETRYNAME' => 'none', - 'FEATUREID' => implode(',', $featureIds), - ), - lizmap::getServices() - ); - $wfsresponse = $wfsrequest->process(); - $featureData = $wfsresponse->getBodyAsString(); - $jsonFeatures = json_decode($featureData)->features; - - // Remove selected features from the list of features to get - $DTLength = $DTLength - count($jsonFeatures); - $wfsParamsData['MAXFEATURES'] = $DTLength; - $wfsParamsData['EXP_FILTER'] = '$id NOT IN ('.implode(' , ', $selectedFeatureIDs).')'; - } - if (count($filteredFeatureIDs) > 0) { $wfsParamsData['EXP_FILTER'] = '$id IN ('.implode(' , ', $filteredFeatureIDs).')'; } diff --git a/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties b/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties index 28601005a8..b49a63e0ec 100644 --- a/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties +++ b/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties @@ -101,7 +101,7 @@ attributeLayers.toolbar.cb.data.detail.title=Display info attributeLayers.toolbar.input.search.title=Search attributeLayers.toolbar.btn.select.searched.title=Select searched line attributeLayers.toolbar.btn.data.unselect.title=Unselect all -attributeLayers.toolbar.btn.data.moveselectedtotop.title=Move selected to top +attributeLayers.toolbar.btn.data.moveselectedtotop.title=Display selection only attributeLayers.toolbar.btn.data.filter.title=Filter attributeLayers.toolbar.btn.data.export.title=Export attributeLayers.toolbar.btn.data.createFeature.title=Create feature From 926ab65e49f2521b996e11cd0ce4bdc0fe92fce9 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Thu, 15 May 2025 11:52:09 +0200 Subject: [PATCH 34/45] DT: set `orderSequence` to have DT 1.x behaviour https://datatables.net/reference/option/columns.orderSequence --- assets/src/legacy/attributeTable.js | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 87416861ed..620820ac6b 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1375,6 +1375,7 @@ var lizAttributeTable = function() { const params = globalThis['lizUrls'].params; params['layerId'] = lConfig.id; + DataTable.defaults.column.orderSequence = ['asc', 'desc']; const oTable = new DataTable(aTable, { serverSide: true ,ajax: { From b24e03f726d3aef5c1db10db110debd27751a934 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 26 Aug 2025 15:31:03 +0200 Subject: [PATCH 35/45] e2e: fix some tests --- tests/end2end/playwright/attribute-table.spec.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/end2end/playwright/attribute-table.spec.js b/tests/end2end/playwright/attribute-table.spec.js index d7e16d7c7c..6d8841b439 100644 --- a/tests/end2end/playwright/attribute-table.spec.js +++ b/tests/end2end/playwright/attribute-table.spec.js @@ -3,7 +3,6 @@ import { test, expect } from '@playwright/test'; import { ProjectPage } from './pages/project'; import { expectParametersToContain, getAuthStorageStatePath } from './globals'; import { AdminPage } from "./pages/admin"; -import { gotoMap } from './globals'; test.describe('Attribute table', () => { @@ -13,7 +12,7 @@ test.describe('Attribute table', () => { const layerName = 'Les_quartiers_a_Montpellier'; await project.openAttributeTable(layerName); - await expect(project.attributeTableWrapper(layerName).locator('div.dataTables_info')) + await expect(project.attributeTableWrapper(layerName).locator('div.dt-info')) .toContainText('Showing 1 to 7 of 7 entries'); await expect(project.attributeTableHtml(layerName).locator('tbody tr')) .toHaveCount(7); @@ -36,21 +35,22 @@ test.describe('Attribute table', () => { const layerName = 'random_points'; await project.openAttributeTable(layerName); - await expect(project.attributeTableWrapper(layerName).locator('div.dataTables_info')) + await expect(project.attributeTableWrapper(layerName).locator('div.dt-info')) .toContainText('Showing 1 to 50 of 700 entries'); await expect(project.attributeTableHtml(layerName).locator('tbody tr')) .toHaveCount(50); - await expect(project.attributeTableWrapper(layerName).locator('ul.pagination > li.paginate_button')) + await expect(project.attributeTableWrapper(layerName).locator('ul.pagination > li.dt-paging-button')) .toHaveCount(9); - // click on last page which is the previous last paginate_button + // click on last page which is the previous last dt-paging-button await project.attributeTableWrapper(layerName).hover(); - project.attributeTableWrapper(layerName).locator('ul.pagination > li.paginate_button:nth-last-child(-0n+2)').dispatchEvent('click'); - await expect(project.attributeTableWrapper(layerName).locator('div.dataTables_info')) + project.attributeTableWrapper(layerName).locator('ul.pagination > li.dt-paging-button:nth-last-child(-0n+2) > button').dispatchEvent('click'); + await expect(project.attributeTableWrapper(layerName).locator('div.dt-info')) .toContainText('Showing 651 to 700 of 700 entries'); }); test('Data filtered by extent', async ({ page }) => { const project = new ProjectPage(page, 'attribute_table'); + await project.open(); const layerName = 'Les_quartiers_a_Montpellier'; const datatablesRequestPromise = page.waitForRequest(request => request.method() === 'POST' && request.postData()?.includes('draw') === true); await project.openAttributeTable(layerName); From d4f4856f21db5ceb4711ceb6d2d90562c8ad8b12 Mon Sep 17 00:00:00 2001 From: rldhont Date: Wed, 10 Sep 2025 12:10:32 +0200 Subject: [PATCH 36/45] Use `active` with `btn-primary` class for filter and select buttons in FeatureToolbar --- assets/src/components/FeatureToolbar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/src/components/FeatureToolbar.js b/assets/src/components/FeatureToolbar.js index c812534b36..fabc9eab88 100644 --- a/assets/src/components/FeatureToolbar.js +++ b/assets/src/components/FeatureToolbar.js @@ -50,7 +50,7 @@ export default class FeatureToolbar extends HTMLElement {