diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js
index 51ba37d0..049a18ed 100644
--- a/umap/static/umap/js/modules/global.js
+++ b/umap/static/umap/js/modules/global.js
@@ -1,8 +1,9 @@
import * as L from '../../vendors/leaflet/leaflet-src.esm.js'
import URLs from './urls.js'
+import { Request, ServerRequest } from './request.js'
// Import modules and export them to the global scope.
// For the not yet module-compatible JS out there.
// Copy the leaflet module, it's expected by leaflet plugins to be writeable.
window.L = { ...L }
-window.umap = { URLs }
+window.umap = { URLs, Request, ServerRequest }
diff --git a/umap/static/umap/js/modules/request.js b/umap/static/umap/js/modules/request.js
new file mode 100644
index 00000000..df8c4033
--- /dev/null
+++ b/umap/static/umap/js/modules/request.js
@@ -0,0 +1,146 @@
+// Uses `L._`` from Leaflet.i18n which we cannot import as a module yet
+import { Evented, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
+
+const BaseRequest = Evented.extend({
+ _fetch: async function (method, uri, headers, data) {
+ const id = Math.random()
+ this.fire('dataloading', { id: id })
+ let response
+
+ try {
+ response = await fetch(uri, {
+ method: method,
+ mode: 'cors',
+ headers: headers,
+ body: data,
+ })
+ } catch (error) {
+ this._onError(error)
+ this.fire('dataload', { id: id })
+ return
+ }
+ if (!response.ok) {
+ this.onNok(response.status, await response.text())
+ }
+ // TODO
+ // - error handling
+ // - UI connection / events
+ // - preflight mode in CORS ?
+
+ this.fire('dataload', { id: id })
+ return response
+ },
+
+ get: async function (uri, headers) {
+ return await this._fetch('GET', uri, headers)
+ },
+
+ post: async function (uri, headers, data) {
+ return await this._fetch('POST', uri, headers, data)
+ },
+
+ _onError: function (error) {
+ console.error(error)
+ this.onError(error)
+ },
+ onError: function (error) {},
+ onNok: function (status) {},
+})
+
+export const Request = BaseRequest.extend({
+ initialize: function (ui) {
+ this.ui = ui
+ },
+ onError: function (error) {
+ console.error(error)
+ this.ui.alert({ content: L._('Problem in the response'), level: 'error' })
+ },
+ onNok: function (status, message) {
+ this.onError(message)
+ },
+})
+
+// Adds uMap specifics to requests handling
+// like logging, CSRF, etc.
+export const ServerRequest = Request.extend({
+ post: async function (uri, headers, data) {
+ const token = document.cookie.replace(
+ /(?:(?:^|.*;\s*)csrftoken\s*\=\s*([^;]*).*$)|^.*$/,
+ '$1'
+ )
+ if (token) {
+ headers = headers || {}
+ headers['X-CSRFToken'] = token
+ }
+ const response = await Request.prototype.post.call(this, uri, headers, data)
+ return this._handle_json_response(response)
+ },
+
+ get: async function (uri, headers) {
+ const response = await Request.prototype.get.call(this, uri, headers)
+ return this._handle_json_response(response)
+ },
+
+ _handle_json_response: async function (response) {
+ try {
+ const data = await response.json()
+ this._handle_server_instructions(data)
+ return [data, response]
+ } catch (error) {
+ this._onError(error)
+ }
+ },
+
+ _handle_server_instructions: function (data) {
+ // In some case, the response contains instructions
+ if (data.redirect) {
+ const newPath = data.redirect
+ if (window.location.pathname == newPath) {
+ window.location.reload() // Keep the hash, so the current view
+ } else {
+ window.location = newPath
+ }
+ } else if (data.info) {
+ this.ui.alert({ content: data.info, level: 'info' })
+ this.ui.closePanel()
+ } else if (data.error) {
+ this.ui.alert({ content: data.error, level: 'error' })
+ } else if (data.html) {
+ const ui_options = { data }
+ let listen_options
+ this.ui.openPanel(ui_options)
+ }
+ },
+
+ onNok: function (status, message) {
+ if (status === 403) {
+ this.ui.alert({
+ content: message || L._('Action not allowed :('),
+ level: 'error',
+ })
+ } else if (status === 412) {
+ const msg = L._(
+ 'Woops! Someone else seems to have edited the data. You can save anyway, but this will erase the changes made by others.'
+ )
+ const actions = [
+ {
+ label: L._('Save anyway'),
+ callback: function () {
+ // TODO
+ delete settings.headers['If-Match']
+ this._fetch(settings)
+ },
+ },
+ {
+ label: L._('Cancel'),
+ },
+ ]
+ this.ui.alert({
+ content: msg,
+ level: 'error',
+ duration: 100000,
+ actions: actions,
+ })
+ }
+ },
+})
diff --git a/umap/static/umap/js/umap.autocomplete.js b/umap/static/umap/js/umap.autocomplete.js
index 1f9b65cb..cbc43153 100644
--- a/umap/static/umap/js/umap.autocomplete.js
+++ b/umap/static/umap/js/umap.autocomplete.js
@@ -13,7 +13,7 @@ L.U.AutoComplete = L.Class.extend({
initialize: function (el, options) {
this.el = el
const ui = new L.U.UI(document.querySelector('header'))
- this.xhr = new L.U.Xhr(ui)
+ this.server = new window.umap.ServerRequest(ui)
L.setOptions(this, options)
let CURRENT = null
try {
@@ -158,21 +158,19 @@ L.U.AutoComplete = L.Class.extend({
}
},
- search: function () {
- const val = this.input.value
+ search: async function () {
+ let val = this.input.value
if (val.length < this.options.minChar) {
this.clear()
return
}
if (`${val}` === `${this.CACHE}`) return
else this.CACHE = val
- this._do_search(
- val,
- function (data) {
- this.handleResults(data.data)
- },
- this
+ val = val.toLowerCase()
+ const [{ data }, response] = await this.server.get(
+ `/agnocomplete/AutocompleteUser/?q=${encodeURIComponent(val)}`
)
+ this.handleResults(data)
},
createResult: function (item) {
@@ -272,14 +270,6 @@ L.U.AutoComplete.Ajax = L.U.AutoComplete.extend({
label: option.innerHTML,
}
},
-
- _do_search: function (val, callback, context) {
- val = val.toLowerCase()
- this.xhr.get(`/agnocomplete/AutocompleteUser/?q=${encodeURIComponent(val)}`, {
- callback: callback,
- context: context || this,
- })
- },
})
L.U.AutoComplete.Ajax.SelectMultiple = L.U.AutoComplete.Ajax.extend({
diff --git a/umap/static/umap/js/umap.datalayer.permissions.js b/umap/static/umap/js/umap.datalayer.permissions.js
index f77e086f..f4a79e92 100644
--- a/umap/static/umap/js/umap.datalayer.permissions.js
+++ b/umap/static/umap/js/umap.datalayer.permissions.js
@@ -51,19 +51,14 @@ L.U.DataLayerPermissions = L.Class.extend({
pk: this.datalayer.umap_id,
})
},
- save: function () {
+ save: async function () {
if (!this.isDirty) return this.datalayer.map.continueSaving()
const formData = new FormData()
formData.append('edit_status', this.options.edit_status)
- this.datalayer.map.post(this.getUrl(), {
- data: formData,
- context: this,
- callback: function (data) {
- this.commit()
- this.isDirty = false
- this.datalayer.map.continueSaving()
- },
- })
+ await this.datalayer.map.server.post(this.getUrl(), {}, formData)
+ this.commit()
+ this.isDirty = false
+ this.datalayer.map.continueSaving()
},
commit: function () {
diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js
index fabf2424..3f36b184 100644
--- a/umap/static/umap/js/umap.forms.js
+++ b/umap/static/umap/js/umap.forms.js
@@ -685,7 +685,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
return !this.value() || this.value() === this.obj.getMap().options.default_iconUrl
},
- showSymbolsTab: function () {
+ showSymbolsTab: async function () {
this.openTab('symbols')
this.searchInput = L.DomUtil.create('input', '', this.body)
this.searchInput.type = 'search'
@@ -695,13 +695,11 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
if (this.pictogram_list) {
this.buildSymbolsList()
} else {
- this.builder.map.get(this.builder.map.options.urls.pictogram_list_json, {
- callback: (data) => {
- this.pictogram_list = data.pictogram_list
- this.buildSymbolsList()
- },
- context: this,
- })
+ const [{ pictogram_list }, response] = await this.builder.map.server.get(
+ this.builder.map.options.urls.pictogram_list_json
+ )
+ this.pictogram_list = pictogram_list
+ this.buildSymbolsList()
}
},
diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js
index bf0f5f91..f142f249 100644
--- a/umap/static/umap/js/umap.js
+++ b/umap/static/umap/js/umap.js
@@ -98,9 +98,12 @@ L.U.Map.include({
this.urls = new window.umap.URLs(this.options.urls)
this.ui = new L.U.UI(this._container)
- this.xhr = new L.U.Xhr(this.ui)
- this.xhr.on('dataloading', (e) => this.fire('dataloading', e))
- this.xhr.on('dataload', (e) => this.fire('dataload', e))
+ this.server = new window.umap.ServerRequest(this.ui)
+ this.server.on('dataloading', (e) => this.fire('dataloading', e))
+ this.server.on('dataload', (e) => this.fire('dataload', e))
+ this.request = new window.umap.Request(this.ui)
+ this.request.on('dataloading', (e) => this.fire('dataloading', e))
+ this.request.on('dataload', (e) => this.fire('dataload', e))
this.initLoader()
this.name = this.options.name
@@ -1083,7 +1086,7 @@ L.U.Map.include({
return properties
},
- saveSelf: function () {
+ saveSelf: async function () {
const geojson = {
type: 'Feature',
geometry: this.geometry(),
@@ -1093,64 +1096,60 @@ L.U.Map.include({
formData.append('name', this.options.name)
formData.append('center', JSON.stringify(this.geometry()))
formData.append('settings', JSON.stringify(geojson))
- this.post(this.urls.get('map_save', { map_id: this.options.umap_id }), {
- data: formData,
- context: this,
- callback: function (data) {
- let duration = 3000,
- alert = { content: L._('Map has been saved!'), level: 'info' }
- if (!this.options.umap_id) {
- alert.content = L._('Congratulations, your map has been created!')
- this.options.umap_id = data.id
- this.permissions.setOptions(data.permissions)
- this.permissions.commit()
- if (
- data.permissions &&
- data.permissions.anonymous_edit_url &&
- this.options.urls.map_send_edit_link
- ) {
- alert.duration = Infinity
- alert.content =
- L._(
- 'Your map has been created! As you are not logged in, here is your secret link to edit the map, please keep it safe:'
- ) + `
${data.permissions.anonymous_edit_url}`
+ const uri = this.urls.get('map_save', { map_id: this.options.umap_id })
+ const [data, response] = await this.server.post(uri, {}, formData)
+ let duration = 3000,
+ alert = { content: L._('Map has been saved!'), level: 'info' }
+ if (!this.options.umap_id) {
+ alert.content = L._('Congratulations, your map has been created!')
+ this.options.umap_id = data.id
+ this.permissions.setOptions(data.permissions)
+ this.permissions.commit()
+ if (
+ data.permissions &&
+ data.permissions.anonymous_edit_url &&
+ this.options.urls.map_send_edit_link
+ ) {
+ alert.duration = Infinity
+ alert.content =
+ L._(
+ 'Your map has been created! As you are not logged in, here is your secret link to edit the map, please keep it safe:'
+ ) + `
${data.permissions.anonymous_edit_url}`
- alert.actions = [
- {
- label: L._('Send me the link'),
- input: L._('Email'),
- callback: this.sendEditLink,
- callbackContext: this,
- },
- {
- label: L._('Copy link'),
- callback: () => {
- L.Util.copyToClipboard(data.permissions.anonymous_edit_url)
- this.ui.alert({
- content: L._('Secret edit link copied to clipboard!'),
- level: 'info',
- })
- },
- callbackContext: this,
- },
- ]
- }
- } else if (!this.permissions.isDirty) {
- // Do not override local changes to permissions,
- // but update in case some other editors changed them in the meantime.
- this.permissions.setOptions(data.permissions)
- this.permissions.commit()
- }
- // Update URL in case the name has changed.
- if (history && history.pushState)
- history.pushState({}, this.options.name, data.url)
- else window.location = data.url
- alert.content = data.info || alert.content
- this.once('saved', () => this.ui.alert(alert))
- this.ui.closePanel()
- this.permissions.save()
- },
- })
+ alert.actions = [
+ {
+ label: L._('Send me the link'),
+ input: L._('Email'),
+ callback: this.sendEditLink,
+ callbackContext: this,
+ },
+ {
+ label: L._('Copy link'),
+ callback: () => {
+ L.Util.copyToClipboard(data.permissions.anonymous_edit_url)
+ this.ui.alert({
+ content: L._('Secret edit link copied to clipboard!'),
+ level: 'info',
+ })
+ },
+ callbackContext: this,
+ },
+ ]
+ }
+ } else if (!this.permissions.isDirty) {
+ // Do not override local changes to permissions,
+ // but update in case some other editors changed them in the meantime.
+ this.permissions.setOptions(data.permissions)
+ this.permissions.commit()
+ }
+ // Update URL in case the name has changed.
+ if (history && history.pushState)
+ history.pushState({}, this.options.name, data.url)
+ else window.location = data.url
+ alert.content = data.info || alert.content
+ this.once('saved', () => this.ui.alert(alert))
+ this.ui.closePanel()
+ this.permissions.save()
},
save: function () {
@@ -1820,23 +1819,6 @@ L.U.Map.include({
this.loader.onAdd(this)
},
- post: function (url, options) {
- options = options || {}
- options.listener = this
- this.xhr.post(url, options)
- },
-
- get: function (url, options) {
- options = options || {}
- options.listener = this
- this.xhr.get(url, options)
- },
-
- ajax: function (options) {
- options.listener = this
- this.xhr._ajax(options)
- },
-
initContextMenu: function () {
this.contextmenu = new L.U.ContextMenu(this)
this.contextmenu.enable()
diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js
index 7fafaea1..61218122 100644
--- a/umap/static/umap/js/umap.layer.js
+++ b/umap/static/umap/js/umap.layer.js
@@ -670,30 +670,26 @@ L.U.DataLayer = L.Evented.extend({
return this
},
- fetchData: function () {
+ fetchData: async function () {
if (!this.umap_id) return
if (this._loading) return
this._loading = true
- this.map.get(this._dataUrl(), {
- callback: function (geojson, response) {
- this._last_modified = response.getResponseHeader('Last-Modified')
- // FIXME: for now this property is set dynamically from backend
- // And thus it's not in the geojson file in the server
- // So do not let all options to be reset
- // Fix is a proper migration so all datalayers settings are
- // in DB, and we remove it from geojson flat files.
- if (geojson._umap_options) {
- geojson._umap_options.editMode = this.options.editMode
- }
- // In case of maps pre 1.0 still around
- if (geojson._storage) geojson._storage.editMode = this.options.editMode
- this.fromUmapGeoJSON(geojson)
- this.backupOptions()
- this.fire('loaded')
- this._loading = false
- },
- context: this,
- })
+ const [geojson, response] = await this.map.server.get(this._dataUrl())
+ this._last_modified = response.headers['Last-Modified']
+ // FIXME: for now this property is set dynamically from backend
+ // And thus it's not in the geojson file in the server
+ // So do not let all options to be reset
+ // Fix is a proper migration so all datalayers settings are
+ // in DB, and we remove it from geojson flat files.
+ if (geojson._umap_options) {
+ geojson._umap_options.editMode = this.options.editMode
+ }
+ // In case of maps pre 1.0 still around
+ if (geojson._storage) geojson._storage.editMode = this.options.editMode
+ this.fromUmapGeoJSON(geojson)
+ this.backupOptions()
+ this.fire('loaded')
+ this._loading = false
},
fromGeoJSON: function (geojson) {
@@ -1062,13 +1058,10 @@ L.U.DataLayer = L.Evented.extend({
reader.onload = (e) => this.importRaw(e.target.result, type)
},
- importFromUrl: function (url, type) {
- url = this.map.localizeUrl(url)
- this.map.xhr._ajax({
- verb: 'GET',
- uri: url,
- callback: (data) => this.importRaw(data, type),
- })
+ importFromUrl: async function (uri, type) {
+ uri = this.map.localizeUrl(uri)
+ const response = await this.map.request.get(uri)
+ this.importRaw(await response.text(), type)
},
getColor: function () {
@@ -1385,8 +1378,8 @@ L.U.DataLayer = L.Evented.extend({
}
},
- buildVersionsFieldset: function (container) {
- const appendVersion = function (data) {
+ buildVersionsFieldset: async function (container) {
+ const appendVersion = (data) => {
const date = new Date(parseInt(data.at, 10))
const content = `${date.toLocaleString(L.lang)} (${parseInt(data.size) / 1000}Kb)`
const el = L.DomUtil.create('div', 'umap-datalayer-version', versionsContainer)
@@ -1402,34 +1395,24 @@ L.U.DataLayer = L.Evented.extend({
}
const versionsContainer = L.DomUtil.createFieldset(container, L._('Versions'), {
- callback: function () {
- this.map.xhr.get(this.getVersionsUrl(), {
- callback: function (data) {
- for (let i = 0; i < data.versions.length; i++) {
- appendVersion.call(this, data.versions[i])
- }
- },
- context: this,
- })
+ callback: async function () {
+ const [{versions}, response] = await this.map.server.get(this.getVersionsUrl())
+ versions.forEach(appendVersion)
},
context: this,
})
},
- restore: function (version) {
+ restore: async function (version) {
if (!this.map.editEnabled) return
if (!confirm(L._('Are you sure you want to restore this version?'))) return
- this.map.xhr.get(this.getVersionUrl(version), {
- callback: function (geojson) {
- if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat.
- if (geojson._umap_options) this.setOptions(geojson._umap_options)
- this.empty()
- if (this.isRemoteLayer()) this.fetchRemoteData()
- else this.addData(geojson)
- this.isDirty = true
- },
- context: this,
- })
+ const [geojson, response] = await this.map.server.get(this.getVersionUrl(version))
+ if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat.
+ if (geojson._umap_options) this.setOptions(geojson._umap_options)
+ this.empty()
+ if (this.isRemoteLayer()) this.fetchRemoteData()
+ else this.addData(geojson)
+ this.isDirty = true
},
featuresToGeoJSON: function () {
@@ -1548,7 +1531,7 @@ L.U.DataLayer = L.Evented.extend({
return this.isReadOnly() || this.isRemoteLayer()
},
- save: function () {
+ save: async function () {
if (this.isDeleted) return this.saveDelete()
if (!this.isLoaded()) {
return
@@ -1566,44 +1549,35 @@ L.U.DataLayer = L.Evented.extend({
map_id: this.map.options.umap_id,
pk: this.umap_id,
})
- this.map.post(saveUrl, {
- data: formData,
- callback: function (data, response) {
- // Response contains geojson only if save has conflicted and conflicts have
- // been resolved. So we need to reload to get extra data (saved from someone else)
- if (data.geojson) {
- this.clear()
- this.fromGeoJSON(data.geojson)
- delete data.geojson
- }
- this._geojson = geojson
- this._last_modified = response.getResponseHeader('Last-Modified')
- this.setUmapId(data.id)
- this.updateOptions(data)
- this.backupOptions()
- this.connectToMap()
- this._loaded = true
- this.redraw() // Needed for reordering features
- this.isDirty = false
- this.permissions.save()
- },
- context: this,
- headers: this._last_modified
- ? { 'If-Unmodified-Since': this._last_modified }
- : {},
- })
+ const headers = this._last_modified
+ ? { 'If-Unmodified-Since': this._last_modified }
+ : {}
+ const [data, response] = await this.map.server.post(saveUrl, headers, formData)
+ // Response contains geojson only if save has conflicted and conflicts have
+ // been resolved. So we need to reload to get extra data (saved from someone else)
+ if (data.geojson) {
+ this.clear()
+ this.fromGeoJSON(data.geojson)
+ delete data.geojson
+ }
+ this._geojson = geojson
+ this._last_modified = response.headers['Last-Modified']
+ this.setUmapId(data.id)
+ this.updateOptions(data)
+ this.backupOptions()
+ this.connectToMap()
+ this._loaded = true
+ this.redraw() // Needed for reordering features
+ this.isDirty = false
+ this.permissions.save()
},
- saveDelete: function () {
- const callback = function () {
- this.isDirty = false
- this.map.continueSaving()
+ saveDelete: async function () {
+ if (this.umap_id) {
+ await this.map.server.post(this.getDeleteUrl())
}
- if (!this.umap_id) return callback.call(this)
- this.map.xhr.post(this.getDeleteUrl(), {
- callback: callback,
- context: this,
- })
+ this.isDirty = false
+ this.map.continueSaving()
},
getMap: function () {
diff --git a/umap/static/umap/js/umap.permissions.js b/umap/static/umap/js/umap.permissions.js
index 8945e18e..087f0c0c 100644
--- a/umap/static/umap/js/umap.permissions.js
+++ b/umap/static/umap/js/umap.permissions.js
@@ -131,21 +131,17 @@ L.U.MapPermissions = L.Class.extend({
this.map.ui.openPanel({ data: { html: container }, className: 'dark' })
},
- attach: function () {
- this.map.post(this.getAttachUrl(), {
- callback: function () {
- this.options.owner = this.map.options.user
- this.map.ui.alert({
- content: L._('Map has been attached to your account'),
- level: 'info',
- })
- this.map.ui.closePanel()
- },
- context: this,
+ attach: async function () {
+ await this.map.server.post(this.getAttachUrl())
+ this.options.owner = this.map.options.user
+ this.map.ui.alert({
+ content: L._('Map has been attached to your account'),
+ level: 'info',
})
+ this.map.ui.closePanel()
},
- save: function () {
+ save: async function () {
if (!this.isDirty) return this.map.continueSaving()
const formData = new FormData()
if (!this.isAnonymousMap() && this.options.editors) {
@@ -159,16 +155,11 @@ L.U.MapPermissions = L.Class.extend({
formData.append('owner', this.options.owner && this.options.owner.id)
formData.append('share_status', this.options.share_status)
}
- this.map.post(this.getUrl(), {
- data: formData,
- context: this,
- callback: function (data) {
- this.commit()
- this.isDirty = false
- this.map.continueSaving()
- this.map.fire('postsync')
- },
- })
+ await this.map.server.post(this.getUrl(), {}, formData)
+ this.commit()
+ this.isDirty = false
+ this.map.continueSaving()
+ this.map.fire('postsync')
},
getUrl: function () {