diff --git a/docs-users/fr/tutorials/2-first-map.md b/docs-users/fr/tutorials/2-first-map.md index 21570f99..4e6480ad 100644 --- a/docs-users/fr/tutorials/2-first-map.md +++ b/docs-users/fr/tutorials/2-first-map.md @@ -89,9 +89,7 @@ texte est affiché en haut de la carte, comme celui ci-dessous : data-alt="Message d’alerte contenant le lien d’édition." data-width="790" data-height="226" - data-selector="#umap-alert-container" - data-wait-for="document.querySelector('#umap-alert-container .umap-alert-actions')" - data-javascript="document.querySelector('button.leaflet-control-edit-save').click()" + data-selector='umap-alert-creation [role="dialog"]' >Message d’alerte contenant le lien d’édition. Ce texte explique que vous venez de créer une carte **anonyme** et vous @@ -99,7 +97,9 @@ donne un lien (une URL) pour pouvoir modifier la carte. En effet la carte que vous avez créée n’est associée à aucun compte, et **uMap** considère que seules les personnes ayant ce *lien secret* peuvent la modifier. Vous devez donc conserver ce lien si vous souhaitez pouvoir -modifier la carte. Nous verrons dans [le prochain tutoriel](3-create-account.md) +modifier la carte ou saisir votre adresse de courriel pour le recevoir. + +Nous verrons dans [le prochain tutoriel](3-create-account.md) comment créer son catalogue de cartes en utilisant un compte, il n’est alors pas nécessaire de conserver de lien secret. diff --git a/docs-users/static/tutoriels/create-map-alert.png b/docs-users/static/tutoriels/create-map-alert.png index d9930086..3537f5cb 100644 Binary files a/docs-users/static/tutoriels/create-map-alert.png and b/docs-users/static/tutoriels/create-map-alert.png differ diff --git a/umap/settings/base.py b/umap/settings/base.py index 7d926869..df56ccd7 100644 --- a/umap/settings/base.py +++ b/umap/settings/base.py @@ -182,6 +182,7 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, + "DIRS": [os.path.join(PROJECT_DIR, STATIC_ROOT)], "OPTIONS": { "context_processors": ( "django.contrib.auth.context_processors.auth", diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index ded44fc6..a015e3a2 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -266,6 +266,11 @@ button.flat, min-height: inherit; width: initial; display: initial; + line-height: inherit; +} +button.flat:hover, +[type="button"].flat:hover, +.dark [type="button"].flat:hover { text-decoration: underline; } .help-text, .helptext { diff --git a/umap/static/umap/content.css b/umap/static/umap/content.css index bab0a051..24d384c1 100644 --- a/umap/static/umap/content.css +++ b/umap/static/umap/content.css @@ -300,7 +300,7 @@ ul.umap-autocomplete { } .messages .error { - background-color: #c60f13; + background-color: var(--color-red); } diff --git a/umap/static/umap/css/alert.css b/umap/static/umap/css/alert.css deleted file mode 100644 index 05833a55..00000000 --- a/umap/static/umap/css/alert.css +++ /dev/null @@ -1,75 +0,0 @@ -#umap-alert-container { - min-height: 46px; - line-height: 46px; - padding-left: 10px; - width: calc(100% - 500px); - position: absolute; - top: -46px; - left: 250px; /* Keep save/cancel button accessible. */ - right: 250px; - box-shadow: 0 1px 7px #999999; - visibility: hidden; - background: none repeat scroll 0 0 rgba(20, 22, 23, 0.8); - font-weight: bold; - color: #fff; - font-size: 0.8em; - z-index: 1012; - border-radius: 2px; -} -#umap-alert-container.error { - background-color: #c60f13; -} -.umap-alert #umap-alert-container { - visibility: visible; - top: 23px; -} -#umap-alert-container .umap-action { - margin-left: 10px; - background-color: #fff; - color: #000; - padding: 5px; - border-radius: 4px; -} -#umap-alert-container .umap-action:hover { - color: #000; -} -#umap-alert-container .error .umap-action { - background-color: #666; - color: #eee; -} -#umap-alert-container .error .umap-action:hover { - color: #fff; -} -#umap-alert-container input { - padding: 5px; - border-radius: 4px; - width: 100%; -} -#umap-alert-container .umap-close-link { - color: #fff; - float: right; - padding-right: 10px; - width: 100px; - line-height: 1; - margin: .5rem; - background-color: #202425; - font-size: .7rem; -} -#umap-alert-container .umap-close-icon { - background-position: -74px -55px; -} -#umap-alert-container .umap-alert-actions { - display: flex; - margin: 1rem; -} -#umap-alert-container .umap-alert-actions .umap-action { - margin-bottom: 0; -} - -@media all and (orientation:portrait) { - #umap-alert-container { - width: 100%; - left: 0; - right: 0; - } -} diff --git a/umap/static/umap/css/dialog.css b/umap/static/umap/css/dialog.css index b83061f6..328c1ac2 100644 --- a/umap/static/umap/css/dialog.css +++ b/umap/static/umap/css/dialog.css @@ -10,6 +10,7 @@ background-color: var(--background-color); color: var(--text-color); border-radius: 5px; + overflow-y: auto; } .umap-dialog .umap-close-link { float: right; diff --git a/umap/static/umap/css/panel.css b/umap/static/umap/css/panel.css index 9591615e..f4ea420d 100644 --- a/umap/static/umap/css/panel.css +++ b/umap/static/umap/css/panel.css @@ -25,7 +25,7 @@ } .panel.full.on { visibility: visible; - right: var(--panel-gutter); + right: calc(var(--panel-gutter) * 2 + var(--control-size)); left: var(--panel-gutter); height: initial; max-height: initial; @@ -41,41 +41,6 @@ height: calc(100% - var(--panel-header-height)); /* Minus size of toolbox */ padding: var(--panel-gutter); } -.panel .toolbox { - padding: 5px 10px; - overflow: hidden; - display: flex; - flex-direction: row-reverse; - font-size: 10px; - justify-content: flex-start; - gap: 5px; - line-height: 2.2em; - background-color: #fff; - position: sticky; - top: 0; - height: var(--panel-header-height); -} -.panel.dark .toolbox { - background-color: var(--color-darkGray); -} -.panel .toolbox li { - cursor: pointer; - display: inline; - padding: 0 2px; - border: 1px solid #b6b6b3; - border-radius: 2px; -} -.panel.dark .toolbox -.panel.dark .toolbox li { - color: #d3dfeb; - border: 1px solid #202425; -} -.panel .toolbox li:hover { - background-color: #d4d4d2; -} -.panel.dark .toolbox li:hover { - background-color: #353c3e; -} .panel h3 { line-height: 120%; } diff --git a/umap/static/umap/css/window.css b/umap/static/umap/css/window.css new file mode 100644 index 00000000..a4768fbd --- /dev/null +++ b/umap/static/umap/css/window.css @@ -0,0 +1,35 @@ +.window .buttons { + padding: 5px 10px; + display: flex; + flex-direction: row-reverse; + font-size: 10px; + justify-content: flex-start; + gap: 5px; + line-height: 2.2em; + background-color: inherit; + position: sticky; + top: 0; + height: var(--panel-header-height); +} +.window .buttons li { + cursor: pointer; + display: inline; + padding: 0 2px; + border: 1px solid #b6b6b3; + border-radius: 2px; +} +.window.dark .buttons +.window.dark .buttons li { + color: #d3dfeb; + border: 1px solid #202425; +} +.window.dark[data-level="error"] .buttons li:hover, +.window.dark[data-level="error"] .buttons li button:hover { + background-color: darkred; +} +.window .buttons li:hover { + background-color: #d4d4d2; +} +.window.dark .buttons li:hover { + background-color: #353c3e; +} diff --git a/umap/static/umap/js/components/alerts/alert.css b/umap/static/umap/js/components/alerts/alert.css new file mode 100644 index 00000000..70f0ba2d --- /dev/null +++ b/umap/static/umap/js/components/alerts/alert.css @@ -0,0 +1,122 @@ +[role="dialog"] { + box-sizing: border-box; + min-height: 46px; + line-height: 46px; + padding: var(--panel-gutter); + position: absolute; + box-shadow: 0 1px 7px #999999; + background: none repeat scroll 0 0 var(--color-darkGray); + font-weight: bold; + color: #fff; + font-size: 0.8em; + z-index: 1012; + border-radius: 2px; + margin-top: calc(var(--header-height) + var(--panel-gutter)); + display: flex; + justify-content: space-between; + align-items: flex-start; + left: 50%; + transform: translate(-50%, 0); + min-width: 80%; +} +[role="dialog"][data-level="error"] { + background-color: var(--color-red); +} +[role="dialog"] a { + text-decoration: underline; +} +[role="dialog"] > div { + margin: 0 auto; +} +[role="group"] { + display: inline-flex; + position: relative; + width: 100%; + border-radius: var(--border-radius); + vertical-align: middle; + margin: 0; + padding: 0; + border: none; +} +[role="group"] input:not([type="checkbox"], [type="radio"]):not(:last-child), +[role="group"] > :not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +[role="group"] input:not([type="checkbox"], [type="radio"]):not(:first-child), +[role="group"] > :not(:first-child) { + margin-left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + width: 33%; +} +[role="group"] input[type="submit"] { + background: var(--color-darkGray); + color: var(--color-light); + border: 1px solid var(--color-light); + line-height: initial; +} +[role="group"] input:not([type="submit"]) { + background: var(--color-light); + color: var(--color-darkGray); + border: 1px solid var(--color-light); +} +[role="group"] input[type="button"] { + background: var(--color-darkGray); + color: var(--color-light); + border: none; + line-height: initial; +} +[role="group"] input[type="button"]:hover { + text-decoration: underline; + border: none; +} +@media only screen and (max-width:770px) { + [role="group"] { + display: flex; + flex-direction: column; + } + [role="group"] input:not([type="checkbox"], [type="radio"]):not(:last-child), + [role="group"] > :not(:last-child) { + border-radius: var(--border-radius); + } + [role="group"] input:not([type="checkbox"], [type="radio"]):not(:first-child), + [role="group"] > :not(:first-child) { + border-radius: var(--border-radius); + } + + [role="group"] input:not([type="checkbox"], [type="radio"]):not(:first-child), + [role="group"] > :not(:first-child) { + width: 100%; + } +} +#link-wrapper { + margin-bottom: 1rem; +} +#conflict-wrapper form { + display: flex; + justify-content: space-around; +} +#conflict-wrapper form [type="submit"] { + width: initial; + background: inherit; + color: var(--color-light); + border: 1px solid var(--color-red); + font-weight: bold; +} +#conflict-wrapper form [type="submit"]:hover { + width: initial; + background: darkred; + color: var(--color-light); + border: 1px solid var(--color-light); +} +@media only screen and (max-width:770px) { + #conflict-wrapper form { + flex-direction: column; + text-align: center; + } +} +umap-alert-choice a { + color: var(--color-light); + text-decoration: underline; +} diff --git a/umap/static/umap/js/components/alerts/alert.html b/umap/static/umap/js/components/alerts/alert.html new file mode 100644 index 00000000..b65cfdde --- /dev/null +++ b/umap/static/umap/js/components/alerts/alert.html @@ -0,0 +1,88 @@ +{% load i18n static %} + + + + + + + + + + + + + + + + diff --git a/umap/static/umap/js/components/alerts/alert.js b/umap/static/umap/js/components/alerts/alert.js new file mode 100644 index 00000000..5daf4cce --- /dev/null +++ b/umap/static/umap/js/components/alerts/alert.js @@ -0,0 +1,160 @@ +import { translate } from '../../modules/i18n.js' +import { ServerRequest } from '../../modules/request.js' +import { uMapElement } from '../base.js' + +class uMapAlert extends uMapElement { + static get observedAttributes() { + return ['open'] + } + + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case 'open': + newValue === 'open' ? this._show() : this._hide() + break + } + } + + static info(message, duration = 5000) { + uMapAlert.emit('alert', { message, duration }) + } + + // biome-ignore lint/style/useNumberNamespace: Number.Infinity returns undefined by default + static error(message, duration = Infinity) { + uMapAlert.emit('alert', { level: 'error', message, duration }) + } + + constructor() { + super() + this._hide() + this.container = this.querySelector('[role="dialog"]') + this.element = this.container.querySelector('[role="alert"]') + } + + _hide() { + this.setAttribute('hidden', 'hidden') + this.removeAttribute('open') + } + + _show() { + this.removeAttribute('hidden') + } + + _handleClose() { + this.addEventListener('click', (event) => { + if (event.target.closest('[data-close]')) { + this._hide() + } + }) + } + + onAlert(event) { + const { level = 'info', duration = 5000, message = '' } = event.detail + this.container.dataset.level = level + this.container.dataset.duration = duration + this.element.textContent = message + this.setAttribute('open', 'open') + if (Number.isFinite(duration)) { + setTimeout(() => { + this._hide() + }, duration) + } + } + + connectedCallback() { + this._handleClose() + this.listen('alert') + } +} + +class uMapAlertCreation extends uMapAlert { + static info( + message, + // biome-ignore lint/style/useNumberNamespace: Number.Infinity returns undefined by default + duration = Infinity, + editLink = undefined, + sendLink = undefined + ) { + uMapAlertCreation.emit('alertCreation', { message, duration, editLink, sendLink }) + } + + constructor() { + super() + this.linkWrapper = this.container.querySelector('#link-wrapper') + this.formWrapper = this.container.querySelector('#form-wrapper') + } + + onAlertCreation(event) { + const { + level = 'info', + duration = 5000, + message = '', + editLink = undefined, + sendLink = undefined, + } = event.detail + uMapAlert.prototype.onAlert.call(this, { detail: { level, duration, message } }) + this.linkWrapper.querySelector('input[type="url"]').value = editLink + const button = this.linkWrapper.querySelector('input[type="button"]') + button.addEventListener('click', (event) => { + event.preventDefault() + L.Util.copyToClipboard(editLink) + event.target.value = translate('✅ Copied!') + }) + if (sendLink) { + this.formWrapper.removeAttribute('hidden') + const form = this.formWrapper.querySelector('form') + form.addEventListener('submit', async (event) => { + event.preventDefault() + const formData = new FormData(form) + const server = new ServerRequest() + this.removeAttribute('open') + await server.post(sendLink, {}, formData) + }) + } + } + + connectedCallback() { + this._handleClose() + this.listen('alertCreation') + } +} + +class uMapAlertConflict extends uMapAlert { + static error( + message, + // biome-ignore lint/style/useNumberNamespace: Number.Infinity returns undefined by default + duration = Infinity + ) { + uMapAlertConflict.emit('alertConflict', { level: 'error', message, duration }) + } + + constructor() { + super() + this.conflictWrapper = this.container.querySelector('#conflict-wrapper') + } + + onAlertConflict(event) { + const { level = 'info', duration = 5000, message = '' } = event.detail + uMapAlert.prototype.onAlert.call(this, { detail: { level, duration, message } }) + const form = this.conflictWrapper.querySelector('form') + form.addEventListener('submit', (event) => { + event.preventDefault() + switch (event.submitter.id) { + case 'your-changes': + uMapAlertConflict.emit('alertConflictOverride') + break + case 'their-changes': + window.location.reload() + break + } + this.removeAttribute('open') + }) + } + + connectedCallback() { + this._handleClose() + this.listen('alertConflict') + } +} + +export { uMapAlert, uMapAlertCreation, uMapAlertConflict } diff --git a/umap/static/umap/js/components/base.js b/umap/static/umap/js/components/base.js new file mode 100644 index 00000000..0d2aaefc --- /dev/null +++ b/umap/static/umap/js/components/base.js @@ -0,0 +1,54 @@ +const EVENT_PREFIX = 'umap' + +export class uMapElement extends HTMLElement { + static emit(type, detail = {}) { + const event = new CustomEvent(`${EVENT_PREFIX}:${type}`, { + bubbles: true, + cancelable: true, + detail: detail, + }) + return document.dispatchEvent(event) + } + + /** + * Retrieves a clone of the content template either using the `template` + * attribute or an id mathing the name of the component: + * + * `umap-alert` component => `umap-alert-template` template id lookup. + */ + get template() { + return document + .getElementById(this.getAttribute('template') || `${this.localName}-template`) + .content.cloneNode(true) + } + + constructor() { + super() + this.append(this.template) + } + + /** + * Special method which allows to easily listen to events + * and have automated event to component method binding. + * + * For instance listening to `alert` will then call `onAlert`. + */ + handleEvent(event) { + event.preventDefault() + // From `umap:alert` to `alert`. + const eventName = event.type.replace(`${EVENT_PREFIX}:`, '') + // From `alert` event type to `onAlert` call against that class. + this[`on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`](event) + } + + listen(eventName) { + // Using `this` as a listener will call `handleEvent` under the hood. + document.addEventListener(`${EVENT_PREFIX}:${eventName}`, this) + } +} + +export function register(klass, name) { + if ('customElements' in globalThis && !customElements.get(name)) { + customElements.define(name, klass) + } +} diff --git a/umap/static/umap/js/modules/autocomplete.js b/umap/static/umap/js/modules/autocomplete.js index 3c2949a1..4fde8b84 100644 --- a/umap/static/umap/js/modules/autocomplete.js +++ b/umap/static/umap/js/modules/autocomplete.js @@ -1,7 +1,6 @@ import { DomUtil, DomEvent, setOptions } from '../../vendors/leaflet/leaflet-src.esm.js' import { translate } from './i18n.js' import { ServerRequest } from './request.js' -import Alert from './ui/alert.js' export class BaseAutocomplete { constructor(el, options) { @@ -220,8 +219,7 @@ export class BaseAutocomplete { class BaseAjax extends BaseAutocomplete { constructor(el, options) { super(el, options) - const alert = new Alert(document.querySelector('header')) - this.server = new ServerRequest(alert) + this.server = new ServerRequest() } optionToResult(option) { return { diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index d158a40b..f42ce016 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -3,7 +3,6 @@ import Browser from './browser.js' import Facets from './facets.js' import Caption from './caption.js' import { Panel, EditPanel, FullPanel } from './ui/panel.js' -import Alert from './ui/alert.js' import Dialog from './ui/dialog.js' import Tooltip from './ui/tooltip.js' import * as Utils from './utils.js' @@ -14,6 +13,11 @@ import Orderable from './orderable.js' import Importer from './importer.js' import Help from './help.js' import { SyncEngine } from './sync/engine.js' +import { + uMapAlert as Alert, + uMapAlertCreation as AlertCreation, + uMapAlertConflict as AlertConflict, +} from '../components/alerts/alert.js' // Import modules and export them to the global scope. // For the not yet module-compatible JS out there. @@ -21,6 +25,8 @@ import { SyncEngine } from './sync/engine.js' // By alphabetic order window.U = { Alert, + AlertCreation, + AlertConflict, AjaxAutocomplete, AjaxAutocompleteMultiple, Browser, diff --git a/umap/static/umap/js/modules/importer.js b/umap/static/umap/js/modules/importer.js index ea017552..889d7ec3 100644 --- a/umap/static/umap/js/modules/importer.js +++ b/umap/static/umap/js/modules/importer.js @@ -1,5 +1,6 @@ import { DomUtil, DomEvent } from '../../vendors/leaflet/leaflet-src.esm.js' import { translate } from './i18n.js' +import { uMapAlert as Alert } from '../components/alerts/alert.js' export default class Importer { constructor(map) { @@ -163,16 +164,14 @@ export default class Importer { this.map.processFileToImport(file, layer, type) } } else { - if (!type) - return this.map.alert.open({ - content: translate('Please choose a format'), - level: 'error', - }) + if (!type) { + return Alert.error(L._('Please choose a format')) + } if (this.rawInput.value && type === 'umap') { try { this.map.importRaw(this.rawInput.value, type) } catch (e) { - this.alert.open({ content: translate('Invalid umap data'), level: 'error' }) + Alert.error(L._('Invalid umap data')) console.error(e) } } else { diff --git a/umap/static/umap/js/modules/request.js b/umap/static/umap/js/modules/request.js index fe392871..07d10cca 100644 --- a/umap/static/umap/js/modules/request.js +++ b/umap/static/umap/js/modules/request.js @@ -1,5 +1,5 @@ -// Uses `L._`` from Leaflet.i18n which we cannot import as a module yet -import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js' +import { translate } from './i18n.js' +import { uMapAlert as Alert } from '../components/alerts/alert.js' export class RequestError extends Error {} @@ -47,11 +47,6 @@ class BaseRequest { // In case of error, an alert is sent, but non 20X status are not handled // The consumer must check the response status by hand export class Request extends BaseRequest { - constructor(alert) { - super() - this.alert = alert - } - fire(name, params) { document.body.dispatchEvent(new CustomEvent(name, params)) } @@ -85,7 +80,7 @@ export class Request extends BaseRequest { } _onError(error) { - this.alert.open({ content: L._('Problem in the response'), level: 'error' }) + Alert.error(translate('Problem in the response')) } _onNOK(error) { @@ -131,9 +126,9 @@ export class ServerRequest extends Request { try { const data = await response.json() if (data.info) { - this.alert.open({ content: data.info, level: 'info' }) + Alert.info(data.info) } else if (data.error) { - this.alert.open({ content: data.error, level: 'error' }) + Alert.error(data.error) return this._onError(new Error(data.error)) } return [data, response, null] @@ -148,10 +143,7 @@ export class ServerRequest extends Request { _onNOK(error) { if (error.status === 403) { - this.alert.open({ - content: error.message || L._('Action not allowed :('), - level: 'error', - }) + Alert.error(error.message || translate('Action not allowed :(')) } return [{}, error.response, error] } diff --git a/umap/static/umap/js/modules/ui/alert.js b/umap/static/umap/js/modules/ui/alert.js deleted file mode 100644 index b02c2d82..00000000 --- a/umap/static/umap/js/modules/ui/alert.js +++ /dev/null @@ -1,82 +0,0 @@ -import { DomUtil, DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js' -import { translate } from '../i18n.js' - -const ALERTS = [] -let ALERT_ID = null - -export default class Alert { - constructor(parent) { - this.parent = parent - this.container = DomUtil.create('div', 'with-transition', this.parent) - this.container.id = 'umap-alert-container' - DomEvent.disableClickPropagation(this.container) - DomEvent.on(this.container, 'contextmenu', DomEvent.stopPropagation) // Do not activate our custom context menu. - DomEvent.on(this.container, 'wheel', DomEvent.stopPropagation) - DomEvent.on(this.container, 'MozMousePixelScroll', DomEvent.stopPropagation) - } - - open(params) { - if (DomUtil.hasClass(this.parent, 'umap-alert')) ALERTS.push(params) - else this._open(params) - } - - _open(params) { - if (!params) { - if (ALERTS.length) params = ALERTS.pop() - else return - } - let timeoutID - const level_class = params.level && params.level == 'info' ? 'info' : 'error' - this.container.innerHTML = '' - DomUtil.addClass(this.parent, 'umap-alert') - DomUtil.addClass(this.container, level_class) - const close = () => { - if (timeoutID && timeoutID !== ALERT_ID) { - return - } // Another alert has been forced - this.container.innerHTML = '' - DomUtil.removeClass(this.parent, 'umap-alert') - DomUtil.removeClass(this.container, level_class) - if (timeoutID) window.clearTimeout(timeoutID) - this._open() - } - const closeButton = DomUtil.createButton( - 'umap-close-link', - this.container, - '', - close, - this - ) - DomUtil.create('i', 'umap-close-icon', closeButton) - const label = DomUtil.create('span', '', closeButton) - label.title = label.textContent = translate('Close') - DomUtil.element({ - tagName: 'div', - innerHTML: params.content, - parent: this.container, - }) - let action, el, input - const form = DomUtil.create('div', 'umap-alert-actions', this.container) - for (let action of params.actions || []) { - if (action.input) { - input = DomUtil.element({ - tagName: 'input', - parent: form, - className: 'umap-alert-input', - placeholder: action.input, - }) - } - el = DomUtil.createButton( - 'umap-action', - form, - action.label, - action.callback, - action.callbackContext - ) - DomEvent.on(el, 'click', close, this) - } - if (params.duration !== Infinity) { - ALERT_ID = timeoutID = window.setTimeout(close, params.duration || 3000) - } - } -} diff --git a/umap/static/umap/js/modules/ui/dialog.js b/umap/static/umap/js/modules/ui/dialog.js index 6833fd13..24aa825a 100644 --- a/umap/static/umap/js/modules/ui/dialog.js +++ b/umap/static/umap/js/modules/ui/dialog.js @@ -4,7 +4,7 @@ import { translate } from '../i18n.js' export default class Dialog { constructor(parent) { this.parent = parent - this.container = DomUtil.create('dialog', 'umap-dialog', this.parent) + this.container = DomUtil.create('dialog', 'umap-dialog window', this.parent) DomEvent.disableClickPropagation(this.container) DomEvent.on(this.container, 'contextmenu', DomEvent.stopPropagation) // Do not activate our custom context menu. DomEvent.on(this.container, 'wheel', DomEvent.stopPropagation) @@ -26,15 +26,14 @@ export default class Dialog { if (className) { this.container.classList.add(className) } - const closeButton = DomUtil.createButton( - 'umap-close-link', - this.container, - '', - () => this.container.close() + const buttonsContainer = DomUtil.create('ul', 'buttons', this.container) + const closeButton = DomUtil.createButtonIcon( + DomUtil.create('li', '', buttonsContainer), + 'icon-close', + translate('Close') ) - DomUtil.createIcon(closeButton, 'icon-close') - const label = DomUtil.create('span', '', closeButton) - label.title = label.textContent = translate('Close') + DomEvent.on(closeButton, 'click', this.close, this) + this.container.appendChild(buttonsContainer) this.container.appendChild(content) } } diff --git a/umap/static/umap/js/modules/ui/panel.js b/umap/static/umap/js/modules/ui/panel.js index 0aa04b05..e236e82a 100644 --- a/umap/static/umap/js/modules/ui/panel.js +++ b/umap/static/umap/js/modules/ui/panel.js @@ -25,27 +25,34 @@ export class Panel { } open({ content, className, actions = [] } = {}) { - this.container.className = `with-transition panel ${this.classname} ${this.mode || ''}` + this.container.className = `with-transition panel window ${this.classname} ${ + this.mode || '' + }` this.container.innerHTML = '' - const actionsContainer = DomUtil.create('ul', 'toolbox', this.container) + const actionsContainer = DomUtil.create('ul', 'buttons', this.container) const body = DomUtil.create('div', 'body', this.container) body.appendChild(content) - const closeLink = DomUtil.create('li', 'umap-close-link', actionsContainer) - DomUtil.add('i', 'icon icon-16 icon-close', closeLink) - closeLink.title = translate('Close') - const resizeLink = DomUtil.create('li', 'umap-resize-link', actionsContainer) - DomUtil.add('i', 'icon icon-16 icon-resize', resizeLink) - resizeLink.title = translate('Toggle size') - for (let action of actions) { - actionsContainer.appendChild(action) + const closeButton = DomUtil.createButtonIcon( + DomUtil.create('li', '', actionsContainer), + 'icon-close', + translate('Close') + ) + const resizeButton = DomUtil.createButtonIcon( + DomUtil.create('li', '', actionsContainer), + 'icon-resize', + translate('Toggle size') + ) + for (const action of actions) { + const element = DomUtil.element({ tagName: 'li', parent: actionsContainer }) + element.appendChild(action) } if (className) DomUtil.addClass(body, className) const promise = new Promise((resolve, reject) => { DomUtil.addClass(this.container, 'on') resolve() }) - DomEvent.on(closeLink, 'click', this.close, this) - DomEvent.on(resizeLink, 'click', this.resize, this) + DomEvent.on(closeButton, 'click', this.close, this) + DomEvent.on(resizeButton, 'click', this.resize, this) return promise } diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 4ae4ffbc..435f8e3a 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -84,7 +84,7 @@ U.UpdateExtentAction = U.BaseAction.extend({ }, addHooks: function () { - this.map.updateExtent() + this.map.setCenterAndZoom() }, }) @@ -1086,7 +1086,7 @@ U.Search = L.PhotonSearch.extend({ if (latlng.isValid()) { this.reverse.doReverse(latlng) } else { - this.map.alert.open({ content: 'Invalid latitude or longitude', mode: 'error' }) + U.Alert.error(L._('Invalid latitude or longitude')) } return } diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index a4cf8f1d..b669df4e 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -764,10 +764,7 @@ U.Marker = L.Marker.extend({ const builder = new U.FormBuilder(this, coordinatesOptions, { callback: function () { if (!this._latlng.isValid()) { - this.map.alert.open({ - content: L._('Invalid latitude or longitude'), - level: 'error', - }) + U.Alert.error(L._('Invalid latitude or longitude')) builder.resetField('_latlng.lat') builder.resetField('_latlng.lng') } @@ -966,7 +963,7 @@ U.PathMixin = { items.push({ text: L._('Display measure'), callback: function () { - this.map.alert.open({ content: this.getMeasure(), level: 'info' }) + U.Alert.info(this.getMeasure()) }, context: this, }) diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index ce455434..9cc41025 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -13,7 +13,7 @@ L.Map.mergeOptions({ // we cannot rely on this because of the y is overriden by Leaflet // See https://github.com/Leaflet/Leaflet/pull/9201 // And let's remove this -y when this PR is merged and released. - demoTileInfos: { 's': 'a', 'z': 9, 'x': 265, 'y': 181, '-y': 181, 'r': '' }, + demoTileInfos: { s: 'a', z: 9, x: 265, y: 181, '-y': 181, r: '' }, licences: [], licence: '', enableMarkerDraw: true, @@ -59,7 +59,6 @@ U.Map = L.Map.extend({ this.urls = new U.URLs(this.options.urls) this.panel = new U.Panel(this) - this.alert = new U.Alert(this._controlContainer) this.tooltip = new U.Tooltip(this._controlContainer) this.dialog = new U.Dialog(this._controlContainer) if (this.hasEditMode()) { @@ -68,8 +67,8 @@ U.Map = L.Map.extend({ } L.DomEvent.on(document.body, 'dataloading', (e) => this.fire('dataloading', e)) L.DomEvent.on(document.body, 'dataload', (e) => this.fire('dataload', e)) - this.server = new U.ServerRequest(this.alert) - this.request = new U.Request(this.alert) + this.server = new U.ServerRequest() + this.request = new U.Request() this.initLoader() this.name = this.options.name @@ -391,7 +390,7 @@ U.Map = L.Map.extend({ icon: 'umap-fake-class', iconLoading: 'umap-fake-class', flyTo: this.options.easing, - onLocationError: (err) => this.alert.open({ content: err.message }), + onLocationError: (err) => U.Alert.error(err.message), }) this._controls.fullscreen = new L.Control.Fullscreen({ title: { false: L._('View Fullscreen'), true: L._('Exit Fullscreen') }, @@ -680,10 +679,7 @@ U.Map = L.Map.extend({ } catch (e) { console.error(e) this.removeLayer(tilelayer) - this.alert.open({ - content: `${L._('Error in the tilelayer URL')}: ${tilelayer._url}`, - level: 'error', - }) + U.Alert.error(`${L._('Error in the tilelayer URL')}: ${tilelayer._url}`) // Users can put tilelayer URLs by hand, and if they add wrong {variable}, // Leaflet throw an error, and then the map is no more editable } @@ -715,10 +711,7 @@ U.Map = L.Map.extend({ } catch (e) { this.removeLayer(overlay) console.error(e) - this.alert.open({ - content: `${L._('Error in the overlay URL')}: ${overlay._url}`, - level: 'error', - }) + U.Alert.error(`${L._('Error in the overlay URL')}: ${overlay._url}`) } }, @@ -835,19 +828,16 @@ U.Map = L.Map.extend({ return this.getDefaultOption(option) }, - updateExtent: function () { + setCenterAndZoom: function () { + this._setCenterAndZoom() + U.Alert.info(L._('The zoom and center have been modified.')) + }, + + _setCenterAndZoom: function () { this.options.center = this.getCenter() this.options.zoom = this.getZoom() this.isDirty = true this._default_extent = false - if (this.options.umap_id) { - // We do not want an extra message during the map creation - // to avoid the double notification/alert. - this.alert.open({ - content: L._('The zoom and center have been modified.'), - level: 'info', - }) - } }, updateTileLayers: function () { @@ -886,12 +876,11 @@ U.Map = L.Map.extend({ processFileToImport: function (file, layer, type) { type = type || U.Utils.detectFileType(file) if (!type) { - this.alert.open({ - content: L._('Unable to detect format of file {filename}', { + U.Alert.error( + L._('Unable to detect format of file {filename}', { filename: file.name, - }), - level: 'error', - }) + }) + ) return } if (type === 'umap') { @@ -947,10 +936,7 @@ U.Map = L.Map.extend({ self.importRaw(rawData) } catch (e) { console.error('Error importing data', e) - self.alert.open({ - content: L._('Invalid umap data in {filename}', { filename: file.name }), - level: 'error', - }) + U.Alert.error(L._('Invalid umap data in {filename}', { filename: file.name })) } } }, @@ -1058,62 +1044,59 @@ U.Map = L.Map.extend({ 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) { - let duration = 3000, - alert = { content: L._('Map has been saved!'), level: 'info' } - if (!this.options.umap_id) { - alert.content = L._('Congratulations, your map has been created!') - this.options.umap_id = data.id - this.permissions.setOptions(data.permissions) - this.permissions.commit() - if (data.permissions && data.permissions.anonymous_edit_url) { - alert.duration = Infinity - alert.content = - L._( - 'Your map has been created! As you are not logged in, here is your secret link to edit the map, please keep it safe:' - ) + `
${data.permissions.anonymous_edit_url}` + if (error) { + return + } - alert.actions = [ - { - label: L._('Copy link'), - callback: () => { - L.Util.copyToClipboard(data.permissions.anonymous_edit_url) - this.alert.open({ - content: L._('Secret edit link copied to clipboard!'), - level: 'info', - }) - }, - callbackContext: this, - }, - ] - if (this.options.urls.map_send_edit_link) { - alert.actions.push({ - label: L._('Send me the link'), - input: L._('Email'), - callback: this.sendEditLink, - callbackContext: this, - }) - } - } - } else if (!this.permissions.isDirty) { + if (!this.options.umap_id) { + this.options.umap_id = data.id + this.permissions.setOptions(data.permissions) + this.permissions.commit() + if (data?.permissions?.anonymous_edit_url) { + const send_edit_link_url = + this.options.urls.map_send_edit_link && + this.urls.get('map_send_edit_link', { + map_id: this.options.umap_id, + }) + this.once('saved', () => { + U.AlertCreation.info( + L._( + 'Your map has been created! As you are not logged in, ' + + 'here is your secret link to edit the map, please keep it safe:' + ), + Number.Infinity, + data.permissions.anonymous_edit_url, + send_edit_link_url + ) + }) + } else { + this.once('saved', () => { + U.Alert.info(L._('Congratulations, your map has been created!')) + }) + } + } else { + if (!this.permissions.isDirty) { // Do not override local changes to permissions, // but update in case some other editors changed them in the meantime. this.permissions.setOptions(data.permissions) this.permissions.commit() } - // Update URL in case the name has changed. - if (history && history.pushState) - history.pushState({}, this.options.name, data.url) - else window.location = data.url - alert.content = data.info || alert.content - this.once('saved', () => this.alert.open(alert)) - this.permissions.save() + this.once('saved', () => { + U.Alert.info(data.info || L._('Map has been saved!')) + }) } + // Update URL in case the name has changed. + if (history?.pushState) { + history.pushState({}, this.options.name, data.url) + } else { + window.location = data.url + } + this.permissions.save() }, save: function () { if (!this.isDirty) return - if (this._default_extent) this.updateExtent() + if (this._default_extent) this._setCenterAndZoom() this.backup() this.once('saved', () => { this.isDirty = false @@ -1126,33 +1109,20 @@ U.Map = L.Map.extend({ } }, - sendEditLink: async function () { - const input = this.alert.container.querySelector('input') - const email = input.value - - const formData = new FormData() - formData.append('email', email) - - const url = this.urls.get('map_send_edit_link', { map_id: this.options.umap_id }) - await this.server.post(url, {}, formData) - }, - star: async function () { - if (!this.options.umap_id) - return this.alert.open({ - content: L._('Please save the map first'), - level: 'error', - }) + if (!this.options.umap_id) { + return U.Alert.error(L._('Please save the map first')) + } const url = this.urls.get('map_star', { map_id: this.options.umap_id }) const [data, response, error] = await this.server.post(url) - if (!error) { - this.options.starred = data.starred - let msg = data.starred - ? L._('Map has been starred') - : L._('Map has been unstarred') - this.alert.open({ content: msg, level: 'info' }) - this.renderControls() + if (error) { + return } + this.options.starred = data.starred + U.Alert.info( + data.starred ? L._('Map has been starred') : L._('Map has been unstarred') + ) + this.renderControls() }, geometry: function () { diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index 064287a7..faf2d5d4 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -958,7 +958,7 @@ U.DataLayer = L.Evented.extend({ const doc = new DOMParser().parseFromString(x, 'text/xml') const errorNode = doc.querySelector('parsererror') if (errorNode) { - this.map.alert.open({ content: L._('Cannot parse data'), level: 'error' }) + U.Alert.error(L._('Cannot parse data')) } return doc } @@ -993,7 +993,7 @@ U.DataLayer = L.Evented.extend({ message: err[0].message, }) } - this.map.alert.open({ content: message, level: 'error', duration: 10000 }) + U.Alert.error(message, 10000) console.error(err) } if (result && result.features.length) { @@ -1020,7 +1020,7 @@ U.DataLayer = L.Evented.extend({ const gj = JSON.parse(c) callback(gj) } catch (err) { - this.map.alert.open({ content: `Invalid JSON file: ${err}` }) + U.Alert.error(`Invalid JSON file: ${err}`) return } } @@ -1121,12 +1121,11 @@ U.DataLayer = L.Evented.extend({ return this.geojsonToFeatures(geometry.geometries) default: - this.map.alert.open({ - content: L._('Skipping unknown geometry.type: {type}', { + U.Alert.error( + L._('Skipping unknown geometry.type: {type}', { type: geometry.type || 'undefined', - }), - level: 'error', - }) + }) + ) } }, @@ -1467,17 +1466,19 @@ U.DataLayer = L.Evented.extend({ '_blank' ) } - const button = L.DomUtil.create('li', '') - L.DomUtil.create('i', 'icon icon-16 icon-back', button) - button.title = L._('Back to layers') + const backButton = L.DomUtil.createButtonIcon( + undefined, + 'icon-back', + L._('Back to layers') + ) // Fixme: remove me when this is merged and released // https://github.com/Leaflet/Leaflet/pull/9052 - L.DomEvent.disableClickPropagation(button) - L.DomEvent.on(button, 'click', this.map.editDatalayers, this.map) + L.DomEvent.disableClickPropagation(backButton) + L.DomEvent.on(backButton, 'click', this.map.editDatalayers, this.map) this.map.editPanel.open({ content: container, - actions: [button], + actions: [backButton], }) }, @@ -1706,27 +1707,14 @@ U.DataLayer = L.Evented.extend({ const [data, response, error] = await this.map.server.post(url, headers, formData) if (error) { if (response && response.status === 412) { - const msg = L._( - 'Woops! Someone else seems to have edited the data. You can save anyway, but this will erase the changes made by others.' + U.AlertConflict.error( + L._( + 'Whoops! Other contributor(s) changed some of the same map elements as you. ' + + 'This situation is tricky, you have to choose carefully which version is pertinent.' + ) ) - const actions = [ - { - label: L._('Save anyway'), - callback: async () => { - // Save again, - // but do not pass the reference version this time - await this._trySave(url, {}, formData) - }, - }, - { - label: L._('Cancel'), - }, - ] - this.map.alert.open({ - content: msg, - level: 'error', - duration: 100000, - actions: actions, + document.addEventListener('umap:alertConflictOverride', async (event) => { + await this._trySave(url, {}, formData) }) } } else { diff --git a/umap/static/umap/js/umap.permissions.js b/umap/static/umap/js/umap.permissions.js index 32b3a977..4fb19295 100644 --- a/umap/static/umap/js/umap.permissions.js +++ b/umap/static/umap/js/umap.permissions.js @@ -52,11 +52,9 @@ U.MapPermissions = L.Class.extend({ edit: function () { if (this.map.options.editMode !== 'advanced') return - if (!this.map.options.umap_id) - return this.map.alert.open({ - content: L._('Please save the map first'), - level: 'info', - }) + if (!this.map.options.umap_id) { + return U.Alert.info(L._('Please save the map first')) + } const container = L.DomUtil.create('div', 'permissions-panel') const fields = [] L.DomUtil.createTitle(container, L._('Update permissions'), 'icon-key') @@ -140,10 +138,7 @@ U.MapPermissions = L.Class.extend({ const [data, response, error] = await this.map.server.post(this.getAttachUrl()) if (!error) { this.options.owner = this.map.options.user - this.map.alert.open({ - content: L._('Map has been attached to your account'), - level: 'info', - }) + U.Alert.info(L._('Map has been attached to your account')) this.map.editPanel.close() } }, diff --git a/umap/static/umap/js/umap.tableeditor.js b/umap/static/umap/js/umap.tableeditor.js index 36515a60..5cbaa034 100644 --- a/umap/static/umap/js/umap.tableeditor.js +++ b/umap/static/umap/js/umap.tableeditor.js @@ -83,10 +83,7 @@ U.TableEditor = L.Class.extend({ validateName: function (name) { if (name.indexOf('.') !== -1) { - this.datalayer.map.alert.open({ - content: L._('Invalide property name: {name}', { name: name }), - level: 'error', - }) + U.Alert.error(L._('Invalide property name: {name}', { name: name })) return false } return true @@ -98,10 +95,13 @@ U.TableEditor = L.Class.extend({ this.renderHeaders() this.body.innerHTML = '' this.datalayer.eachLayer(this.renderRow, this) - const addButton = L.DomUtil.create('li', 'add-property') - L.DomUtil.createIcon(addButton, 'icon-add') - const label = L.DomUtil.create('span', '', addButton) - label.textContent = label.title = L._('Add a new property') + const addButton = L.DomUtil.createButton( + 'flat', + undefined, + L._('Add a new property') + ) + const iconElement = L.DomUtil.createIcon(addButton, 'icon-add') + addButton.insertBefore(iconElement, addButton.firstChild) const addProperty = function () { const newName = prompt(L._('Please enter the name of the property')) if (!newName || !this.validateName(newName)) return diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index e17d8d92..d36c2cf9 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -659,14 +659,14 @@ ul.photon-autocomplete { } .umap-caption-bar-enabled .umap-caption-bar { display: block; - height: var(--header-height); + height: var(--footer-height); background-color: #fff; width: 100%; position: absolute; left: 0; bottom: 0; right: 0; - padding: 0 0 0 5px; + padding: var(--gutter); text-align: left; line-height: 100%; cursor: auto; diff --git a/umap/static/umap/vars.css b/umap/static/umap/vars.css index 8d76d200..f2023841 100644 --- a/umap/static/umap/vars.css +++ b/umap/static/umap/vars.css @@ -8,6 +8,7 @@ --color-limeGreen: #b9f5d2; --color-brightCyan: #46ece6; --color-lightCyan: #d4fbf9; + --color-red: #c60f13; --background-color: var(--color-light); --color-accent: var(--color-brightCyan); @@ -20,15 +21,17 @@ --button-neutral-color: var(--color-darkGray); /* Sizes and spaces */ + --gutter: 8px; --panel-gutter: 10px; --panel-bottom: 40px; --panel-header-height: 36px; --panel-width: 400px; --header-height: 46px; --current-header-height: 0px; - --footer-height: 46px; + --footer-height: 28px; --current-footer-height: 0px; --control-size: 36px; + --border-radius: 4px; } .dark { --background-color: var(--color-darkGray); diff --git a/umap/templates/umap/content.html b/umap/templates/umap/content.html index 01e3ed43..49f035a1 100644 --- a/umap/templates/umap/content.html +++ b/umap/templates/umap/content.html @@ -38,8 +38,7 @@ {{ block.super }} diff --git a/umap/templates/umap/messages.html b/umap/templates/umap/messages.html index c2d19217..6eefee47 100644 --- a/umap/templates/umap/messages.html +++ b/umap/templates/umap/messages.html @@ -1,11 +1,9 @@ -
-
- {% if messages %} - - {% endif %} -
-
+{% load i18n %} + +{% include "umap/js/components/alerts/alert.html" %} + +{% for message in messages %} + +{% endfor %} diff --git a/umap/tests/base.py b/umap/tests/base.py index e1abba32..9491dada 100644 --- a/umap/tests/base.py +++ b/umap/tests/base.py @@ -127,6 +127,8 @@ class DataLayerFactory(factory.django.DjangoModelFactory): data.setdefault("_umap_options", {}) kwargs["settings"]["name"] = kwargs["name"] data["_umap_options"]["name"] = kwargs["name"] + data.setdefault("type", "FeatureCollection") + data.setdefault("features", []) kwargs["geojson"] = ContentFile(json.dumps(data), "foo.json") return kwargs diff --git a/umap/tests/integration/conftest.py b/umap/tests/integration/conftest.py index b808a707..a0b4a04b 100644 --- a/umap/tests/integration/conftest.py +++ b/umap/tests/integration/conftest.py @@ -7,6 +7,11 @@ import pytest from playwright.sync_api import expect +@pytest.fixture(scope="session") +def browser_context_args(browser_context_args): + return {**browser_context_args, "locale": "en-GB", "timezone_id": "Europe/Paris"} + + @pytest.fixture(autouse=True) def set_timeout(context): timeout = int(os.environ.get("PLAYWRIGHT_TIMEOUT", 7500)) diff --git a/umap/tests/integration/test_anonymous_owned_map.py b/umap/tests/integration/test_anonymous_owned_map.py index 523589c2..b0d4d255 100644 --- a/umap/tests/integration/test_anonymous_owned_map.py +++ b/umap/tests/integration/test_anonymous_owned_map.py @@ -164,7 +164,7 @@ def test_alert_message_after_create( page.goto(f"{live_server.url}/en/map/new") save = page.get_by_role("button", name="Save") expect(save).to_be_visible() - alert = page.locator("#umap-alert-container") + alert = page.locator('umap-alert-creation div[role="dialog"]') expect(alert).to_be_hidden() with page.expect_response(re.compile(r".*/map/create/")): save.click() @@ -194,14 +194,15 @@ def test_alert_message_after_create( def test_email_sending_error_are_catched(tilelayer, page, live_server): page.goto(f"{live_server.url}/en/map/new") - alert = page.locator("#umap-alert-container") + alert_creation = page.locator('umap-alert-creation div[role="dialog"]') with page.expect_response(re.compile(r".*/map/create/")): page.get_by_role("button", name="Save").click() - alert.get_by_placeholder("Email").fill("foo@bar.com") + alert_creation.get_by_placeholder("Email").fill("foo@bar.com") with patch("umap.views.send_mail", side_effect=SMTPException) as patched: with page.expect_response(re.compile("/en/map/.*/send-edit-link/")): - alert.get_by_role("button", name="Send me the link").click() + alert_creation.get_by_role("button", name="Send me the link").click() assert patched.called + alert = page.locator('umap-alert div[role="dialog"]') expect(alert.get_by_text("Can't send email to foo@bar.com")).to_be_visible() @@ -214,7 +215,7 @@ def test_alert_message_after_create_show_link_even_without_mail( page.goto(f"{live_server.url}/en/map/new") with page.expect_response(re.compile(r".*/map/create/")): page.get_by_role("button", name="Save").click() - alert = page.locator("#umap-alert-container") + alert = page.locator('umap-alert-creation div[role="dialog"]') expect(alert).to_be_visible() expect( alert.get_by_text( diff --git a/umap/tests/integration/test_facets_browser.py b/umap/tests/integration/test_facets_browser.py index c386cc3c..4ba0b2c5 100644 --- a/umap/tests/integration/test_facets_browser.py +++ b/umap/tests/integration/test_facets_browser.py @@ -228,7 +228,7 @@ def test_facets_search_are_persistent_when_closing_panel(live_server, page, map) DataLayerFactory(map=map, data=DATALAYER_DATA1) DataLayerFactory(map=map, data=DATALAYER_DATA2) page.goto(f"{live_server.url}{map.get_absolute_url()}#6/48.948/1.670") - panel = page.locator(".umap-browser") + panel = page.locator(".panel.left") # Facet values odd = page.get_by_label("odd") @@ -266,7 +266,7 @@ def test_facets_search_are_persistent_when_closing_panel(live_server, page, map) # Close panel expect(panel.locator("summary")).to_have_attribute("data-badge", " ") expect(page.locator(".umap-control-browse")).to_have_attribute("data-badge", " ") - page.get_by_role("listitem", name="Close").click() + panel.get_by_role("button", name="Close").click() page.get_by_role("button", name="See layers").click() expect(panel.get_by_label("Min")).to_have_value("13") expect(panel.get_by_label("Min")).to_have_attribute("data-modified", "true") diff --git a/umap/tests/integration/test_import.py b/umap/tests/integration/test_import.py index ac56d0ae..de7fd972 100644 --- a/umap/tests/integration/test_import.py +++ b/umap/tests/integration/test_import.py @@ -448,4 +448,4 @@ def test_import_csv_without_valid_latlon_headers(tilelayer, live_server, page): # FIXME do not create a layer expect(layers).to_have_count(1) expect(markers).to_have_count(0) - expect(page.locator("#umap-alert-container")).to_be_visible() + expect(page.locator('umap-alert div[data-level="error"]')).to_be_visible() diff --git a/umap/tests/integration/test_optimistic_merge.py b/umap/tests/integration/test_optimistic_merge.py index ee7a1e6a..6fad8653 100644 --- a/umap/tests/integration/test_optimistic_merge.py +++ b/umap/tests/integration/test_optimistic_merge.py @@ -288,9 +288,9 @@ def test_should_display_alert_on_conflict(context, live_server, datalayer, openm saved = DataLayer.objects.last() data = json.loads(Path(saved.geojson.path).read_text()) assert data["features"][0]["properties"]["name"] == "new name" - expect(page_two.get_by_text("Woops! Someone else seems to")).to_be_visible() + expect(page_two.get_by_text("Whoops! Other contributor(s) changed")).to_be_visible() with page_two.expect_response(re.compile(r".*/datalayer/update/.*")): - page_two.get_by_role("button", name="Save anyway").click() + page_two.get_by_text("Keep your changes and loose theirs").click() saved = DataLayer.objects.last() data = json.loads(Path(saved.geojson.path).read_text()) assert data["features"][0]["properties"]["name"] == "custom name" diff --git a/umap/tests/integration/test_picto.py b/umap/tests/integration/test_picto.py index b8541f8b..d4b38954 100644 --- a/umap/tests/integration/test_picto.py +++ b/umap/tests/integration/test_picto.py @@ -171,7 +171,7 @@ def test_can_use_remote_url_as_picto(openmap, live_server, page, pictos): input_el.blur() expect(marker).to_have_attribute("src", "https://foo.bar/img.jpg") # Now close and reopen the form, it should still be the URL tab - close = page.locator(".panel.right.on .toolbox").get_by_title("Close") + close = page.locator(".panel.right.on .buttons").get_by_title("Close") expect(close).to_be_visible() close.click() edit_settings.click() @@ -210,7 +210,7 @@ def test_can_use_char_as_picto(openmap, live_server, page, pictos): expect(marker).to_have_count(1) expect(marker).to_have_text("♩") # Now close and reopen the form, it should still be the URL tab - close = page.locator(".panel.right.on .toolbox").get_by_title("Close") + close = page.locator(".panel.right.on .buttons").get_by_title("Close") expect(close).to_be_visible() close.click() edit_settings.click() diff --git a/umap/views.py b/umap/views.py index 9a3e2dd2..e53b382c 100644 --- a/umap/views.py +++ b/umap/views.py @@ -906,6 +906,7 @@ class MapDelete(DeleteView): return HttpResponseForbidden(_("Only its owner can delete the map.")) self.object.delete() home_url = reverse("home") + messages.info(self.request, _("Map successfully deleted.")) if is_ajax(self.request): return simple_json_response(redirect=home_url) else: @@ -1109,7 +1110,7 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView): reference = json.loads(f.read()) break else: - # If the document is not found, we can't merge. + # If the reference document is not found, we can't merge. return None # New data received in the request. incoming = json.loads(self.request.FILES["geojson"].read())