diff --git a/application/Espo/Core/CronManager.php b/application/Espo/Core/CronManager.php index 56daa61a79..322d1f6dcf 100644 --- a/application/Espo/Core/CronManager.php +++ b/application/Espo/Core/CronManager.php @@ -310,7 +310,7 @@ class CronManager } else { $this->runService($job); } - } catch (\Exception $e) { + } catch (\Throwable $e) { $isSuccess = false; if ($e->getCode() === -1) { $job->set('attempts', 0); diff --git a/application/Espo/Core/Utils/Cron/JobTask.php b/application/Espo/Core/Utils/Cron/JobTask.php index 2a588cd7e4..fcdb204b71 100644 --- a/application/Espo/Core/Utils/Cron/JobTask.php +++ b/application/Espo/Core/Utils/Cron/JobTask.php @@ -45,6 +45,10 @@ class JobTask extends \Spatie\Async\Task public function run() { $app = new \Espo\Core\Application(); - $app->runJob($this->jobId); + try { + $app->runJob($this->jobId); + } catch (\Throwable $e) { + $GLOBALS['log']->error("JobTask: Failed job run. Job id: ".$this->jobId.". Error details: ".$e->getMessage()); + } } } diff --git a/application/Espo/Core/defaults/systemConfig.php b/application/Espo/Core/defaults/systemConfig.php index 5b6bae877d..ce6bdfacea 100644 --- a/application/Espo/Core/defaults/systemConfig.php +++ b/application/Espo/Core/defaults/systemConfig.php @@ -183,6 +183,7 @@ return [ 'adminNotificationsCronIsNotConfigured', 'adminNotificationsNewExtensionVersion', 'leadCaptureAllowOrigin', + 'cronDisabled', ], 'superAdminItems' => [ 'jobMaxPortion', @@ -196,6 +197,9 @@ return [ 'daemonProcessTimeout', 'daemonMaxProcessNumber', 'adminPanelIframeUrl', + 'cronDisabled', + 'maintenanceMode', + 'siteUrl', ], 'userItems' => [ 'outboundEmailFromAddress', diff --git a/application/Espo/Entities/Email.php b/application/Espo/Entities/Email.php index a547916030..2598611240 100644 --- a/application/Espo/Entities/Email.php +++ b/application/Espo/Entities/Email.php @@ -51,25 +51,51 @@ class Email extends \Espo\Core\ORM\Entity return $this->has('fromString'); } - protected function _getFromName() - { - if (!$this->has('fromString')) return null; - - return \Espo\Services\Email::parseFromName($this->get('fromString')); - } - protected function _hasFromAddress() { return $this->has('fromString'); } + protected function _hasReplyToName() + { + return $this->has('replyToString'); + } + + protected function _hasReplyToAddress() + { + return $this->has('replyToString'); + } + + protected function _getFromName() + { + if (!$this->has('fromString')) return null; + return \Espo\Services\Email::parseFromName($this->get('fromString')); + } + protected function _getFromAddress() { if (!$this->has('fromString')) return null; - return \Espo\Services\Email::parseFromAddress($this->get('fromString')); } + protected function _getReplyToName() + { + if (!$this->has('replyToString')) return null; + $string = $this->get('replyToString'); + if (!$string) return null; + $string = trim(explode(';', $string)[0]); + return \Espo\Services\Email::parseFromName($string); + } + + protected function _getReplyToAddress() + { + if (!$this->has('replyToString')) return null; + $string = $this->get('replyToString'); + if (!$string) return null; + $string = trim(explode(';', $string)[0]); + return \Espo\Services\Email::parseFromAddress($string); + } + protected function _setIsRead($value) { $this->setValue('isRead', $value !== false); diff --git a/application/Espo/Hooks/Common/Formula.php b/application/Espo/Hooks/Common/Formula.php index f30eef41d0..076462e73d 100644 --- a/application/Espo/Hooks/Common/Formula.php +++ b/application/Espo/Hooks/Common/Formula.php @@ -66,7 +66,7 @@ class Formula extends \Espo\Core\Hooks\Base } } - $customScript = $this->getMetadata()->get(['formula', $entity->getEntityType(), 'beforeSaveCustomScript'], []); + $customScript = $this->getMetadata()->get(['formula', $entity->getEntityType(), 'beforeSaveCustomScript']); if ($customScript) { try { $this->getFormulaManager()->run($customScript, $entity, $variables); diff --git a/application/Espo/Resources/i18n/en_US/Email.json b/application/Espo/Resources/i18n/en_US/Email.json index 97b6a6b414..e5ed7de9d2 100644 --- a/application/Espo/Resources/i18n/en_US/Email.json +++ b/application/Espo/Resources/i18n/en_US/Email.json @@ -49,6 +49,8 @@ "fromName": "From Name", "fromString": "From String", "fromAddress": "From Address", + "replyToName": "Reply-To Name", + "replyToAddress": "Reply-To Address", "isSystem": "Is System" }, "links": { diff --git a/application/Espo/Resources/i18n/en_US/Settings.json b/application/Espo/Resources/i18n/en_US/Settings.json index 4dcbd73f9d..027369c82c 100644 --- a/application/Espo/Resources/i18n/en_US/Settings.json +++ b/application/Espo/Resources/i18n/en_US/Settings.json @@ -114,7 +114,9 @@ "jobPoolConcurrencyNumber": "Jobs Pool Concurrency Number", "daemonInterval": "Daemon Interval", "daemonMaxProcessNumber": "Daemon Max Process Number", - "daemonProcessTimeout": "Daemon Process Timeout" + "daemonProcessTimeout": "Daemon Process Timeout", + "cronDisabled": "Disable Cron", + "maintenanceMode": "Maintenance Mode" }, "options": { "weekStart": { @@ -178,11 +180,15 @@ "jobMaxPortion": "Max number of jobs processed per one execution.", "daemonInterval": "Interval between process cron runs in seconds.", "daemonMaxProcessNumber": "Max number of cron processes run simultaneously.", - "daemonProcessTimeout": "Max execution time (in seconds) allocated for a single cron process." + "daemonProcessTimeout": "Max execution time (in seconds) allocated for a single cron process.", + "cronDisabled": "Cron will not run.", + "maintenanceMode": "Only administrators will have access to the system." }, "labels": { "System": "System", "Locale": "Locale", + "Search": "Search", + "Misc": "Misc", "SMTP": "SMTP", "Configuration": "Configuration", "In-app Notifications": "In-app Notifications", diff --git a/application/Espo/Resources/layouts/Settings/settings.json b/application/Espo/Resources/layouts/Settings/settings.json index 57535a135d..5fd89f386f 100644 --- a/application/Espo/Resources/layouts/Settings/settings.json +++ b/application/Espo/Resources/layouts/Settings/settings.json @@ -3,11 +3,22 @@ "label": "System", "rows": [ [{"name": "useCache"}, {"name": "siteUrl"}], - [{"name": "exportDisabled"}, {"name": "globalSearchEntityList"}], - [{"name": "followCreatedEntities"}, {"name": "b2cMode"}], - [{"name": "aclStrictMode"}, {"name": "aclAllowDeleteCreated"}], - [{"name": "textFilterUseContainsForVarchar"}, {"name": "emailAddressIsOptedOutByDefault"}], - [{"name": "cleanupDeletedRecords"}, false] + [{"name": "b2cMode"}, {"name": "aclStrictMode"}], + [{"name": "maintenanceMode"}, {"name": "cronDisabled"}] + ] + }, + { + "label": "Search", + "rows": [ + [{"name": "textFilterUseContainsForVarchar"}, {"name": "globalSearchEntityList"}] + ] + }, + { + "label": "Misc", + "rows": [ + [{"name": "followCreatedEntities"}, {"name": "emailAddressIsOptedOutByDefault"}], + [{"name": "aclAllowDeleteCreated"}, {"name": "cleanupDeletedRecords"}], + [{"name": "exportDisabled"}, false] ] }, { diff --git a/application/Espo/Resources/metadata/entityDefs/Email.json b/application/Espo/Resources/metadata/entityDefs/Email.json index 5b7f57a592..20bd99e10b 100644 --- a/application/Espo/Resources/metadata/entityDefs/Email.json +++ b/application/Espo/Resources/metadata/entityDefs/Email.json @@ -33,6 +33,20 @@ "replyToString": { "type": "varchar" }, + "replyToName": { + "type": "varchar", + "readOnly": true, + "notStorable": true, + "textFilterDisabled": true, + "layoutFiltersDisabled": true + }, + "replyToAddress": { + "type": "varchar", + "readOnly": true, + "notStorable": true, + "textFilterDisabled": true, + "layoutFiltersDisabled": true + }, "addressNameMap": { "type": "jsonObject", "disabled": true, diff --git a/application/Espo/Resources/metadata/entityDefs/Settings.json b/application/Espo/Resources/metadata/entityDefs/Settings.json index bb4606dbc4..179eb890dd 100644 --- a/application/Espo/Resources/metadata/entityDefs/Settings.json +++ b/application/Espo/Resources/metadata/entityDefs/Settings.json @@ -536,6 +536,14 @@ "daemonProcessTimeout": { "type": "int", "tooltip": true + }, + "cronDisabled": { + "type": "bool", + "tooltip": true + }, + "maintenanceMode": { + "type": "bool", + "tooltip": true } } } diff --git a/application/Espo/Services/EmailNotification.php b/application/Espo/Services/EmailNotification.php index 22c39aa862..1fc2cdedc9 100644 --- a/application/Espo/Services/EmailNotification.php +++ b/application/Espo/Services/EmailNotification.php @@ -120,12 +120,11 @@ class EmailNotification extends \Espo\Core\Services\Base $assignerUser = $this->getEntityManager()->getEntity('User', $assignerUserId); $entity = $this->getEntityManager()->getEntity($entityType, $entityId); - - $this->loadParentNameFields($entity); - if (!$entity) return true; if (!$assignerUser) return true; + $this->loadParentNameFields($entity); + if (!$entity->hasLinkMultipleField('assignedUsers')) { if ($entity->get('assignedUserId') !== $userId) return true; } diff --git a/client/lib/bull.js b/client/lib/bull.js index ee5c22f5ca..e683d8f5bc 100644 --- a/client/lib/bull.js +++ b/client/lib/bull.js @@ -202,13 +202,13 @@ var Bull = Bull || {}; _wait: false, - expectedViews: null, + _waitViewList: null, optionsToPass: null, _nestedViewsFromLayoutLoaded: false, - _readyConditions: null, + _readyConditionList: null, _isRendered: false, @@ -243,12 +243,14 @@ var Bull = Bull || {}; this.nestedViews = {}; this._nestedViewDefs = {}; - if (this.expectedViews == null) { - this.expectedViews = []; + if (this._waitViewList == null) { + this._waitViewList = []; } - if (this._readyConditions == null) { - this._readyConditions = []; + this._waitPromiseCount = 0; + + if (this._readyConditionList == null) { + this._readyConditionList = []; } this.optionsToPass = this.options.optionsToPass || this.optionsToPass || []; @@ -279,6 +281,8 @@ var Bull = Bull || {}; this._layout = this.options._layout || this._layout; this.layoutData = this.options.layoutData || this.layoutData; + this._template = this.templateContent || this._template; + if (this._template != null && this._templator.compilable) { this._templateCompiled = this._templator.compileTemplate(this._template); } @@ -398,38 +402,46 @@ var Bull = Bull || {}; this._isRendered = false; this._isFullyRendered = false; - this._getHtml(function (html) { - if (this._isRenderCanceled) { - this._isRenderCanceled = false; - this._isBeingRendered = false; - return; - } - if (this.$el.size()) { - this.$el.html(html); - } else { - if (this.options.el) { - this.setElement(this.options.el); + return new Promise(function (resolve, reject) { + this._getHtml(function (html) { + if (this._isRenderCanceled) { + this._isRenderCanceled = false; + this._isBeingRendered = false; + reject(); + return; } - this.$el.html(html); - } - this._afterRender(); - if (typeof callback === 'function') { - callback(); - } + if (this.$el.size()) { + this.$el.html(html); + } else { + if (this.options.el) { + this.setElement(this.options.el); + } + this.$el.html(html); + } + this._afterRender(); + if (typeof callback === 'function') { + callback(); + } + resolve(this); + }.bind(this)); }.bind(this)); - }, + /** + * Re-render view. + */ reRender: function (force) { if (this.isRendered()) { - this.render(); + return this.render(); } else if (this.isBeingRendered()) { - this.once('after:render', function () { - this.render(); - }, this); + return new Promise(function (resolve, reject) { + this.once('after:render', function () { + this.render().then(resolve).catch(reject); + }, this); + }.bind(this)); } else { if (force) { - this.render(); + return this.render(); } } }, @@ -455,29 +467,23 @@ var Bull = Bull || {}; afterRender: function () {}, _tryReady: function () { - if (this.isReady) { - return; + if (this.isReady) return; + + if (this._wait) return; + + if (!this._nestedViewsFromLayoutLoaded) return; + + for (var i = 0; i < this._waitViewList.length; i++) { + if (!this.hasView(this._waitViewList[i])) return; } - if (this._wait) { - return; - } - if (!this._nestedViewsFromLayoutLoaded) { - return; - } - for (var i in this.expectedViews) { - if (!this.hasView(this.expectedViews[i])) { - return; - } - } - for (var i in this._readyConditions) { - if (typeof this._readyConditions[i] === 'function') { - if (!this._readyConditions[i]()) { - return; - } + + if (this._waitPromiseCount) return; + + for (var i = 0; i < this._readyConditionList.length; i++) { + if (typeof this._readyConditionList[i] === 'function') { + if (!this._readyConditionList[i]()) return; } else { - if (!this._readyConditions) { - return; - } + if (!this._readyConditionList) return; } } @@ -669,6 +675,7 @@ var Bull = Bull || {}; if (this.model || null) { data.model = this.model; } + data.viewObject = this; this.handleDataBeforeRender(data); this._getTemplate(function (template) { var html = this._renderer.render(template, data); @@ -802,27 +809,30 @@ var Bull = Bull || {}; * @param {Bool} wait True be default. Set false if no need parent view wait for nested view loaded. */ createView: function (key, viewName, options, callback, wait) { - wait = (typeof wait === 'undefined') ? true : wait; - var context = this; - if (wait) { - this.waitForView(key); - } - options = options || {}; - if (!options.el) { - options.el = this.getSelector() + ' [data-view="'+key+'"]'; - } - this._factory.create(viewName, options, function (view) { - var isSet = false; - if (this._isRendered || options.setViewBeforeCallback) { - this.setView(key, view); - isSet = true; + return new Promise(function (resolve) { + wait = (typeof wait === 'undefined') ? true : wait; + var context = this; + if (wait) { + this.waitForView(key); } - if (typeof callback === 'function') { - callback.call(context, view); - } - if (!this._isRendered && !options.setViewBeforeCallback && !isSet) { - this.setView(key, view); + options = options || {}; + if (!options.el) { + options.el = this.getSelector() + ' [data-view="'+key+'"]'; } + this._factory.create(viewName, options, function (view) { + var isSet = false; + if (this._isRendered || options.setViewBeforeCallback) { + this.setView(key, view); + isSet = true; + } + if (typeof callback === 'function') { + callback.call(context, view); + } + resolve(view); + if (!this._isRendered && !options.setViewBeforeCallback && !isSet) { + this.setView(key, view); + } + }.bind(this)); }.bind(this)); }, @@ -897,7 +907,7 @@ var Bull = Bull || {}; * @param {Function} or {Bool} */ addReadyCondition: function (condition) { - this._readyConditions.push(condition); + this._readyConditionList.push(condition); }, /** @@ -905,14 +915,34 @@ var Bull = Bull || {}; * @param {String} key */ waitForView: function (key) { - this.expectedViews.push(key); + this._waitViewList.push(key); }, /** - * Add wait condition is true is passed. Remove wait condition if false. - * @param {Bool} + * Make view wait for promise if Promise is passed as a parameter. + * Add wait condition if true is passed. Remove wait condition if false. + * @param wait Promise | Function | {Bool} */ wait: function (wait) { + if (typeof wait === 'object' && (wait instanceof Promise || typeof wait.then === 'function')) { + this._waitPromiseCount++; + wait.then(function () { + this._waitPromiseCount--; + this._tryReady(); + }.bind(this)); + return; + } + if (typeof wait == 'function') { + this._waitPromiseCount++; + var promise = new Promise(function (resolve) { + resolve(wait.call(this)); + }.bind(this)) + promise.then(function () { + this._waitPromiseCount--; + this._tryReady(); + }.bind(this)); + return promise; + } if (wait) { this._wait = true; } else { diff --git a/client/modules/crm/src/views/call/fields/contacts.js b/client/modules/crm/src/views/call/fields/contacts.js index 691d011bc8..598c96ab0b 100644 --- a/client/modules/crm/src/views/call/fields/contacts.js +++ b/client/modules/crm/src/views/call/fields/contacts.js @@ -45,7 +45,7 @@ Espo.define('crm:views/call/fields/contacts', 'crm:views/meeting/fields/contacts if (key in phoneNumbersMap) { number = phoneNumbersMap[key]; var innerHtml = $(html).html(); - innerHtml += ' » ' + '' + number + ''; + innerHtml += ' » ' + '' + number + ''; html = '