Compare commits

..

20 commits

Author SHA1 Message Date
Yohan Boniface
3b6ff0c57c wip: allow to sync version restore
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-26 18:46:59 +01:00
Yohan Boniface
2a460f03b2 wip: add test to make sure saving also save remote dirty datalayers 2025-03-26 18:05:16 +01:00
Yohan Boniface
bbde111fdf wip: do not call document during JS unittests 2025-03-26 18:05:16 +01:00
Yohan Boniface
9c4287ac1e wip: allow to sync/undo filter added/removed from table editor 2025-03-26 18:05:16 +01:00
Yohan Boniface
e9f2ff9a6c wip: permissions does not inherit from ServerStored anymore 2025-03-26 18:05:16 +01:00
Yohan Boniface
12e456d24e wip: allow to undo/sync rules
When editing Rule(s), we are not editing the map data itself, but a
sort of proxy objects. This was done mainly because map.properties.rules
is an array of object, and at this time Leaflet.FormBuilder did not know
how to edit an array (something like properties.rules.0.condition).
Now that we integrated FormBuilder, it still does not know how to do this
but we could teach it, or find another way (real Proxy or use reference
to the original object in the Rule).
2025-03-26 18:05:16 +01:00
Yohan Boniface
e004cd461d wip: uncreated map should always appear as dirty 2025-03-26 18:05:16 +01:00
Yohan Boniface
6bea9339b6 wip: DataLayer does not inherit anymore from ServerStored 2025-03-26 18:05:16 +01:00
Yohan Boniface
90ea3737f2 wip: allow DataLayer.clear to be sync and undone 2025-03-26 18:05:16 +01:00
Yohan Boniface
a7b750740c wip: uMap does not inherit anymore from ServerStored
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-26 18:05:16 +01:00
Yohan Boniface
101b036a66 wip: remove not effective code
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-26 18:05:16 +01:00
Yohan Boniface
983f7f8cb1 wip: add permissions related fields in schema
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-26 18:05:16 +01:00
Yohan Boniface
9718f11faf wip: allow to mark an operation as not undoable
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-26 18:05:16 +01:00
Yohan Boniface
88382ab00b wip: tests pass
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-26 18:05:16 +01:00
Yohan Boniface
0b84084c6b fixup: make sure to toggle remote client state at save too
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-26 18:05:16 +01:00
Yohan Boniface
8b2454936b wip: derive the dirty status from the undoManager
This should pave the way for removing the SaveManager.

Co-authored-by: David Larlet <david@larlet.fr>
2025-03-26 18:05:16 +01:00
Yohan Boniface
98f2f8df65 Update the tests and remove cancel edits
Co-authored-by: Alexis Métaireau <alexis@notmyidea.org>
2025-03-26 18:05:16 +01:00
757cb375d1 Add integration test for batch undo/redo 2025-03-26 18:05:16 +01:00
Yohan Boniface
4ef1411102 Batch operations
Co-authored-by: Alexis Métaireau <alexis@notmyidea.org>
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-26 18:05:16 +01:00
Yohan Boniface
01b2053030 wip: undo redo
Co-authored-by: Alexis Métaireau <alexis@notmyidea.org>
2025-03-26 18:05:16 +01:00
50 changed files with 201 additions and 303 deletions

View file

@ -5,7 +5,7 @@
"es6": true
},
"parserOptions": {
"ecmaVersion": 2021,
"ecmaVersion": 2020,
"sourceType": "module"
}
}

View file

