diff --git a/addons/website/static/src/js/editor/snippets.editor.js b/addons/website/static/src/js/editor/snippets.editor.js index f1d3bba6c25..53626fa1db9 100644 --- a/addons/website/static/src/js/editor/snippets.editor.js +++ b/addons/website/static/src/js/editor/snippets.editor.js @@ -73,22 +73,47 @@ const wSnippetMenu = weSnippetEditor.SnippetsMenu.extend({ * if not already defined. * * @private - * @param {boolean} [reconfigure=false] - * @param {boolean} [onlyIfUndefined=false] + * @param {boolean} [alwaysReconfigure=false] + * @param {boolean} [configureIfNecessary=false] */ - async _configureGMapAPI({reconfigure, onlyIfUndefined}) { + async _configureGMapAPI({alwaysReconfigure, configureIfNecessary}) { + if (!alwaysReconfigure && !configureIfNecessary) { + // TODO should review, parameters are weird... only one necessary? + return false; + } + const apiKey = await new Promise(resolve => { this.getParent().trigger_up('gmap_api_key_request', { onSuccess: key => resolve(key), }); }); - if (!reconfigure && (apiKey || !onlyIfUndefined)) { + const apiKeyValidation = apiKey ? await this._validateGMapAPIKey(apiKey) : { + isValid: false, + message: undefined, + }; + if (!alwaysReconfigure && configureIfNecessary && apiKey && apiKeyValidation.isValid) { return false; } + let websiteId; this.trigger_up('context_get', { callback: ctx => websiteId = ctx['website_id'], }); + + function applyError(message) { + const $apiKeyInput = this.find('#api_key_input'); + const $apiKeyHelp = this.find('#api_key_help'); + $apiKeyInput.addClass('is-invalid'); + $apiKeyHelp.empty().text(message); + } + + const $content = $(qweb.render('website.s_google_map_modal', { + apiKey: apiKey, + })); + if (!apiKeyValidation.isValid && apiKeyValidation.message) { + applyError.call($content, apiKeyValidation.message); + } + return new Promise(resolve => { let invalidated = false; const dialog = new Dialog(this, { @@ -96,55 +121,57 @@ const wSnippetMenu = weSnippetEditor.SnippetsMenu.extend({ title: _t("Google Map API Key"), buttons: [ {text: _t("Save"), classes: 'btn-primary', click: async (ev) => { - const $apiKeyInput = dialog.$('#api_key_input'); - const valueAPIKey = $apiKeyInput.val(); - const $apiKeyHelp = dialog.$('#api_key_help'); + const valueAPIKey = dialog.$('#api_key_input').val(); if (!valueAPIKey) { - $apiKeyInput.addClass('is-invalid'); - $apiKeyHelp.text(_t("Enter an API Key")); + applyError.call(dialog.$el, _t("Enter an API Key")); return; } const $button = $(ev.currentTarget); $button.prop('disabled', true); - try { - const response = await fetch(`https://maps.googleapis.com/maps/api/staticmap?center=belgium&size=10x10&key=${valueAPIKey}`); - if (response.status === 200) { - await this._rpc({ - model: 'website', - method: 'write', - args: [ - [websiteId], - {google_maps_api_key: valueAPIKey}, - ], - }); - invalidated = true; - dialog.close(); - } else { - const text = await response.text(); - $apiKeyInput.addClass('is-invalid'); - $apiKeyHelp.empty().text( - _t("Invalid API Key. The following error was returned by Google:") - ).append($('', { - text: text, - class: 'ms-1', - })); - } - } catch { - $apiKeyHelp.text(_t("Check your connection and try again")); - } finally { - $button.prop("disabled", false); + const res = await this._validateGMapAPIKey(valueAPIKey); + if (res.isValid) { + await this._rpc({ + model: 'website', + method: 'write', + args: [ + [websiteId], + {google_maps_api_key: valueAPIKey}, + ], + }); + invalidated = true; + dialog.close(); + } else { + applyError.call(dialog.$el, res.message); } + $button.prop("disabled", false); }}, {text: _t("Cancel"), close: true} ], - $content: $(qweb.render('website.s_google_map_modal', { - apiKey: apiKey, - })), + $content: $content, }); dialog.on('closed', this, () => resolve(invalidated)); dialog.open(); }); }, + /** + * @private + */ + async _validateGMapAPIKey(key) { + try { + const response = await fetch(`https://maps.googleapis.com/maps/api/staticmap?center=belgium&size=10x10&key=${key}`); + const isValid = (response.status === 200); + return { + isValid: isValid, + message: !isValid && + _t("Invalid API Key. The following error was returned by Google:") + " " + (await response.text()), + }; + } catch { + return { + isValid: false, + message: _t("Check your connection and try again"), + }; + } + }, /** * @override */ @@ -168,8 +195,8 @@ const wSnippetMenu = weSnippetEditor.SnippetsMenu.extend({ async _handleGMapRequest(ev, gmapRequestEventName) { ev.stopPropagation(); const reconfigured = await this._configureGMapAPI({ - reconfigure: ev.data.reconfigure, - onlyIfUndefined: ev.data.configureIfNecessary, + alwaysReconfigure: ev.data.reconfigure, + configureIfNecessary: ev.data.configureIfNecessary, }); this.getParent().trigger_up(gmapRequestEventName, { refetch: reconfigured, diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index 1d114394832..8a5e508a86a 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -324,10 +324,20 @@ const GPSPicker = InputUserValueWidget.extend({ this.trigger_up('gmap_api_request', { editableMode: true, configureIfNecessary: true, - onSuccess: key => resolve(!!key), + onSuccess: key => { + if (!key) { + resolve(false); + return; + } + + // TODO see _notifyGMapError, this tries to trigger an error + // early but this is not consistent with new gmap keys. + this._nearbySearch('(50.854975,4.3753899)', !!key) + .then(place => resolve(!!place)); + }, }); }); - if (!this._gmapLoaded) { + if (!this._gmapLoaded && !this._gmapErrorNotified) { this.trigger_up('user_value_widget_critical'); return; } @@ -361,17 +371,36 @@ const GPSPicker = InputUserValueWidget.extend({ */ async setValue() { await this._super(...arguments); + if (!this._gmapLoaded) { + return; + } - await new Promise(resolve => { - const gps = this._value; - if (this._gmapCacheGPSToPlace[gps]) { - this._gmapPlace = this._gmapCacheGPSToPlace[gps]; - resolve(); - return; - } + this._gmapPlace = await this._nearbySearch(this._value); + + if (this._gmapPlace) { + this.inputEl.value = this._gmapPlace.formatted_address; + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {string} gps + * @param {boolean} [notify=true] + * @returns {Promise} + */ + async _nearbySearch(gps, notify = true) { + if (this._gmapCacheGPSToPlace[gps]) { + return this._gmapCacheGPSToPlace[gps]; + } + + const p = gps.substring(1).slice(0, -1).split(','); + const location = new google.maps.LatLng(p[0] || 0, p[1] || 0); + return new Promise(resolve => { const service = new google.maps.places.PlacesService(document.createElement('div')); - const p = gps.substring(1).slice(0, -1).split(','); - const location = new google.maps.LatLng(p[0] || 0, p[1] || 0); service.nearbySearch({ // Do a 'nearbySearch' followed by 'getDetails' to avoid using // GMap Geocoder which the user may not have enabled... but @@ -386,23 +415,59 @@ const GPSPicker = InputUserValueWidget.extend({ placeId: results[0].place_id, fields: ['geometry', 'formatted_address'], }, (place, status) => { - resolve(); if (status === google.maps.places.PlacesServiceStatus.OK) { this._gmapCacheGPSToPlace[gps] = place; - this._gmapPlace = place; + resolve(place); } else if (GMAP_CRITICAL_ERRORS.includes(status)) { - this.trigger_up('user_value_widget_critical'); + if (notify) { + this._notifyGMapError(); + } + resolve(); } }); } else if (GMAP_CRITICAL_ERRORS.includes(status)) { + if (notify) { + this._notifyGMapError(); + } + resolve(); + } else { resolve(); - this.trigger_up('user_value_widget_critical'); } }); }); - if (this._gmapPlace) { - this.inputEl.value = this._gmapPlace.formatted_address; + }, + /** + * Indicates to the user there is an error with the google map API and + * re-opens the configuration dialog. For good measures, this also notifies + * a critical error which normally removes the related snippet entirely. + * + * @private + */ + _notifyGMapError() { + // TODO this should be better to detect all errors. This is random. + // When misconfigured (wrong APIs enabled), sometimes Google throw + // errors immediately (which then reaches this code), sometimes it + // throws them later (which then induces an error log in the console + // and random behaviors). + if (this._gmapErrorNotified) { + return; } + this._gmapErrorNotified = true; + + this.displayNotification({ + type: 'danger', + sticky: true, + message: _t("A Google Map error occurred. Make sure to read the key configuration popup carefully."), + }); + this.trigger_up('gmap_api_request', { + editableMode: true, + reconfigure: true, + onSuccess: () => { + this._gmapErrorNotified = false; + }, + }); + + setTimeout(() => this.trigger_up('user_value_widget_critical')); }, //-------------------------------------------------------------------------- diff --git a/addons/website/static/src/xml/website.editor.xml b/addons/website/static/src/xml/website.editor.xml index 32fbafee5aa..b0dbafcc725 100644 --- a/addons/website/static/src/xml/website.editor.xml +++ b/addons/website/static/src/xml/website.editor.xml @@ -132,6 +132,25 @@ Enable billing on your Google Project +