Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
bd59d0d
Migrate datatables from client to server side
nboisteault Mar 18, 2025
89892b0
DT: handle `selected` class for lines in `createdRow` callback
nboisteault Mar 20, 2025
6e0c2cf
Attribute table: handle moveSelectedToTop w/ serverSide
nboisteault Mar 21, 2025
7748131
Attribute table: handle filter w/ serverSide
nboisteault Mar 25, 2025
513dc57
AttributeTable: add `data-layerid` in <table>s attributes to ease sel…
nboisteault Mar 27, 2025
8a6e2a4
Handle 'selected' state for rows without querying server
nboisteault Mar 27, 2025
70ea075
Get total number of records with WFS `RESULTTYPE=hits`
nboisteault Mar 28, 2025
98fdcd2
Don't request WFS GetFeature at startup to get data as DT does
nboisteault Mar 28, 2025
9d10f23
Handle children tables
nboisteault Mar 31, 2025
fce61a8
Return JSON features as they are cached in `lizMap.config`
nboisteault Mar 31, 2025
36c43f9
Handle link/unlink in featureToolbars in attribute table
nboisteault Apr 1, 2025
8c04889
Return `recordsFiltered` for Datatables
nboisteault Apr 1, 2025
701c559
Redraw table if it already exists
nboisteault Apr 1, 2025
8c58a19
Handle children tables follow up
nboisteault Apr 2, 2025
9707926
Remove legacy code linked to `limitDataToBbox` parameter
nboisteault Apr 8, 2025
b765c7b
Attribute table: add a toggle button to filter features in the curren…
nboisteault Apr 8, 2025
8626e72
e2e: test data filtered by extent in attribute table
nboisteault Apr 8, 2025
7d11ae8
Attribute table: install Datatables v 2.2.2
nboisteault Apr 11, 2025
12268e9
Attribute table: update code for Datatables v2.2.2
nboisteault Apr 11, 2025
855d7fe
Add Datataables searchBuilder
nboisteault Apr 15, 2025
7bef459
Add Datatables searchBuilder backend logic
nboisteault Apr 15, 2025
5fcf493
Returns editableFeatures in datatables request
nboisteault Apr 25, 2025
a2b1849
e2e: migrate tests for DT2
nboisteault Apr 25, 2025
8c42b9b
Refresh tables afetr edition
nboisteault May 6, 2025
2b48053
e2e: update tests for DT2
nboisteault May 6, 2025
87cb4d1
Some fixes to pass playwright tests
nboisteault May 12, 2025
6a70e7a
e2e: update key_value_mapping test to wait for datatables to be loaded
nboisteault May 13, 2025
612bf8e
Update DT tables when filters change
nboisteault May 13, 2025
97f6494
Use `active` and not `btn-primary` class for filter and select button…
nboisteault May 13, 2025
92a9767
Fix phpStan errors
nboisteault May 13, 2025
de405fb
Datatables: update i18n
nboisteault May 13, 2025
6843865
Move selected to top button can now be toggled and only display selec…
nboisteault May 15, 2025
99f6a49
DT: set `orderSequence` to have DT 1.x behaviour
nboisteault May 15, 2025
4269d6c
e2e: fix some tests
nboisteault Aug 26, 2025
b466b0b
Use `active` with `btn-primary` class for filter and select buttons i…
rldhont Sep 10, 2025
70927b8
DataTables request: send error and tests
rldhont Sep 12, 2025
325a620
Tests requests DataTables: bbox filter
rldhont Sep 17, 2025
1707a36
Fix and test request DataTables order
rldhont Sep 17, 2025
5286243
Define \Lizmap\DataTables\DataTables with static method to convert DT…
rldhont Sep 22, 2025
6ba83a0
DataTables: manage invalid criteria
rldhont Sep 23, 2025
fb150ab
Tests requests DataTables: Pages
rldhont Sep 23, 2025
4de9bb7
Tests requests DataTables: Filter featureIds
rldhont Sep 23, 2025
398e6b6
DataTables fix recordsTotal and recordsFiltered type to int
rldhont Sep 23, 2025
72ffd1d
Tests requests DataTables: fix features type
rldhont Sep 23, 2025
b53e96e
Tests e2e Playwright attribute table: Fix bootstrap class
rldhont Nov 24, 2025
19ff64e
Fix datatables requests to get number of features as JSON
rldhont Nov 24, 2025
081217c
Fixing lizmap-feature-toolbar bbox to zoom to
rldhont Dec 4, 2025
3fb1d0f
Fixing attribute table e2e tests
rldhont Dec 4, 2025
11ab189
Fixing editable features e2e tests
rldhont Dec 4, 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,163 changes: 437 additions & 726 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
250 changes: 250 additions & 0 deletions lizmap/modules/lizmap/controllers/datatables.classic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
<?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,
'OUTPUTFORMAT' => 'GeoJSON',
);

// Get total number of features
$hits = 0;
$wfsParamsHits = array(
'RESULTTYPE' => 'hits',
);
Comment on lines +140 to +142
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()), 'application/vnd.geo+json')) {
return $this->setErrorResponse($rep, 400, 'The request to get the total number of features failed, mime-type: '.$wfsresponse->getMime());
}

$hitsData = json_decode($wfsresponse->getBodyAsString());
if (!property_exists($hitsData, 'numberOfFeatures')) {
return $this->setErrorResponse($rep, 400, 'The response of the request to get the total number of features is not well formed.');
}

$hits = $hitsData->numberOfFeatures;
$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(
'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()), 'application/vnd.geo+json')) {
return $this->setErrorResponse($rep, 400, 'The request to get the number of paginated features failed, mime-type: '.$wfsresponse->getMime());
}

$filterByExtentHitsData = json_decode($wfsresponse->getBodyAsString());
if (!property_exists($hitsData, 'numberOfFeatures')) {
return $this->setErrorResponse($rep, 400, 'The response of the request to get the number of paginated features is not well formed.');
}

$recordsFiltered = $filterByExtentHitsData->numberOfFeatures;
}

// 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 +228 to +233
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