Merge pull request #1763 from umap-project/flammermann-facet-date

Date and number support for facets
This commit is contained in:
David Larlet 2024-04-19 10:58:02 -04:00 committed by GitHub
commit d8c2e14b42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 429 additions and 112 deletions

View file

@ -135,7 +135,7 @@ ul {
/* forms */ /* forms */
/* *********** */ /* *********** */
input[type="text"], input[type="password"], input[type="date"], 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="search"], input[type="tel"], input[type="time"],
input[type="url"], textarea { input[type="url"], textarea {
background-color: white; background-color: white;
@ -263,6 +263,9 @@ input[type="checkbox"] + label {
display: inline; display: inline;
padding: 0 14px; padding: 0 14px;
} }
label input[type="radio"] {
margin-right: 10px;
}
select + .error, select + .error,
input + .error { input + .error {
display: block; display: block;
@ -343,6 +346,11 @@ input:invalid {
.fieldset.toggle.on .legend:before { .fieldset.toggle.on .legend:before {
background-position: -144px -51px; background-position: -144px -51px;
} }
fieldset legend {
font-size: 1.1rem;
padding: 0 5px;
}
/* Switch */ /* Switch */
input.switch:empty { input.switch:empty {
display: none; display: none;

View file

@ -142,8 +142,7 @@ export default class Browser {
['options.filter', { handler: 'Input', placeholder: translate('Filter') }], ['options.filter', { handler: 'Input', placeholder: translate('Filter') }],
['options.inBbox', { handler: 'Switch', label: translate('Current map view') }], ['options.inBbox', { handler: 'Switch', label: translate('Current map view') }],
] ]
const builder = new U.FormBuilder(this, fields, { const builder = new L.FormBuilder(this, fields, {
makeDirty: false,
callback: () => this.onFormChange(), callback: () => this.onFormChange(),
}) })
formContainer.appendChild(builder.build()) formContainer.appendChild(builder.build())

View file

@ -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('<empty value>')
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 || '')
}
}
}

View file

@ -1,5 +1,6 @@
import URLs from './urls.js' import URLs from './urls.js'
import Browser from './browser.js' import Browser from './browser.js'
import Facets from './facets.js'
import { Panel, EditPanel, FullPanel } from './panel.js' import { Panel, EditPanel, FullPanel } from './panel.js'
import * as Utils from './utils.js' import * as Utils from './utils.js'
import { SCHEMA } from './schema.js' import { SCHEMA } from './schema.js'
@ -17,6 +18,7 @@ window.U = {
HTTPError, HTTPError,
NOKError, NOKError,
Browser, Browser,
Facets,
Panel, Panel,
EditPanel, EditPanel,
FullPanel, FullPanel,

View file

@ -27,9 +27,9 @@ export function checkId(string) {
/** /**
* Compute the impacts for a given list of fields. * Compute the impacts for a given list of fields.
* *
* Return an array of unique impacts. * Return an array of unique impacts.
* *
* @param {fields} list[fields] * @param {fields} list[fields]
* @returns Array[string] * @returns Array[string]
*/ */
@ -356,3 +356,9 @@ export function template(str, data) {
return value 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()))
}

View file

@ -661,55 +661,7 @@ const ControlsMixin = {
'tilelayers', 'tilelayers',
], ],
_openFacet: function () { _openFacet: function () {
const container = L.DomUtil.create('div', 'umap-facet-search'), this.facets.open()
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 })
}, },
displayCaption: function () { displayCaption: function () {
@ -865,7 +817,7 @@ const ControlsMixin = {
L.DomUtil.createLink( L.DomUtil.createLink(
'umap-user', 'umap-user',
rightContainer, rightContainer,
L._(`My Dashboard <span>({username})</span>`, { L._(`My Dashboard ({username})`, {
username: this.options.user.name, username: this.options.user.name,
}), }),
this.options.user.url this.options.user.url

View file

@ -73,7 +73,7 @@ L.DomUtil.add = (tagName, className, container, content) => {
if (content.nodeType && content.nodeType === 1) { if (content.nodeType && content.nodeType === 1) {
el.appendChild(content) el.appendChild(content)
} else { } else {
el.innerHTML = content el.textContent = content
} }
} }
return el return el
@ -541,7 +541,7 @@ U.Help = L.Class.extend({
slugKey: L._('The name of the property to use as feature unique identifier.'), 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'), filterKey: L._('Comma separated list of properties to use when filtering features'),
facetKey: L._( 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._( interactive: L._(
'If false, the polygon or line will act as a part of the underlying map.' 'If false, the polygon or line will act as a part of the underlying map.'

View file

@ -493,11 +493,24 @@ U.FeatureMixin = {
}, },
matchFacets: function () { matchFacets: function () {
const facets = this.map.facets const selected = this.map.facets.selected
for (const [property, expected] of Object.entries(facets)) { for (let [name, { type, min, max, choices }] of Object.entries(selected)) {
if (expected.length) { let value = this.properties[name]
let value = this.properties[property] let parser = this.map.facets.getParser(type)
if (!value || !expected.includes(value)) return false 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._('<empty value>')
if (choices?.length && !choices.includes(value)) return false
break
} }
} }
return true return true

View file

@ -527,11 +527,11 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
) )
} }
const symbol = L.DomUtil.add( const symbol = L.DomUtil.add(
'button', 'button',
'flat tab-symbols', 'flat tab-symbols',
this.tabs, this.tabs,
L._('Symbol') L._('Symbol')
), ),
char = L.DomUtil.add( char = L.DomUtil.add(
'button', 'button',
'flat tab-chars', '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 () { 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) 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.sort()
choices.forEach((value) => this.buildLi(value)) choices.forEach((value) => this.buildLi(value))
}, },
buildLabel: function () { 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) { buildLi: function (value) {
const property_li = L.DomUtil.create('li', '', this.ul), const property_li = L.DomUtil.create('li', '', this.ul)
input = L.DomUtil.create('input', '', property_li), const label = L.DomUtil.add('label', '', property_li)
label = L.DomUtil.create('label', '', property_li) const input = L.DomUtil.create('input', '', label)
input.type = 'checkbox' L.DomUtil.add('span', '', label, value)
input.id = `checkbox_${this.name}_${value}`
input.checked = this.get().includes(value) input.type = this.type
input.name = `${this.type}_${this.name}`
input.checked = this.get()['choices'].includes(value)
input.dataset.value = value input.dataset.value = value
label.htmlFor = `checkbox_${this.name}_${value}`
label.innerHTML = value
L.DomEvent.on(input, 'change', (e) => this.sync()) L.DomEvent.on(input, 'change', (e) => this.sync())
}, },
toJS: function () { 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) { setter: function (field, value) {
L.FormBuilder.prototype.setter.call(this, field, value) L.FormBuilder.prototype.setter.call(this, field, value)
if (this.options.makeDirty !== false) { this.obj.isDirty = true
this.obj.isDirty = true if ('render' in this.obj) this.obj.render([field], this)
if ('render' in this.obj) this.obj.render([field], this)
}
}, },
finish: function () { finish: function () {

View file

@ -106,17 +106,19 @@ U.Map = L.Map.extend({
this.options.slideshow && this.options.slideshow &&
this.options.slideshow.delay && this.options.slideshow.delay &&
this.options.slideshow.active === undefined this.options.slideshow.active === undefined
) ) {
this.options.slideshow.active = true this.options.slideshow.active = true
if (this.options.advancedFilterKey) }
if (this.options.advancedFilterKey) {
this.options.facetKey = this.options.advancedFilterKey this.options.facetKey = this.options.advancedFilterKey
delete this.options.advancedFilterKey
}
// Global storage for retrieving datalayers and features // Global storage for retrieving datalayers and features
this.datalayers = {} this.datalayers = {}
this.datalayers_index = [] this.datalayers_index = []
this.dirty_datalayers = [] this.dirty_datalayers = []
this.features_index = {} this.features_index = {}
this.facets = {}
// Needed for actions labels // Needed for actions labels
this.help = new U.Help(this) this.help = new U.Help(this)
@ -218,6 +220,7 @@ U.Map = L.Map.extend({
this.panel.mode = 'condensed' this.panel.mode = 'condensed'
this.displayCaption() this.displayCaption()
} else if (['facet', 'datafilters'].includes(this.options.onLoadPanel)) { } else if (['facet', 'datafilters'].includes(this.options.onLoadPanel)) {
this.panel.mode = 'expanded'
this.openFacet() this.openFacet()
} }
if (L.Util.queryString('edit')) { if (L.Util.queryString('edit')) {
@ -251,6 +254,7 @@ U.Map = L.Map.extend({
this.initCaptionBar() this.initCaptionBar()
this.renderEditToolbar() this.renderEditToolbar()
this.renderControls() this.renderControls()
this.facets.redraw()
break break
case 'data': case 'data':
this.redrawVisibleDataLayers() this.redrawVisibleDataLayers()
@ -376,6 +380,7 @@ U.Map = L.Map.extend({
if (this.options.scrollWheelZoom) this.scrollWheelZoom.enable() if (this.options.scrollWheelZoom) this.scrollWheelZoom.enable()
else this.scrollWheelZoom.disable() else this.scrollWheelZoom.disable()
this.browser = new U.Browser(this) this.browser = new U.Browser(this)
this.facets = new U.Facets(this)
this.importer = new U.Importer(this) this.importer = new U.Importer(this)
this.drop = new U.DropControl(this) this.drop = new U.DropControl(this)
this.share = new U.Share(this) this.share = new U.Share(this)
@ -1234,9 +1239,9 @@ U.Map = L.Map.extend({
[ [
'options.facetKey', 'options.facetKey',
{ {
handler: 'Input', handler: 'BlurInput',
helpEntries: 'facetKey', helpEntries: 'facetKey',
placeholder: L._('Example: key1,key2,key3'), placeholder: L._('Example: key1,key2|Label 2,key3|Label 3|checkbox'),
label: L._('Facet keys'), label: L._('Facet keys'),
}, },
], ],
@ -1845,14 +1850,6 @@ U.Map = L.Map.extend({
return (this.options.filterKey || this.options.sortKey || 'name').split(',') 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 () { getLayersBounds: function () {
const bounds = new L.latLngBounds() const bounds = new L.latLngBounds()
this.eachBrowsableDataLayer((d) => { this.eachBrowsableDataLayer((d) => {

View file

@ -927,7 +927,6 @@ a.umap-control-caption,
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.umap-facet-search li:nth-child(even),
.umap-browser .datalayer li:nth-child(even) { .umap-browser .datalayer li:nth-child(even) {
background-color: #efefef; background-color: #efefef;
} }

View file

@ -590,4 +590,21 @@ describe('Utils', function () {
assert.deepEqual(getImpactsFromSchema(['foo', 'bar', 'baz'], schema), ['A', 'B']) 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")
})
})
}) })

View file

@ -1,3 +1,5 @@
import copy
import pytest import pytest
from playwright.sync_api import expect from playwright.sync_api import expect
@ -11,12 +13,22 @@ DATALAYER_DATA1 = {
"features": [ "features": [
{ {
"type": "Feature", "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]}, "geometry": {"type": "Point", "coordinates": [0.065918, 48.385442]},
}, },
{ {
"type": "Feature", "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]}, "geometry": {"type": "Point", "coordinates": [3.55957, 49.767074]},
}, },
], ],
@ -31,12 +43,22 @@ DATALAYER_DATA2 = {
"features": [ "features": [
{ {
"type": "Feature", "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]}, "geometry": {"type": "Point", "coordinates": [0.856934, 45.290347]},
}, },
{ {
"type": "Feature", "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]}, "geometry": {"type": "Point", "coordinates": [4.372559, 47.945786]},
}, },
], ],
@ -70,23 +92,19 @@ DATALAYER_DATA3 = {
} }
@pytest.fixture def test_simple_facet_search(live_server, page, map):
def bootstrap(map, live_server):
map.settings["properties"]["onLoadPanel"] = "facet" 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.settings["properties"]["showLabel"] = True
map.save() map.save()
DataLayerFactory(map=map, data=DATALAYER_DATA1) DataLayerFactory(map=map, data=DATALAYER_DATA1)
DataLayerFactory(map=map, data=DATALAYER_DATA2) DataLayerFactory(map=map, data=DATALAYER_DATA2)
DataLayerFactory(map=map, data=DATALAYER_DATA3) DataLayerFactory(map=map, data=DATALAYER_DATA3)
page.goto(f"{live_server.url}{map.get_absolute_url()}#6/48.948/1.670")
def test_simple_facet_search(live_server, page, bootstrap, map):
page.goto(f"{live_server.url}{map.get_absolute_url()}")
panel = page.locator(".umap-facet-search") panel = page.locator(".umap-facet-search")
# From a non browsable datalayer, should not be impacted # From a non browsable datalayer, should not be impacted
paths = page.locator(".leaflet-overlay-pane path") paths = page.locator(".leaflet-overlay-pane path")
expect(paths).to_be_visible expect(paths).to_be_visible()
expect(panel).to_be_visible() expect(panel).to_be_visible()
# Facet name # Facet name
expect(page.get_by_text("My type")).to_be_visible() 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") odd = page.get_by_text("odd")
expect(oven).to_be_visible() expect(oven).to_be_visible()
expect(odd).to_be_visible() expect(odd).to_be_visible()
expect(paths).to_be_visible expect(paths).to_be_visible()
markers = page.locator(".leaflet-marker-icon") markers = page.locator(".leaflet-marker-icon")
expect(markers).to_have_count(4) expect(markers).to_have_count(4)
# Tooltips # Tooltips
@ -114,4 +132,76 @@ def test_simple_facet_search(live_server, page, bootstrap, map):
# Now let's filter # Now let's filter
odd.click() odd.click()
expect(markers).to_have_count(4) 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("<empty value>")).to_be_visible()
markers = page.locator(".leaflet-marker-icon")
expect(markers).to_have_count(4)
page.get_by_text("<empty value>").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)