Compare commits

..

76 Commits

Author SHA1 Message Date
Yuri Kuznetsov 9a013b7ea7 8.0.5 2023-11-08 10:04:43 +02:00
Yuri Kuznetsov 124214aee3 curl protocols option 2023-11-07 10:01:43 +02:00
Yuri Kuznetsov 3bd6b3bd46 webhook allow http and https only 2023-11-06 16:03:49 +02:00
Yuri Kuznetsov c536cee637 image upload url check 2023-11-06 16:02:13 +02:00
Yuri Kuznetsov b1d2795347 not copying status field on duplicate 2023-11-03 11:38:46 +02:00
Yuri Kuznetsov 0f9bce70cf mass update use select 2023-11-01 16:56:05 +02:00
Yuri Kuznetsov 6f1f72127f cs 2023-11-01 16:55:55 +02:00
Yuri Kuznetsov 986edeb79e cs 2023-11-01 16:49:13 +02:00
Yuri Kuznetsov b3391df6e8 mass update targetLists 2023-11-01 16:43:05 +02:00
Yuri Kuznetsov d8a114c90a popover improvements 2023-10-30 15:48:18 +02:00
Yuri Kuznetsov 3fa9f4bf91 8.0.4 2023-10-27 10:22:00 +03:00
Yuri Kuznetsov 6c4a139e66 KB portal 2023-10-26 21:29:49 +03:00
Yuri Kuznetsov d3a1db25c2 comment 2023-10-26 16:57:22 +03:00
Yuri Kuznetsov 0fcd3cd780 msg fix 2023-10-26 16:48:54 +03:00
Yuri Kuznetsov 61cb573eec cs 2023-10-26 14:26:16 +03:00
Yuri Kuznetsov 5dd84bf9a1 SMTP translated message 2023-10-26 14:24:55 +03:00
Yuri Kuznetsov 5dad7c6bba cs 2023-10-26 14:01:06 +03:00
Yuri Kuznetsov 3007d50299 cs 2023-10-26 14:00:05 +03:00
Yuri Kuznetsov 11c0ac6987 noopener 2023-10-26 13:52:33 +03:00
Yuri Kuznetsov 2bb62883a4 cs 2023-10-26 13:33:03 +03:00
Yuri Kuznetsov 5796d2fb85 cs 2023-10-26 13:28:05 +03:00
Yuri Kuznetsov dae318c9bb cs 2023-10-26 13:26:07 +03:00
Yuri Kuznetsov 342764e9ff related records modal fetch when rendered 2023-10-26 13:19:43 +03:00
Yuri Kuznetsov 27b9e28e20 cs 2023-10-26 12:35:16 +03:00
Yuri Kuznetsov f8d5dd7fa6 modal select related sync mass remove 2023-10-26 12:06:49 +03:00
Yuri Kuznetsov 7408f97fbd cs 2023-10-26 11:56:03 +03:00
Yuri Kuznetsov d1a9cf840f inspection let/const 2023-10-26 11:29:35 +03:00
Yuri Kuznetsov d593927b14 cs 2023-10-26 11:29:25 +03:00
Yuri Kuznetsov f62ebb8fe8 ref 2023-10-26 11:22:51 +03:00
Yuri Kuznetsov ede798dbe5 schema additions 2023-10-26 11:15:54 +03:00
Yuri Kuznetsov c3095b6a32 schema dynamic logic options 2023-10-26 11:06:09 +03:00
Yuri Kuznetsov 9eb5bfc9fe css fix 2023-10-26 10:59:11 +03:00
Yuri Kuznetsov bf2c529884 labels 2023-10-26 10:48:49 +03:00
Yuri Kuznetsov 7ce8150541 fix link fields advanced filter 2023-10-25 20:38:20 +03:00
Yuri Kuznetsov 44de149923 link, link-multiple select method 2023-10-25 14:04:14 +03:00
Yuri Kuznetsov 8ecdc7b9d9 first panel filter 2023-10-25 12:41:31 +03:00
Yuri Kuznetsov 95c64359d2 cons 2023-10-25 12:06:46 +03:00
Yuri Kuznetsov 44203e2178 one primary filter 2023-10-25 12:03:22 +03:00
Yuri Kuznetsov d3d940d9c9 fix 2023-10-25 11:03:29 +03:00
Yuri Kuznetsov 9d3efae141 fix list related modal unlink disabled 2023-10-24 20:20:30 +03:00
Yuri Kuznetsov e6eb3e7099 type fix 2023-10-24 10:37:15 +03:00
DrWarpMan 3ab2b37471 fix validatorClassNameMap (#2881) 2023-10-24 10:29:04 +03:00
Yuri Kuznetsov 33e7f4e3ef json retrive support empty path 2023-10-23 20:59:59 +03:00
Yuri Kuznetsov 6463eaa6f6 less placement.less 2023-10-23 14:22:25 +03:00
Yuri Kuznetsov a066c0a187 style fix 2023-10-23 14:09:54 +03:00
Yuri Kuznetsov 176f65713f list-group css fix 2023-10-23 13:57:25 +03:00
Yuri Kuznetsov 493afdf3d4 tableClassName 2023-10-19 17:22:30 +03:00
Yuri Kuznetsov c85b6a37a6 cs 2023-10-19 17:17:03 +03:00
Yuri Kuznetsov 81971d0eda doc fix 2023-10-19 17:02:38 +03:00
Yuri Kuznetsov 8a6cc9ea35 before upgrade version check 2023-10-19 16:55:30 +03:00
Yuri Kuznetsov 4fd125ddfc base url support index file 2023-10-19 16:48:50 +03:00
Yuri Kuznetsov 1298a8382d export: use additional applier 2023-10-19 11:17:35 +03:00
Yuri Kuznetsov bb9c352f55 export: do not list not exportable fields 2023-10-19 10:40:23 +03:00
Yuri Kuznetsov 045533d080 export: do not load link-multiple if no field 2023-10-19 10:34:00 +03:00
Yuri Kuznetsov 6a5ab5f738 command listed 2023-10-17 15:22:28 +03:00
Yuri Kuznetsov c1b0e4fd17 throw docs 2023-10-17 13:12:59 +03:00
Yuri Kuznetsov e71928ffa7 css fix 2023-10-17 13:12:25 +03:00
Yuri Kuznetsov de3f5de029 fix translation 2023-10-17 13:08:00 +03:00
Yuri Kuznetsov 57ceee4d4c export: do not sanitize numeric values 2023-10-17 13:04:38 +03:00
Yuri Kuznetsov 9a5cd7609b import ksort row 2023-10-17 12:45:14 +03:00
Yuri Kuznetsov 1382dca3e3 cs 2023-10-17 11:46:14 +03:00
Yuri Kuznetsov bc7a3e8839 cs, browser compatibility fix 2023-10-17 11:38:17 +03:00
Yuri Kuznetsov c5d6f4c63f entity manager disabled fix 2023-10-16 17:11:03 +03:00
Yuri Kuznetsov 6776f813af schema 2023-10-16 16:48:53 +03:00
Yuri Kuznetsov 6d18f2485d record helper as option 2023-10-13 16:19:50 +03:00
Yuri Kuznetsov 73753b364e unchain main view 2023-10-13 10:21:06 +03:00
Yuri Kuznetsov 68d6a14d04 orm: wrap and/or expr into braces 2023-10-11 10:43:39 +03:00
Yuri Kuznetsov c155d37790 comment 2023-10-10 17:04:25 +03:00
Yuri Kuznetsov 6a6446d0b7 whenReady, field re-render with prepare fix 2023-10-10 16:34:55 +03:00
Yuri Kuznetsov b0b4fe5570 set field read only race condition fix 2023-10-10 15:06:12 +03:00
Yuri Kuznetsov 6de8c3d1ca calendar dashlet size update 2023-10-10 12:01:25 +03:00
Yuri Kuznetsov 319f3f645a calaendar css fix 2023-10-10 11:52:23 +03:00
Yuri Kuznetsov bedfbb0e6b calendar slot duration 2023-10-10 11:15:16 +03:00
Yuri Kuznetsov 148ffce6b7 re-reject on save error 2023-10-09 13:13:31 +03:00
Yuri Kuznetsov 85cb969780 fix schema docs 2023-10-09 11:51:56 +03:00
Yuri Kuznetsov c1a75ad051 schema 2023-10-09 11:46:07 +03:00
82 changed files with 1505 additions and 753 deletions
+1
View File
@@ -2,6 +2,7 @@
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ES6ConvertLetToConst" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
<inspection_tool class="ES6ConvertVarToLetConst" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="JSIgnoredPromiseFromCall" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PhpDocMissingThrowsInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
+2
View File
@@ -30,6 +30,7 @@
namespace Espo\Core\Api;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Conflict;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
@@ -47,6 +48,7 @@ interface Action
* @throws BadRequest
* @throws Forbidden
* @throws NotFound
* @throws Conflict
* @throws Error
*/
public function process(Request $request): Response;
@@ -112,6 +112,7 @@ class KeysProvider
CURLOPT_TIMEOUT => self::REQUEST_TIMEOUT,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
]);
/** @var string|false $response */
@@ -229,6 +229,7 @@ class Login implements LoginInterface
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_HTTPHEADER => ['content-type: application/x-www-form-urlencoded'],
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
]);
/** @var string|false $response */
@@ -53,7 +53,7 @@ class Help implements Command
$commandList = array_filter(
$fullCommandList,
function ($item): bool {
return (bool) $this->metadata->get(['app', 'consoleCommands', $item]);
return (bool) $this->metadata->get(['app', 'consoleCommands', $item, 'listed']);
}
);
@@ -48,6 +48,8 @@ class Record extends RecordBase
* @throws BadRequest
* @throws NotFound
* @throws Forbidden
* @throws Error
* @noinspection PhpUnused
*/
public function getActionListLinked(Request $request): stdClass
{
@@ -167,6 +169,7 @@ class Record extends RecordBase
* @throws BadRequest
* @throws NotFoundSilent
* @throws Forbidden
* @noinspection PhpUnused
*/
public function putActionFollow(Request $request): bool
{
@@ -186,6 +189,7 @@ class Record extends RecordBase
*
* @throws NotFoundSilent
* @throws BadRequest
* @noinspection PhpUnused
*/
public function deleteActionUnfollow(Request $request): bool
{
@@ -138,11 +138,12 @@ class FieldValidationManager
*/
private function getMandatoryValidationList(string $entityType, string $field): array
{
/** @var ?string $fieldType */
$fieldType = $this->fieldUtil->getEntityTypeFieldParam($entityType, $field, 'type');
return
$this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'mandatoryValidationList']) ??
$this->metadata->get(['fields', $fieldType, 'mandatoryValidationList']) ?? [];
$this->metadata->get(['fields', $fieldType ?? '', 'mandatoryValidationList']) ?? [];
}
/**
@@ -150,11 +151,12 @@ class FieldValidationManager
*/
private function getValidationList(string $entityType, string $field): array
{
/** @var ?string $fieldType */
$fieldType = $this->fieldUtil->getEntityTypeFieldParam($entityType, $field, 'type');
return
$this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'validationList']) ??
$this->metadata->get(['fields', $fieldType, 'validationList']) ?? [];
$this->metadata->get(['fields', $fieldType ?? '', 'validationList']) ?? [];
}
/**
@@ -31,6 +31,7 @@ namespace Espo\Core\FieldValidation;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use Espo\Core\Utils\FieldUtil;
use Espo\ORM\Entity;
use RuntimeException;
@@ -40,7 +41,8 @@ class ValidatorFactory
public function __construct(
private InjectableFactory $injectableFactory,
private Metadata $metadata
private Metadata $metadata,
private FieldUtil $fieldUtil
) {}
public function isCreatable(string $entityType, string $field, string $type): bool
@@ -67,9 +69,12 @@ class ValidatorFactory
*/
private function getClassName(string $entityType, string $field, string $type): ?string
{
/** @var ?string $fieldType */
$fieldType = $this->fieldUtil->getEntityTypeFieldParam($entityType, $field, 'type');
return
$this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'validatorClassNameMap', $type]) ??
$this->metadata->get(['fields', $field, 'validatorClassNameMap', $type]);
$this->metadata->get(['fields', $fieldType ?? '', 'validatorClassNameMap', $type]);
}
/**
@@ -29,26 +29,31 @@
namespace Espo\Core\Formula\Functions\JsonGroup;
use Espo\Core\Formula\{
Functions\BaseFunction,
ArgumentList,
};
use Espo\Core\Formula\ArgumentList;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\ExecutionException;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Functions\BaseFunction;
class RetrieveType extends BaseFunction
{
/**
* @return mixed
* @throws \Espo\Core\Formula\Exceptions\TooFewArguments
* @throws \Espo\Core\Formula\Exceptions\Error
* @throws TooFewArguments
* @throws Error
* @throws ExecutionException
*/
public function process(ArgumentList $args)
{
if (count($args) < 2) {
if (count($args) < 1) {
$this->throwTooFewArguments();
}
$jsonString = $this->evaluate($args[0]);
$path = $this->evaluate($args[1]);
$path = count($args) > 1 ?
$this->evaluate($args[1]) :
'';
if (!is_string($jsonString)) {
$this->throwBadArgumentType(1, 'string');
@@ -58,10 +63,6 @@ class RetrieveType extends BaseFunction
$this->throwBadArgumentType(2, 'string');
}
if ($path === '') {
$this->throwBadArgumentValue(2);
}
$item = json_decode($jsonString);
$pathArray = $this->splitPath($path);
@@ -75,6 +76,10 @@ class RetrieveType extends BaseFunction
*/
private function splitPath(string $path): array
{
if ($path === '') {
return [];
}
/** @var string[] $pathArray */
$pathArray = preg_split('/(?<!\\\)\./', $path);
+13 -5
View File
@@ -85,6 +85,7 @@ class Sender
private FileStorageManager $fileStorageManager
) {
/** @noinspection PhpDeprecationInspection */
$this->useGlobal();
}
@@ -149,6 +150,7 @@ class Sender
throw new InvalidArgumentException();
}
/** @noinspection PhpDeprecationInspection */
return $this->useSmtp($params);
}
@@ -171,6 +173,7 @@ class Sender
*/
public function withEnvelopeOptions(array $options): self
{
/** @noinspection PhpDeprecationInspection */
return $this->setEnvelopeOptions($options);
}
@@ -252,8 +255,9 @@ class Sender
$authMechanism = $params['authMechanism'] ?? $params['smtpAuthMechanism'] ?? null;
if ($authMechanism) {
$authMechanism = preg_replace("([\.]{2,})", '', $authMechanism);
$authMechanism = preg_replace("([.]{2,})", '', $authMechanism);
/** @noinspection SpellCheckingInspection */
if (in_array($authMechanism, ['login', 'crammd5', 'plain'])) {
$options['connectionClass'] = $authMechanism;
}
@@ -571,7 +575,7 @@ class Sender
empty($messageId) ||
!is_string($messageId) ||
strlen($messageId) < 4 ||
strpos($messageId, 'dummy:') === 0
str_starts_with($messageId, 'dummy:')
) {
$messageId = $this->generateMessageId($email);
@@ -597,13 +601,17 @@ class Sender
$email->set('dateSent', DateTime::createNow()->getString());
}
catch (Exception $e) {
/** @noinspection PhpDeprecationInspection */
$this->resetParams();
/** @noinspection PhpDeprecationInspection */
$this->useGlobal();
$this->handleException($e);
}
/** @noinspection PhpDeprecationInspection */
$this->resetParams();
/** @noinspection PhpDeprecationInspection */
$this->useGlobal();
}
@@ -642,14 +650,14 @@ class Sender
private function handleException(Exception $e): void
{
if ($e instanceof ProtocolRuntimeException) {
$message = "Unknown error.";
$message = "unknownError";
if (
stripos($e->getMessage(), 'password') !== false ||
stripos($e->getMessage(), 'credentials') !== false ||
stripos($e->getMessage(), '5.7.8') !== false
) {
$message = 'Invalid credentials.';
$message = 'invalidCredentials';
}
$this->log->error("Email sending error: " . $e->getMessage());
@@ -671,7 +679,7 @@ class Sender
}
else {
$messageId =
'' . md5($email->get('name')) . '/' .time() . '/' .
md5($email->get('name')) . '/' .time() . '/' .
$rand . '@espo';
}
@@ -29,6 +29,7 @@
namespace Espo\Core\Record\Duplicator;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Entity;
use Espo\ORM\Defs;
use Espo\ORM\Defs\FieldDefs;
@@ -44,7 +45,8 @@ class EntityDuplicator
public function __construct(
private Defs $defs,
private FieldDuplicatorFactory $fieldDuplicatorFactory,
private FieldUtil $fieldUtil
private FieldUtil $fieldUtil,
private Metadata $metadata
) {}
public function duplicate(Entity $entity): stdClass
@@ -68,7 +70,7 @@ class EntityDuplicator
$entityType = $entity->getEntityType();
$field = $fieldDefs->getName();
if ($fieldDefs->getParam('duplicateIgnore')) {
if ($this->toIgnoreField($entityType, $fieldDefs)) {
$attributeList = $this->fieldUtil->getAttributeList($entityType, $field);
foreach ($attributeList as $attribute) {
@@ -90,4 +92,20 @@ class EntityDuplicator
$valueMap->$attribute = $value;
}
}
private function toIgnoreField(string $entityType, FieldDefs $fieldDefs): bool
{
$type = $fieldDefs->getType();
// @todo Use FieldType constants.
if (in_array($type, ['autoincrement', 'number'])) {
return true;
}
if ($this->metadata->get(['scopes', $entityType, 'statusField']) === $fieldDefs->getName()) {
return true;
}
return (bool) $fieldDefs->getParam('duplicateIgnore');
}
}
+6 -3
View File
@@ -277,7 +277,9 @@ class Service implements Crud,
->create()
->from($this->entityType)
->withSearchParams(
SearchParams::create()->withSelect(['*'])
SearchParams::create()
->withSelect(['*'])
->withPrimaryFilter('one')
)
->withAdditionalApplierClassNameList(
$this->createSelectApplierClassNameListProvider()->get($this->entityType)
@@ -1385,7 +1387,7 @@ class Service implements Crud,
}
if (!$id || !$link) {
throw new BadRequest;
throw new BadRequest();
}
$this->processForbiddenLinkEditCheck($link);
@@ -1396,6 +1398,7 @@ class Service implements Crud,
throw new NotFound();
}
// Not used link-check deliberately. Only edit access.
if (!$this->acl->check($entity, AclTable::ACTION_EDIT)) {
throw new Forbidden();
}
@@ -1407,7 +1410,7 @@ class Service implements Crud,
}
if (!$entity instanceof CoreEntity) {
throw new LogicException("Only core entities are supported");
throw new LogicException("Only core entities are supported.");
}
$foreignEntityType = $entity->getRelationParam($link, 'entity');
@@ -0,0 +1,45 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* 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
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU 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\ORM\Query\SelectBuilder as QueryBuilder;
/**
* A dummy filter 'one'. Applied only when reading a single record (from the detail view).
* Can be detected in a custom AdditionalApplier to distinguish a read request from a find request.
*/
class One implements Filter
{
public const NAME = 'one';
public function apply(QueryBuilder $queryBuilder): void
{}
}
@@ -37,6 +37,9 @@ use Espo\Core\AclManager;
use Espo\Core\Portal\AclManagerContainer as PortalAclManagerContainer;
use Espo\Core\ApplicationState;
/**
* @todo Use WeakMap (User as a key).
*/
class UserAclManagerProvider
{
/** @var array<string, AclManager> */
@@ -0,0 +1,95 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* 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
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Security;
use const DNS_A;
use const FILTER_FLAG_NO_PRIV_RANGE;
use const FILTER_FLAG_NO_RES_RANGE;
use const FILTER_VALIDATE_IP;
use const FILTER_VALIDATE_URL;
use const PHP_URL_HOST;
class UrlCheck
{
public function isUrl(string $url): bool
{
return filter_var($url, FILTER_VALIDATE_URL) !== false;
}
/**
* Checks whether a URL does not follow to an internal host.
*/
public function isNotInternalUrl(string $url): bool
{
if (!$this->isUrl($url)) {
return false;
}
$host = parse_url($url, PHP_URL_HOST);
if (!is_string($host)) {
return false;
}
$records = dns_get_record($host, DNS_A);
if (filter_var($host, FILTER_VALIDATE_IP)) {
return $this->ipAddressIsNotInternal($host);
}
if (!$records) {
return false;
}
foreach ($records as $record) {
/** @var ?string $idAddress */
$idAddress = $record['ip'] ?? null;
if (!$idAddress) {
return false;
}
if (!$this->ipAddressIsNotInternal($idAddress)) {
return false;
}
}
return true;
}
private function ipAddressIsNotInternal(string $ipAddress): bool
{
return (bool) filter_var(
$ipAddress,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
);
}
}
+2
View File
@@ -92,6 +92,8 @@ class Sender
curl_setopt($handler, \CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($handler, \CURLOPT_CONNECTTIMEOUT, $connectTimeout);
curl_setopt($handler, \CURLOPT_TIMEOUT, $timeout);
curl_setopt($handler, \CURLOPT_PROTOCOLS, \CURLPROTO_HTTPS | \CURLPROTO_HTTP);
curl_setopt($handler, \CURLOPT_REDIR_PROTOCOLS, \CURLPROTO_HTTPS);
curl_setopt($handler, \CURLOPT_HTTPHEADER, $headerList);
curl_setopt($handler, \CURLOPT_POSTFIELDS, $payload);
@@ -9,5 +9,6 @@
"modeList": ["month", "agendaWeek", "timeline", "agendaDay"],
"sharedViewModeList": ["basicWeek", "month", "basicDay"],
"additionalColorList": ["#AB78AD", "#CC9B45"],
"iconClass": "far fa-calendar-alt"
"iconClass": "far fa-calendar-alt",
"slotDuration": 30
}
@@ -194,7 +194,6 @@
"type": "linkMultiple",
"layoutDetailDisabled": true,
"layoutListDisabled": true,
"layoutMassUpdateDisabled": true,
"importDisabled": true,
"exportDisabled": true,
"noLoad": true
@@ -466,7 +466,6 @@
"type": "linkMultiple",
"layoutDetailDisabled": true,
"layoutListDisabled": true,
"layoutMassUpdateDisabled": true,
"importDisabled": true,
"noLoad": true
},
@@ -216,7 +216,6 @@
"type": "linkMultiple",
"layoutDetailDisabled": true,
"layoutListDisabled": true,
"layoutMassUpdateDisabled": true,
"importDisabled": true,
"noLoad": true
},
@@ -3,6 +3,7 @@
"layouts": true,
"tab": true,
"acl": true,
"aclPortal": true,
"aclPortalLevelList": ["all", "no"],
"aclPortalActionList": ["read"],
"module": "Crm",
@@ -974,7 +974,7 @@ abstract class BaseQueryComposer implements QueryComposer
}
if (in_array($function, ['OR', 'AND'])) {
return implode(' ' . $function . ' ', $argumentPartList);
return '(' . implode(' ' . $function . ' ', $argumentPartList) . ')';
}
if (!$isBuiltIn && $this->functionConverterFactory) {
@@ -121,6 +121,9 @@
"sendingFailed": "Email sending failed"
},
"messages": {
"invalidCredentials": "Invalid credentials.",
"unknownError": "Unknown error.",
"recipientAddressRejected": "Recipient address rejected.",
"noSmtpSetup": "SMTP is not configured: {link}",
"testEmailSent": "Test email has been sent",
"emailSent": "Email has been sent",
@@ -4,7 +4,8 @@
"type": "enum",
"translation": "Global.scopeNames",
"required": true,
"readOnly": true
"readOnly": true,
"view": "views/fields/entity-type"
},
"status": {
"type": "enum",
@@ -57,6 +57,7 @@ class LatestReleaseDataRequester
curl_setopt($ch, CURLOPT_URL, $requestUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS | CURLPROTO_HTTP);
/** @var string|false $result */
$result = curl_exec($ch);
@@ -32,8 +32,10 @@ namespace Espo\Tools\Attachment;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\ErrorSilent;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\ForbiddenSilent;
use Espo\Core\Utils\File\MimeType;
use Espo\Core\Utils\Metadata;
use Espo\Core\Utils\Security\UrlCheck;
use Espo\Entities\Attachment as Attachment;
use Espo\ORM\EntityManager;
use Espo\Repositories\Attachment as AttachmentRepository;
@@ -51,7 +53,8 @@ class UploadUrlService
Metadata $metadata,
EntityManager $entityManager,
MimeType $mimeType,
DetailsObtainer $detailsObtainer
DetailsObtainer $detailsObtainer,
private UrlCheck $urlCheck
) {
$this->accessChecker = $accessChecker;
$this->metadata = $metadata;
@@ -68,6 +71,10 @@ class UploadUrlService
*/
public function uploadImage(string $url, FieldData $data): Attachment
{
if (!$this->urlCheck->isNotInternalUrl($url)) {
throw new ForbiddenSilent("Not allowed URL.");
}
$attachment = $this->getAttachmentRepository()->getNew();
$this->accessChecker->check($data);
@@ -130,9 +137,12 @@ class UploadUrlService
$opts[\CURLOPT_SSL_VERIFYPEER] = true;
$opts[\CURLOPT_SSL_VERIFYHOST] = 2;
$opts[\CURLOPT_RETURNTRANSFER] = true;
$opts[\CURLOPT_FOLLOWLOCATION] = true;
// Prevents Server Side Request Forgery by redirecting to an internal host.
$opts[\CURLOPT_FOLLOWLOCATION] = false;
$opts[\CURLOPT_MAXREDIRS] = 2;
$opts[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
$opts[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTPS | \CURLPROTO_HTTP;
$opts[\CURLOPT_REDIR_PROTOCOLS] = \CURLPROTO_HTTPS;
$ch = curl_init();
+7 -2
View File
@@ -30,6 +30,7 @@
namespace Espo\Tools\Export;
use Espo\Core\ORM\Repository\Option\SaveOption;
use Espo\Core\Record\Select\ApplierClassNameListProvider;
use Espo\Tools\Export\Collection as ExportCollection;
use Espo\Tools\Export\Processor\Params as ProcessorParams;
use Espo\ORM\Entity;
@@ -72,7 +73,8 @@ class Export
private FileStorageManager $fileStorageManager,
private ListLoadProcessor $listLoadProcessor,
private FieldUtil $fieldUtil,
private User $user
private User $user,
private ApplierClassNameListProvider $applierClassNameListProvider
) {}
public function setParams(Params $params): self
@@ -174,7 +176,7 @@ class Export
$fileName = $fileName ?
$fileName . '.' . $fileExtension :
"Export_{$entityType}.{$fileExtension}";
"Export_$entityType.$fileExtension";
$processorParams = (new ProcessorParams($fileName, $attributeList, $fieldList))
->withName($params->getName())
@@ -291,6 +293,9 @@ class Export
->create()
->forUser($this->user)
->from($entityType)
->withAdditionalApplierClassNameList(
$this->applierClassNameListProvider->get($entityType)
)
->withSearchParams($searchParams);
if ($params->applyAccessControl()) {
@@ -34,6 +34,9 @@ use Espo\Core\Utils\Metadata;
use Espo\ORM\Entity;
use Espo\Tools\Export\AdditionalFieldsLoader as AdditionalFieldsLoaderInterface;
/**
* @noinspection PhpUnused
*/
class AdditionalFieldsLoader implements AdditionalFieldsLoaderInterface
{
public function __construct(private Metadata $metadata) {}
@@ -52,7 +55,7 @@ class AdditionalFieldsLoader implements AdditionalFieldsLoaderInterface
$fieldType === 'linkMultiple' ||
$fieldType === 'attachmentMultiple'
) {
if (!$entity->has($field . 'Ids')) {
if (!$entity->has($field . 'Ids') && $entity->hasLinkMultipleField($field)) {
$entity->loadLinkMultipleField($field);
}
}
@@ -110,6 +110,10 @@ class Processor implements ProcessorInterface
return $value;
}
if (is_numeric($value)) {
return $value;
}
if (in_array($value[0], ['+', '-', '@', '='])) {
return "'" . $value;
}
@@ -34,6 +34,9 @@ use Espo\Core\Utils\Metadata;
use Espo\ORM\Entity;
use Espo\Tools\Export\AdditionalFieldsLoader as AdditionalFieldsLoaderInterface;
/**
* @noinspection PhpUnused
*/
class AdditionalFieldsLoader implements AdditionalFieldsLoaderInterface
{
public function __construct(private Metadata $metadata)
@@ -76,7 +79,7 @@ class AdditionalFieldsLoader implements AdditionalFieldsLoaderInterface
->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']);
if ($fieldType === 'linkMultiple' || $fieldType === 'attachmentMultiple') {
if (!$entity->has($field . 'Ids')) {
if (!$entity->has($field . 'Ids') && $entity->hasLinkMultipleField($field)) {
$entity->loadLinkMultipleField($field);
}
}
@@ -283,6 +283,10 @@ class OpenSpoutProcessor implements ProcessorInterface
return $value;
}
if (is_numeric($value)) {
return $value;
}
if (in_array($value[0], ['+', '-', '@', '='])) {
return "'" . $value;
}
@@ -637,6 +637,10 @@ class PhpSpreadsheetProcessor implements ProcessorInterface
return $value;
}
if (is_numeric($value)) {
return $value;
}
if (in_array($value[0], ['+', '-', '@', '='])) {
return "'" . $value;
}
+10 -3
View File
@@ -146,6 +146,7 @@ class Params
return $obj;
}
/** @noinspection PhpUnused */
public function withFileName(?string $fileName): self
{
$obj = clone $this;
@@ -213,11 +214,17 @@ class Params
*/
public function getSearchParams(): SearchParams
{
if (!$this->searchParams) {
return SearchParams::create();
$searchParams = $this->searchParams ?? SearchParams::create();
if ($searchParams->getSelect() !== null) {
return $searchParams;
}
return $this->searchParams;
if ($this->getAttributeList()) {
$searchParams = $searchParams->withSelect($this->getAttributeList());
}
return $searchParams;
}
/**
+2
View File
@@ -1211,6 +1211,8 @@ class Import
}
}
ksort($o);
return $o;
}
@@ -164,7 +164,6 @@ class CalendarView extends View {
this.date = this.options.date || null;
this.mode = this.options.mode || this.defaultMode;
this.header = ('header' in this.options) ? this.options.header : this.header;
this.slotDuration = this.options.slotDuration || this.slotDuration;
this.setupMode();
@@ -172,13 +171,20 @@ class CalendarView extends View {
this.colors = Espo.Utils
.clone(this.getMetadata().get('clientDefs.Calendar.colors') || this.colors);
this.modeList = this.getMetadata()
.get('clientDefs.Calendar.modeList') || this.modeList;
this.scopeList = this.getConfig()
.get('calendarEntityList') || Espo.Utils.clone(this.scopeList);
this.allDayScopeList = this.getMetadata()
.get('clientDefs.Calendar.allDayScopeList') || this.allDayScopeList;
this.slotDuration = this.options.slotDuration ||
this.getMetadata().get('clientDefs.Calendar.slotDuration') ||
this.slotDuration;
this.colors = {...this.colors, ...this.getHelper().themeManager.getParam('calendarColors')};
this.isCustomViewAvailable = this.getAcl().getPermissionLevel('userPermission') !== 'no';
@@ -718,6 +724,7 @@ class CalendarView extends View {
const height = this.getCalculatedHeight();
this.calendar.setOption('contentHeight', height);
this.calendar.updateSize();
}
afterRender() {
+5 -1
View File
@@ -7,7 +7,11 @@
title="{{translate 'Copy to Clipboard'}}"
><span class="far fa-copy"></span></a>
{{~/if~}}
<a href="{{url}}" target="_blank">{{value}}</a>
<a
href="{{url}}"
target="_blank"
rel="noopener noreferrer"
>{{value}}</a>
{{~else}}
{{#if valueIsSet}}<span class="none-value">{{translate 'None'}}</span>{{else}}
<span class="loading-value">...</span>{{/if}}
+6 -1
View File
@@ -1,3 +1,8 @@
{{#if value}}
<a href="{{url}}" target="_blank" title="{{value}}">{{value}}</a>
<a
href="{{url}}"
target="_blank"
title="{{value}}"
rel="noopener noreferrer"
>{{value}}</a>
{{/if}}
+2 -1
View File
@@ -29,6 +29,7 @@
/** @module ajax */
import $ from 'jquery';
import Utils from 'utils';
let isConfigured = false;
/** @type {number} */
@@ -62,7 +63,7 @@ let onTimeout;
* @property {boolean} [resolveWithXhr] To resolve with `XMLHttpRequest`.
*/
const baseUrl = window.location.origin + window.location.pathname;
const baseUrl = Utils.obtainBaseUrl();
// noinspection JSUnusedGlobalSymbols
/**
+2 -2
View File
@@ -251,7 +251,7 @@ class App {
viewFactory = null
/**
* @type function(string, function(View))
* @type {function(string, function(View))}
* @private
*/
viewLoader = null
@@ -1442,7 +1442,7 @@ class App {
files.push(file);
});
let baseUrl = window.location.origin + window.location.pathname;
let baseUrl = Utils.obtainBaseUrl();
let timestamp = this.loader.getCacheTimestamp();
let promiseList = files.map(file => {
+26 -9
View File
@@ -42,6 +42,7 @@ import $ from 'jquery';
* @param {module:views/site/master} view A master view.
*/
/**
* A controller. To be extended.
*
@@ -446,6 +447,28 @@ class Controller {
});
}
/**
* @param {module:views/site/master} masterView
* @private
*/
_unchainMainView(masterView) {
if (
!masterView.currentViewKey ||
!this.hasStoredMainView(masterView.currentViewKey)
) {
return;
}
const currentMainView = masterView.getView('main');
if (!currentMainView) {
return;
}
currentMainView.propagateEvent('remove', {ignoreCleaning: true});
masterView.unchainView('main');
}
/**
* @typedef {Object} module:controller~mainParams
* @property {boolean} [useStored] Use a stored view if available.
@@ -524,6 +547,8 @@ class Controller {
}
if (mainView) {
this._unchainMainView(masterView);
masterView.assignView('main', mainView, selector)
.then(() => {
dto.isSet = true;
@@ -573,15 +598,7 @@ class Controller {
if (masterView.currentViewKey) {
this.set('storedScrollTop-' + masterView.currentViewKey, $(window).scrollTop());
if (this.hasStoredMainView(masterView.currentViewKey)) {
let mainView = masterView.getView('main');
if (mainView) {
mainView.propagateEvent('remove', {ignoreCleaning: true});
}
masterView.unchainView('main');
}
this._unchainMainView(masterView);
}
masterView.currentViewKey = key;
+17 -17
View File
@@ -129,9 +129,9 @@ class FieldManager {
* @returns {string[]}
*/
getEntityTypeAttributeList(entityType) {
let list = [];
const list = [];
let defs = this.metadata.get('entityDefs.' + entityType + '.fields') || {};
const defs = this.metadata.get('entityDefs.' + entityType + '.fields') || {};
Object.keys(defs).forEach(field => {
this.getAttributeList(defs[field]['type'], field).forEach(attr => {
@@ -154,11 +154,11 @@ class FieldManager {
* @returns {string[]}
*/
getActualAttributeList(fieldType, fieldName) {
let fieldNames = [];
const fieldNames = [];
if (fieldType in this.defs) {
if ('actualFields' in this.defs[fieldType]) {
let actualFields = this.defs[fieldType].actualFields;
const actualFields = this.defs[fieldType].actualFields;
let naming = 'suffix';
@@ -195,11 +195,11 @@ class FieldManager {
* @returns {string[]}
*/
getNotActualAttributeList(fieldType, fieldName) {
let fieldNames = [];
const fieldNames = [];
if (fieldType in this.defs) {
if ('notActualFields' in this.defs[fieldType]) {
let notActualFields = this.defs[fieldType].notActualFields;
const notActualFields = this.defs[fieldType].notActualFields;
let naming = 'suffix';
@@ -236,7 +236,7 @@ class FieldManager {
* @returns {string[]}
*/
getEntityTypeFieldAttributeList(entityType, field) {
let type = this.metadata.get(['entityDefs', entityType, 'fields', field, 'type']);
const type = this.metadata.get(['entityDefs', entityType, 'fields', field, 'type']);
if (!type) {
return [];
@@ -256,7 +256,7 @@ class FieldManager {
* @returns {string[]}
*/
getEntityTypeFieldActualAttributeList(entityType, field) {
let type = this.metadata.get(['entityDefs', entityType, 'fields', field, 'type']);
const type = this.metadata.get(['entityDefs', entityType, 'fields', field, 'type']);
if (!type) {
return [];
@@ -272,22 +272,22 @@ class FieldManager {
* @private
*/
_getEntityTypeFieldAdditionalAttributeList(entityType, field) {
let type = this.metadata.get(['entityDefs', entityType, 'fields', field, 'type']);
const type = this.metadata.get(['entityDefs', entityType, 'fields', field, 'type']);
if (!type) {
return [];
}
let partList = this.metadata
const partList = this.metadata
.get(['entityDefs', entityType, 'fields', field, 'additionalAttributeList']) || [];
if (partList.length === 0) {
return [];
}
let isPrefix = (this.defs[type] || {}).naming === 'prefix';
const isPrefix = (this.defs[type] || {}).naming === 'prefix';
let list = [];
const list = [];
partList.forEach(item => {
if (isPrefix) {
@@ -346,7 +346,7 @@ class FieldManager {
if (typeList) {
list = list.filter(item => {
let type = this.metadata.get(['entityDefs', entityType, 'fields', item, 'type']);
const type = this.metadata.get(['entityDefs', entityType, 'fields', item, 'type']);
return ~typeList.indexOf(type);
});
@@ -359,9 +359,9 @@ class FieldManager {
}
if (o.acl) {
let level = o.acl || 'read';
const level = o.acl || 'read';
let forbiddenEditFieldList = this.acl.getScopeForbiddenFieldList(entityType, level);
const forbiddenEditFieldList = this.acl.getScopeForbiddenFieldList(entityType, level);
list = list.filter(item => {
return !~forbiddenEditFieldList.indexOf(item);
@@ -442,7 +442,7 @@ class FieldManager {
* @returns {boolean}
*/
isEntityTypeFieldAvailable(entityType, field) {
let defs = this.metadata.get(['entityDefs', entityType, 'fields', field]) || {};
const defs = this.metadata.get(['entityDefs', entityType, 'fields', field]) || {};
if (
defs.disabled ||
@@ -451,7 +451,7 @@ class FieldManager {
return false;
}
let aclDefs = this.metadata.get(['entityAcl', entityType, 'fields', field]) || {};
const aclDefs = this.metadata.get(['entityAcl', entityType, 'fields', field]) || {};
if (
aclDefs.onlyAdmin ||
+46 -38
View File
@@ -28,7 +28,7 @@
(function () {
let root = this;
const root = this;
if (!root.Espo) {
root.Espo = {};
@@ -99,7 +99,15 @@
this._internalModuleMap = {};
this._isDeveloperMode = false;
this._baseUrl = window.location.origin + window.location.pathname;
let baseUrl = window.location.origin + window.location.pathname;
if (baseUrl.slice(-1) !== '/') {
baseUrl = window.location.pathname.includes('.') ?
baseUrl.slice(0, baseUrl.lastIndexOf('/')) + '/' :
baseUrl + '/';
}
this._baseUrl = baseUrl;
this._isDeveloperModeIsSet = false;
this._basePathIsSet = false;
@@ -207,7 +215,7 @@
this._definedMap[id] = value;
if (id.slice(0, 4) === 'lib!') {
let libName = id.slice(4);
const libName = id.slice(4);
const libsData = this._libsConfig[libName];
@@ -263,7 +271,7 @@
/** @var {?string} */
let module = null;
let colonIndex = id.indexOf(':');
const colonIndex = id.indexOf(':');
if (colonIndex > 0) {
module = id.substring(0, colonIndex);
@@ -274,13 +282,13 @@
if (!module && id.indexOf('lib!') === 0) {
noStrictMode = true;
let realName = id.substring(4);
const realName = id.substring(4);
let libsData = this._libsConfig[realName] || {};
const libsData = this._libsConfig[realName] || {};
if (!this._isDeveloperMode) {
if (libsData.sourceMap) {
let realPath = path.split('?')[0];
const realPath = path.split('?')[0];
script += `\n//# sourceMappingURL=${this._baseUrl + realPath}.map`;
}
@@ -433,7 +441,7 @@
if (totalCount) {
let readyCount = 0;
let loaded = {};
const loaded = {};
list.forEach(depId => {
this._load(depId, c => {
@@ -476,7 +484,7 @@
* @private
*/
_normalizeIdPath(id, subjectId) {
if (id.at(0) !== '.') {
if (id.charAt(0) !== '.') {
return id;
}
@@ -486,17 +494,17 @@
let outputPath = id;
let dirParts = subjectId.split('/').slice(0, -1);
const dirParts = subjectId.split('/').slice(0, -1);
if (id.slice(0, 2) === './') {
outputPath = dirParts.join('/') + '/' + id.slice(2);
}
let parts = outputPath.split('/');
const parts = outputPath.split('/');
let up = 0;
for (let part of parts) {
for (const part of parts) {
if (part === '..') {
up++;
@@ -527,7 +535,7 @@
return id;
}
let [mod, part] = id.split(':');
const [mod, part] = id.split(':');
return `modules/${mod}/${part}`;
}
@@ -548,9 +556,9 @@
if (!!/[A-Z]/.exec(id[0])) {
if (id.indexOf(':') !== -1) {
let arr = id.split(':');
let modulePart = arr[0];
let namePart = arr[1];
const arr = id.split(':');
const modulePart = arr[0];
const namePart = arr[1];
return this._convertCamelCaseToHyphen(modulePart) + ':' +
this._convertCamelCaseToHyphen(namePart)
@@ -564,10 +572,10 @@
if (id.startsWith('modules/')) {
id = id.slice(8);
let index = id.indexOf('/');
const index = id.indexOf('/');
if (index > 0) {
let mod = id.slice(0, index);
const mod = id.slice(0, index);
id = id.slice(index + 1);
return mod + ':' + id;
@@ -688,7 +696,7 @@
throw new Error("Can't load with empty module ID.");
}
let value = this._get(id);
const value = this._get(id);
if (typeof value !== 'undefined') {
callback(value);
@@ -696,7 +704,7 @@
return;
}
let restoredId = this._restoreId(id);
const restoredId = this._restoreId(id);
if (restoredId in this._bundleMapping) {
let bundleName = this._bundleMapping[restoredId];
@@ -727,7 +735,7 @@
}
/** @type {Loader~dto} */
let dto = {
const dto = {
id: id,
type: type,
dataType: dataType,
@@ -752,12 +760,12 @@
if (this._cacheTimestamp) {
useCache = true;
let sep = (path.indexOf('?') > -1) ? '&' : '?';
const sep = (path.indexOf('?') > -1) ? '&' : '?';
path += sep + 'r=' + this._cacheTimestamp;
}
let url = this._basePath + path;
const url = this._basePath + path;
dto.path = path;
dto.url = url;
@@ -793,7 +801,7 @@
return this._bundlePromiseMap[name];
}
let dependencies = this._bundleDependenciesMap[name] || [];
const dependencies = this._bundleDependenciesMap[name] || [];
if (!dependencies.length) {
this._bundlePromiseMap[name] = this._addBundle(name);
@@ -838,7 +846,7 @@
src = this._basePath + src;
let scriptEl = document.createElement('script');
const scriptEl = document.createElement('script');
scriptEl.setAttribute('type', 'text/javascript')
scriptEl.setAttribute('src', src);
@@ -865,7 +873,7 @@
from = root;
}
else {
for (let item of exportsTo.split('.')) {
for (const item of exportsTo.split('.')) {
from = from[item];
if (typeof from === 'undefined') {
@@ -886,12 +894,12 @@
* @param {Loader~dto} dto
*/
_processRequest(dto) {
let url = dto.url;
let errorCallback = dto.errorCallback;
let path = dto.path;
let useCache = dto.useCache;
const url = dto.url;
const errorCallback = dto.errorCallback;
const path = dto.path;
const useCache = dto.useCache;
let urlObj = new URL(this._baseUrl + url);
const urlObj = new URL(this._baseUrl + url);
if (!useCache) {
urlObj.searchParams.append('_', Date.now().toString())
@@ -934,13 +942,13 @@
* @param {string} text
*/
_handleResponseText(dto, text) {
let id = dto.id;
let callback = dto.callback;
let type = dto.type;
let dataType = dto.dataType;
let exportsAs = dto.exportsAs;
let exportsTo = dto.exportsTo;
let suppressAmd = dto.suppressAmd;
const id = dto.id;
const callback = dto.callback;
const type = dto.type;
const dataType = dto.dataType;
const exportsAs = dto.exportsAs;
const exportsTo = dto.exportsTo;
const suppressAmd = dto.suppressAmd;
this._addLoadCallback(id, callback);
+66 -36
View File
@@ -886,6 +886,7 @@ Espo.Ui = {
* @property {'manual'|'click'|'hover'|'focus'} [trigger='manual'] A trigger type.
* @property {boolean} [noToggleInit=false] Skip init toggle on click.
* @property {boolean} [preventDestroyOnRender=false] Don't destroy on re-render.
* @property {boolean} [noHideOnOutsideClick=false] Don't hide on clicking outside.
* @property {function(): void} [onShow] On-show callback.
* @property {function(): void} [onHide] On-hide callback.
*/
@@ -896,18 +897,18 @@ Espo.Ui = {
* @param {Element|JQuery} element An element.
* @param {Espo.Ui~PopoverOptions} o Options.
* @param {module:view} [view] A view.
* @return {{hide: function(), destroy: function(), show: function(), detach: function()}}
*/
popover: function (element, o, view) {
let $el = $(element);
let $body = $('body')
let content = o.content || Handlebars.Utils.escapeExpression(o.text || '');
const $el = $(element);
const $body = $('body');
const content = o.content || Handlebars.Utils.escapeExpression(o.text || '');
let isShown = false;
let container = o.container;
if (!container) {
let $modalBody = $el.closest('.modal-body');
const $modalBody = $el.closest('.modal-body');
container = $modalBody.length ? $modalBody : 'body';
}
@@ -929,25 +930,27 @@ Espo.Ui = {
return;
}
$body.off('click.popover-' + view.cid);
$body.on('click.popover-' + view.cid, e => {
if ($(e.target).closest('.popover-content').get(0)) {
return;
}
if ($.contains($el.get(0), e.target)) {
return;
}
if ($el.get(0) === e.target) {
return;
}
if (view && !o.noHideOnOutsideClick) {
$body.off('click.popover-' + view.cid);
// noinspection JSUnresolvedReference
$el.popover('hide');
});
$body.on('click.popover-' + view.cid, e => {
if ($(e.target).closest('.popover-content').get(0)) {
return;
}
if ($.contains($el.get(0), e.target)) {
return;
}
if ($el.get(0) === e.target) {
return;
}
$body.off('click.popover-' + view.cid);
// noinspection JSUnresolvedReference
$el.popover('hide');
});
}
if (o.onShow) {
o.onShow();
@@ -968,26 +971,46 @@ Espo.Ui = {
});
}
if (view) {
let hide = () => {
if (!isShown) {
return;
}
let isDetached = false;
// noinspection JSUnresolvedReference
$el.popover('hide');
};
let destroy = () => {
// noinspection JSUnresolvedReference
$el.popover('destroy');
const detach = () => {
if (view) {
$body.off('click.popover-' + view.cid);
view.off('remove', destroy);
view.off('render', destroy);
view.off('render', hide);
};
}
isDetached = true;
};
const destroy = () => {
if (isDetached) {
return;
}
// noinspection JSUnresolvedReference
$el.popover('destroy');
detach();
};
const hide = () => {
if (!isShown) {
return;
}
// noinspection JSUnresolvedReference
$el.popover('hide');
};
const show = () => {
// noinspection JSUnresolvedReference
$el.popover('show');
};
if (view) {
view.once('remove', destroy);
if (!o.preventDestroyOnRender) {
@@ -998,6 +1021,13 @@ Espo.Ui = {
view.on('render', hide);
}
}
return {
hide: () => hide(),
destroy: () => destroy(),
show: () => show(),
detach: () => detach(),
};
},
/**
+44 -44
View File
@@ -74,21 +74,22 @@ const Select = {
* @param {Element|JQuery} element An element.
* @param {module:ui/select~Options} [options] Options.
*/
init: function (element, options) {
let $el = $(element);
init: function (element, options = {}) {
const score = options.score;
const $el = $(element);
options = Select.applyDefaultOptions(options || {});
let plugins = [];
const plugins = [];
Select.loadEspoSelectPlugin();
plugins.push('espo_select');
let itemClasses = {};
const itemClasses = {};
let allowedValues = $el.children().toArray().map(item => {
let value = item.getAttributeNode('value').value;
const allowedValues = $el.children().toArray().map(item => {
const value = item.getAttributeNode('value').value;
if (item.classList) {
itemClasses[value] = item.classList.toString();
@@ -99,13 +100,13 @@ const Select = {
let $relativeParent = null;
let $modalBody = $el.closest('.modal-body');
const $modalBody = $el.closest('.modal-body');
if ($modalBody.length) {
$relativeParent = $modalBody;
}
let selectizeOptions = {
const selectizeOptions = {
sortField: [{field: options.sortBy, direction: options.sortDirection}],
load: options.load,
loadThrottle: 1,
@@ -125,7 +126,7 @@ const Select = {
.get(0).outerHTML;
},
option: function (data) {
let $div = $('<div>')
const $div = $('<div>')
.addClass('option')
.addClass(data.value === '' ? 'selectize-dropdown-emptyoptionlabel' : '')
.addClass(itemClasses[data.value] || '')
@@ -153,7 +154,7 @@ const Select = {
if (!options.matchAnyWord) {
/** @this Selectize */
selectizeOptions.score = function (search) {
let score = this.getScoreFunction(search);
const score = this.getScoreFunction(search);
search = search.toLowerCase();
@@ -170,12 +171,12 @@ const Select = {
if (options.matchAnyWord) {
/** @this Selectize */
selectizeOptions.score = function (search) {
let score = this.getScoreFunction(search);
const score = this.getScoreFunction(search);
search = search.toLowerCase();
return function (item) {
let text = item.text.toLowerCase();
const text = item.text.toLowerCase();
if (
!text.split(' ').find(item => item.startsWith(search)) &&
@@ -190,7 +191,6 @@ const Select = {
}
if (options.score) {
let score = options.score;
selectizeOptions.score = function (search) {
return function (item) {
@@ -209,7 +209,7 @@ const Select = {
* @param {{noTrigger?: boolean}} [options] Options.
*/
focus: function (element, options) {
let $el = $(element);
const $el = $(element);
options = options || {};
@@ -220,7 +220,7 @@ const Select = {
return;
}
let selectize = $el[0].selectize;
const selectize = $el[0].selectize;
if (options.noTrigger) {
selectize.focusNoTrigger = true;
@@ -240,9 +240,9 @@ const Select = {
* @param {{value: string, text: string}[]} options Options.
*/
setOptions: function (element, options) {
let $el = $(element);
const $el = $(element);
let selectize = $el.get(0).selectize;
const selectize = $el.get(0).selectize;
selectize.clearOptions(true);
selectize.load(callback => {
@@ -264,7 +264,7 @@ const Select = {
* @param {string} value A value.
*/
setValue: function ($el, value) {
let selectize = $el.get(0).selectize;
const selectize = $el.get(0).selectize;
selectize.setValue(value, true);
},
@@ -290,14 +290,14 @@ const Select = {
applyDefaultOptions: function (options) {
options = Espo.Utils.clone(options);
let defaults = {
const defaults = {
selectOnTab: false,
matchAnyWord: false,
sortBy: '$order',
sortDirection: 'asc',
};
for (let key in defaults) {
for (const key in defaults) {
if (key in options) {
continue;
}
@@ -320,10 +320,10 @@ const Select = {
const KEY_BACKSPACE = 8;
Selectize.define('espo_select', function () {
let self = this;
const self = this;
this.setup = (function () {
let original = self.setup;
const original = self.setup;
return function () {
original.apply(this, arguments);
@@ -367,7 +367,7 @@ const Select = {
})();*/
this.refreshOptions = (function () {
let original = self.refreshOptions;
const original = self.refreshOptions;
return function () {
if (self.focusNoTrigger) {
@@ -380,7 +380,7 @@ const Select = {
})();
this.blur = (function () {
let original = self.blur;
const original = self.blur;
return function () {
// Prevent closing on mouse down.
@@ -393,7 +393,7 @@ const Select = {
})();
this.close = (function () {
let original = self.close;
const original = self.close;
return function () {
if (self.preventClose) {
@@ -405,7 +405,7 @@ const Select = {
})();
this.onOptionSelect = (function () {
let original = self.onOptionSelect;
const original = self.onOptionSelect;
return function (e) {
if (e.type === 'mousedown' || e.type === 'click') {
@@ -428,10 +428,10 @@ const Select = {
})();
this.open = (function() {
let original = self.open;
const original = self.open;
return function () {
let toProcess = !(self.isLocked || self.isOpen);
const toProcess = !(self.isLocked || self.isOpen);
original.apply(this, arguments);
@@ -439,8 +439,8 @@ const Select = {
return;
}
let $dropdownContent = self.$dropdown.children().first();
let $selected = $dropdownContent.find('.selected');
const $dropdownContent = self.$dropdown.children().first();
const $selected = $dropdownContent.find('.selected');
if (!$selected.length) {
return;
@@ -456,7 +456,7 @@ const Select = {
})();
this.onMouseDown = (function() {
let original = self.onMouseDown;
const original = self.onMouseDown;
return function (e) {
// Prevent flicking when clicking on input.
@@ -473,7 +473,7 @@ const Select = {
})();
this.onFocus = (function() {
let original = self.onFocus;
const original = self.onFocus;
return function (e) {
if (self.preventReOpenOnFocus) {
@@ -504,7 +504,7 @@ const Select = {
};
this.onBlur = (function() {
let original = self.onBlur;
const original = self.onBlur;
return function () {
// Prevent closing on mouse down.
@@ -521,7 +521,7 @@ const Select = {
})();
this.onKeyDown = (function() {
let original = self.onKeyDown;
const original = self.onKeyDown;
return function (e) {
if (IS_MAC ? e.metaKey : e.ctrlKey) {
@@ -558,7 +558,7 @@ const Select = {
RegExp(/^\p{L}/, 'u').test(e.key) // is letter
)
) {
let keyCode = e.keyCode;
const keyCode = e.keyCode;
e.keyCode = KEY_BACKSPACE;
self.deleteSelection(e);
e.keyCode = keyCode;
@@ -578,30 +578,30 @@ const Select = {
};
return function() {
let $control = self.$control;
const $control = self.$control;
let offset = this.settings.dropdownParent === 'body' ?
const offset = this.settings.dropdownParent === 'body' ?
$control.offset() :
$control.position();
offset.top += $control.outerHeight(true);
let dropdownHeight = self.$dropdown.prop('scrollHeight') + 5;
let controlPosTop = self.$control.get(0).getBoundingClientRect().top;
let wrapperHeight = self.$wrapper.height();
const dropdownHeight = self.$dropdown.prop('scrollHeight') + 5;
const controlPosTop = self.$control.get(0).getBoundingClientRect().top;
const wrapperHeight = self.$wrapper.height();
let controlPosBottom = self.$control.get(0).getBoundingClientRect().bottom;
const controlPosBottom = self.$control.get(0).getBoundingClientRect().bottom;
let boundaryTop = !this.settings.$relativeParent ? 0 :
const boundaryTop = !this.settings.$relativeParent ? 0 :
this.settings.$relativeParent.get(0).getBoundingClientRect().top;
let position =
const position =
controlPosTop + dropdownHeight + wrapperHeight > window.innerHeight &&
controlPosBottom - dropdownHeight - wrapperHeight >= boundaryTop ?
POSITION.top :
POSITION.bottom;
let styles = {
const styles = {
width: $control.outerWidth(),
left: offset.left,
};
+17
View File
@@ -662,6 +662,23 @@ Espo.Utils = {
generateId: function () {
return (Math.floor(Math.random() * 10000001)).toString()
},
/**
* Not to be used in custom code. Can be removed in future versions.
* @internal
* @return {string}
*/
obtainBaseUrl: function () {
let baseUrl = window.location.origin + window.location.pathname;
if (baseUrl.slice(-1) !== '/') {
baseUrl = window.location.pathname.includes('.') ?
baseUrl.slice(0, baseUrl.lastIndexOf('/')) + '/' :
baseUrl + '/';
}
return baseUrl;
}
};
const keyMap = {
+58 -56
View File
@@ -75,13 +75,16 @@ class ViewHelper {
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
if (node instanceof HTMLAnchorElement) {
const href = node.getAttribute('href');
if (href && !href.startsWith('#')) {
node.setAttribute('rel', 'noopener noreferrer');
}
if (node.targetBlank) {
node.setAttribute('target', '_blank');
node.setAttribute('rel', 'noopener noreferrer');
}
else {
node.removeAttribute('rel');
}
}
});
}
@@ -316,7 +319,7 @@ class ViewHelper {
});
Handlebars.registerHelper('ifAttrNotEmpty', function (model, attr, options) {
let value = model.get(attr);
const value = model.get(attr);
if (value !== null && typeof value !== 'undefined') {
return options.fn(this);
@@ -340,8 +343,8 @@ class ViewHelper {
Handlebars.registerHelper('length', arr => arr.length);
Handlebars.registerHelper('translate', (name, options) => {
let scope = options.hash.scope || null;
let category = options.hash.category || null;
const scope = options.hash.scope || null;
const category = options.hash.category || null;
if (name === 'null') {
return '';
@@ -351,16 +354,16 @@ class ViewHelper {
});
Handlebars.registerHelper('dropdownItem', (name, options) => {
let scope = options.hash.scope || null;
let label = options.hash.label;
let labelTranslation = options.hash.labelTranslation;
let data = options.hash.data;
let hidden = options.hash.hidden;
let disabled = options.hash.disabled;
let title = options.hash.title;
let link = options.hash.link;
let action = options.hash.action || name;
let iconHtml = options.hash.iconHtml;
const scope = options.hash.scope || null;
const label = options.hash.label;
const labelTranslation = options.hash.labelTranslation;
const data = options.hash.data;
const hidden = options.hash.hidden;
const disabled = options.hash.disabled;
const title = options.hash.title;
const link = options.hash.link;
const action = options.hash.action || name;
const iconHtml = options.hash.iconHtml;
const iconClass = options.hash.iconClass;
let html =
@@ -385,11 +388,11 @@ class ViewHelper {
html = iconHtml + ' ' + html;
}
let $li = $('<li>')
const $li = $('<li>')
.addClass(hidden ? 'hidden' : '')
.addClass(disabled ? 'disabled' : '');
let $a = $('<a>')
const $a = $('<a>')
.attr('role', 'button')
.attr('tabindex', '0')
.attr('data-name', name)
@@ -408,7 +411,7 @@ class ViewHelper {
$a.attr('role', 'button');
if (data) {
for (let key in data) {
for (const key in data) {
$a.attr('data-' + Espo.Utils.camelCaseToHyphen(key), data[key]);
}
}
@@ -425,12 +428,12 @@ class ViewHelper {
});
Handlebars.registerHelper('button', (name, options) => {
let style = options.hash.style || 'default';
let scope = options.hash.scope || null;
let label = options.hash.label || name;
let labelTranslation = options.hash.labelTranslation;
let link = options.hash.link;
let iconHtml = options.hash.iconHtml;
const style = options.hash.style || 'default';
const scope = options.hash.scope || null;
const label = options.hash.label || name;
const labelTranslation = options.hash.labelTranslation;
const link = options.hash.link;
const iconHtml = options.hash.iconHtml;
const iconClass = options.hash.iconClass;
let html =
@@ -455,9 +458,9 @@ class ViewHelper {
html = iconHtml + ' ' + html;
}
let tag = link ? '<a>' : '<button>';
const tag = link ? '<a>' : '<button>';
let $button = $(tag)
const $button = $(tag)
.addClass('btn action')
.addClass(options.hash.className || '')
.addClass(options.hash.hidden ? 'hidden' : '')
@@ -503,8 +506,8 @@ class ViewHelper {
});
Handlebars.registerHelper('translateOption', (name, options) => {
let scope = options.hash.scope || null;
let field = options.hash.field || null;
const scope = options.hash.scope || null;
const field = options.hash.field || null;
if (!field) {
return '';
@@ -536,9 +539,9 @@ class ViewHelper {
let html = '';
let multiple = (Object.prototype.toString.call(value) === '[object Array]');
const multiple = (Object.prototype.toString.call(value) === '[object Array]');
let checkOption = name => {
const checkOption = name => {
if (multiple) {
return value.indexOf(name) !== -1;
}
@@ -548,10 +551,10 @@ class ViewHelper {
options.hash = /** @type {Object.<string, *>} */ options.hash || {};
let scope = options.hash.scope || false;
let category = options.hash.category || false;
let field = options.hash.field || false;
let styleMap = options.hash.styleMap || {};
const scope = options.hash.scope || false;
const category = options.hash.category || false;
const field = options.hash.field || false;
const styleMap = options.hash.styleMap || {};
if (!multiple && options.hash.includeMissingOption && (value || value === '')) {
if (!~list.indexOf(value)) {
@@ -579,7 +582,7 @@ class ViewHelper {
}
}
let translate = name => {
const translate = name => {
if (!category) {
return translationHash[name] || name;
}
@@ -587,14 +590,14 @@ class ViewHelper {
return this.language.translate(name, category, /** @type {string} */scope);
};
for (let key in list) {
let value = list[key];
let label = translate(value);
for (const key in list) {
const value = list[key];
const label = translate(value);
let $option =
const $option =
$('<option>')
.attr('value', value)
.addClass(styleMap[value] ? 'text-' + styleMap[value]: '')
.addClass(styleMap[value] ? 'text-' + styleMap[value] : '')
.text(label);
if (checkOption(list[key])) {
@@ -646,9 +649,9 @@ class ViewHelper {
return '';
}
let t = this.cache ? this.cache.get('app', 'timestamp') : Date.now();
const t = this.cache ? this.cache.get('app', 'timestamp') : Date.now();
let basePath = this.basePath || '';
const basePath = this.basePath || '';
size = size || 'small';
width = width || 16;
@@ -729,12 +732,12 @@ class ViewHelper {
return '';
}
let color = this.metadata.get(['clientDefs', scope, 'color']);
const color = this.metadata.get(['clientDefs', scope, 'color']);
let html = '';
if (color) {
let $span = $('<span class="color-icon fas fa-square">');
const $span = $('<span class="color-icon fas fa-square">');
$span.css('color', color);
@@ -874,19 +877,19 @@ class ViewHelper {
* @returns {number}
*/
calculateContentContainerHeight($el) {
let smallScreenWidth = this.themeManager.getParam('screenWidthXs');
const smallScreenWidth = this.themeManager.getParam('screenWidthXs');
let $window = $(window);
const $window = $(window);
let footerHeight = $('#footer').height() || 26;
const footerHeight = $('#footer').height() || 26;
let top = 0;
let element = $el.get(0);
const element = $el.get(0);
if (element) {
top = element.getBoundingClientRect().top;
if ($window.width() < smallScreenWidth) {
let $navbarCollapse = $('#navbar .navbar-body');
const $navbarCollapse = $('#navbar .navbar-body');
if ($navbarCollapse.hasClass('in') || $navbarCollapse.hasClass('collapsing')) {
top -= $navbarCollapse.height();
@@ -894,7 +897,7 @@ class ViewHelper {
}
}
let spaceHeight = top + footerHeight;
const spaceHeight = top + footerHeight;
return $window.height() - spaceHeight - 20;
}
@@ -934,13 +937,12 @@ class ViewHelper {
* @name ViewHelper~Handler#process
* @param {module:view} [view] Deprecated.
*/
const promiseList = [];
let promiseList = [];
for (let id of handlerIdList) {
let promise = new Promise(resolve => {
for (const id of handlerIdList) {
const promise = new Promise(resolve => {
Espo.loader.require(id, /** typeof ViewHelper~Handler */Handler => {
let result = (new Handler(view)).process(view);
const result = (new Handler(view)).process(view);
if (result && Object.prototype.toString.call(result) === '[object Promise]') {
result.then(() => resolve());
+2
View File
@@ -103,6 +103,8 @@ class ViewRecordHelper {
this.fieldStateMap[field] = this.fieldStateMap[field] || {};
this.fieldStateMap[field][name] = value;
this.trigger('field-change');
}
/**
+17
View File
@@ -71,6 +71,23 @@ class View extends BullView {
* @private
*/
/**
* When the view is ready. Can be useful to prevent race condition when re-initialization is needed
* in-between initialization and render.
*
* @return Promise
* @todo Move to Bull.View.
*/
whenReady() {
if (this.isReady) {
return Promise.resolve();
}
return new Promise(resolve => {
this.once('ready', () => resolve());
});
}
/**
* Add a DOM click event handler for a target defined by `data-action="{name}"` attribute.
*
+42 -36
View File
@@ -52,8 +52,8 @@ class EntityManagerEditView extends View {
}
setupData() {
let scope = this.scope;
let templateType = this.getMetadata().get(['scopes', scope, 'type']) || null;
const scope = this.scope;
const templateType = this.getMetadata().get(['scopes', scope, 'type']) || null;
this.hasStreamField = true;
@@ -117,20 +117,20 @@ class EntityManagerEditView extends View {
this.getMetadata().get(['scopes', scope, 'kanbanStatusIgnoreList']) || []
);
for (let param in this.additionalParams) {
for (const param in this.additionalParams) {
/** @type {{fieldDefs: Object, location?: string}} */
let defs = this.additionalParams[param];
let location = defs.location || this.defaultParamLocation;
let defaultValue = defs.fieldDefs.type === 'bool' ? false : null;
const defs = this.additionalParams[param];
const location = defs.location || this.defaultParamLocation;
const defaultValue = defs.fieldDefs.type === 'bool' ? false : null;
let value = this.getMetadata().get([location, scope, param]) || defaultValue;
const value = this.getMetadata().get([location, scope, param]) || defaultValue;
this.model.set(param, value);
}
}
if (scope) {
let fieldDefs = this.getMetadata().get('entityDefs.' + scope + '.fields') || {};
const fieldDefs = this.getMetadata().get('entityDefs.' + scope + '.fields') || {};
this.orderableFieldList = Object.keys(fieldDefs)
.filter(item => {
@@ -161,10 +161,10 @@ class EntityManagerEditView extends View {
this.filtersOptionList.forEach(item => {
if (~item.indexOf('.')) {
let link = item.split('.')[0];
let foreignField = item.split('.')[1];
const link = item.split('.')[0];
const foreignField = item.split('.')[1];
let foreignEntityType = this.getMetadata()
const foreignEntityType = this.getMetadata()
.get(['entityDefs', scope, 'links', link, 'entity']);
this.textFilterFieldsTranslation[item] =
@@ -293,22 +293,22 @@ class EntityManagerEditView extends View {
];
if (this.scope) {
let rows1 = [];
let rows2 = [];
const rows1 = [];
const rows2 = [];
let paramList1 = Object.keys(this.additionalParams)
const paramList1 = Object.keys(this.additionalParams)
.filter(item => !!this.getMetadata().get(['app', 'entityManagerParams', 'Global', item]));
let paramList2 = Object.keys(this.additionalParams)
const paramList2 = Object.keys(this.additionalParams)
.filter(item => !paramList1.includes(item));
let add = function (rows, list) {
const add = function (rows, list) {
list.forEach((param, i) => {
if (i % 2 === 0) {
rows.push([]);
}
let row = rows[rows.length - 1];
const row = rows[rows.length - 1];
row.push({name: param});
@@ -345,7 +345,13 @@ class EntityManagerEditView extends View {
this.isCustom = this.getMetadata().get(['scopes', scope, 'isCustom'])
}
if (this.scope && !this.getMetadata().get(`scopes.${scope}.customizable`)) {
if (
this.scope &&
(
!this.getMetadata().get(`scopes.${scope}.customizable`) ||
this.getMetadata().get(`scopes.${scope}.entityManager.edit`) === false
)
) {
throw new Espo.Exceptions.NotFound("The entity type is not customizable.");
}
@@ -358,9 +364,9 @@ class EntityManagerEditView extends View {
}
setupDefs() {
let scope = this.scope;
const scope = this.scope;
let defs = {
const defs = {
fields: {
type: {
type: 'enum',
@@ -443,7 +449,7 @@ class EntityManagerEditView extends View {
defs.fields.statusField.readOnly = true;
}
for (let param in this.additionalParams) {
for (const param in this.additionalParams) {
defs.fields[param] = this.additionalParams[param].fieldDefs;
}
@@ -544,7 +550,7 @@ class EntityManagerEditView extends View {
fieldList.push('color');
}
let fetchedAttributes = Espo.Utils.cloneDeep(this.model.fetchedAttributes) || {};
const fetchedAttributes = Espo.Utils.cloneDeep(this.model.fetchedAttributes) || {};
let notValid = false;
@@ -584,9 +590,9 @@ class EntityManagerEditView extends View {
url = 'EntityManager/action/updateEntity';
}
let name = this.model.get('name');
const name = this.model.get('name');
let data = {
const data = {
name: name,
labelSingular: this.model.get('labelSingular'),
labelPlural: this.model.get('labelPlural'),
@@ -614,8 +620,8 @@ class EntityManagerEditView extends View {
data.kanbanViewMode = this.model.get('kanbanViewMode');
data.kanbanStatusIgnoreList = this.model.get('kanbanStatusIgnoreList');
for (let param in this.additionalParams) {
let type = this.additionalParams[param].fieldDefs.type;
for (const param in this.additionalParams) {
const type = this.additionalParams[param].fieldDefs.type;
this.getFieldManager().getAttributeList(type, param).forEach(attribute => {
data[attribute] = this.model.get(attribute);
@@ -650,7 +656,7 @@ class EntityManagerEditView extends View {
])
)
.then(() => {
let rebuildRequired =
const rebuildRequired =
data.fullTextSearch && !fetchedAttributes.fullTextSearch;
this.broadcastUpdate();
@@ -735,10 +741,10 @@ class EntityManagerEditView extends View {
}
getTextFiltersOptionList(scope) {
let fieldDefs = this.getMetadata().get(['entityDefs', scope, 'fields']) || {};
const fieldDefs = this.getMetadata().get(['entityDefs', scope, 'fields']) || {};
let filtersOptionList = Object.keys(fieldDefs).filter(item => {
let fieldType = fieldDefs[item].type;
const filtersOptionList = Object.keys(fieldDefs).filter(item => {
const fieldType = fieldDefs[item].type;
if (!this.getMetadata().get(['fields', fieldType, 'textFilter'])) {
return false;
@@ -757,20 +763,20 @@ class EntityManagerEditView extends View {
filtersOptionList.unshift('id');
let linkList = Object.keys(this.getMetadata().get(['entityDefs', scope, 'links']) || {});
const linkList = Object.keys(this.getMetadata().get(['entityDefs', scope, 'links']) || {});
linkList.sort((v1, v2) => {
return this.translate(v1, 'links', scope).localeCompare(this.translate(v2, 'links', scope));
});
linkList.forEach((link) => {
let linkType = this.getMetadata().get(['entityDefs', scope, 'links', link, 'type']);
const linkType = this.getMetadata().get(['entityDefs', scope, 'links', link, 'type']);
if (linkType !== 'belongsTo') {
return;
}
let foreignEntityType = this.getMetadata().get(['entityDefs', scope, 'links', link, 'entity']);
const foreignEntityType = this.getMetadata().get(['entityDefs', scope, 'links', link, 'entity']);
if (!foreignEntityType) {
return;
@@ -780,9 +786,9 @@ class EntityManagerEditView extends View {
return;
}
let fields = this.getMetadata().get(['entityDefs', foreignEntityType, 'fields']) || {};
const fields = this.getMetadata().get(['entityDefs', foreignEntityType, 'fields']) || {};
let fieldList = Object.keys(fields);
const fieldList = Object.keys(fields);
fieldList.sort((v1, v2) => {
return this.translate(v1, 'fields', foreignEntityType)
@@ -791,7 +797,7 @@ class EntityManagerEditView extends View {
fieldList
.filter(item => {
let fieldType = this.getMetadata()
const fieldType = this.getMetadata()
.get(['entityDefs', foreignEntityType, 'fields', item, 'type']);
if (!this.getMetadata().get(['fields', fieldType, 'textFilter'])) {
@@ -62,7 +62,10 @@ class EntityManagerFormulaView extends View {
}
if (!this.getMetadata().get(['scopes', this.scope, 'customizable'])) {
if (
!this.getMetadata().get(['scopes', this.scope, 'customizable']) ||
this.getMetadata().get(`scopes.${this.scope}.entityManager.formula`) === false
) {
throw new Espo.Exceptions.NotFound("Entity type is not customizable.");
}
+8 -1
View File
@@ -398,7 +398,14 @@ define('views/admin/field-manager/edit', ['view', 'model'], function (Dep, Model
this.isNew = !this.field;
if (!this.getMetadata().get(['scopes', this.scope, 'customizable'])) {
if (
!this.getMetadata().get(['scopes', this.scope, 'customizable']) ||
this.getMetadata().get(`scopes.${this.scope}.entityManager.fields`) === false ||
(
this.field &&
this.getMetadata().get(`entityDefs.${this.scope}.fields.${this.field}.customizationDisabled`)
)
) {
Espo.Ui.notify(false);
throw new Espo.Exceptions.NotFound("Entity type is not customizable.");
+3 -1
View File
@@ -55,7 +55,9 @@ define('views/admin/field-manager/list', ['view'], function (Dep) {
setup: function () {
this.scope = this.options.scope;
this.isCustomizable = !!this.getMetadata().get(`scopes.${this.scope}.customizable`);
this.isCustomizable =
!!this.getMetadata().get(`scopes.${this.scope}.customizable`) &&
this.getMetadata().get(`scopes.${this.scope}.entityManager.fields`) !== false;
this.hasAddField = true;
+3 -1
View File
@@ -117,7 +117,9 @@ class LinkManagerIndexView extends View {
setupLinkData() {
this.linkDataList = [];
this.isCustomizable = !!this.getMetadata().get(`scopes.${this.scope}.customizable`);
this.isCustomizable =
!!this.getMetadata().get(`scopes.${this.scope}.customizable`) &&
this.getMetadata().get(`scopes.${this.scope}.entityManager.relationships`) !== false;
const links = this.getMetadata().get('entityDefs.' + this.scope + '.links');
+7 -1
View File
@@ -527,7 +527,13 @@ class EmailDetailRecordView extends DetailRecordView {
let msg = this.translate('sendingFailed', 'strings', 'Email');
if (data.message) {
msg += ': ' + data.message;
let part = data.message;
if (this.getLanguage().has(part, 'messages', 'Email')) {
part = this.translate(part, 'messages', 'Email');
}
msg += ': ' + part;
}
Espo.Ui.error(msg, true);
+7 -1
View File
@@ -64,7 +64,13 @@ define('views/export/modals/export', ['views/modal', 'model'], function (Dep, Mo
this.scope = this.options.scope;
if (this.options.fieldList) {
this.model.set('fieldList', this.options.fieldList);
const fieldList = this.options.fieldList
.filter(field => {
return !this.getMetadata()
.get(`entityDefs.${this.scope}.fields.${field}.exportDisabled`);
});
this.model.set('fieldList', fieldList);
this.model.set('exportAllFields', false);
} else {
this.model.set('exportAllFields', true);
+41 -36
View File
@@ -885,6 +885,18 @@ class BaseFieldView extends View {
}
};
if (!this.isReady) {
this.once('ready', () => {
const promise = this.prepare();
if (promise) {
promise.then(() => reRender());
}
});
return;
}
let promise = this.prepare();
if (promise) {
@@ -1431,8 +1443,10 @@ class BaseFieldView extends View {
*
* @param {string} message A message.
* @param {string|JQuery|Element} [target] A target element or selector.
* @param {module:view} [view] A child view that contains the target. The closest view should to passed.
* Should be omitted if there is no child views or the target is not rendered by a child view.
*/
showValidationMessage(message, target) {
showValidationMessage(message, target, view) {
if (this.validationMessageSuspended) {
return;
}
@@ -1456,7 +1470,7 @@ class BaseFieldView extends View {
}
if ($el.length) {
let rect = $el.get(0).getBoundingClientRect();
const rect = $el.get(0).getBoundingClientRect();
this.lastValidationMessage = message;
@@ -1465,51 +1479,42 @@ class BaseFieldView extends View {
}
}
$el
.popover({
placement: 'bottom',
container: 'body',
content: this.getHelper().transformMarkdownText(message).toString(),
html: true,
trigger: 'manual'
})
.popover('show');
this._popoverMap = this._popoverMap || new WeakMap();
const element = $el.get(0);
let isDestroyed = false;
if (!element) {
return;
}
$el.closest('.field').one('mousedown click', () => {
if (isDestroyed) {
return;
if (this._popoverMap[element]) {
try {
this._popoverMap[element].detach();
}
catch (e) {}
}
$el.popover('destroy');
isDestroyed = true;
});
const popover = Espo.Ui.popover($el, {
placement: 'bottom',
container: 'body',
content: this.getHelper().transformMarkdownText(message).toString(),
trigger: 'manual',
noToggleInit: true,
noHideOnOutsideClick: true,
}, view || this);
this.once('render remove', () => {
if (isDestroyed) {
return;
}
popover.show();
if ($el) {
$el.popover('destroy');
isDestroyed = true;
}
});
this._popoverMap[element] = popover;
$el.closest('.field').one('mousedown click', () => popover.destroy());
this.once('render remove', () => popover.destroy());
if (this._timeout) {
clearTimeout(this._timeout);
}
this._timeout = setTimeout(() => {
if (isDestroyed) {
return;
}
$el.popover('destroy');
isDestroyed = true;
}, this.VALIDATION_POPOVER_TIMEOUT);
this._timeout = setTimeout(() => popover.destroy(), this.VALIDATION_POPOVER_TIMEOUT);
}
/**
+22 -8
View File
@@ -507,11 +507,14 @@ class LinkMultipleFieldView extends BaseFieldView {
};
},
onSelect: s => {
// noinspection JSUnresolvedReference
this.addLink(s.id, s.name);
this.getModelFactory().create(this.foreignScope, model => {
model.set(s.attributes);
this.$element.val('');
this.$element.focus();
this.select([model])
this.$element.val('');
this.$element.focus();
});
},
});
@@ -966,14 +969,25 @@ class LinkMultipleFieldView extends BaseFieldView {
models = [models];
}
models.forEach(model => {
this.addLink(model.id, model.get('name'));
});
this.select(models);
});
});
});
}
/**
* On records select.
*
* @protected
* @param {module:model[]} models
* @since 8.0.4
*/
select(models) {
models.forEach(model => {
this.addLink(model.id, model.get('name'));
});
}
/**
* @private
* @return {Promise<{bool?: string[], advanced?: Object, primary?: string}>}
@@ -994,7 +1008,7 @@ class LinkMultipleFieldView extends BaseFieldView {
return Promise.resolve({
primary: this.getSelectPrimaryFilterName() || this.panelDefs.selectPrimaryFilterName,
bool: boolFilterList,
advanced: this.getSelectFilters() || this.panelDefs.selectPrimaryFilterName || undefined,
advanced: this.getSelectFilters() || undefined,
});
}
+27 -9
View File
@@ -611,7 +611,7 @@ class LinkFieldView extends BaseFieldView {
formatResult: suggestion => {
return this.getHelper().escapeString(suggestion.name);
},
onSelect: (s) => {
onSelect: s => {
this.getModelFactory().create(this.foreignScope, (model) => {
model.set(s.attributes);
@@ -648,6 +648,11 @@ class LinkFieldView extends BaseFieldView {
let $elementOneOf = this.$el.find('input.element-one-of');
$elementOneOf.autocomplete({
beforeRender: $c => {
if (this.$elementName.hasClass('input-sm')) {
$c.addClass('small');
}
},
serviceUrl: () => {
return this.getAutocompleteUrl();
},
@@ -662,11 +667,15 @@ class LinkFieldView extends BaseFieldView {
return this._transformAutocompleteResult(JSON.parse(response));
},
onSelect: s => {
// noinspection JSUnresolvedReference
this.addLinkOneOf(s.id, s.name);
this.getModelFactory().create(this.foreignScope, model => {
// noinspection JSUnresolvedReference
model.set(s.attributes);
$elementOneOf.val('');
setTimeout(() => $elementOneOf.focus(), 50);
this.selectOneOf([model]);
$elementOneOf.val('');
setTimeout(() => $elementOneOf.focus(), 50);
});
},
});
@@ -1097,7 +1106,7 @@ class LinkFieldView extends BaseFieldView {
return Promise.resolve({
primary: this.getSelectPrimaryFilterName() || this.panelDefs.selectPrimaryFilterName,
bool: boolFilterList,
advanced: this.getSelectFilters() || this.panelDefs.selectPrimaryFilterName || undefined,
advanced: this.getSelectFilters() || undefined,
});
}
@@ -1156,9 +1165,7 @@ class LinkFieldView extends BaseFieldView {
models = [models];
}
models.forEach(model => {
this.addLinkOneOf(model.id, model.get('name'));
});
this.selectOneOf(models);
});
});
}
@@ -1191,6 +1198,17 @@ class LinkFieldView extends BaseFieldView {
});
});
}
/**
* @protected
* @param {module:model[]} models
* @since 8.0.4
*/
selectOneOf(models) {
models.forEach(model => {
this.addLinkOneOf(model.id, model.get('name'));
});
}
}
export default LinkFieldView;
+17 -15
View File
@@ -49,7 +49,7 @@ class UrlFieldView extends VarcharFieldView {
DEFAULT_MAX_LENGTH =255
data() {
let data = super.data();
const data = super.data();
data.url = this.getUrl();
@@ -61,15 +61,15 @@ class UrlFieldView extends VarcharFieldView {
if (this.isEditMode()) {
this.$element.on('change', () => {
let value = this.$element.val() || '';
const value = this.$element.val() || '';
let parsedValue = this.parse(value);
const parsedValue = this.parse(value);
if (parsedValue === value) {
return;
}
let decoded = parsedValue ? decodeURI(parsedValue) : '';
const decoded = parsedValue ? decodeURI(parsedValue) : '';
this.$element.val(decoded);
});
@@ -77,7 +77,7 @@ class UrlFieldView extends VarcharFieldView {
}
getValueForDisplay() {
let value = this.model.get(this.name);
const value = this.model.get(this.name);
return value ? decodeURI(value) : null;
}
@@ -106,7 +106,7 @@ class UrlFieldView extends VarcharFieldView {
*/
strip(value) {
if (value.indexOf('//') !== -1) {
value = value.substr(value.indexOf('//') + 2);
value = value.substring(value.indexOf('//') + 2);
}
value = value.replace(/\/+$/, '');
@@ -128,23 +128,24 @@ class UrlFieldView extends VarcharFieldView {
return url;
}
// noinspection JSUnusedGlobalSymbols
validateValid() {
let value = this.model.get(this.name);
const value = this.model.get(this.name);
if (!value) {
return false;
}
/** @var {string} */
let pattern = this.getMetadata().get(['app', 'regExpPatterns', 'uriOptionalProtocol', 'pattern']);
const pattern = this.getMetadata().get(['app', 'regExpPatterns', 'uriOptionalProtocol', 'pattern']);
let regExp = new RegExp('^' + pattern + '$');
const regExp = new RegExp('^' + pattern + '$');
if (regExp.test(value)) {
return false;
}
let msg = this.translate('fieldInvalid', 'messages')
const msg = this.translate('fieldInvalid', 'messages')
.replace('{field}', this.translate(this.name, 'fields', this.entityType));
this.showValidationMessage(msg);
@@ -152,10 +153,11 @@ class UrlFieldView extends VarcharFieldView {
return true;
}
// noinspection JSUnusedGlobalSymbols
validateMaxLength() {
let maxLength = this.params.maxLength || this.DEFAULT_MAX_LENGTH;
const maxLength = this.params.maxLength || this.DEFAULT_MAX_LENGTH;
let value = this.model.get(this.name);
const value = this.model.get(this.name);
if (!value || !value.length) {
return false;
@@ -165,7 +167,7 @@ class UrlFieldView extends VarcharFieldView {
return false;
}
let msg = this.translate('fieldUrlExceedsMaxLength', 'messages')
const msg = this.translate('fieldUrlExceedsMaxLength', 'messages')
.replace('{maxLength}', maxLength)
.replace('{field}', this.translate(this.name, 'fields', this.entityType));
@@ -175,9 +177,9 @@ class UrlFieldView extends VarcharFieldView {
}
fetch() {
let data = super.fetch();
const data = super.fetch();
let value = data[this.name];
const value = data[this.name];
if (!value) {
return data;
+5
View File
@@ -102,6 +102,11 @@ class UserFieldView extends LinkFieldView {
$elementTeams.autocomplete({
beforeRender: $c => {
if (this.$elementName.hasClass('input-sm')) {
$c.addClass('small');
}
},
serviceUrl: () => {
return 'Team?&maxSize=' + this.getAutocompleteMaxCount() + '&select=id,name';
},
+26 -16
View File
@@ -49,6 +49,7 @@ class WysiwygFieldView extends TextFieldView {
fetchEmptyValueAsNull = false
validationElementSelector = '.note-editor'
htmlPurificationDisabled = false
tableClassName = 'table table-bordered'
events = {
/** @this WysiwygFieldView */
@@ -157,11 +158,12 @@ class WysiwygFieldView extends TextFieldView {
}
data() {
let data = super.data();
const data = super.data();
data.useIframe = this.useIframe;
data.isPlain = this.isPlain();
// noinspection JSValidateTypes
return data;
}
@@ -299,7 +301,8 @@ class WysiwygFieldView extends TextFieldView {
let $iframe = this.$el.find('iframe');
let iframeElement = this.iframe = $iframe.get(0);
/** @type {HTMLIFrameElement} */
const iframeElement = this.iframe = $iframe.get(0);
if (!iframeElement) {
return;
@@ -370,10 +373,9 @@ class WysiwygFieldView extends TextFieldView {
// Make shortcuts working.
$document.on('keydown', e => {
/** @var {KeyboardEvent} originalEvent */
let originalEvent = e.originalEvent;
const originalEvent = /** @type {KeyboardEvent} */ e.originalEvent;
let event = new KeyboardEvent('keydown', {
const event = new KeyboardEvent('keydown', {
bubbles: true,
code: originalEvent.code,
ctrlKey: originalEvent.ctrlKey,
@@ -464,7 +466,7 @@ class WysiwygFieldView extends TextFieldView {
let height = $body.height();
if (height === 0) {
height = $body.children(0).height() + 100;
height = $body.children().height() + 100;
}
iframeElement.style.height = height + 'px';
@@ -541,17 +543,18 @@ class WysiwygFieldView extends TextFieldView {
delete keyMap.pc['CTRL+BACKSLASH'];
delete keyMap.mac['CMD+BACKSLASH'];
let toolbar = this.toolbar;
const toolbar = this.toolbar;
let lastChangeKeydown = new Date();
const changeKeydownInterval = this.changeInterval * 1000;
let options = {
// noinspection JSUnusedGlobalSymbols
const options = {
espoView: this,
lang: this.getConfig().get('language'),
keyMap: keyMap,
callbacks: {
onImageUpload: (files) => {
onImageUpload: (files) => {
let file = files[0];
Espo.Ui.notify(this.translate('Uploading...'));
@@ -581,6 +584,7 @@ class WysiwygFieldView extends TextFieldView {
buttons: this.buttons,
dialogsInBody: this.$el,
codeviewFilter: true,
tableClassName: this.tableClassName,
};
if (this.height) {
@@ -745,14 +749,15 @@ class WysiwygFieldView extends TextFieldView {
}
onScrollEdit(e) {
let $target = $(e.target);
let toolbarHeight = this.$toolbar.height();
let toolbarWidth = this.$toolbar.parent().width();
const $target = $(e.target);
const toolbarHeight = this.$toolbar.height();
const toolbarWidth = this.$toolbar.parent().width();
let edgeTop, edgeTopAbsolute;
// noinspection JSIncompatibleTypesComparison
if ($target.get(0) === window.document) {
let $buttonContainer = $target.find('.detail-button-container:not(.hidden)');
let offset = $buttonContainer.offset();
const $buttonContainer = $target.find('.detail-button-container:not(.hidden)');
const offset = $buttonContainer.offset();
if (offset) {
edgeTop = offset.top + $buttonContainer.height();
@@ -996,8 +1001,8 @@ class WysiwygFieldView extends TextFieldView {
},
'fullscreen': function (context) {
let options = context.options;
let self = options.espoView;
const options = context.options;
const self = options.espoView;
//let lang = options.langInfo;
//let ui = $.summernote.ui;
@@ -1018,11 +1023,14 @@ class WysiwygFieldView extends TextFieldView {
this.isInModal = this.$modal.length > 0;
};
this.resizeTo = function (size) {
this.$editable.css('height', size.h);
this.$codable.css('height', size.h);
// noinspection SpellCheckingInspection
if (this.$codable.data('cmeditor')) {
// noinspection SpellCheckingInspection,JSUnresolvedReference
this.$codable.data('cmeditor').setsize(null, size.h);
}
};
@@ -1067,6 +1075,7 @@ class WysiwygFieldView extends TextFieldView {
this.$scrollbar.css('overflow', 'hidden');
}
// noinspection JSUnusedGlobalSymbols
this._isFullscreen = true;
}
else {
@@ -1080,6 +1089,7 @@ class WysiwygFieldView extends TextFieldView {
this.$scrollbar.css('overflow', '');
}
// noinspection JSUnusedGlobalSymbols
this._isFullscreen = false;
}
+22 -19
View File
@@ -28,6 +28,7 @@
import ModalView from 'views/modal';
import MassActionHelper from 'helpers/mass-action';
import Select from 'ui/select';
class MassUpdateModalView extends ModalView {
@@ -52,7 +53,7 @@ class MassUpdateModalView extends ModalView {
events = {
/** @this MassUpdateModalView */
'click a[data-action="add-field"]': function (e) {
let field = $(e.currentTarget).data('name');
const field = $(e.currentTarget).data('name');
this.addField(field);
},
@@ -86,7 +87,7 @@ class MassUpdateModalView extends ModalView {
this.hasActionMap = {};
let totalCount = this.options.totalCount;
const totalCount = this.options.totalCount;
this.helper = new MassActionHelper(this);
@@ -139,11 +140,11 @@ class MassUpdateModalView extends ModalView {
this.addedFieldList.push(name);
let label = this.getHelper().escapeString(
const label = this.getHelper().escapeString(
this.translate(name, 'fields', this.entityType)
);
let $cell =
const $cell =
$('<div>')
.addClass('cell form-group')
.attr('data-name', name)
@@ -158,7 +159,7 @@ class MassUpdateModalView extends ModalView {
.attr('data-name', name)
);
let $row =
const $row =
$('<div>')
.addClass('item grid-auto-fill-md')
.attr('data-name', name)
@@ -166,13 +167,13 @@ class MassUpdateModalView extends ModalView {
this.$el.find('.fields-container').append($row);
let type = this.model.getFieldType(name);
let viewName = this.model.getFieldParam(name, 'view') || this.getFieldManager().getViewName(type);
const type = this.model.getFieldType(name);
const viewName = this.model.getFieldParam(name, 'view') || this.getFieldManager().getViewName(type);
let actionList = this.getMetadata().get(['entityDefs', this.entityType, name, 'massUpdateActionList']) ||
const actionList = this.getMetadata().get(['entityDefs', this.entityType, name, 'massUpdateActionList']) ||
this.getMetadata().get(['fields', type, 'massUpdateActionList']);
let hasActionDropdown = actionList !== null;
const hasActionDropdown = actionList !== null;
this.hasActionMap[name] = hasActionDropdown;
@@ -192,13 +193,13 @@ class MassUpdateModalView extends ModalView {
});
if (hasActionDropdown) {
let $select =
const $select =
$('<select>')
.addClass('item-action form-control')
.attr('data-name', name);
actionList.forEach(action => {
let label = this.translate(Espo.Utils.upperCaseFirst(action));
const label = this.translate(Espo.Utils.upperCaseFirst(action));
$select.append(
$('<option>')
@@ -207,7 +208,7 @@ class MassUpdateModalView extends ModalView {
);
});
let $cellAction =
const $cellAction =
$('<div>')
.addClass('cell call-action form-group')
.attr('data-name', name)
@@ -224,6 +225,8 @@ class MassUpdateModalView extends ModalView {
);
$row.append($cellAction);
Select.init($select.get(0));
}
}
@@ -239,14 +242,14 @@ class MassUpdateModalView extends ModalView {
actionUpdate() {
this.disableButton('update');
let attributes = {};
let actions = {};
const attributes = {};
const actions = {};
this.addedFieldList.forEach(field => {
let action = this.fetchAction(field);
let itemAttributes = this.getFieldView(field).fetch();
const action = this.fetchAction(field);
const itemAttributes = this.getFieldView(field).fetch();
let itemActualAttributes = {};
const itemActualAttributes = {};
this.getFieldManager()
.getEntityTypeFieldActualAttributeList(this.entityType, field)
@@ -264,7 +267,7 @@ class MassUpdateModalView extends ModalView {
let notValid = false;
this.addedFieldList.forEach(field => {
let view = this.getFieldView(field);
const view = this.getFieldView(field);
notValid = view.validate() || notValid;
});
@@ -328,7 +331,7 @@ class MassUpdateModalView extends ModalView {
return this.ACTION_UPDATE;
}
let $dropdown = this.$el.find('select.item-action[data-name="' + name + '"]');
const $dropdown = this.$el.find('select.item-action[data-name="' + name + '"]');
return $dropdown.val() || this.ACTION_UPDATE;
}
+36 -24
View File
@@ -75,7 +75,7 @@ class RelatedListModalView extends ModalView {
},
/** @this RelatedListModalView */
'click .action': function (e) {
let isHandled = Espo.Utils.handleAction(this, e.originalEvent, e.currentTarget);
const isHandled = Espo.Utils.handleAction(this, e.originalEvent, e.currentTarget);
if (isHandled) {
return;
@@ -141,15 +141,15 @@ class RelatedListModalView extends ModalView {
return;
}
let model = this.collection.get(m.id);
const model = this.collection.get(m.id);
if (!model) {
return;
}
let attributes = {};
const attributes = {};
for (let name in m.attributes) {
for (const name in m.attributes) {
if (m.hasChanged(name)) {
attributes[name] = m.attributes[name];
}
@@ -187,7 +187,7 @@ class RelatedListModalView extends ModalView {
}
}
this.unlinkDisabled = this.unlinkDisabled || this.options.unlinkDisabled;
this.unlinkDisabled = this.unlinkDisabled || this.options.unlinkDisabled || this.defs.unlinkDisabled;
if (!this.massUnlinkDisabled) {
if (this.unlinkDisabled || this.defs.massUnlinkDisabled || this.defs.unlinkDisabled) {
@@ -253,7 +253,7 @@ class RelatedListModalView extends ModalView {
this.options.fullFormUrl
)
) {
let url = this.options.fullFormUrl ||
const url = this.options.fullFormUrl ||
'#' + this.model.entityType + '/related/' + this.model.id + '/' + this.link;
this.buttonList.unshift({
@@ -267,7 +267,7 @@ class RelatedListModalView extends ModalView {
.append(this.$header);
}
let iconHtml = this.getHelper().getScopeColorIconHtml(this.scope);
const iconHtml = this.getHelper().getScopeColorIconHtml(this.scope);
if (iconHtml) {
this.$header = $('<span>')
@@ -281,7 +281,7 @@ class RelatedListModalView extends ModalView {
this.waitForView('search');
}
this.getCollectionFactory().create(this.scope, (collection) => {
this.getCollectionFactory().create(this.scope, collection => {
collection.maxSize = this.getConfig().get('recordsPerPage');
collection.url = this.url;
@@ -291,12 +291,18 @@ class RelatedListModalView extends ModalView {
if (this.panelCollection) {
this.listenTo(collection, 'change', (model) => {
let panelModel = this.panelCollection.get(model.id);
const panelModel = this.panelCollection.get(model.id);
if (panelModel) {
panelModel.set(model.attributes);
}
});
this.listenTo(collection, 'after:mass-remove', () => {
this.panelCollection.fetch({
skipCollectionSync: true,
});
});
}
this.setupSearch();
@@ -329,12 +335,12 @@ class RelatedListModalView extends ModalView {
}
setupSearch() {
let searchManager = this.searchManager =
const searchManager = this.searchManager =
new SearchManager(this.collection, 'listSelect', null, this.getDateTime());
searchManager.emptyOnReset = true;
let primaryFilterName = this.primaryFilterName;
const primaryFilterName = this.primaryFilterName;
if (primaryFilterName) {
searchManager.setPrimary(primaryFilterName);
@@ -348,14 +354,14 @@ class RelatedListModalView extends ModalView {
this.filterList.forEach(item1 => {
let isFound = false;
let name1 = item1.name || item1;
const name1 = item1.name || item1;
if (!name1 || name1 === 'all') {
return;
}
filterList.forEach(item2 => {
let name2 = item2.name || item2;
const name2 = item2.name || item2;
if (name1 === name2) {
isFound = true;
@@ -430,13 +436,19 @@ class RelatedListModalView extends ModalView {
}
const fetch = () => {
// Timeout to make notify work.
setTimeout(() => {
this.whenRendered().then(() => {
Espo.Ui.notify(' ... ');
this.collection.fetch()
.then(() => Espo.Ui.notify(false));
}, 1);
});
// Timeout to make notify work.
/*setTimeout(() => {
Espo.Ui.notify(' ... ');
this.collection.fetch()
.then(() => Espo.Ui.notify(false));
}, 1);*/
};
if (this.options.forceSelectAllAttributes || this.forceSelectAllAttributes) {
@@ -472,7 +484,7 @@ class RelatedListModalView extends ModalView {
// noinspection JSUnusedGlobalSymbols
actionUnlinkRelated(data) {
let id = data.id;
const id = data.id;
this.confirm({
message: this.translate('unlinkRecordConfirmation', 'messages'),
@@ -493,8 +505,8 @@ class RelatedListModalView extends ModalView {
actionCreateRelated() {
// noinspection JSUnresolvedReference
let actionName = this.defs.createAction || 'createRelated';
let methodName = 'action' + Espo.Utils.upperCaseFirst(actionName);
const actionName = this.defs.createAction || 'createRelated';
const methodName = 'action' + Espo.Utils.upperCaseFirst(actionName);
let p = this.getParentView();
@@ -519,8 +531,8 @@ class RelatedListModalView extends ModalView {
// noinspection JSUnusedGlobalSymbols
actionSelectRelated() {
// noinspection JSUnresolvedReference
let actionName = this.defs.selectAction || 'selectRelated';
let methodName = 'action' + Espo.Utils.upperCaseFirst(actionName);
const actionName = this.defs.selectAction || 'selectRelated';
const methodName = 'action' + Espo.Utils.upperCaseFirst(actionName);
let p = this.getParentView();
@@ -546,13 +558,13 @@ class RelatedListModalView extends ModalView {
// noinspection JSUnusedGlobalSymbols
actionRemoveRelated(data) {
let id = data.id;
const id = data.id;
this.confirm({
message: this.translate('removeRecordConfirmation', 'messages'),
confirmText: this.translate('Remove'),
}, () => {
let model = this.collection.get(id);
const model = this.collection.get(id);
Espo.Ui.notify(' ... ');
@@ -578,7 +590,7 @@ class RelatedListModalView extends ModalView {
return;
}
let $search = this.$el.find('input.text-filter').first();
const $search = this.$el.find('input.text-filter').first();
if (!$search.length) {
return;
+21 -15
View File
@@ -125,8 +125,8 @@ class SelectRecordsModalView extends ModalView {
this.scope = this.entityType = this.options.scope || this.scope;
let customDefaultOrderBy = this.getMetadata().get(['clientDefs', this.scope, 'selectRecords', 'orderBy']);
let customDefaultOrder = this.getMetadata().get(['clientDefs', this.scope, 'selectRecords', 'order']);
const customDefaultOrderBy = this.getMetadata().get(['clientDefs', this.scope, 'selectRecords', 'orderBy']);
const customDefaultOrder = this.getMetadata().get(['clientDefs', this.scope, 'selectRecords', 'order']);
if (customDefaultOrderBy) {
this.defaultOrderBy = customDefaultOrderBy;
@@ -211,7 +211,7 @@ class SelectRecordsModalView extends ModalView {
}
setupSearch() {
let searchManager = this.searchManager =
const searchManager = this.searchManager =
new SearchManager(this.collection, 'listSelect', null, this.getDateTime());
searchManager.emptyOnReset = true;
@@ -220,11 +220,11 @@ class SelectRecordsModalView extends ModalView {
searchManager.setAdvanced(this.filters);
}
let boolFilterList = this.boolFilterList ||
const boolFilterList = this.boolFilterList ||
this.getMetadata().get('clientDefs.' + this.scope + '.selectDefaultFilters.boolFilterList');
if (boolFilterList) {
let d = {};
const d = {};
boolFilterList.forEach(item => {
d[item] = true;
@@ -233,7 +233,7 @@ class SelectRecordsModalView extends ModalView {
searchManager.setBool(d);
}
let primaryFilterName = this.primaryFilterName ||
const primaryFilterName = this.primaryFilterName ||
this.getMetadata().get('clientDefs.' + this.scope + '.selectDefaultFilters.filter');
if (primaryFilterName) {
@@ -297,13 +297,19 @@ class SelectRecordsModalView extends ModalView {
}
const fetch = () => {
// Timeout to make notify work.
setTimeout(() => {
this.whenRendered().then(() => {
Espo.Ui.notify(' ... ');
this.collection.fetch()
.then(() => Espo.Ui.notify(false));
}, 1);
});
// Timeout to make notify work.
/*setTimeout(() => {
Espo.Ui.notify(' ... ');
this.collection.fetch()
.then(() => Espo.Ui.notify(false));
}, 1);*/
};
if (this.options.forceSelectAllAttributes || this.forceSelectAllAttributes) {
@@ -317,7 +323,7 @@ class SelectRecordsModalView extends ModalView {
selectAttributeList.push('name');
}
let mandatorySelectAttributeList = this.options.mandatorySelectAttributeList ||
const mandatorySelectAttributeList = this.options.mandatorySelectAttributeList ||
this.mandatorySelectAttributeList || [];
mandatorySelectAttributeList.forEach(attribute => {
@@ -346,8 +352,8 @@ class SelectRecordsModalView extends ModalView {
Espo.Ui.notify(' ... ');
let viewName = this.getMetadata()
.get(['clientDefs', this.scope, 'modalViews', 'edit']) ||
const viewName = this.getMetadata()
.get(['clientDefs', this.scope, 'modalViews', 'edit']) ||
'views/modals/edit';
new Promise(resolve => {
@@ -391,7 +397,7 @@ class SelectRecordsModalView extends ModalView {
return;
}
let listView = this.getRecordView();
const listView = this.getRecordView();
if (listView.allResultIsChecked) {
this.trigger('select', {
@@ -405,7 +411,7 @@ class SelectRecordsModalView extends ModalView {
return;
}
let list = listView.getSelected();
const list = listView.getSelected();
if (list.length) {
this.trigger('select', list);
@@ -439,7 +445,7 @@ class SelectRecordsModalView extends ModalView {
return;
}
let $search = this.$el.find('input.text-filter').first();
const $search = this.$el.find('input.text-filter').first();
if (!$search.length) {
return;
+50 -17
View File
@@ -224,7 +224,7 @@ class BaseRecordView extends View {
* @param {boolean } [locked] To lock. Won't be able to un-set.
*/
setFieldReadOnly(name, locked) {
let previousValue = this.recordHelper.getFieldStateParam(name, 'readOnly');
const previousValue = this.recordHelper.getFieldStateParam(name, 'readOnly');
this.recordHelper.setFieldStateParam(name, 'readOnly', true);
@@ -232,7 +232,7 @@ class BaseRecordView extends View {
this.recordHelper.setFieldStateParam(name, 'readOnlyLocked', true);
}
let view = this.getFieldView(name);
const view = this.getFieldView(name);
if (view) {
view.setReadOnly(locked);
@@ -241,6 +241,27 @@ class BaseRecordView extends View {
if (!previousValue) {
this.trigger('set-field-read-only', name);
}
/**
* @todo
* Move to fields/base. Listen to recordHelper 'field-change' (if recordHelper is available).
* Same for set state methods.
* Issue is that sometimes state is changed in between view initialization (for bottom views with fields).
*/
if (!view && !this.isReady) {
this.once('ready', () => {
const view = this.getFieldView(name);
if (
view &&
!view.readOnly &&
this.recordHelper.getFieldStateParam(name, 'readOnly')
) {
view.setReadOnly(locked);
}
})
}
}
/**
@@ -249,21 +270,19 @@ class BaseRecordView extends View {
* @param {string} name A field name.
*/
setFieldNotReadOnly(name) {
let previousValue = this.recordHelper.getFieldStateParam(name, 'readOnly');
const previousValue = this.recordHelper.getFieldStateParam(name, 'readOnly');
this.recordHelper.setFieldStateParam(name, 'readOnly', false);
let view = this.getFieldView(name);
const view = this.getFieldView(name);
if (view) {
if (view.readOnly) {
view.setNotReadOnly();
if (view && view.readOnly) {
view.setNotReadOnly();
if (this.mode === this.MODE_EDIT) {
if (!view.readOnlyLocked && view.isDetailMode()) {
view.setEditMode()
.then(() => view.reRender());
}
if (this.mode === this.MODE_EDIT) {
if (!view.readOnlyLocked && view.isDetailMode()) {
view.setEditMode()
.then(() => view.reRender());
}
}
}
@@ -271,6 +290,20 @@ class BaseRecordView extends View {
if (previousValue) {
this.trigger('set-field-not-read-only', name);
}
if (!view && !this.isReady) {
this.once('ready', () => {
const view = this.getFieldView(name);
if (
view &&
view.readOnly &&
!this.recordHelper.getFieldStateParam(name, 'readOnly')
) {
view.setNotReadOnly();
}
})
}
}
/**
@@ -279,11 +312,11 @@ class BaseRecordView extends View {
* @param {string} name A field name.
*/
setFieldRequired(name) {
let previousValue = this.recordHelper.getFieldStateParam(name, 'required');
const previousValue = this.recordHelper.getFieldStateParam(name, 'required');
this.recordHelper.setFieldStateParam(name, 'required', true);
let view = this.getFieldView(name);
const view = this.getFieldView(name);
if (view) {
view.setRequired();
@@ -300,11 +333,11 @@ class BaseRecordView extends View {
* @param {string} name A field name.
*/
setFieldNotRequired(name) {
let previousValue = this.recordHelper.getFieldStateParam(name, 'required');
const previousValue = this.recordHelper.getFieldStateParam(name, 'required');
this.recordHelper.setFieldStateParam(name, 'required', false);
let view = this.getFieldView(name);
const view = this.getFieldView(name);
if (view) {
view.setNotRequired();
@@ -585,7 +618,7 @@ class BaseRecordView extends View {
}
/** @type {module:view-record-helper} */
this.recordHelper = new ViewRecordHelper();
this.recordHelper = this.options.recordHelper || new ViewRecordHelper();
this.dynamicLogicDefs = this.options.dynamicLogicDefs || this.dynamicLogicDefs;
+5 -1
View File
@@ -57,6 +57,7 @@ class DetailRecordView extends BaseRecordView {
* @property {string} [inlineEditDisabled] Disable inline edit.
* @property {string} [navigateButtonsDisabled]
* @property {Object} [dynamicLogicDefs]
* @property {module:view-record-helper} [recordHelper] A record helper. For a form state management.
* @property {Object.<string, *>} [attributes]
* @property {module:views/record/detail~button[]} [buttonList] Buttons.
* @property {module:views/record/detail~dropdownItem[]} [dropdownItemList] Dropdown items.
@@ -636,6 +637,8 @@ class DetailRecordView extends BaseRecordView {
if (modeBeforeSave === this.MODE_EDIT && reason === 'error') {
this.setEditMode();
}
return Promise.reject(reason);
});
if (!this.lastSaveCancelReason || this.lastSaveCancelReason === 'notModified') {
@@ -1864,7 +1867,8 @@ class DetailRecordView extends BaseRecordView {
throw new Error('Model has not been injected into record view.');
}
this.recordHelper = new ViewRecordHelper(this.defaultFieldStates, this.defaultFieldStates);
this.recordHelper = this.options.recordHelper ||
new ViewRecordHelper(this.defaultFieldStates, this.defaultFieldStates);
this._initInlineEditSave();
+1 -1
View File
@@ -100,7 +100,7 @@ class EditRecordView extends DetailRecordView {
this.exit(isNew ? 'create' : 'save');
})
.catch(() => {});
.catch(reason => Promise.reject(reason));
}
/**
File diff suppressed because it is too large Load Diff
+66 -62
View File
@@ -95,15 +95,15 @@ class RelationshipPanelView extends BottomPanelView {
this.link = this.link || this.defs.link || this.panelName;
if (!this.scope && !(this.link in this.model.defs.links)) {
throw new Error('Link \'' + this.link + '\' is not defined in model \'' + this.model.entityType + '\'');
throw new Error(`Link '${this.link}' is not defined in model '${this.model.entityType}'`);
}
this.scope = this.scope || this.model.defs.links[this.link].entity;
let linkReadOnly = this.getMetadata()
const linkReadOnly = this.getMetadata()
.get(['entityDefs', this.model.entityType, 'links', this.link, 'readOnly']) || false;
let url = this.url = this.url || this.model.entityType + '/' + this.model.id + '/' + this.link;
const url = this.url = this.url || this.model.entityType + '/' + this.model.id + '/' + this.link;
if (!('create' in this.defs)) {
this.defs.create = true;
@@ -125,7 +125,11 @@ class RelationshipPanelView extends BottomPanelView {
this.filterList = this.defs.filterList || this.filterList || null;
if (this.filterList && this.filterList.length) {
this.filter = this.getStoredFilter();
this.filter = this.getStoredFilter() || this.filterList[0];
if (this.filter === 'all') {
this.filter = null;
}
}
this.setupTitle();
@@ -165,7 +169,7 @@ class RelationshipPanelView extends BottomPanelView {
}
if (this.defs.select) {
let data = {link: this.link};
const data = {link: this.link};
if (this.defs.selectPrimaryFilterName) {
data.primaryFilterName = this.defs.selectPrimaryFilterName;
@@ -205,7 +209,7 @@ class RelationshipPanelView extends BottomPanelView {
let listLayout = null;
let layout = this.defs.layout || null;
const layout = this.defs.layout || null;
if (layout) {
if (typeof layout === 'string') {
@@ -227,7 +231,8 @@ class RelationshipPanelView extends BottomPanelView {
collection.maxSize = this.recordsPerPage || this.getConfig().get('recordsPerPageSmall') || 5;
if (this.defs.filters) {
let searchManager = new SearchManager(collection, 'listRelationship', false, this.getDateTime());
const searchManager = new SearchManager(collection, 'listRelationship', null, this.getDateTime());
searchManager.setAdvanced(this.defs.filters);
collection.where = searchManager.getWhere();
}
@@ -250,7 +255,7 @@ class RelationshipPanelView extends BottomPanelView {
this.listenTo(this.model, 'update-all', () => collection.fetch());
let viewName =
const viewName =
this.defs.recordListView ||
this.getMetadata().get(['clientDefs', this.scope, 'recordViews', 'listRelated']) ||
this.getMetadata().get(['clientDefs', this.scope, 'recordViews', 'list']) ||
@@ -377,41 +382,40 @@ class RelationshipPanelView extends BottomPanelView {
* @protected
*/
setupFilterActions() {
if (this.filterList && this.filterList.length) {
this.actionList.push(false);
this.filterList.slice(0).forEach((item) => {
let selected;
if (item === 'all') {
selected = !this.filter;
}
else {
selected = item === this.filter;
}
let label = this.translateFilter(item);
let $item =
$('<div>')
.append(
$('<span>')
.addClass('check-icon fas fa-check pull-right')
.addClass(!selected ? 'hidden' : '')
)
.append(
$('<div>').text(label)
);
this.actionList.push({
action: 'selectFilter',
html: $item.get(0).innerHTML,
data: {
name: item,
},
});
});
if (!(this.filterList && this.filterList.length)) {
return;
}
this.actionList.push(false);
this.filterList.slice(0).forEach((item) => {
let selected;
selected = item === 'all' ?
!this.filter :
item === this.filter;
const label = this.translateFilter(item);
const $item =
$('<div>')
.append(
$('<span>')
.addClass('check-icon fas fa-check pull-right')
.addClass(!selected ? 'hidden' : '')
)
.append(
$('<div>').text(label)
);
this.actionList.push({
action: 'selectFilter',
html: $item.get(0).innerHTML,
data: {
name: item,
},
});
});
}
/**
@@ -428,7 +432,7 @@ class RelationshipPanelView extends BottomPanelView {
* @protected
*/
getStoredFilter() {
let key = 'panelFilter' + this.model.entityType + '-' + (this.panelName || this.name);
const key = 'panelFilter' + this.model.entityType + '-' + (this.panelName || this.name);
return this.getStorage().get('state', key) || null;
}
@@ -437,7 +441,7 @@ class RelationshipPanelView extends BottomPanelView {
* @private
*/
storeFilter(filter) {
let key = 'panelFilter' + this.model.entityType + '-' + (this.panelName || this.name);
const key = 'panelFilter' + this.model.entityType + '-' + (this.panelName || this.name);
if (filter) {
this.getStorage().set('state', key, filter);
@@ -467,7 +471,7 @@ class RelationshipPanelView extends BottomPanelView {
* @protected
*/
actionSelectFilter(data) {
let filter = data.name;
const filter = data.name;
let filterInternal = filter;
if (filter === 'all') {
@@ -478,7 +482,7 @@ class RelationshipPanelView extends BottomPanelView {
this.setFilter(filterInternal);
this.filterList.forEach(item => {
let $el = this.$el.closest('.panel').find('[data-name="'+item+'"] span');
const $el = this.$el.closest('.panel').find('[data-name="' + item + '"] span');
if (item === filter) {
$el.removeClass('hidden');
@@ -489,10 +493,10 @@ class RelationshipPanelView extends BottomPanelView {
this.collection.reset();
let listView = this.getView('list');
const listView = this.getView('list');
if (listView && listView.$el) {
let height = listView.$el.parent().get(0).clientHeight;
const height = listView.$el.parent().get(0).clientHeight;
listView.$el.empty();
@@ -529,7 +533,7 @@ class RelationshipPanelView extends BottomPanelView {
* @protected
*/
actionViewRelatedList(data) {
let viewName =
const viewName =
this.getMetadata().get(
['clientDefs', this.model.entityType, 'relationshipPanels', this.name, 'viewModalView']
) ||
@@ -537,7 +541,7 @@ class RelationshipPanelView extends BottomPanelView {
this.viewModalView ||
'views/modals/related-list';
let scope = data.scope || this.scope;
const scope = data.scope || this.scope;
let filter = this.filter;
@@ -545,7 +549,7 @@ class RelationshipPanelView extends BottomPanelView {
filter = null;
}
let options = {
const options = {
model: this.model,
panelName: this.panelName,
link: this.link,
@@ -567,7 +571,7 @@ class RelationshipPanelView extends BottomPanelView {
};
if (data.viewOptions) {
for (let item in data.viewOptions) {
for (const item in data.viewOptions) {
options[item] = data.viewOptions[item];
}
}
@@ -619,16 +623,16 @@ class RelationshipPanelView extends BottomPanelView {
* @protected
*/
actionViewRelated(data) {
let id = data.id;
let model = this.collection.get(id);
const id = data.id;
const model = this.collection.get(id);
if (!model) {
return;
}
let scope = model.entityType;
const scope = model.entityType;
let helper = new RecordModal(this.getMetadata(), this.getAcl());
const helper = new RecordModal(this.getMetadata(), this.getAcl());
helper
.showDetail(this, {
@@ -650,10 +654,10 @@ class RelationshipPanelView extends BottomPanelView {
* @protected
*/
actionEditRelated(data) {
let id = data.id;
let scope = this.collection.get(id).name;
const id = data.id;
const scope = this.collection.get(id).name;
let viewName = this.getMetadata().get('clientDefs.' + scope + '.modalViews.edit') ||
const viewName = this.getMetadata().get('clientDefs.' + scope + '.modalViews.edit') ||
'views/modals/edit';
Espo.Ui.notify(' ... ');
@@ -681,7 +685,7 @@ class RelationshipPanelView extends BottomPanelView {
* @protected
*/
actionUnlinkRelated(data) {
let id = data.id;
const id = data.id;
this.confirm({
message: this.translate('unlinkRecordConfirmation', 'messages'),
@@ -709,13 +713,13 @@ class RelationshipPanelView extends BottomPanelView {
* @protected
*/
actionRemoveRelated(data) {
let id = data.id;
const id = data.id;
this.confirm({
message: this.translate('removeRecordConfirmation', 'messages'),
confirmText: this.translate('Remove'),
}, () => {
let model = this.collection.get(id);
const model = this.collection.get(id);
Espo.Ui.notify(' ... ');
+87 -35
View File
@@ -598,33 +598,21 @@ input.global-search-input {
.field .link-container {
margin-bottom: 0;
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
&:not(.no-input) {
> .list-group-item:last-child {
border-bottom-width: 0;
}
> .list-group-item:first-child {
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
> .list-group-item:last-child {
border-bottom-width: 0;
}
&.no-input {
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
> .list-group-item:last-child {
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
border-bottom-width: 1px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom-width: 0;
}
}
> .list-group-item.ui-sortable-handle {
cursor: default;
}
> .list-group-item {
.text:empty:before {
content: "\200b";
@@ -632,24 +620,79 @@ input.global-search-input {
}
}
.list-group {
border-radius: var(--border-radius);
> .list-group-item {
&:first-child {
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
&:last-child {
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
border-bottom-width: 1px;
}
}
> .list-group-item {
&.ui-sortable-handle {
cursor: default;
}
}
}
.list-group {
&.no-side-margin,
&.no-margin {
border-radius: 0;
> .list-group-item {
&:first-child {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
&:last-child {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
}
margin-bottom: var(--vertical-gap);
}
.list-group-item {
margin-bottom: 0;
border-top-width: 0;
}
.list-group-item:first-child {
border-top-width: 1px;
}
.list-group-item.ui-sortable-helper {
border-top-width: 1px !important;
border-bottom-width: 1px !important;
border-left-width: 1px !important;
border-right-width: 1px !important;
}
.list-group-item {
background-color: transparent;
&:first-child {
border-top-width: 1px;
}
&.ui-sortable-helper {
border-top-width: 1px !important;
border-bottom-width: 1px !important;
border-left-width: 1px !important;
border-right-width: 1px !important;
}
}
.list > .list-group {
> .list-group-item {
&:first-child {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
&:last-child {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
}
.field {
@@ -3486,8 +3529,17 @@ body > .autocomplete-suggestions.text-search-suggestions {
}
}
.popover {
max-width: 300px;
.popover-title {
line-height: 1.5;
}
}
@import "misc/kanban.less";
@import "elements/placement.less";
@import "elements/site.less";
@import "elements/modal.less";
@import "elements/buttons.less";
+1 -1
View File
@@ -105,7 +105,7 @@ ul.dropdown-menu {
}
> .check-icon + div {
padding-right: 20px;
padding-right: 24px;
}
span.fa-check {
+3 -3
View File
@@ -114,8 +114,8 @@
}
}
.list-group:not(:empty) + div,
.list-group:not(:empty) + div > div.input-group {
.link-container:not(:empty) + div,
.link-container:not(:empty) + div > div.input-group {
> .form-control,
> .btn,
> .input-group-btn > .btn {
@@ -124,7 +124,7 @@
}
}
.list-group:not(:empty) + .form-control {
.link-container:not(:empty) + .form-control {
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
@@ -0,0 +1,47 @@
.margin {
margin: @table-cell-padding 0;
}
.margin-top-sm {
margin-top: calc(@table-cell-padding / 2);
}
.margin-top {
margin-top: @table-cell-padding;
}
.margin-top-2x {
margin-top: @table-cell-padding * 2;
}
.margin-bottom-sm {
margin-bottom: calc(@table-cell-padding / 2);
}
.margin-bottom {
margin-bottom: @table-cell-padding;
}
.margin-bottom-sm {
margin-bottom: calc(@table-cell-padding / 2);
}
.margin-bottom-2x {
margin-bottom: @table-cell-padding * 2;
}
.margin-bottom-3x {
margin-bottom: @table-cell-padding * 3;
}
.margin-bottom-4x {
margin-bottom: @table-cell-padding * 4;
}
.margin-bottom.margin {
margin-top: 0;
}
.center-align {
text-align: center;
}
@@ -34,6 +34,12 @@
font-weight: normal;
}
.fc-daygrid-event-harness {
.fc-event-main-frame > .fc-event-time {
overflow: unset;
}
}
.fc-daygrid-block-event .fc-event-time {
font-weight: 400;
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "espocrm",
"version": "8.0.3",
"version": "8.0.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "espocrm",
"version": "8.0.3",
"version": "8.0.5",
"hasInstallScript": true,
"license": "GPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "espocrm",
"version": "8.0.3",
"version": "8.0.5",
"description": "Open-source CRM.",
"repository": {
"type": "git",
+25
View File
@@ -578,6 +578,31 @@
}
}
}
},
"options": {
"type": "object",
"description": "Enum options.",
"additionalProperties": {
"type": "array",
"description": "A field name.",
"items": {
"type": "object",
"properties": {
"optionList": {
"type": "array",
"items": {"type": "string"},
"description": "Options."
},
"conditionGroup": {
"type": "array",
"description": "An AND where clause.",
"items": {
"$ref": "#/definitions/dynamicLogicItem"
}
}
}
}
}
}
}
}
+42
View File
@@ -359,6 +359,41 @@
}
}
}
},
{
"if": {
"properties": {
"type": {
"anyOf": [
{"const": "linkMultiple"}
]
}
}
},
"then": {
"properties": {
"sortable": {
"type": "boolean",
"description": "Sortable."
},
"orderBy": {
"type": "string",
"description": "An order-by field."
},
"orderDirection": {
"enum": ["asc", "desc"],
"description": "An Order direction."
},
"columns": {
"type": "object",
"description": "A column => attribute map.",
"additionalProperties": {
"type": "string",
"description": "A column."
}
}
}
}
}
],
"properties": {
@@ -571,6 +606,13 @@
"type": "string",
"description": "Duplicates attributes of a field. Some fields can require some processing when an entity is being duplicated. Should implement Espo\\Core\\Record\\Duplicator\\FieldDuplicator."
},
"additionalAttributeList": {
"type": "array",
"items": {
"type": "string"
},
"description": "Additional attributes for a field. Attribute name will be derived by concatenation with the field name."
},
"select": {
"type": "object",
"description": "A custom complex expression to be used for the field when reading from DB. It's possible to define leftJoins and joins that are needed to be applied when the field is being selected.",
+37
View File
@@ -288,6 +288,43 @@
"items": {
"type": "string"
}
},
"duplicateCheckFieldList": {
"description": "Fields to be check for duplicates. Requires recordDefs > duplicateWhereBuilderClassName set to Espo\\Classes\\DuplicateWhereBuilders\\General.",
"type": "array",
"items": {
"type": "string"
}
},
"entityManager": {
"description": "Entity manager items availability.",
"type": "object",
"properties": {
"layouts": {
"type": "boolean",
"description": "Layouts."
},
"edit": {
"type": "boolean",
"description": "Edit the entity."
},
"formula": {
"type": "boolean",
"description": "Formula scripts."
},
"fields": {
"type": "boolean",
"description": "Fields."
},
"relationships": {
"type": "boolean",
"description": "Relationships."
},
"addField": {
"type": "boolean",
"description": "The ability to add new fields."
}
}
}
}
}
@@ -585,6 +585,21 @@ class EvaluatorTest extends \PHPUnit\Framework\TestCase
$this->assertEquals('test', $result);
}
public function testJsonRetrieve8()
{
$value = (object) [
0 => 'test',
];
$expression = "json\\retrieve(\$value)";
$result = $this->evaluator->process($expression, null, (object) [
'value' => json_encode($value),
]);
$this->assertEquals($value, $result);
}
public function testNegate1()
{
$expression = "!string\contains('hello', 'test')";
+26 -1
View File
@@ -1930,7 +1930,7 @@ class MysqlQueryComposerTest extends \PHPUnit\Framework\TestCase
'withDeleted' => true
]));
$expectedSql =
"SELECT IF('1' OR '0', '1', ' ') AS `IF:(OR:('1','0'),'1',' ')` FROM `comment`";
"SELECT IF(('1' OR '0'), '1', ' ') AS `IF:(OR:('1','0'),'1',' ')` FROM `comment`";
$this->assertEquals($expectedSql, $sql);
}
@@ -2501,6 +2501,31 @@ class MysqlQueryComposerTest extends \PHPUnit\Framework\TestCase
$this->assertEquals($expectedSql, $sql);
}
public function testWhereExpression5(): void
{
$expectedSql =
"SELECT post.id AS `id` FROM `post` " .
"WHERE ((1 OR 0) AND 1)";
$query = SelectBuilder::create()
->from('Post')
->select('id')
->where(
Expression::and(
Expression::or(
Expression::value(1),
Expression::value(0)
),
Expression::value(1)
)
)
->withDeleted()
->build();
$sql = $this->query->composeSelect($query);
$this->assertEquals($expectedSql, $sql);
}
public function testSelectValue1()
{
$queryBuilder = new QueryBuilder();
+1 -1
View File
@@ -52,6 +52,7 @@ class BeforeUpgrade
{
$errorMessageList = [];
$this->processCheckExtension('Advanced Pack', '3.0.0', $errorMessageList);
$this->processCheckExtension('Google Integration', '1.6.0', $errorMessageList);
if (!count($errorMessageList)) {
@@ -63,7 +64,6 @@ class BeforeUpgrade
throw new Error($message);
}
/** @noinspection PhpSameParameterValueInspection */
private function processCheckExtension(string $name, string $minVersion, array &$errorMessageList): void
{
$em = $this->container->get('entityManager');