@ -1,7 +1,11 @@
{
"files": {
"include": ["umap/static/umap/js/**"],
"ignore": ["umap/static/umap/vendors/**"]
"include": [
"umap/static/umap/js/**"
],
"ignore": [
"umap/static/umap/vendors/**"
]
},
"formatter": {
"enabled": true,
@ -18,11 +22,7 @@
"rules": {
"style": {
"useBlockStatements": "off",
"noShoutyConstants": "warn",
"noParameterAssign": "off"
},
"complexity": {
"noForEach": "off"
"noShoutyConstants": "warn"
},
"performance": {
"noDelete": "off"

View file

@ -526,10 +526,7 @@ class DataLayer(NamedModel):
metadata = self.settings
if not metadata:
# Fallback to file for old datalayers.
try:
data = json.loads(self.geojson.read().decode())
except FileNotFoundError:
data = {}
metadata = data.get("_umap_options")
if not metadata:
metadata = {

View file

@ -84,11 +84,6 @@ hgroup {
hgroup > :not(:first-child):last-child {
font-weight: normal;
}
hgroup p,
hgroup button {
margin: 0;
}
/*
* List
@ -163,23 +158,10 @@ dt {
}
.grid-container {
display: grid;
--grid-layout-gap: calc(var(--gutter) * 2);
--grid-column-count: 3;
--grid-item--min-width: 300px;
/**
* Calculated values.
*/
--gap-count: calc(var(--grid-column-count) - 1);
--total-gap-width: calc(var(--gap-count) * var(--grid-layout-gap));
--grid-item--max-width: calc((100% - var(--total-gap-width)) / var(--grid-column-count));
grid-template-columns: repeat(auto-fill, minmax(max(var(--grid-item--min-width), var(--grid-item--max-width)), 1fr));
grid-gap: var(--grid-layout-gap);
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-container.by4 {
--grid-column-count: 4;
--grid-item--min-width: 60px;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.grid-container > * {
text-align: center;

View file

@ -56,9 +56,9 @@ body.login header {
.map_fragment {
width: 100%;
}
.map_fragment,
.map_list .map_fragment,
.demo_map .map_fragment {
height: var(--map-fragment-height);
height: 210px;
}
.map_list .legend {
padding-top: 7px;
@ -164,9 +164,6 @@ h2.tabs a:hover {
color: #efefef;
text-decoration: underline;
}
.more_button {
min-height: var(--map-fragment-height);
}
/* **************************** */

View file

@ -111,6 +111,8 @@
.umap-right-edit-toolbox {
display: flex;
column-gap: 10px;
}
.umap-right-edit-toolbox {
align-items: center;
}
@ -129,20 +131,17 @@
text-indent: -9999px;
}
.umap-main-edit-toolbox .map-name {
font-weight: bold;
text-align: start;
}
.truncate {
display: inline-flex;
display: inline-block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.umap-main-edit-toolbox .username {
max-width: 100px;
font-weight: bold;
text-align: start;
}
.umap-main-edit-toolbox .share-status {
font-style: italic;
overflow: hidden;
text-overflow: ellipsis;
}
.map-name:after {
content: '\00a0';
@ -160,13 +159,11 @@
.umap-main-edit-toolbox h3 {
display: inline;
}
.umap-caption-bar .umap-map-author {
margin-inline-end: 10px;
.umap-caption-bar button {
margin-inline-start: 10px;
}
.umap-caption-bar > button + button:after,
.umap-caption-bar > button + button:before {
.umap-caption-bar button + button:before {
content: '|';
padding-inline-start: 10px;
padding-inline-end: 10px;
}
.umap-main-edit-toolbox .umap-user:hover {
@ -195,14 +192,7 @@
z-index: var(--zindex-panels);
}
.umap-caption-bar-enabled .umap-caption-bar {
display: flex;
align-items: baseline;
}
.umap-caption-bar select {
margin-top: 0;
line-height: initial;
height: initial;
width: auto;
display: block;
}
.umap-caption-bar-enabled {
--current-footer-height: var(--footer-height);
@ -239,14 +229,3 @@
padding: 0;
margin: 0;
}
@media all and (max-width: 980px) {
.umap-main-edit-toolbox button span {
display: none;
}
}
@media all and (max-width: 770px) {
.umap-main-edit-toolbox .umap-help-link,
.umap-main-edit-toolbox .share-status {
display: none !important;
}
}

View file

@ -14,9 +14,6 @@
height: fit-content;
max-height: 90vh;
}
.umap-dialog ul + h4 {
margin-top: var(--box-margin);
}
:where([data-component="no-dialog"]:not([hidden])) {
display: block;
position: relative;

View file

@ -140,6 +140,7 @@ class uMapAlertConflict extends uMapAlert {
}
onAlertConflict(event) {
// biome-ignore lint/style/useNumberNamespace: Number.Infinity returns undefined by default
const {
level = 'info',
duration = Number.POSITIVE_INFINITY,

View file

@ -1,10 +1,10 @@
import { DomEvent, DomUtil, stamp } from '../../vendors/leaflet/leaflet-src.esm.js'
import { Form } from './form/builder.js'
import { EXPORT_FORMATS } from './formatter.js'
import { translate } from './i18n.js'
import * as Icon from './rendering/icon.js'
import ContextMenu from './ui/contextmenu.js'
import * as Utils from './utils.js'
import { EXPORT_FORMATS } from './formatter.js'
import ContextMenu from './ui/contextmenu.js'
import { Form } from './form/builder.js'
export default class Browser {
constructor(umap, leafletMap) {

View file

@ -1,6 +1,6 @@
import { uMapAlert as Alert } from '../components/alerts/alert.js'
import { translate } from './i18n.js'
import * as Utils from './utils.js'
import { uMapAlert as Alert } from '../components/alerts/alert.js'
const TEMPLATE = `
<div class="umap-caption">

View file

@ -1,22 +1,22 @@
import {
DomEvent,
DomUtil,
DomEvent,
stamp,
GeoJSON,
LineUtil,
stamp,
} from '../../../vendors/leaflet/leaflet-src.esm.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import { MutatingForm } from '../form/builder.js'
import * as Utils from '../utils.js'
import { SCHEMA } from '../schema.js'
import { translate } from '../i18n.js'
import loadPopup from '../rendering/popup.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import {
LeafletMarker,
LeafletPolygon,
LeafletPolyline,
LeafletPolygon,
MaskPolygon,
} from '../rendering/ui.js'
import { SCHEMA } from '../schema.js'
import * as Utils from '../utils.js'
import loadPopup from '../rendering/popup.js'
import { MutatingForm } from '../form/builder.js'
class Feature {
constructor(umap, datalayer, geojson = {}, id = null) {

View file

@ -1,25 +1,25 @@
// FIXME: this module should not depend on Leaflet
import {
DomEvent,
DomUtil,
GeoJSON,
DomEvent,
stamp,
GeoJSON,
} from '../../../vendors/leaflet/leaflet-src.esm.js'
import * as Utils from '../utils.js'
import { Default as DefaultLayer } from '../rendering/layers/base.js'
import { Cluster } from '../rendering/layers/cluster.js'
import { Heat } from '../rendering/layers/heat.js'
import { Categorized, Choropleth, Circles } from '../rendering/layers/classified.js'
import {
uMapAlert as Alert,
uMapAlertConflict as AlertConflict,
} from '../../components/alerts/alert.js'
import { MutatingForm } from '../form/builder.js'
import { translate } from '../i18n.js'
import { DataLayerPermissions } from '../permissions.js'
import { Default as DefaultLayer } from '../rendering/layers/base.js'
import { Categorized, Choropleth, Circles } from '../rendering/layers/classified.js'
import { Cluster } from '../rendering/layers/cluster.js'
import { Heat } from '../rendering/layers/heat.js'
import * as Schema from '../schema.js'
import { Point, LineString, Polygon } from './features.js'
import TableEditor from '../tableeditor.js'
import * as Utils from '../utils.js'
import { LineString, Point, Polygon } from './features.js'
import * as Schema from '../schema.js'
import { MutatingForm } from '../form/builder.js'
export const LAYER_TYPES = [
DefaultLayer,
@ -946,18 +946,12 @@ export class DataLayer {
this.propagateHide()
}
toggle(force) {
toggle() {
// From now on, do not try to how/hidedataChanged
// automatically this layer.
let display = force
this._forcedVisibility = true
if (force === undefined) {
if (!this.isVisible()) display = true
else display = false
}
if (display) this.show()
if (!this.isVisible()) this.show()
else this.hide()
this._umap.bottomBar.redraw()
}
zoomTo() {
@ -1245,7 +1239,7 @@ export class DataLayer {
this
)
}
DomEvent.on(toggle, 'click', () => this.toggle())
DomEvent.on(toggle, 'click', this.toggle, this)
DomEvent.on(zoomTo, 'click', this.zoomTo, this)
container.classList.add(this.getHidableClass())
container.classList.toggle('off', !this.isVisible())

View file

@ -1,7 +1,7 @@
import { translate } from '../i18n.js'
import { SCHEMA } from '../schema.js'
import * as Utils from '../utils.js'
import getClass from './fields.js'
import * as Utils from '../utils.js'
import { SCHEMA } from '../schema.js'
import { translate } from '../i18n.js'
export class Form extends Utils.WithEvents {
constructor(obj, fields, properties) {

View file

@ -1,12 +1,12 @@
import * as Utils from '../utils.js'
import { translate } from '../i18n.js'
import {
AjaxAutocomplete,
AjaxAutocompleteMultiple,
AutocompleteDatalist,
} from '../autocomplete.js'
import { translate } from '../i18n.js'
import * as Icon from '../rendering/icon.js'
import { SCHEMA } from '../schema.js'
import * as Utils from '../utils.js'
import * as Icon from '../rendering/icon.js'
const Fields = {}
@ -254,8 +254,8 @@ Fields.BlurInput = class extends Fields.Input {
const IntegerMixin = (Base) =>
class extends Base {
value() {
return !Number.isNaN(this.input.value) && this.input.value !== ''
? Number.parseInt(this.input.value, 10)
return !isNaN(this.input.value) && this.input.value !== ''
? parseInt(this.input.value, 10)
: undefined
}
@ -270,8 +270,8 @@ Fields.BlurIntInput = class extends IntegerMixin(Fields.BlurInput) {}
const FloatMixin = (Base) =>
class extends Base {
value() {
return !Number.isNaN(this.input.value) && this.input.value !== ''
? Number.parseFloat(this.input.value)
return !isNaN(this.input.value) && this.input.value !== ''
? parseFloat(this.input.value)
: undefined
}
@ -390,7 +390,7 @@ Fields.Select = class extends BaseElement {
Fields.IntSelect = class extends Fields.Select {
value() {
return Number.parseInt(super.value(), 10)
return parseInt(super.value(), 10)
}
}

View file

@ -1,6 +1,6 @@
import { uMapAlert as Alert } from '../components/alerts/alert.js'
/* Uses globals for: csv2geojson, osmtogeojson (not available as ESM) */
import { translate } from './i18n.js'
import { uMapAlert as Alert } from '../components/alerts/alert.js'
export const EXPORT_FORMATS = {
geojson: {

View file

@ -4,14 +4,14 @@ import {
AjaxAutocompleteMultiple,
AutocompleteDatalist,
} from './autocomplete.js'
import { LineString, Point, Polygon } from './data/features.js'
import { LAYER_TYPES } from './data/layer.js'
import Help from './help.js'
import * as Icon from './rendering/icon.js'
import { LeafletMarker, LeafletPolygon, LeafletPolyline } from './rendering/ui.js'
import { ServerRequest } from './request.js'
import { SCHEMA } from './schema.js'
import * as Utils from './utils.js'
import * as Icon from './rendering/icon.js'
import { LAYER_TYPES } from './data/layer.js'
import { Point, LineString, Polygon } from './data/features.js'
import { LeafletMarker, LeafletPolyline, LeafletPolygon } from './rendering/ui.js'
// Import modules and export them to the global scope.
// For the not yet module-compatible JS out there.

View file

@ -1,7 +1,7 @@
import { DomEvent, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from './i18n.js'
import Dialog from './ui/dialog.js'
import * as Utils from './utils.js'
import Dialog from './ui/dialog.js'
const SHORTCUTS = {
DRAW_MARKER: {
@ -135,23 +135,14 @@ const ENTRIES = {
<li>${translate('# one hash for main heading')}</li>
<li>${translate('## two hashes for second heading')}</li>
<li>${translate('### three hashes for third heading')}</li>
<li>${translate('--- for a horizontal rule')}</li>
</ul>
<h4>${translate('Links')}</h4>
<ul>
<li>${translate('Simple link: [[https://example.com]]')}</li>
<li>${translate('Link with text: [[https://example.com|text of the link]]')}</li>
</ul>
<h4>${translate('Images')}</h4>
<ul>
<li>${translate('Image: {{https://image.url.com}}')}</li>
<li>${translate('Image with custom width (in px): {{https://image.url.com|width}}')}</li>
</ul>
<h4>${translate('Iframes')}</h4>
<ul>
<li>${translate('Iframe: {{{https://iframe.url.com}}}')}</li>
<li>${translate('Iframe with custom height (in px): {{{https://iframe.url.com|height}}}')}</li>
<li>${translate('Iframe with custom height and width (in px): {{{https://iframe.url.com|height*width}}}')}</li>
<li>${translate('--- for a horizontal rule')}</li>
</ul>
</div>
`,

View file

@ -1,9 +1,9 @@
import { DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import { BaseAjax, SingleMixin } from '../autocomplete.js'
import { translate } from '../i18n.js'
import * as Utils from '../utils.js'
import { AutocompleteCommunes } from './communesfr.js'
import { translate } from '../i18n.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
const TEMPLATE = `
<div>

View file

@ -1,9 +1,9 @@
import { DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import { BaseAjax, SingleMixin } from '../autocomplete.js'
import { translate } from '../i18n.js'
import * as Util from '../utils.js'
import { AutocompleteCommunes } from './communesfr.js'
import { translate } from '../i18n.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
const TEMPLATE = `
<h3>Cadastre</h3>

View file

@ -15,7 +15,7 @@ export class AutocompleteCommunes extends SingleMixin(BaseAjax) {
let options = { q: encodeURIComponent(value) }
const re = /^(0[1-9]|[1-9][ABab\d])\d{3}$/gm
if (re.test(value)) {
url = 'https://geo.api.gouv.fr/communes?code={code}&limit=5'
url = "https://geo.api.gouv.fr/communes?code={code}&limit=5"
options = { code: encodeURIComponent(value) }
}
return Util.template(url, options)

View file

@ -1,8 +1,8 @@
import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
import { uMapAlert as Alert } from '../components/alerts/alert.js'
import { MutatingForm } from './form/builder.js'
import { translate } from './i18n.js'
import { uMapAlert as Alert } from '../components/alerts/alert.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.

View file

@ -1,11 +1,11 @@
import {
DivIcon,
DomEvent,
DomUtil,
DivIcon,
Icon,
} from '../../../vendors/leaflet/leaflet-src.esm.js'
import { SCHEMA } from '../schema.js'
import * as Utils from '../utils.js'
import { SCHEMA } from '../schema.js'
export function getClass(name) {
switch (name) {

View file

@ -1,9 +1,9 @@
import colorbrewer from '../../../../vendors/colorbrewer/colorbrewer.js'
import { DomUtil, FeatureGroup } from '../../../../vendors/leaflet/leaflet-src.esm.js'
import { FeatureGroup, DomUtil } from '../../../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from '../../i18n.js'
import { LayerMixin } from './base.js'
import * as Utils from '../../utils.js'
import { CircleMarker } from '../ui.js'
import { LayerMixin } from './base.js'
import colorbrewer from '../../../../vendors/colorbrewer/colorbrewer.js'
// Layer where each feature color is relative to the others,
// so we need all features before behing able to set one

View file

@ -1,10 +1,10 @@
import { Evented } from '../../../../vendors/leaflet/leaflet-src.esm.js'
// WARNING must be loaded dynamically, or at least after leaflet.markercluster
// Uses global L.MarkerCluster and L.MarkerClusterGroup, not exposed as ESM
import { translate } from '../../i18n.js'
import * as Utils from '../../utils.js'
import { Cluster as ClusterIcon } from '../icon.js'
import { LayerMixin } from './base.js'
import * as Utils from '../../utils.js'
import { Evented } from '../../../../vendors/leaflet/leaflet-src.esm.js'
import { Cluster as ClusterIcon } from '../icon.js'
const MarkerCluster = L.MarkerCluster.extend({
// Custom class so we can call computeTextColor

View file

@ -1,14 +1,14 @@
// Uses global L.HeatLayer, not exposed as ESM
import {
Bounds,
LatLng,
Marker,
LatLng,
latLngBounds,
Bounds,
point,
} from '../../../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from '../../i18n.js'
import * as Utils from '../../utils.js'
import { LayerMixin } from './base.js'
import * as Utils from '../../utils.js'
import { translate } from '../../i18n.js'
export const Heat = L.HeatLayer.extend({
statics: {

View file

@ -1,18 +1,18 @@
// Goes here all code related to Leaflet, DOM and user interactions.
import {
Map as BaseMap,
Control,
DomEvent,
DomUtil,
latLng,
DomEvent,
latLngBounds,
latLng,
Control,
setOptions,
} from '../../../vendors/leaflet/leaflet-src.esm.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import DropControl from '../drop.js'
import { translate } from '../i18n.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import * as Utils from '../utils.js'
import * as Icon from './icon.js'
import DropControl from '../drop.js'
// Those options are not saved on the server, so they can live here
// instead of in umap.properties

View file

@ -1,11 +1,11 @@
import {
Popup as BasePopup,
DomEvent,
DomUtil,
Path,
Popup as BasePopup,
} from '../../../vendors/leaflet/leaflet-src.esm.js'
import Browser from '../browser.js'
import loadTemplate from './template.js'
import Browser from '../browser.js'
export default function loadPopup(name) {
switch (name) {

View file

@ -1,8 +1,8 @@
import { DomEvent, DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { getLocale, translate } from '../i18n.js'
import { Request } from '../request.js'
import { DomUtil, DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { translate, getLocale } from '../i18n.js'
import * as Utils from '../utils.js'
import * as Icon from './icon.js'
import { Request } from '../request.js'
export default async function loadTemplate(name, feature, container) {
let klass = PopupTemplate

View file

@ -1,18 +1,18 @@
// Goes here all code related to Leaflet, DOM and user interactions.
import {
Marker,
Polyline,
Polygon,
CircleMarker as BaseCircleMarker,
DomEvent,
DomUtil,
LineUtil,
latLng,
LatLng,
LatLngBounds,
LineUtil,
Marker,
Polygon,
Polyline,
latLng,
DomEvent,
} from '../../../vendors/leaflet/leaflet-src.esm.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import { translate } from '../i18n.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import * as Utils from '../utils.js'
import * as Icon from './icon.js'

View file

@ -1,9 +1,9 @@
import { DomEvent, DomUtil, stamp } from '../../vendors/leaflet/leaflet-src.esm.js'
import { AutocompleteDatalist } from './autocomplete.js'
import { MutatingForm } from './form/builder.js'
import { translate } from './i18n.js'
import Orderable from './orderable.js'
import * as Utils from './utils.js'
import { AutocompleteDatalist } from './autocomplete.js'
import Orderable from './orderable.js'
import { MutatingForm } from './form/builder.js'
const EMPTY_VALUES = ['', undefined, null]

View file

@ -1,8 +1,8 @@
import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
import { MutatingForm } from './form/builder.js'
import { EXPORT_FORMATS } from './formatter.js'
import { translate } from './i18n.js'
import * as Utils from './utils.js'
import { MutatingForm } from './form/builder.js'
export default class Share {
constructor(umap) {

View file

@ -18,7 +18,6 @@ export default class Slideshow extends WithTemplate {
this._umap = umap
this._id = null
this.CLASSNAME = 'umap-slideshow-active'
this._umap.properties.slideshow ??= {}
this.load()
this._current = null

View file

@ -1,6 +1,5 @@
import * as Utils from '../utils.js'
import { HybridLogicalClock } from './hlc.js'
import { UndoManager } from './undo.js'
import {
DataLayerUpdater,
FeatureUpdater,
@ -9,6 +8,7 @@ import {
DataLayerPermissionsUpdater,
} from './updaters.js'
import { WebSocketTransport } from './websocket.js'
import { UndoManager } from './undo.js'
// Start reconnecting after 2 seconds, then double the delay each time
// maxing out at 32 seconds.

View file

@ -1,8 +1,8 @@
import { DomEvent, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
import { MutatingForm } from './form/builder.js'
import { translate } from './i18n.js'
import ContextMenu from './ui/contextmenu.js'
import { WithTemplate, loadTemplate } from './utils.js'
import { MutatingForm } from './form/builder.js'
const TEMPLATE = `
<table>

View file

@ -1,24 +1,16 @@
import { DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { LineString, Point, Polygon } from '../data/features.js'
import { translate } from '../i18n.js'
import { WithTemplate } from '../utils.js'
import * as Utils from '../utils.js'
import ContextMenu from './contextmenu.js'
import * as Utils from '../utils.js'
import { Point, LineString, Polygon } from '../data/features.js'
const TOP_BAR_TEMPLATE = `
<div class="umap-main-edit-toolbox with-transition dark">
<div class="umap-left-edit-toolbox" data-ref="left">
<div class="logo"><a class="" href="/" title="${translate('Go to the homepage')}">uMap</a></div>
<button class="map-name flat truncate" type="button" data-ref="name"></button>
<button class="share-status flat truncate" type="button" data-ref="share"></button>
<button class="edit-undo round" type="button" data-ref="undo" disabled>
<i class="icon icon-16 icon-undo"></i>
<span>${translate('Undo')}</span>
</button>
<button class="edit-redo round" type="button" data-ref="redo" disabled>
<i class="icon icon-16 icon-redo"></i>
<span>${translate('Redo')}</span>
</button>
<button class="map-name flat" type="button" data-ref="name"></button>
<button class="share-status flat" type="button" data-ref="share"></button>
</div>
<div class="umap-right-edit-toolbox" data-ref="right">
<button class="connected-peers round" type="button" data-ref="peers">
@ -27,12 +19,20 @@ const TOP_BAR_TEMPLATE = `
</button>
<button class="umap-user flat" type="button" data-ref="user">
<i class="icon icon-16 icon-profile"></i>
<span class="username truncate" data-ref="username"></span>
<span class="username" data-ref="username"></span>
</button>
<button class="umap-help-link flat" type="button" title="${translate('Help')}" data-ref="help">${translate('Help')}</button>
<button class="edit-undo round" type="button" data-ref="undo" disabled>
<i class="icon icon-16 icon-undo"></i>
<span class="">${translate('Undo')}</span>
</button>
<button class="edit-redo round" type="button" data-ref="redo" disabled>
<i class="icon icon-16 icon-redo"></i>
<span class="">${translate('Redo')}</span>
</button>
<button class="edit-disable round disabled-on-dirty" type="button" data-ref="view">
<i class="icon icon-16 icon-eye"></i>
<span>${translate('View')}</span>
<span class="">${translate('View')}</span>
</button>
<button class="edit-save button round enabled-on-dirty" type="button" data-ref="save">
<i class="icon icon-16 icon-save"></i>
@ -173,7 +173,6 @@ const BOTTOM_BAR_TEMPLATE = `
<button class="umap-about-link flat" type="button" title="${translate('Open caption')}" data-ref="caption">${translate('Open caption')}</button>
<button class="umap-open-browser-link flat" type="button" title="${translate('Browse data')}" data-ref="browse">${translate('Browse data')}</button>
<button class="umap-open-browser-link flat" type="button" title="${translate('Filter data')}" data-ref="filter">${translate('Filter data')}</button>
<select data-ref=layers></select>
</div>
`
@ -196,14 +195,6 @@ export class BottomBar extends WithTemplate {
this._umap.openBrowser('filters')
)
this._slideshow.renderToolbox(this.element)
this.elements.layers.addEventListener('change', () => {
const select = this.elements.layers
const selected = select.options[select.selectedIndex].value
if (!selected) return
this._umap.eachDataLayer((datalayer) => {
datalayer.toggle(datalayer.id === selected)
})
})
this.redraw()
}
@ -216,27 +207,6 @@ export class BottomBar extends WithTemplate {
this.elements.caption.hidden = !showMenus
this.elements.browse.hidden = !showMenus
this.elements.filter.hidden = !showMenus || !this._umap.properties.facetKey
this.buildDataLayerSwitcher()
}
buildDataLayerSwitcher() {
this.elements.layers.innerHTML = ''
const datalayers = this._umap.datalayersIndex.filter((d) => d.options.inCaption)
if (datalayers.length < 2) {
this.elements.layers.hidden = true
} else {
this.elements.layers.appendChild(Utils.loadTemplate(`<option value=""></option>`))
this.elements.layers.hidden = false
const visible = datalayers.filter((datalayer) => datalayer.isVisible())
for (const datalayer of datalayers) {
const selected = visible.length === 1 && datalayer.isVisible() ? 'selected' : ''
this.elements.layers.appendChild(
Utils.loadTemplate(
`<option value="${datalayer.id}" ${selected}>${datalayer.getName()}</option>`
)
)
}
}
}
}

View file

@ -1,7 +1,7 @@
import { DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from '../i18n.js'
import * as Utils from '../utils.js'
import { Positioned } from './base.js'
import * as Utils from '../utils.js'
export default class Tooltip extends Positioned {
constructor(parent) {

View file

@ -2,37 +2,36 @@ import {
DomUtil,
Util as LeafletUtil,
latLngBounds,
stamp,
} from '../../vendors/leaflet/leaflet-src.esm.js'
import {
uMapAlert as Alert,
uMapAlertCreation as AlertCreation,
} from '../components/alerts/alert.js'
import Browser from './browser.js'
import Caption from './caption.js'
import { translate, setLocale, getLocale } from './i18n.js'
import * as Utils from './utils.js'
import { SyncEngine } from './sync/engine.js'
import { LeafletMap } from './rendering/map.js'
import URLs from './urls.js'
import { Panel, EditPanel, FullPanel } from './ui/panel.js'
import Dialog from './ui/dialog.js'
import { BottomBar, TopBar, EditBar } from './ui/bar.js'
import Tooltip from './ui/tooltip.js'
import ContextMenu from './ui/contextmenu.js'
import { Request, ServerRequest } from './request.js'
import Help from './help.js'
import { Formatter } from './formatter.js'
import Slideshow from './slideshow.js'
import { MapPermissions } from './permissions.js'
import { SCHEMA } from './schema.js'
import { DataLayer } from './data/layer.js'
import Facets from './facets.js'
import { MutatingForm } from './form/builder.js'
import { Formatter } from './formatter.js'
import Help from './help.js'
import { getLocale, setLocale, translate } from './i18n.js'
import Browser from './browser.js'
import Caption from './caption.js'
import Importer from './importer.js'
import Orderable from './orderable.js'
import { MapPermissions } from './permissions.js'
import { LeafletMap } from './rendering/map.js'
import { Request, ServerRequest } from './request.js'
import Rules from './rules.js'
import { SCHEMA } from './schema.js'
import Share from './share.js'
import Slideshow from './slideshow.js'
import { SyncEngine } from './sync/engine.js'
import { BottomBar, EditBar, TopBar } from './ui/bar.js'
import ContextMenu from './ui/contextmenu.js'
import Dialog from './ui/dialog.js'
import { EditPanel, FullPanel, Panel } from './ui/panel.js'
import Tooltip from './ui/tooltip.js'
import URLs from './urls.js'
import * as Utils from './utils.js'
import {
uMapAlertCreation as AlertCreation,
uMapAlert as Alert,
} from '../components/alerts/alert.js'
import Orderable from './orderable.js'
import { MutatingForm } from './form/builder.js'
export default class Umap {
constructor(element, geojson) {

View file

@ -368,13 +368,10 @@ export function isDataImage(value) {
* characters and no diacritics.
*/
export function normalize(s) {
return (
(s || '')
return (s || '')
.toLowerCase()
.normalize('NFD')
// biome-ignore lint/suspicious/noMisleadingCharacterClass: <explanation>
.replace(/[\u0300-\u036f]/g, '')
)
}
// Vendorized from leaflet.utils

View file

@ -660,6 +660,10 @@ a.umap-control-caption,
.umap-caption .header i.icon {
flex-shrink: 0;
}
.umap-caption hgroup p,
.umap-caption hgroup button {
margin: 0;
}
.umap-browser .main-toolbox {
padding-left: 4px; /* Align with toolbox below */
border-top: 1px solid var(--color-mediumGray);

View file

@ -45,7 +45,6 @@
--box-margin: 14px;
--text-margin: 7px;
--dialog-width: 40vw;
--map-fragment-height: 210px;
/* z-indexes (leaflet CSS sets the map at 400 by default) */
--zindex-alert: 500;

View file

@ -13,7 +13,7 @@
</h2>
</div>
<div class="wrapper">
<div class="row grid-container">
<div class="map_list row">
{% if maps %}
{% include "umap/map_list.html" %}
{% else %}

View file

@ -13,7 +13,7 @@
</h2>
</div>
<div class="wrapper">
<div class="grid-container row">
<div class="map_list row">
{% if maps %}
{% include "umap/map_list.html" %}
{% else %}

View file

@ -43,27 +43,22 @@
<script type="text/javascript">
window.addEventListener('DOMContentLoaded', event => {
const server = new U.ServerRequest()
const getMore = async function (link) {
const container = link.parentNode
container.removeChild(link)
const [{html}, response, error] = await server.get(link.href)
const getMore = async function (e) {
L.DomEvent.stop(e)
const [{html}, response, error] = await server.get(this.href)
if (!error) {
const template = document.createElement('template')
template.innerHTML = html
container.appendChild(template.content)
listenForMore()
}
}
const listenForMore = () => {
const container = this.parentNode
container.innerHTML = html
const more = document.querySelector('.more_button')
if (more) {
L.DomEvent.on(more, 'click', (e) => {
L.DomEvent.stop(e)
getMore(more)
})
L.DomEvent.on(more, 'click', getMore, more)
}
}
listenForMore()
}
const more = document.querySelector('.more_button')
if (more) {
L.DomEvent.on(more, 'click', getMore, more)
}
})
</script>
{% endblock bottom_js %}

View file

@ -20,14 +20,12 @@
{% endif %}
<div class="wrapper">
{% if maps %}
<div class="row">
<h2 class="section">
{% blocktrans %}Get inspired, browse maps{% endblocktrans %}
</h2>
<div class="grid-container">
<div class="map_list row">
{% include "umap/map_list.html" %}
</div>
</div>
{% endif %}
</div>
{% endblock maincontent %}

View file

@ -1,19 +1,22 @@
{% load umap_tags i18n %}
{% for map_inst in maps %}
<div>
<hr />
<div class="col wide">
{% map_fragment map_inst prefix=prefix page=request.GET.p %}
<hgroup>
<h3><a href="{{ map_inst.get_absolute_url }}">{{ map_inst.name }}</a></h3>
<div class="legend">
<a href="{{ map_inst.get_absolute_url }}">{{ map_inst.name }}</a>
{% with author=map_inst.get_author %}
{% if author %}
<p>{% trans "by" %} <a href="{{ author.get_url }}">{{ author }}</a></p>
<em>{% trans "by" %} <a href="{{ author.get_url }}">{{ author }}</a></em>
{% endif %}
{% endwith %}
</hgroup>
</div>
</div>
{% endfor %}
{% if maps.has_next %}
<div class="col wide">
<a href="?{% paginate_querystring maps.next_page_number %}"
class="button more_button neutral">{% trans "More" %}</a>
</div>
{% endif %}

View file

@ -12,7 +12,7 @@
{% block maincontent %}
{% include "umap/search_bar.html" %}
<div class="wrapper">
<div class="row">
<div class="map_list row">
{% if request.GET.q %}
{% if maps %}
<h2>
@ -22,9 +22,7 @@
{{ count }} maps found:
{% endblocktranslate %}
</h2>
<div class="grid-container">
{% include "umap/map_list.html" with prefix="search_map" %}
</div>
{% else %}
<h2>
{% trans "No map found." %}
@ -34,9 +32,7 @@
<h2>
{% trans "Latest created maps" %}
</h2>
<div class="grid-container">
{% include "umap/map_list.html" with prefix="search_map" %}
</div>
{% endif %}
</div>
</div>

View file

@ -18,7 +18,7 @@
{% endif %}
</div>
</div>
<div class="grid-container row">
<div class="map_list row">
{% if maps %}
{% include "umap/map_list.html" %}
{% else %}

View file

@ -180,7 +180,7 @@ def test_can_restore_version(live_server, openmap, page, datalayer):
page.get_by_role("button", name="Manage layers").click()
page.locator(".panel.right").get_by_title("Edit", exact=True).click()
page.get_by_text("Versions").click()
page.get_by_title("Restore this version").last.click()
page.get_by_role("button", name="Restore this version").last.click()
page.get_by_role("button", name="OK").click()
expect(marker).to_have_class(re.compile(".*umap-ball-icon.*"))

View file

@ -86,8 +86,8 @@ def test_umap_import_from_textarea(live_server, tilelayer, page, settings):
expect(page.locator(".umap-main-edit-toolbox .map-name")).to_have_text(
"Imported map"
)
expect(page.locator(".panel.left").get_by_text("Tunnels")).to_be_visible()
expect(page.locator(".panel.left").get_by_text("Cities")).to_be_visible()
expect(page.get_by_text("Tunnels")).to_be_visible()
expect(page.get_by_text("Cities")).to_be_visible()
expect(page.locator(".leaflet-control-minimap")).to_be_visible()
expect(
page.locator('img[src="https://tile.openstreetmap.fr/hot/6/32/21.png"]')

View file

@ -477,23 +477,23 @@ def test_should_sync_datalayers_delete(new_page, asgi_live_server, tilelayer):
peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
peerA.get_by_role("button", name="Open browser").click()
expect(peerA.locator(".panel").get_by_text("datalayer 1")).to_be_visible()
expect(peerA.locator(".panel").get_by_text("datalayer 2")).to_be_visible()
expect(peerA.get_by_text("datalayer 1")).to_be_visible()
expect(peerA.get_by_text("datalayer 2")).to_be_visible()
peerB.get_by_role("button", name="Open browser").click()
expect(peerB.locator(".panel").get_by_text("datalayer 1")).to_be_visible()
expect(peerB.locator(".panel").get_by_text("datalayer 2")).to_be_visible()
expect(peerB.get_by_text("datalayer 1")).to_be_visible()
expect(peerB.get_by_text("datalayer 2")).to_be_visible()
# Delete "datalayer 2" in peerA
peerA.locator(".datalayer").get_by_role("button", name="Delete layer").first.click()
peerA.get_by_role("button", name="OK").click()
expect(peerA.locator(".panel").get_by_text("datalayer 2")).to_be_hidden()
expect(peerB.locator(".panel").get_by_text("datalayer 2")).to_be_hidden()
expect(peerA.get_by_text("datalayer 2")).to_be_hidden()
expect(peerB.get_by_text("datalayer 2")).to_be_hidden()
# Save delete to the server
with peerA.expect_response(re.compile(".*/datalayer/delete/.*")):
peerA.get_by_role("button", name="Save").click()
expect(peerA.locator(".panel").get_by_text("datalayer 2")).to_be_hidden()
expect(peerB.locator(".panel").get_by_text("datalayer 2")).to_be_hidden()
expect(peerA.get_by_text("datalayer 2")).to_be_hidden()
expect(peerB.get_by_text("datalayer 2")).to_be_hidden()
@pytest.mark.xdist_group(name="websockets")