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) {}
+ }
}