From 19784fc09cfbd40d9e80c1f0ad0688bb490ee830 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Fri, 5 Dec 2014 17:46:19 +0200 Subject: [PATCH] popup notifications 1 --- .../Modules/Crm/Controllers/Activities.php | 11 +- .../metadata/app/popupNotifications.json | 7 + .../Espo/Modules/Crm/Services/Activities.php | 16 ++- .../templates/meeting/popup-notification.tpl | 19 +++ .../src/views/meeting/popup-notification.js | 46 +++++++ .../client/src/views/notifications/badge.js | 121 ++++++++++++++++-- .../client/src/views/popup-notification.js | 84 ++++++++++++ frontend/less/espo/custom.less | 74 +++++++++++ 8 files changed, 361 insertions(+), 17 deletions(-) create mode 100644 application/Espo/Modules/Crm/Resources/metadata/app/popupNotifications.json create mode 100644 frontend/client/modules/crm/res/templates/meeting/popup-notification.tpl create mode 100644 frontend/client/modules/crm/src/views/meeting/popup-notification.js create mode 100644 frontend/client/src/views/popup-notification.js diff --git a/application/Espo/Modules/Crm/Controllers/Activities.php b/application/Espo/Modules/Crm/Controllers/Activities.php index 4213e428cd..4c19b4bd4d 100644 --- a/application/Espo/Modules/Crm/Controllers/Activities.php +++ b/application/Espo/Modules/Crm/Controllers/Activities.php @@ -28,7 +28,7 @@ use \Espo\Core\Exceptions\Error, class Activities extends \Espo\Core\Controllers\Base { - public static $defaultAction = 'index'; + public static $defaultAction = 'index'; public function actionListCalendarEvents($params, $data, $request) { @@ -48,6 +48,13 @@ class Activities extends \Espo\Core\Controllers\Base return $service->getEvents($this->getUser()->id, $from, $to); } + public function actionPopupNotifications() + { + $userId = $this->getUser()->id; + + return $this->getService('Activities')->getPopupNotifications($userId); + } + public function actionList($params, $data, $request) { $name = $params['name']; @@ -68,7 +75,7 @@ class Activities extends \Espo\Core\Controllers\Base $scope = null; if (!empty($where) && !empty($where['scope']) && $where['scope'] !== 'false') { $scope = $where['scope']; - } + } $service = $this->getService('Activities'); diff --git a/application/Espo/Modules/Crm/Resources/metadata/app/popupNotifications.json b/application/Espo/Modules/Crm/Resources/metadata/app/popupNotifications.json new file mode 100644 index 0000000000..63341c644a --- /dev/null +++ b/application/Espo/Modules/Crm/Resources/metadata/app/popupNotifications.json @@ -0,0 +1,7 @@ +{ + "event": { + "url": "Activities/action/popupNotifications", + "interval": 5, + "view": "Crm:Meeting.PopupNotification" + } +} diff --git a/application/Espo/Modules/Crm/Services/Activities.php b/application/Espo/Modules/Crm/Services/Activities.php index aef76bad0f..7084cf7004 100644 --- a/application/Espo/Modules/Crm/Services/Activities.php +++ b/application/Espo/Modules/Crm/Services/Activities.php @@ -430,11 +430,23 @@ class Activities extends \Espo\Core\Services\Base "; - $sth = $pdo->prepare($sql); - $sth->execute(); + $sth = $pdo->prepare($sql); + $sth->execute(); $rows = $sth->fetchAll(PDO::FETCH_ASSOC); return $rows; } + + public function getPopupNotifications($userId) + { + return array( + array( + 'id' => '43276532423', + 'entityType' => 'Call', + 'dateStart' => '2014-12-05 14:10', + 'name' => 'Test Call' + ) + ); + } } diff --git a/frontend/client/modules/crm/res/templates/meeting/popup-notification.tpl b/frontend/client/modules/crm/res/templates/meeting/popup-notification.tpl new file mode 100644 index 0000000000..086f7094e8 --- /dev/null +++ b/frontend/client/modules/crm/res/templates/meeting/popup-notification.tpl @@ -0,0 +1,19 @@ +{{#if closeButton}} + +{{/if}} +

{{header}}

+ + +
+
+ {{notificationData.name}} +
+ +
+ +
+
+ {{{dateStart}}} +
+
+ diff --git a/frontend/client/modules/crm/src/views/meeting/popup-notification.js b/frontend/client/modules/crm/src/views/meeting/popup-notification.js new file mode 100644 index 0000000000..df96570d89 --- /dev/null +++ b/frontend/client/modules/crm/src/views/meeting/popup-notification.js @@ -0,0 +1,46 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://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/. + ************************************************************************/ + +Espo.define('Crm:Views.Meeting.PopupNotification', 'Views.PopupNotification', function (Dep) { + + return Dep.extend({ + + type: 'event', + + style: 'warning', + + template: 'crm:meeting.popup-notification', + + closeButton: true, + + setup: function () { + + }, + + data: function () { + return _.extend({ + header: this.translate(this.notificationData.entityType, 'scopeNames') + }, Dep.prototype.data.call(this)); + } + + }); +}); + diff --git a/frontend/client/src/views/notifications/badge.js b/frontend/client/src/views/notifications/badge.js index ab919e62aa..1095b93e02 100644 --- a/frontend/client/src/views/notifications/badge.js +++ b/frontend/client/src/views/notifications/badge.js @@ -17,7 +17,7 @@ * * You should have received a copy of the GNU General Public License * along with EspoCRM. If not, see http://www.gnu.org/licenses/. - ************************************************************************/ + ************************************************************************/ Espo.define('Views.Notifications.Badge', 'View', function (Dep) { @@ -28,6 +28,8 @@ Espo.define('Views.Notifications.Badge', 'View', function (Dep) { updateFrequency: 10, timeout: null, + + popupNotificationsData: null, events: { 'click a[data-action="showNotifications"]': function (e) { @@ -40,13 +42,48 @@ Espo.define('Views.Notifications.Badge', 'View', function (Dep) { if (this.timeout) { clearTimeout(this.timeout); } + for (var name in this.popoupTimeouts) { + clearTimeout(this.popoupTimeouts[name]); + } }, this); + + this.lastId = 0; + this.shownNotificationIds = []; + this.closedNotificationIds = []; + this.popoupTimeouts = {}; + + delete localStorage['closePopupNotificationId']; + + window.addEventListener('storage', function (e) { + if (e.key == 'closePopupNotificationId') { + var id = localStorage.getItem('closePopupNotificationId'); + if (id) { + var key = 'popup-' + id; + if (this.hasView(key)) { + this.markPopupRemoved(id); + this.clearView(key); + } + } + + } + }.bind(this), false); }, afterRender: function () { this.$badge = this.$el.find('.notifications-button'); - this.$icon = this.$el.find('.notifications-button .icon'); + this.$icon = this.$el.find('.notifications-button .icon'); + this.checkUpdates(); + + this.$popupContainer = $('#popup-notifications-container'); + if (!$(this.$popupContainer).size()) { + this.$popupContainer = $('
').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); + } }, showNotRead: function (count) { @@ -59,7 +96,7 @@ Espo.define('Views.Notifications.Badge', 'View', function (Dep) { this.$badge.attr('title', ''); }, - checkUpdates: function () { + checkUpdates: function () { $.ajax('Notification/action/notReadCount').done(function (count) { if (count) { this.showNotRead(count); @@ -70,7 +107,65 @@ Espo.define('Views.Notifications.Badge', 'View', function (Dep) { this.timeout = setTimeout(function () { this.checkUpdates(); - }.bind(this), this.updateFrequency * 1000) + }.bind(this), this.updateFrequency * 1000); + }, + + checkPopupNotifications: function (name) { + var data = this.popupNotificationsData[name] || {}; + var url = data.url; + var interval = data.interval; + + if (!url || !interval) return; + + $.ajax(url).done(function (list) { + list.forEach(function (d) { + this.showPopupNotification(name, d); + }, this); + }.bind(this)); + + this.popoupTimeouts[name] = setTimeout(function () { + this.checkPopupNotifications(name); + }.bind(this), interval * 1000); + }, + + showPopupNotification: function (name, data) { + var view = this.popupNotificationsData[name].view; + if (!view) return; + + var id = data.id || null; + + if (id) { + id = name + '_' + id; + if (~this.shownNotificationIds.indexOf(id)) return; + if (~this.closedNotificationIds.indexOf(id)) return; + } else { + id = this.lastId++; + } + + this.shownNotificationIds.push(id); + + this.createView('popup-' + id, view, { + notificationData: data, + id: id + }, function (view) { + view.render(); + this.$popupContainer.removeClass('hidden'); + this.listenTo(view, 'remove', function () { + this.markPopupRemoved(id); + localStorage.setItem('closePopupNotificationId', id); + }, this); + }.bind(this)); + }, + + 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); }, showNotifications: function () { @@ -82,12 +177,12 @@ Espo.define('Views.Notifications.Badge', 'View', function (Dep) { 'z-index': 1001, 'right': 0, 'left': 'auto' - }); + }); $container.appendTo(this.$el.find('.notifications-panel-container')); this.createView('panel', 'Notifications.Panel', { - el: '#notifications-panel', + el: '#notifications-panel', }, function (view) { view.render(); this.listenTo(view, 'all-read', function () { @@ -96,24 +191,24 @@ Espo.define('Views.Notifications.Badge', 'View', function (Dep) { }, this); }.bind(this)); - $document = $(document); - $document.on('mouseup.notification', function (e) { - if (!$container.is(e.target) && $container.has(e.target).length === 0) { + $document = $(document); + $document.on('mouseup.notification', function (e) { + if (!$container.is(e.target) && $container.has(e.target).length === 0) { this.closeNotifications(); - } + } }.bind(this)); }, closeNotifications: function () { $container = $('#notifications-panel'); - $('#notifications-panel').remove(); + $('#notifications-panel').remove(); $document = $(document); if (this.hasView('panel')) { this.getView('panel').remove(); }; - $document.off('mouseup.notification'); - $container.remove(); + $document.off('mouseup.notification'); + $container.remove(); }, }); diff --git a/frontend/client/src/views/popup-notification.js b/frontend/client/src/views/popup-notification.js new file mode 100644 index 0000000000..c3f594459b --- /dev/null +++ b/frontend/client/src/views/popup-notification.js @@ -0,0 +1,84 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://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/. + ************************************************************************/ + +Espo.define('Views.PopupNotification', 'View', function (Dep) { + + return Dep.extend({ + + type: 'default', + + style: 'default', + + closeButton: true, + + init: function () { + Dep.prototype.init.call(this); + + var id = this.options.id; + var containerSelector = this.containerSelector = '#' + id; + + this.on('render', function () { + $(containerSelector).remove(); + + var className = 'popup-notification-' + Espo.Utils.toDom(this.type); + + $('
').attr('id', id) + .addClass('popup-notification') + .addClass(className) + .addClass('popup-notification-' + this.style) + .appendTo('#popup-notifications-container'); + + this.setElement(containerSelector); + }, this); + + this.on('after:render', function () { + this.$el.find('[data-action="close"]').on('click', function () { + this.cancel(); + }.bind(this)); + }, this); + + this.once('remove', function () { + $(containerSelector).remove(); + }); + + this.notificationData = this.options.notificationData; + }, + + data: function () { + return { + closeButton: this.closeButton, + notificationData: this.notificationData + }; + }, + + onCancel: function () { + }, + + cancel: function () { + this.onCancel(); + this.trigger('cancel'); + this.remove(); + } + + + }); +}); + diff --git a/frontend/less/espo/custom.less b/frontend/less/espo/custom.less index 93d53d2dd3..adb3411258 100644 --- a/frontend/less/espo/custom.less +++ b/frontend/less/espo/custom.less @@ -677,3 +677,77 @@ img.avatar.avatar-link { .complext-text a { word-wrap: break-word; } + +#popup-notifications-container { + overflow-y: auto; + overflow-x: hidden; + + right: 0px; + bottom: 0px; + position: fixed; + height: auto; + z-index: 1000; + + float: right; +} + +.popup-notification { + width: 400px; + padding: 14px; + background-color: @panel-bg; + border: 1px solid @main-gray; + margin-top: 10px; +} + +.popup-notification a.close{ + margin-top: -8px; + margin-right: -3px; +} + +.popup-notification-default { + border-color: @gray-light; +} + +.popup-notification-danger { + border-color: @brand-danger; +} + +.popup-notification-warning { + border-color: @brand-warning; +} + +.popup-notification-primary { + border-color: @brand-primary; +} + +.popup-notification-success { + border-color: @brand-success; +} + +.popup-notification-info { + border-color: @brand-info; +} + +.popup-notification h4 { + margin-top: 0px; +} + +.popup-notification-default h4 { + color: @gray-light; +} + +.popup-notification-danger h4 { + color: @brand-danger; +} + +.popup-notification-warning h4 { + color: @brand-warning; +} + +.popup-notification-primary h4 { + color: @brand-primary; +} + +.popup-notification-success h4 { + color: @brand-success; +}