calendar websocket

This commit is contained in:
Yuri Kuznetsov
2025-06-28 14:35:06 +03:00
parent b182e51a38
commit 8a177d5ac8
7 changed files with 372 additions and 5 deletions
@@ -29,6 +29,8 @@
namespace Espo\Core\Templates\Entities;
use Espo\Core\Field\Link;
use Espo\Core\Name\Field;
use Espo\Core\ORM\Entity;
class Event extends Entity
@@ -48,4 +50,13 @@ class Event extends Entity
{
return $this->set('status', $status);
}
/**
* @since 9.2.0
*/
public function getAssignedUser(): ?Link
{
/** @var ?Link */
return $this->getValueObject(Field::ASSIGNED_USER);
}
}
@@ -0,0 +1,66 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Modules\Crm\Hooks\Call;
use Espo\Core\Hook\Hook\AfterRemove;
use Espo\Core\Hook\Hook\AfterSave;
use Espo\Core\WebSocket\Submission;
use Espo\Modules\Crm\Entities\Call;
use Espo\ORM\Entity;
use Espo\ORM\Repository\Option\RemoveOptions;
use Espo\ORM\Repository\Option\SaveOptions;
/**
* @implements AfterSave<Call>
* @implements AfterRemove<Call>
*/
class CalendarWebSocket implements AfterSave, AfterRemove
{
public function __construct(
private Submission $submission,
) {}
public function afterSave(Entity $entity, SaveOptions $options): void
{
$this->process($entity);
}
public function afterRemove(Entity $entity, RemoveOptions $options): void
{
$this->process($entity);
}
private function process(Call $entity): void
{
foreach ($entity->getUsers()->getIdList() as $userId) {
$this->submission->submit('calendarUpdate', $userId);
}
}
}
@@ -0,0 +1,83 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Modules\Crm\Hooks\Common;
use Espo\Core\Hook\Hook\AfterRemove;
use Espo\Core\Hook\Hook\AfterSave;
use Espo\Core\Name\Field;
use Espo\Core\Templates\Entities\Event;
use Espo\Core\WebSocket\Submission;
use Espo\ORM\Entity;
use Espo\ORM\Repository\Option\RemoveOptions;
use Espo\ORM\Repository\Option\SaveOptions;
/**
* @implements AfterSave<Entity>
* @implements AfterRemove<Entity>
*/
class CalendarWebSocket implements AfterSave, AfterRemove
{
public function __construct(
private Submission $submission,
) {}
public function afterSave(Entity $entity, SaveOptions $options): void
{
if (!$entity instanceof Event) {
return;
}
$this->process($entity);
}
public function afterRemove(Entity $entity, RemoveOptions $options): void
{
if (!$entity instanceof Event) {
return;
}
$this->process($entity);
}
private function process(Event $entity): void
{
if ($entity->hasLinkMultipleField(Field::ASSIGNED_USERS)) {
foreach ($entity->getLinkMultipleIdList(Field::ASSIGNED_USER) as $userId) {
$this->submission->submit('calendarUpdate', $userId);
}
return;
}
if ($entity->getAssignedUser()) {
$this->submission->submit('calendarUpdate', $entity->getAssignedUser()->getId());
}
}
}
@@ -0,0 +1,66 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Modules\Crm\Hooks\Meeting;
use Espo\Core\Hook\Hook\AfterRemove;
use Espo\Core\Hook\Hook\AfterSave;
use Espo\Core\WebSocket\Submission;
use Espo\Modules\Crm\Entities\Meeting;
use Espo\ORM\Entity;
use Espo\ORM\Repository\Option\RemoveOptions;
use Espo\ORM\Repository\Option\SaveOptions;
/**
* @implements AfterSave<Meeting>
* @implements AfterRemove<Meeting>
*/
class CalendarWebSocket implements AfterSave, AfterRemove
{
public function __construct(
private Submission $submission,
) {}
public function afterSave(Entity $entity, SaveOptions $options): void
{
$this->process($entity);
}
public function afterRemove(Entity $entity, RemoveOptions $options): void
{
$this->process($entity);
}
private function process(Meeting $entity): void
{
foreach ($entity->getUsers()->getIdList() as $userId) {
$this->submission->submit('calendarUpdate', $userId);
}
}
}
@@ -0,0 +1,75 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Modules\Crm\Hooks\Task;
use Espo\Core\Hook\Hook\AfterRemove;
use Espo\Core\Hook\Hook\AfterSave;
use Espo\Core\Name\Field;
use Espo\Core\WebSocket\Submission;
use Espo\Modules\Crm\Entities\Task;
use Espo\ORM\Entity;
use Espo\ORM\Repository\Option\RemoveOptions;
use Espo\ORM\Repository\Option\SaveOptions;
/**
* @implements AfterSave<Task>
* @implements AfterRemove<Task>
*/
class CalendarWebSocket implements AfterSave, AfterRemove
{
public function __construct(
private Submission $submission,
) {}
public function afterSave(Entity $entity, SaveOptions $options): void
{
$this->process($entity);
}
public function afterRemove(Entity $entity, RemoveOptions $options): void
{
$this->process($entity);
}
private function process(Task $entity): void
{
if ($entity->hasLinkMultipleField(Field::ASSIGNED_USERS)) {
foreach ($entity->getLinkMultipleIdList(Field::ASSIGNED_USER) as $userId) {
$this->submission->submit('calendarUpdate', $userId);
}
return;
}
if ($entity->getAssignedUser()) {
$this->submission->submit('calendarUpdate', $entity->getAssignedUser()->getId());
}
}
}
@@ -1,5 +1,8 @@
{
"categories": {
"popupNotifications.event": {}
"popupNotifications.event": {},
"calendarUpdate": {
"accessCheckCommand": "AclCheck --userId=:userId --scope=Calendar"
}
}
}
@@ -30,12 +30,13 @@ import View from 'view';
import CalendarEditViewModal from 'crm:views/calendar/modals/edit-view';
import {inject} from 'di';
import {ShortcutManager} from 'helpers/site/shortcut-manager';
import DebounceHelper from 'helpers/util/debounce';
class CalendarPage extends View {
template = 'crm:calendar/calendar-page'
el = '#main'
//el = '#main'
fullCalendarModeList = [
'month',
@@ -64,6 +65,18 @@ class CalendarPage extends View {
@inject(ShortcutManager)
shortcutManager
/**
* @private
* @type {DebounceHelper}
*/
webSocketDebounceHelper
/**
* @private
* @type {number}
*/
webSocketDebounceInterval = 500
/**
* A shortcut-key => action map.
*
@@ -149,6 +162,20 @@ class CalendarPage extends View {
},
}
/**
* @param {{
* userId?: string,
* userName?: string|null,
* mode?: string|null,
* date?: string|null,
* }} options
*/
constructor(options) {
super(options);
this.options = options;
}
setup() {
this.mode = this.mode || this.options.mode || null;
this.date = this.date || this.options.date || null;
@@ -179,16 +206,52 @@ class CalendarPage extends View {
this.shortcutManager.add(this, this.shortcutKeys);
this.once('remove', () => {
this.on('remove', () => {
this.shortcutManager.remove(this);
});
if (!this.mode || ~this.fullCalendarModeList.indexOf(this.mode) || this.mode.indexOf('view-') === 0) {
this.setupCalendar();
}
else if (this.mode === 'timeline') {
} else if (this.mode === 'timeline') {
this.setupTimeline();
}
this.initWebSocket();
}
/**
* @private
*/
initWebSocket() {
if (this.options.userId && this.getUser().id !== this.options.userId) {
return;
}
this.webSocketDebounceHelper = new DebounceHelper({
interval: this.webSocketDebounceInterval,
handler: () => this.handleWebSocketUpdate(),
});
if (!this.getHelper().webSocketManager) {
const testHandler = () => this.webSocketDebounceHelper.process();
this.on('remove', () => window.removeEventListener('calendar-update', testHandler));
// For testing purpose.
window.addEventListener('calendar-update', testHandler);
return;
}
this.getHelper().webSocketManager.subscribe('calendarUpdate', () => this.webSocketDebounceHelper.process());
this.on('remove', () => this.getHelper().webSocketManager.unsubscribe('calendarUpdate'));
}
/**
* @private
*/
handleWebSocketUpdate() {
this.getCalendarView()?.actionRefresh({suppressLoadingAlert: true});
}
afterRender() {