This commit is contained in:
Yuri Kuznetsov
2020-11-03 12:25:51 +02:00
15 changed files with 1107 additions and 59 deletions
@@ -0,0 +1,73 @@
<?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\Controllers;
use Espo\Core\{
Exceptions\BadRequest,
Api\Request,
ServiceFactory,
};
class KanbanOrder
{
protected $serviceFactory;
public function __construct(ServiceFactory $serviceFactory)
{
$this->serviceFactory = $serviceFactory;
}
public function postActionStore(Request $request)
{
$data = $request->getParsedBody();
$entityType = $data->entityType;
$group = $data->group;
$ids = $data->ids;
if (empty($entityType) || !is_string($entityType)) {
throw new BadRequest();
}
if (empty($group) || !is_string($group)) {
throw new BadRequest();
}
if (!is_array($ids)) {
throw new BadRequest();
}
$this->serviceFactory
->create('KanbanOrder')
->order($entityType, $group, $ids);
return true;
}
}
@@ -0,0 +1,51 @@
<?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\Controllers;
use Espo\Core\{
ServiceFactory,
};
class PopupNotification
{
protected $serviceFactory;
public function __construct(ServiceFactory $serviceFactory)
{
$this->serviceFactory = $serviceFactory;
}
public function getActionGrouped()
{
return $this->serviceFactory
->create('PopupNotification')
->getGroupedList();
}
}
@@ -1,6 +1,9 @@
{
"event": {
"url": "Activities/action/popupNotifications",
"grouped": true,
"serviceName": "Activities",
"methodName": "getPopupNotifications",
"interval": 15,
"useWebSocket": true,
"portalDisabled": true,
@@ -0,0 +1,35 @@
{
"fields": {
"order": {
"type": "int",
"dbType": "smallint"
},
"entity": {
"type": "linkParent"
},
"group": {
"type": "varchar",
"maxLength": 100
},
"userId": {
"type": "varchar",
"maxLength": 11
}
},
"links": {
"entity": {
"type": "belongsToParent"
}
},
"indexes": {
"entityUserId": {
"columns": ["entityType", "entityId", "userId"]
},
"entityType": {
"columns": ["entityType"]
},
"entityTypeUserId": {
"columns": ["entityType", "userId"]
}
}
}
+83
View File
@@ -0,0 +1,83 @@
<?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\Services;
use Espo\Core\{
Exceptions\Forbidden,
Acl,
Utils\Config,
};
use Espo\{
Entities\User,
Tools\Kanban\Orderer,
};
class KanbanOrder
{
protected $orderer;
protected $acl;
protected $user;
protected $config;
protected $serviceFactory;
public function __construct(Orderer $orderer, Acl $acl, User $user, Config $config)
{
$this->orderer = $orderer;
$this->acl = $acl;
$this->user = $user;
$this->config = $config;
}
public function order(string $entityType, string $group, array $ids)
{
if (!$this->acl->check($entityType, 'read')) {
throw new Forbidden();
}
if ($this->user->isPortal()) {
throw new Forbidden();
}
$processor = $this->orderer
->createProcessor()
->setEntityType($entityType)
->setGroup($group)
->setUserId($this->user->id);
$maxOrderNumber = $this->config->get('kanbanMaxOrderNumber') ?? null;
if ($maxOrderNumber) {
$processor->setMaxNumber($maxOrderNumber);
}
$processor->order($ids);
}
}
@@ -0,0 +1,101 @@
<?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\Services;
use Espo\Core\{
ServiceFactory,
Utils\Metadata,
};
use Espo\{
Entities\User,
};
use StdClass;
use Throwable;
class PopupNotification
{
protected $metadata;
protected $serviceFactory;
protected $user;
public function __construct(Metadata $metadata, ServiceFactory $serviceFactory, User $user)
{
$this->metadata = $metadata;
$this->serviceFactory = $serviceFactory;
$this->user = $user;
}
public function getGroupedList() : StdClass
{
$data = $this->metadata->get(['app', 'popupNotifications']) ?? [];
$data = array_filter($data, function ($item) {
if (!($item['grouped'] ?? false)) {
return false;
}
if (!($item['serviceName'] ?? null)) {
return false;
}
if (!($item['methodName'] ?? null)) {
return false;
}
$portalDisabled = $item['portalDisabled'] ?? false;
if ($portalDisabled && $this->user->isPortal()) {
return false;
}
return true;
});
$result = (object) [];
foreach ($data as $type => $item) {
$serviceName = $item['serviceName'];
$methodName = $item['methodName'];
try {
$service = $this->serviceFactory->create($serviceName);
$result->$type = $service->$methodName($this->user->id);
}
catch (Throwable $e) {
$this->log->error("Popup notifications: " . $e->getMessage());
}
}
return $result;
}
}
+15 -2
View File
@@ -1356,17 +1356,30 @@ class Record implements Crud,
$disableCount = true;
}
$orderDisabled = $this->metadata->get(['scopes', $this->entityType, 'kanbanOrderDisabled']) ?? false;
$this->handleListParams($params);
$kanban = $this->injectableFactory->create(KanbanTool::class);
return $kanban
$kanban
->setEntityType($this->entityType)
->setRecordService($this)
->setSearchParams($params)
->setCountDisabled($disableCount)
->setOrderDisabled($orderDisabled)
->setMaxSelectTextAttributeLength($this->getMaxSelectTextAttributeLength())
->getResult();
->setUserId($this->getUser()->id);
$maxOrderNumber = $this->getConfig()->get('kanbanMaxOrderNumber') ?? null;
if ($maxOrderNumber) {
$kanban->setMaxOrderNumber($maxOrderNumber);
}
return $kanban->getResult();
}
protected function getEntityEvenDeleted(string $id) : ?Entity
+65 -3
View File
@@ -34,11 +34,11 @@ use Espo\Core\{
Utils\Metadata,
Select\SelectManagerFactory,
ORM\EntityManager,
//Record\Collection as RecordCollection,
};
use Espo\{
Services\Record as RecordService,
ORM\QueryParams\Select as SelectQuery,
};
class Kanban
@@ -47,10 +47,18 @@ class Kanban
protected $countDisabled = false;
protected $orderDisabled = false;
protected $searchParams = [];
protected $maxSelectTextAttributeLength = null;
protected $userId = null;
protected $maxOrderNumber = 50;
const MAX_GROUP_LENGTH = 100;
protected $metadata;
protected $selectManagerFactory;
protected $entityManager;
@@ -93,6 +101,27 @@ class Kanban
return $this;
}
public function setOrderDisabled(bool $orderDisabled) : self
{
$this->orderDisabled = $orderDisabled;
return $this;
}
public function setUserId(string $userId) : self
{
$this->userId = $userId;
return $this;
}
public function setMaxOrderNumber(int $maxOrderNumber) : self
{
$this->maxOrderNumber = $maxOrderNumber;
return $this;
}
public function setMaxSelectTextAttributeLength(?int $maxSelectTextAttributeLength) : self
{
$this->maxSelectTextAttributeLength = $maxSelectTextAttributeLength;
@@ -159,14 +188,47 @@ class Kanban
$selectParamsSub = $selectParams;
$selectParamsSub['whereClause'][] = [
$statusField => $status
$statusField => $status,
];
$o = (object) [
'name' => $status,
];
$collectionSub = $repository->find($selectParamsSub);
$query = SelectQuery::fromRaw($selectParamsSub);
$newOrder = $selectParamsSub['orderBy'] ?? [];
array_unshift($newOrder, [
'COALESCE:(kanbanOrder.order, ' . strval($this->maxOrderNumber + 1) . ')',
'ASC',
]);
if ($this->userId && !$this->orderDisabled) {
$group = mb_substr($status, 0, self::MAX_GROUP_LENGTH);
$builder = $this->entityManager
->getQueryBuilder()
->select()
->clone($query)
->order($newOrder)
->leftJoin(
'KanbanOrder',
'kanbanOrder',
[
'kanbanOrder.entityType' => $this->entityType,
'kanbanOrder.entityId:' => 'id',
'kanbanOrder.group' => $group,
'kanbanOrder.userId' => $this->userId,
]
);
$query = $builder->build();
}
$collectionSub = $repository
->clone($query)
->find();
if (!$this->countDisabled) {
$totalSub = $repository->count($selectParamsSub);
+76
View File
@@ -0,0 +1,76 @@
<?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\Tools\Kanban;
use Espo\Core\{
ORM\EntityManager,
Utils\Metadata,
};
class Orderer
{
private $entityManager;
private $metadata;
public function __construct(EntityManager $entityManager, Metadata $metadata)
{
$this->entityManager = $entityManager;
$this->metadata = $metadata;
}
public function setEntityType(string $entityType) : OrdererProcessor
{
return $this->createProcessor()->setEntityType($entityType);
}
public function setGroup(string $group) : OrdererProcessor
{
return $this->createProcessor()->setGroup($group);
}
public function setUserId(string $userId) : OrdererProcessor
{
return $this->createProcessor()->setUserId($userId);
}
public function setMaxNumber(int $maxNumber) : OrdererProcessor
{
return $this->createProcessor()->setMaxNumber($maxNumber);
}
public function createProcessor() : OrdererProcessor
{
return new OrdererProcessor(
$this->entityManager,
$this->metadata
);
}
}
@@ -0,0 +1,232 @@
<?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\Tools\Kanban;
use Espo\Core\{
ORM\EntityManager,
Utils\Metadata,
Utils\Util,
};
use LogicException;
class OrdererProcessor
{
private $entityType;
private $group;
private $userId = null;
private $maxNumber = 50;
private $entityManager;
private $metadata;
const MAX_GROUP_LENGTH = 100;
public function __construct(EntityManager $entityManager, Metadata $metadata)
{
$this->entityManager = $entityManager;
$this->metadata = $metadata;
}
public function setEntityType(string $entityType) : self
{
$this->entityType = $entityType;
return $this;
}
public function setGroup(string $group) : self
{
$group = mb_substr($group, 0, self::MAX_GROUP_LENGTH);
$this->group = $group;
return $this;
}
public function setUserId(string $userId) : self
{
$this->userId = $userId;
return $this;
}
public function setMaxNumber(int $maxNumber) : self
{
$this->maxNumber = $maxNumber;
return $this;
}
public function order(array $ids)
{
$this->validate();
$count = count($ids);
if (!$count) {
return;
}
$this->entityManager
->getTransactionManager()
->start();
$deleteQuery = $this->entityManager
->getQueryBuilder()
->delete()
->from('KanbanOrder')
->where([
'entityType' => $this->entityType,
'userId' => $this->userId,
'entityId' => $ids,
])
->build();
$this->entityManager->getQueryExecutor()->execute($deleteQuery);
$minOrder = null;
$first = $this->entityManager
->getRepository('KanbanOrder')
->select(['id', 'order'])
->where([
'entityType' => $this->entityType,
'userId' => $this->userId,
'group' => $this->group,
])
->order('order')
->findOne();
if ($first) {
$minOrder = $first->get('order');
}
if ($minOrder !== null) {
$offset = $count - $minOrder;
$updateQuery = $this->entityManager
->getQueryBuilder()
->update()
->in('KanbanOrder')
->where([
'entityType' => $this->entityType,
'group' => $this->group,
'userId' => $this->userId,
])
->set([
'order:' => 'ADD:(order, ' . strval($offset) . ')'
])
->build();
$this->entityManager->getQueryExecutor()->execute($updateQuery);
}
$collection = $this->entityManager
->getCollectionFactory()
->create('KanbanOrder');
foreach ($ids as $i => $id) {
$item = $this->entityManager->getEntity('KanbanOrder');
$item->set([
'id' => Util::generateId(),
'entityId' => $id,
'entityType' => $this->entityType,
'group' => $this->group,
'userId' => $this->userId,
'order' => $i,
]);
$collection[] = $item;
}
$this->entityManager->getMapper()->massInsert($collection);
$deleteQuery = $this->entityManager
->getQueryBuilder()
->delete()
->from('KanbanOrder')
->where([
'entityType' => $this->entityType,
'group' => $this->group,
'userId' => $this->userId,
'order>' => $this->maxNumber,
])
->build();
$this->entityManager->getQueryExecutor()->execute($deleteQuery);
$this->entityManager
->getTransactionManager()
->commit();
}
private function validate()
{
if (! $this->entityType) {
throw new LogicException("No entity type.");
}
if (! $this->group) {
throw new LogicException("No group.");
}
if (! $this->userId) {
throw new LogicException("No user ID.");
}
if (! $this->metadata->get(['scopes', $this->entityType, 'object'])) {
throw new LogicException("Not allowed entity type.");
}
$orderDisabled = $this->metadata->get(['scopes', $this->entityType, 'kanbanOrderDisabled']);
if ($orderDisabled) {
throw new LogicException("Order is disabled.");
}
$statusField = $this->metadata->get(['scopes', $this->entityType, 'statusField']);
if (! $statusField) {
throw new LogicException("Not status field.");
}
$statusList = $this->metadata->get(['entityDefs', $this->entityType, 'fields', $statusField, 'options']) ?? [];
if (! in_array($this->group, $statusList)) {
throw new LogicException("Group is not available in status list.");
}
}
}
@@ -1,4 +1,4 @@
<ul class="list-group">
<ul class="list-group no-side-margin">
{{#each optionDataList}}
<li class="list-group-item">
<a href="javascript:" data-action="move" data-value="{{value}}">{{label}}</a>
+18 -5
View File
@@ -68,20 +68,33 @@ define('views/modals/kanban-move-over', 'views/modal', function (Dep) {
this.optionDataList = [];
(this.getMetadata().get(['entityDefs', this.scope, 'fields', this.statusField, 'options']) || []).forEach(function (item) {
(this.getMetadata().get(
['entityDefs', this.scope, 'fields', this.statusField, 'options']) || []
).forEach(function (item) {
this.optionDataList.push({
value: item,
label: this.getLanguage().translateOption(item, this.statusField, this.scope)
label: this.getLanguage().translateOption(item, this.statusField, this.scope),
});
}, this);
},
moveTo: function (status) {
var attributes = {};
attributes[this.statusField] = status;
this.model.save(attributes, {patch: true}).then(function () {
Espo.Ui.success(this.translate('Done'));
}.bind(this));
this.model
.save(
attributes,
{
patch: true,
isMoveTo: true,
}
)
.then(function () {
Espo.Ui.success(this.translate('Done'));
}.bind(this));
this.close();
},
+129 -13
View File
@@ -57,6 +57,7 @@ define('views/notification/badge', 'view', function (Dep) {
if (this.timeout) {
clearTimeout(this.timeout);
}
for (var name in this.popoupTimeouts) {
clearTimeout(this.popoupTimeouts[name]);
}
@@ -76,8 +77,10 @@ define('views/notification/badge', 'view', function (Dep) {
window.addEventListener('storage', function (e) {
if (e.key == 'messageClosePopupNotificationId') {
var id = localStorage.getItem('messageClosePopupNotificationId');
if (id) {
var key = 'popup-' + id;
if (this.hasView(key)) {
this.markPopupRemoved(id);
this.clearView(key);
@@ -102,18 +105,24 @@ define('views/notification/badge', 'view', function (Dep) {
this.runCheckUpdates(true);
this.$popupContainer = $('#popup-notifications-container');
if (!$(this.$popupContainer).length) {
this.$popupContainer = $('<div>').attr('id', 'popup-notifications-container').addClass('hidden').appendTo('body');
}
var popupNotificationsData = this.popupNotificationsData = this.getMetadata().get('app.popupNotifications') || {};
for (var name in popupNotificationsData) {
this.checkPopupNotifications(name);
}
this.checkGroupedPopupNotifications();
},
playSound: function () {
if (this.notificationSoundsDisabled) return;
if (this.notificationSoundsDisabled) {
return;
}
var html = '' +
'<audio autoplay="autoplay">'+
@@ -142,26 +151,33 @@ define('views/notification/badge', 'view', function (Dep) {
checkBypass: function () {
var last = this.getRouter().getLast() || {};
if (last.controller == 'Admin' && last.action == 'upgrade') {
return true;
}
},
checkUpdates: function (isFirstCheck) {
if (this.checkBypass()) return;
if (this.checkBypass()) {
return;
}
Espo.Ajax.getRequest('Notification/action/notReadCount').then(function (count) {
if (!isFirstCheck && count > this.unreadCount) {
var messageBlockPlayNotificationSound = localStorage.getItem('messageBlockPlayNotificationSound');
if (!messageBlockPlayNotificationSound) {
this.playSound();
localStorage.setItem('messageBlockPlayNotificationSound', true);
setTimeout(function () {
delete localStorage['messageBlockPlayNotificationSound'];
}, this.notificationsCheckInterval * 1000);
}
}
this.unreadCount = count;
if (count) {
this.showNotRead(count);
} else {
@@ -176,40 +192,112 @@ define('views/notification/badge', 'view', function (Dep) {
if (this.useWebSocket) {
this.getHelper().webSocketManager.subscribe('newNotification', function () {
this.checkUpdates();
}.bind(this))
}.bind(this));
return;
}
this.timeout = setTimeout(function () {
this.runCheckUpdates();
}.bind(this), this.notificationsCheckInterval * 1000);
},
checkGroupedPopupNotifications: function () {
var toCheck = false;
for (var name in this.popupNotificationsData) {
var data = this.popupNotificationsData[name] || {};
if (!data.grouped) {
continue;
}
if (data.portalDisabled && this.getUser().isPortal()) {
return;
}
toCheck = true;
}
if (!toCheck) {
return;
}
Espo.Ajax.getRequest('PopupNotification/action/grouped')
.then(
function (result) {
for (const type in result) {
const list = result[name];
list.forEach(function (item) {
this.showPopupNotification(type, item);
}, this);
}
}.bind(this)
);
},
checkPopupNotifications: function (name, isNotFirstCheck) {
var data = this.popupNotificationsData[name] || {};
var url = data.url;
var interval = data.interval;
var disabled = data.disabled || false;
if (disabled) return;
if (data.portalDisabled && this.getUser().isPortal()) return;
var isFirstCheck = !isNotFirstCheck;
if (disabled) {
return;
}
if (data.portalDisabled && this.getUser().isPortal()) {
return;
}
var useWebSocket = this.useWebSocket && data.useWebSocket;
if (useWebSocket) {
var category = 'popupNotifications.' + (data.webSocketCategory || name);
this.getHelper().webSocketManager.subscribe(category, function (c, response) {
if (!response.list) return;
if (!response.list) {
return;
}
response.list.forEach(function (item) {
this.showPopupNotification(name, item);
}, this);
}.bind(this))
}
if (!url || !interval) return;
if (data.grouped && interval && !useWebSocket && isFirstCheck) {
this.popoupTimeouts[name] = setTimeout(
function () {
this.checkPopupNotifications(name, true);
}.bind(this),
interval * 1000
);
return;
}
if (data.grouped && isFirstCheck) {
return;
}
if (!url) {
return;
}
if (!interval) {
return;
}
(new Promise(
function (resolve) {
if (this.checkBypass()) {
resolve();
return;
}
@@ -230,10 +318,12 @@ define('views/notification/badge', 'view', function (Dep) {
}.bind(this)
)).then(
function () {
if (useWebSocket) return;
if (useWebSocket) {
return;
}
this.popoupTimeouts[name] = setTimeout(function () {
this.checkPopupNotifications(name, isNotFirstCheck);
this.checkPopupNotifications(name, true);
}.bind(this), interval * 1000);
}.bind(this)
);
@@ -241,20 +331,29 @@ define('views/notification/badge', 'view', function (Dep) {
showPopupNotification: function (name, data, isNotFirstCheck) {
var view = this.popupNotificationsData[name].view;
if (!view) return;
if (!view) {
return;
}
var id = data.id || null;
if (id) {
id = name + '_' + id;
if (~this.shownNotificationIds.indexOf(id)) {
var notificationView = this.getView('popup-' + id);
if (notificationView) {
notificationView.trigger('update-data', data.data);
}
return;
}
if (~this.closedNotificationIds.indexOf(id)) {
return;
}
if (~this.closedNotificationIds.indexOf(id)) return;
} else {
id = this.lastId++;
}
@@ -268,7 +367,9 @@ define('views/notification/badge', 'view', function (Dep) {
isFirstCheck: !isNotFirstCheck,
}, function (view) {
view.render();
this.$popupContainer.removeClass('hidden');
this.listenTo(view, 'remove', function () {
this.markPopupRemoved(id);
localStorage.setItem('messageClosePopupNotificationId', id);
@@ -278,22 +379,30 @@ define('views/notification/badge', 'view', function (Dep) {
markPopupRemoved: function (id) {
var index = this.shownNotificationIds.indexOf(id);
if (index > -1) {
this.shownNotificationIds.splice(index, 1);
}
if (this.shownNotificationIds.length == 0) {
this.$popupContainer.addClass('hidden');
}
this.closedNotificationIds.push(id);
},
broadcastNotificationsRead: function () {
if (!this.useWebSocket) return;
if (!this.useWebSocket) {
return;
}
this.isBroadcustingNotificationRead = true;
localStorage.setItem('messageNotificationRead', true);
setTimeout(function () {
this.isBroadcustingNotificationRead = false;
delete localStorage['messageNotificationRead'];
}.bind(this), 500);
},
@@ -309,6 +418,7 @@ define('views/notification/badge', 'view', function (Dep) {
el: '#notifications-panel',
}, function (view) {
view.render();
this.listenTo(view, 'all-read', function () {
this.hideNotRead();
this.$el.find('.badge-circle-warning').remove();
@@ -326,6 +436,7 @@ define('views/notification/badge', 'view', function (Dep) {
});
$document = $(document);
$document.on('mouseup.notification', function (e) {
if (!$container.is(e.target) && $container.has(e.target).length === 0) {
if (!$(e.target).closest('div.modal-dialog').length) {
@@ -345,12 +456,17 @@ define('views/notification/badge', 'view', function (Dep) {
$container = $('#notifications-panel');
$('#notifications-panel').remove();
$document = $(document);
if (this.hasView('panel')) {
this.getView('panel').remove();
};
}
$document.off('mouseup.notification');
$container.remove();
},
});
});
+203 -35
View File
@@ -57,10 +57,13 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
events: {
'click a.link': function (e) {
e.stopPropagation();
if (!this.scope || this.selectable) {
return;
}
e.preventDefault();
var id = $(e.currentTarget).data('id');
var model = this.collection.get(id);
@@ -68,8 +71,9 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
var options = {
id: id,
model: model
model: model,
};
if (this.options.keepCurrentRootUrl) {
options.rootUrl = this.getRouter().getCurrentUrl();
}
@@ -79,7 +83,9 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
},
'click [data-action="groupShowMore"]': function (e) {
var $target = $(e.currentTarget);
var group = $target.data('name');
this.groupShowMore(group);
},
'click .action': function (e) {
@@ -122,7 +128,9 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
this.layoutName = this.options.layoutName || this.layoutName || this.type;
this.rowActionsView = _.isUndefined(this.options.rowActionsView) ? this.rowActionsView : this.options.rowActionsView;
this.rowActionsView = _.isUndefined(this.options.rowActionsView) ?
this.rowActionsView :
this.options.rowActionsView;
if (this.massActionsDisabled && !this.selectable) {
this.checkboxes = false;
@@ -156,6 +164,7 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
if ('showCount' in this.options) {
this.showCount = this.options.showCount;
}
this.displayTotalCount = this.showCount && this.getConfig().get('displayListViewRecordCount');
if ('displayTotalCount' in this.options) {
@@ -168,17 +177,25 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
}
}
this.orderDisabled = this.getMetadata().get(['scopes', this.scope, 'kanbanOrderDisabled']);
this.statusField = this.getMetadata().get(['scopes', this.scope, 'statusField']);
if (!this.statusField) {
throw new Error("No status field for entity type '" + this.scope + "'.");
}
this.statusList = Espo.Utils.clone(this.getMetadata().get(['entityDefs', this.scope, 'fields', this.statusField, 'options']));
this.statusList = Espo.Utils.clone(this.getMetadata().get(
['entityDefs', this.scope, 'fields', this.statusField, 'options'])
);
var statusIgnoreList = this.getMetadata().get(['scopes', this.scope, 'kanbanStatusIgnoreList']) || [];
this.statusList = this.statusList.filter(function (item) {
if (~statusIgnoreList.indexOf(item)) return;
if (~statusIgnoreList.indexOf(item)) {
return;
}
return true;
}, this);
@@ -191,7 +208,9 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
this.seedCollection.order = this.collection.defaultOrder;
this.listenTo(this.collection, 'sync', function (c, r, options) {
if (this.hasView('modal') && this.getView('modal').isRendered()) return;
if (this.hasView('modal') && this.getView('modal').isRendered()) {
return;
}
this.buildRows(function () {
this.render();
@@ -272,6 +291,7 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
$container.css('width', '');
$block.hide();
$container.show();
return;
}
@@ -290,6 +310,7 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
}
} else {
$container.css('width', '');
if ($container.hasClass('sticked')) {
$container.removeClass('sticked');
$block.hide();
@@ -312,6 +333,8 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
e.originalEvent.stopPropagation();
}.bind(this));
var orderDisabled = this.orderDisabled;
$list.sortable({
connectWith: '.group-column-list',
cancel: '.dropdown-menu *',
@@ -319,48 +342,122 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
if (this.isItemBeingMoved) {
}
this.draggedGroupFrom = $(ui.item).closest('.group-column-list').data('name');
}.bind(this),
stop: function (e, ui) {
var $item = $(ui.item);
var group = $item.closest('.group-column-list').data('name');
var id = $item.data('id');
var draggedGroupFrom = this.draggedGroupFrom;
this.draggedGroupFrom = null;
if (group !== draggedGroupFrom) {
var model = this.collection.get(id);
if (!model) {
$list.sortable('cancel');
return;
}
var attributes = {};
attributes[this.statusField] = group;
this.handleAttributesOnGroupChange(model, attributes, group);
$list.sortable('disable');
model.save(attributes, {patch: true, isDrop: true}).then(function () {
Espo.Ui.success(this.translate('Saved'));
$list.sortable('destroy');
this.initSortable();
}.bind(this)).fail(function () {
model
.save(attributes, {patch: true, isDrop: true})
.then(function () {
Espo.Ui.success(this.translate('Saved'));
$list.sortable('destroy');
this.initSortable();
if (!orderDisabled) {
this.reOrderGroup(group);
this.storeGroupOrder(group);
}
}.bind(this))
.fail(function () {
$list.sortable('cancel');
$list.sortable('enable');
}.bind(this));
} else {
if (orderDisabled) {
$list.sortable('cancel');
$list.sortable('enable');
}.bind(this));
} else {
$list.sortable('cancel');
$list.sortable('enable');
return;
}
this.reOrderGroup(group);
this.storeGroupOrder(group);
}
}.bind(this)
});
},
storeGroupOrder: function (group) {
Espo.Ajax.postRequest('KanbanOrder/action/store', {
entityType: this.entityType,
group: group,
ids: this.getGroupOrderFromDom(group),
});
},
getGroupOrderFromDom: function (group) {
var ids = [];
var $group = this.$el.find('.group-column-list[data-name="'+group+'"]');
$group.children().each(function (i, el) {
ids.push($(el).data('id'));
});
return ids;
},
reOrderGroup: function (group) {
var groupCollection = this.getGroupCollection(group);
var ids = this.getGroupOrderFromDom(group);
var modelMap = {};
groupCollection.models.forEach(function (m) {
modelMap[m.id] = m;
});
while (groupCollection.models.length) {
groupCollection.pop({silent: true})
}
ids.forEach(function (id) {
var model = modelMap[id];
if (!model) {
return;
}
groupCollection.add(model, {silent: true});
});
},
handleAttributesOnGroupChange: function (model, attributes, group) {},
adjustMinHeight: function () {
if (this.collection.models.length === 0) return;
if (this.collection.models.length === 0) {
return;
}
var top = this.$listKanban.find('table > tbody').position().top;
var bottom = this.$content.position().top + this.$content.outerHeight(true);
@@ -374,13 +471,14 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
}
this.$listKanban.find('td.group-column > div').css({
minHeight: height + 'px'
minHeight: height + 'px',
});
},
getListLayout: function (callback) {
if (this.listLayout) {
callback.call(this, this.listLayout);
return;
}
@@ -397,6 +495,7 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
attrubuteList.push(this.statusField);
}
}
callback(attrubuteList);
}.bind(this));
},
@@ -420,20 +519,25 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
groupList.forEach(function (item, i) {
var collection = this.seedCollection.clone();
collection.total = item.total;
collection.url = this.scope;
//collection.url = this.scope;
collection.url = this.collection.url;
collection.where = this.collection.where;
collection.name = this.seedCollection.name;
collection.maxSize = this.seedCollection.maxSize;
collection.orderBy = this.seedCollection.orderBy;
collection.order = this.seedCollection.order;
collection.whereAdditional = [
{
field: this.statusField,
type: 'equals',
value: item.name
value: item.name,
}
];
collection.groupName = item.name;
collection.set(item.list);
@@ -445,15 +549,19 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
collection.models.forEach(function (model) {
count ++;
itemDataList.push({
key: model.id,
id: model.id
id: model.id,
});
}, this);
var nextStyle = null;
if (i < groupList.length - 1) {
nextStyle = this.getMetadata().get(['entityDefs', this.scope, 'fields', this.statusField, 'style', groupList[i + 1].name]);
nextStyle = this.getMetadata().get(
['entityDefs', this.scope, 'fields', this.statusField, 'style', groupList[i + 1].name]
);
}
var o = {
@@ -463,8 +571,10 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
collection: collection,
isLast: i === groupList.length - 1,
hasShowMore: collection.total > collection.length || collection.total == -1,
style: this.getMetadata().get(['entityDefs', this.scope, 'fields', this.statusField, 'style', item.name]),
nextStyle: nextStyle
style: this.getMetadata().get(
['entityDefs', this.scope, 'fields', this.statusField, 'style', item.name]
),
nextStyle: nextStyle,
};
this.groupDataList.push(o);
@@ -472,6 +582,7 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
if (count === 0) {
this.wait(false);
if (callback) {
callback();
}
@@ -479,10 +590,13 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
this.groupDataList.forEach(function (groupItem) {
groupItem.dataList.forEach(function (item, j) {
var model = groupItem.collection.get(item.id);
this.buildRow(j, model, function (view) {
loadedCount++;
if (loadedCount === count) {
this.wait(false);
if (callback) {
callback();
}
@@ -504,15 +618,17 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
rowActionsDisabled: this.rowActionsDisabled,
rowActionsView: this.rowActionsView,
setViewBeforeCallback: this.options.skipBuildRows && !this.isRendered(),
statusFieldIsEditable: this.statusFieldIsEditable
statusFieldIsEditable: this.statusFieldIsEditable,
}, callback);
},
removeRecordFromList: function (id) {
this.collection.remove(id);
if (this.collection.total > 0) {
this.collection.total--;
}
this.totalCount = this.collection.total;
this.$el.find('.total-count-span').text(this.totalCount.toString());
@@ -529,14 +645,20 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
for (var i in this.groupDataList) {
var groupItem = this.groupDataList[i];
for (var j in groupItem.dataList) {
var item = groupItem.dataList[j];
if (item.id === id) {
groupItem.dataList.splice(j, 1);
if (groupItem.collection.total > 0) {
groupItem.collection.total--;
}
groupItem.hasShowMore = groupItem.collection.total > groupItem.collection.length || groupItem.collection.total == -1;
groupItem.hasShowMore = groupItem.collection.total > groupItem.collection.length ||
groupItem.collection.total == -1;
break;
}
}
@@ -550,6 +672,7 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
this.collection.subCollectionList.forEach(function (collection) {
if (collection.get(id)) {
collection.remove(id);
if (collection.total > 0) {
collection.total--;
}
@@ -560,27 +683,42 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
for (var i in this.groupDataList) {
var groupItem = this.groupDataList[i];
for (var j in groupItem.dataList) {
var item = groupItem.dataList[j];
if (item.id === id) {
dataItem = item;
groupItem.dataList.splice(j, 1);
break;
}
}
}
if (!group) return;
if (o.isDrop) return;
if (!group) {
return;
}
if (o.isDrop) {
return;
}
for (var i in this.groupDataList) {
var groupItem = this.groupDataList[i];
if (groupItem.name !== group) continue;
if (groupItem.name !== group) {
continue;
}
groupItem.collection.unshift(model);
groupItem.collection.total++;
if (dataItem) {
groupItem.dataList.unshift(dataItem);
groupItem.hasShowMore = groupItem.collection.total > groupItem.collection.length || groupItem.collection.total == -1;
groupItem.hasShowMore = groupItem.collection.total > groupItem.collection.length ||
groupItem.collection.total == -1;
}
}
@@ -592,21 +730,29 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
} else {
$item.remove();
}
if (!this.orderDisabled) {
this.storeGroupOrder(group);
}
},
groupShowMore: function (group) {
for (var i in this.groupDataList) {
var groupItem = this.groupDataList[i];
if (groupItem.name === group) {
break;
} else {
groupItem = null;
}
groupItem = null;
}
if (!groupItem) return;
if (!groupItem) {
return;
}
var collection = groupItem.collection;
var $list = this.$el.find('.group-column-list[data-name="'+group+'"]');
var $showMore = this.$el.find('.group-column[data-name="'+group+'"] .show-more');
@@ -614,17 +760,26 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
this.showMoreRecords(collection, $list, $showMore, function () {
this.noRebuild = false;
collection.models.forEach(function (model) {
if (this.collection.get(model.id)) return;
if (this.collection.get(model.id)) {
return;
}
this.collection.add(model);
groupItem.dataList.push({
key: model.id,
id: model.id
id: model.id,
});
}, this);
});
},
getDomRowItem: function (id) {
return this.$el.find('.item[data-id="'+id+'"]');
},
getRowContainerHtml: function (id) {
return '<div class="item" data-id="'+id+'">';
},
@@ -634,11 +789,24 @@ define('views/record/kanban', ['views/record/list'], function (Dep) {
this.createView('moveOverDialog', 'views/modals/kanban-move-over', {
model: model,
statusField: this.statusField
statusField: this.statusField,
}, function (view) {
view.render();
});
}
},
getGroupCollection: function (group) {
var collection = null;
this.collection.subCollectionList.forEach(function (itemCollection) {
if (itemCollection.groupName === group) {
collection = itemCollection;
}
}, this);
return collection;
},
});
});
+22
View File
@@ -1574,32 +1574,50 @@ define('views/record/list', 'view', function (Dep) {
var success = function () {
Espo.Ui.notify(false);
$showMore.addClass('hidden');
var rowCount = collection.length - initialCount;
var rowsReady = 0;
if (collection.length <= initialCount) {
final();
}
for (var i = initialCount; i < collection.length; i++) {
var model = collection.at(i);
this.buildRow(i, model, function (view) {
var model = view.model;
view.getHtml(function (html) {
var $row = $(this.getRowContainerHtml(model.id));
$row.append(html);
var $existingRowItem = this.getDomRowItem(model.id);
if ($existingRowItem && $existingRowItem.length) {
$existingRowItem.remove();
}
$list.append($row);
rowsReady++;
if (rowsReady == rowCount) {
final();
}
view._afterRender();
if (view.options.el) {
view.setElement(view.options.el);
}
}.bind(this));
});
}
this.noRebuild = true;
}.bind(this);
@@ -1616,6 +1634,10 @@ define('views/record/list', 'view', function (Dep) {
});
},
getDomRowItem: function (id) {
return null;
},
getRowContainerHtml: function (id) {
return '<tr data-id="'+id+'" class="list-row"></tr>';
},