mirror of
https://github.com/espocrm/espocrm.git
synced 2026-04-18 12:10:05 +00:00
@@ -344,7 +344,8 @@
|
||||
"Reactions": "Reactions",
|
||||
"Lock": "Lock",
|
||||
"Unlock": "Unlock",
|
||||
"Assigned": "Assigned"
|
||||
"Assigned": "Assigned",
|
||||
"Notification": "Notification"
|
||||
},
|
||||
"messages": {
|
||||
"cannotRemoveUsed": "Cannot remove the record as it is used.",
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
{{#if closeButton}}
|
||||
<a role="button" tabindex="0" class="pull-right close" data-action="close" aria-hidden="true"><span class="fas fa-times"></span></a>
|
||||
{{/if}}
|
||||
{{#if closeButton~}}
|
||||
<a
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="pull-right close"
|
||||
data-action="close"
|
||||
aria-hidden="true"
|
||||
title="{{translate 'Close'}}"
|
||||
><span class="fas fa-times"></span></a>
|
||||
{{~/if~}}
|
||||
{{#if collapseButton~}}
|
||||
<a
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="pull-right text-muted"
|
||||
data-action="collapse"
|
||||
aria-hidden="true"
|
||||
title="{{translate 'Collapse'}}"
|
||||
><span class="fas fa-minus"></span></a>
|
||||
{{~/if}}
|
||||
<h4>{{header}}</h4>
|
||||
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ class MeetingPopupNotificationView extends PopupNotificationView {
|
||||
type = 'event'
|
||||
style = 'primary'
|
||||
closeButton = true
|
||||
collapseButton = true
|
||||
|
||||
setup() {
|
||||
if (!this.notificationData.entityType) {
|
||||
@@ -71,6 +72,11 @@ class MeetingPopupNotificationView extends PopupNotificationView {
|
||||
onCancel() {
|
||||
Espo.Ajax.postRequest('Activities/action/removePopupNotification', {id: this.notificationId});
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return this.notificationData.name ??
|
||||
this.translate(this.notificationData.entityType, 'scopeNames');
|
||||
}
|
||||
}
|
||||
|
||||
export default MeetingPopupNotificationView;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
import View from 'view';
|
||||
import CollapsedModalView from 'views/collapsed-modal';
|
||||
import PopupNotificationView from 'views/popup-notification';
|
||||
|
||||
class CollapsedModalBarView extends View {
|
||||
|
||||
@@ -56,6 +57,12 @@ class CollapsedModalBarView extends View {
|
||||
*/
|
||||
lastNumber
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {WeakMap<import('view').default, number>}
|
||||
*/
|
||||
map
|
||||
|
||||
data() {
|
||||
return {
|
||||
dataList: this.getDataList(),
|
||||
@@ -78,6 +85,8 @@ class CollapsedModalBarView extends View {
|
||||
setup() {
|
||||
this.lastNumber = 0;
|
||||
this.numberList = [];
|
||||
|
||||
this.map = new WeakMap();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,7 +163,7 @@ class CollapsedModalBarView extends View {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('views/modal').default} modalView
|
||||
* @param {import('views/modal').default|import('views/popup-notification').default} modalView
|
||||
* @param {{title: string}} options
|
||||
*/
|
||||
async addModalView(modalView, options) {
|
||||
@@ -166,16 +175,30 @@ class CollapsedModalBarView extends View {
|
||||
|
||||
this.lastNumber++;
|
||||
|
||||
this.map.set(modalView, number);
|
||||
|
||||
const view = new CollapsedModalView({
|
||||
modalView: modalView,
|
||||
title: options.title,
|
||||
duplicateNumber: this.calculateDuplicateNumber(options.title),
|
||||
onClose: () => this.removeModalView(number),
|
||||
onClose: () => {
|
||||
this.removeModalViewByNumber(number);
|
||||
|
||||
if (modalView instanceof PopupNotificationView) {
|
||||
modalView.resolveCancel();
|
||||
}
|
||||
},
|
||||
onExpand: () => {
|
||||
this.removeModalView(number, true);
|
||||
this.removeModalViewByNumber(number, true);
|
||||
|
||||
// Use timeout to prevent DOM being updated after modal is re-rendered.
|
||||
setTimeout(async () => {
|
||||
if (modalView instanceof PopupNotificationView) {
|
||||
this.expandPopupNotification(modalView);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `dialog-${number}`;
|
||||
|
||||
this.setView(key, modalView);
|
||||
@@ -193,11 +216,25 @@ class CollapsedModalBarView extends View {
|
||||
await this.reRender(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('views/modal').default|import('views/popup-notification').default} modalView
|
||||
* @since 10.0
|
||||
*/
|
||||
removeModalView(modalView) {
|
||||
const number = this.map.get(modalView);
|
||||
|
||||
if (number === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeModalViewByNumber(number);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} number
|
||||
* @param {boolean} [noReRender]
|
||||
*/
|
||||
removeModalView(number, noReRender = false) {
|
||||
removeModalViewByNumber(number, noReRender = false) {
|
||||
const key = this.composeKey(number);
|
||||
|
||||
const index = this.numberList.indexOf(number);
|
||||
@@ -229,6 +266,14 @@ class CollapsedModalBarView extends View {
|
||||
composeKey(number) {
|
||||
return `key-${number}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {PopupNotificationView} view
|
||||
*/
|
||||
expandPopupNotification(view) {
|
||||
view.expand();
|
||||
}
|
||||
}
|
||||
|
||||
export default CollapsedModalBarView;
|
||||
|
||||
@@ -57,7 +57,7 @@ class CollapsedModalView extends View {
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* modalView: import('views/modal').default,
|
||||
* modalView: import('views/modal').default|import('views/popup-notification').default,
|
||||
* onClose: function(),
|
||||
* onExpand: function(),
|
||||
* duplicateNumber?: number|null,
|
||||
|
||||
@@ -30,6 +30,7 @@ import View from 'view';
|
||||
import {inject} from 'di';
|
||||
import WebSocketManager from 'web-socket-manager';
|
||||
import WindowPanelHelper from 'helpers/site/window-panel-helper';
|
||||
import ModalBarProvider from 'helpers/site/modal-bar-provider';
|
||||
|
||||
class NotificationBadgeView extends View {
|
||||
|
||||
@@ -96,6 +97,13 @@ class NotificationBadgeView extends View {
|
||||
@inject(WebSocketManager)
|
||||
webSocketManager
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {ModalBarProvider}
|
||||
*/
|
||||
@inject(ModalBarProvider)
|
||||
modalBarProvider
|
||||
|
||||
setup() {
|
||||
this.addActionHandler('showNotifications', () => this.showNotifications());
|
||||
|
||||
@@ -134,30 +142,14 @@ class NotificationBadgeView extends View {
|
||||
delete localStorage['messageBlockPlayNotificationSound'];
|
||||
delete localStorage['messageClosePopupNotificationId'];
|
||||
delete localStorage['messageNotificationRead'];
|
||||
delete localStorage['messageCollapsePopupNotificationId'];
|
||||
delete localStorage['messageExpandPopupNotificationId'];
|
||||
|
||||
window.addEventListener('storage', e => {
|
||||
if (e.key === 'messageClosePopupNotificationId') {
|
||||
const id = localStorage.getItem('messageClosePopupNotificationId');
|
||||
const onWindowStorageBind = this.onWindowStorage.bind(this);
|
||||
|
||||
if (id) {
|
||||
const key = 'popup-' + id;
|
||||
window.addEventListener('storage', onWindowStorageBind, false);
|
||||
|
||||
if (this.hasView(key)) {
|
||||
this.markPopupRemoved(id);
|
||||
this.clearView(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'messageNotificationRead') {
|
||||
if (
|
||||
!this.isBroadcastingNotificationRead &&
|
||||
localStorage.getItem('messageNotificationRead')
|
||||
) {
|
||||
this.checkUpdates();
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
this.on('remove', () => window.removeEventListener('storage', onWindowStorageBind))
|
||||
}
|
||||
|
||||
afterRender() {
|
||||
@@ -187,6 +179,66 @@ class NotificationBadgeView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {StorageEvent} e
|
||||
*/
|
||||
onWindowStorage(e) {
|
||||
if (e.key === 'messageClosePopupNotificationId') {
|
||||
const id = localStorage.getItem(e.key);
|
||||
|
||||
this.closePopupNotification(id);
|
||||
|
||||
delete localStorage[e.key];
|
||||
}
|
||||
|
||||
if (e.key === 'messageCollapsePopupNotificationId') {
|
||||
const id = localStorage.getItem(e.key);
|
||||
|
||||
this.collapsePopupNotification(id, true);
|
||||
|
||||
delete localStorage[e.key];
|
||||
}
|
||||
|
||||
if (e.key === 'messageExpandPopupNotificationId') {
|
||||
const id = localStorage.getItem(e.key);
|
||||
|
||||
this.expandPopupNotification(id, true);
|
||||
|
||||
delete localStorage[e.key];
|
||||
}
|
||||
|
||||
if (e.key === 'messageNotificationRead') {
|
||||
if (
|
||||
!this.isBroadcastingNotificationRead &&
|
||||
localStorage.getItem('messageNotificationRead')
|
||||
) {
|
||||
this.checkUpdates();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} id
|
||||
*/
|
||||
closePopupNotification(id) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `popup-${id}`;
|
||||
const view = this.getPopupNotificationView(id);
|
||||
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.modalBarProvider?.get().removeModalView(view);
|
||||
this.markPopupRemoved(id);
|
||||
this.clearView(key);
|
||||
}
|
||||
|
||||
playSound() {
|
||||
if (this.notificationSoundsDisabled) {
|
||||
return;
|
||||
@@ -469,20 +521,28 @@ class NotificationBadgeView extends View {
|
||||
});
|
||||
}
|
||||
|
||||
showPopupNotification(name, data, isNotFirstCheck) {
|
||||
const view = this.popupNotificationsData[name].view;
|
||||
/**
|
||||
* @private
|
||||
* @param {string} name
|
||||
* @param {Record} data
|
||||
* @param {boolean} isNotFirstCheck
|
||||
*/
|
||||
async showPopupNotification(name, data, isNotFirstCheck = false) {
|
||||
const viewName = this.popupNotificationsData[name].view;
|
||||
|
||||
if (!view) {
|
||||
if (!viewName) {
|
||||
return;
|
||||
}
|
||||
|
||||
let id = data.id || null;
|
||||
let id;
|
||||
|
||||
if (id) {
|
||||
id = name + '_' + id;
|
||||
const notificationId = data.id || null;
|
||||
|
||||
if (~this.shownNotificationIds.indexOf(id)) {
|
||||
const notificationView = this.getView('popup-' + id);
|
||||
if (notificationId) {
|
||||
id = name + '_' + notificationId;
|
||||
|
||||
if (this.shownNotificationIds.includes(id)) {
|
||||
const notificationView = this.getPopupNotificationView(id);
|
||||
|
||||
if (notificationView) {
|
||||
notificationView.trigger('update-data', data.data);
|
||||
@@ -491,34 +551,127 @@ class NotificationBadgeView extends View {
|
||||
return;
|
||||
}
|
||||
|
||||
if (~this.closedNotificationIds.indexOf(id)) {
|
||||
if (this.closedNotificationIds.includes(notificationId)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
id = this.lastId++;
|
||||
}
|
||||
|
||||
this.shownNotificationIds.push(id);
|
||||
|
||||
this.createView('popup-' + id, view, {
|
||||
notificationData: data.data || {},
|
||||
notificationId: data.id,
|
||||
id: id,
|
||||
isFirstCheck: !isNotFirstCheck,
|
||||
}, view => {
|
||||
view.render();
|
||||
/** @type {import('views/popup-notification').default} */
|
||||
let view;
|
||||
|
||||
this.$popupContainer.removeClass('hidden');
|
||||
|
||||
this.listenTo(view, 'remove', () => {
|
||||
this.markPopupRemoved(id);
|
||||
|
||||
localStorage.setItem('messageClosePopupNotificationId', id);
|
||||
view = /** @type {import('views/popup-notification').default} */
|
||||
await this.createView(`popup-${id}`, viewName, {
|
||||
notificationData: data.data ?? {},
|
||||
notificationId: data.id,
|
||||
id: id,
|
||||
isFirstCheck: !isNotFirstCheck,
|
||||
onCollapse: () => {
|
||||
this.collapsePopupNotification(id);
|
||||
},
|
||||
onExpand: () => {
|
||||
this.expandPopupNotification(id);
|
||||
},
|
||||
});
|
||||
|
||||
this.$popupContainer.removeClass('hidden');
|
||||
|
||||
this.listenTo(view, 'remove', () => {
|
||||
this.markPopupRemoved(id);
|
||||
|
||||
localStorage.setItem('messageClosePopupNotificationId', id);
|
||||
});
|
||||
|
||||
if (data.id && this.getStorage().get('state', this.getCollapsedStorageKey(id))) {
|
||||
this.collapsePopupNotification(id, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await view.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} id
|
||||
* @param {boolean} silent
|
||||
*/
|
||||
collapsePopupNotification(id, silent = false) {
|
||||
const view = this.getPopupNotificationView(id);
|
||||
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!silent || !view.isCollapsed) {
|
||||
this.modalBarProvider.get()?.addModalView(view, {
|
||||
title: view.getTitle() ?? this.translate('Notification'),
|
||||
});
|
||||
}
|
||||
|
||||
if (silent) {
|
||||
view.makeCollapsed();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem('messageCollapsePopupNotificationId', id);
|
||||
|
||||
this.getStorage().set('state', this.getCollapsedStorageKey(id), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} id
|
||||
* @param {boolean} silent
|
||||
*/
|
||||
expandPopupNotification(id, silent = false) {
|
||||
const view = this.getPopupNotificationView(id);
|
||||
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!silent || view.isCollapsed) {
|
||||
this.modalBarProvider.get()?.removeModalView(view);
|
||||
}
|
||||
|
||||
if (silent) {
|
||||
view.makeExpanded();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem('messageExpandPopupNotificationId', id);
|
||||
|
||||
this.getStorage().clear('state', this.getCollapsedStorageKey(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} id
|
||||
* @return {string}
|
||||
*/
|
||||
getCollapsedStorageKey(id) {
|
||||
return `popupNotificationCollapsed-${id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} id
|
||||
* @return {import('views/popup-notification').default}
|
||||
*/
|
||||
getPopupNotificationView(id) {
|
||||
return this.getView(`popup-${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} id
|
||||
*/
|
||||
markPopupRemoved(id) {
|
||||
const index = this.shownNotificationIds.indexOf(id);
|
||||
|
||||
|
||||
@@ -38,14 +38,50 @@ class PopupNotificationView extends View {
|
||||
|
||||
type = 'default'
|
||||
style = 'default'
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @type {boolean}
|
||||
*/
|
||||
closeButton = true
|
||||
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @type {boolean}
|
||||
* @since 10.0
|
||||
*/
|
||||
collapseButton = true
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
* @internal
|
||||
*/
|
||||
isCollapsed = false
|
||||
|
||||
soundPath = 'client/sounds/pop_cork'
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* id: string,
|
||||
* notificationData: Record,
|
||||
* notificationId: string|null,
|
||||
* isFirstCheck: boolean,
|
||||
* onCollapse: function(),
|
||||
* onExpand: function(),
|
||||
* }} options
|
||||
*/
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
const id = this.options.id;
|
||||
const containerSelector = this.containerSelector = '#' + id;
|
||||
const containerSelector = this.containerSelector = `#${id}`;
|
||||
|
||||
this.setSelector(containerSelector);
|
||||
|
||||
@@ -55,9 +91,11 @@ class PopupNotificationView extends View {
|
||||
(this.getConfig().get('popupNotificationSound') || this.soundPath);
|
||||
|
||||
this.on('render', () => {
|
||||
this.element = undefined;
|
||||
this.hide();
|
||||
|
||||
$(containerSelector).remove();
|
||||
if (this.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const className = 'popup-notification-' + Espo.Utils.toDom(this.type);
|
||||
|
||||
@@ -88,6 +126,12 @@ class PopupNotificationView extends View {
|
||||
this.notificationData = this.options.notificationData;
|
||||
this.notificationId = this.options.notificationId;
|
||||
this.id = this.options.id;
|
||||
|
||||
if (!this.notificationId) {
|
||||
this.collapseButton = false;
|
||||
}
|
||||
|
||||
this.addActionHandler('collapse', () => this.collapse());
|
||||
}
|
||||
|
||||
data() {
|
||||
@@ -95,9 +139,20 @@ class PopupNotificationView extends View {
|
||||
closeButton: this.closeButton,
|
||||
notificationData: this.notificationData,
|
||||
notificationId: this.notificationId,
|
||||
collapseButton: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @since 10.0
|
||||
*/
|
||||
hide() {
|
||||
this.element = undefined;
|
||||
|
||||
$(this.containerSelector).remove();
|
||||
}
|
||||
|
||||
playSound() {
|
||||
if (this.notificationSoundsDisabled) {
|
||||
return;
|
||||
@@ -170,6 +225,68 @@ class PopupNotificationView extends View {
|
||||
|
||||
this.resolveCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse.
|
||||
*
|
||||
* @since 10.0
|
||||
* @private
|
||||
*/
|
||||
collapse() {
|
||||
this.isCollapsed = true;
|
||||
|
||||
this.options.onCollapse();
|
||||
|
||||
this.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand.
|
||||
*
|
||||
* @since 10.0
|
||||
* @internal
|
||||
*/
|
||||
expand() {
|
||||
this.isCollapsed = false;
|
||||
|
||||
this.options.onExpand();
|
||||
|
||||
this.reRender(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse silently.
|
||||
*
|
||||
* @since 10.0
|
||||
* @internal
|
||||
*/
|
||||
makeCollapsed() {
|
||||
this.isCollapsed = true;
|
||||
|
||||
this.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand silently.
|
||||
*
|
||||
* @since 10.0
|
||||
* @internal
|
||||
*/
|
||||
makeExpanded() {
|
||||
this.isCollapsed = false;
|
||||
|
||||
this.reRender(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get title.
|
||||
*
|
||||
* @return string|null
|
||||
* @since 10.0
|
||||
*/
|
||||
getTitle() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default PopupNotificationView;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
overflow-x: hidden;
|
||||
// Some space to fix the scrollbar gutter issue https://github.com/w3c/csswg-drafts/issues/9904.
|
||||
right: var(--12px);
|
||||
bottom: 0;
|
||||
bottom: var(--24px);
|
||||
position: fixed;
|
||||
height: auto;
|
||||
z-index: 1000;
|
||||
@@ -38,6 +38,11 @@
|
||||
margin-top: var(--minus-8px);
|
||||
margin-right: var(--minus-3px);
|
||||
}
|
||||
|
||||
a[data-action="collapse"] {
|
||||
margin-top: var(--minus-6px);
|
||||
margin-right: var(--12px);
|
||||
}
|
||||
}
|
||||
|
||||
.popup-notification-default {
|
||||
|
||||
Reference in New Issue
Block a user