mirror of
https://github.com/umap-project/umap.git
synced 2025-04-28 11:32:38 +02:00
Merge 626b669d89
into 1a9c325f16
This commit is contained in:
commit
53072b6f5b
18 changed files with 188 additions and 175 deletions
|
@ -551,6 +551,7 @@ class DataLayer(NamedModel):
|
||||||
if self.old_id:
|
if self.old_id:
|
||||||
metadata["old_id"] = self.old_id
|
metadata["old_id"] = self.old_id
|
||||||
metadata["id"] = self.pk
|
metadata["id"] = self.pk
|
||||||
|
metadata["rank"] = self.rank
|
||||||
metadata["permissions"] = {"edit_status": self.edit_status}
|
metadata["permissions"] = {"edit_status": self.edit_status}
|
||||||
metadata["editMode"] = "advanced" if self.can_edit(request) else "disabled"
|
metadata["editMode"] = "advanced" if self.can_edit(request) else "disabled"
|
||||||
metadata["_referenceVersion"] = self.reference_version
|
metadata["_referenceVersion"] = self.reference_version
|
||||||
|
|
|
@ -113,7 +113,7 @@ export default class Browser {
|
||||||
}
|
}
|
||||||
|
|
||||||
onFormChange() {
|
onFormChange() {
|
||||||
this._umap.eachBrowsableDataLayer((datalayer) => {
|
this._umap.datalayers.browsable().map((datalayer) => {
|
||||||
datalayer.resetLayer(true)
|
datalayer.resetLayer(true)
|
||||||
this.updateDatalayer(datalayer)
|
this.updateDatalayer(datalayer)
|
||||||
if (this._umap.fullPanel?.isOpen()) datalayer.tableEdit()
|
if (this._umap.fullPanel?.isOpen()) datalayer.tableEdit()
|
||||||
|
@ -136,7 +136,7 @@ export default class Browser {
|
||||||
onMoveEnd() {
|
onMoveEnd() {
|
||||||
if (!this.isOpen()) return
|
if (!this.isOpen()) return
|
||||||
const isListDynamic = this.options.inBbox
|
const isListDynamic = this.options.inBbox
|
||||||
this._umap.eachBrowsableDataLayer((datalayer) => {
|
this._umap.datalayers.browsable().map((datalayer) => {
|
||||||
if (!isListDynamic && !datalayer.hasDynamicData()) return
|
if (!isListDynamic && !datalayer.hasDynamicData()) return
|
||||||
this.updateDatalayer(datalayer)
|
this.updateDatalayer(datalayer)
|
||||||
})
|
})
|
||||||
|
@ -145,7 +145,7 @@ export default class Browser {
|
||||||
update() {
|
update() {
|
||||||
if (!this.isOpen()) return
|
if (!this.isOpen()) return
|
||||||
this.dataContainer.innerHTML = ''
|
this.dataContainer.innerHTML = ''
|
||||||
this._umap.eachBrowsableDataLayer((datalayer) => {
|
this._umap.datalayers.browsable().map((datalayer) => {
|
||||||
this.addDataLayer(datalayer, this.dataContainer)
|
this.addDataLayer(datalayer, this.dataContainer)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -254,10 +254,10 @@ export default class Browser {
|
||||||
// If at least one layer is shown, hide it
|
// If at least one layer is shown, hide it
|
||||||
// otherwise show all
|
// otherwise show all
|
||||||
let allHidden = true
|
let allHidden = true
|
||||||
this._umap.eachBrowsableDataLayer((datalayer) => {
|
this._umap.datalayers.browsable().map((datalayer) => {
|
||||||
if (datalayer.isVisible()) allHidden = false
|
if (datalayer.isVisible()) allHidden = false
|
||||||
})
|
})
|
||||||
this._umap.eachBrowsableDataLayer((datalayer) => {
|
this._umap.datalayers.browsable().map((datalayer) => {
|
||||||
datalayer._forcedVisibility = true
|
datalayer._forcedVisibility = true
|
||||||
if (allHidden) {
|
if (allHidden) {
|
||||||
datalayer.show()
|
datalayer.show()
|
||||||
|
|
|
@ -68,9 +68,11 @@ export default class Caption extends Utils.WithTemplate {
|
||||||
this.elements.description.hidden = true
|
this.elements.description.hidden = true
|
||||||
}
|
}
|
||||||
this.elements.datalayersContainer.innerHTML = ''
|
this.elements.datalayersContainer.innerHTML = ''
|
||||||
this._umap.eachDataLayerReverse((datalayer) =>
|
this._umap.datalayers
|
||||||
this.addDataLayer(datalayer, this.elements.datalayersContainer)
|
.reverse()
|
||||||
)
|
.map((datalayer) =>
|
||||||
|
this.addDataLayer(datalayer, this.elements.datalayersContainer)
|
||||||
|
)
|
||||||
this.addCredits()
|
this.addCredits()
|
||||||
if (this._umap.properties.created_at) {
|
if (this._umap.properties.created_at) {
|
||||||
const created_at = translate('created at {date}', {
|
const created_at = translate('created at {date}', {
|
||||||
|
@ -85,7 +87,7 @@ export default class Caption extends Utils.WithTemplate {
|
||||||
}
|
}
|
||||||
this._umap.panel.open({ content: this.element }).then(() => {
|
this._umap.panel.open({ content: this.element }).then(() => {
|
||||||
// Create the legend when the panel is actually on the DOM
|
// Create the legend when the panel is actually on the DOM
|
||||||
this._umap.eachDataLayerReverse((datalayer) => datalayer.renderLegend())
|
this._umap.datalayers.reverse().map((datalayer) => datalayer.renderLegend())
|
||||||
this._umap.propagate()
|
this._umap.propagate()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,9 @@ export class DataLayer {
|
||||||
|
|
||||||
this.setOptions(data)
|
this.setOptions(data)
|
||||||
this.pane.dataset.id = this.id
|
this.pane.dataset.id = this.id
|
||||||
|
if (this.options.rank === undefined) {
|
||||||
|
this.options.rank = this._umap.datalayers.count()
|
||||||
|
}
|
||||||
|
|
||||||
if (!Utils.isObject(this.options.remoteData)) {
|
if (!Utils.isObject(this.options.remoteData)) {
|
||||||
this.options.remoteData = {}
|
this.options.remoteData = {}
|
||||||
|
@ -153,6 +156,9 @@ export class DataLayer {
|
||||||
case 'remote-data':
|
case 'remote-data':
|
||||||
this.fetchRemoteData()
|
this.fetchRemoteData()
|
||||||
break
|
break
|
||||||
|
case 'datalayer-rank':
|
||||||
|
this._umap.reorderDataLayers()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -249,16 +255,9 @@ export class DataLayer {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
this._umap.modifiedAt = response.headers.get('last-modified')
|
this._umap.modifiedAt = response.headers.get('last-modified')
|
||||||
this.setReferenceVersion({ response, sync: false })
|
this.setReferenceVersion({ response, sync: false })
|
||||||
// FIXME: for now the _umap_options property is set dynamically from backend
|
delete geojson._umap_options
|
||||||
// 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
|
// In case of maps pre 1.0 still around
|
||||||
if (geojson._storage) geojson._storage.editMode = this.options.editMode
|
delete geojson._storage
|
||||||
await this.fromUmapGeoJSON(geojson)
|
await this.fromUmapGeoJSON(geojson)
|
||||||
this.backupOptions()
|
this.backupOptions()
|
||||||
this._loading = false
|
this._loading = false
|
||||||
|
@ -287,7 +286,6 @@ export class DataLayer {
|
||||||
|
|
||||||
async fromUmapGeoJSON(geojson) {
|
async fromUmapGeoJSON(geojson) {
|
||||||
if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat
|
if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat
|
||||||
geojson._umap_options.id = this.id
|
|
||||||
if (geojson._umap_options) this.setOptions(geojson._umap_options)
|
if (geojson._umap_options) this.setOptions(geojson._umap_options)
|
||||||
if (this.isRemoteLayer()) {
|
if (this.isRemoteLayer()) {
|
||||||
await this.fetchRemoteData()
|
await this.fetchRemoteData()
|
||||||
|
@ -297,6 +295,7 @@ export class DataLayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
|
// TODO do not startBatch for remoteData layer
|
||||||
this.sync.startBatch()
|
this.sync.startBatch()
|
||||||
for (const feature of Object.values(this._features)) {
|
for (const feature of Object.values(this._features)) {
|
||||||
feature.del()
|
feature.del()
|
||||||
|
@ -395,12 +394,7 @@ export class DataLayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
connectToMap() {
|
connectToMap() {
|
||||||
if (!this._umap.datalayers[this.id]) {
|
this._umap.datalayers.add(this)
|
||||||
this._umap.datalayers[this.id] = this
|
|
||||||
}
|
|
||||||
if (!this._umap.datalayersIndex.includes(this)) {
|
|
||||||
this._umap.datalayersIndex.push(this)
|
|
||||||
}
|
|
||||||
this._umap.onDataLayersChanged()
|
this._umap.onDataLayersChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -644,11 +638,19 @@ export class DataLayer {
|
||||||
|
|
||||||
del(sync = true) {
|
del(sync = true) {
|
||||||
const oldValue = Utils.CopyJSON(this.umapGeoJSON())
|
const oldValue = Utils.CopyJSON(this.umapGeoJSON())
|
||||||
this.erase()
|
// TODO merge datalayer del and features del in same
|
||||||
|
// batch
|
||||||
|
this.clear()
|
||||||
if (sync) {
|
if (sync) {
|
||||||
this.isDeleted = true
|
this.isDeleted = true
|
||||||
this.sync.delete(oldValue)
|
this.sync.delete(oldValue)
|
||||||
}
|
}
|
||||||
|
this.hide()
|
||||||
|
this.parentPane.removeChild(this.pane)
|
||||||
|
this._umap.onDataLayersChanged()
|
||||||
|
this.layer.onDelete(this._leafletMap)
|
||||||
|
this.propagateDelete()
|
||||||
|
this._leaflet_events_bk = this._leaflet_events
|
||||||
}
|
}
|
||||||
|
|
||||||
empty() {
|
empty() {
|
||||||
|
@ -666,17 +668,6 @@ export class DataLayer {
|
||||||
return datalayer
|
return datalayer
|
||||||
}
|
}
|
||||||
|
|
||||||
erase() {
|
|
||||||
this.hide()
|
|
||||||
this._umap.datalayersIndex.splice(this.getRank(), 1)
|
|
||||||
this.parentPane.removeChild(this.pane)
|
|
||||||
this._umap.onDataLayersChanged()
|
|
||||||
this.layer.onDelete(this._leafletMap)
|
|
||||||
this.propagateDelete()
|
|
||||||
this._leaflet_events_bk = this._leaflet_events
|
|
||||||
this.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
redraw() {
|
redraw() {
|
||||||
if (!this.isVisible()) return
|
if (!this.isVisible()) return
|
||||||
this.eachFeature((feature) => feature.redraw())
|
this.eachFeature((feature) => feature.redraw())
|
||||||
|
@ -1091,23 +1082,11 @@ export class DataLayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
getPreviousBrowsable() {
|
getPreviousBrowsable() {
|
||||||
let id = this.getRank()
|
return this._umap.datalayers.prev(this)
|
||||||
let next
|
|
||||||
const index = this._umap.datalayersIndex
|
|
||||||
while (((id = index[++id] ? id : 0), (next = index[id]))) {
|
|
||||||
if (next === this || next.canBrowse()) break
|
|
||||||
}
|
|
||||||
return next
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getNextBrowsable() {
|
getNextBrowsable() {
|
||||||
let id = this.getRank()
|
return this._umap.datalayers.next(this)
|
||||||
let prev
|
|
||||||
const index = this._umap.datalayersIndex
|
|
||||||
while (((id = index[--id] ? id : index.length - 1), (prev = index[id]))) {
|
|
||||||
if (prev === this || prev.canBrowse()) break
|
|
||||||
}
|
|
||||||
return prev
|
|
||||||
}
|
}
|
||||||
|
|
||||||
umapGeoJSON() {
|
umapGeoJSON() {
|
||||||
|
@ -1118,8 +1097,8 @@ export class DataLayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getRank() {
|
getDOMOrder() {
|
||||||
return this._umap.datalayersIndex.indexOf(this)
|
return Array.from(this.parentPane.children).indexOf(this.pane)
|
||||||
}
|
}
|
||||||
|
|
||||||
isReadOnly() {
|
isReadOnly() {
|
||||||
|
@ -1141,6 +1120,12 @@ export class DataLayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepareOptions() {
|
||||||
|
const options = Utils.CopyJSON(this.options)
|
||||||
|
delete options.permissions
|
||||||
|
return JSON.stringify(options)
|
||||||
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
if (this.isDeleted) return await this.saveDelete()
|
if (this.isDeleted) return await this.saveDelete()
|
||||||
if (!this.isRemoteLayer() && !this.isLoaded()) return
|
if (!this.isRemoteLayer() && !this.isLoaded()) return
|
||||||
|
@ -1148,8 +1133,8 @@ export class DataLayer {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('name', this.options.name)
|
formData.append('name', this.options.name)
|
||||||
formData.append('display_on_load', !!this.options.displayOnLoad)
|
formData.append('display_on_load', !!this.options.displayOnLoad)
|
||||||
formData.append('rank', this.getRank())
|
formData.append('rank', this.options.rank)
|
||||||
formData.append('settings', JSON.stringify(this.options))
|
formData.append('settings', this.prepareOptions())
|
||||||
// Filename support is shaky, don't do it for now.
|
// Filename support is shaky, don't do it for now.
|
||||||
const blob = new Blob([JSON.stringify(geojson)], { type: 'application/json' })
|
const blob = new Blob([JSON.stringify(geojson)], { type: 'application/json' })
|
||||||
formData.append('geojson', blob)
|
formData.append('geojson', blob)
|
||||||
|
|
|
@ -24,7 +24,7 @@ export default class Facets {
|
||||||
this.selected[name] = selected
|
this.selected[name] = selected
|
||||||
}
|
}
|
||||||
|
|
||||||
this._umap.eachBrowsableDataLayer((datalayer) => {
|
this._umap.datalayers.browsable().map((datalayer) => {
|
||||||
datalayer.eachFeature((feature) => {
|
datalayer.eachFeature((feature) => {
|
||||||
for (const name of names) {
|
for (const name of names) {
|
||||||
let value = feature.properties[name]
|
let value = feature.properties[name]
|
||||||
|
|
|
@ -560,7 +560,7 @@ Fields.SlideshowDelay = class extends Fields.IntSelect {
|
||||||
Fields.DataLayerSwitcher = class extends Fields.Select {
|
Fields.DataLayerSwitcher = class extends Fields.Select {
|
||||||
getOptions() {
|
getOptions() {
|
||||||
const options = []
|
const options = []
|
||||||
this.builder._umap.eachDataLayerReverse((datalayer) => {
|
this.builder._umap.datalayers.reverse().map((datalayer) => {
|
||||||
if (
|
if (
|
||||||
datalayer.isLoaded() &&
|
datalayer.isLoaded() &&
|
||||||
!datalayer.isDataReadOnly() &&
|
!datalayer.isDataReadOnly() &&
|
||||||
|
|
|
@ -243,7 +243,7 @@ export default class Importer extends Utils.WithTemplate {
|
||||||
this.raw = null
|
this.raw = null
|
||||||
const layerSelect = this.qs('[name="layer-id"]')
|
const layerSelect = this.qs('[name="layer-id"]')
|
||||||
layerSelect.innerHTML = ''
|
layerSelect.innerHTML = ''
|
||||||
this._umap.eachDataLayerReverse((datalayer) => {
|
this._umap.datalayers.reverse().map((datalayer) => {
|
||||||
if (datalayer.isLoaded() && !datalayer.isRemoteLayer()) {
|
if (datalayer.isLoaded() && !datalayer.isRemoteLayer()) {
|
||||||
DomUtil.element({
|
DomUtil.element({
|
||||||
tagName: 'option',
|
tagName: 'option',
|
||||||
|
|
46
umap/static/umap/js/modules/managers.js
Normal file
46
umap/static/umap/js/modules/managers.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
export class DataLayerManager extends Object {
|
||||||
|
add(datalayer) {
|
||||||
|
this[datalayer.id] = datalayer
|
||||||
|
}
|
||||||
|
active() {
|
||||||
|
return Object.values(this)
|
||||||
|
.filter((datalayer) => !datalayer.isDeleted)
|
||||||
|
.sort((a, b) => a.options.rank > b.options.rank)
|
||||||
|
}
|
||||||
|
reverse() {
|
||||||
|
return this.active().reverse()
|
||||||
|
}
|
||||||
|
count() {
|
||||||
|
return this.active().length
|
||||||
|
}
|
||||||
|
find(func) {
|
||||||
|
for (const datalayer of this.reverse()) {
|
||||||
|
if (func.call(datalayer, datalayer)) {
|
||||||
|
return datalayer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filter(func) {
|
||||||
|
return this.active().filter(func)
|
||||||
|
}
|
||||||
|
visible() {
|
||||||
|
return this.filter((datalayer) => datalayer.isVisible())
|
||||||
|
}
|
||||||
|
browsable() {
|
||||||
|
return this.reverse().filter((datalayer) => datalayer.allowBrowse())
|
||||||
|
}
|
||||||
|
prev(datalayer) {
|
||||||
|
const browsable = this.browsable()
|
||||||
|
const current = browsable.indexOf(datalayer)
|
||||||
|
const prev = browsable[current - 1] || browsable[browsable.length - 1]
|
||||||
|
if (!prev.canBrowse()) return this.prev(prev)
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
next(datalayer) {
|
||||||
|
const browsable = this.browsable()
|
||||||
|
const current = browsable.indexOf(datalayer)
|
||||||
|
const next = browsable[current + 1] || browsable[0]
|
||||||
|
if (!next.canBrowse()) return this.next(next)
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
}
|
|
@ -159,7 +159,7 @@ export class MapPermissions {
|
||||||
`<fieldset class="separator"><legend>${translate('Datalayers')}</legend></fieldset>`
|
`<fieldset class="separator"><legend>${translate('Datalayers')}</legend></fieldset>`
|
||||||
)
|
)
|
||||||
container.appendChild(fieldset)
|
container.appendChild(fieldset)
|
||||||
this._umap.eachDataLayer((datalayer) => {
|
this._umap.datalayers.active().map((datalayer) => {
|
||||||
datalayer.permissions.edit(fieldset)
|
datalayer.permissions.edit(fieldset)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -327,7 +327,7 @@ export const LeafletMap = BaseMap.extend({
|
||||||
} else if (this.options.defaultView === 'latest') {
|
} else if (this.options.defaultView === 'latest') {
|
||||||
this._umap.onceDataLoaded(() => {
|
this._umap.onceDataLoaded(() => {
|
||||||
if (!this._umap.hasData()) return
|
if (!this._umap.hasData()) return
|
||||||
const datalayer = this._umap.firstVisibleDatalayer()
|
const datalayer = this._umap.datalayers.visible()[0]
|
||||||
let feature
|
let feature
|
||||||
if (datalayer) {
|
if (datalayer) {
|
||||||
const feature = datalayer.getFeatureByIndex(-1)
|
const feature = datalayer.getFeatureByIndex(-1)
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { translate } from './i18n.js'
|
||||||
* - `type`: The type of the data
|
* - `type`: The type of the data
|
||||||
* - `impacts`: A list of impacts than happen when this property is updated, among
|
* - `impacts`: A list of impacts than happen when this property is updated, among
|
||||||
* 'ui', 'data', 'limit-bounds', 'datalayer-index', 'remote-data',
|
* 'ui', 'data', 'limit-bounds', 'datalayer-index', 'remote-data',
|
||||||
* 'background' 'sync'.
|
* 'background', 'sync', 'datalayer-rank'.
|
||||||
*
|
*
|
||||||
* - Extra keys are being passed to the FormBuilder automatically.
|
* - Extra keys are being passed to the FormBuilder automatically.
|
||||||
*/
|
*/
|
||||||
|
@ -436,6 +436,10 @@ export const SCHEMA = {
|
||||||
],
|
],
|
||||||
default: 'Default',
|
default: 'Default',
|
||||||
},
|
},
|
||||||
|
rank: {
|
||||||
|
type: Number,
|
||||||
|
impacts: ['datalayer-rank'],
|
||||||
|
},
|
||||||
remoteData: {
|
remoteData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
impacts: ['remote-data'],
|
impacts: ['remote-data'],
|
||||||
|
|
|
@ -204,8 +204,8 @@ class IframeExporter {
|
||||||
delete this.queryString.feature
|
delete this.queryString.feature
|
||||||
}
|
}
|
||||||
if (this.options.keepCurrentDatalayers) {
|
if (this.options.keepCurrentDatalayers) {
|
||||||
this._umap.eachDataLayer((datalayer) => {
|
this._umap.datalayers.visible().map((datalayer) => {
|
||||||
if (datalayer.isVisible() && datalayer.createdOnServer) {
|
if (datalayer.createdOnServer) {
|
||||||
datalayers.push(datalayer.id)
|
datalayers.push(datalayer.id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -66,7 +66,7 @@ export default class Slideshow extends WithTemplate {
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultDatalayer() {
|
defaultDatalayer() {
|
||||||
return this._umap.findDataLayer((d) => d.canBrowse())
|
return this._umap.datalayers.find((d) => d.canBrowse())
|
||||||
}
|
}
|
||||||
|
|
||||||
startSpinner() {
|
startSpinner() {
|
||||||
|
|
|
@ -207,22 +207,35 @@ export class SyncEngine {
|
||||||
this._send(operation)
|
this._send(operation)
|
||||||
}
|
}
|
||||||
|
|
||||||
async save() {
|
_getDirtyObjects() {
|
||||||
const needSave = new Map()
|
const dirty = new Map()
|
||||||
if (!this._umap.id) {
|
if (!this._umap.id) {
|
||||||
// There is no operation for first map save
|
// There is no operation for first map save
|
||||||
needSave.set(this._umap, [])
|
dirty.set(this._umap, [])
|
||||||
|
}
|
||||||
|
const addDirtyObject = (operation) => {
|
||||||
|
const updater = this._getUpdater(operation.subject)
|
||||||
|
const obj = updater.getStoredObject(operation.metadata)
|
||||||
|
if (!dirty.has(obj)) {
|
||||||
|
dirty.set(obj, [])
|
||||||
|
}
|
||||||
|
dirty.get(obj).push(operation)
|
||||||
}
|
}
|
||||||
for (const operation of this._operations.sorted()) {
|
for (const operation of this._operations.sorted()) {
|
||||||
if (operation.dirty) {
|
if (operation.dirty) {
|
||||||
const updater = this._getUpdater(operation.subject)
|
addDirtyObject(operation)
|
||||||
const obj = updater.getStoredObject(operation.metadata)
|
if (operation.verb === 'batch') {
|
||||||
if (!needSave.has(obj)) {
|
for (const op of operation.operations) {
|
||||||
needSave.set(obj, [])
|
addDirtyObject(op)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
needSave.get(obj).push(operation)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return dirty
|
||||||
|
}
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
const needSave = this._getDirtyObjects()
|
||||||
for (const [obj, operations] of needSave.entries()) {
|
for (const [obj, operations] of needSave.entries()) {
|
||||||
const ok = await obj.save()
|
const ok = await obj.save()
|
||||||
if (!ok) return false
|
if (!ok) return false
|
||||||
|
|
|
@ -207,7 +207,7 @@ export class BottomBar extends WithTemplate {
|
||||||
const select = this.elements.layers
|
const select = this.elements.layers
|
||||||
const selected = select.options[select.selectedIndex].value
|
const selected = select.options[select.selectedIndex].value
|
||||||
if (!selected) return
|
if (!selected) return
|
||||||
this._umap.eachDataLayer((datalayer) => {
|
this._umap.datalayers.active().map((datalayer) => {
|
||||||
datalayer.toggle(datalayer.id === selected)
|
datalayer.toggle(datalayer.id === selected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -228,7 +228,7 @@ export class BottomBar extends WithTemplate {
|
||||||
|
|
||||||
buildDataLayerSwitcher() {
|
buildDataLayerSwitcher() {
|
||||||
this.elements.layers.innerHTML = ''
|
this.elements.layers.innerHTML = ''
|
||||||
const datalayers = this._umap.datalayersIndex.filter((d) => d.options.inCaption)
|
const datalayers = this._umap.datalayers.filter((d) => d.options.inCaption)
|
||||||
if (datalayers.length < 2) {
|
if (datalayers.length < 2) {
|
||||||
this.elements.layers.hidden = true
|
this.elements.layers.hidden = true
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -33,6 +33,7 @@ import { EditPanel, FullPanel, Panel } from './ui/panel.js'
|
||||||
import Tooltip from './ui/tooltip.js'
|
import Tooltip from './ui/tooltip.js'
|
||||||
import URLs from './urls.js'
|
import URLs from './urls.js'
|
||||||
import * as Utils from './utils.js'
|
import * as Utils from './utils.js'
|
||||||
|
import { DataLayerManager } from './managers.js'
|
||||||
|
|
||||||
export default class Umap {
|
export default class Umap {
|
||||||
constructor(element, geojson) {
|
constructor(element, geojson) {
|
||||||
|
@ -166,8 +167,7 @@ export default class Umap {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global storage for retrieving datalayers and features.
|
// Global storage for retrieving datalayers and features.
|
||||||
this.datalayers = {} // All datalayers, including deleted.
|
this.datalayers = new DataLayerManager()
|
||||||
this.datalayersIndex = [] // Datalayers actually on the map and ordered.
|
|
||||||
this.featuresIndex = {}
|
this.featuresIndex = {}
|
||||||
|
|
||||||
this.formatter = new Formatter(this)
|
this.formatter = new Formatter(this)
|
||||||
|
@ -217,7 +217,6 @@ export default class Umap {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.onbeforeunload = () => (this.editEnabled && this.isDirty) || null
|
window.onbeforeunload = () => (this.editEnabled && this.isDirty) || null
|
||||||
this.backup()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get isDirty() {
|
get isDirty() {
|
||||||
|
@ -616,7 +615,7 @@ export default class Umap {
|
||||||
this.datalayersLoaded = true
|
this.datalayersLoaded = true
|
||||||
this.fire('datalayersloaded')
|
this.fire('datalayersloaded')
|
||||||
const toLoad = []
|
const toLoad = []
|
||||||
for (const datalayer of this.datalayersIndex) {
|
for (const datalayer of this.datalayers.active()) {
|
||||||
if (datalayer.showAtLoad()) toLoad.push(() => datalayer.show())
|
if (datalayer.showAtLoad()) toLoad.push(() => datalayer.show())
|
||||||
}
|
}
|
||||||
while (toLoad.length) {
|
while (toLoad.length) {
|
||||||
|
@ -630,7 +629,7 @@ export default class Umap {
|
||||||
|
|
||||||
createDataLayer(options = {}, sync = true) {
|
createDataLayer(options = {}, sync = true) {
|
||||||
options.name =
|
options.name =
|
||||||
options.name || `${translate('Layer')} ${this.datalayersIndex.length + 1}`
|
options.name || `${translate('Layer')} ${this.datalayers.count() + 1}`
|
||||||
const datalayer = new DataLayer(this, this._leafletMap, options)
|
const datalayer = new DataLayer(this, this._leafletMap, options)
|
||||||
|
|
||||||
if (sync !== false) {
|
if (sync !== false) {
|
||||||
|
@ -651,19 +650,21 @@ export default class Umap {
|
||||||
}
|
}
|
||||||
|
|
||||||
reindexDataLayers() {
|
reindexDataLayers() {
|
||||||
this.eachDataLayer((datalayer) => datalayer.reindex())
|
this.datalayers.active().map((datalayer) => datalayer.reindex())
|
||||||
this.onDataLayersChanged()
|
this.onDataLayersChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
indexDatalayers() {
|
reorderDataLayers() {
|
||||||
const panes = this._leafletMap.getPane('overlayPane')
|
const parent = this._leafletMap.getPane('overlayPane')
|
||||||
|
const datalayers = Object.values(this.datalayers)
|
||||||
this.datalayersIndex = []
|
.filter((datalayer) => !datalayer._isDeleted)
|
||||||
for (const pane of panes.children) {
|
.sort(
|
||||||
if (!pane.dataset || !pane.dataset.id) continue
|
(datalayer1, datalayer2) => datalayer1.options.rank > datalayer2.options.rank
|
||||||
this.datalayersIndex.push(this.datalayers[pane.dataset.id])
|
)
|
||||||
|
for (const datalayer of datalayers) {
|
||||||
|
const child = parent.querySelector(`[data-id="${datalayer.id}"]`)
|
||||||
|
parent.appendChild(child)
|
||||||
}
|
}
|
||||||
this.onDataLayersChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onceDatalayersLoaded(callback, context) {
|
onceDatalayersLoaded(callback, context) {
|
||||||
|
@ -694,7 +695,6 @@ export default class Umap {
|
||||||
async saveAll() {
|
async saveAll() {
|
||||||
if (!this.isDirty) return
|
if (!this.isDirty) return
|
||||||
if (this._defaultExtent) this._setCenterAndZoom()
|
if (this._defaultExtent) this._setCenterAndZoom()
|
||||||
this.backup()
|
|
||||||
const status = await this.sync.save()
|
const status = await this.sync.save()
|
||||||
if (!status) return
|
if (!status) return
|
||||||
// Do a blind render for now, as we are not sure what could
|
// Do a blind render for now, as we are not sure what could
|
||||||
|
@ -714,24 +714,6 @@ export default class Umap {
|
||||||
return this.properties.name || translate('Untitled map')
|
return this.properties.name || translate('Untitled map')
|
||||||
}
|
}
|
||||||
|
|
||||||
backup() {
|
|
||||||
this.backupProperties()
|
|
||||||
this._datalayersIndex_bk = [].concat(this.datalayersIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
backupProperties() {
|
|
||||||
this._backupProperties = Object.assign({}, this.properties)
|
|
||||||
this._backupProperties.tilelayer = Object.assign({}, this.properties.tilelayer)
|
|
||||||
this._backupProperties.limitBounds = Object.assign({}, this.properties.limitBounds)
|
|
||||||
this._backupProperties.permissions = Object.assign({}, this.permissions.properties)
|
|
||||||
}
|
|
||||||
|
|
||||||
resetProperties() {
|
|
||||||
this.properties = Object.assign({}, this._backupProperties)
|
|
||||||
this.properties.tilelayer = Object.assign({}, this._backupProperties.tilelayer)
|
|
||||||
this.permissions.properties = Object.assign({}, this._backupProperties.permissions)
|
|
||||||
}
|
|
||||||
|
|
||||||
setProperties(newProperties) {
|
setProperties(newProperties) {
|
||||||
for (const key of Object.keys(SCHEMA)) {
|
for (const key of Object.keys(SCHEMA)) {
|
||||||
if (newProperties[key] !== undefined) {
|
if (newProperties[key] !== undefined) {
|
||||||
|
@ -744,24 +726,24 @@ export default class Umap {
|
||||||
}
|
}
|
||||||
|
|
||||||
hasData() {
|
hasData() {
|
||||||
for (const datalayer of this.datalayersIndex) {
|
for (const datalayer of this.datalayers.active()) {
|
||||||
if (datalayer.hasData()) return true
|
if (datalayer.hasData()) return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasLayers() {
|
hasLayers() {
|
||||||
return Boolean(this.datalayersIndex.length)
|
return Boolean(this.datalayers.count())
|
||||||
}
|
}
|
||||||
|
|
||||||
allProperties() {
|
allProperties() {
|
||||||
return [].concat(...this.datalayersIndex.map((dl) => dl.allProperties()))
|
return [].concat(...this.datalayers.active().map((dl) => dl.allProperties()))
|
||||||
}
|
}
|
||||||
|
|
||||||
sortedValues(property) {
|
sortedValues(property) {
|
||||||
return []
|
return []
|
||||||
.concat(...this.datalayersIndex.map((dl) => dl.sortedValues(property)))
|
.concat(...this.datalayers.active().map((dl) => dl.sortedValues(property)))
|
||||||
.filter((val, idx, arr) => arr.indexOf(val) === idx)
|
.filter((val, idx, arr) => arr.indexOf(val) === idx)
|
||||||
.sort(U.Utils.naturalSort)
|
.sort(Utils.naturalSort)
|
||||||
}
|
}
|
||||||
|
|
||||||
editCaption() {
|
editCaption() {
|
||||||
|
@ -1278,7 +1260,7 @@ export default class Umap {
|
||||||
|
|
||||||
toGeoJSON() {
|
toGeoJSON() {
|
||||||
let features = []
|
let features = []
|
||||||
this.eachDataLayer((datalayer) => {
|
this.datalayers.active().map((datalayer) => {
|
||||||
if (datalayer.isVisible()) {
|
if (datalayer.isVisible()) {
|
||||||
features = features.concat(datalayer.featuresToGeoJSON())
|
features = features.concat(datalayer.featuresToGeoJSON())
|
||||||
}
|
}
|
||||||
|
@ -1354,13 +1336,20 @@ export default class Umap {
|
||||||
if (fields.includes('properties.rules')) {
|
if (fields.includes('properties.rules')) {
|
||||||
this.rules.load()
|
this.rules.load()
|
||||||
}
|
}
|
||||||
this.eachVisibleDataLayer((datalayer) => {
|
this.datalayers.visible().map((datalayer) => {
|
||||||
datalayer.redraw()
|
datalayer.redraw()
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'datalayer-index':
|
case 'datalayer-index':
|
||||||
this.reindexDataLayers()
|
this.reindexDataLayers()
|
||||||
break
|
break
|
||||||
|
case 'datalayer-rank':
|
||||||
|
// When drag'n'dropping datalayers,
|
||||||
|
// this get called once per datalayers.
|
||||||
|
// (and same for undo/redo of the action)
|
||||||
|
// TODO: call only once
|
||||||
|
this.reorderDataLayers()
|
||||||
|
break
|
||||||
case 'background':
|
case 'background':
|
||||||
this._leafletMap.initTileLayers()
|
this._leafletMap.initTileLayers()
|
||||||
break
|
break
|
||||||
|
@ -1449,7 +1438,7 @@ export default class Umap {
|
||||||
) {
|
) {
|
||||||
return datalayer
|
return datalayer
|
||||||
}
|
}
|
||||||
datalayer = this.findDataLayer((datalayer) => {
|
datalayer = this.datalayers.find((datalayer) => {
|
||||||
if (!datalayer.isDataReadOnly() && datalayer.isBrowsable()) {
|
if (!datalayer.isDataReadOnly() && datalayer.isBrowsable()) {
|
||||||
fallback = datalayer
|
fallback = datalayer
|
||||||
if (datalayer.isVisible()) return true
|
if (datalayer.isVisible()) return true
|
||||||
|
@ -1464,49 +1453,20 @@ export default class Umap {
|
||||||
return this.createDirtyDataLayer()
|
return this.createDirtyDataLayer()
|
||||||
}
|
}
|
||||||
|
|
||||||
findDataLayer(method, context) {
|
eachFeature(callback) {
|
||||||
for (let i = this.datalayersIndex.length - 1; i >= 0; i--) {
|
this.datalayers.browsable().map((datalayer) => {
|
||||||
if (method.call(context, this.datalayersIndex[i])) {
|
if (datalayer.isVisible()) datalayer.eachFeature(callback)
|
||||||
return this.datalayersIndex[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eachDataLayer(method, context) {
|
|
||||||
for (let i = 0; i < this.datalayersIndex.length; i++) {
|
|
||||||
method.call(context, this.datalayersIndex[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eachDataLayerReverse(method, context, filter) {
|
|
||||||
for (let i = this.datalayersIndex.length - 1; i >= 0; i--) {
|
|
||||||
if (filter && !filter.call(context, this.datalayersIndex[i])) continue
|
|
||||||
method.call(context, this.datalayersIndex[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eachBrowsableDataLayer(method, context) {
|
|
||||||
this.eachDataLayerReverse(method, context, (d) => d.allowBrowse())
|
|
||||||
}
|
|
||||||
|
|
||||||
eachVisibleDataLayer(method, context) {
|
|
||||||
this.eachDataLayerReverse(method, context, (d) => d.isVisible())
|
|
||||||
}
|
|
||||||
|
|
||||||
eachFeature(callback, context) {
|
|
||||||
this.eachBrowsableDataLayer((datalayer) => {
|
|
||||||
if (datalayer.isVisible()) datalayer.eachFeature(callback, context)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
removeDataLayers() {
|
removeDataLayers() {
|
||||||
this.eachDataLayerReverse((datalayer) => {
|
this.datalayers.active().map((datalayer) => {
|
||||||
datalayer.del()
|
datalayer.del()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
emptyDataLayers() {
|
emptyDataLayers() {
|
||||||
this.eachDataLayerReverse((datalayer) => {
|
this.datalayers.active().map((datalayer) => {
|
||||||
datalayer.empty()
|
datalayer.empty()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1520,7 +1480,7 @@ export default class Umap {
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
const [container, { ul }] = Utils.loadTemplateWithRefs(template)
|
const [container, { ul }] = Utils.loadTemplateWithRefs(template)
|
||||||
this.eachDataLayerReverse((datalayer) => {
|
this.datalayers.reverse().map((datalayer) => {
|
||||||
const row = Utils.loadTemplate(
|
const row = Utils.loadTemplate(
|
||||||
`<li class="orderable"><i class="icon icon-16 icon-drag" title="${translate('Drag to reorder')}"></i></li>`
|
`<li class="orderable"><i class="icon icon-16 icon-drag" title="${translate('Drag to reorder')}"></i></li>`
|
||||||
)
|
)
|
||||||
|
@ -1539,16 +1499,22 @@ export default class Umap {
|
||||||
const onReorder = (src, dst, initialIndex, finalIndex) => {
|
const onReorder = (src, dst, initialIndex, finalIndex) => {
|
||||||
const movedLayer = this.datalayers[src.dataset.id]
|
const movedLayer = this.datalayers[src.dataset.id]
|
||||||
const targetLayer = this.datalayers[dst.dataset.id]
|
const targetLayer = this.datalayers[dst.dataset.id]
|
||||||
const minIndex = Math.min(movedLayer.getRank(), targetLayer.getRank())
|
const minIndex = Math.min(movedLayer.getDOMOrder(), targetLayer.getDOMOrder())
|
||||||
const maxIndex = Math.max(movedLayer.getRank(), targetLayer.getRank())
|
const maxIndex = Math.max(movedLayer.getDOMOrder(), targetLayer.getDOMOrder())
|
||||||
if (finalIndex === 0) movedLayer.bringToTop()
|
if (finalIndex === 0) movedLayer.bringToTop()
|
||||||
else if (finalIndex > initialIndex) movedLayer.insertBefore(targetLayer)
|
else if (finalIndex > initialIndex) movedLayer.insertBefore(targetLayer)
|
||||||
else movedLayer.insertAfter(targetLayer)
|
else movedLayer.insertAfter(targetLayer)
|
||||||
this.eachDataLayerReverse((datalayer) => {
|
this.sync.startBatch()
|
||||||
if (datalayer.getRank() >= minIndex && datalayer.getRank() <= maxIndex)
|
this.datalayers.reverse().map((datalayer) => {
|
||||||
datalayer.isDirty = true
|
const rank = datalayer.getDOMOrder()
|
||||||
|
if (rank >= minIndex && rank <= maxIndex) {
|
||||||
|
const oldRank = datalayer.options.rank
|
||||||
|
datalayer.options.rank = rank
|
||||||
|
datalayer.sync.update('options.rank', rank, oldRank)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
this.indexDatalayers()
|
this.sync.commitBatch()
|
||||||
|
this.onDataLayersChanged()
|
||||||
}
|
}
|
||||||
const orderable = new Orderable(ul, onReorder)
|
const orderable = new Orderable(ul, onReorder)
|
||||||
|
|
||||||
|
@ -1570,18 +1536,6 @@ export default class Umap {
|
||||||
return datalayer
|
return datalayer
|
||||||
}
|
}
|
||||||
|
|
||||||
firstVisibleDatalayer() {
|
|
||||||
return this.findDataLayer((datalayer) => {
|
|
||||||
if (datalayer.isVisible()) return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ensurePanesOrder() {
|
|
||||||
this.eachDataLayer((datalayer) => {
|
|
||||||
datalayer.bringToTop()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
openBrowser(mode) {
|
openBrowser(mode) {
|
||||||
this.onceDatalayersLoaded(() => this.browser.open(mode))
|
this.onceDatalayersLoaded(() => this.browser.open(mode))
|
||||||
}
|
}
|
||||||
|
@ -1732,7 +1686,7 @@ export default class Umap {
|
||||||
|
|
||||||
getLayersBounds() {
|
getLayersBounds() {
|
||||||
const bounds = new latLngBounds()
|
const bounds = new latLngBounds()
|
||||||
this.eachBrowsableDataLayer((d) => {
|
this.datalayers.browsable().map((d) => {
|
||||||
if (d.isVisible()) bounds.extend(d.layer.getBounds())
|
if (d.isVisible()) bounds.extend(d.layer.getBounds())
|
||||||
})
|
})
|
||||||
return bounds
|
return bounds
|
||||||
|
|
|
@ -131,8 +131,8 @@ class DataLayerFactory(factory.django.DjangoModelFactory):
|
||||||
data.setdefault("_umap_options", {})
|
data.setdefault("_umap_options", {})
|
||||||
if "name" in data["_umap_options"] and kwargs["name"] == cls.name:
|
if "name" in data["_umap_options"] and kwargs["name"] == cls.name:
|
||||||
kwargs["name"] = data["_umap_options"]["name"]
|
kwargs["name"] = data["_umap_options"]["name"]
|
||||||
if "settings" not in kwargs:
|
kwargs.setdefault("settings", {})
|
||||||
kwargs["settings"] = data.get("_umap_options", {})
|
kwargs["settings"].update(data.get("_umap_options", {}))
|
||||||
else:
|
else:
|
||||||
data = DATALAYER_DATA.copy()
|
data = DATALAYER_DATA.copy()
|
||||||
data["_umap_options"] = {
|
data["_umap_options"] = {
|
||||||
|
|
|
@ -50,6 +50,8 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
|
||||||
"editMode": "advanced",
|
"editMode": "advanced",
|
||||||
"inCaption": True,
|
"inCaption": True,
|
||||||
"id": str(datalayer.pk),
|
"id": str(datalayer.pk),
|
||||||
|
"rank": 0,
|
||||||
|
"remoteData": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Now navigate to this map from another tab
|
# Now navigate to this map from another tab
|
||||||
|
@ -87,6 +89,8 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
|
||||||
"inCaption": True,
|
"inCaption": True,
|
||||||
"editMode": "advanced",
|
"editMode": "advanced",
|
||||||
"id": str(datalayer.pk),
|
"id": str(datalayer.pk),
|
||||||
|
"rank": 0,
|
||||||
|
"remoteData": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Now create another marker in the first tab
|
# Now create another marker in the first tab
|
||||||
|
@ -105,7 +109,8 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
|
||||||
"inCaption": True,
|
"inCaption": True,
|
||||||
"editMode": "advanced",
|
"editMode": "advanced",
|
||||||
"id": str(datalayer.pk),
|
"id": str(datalayer.pk),
|
||||||
"permissions": {"edit_status": 1},
|
"rank": 0,
|
||||||
|
"remoteData": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
# And again
|
# And again
|
||||||
|
@ -124,7 +129,8 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
|
||||||
"inCaption": True,
|
"inCaption": True,
|
||||||
"editMode": "advanced",
|
"editMode": "advanced",
|
||||||
"id": str(datalayer.pk),
|
"id": str(datalayer.pk),
|
||||||
"permissions": {"edit_status": 1},
|
"rank": 0,
|
||||||
|
"remoteData": {},
|
||||||
}
|
}
|
||||||
expect(marker_pane_p1).to_have_count(4)
|
expect(marker_pane_p1).to_have_count(4)
|
||||||
|
|
||||||
|
@ -145,7 +151,8 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
|
||||||
"inCaption": True,
|
"inCaption": True,
|
||||||
"editMode": "advanced",
|
"editMode": "advanced",
|
||||||
"id": str(datalayer.pk),
|
"id": str(datalayer.pk),
|
||||||
"permissions": {"edit_status": 1},
|
"rank": 0,
|
||||||
|
"remoteData": {},
|
||||||
}
|
}
|
||||||
expect(marker_pane_p2).to_have_count(5)
|
expect(marker_pane_p2).to_have_count(5)
|
||||||
|
|
||||||
|
@ -271,7 +278,8 @@ def test_same_second_edit_doesnt_conflict(context, live_server, tilelayer):
|
||||||
"inCaption": True,
|
"inCaption": True,
|
||||||
"editMode": "advanced",
|
"editMode": "advanced",
|
||||||
"id": str(datalayer.pk),
|
"id": str(datalayer.pk),
|
||||||
"permissions": {"edit_status": 1},
|
"rank": 0,
|
||||||
|
"remoteData": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue