Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| acf5c2984d | |||
| 98eb304903 | |||
| 1b385f987b | |||
| 34c1cdbe86 | |||
| 8563945199 | |||
| 162e05449c | |||
| 0b6fd83b14 | |||
| 3631f17a77 | |||
| 4100af5ad3 | |||
| b16024f205 | |||
| a0dbef3b78 | |||
| 12c24098c3 | |||
| 8d03944e68 | |||
| 3b5207871b | |||
| 0f2689deee | |||
| 37d437dbae | |||
| bfe109e9e4 | |||
| d8f7127528 | |||
| 986af05002 | |||
| bdbfbdabf1 | |||
| 04f74150ac | |||
| 74ce903cb7 | |||
| 666dd116bd | |||
| e0d262e733 | |||
| 128d7ec94d | |||
| e0277038cd | |||
| 845a50a99f | |||
| a30a353156 | |||
| 27a7e2f6f5 | |||
| febf78dab6 | |||
| fab0efa5e5 | |||
| 72794768af | |||
| 75c71d4b28 | |||
| 81364eefb3 | |||
| 07a09dd08e | |||
| 673370b9a5 | |||
| c398ab0a40 | |||
| 86f3801513 | |||
| 678be304c5 | |||
| 0243a2a5a0 | |||
| 15d95ddac9 | |||
| 33a15bdbd6 | |||
| b71a237cad | |||
| 11d9d2aee4 | |||
| 6a1a3b67c8 | |||
| d1bb6be354 | |||
| 55a387c785 | |||
| 6efa058293 | |||
| 8f054a475c | |||
| 5b1919b760 | |||
| 50ded4b933 | |||
| c4b0629e4f |
+6
-2
@@ -184,8 +184,12 @@ module.exports = grunt => {
|
||||
comments: /^!/,
|
||||
},
|
||||
beautify: false,
|
||||
mangle: true,
|
||||
compress: true
|
||||
compress: {
|
||||
sequences: false,
|
||||
collapse_vars: false,
|
||||
keep_fargs: true,
|
||||
webkit: true,
|
||||
},
|
||||
},
|
||||
bundle: {
|
||||
options: {
|
||||
|
||||
@@ -31,11 +31,10 @@ namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Controllers\RecordBase;
|
||||
|
||||
use Espo\Core\Controllers\Record;
|
||||
use stdClass;
|
||||
|
||||
class Webhook extends RecordBase
|
||||
class Webhook extends Record
|
||||
{
|
||||
protected function checkAccess(): bool
|
||||
{
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?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\Controllers;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Controllers\RecordBase;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class WebhookEventQueueItem extends RecordBase
|
||||
{
|
||||
protected function checkAccess(): bool
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function postActionCreate(Request $request, Response $response): stdClass
|
||||
{
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
public function putActionUpdate(Request $request, Response $response): stdClass
|
||||
{
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,6 @@ use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Field\DateTime as DateTimeField;
|
||||
use Espo\Entities\EmailFilter;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\GroupEmailFolder;
|
||||
use Espo\Entities\InboundEmail;
|
||||
use Espo\ORM\Collection;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
@@ -46,6 +46,7 @@ use Espo\ORM\EntityManager;
|
||||
use Laminas\Mail\Headers;
|
||||
use Laminas\Mail\Message as LaminasMessage;
|
||||
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
@@ -292,8 +293,8 @@ class Sender
|
||||
|
||||
$this->applyHeaders($message);
|
||||
$this->applyFrom($email, $message, $params);
|
||||
$this->applyReplyTo($email, $message, $params);
|
||||
$this->addRecipientAddresses($email, $message);
|
||||
$this->applyReplyTo($email, $message, $params);
|
||||
$this->applySubject($email, $message);
|
||||
$this->applyBody($email, $message);
|
||||
$this->applyMessageId($email, $message);
|
||||
@@ -355,7 +356,7 @@ class Sender
|
||||
|
||||
$part = new DataPart(
|
||||
body: $contents,
|
||||
filename: $this->prepareAttachmentFileName($attachment),
|
||||
filename: $attachment->getName() ?? '',
|
||||
contentType: $attachment->getType(),
|
||||
);
|
||||
|
||||
@@ -470,15 +471,6 @@ class Sender
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function prepareAttachmentFileName(mixed $attachment): string
|
||||
{
|
||||
$namePart = base64_encode($attachment->getName() ?? '');
|
||||
|
||||
return "=?utf-8?B?$namePart?=";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
* @throws NoSmtp
|
||||
@@ -618,11 +610,17 @@ class Sender
|
||||
return;
|
||||
}
|
||||
|
||||
$parts = preg_split("/\R\R/", $message->toString(), 2);
|
||||
|
||||
if (!is_array($parts) || count($parts) < 2) {
|
||||
throw new RuntimeException("Could not split email.");
|
||||
}
|
||||
|
||||
/** @noinspection PhpMultipleClassDeclarationsInspection */
|
||||
$this->laminasMessage
|
||||
->setHeaders(
|
||||
Headers::fromString($message->getPreparedHeaders()->toString())
|
||||
Headers::fromString($parts[0])
|
||||
)
|
||||
->setBody($message->getBody()->toString());
|
||||
->setBody($parts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ class Manager
|
||||
/** @var string[] */
|
||||
protected $skipAttributeList = [
|
||||
Field::IS_FOLLOWED,
|
||||
Field::IS_STARRED,
|
||||
Field::FOLLOWERS . 'Ids',
|
||||
Field::FOLLOWERS . 'Names',
|
||||
Field::MODIFIED_AT,
|
||||
Field::MODIFIED_BY,
|
||||
Field::STREAM_UPDATED_AT,
|
||||
|
||||
@@ -53,15 +53,16 @@ class GoogleMaps
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$entity->get('enabled') || !$entity->get('apiKey')) {
|
||||
$this->configWriter->set('googleMapsApiKey', null);
|
||||
$apiKey = $entity->get('apiKey');
|
||||
$mapId = $entity->get('mapId');
|
||||
|
||||
$this->configWriter->save();
|
||||
|
||||
return;
|
||||
if (!$entity->isEnabled()) {
|
||||
$apiKey = null;
|
||||
$mapId = null;
|
||||
}
|
||||
|
||||
$this->configWriter->set('googleMapsApiKey', $entity->get('apiKey'));
|
||||
$this->configWriter->set('googleMapsApiKey', $apiKey);
|
||||
$this->configWriter->set('googleMapsMapId', $mapId);
|
||||
|
||||
$this->configWriter->save();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ use Espo\Core\Api\Request;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Select\SearchParams;
|
||||
use Espo\Core\Select\Where\Item as WhereItem;
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Modules\Crm\Tools\KnowledgeBase\Service as KBService;
|
||||
@@ -105,15 +106,9 @@ class KnowledgeBaseArticle extends Record
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$where = null;
|
||||
$searchParams = $this->fetchSearchParams($request);
|
||||
|
||||
if (!empty($data->where)) {
|
||||
$where = WhereItem::fromRawAndGroup(
|
||||
Json::decode(Json::encode($data->where), true)
|
||||
);
|
||||
}
|
||||
|
||||
$this->getArticleService()->moveToTop($data->id, $where);
|
||||
$this->getArticleService()->moveToTop($data->id, $searchParams);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -132,15 +127,9 @@ class KnowledgeBaseArticle extends Record
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$where = null;
|
||||
$searchParams = $this->fetchSearchParams($request);
|
||||
|
||||
if (!empty($data->where)) {
|
||||
$where = WhereItem::fromRawAndGroup(
|
||||
Json::decode(Json::encode($data->where), true)
|
||||
);
|
||||
}
|
||||
|
||||
$this->getArticleService()->moveUp($data->id, $where);
|
||||
$this->getArticleService()->moveUp($data->id, $searchParams);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -159,15 +148,9 @@ class KnowledgeBaseArticle extends Record
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$where = null;
|
||||
$searchParams = $this->fetchSearchParams($request);
|
||||
|
||||
if (!empty($data->where)) {
|
||||
$where = WhereItem::fromRawAndGroup(
|
||||
Json::decode(Json::encode($data->where), true)
|
||||
);
|
||||
}
|
||||
|
||||
$this->getArticleService()->moveDown($data->id, $where);
|
||||
$this->getArticleService()->moveDown($data->id, $searchParams);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -186,15 +169,9 @@ class KnowledgeBaseArticle extends Record
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$where = null;
|
||||
$searchParams = $this->fetchSearchParams($request);
|
||||
|
||||
if (!empty($data->where)) {
|
||||
$where = WhereItem::fromRawAndGroup(
|
||||
Json::decode(Json::encode($data->where), true)
|
||||
);
|
||||
}
|
||||
|
||||
$this->getArticleService()->moveToBottom($data->id, $where);
|
||||
$this->getArticleService()->moveToBottom($data->id, $searchParams);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -203,4 +180,19 @@ class KnowledgeBaseArticle extends Record
|
||||
{
|
||||
return $this->injectableFactory->create(KBService::class);
|
||||
}
|
||||
|
||||
private function fetchSearchParams(Request $request): SearchParams
|
||||
{
|
||||
$body = $request->getParsedBody();
|
||||
|
||||
$searchParams = SearchParams::create();
|
||||
|
||||
if ($body->whereGroup ?? null) {
|
||||
$rawWhere = Json::decode(Json::encode($body->whereGroup), true);
|
||||
|
||||
$searchParams = SearchParams::fromRaw(['where' => $rawWhere]);
|
||||
}
|
||||
|
||||
return $searchParams;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Modules\Crm\Tools\KnowledgeBase;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
@@ -46,25 +47,13 @@ use Espo\Tools\Attachment\FieldData;
|
||||
|
||||
class Service
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
private AttachmentAccessChecker $attachmentAccessChecker;
|
||||
private ServiceContainer $serviceContainer;
|
||||
private SelectBuilderFactory $selectBuilderFactory;
|
||||
private Acl $acl;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
AttachmentAccessChecker $attachmentAccessChecker,
|
||||
ServiceContainer $serviceContainer,
|
||||
SelectBuilderFactory $selectBuilderFactory,
|
||||
Acl $acl
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->attachmentAccessChecker = $attachmentAccessChecker;
|
||||
$this->serviceContainer = $serviceContainer;
|
||||
$this->selectBuilderFactory = $selectBuilderFactory;
|
||||
$this->acl = $acl;
|
||||
}
|
||||
private EntityManager $entityManager,
|
||||
private AttachmentAccessChecker $attachmentAccessChecker,
|
||||
private ServiceContainer $serviceContainer,
|
||||
private SelectBuilderFactory $selectBuilderFactory,
|
||||
private Acl $acl,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Copy article attachments for re-using (e.g. in an email).
|
||||
@@ -132,8 +121,9 @@ class Service
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function moveUp(string $id, ?WhereItem $where = null): void
|
||||
public function moveUp(string $id, SearchParams $params): void
|
||||
{
|
||||
/** @var ?KnowledgeBaseArticle $entity */
|
||||
$entity = $this->entityManager->getEntityById(KnowledgeBaseArticle::ENTITY_TYPE, $id);
|
||||
@@ -152,12 +142,6 @@ class Service
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$params = SearchParams::create();
|
||||
|
||||
if ($where) {
|
||||
$params = $params->withWhere($where);
|
||||
}
|
||||
|
||||
$query = $this->selectBuilderFactory
|
||||
->create()
|
||||
->from(KnowledgeBaseArticle::ENTITY_TYPE)
|
||||
@@ -194,8 +178,9 @@ class Service
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function moveDown(string $id, ?WhereItem $where = null): void
|
||||
public function moveDown(string $id, SearchParams $params): void
|
||||
{
|
||||
/** @var ?KnowledgeBaseArticle $entity */
|
||||
$entity = $this->entityManager->getEntityById(KnowledgeBaseArticle::ENTITY_TYPE, $id);
|
||||
@@ -214,12 +199,6 @@ class Service
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$params = SearchParams::create();
|
||||
|
||||
if ($where) {
|
||||
$params = $params->withWhere($where);
|
||||
}
|
||||
|
||||
$query = $this->selectBuilderFactory
|
||||
->create()
|
||||
->from(KnowledgeBaseArticle::ENTITY_TYPE)
|
||||
@@ -256,8 +235,9 @@ class Service
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function moveToTop(string $id, ?WhereItem $where = null): void
|
||||
public function moveToTop(string $id, SearchParams $params): void
|
||||
{
|
||||
/** @var ?KnowledgeBaseArticle $entity */
|
||||
$entity = $this->entityManager->getEntityById(KnowledgeBaseArticle::ENTITY_TYPE, $id);
|
||||
@@ -276,12 +256,6 @@ class Service
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$params = SearchParams::create();
|
||||
|
||||
if ($where) {
|
||||
$params = $params->withWhere($where);
|
||||
}
|
||||
|
||||
$query = $this->selectBuilderFactory
|
||||
->create()
|
||||
->from(KnowledgeBaseArticle::ENTITY_TYPE)
|
||||
@@ -315,8 +289,9 @@ class Service
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function moveToBottom(string $id, ?WhereItem $where = null): void
|
||||
public function moveToBottom(string $id, SearchParams $params): void
|
||||
{
|
||||
/** @var ?KnowledgeBaseArticle $entity */
|
||||
$entity = $this->entityManager->getEntityById(KnowledgeBaseArticle::ENTITY_TYPE, $id);
|
||||
@@ -335,12 +310,6 @@ class Service
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$params = SearchParams::create();
|
||||
|
||||
if ($where) {
|
||||
$params = $params->withWhere($where);
|
||||
}
|
||||
|
||||
$query = $this->selectBuilderFactory
|
||||
->create()
|
||||
->from(KnowledgeBaseArticle::ENTITY_TYPE)
|
||||
|
||||
@@ -196,10 +196,10 @@ class PostgresqlQueryComposer extends BaseQueryComposer
|
||||
$minutes = (int) (floatval($offsetHoursString) * 60);
|
||||
$minutesString = (string) $minutes;
|
||||
|
||||
return "$argumentPartList[0] + INTERVAL 'MINUTE $minutesString'";
|
||||
return "$argumentPartList[0] + INTERVAL '$minutesString MINUTE'";
|
||||
}
|
||||
|
||||
return "$argumentPartList[0] + INTERVAL 'HOUR $offsetHoursString'";
|
||||
return "$argumentPartList[0] + INTERVAL '$offsetHoursString HOUR'";
|
||||
}
|
||||
|
||||
if ($function === 'POSITION_IN_LIST') {
|
||||
@@ -333,7 +333,7 @@ class PostgresqlQueryComposer extends BaseQueryComposer
|
||||
return "EXTRACT(YEAR FROM $to - $from)";
|
||||
|
||||
case 'TIMESTAMPDIFF_MONTH':
|
||||
return "EXTRACT(MONTH FROM $to) - $from)";
|
||||
return "EXTRACT(MONTH FROM $to - $from)";
|
||||
|
||||
case 'TIMESTAMPDIFF_WEEK':
|
||||
return "FLOOR(EXTRACT(DAY FROM $to - $from) / 7)";
|
||||
@@ -617,4 +617,40 @@ class PostgresqlQueryComposer extends BaseQueryComposer
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function composeDeleteQuery(
|
||||
string $table,
|
||||
?string $alias,
|
||||
string $where,
|
||||
?string $joins,
|
||||
?string $order,
|
||||
?int $limit
|
||||
): string {
|
||||
|
||||
$sql = "DELETE ";
|
||||
|
||||
$sql .= "FROM " . $this->quoteIdentifier($table);
|
||||
|
||||
if ($alias) {
|
||||
$sql .= " AS " . $this->quoteIdentifier($alias);
|
||||
}
|
||||
|
||||
if ($joins) {
|
||||
$sql .= " $joins";
|
||||
}
|
||||
|
||||
if ($where) {
|
||||
$sql .= " WHERE $where";
|
||||
}
|
||||
|
||||
if ($order) {
|
||||
$sql .= " ORDER BY $order";
|
||||
}
|
||||
|
||||
if ($limit !== null) {
|
||||
$sql = $this->limit($sql, null, $limit);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"LayoutSet": "Layout Set",
|
||||
"Webhook": "Webhook",
|
||||
"WebhookQueueItem": "Webhook Queue Item",
|
||||
"WebhookEventQueueItem": "Webhook Event Queue Item",
|
||||
"Mass Action": "Mass Action",
|
||||
"WorkingTimeCalendar": "Working Time Calendar",
|
||||
"WorkingTimeRange": "Working Time Exception",
|
||||
@@ -116,6 +117,7 @@
|
||||
"LayoutSet": "Layout Sets",
|
||||
"Webhook": "Webhooks",
|
||||
"WebhookQueueItem": "Webhook Queue Items",
|
||||
"WebhookEventQueueItem": "Webhook Event Queue Items",
|
||||
"WorkingTimeCalendar": "Working Time Calendars",
|
||||
"WorkingTimeRange": "Working Time Exceptions",
|
||||
"AuthenticationProvider": "Authentication Providers",
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"apiKey": "API Key",
|
||||
"siteKey": "Site Key",
|
||||
"secretKey": "Secret Key",
|
||||
"scoreThreshold": "Score Threshold"
|
||||
"scoreThreshold": "Score Threshold",
|
||||
"mapId": "Map ID"
|
||||
},
|
||||
"titles": {
|
||||
"GoogleMaps": "Google Maps",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"secretKey": "Secret Key"
|
||||
},
|
||||
"links": {
|
||||
"user": "User"
|
||||
"user": "User",
|
||||
"queueItems": "Queue Items"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"fields": {
|
||||
"event": "Event",
|
||||
"target": "Target",
|
||||
"data": "Data",
|
||||
"isProcessed": "Is Processed"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
[
|
||||
{"name":"name", "link": true},
|
||||
{"name":"isActive", "widthPx": "100"},
|
||||
{"name":"campaign", "width": "30"}
|
||||
{"name": "name", "link": true},
|
||||
{"name": "isActive", "widthPx": 120},
|
||||
{"name": "campaign", "width": 25}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"queueItems"
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"rows": [
|
||||
[
|
||||
{"name": "event"},
|
||||
{"name": "isProcessed"}
|
||||
],
|
||||
[
|
||||
{"name": "target"},
|
||||
false
|
||||
],
|
||||
[
|
||||
{"name": "data"}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"rows": [
|
||||
[
|
||||
{"name": "event"},
|
||||
{"name": "isProcessed"}
|
||||
],
|
||||
[
|
||||
{"name": "target"},
|
||||
false
|
||||
],
|
||||
[
|
||||
{"name": "data"}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
"event",
|
||||
"target",
|
||||
"isProcessed",
|
||||
"createdAt"
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"name": "event",
|
||||
"link": true
|
||||
},
|
||||
{
|
||||
"name": "isProcessed",
|
||||
"width": 12
|
||||
},
|
||||
{
|
||||
"name": "target",
|
||||
"width": 20
|
||||
},
|
||||
{
|
||||
"name": "createdAt",
|
||||
"width": 14
|
||||
}
|
||||
]
|
||||
@@ -16,6 +16,9 @@
|
||||
[
|
||||
{"name": "attempts"},
|
||||
{"name": "processAt"}
|
||||
],
|
||||
[
|
||||
{"name": "data"}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
"status",
|
||||
"webhook",
|
||||
"event",
|
||||
"createdAt",
|
||||
"processedAt"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"name": "target"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"width": 20
|
||||
},
|
||||
{
|
||||
"name": "createdAt",
|
||||
"width": 20
|
||||
},
|
||||
{
|
||||
"name": "processedAt",
|
||||
"width": 20
|
||||
}
|
||||
]
|
||||
@@ -107,6 +107,9 @@
|
||||
"rowActionsView": "crm:views/record/row-actions/relationship-target",
|
||||
"layout": "listForTarget",
|
||||
"view": "crm:views/record/panels/target-lists"
|
||||
},
|
||||
"contact": {
|
||||
"selectHandler": "handlers/user/select-contact"
|
||||
}
|
||||
},
|
||||
"layoutDefaultSidePanelDisabled": true,
|
||||
|
||||
@@ -11,8 +11,21 @@
|
||||
"labelTranslation": "Global.scopeNamesPlural.WebhookQueueItem",
|
||||
"link": "#WebhookQueueItem",
|
||||
"aclScope": "WebhookQueueItem"
|
||||
},
|
||||
{
|
||||
"labelTranslation": "Global.scopeNamesPlural.WebhookEventQueueItem",
|
||||
"link": "#WebhookEventQueueItem",
|
||||
"aclScope": "WebhookEventQueueItem"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"relationshipPanels": {
|
||||
"queueItems": {
|
||||
"unlinkDisabled": true,
|
||||
"createDisabled": true,
|
||||
"selectDisabled": true,
|
||||
"layout": "listForWebhook"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"controller": "controllers/record",
|
||||
"createDisabled": true,
|
||||
"editDisabled": true,
|
||||
"mergeDisabled": true,
|
||||
"exportDisabled": true,
|
||||
"textFilterDisabled": true,
|
||||
"menu": {
|
||||
"list": {
|
||||
"dropdown": [
|
||||
{
|
||||
"labelTranslation": "Global.scopeNamesPlural.Webhook",
|
||||
"link": "#Webhook",
|
||||
"aclScope": "Webhook"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,16 @@
|
||||
"editDisabled": true,
|
||||
"mergeDisabled": true,
|
||||
"exportDisabled": true,
|
||||
"textFilterDisabled": true
|
||||
"textFilterDisabled": true,
|
||||
"menu": {
|
||||
"list": {
|
||||
"dropdown": [
|
||||
{
|
||||
"labelTranslation": "Global.scopeNamesPlural.Webhook",
|
||||
"link": "#Webhook",
|
||||
"aclScope": "Webhook"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,12 @@
|
||||
"type": "belongsTo",
|
||||
"entity": "User"
|
||||
},
|
||||
"queueItems": {
|
||||
"type": "hasMany",
|
||||
"entity": "WebhookQueueItem",
|
||||
"foreign": "webhook",
|
||||
"readOnly": true
|
||||
},
|
||||
"createdBy": {
|
||||
"type": "belongsTo",
|
||||
"entity": "User"
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"event": {
|
||||
"type": "varchar",
|
||||
"maxLength": 100,
|
||||
"required": true
|
||||
"required": true,
|
||||
"view": "views/webhook/fields/event"
|
||||
},
|
||||
"target": {
|
||||
"type": "linkParent"
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"event": {
|
||||
"type": "varchar",
|
||||
"maxLength": 100,
|
||||
"required": true
|
||||
"required": true,
|
||||
"view": "views/webhook/fields/event"
|
||||
},
|
||||
"webhook": {
|
||||
"type": "link"
|
||||
@@ -51,7 +52,8 @@
|
||||
"webhook": {
|
||||
"type": "belongsTo",
|
||||
"entity": "Webhook",
|
||||
"foreignName": "id"
|
||||
"foreignName": "id",
|
||||
"foreign": "queueItems"
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
"type": "varchar",
|
||||
"maxLength": 255,
|
||||
"required": true
|
||||
},
|
||||
"mapId": {
|
||||
"type": "varchar",
|
||||
"maxLength": 64,
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"allowUserAccounts": false,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"massActions": {
|
||||
"delete": {
|
||||
"allowed": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"massActions": {
|
||||
"delete": {
|
||||
"allowed": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,10 +303,10 @@ class SendService
|
||||
try {
|
||||
$this->groupAccountService->storeSentMessage($id, $message);
|
||||
} catch (Exception $e) {
|
||||
$this->log->error(
|
||||
"Email sending: Could not store sent email (Group Email Account {$groupAccount->getId()}): " .
|
||||
$e->getMessage() . "."
|
||||
);
|
||||
$text = "Could not store sent email; group account {$groupAccount->getId()}); " .
|
||||
$e->getMessage() . ".";
|
||||
|
||||
$this->log->error($text, ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,10 +320,10 @@ class SendService
|
||||
try {
|
||||
$this->personalAccountService->storeSentMessage($id, $message);
|
||||
} catch (Exception $e) {
|
||||
$this->log->error(
|
||||
"Email sending: Could not store sent email (Email Account {$personalAccount->getId()}): " .
|
||||
$e->getMessage() . "."
|
||||
);
|
||||
$text = "Could not store sent email; personal account {$personalAccount->getId()}; " .
|
||||
$e->getMessage() . ".";
|
||||
|
||||
$this->log->error($text, ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class MoveActionHandler extends RowActionHandler {
|
||||
|
||||
Espo.Ajax.postRequest('KnowledgeBaseArticle/action/moveToTop', {
|
||||
id: model.id,
|
||||
where: this.collection.getWhere(),
|
||||
whereGroup: this.collection.getWhere(),
|
||||
}).then(() => {
|
||||
this.collection.fetch()
|
||||
.then(() => Espo.Ui.notify(false));
|
||||
@@ -89,7 +89,7 @@ class MoveActionHandler extends RowActionHandler {
|
||||
|
||||
Espo.Ajax.postRequest('KnowledgeBaseArticle/action/moveUp', {
|
||||
id: model.id,
|
||||
where: this.collection.getWhere(),
|
||||
whereGroup: this.collection.getWhere(),
|
||||
}).then(() => {
|
||||
this.collection.fetch()
|
||||
.then(() => Espo.Ui.notify(false));
|
||||
@@ -107,7 +107,7 @@ class MoveActionHandler extends RowActionHandler {
|
||||
|
||||
Espo.Ajax.postRequest('KnowledgeBaseArticle/action/moveDown', {
|
||||
id: model.id,
|
||||
where: this.collection.getWhere(),
|
||||
whereGroup: this.collection.getWhere(),
|
||||
}).then(() => {
|
||||
this.collection.fetch()
|
||||
.then(() => Espo.Ui.notify(false));
|
||||
@@ -125,7 +125,7 @@ class MoveActionHandler extends RowActionHandler {
|
||||
|
||||
Espo.Ajax.postRequest('KnowledgeBaseArticle/action/moveToBottom', {
|
||||
id: model.id,
|
||||
where: this.collection.getWhere(),
|
||||
whereGroup: this.collection.getWhere(),
|
||||
}).then(() => {
|
||||
this.collection.fetch()
|
||||
.then(() => Espo.Ui.notify(false));
|
||||
|
||||
+12
-4
@@ -1,5 +1,13 @@
|
||||
<div>
|
||||
<div class="top-group-string-container">
|
||||
{{{conditionGroup}}}
|
||||
{{#if isNotEmpty}}
|
||||
<div>
|
||||
<div class="top-group-string-container">
|
||||
{{{conditionGroup}}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if isSet}}
|
||||
<span class="none-value">{{translate 'None'}}</span>
|
||||
{{else}}
|
||||
<span class="loading-value"></span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@@ -112,6 +112,15 @@ class TreeCollection extends Collection {
|
||||
|
||||
return super.fetch(options);
|
||||
}
|
||||
|
||||
clone(options = {}) {
|
||||
options = {...options};
|
||||
|
||||
// Prevents recurring clone.
|
||||
options.withModels = false;
|
||||
|
||||
return super.clone(options);
|
||||
}
|
||||
}
|
||||
|
||||
export default TreeCollection;
|
||||
|
||||
@@ -58,7 +58,7 @@ class GoogleMapsRenderer extends MapRenderer {
|
||||
// noinspection SpellCheckingInspection
|
||||
window.mapapiloaded = () => this.initMapGoogle(addressData);
|
||||
|
||||
let src = 'https://maps.googleapis.com/maps/api/js?callback=mapapiloaded';
|
||||
let src = 'https://maps.googleapis.com/maps/api/js?callback=mapapiloaded&loading=async&v=weekly&libraries=marker';
|
||||
const apiKey = this.view.getConfig().get('googleMapsApiKey');
|
||||
|
||||
if (apiKey) {
|
||||
@@ -67,7 +67,7 @@ class GoogleMapsRenderer extends MapRenderer {
|
||||
|
||||
const scriptElement = document.createElement('script');
|
||||
|
||||
scriptElement.setAttribute('async', 'async');
|
||||
scriptElement.setAttribute('defer', 'defer');
|
||||
scriptElement.src = src;
|
||||
|
||||
document.head.appendChild(scriptElement);
|
||||
@@ -81,12 +81,15 @@ class GoogleMapsRenderer extends MapRenderer {
|
||||
const geocoder = new google.maps.Geocoder();
|
||||
let map;
|
||||
|
||||
const mapId = this.view.getConfig().get('googleMapsMapId') || 'DEMO_MAP_ID';
|
||||
|
||||
try {
|
||||
// noinspection SpellCheckingInspection,JSUnresolvedReference
|
||||
map = new google.maps.Map(this.view.$el.find('.map').get(0), {
|
||||
zoom: 15,
|
||||
center: {lat: 0, lng: 0},
|
||||
scrollwheel: false,
|
||||
mapId: mapId,
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
@@ -146,7 +149,7 @@ class GoogleMapsRenderer extends MapRenderer {
|
||||
map.setCenter(results[0].geometry.location);
|
||||
|
||||
// noinspection JSUnresolvedReference
|
||||
new google.maps.Marker({
|
||||
new google.maps.marker.AdvancedMarkerElement({
|
||||
map: map,
|
||||
position: results[0].geometry.location,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/************************************************************************
|
||||
* 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.
|
||||
************************************************************************/
|
||||
|
||||
import SelectRelatedHandler from 'handlers/select-related';
|
||||
|
||||
export default class extends SelectRelatedHandler {
|
||||
|
||||
/**
|
||||
* @param {import('model').default} model
|
||||
*/
|
||||
async getFilters(model) {
|
||||
/** @type {string[]|null} */
|
||||
const accountIds = model.attributes.accountsIds;
|
||||
|
||||
if (accountIds && accountIds.length) {
|
||||
return {
|
||||
advanced: {
|
||||
accounts: {
|
||||
field: 'accounts',
|
||||
type: 'linkedWith',
|
||||
value: accountIds,
|
||||
data: {nameHash: model.attributes.accountsNames || {}},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -117,6 +117,7 @@ class Autocomplete {
|
||||
autoSelectFirst: options.autoSelectFirst,
|
||||
appendTo: $modalBody.length ? $modalBody : 'body',
|
||||
forceFixPosition: true,
|
||||
maxHeight: 308,
|
||||
formatResult: item => {
|
||||
if (options.formatResult) {
|
||||
return options.formatResult(item);
|
||||
|
||||
@@ -28,11 +28,21 @@
|
||||
|
||||
import BaseFieldView from 'views/fields/base';
|
||||
|
||||
/**
|
||||
* Important. Used in extensions.
|
||||
*/
|
||||
export default class extends BaseFieldView {
|
||||
|
||||
detailTemplate = 'admin/field-manager/fields/dynamic-logic-conditions/detail'
|
||||
editTemplate = 'admin/field-manager/fields/dynamic-logic-conditions/edit'
|
||||
|
||||
data() {
|
||||
return {
|
||||
isSet: this.model.has(this.name),
|
||||
isNotEmpty: this.conditionGroup && this.conditionGroup.length,
|
||||
};
|
||||
}
|
||||
|
||||
setup() {
|
||||
this.addActionHandler('editConditions', () => this.edit());
|
||||
|
||||
|
||||
@@ -36,23 +36,33 @@ class EmailComposeRecordView extends EditRecordView {
|
||||
isWide = true
|
||||
sideView = false
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {string|null}
|
||||
*/
|
||||
initialBody
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {boolean|null}
|
||||
*/
|
||||
initialIsHtml
|
||||
|
||||
setupBeforeFinal() {
|
||||
super.setupBeforeFinal();
|
||||
|
||||
this.initialBody = null;
|
||||
this.initialIsHtml = null;
|
||||
this.initialBody = this.model.attributes.body;
|
||||
this.initialIsHtml = this.model.attributes.isHtml;
|
||||
|
||||
if (!this.model.get('isHtml') && this.getPreferences().get('emailReplyForceHtml')) {
|
||||
const body = (this.model.get('body') || '').replace(/\n/g, '<br>');
|
||||
|
||||
this.model.set('body', body, {silent: true});
|
||||
this.model.set('isHtml', true, {silent: true});
|
||||
if (
|
||||
!this.model.attributes.isHtml &&
|
||||
this.getPreferences().get('emailReplyForceHtml')
|
||||
) {
|
||||
this.initialBody = (this.initialBody || '').replace(/\n/g, '<br>') || null;
|
||||
this.initialIsHtml = true;
|
||||
}
|
||||
|
||||
if (this.model.get('body')) {
|
||||
this.initialBody = this.model.get('body');
|
||||
this.initialIsHtml = this.model.get('isHtml');
|
||||
}
|
||||
let body = this.initialBody;
|
||||
|
||||
if (!this.options.signatureDisabled && this.hasSignature()) {
|
||||
let addSignatureMethod = 'prependSignature';
|
||||
@@ -61,10 +71,11 @@ class EmailComposeRecordView extends EditRecordView {
|
||||
addSignatureMethod = 'appendSignature';
|
||||
}
|
||||
|
||||
const body = this[addSignatureMethod](this.model.get('body') || '', this.model.get('isHtml'));
|
||||
|
||||
this.model.set('body', body, {silent: true});
|
||||
body = this[addSignatureMethod](body || '', this.initialIsHtml) || null;
|
||||
}
|
||||
|
||||
this.model.set('body', body, {silent: true});
|
||||
this.model.set('isHtml', this.initialIsHtml, {silent: true});
|
||||
}
|
||||
|
||||
setup() {
|
||||
|
||||
@@ -875,7 +875,7 @@ class BaseFieldView extends View {
|
||||
this.attributeList = this.getAttributeList(); // for backward compatibility, to be removed
|
||||
|
||||
this.listenTo(this.model, 'change', (model, options) => {
|
||||
if (options.ui) {
|
||||
if (options.ui && (!options.fromField || options.fromField === this.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -905,6 +905,10 @@ class BaseFieldView extends View {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isEditMode() && this.toSkipReRenderOnChange()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reRender = () => {
|
||||
if (!this.isRendered() && !this.isBeingRendered()) {
|
||||
return;
|
||||
@@ -946,6 +950,7 @@ class BaseFieldView extends View {
|
||||
this.model.set(attributes, {
|
||||
ui: true,
|
||||
fromView: this,
|
||||
fromField: this.name,
|
||||
action: 'ui',
|
||||
});
|
||||
});
|
||||
@@ -1688,6 +1693,16 @@ class BaseFieldView extends View {
|
||||
fetchSearchType() {
|
||||
return this.$el.find('select.search-type').val();
|
||||
}
|
||||
|
||||
/**
|
||||
* To skip re-render on change in edit mode.
|
||||
*
|
||||
* @protected
|
||||
* @since 9.1.2
|
||||
*/
|
||||
toSkipReRenderOnChange() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseFieldView;
|
||||
|
||||
@@ -33,10 +33,24 @@ class LinkOneFieldView extends LinkFieldView {
|
||||
searchTypeList = ['is', 'isEmpty', 'isNotEmpty', 'isOneOf']
|
||||
|
||||
fetchSearch() {
|
||||
let type = this.$el.find('select.search-type').val();
|
||||
let value = this.$el.find('[data-name="' + this.idName + '"]').val();
|
||||
const type = this.$el.find('select.search-type').val();
|
||||
const value = this.$el.find('[data-name="' + this.idName + '"]').val();
|
||||
|
||||
if (['isOneOf'].includes(type) && !this.searchData.oneOfIdList.length) {
|
||||
return {
|
||||
type: 'isNotNull',
|
||||
attribute: 'id',
|
||||
data: {
|
||||
type: type,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'isOneOf') {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'linkedWith',
|
||||
field: this.name,
|
||||
@@ -48,7 +62,8 @@ class LinkOneFieldView extends LinkFieldView {
|
||||
},
|
||||
};
|
||||
}
|
||||
else if (type === 'is' || !type) {
|
||||
|
||||
if (type === 'is' || !type) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
@@ -63,7 +78,8 @@ class LinkOneFieldView extends LinkFieldView {
|
||||
},
|
||||
};
|
||||
}
|
||||
else if (type === 'isEmpty') {
|
||||
|
||||
if (type === 'isEmpty') {
|
||||
return {
|
||||
type: 'isNotLinked',
|
||||
data: {
|
||||
@@ -71,7 +87,8 @@ class LinkOneFieldView extends LinkFieldView {
|
||||
},
|
||||
};
|
||||
}
|
||||
else if (type === 'isNotEmpty') {
|
||||
|
||||
if (type === 'isNotEmpty') {
|
||||
return {
|
||||
type: 'isLinked',
|
||||
data: {
|
||||
|
||||
@@ -424,7 +424,7 @@ class LinkFieldView extends BaseFieldView {
|
||||
this.model.set({
|
||||
...attributes,
|
||||
...(await handler.getAttributes(model)),
|
||||
});
|
||||
}, {fromField: this.name});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
|
||||
import BaseFieldView from 'views/fields/base';
|
||||
import MailtoHelper from 'helpers/misc/mailto';
|
||||
import Modal from 'views/modal';
|
||||
import TextPreviewModalView from 'views/modals/text-preview';
|
||||
|
||||
/**
|
||||
@@ -639,14 +638,12 @@ class TextFieldView extends BaseFieldView {
|
||||
const after = value.substring(selectionEnd);
|
||||
|
||||
// Last line, a list item syntax.
|
||||
const match = before.match(/(^|\n)( *[-*]|\d+\.)([^\n]*)$/);
|
||||
const match = before.match(/(^|\n)( *[-*]|\d+\.)([^*\-\n]*)$/);
|
||||
|
||||
if (!match) {
|
||||
// Prevent unwanted scroll applied by the browser on enter.
|
||||
event.preventDefault();
|
||||
|
||||
target.value = before + "\n" + after;
|
||||
target.selectionStart = target.selectionEnd = before.length + 1;
|
||||
const previousWindowScroll = window.scrollY;
|
||||
setTimeout(() => window.scrollTo({top: previousWindowScroll}), 0);
|
||||
|
||||
this.controlTextareaHeight();
|
||||
|
||||
|
||||
@@ -282,10 +282,12 @@ class VarcharFieldView extends BaseFieldView {
|
||||
mainElement.classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (['anyOf', 'noneOf'].includes(type)) {
|
||||
multiSelectContainer.classList.remove('hidden');
|
||||
} else {
|
||||
multiSelectContainer.classList.add('hidden');
|
||||
if (multiSelectContainer) {
|
||||
if (['anyOf', 'noneOf'].includes(type)) {
|
||||
multiSelectContainer.classList.remove('hidden');
|
||||
} else {
|
||||
multiSelectContainer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -977,6 +977,18 @@ class WysiwygFieldView extends TextFieldView {
|
||||
this.$summernote.summernote('insertText', text);
|
||||
}
|
||||
}
|
||||
|
||||
toSkipReRenderOnChange() {
|
||||
if (!this.element || !this.element.contains(document.activeElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.model.hasChanged(this.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default WysiwygFieldView;
|
||||
|
||||
@@ -146,6 +146,10 @@ class SelectCategoryTreeRecordsModalView extends SelectRecordsModalView {
|
||||
buttonsDisabled: true,
|
||||
}, listView => {
|
||||
listView.once('select', models => {
|
||||
if (!Array.isArray(models)) {
|
||||
models = [models];
|
||||
}
|
||||
|
||||
this.trigger('select', models);
|
||||
|
||||
if (this.options.onSelect) {
|
||||
|
||||
@@ -1404,10 +1404,10 @@ class DetailRecordView extends BaseRecordView {
|
||||
}
|
||||
|
||||
afterRender() {
|
||||
this.$middle = this.$el.find('.middle');
|
||||
this.$middle = this.$el.find('.middle').first();
|
||||
|
||||
if (this.bottomView) {
|
||||
this.$bottom = this.$el.find('.bottom');
|
||||
this.$bottom = this.$el.find('.bottom').first();
|
||||
}
|
||||
|
||||
this.initElementReferences();
|
||||
|
||||
@@ -77,6 +77,10 @@ class PostNoteStreamView extends NoteStreamView {
|
||||
}
|
||||
});
|
||||
|
||||
if (this.messageName === 'postThis') {
|
||||
this.messageTemplate = '{user}';
|
||||
}
|
||||
|
||||
if (this.model.get('parentId')) {
|
||||
this.createMessage();
|
||||
|
||||
|
||||
@@ -346,8 +346,8 @@ class UserDetailRecordView extends DetailRecordView {
|
||||
"label": "Portal",
|
||||
"name": "portal",
|
||||
"rows": [
|
||||
[{"name":"portals"}, {"name":"contact"}],
|
||||
[{"name":"portalRoles"}, {"name":"accounts"}],
|
||||
[{"name": "portals"}, {"name": "accounts"}],
|
||||
[{"name": "portalRoles"}, {"name": "contact"}],
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
@@ -175,9 +175,13 @@ class UserEditRecordView extends EditRecordView {
|
||||
getGridLayout(callback) {
|
||||
this.getHelper().layoutManager
|
||||
.get(this.model.entityType, this.options.layoutName || this.layoutName, simpleLayout => {
|
||||
/** @type {module:views/record/detail~panelDefs[]} */
|
||||
const layout = Espo.Utils.cloneDeep(simpleLayout);
|
||||
|
||||
layout.push({
|
||||
/** @type {module:views/record/detail~panelDefs[]} */
|
||||
const panels = [];
|
||||
|
||||
panels.push({
|
||||
"label": "Teams and Access Control",
|
||||
"name": "accessControl",
|
||||
"rows": [
|
||||
@@ -187,17 +191,17 @@ class UserEditRecordView extends EditRecordView {
|
||||
]
|
||||
});
|
||||
|
||||
layout.push({
|
||||
panels.push({
|
||||
"label": "Portal",
|
||||
"name": "portal",
|
||||
"rows": [
|
||||
[{"name": "portals"}, {"name": "contact"}],
|
||||
[{"name": "portalRoles"}, {"name": "accounts"}]
|
||||
[{"name": "portals"}, {"name": "accounts"}],
|
||||
[{"name": "portalRoles"}, {"name": "contact"}]
|
||||
]
|
||||
});
|
||||
|
||||
if (this.getUser().isAdmin() && this.model.isPortal()) {
|
||||
layout.push({
|
||||
panels.push({
|
||||
"label": "Misc",
|
||||
"name": "portalMisc",
|
||||
"rows": [
|
||||
@@ -207,7 +211,7 @@ class UserEditRecordView extends EditRecordView {
|
||||
}
|
||||
|
||||
if (this.model.isAdmin() || this.model.isRegular()) {
|
||||
layout.push({
|
||||
panels.push({
|
||||
"label": "Misc",
|
||||
"name": "misc",
|
||||
"rows": [
|
||||
@@ -221,7 +225,7 @@ class UserEditRecordView extends EditRecordView {
|
||||
this.getUser().isAdmin() &&
|
||||
!this.model.isApi()
|
||||
) {
|
||||
layout.push({
|
||||
panels.push({
|
||||
label: 'Password',
|
||||
rows: [
|
||||
[
|
||||
@@ -273,7 +277,7 @@ class UserEditRecordView extends EditRecordView {
|
||||
}
|
||||
|
||||
if (this.getUser().isAdmin() && this.model.isApi()) {
|
||||
layout.push({
|
||||
panels.push({
|
||||
"name": "auth",
|
||||
"rows": [
|
||||
[{"name": "authMethod"}, false]
|
||||
@@ -281,6 +285,24 @@ class UserEditRecordView extends EditRecordView {
|
||||
});
|
||||
}
|
||||
|
||||
let hasTab = false;
|
||||
|
||||
for (const [i, panel] of layout.entries()) {
|
||||
if (panel.tabBreak && i > 0) {
|
||||
layout.splice(i, 0, ...panels);
|
||||
|
||||
hasTab = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasTab) {
|
||||
layout.push(...panels);
|
||||
}
|
||||
|
||||
this.detailLayout = layout;
|
||||
|
||||
const gridLayout = {
|
||||
type: 'record',
|
||||
layout: this.convertDetailLayout(layout),
|
||||
|
||||
@@ -2305,6 +2305,28 @@ td > span.color-icon {
|
||||
text-align: center;
|
||||
margin-right: var(--6px);
|
||||
}
|
||||
|
||||
[data-key="user"] {
|
||||
> a {
|
||||
&,
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--btn-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
a {
|
||||
&[data-scope="User"] {
|
||||
&,
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--btn-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stream-post-container,
|
||||
@@ -2767,7 +2789,7 @@ table.table td.cell .html-container {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
h5, h6 {
|
||||
h5, h6 {
|
||||
margin-top: calc(var(--line-height-computed) / 2);
|
||||
margin-bottom: calc(var(--line-height-computed) / 2);
|
||||
}
|
||||
@@ -2835,6 +2857,21 @@ table.table td.cell .html-container {
|
||||
ul, ol {
|
||||
padding-left: var(--30px);
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
|
||||
td, th {
|
||||
padding: var(--table-cell-padding-half) var(--table-cell-padding);
|
||||
border: var(--1px) solid var(--default-border-color);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
+ table {
|
||||
margin-top: var(--line-height-computed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field > .html-container {
|
||||
|
||||
@@ -36,12 +36,13 @@
|
||||
.selectize-dropdown,
|
||||
.selectize-dropdown.form-control,
|
||||
.autocomplete-suggestions,
|
||||
.ui-timepicker-wrapper,
|
||||
.ui-timepicker-wrapper {
|
||||
.dropdown-border();
|
||||
}
|
||||
|
||||
// Separately intentionally as this one is not supported in FF.
|
||||
select.form-control::picker(select) {
|
||||
border: var(--dropdown-border-width) solid var(--dropdown-border);
|
||||
box-shadow: var(--dropdown-box-shadow);
|
||||
border-radius: var(--dropdown-border-radius);
|
||||
border-width: var(--dropdown-border-width);
|
||||
.dropdown-border();
|
||||
}
|
||||
|
||||
.popover {
|
||||
|
||||
@@ -214,13 +214,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
.modal-dialog .modal-body .list-sticky-bar {
|
||||
top: var(--94px) !important;
|
||||
width: 100% !important;
|
||||
right: 0 !important;
|
||||
left: unset !important;
|
||||
padding-left: calc(var(--container-padding) - var(--1px));
|
||||
padding-right: calc(var(--container-padding) - var(--1px) + var(--scroll-width));
|
||||
.modal-dialog {
|
||||
|
||||
.modal-body {
|
||||
.list-sticky-bar {
|
||||
top: var(--94px) !important;
|
||||
width: 100% !important;
|
||||
right: 0 !important;
|
||||
left: unset !important;
|
||||
padding-left: calc(var(--container-padding) - var(--1px));
|
||||
padding-right: calc(var(--container-padding) - var(--1px) + var(--scroll-width));
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:has(footer.modal-footer)) {
|
||||
.modal-body {
|
||||
.list-sticky-bar {
|
||||
top: var(--43px) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-xs-max) {
|
||||
|
||||
@@ -213,3 +213,10 @@
|
||||
border-bottom-right-radius: var(--panel-border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-border() {
|
||||
border: var(--dropdown-border-width) solid var(--dropdown-border);
|
||||
box-shadow: var(--dropdown-box-shadow);
|
||||
border-radius: var(--dropdown-border-radius);
|
||||
border-width: var(--dropdown-border-width);
|
||||
}
|
||||
|
||||
Generated
+7
-7
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "9.1.1",
|
||||
"version": "9.1.5",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "espocrm",
|
||||
"version": "9.1.1",
|
||||
"version": "9.1.5",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -55,7 +55,7 @@
|
||||
"devDependencies": {
|
||||
"archiver": "^5.3.0",
|
||||
"chromedriver": "^127.0.1",
|
||||
"espo-frontend-build-tools": "github:espocrm/frontend-build-tools#0.1.1",
|
||||
"espo-frontend-build-tools": "github:espocrm/frontend-build-tools#0.2.0",
|
||||
"fstream": ">=1.0.12",
|
||||
"glob": "^10.2.6",
|
||||
"grunt": "^1.5.3",
|
||||
@@ -3270,8 +3270,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/espo-frontend-build-tools": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "git+ssh://git@github.com/espocrm/frontend-build-tools.git#7f4723b205cd35c602dc5c5aa81a21ad0a7ce0c1",
|
||||
"version": "0.2.0",
|
||||
"resolved": "git+ssh://git@github.com/espocrm/frontend-build-tools.git#bcbd295c2728b391bebd446ef4ad3ca873d1f6eb",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/cli": "^7.25.0",
|
||||
@@ -9713,9 +9713,9 @@
|
||||
}
|
||||
},
|
||||
"espo-frontend-build-tools": {
|
||||
"version": "git+ssh://git@github.com/espocrm/frontend-build-tools.git#7f4723b205cd35c602dc5c5aa81a21ad0a7ce0c1",
|
||||
"version": "git+ssh://git@github.com/espocrm/frontend-build-tools.git#bcbd295c2728b391bebd446ef4ad3ca873d1f6eb",
|
||||
"dev": true,
|
||||
"from": "espo-frontend-build-tools@github:espocrm/frontend-build-tools#0.1.1",
|
||||
"from": "espo-frontend-build-tools@github:espocrm/frontend-build-tools#0.2.0",
|
||||
"requires": {
|
||||
"@babel/cli": "^7.25.0",
|
||||
"@babel/core": "^7.25.0",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "9.1.1",
|
||||
"version": "9.1.5",
|
||||
"description": "Open-source CRM.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -21,7 +21,7 @@
|
||||
"devDependencies": {
|
||||
"archiver": "^5.3.0",
|
||||
"chromedriver": "^127.0.1",
|
||||
"espo-frontend-build-tools": "github:espocrm/frontend-build-tools#0.1.1",
|
||||
"espo-frontend-build-tools": "github:espocrm/frontend-build-tools#0.2.0",
|
||||
"fstream": ">=1.0.12",
|
||||
"glob": "^10.2.6",
|
||||
"grunt": "^1.5.3",
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"type": "object",
|
||||
"description": "A scope level.",
|
||||
"additionalProperties": {
|
||||
"description": "A scope. True enables access to scopes that don't have actions. False fully restricts access to a scope. A string value makes the framework to use roles from another scope and apply it to our scope. It can be useful to have roles only for one parent scope, when child scopes uses roles of the parent scope. `boolean:{scope}` format will convert a foreign scope data to the boolean type.",
|
||||
"description": "A scope. True enables access to scopes that don't have actions. False fully restricts access to the scope. A string value makes the framework to use roles from another scope and apply them to our scope. It can be useful to have roles only for one parent scope, when child scopes use roles of the parent scope. `boolean:{scope}` format will convert a foreign scope data to the boolean type.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
|
||||
@@ -296,7 +296,7 @@ class PostgresqlQueryComposerTest extends \PHPUnit\Framework\TestCase
|
||||
$sql = $this->queryComposer->composeSelect($query);
|
||||
|
||||
$expectedSql =
|
||||
'SELECT "comment"."created_at" + INTERVAL \'MINUTE 330\' AS "createdAt" ' .
|
||||
'SELECT "comment"."created_at" + INTERVAL \'330 MINUTE\' AS "createdAt" ' .
|
||||
'FROM "comment"';
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
@@ -319,7 +319,7 @@ class PostgresqlQueryComposerTest extends \PHPUnit\Framework\TestCase
|
||||
$sql = $this->queryComposer->composeSelect($query);
|
||||
|
||||
$expectedSql =
|
||||
'SELECT "comment"."created_at" + INTERVAL \'MINUTE -330\' AS "createdAt" ' .
|
||||
'SELECT "comment"."created_at" + INTERVAL \'-330 MINUTE\' AS "createdAt" ' .
|
||||
'FROM "comment"';
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
@@ -342,9 +342,28 @@ class PostgresqlQueryComposerTest extends \PHPUnit\Framework\TestCase
|
||||
$sql = $this->queryComposer->composeSelect($query);
|
||||
|
||||
$expectedSql =
|
||||
'SELECT "comment"."created_at" + INTERVAL \'HOUR 5\' AS "createdAt" ' .
|
||||
'SELECT "comment"."created_at" + INTERVAL \'5 HOUR\' AS "createdAt" ' .
|
||||
'FROM "comment"';
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
}
|
||||
|
||||
public function testDeleteWithAlias()
|
||||
{
|
||||
$query = $this->queryBuilder
|
||||
->delete()
|
||||
->from('Account', 'a')
|
||||
->where([
|
||||
'a.name' => 'test',
|
||||
])
|
||||
->build();
|
||||
|
||||
$sql = $this->queryComposer->compose($query);
|
||||
|
||||
$expectedSql =
|
||||
"DELETE FROM \"account\" AS \"a\" " .
|
||||
"WHERE \"a\".\"name\" = 'test'";
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user