Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
423af9c
Migrate datatables from client to server side
nboisteault Mar 18, 2025
26d20ed
DT: handle `selected` class for lines in `createdRow` callback
nboisteault Mar 20, 2025
8eb7205
Attribute table: handle moveSelectedToTop w/ serverSide
nboisteault Mar 21, 2025
e2d9074
Attribute table: handle filter w/ serverSide
nboisteault Mar 25, 2025
3d22007
AttributeTable: add `data-layerid` in <table>s attributes to ease sel…
nboisteault Mar 27, 2025
a6fc545
Handle 'selected' state for rows without querying server
nboisteault Mar 27, 2025
26ab40c
Get total number of records with WFS `RESULTTYPE=hits`
nboisteault Mar 28, 2025
68864d6
Don't request WFS GetFeature at startup to get data as DT does
nboisteault Mar 28, 2025
8c1adb8
e2e: refactor attributeTable tests
nboisteault Mar 28, 2025
f6c6c4a
Handle children tables
nboisteault Mar 31, 2025
98ad860
Return JSON features as they are cached in `lizMap.config`
nboisteault Mar 31, 2025
037c9c5
Handle link/unlink in featureToolbars in attribute table
nboisteault Apr 1, 2025
45970f2
Return `recordsFiltered` for Datatables
nboisteault Apr 1, 2025
f932b51
Redraw table if it already exists
nboisteault Apr 1, 2025
9c4ea10
Handle children tables follow up
nboisteault Apr 2, 2025
af1ff66
Remove legacy code linked to `limitDataToBbox` parameter
nboisteault Apr 8, 2025
122c218
Attribute table: add a toggle button to filter features in the curren…
nboisteault Apr 8, 2025
193edfa
e2e: test data filtered by extent in attribute table
nboisteault Apr 8, 2025
d04cd25
Attribute table: install Datatables v 2.2.2
nboisteault Apr 11, 2025
7ed6538
Attribute table: update code for Datatables v2.2.2
nboisteault Apr 11, 2025
31641b5
Add Datataables searchBuilder
nboisteault Apr 15, 2025
af35b66
Add Datatables searchBuilder backend logic
nboisteault Apr 15, 2025
c9b4592
Returns editableFeatures in datatables request
nboisteault Apr 25, 2025
957d116
e2e: migrate tests for DT2
nboisteault Apr 25, 2025
fd75da4
Refresh tables afetr edition
nboisteault May 6, 2025
fbc62a0
e2e: update tests for DT2
nboisteault May 6, 2025
b216f88
Some fixes to pass playwright tests
nboisteault May 12, 2025
1b1b5f3
e2e: update key_value_mapping test to wait for datatables to be loaded
nboisteault May 13, 2025
37994ac
Update DT tables when filters change
nboisteault May 13, 2025
2ccd2a0
Use `active` and not `btn-primary` class for filter and select button…
nboisteault May 13, 2025
83aca01
Fix phpStan errors
nboisteault May 13, 2025
569c1dd
Datatables: update i18n and add deutsch
nboisteault May 13, 2025
fd4299c
Move selected to top button can now be toggled and only display selec…
nboisteault May 15, 2025
926ab65
DT: set `orderSequence` to have DT 1.x behaviour
nboisteault May 15, 2025
b24e03f
e2e: fix some tests
nboisteault Aug 26, 2025
d4f4856
Use `active` with `btn-primary` class for filter and select buttons i…
rldhont Sep 10, 2025
7d9b1e9
DataTables request: send error and tests
rldhont Sep 12, 2025
fb2576f
Tests requests DataTables: bbox filter
rldhont Sep 17, 2025
451ac90
Fix and test request DataTables order
rldhont Sep 17, 2025
a3e06eb
Define \Lizmap\DataTables\DataTables with static method to convert DT…
rldhont Sep 22, 2025
dc5da1d
DataTables: manage invalid criteria
rldhont Sep 23, 2025
e3ad7ab
Tests requests DataTables: Pages
rldhont Sep 23, 2025
96ece6e
Tests requests DataTables: Filter featureIds
rldhont Sep 23, 2025
aa592e4
DataTables fix recordsTotal and recordsFiltered type to int
rldhont Sep 23, 2025
b73b338
Tests requests DataTables: fix features type
rldhont Sep 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions assets/src/components/FeatureToolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ export default class FeatureToolbar extends HTMLElement {
<div class="feature-toolbar">
<button
type="button"
class="btn btn-sm feature-select ${this.attributeTableConfig ? '' : 'hide'} ${this.isSelected ? 'btn-primary' : ''}"
class="btn btn-sm feature-select ${this.attributeTableConfig ? '' : 'hide'} ${this.isSelected ? 'btn-primary active' : ''}"
@click=${() => this.select()}
data-bs-toggle="tooltip"
data-bs-title="${lizDict['attributeLayers.btn.select.title']}"
><i class="icon-ok"></i>
</button>
<button
type="button"
class="btn btn-sm feature-filter ${this.attributeTableConfig && this.hasFilter ? '' : 'hide'} ${this.isFiltered ? 'btn-primary' : ''}"
class="btn btn-sm feature-filter ${this.attributeTableConfig && this.hasFilter ? '' : 'hide'} ${this.isFiltered ? 'btn-primary active' : ''}"
@click=${() => this.filter()}
data-bs-toggle="tooltip"
data-bs-title="${lizDict['attributeLayers.toolbar.btn.data.filter.title']}"
Expand Down
4 changes: 4 additions & 0 deletions assets/src/images/svg/filter-square.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,134 changes: 417 additions & 717 deletions assets/src/legacy/attributeTable.js

Large diffs are not rendered by default.

9 changes: 3 additions & 6 deletions assets/src/legacy/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1111,11 +1113,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 = {
Expand Down Expand Up @@ -1336,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"] }
});
Expand Down
6 changes: 0 additions & 6 deletions assets/src/modules/SelectionTool.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
3 changes: 2 additions & 1 deletion lizmap/app/responses/myHtmlMapResponse.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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/'));
}

