Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a013b7ea7 | |||
| 124214aee3 | |||
| 3bd6b3bd46 | |||
| c536cee637 | |||
| b1d2795347 | |||
| 0f9bce70cf | |||
| 6f1f72127f | |||
| 986edeb79e | |||
| b3391df6e8 | |||
| d8a114c90a | |||
| 3fa9f4bf91 | |||
| 6c4a139e66 | |||
| d3a1db25c2 | |||
| 0fcd3cd780 | |||
| 61cb573eec | |||
| 5dd84bf9a1 | |||
| 5dad7c6bba | |||
| 3007d50299 | |||
| 11c0ac6987 | |||
| 2bb62883a4 | |||
| 5796d2fb85 | |||
| dae318c9bb | |||
| 342764e9ff | |||
| 27b9e28e20 | |||
| f8d5dd7fa6 | |||
| 7408f97fbd | |||
| d1a9cf840f | |||
| d593927b14 | |||
| f62ebb8fe8 | |||
| ede798dbe5 | |||
| c3095b6a32 | |||
| 9eb5bfc9fe | |||
| bf2c529884 | |||
| 7ce8150541 | |||
| 44de149923 | |||
| 8ecdc7b9d9 | |||
| 95c64359d2 | |||
| 44203e2178 | |||
| d3d940d9c9 | |||
| 9d3efae141 | |||
| e6eb3e7099 | |||
| 3ab2b37471 | |||
| 33e7f4e3ef | |||
| 6463eaa6f6 | |||
| a066c0a187 | |||
| 176f65713f | |||
| 493afdf3d4 | |||
| c85b6a37a6 | |||
| 81971d0eda | |||
| 8a6cc9ea35 | |||
| 4fd125ddfc | |||
| 1298a8382d | |||
| bb9c352f55 | |||
| 045533d080 | |||
| 6a5ab5f738 | |||
| c1b0e4fd17 | |||
| e71928ffa7 | |||
| de3f5de029 | |||
| 57ceee4d4c | |||
| 9a5cd7609b | |||
| 1382dca3e3 | |||
| bc7a3e8839 | |||
| c5d6f4c63f | |||
| 6776f813af | |||
| 6d18f2485d | |||
| 73753b364e | |||
| 68d6a14d04 | |||
| c155d37790 | |||
| 6a6446d0b7 | |||
| b0b4fe5570 | |||
| 6de8c3d1ca | |||
| 319f3f645a | |||
| bedfbb0e6b | |||
| 148ffce6b7 | |||
| 85cb969780 | |||
| c1a75ad051 |
+1
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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
@@ -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
@@ -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 => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
@@ -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());
|
||||
|
||||
@@ -103,6 +103,8 @@ class ViewRecordHelper {
|
||||
|
||||
this.fieldStateMap[field] = this.fieldStateMap[field] || {};
|
||||
this.fieldStateMap[field][name] = value;
|
||||
|
||||
this.trigger('field-change');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ class EditRecordView extends DetailRecordView {
|
||||
|
||||
this.exit(isNew ? 'create' : 'save');
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(reason => Promise.reject(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+200
-196
File diff suppressed because it is too large
Load Diff
@@ -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(' ... ');
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -105,7 +105,7 @@ ul.dropdown-menu {
|
||||
}
|
||||
|
||||
> .check-icon + div {
|
||||
padding-right: 20px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
span.fa-check {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "8.0.3",
|
||||
"version": "8.0.5",
|
||||
"description": "Open-source CRM.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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')";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user