This commit is contained in:
Yuri Kuznetsov
2024-04-16 18:29:02 +03:00
parent 1b4bdfaa12
commit 76397085a3
17 changed files with 593 additions and 3 deletions
@@ -0,0 +1,57 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 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\Core\FieldProcessing\Stars;
use Espo\Core\FieldProcessing\Loader;
use Espo\Core\FieldProcessing\Loader\Params;
use Espo\ORM\Entity;
use Espo\Tools\Stars\StarService;
/**
* @implements Loader<Entity>
*/
class StarLoader implements Loader
{
public function __construct(
private StarService $service,
) {}
public function process(Entity $entity, Params $params): void
{
if (
!$entity->hasAttribute('isStarred') ||
!$this->service->isEnabled($entity->getEntityType())
) {
return;
}
$entity->set('isStarred', $this->service->checkIsStarred($entity));
}
}
@@ -0,0 +1,59 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 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\Core\Select\Primary\Filters;
use Espo\Core\Select\Primary\Filter;
use Espo\Entities\StarSubscription;
use Espo\Entities\User;
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
/**
* @noinspection PhpUnused
*/
class Starred implements Filter
{
public function __construct(private string $entityType, private User $user)
{}
public function apply(QueryBuilder $queryBuilder): void
{
$alias = 'starredPrimaryFilter';
$queryBuilder->join(
StarSubscription::ENTITY_TYPE,
$alias,
[
"$alias.entityType" => $this->entityType,
"$alias.entityId=:" => 'id',
"$alias.userId" => $this->user->getId(),
]
);
}
}
@@ -519,6 +519,18 @@ class Converter
}
}
// @todo Refactor.
if ($scopeDefs['stars'] ?? false) {
if (!isset($entityMetadata['fields']['isStarred'])) {
$ormMetadata[$entityType]['attributes']['isStarred'] = [
'type' => Entity::BOOL,
'notStorable' => true,
'notExportable' => true,
'readOnly' => true,
];
}
}
// @todo Refactor.
if ($this->metadata->get(['entityDefs', $entityType, 'optimisticConcurrencyControl'])) {
$ormMetadata[$entityType]['attributes']['versionNumber'] = [
@@ -0,0 +1,37 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 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\Entities;
use Espo\Core\ORM\Entity;
class StarSubscription extends Entity
{
public const ENTITY_TYPE = 'StarSubscription';
}
@@ -43,6 +43,7 @@
"optimisticConcurrencyControl": "Optimistic concurrency control",
"updateDuplicateCheck": "Duplicate check on update",
"duplicateCheckFieldList": "Duplicate check fields",
"stars": "Stars",
"layout": "Layout",
"selectFilter": "Select Filter",
"author": "Author",
@@ -94,6 +95,7 @@
"duplicateCheckFieldList": "Which fields to check when performing checking for duplicates.",
"updateDuplicateCheck": "Perform checking for duplicates when updating a record.",
"optimisticConcurrencyControl": "Prevents writing conflicts.",
"stars": "The ability to star records. Stars can be used by users to bookmark records.",
"statusField": "Updates of this field are logged in stream.",
"textFilterFields": "Fields used by text search.",
"stream": "Whether entity has a Stream.",
@@ -193,6 +193,9 @@
"Country": "Country",
"City": "City",
"PostalCode": "Postal Code",
"Star": "Star",
"Unstar": "Unstar",
"Starred": "Starred",
"Followed": "Followed",
"Follow": "Follow",
"Followers": "Followers",
@@ -407,7 +410,8 @@
},
"presetFilters": {
"followed": "Followed",
"all": "All"
"all": "All",
"starred": "Starred"
},
"massActions": {
"delete": "Delete",
@@ -1,5 +1,12 @@
{
"Global": {
"stars": {
"location": "scopes",
"fieldDefs": {
"type": "bool",
"tooltip": true
}
},
"optimisticConcurrencyControl": {
"location": "entityDefs",
"fieldDefs": {
@@ -6,7 +6,8 @@
"Espo\\Core\\FieldProcessing\\LinkParent\\Loader",
"Espo\\Core\\FieldProcessing\\EmailAddress\\Loader",
"Espo\\Core\\FieldProcessing\\PhoneNumber\\Loader",
"Espo\\Core\\FieldProcessing\\Stream\\FollowersLoader"
"Espo\\Core\\FieldProcessing\\Stream\\FollowersLoader",
"Espo\\Core\\FieldProcessing\\Stars\\StarLoader"
],
"listLoaderClassNameList": [
"Espo\\Core\\FieldProcessing\\LinkParent\\Loader",
@@ -0,0 +1,23 @@
{
"fields": {
"id": {
"type": "id",
"dbType": "bigint",
"autoincrement": true
},
"entity": {
"type": "linkParent"
},
"user": {
"type": "link"
},
"createdAt": {
"type": "datetime"
}
},
"indexes": {
"userEntity": {
"columns": ["userId", "entityId", "entityType"]
}
}
}
+10
View File
@@ -564,6 +564,16 @@
"action": "unfollow"
}
},
{
"route": "/:entityType/:id/starSubscription",
"method": "put",
"actionClassName": "Espo\\Tools\\Stars\\Api\\PutStar"
},
{
"route": "/:entityType/:id/starSubscription",
"method": "delete",
"actionClassName": "Espo\\Tools\\Stars\\Api\\DeleteUnstar"
},
{
"route": "/:controller/:id/:link",
"method": "get",
@@ -66,6 +66,7 @@ class NameUtil
'stream',
'subscription',
'followers',
'starSubscription',
'action',
'null',
'false',
@@ -57,6 +57,7 @@ class FieldManager
'deleted',
'skipDuplicateCheck',
'isFollowed',
'isStarred',
'versionNumber',
'null',
'false',
@@ -0,0 +1,93 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 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\Tools\Stars\Api;
use Espo\Core\Acl;
use Espo\Core\Api\Action;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Api\ResponseComposer;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Entities\User;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\Tools\Stars\StarService;
/**
* @noinspection PhpUnused
*/
class DeleteUnstar implements Action
{
public function __construct(
private EntityManager $entityManager,
private Acl $acl,
private User $user,
private StarService $service,
) {}
public function process(Request $request): Response
{
$id = $request->getRouteParam('id');
$entityType = $request->getRouteParam('entityType');
if (!is_string($id) || !is_string($entityType)) {
throw new BadRequest();
}
$entity = $this->getEntity($entityType, $id);
$this->service->unstar($entity, $this->user);
return ResponseComposer::json(true);
}
/**
* @throws Forbidden
* @throws NotFound
*/
private function getEntity(string $entityType, string $id): Entity
{
$entity = $this->entityManager
->getRDBRepository($entityType)
->getById($id);
if (!$entity) {
throw new NotFound();
}
if (!$this->acl->checkEntityRead($entity)) {
throw new Forbidden();
}
return $entity;
}
}
@@ -0,0 +1,93 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 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\Tools\Stars\Api;
use Espo\Core\Acl;
use Espo\Core\Api\Action;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Api\ResponseComposer;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Entities\User;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\Tools\Stars\StarService;
/**
* @noinspection PhpUnused
*/
class PutStar implements Action
{
public function __construct(
private EntityManager $entityManager,
private Acl $acl,
private User $user,
private StarService $service,
) {}
public function process(Request $request): Response
{
$id = $request->getRouteParam('id');
$entityType = $request->getRouteParam('entityType');
if (!is_string($id) || !is_string($entityType)) {
throw new BadRequest();
}
$entity = $this->getEntity($entityType, $id);
$this->service->star($entity, $this->user);
return ResponseComposer::json(true);
}
/**
* @throws Forbidden
* @throws NotFound
*/
private function getEntity(string $entityType, string $id): Entity
{
$entity = $this->entityManager
->getRDBRepository($entityType)
->getById($id);
if (!$entity) {
throw new NotFound();
}
if (!$this->acl->checkEntityRead($entity)) {
throw new Forbidden();
}
return $entity;
}
}
@@ -0,0 +1,103 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 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\Tools\Stars;
use Espo\Core\Utils\DateTime;
use Espo\Core\Utils\Metadata;
use Espo\Entities\StarSubscription;
use Espo\Entities\User;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
class StarService
{
public function __construct(
private EntityManager $entityManager,
private User $user,
private Metadata $metadata,
) {}
public function checkIsStarred(Entity $entity, ?string $userId = null): bool
{
$userId ??= $this->user->getId();
return (bool) $this->entityManager
->getRDBRepository(StarSubscription::ENTITY_TYPE)
->select(['id'])
->where([
'userId' => $userId,
'entityType' => $entity->getEntityType(),
'entityId' => $entity->getId(),
])
->findOne();
}
public function isEnabled(string $entityType): bool
{
return (bool) $this->metadata->get("scopes.$entityType.stars");
}
public function star(Entity $entity, User $user): bool
{
if ($this->checkIsStarred($entity, $user->getId())) {
return true;
}
$this->entityManager->createEntity(StarSubscription::ENTITY_TYPE, [
'entityId' => $entity->getId(),
'entityType' => $entity->getEntityType(),
'userId' => $user->getId(),
'createdAt' => DateTime::getSystemNowString(),
]);
return true;
}
public function unstar(Entity $entity, User $user): bool
{
if (!$this->isEnabled($entity->getEntityType())) {
return false;
}
$delete = $this->entityManager->getQueryBuilder()
->delete()
->from(StarSubscription::ENTITY_TYPE)
->where([
'userId' => $user->getId(),
'entityId' => $entity->getId(),
'entityType' => $entity->getEntityType(),
])
->build();
$this->entityManager->getQueryExecutor()->execute($delete);
return true;
}
}
+81
View File
@@ -109,6 +109,7 @@ class DetailView extends MainView {
this.setupRecord();
this.setupPageTitle();
this.initFollowButtons();
this.initStarButtons();
this.initRedirect();
}
@@ -221,6 +222,86 @@ class DetailView extends MainView {
.get('clientDefs.' + this.scope + '.recordViews.detail') || this.recordView;
}
/** @private */
initStarButtons() {
if (!this.getMetadata().get(`scopes.${this.scope}.stars`)) {
return;
}
this.addStarButtons();
this.listenTo(this.model, 'change:isStarred', () => this.controlStarButtons());
}
/** @private */
addStarButtons() {
const isStarred = this.model.get('isStarred');
this.addMenuItem('buttons', {
name: 'unstar',
iconHtml: '<span class="fas fa-star fa-sm text-warning"></span>',
text: this.translate('Starred'),
hidden: !isStarred,
//title: this.translate('Unstar'),
onClick: () => this.actionUnstar(),
}, true);
this.addMenuItem('buttons', {
name: 'star',
iconHtml: '<span class="far fa-star fa-sm"></span>',
text: this.translate('Star'),
//title: this.translate('Star'),
hidden: isStarred || !this.model.has('isStarred'),
onClick: () => this.actionStar(),
}, true);
}
/** @private */
controlStarButtons() {
const isStarred = this.model.get('isStarred');
if (isStarred) {
this.hideHeaderActionItem('star');
this.showHeaderActionItem('unstar');
return;
}
this.hideHeaderActionItem('unstar');
this.showHeaderActionItem('star');
}
/**
* Action 'star'.
*/
actionStar() {
this.disableMenuItem('star');
Espo.Ajax
.putRequest(`${this.entityType}/${this.model.id}/starSubscription`)
.then(() => {
this.hideHeaderActionItem('star');
this.model.set('isStarred', true, {sync: true});
})
.finally(() => this.enableMenuItem('star'));
}
/**
* Action 'unstar'.
*/
actionUnstar() {
this.disableMenuItem('unstar');
Espo.Ajax
.deleteRequest(`${this.entityType}/${this.model.id}/starSubscription`)
.then(() => {
this.hideHeaderActionItem('unstar');
this.model.set('isStarred', false, {sync: true});
})
.finally(() => this.enableMenuItem('unstar'));
}
/** @private */
initFollowButtons() {
if (!this.getMetadata().get(['scopes', this.scope, 'stream'])) {
+7 -1
View File
@@ -253,7 +253,7 @@ class SearchView extends View {
const filterList = this.options.filterList ||
this.getMetadata().get(['clientDefs', this.scope, 'filterList']) || [];
this.presetFilterList = Espo.Utils.clone(filterList).filter(item => {
this.presetFilterList = filterList.filter(item => {
if (typeof item === 'string') {
return true;
}
@@ -281,6 +281,12 @@ class SearchView extends View {
return true;
});
if (this.getMetadata().get(`scopes.${this.scope}.stars`)) {
this.presetFilterList.unshift({
name: 'starred',
});
}
((this.getPreferences().get('presetFilters') || {})[this.scope] || [])
.forEach(item => {
this.presetFilterList.push(item);