Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG-3.10.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ with some extra keywords: backend, tests, test, translation, funders, important
* Short link permalink functionality
* Edition - Support QGIS dynamic default-value expressions in edit forms, including geometry-based (`$x`, `$y`, `$area`, `$length`, `$geometry`) and field-referencing expressions (e.g. `"firstname" || ' ' || "lastname"`). Defaults are re-evaluated when the geometry is drawn/edited and when a referenced field changes, honoring QGIS's `applyOnUpdate` flag.
* UI - Auto-activate box selection when opening the selection tool
* Filter - Per-layer filter-removal button next to each filtered layer in the legend (#1551)

### Security

Expand All @@ -44,10 +45,12 @@ with some extra keywords: backend, tests, test, translation, funders, important
* Map - WMS baselayers from QGIS layers now proxy through QGIS Server
* Popup - Place children features tables inside drag-and-drop relation placeholders
* Print - Respect cfg layout order in print panel
* Filter - The "deactivate filter" button now clears filters on all filtered layers, not only the last one (#1551)

### Tests

* e2e snap edition: Enhance Snap panel functionalities
* e2e: form filter - test deactivate-all button and per-layer unfilter icon in legend panel (#1551)

### Backend

Expand Down
10 changes: 10 additions & 0 deletions assets/src/components/Treeview.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ export default class Treeview extends HTMLElement {
<img class="legend" src="${layer.icon}">
<label for="node-${layer.name}" >${layer.layerConfig.title}</label>
<div class="layer-actions">
${layer.isFiltered
? html`<i class="icon-filter" title="${lizDict['tree.button.removeFilter']}" @click=${() => this._removeLayerFilter(layer)}></i>`
: ''
}
<a href="${this._createDocLink(layer.name)}" target="_blank" title="${lizDict['tree.button.link']}">
<i class="icon-share"></i>
</a>
Expand Down Expand Up @@ -338,4 +342,10 @@ export default class Treeview extends HTMLElement {
event.preventDefault();
}
}

_removeLayerFilter(layer) {
lizMap.events.triggerEvent('layerfeatureremovefilter',
{ 'featureType': layer.name }
);
}
}
27 changes: 25 additions & 2 deletions assets/src/legacy/attributeTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -2646,6 +2646,24 @@ var lizAttributeTable = function() {
);
}

/**
* Whether at least one layer currently has an active filter.
* @returns {boolean}
*/
function hasFilteredLayer() {
for ( var ln in config.layers ) {
var lc = config.layers[ln];
if ( Array.isArray(lc['filteredFeatures']) && lc['filteredFeatures'].length ) {
return true;
}
var rp = lc['request_params'];
if ( rp && (rp['exp_filter'] || rp['filter'] || rp['filtertoken']) ) {
return true;
}
}
return false;
}

/**
*
* @param typeNamePile
Expand All @@ -2672,7 +2690,9 @@ var lizAttributeTable = function() {
applyEmptyLayerFilter( typeName, typeNamePile, typeNameFilter, typeNameDone, cascade );
}

$('#layerActionUnfilter').toggle((lizMap.lizmapLayerFilterActive !== null));
// Keep the button visible while any layer is still filtered,
// not only when lizmapLayerFilterActive is set (#1551)
$('#layerActionUnfilter').toggle(hasFilteredLayer());
}

/**
Expand Down Expand Up @@ -2731,7 +2751,10 @@ var lizAttributeTable = function() {
function getPivotParam( typeNameId, attributeLayerConfig, typeNameDone ) {
var isPivot = false;
var pivotParam = null;
if( 'pivot' in attributeLayerConfig
// attributeLayerConfig is null for layers without an attribute
// table config; applyEmptyLayerFilter passes it through (#1551)
if( attributeLayerConfig
&& 'pivot' in attributeLayerConfig
&& attributeLayerConfig.pivot == 'True'
&& attributeLayerConfig.layerId in config.relations.pivot
){
Expand Down
35 changes: 30 additions & 5 deletions assets/src/legacy/switcher-layers-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -635,13 +635,38 @@ var lizLayerActionButtons = function() {

// Cancel Lizmap global filter
$('#layerActionUnfilter').click(function(){
var layerName = lizMap.lizmapLayerFilterActive;
if( !layerName )
// Collect every layer that currently has an active filter, not
// only the last one stored in lizmapLayerFilterActive (#1551).
// The selection tool can filter several layers at once.
var filteredLayers = [];
for (var lName in lizMap.config.layers) {
var lConfig = lizMap.config.layers[lName];
// Selection tool / attribute-table filter sets filteredFeatures
var lFilteredFeatures = lConfig['filteredFeatures'];
var hasFilteredFeatures = Array.isArray(lFilteredFeatures) && lFilteredFeatures.length;
// Form filter "simple" method sets a WMS filter on request_params
var rParams = lConfig['request_params'];
var hasRequestFilter = rParams
&& (rParams['exp_filter'] || rParams['filter'] || rParams['filtertoken']);
if (hasFilteredFeatures || hasRequestFilter) {
filteredLayers.push(lName);
}
}

// Keep the legacy single active layer as a fallback
if (lizMap.lizmapLayerFilterActive
&& filteredLayers.indexOf(lizMap.lizmapLayerFilterActive) === -1) {
filteredLayers.push(lizMap.lizmapLayerFilterActive);
}

if( !filteredLayers.length )
return false;

lizMap.events.triggerEvent("layerfeatureremovefilter",
{ 'featureType': layerName}
);
for (var i = 0; i < filteredLayers.length; i++) {
lizMap.events.triggerEvent("layerfeatureremovefilter",
{ 'featureType': filteredLayers[i]}
);
}
lizMap.lizmapLayerFilterActive = null;
$(this).hide();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ tree.button.checkbox=Display/Hide
tree.button.link=Open documentation
tree.button.removeCache=Remove server's cache for this layer
tree.button.removeCache.confirmation=Remove server's cache for this layer?
tree.button.removeFilter=Remove the filter for this layer
tree.button.expand=Expand
tree.button.collapse=Collapse

Expand Down
63 changes: 63 additions & 0 deletions tests/end2end/playwright/form-filter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { test, expect } from '@playwright/test';
import { ProjectPage } from './pages/project';
import { getEchoRequestParams } from './globals';

/**
* Layer name as used in the treeview data-testid attribute
* @type {string}
*/
const LAYER_NAME = 'form filter (à)';

test.describe('Form filter', () => {
test.beforeEach(async ({ page }) => {
const project = new ProjectPage(page, 'form_filter');
Expand Down Expand Up @@ -100,3 +106,60 @@ test.describe('Form filter', () => {
await expect(page.locator('#ui-id-2 .ui-menu-item div')).toHaveText('monuments');
});
});

test.describe('Form filter - Legend panel interactions', () => {
test.beforeEach(async ({ page }) => {
const project = new ProjectPage(page, 'form_filter');
await project.open();
// Open the form filter panel
await page.locator('#button-filter').click();
});

test('Deactivate all filters button in legend panel clears the active filter', async ({ page }) => {
// Apply a filter via the form filter panel
const getMapPromise = page.waitForRequest(/GetMap/);
await page.locator('#liz-filter-field-test_filter').selectOption('_uvres_d_art_et_monuments_de_l_espace_urbain');
await getMapPromise;

// Switch to the layer panel (switcher) — opening the filter panel hid it
await page.locator('#button-switcher').click();

// The "deactivate all filters" button in the layer legend panel must be visible
await expect(page.locator('#layerActionUnfilter')).toBeVisible();

// The layer node in the treeview must have the 'filtered' class
await expect(page.getByTestId(LAYER_NAME).locator('.node')).toContainClass('filtered');

// Click the deactivate-all button in the legend panel
const getMapAfterUnfilter = page.waitForRequest(/GetMap/);
await page.locator('#layerActionUnfilter').click();
await getMapAfterUnfilter;

// The button must be hidden and the 'filtered' class must be removed
await expect(page.locator('#layerActionUnfilter')).not.toBeVisible();
await expect(page.getByTestId(LAYER_NAME).locator('.node')).not.toContainClass('filtered');
});

test('Per-layer filter icon in legend removes the filter for that layer', async ({ page }) => {
// Apply a filter via the form filter panel
const getMapPromise = page.waitForRequest(/GetMap/);
await page.locator('#liz-filter-field-test_filter').selectOption('_uvres_d_art_et_monuments_de_l_espace_urbain');
await getMapPromise;

// Switch to the layer panel (switcher) — opening the filter panel hid it
await page.locator('#button-switcher').click();

// The per-layer icon-filter button must be visible inside the treeview node
await expect(page.getByTestId(LAYER_NAME).locator('.icon-filter')).toBeVisible();

// Click the per-layer icon-filter to remove the filter for that layer only
const getMapAfterUnfilter = page.waitForRequest(/GetMap/);
await page.getByTestId(LAYER_NAME).locator('.icon-filter').click();
await getMapAfterUnfilter;

// Filter must be gone: 'filtered' class removed, icon hidden, global button hidden
await expect(page.getByTestId(LAYER_NAME).locator('.node')).not.toContainClass('filtered');
await expect(page.getByTestId(LAYER_NAME).locator('.icon-filter')).not.toBeVisible();
await expect(page.locator('#layerActionUnfilter')).not.toBeVisible();
});
});
Loading