diff --git a/.idea/.gitignore b/.idea/.gitignore index 5343c234d4..3ce7dcdfbe 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -3,3 +3,4 @@ !/codeStyles !/fileTemplates !/inspectionProfiles +!misc.xml diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..7053f4fa1a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/application/Espo/Core/ApplicationUser.php b/application/Espo/Core/ApplicationUser.php index 3bd9ab6dc0..c2859aa090 100644 --- a/application/Espo/Core/ApplicationUser.php +++ b/application/Espo/Core/ApplicationUser.php @@ -55,7 +55,20 @@ class ApplicationUser */ public function setupSystemUser(): void { - $user = $this->entityManagerProxy->getEntityById(User::ENTITY_TYPE, self::SYSTEM_USER_ID); + $user = $this->entityManagerProxy + ->getRDBRepository(User::ENTITY_TYPE) + ->select([ + 'id', + 'name', + 'userName', + 'type', + 'isActive', + 'firstName', + 'lastName', + 'deleted', + ]) + ->where(['id' => self::SYSTEM_USER_ID]) + ->findOne(); if (!$user) { throw new RuntimeException("System user is not found."); diff --git a/application/Espo/Modules/Crm/Resources/metadata/app/popupNotifications.json b/application/Espo/Modules/Crm/Resources/metadata/app/popupNotifications.json index 25abf43ef5..7d2ad576ba 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/app/popupNotifications.json +++ b/application/Espo/Modules/Crm/Resources/metadata/app/popupNotifications.json @@ -4,7 +4,6 @@ "providerClassName": "Espo\\Modules\\Crm\\Tools\\Activities\\PopupNotificationsProvider", "serviceName": "Activities", "methodName": "getPopupNotifications", - "interval": 15, "useWebSocket": true, "portalDisabled": true, "view": "crm:views/meeting/popup-notification" diff --git a/application/Espo/Resources/defaults/class-aliases.php b/application/Espo/Resources/defaults/class-aliases.php index 2426cad869..1b3601ee82 100644 --- a/application/Espo/Resources/defaults/class-aliases.php +++ b/application/Espo/Resources/defaults/class-aliases.php @@ -29,6 +29,19 @@ /** @todo Remove in v8.0. */ -class_alias('Laminas\\Mail\\Protocol\\Smtp', 'Zend\\Mail\\Protocol\\Smtp'); -class_alias('Laminas\\Mail\\Protocol\\Imap', 'Zend\\Mail\\Protocol\\Imap'); -class_alias('Laminas\\Mail\\Message', 'Zend\\Mail\\Message'); +$map = [ + ['Laminas\\Mail\\Protocol\\Smtp', 'Zend\\Mail\\Protocol\\Smtp'], + ['Laminas\\Mail\\Protocol\\Imap', 'Zend\\Mail\\Protocol\\Imap'], + ['Laminas\\Mail\\Message', 'Zend\\Mail\\Message'], +]; + +foreach ($map as $item) { + $className = $item[0]; + $alias = $item[1]; + + if (!class_exists($className)) { + continue; + } + + class_alias($className, $alias); +} diff --git a/application/Espo/Resources/defaults/config.php b/application/Espo/Resources/defaults/config.php index 26859944c8..acad56120b 100644 --- a/application/Espo/Resources/defaults/config.php +++ b/application/Espo/Resources/defaults/config.php @@ -133,6 +133,7 @@ return [ 'emailNotificationsDelay' => 30, 'emailMessageMaxSize' => 10, 'notificationsCheckInterval' => 10, + 'popupNotificationsCheckInterval' => 15, 'maxEmailAccountCount' => 2, 'followCreatedEntities' => false, 'b2cMode' => false, diff --git a/application/Espo/Resources/metadata/app/acl.json b/application/Espo/Resources/metadata/app/acl.json index 771e2162c5..ef01a14ee9 100644 --- a/application/Espo/Resources/metadata/app/acl.json +++ b/application/Espo/Resources/metadata/app/acl.json @@ -85,7 +85,10 @@ }, "User": { "dashboardTemplate": false, - "workingTimeCalendar": false, + "workingTimeCalendar": { + "read": "yes", + "edit": "no" + }, "password": false, "passwordConfirm": false, "auth2FA": false, diff --git a/application/Espo/Resources/metadata/entityAcl/User.json b/application/Espo/Resources/metadata/entityAcl/User.json index ed394972b3..ef29a9ae06 100644 --- a/application/Espo/Resources/metadata/entityAcl/User.json +++ b/application/Espo/Resources/metadata/entityAcl/User.json @@ -48,7 +48,7 @@ "nonAdminReadOnly": true }, "workingTimeCalendar": { - "onlyAdmin": true + "nonAdminReadOnly": true }, "accounts": { "nonAdminReadOnly": true diff --git a/application/Espo/Resources/metadata/entityDefs/User.json b/application/Espo/Resources/metadata/entityDefs/User.json index f4f147b793..0aa2caf173 100644 --- a/application/Espo/Resources/metadata/entityDefs/User.json +++ b/application/Espo/Resources/metadata/entityDefs/User.json @@ -441,7 +441,8 @@ }, "workingTimeCalendar": { "type": "belongsTo", - "entity": "WorkingTimeCalendar" + "entity": "WorkingTimeCalendar", + "noJoin": true }, "workingTimeRanges": { "type": "hasMany", diff --git a/client/src/views/email/list.js b/client/src/views/email/list.js index db53b34f54..65aefd1b95 100644 --- a/client/src/views/email/list.js +++ b/client/src/views/email/list.js @@ -156,6 +156,7 @@ define('views/email/list', ['views/list'], function (Dep) { let $row = $container.find(`.list-row[data-id="${m.id}"]`).first(); $row.draggable({ + cancel: 'input,textarea,button,select,option,.dropdown-menu', helper: () => { let text = this.translate('Moving to Folder', 'labels', 'Email'); diff --git a/client/src/views/notification/badge.js b/client/src/views/notification/badge.js index 70c009e9fc..75ccaa6e36 100644 --- a/client/src/views/notification/badge.js +++ b/client/src/views/notification/badge.js @@ -33,8 +33,10 @@ define('views/notification/badge', ['view'], function (Dep) { template: 'notification/badge', notificationsCheckInterval: 10, + groupedCheckInterval: 15, timeout: null, + groupedTimeout: null, popupNotificationsData: null, @@ -58,6 +60,10 @@ define('views/notification/badge', ['view'], function (Dep) { clearTimeout(this.timeout); } + if (this.groupedTimeout) { + clearTimeout(this.groupedTimeout); + } + for (let name in this.popupTimeouts) { clearTimeout(this.popupTimeouts[name]); } @@ -69,6 +75,9 @@ define('views/notification/badge', ['view'], function (Dep) { this.notificationsCheckInterval = this.getConfig().get('notificationsCheckInterval') || this.notificationsCheckInterval; + this.groupedCheckInterval = this.getConfig().get('popupNotificationsCheckInterval') || + this.groupedCheckInterval; + this.lastId = 0; this.shownNotificationIds = []; this.closedNotificationIds = []; @@ -80,10 +89,10 @@ define('views/notification/badge', ['view'], function (Dep) { window.addEventListener('storage', e => { if (e.key === 'messageClosePopupNotificationId') { - var id = localStorage.getItem('messageClosePopupNotificationId'); + let id = localStorage.getItem('messageClosePopupNotificationId'); if (id) { - var key = 'popup-' + id; + let key = 'popup-' + id; if (this.hasView(key)) { this.markPopupRemoved(id); @@ -105,7 +114,6 @@ define('views/notification/badge', ['view'], function (Dep) { afterRender: function () { this.$badge = this.$el.find('.notifications-button'); - this.$icon = this.$el.find('.notifications-button .icon'); this.$number = this.$el.find('.number-badge'); this.runCheckUpdates(true); @@ -119,14 +127,16 @@ define('views/notification/badge', ['view'], function (Dep) { .appendTo('body'); } - var popupNotificationsData = this.popupNotificationsData = + let popupNotificationsData = this.popupNotificationsData = this.getMetadata().get('app.popupNotifications') || {}; - for (var name in popupNotificationsData) { + for (let name in popupNotificationsData) { this.checkPopupNotifications(name); } - this.checkGroupedPopupNotifications(); + if (this.hasGroupedPopupNotifications()) { + this.checkGroupedPopupNotifications(); + } }, playSound: function () { @@ -186,6 +196,8 @@ define('views/notification/badge', ['view'], function (Dep) { ) { return true; } + + return false; }, checkUpdates: function (isFirstCheck) { @@ -197,7 +209,7 @@ define('views/notification/badge', ['view'], function (Dep) { .getRequest('Notification/action/notReadCount') .then(count => { if (!isFirstCheck && count > this.unreadCount) { - var messageBlockPlayNotificationSound = + let messageBlockPlayNotificationSound = localStorage.getItem('messageBlockPlayNotificationSound'); if (!messageBlockPlayNotificationSound) { @@ -240,45 +252,57 @@ define('views/notification/badge', ['view'], function (Dep) { ); }, - checkGroupedPopupNotifications: function () { - var toCheck = false; - - for (var name in this.popupNotificationsData) { - var data = this.popupNotificationsData[name] || {}; + /** + * @private + * @return {boolean} + */ + hasGroupedPopupNotifications: function () { + for (let name in this.popupNotificationsData) { + let data = this.popupNotificationsData[name] || {}; if (!data.grouped) { continue; } if (data.portalDisabled && this.getUser().isPortal()) { - return; + continue; } - toCheck = true; + return true; } - if (!toCheck) { - return; + return false; + }, + + /** + * @private + */ + checkGroupedPopupNotifications: function () { + if (!this.checkBypass()) { + Espo.Ajax.getRequest('PopupNotification/action/grouped') + .then(result => { + for (let type in result) { + let list = result[type]; + + list.forEach(item => this.showPopupNotification(type, item)); + } + }); } - Espo.Ajax.getRequest('PopupNotification/action/grouped') - .then(result => { - for (const type in result) { - const list = result[type]; - - list.forEach(item => this.showPopupNotification(type, item)); - } - }); + this.groupedTimeout = setTimeout( + () => this.checkGroupedPopupNotifications(), + this.groupedCheckInterval * 1000 + ); }, checkPopupNotifications: function (name, isNotFirstCheck) { - var data = this.popupNotificationsData[name] || {}; + let data = this.popupNotificationsData[name] || {}; - var url = data.url; - var interval = data.interval; - var disabled = data.disabled || false; + let url = data.url; + let interval = data.interval; + let disabled = data.disabled || false; - var isFirstCheck = !isNotFirstCheck; + let isFirstCheck = !isNotFirstCheck; if (disabled) { return; @@ -288,10 +312,10 @@ define('views/notification/badge', ['view'], function (Dep) { return; } - var useWebSocket = this.useWebSocket && data.useWebSocket; + let useWebSocket = this.useWebSocket && data.useWebSocket; if (useWebSocket) { - var category = 'popupNotifications.' + (data.webSocketCategory || name); + let category = 'popupNotifications.' + (data.webSocketCategory || name); this.getHelper().webSocketManager.subscribe(category, (c, response) => { if (!response.list) { @@ -356,19 +380,19 @@ define('views/notification/badge', ['view'], function (Dep) { }, showPopupNotification: function (name, data, isNotFirstCheck) { - var view = this.popupNotificationsData[name].view; + let view = this.popupNotificationsData[name].view; if (!view) { return; } - var id = data.id || null; + let id = data.id || null; if (id) { id = name + '_' + id; if (~this.shownNotificationIds.indexOf(id)) { - var notificationView = this.getView('popup-' + id); + let notificationView = this.getView('popup-' + id); if (notificationView) { notificationView.trigger('update-data', data.data); @@ -406,7 +430,7 @@ define('views/notification/badge', ['view'], function (Dep) { }, markPopupRemoved: function (id) { - var index = this.shownNotificationIds.indexOf(id); + let index = this.shownNotificationIds.indexOf(id); if (index > -1) { this.shownNotificationIds.splice(index, 1); @@ -437,7 +461,7 @@ define('views/notification/badge', ['view'], function (Dep) { showNotifications: function () { this.closeNotifications(); - var $container = $('
').attr('id', 'notifications-panel'); + let $container = $('
').attr('id', 'notifications-panel'); $container.appendTo(this.$el.find('.notifications-panel-container')); diff --git a/client/src/views/user/record/edit.js b/client/src/views/user/record/edit.js index 8542801541..a2307dafe4 100644 --- a/client/src/views/user/record/edit.js +++ b/client/src/views/user/record/edit.js @@ -192,6 +192,16 @@ define('views/user/record/edit', ['views/record/edit', 'views/user/record/detail }); } + if (this.model.isAdmin() || this.model.isRegular()) { + layout.push({ + "label": "Misc", + "name": "misc", + "rows": [ + [{"name": "workingTimeCalendar"}, false] + ] + }); + } + if (this.type == 'edit' && this.getUser().isAdmin() && !this.model.isApi()) { layout.push({ label: 'Password', diff --git a/package-lock.json b/package-lock.json index 0ed202d427..02b36972d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "espocrm", - "version": "7.3.0", + "version": "7.3.1", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index f262899e2c..22bfa39722 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "espocrm", - "version": "7.3.0", + "version": "7.3.1", "description": "Open-source CRM.", "repository": { "type": "git", diff --git a/upgrades/7.3/scripts/BeforeUpgrade.php b/upgrades/7.3/scripts/BeforeUpgrade.php index 3ead387369..73105d65ad 100644 --- a/upgrades/7.3/scripts/BeforeUpgrade.php +++ b/upgrades/7.3/scripts/BeforeUpgrade.php @@ -3,7 +3,7 @@ * This file is part of EspoCRM. * * EspoCRM - Open Source CRM application. - * Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko * Website: https://www.espocrm.com * * EspoCRM is free software: you can redistribute it and/or modify @@ -32,11 +32,14 @@ use Espo\Entities\EmailAccount; use Espo\Entities\User; use Espo\Entities\Preferences; use Espo\ORM\EntityManager; +use Espo\Core\Utils\Config; +use Espo\Core\Utils\Database\Helper as DatabaseHelper; class BeforeUpgrade { public function run(Container $container): void { + $this->addUserColumn($container->get('config')); $this->migrateSmtp($container->get('entityManager')); } @@ -121,4 +124,21 @@ class BeforeUpgrade $entityManager->saveEntity($preferences); } + + private function addUserColumn(Config $config) + { + $databaseHelper = new DatabaseHelper($config); + + $pdo = $databaseHelper->createPdoConnection(); + + $query = " + ALTER TABLE `user` ADD `working_time_calendar_id` VARCHAR(24) + DEFAULT NULL COLLATE `utf8mb4_unicode_ci` + "; + + try { + $sth = $pdo->prepare($query); + $sth->execute(); + } catch (\Exception $e) {} + } }