Expand Down
19 changes: 15 additions & 4 deletions lizmap/modules/lizmap/classes/qgisVectorLayer.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -1168,11 +1168,12 @@ public function isFeatureEditable($feature)
* when there is a filter by login (or by polygon). This allows to deactivate the editing icon
* for the non-editable features inside the popup and attribute table.
*
* @param array<string, string> $wfsParams Extra WFS parameters to filter the layer : FEATUREID or EXP_FILTER could be use
* @param array<string, string> $wfsParams Extra WFS parameters to filter the layer : FEATUREID or EXP_FILTER could be use
* @param bool $featuresAsStream return the features as a stream or as an array
*
* @return array Data containing the status (restricted|unrestricted) and the features if restricted
*/
public function editableFeatures($wfsParams = array())
public function editableFeatures($wfsParams = array(), $featuresAsStream = true)
{
// Editable features are a restricted list
$restricted_empty_data = array(
Expand Down Expand Up @@ -1270,8 +1271,18 @@ public function editableFeatures($wfsParams = array())
}

// Features as iterator
$featureStream = Psr7StreamWrapper::getResource($result->getBodyAsStream());
$features = JsonMachineItems::fromStream($featureStream, array('pointer' => '/features'));
if ($featuresAsStream) {
$featureStream = Psr7StreamWrapper::getResource($result->getBodyAsStream());
$features = JsonMachineItems::fromStream($featureStream, array('pointer' => '/features'));
} else {
// Features as array
$features = json_decode($result->getBodyAsString(), true);
if (isset($features['features'])) {
$features = $features['features'];
} else {
$features = array();
}
}

return array(
'status' => 'restricted',
Expand Down
254 changes: 254 additions & 0 deletions lizmap/modules/lizmap/controllers/datatables.classic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
<?php

/**
* Send data to datatables ajax requests.
*
* @author 3liz
* @copyright 2025 3liz
*
* @see https://3liz.com
*
* @license Mozilla Public License : http://www.mozilla.org/MPL/
*/

use Lizmap\DataTables\DataTables;
use Lizmap\Project\UnknownLizmapProjectException;
use Lizmap\Request\Proxy;
use Lizmap\Request\WFSRequest;

class datatablesCtrl extends jController
{
/**
* Sets the error in the provided response object based on the given HTTP error code.
*
* @param jResponseJson $rep - the response object to which the error details will be assigned
* @param int $code - the HTTP error code
* @param string $errorMessage - the custom error message
*
* @return jResponseJson returns the updated response object containing the error details
*/
protected function setErrorResponse(jResponseJson $rep, int $code, string $errorMessage): jResponseJson
{
$rep->setHttpStatus($code, Proxy::getHttpStatusMsg($code));
$rep->data = array(
'code' => Proxy::getHttpStatusMsg($code),
'status' => $code,
'message' => $errorMessage,
);

return $rep;
}

public function index()
{
/** @var jResponseJson $rep */
$rep = $this->getResponse('json');

// Lizmap parameters
$repository = $this->param('repository');
$project = $this->param('project');
$layerId = $this->param('layerId');

if (!$repository || !$project || !$layerId) {
return $this->setErrorResponse($rep, 400, 'The parameters repository, project and layerId are mandatory.');
}

// DataTables parameters
$DTStart = $this->param('start');
$DTLength = $this->param('length');
$DTOrder = $this->param('order');
$DTColumns = $this->param('columns');

// Check DataTables parameters
if (!isset($DTStart) || !isset($DTLength) || !isset($DTOrder) || !isset($DTColumns)) {
return $this->setErrorResponse($rep, 400, 'The DataTables parameters start, length'.
', order and columns are mandatory.');
}
if (!is_array($DTOrder) || count($DTOrder) == 0 || !array_key_exists(0, $DTOrder)
|| !array_key_exists('column', $DTOrder[0]) || !array_key_exists('dir', $DTOrder[0])) {
return $this->setErrorResponse($rep, 400, 'The DataTables parameter order '.json_encode($DTOrder).
' is not well formed.');
}
if (!is_array($DTColumns) || count($DTColumns) == 0) {
return $this->setErrorResponse($rep, 400, 'The DataTables parameter columns '.json_encode($DTColumns).
' is not well formed.');
}

// Extract info for DataTables parameters
$DTOrderColumnIndex = $DTOrder[0]['column'];
$DTOrderColumnDirection = $DTOrder[0]['dir'] == 'desc' ? 'DESC' : 'ASC';
if (!array_key_exists($DTOrderColumnIndex, $DTColumns)) {
return $this->setErrorResponse($rep, 400, 'The DataTables parameters order and columns are not compatible.');
}
if (!array_key_exists('data', $DTColumns[$DTOrderColumnIndex])) {
return $this->setErrorResponse($rep, 400, 'The DataTables parameter columns '.json_encode($DTColumns).
' is not well formed.');
}
$DTOrderColumnName = $DTColumns[$DTOrderColumnIndex]['data'];

$DTSearchBuilder = '';
if ($this->param('searchBuilder')) {
$DTSearchBuilder = $this->param('searchBuilder');
}

$filteredFeatureIDs = array();
if ($this->param('filteredfeatureids')) {
$filteredFeatureIDs = explode(',', $this->param('filteredfeatureids'));
}
$expFilter = $this->param('exp_filter');

// Filter by bounding box
$bbox = array();
$srsName = $this->param('srsname');
if ($this->param('bbox') && $srsName) {
$bbox = explode(',', $this->param('bbox'));
}

// Check if when the bbox is defined, it contains 4 number
if (count($bbox) > 0 && count($bbox) != 4) {
return $this->setErrorResponse($rep, 400, 'The bbox parameter must contain 4 numbers separated by a comma.');
}

try {
$lproj = lizmap::getProject($repository.'~'.$project);
if (!$lproj) {
return $this->setErrorResponse($rep, 404, 'The lizmap project '.$repository.'~'.$project.' does not exist.');
}
} catch (UnknownLizmapProjectException $e) {
return $this->setErrorResponse($rep, 404, 'The lizmap project '.$repository.'~'.$project.' does not exist.');
}

/** @var null|qgisVectorLayer $layer */
$layer = $lproj->getLayer($layerId);
if (!$layer) {
return $this->setErrorResponse($rep, 404, 'The layerId '.$layerId.' does not exist.');
}
$typeName = $layer->getWfsTypeName();

$jsonFeatures = array();

$wfsParamsData = array(
'SERVICE' => 'WFS',
'VERSION' => '1.0.0',
'REQUEST' => 'GetFeature',
'TYPENAME' => $typeName,
);
Comment on lines +130 to +135
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add to Lizmap\Request\WFSRequest a static method to provide this array.


// Get total number of features
$hits = 0;
$wfsParamsHits = array(
'RESULTTYPE' => 'hits',
);
Comment on lines +139 to +141
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add to Lizmap\Request\WFSRequest a static const property.

// Get hits with WFS request
$wfsrequest = new WFSRequest($lproj, array_merge($wfsParamsData, $wfsParamsHits), lizmap::getServices());
$wfsresponse = $wfsrequest->process();

// Check response
if ($wfsresponse->getCode() >= 400) {
return $this->setErrorResponse($rep, 400, 'The request to get the total number of features failed, code: '.$wfsresponse->getCode());
}
if (!str_contains(strtolower($wfsresponse->getMime()), 'text/xml')) {
return $this->setErrorResponse($rep, 400, 'The request to get the total number of features failed, mime-type: '.$wfsresponse->getMime());
}

$hitsData = $wfsresponse->getBodyAsString();
preg_match('/numberOfFeatures="([0-9]+)"/', $hitsData, $matches);

if (count($matches) < 2) {
return $this->setErrorResponse($rep, 400, 'The response of the request to get the total number of features is not well formed.');
}

$hits = $matches[1];
$recordsFiltered = $hits;
if (count($filteredFeatureIDs) > 0) {
$recordsFiltered = count($filteredFeatureIDs);
}

if (count($filteredFeatureIDs) > 0) {
$wfsParamsData['EXP_FILTER'] = '$id IN ('.implode(' , ', $filteredFeatureIDs).')';
}

// Handle search made by searchBuilder
if ($DTSearchBuilder) {
$expFilter = DataTables::convertSearchToExpression($DTSearchBuilder);
}

if ($expFilter) {
$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;
}

$wfsParamsPaginated = array(
'OUTPUTFORMAT' => 'GeoJSON',
'MAXFEATURES' => $DTLength,
'STARTINDEX' => $DTStart,
'SORTBY' => $DTOrderColumnName.' '.$DTOrderColumnDirection,
);
// Get paginated features by a WFS resquest
$wfsrequest = new WFSRequest($lproj, array_merge($wfsParamsData, $wfsParamsPaginated), lizmap::getServices());
$wfsresponse = $wfsrequest->process();

// Check response
if ($wfsresponse->getCode() >= 400) {
return $this->setErrorResponse($rep, 400, 'The request to get paginated features failed, code: '.$wfsresponse->getCode());
}
if (!str_contains(strtolower($wfsresponse->getMime()), 'application/vnd.geo+json')) {
return $this->setErrorResponse($rep, 400, 'The request to get paginated features failed, mime-type: '.$wfsresponse->getMime());
}

$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();

// Check response
if ($wfsresponse->getCode() >= 400) {
return $this->setErrorResponse($rep, 400, 'The request to get the number of paginated features failed, code: '.$wfsresponse->getCode());
}
if (!str_contains(strtolower($wfsresponse->getMime()), 'text/xml')) {
return $this->setErrorResponse($rep, 400, 'The request to get the number of paginated features failed, mime-type: '.$wfsresponse->getMime());
}

$filterByExtentHitsData = $wfsresponse->getBodyAsString();
preg_match('/numberOfFeatures="([0-9]+)"/', $filterByExtentHitsData, $matches);

if (count($matches) < 2) {
return $this->setErrorResponse($rep, 400, 'The response of the request to get the number of paginated features is not well formed.');
}

$recordsFiltered = $matches[1];
}

// Handle editable features
$editableFeaturesRep = $layer->editableFeatures(array_merge($wfsParamsData, $wfsParamsPaginated), false);
$editableFeaturesIds = array();
foreach ($editableFeaturesRep['features'] as $feature) {
$editableFeaturesIds[] = (int) explode('.', $feature['id'])[1];
}
Comment on lines +232 to +237
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add to Lizmap\DataTables\DataTables a static method to handle editable features


unset($editableFeaturesRep['features']);
$editableFeaturesRep['featuresids'] = $editableFeaturesIds;

$returnedData = array(
'draw' => (int) $this->param('draw'),
'recordsTotal' => (int) $hits,
'recordsFiltered' => (int) $recordsFiltered,
'data' => json_decode($featureData),
'editableFeatures' => $editableFeaturesRep,
);

$rep->data = $returnedData;

return $rep;
}
}
Loading
Loading