From 5e692d2280cbda4c5db45079e5a9f6457b4aa087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20M=C3=A9taireau?= Date: Tue, 30 Apr 2024 17:22:33 +0200 Subject: [PATCH] feat(sync): Add a `enableSync` option. This changes how the syncEngine works. At the moment, it's always instanciated, even if no syncing is configured. It just does nothing. This is to avoid doing `if (engine) engine.update()` calls everywhere we use it. You now need to `start()` and `stop()` it. --- umap/static/umap/js/modules/schema.js | 23 +++++++++------ umap/static/umap/js/modules/sync/engine.js | 19 +++++++++++-- umap/static/umap/js/modules/sync/websocket.js | 4 +++ umap/static/umap/js/umap.forms.js | 4 +-- umap/static/umap/js/umap.js | 28 ++++++++++++------- umap/static/umap/js/umap.layer.js | 7 ++--- umap/static/umap/js/umap.permissions.js | 1 + umap/ws.py | 2 +- 8 files changed, 60 insertions(+), 28 deletions(-) diff --git a/umap/static/umap/js/modules/schema.js b/umap/static/umap/js/modules/schema.js index 3db6f726..9b95b7d6 100644 --- a/umap/static/umap/js/modules/schema.js +++ b/umap/static/umap/js/modules/schema.js @@ -1,8 +1,9 @@ import { translate } from './i18n.js' // Possible impacts -// ['ui', 'data', 'limit-bounds', 'datalayer-index', 'remote-data', 'background'] +// ['ui', 'data', 'limit-bounds', 'datalayer-index', 'remote-data', 'background' 'sync'] +// This is sorted alphabetically export const SCHEMA = { browsable: { impacts: ['ui'], @@ -14,6 +15,12 @@ export const SCHEMA = { label: translate('Do you want to display a caption bar?'), default: false, }, + captionControl: { + type: Boolean, + nullable: true, + label: translate('Display the caption control'), + default: true, + }, captionMenus: { type: Boolean, impacts: ['ui'], @@ -184,7 +191,6 @@ export const SCHEMA = { type: Boolean, impacts: ['ui'], }, - interactive: { type: Boolean, impacts: ['data'], @@ -373,12 +379,6 @@ export const SCHEMA = { impacts: ['ui'], label: translate('Allow scroll wheel zoom?'), }, - captionControl: { - type: Boolean, - nullable: true, - label: translate('Display the caption control'), - default: true, - }, searchControl: { type: Boolean, impacts: ['ui'], @@ -437,6 +437,13 @@ export const SCHEMA = { inheritable: true, default: true, }, + syncEnabled: { + type: Boolean, + impacts: ['sync', 'ui'], + label: translate('Enable real-time collaboration'), + helpEntries: 'sync', + default: false, + }, tilelayer: { type: Object, impacts: ['background'], diff --git a/umap/static/umap/js/modules/sync/engine.js b/umap/static/umap/js/modules/sync/engine.js index ed971c6a..2fac3da8 100644 --- a/umap/static/umap/js/modules/sync/engine.js +++ b/umap/static/umap/js/modules/sync/engine.js @@ -8,16 +8,31 @@ import { } from './updaters.js' export class SyncEngine { - constructor(map, webSocketURI, authToken) { + constructor(map) { this.map = map this.receiver = new MessagesDispatcher(this.map) + + this._initialize() + } + _initialize() { + this.transport = undefined + const noop = () => undefined + // by default, all operations do nothing, until the engine is started. + this.upsert = this.update = this.delete = noop + } + + start(webSocketURI, authToken) { this.transport = new WebSocketTransport(webSocketURI, authToken, this.receiver) this.sender = new MessagesSender(this.transport) - this.upsert = this.sender.upsert.bind(this.sender) this.update = this.sender.update.bind(this.sender) this.delete = this.sender.delete.bind(this.sender) } + + stop() { + if (this.transport) this.transport.close() + this._initialize() + } } export class MessagesDispatcher { diff --git a/umap/static/umap/js/modules/sync/websocket.js b/umap/static/umap/js/modules/sync/websocket.js index fa103119..87540168 100644 --- a/umap/static/umap/js/modules/sync/websocket.js +++ b/umap/static/umap/js/modules/sync/websocket.js @@ -19,4 +19,8 @@ export class WebSocketTransport { let encoded = JSON.stringify(message) this.websocket.send(encoded) } + + close() { + this.websocket.close() + } } diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 1a0ec375..99ebded4 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -1186,10 +1186,8 @@ U.FormBuilder = L.FormBuilder.extend({ L.FormBuilder.prototype.setter.call(this, field, value) this.obj.isDirty = true if ('render' in this.obj) this.obj.render([field], this) - console.log('setter', field, value) const { subject, metadata, engine } = this.obj.getSyncMetadata() - console.log('metadata', metadata) - engine.update(subject, metadata, field, value) + if (engine) engine.update(subject, metadata, field, value) }, finish: function () { diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 102d78cc..b2aec42e 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -248,19 +248,25 @@ U.Map = L.Map.extend({ this.backup() this.initContextMenu() this.on('click contextmenu.show', this.closeInplaceToolbar) + this.sync = new U.SyncEngine(this) Promise.resolve(this.initSyncEngine()) }, initSyncEngine: async function () { - // Get the authentication token from the server - // And pass it to the sync engine. - // FIXME: use `this.urls` - const [response, _, error] = await this.server.get( - `/map/${this.options.umap_id}/ws-token/` - ) - if (!error) { - this.sync = new U.SyncEngine(this, 'ws://localhost:8001', response.token) + console.log('this.options.syncEnabled', this.options.syncEnabled) + if (this.options.syncEnabled != true) { + this.sync.stop() + } else { + // Get the authentication token from the server + // And pass it to the sync engine. + // FIXME: use `this.urls` + const [response, _, error] = await this.server.get( + `/map/${this.options.umap_id}/ws-token/` + ) + if (!error) { + this.sync.start('ws://localhost:8001', response.token) + } } }, @@ -294,6 +300,8 @@ U.Map = L.Map.extend({ case 'bounds': this.handleLimitBounds() break + case 'sync': + this.initSyncEngine() } } }, @@ -1036,7 +1044,7 @@ U.Map = L.Map.extend({ formData.append('center', JSON.stringify(this.geometry())) formData.append('settings', JSON.stringify(geojson)) const uri = this.urls.get('map_save', { map_id: this.options.umap_id }) - const [data, response, error] = await this.server.post(uri, {}, formData) + const [data, _, error] = await this.server.post(uri, {}, formData) // FIXME: login_required response will not be an error, so it will not // stop code while it should if (!error) { @@ -1538,7 +1546,7 @@ U.Map = L.Map.extend({ if (!this.editEnabled) return if (this.options.editMode !== 'advanced') return const container = L.DomUtil.create('div', 'umap-edit-container'), - metadataFields = ['options.name', 'options.description'], + metadataFields = ['options.name', 'options.description', 'options.syncEnabled'], title = L.DomUtil.create('h3', '', container) title.textContent = L._('Edit map details') const builder = new U.FormBuilder(this, metadataFields, { diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index 5a3324cc..b9ad5d54 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -60,6 +60,9 @@ U.Layer.Cluster = L.MarkerClusterGroup.extend({ initialize: function (datalayer) { this.datalayer = datalayer + if (!U.Utils.isObject(this.datalayer.options.cluster)) { + this.datalayer.options.cluster = {} + } const options = { polygonOptions: { color: this.datalayer.getColor(), @@ -74,10 +77,6 @@ U.Layer.Cluster = L.MarkerClusterGroup.extend({ L.MarkerClusterGroup.prototype.initialize.call(this, options) this._markerCluster = U.MarkerCluster this._layers = [] - - if (!U.Utils.isObject(this.datalayer.options.cluster)) { - this.datalayer.options.cluster = {} - } }, onRemove: function (map) { diff --git a/umap/static/umap/js/umap.permissions.js b/umap/static/umap/js/umap.permissions.js index e9eab799..32b3a977 100644 --- a/umap/static/umap/js/umap.permissions.js +++ b/umap/static/umap/js/umap.permissions.js @@ -109,6 +109,7 @@ U.MapPermissions = L.Class.extend({ { handler: 'ManageEditors', label: L._("Map's editors") }, ]) } + const builder = new U.FormBuilder(this, fields) const form = builder.build() container.appendChild(form) diff --git a/umap/ws.py b/umap/ws.py index 646017f0..76279b89 100644 --- a/umap/ws.py +++ b/umap/ws.py @@ -51,7 +51,7 @@ class OperationMessage(BaseModel): subject: str = Literal["map", "layer", "feature"] metadata: Optional[dict] = None key: Optional[str] = None - value: Optional[str | bool | int | GeometryValue] = None + value: Optional[str | bool | int | GeometryValue | Geometry] = None async def join_and_listen(