From 093ed061c15e6f3d02585c5468a29fcc432f9197 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 21 Mar 2025 16:17:31 +0100 Subject: [PATCH] wip: tests pass Co-authored-by: David Larlet --- umap/static/umap/js/modules/data/features.js | 2 - umap/static/umap/js/modules/data/layer.js | 16 ++++---- umap/static/umap/js/modules/form/fields.js | 4 +- umap/static/umap/js/modules/importer.js | 2 +- umap/static/umap/js/modules/permissions.js | 16 ++++++++ umap/static/umap/js/modules/sync/engine.js | 39 ++++++++++++++----- umap/static/umap/js/modules/sync/undo.js | 9 +++-- umap/static/umap/js/modules/sync/updaters.js | 34 ++++++++++++---- umap/static/umap/js/modules/ui/bar.js | 7 +--- umap/static/umap/js/modules/umap.js | 15 ++++--- umap/static/umap/js/umap.controls.js | 1 - umap/sync/payloads.py | 2 +- umap/tests/integration/test_edit_datalayer.py | 1 - .../integration/test_optimistic_merge.py | 7 ++-- umap/tests/integration/test_save.py | 3 +- umap/tests/integration/test_undo_redo.py | 4 +- 16 files changed, 104 insertions(+), 58 deletions(-) diff --git a/umap/static/umap/js/modules/data/features.js b/umap/static/umap/js/modules/data/features.js index 8278d498..cd04dba3 100644 --- a/umap/static/umap/js/modules/data/features.js +++ b/umap/static/umap/js/modules/data/features.js @@ -108,13 +108,11 @@ class Feature { const oldGeometry = Utils.CopyJSON(this._geometry) this.fromLatLngs(this._getLatLngs()) if (sync) { - console.log('sync geometry') this.sync.update('geometry', this.geometry, oldGeometry) } } fromLatLngs(latlngs) { - console.log('fromLatLngs', latlngs) this._geometry_bk = Utils.CopyJSON(this._geometry) this._geometry = this.convertLatLngs(latlngs) } diff --git a/umap/static/umap/js/modules/data/layer.js b/umap/static/umap/js/modules/data/layer.js index 26a767ef..793e5c78 100644 --- a/umap/static/umap/js/modules/data/layer.js +++ b/umap/static/umap/js/modules/data/layer.js @@ -49,7 +49,6 @@ export class DataLayer extends ServerStored { this._leafletMap = leafletMap this.parentPane = this._leafletMap.getPane('overlayPane') this.pane = this._leafletMap.createPane(`datalayer${stamp(this)}`, this.parentPane) - this.pane.dataset.id = stamp(this) // FIXME: should be on layer this.renderer = L.svg({ pane: this.pane }) this.defaultOptions = { @@ -66,6 +65,7 @@ export class DataLayer extends ServerStored { data.id = data.id || crypto.randomUUID() this.setOptions(data) + this.pane.dataset.id = this.id if (!Utils.isObject(this.options.remoteData)) { this.options.remoteData = {} @@ -366,9 +366,8 @@ export class DataLayer extends ServerStored { } connectToMap() { - const id = stamp(this) - if (!this._umap.datalayers[id]) { - this._umap.datalayers[id] = this + if (!this._umap.datalayers[this.id]) { + this._umap.datalayers[this.id] = this } if (!this._umap.datalayersIndex.includes(this)) { this._umap.datalayersIndex.push(this) @@ -419,7 +418,6 @@ export class DataLayer extends ServerStored { const id = stamp(feature) if (sync !== false) { const oldValue = feature.toGeoJSON() - console.log('oldValue in removeFeature', oldValue) feature.sync.delete(oldValue) } this.hideFeature(feature) @@ -460,13 +458,13 @@ export class DataLayer extends ServerStored { .sort(Utils.naturalSort) } - addData(geojson, sync) { + addData(geojson, sync, batch = true) { try { // Do not fail if remote data is somehow invalid, // otherwise the layer becomes uneditable. - this.sync.startBatch() + if (batch) this.sync.startBatch() const features = this.makeFeatures(geojson, sync) - this.sync.commitBatch() + if (batch) this.sync.commitBatch() return features } catch (err) { console.debug('Error with DataLayer', this.id) @@ -1187,7 +1185,7 @@ export class DataLayer extends ServerStored { } commitDelete() { - delete this._umap.datalayers[stamp(this)] + delete this._umap.datalayers[this.id] } getName() { diff --git a/umap/static/umap/js/modules/form/fields.js b/umap/static/umap/js/modules/form/fields.js index 71abe87d..5a8be8d6 100644 --- a/umap/static/umap/js/modules/form/fields.js +++ b/umap/static/umap/js/modules/form/fields.js @@ -541,14 +541,14 @@ Fields.DataLayerSwitcher = class extends Fields.Select { !datalayer.isDataReadOnly() && datalayer.isBrowsable() ) { - options.push([L.stamp(datalayer), datalayer.getName()]) + options.push([datalayer.id, datalayer.getName()]) } }) return options } toHTML() { - return L.stamp(this.obj.datalayer) + return this.obj.datalayer.id } toJS() { diff --git a/umap/static/umap/js/modules/importer.js b/umap/static/umap/js/modules/importer.js index 83e09812..f2738f08 100644 --- a/umap/static/umap/js/modules/importer.js +++ b/umap/static/umap/js/modules/importer.js @@ -249,7 +249,7 @@ export default class Importer extends Utils.WithTemplate { tagName: 'option', parent: layerSelect, textContent: datalayer.options.name, - value: L.stamp(datalayer), + value: datalayer.id, }) } }) diff --git a/umap/static/umap/js/modules/permissions.js b/umap/static/umap/js/modules/permissions.js index ffaab4fa..d4eca1e0 100644 --- a/umap/static/umap/js/modules/permissions.js +++ b/umap/static/umap/js/modules/permissions.js @@ -13,6 +13,7 @@ export class MapPermissions extends ServerStored { this.setProperties(umap.properties.permissions) this._umap = umap this._isDirty = false + this.sync = umap.syncEngine.proxy(this) } setProperties(properties) { @@ -28,6 +29,13 @@ export class MapPermissions extends ServerStored { ) } + getSyncMetadata() { + return { + subject: 'mappermissions', + metadata: {}, + } + } + render() { this._umap.render(['properties.permissions']) } @@ -259,6 +267,14 @@ export class DataLayerPermissions extends ServerStored { ) this.datalayer = datalayer + this.sync = umap.syncEngine.proxy(this) + } + + getSyncMetadata() { + return { + subject: 'datalayerpermissions', + metadata: { id: this.datalayer.id }, + } } edit(container) { diff --git a/umap/static/umap/js/modules/sync/engine.js b/umap/static/umap/js/modules/sync/engine.js index 2e098efc..a12c01a5 100644 --- a/umap/static/umap/js/modules/sync/engine.js +++ b/umap/static/umap/js/modules/sync/engine.js @@ -2,7 +2,13 @@ import * as SaveManager from '../saving.js' import * as Utils from '../utils.js' import { HybridLogicalClock } from './hlc.js' import { UndoManager } from './undo.js' -import { DataLayerUpdater, FeatureUpdater, MapUpdater } from './updaters.js' +import { + DataLayerUpdater, + FeatureUpdater, + MapUpdater, + MapPermissionsUpdater, + DataLayerPermissionsUpdater, +} from './updaters.js' import { WebSocketTransport } from './websocket.js' // Start reconnecting after 2 seconds, then double the delay each time @@ -56,6 +62,8 @@ export class SyncEngine { map: new MapUpdater(umap), feature: new FeatureUpdater(umap), datalayer: new DataLayerUpdater(umap), + mappermissions: new MapPermissionsUpdater(umap), + datalayerpermissions: new DataLayerPermissionsUpdater(umap), } this.transport = undefined this._operations = new Operations() @@ -129,17 +137,15 @@ export class SyncEngine { this._batch = [] } - commitBatch() { + commitBatch(subject, metadata) { if (!this._batch.length) { this._batch = null return } - this._undoManager.add({ - verb: 'batch', - operations: this._batch, - }) const operations = this._batch.map((stage) => stage.operation) - this._send({ verb: 'batch', operations: operations, subject: 'batch' }) + const operation = { verb: 'batch', operations, subject, metadata } + this._undoManager.add({ operation, stages: this._batch }) + this._send(operation) this._batch = null } @@ -204,6 +210,10 @@ export class SyncEngine { async save() { const needSave = new Map() + if (!this._umap.id) { + // There is no operation for fist map save + needSave.set(this._umap, []) + } for (const operation of this._operations.sorted()) { if (operation.dirty) { const updater = this._getUpdater(operation.subject) @@ -215,7 +225,8 @@ export class SyncEngine { } } for (const [obj, operations] of needSave.entries()) { - await obj.save() + const ok = await obj.save() + if (!ok) break for (const operation of operations) { operation.dirty = false } @@ -243,7 +254,11 @@ export class SyncEngine { } } - _getUpdater(subject, metadata) { + _getUpdater(subject, metadata, sync) { + // For now, prevent permissions to be synced, for security reasons + if (sync && (subject === 'mappermissions' || subject === 'datalayerpermissions')) { + return + } if (Object.keys(this.updaters).includes(subject)) { return this.updaters[subject] } @@ -256,6 +271,10 @@ export class SyncEngine { return } const updater = this._getUpdater(operation.subject, operation.metadata) + if (!updater) { + debug('No updater for', operation) + return + } updater.applyMessage(operation) } @@ -449,7 +468,7 @@ export class SyncEngine { const handler = { get(target, prop) { // Only proxy these methods - if (['upsert', 'update', 'delete'].includes(prop)) { + if (['upsert', 'update', 'delete', 'commitBatch'].includes(prop)) { const { subject, metadata } = object.getSyncMetadata() // Reflect.get is calling the original method. // .bind is adding the parameters automatically diff --git a/umap/static/umap/js/modules/sync/undo.js b/umap/static/umap/js/modules/sync/undo.js index a9e7af2e..8ee57890 100644 --- a/umap/static/umap/js/modules/sync/undo.js +++ b/umap/static/umap/js/modules/sync/undo.js @@ -19,6 +19,9 @@ export class UndoManager { for (const button of document.querySelectorAll('.disabled-on-dirty')) { button.disabled = dirty } + for (const button of document.querySelectorAll('.enabled-on-dirty')) { + button.disabled = !dirty + } } isDirty() { @@ -33,7 +36,7 @@ export class UndoManager { add(stage) { // FIXME make it more generic - if (stage.operation.key !== '_referenceVersion') { + if (!stage.operation || stage.operation.key !== '_referenceVersion') { stage.operation.dirty = true this._redoStack = [] this._undoStack.push(stage) @@ -54,8 +57,8 @@ export class UndoManager { if (!stage) return stage.operation.dirty = !stage.operation.dirty if (stage.operation.verb === 'batch') { - for (const op of stage.operations) { - this.applyOperation(this.copyOperation(op, redo)) + for (const st of stage.stages) { + this.applyOperation(this.copyOperation(st, redo)) } } else { this.applyOperation(this.copyOperation(stage, redo)) diff --git a/umap/static/umap/js/modules/sync/updaters.js b/umap/static/umap/js/modules/sync/updaters.js index 082901bf..606ea63f 100644 --- a/umap/static/umap/js/modules/sync/updaters.js +++ b/umap/static/umap/js/modules/sync/updaters.js @@ -43,7 +43,6 @@ class BaseUpdater { export class MapUpdater extends BaseUpdater { update({ key, value }) { - console.log('updating', key, value) if (fieldInSchema(key)) { this.updateObjectValue(this._umap, key, value) } @@ -61,16 +60,11 @@ export class DataLayerUpdater extends BaseUpdater { upsert({ value }) { // Upsert only happens when a new datalayer is created. try { - console.log( - 'found datalayer with id', - value.id, - this.getDataLayerFromID(value.id) - ) + this.getDataLayerFromID(value.id) } catch { - console.log('we are the fucking catch', value) const datalayer = this._umap.createDataLayer(value._umap_options || value, false) if (value.features) { - datalayer.addData(value) + datalayer.addData(value, true, false) } } } @@ -149,3 +143,27 @@ export class FeatureUpdater extends BaseUpdater { return this.getDataLayerFromID(metadata.layerId) } } + +export class MapPermissionsUpdater extends BaseUpdater { + update({ key, value }) { + this.updateObjectValue(this._umap.permissions, key, value) + // if (fieldInSchema(key)) { + // } + } + + getStoredObject(metadata) { + return this._umap.permissions + } +} + +export class DataLayerPermissionsUpdater extends BaseUpdater { + update({ key, value, metadata }) { + this.updateObjectValue(this.getDataLayerFromID(metadata.id), key, value) + // if (fieldInSchema(key)) { + // } + } + + getStoredObject(metadata) { + return this.getDataLayerFromID(metadata.id).permissions + } +} diff --git a/umap/static/umap/js/modules/ui/bar.js b/umap/static/umap/js/modules/ui/bar.js index 28eb3677..d54ea866 100644 --- a/umap/static/umap/js/modules/ui/bar.js +++ b/umap/static/umap/js/modules/ui/bar.js @@ -34,7 +34,7 @@ const TOP_BAR_TEMPLATE = ` ${translate('View')} -