websocket update
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy 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.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Hooks\Common;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class WebSocketSubmit extends \Espo\Core\Hooks\Base
|
||||
{
|
||||
public static $order = 20;
|
||||
|
||||
protected function init()
|
||||
{
|
||||
$this->addDependency('metadata');
|
||||
$this->addDependency('webSocketSubmission');
|
||||
}
|
||||
|
||||
protected function getMetadata()
|
||||
{
|
||||
return $this->getInjection('metadata');
|
||||
}
|
||||
|
||||
protected function getWebSocketSubmission()
|
||||
{
|
||||
return $this->getInjection('webSocketSubmission');
|
||||
}
|
||||
|
||||
public function afterSave(Entity $entity, array $options = [])
|
||||
{
|
||||
if ($options['silent'] ?? false) return;
|
||||
if ($entity->isNew()) return;
|
||||
if (!$this->getConfig()->get('useWebSocket')) return;
|
||||
|
||||
$scope = $entity->getEntityType();
|
||||
$id = $entity->id;
|
||||
|
||||
if (!$this->getMetadata()->get(['scopes', $scope, 'object'])) return;
|
||||
|
||||
$data = (object) [];
|
||||
|
||||
$topic = "recordUpdate.{$scope}.{$id}";
|
||||
$this->getInjection('webSocketSubmission')->submit($topic, null, $data);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"categories": {
|
||||
"newNotification": {},
|
||||
"recordUpdate": {
|
||||
"paramList": ["scope", "id"],
|
||||
"accessCheckCommand": "AclCheck --userId=:userId --scope=:scope --id=:id --action=read"
|
||||
},
|
||||
"streamUpdate": {
|
||||
"paramList": ["scope", "id"],
|
||||
"accessCheckCommand": "AclCheck --userId=:userId --scope=:scope --id=:id --action=stream"
|
||||
|
||||
@@ -301,6 +301,21 @@ Espo.define('views/fields/base', 'view', function (Dep) {
|
||||
this.setupSearch();
|
||||
}
|
||||
|
||||
this.on('highlight', function () {
|
||||
var $cell = this.getCellElement();
|
||||
$cell.addClass('highlighted');
|
||||
$cell.addClass('transition');
|
||||
|
||||
setTimeout(function () {
|
||||
$cell.removeClass('highlighted');
|
||||
}, this.highlightPeriod || 3000);
|
||||
|
||||
setTimeout(function () {
|
||||
$cell.removeClass('transition');
|
||||
}, (this.highlightPeriod || 3000) + 2000);
|
||||
|
||||
}, this);
|
||||
|
||||
this.on('invalid', function () {
|
||||
var $cell = this.getCellElement();
|
||||
$cell.addClass('has-error');
|
||||
@@ -356,6 +371,10 @@ Espo.define('views/fields/base', 'view', function (Dep) {
|
||||
if (changed && !options.skipReRender) {
|
||||
this.reRender();
|
||||
}
|
||||
|
||||
if (changed && options.highlight) {
|
||||
this.trigger('highlight');
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
@@ -595,6 +614,8 @@ Espo.define('views/fields/base', 'view', function (Dep) {
|
||||
}
|
||||
|
||||
this.reRender(true);
|
||||
|
||||
this.trigger('after:inline-edit-off');
|
||||
},
|
||||
|
||||
inlineEdit: function () {
|
||||
|
||||
@@ -326,6 +326,11 @@ define('views/record/base', ['view', 'view-record-helper', 'dynamic-logic'], fun
|
||||
},
|
||||
|
||||
resetModelChanges: function () {
|
||||
if (this.updatedAttributes) {
|
||||
this.attributes = this.updatedAttributes;
|
||||
this.updatedAttributes = null;
|
||||
}
|
||||
|
||||
var attributes = this.model.attributes;
|
||||
for (var attr in attributes) {
|
||||
if (!(attr in this.attributes)) {
|
||||
@@ -333,7 +338,7 @@ define('views/record/base', ['view', 'view-record-helper', 'dynamic-logic'], fun
|
||||
}
|
||||
}
|
||||
|
||||
this.model.set(this.attributes);
|
||||
this.model.set(this.attributes, {skipReRender: true});
|
||||
},
|
||||
|
||||
setModelAttributes: function (setAttributes, options) {
|
||||
|
||||
@@ -40,6 +40,8 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
|
||||
|
||||
fieldsMode: 'detail',
|
||||
|
||||
mode: 'detail',
|
||||
|
||||
gridLayout: null,
|
||||
|
||||
detailLayout: null,
|
||||
@@ -519,6 +521,11 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
|
||||
this.inlineEditModeIsOn = false;
|
||||
this.setIsNotChanged();
|
||||
}, this);
|
||||
this.listenTo(fieldView, 'after:inline-edit-off', function () {
|
||||
if (this.updatedAttributes) {
|
||||
this.resetModelChanges();
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -652,6 +659,13 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
|
||||
},
|
||||
|
||||
resetModelChanges: function () {
|
||||
var skipReRender = true;
|
||||
if (this.updatedAttributes) {
|
||||
this.attributes = this.updatedAttributes;
|
||||
this.updatedAttributes = null;
|
||||
skipReRender = false;
|
||||
}
|
||||
|
||||
var attributes = this.model.attributes;
|
||||
for (var attr in attributes) {
|
||||
if (!(attr in this.attributes)) {
|
||||
@@ -659,7 +673,7 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
|
||||
}
|
||||
}
|
||||
|
||||
this.model.set(this.attributes, {skipReRender: true});
|
||||
this.model.set(this.attributes, {skipReRender: skipReRender});
|
||||
},
|
||||
|
||||
delete: function () {
|
||||
@@ -914,6 +928,20 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
|
||||
this.$detailButtonContainer = this.$el.find('.detail-button-container');
|
||||
this.$dropdownItemListButton = this.$detailButtonContainer.find('.dropdown-item-list-button');
|
||||
}, this);
|
||||
|
||||
if (
|
||||
!this.isNew &&
|
||||
this.getConfig().get('useWebSocket') &&
|
||||
this.getMetadata().get(['scopes', this.entityType, 'object'])
|
||||
) {
|
||||
this.subscribeToWebSocket();
|
||||
|
||||
this.once('remove', function () {
|
||||
if (this.isSubscribedToWebSocked) {
|
||||
this.unsubscribeFromWebSocket();
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
setupBeforeFinal: function () {
|
||||
@@ -1185,10 +1213,15 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
|
||||
}
|
||||
this.enableButtons();
|
||||
this.setIsNotChanged();
|
||||
|
||||
setTimeout(function () {
|
||||
this.unblockUpdateWebSocket();
|
||||
}.bind(this), this.blockUpdateWebSocketPeriod || 500);
|
||||
},
|
||||
|
||||
beforeSave: function () {
|
||||
this.notify('Saving...');
|
||||
this.blockUpdateWebSocket();
|
||||
},
|
||||
|
||||
beforeBeforeSave: function () {
|
||||
@@ -1718,5 +1751,46 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
|
||||
this.getRouter().navigate(url, {trigger: true});
|
||||
},
|
||||
|
||||
subscribeToWebSocket: function () {
|
||||
var topic = 'recordUpdate.' + this.entityType + '.' + this.model.id;
|
||||
this.recordUpdateWebSocketTopic = topic;
|
||||
|
||||
this.isSubscribedToWebSocked = true;
|
||||
|
||||
this.getHelper().webSocketManager.subscribe(topic, function (t, data) {
|
||||
this.handleRecordUpdate();
|
||||
}.bind(this))
|
||||
},
|
||||
|
||||
unsubscribeFromWebSocket: function () {
|
||||
if (!this.isSubscribedToWebSocked) return;
|
||||
this.getHelper().webSocketManager.unsubscribe(this.recordUpdateWebSocketTopic);
|
||||
},
|
||||
|
||||
handleRecordUpdate: function () {
|
||||
if (this.updateWebSocketIsBlocked) return;
|
||||
|
||||
if (this.inlineEditModeIsOn || this.mode == 'edit') {
|
||||
var m = this.model.clone();
|
||||
m.fetch().then(
|
||||
function () {
|
||||
if (this.inlineEditModeIsOn || this.mode == 'edit') {
|
||||
this.updatedAttributes = Espo.Utils.cloneDeep(m.attributes);
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
} else {
|
||||
this.model.fetch({highlight: true});
|
||||
}
|
||||
},
|
||||
|
||||
blockUpdateWebSocket: function () {
|
||||
this.updateWebSocketIsBlocked = true;
|
||||
},
|
||||
|
||||
unblockUpdateWebSocket: function () {
|
||||
this.updateWebSocketIsBlocked = false;
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -770,6 +770,20 @@ table.table > thead th {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.cell.highlighted {
|
||||
> label {
|
||||
color: @state-warning-text;
|
||||
}
|
||||
}
|
||||
|
||||
.cell.transition {
|
||||
> label {
|
||||
-webkit-transition: color .5s ease;
|
||||
-o-transition: color .5s ease;
|
||||
transition: color .5s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.filter .selectize-input {
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user