mirror of
https://github.com/umap-project/umap.git
synced 2025-05-05 22:11:50 +02:00
Compare commits
22 commits
ce876bc69b
...
159d2c0b77
Author | SHA1 | Date | |
---|---|---|---|
![]() |
159d2c0b77 | ||
![]() |
3b6ff0c57c | ||
![]() |
2a460f03b2 | ||
![]() |
bbde111fdf | ||
![]() |
9c4287ac1e | ||
![]() |
e9f2ff9a6c | ||
![]() |
12e456d24e | ||
![]() |
e004cd461d | ||
![]() |
6bea9339b6 | ||
![]() |
90ea3737f2 | ||
![]() |
a7b750740c | ||
![]() |
101b036a66 | ||
![]() |
983f7f8cb1 | ||
![]() |
9718f11faf | ||
![]() |
88382ab00b | ||
![]() |
0b84084c6b | ||
![]() |
8b2454936b | ||
![]() |
98f2f8df65 | ||
757cb375d1 | |||
![]() |
4ef1411102 | ||
![]() |
01b2053030 | ||
![]() |
64c7fe1ec9 |
20 changed files with 189 additions and 250 deletions
|
@ -18,7 +18,6 @@ import { translate } from '../i18n.js'
|
|||
import { DataLayerPermissions } from '../permissions.js'
|
||||
import { Point, LineString, Polygon } from './features.js'
|
||||
import TableEditor from '../tableeditor.js'
|
||||
import { ServerStored } from '../saving.js'
|
||||
import * as Schema from '../schema.js'
|
||||
import { MutatingForm } from '../form/builder.js'
|
||||
|
||||
|
@ -36,9 +35,8 @@ const LAYER_MAP = LAYER_TYPES.reduce((acc, klass) => {
|
|||
return acc
|
||||
}, {})
|
||||
|
||||
export class DataLayer extends ServerStored {
|
||||
export class DataLayer {
|
||||
constructor(umap, leafletMap, data = {}) {
|
||||
super()
|
||||
this._umap = umap
|
||||
this.sync = umap.syncEngine.proxy(this)
|
||||
this._index = Array()
|
||||
|
@ -114,7 +112,6 @@ export class DataLayer extends ServerStored {
|
|||
|
||||
set isDeleted(status) {
|
||||
this._isDeleted = status
|
||||
if (status) this.isDirty = status
|
||||
}
|
||||
|
||||
get isDeleted() {
|
||||
|
@ -530,10 +527,6 @@ export class DataLayer extends ServerStored {
|
|||
return this._umap.formatter
|
||||
.parse(raw, format)
|
||||
.then((geojson) => this.addData(geojson))
|
||||
.then((data) => {
|
||||
if (data?.length) this.isDirty = true
|
||||
return data
|
||||
})
|
||||
.catch((error) => {
|
||||
console.debug(error)
|
||||
Alert.error(translate('Import failed: invalid data'))
|
||||
|
@ -610,7 +603,6 @@ export class DataLayer extends ServerStored {
|
|||
empty() {
|
||||
if (this.isRemoteLayer()) return
|
||||
this.clear()
|
||||
this.isDirty = true
|
||||
}
|
||||
|
||||
clone() {
|
||||
|
@ -634,25 +626,6 @@ export class DataLayer extends ServerStored {
|
|||
this.clear()
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (!this.createdOnServer) {
|
||||
this.erase()
|
||||
return
|
||||
}
|
||||
|
||||
this.resetOptions()
|
||||
this.parentPane.appendChild(this.pane)
|
||||
if (this._leaflet_events_bk && !this._leaflet_events) {
|
||||
this._leaflet_events = this._leaflet_events_bk
|
||||
}
|
||||
this.clear()
|
||||
this.hide()
|
||||
if (this.isRemoteLayer()) this.fetchRemoteData()
|
||||
else if (this._geojson_bk) this.fromGeoJSON(this._geojson_bk)
|
||||
this.show()
|
||||
this.isDirty = false
|
||||
}
|
||||
|
||||
redraw() {
|
||||
if (!this.isVisible()) return
|
||||
this.eachFeature((feature) => feature.redraw())
|
||||
|
@ -835,8 +808,9 @@ export class DataLayer extends ServerStored {
|
|||
this
|
||||
)
|
||||
|
||||
if (this._umap.properties.urls.datalayer_versions)
|
||||
if (this._umap.properties.urls.datalayer_versions) {
|
||||
this.buildVersionsFieldset(container)
|
||||
}
|
||||
|
||||
const advancedActions = DomUtil.createFieldset(
|
||||
container,
|
||||
|
@ -911,10 +885,15 @@ export class DataLayer extends ServerStored {
|
|||
const appendVersion = (data) => {
|
||||
const date = new Date(Number.parseInt(data.at, 10))
|
||||
const content = `${date.toLocaleString(U.lang)} (${Number.parseInt(data.size) / 1000}Kb)`
|
||||
const el = DomUtil.create('div', 'umap-datalayer-version', versionsContainer)
|
||||
const button = DomUtil.createButton('', el, '', () => this.restore(data.ref))
|
||||
button.title = translate('Restore this version')
|
||||
DomUtil.add('span', '', el, content)
|
||||
const [el, { button }] = Utils.loadTemplateWithRefs(
|
||||
`<div class="umap-datalayer-version">
|
||||
<button type="button" title="${translate('Restore this version')}" data-ref=button>
|
||||
<i class="icon icon-16 icon-restore"></i> ${content}
|
||||
</button>
|
||||
</div>`
|
||||
)
|
||||
versionsContainer.appendChild(el)
|
||||
button.addEventListener('click', () => this.restore(data.ref))
|
||||
}
|
||||
|
||||
const versionsContainer = DomUtil.createFieldset(container, translate('Versions'), {
|
||||
|
@ -938,11 +917,14 @@ export class DataLayer extends ServerStored {
|
|||
)
|
||||
if (!error) {
|
||||
if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat.
|
||||
if (geojson._umap_options) this.setOptions(geojson._umap_options)
|
||||
if (geojson._umap_options) {
|
||||
const oldOptions = Utils.CopyJSON(this.options)
|
||||
this.setOptions(geojson._umap_options)
|
||||
this.sync.update('options', this.options, oldOptions)
|
||||
}
|
||||
this.empty()
|
||||
if (this.isRemoteLayer()) this.fetchRemoteData()
|
||||
else this.addData(geojson)
|
||||
this.isDirty = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1123,6 +1105,10 @@ export class DataLayer extends ServerStored {
|
|||
}
|
||||
|
||||
async _trySave(url, headers, formData) {
|
||||
if (this._forceSave) {
|
||||
headers = {}
|
||||
this._forceSave = false
|
||||
}
|
||||
const [data, response, error] = await this._umap.server.post(url, headers, formData)
|
||||
if (error) {
|
||||
if (response && response.status === 412) {
|
||||
|
@ -1132,15 +1118,8 @@ export class DataLayer extends ServerStored {
|
|||
'This situation is tricky, you have to choose carefully which version is pertinent.'
|
||||
),
|
||||
async () => {
|
||||
// Save again this layer
|
||||
const status = await this._trySave(url, {}, formData)
|
||||
if (status) {
|
||||
this.isDirty = false
|
||||
|
||||
// Call the main save, in case something else needs to be saved
|
||||
// as the conflict stopped the saving flow
|
||||
await this._umap.saveAll()
|
||||
}
|
||||
this._forceSave = true
|
||||
await this._umap.saveAll()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -135,7 +135,13 @@ export default class Facets {
|
|||
for (const [property, { label, type }] of parsed) {
|
||||
dumped.push([property, label, type].filter(Boolean).join('|'))
|
||||
}
|
||||
return dumped.join(',')
|
||||
const oldValue = this._umap.properties.facetKey
|
||||
this._umap.properties.facetKey = dumped.join(',')
|
||||
this._umap.sync.update(
|
||||
'properties.facetKey',
|
||||
this._umap.properties.facetKey,
|
||||
oldValue
|
||||
)
|
||||
}
|
||||
|
||||
has(property) {
|
||||
|
@ -146,15 +152,13 @@ export default class Facets {
|
|||
const defined = this.getDefined()
|
||||
if (!defined.has(property)) {
|
||||
defined.set(property, { label, type })
|
||||
this._umap.properties.facetKey = this.dumps(defined)
|
||||
this._umap.isDirty = true
|
||||
this.dumps(defined)
|
||||
}
|
||||
}
|
||||
|
||||
remove(property) {
|
||||
const defined = this.getDefined()
|
||||
defined.delete(property)
|
||||
this._umap.properties.facetKey = this.dumps(defined)
|
||||
this._umap.isDirty = true
|
||||
this.dumps(defined)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,21 +70,7 @@ export class Form extends Utils.WithEvents {
|
|||
}
|
||||
|
||||
setter(field, value) {
|
||||
const path = field.split('.')
|
||||
let obj = this.obj
|
||||
let what
|
||||
for (let i = 0, l = path.length; i < l; i++) {
|
||||
what = path[i]
|
||||
if (what === path[l - 1]) {
|
||||
if (typeof value === 'undefined') {
|
||||
delete obj[what]
|
||||
} else {
|
||||
obj[what] = value
|
||||
}
|
||||
} else {
|
||||
obj = obj[what]
|
||||
}
|
||||
}
|
||||
Utils.setObjectValue(this.obj, field, value)
|
||||
}
|
||||
|
||||
restoreField(field) {
|
||||
|
@ -191,7 +177,11 @@ export class MutatingForm extends Form {
|
|||
|
||||
setter(field, value) {
|
||||
const oldValue = this.getter(field)
|
||||
super.setter(field, value)
|
||||
if ('setter' in this.obj) {
|
||||
this.obj.setter(field, value)
|
||||
} else {
|
||||
super.setter(field, value)
|
||||
}
|
||||
if ('render' in this.obj) {
|
||||
this.obj.render([field], this)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
|
||||
import { translate } from './i18n.js'
|
||||
import { uMapAlert as Alert } from '../components/alerts/alert.js'
|
||||
import { ServerStored } from './saving.js'
|
||||
import * as Utils from './utils.js'
|
||||
import { MutatingForm } from './form/builder.js'
|
||||
|
||||
// Dedicated object so we can deal with a separate dirty status, and thus
|
||||
// call the endpoint only when needed, saving one call at each save.
|
||||
export class MapPermissions extends ServerStored {
|
||||
export class MapPermissions {
|
||||
constructor(umap) {
|
||||
super()
|
||||
this.setProperties(umap.properties.permissions)
|
||||
this._umap = umap
|
||||
this._isDirty = false
|
||||
this.sync = umap.syncEngine.proxy(this)
|
||||
}
|
||||
|
||||
|
@ -196,7 +193,6 @@ export class MapPermissions extends ServerStored {
|
|||
}
|
||||
|
||||
async save() {
|
||||
if (!this.isDirty) return
|
||||
const formData = new FormData()
|
||||
if (!this.isAnonymousMap() && this.properties.editors) {
|
||||
const editors = this.properties.editors.map((u) => u.id)
|
||||
|
@ -255,9 +251,8 @@ export class MapPermissions extends ServerStored {
|
|||
}
|
||||
}
|
||||
|
||||
export class DataLayerPermissions extends ServerStored {
|
||||
export class DataLayerPermissions {
|
||||
constructor(umap, datalayer) {
|
||||
super()
|
||||
this._umap = umap
|
||||
this.properties = Object.assign(
|
||||
{
|
||||
|
@ -305,7 +300,6 @@ export class DataLayerPermissions extends ServerStored {
|
|||
}
|
||||
|
||||
async save() {
|
||||
if (!this.isDirty) return
|
||||
const formData = new FormData()
|
||||
formData.append('edit_status', this.properties.edit_status)
|
||||
const [data, response, error] = await this._umap.server.post(
|
||||
|
|
|
@ -117,7 +117,7 @@ export const Choropleth = FeatureGroup.extend({
|
|||
},
|
||||
|
||||
_getValue: function (feature) {
|
||||
const key = this.datalayer.options.choropleth.property || 'value'
|
||||
const key = this.datalayer.options.choropleth?.property || 'value'
|
||||
const value = +feature.properties[key]
|
||||
if (!Number.isNaN(value)) return value
|
||||
},
|
||||
|
@ -130,12 +130,12 @@ export const Choropleth = FeatureGroup.extend({
|
|||
this.options.colors = []
|
||||
return
|
||||
}
|
||||
const mode = this.datalayer.options.choropleth.mode
|
||||
let classes = +this.datalayer.options.choropleth.classes || 5
|
||||
const mode = this.datalayer.options.choropleth?.mode
|
||||
let classes = +this.datalayer.options.choropleth?.classes || 5
|
||||
let breaks
|
||||
classes = Math.min(classes, values.length)
|
||||
if (mode === 'manual') {
|
||||
const manualBreaks = this.datalayer.options.choropleth.breaks
|
||||
const manualBreaks = this.datalayer.options.choropleth?.breaks
|
||||
if (manualBreaks) {
|
||||
breaks = manualBreaks
|
||||
.split(',')
|
||||
|
|
|
@ -17,20 +17,10 @@ class Rule {
|
|||
this.parse()
|
||||
}
|
||||
|
||||
get isDirty() {
|
||||
return this._isDirty
|
||||
}
|
||||
|
||||
set isDirty(status) {
|
||||
this._isDirty = status
|
||||
if (status) this._umap.isDirty = status
|
||||
}
|
||||
|
||||
constructor(umap, condition = '', options = {}) {
|
||||
// TODO make this public properties when browser coverage is ok
|
||||
// cf https://caniuse.com/?search=public%20class%20field
|
||||
this._condition = null
|
||||
this._isDirty = false
|
||||
this.OPERATORS = [
|
||||
['>', this.gt],
|
||||
['<', this.lt],
|
||||
|
@ -190,17 +180,25 @@ class Rule {
|
|||
|
||||
_delete() {
|
||||
this._umap.rules.rules = this._umap.rules.rules.filter((rule) => rule !== this)
|
||||
this._umap.rules.commit()
|
||||
}
|
||||
|
||||
setter(key, value) {
|
||||
const oldRules = Utils.CopyJSON(this._umap.properties.rules || {})
|
||||
Utils.setObjectValue(this, key, value)
|
||||
this._umap.rules.commit()
|
||||
this._umap.sync.update('properties.rules', this._umap.properties.rules, oldRules)
|
||||
}
|
||||
}
|
||||
|
||||
export default class Rules {
|
||||
constructor(umap) {
|
||||
this._umap = umap
|
||||
this.rules = []
|
||||
this.load()
|
||||
}
|
||||
|
||||
load() {
|
||||
this.rules = []
|
||||
if (!this._umap.properties.rules?.length) return
|
||||
for (const { condition, options } of this._umap.properties.rules) {
|
||||
if (!condition) continue
|
||||
|
@ -222,8 +220,8 @@ export default class Rules {
|
|||
else if (finalIndex > initialIndex) newIdx = referenceIdx
|
||||
else newIdx = referenceIdx + 1
|
||||
this.rules.splice(newIdx, 0, moved)
|
||||
moved.isDirty = true
|
||||
this._umap.render(['rules'])
|
||||
this.commit()
|
||||
}
|
||||
|
||||
edit(container) {
|
||||
|
@ -242,7 +240,6 @@ export default class Rules {
|
|||
|
||||
addRule() {
|
||||
const rule = new Rule(this._umap)
|
||||
rule.isDirty = true
|
||||
this.rules.push(rule)
|
||||
rule.edit(map)
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
const _queue = new Set()
|
||||
|
||||
export let isDirty = false
|
||||
|
||||
export async function save() {
|
||||
for (const obj of _queue) {
|
||||
const ok = await obj.save()
|
||||
if (!ok) break
|
||||
remove(obj)
|
||||
}
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
_queue.clear()
|
||||
onUpdate()
|
||||
}
|
||||
|
||||
function add(obj) {
|
||||
_queue.add(obj)
|
||||
onUpdate()
|
||||
}
|
||||
|
||||
function remove(obj) {
|
||||
_queue.delete(obj)
|
||||
onUpdate()
|
||||
}
|
||||
|
||||
function has(obj) {
|
||||
return _queue.has(obj)
|
||||
}
|
||||
|
||||
function onUpdate() {
|
||||
isDirty = Boolean(_queue.size)
|
||||
// document.body.classList.toggle('umap-is-dirty', isDirty)
|
||||
}
|
||||
|
||||
export class ServerStored {
|
||||
set isDirty(status) {
|
||||
if (status) {
|
||||
add(this)
|
||||
} else {
|
||||
remove(this)
|
||||
}
|
||||
this.onDirty(status)
|
||||
}
|
||||
|
||||
get isDirty() {
|
||||
return has(this)
|
||||
}
|
||||
|
||||
onDirty(status) {}
|
||||
}
|
|
@ -9,7 +9,6 @@ import {
|
|||
} from './updaters.js'
|
||||
import { WebSocketTransport } from './websocket.js'
|
||||
import { UndoManager } from './undo.js'
|
||||
import * as SaveManager from '../saving.js'
|
||||
|
||||
// Start reconnecting after 2 seconds, then double the delay each time
|
||||
// maxing out at 32 seconds.
|
||||
|
@ -73,7 +72,7 @@ export class SyncEngine {
|
|||
this.websocketConnected = false
|
||||
this.closeRequested = false
|
||||
this.peerId = Utils.generateId()
|
||||
this._undoManager = new UndoManager(this.updaters, this)
|
||||
this._undoManager = new UndoManager(umap, this.updaters, this)
|
||||
}
|
||||
|
||||
get isOpen() {
|
||||
|
|
|
@ -2,7 +2,8 @@ import * as Utils from '../utils.js'
|
|||
import { DataLayerUpdater, FeatureUpdater, MapUpdater } from './updaters.js'
|
||||
|
||||
export class UndoManager {
|
||||
constructor(updaters, syncEngine) {
|
||||
constructor(umap, updaters, syncEngine) {
|
||||
this._umap = umap
|
||||
this._syncEngine = syncEngine
|
||||
this.updaters = updaters
|
||||
this._undoStack = []
|
||||
|
@ -10,6 +11,8 @@ export class UndoManager {
|
|||
}
|
||||
|
||||
toggleState() {
|
||||
// document is undefined during unittests
|
||||
if (typeof document === 'undefined') return
|
||||
const undoButton = document.querySelector('.edit-undo')
|
||||
const redoButton = document.querySelector('.edit-redo')
|
||||
if (undoButton) undoButton.disabled = !this._undoStack.length
|
||||
|
@ -25,6 +28,7 @@ export class UndoManager {
|
|||
}
|
||||
|
||||
isDirty() {
|
||||
if (!this._umap.id) return true
|
||||
for (const stage of this._undoStack) {
|
||||
if (stage.operation.dirty) return true
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { fieldInSchema } from '../utils.js'
|
||||
import * as Utils from '../utils.js'
|
||||
|
||||
/**
|
||||
* Updaters are classes able to convert messages
|
||||
|
@ -10,27 +10,6 @@ class BaseUpdater {
|
|||
this._umap = umap
|
||||
}
|
||||
|
||||
updateObjectValue(obj, key, value) {
|
||||
const parts = key.split('.')
|
||||
const lastKey = parts.pop()
|
||||
|
||||
// Reduce the current list of attributes,
|
||||
// to find the object to set the property onto
|
||||
const objectToSet = parts.reduce((currentObj, part) => {
|
||||
if (currentObj !== undefined && part in currentObj) return currentObj[part]
|
||||
}, obj)
|
||||
|
||||
// In case the given path doesn't exist, stop here
|
||||
if (objectToSet === undefined) return
|
||||
|
||||
// Set the value (or delete it)
|
||||
if (typeof value === 'undefined') {
|
||||
delete objectToSet[lastKey]
|
||||
} else {
|
||||
objectToSet[lastKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
getDataLayerFromID(layerId) {
|
||||
return this._umap.getDataLayerByUmapId(layerId)
|
||||
}
|
||||
|
@ -43,8 +22,8 @@ class BaseUpdater {
|
|||
|
||||
export class MapUpdater extends BaseUpdater {
|
||||
update({ key, value }) {
|
||||
if (fieldInSchema(key)) {
|
||||
this.updateObjectValue(this._umap, key, value)
|
||||
if (Utils.fieldInSchema(key)) {
|
||||
Utils.setObjectValue(this._umap, key, value)
|
||||
}
|
||||
|
||||
this._umap.onPropertiesUpdated([key])
|
||||
|
@ -73,8 +52,10 @@ export class DataLayerUpdater extends BaseUpdater {
|
|||
|
||||
update({ key, metadata, value }) {
|
||||
const datalayer = this.getDataLayerFromID(metadata.id)
|
||||
if (fieldInSchema(key)) {
|
||||
this.updateObjectValue(datalayer, key, value)
|
||||
if (key === 'options') {
|
||||
datalayer.setOptions(value)
|
||||
} else if (Utils.fieldInSchema(key)) {
|
||||
Utils.setObjectValue(datalayer, key, value)
|
||||
} else {
|
||||
console.debug(
|
||||
'Not applying update for datalayer because key is not in the schema',
|
||||
|
@ -127,7 +108,7 @@ export class FeatureUpdater extends BaseUpdater {
|
|||
const feature = this.getFeatureFromMetadata(metadata)
|
||||
feature.geometry = value
|
||||
} else {
|
||||
this.updateObjectValue(feature, key, value)
|
||||
Utils.setObjectValue(feature, key, value)
|
||||
feature.datalayer.indexProperties(feature)
|
||||
}
|
||||
|
||||
|
@ -148,8 +129,8 @@ export class FeatureUpdater extends BaseUpdater {
|
|||
|
||||
export class MapPermissionsUpdater extends BaseUpdater {
|
||||
update({ key, value }) {
|
||||
if (fieldInSchema(key)) {
|
||||
this.updateObjectValue(this._umap.permissions, key, value)
|
||||
if (Utils.fieldInSchema(key)) {
|
||||
Utils.setObjectValue(this._umap.permissions, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,8 +141,8 @@ export class MapPermissionsUpdater extends BaseUpdater {
|
|||
|
||||
export class DataLayerPermissionsUpdater extends BaseUpdater {
|
||||
update({ key, value, metadata }) {
|
||||
if (fieldInSchema(key)) {
|
||||
this.updateObjectValue(this.getDataLayerFromID(metadata.id), key, value)
|
||||
if (Utils.fieldInSchema(key)) {
|
||||
Utils.setObjectValue(this.getDataLayerFromID(metadata.id), key, value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -162,6 +162,7 @@ export class TopBar extends WithTemplate {
|
|||
this.elements.view.disabled = this._umap.sync._undoManager.isDirty()
|
||||
this.elements.saveLabel.hidden = this._umap.permissions.isDraft()
|
||||
this.elements.saveDraftLabel.hidden = !this._umap.permissions.isDraft()
|
||||
this._umap.sync._undoManager.toggleState()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1159,7 +1159,6 @@ export default class Umap {
|
|||
}
|
||||
|
||||
async save() {
|
||||
this.rules.commit()
|
||||
const geojson = {
|
||||
type: 'Feature',
|
||||
geometry: this.geometry(),
|
||||
|
@ -1320,6 +1319,9 @@ export default class Umap {
|
|||
this.bottomBar.redraw()
|
||||
break
|
||||
case 'data':
|
||||
if (fields.includes('properties.rules')) {
|
||||
this.rules.load()
|
||||
}
|
||||
this.eachVisibleDataLayer((datalayer) => {
|
||||
datalayer.redraw()
|
||||
})
|
||||
|
|
|
@ -481,6 +481,27 @@ export const debounce = (callback, wait) => {
|
|||
}
|
||||
}
|
||||
|
||||
export function setObjectValue(obj, key, value) {
|
||||
const parts = key.split('.')
|
||||
const lastKey = parts.pop()
|
||||
|
||||
// Reduce the current list of attributes,
|
||||
// to find the object to set the property onto
|
||||
const objectToSet = parts.reduce((currentObj, part) => {
|
||||
if (currentObj !== undefined && part in currentObj) return currentObj[part]
|
||||
}, obj)
|
||||
|
||||
// In case the given path doesn't exist, stop here
|
||||
if (objectToSet === undefined) return
|
||||
|
||||
// Set the value (or delete it)
|
||||
if (typeof value === 'undefined') {
|
||||
delete objectToSet[lastKey]
|
||||
} else {
|
||||
objectToSet[lastKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
export const COLORS = [
|
||||
'Black',
|
||||
'Navy',
|
||||
|
|
|
@ -475,22 +475,6 @@ ul.photon-autocomplete {
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.umap-datalayer-version {
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #202425;
|
||||
}
|
||||
.umap-datalayer-version button {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
min-height: 24px;
|
||||
background-position: -122px -73px;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url('./img/16-white.svg');
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
|
||||
.leaflet-toolbar-tip {
|
||||
background-color: var(--color-darkGray);
|
||||
}
|
||||
|
|
|
@ -49,63 +49,6 @@ describe('#dispatch', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Updaters', () => {
|
||||
describe('BaseUpdater', () => {
|
||||
let updater
|
||||
let map
|
||||
let obj
|
||||
|
||||
beforeEach(() => {
|
||||
map = {}
|
||||
updater = new MapUpdater(map)
|
||||
obj = {}
|
||||
})
|
||||
it('should be able to set object properties', () => {
|
||||
let obj = {}
|
||||
updater.updateObjectValue(obj, 'foo', 'foo')
|
||||
expect(obj).deep.equal({ foo: 'foo' })
|
||||
})
|
||||
|
||||
it('should be able to set object properties recursively on existing objects', () => {
|
||||
let obj = { foo: {} }
|
||||
updater.updateObjectValue(obj, 'foo.bar', 'foo')
|
||||
expect(obj).deep.equal({ foo: { bar: 'foo' } })
|
||||
})
|
||||
|
||||
it('should be able to set object properties recursively on deep objects', () => {
|
||||
let obj = { foo: { bar: { baz: {} } } }
|
||||
updater.updateObjectValue(obj, 'foo.bar.baz.test', 'value')
|
||||
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
|
||||
})
|
||||
|
||||
it('should be able to replace object properties recursively on deep objects', () => {
|
||||
let obj = { foo: { bar: { baz: { test: 'test' } } } }
|
||||
updater.updateObjectValue(obj, 'foo.bar.baz.test', 'value')
|
||||
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
|
||||
})
|
||||
|
||||
it('should not set object properties recursively on non-existing objects', () => {
|
||||
let obj = { foo: {} }
|
||||
updater.updateObjectValue(obj, 'bar.bar', 'value')
|
||||
|
||||
expect(obj).deep.equal({ foo: {} })
|
||||
})
|
||||
|
||||
it('should delete keys for undefined values', () => {
|
||||
let obj = { foo: 'foo' }
|
||||
updater.updateObjectValue(obj, 'foo', undefined)
|
||||
|
||||
expect(obj).deep.equal({})
|
||||
})
|
||||
|
||||
it('should delete keys for undefined values, recursively', () => {
|
||||
let obj = { foo: { bar: 'bar' } }
|
||||
updater.updateObjectValue(obj, 'foo.bar', undefined)
|
||||
|
||||
expect(obj).deep.equal({ foo: {} })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Operations', () => {
|
||||
describe('haveSameContext', () => {
|
||||
|
|
|
@ -862,4 +862,51 @@ describe('Utils', () => {
|
|||
assert.equal(Utils.isObject(''), false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setObjectValue', () => {
|
||||
it('should be able to set object properties', () => {
|
||||
let obj = {}
|
||||
Utils.setObjectValue(obj, 'foo', 'foo')
|
||||
expect(obj).deep.equal({ foo: 'foo' })
|
||||
})
|
||||
|
||||
it('should be able to set object properties recursively on existing objects', () => {
|
||||
let obj = { foo: {} }
|
||||
Utils.setObjectValue(obj, 'foo.bar', 'foo')
|
||||
expect(obj).deep.equal({ foo: { bar: 'foo' } })
|
||||
})
|
||||
|
||||
it('should be able to set object properties recursively on deep objects', () => {
|
||||
let obj = { foo: { bar: { baz: {} } } }
|
||||
Utils.setObjectValue(obj, 'foo.bar.baz.test', 'value')
|
||||
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
|
||||
})
|
||||
|
||||
it('should be able to replace object properties recursively on deep objects', () => {
|
||||
let obj = { foo: { bar: { baz: { test: 'test' } } } }
|
||||
Utils.setObjectValue(obj, 'foo.bar.baz.test', 'value')
|
||||
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
|
||||
})
|
||||
|
||||
it('should not set object properties recursively on non-existing objects', () => {
|
||||
let obj = { foo: {} }
|
||||
Utils.setObjectValue(obj, 'bar.bar', 'value')
|
||||
|
||||
expect(obj).deep.equal({ foo: {} })
|
||||
})
|
||||
|
||||
it('should delete keys for undefined values', () => {
|
||||
let obj = { foo: 'foo' }
|
||||
Utils.setObjectValue(obj, 'foo', undefined)
|
||||
|
||||
expect(obj).deep.equal({})
|
||||
})
|
||||
|
||||
it('should delete keys for undefined values, recursively', () => {
|
||||
let obj = { foo: { bar: 'bar' } }
|
||||
Utils.setObjectValue(obj, 'foo.bar', undefined)
|
||||
|
||||
expect(obj).deep.equal({ foo: {} })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -261,6 +261,9 @@ def test_can_create_new_rule(live_server, page, openmap):
|
|||
page.get_by_title("AliceBlue").first.click()
|
||||
colors = getColors(markers)
|
||||
assert colors.count("rgb(240, 248, 255)") == 3
|
||||
page.get_by_role("button", name="Undo").click()
|
||||
colors = getColors(markers)
|
||||
assert colors.count("rgb(240, 248, 255)") == 0
|
||||
|
||||
|
||||
def test_can_deactive_rule_from_list(live_server, page, openmap):
|
||||
|
|
|
@ -689,3 +689,43 @@ def test_should_sync_datalayer_clear(
|
|||
peerA.get_by_role("button", name="Undo").click()
|
||||
expect(peerA.locator(".leaflet-marker-icon")).to_have_count(1)
|
||||
expect(peerB.locator(".leaflet-marker-icon")).to_have_count(1)
|
||||
|
||||
|
||||
@pytest.mark.xdist_group(name="websockets")
|
||||
def test_should_save_remote_dirty_datalayers(new_page, asgi_live_server, tilelayer):
|
||||
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
|
||||
map.settings["properties"]["syncEnabled"] = True
|
||||
map.save()
|
||||
|
||||
assert not DataLayer.objects.count()
|
||||
|
||||
# Create two tabs
|
||||
peerA = new_page("Page A")
|
||||
peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
||||
peerB = new_page("Page B")
|
||||
peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
||||
|
||||
# Create a new layer from peerA
|
||||
peerA.get_by_role("button", name="Manage layers").click()
|
||||
peerA.get_by_role("button", name="Add a layer").click()
|
||||
|
||||
# Create a new layer from peerB
|
||||
peerB.get_by_role("button", name="Manage layers").click()
|
||||
peerB.get_by_role("button", name="Add a layer").click()
|
||||
|
||||
# Save from peerA to the server
|
||||
counter = 0
|
||||
|
||||
def on_response(response):
|
||||
nonlocal counter
|
||||
if "/datalayer/create/" in response.url:
|
||||
counter += 1
|
||||
# Wait for the two datalayer saves
|
||||
if counter == 2:
|
||||
return True
|
||||
return False
|
||||
|
||||
with peerA.expect_response(on_response):
|
||||
peerA.get_by_role("button", name="Save").click()
|
||||
|
||||
assert DataLayer.objects.count() == 2
|
||||
|
|
|
@ -103,6 +103,7 @@ def test_get_version(map, datalayer):
|
|||
],
|
||||
"type": "Point",
|
||||
},
|
||||
"id": "ExNTQ",
|
||||
"properties": {
|
||||
"_umap_options": {
|
||||
"color": "DarkCyan",
|
||||
|
|
|
@ -694,6 +694,7 @@ def test_download(client, map, datalayer):
|
|||
"coordinates": [14.68896484375, 48.55297816440071],
|
||||
"type": "Point",
|
||||
},
|
||||
"id": "ExNTQ",
|
||||
"properties": {
|
||||
"_umap_options": {"color": "DarkCyan", "iconClass": "Ball"},
|
||||
"description": "Da place anonymous again 755",
|
||||
|
|
Loading…
Reference in a new issue