From 84b5e2188aa8e7f701c2d4c83061f1518496996b Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 1 Dec 2023 10:30:06 +0100 Subject: [PATCH 1/6] Move importer panel to a separate class --- umap/static/umap/js/umap.controls.js | 130 +-------------------- umap/static/umap/js/umap.importer.js | 166 +++++++++++++++++++++++++++ umap/static/umap/js/umap.js | 3 +- umap/static/umap/test/index.html | 1 + umap/templates/umap/js.html | 1 + 5 files changed, 171 insertions(+), 130 deletions(-) create mode 100644 umap/static/umap/js/umap.importer.js diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index b905e13f..ad6cff9f 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -19,7 +19,7 @@ L.U.ImportAction = L.U.BaseAction.extend({ }, addHooks: function () { - this.map.importPanel() + this.map.importer.open() }, }) @@ -1172,134 +1172,6 @@ L.U.Map.include({ this.ui.openPanel({ data: { html: container } }) }, - importPanel: function () { - const container = L.DomUtil.create('div', 'umap-upload') - const title = L.DomUtil.create('h4', '', container) - const presetBox = L.DomUtil.create('div', 'formbox', container) - const presetSelect = L.DomUtil.create('select', '', presetBox) - const fileBox = L.DomUtil.create('div', 'formbox', container) - const fileInput = L.DomUtil.create('input', '', fileBox) - const urlInput = L.DomUtil.create('input', '', container) - const rawInput = L.DomUtil.create('textarea', '', container) - const typeLabel = L.DomUtil.create('label', '', container) - const layerLabel = L.DomUtil.create('label', '', container) - const clearLabel = L.DomUtil.create('label', '', container) - const submitInput = L.DomUtil.create('input', '', container) - const map = this - let option - const types = ['geojson', 'csv', 'gpx', 'kml', 'osm', 'georss', 'umap'] - title.textContent = L._('Import data') - fileInput.type = 'file' - fileInput.multiple = 'multiple' - submitInput.type = 'button' - submitInput.value = L._('Import') - submitInput.className = 'button' - typeLabel.textContent = L._('Choose the format of the data to import') - this.help.button(typeLabel, 'importFormats') - const typeInput = L.DomUtil.create('select', '', typeLabel) - typeInput.name = 'format' - layerLabel.textContent = L._('Choose the layer to import in') - const layerInput = L.DomUtil.create('select', '', layerLabel) - layerInput.name = 'datalayer' - urlInput.type = 'text' - urlInput.placeholder = L._('Provide an URL here') - rawInput.placeholder = L._('Paste your data here') - clearLabel.textContent = L._('Replace layer content') - const clearFlag = L.DomUtil.create('input', '', clearLabel) - clearFlag.type = 'checkbox' - clearFlag.name = 'clear' - this.eachDataLayerReverse((datalayer) => { - if (datalayer.isLoaded() && !datalayer.isRemoteLayer()) { - const id = L.stamp(datalayer) - option = L.DomUtil.create('option', '', layerInput) - option.value = id - option.textContent = datalayer.options.name - } - }) - L.DomUtil.element( - 'option', - { value: '', textContent: L._('Import in a new layer') }, - layerInput - ) - L.DomUtil.element( - 'option', - { value: '', textContent: L._('Choose the data format') }, - typeInput - ) - for (let i = 0; i < types.length; i++) { - option = L.DomUtil.create('option', '', typeInput) - option.value = option.textContent = types[i] - } - if (this.options.importPresets.length) { - const noPreset = L.DomUtil.create('option', '', presetSelect) - noPreset.value = noPreset.textContent = L._('Choose a preset') - for (let j = 0; j < this.options.importPresets.length; j++) { - option = L.DomUtil.create('option', '', presetSelect) - option.value = this.options.importPresets[j].url - option.textContent = this.options.importPresets[j].label - } - } else { - presetBox.style.display = 'none' - } - - const submit = function () { - let type = typeInput.value - const layerId = layerInput[layerInput.selectedIndex].value - let layer - if (type === 'umap') { - this.once('postsync', function () { - this.setView(this.latLng(this.options.center), this.options.zoom) - }) - } - if (layerId) layer = map.datalayers[layerId] - if (layer && clearFlag.checked) layer.empty() - if (fileInput.files.length) { - for (let i = 0, file; (file = fileInput.files[i]); i++) { - this.processFileToImport(file, layer, type) - } - } else { - if (!type) - return this.ui.alert({ - content: L._('Please choose a format'), - level: 'error', - }) - if (rawInput.value && type === 'umap') { - try { - this.importRaw(rawInput.value, type) - } catch (e) { - this.ui.alert({ content: L._('Invalid umap data'), level: 'error' }) - console.error(e) - } - } else { - if (!layer) layer = this.createDataLayer() - if (rawInput.value) layer.importRaw(rawInput.value, type) - else if (urlInput.value) layer.importFromUrl(urlInput.value, type) - else if (presetSelect.selectedIndex > 0) - layer.importFromUrl(presetSelect[presetSelect.selectedIndex].value, type) - } - } - } - L.DomEvent.on(submitInput, 'click', submit, this) - L.DomEvent.on( - fileInput, - 'change', - (e) => { - let type = '', - newType - for (let i = 0; i < e.target.files.length; i++) { - newType = L.Util.detectFileType(e.target.files[i]) - if (!type && newType) type = newType - if (type && newType !== type) { - type = '' - break - } - } - typeInput.value = type - }, - this - ) - this.ui.openPanel({ data: { html: container }, className: 'dark' }) - }, }) L.U.TileLayerControl = L.Control.extend({ diff --git a/umap/static/umap/js/umap.importer.js b/umap/static/umap/js/umap.importer.js new file mode 100644 index 00000000..2b290d6f --- /dev/null +++ b/umap/static/umap/js/umap.importer.js @@ -0,0 +1,166 @@ +L.U.Importer = L.Class.extend({ + TYPES: ['geojson', 'csv', 'gpx', 'kml', 'osm', 'georss', 'umap'], + initialize: function (map) { + this.map = map + this.presets = map.options.importPresets + }, + + build: function () { + this.container = L.DomUtil.create('div', 'umap-upload') + this.title = L.DomUtil.add('h4', '', this.container, L._('Import data')) + this.presetBox = L.DomUtil.create('div', 'formbox', this.container) + this.presetSelect = L.DomUtil.create('select', '', this.presetBox) + this.fileBox = L.DomUtil.create('div', 'formbox', this.container) + this.fileInput = L.DomUtil.element( + 'input', + { type: 'file', multiple: 'multiple' }, + this.fileBox + ) + this.urlInput = L.DomUtil.element( + 'input', + { type: 'text', placeholder: L._('Provide an URL here') }, + this.container + ) + this.rawInput = L.DomUtil.element( + 'textarea', + { placeholder: L._('Paste your data here') }, + this.container + ) + this.typeLabel = L.DomUtil.add( + 'label', + '', + this.container, + L._('Choose the format of the data to import') + ) + this.layerLabel = L.DomUtil.add( + 'label', + '', + this.container, + L._('Choose the layer to import in') + ) + this.clearLabel = L.DomUtil.add( + 'label', + '', + this.container, + L._('Replace layer content') + ) + this.submitInput = L.DomUtil.element( + 'input', + { type: 'button', value: L._('Import'), className: 'button' }, + this.container + ) + this.map.help.button(this.typeLabel, 'importFormats') + this.typeInput = L.DomUtil.element('select', { name: 'format' }, this.typeLabel) + this.layerInput = L.DomUtil.element( + 'select', + { name: 'datalayer' }, + this.layerLabel + ) + this.clearFlag = L.DomUtil.element( + 'input', + { type: 'checkbox', name: 'clear' }, + this.clearLabel + ) + let option + this.map.eachDataLayerReverse((datalayer) => { + if (datalayer.isLoaded() && !datalayer.isRemoteLayer()) { + const id = L.stamp(datalayer) + option = L.DomUtil.add('option', '', this.layerInput, datalayer.options.name) + option.value = id + } + }) + L.DomUtil.element( + 'option', + { value: '', textContent: L._('Import in a new layer') }, + this.layerInput + ) + L.DomUtil.element( + 'option', + { value: '', textContent: L._('Choose the data format') }, + this.typeInput + ) + for (let i = 0; i < this.TYPES.length; i++) { + option = L.DomUtil.create('option', '', this.typeInput) + option.value = option.textContent = this.TYPES[i] + } + if (this.presets.length) { + const noPreset = L.DomUtil.create('option', '', this.presetSelect) + noPreset.value = noPreset.textContent = L._('Choose a preset') + for (let j = 0; j < this.presets.length; j++) { + option = L.DomUtil.create('option', '', presetSelect) + option.value = this.presets[j].url + option.textContent = this.presets[j].label + } + } else { + this.presetBox.style.display = 'none' + } + L.DomEvent.on(this.submitInput, 'click', this.submit, this) + L.DomEvent.on( + this.fileInput, + 'change', + (e) => { + let type = '', + newType + for (let i = 0; i < e.target.files.length; i++) { + newType = L.Util.detectFileType(e.target.files[i]) + if (!type && newType) type = newType + if (type && newType !== type) { + type = '' + break + } + } + this.typeInput.value = type + }, + this + ) + }, + + open: function () { + if (!this.container) this.build() + this.map.ui.openPanel({ data: { html: this.container }, className: 'dark' }) + }, + + openFiles: function () { + this.open() + this.fileInput.click() + }, + + submit: function () { + let type = this.typeInput.value + const layerId = this.layerInput[this.layerInput.selectedIndex].value + let layer + if (type === 'umap') { + this.map.once('postsync', this.map._setDefaultCenter) + } + if (layerId) layer = this.map.datalayers[layerId] + if (layer && this.clearFlag.checked) layer.empty() + if (this.fileInput.files.length) { + for (let i = 0, file; (file = this.fileInput.files[i]); i++) { + this.map.processFileToImport(file, layer, type) + } + } else { + if (!type) + return this.map.ui.alert({ + content: L._('Please choose a format'), + level: 'error', + }) + if (this.rawInput.value && type === 'umap') { + try { + this.map.importRaw(this.rawInput.value, type) + } catch (e) { + this.ui.alert({ content: L._('Invalid umap data'), level: 'error' }) + console.error(e) + } + } else { + if (!layer) layer = this.map.createDataLayer() + if (this.rawInput.value) layer.importRaw(this.rawInput.value, type) + else if (this.urlInput.value) layer.importFromUrl(this.urlInput.value, type) + else if (this.presetSelect.selectedIndex > 0) + layer.importFromUrl( + this.presetSelect[this.presetSelect.selectedIndex].value, + type + ) + } + } + }, +}) diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 4cf6f74e..3619ab34 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -343,6 +343,7 @@ L.U.Map.include({ if (this.options.scrollWheelZoom) this.scrollWheelZoom.enable() else this.scrollWheelZoom.disable() this.browser = new L.U.Browser(this) + this.importer = new L.U.Importer(this) this.drop = new L.U.DropControl(this) this.renderControls() }, @@ -560,7 +561,7 @@ L.U.Map.include({ } if (key === L.U.Keys.I && modifierKey && this.editEnabled) { L.DomEvent.stop(e) - this.importPanel() + this.importer.open() } if (key === L.U.Keys.H && modifierKey && this.editEnabled) { L.DomEvent.stop(e) diff --git a/umap/static/umap/test/index.html b/umap/static/umap/test/index.html index e56ff9b5..4f11268b 100644 --- a/umap/static/umap/test/index.html +++ b/umap/static/umap/test/index.html @@ -41,6 +41,7 @@ + diff --git a/umap/templates/umap/js.html b/umap/templates/umap/js.html index 11693494..6a5b29af 100644 --- a/umap/templates/umap/js.html +++ b/umap/templates/umap/js.html @@ -43,6 +43,7 @@ + {% endcompress %} From 42906ea8b4976e60adc28fc034a0dc772dd3a3b0 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Sat, 2 Dec 2023 09:55:58 +0100 Subject: [PATCH 2/6] Add non working way to open files dialog --- umap/static/umap/js/umap.core.js | 1 + umap/static/umap/js/umap.js | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index 02384ec0..7b0fbb10 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -426,6 +426,7 @@ L.U.Keys = { I: 73, L: 76, M: 77, + O: 79, P: 80, S: 83, Z: 90, diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 3619ab34..dfed30cc 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -563,6 +563,10 @@ L.U.Map.include({ L.DomEvent.stop(e) this.importer.open() } + if (key === L.U.Keys.O && modifierKey && this.editEnabled) { + L.DomEvent.stop(e) + this.importer.openFiles() + } if (key === L.U.Keys.H && modifierKey && this.editEnabled) { L.DomEvent.stop(e) this.help.show('edit') From 67f6fa758784c5d9e5436d2fa5af7ad0de4c892e Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Sat, 2 Dec 2023 09:51:07 +0100 Subject: [PATCH 3/6] Add non working ways to focus the importer file input --- umap/static/umap/js/umap.importer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/umap/static/umap/js/umap.importer.js b/umap/static/umap/js/umap.importer.js index 2b290d6f..db02d69b 100644 --- a/umap/static/umap/js/umap.importer.js +++ b/umap/static/umap/js/umap.importer.js @@ -13,7 +13,7 @@ L.U.Importer = L.Class.extend({ this.fileBox = L.DomUtil.create('div', 'formbox', this.container) this.fileInput = L.DomUtil.element( 'input', - { type: 'file', multiple: 'multiple' }, + { type: 'file', multiple: 'multiple', autofocus: true }, this.fileBox ) this.urlInput = L.DomUtil.element( @@ -117,6 +117,7 @@ L.U.Importer = L.Class.extend({ open: function () { if (!this.container) this.build() + this.fileInput.focus() this.map.ui.openPanel({ data: { html: this.container }, className: 'dark' }) }, From 1bfbde320c85a04fb43c7d62ec9f5e032e316ae5 Mon Sep 17 00:00:00 2001 From: David Larlet Date: Sat, 2 Dec 2023 11:34:02 -0500 Subject: [PATCH 4/6] Add working `showPicker` to open files dialog --- umap/static/umap/js/umap.importer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umap/static/umap/js/umap.importer.js b/umap/static/umap/js/umap.importer.js index db02d69b..d57156c7 100644 --- a/umap/static/umap/js/umap.importer.js +++ b/umap/static/umap/js/umap.importer.js @@ -117,7 +117,7 @@ L.U.Importer = L.Class.extend({ open: function () { if (!this.container) this.build() - this.fileInput.focus() + this.fileInput.showPicker() this.map.ui.openPanel({ data: { html: this.container }, className: 'dark' }) }, From c62c32787877da6a3c04c66f7554cf26970748c1 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 5 Dec 2023 12:58:00 +0100 Subject: [PATCH 5/6] Importer: call showPicker only on openFiles, not at each open This allows to open the files dialog with Ctrl+O --- umap/static/umap/js/umap.importer.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/umap/static/umap/js/umap.importer.js b/umap/static/umap/js/umap.importer.js index d57156c7..9cd96c76 100644 --- a/umap/static/umap/js/umap.importer.js +++ b/umap/static/umap/js/umap.importer.js @@ -117,13 +117,12 @@ L.U.Importer = L.Class.extend({ open: function () { if (!this.container) this.build() - this.fileInput.showPicker() this.map.ui.openPanel({ data: { html: this.container }, className: 'dark' }) }, openFiles: function () { this.open() - this.fileInput.click() + this.fileInput.showPicker() }, submit: function () { From 50da2c0e1c61ee305a0195a6b33b0fa580dff66b Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 5 Dec 2023 14:28:11 +0100 Subject: [PATCH 6/6] Add integration test for textarea import --- umap/tests/integration/test_import.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/umap/tests/integration/test_import.py b/umap/tests/integration/test_import.py index 0d492221..fbc21418 100644 --- a/umap/tests/integration/test_import.py +++ b/umap/tests/integration/test_import.py @@ -6,7 +6,7 @@ from playwright.sync_api import expect pytestmark = pytest.mark.django_db -def test_umap_import(live_server, datalayer, page): +def test_umap_import_from_file(live_server, datalayer, page): page.goto(f"{live_server.url}/map/new/") button = page.get_by_title("Import data (Ctrl+I)") expect(button).to_be_visible() @@ -23,3 +23,27 @@ def test_umap_import(live_server, datalayer, page): expect(layers).to_have_count(3) nonloaded = page.locator(".umap-browse-datalayers li.off") expect(nonloaded).to_have_count(1) + + +def test_umap_import_geojson_from_textarea(live_server, datalayer, page): + page.goto(f"{live_server.url}/map/new/") + layers = page.locator(".umap-browse-datalayers li") + markers = page.locator(".leaflet-marker-icon") + paths = page.locator("path") + expect(markers).to_have_count(0) + expect(paths).to_have_count(0) + expect(layers).to_have_count(1) + button = page.get_by_title("Import data (Ctrl+I)") + expect(button).to_be_visible() + button.click() + textarea = page.locator(".umap-upload textarea") + path = Path(__file__).parent.parent / "fixtures/test_upload_data.json" + textarea.fill(path.read_text()) + page.locator('select[name="format"]').select_option("geojson") + button = page.get_by_role("button", name="Import", exact=True) + expect(button).to_be_visible() + button.click() + # No layer has been created + expect(layers).to_have_count(1) + expect(markers).to_have_count(2) + expect(paths).to_have_count(3)