diff --git a/.idea/jsonSchemas.xml b/.idea/jsonSchemas.xml
index b467eac21d..6cc93979a4 100644
--- a/.idea/jsonSchemas.xml
+++ b/.idea/jsonSchemas.xml
@@ -854,6 +854,25 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.vscode/settings.json b/.vscode/settings.json
index cdefcea218..4b205939fd 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -352,6 +352,12 @@
],
"url": "./schema/metadata/app/entityManager.json"
},
+ {
+ "fileMatch": [
+ "*/Resources/metadata/app/mapProviders.json"
+ ],
+ "url": "./schema/metadata/app/mapProviders.json"
+ },
{
"fileMatch": [
"*/Resources/metadata/app/massActions.json"
diff --git a/application/Espo/Resources/defaults/config.php b/application/Espo/Resources/defaults/config.php
index 5e10b8b198..caea07c57a 100644
--- a/application/Espo/Resources/defaults/config.php
+++ b/application/Espo/Resources/defaults/config.php
@@ -256,6 +256,7 @@ return [
'newNotificationCountInTitle' => false,
'pdfEngine' => 'Dompdf',
'smsProvider' => null,
+ 'mapProvider' => 'Google',
'defaultFileStorage' => 'EspoUploadDir',
'ldapUserNameAttribute' => 'sAMAccountName',
'ldapUserFirstNameAttribute' => 'givenName',
diff --git a/application/Espo/Resources/metadata/app/mapProviders.json b/application/Espo/Resources/metadata/app/mapProviders.json
new file mode 100644
index 0000000000..acd6e70a15
--- /dev/null
+++ b/application/Espo/Resources/metadata/app/mapProviders.json
@@ -0,0 +1,5 @@
+{
+ "Google": {
+ "renderer": "handlers/map/google-maps-renderer"
+ }
+}
diff --git a/application/Espo/Resources/metadata/fields/map.json b/application/Espo/Resources/metadata/fields/map.json
index bbf184f924..350af95a3f 100644
--- a/application/Espo/Resources/metadata/fields/map.json
+++ b/application/Espo/Resources/metadata/fields/map.json
@@ -1,13 +1,5 @@
{
"params": [
- {
- "name": "provider",
- "type": "enum",
- "options": [
- "Google"
- ],
- "default": "Google"
- },
{
"name": "height",
"type": "int",
diff --git a/client/src/handlers/map/google-maps-renderer.js b/client/src/handlers/map/google-maps-renderer.js
new file mode 100644
index 0000000000..b82a592ebb
--- /dev/null
+++ b/client/src/handlers/map/google-maps-renderer.js
@@ -0,0 +1,160 @@
+/************************************************************************
+ * This file is part of EspoCRM.
+ *
+ * EspoCRM - Open Source CRM application.
+ * Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
+ * Website: https://www.espocrm.com
+ *
+ * EspoCRM is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * EspoCRM is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with EspoCRM. If not, see http://www.gnu.org/licenses/.
+ *
+ * The interactive user interfaces in modified source and object code versions
+ * of this program must display Appropriate Legal Notices, as required under
+ * Section 5 of the GNU General Public License version 3.
+ *
+ * In accordance with Section 7(b) of the GNU General Public License version 3,
+ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
+ ************************************************************************/
+
+import MapRenderer from 'handlers/map/renderer';
+
+class GoogleMapsRenderer extends MapRenderer {
+
+ /**
+ * @param {module:handlers/map/renderer~addressData} addressData
+ */
+ render(addressData) {
+ if ('google' in window && window.google.maps) {
+ this.initMapGoogle(addressData);
+
+ return;
+ }
+
+ // noinspection SpellCheckingInspection
+ if (typeof window.mapapiloaded === 'function') {
+ // noinspection SpellCheckingInspection
+ const mapapiloaded = window.mapapiloaded;
+
+ // noinspection SpellCheckingInspection
+ window.mapapiloaded = () => {
+ this.initMapGoogle(addressData);
+
+ mapapiloaded();
+ };
+
+ return;
+ }
+
+ // noinspection SpellCheckingInspection
+ window.mapapiloaded = () => this.initMapGoogle(addressData);
+
+ let src = 'https://maps.googleapis.com/maps/api/js?callback=mapapiloaded';
+ const apiKey = this.view.getConfig().get('googleMapsApiKey');
+
+ if (apiKey) {
+ src += '&key=' + apiKey;
+ }
+
+ const scriptElement = document.createElement('script');
+
+ scriptElement.setAttribute('async', 'async');
+ scriptElement.src = src;
+
+ document.head.appendChild(scriptElement);
+ }
+
+ /**
+ * @param {module:handlers/map/renderer~addressData} addressData
+ */
+ initMapGoogle(addressData) {
+ // noinspection JSUnresolvedReference
+ const geocoder = new google.maps.Geocoder();
+ let map;
+
+ try {
+ // noinspection SpellCheckingInspection,JSUnresolvedReference
+ map = new google.maps.Map(this.view.$el.find('.map').get(0), {
+ zoom: 15,
+ center: {lat: 0, lng: 0},
+ scrollwheel: false,
+ });
+ }
+ catch (e) {
+ console.error(e.message);
+
+ return;
+ }
+
+ let address = '';
+
+ if (addressData.street) {
+ address += addressData.street;
+ }
+
+ if (addressData.city) {
+ if (address !== '') {
+ address += ', ';
+ }
+
+ address += addressData.city;
+ }
+
+ if (addressData.state) {
+ if (address !== '') {
+ address += ', ';
+ }
+
+ address += addressData.state;
+ }
+
+ if (addressData.postalCode) {
+ if (addressData.state || addressData.city) {
+ address += ' ';
+ }
+ else {
+ if (address) {
+ address += ', ';
+ }
+ }
+
+ address += addressData.postalCode;
+ }
+
+ if (addressData.country) {
+ if (address !== '') {
+ address += ', ';
+ }
+
+ address += addressData.country;
+ }
+
+ // noinspection JSUnresolvedReference
+ geocoder.geocode({'address': address}, (results, status) => {
+ // noinspection JSUnresolvedReference
+ if (status === google.maps.GeocoderStatus.OK) {
+ // noinspection JSUnresolvedReference
+ map.setCenter(results[0].geometry.location);
+
+ // noinspection JSUnresolvedReference
+ new google.maps.Marker({
+ map: map,
+ position: results[0].geometry.location,
+ });
+ }
+ });
+ }
+}
+
+// noinspection JSUnusedGlobalSymbols
+export default GoogleMapsRenderer;
+
diff --git a/client/src/handlers/map/renderer.js b/client/src/handlers/map/renderer.js
new file mode 100644
index 0000000000..d140ced8bb
--- /dev/null
+++ b/client/src/handlers/map/renderer.js
@@ -0,0 +1,63 @@
+/************************************************************************
+ * This file is part of EspoCRM.
+ *
+ * EspoCRM - Open Source CRM application.
+ * Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
+ * Website: https://www.espocrm.com
+ *
+ * EspoCRM is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * EspoCRM is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with EspoCRM. If not, see http://www.gnu.org/licenses/.
+ *
+ * The interactive user interfaces in modified source and object code versions
+ * of this program must display Appropriate Legal Notices, as required under
+ * Section 5 of the GNU General Public License version 3.
+ *
+ * In accordance with Section 7(b) of the GNU General Public License version 3,
+ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
+ ************************************************************************/
+
+/**
+ * @module handlers/map/renderer
+ */
+
+/**
+ * A map renderer.
+ *
+ * @abstract
+ */
+class MapRenderer {
+
+ /**
+ * @typedef {Object} module:handlers/map/renderer~addressData
+ * @property {string|null} street
+ * @property {string|null} city
+ * @property {string|null} country
+ * @property {string|null} state
+ * @property {string|null} postalCode
+ */
+
+ /**
+ * @param {import('views/fields/map').default} view A field view.
+ */
+ constructor(view) {
+ this.view = view;
+ }
+
+ /**
+ * @param {module:handlers/map/renderer~addressData} addressData
+ * @abstract
+ */
+ render(addressData) {}
+}
+
+export default MapRenderer;
diff --git a/client/src/views/fields/map.js b/client/src/views/fields/map.js
index c40c26a46b..06e07a7ea7 100644
--- a/client/src/views/fields/map.js
+++ b/client/src/views/fields/map.js
@@ -28,10 +28,14 @@ class MapFieldView extends BaseFieldView {
detailTemplate = 'fields/map/detail'
listTemplate = 'fields/map/detail'
- addressField = null
- provider = null
+ /** @type {string} */
+ addressField
+ /** @type {string} */
+ provider
height = 300
+ DEFAULT_PROVIDER = 'Google';
+
// noinspection JSCheckFunctionSignatures
data() {
const data = super.data();
@@ -45,7 +49,7 @@ class MapFieldView extends BaseFieldView {
setup() {
this.addressField = this.name.slice(0, this.name.length - 3);
- this.provider = this.options.provider || this.params.provider;
+ this.provider = this.provider || this.getConfig().get('mapProvider') || this.DEFAULT_PROVIDER;
this.height = this.options.height || this.params.height || this.height;
const addressAttributeList = Object.keys(this.getMetadata().get('fields.address.fields') || {})
@@ -93,71 +97,43 @@ class MapFieldView extends BaseFieldView {
this.$map = this.$el.find('.map');
if (this.hasAddress()) {
- this.processSetHeight(true);
-
- if (this.height === 'auto') {
- $(window).off('resize.' + this.cid);
- $(window).on('resize.' + this.cid, this.processSetHeight.bind(this));
- }
-
- let methodName = 'afterRender' + this.provider.replace(/\s+/g, '');
-
- if (typeof this[methodName] === 'function') {
- this[methodName]();
- }
- else {
- let implClassName = this.getMetadata()
- .get(['clientDefs', 'AddressMap', 'implementations', this.provider]);
-
- if (implClassName) {
- Espo.loader.require(implClassName, impl => {
- impl.render(this);
- });
- }
- }
+ this.renderMap();
}
}
- // noinspection JSUnusedGlobalSymbols
- afterRenderGoogle() {
- if (window.google && window.google.maps) {
- this.initMapGoogle();
+ renderMap() {
+ this.processSetHeight(true);
+
+ if (this.height === 'auto') {
+ $(window).off('resize.' + this.cid);
+ $(window).on('resize.' + this.cid, this.processSetHeight.bind(this));
+ }
+
+ const rendererId = this.getMetadata().get(['app', 'mapProviders', this.provider, 'renderer']);
+
+ if (rendererId) {
+ Espo.loader.require(rendererId, Renderer => {
+ (new Renderer(this)).render(this.addressData);
+ });
return;
}
- // noinspection SpellCheckingInspection
- if (typeof window.mapapiloaded === 'function') {
- // noinspection SpellCheckingInspection
- let mapapiloaded = window.mapapiloaded;
+ const methodName = 'afterRender' + this.provider.replace(/\s+/g, '');
- // noinspection SpellCheckingInspection
- window.mapapiloaded = () => {
- this.initMapGoogle();
- mapapiloaded();
- };
+ if (typeof this[methodName] === 'function') {
+ this[methodName]();
return;
}
- // noinspection SpellCheckingInspection
- window.mapapiloaded = () => {
- this.initMapGoogle();
- };
+ // For bc.
+ // @todo Remove in v9.0.
+ const implId = this.getMetadata().get(['clientDefs', 'AddressMap', 'implementations', this.provider]);
- let src = 'https://maps.googleapis.com/maps/api/js?callback=mapapiloaded';
- let apiKey = this.getConfig().get('googleMapsApiKey');
-
- if (apiKey) {
- src += '&key=' + apiKey;
+ if (implId) {
+ Espo.loader.require(implId, impl => impl.render(this));
}
-
- let scriptElement = document.createElement('script');
-
- scriptElement.setAttribute('async', 'async');
- scriptElement.src = src;
-
- document.head.appendChild(scriptElement);
}
processSetHeight(init) {
@@ -167,9 +143,7 @@ class MapFieldView extends BaseFieldView {
height = this.$el.parent().height();
if (init && height <= 0) {
- setTimeout(() => {
- this.processSetHeight(true);
- }, 50);
+ setTimeout(() => this.processSetHeight(true), 50);
return;
}
@@ -177,79 +151,6 @@ class MapFieldView extends BaseFieldView {
this.$map.css('height', height + 'px');
}
-
- initMapGoogle() {
- const geocoder = new google.maps.Geocoder();
- let map;
-
- try {
- // noinspection SpellCheckingInspection
- map = new google.maps.Map(this.$el.find('.map').get(0), {
- zoom: 15,
- center: {lat: 0, lng: 0},
- scrollwheel: false,
- });
- }
- catch (e) {
- console.error(e.message);
-
- return;
- }
-
- let address = '';
-
- if (this.addressData.street) {
- address += this.addressData.street;
- }
-
- if (this.addressData.city) {
- if (address !== '') {
- address += ', ';
- }
-
- address += this.addressData.city;
- }
-
- if (this.addressData.state) {
- if (address !== '') {
- address += ', ';
- }
-
- address += this.addressData.state;
- }
-
- if (this.addressData.postalCode) {
- if (this.addressData.state || this.addressData.city) {
- address += ' ';
- }
- else {
- if (address) {
- address += ', ';
- }
- }
-
- address += this.addressData.postalCode;
- }
-
- if (this.addressData.country) {
- if (address !== '') {
- address += ', ';
- }
-
- address += this.addressData.country;
- }
-
- geocoder.geocode({'address': address}, (results, status) => {
- if (status === google.maps.GeocoderStatus.OK) {
- map.setCenter(results[0].geometry.location);
-
- new google.maps.Marker({
- map: map,
- position: results[0].geometry.location,
- });
- }
- });
- }
}
export default MapFieldView;
diff --git a/schema/metadata/app/mapProviders.json b/schema/metadata/app/mapProviders.json
new file mode 100644
index 0000000000..ee147f186f
--- /dev/null
+++ b/schema/metadata/app/mapProviders.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://www.espocrm.com/schema/metadata/app/mapProviders.json",
+ "title": "app/mapProviders",
+ "description": "Map providers.",
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "description": "A provider name.",
+ "properties": {
+ "renderer": {
+ "type": "string",
+ "description": "A frontend renderer. Should extend handlers/map/renderer."
+ }
+ }
+ }
+}