diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index ec75c4a0..c47bd062 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -135,7 +135,7 @@ ul { /* forms */ /* *********** */ input[type="text"], input[type="password"], input[type="date"], -input[type="datetime"], input[type="email"], input[type="number"], +input[type="datetime-local"], input[type="email"], input[type="number"], input[type="search"], input[type="tel"], input[type="time"], input[type="url"], textarea { background-color: white; @@ -263,6 +263,9 @@ input[type="checkbox"] + label { display: inline; padding: 0 14px; } +label input[type="radio"] { + margin-right: 10px; +} select + .error, input + .error { display: block; @@ -343,6 +346,11 @@ input:invalid { .fieldset.toggle.on .legend:before { background-position: -144px -51px; } +fieldset legend { + font-size: 1.1rem; + padding: 0 5px; +} + /* Switch */ input.switch:empty { display: none; diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js index ac0293da..046324bb 100644 --- a/umap/static/umap/js/modules/browser.js +++ b/umap/static/umap/js/modules/browser.js @@ -142,8 +142,7 @@ export default class Browser { ['options.filter', { handler: 'Input', placeholder: translate('Filter') }], ['options.inBbox', { handler: 'Switch', label: translate('Current map view') }], ] - const builder = new U.FormBuilder(this, fields, { - makeDirty: false, + const builder = new L.FormBuilder(this, fields, { callback: () => this.onFormChange(), }) formContainer.appendChild(builder.build()) diff --git a/umap/static/umap/js/modules/facets.js b/umap/static/umap/js/modules/facets.js new file mode 100644 index 00000000..2d0c6e62 --- /dev/null +++ b/umap/static/umap/js/modules/facets.js @@ -0,0 +1,148 @@ +import { DomUtil, DomEvent, stamp } from '../../vendors/leaflet/leaflet-src.esm.js' +import { translate } from './i18n.js' +import * as Utils from './utils.js' + +export default class Facets { + constructor(map) { + this.map = map + this.selected = {} + } + + compute(names, defined) { + const properties = {} + + names.forEach((name) => { + const type = defined[name]['type'] + properties[name] = { type: type } + this.selected[name] = { type: type } + if (!['date', 'datetime', 'number'].includes(type)) { + properties[name].choices = [] + this.selected[name].choices = [] + } + }) + + this.map.eachBrowsableDataLayer((datalayer) => { + datalayer.eachFeature((feature) => { + names.forEach((name) => { + let value = feature.properties[name] + const type = defined[name]['type'] + const parser = this.getParser(type) + value = parser(value) + switch (type) { + case 'date': + case 'datetime': + case 'number': + if (!isNaN(value)) { + if (isNaN(properties[name].min) || properties[name].min > value) { + properties[name].min = value + } + if (isNaN(properties[name].max) || properties[name].max < value) { + properties[name].max = value + } + } + break + default: + value = value || translate('') + if (!properties[name].choices.includes(value)) { + properties[name].choices.push(value) + } + } + }) + }) + }) + return properties + } + + redraw() { + if (this.isOpen()) this.open() + } + + isOpen() { + return !!document.querySelector('.umap-facet-search') + } + + open() { + const container = L.DomUtil.create('div', 'umap-facet-search') + const title = L.DomUtil.add( + 'h3', + 'umap-filter-title', + container, + translate('Facet search') + ) + const defined = this.getDefined() + const names = Object.keys(defined) + const facetProperties = this.compute(names, defined) + + const filterFeatures = function () { + let found = false + this.map.eachBrowsableDataLayer((datalayer) => { + datalayer.resetLayer(true) + if (datalayer.hasDataVisible()) found = true + }) + // TODO: display a results counter in the panel instead. + if (!found) { + this.map.ui.alert({ + content: translate('No results for these facets'), + level: 'info', + }) + } + } + + const fields = names.map((name) => { + let criteria = facetProperties[name] + let handler = 'FacetSearchChoices' + switch (criteria['type']) { + case 'number': + handler = 'FacetSearchNumber' + break + case 'date': + handler = 'FacetSearchDate' + break + case 'datetime': + handler = 'FacetSearchDateTime' + break + } + let label = defined[name]['label'] + return [ + `selected.${name}`, + { + criteria: criteria, + handler: handler, + label: label, + }, + ] + }) + + const builder = new L.FormBuilder(this, fields, { + callback: filterFeatures, + callbackContext: this, + }) + container.appendChild(builder.build()) + + this.map.panel.open({ content: container }) + } + + getDefined() { + const defaultType = 'checkbox' + const allowedTypes = [defaultType, 'radio', 'number', 'date', 'datetime'] + return (this.map.options.facetKey || '').split(',').reduce((acc, curr) => { + let [name, label, type] = curr.split('|') + type = allowedTypes.includes(type) ? type : defaultType + acc[name] = { label: label || name, type: type } + return acc + }, {}) + } + + getParser(type) { + switch (type) { + case 'number': + return parseFloat + case 'datetime': + return (v) => new Date(v) + case 'date': + return Utils.parseNaiveDate + default: + return (v) => String(v || '') + } + } +} diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index 7d07ba2f..16742508 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -1,5 +1,6 @@ import URLs from './urls.js' import Browser from './browser.js' +import Facets from './facets.js' import { Panel, EditPanel, FullPanel } from './panel.js' import * as Utils from './utils.js' import { SCHEMA } from './schema.js' @@ -17,6 +18,7 @@ window.U = { HTTPError, NOKError, Browser, + Facets, Panel, EditPanel, FullPanel, diff --git a/umap/static/umap/js/modules/utils.js b/umap/static/umap/js/modules/utils.js index 125945ee..5b18286e 100644 --- a/umap/static/umap/js/modules/utils.js +++ b/umap/static/umap/js/modules/utils.js @@ -27,9 +27,9 @@ export function checkId(string) { /** * Compute the impacts for a given list of fields. - * + * * Return an array of unique impacts. - * + * * @param {fields} list[fields] * @returns Array[string] */ @@ -356,3 +356,9 @@ export function template(str, data) { return value }) } + +export function parseNaiveDate(value) { + const naive = new Date(value) + // Let's pretend naive date are UTC, and remove time… + return new Date(Date.UTC(naive.getFullYear(), naive.getMonth(), naive.getDate())) +} diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 6f9a853b..a77d7ae6 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -661,55 +661,7 @@ const ControlsMixin = { 'tilelayers', ], _openFacet: function () { - const container = L.DomUtil.create('div', 'umap-facet-search'), - title = L.DomUtil.add('h3', 'umap-filter-title', container, L._('Facet search')), - keys = Object.keys(this.getFacetKeys()) - - const knownValues = {} - - keys.forEach((key) => { - knownValues[key] = [] - if (!this.facets[key]) this.facets[key] = [] - }) - - this.eachBrowsableDataLayer((datalayer) => { - datalayer.eachFeature((feature) => { - keys.forEach((key) => { - let value = feature.properties[key] - if (typeof value !== 'undefined' && !knownValues[key].includes(value)) { - knownValues[key].push(value) - } - }) - }) - }) - - const filterFeatures = function () { - let found = false - this.eachBrowsableDataLayer((datalayer) => { - datalayer.resetLayer(true) - if (datalayer.hasDataVisible()) found = true - }) - // TODO: display a results counter in the panel instead. - if (!found) - this.ui.alert({ content: L._('No results for these facets'), level: 'info' }) - } - - const fields = keys.map((current) => [ - `facets.${current}`, - { - handler: 'FacetSearch', - choices: knownValues[current], - label: this.getFacetKeys()[current], - }, - ]) - const builder = new U.FormBuilder(this, fields, { - makeDirty: false, - callback: filterFeatures, - callbackContext: this, - }) - container.appendChild(builder.build()) - - this.panel.open({ content: container }) + this.facets.open() }, displayCaption: function () { @@ -865,7 +817,7 @@ const ControlsMixin = { L.DomUtil.createLink( 'umap-user', rightContainer, - L._(`My Dashboard ({username})`, { + L._(`My Dashboard ({username})`, { username: this.options.user.name, }), this.options.user.url diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index c4c2c5e4..aae48995 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -73,7 +73,7 @@ L.DomUtil.add = (tagName, className, container, content) => { if (content.nodeType && content.nodeType === 1) { el.appendChild(content) } else { - el.innerHTML = content + el.textContent = content } } return el @@ -541,7 +541,7 @@ U.Help = L.Class.extend({ slugKey: L._('The name of the property to use as feature unique identifier.'), filterKey: L._('Comma separated list of properties to use when filtering features'), facetKey: L._( - 'Comma separated list of properties to use for facet search (eg.: mykey,otherkey). To control label, add it after a | (eg.: mykey|My Key,otherkey|Other Key)' + 'Comma separated list of properties to use for facet search (eg.: mykey,otherkey). To control label, add it after a | (eg.: mykey|My Key,otherkey|Other Key). To control input field type, add it after another | (eg.: mykey|My Key|checkbox,otherkey|Other Key|datetime). Allowed values for the input field type are checkbox (default), radio, number, date and datetime.' ), interactive: L._( 'If false, the polygon or line will act as a part of the underlying map.' diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index 60814720..d0749a81 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -493,11 +493,24 @@ U.FeatureMixin = { }, matchFacets: function () { - const facets = this.map.facets - for (const [property, expected] of Object.entries(facets)) { - if (expected.length) { - let value = this.properties[property] - if (!value || !expected.includes(value)) return false + const selected = this.map.facets.selected + for (let [name, { type, min, max, choices }] of Object.entries(selected)) { + let value = this.properties[name] + let parser = this.map.facets.getParser(type) + value = parser(value) + switch (type) { + case 'date': + case 'datetime': + case 'number': + min = parser(min) + max = parser(max) + if (!isNaN(min) && !isNaN(value) && min > value) return false + if (!isNaN(max) && !isNaN(value) && max < value) return false + break + default: + value = value || L._('') + if (choices?.length && !choices.includes(value)) return false + break } } return true diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index f29460fd..8f9619e2 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -527,11 +527,11 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ ) } const symbol = L.DomUtil.add( - 'button', - 'flat tab-symbols', - this.tabs, - L._('Symbol') - ), + 'button', + 'flat tab-symbols', + this.tabs, + L._('Symbol') + ), char = L.DomUtil.add( 'button', 'flat tab-chars', @@ -744,34 +744,122 @@ L.FormBuilder.Switch = L.FormBuilder.CheckBox.extend({ }, }) -L.FormBuilder.FacetSearch = L.FormBuilder.Element.extend({ +L.FormBuilder.FacetSearchChoices = L.FormBuilder.Element.extend({ build: function () { - this.container = L.DomUtil.create('div', 'umap-facet', this.parentNode) + this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode) + this.container.appendChild(this.label) this.ul = L.DomUtil.create('ul', '', this.container) - const choices = this.options.choices + this.type = this.options.criteria['type'] + + const choices = this.options.criteria['choices'] choices.sort() choices.forEach((value) => this.buildLi(value)) }, buildLabel: function () { - this.label = L.DomUtil.add('h5', '', this.parentNode, this.options.label) + this.label = L.DomUtil.element('legend', {textContent: this.options.label}) }, buildLi: function (value) { - const property_li = L.DomUtil.create('li', '', this.ul), - input = L.DomUtil.create('input', '', property_li), - label = L.DomUtil.create('label', '', property_li) - input.type = 'checkbox' - input.id = `checkbox_${this.name}_${value}` - input.checked = this.get().includes(value) + const property_li = L.DomUtil.create('li', '', this.ul) + const label = L.DomUtil.add('label', '', property_li) + const input = L.DomUtil.create('input', '', label) + L.DomUtil.add('span', '', label, value) + + input.type = this.type + input.name = `${this.type}_${this.name}` + input.checked = this.get()['choices'].includes(value) input.dataset.value = value - label.htmlFor = `checkbox_${this.name}_${value}` - label.innerHTML = value + L.DomEvent.on(input, 'change', (e) => this.sync()) }, toJS: function () { - return [...this.ul.querySelectorAll('input:checked')].map((i) => i.dataset.value) + return { + type: this.type, + choices: [...this.ul.querySelectorAll('input:checked')].map( + (i) => i.dataset.value + ), + } + }, +}) + +L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({ + getInputType: function (type) { + return type + }, + + getLabels: function () { + return [L._('Min'), L._('Max')] + }, + + castValue: function (value) { + return value.valueOf() + }, + + build: function () { + this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode) + this.container.appendChild(this.label) + const {min, max, type} = this.options.criteria + this.type = type + this.inputType = this.getInputType(this.type) + + const [minLabel, maxLabel] = this.getLabels() + + this.minLabel = L.DomUtil.create('label', '', this.container) + this.minLabel.innerHTML = minLabel + + this.minInput = L.DomUtil.create('input', '', this.minLabel) + this.minInput.type = this.inputType + this.minInput.step = 'any' + if (min != null) { + this.minInput.valueAsNumber = this.castValue(min) + this.minInput.dataset.value = min + } + + + this.maxLabel = L.DomUtil.create('label', '', this.container) + this.maxLabel.innerHTML = maxLabel + + this.maxInput = L.DomUtil.create('input', '', this.maxLabel) + this.maxInput.type = this.inputType + this.maxInput.step = 'any' + if (max != null) { + this.maxInput.valueAsNumber = this.castValue(max) + this.maxInput.dataset.value = max + } + + L.DomEvent.on(this.minInput, 'change', (e) => this.sync()) + L.DomEvent.on(this.maxInput, 'change', (e) => this.sync()) + }, + + buildLabel: function () { + this.label = L.DomUtil.element('legend', {textContent: this.options.label}) + }, + + toJS: function () { + return { + type: this.type, + min: this.minInput.value, + max: this.maxInput.value, + } + }, +}) + +L.FormBuilder.FacetSearchNumber = L.FormBuilder.MinMaxBase.extend({}) + +L.FormBuilder.FacetSearchDate = L.FormBuilder.MinMaxBase.extend({ + castValue: function (value) { + return value.valueOf() - value.getTimezoneOffset() * 60000 + }, + getLabels: function () { + return [L._('From'), L._('Until')] + }, +}) + +L.FormBuilder.FacetSearchDateTime = L.FormBuilder.FacetSearchDate.extend({ + getInputType: function (type) { + return 'datetime-local' }, }) @@ -1029,10 +1117,8 @@ U.FormBuilder = L.FormBuilder.extend({ setter: function (field, value) { L.FormBuilder.prototype.setter.call(this, field, value) - if (this.options.makeDirty !== false) { - this.obj.isDirty = true - if ('render' in this.obj) this.obj.render([field], this) - } + this.obj.isDirty = true + if ('render' in this.obj) this.obj.render([field], this) }, finish: function () { diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 2c247447..0f4f42f2 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -106,17 +106,19 @@ U.Map = L.Map.extend({ this.options.slideshow && this.options.slideshow.delay && this.options.slideshow.active === undefined - ) + ) { this.options.slideshow.active = true - if (this.options.advancedFilterKey) + } + if (this.options.advancedFilterKey) { this.options.facetKey = this.options.advancedFilterKey + delete this.options.advancedFilterKey + } // Global storage for retrieving datalayers and features this.datalayers = {} this.datalayers_index = [] this.dirty_datalayers = [] this.features_index = {} - this.facets = {} // Needed for actions labels this.help = new U.Help(this) @@ -218,6 +220,7 @@ U.Map = L.Map.extend({ this.panel.mode = 'condensed' this.displayCaption() } else if (['facet', 'datafilters'].includes(this.options.onLoadPanel)) { + this.panel.mode = 'expanded' this.openFacet() } if (L.Util.queryString('edit')) { @@ -251,6 +254,7 @@ U.Map = L.Map.extend({ this.initCaptionBar() this.renderEditToolbar() this.renderControls() + this.facets.redraw() break case 'data': this.redrawVisibleDataLayers() @@ -376,6 +380,7 @@ U.Map = L.Map.extend({ if (this.options.scrollWheelZoom) this.scrollWheelZoom.enable() else this.scrollWheelZoom.disable() this.browser = new U.Browser(this) + this.facets = new U.Facets(this) this.importer = new U.Importer(this) this.drop = new U.DropControl(this) this.share = new U.Share(this) @@ -1234,9 +1239,9 @@ U.Map = L.Map.extend({ [ 'options.facetKey', { - handler: 'Input', + handler: 'BlurInput', helpEntries: 'facetKey', - placeholder: L._('Example: key1,key2,key3'), + placeholder: L._('Example: key1,key2|Label 2,key3|Label 3|checkbox'), label: L._('Facet keys'), }, ], @@ -1845,14 +1850,6 @@ U.Map = L.Map.extend({ return (this.options.filterKey || this.options.sortKey || 'name').split(',') }, - getFacetKeys: function () { - return (this.options.facetKey || '').split(',').reduce((acc, curr) => { - const els = curr.split('|') - acc[els[0]] = els[1] || els[0] - return acc - }, {}) - }, - getLayersBounds: function () { const bounds = new L.latLngBounds() this.eachBrowsableDataLayer((d) => { diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index 4d5c5677..553f6047 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -927,7 +927,6 @@ a.umap-control-caption, overflow: hidden; text-overflow: ellipsis; } -.umap-facet-search li:nth-child(even), .umap-browser .datalayer li:nth-child(even) { background-color: #efefef; } diff --git a/umap/static/umap/unittests/utils.js b/umap/static/umap/unittests/utils.js index 68148c56..4624b273 100644 --- a/umap/static/umap/unittests/utils.js +++ b/umap/static/umap/unittests/utils.js @@ -590,4 +590,21 @@ describe('Utils', function () { assert.deepEqual(getImpactsFromSchema(['foo', 'bar', 'baz'], schema), ['A', 'B']) }) }) + describe('parseNaiveDate', () => { + it('should parse a date', () => { + assert.equal(Utils.parseNaiveDate("2024/03/04").toISOString(), "2024-03-04T00:00:00.000Z") + }) + it('should parse a datetime', () => { + assert.equal(Utils.parseNaiveDate("2024/03/04 12:13:14").toISOString(), "2024-03-04T00:00:00.000Z") + }) + it('should parse an iso datetime', () => { + assert.equal(Utils.parseNaiveDate("2024-03-04T00:00:00.000Z").toISOString(), "2024-03-04T00:00:00.000Z") + }) + it('should parse a GMT time', () => { + assert.equal(Utils.parseNaiveDate("04 Mar 2024 00:12:00 GMT").toISOString(), "2024-03-04T00:00:00.000Z") + }) + it('should parse a GMT time with explicit timezone', () => { + assert.equal(Utils.parseNaiveDate("Thu, 04 Mar 2024 00:00:00 GMT+0300").toISOString(), "2024-03-03T00:00:00.000Z") + }) + }) }) diff --git a/umap/tests/integration/test_facets_browser.py b/umap/tests/integration/test_facets_browser.py index 68e64ee3..f72d3124 100644 --- a/umap/tests/integration/test_facets_browser.py +++ b/umap/tests/integration/test_facets_browser.py @@ -1,3 +1,5 @@ +import copy + import pytest from playwright.sync_api import expect @@ -11,12 +13,22 @@ DATALAYER_DATA1 = { "features": [ { "type": "Feature", - "properties": {"mytype": "even", "name": "Point 2"}, + "properties": { + "mytype": "even", + "name": "Point 2", + "mynumber": 10, + "mydate": "2024/04/14 12:19:17", + }, "geometry": {"type": "Point", "coordinates": [0.065918, 48.385442]}, }, { "type": "Feature", - "properties": {"mytype": "odd", "name": "Point 1"}, + "properties": { + "mytype": "odd", + "name": "Point 1", + "mynumber": 12, + "mydate": "2024/03/13 12:20:20", + }, "geometry": {"type": "Point", "coordinates": [3.55957, 49.767074]}, }, ], @@ -31,12 +43,22 @@ DATALAYER_DATA2 = { "features": [ { "type": "Feature", - "properties": {"mytype": "even", "name": "Point 4"}, + "properties": { + "mytype": "even", + "name": "Point 4", + "mynumber": 10, + "mydate": "2024/08/18 13:14:15", + }, "geometry": {"type": "Point", "coordinates": [0.856934, 45.290347]}, }, { "type": "Feature", - "properties": {"mytype": "odd", "name": "Point 3"}, + "properties": { + "mytype": "odd", + "name": "Point 3", + "mynumber": 14, + "mydate": "2024-04-14T10:19:17.000Z", + }, "geometry": {"type": "Point", "coordinates": [4.372559, 47.945786]}, }, ], @@ -70,23 +92,19 @@ DATALAYER_DATA3 = { } -@pytest.fixture -def bootstrap(map, live_server): +def test_simple_facet_search(live_server, page, map): map.settings["properties"]["onLoadPanel"] = "facet" - map.settings["properties"]["facetKey"] = "mytype|My type" + map.settings["properties"]["facetKey"] = "mytype|My type,mynumber|My Number|number" map.settings["properties"]["showLabel"] = True map.save() DataLayerFactory(map=map, data=DATALAYER_DATA1) DataLayerFactory(map=map, data=DATALAYER_DATA2) DataLayerFactory(map=map, data=DATALAYER_DATA3) - - -def test_simple_facet_search(live_server, page, bootstrap, map): - page.goto(f"{live_server.url}{map.get_absolute_url()}") + page.goto(f"{live_server.url}{map.get_absolute_url()}#6/48.948/1.670") panel = page.locator(".umap-facet-search") # From a non browsable datalayer, should not be impacted paths = page.locator(".leaflet-overlay-pane path") - expect(paths).to_be_visible + expect(paths).to_be_visible() expect(panel).to_be_visible() # Facet name expect(page.get_by_text("My type")).to_be_visible() @@ -95,7 +113,7 @@ def test_simple_facet_search(live_server, page, bootstrap, map): odd = page.get_by_text("odd") expect(oven).to_be_visible() expect(odd).to_be_visible() - expect(paths).to_be_visible + expect(paths).to_be_visible() markers = page.locator(".leaflet-marker-icon") expect(markers).to_have_count(4) # Tooltips @@ -114,4 +132,76 @@ def test_simple_facet_search(live_server, page, bootstrap, map): # Now let's filter odd.click() expect(markers).to_have_count(4) - expect(paths).to_be_visible + expect(paths).to_be_visible() + + # Let's filter using the number facet + expect(page.get_by_text("My Number")).to_be_visible() + expect(page.get_by_label("Min")).to_have_value("10") + expect(page.get_by_label("Max")).to_have_value("14") + page.get_by_label("Min").fill("11") + page.keyboard.press("Tab") # Move out of the input, so the "change" event is sent + expect(markers).to_have_count(2) + expect(paths).to_be_visible() + page.get_by_label("Max").fill("13") + page.keyboard.press("Tab") + expect(markers).to_have_count(1) + + # Now let's combine + page.get_by_label("Min").fill("10") + page.keyboard.press("Tab") + expect(markers).to_have_count(3) + odd.click() + expect(markers).to_have_count(1) + expect(paths).to_be_visible() + + +def test_date_facet_search(live_server, page, map): + map.settings["properties"]["onLoadPanel"] = "facet" + map.settings["properties"]["facetKey"] = "mydate|Date filter|date" + map.save() + DataLayerFactory(map=map, data=DATALAYER_DATA1) + DataLayerFactory(map=map, data=DATALAYER_DATA2) + page.goto(f"{live_server.url}{map.get_absolute_url()}") + markers = page.locator(".leaflet-marker-icon") + expect(markers).to_have_count(4) + expect(page.get_by_text("Date Filter")).to_be_visible() + expect(page.get_by_label("From")).to_have_value("2024-03-13") + expect(page.get_by_label("Until")).to_have_value("2024-08-18") + page.get_by_label("From").fill("2024-03-14") + expect(markers).to_have_count(3) + page.get_by_label("Until").fill("2024-08-17") + expect(markers).to_have_count(2) + + +def test_choice_with_empty_value(live_server, page, map): + map.settings["properties"]["onLoadPanel"] = "facet" + map.settings["properties"]["facetKey"] = "mytype|My type" + map.save() + data = copy.deepcopy(DATALAYER_DATA1) + data["features"][0]["properties"]["mytype"] = "" + del data["features"][1]["properties"]["mytype"] + DataLayerFactory(map=map, data=data) + DataLayerFactory(map=map, data=DATALAYER_DATA2) + page.goto(f"{live_server.url}{map.get_absolute_url()}") + expect(page.get_by_text("")).to_be_visible() + markers = page.locator(".leaflet-marker-icon") + expect(markers).to_have_count(4) + page.get_by_text("").click() + expect(markers).to_have_count(2) + + +def test_number_with_zero_value(live_server, page, map): + map.settings["properties"]["onLoadPanel"] = "facet" + map.settings["properties"]["facetKey"] = "mynumber|Filter|number" + map.save() + data = copy.deepcopy(DATALAYER_DATA1) + data["features"][0]["properties"]["mynumber"] = 0 + DataLayerFactory(map=map, data=data) + DataLayerFactory(map=map, data=DATALAYER_DATA2) + page.goto(f"{live_server.url}{map.get_absolute_url()}") + expect(page.get_by_label("Min")).to_have_value("0") + expect(page.get_by_label("Max")).to_have_value("14") + page.get_by_label("Min").fill("1") + page.keyboard.press("Tab") # Move out of the input, so the "change" event is sent + markers = page.locator(".leaflet-marker-icon") + expect(markers).to_have_count(3)