Compare commits

...

54 Commits

Author SHA1 Message Date
Yuri Kuznetsov d7829e7801 v 2021-10-26 12:07:42 +03:00
Yuri Kuznetsov 3d4f6055e4 merge fixes 2021-10-26 12:07:15 +03:00
Yuri Kuznetsov 383ae3fe3e entity inteface add methods 2021-10-26 11:45:10 +03:00
Yuri Kuznetsov 47cbd18c88 ldap user save fix 2021-10-26 11:43:18 +03:00
Yuri Kuznetsov d5779878c7 fix link multiple loading on list 2021-10-25 15:30:16 +03:00
Shefqet Avdullau c65cabffd5 Fixed typo in Tasks default filter Completed (#2126)
Fixes #2125
2021-10-25 15:04:42 +03:00
Yuri Kuznetsov fe90a62239 autocomplete new password 2021-10-25 13:24:21 +03:00
Yuri Kuznetsov 13ae33266f dummy file 2021-10-25 13:14:19 +03:00
Yuri Kuznetsov 399b689bcd fix personal email account assigned user 2021-10-25 12:01:38 +03:00
Yuri Kuznetsov 1219e7be12 fix category tree order 2021-10-25 11:02:23 +03:00
Yuri Kuznetsov 10f67ab360 mass update opp stage 2021-10-21 13:21:01 +03:00
Yuri Kuznetsov ce1f755612 action data cloning 2021-10-21 13:20:36 +03:00
Yuri Kuznetsov fa943d1166 mass action data cloning 2021-10-21 13:18:51 +03:00
Yuri Kuznetsov dc09f91e35 refactoring 2021-10-21 12:56:47 +03:00
Yuri Kuznetsov 34b8fa2728 acceptanceStatus customization fields disabled 2021-10-20 16:04:33 +03:00
Yuri Kuznetsov 02f7bebf67 outbound email address autocomplete 2021-10-19 16:01:19 +03:00
Yuri Kuznetsov 4ffc384764 formatting fix 2021-10-19 15:27:19 +03:00
Yuri Kuznetsov e1ef2b34ea orphan email address filter fix 2021-10-19 15:14:32 +03:00
Yuri Kuznetsov d5a83a8819 fix 2021-10-19 12:43:30 +03:00
Yuri Kuznetsov 661f499f67 orm custom functions 2021-10-19 12:40:07 +03:00
Yuri Kuznetsov a073beeea7 lastAccess export disable 2021-10-18 17:35:07 +03:00
Yuri Kuznetsov 07f6bf7a7a signature with insert email fix 2021-10-18 13:56:42 +03:00
Yuri Kuznetsov 0cfbb269f2 fix ldap 2021-10-18 13:15:23 +03:00
Yuri Kuznetsov f3791e5fb4 fix ldap create user 2021-10-18 13:03:06 +03:00
Yuri Kuznetsov 37d8b0d3bd cleanup 2021-10-15 15:47:17 +03:00
Arkadiy 4ae383033a fix missing class imports, exceptions and undefined properties (#2112)
* fix exception

* fix missing imports

* fix access to an undefined property
2021-10-15 15:46:45 +03:00
Yuri Kuznetsov ea71675701 fix typo 2021-10-15 10:56:48 +03:00
Arkadiy 1178d01baa fix for SelectingBuilderTrait (#2110)
* fix undefined type

* fix exception
2021-10-14 15:13:55 +03:00
Yuri Kuznetsov ace53f9494 v 2021-10-14 13:01:32 +03:00
Yuri Kuznetsov 3c17ff2da9 layout set fix 2021-10-14 10:35:08 +03:00
Yuri Kuznetsov 163991a36b fix 2021-10-14 10:07:09 +03:00
Yuri Kuznetsov 291721c1d0 fix checkboxes 2021-10-13 16:30:36 +03:00
Yuri Kuznetsov d96301d544 7.0.6 2021-10-13 15:56:19 +03:00
Yuri Kuznetsov 54d9674055 fix emails folderId 2021-10-13 10:35:47 +03:00
Yuri Kuznetsov 4a5a07649a cs fix 2021-10-13 10:32:39 +03:00
Yuri Kuznetsov cfdb65fad8 fix missing checkboxes 2021-10-13 10:12:44 +03:00
Yuri Kuznetsov 16544a4a70 fix email address parsing 2021-10-12 15:52:59 +03:00
Yuri Kuznetsov 02939d0840 v 2021-10-12 13:06:02 +03:00
Yuri Kuznetsov bf331c7020 cs fix 2021-10-12 12:05:58 +03:00
Yuri Kuznetsov e80b55572b fix lead filter 2021-10-12 11:32:10 +03:00
Yuri Kuznetsov 51c29f0a6e fix link & link multiple search empty 2021-10-12 11:30:20 +03:00
Yuri Kuznetsov 056191db82 fix pdf filename 2021-10-11 13:01:59 +03:00
Yuri Kuznetsov 9ace150efa v 2021-10-11 11:31:48 +03:00
Yuri Kuznetsov 79ae89d172 fix duration 2021-10-10 11:38:15 +03:00
Yuri Kuznetsov bb703e2060 fix export not existing fields 2021-10-10 11:22:58 +03:00
Yuri Kuznetsov 972d9019e1 fix log 2021-10-10 11:19:38 +03:00
Yuri Kuznetsov c99566410a note acl refactoring and period change 2021-10-08 15:35:05 +03:00
Yuri Kuznetsov 9c56a14fb0 file manager allow create file w/ wrong ownership 2021-10-08 11:45:56 +03:00
Yuri Kuznetsov b95e58317b fix msg 2021-10-08 11:27:09 +03:00
Yuri Kuznetsov 6fd9d6e15c upgrade fix 2021-10-07 17:42:31 +03:00
Yuri Kuznetsov 518b9fa2ba unlink file in checkCreateFile 2021-10-07 17:17:22 +03:00
Yuri Kuznetsov c113b7bd88 throw exception in auth 2021-10-07 15:00:11 +03:00
Yuri Kuznetsov fb0a302f62 v 2021-10-06 18:27:50 +03:00
Yuri Kuznetsov 43127379cd fix settings 2021-10-06 18:27:07 +03:00
106 changed files with 1848 additions and 612 deletions
+1
View File
@@ -110,6 +110,7 @@ module.exports = grunt => {
'build/tmp/client/custom/*',
'!build/tmp/client/custom/modules',
'build/tmp/client/custom/modules/*',
'!build/tmp/client/custom/modules/dummy.txt',
]
}
},
@@ -36,19 +36,36 @@ class Orphan implements Filter
{
public function apply(SelectBuilder $queryBuilder): void
{
$queryBuilder->where([
'entityEmailAddress.id' => null,
]);
$queryBuilder->leftJoin(
'EntityEmailAddress',
'entityEmailAddress',
[
'emailAddressId:' => 'id',
'deleted' => false,
]
);
$queryBuilder->distinct();
$queryBuilder
->distinct()
->leftJoin(
'EntityEmailAddress',
'entityEmailAddress',
[
'emailAddressId:' => 'id',
'deleted' => false,
]
)
->leftJoin(
'EmailEmailAddress',
'emailEmailAddress',
[
'emailAddressId:' => 'id',
'deleted' => false,
]
)
->leftJoin(
'Email',
'email',
[
'fromEmailAddressId:' => 'id',
'deleted' => false,
]
)
->where([
'entityEmailAddress.id' => null,
'emailEmailAddress.id' => null,
'email.id' => null,
]);
}
}
@@ -29,6 +29,7 @@
namespace Espo\Controllers;
use Espo\Core\Exceptions\Error;
use Espo\Core\{
Controllers\Record,
Api\Request,
@@ -42,6 +42,8 @@ class TwoFactorEmail
{
private $service;
private $user;
public function __construct(Service $service, User $user)
{
$this->service = $service;
@@ -42,6 +42,8 @@ class TwoFactorSms
{
private $service;
private $user;
public function __construct(Service $service, User $user)
{
$this->service = $service;
@@ -44,6 +44,8 @@ class UserSecurity
{
private $service;
private $user;
public function __construct(Service $service, User $user)
{
$this->service = $service;
+27 -3
View File
@@ -31,7 +31,7 @@ namespace Espo\Core\Action;
use Espo\Core\Utils\ObjectUtil;
use StdClass;
use stdClass;
class Data
{
@@ -40,12 +40,14 @@ class Data
$this->data = (object) [];
}
public function getRaw(): StdClass
public function getRaw(): stdClass
{
return ObjectUtil::clone($this->data);
}
/**
* Get an item value.
*
* @return mixed
*/
public function get(string $name)
@@ -53,12 +55,15 @@ class Data
return $this->getRaw()->$name ?? null;
}
/**
* Has an item.
*/
public function has(string $name): bool
{
return property_exists($this->data, $name);
}
public static function fromRaw(StdClass $data): self
public static function fromRaw(stdClass $data): self
{
$obj = new self();
@@ -66,4 +71,23 @@ class Data
return $obj;
}
/**
* Clone with an item value.
*
* @param mixed $value
*/
public function with(string $name, $value): self
{
$obj = clone $this;
$obj->data->$name = $value;
return $obj;
}
public function __clone()
{
$this->data = ObjectUtil::clone($this->data);
}
}
+1 -3
View File
@@ -218,9 +218,7 @@ class Auth
return;
}
$response->setStatus(500);
$this->log->error("Auth: " . $e->getMessage());
throw $e;
}
protected function handleUnauthorized(Response $response, bool $showDialog): void
@@ -29,6 +29,7 @@
namespace Espo\Core;
use Espo\Core\Exceptions\Error;
use Espo\Entities\Portal as PortalEntity;
use Espo\Entities\User as UserEntity;
@@ -86,6 +86,8 @@ class Authentication
private $twoFactorLoginFactory;
private $authTokenManager;
private $hookManager;
private $log;
@@ -0,0 +1,38 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 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\Authentication\LDAP;
class ClientFactory
{
public function create(array $options): Client
{
return new Client($options);
}
}
@@ -29,6 +29,14 @@
namespace Espo\Core\Authentication\Logins;
use Espo\Core\FieldProcessing\Relation\LinkMultipleSaver;
use Espo\Core\FieldProcessing\EmailAddress\Saver as EmailAddressSaver;
use Espo\Core\FieldProcessing\PhoneNumber\Saver as PhoneNumberSaver;
use Espo\Core\FieldProcessing\Saver\Params as SaverParams;
use Espo\Entities\User;
use Espo\Core\{
ORM\EntityManager,
Api\Request,
@@ -40,7 +48,8 @@ use Espo\Core\{
Authentication\Login\Data,
Authentication\Result,
Authentication\LDAP\Utils as LDAPUtils,
Authentication\LDAP\Client as LDAPClient,
Authentication\LDAP\Client as Client,
Authentication\LDAP\ClientFactory as ClientFactory,
Authentication\AuthToken\AuthToken,
Authentication\Result\FailReason,
};
@@ -51,9 +60,7 @@ class LDAP implements Login
{
private $utils;
private $ldapClient;
private $baseLogin;
private $client;
private $isPortal;
@@ -67,6 +74,16 @@ class LDAP implements Login
private $log;
private $baseLogin;
private $clientFactory;
private $linkMultipleSaver;
private $emailAddressSaver;
private $phoneNumberSaver;
public function __construct(
Config $config,
EntityManager $entityManager,
@@ -74,6 +91,10 @@ class LDAP implements Login
Language $defaultLanguage,
Log $log,
Espo $baseLogin,
ClientFactory $clientFactyory,
LinkMultipleSaver $linkMultipleSaver,
EmailAddressSaver $emailAddressSaver,
PhoneNumberSaver $phoneNumberSaver,
bool $isPortal = false
) {
$this->config = $config;
@@ -82,6 +103,10 @@ class LDAP implements Login
$this->language = $defaultLanguage;
$this->log = $log;
$this->baseLogin = $baseLogin;
$this->clientFactory = $clientFactyory;
$this->linkMultipleSaver = $linkMultipleSaver;
$this->emailAddressSaver = $emailAddressSaver;
$this->phoneNumberSaver = $phoneNumberSaver;
$this->isPortal = $isPortal;
@@ -227,26 +252,26 @@ class LDAP implements Login
return Result::success($user);
}
private function getLdapClient()
private function getLdapClient(): Client
{
if (!isset($this->ldapClient)) {
if (!isset($this->client)) {
$options = $this->utils->getLdapClientOptions();
try {
$this->ldapClient = new LDAPClient($options);
$this->client = $this->clientFactory->create($options);
}
catch (Exception $e) {
$this->log->error('LDAP error: ' . $e->getMessage());
}
}
return $this->ldapClient;
return $this->client;
}
/**
* Login by authorization token.
*/
private function loginByToken($username, AuthToken $authToken = null)
private function loginByToken($username, AuthToken $authToken = null): ?User
{
if (!isset($authToken)) {
return null;
@@ -256,6 +281,10 @@ class LDAP implements Login
$user = $this->entityManager->getEntity('User', $userId);
if (!$user) {
return null;
}
$tokenUsername = $user->get('userName');
if (strtolower($username) != strtolower($tokenUsername)) {
@@ -333,9 +362,23 @@ class LDAP implements Login
$this->entityManager->saveEntity($user, [
// Prevent `user` service being loaded by hooks.
'skipHooks' => true,
'keepNew' => true,
]);
return $this->entityManager->getEntity('User', $user->id);
$saverParams = SaverParams::create()
->withRawOptions([
'skipLinkMultipleHooks' => true,
]);
$this->linkMultipleSaver->process($user, 'teams', $saverParams);
$this->linkMultipleSaver->process($user, 'portalRoles', $saverParams);
$this->emailAddressSaver->process($user, $saverParams);
$this->phoneNumberSaver->process($user, $saverParams);
$user->setAsNotNew();
$user->updateFetchedValues();
return $this->entityManager->getEntity('User', $user->getId());
}
/**
@@ -358,7 +401,7 @@ class LDAP implements Login
'(' . $options['userNameAttribute'] . '=' . $username . ')' .
$loginFilterString . ')';
$result = $ldapClient->search($searchString, null, LDAPClient::SEARCH_SCOPE_SUB);
$result = $ldapClient->search($searchString, null, Client::SEARCH_SCOPE_SUB);
$this->log->debug('LDAP: user search string: "' . $searchString . '"');
@@ -81,11 +81,6 @@ class Data
return $this->loggedUser;
}
public function getStatus(): string
{
return $this->status;
}
public function getView(): ?string
{
return $this->view;
+2
View File
@@ -52,6 +52,8 @@ class Container implements ContainerInterface
private $configuration = null;
private $bindingContainer;
private $injectableFactory;
public function __construct(
@@ -398,79 +398,8 @@ class Saver implements SaverInterface
$emailAddressValue = trim($emailAddressValue);
}
$entityRepository = $this->entityManager->getRepository($entity->getEntityType());
if (!empty($emailAddressValue)) {
if ($emailAddressValue != $entity->getFetched('emailAddress')) {
$emailAddressNew = $this->entityManager
->getRDBRepository('EmailAddress')
->where([
'lower' => strtolower($emailAddressValue),
])
->findOne();
$isNewEmailAddress = false;
if (!$emailAddressNew) {
$emailAddressNew = $this->entityManager->getEntity('EmailAddress');
$emailAddressNew->set('name', $emailAddressValue);
if ($entity->has('emailAddressIsOptedOut')) {
$emailAddressNew->set('optOut', (bool) $entity->get('emailAddressIsOptedOut'));
}
$this->entityManager->saveEntity($emailAddressNew);
$isNewEmailAddress = true;
}
$emailAddressValueOld = $entity->getFetched('emailAddress');
if (!empty($emailAddressValueOld)) {
$emailAddressOld = $this->getByAddress($emailAddressValueOld);
if ($emailAddressOld) {
$entityRepository->unrelate($entity, 'emailAddresses', $emailAddressOld);
}
}
$entityRepository->relate($entity, 'emailAddresses', $emailAddressNew);
if ($entity->has('emailAddressIsOptedOut')) {
$this->markAddressOptedOut($emailAddressValue, (bool) $entity->get('emailAddressIsOptedOut'));
}
$update = $this->entityManager->getQueryBuilder()
->update()
->in('EntityEmailAddress')
->set(['primary' => true])
->where([
'entityId' => $entity->id,
'entityType' => $entity->getEntityType(),
'emailAddressId' => $emailAddressNew->id,
])
->build();
$this->entityManager->getQueryExecutor()->execute($update);
} else {
if (
$entity->has('emailAddressIsOptedOut')
&&
(
$entity->isNew()
||
(
$entity->hasFetched('emailAddressIsOptedOut')
&&
$entity->get('emailAddressIsOptedOut') !== $entity->getFetched('emailAddressIsOptedOut')
)
)
) {
$this->markAddressOptedOut($emailAddressValue, (bool) $entity->get('emailAddressIsOptedOut'));
}
}
$this->storePrimaryNotEmpty($entity, $emailAddressValue);
return;
}
@@ -481,11 +410,94 @@ class Saver implements SaverInterface
$emailAddressOld = $this->getByAddress($emailAddressValueOld);
if ($emailAddressOld) {
$entityRepository->unrelate($entity, 'emailAddresses', $emailAddressOld);
$this->entityManager
->getRDBRepository($entity->getEntityType())
->unrelate($entity, 'emailAddresses', $emailAddressOld, [
'skipHooks' => true,
]);
}
}
}
private function storePrimaryNotEmpty(Entity $entity, string $emailAddressValue): void
{
if ($emailAddressValue === $entity->getFetched('emailAddress')) {
if (
$entity->has('emailAddressIsOptedOut') &&
(
$entity->isNew() ||
(
$entity->hasFetched('emailAddressIsOptedOut') &&
$entity->get('emailAddressIsOptedOut') !== $entity->getFetched('emailAddressIsOptedOut')
)
)
) {
$this->markAddressOptedOut($emailAddressValue, (bool) $entity->get('emailAddressIsOptedOut'));
}
return;
}
$entityRepository = $this->entityManager->getRDBRepository($entity->getEntityType());
$emailAddressNew = $this->entityManager
->getRDBRepository('EmailAddress')
->where([
'lower' => strtolower($emailAddressValue),
])
->findOne();
$isNewEmailAddress = false;
if (!$emailAddressNew) {
$emailAddressNew = $this->entityManager->getEntity('EmailAddress');
$emailAddressNew->set('name', $emailAddressValue);
if ($entity->has('emailAddressIsOptedOut')) {
$emailAddressNew->set('optOut', (bool) $entity->get('emailAddressIsOptedOut'));
}
$this->entityManager->saveEntity($emailAddressNew);
$isNewEmailAddress = true;
}
$emailAddressValueOld = $entity->getFetched('emailAddress');
if (!empty($emailAddressValueOld)) {
$emailAddressOld = $this->getByAddress($emailAddressValueOld);
if ($emailAddressOld) {
$entityRepository->unrelate($entity, 'emailAddresses', $emailAddressOld, [
'skipHooks' => true,
]);
}
}
$entityRepository->relate($entity, 'emailAddresses', $emailAddressNew, null, [
'skipHooks' => true,
]);
if ($entity->has('emailAddressIsOptedOut')) {
$this->markAddressOptedOut($emailAddressValue, (bool) $entity->get('emailAddressIsOptedOut'));
}
$updateQuery = $this->entityManager
->getQueryBuilder()
->update()
->in('EntityEmailAddress')
->set(['primary' => true])
->where([
'entityId' => $entity->getId(),
'entityType' => $entity->getEntityType(),
'emailAddressId' => $emailAddressNew->getId(),
])
->build();
$this->entityManager->getQueryExecutor()->execute($updateQuery);
}
private function getByAddress(string $address): ?EmailAddress
{
return $this->entityManager
@@ -61,7 +61,7 @@ class ListLoader implements LoaderInterface
foreach ($this->getFieldList($entityType) as $field) {
if (
!in_array($field . 'Ids', $select) ||
!in_array($field . 'Ids', $select) &&
!in_array($field . 'Names', $select)
) {
continue;
@@ -445,11 +445,15 @@ class Saver implements SaverInterface
$phoneNumberOld = $this->getByNumber($phoneNumberValueOld);
if ($phoneNumberOld) {
$entityRepository->unrelate($entity, 'phoneNumbers', $phoneNumberOld);
$entityRepository->unrelate($entity, 'phoneNumbers', $phoneNumberOld, [
'skipHooks' => true,
]);
}
}
$entityRepository->relate($entity, 'phoneNumbers', $phoneNumberNew);
$entityRepository->relate($entity, 'phoneNumbers', $phoneNumberNew, null, [
'skipHooks' => true,
]);
if ($entity->has('phoneNumberIsOptedOut')) {
$this->markNumberOptedOut($phoneNumberValue, (bool) $entity->get('phoneNumberIsOptedOut'));
@@ -499,7 +503,9 @@ class Saver implements SaverInterface
$phoneNumberOld = $this->getByNumber($phoneNumberValueOld);
if ($phoneNumberOld) {
$entityRepository->unrelate($entity, 'phoneNumbers', $phoneNumberOld);
$entityRepository->unrelate($entity, 'phoneNumbers', $phoneNumberOld, [
'skipHooks' => true,
]);
}
}
}
@@ -59,6 +59,7 @@ class LinkMultipleSaver
$skipCreate = $params->getOption('skipLinkMultipleCreate') ?? false;
$skipRemove = $params->getOption('skipLinkMultipleRemove') ?? false;
$skipUpdate = $params->getOption('skipLinkMultipleUpdate') ?? false;
$skipHooks = $params->getOption('skipLinkMultipleHooks') ?? false;
if ($entity->isNew()) {
$skipRemove = true;
@@ -183,11 +184,15 @@ class LinkMultipleSaver
$data = (array) $columnData->$id;
}
$repository->getRelation($entity, $name)->relateById($id, $data);
$repository->getRelation($entity, $name)->relateById($id, $data, [
'skipHooks' => $skipHooks,
]);
}
foreach ($toRemoveIdList as $id) {
$repository->getRelation($entity, $name)->unrelateById($id);
$repository->getRelation($entity, $name)->unrelateById($id, [
'skipHooks' => $skipHooks,
]);
}
foreach ($toUpdateIdList as $id) {
@@ -68,7 +68,7 @@ class ValidatorFactory
$className = $this->getClassName($entityType, $field);
if (!$className) {
throw RuntimeException("Validator for '{$entityType}.{$field}' does not exist.");
throw new RuntimeException("Validator for '{$entityType}.{$field}' does not exist.");
}
return $this->injectableFactory->create($className);
@@ -49,6 +49,8 @@ class Evaluator
private $attributeFetcher;
private $injectableFactory;
private $parsedHash;
public function __construct(InjectableFactory $injectableFactory, array $functionClassNameMap = [])
@@ -41,7 +41,7 @@ class ValueType extends BaseFunction
public function process(ArgumentList $args)
{
if (!count($args)) {
throw Error("Bad value.");
throw new Error("Bad value.");
}
$value = $args[0]->getData();
@@ -41,7 +41,7 @@ class VariableType extends BaseFunction
public function process(ArgumentList $args)
{
if (!count($args)) {
throw Error("No variable name.");
throw new Error("No variable name.");
}
$name = $args[0]->getData();
@@ -65,6 +65,8 @@ class Htmlizer
private $dateTime;
private $number;
private $config;
private $acl;
@@ -61,17 +61,17 @@ class EspoFileHandler extends MonologStreamHandler
{
if (!$this->url) {
throw new RuntimeException(
"Missing a logger file path. Check logger params in `data/config.php`."
"Missing a logger file path. Check logger params in config."
);
}
try {
if (!is_writable($this->url)) {
$this->fileManager->checkCreateFile($this->url);
}
$checkFileResult = $this->fileManager->checkCreateFile($this->url);
if (!is_writable($this->url)) {
return;
if (!$checkFileResult) {
return;
}
}
$this->fileManager->appendContents(
@@ -35,6 +35,8 @@ use Espo\Core\Mail\Mail\Storage\Imap;
class MessageWrapper
{
private $parser;
private $storage;
private $id;
+27 -3
View File
@@ -31,7 +31,7 @@ namespace Espo\Core\MassAction;
use Espo\Core\Utils\ObjectUtil;
use StdClass;
use stdClass;
class Data
{
@@ -40,12 +40,14 @@ class Data
$this->data = (object) [];
}
public function getRaw(): StdClass
public function getRaw(): stdClass
{
return ObjectUtil::clone($this->data);
}
/**
* Get an item value.
*
* @return mixed
*/
public function get(string $name)
@@ -53,12 +55,15 @@ class Data
return $this->getRaw()->$name ?? null;
}
/**
* Has an item.
*/
public function has(string $name): bool
{
return property_exists($this->data, $name);
}
public static function fromRaw(StdClass $data): self
public static function fromRaw(stdClass $data): self
{
$obj = new self();
@@ -66,4 +71,23 @@ class Data
return $obj;
}
/**
* Clone with an item value.
*
* @param mixed $value
*/
public function with(string $name, $value): self
{
$obj = clone $this;
$obj->data->$name = $value;
return $obj;
}
public function __clone()
{
$this->data = ObjectUtil::clone($this->data);
}
}
@@ -42,6 +42,9 @@ use Espo\ORM\Value\ValueFactoryFactory as ValueFactoryFactoryInteface;
use Espo\ORM\Value\AttributeExtractorFactory as AttributeExtractorFactoryInteface;
use Espo\ORM\PDO\PDOProvider;
use Espo\ORM\PDO\DefaultPDOProvider;
use Espo\ORM\QueryComposer\Part\FunctionConverterFactory as FunctionConverterFactoryInterface;
use Espo\Core\ORM\QueryComposer\Part\FunctionConverterFactory;
use RuntimeException;
@@ -101,6 +104,13 @@ class EntityManagerFactory
->build()
);
$functionConverterFactory = $this->injectableFactory->createWithBinding(
FunctionConverterFactory::class,
BindingContainerBuilder::create()
->bindInstance(DatabaseParams::class, $databaseParams)
->build()
);
$binding = BindingContainerBuilder::create()
->bindInstance(DatabaseParams::class, $databaseParams)
->bindInstance(Metadata::class, $metadata)
@@ -110,6 +120,7 @@ class EntityManagerFactory
->bindInstance(AttributeExtractorFactoryInteface::class, $attributeExtractorFactory)
->bindInstance(EventDispatcher::class, $this->eventDispatcher)
->bindImplementation(PDOProvider::class, DefaultPDOProvider::class)
->bindInstance(FunctionConverterFactoryInterface::class, $functionConverterFactory)
->build();
return $this->injectableFactory->createWithBinding(EntityManager::class, $binding);
@@ -0,0 +1,93 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 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\ORM\QueryComposer\Part;
use Espo\ORM\QueryComposer\Part\FunctionConverterFactory as FunctionConverterFactoryInterface;
use Espo\ORM\QueryComposer\Part\FunctionConverter;
use Espo\ORM\DatabaseParams;
use Espo\Core\Utils\Metadata;
use Espo\Core\InjectableFactory;
use LogicException;
class FunctionConverterFactory implements FunctionConverterFactoryInterface
{
private $hash = [];
private $metadata;
private $databaseParams;
private $injectableFactory;
public function __construct(
Metadata $metadata,
InjectableFactory $injectableFactory,
DatabaseParams $databaseParams
) {
$this->metadata = $metadata;
$this->injectableFactory = $injectableFactory;
$this->databaseParams = $databaseParams;
}
public function create(string $name): FunctionConverter
{
$className = $this->getClassName($name);
if ($className === null) {
throw new LogicException();
}
$converter = $this->injectableFactory->create($className);
return $converter;
}
public function isCreatable(string $name): bool
{
if ($this->getClassName($name) === null) {
return false;
}
return true;
}
private function getClassName(string $name): ?string
{
if (!array_key_exists($name, $this->hash)) {
$this->hash[$name] = $this->metadata
->get(['app', 'orm', 'functionConverterClassNameMap_' . $this->databaseParams->getPlatform(), $name]);
}
return $this->hash[$name];
}
}
@@ -0,0 +1,40 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 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\ORM\QueryComposer\Part\FunctionConverters;
use Espo\ORM\QueryComposer\Part\FunctionConverter;
class Abs implements FunctionConverter
{
public function convert(string ...$argumentList): string
{
return 'ABS(' . implode(', ', $argumentList) . ')';
}
}
@@ -53,6 +53,7 @@ class SelectManagerFactory
private $user;
private $acl;
private $aclManager;
private $injectableFactory;
private $classFinder;
@@ -355,7 +355,7 @@ class ItemGeneralConverter implements ItemConverter
}
if (!$arrayAttributeLink || !$arrayAttribute) {
throw Error("Bad where item.");
throw new Error("Bad where item.");
}
$arrayEntityType = $entityDefs->getRelation($arrayAttributeLink)->getForeignEntityType();
@@ -29,6 +29,8 @@
namespace Espo\Core\Upgrades\Actions\Base;
use Espo\Core\Exceptions\Error;
class Delete extends \Espo\Core\Upgrades\Actions\Base
{
public function run($data)
@@ -37,6 +37,8 @@ class RelationManager
{
private $metadata;
private $config;
public function __construct(Metadata $metadata, Config $config)
{
$this->metadata = $metadata;
+25 -6
View File
@@ -607,7 +607,7 @@ class Manager
}
/**
* Create a new file if not exists with all folders in the path.
* Checks whether a new file can be created. It will also create all needed directories.
*/
public function checkCreateFile(string $filePath): bool
{
@@ -630,8 +630,11 @@ class Manager
$pathParts = pathinfo($filePath);
if (!file_exists($pathParts['dirname'])) {
$dirPermission = $defaultPermissions['dir'];
$dirPermission = is_string($dirPermission) ? base_convert($dirPermission,8,10) : $dirPermission;
$dirPermissionOriginal = $defaultPermissions['dir'];
$dirPermission = is_string($dirPermissionOriginal) ?
base_convert($dirPermissionOriginal, 8, 10) :
$dirPermissionOriginal;
if (!$this->mkdir($pathParts['dirname'], $dirPermission, true)) {
throw new Error(
@@ -640,11 +643,27 @@ class Manager
}
}
if (touch($filePath)) {
return $this->getPermissionUtils()->setDefaultPermissions($filePath);
$touchResult = touch($filePath);
if (!$touchResult) {
return false;
}
return false;
$setPrermissionsResult = $this->getPermissionUtils()->setDefaultPermissions($filePath);
if (!$setPrermissionsResult) {
$this->unlink($filePath);
/**
* Returning true will cause situations when files are created with
* a wrong ownership. This is a trade-off for being able to run
* Espo under a user that is neither webserver-user nor root. A file
* will be created owned by a user running the process.
*/
return true;
}
return true;
}
/**
@@ -32,6 +32,7 @@ namespace Espo\Core\WebSocket;
use Espo\Core\Utils\Log;
use stdClass;
use Throwable;
class Submission
{
@@ -29,7 +29,23 @@
namespace Espo\Entities;
use Espo\Core\Utils\Json;
class LayoutRecord extends \Espo\Core\ORM\Entity
{
public const ENTITY_TYPE = 'LayoutRecord';
/*protected function _getData()
{
$data = $this->getFromContainer('data');
return Json::decode($data);
}
protected function _setData($value)
{
$data = Json::encode($value);
$this->setInContainer('data', $data);
}*/
}
+7
View File
@@ -31,6 +31,8 @@ namespace Espo\Entities;
use Espo\Core\ORM\Entity;
use Espo\Core\Field\DateTime;
use stdClass;
class Note extends Entity
@@ -117,6 +119,11 @@ class Note extends Entity
return $this->get('post');
}
public function getCreatedAt(): ?DateTime
{
return $this->getValueObject('createdAt');
}
public function setAclIsProcessed(): void
{
$this->aclIsProcessed = true;
+1 -1
View File
@@ -84,7 +84,7 @@ class Pdf implements EntryPoint
->setHeader('Pragma', 'public')
->setHeader('Expires', 'Sat, 26 Jul 1997 05:00:00 GMT')
->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT')
->setHeader('Content', 'inline; filename="' . basename($fileName) . '"');
->setHeader('Content-Disposition', 'inline; filename="' . basename($fileName) . '"');
if (!$request->getServerParam('HTTP_ACCEPT_ENCODING')) {
$response->setHeader('Content-Length', $contents->getLength());
@@ -46,7 +46,7 @@ class FieldProcessing
public function afterSave(Entity $entity, array $options = []): void
{
if (!empty($options['skipFieldProcessng'])) {
if (!empty($options['skipFieldProcessing'])) {
return;
}
@@ -0,0 +1,72 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 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\Modules\Crm\Classes\MassAction\Opportunity;
use Espo\Core\MassAction\Actions\MassUpdate as MassUpdateOriginal;
use Espo\Core\MassAction\Params;
use Espo\Core\MassAction\Result;
use Espo\Core\MassAction\Data;
use Espo\Core\MassAction\MassAction;
use Espo\Core\Utils\Metadata;
class MassUpdate implements MassAction
{
private $massUpdateOriginal;
private $metadata;
public function __construct(
MassUpdateOriginal $massUpdateOriginal,
Metadata $metadata
) {
$this->massUpdateOriginal = $massUpdateOriginal;
$this->metadata = $metadata;
}
public function process(Params $params, Data $data): Result
{
$probability = null;
if (
$data->get('stage') &&
!$data->has('probability')
) {
$probability = $this->metadata
->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap', $data->get('stage')]);
}
if ($probability !== null) {
$data = $data->with('probability', $probability);
}
return $this->massUpdateOriginal->process($params, $data);
}
}
@@ -46,7 +46,7 @@ class Actual implements Filter
public function apply(SelectBuilder $queryBuilder): void
{
$notActualStatusList = $this->metadata
->get(['entityDefs', 'Actual', 'fields', 'status', 'notActualOptions']) ?? [];
->get(['entityDefs', 'Lead', 'fields', 'status', 'notActualOptions']) ?? [];
$queryBuilder->where(
Cond::notIn(
@@ -41,7 +41,7 @@ class Completed implements Filter
$queryBuilder->where(
Cond::equal(
Cond::column('status'),
'Completer'
'Completed'
)
);
}
@@ -29,6 +29,9 @@
namespace Espo\Modules\Crm\Controllers;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\BadRequest;
class CaseObj extends \Espo\Core\Controllers\Record
{
protected $name = 'Case';
@@ -29,6 +29,8 @@
namespace Espo\Modules\Crm\Controllers;
use Espo\Core\Exceptions\BadRequest;
class KnowledgeBaseArticle extends \Espo\Core\Controllers\Record
{
public function postActionGetCopiedAttachments($params, $data, $request)
@@ -30,6 +30,7 @@
namespace Espo\Modules\Crm\Controllers;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
class Lead extends \Espo\Core\Controllers\Record
{
@@ -30,6 +30,7 @@
namespace Espo\Modules\Crm\Controllers;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
class Target extends \Espo\Core\Controllers\Record
{
@@ -74,4 +74,29 @@ class Opportunity extends Entity
return $this;
}
public function getStage(): ?string
{
return $this->get('stage');
}
public function setStage(?string $stage): void
{
$this->set('stage', $stage);
}
public function getLastStage(): ?string
{
return $this->get('lastStage');
}
public function getProbability(): ?int
{
return $this->get('probability');
}
public function setProbability(?int $probability): void
{
$this->set('probability', $probability);
}
}
@@ -29,73 +29,16 @@
namespace Espo\Modules\Crm\Repositories;
use Espo\Modules\Crm\Entities\Opportunity as OpportunityEntity;
use Espo\ORM\Entity;
class Opportunity extends \Espo\Core\Repositories\Database
{
public function beforeSave(Entity $entity, array $options = [])
{
if ($entity->isNew()) {
if (!$entity->has('probability') && $entity->get('stage')) {
$probability = $this->metadata->get(
'entityDefs.Opportunity.fields.stage.probabilityMap.' . $entity->get('stage'), 0
);
if (!is_null($probability)) {
$entity->set('probability', $probability);
}
}
}
if (!$entity->isAttributeChanged('lastStage') && $entity->isAttributeChanged('stage')) {
$probability = $this->metadata->get(
['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap', $entity->get('stage')], 0
);
$probabilityMap = $this->metadata->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap'], []);
if (!$probability) {
$stageList = $this->metadata->get('entityDefs.Opportunity.fields.stage.options', []);
if ($entity->isNew()) {
if (count($stageList)) {
$min = 100;
$minStage = null;
foreach ($stageList as $stage) {
if (!empty($probabilityMap[$stage]) && $probabilityMap[$stage] !== 100) {
if ($probabilityMap[$stage] < $min) {
$min = $probabilityMap[$stage];
$minStage = $stage;
}
}
}
if ($minStage) {
$entity->set('lastStage', $minStage);
}
}
} else {
$lastStageProbability = $this->metadata->get(
['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap', $entity->get('lastStage')], 0
);
if ($lastStageProbability === 100) {
if (count($stageList)) {
$max = 0;
$maxStage = null;
foreach ($stageList as $stage) {
if (!empty($probabilityMap[$stage]) && $probabilityMap[$stage] !== 100) {
if ($probabilityMap[$stage] > $max) {
$max = $probabilityMap[$stage];
$maxStage = $stage;
}
}
}
if ($maxStage) {
$entity->set('lastStage', $maxStage);
}
}
}
}
} else {
$entity->set('lastStage', $entity->get('stage'));
}
}
$this->processProbability($entity);
$this->processLastStage($entity);
parent::beforeSave($entity, $options);
}
@@ -103,33 +46,160 @@ class Opportunity extends \Espo\Core\Repositories\Database
public function afterSave(Entity $entity, array $options = [])
{
parent::afterSave($entity, $options);
if ($entity->isAttributeChanged('amount') || $entity->isAttributeChanged('probability')) {
$amountConverted = $entity->get('amountConverted');
$probability = $entity->get('probability');
$amountWeightedConverted = round($amountConverted * $probability / 100, 2);
$entity->set('amountWeightedConverted', $amountWeightedConverted);
}
$this->handleAfterSaveContacts($entity, $options);
$this->handleAfterSaveContacts($entity);
}
protected function handleAfterSaveContacts(Entity $entity, array $options = [])
protected function handleAfterSaveContacts(OpportunityEntity $entity): void
{
if ($entity->isAttributeChanged('contactId')) {
$contactId = $entity->get('contactId');
$contactIdList = $entity->get('contactsIds') ?? [];
$fetchedContactId = $entity->getFetched('contactId');
if (!$entity->isAttributeChanged('contactId')) {
return;
}
if (!$contactId) {
if ($fetchedContactId) {
$this->unrelate($entity, 'contacts', $fetchedContactId);
$contactId = $entity->get('contactId');
$contactIdList = $entity->get('contactsIds') ?? [];
$fetchedContactId = $entity->getFetched('contactId');
if (!$contactId) {
if ($fetchedContactId) {
$this->unrelate($entity, 'contacts', $fetchedContactId);
}
return;
}
if (!in_array($contactId, $contactIdList) && !$this->isRelated($entity, 'contacts', $contactId)) {
$this->relate($entity, 'contacts', $contactId);
}
}
private function processProbability(OpportunityEntity $entity): void
{
if (!$entity->isNew()) {
return;
}
if ($entity->has('probability')) {
return;
}
if (!$entity->getStage()) {
return;
}
$probability = $this->metadata
->get('entityDefs.Opportunity.fields.stage.probabilityMap.' . $entity->getStage()) ?? 0;
if ($probability === null) {
return;
}
$entity->setProbability($probability);
}
private function processLastStage(OpportunityEntity $entity): void
{
if (
$entity->isAttributeChanged('lastStage') ||
!$entity->isAttributeChanged('stage')
) {
return;
}
$probability = $this->metadata
->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap', $entity->getStage()]) ?? 0;
if ($probability) {
$entity->set('lastStage', $entity->getStage());
return;
}
$probabilityMap = $this->metadata
->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap']) ?? [];
$stageList = $this->metadata->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'options']) ?? [];
if (!count($stageList)) {
return;
}
if ($entity->isNew()) {
// Created as Lost.
$min = 100;
$minStage = null;
foreach ($stageList as $stage) {
$itemProbability = $probabilityMap[$stage] ?? null;
if (
$itemProbability === null ||
$itemProbability === 100 ||
$itemProbability === 0 ||
$itemProbability >= $min
) {
continue;
}
$min = $itemProbability;
$minStage = $stage;
}
if (!$minStage) {
return;
}
if (!in_array($contactId, $contactIdList) && !$this->isRelated($entity, 'contacts', $contactId)) {
$this->relate($entity, 'contacts', $contactId);
}
$entity->set('lastStage', $minStage);
return;
}
// Won changed to Lost.
if (!$entity->getLastStage()) {
return;
}
$lastStageProbability = $this->metadata
->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap', $entity->getLastStage()]) ?? 0;
if ($lastStageProbability !== 100) {
return;
}
$max = 0;
$maxStage = null;
foreach ($stageList as $stage) {
$itemProbability = $probabilityMap[$stage] ?? null;
if (
$itemProbability === null ||
$itemProbability === 100 ||
$itemProbability === 0 ||
$itemProbability <= $max
) {
continue;
}
$max = $itemProbability;
$maxStage = $stage;
}
if (!$maxStage) {
return;
}
$entity->set('lastStage', $maxStage);
}
}
@@ -77,6 +77,11 @@
"layoutMassUpdateDisabled": true,
"importDisabled": true,
"exportDisabled": true,
"customizationDefaultDisabled": true,
"customizationRequiredDisabled": true,
"customizationIsSortedDisabled": true,
"customizationReadOnlyDisabled": true,
"customizationAuditedDisabled": true,
"where": {
"=": {
"whereClause": {
@@ -80,6 +80,11 @@
"layoutMassUpdateDisabled": true,
"importDisabled": true,
"exportDisabled": true,
"customizationDefaultDisabled": true,
"customizationRequiredDisabled": true,
"customizationIsSortedDisabled": true,
"customizationReadOnlyDisabled": true,
"customizationAuditedDisabled": true,
"where": {
"=": {
"whereClause": {
@@ -0,0 +1,7 @@
{
"massActions": {
"update": {
"implementationClassName": "Espo\\Modules\\Crm\\Classes\\MassAction\\Opportunity\\MassUpdate"
}
}
}
@@ -29,6 +29,7 @@
namespace Espo\Modules\Crm\Services;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\Error;
@@ -34,6 +34,7 @@ use Espo\Modules\Crm\Business\Event\Invitations;
use Espo\Services\Email as EmailService;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Di;
@@ -36,6 +36,7 @@ use Espo\ORM\Query\Select;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\Error;
use Espo\Core\{
Record\Collection as RecordCollection,
+11
View File
@@ -169,4 +169,15 @@ interface Entity
* Get values.
*/
public function getValueMap(): stdClass;
/**
* Set as not new. Meaning the entity is fetched or already saved.
*/
public function setAsNotNew(): void;
/**
* Copy all current values to fetched values. All current attribute values will beset as those
* that are fetched from DB.
*/
public function updateFetchedValues(): void;
}
+14 -3
View File
@@ -49,6 +49,8 @@ use Espo\ORM\Value\AttributeExtractorFactory;
use Espo\ORM\PDO\PDOProvider;
use Espo\ORM\QueryComposer\Part\FunctionConverterFactory;
use PDO;
use RuntimeException;
@@ -67,6 +69,8 @@ class EntityManager
private $mapperFactory = null;
private $functionConverterFactory = null;
private $mappers = [];
private $metadata;
@@ -101,15 +105,17 @@ class EntityManager
AttributeExtractorFactory $attributeExtractorFactory,
EventDispatcher $eventDispatcher,
PDOProvider $pdoProvider,
?MapperFactory $mapperFactory = null
?MapperFactory $mapperFactory = null,
?FunctionConverterFactory $functionConverterFactory = null
) {
$this->databaseParams = $databaseParams;
$this->metadata = $metadata;
$this->eventDispatcher = $eventDispatcher;
$this->entityFactory = $entityFactory;
$this->repositoryFactory = $repositoryFactory;
$this->mapperFactory = $mapperFactory;
$this->pdoProvider = $pdoProvider;
$this->mapperFactory = $mapperFactory;
$this->functionConverterFactory = $functionConverterFactory;
if (!$this->databaseParams->getPlatform()) {
throw new RuntimeException("No 'platform' parameter.");
@@ -145,7 +151,12 @@ class EntityManager
throw new RuntimeException("Query composer for '{$platform}' platform does not exits.");
}
$this->queryComposer = new $className($this->pdoProvider->get(), $this->entityFactory, $this->metadata);
$this->queryComposer = new $className(
$this->pdoProvider->get(),
$this->entityFactory,
$this->metadata,
$this->functionConverterFactory
);
}
private function initLocker(): void
@@ -29,6 +29,8 @@
namespace Espo\ORM\Query;
use RuntimeException;
class DeleteBuilder implements Builder
{
use SelectingBuilderTrait;
@@ -35,6 +35,7 @@ use Espo\ORM\Query\Part\Order;
use Espo\ORM\Query\Part\Join;
use InvalidArgumentException;
use RuntimeException;
trait SelectingBuilderTrait
{
@@ -128,7 +129,7 @@ trait SelectingBuilderTrait
}
if (!$orderBy) {
throw InvalidArgumentException();
throw new InvalidArgumentException();
}
$this->params['orderBy'] = $this->params['orderBy'] ?? [];
@@ -43,6 +43,7 @@ use Espo\ORM\{
Query\Delete as DeleteQuery,
Query\Union as UnionQuery,
Query\LockTable as LockTableQuery,
QueryComposer\Part\FunctionConverterFactory,
};
use PDO;
@@ -151,6 +152,8 @@ abstract class BaseQueryComposer implements QueryComposer
protected $metadata;
protected $functionConverterFactory;
protected $helper;
protected $attributeDbMapCache = [];
@@ -159,11 +162,16 @@ abstract class BaseQueryComposer implements QueryComposer
protected $seedCache = [];
public function __construct(PDO $pdo, EntityFactory $entityFactory, Metadata $metadata)
{
public function __construct(
PDO $pdo,
EntityFactory $entityFactory,
Metadata $metadata,
?FunctionConverterFactory $functionConverterFactory = null
) {
$this->entityFactory = $entityFactory;
$this->pdo = $pdo;
$this->metadata = $metadata;
$this->functionConverterFactory = $functionConverterFactory;
$this->helper = new Helper($metadata);
}
@@ -869,7 +877,15 @@ abstract class BaseQueryComposer implements QueryComposer
?array $argumentPartList = null
): string {
if (!in_array($function, Functions::FUNCTION_LIST)) {
$isBuiltIn = in_array($function, Functions::FUNCTION_LIST);
if (
!$isBuiltIn &&
(
!$this->functionConverterFactory ||
!$this->functionConverterFactory->isCreatable($function)
)
) {
throw new RuntimeException("ORM Query: Not allowed function '{$function}'.");
}
@@ -951,6 +967,10 @@ abstract class BaseQueryComposer implements QueryComposer
return implode(' ' . $function . ' ', $argumentPartList);
}
if (!$isBuiltIn && $this->functionConverterFactory) {
return $this->getFunctionPartFromFactory($function, $argumentPartList);
}
switch ($function) {
case 'MONTH':
return "DATE_FORMAT({$part}, '%Y-%m')";
@@ -1050,6 +1070,13 @@ abstract class BaseQueryComposer implements QueryComposer
return $function . '(' . $part . ')';
}
private function getFunctionPartFromFactory(string $function, array $argumentPartList): ?string
{
$obj = $this->functionConverterFactory->create($function);
return $obj->convert(...$argumentPartList);
}
protected function getFunctionPartTZ(?array $argumentPartList = null)
{
if (!$argumentPartList || count($argumentPartList) < 2) {
@@ -1176,7 +1203,7 @@ abstract class BaseQueryComposer implements QueryComposer
$argumentPartList = null;
if ($function && in_array($function, Functions::MULTIPLE_ARGUMENT_FUNCTION_LIST)) {
if ($function) {
$arguments = $attribute;
$argumentList = Util::parseArgumentListFromFunctionContent($arguments);
@@ -1188,8 +1215,8 @@ abstract class BaseQueryComposer implements QueryComposer
}
$part = implode(', ', $argumentPartList);
} else {
}
else {
$part = $this->getFunctionArgumentPart($entity, $attribute, $distinct, $params);
}
@@ -132,43 +132,6 @@ class Functions
'POSITION_IN_LIST',
];
public const MULTIPLE_ARGUMENT_FUNCTION_LIST = [
'CONCAT',
'LEFT',
'TZ',
'ROUND',
'COALESCE',
'IF',
'LIKE',
'NOT_LIKE',
'EQUAL',
'NOT_EQUAL',
'GREATER_THAN',
'LESS_THAN',
'GREATER_THAN_OR_EQUAL',
'LESS_THAN_OR_EQUAL',
'OR',
'AND',
'IN',
'NOT_IN',
'ADD',
'SUB',
'MUL',
'DIV',
'MOD',
'IFNULL',
'NULLIF',
'REPLACE',
'TIMESTAMPDIFF_DAY',
'TIMESTAMPDIFF_MONTH',
'TIMESTAMPDIFF_YEAR',
'TIMESTAMPDIFF_WEEK',
'TIMESTAMPDIFF_HOUR',
'TIMESTAMPDIFF_MINUTE',
'TIMESTAMPDIFF_SECOND',
'POSITION_IN_LIST',
];
public const COMPARISON_FUNCTION_LIST = [
'LIKE',
'NOT_LIKE',
@@ -32,6 +32,7 @@ namespace Espo\ORM\QueryComposer;
use Espo\ORM\{
Query\LockTable as LockTableQuery,
};
use LogicException;
class MysqlQueryComposer extends BaseQueryComposer
{
@@ -0,0 +1,35 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 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\ORM\QueryComposer\Part;
interface FunctionConverter
{
public function convert(string ...$argumentList): string;
}
@@ -0,0 +1,37 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 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\ORM\QueryComposer\Part;
interface FunctionConverterFactory
{
public function create(string $name): FunctionConverter;
public function isCreatable(string $name): bool;
}
@@ -49,6 +49,8 @@ class Preferences implements Repository,
use Di\ConfigSetter;
use Di\EntityManagerSetter;
protected $entityFactory;
public function __construct(
EntityManager $entityManager,
EntityFactory $entityFactory
@@ -58,6 +58,12 @@
"fieldLevel": {
},
"scopeFieldLevel": {
"EmailAccount": {
"assignedUser": {
"read": "yes",
"edit": "no"
}
},
"EmailFolder": {
"assignedUser": {
"read": "yes",
@@ -26,6 +26,7 @@
["app", "config"],
["app", "rebuild"],
["app", "smsProviders", "__ANY__", "senderClassName"],
["app", "orm"],
["selectDefs"],
["recordDefs"],
["pdfDefs"],
@@ -0,0 +1,5 @@
{
"functionConverterClassNameMap_Mysql": {
"ABS": "Espo\\Core\\ORM\\QueryComposer\\Part\\FunctionConverters\\Abs"
}
}
@@ -7,7 +7,7 @@
"type": "link"
},
"data": {
"type": "jsonObject"
"type": "text"
}
},
"links": {
File diff suppressed because one or more lines are too long
@@ -328,7 +328,8 @@
"layoutFiltersDisabled": true,
"layoutMassUpdateDisabled": true,
"layoutDetailDisabled": true,
"directAccessDisabled": true
"directAccessDisabled": true,
"exportDisabled": true
}
},
"links": {
@@ -36,6 +36,7 @@ use Espo\Core\ExternalAccount\ClientManager;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\NotFoundSilent;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Record\ReadParams;
+11 -9
View File
@@ -38,6 +38,7 @@ use Espo\Core\{
ORM\EntityManager,
Utils\Metadata,
DataManager,
Utils\Json,
};
use Espo\{
@@ -89,13 +90,13 @@ class Layout
if ($setId) {
$layout = $this->getRecordFromSet($scope, $name, $setId, true);
if ($layout) {
$result = $layout->get('data');
if ($layout && $layout->get('data') !== null) {
$result = Json::decode($layout->get('data'));
}
}
if (!$result) {
$result = json_decode(
$result = Json::decode(
$this->layout->get($scope, $name)
);
}
@@ -259,21 +260,22 @@ class Layout
if ($setId) {
$layout = $this->getRecordFromSet($scope, $name, $setId);
$em = $this->entityManager;
if (!$layout) {
$layout = $em->getEntity('LayoutRecord');
$layout = $this->entityManager->getEntity('LayoutRecord');
$layout->set([
'layoutSetId' => $setId,
'name' => $scope . '.' . $name,
]);
}
$layout->set('data', $data);
$layout->set('data', Json::encode($data));
$em->saveEntity($layout);
$this->entityManager->saveEntity($layout);
return $layout->get('data');
return Json::decode(
$layout->get('data')
);
}
$layoutManager = $this->layoutManager;
@@ -35,6 +35,7 @@ use Espo\{
};
use Espo\Core\{
Exceptions\Forbidden,
Exceptions\NotFound,
Exceptions\Error,
Utils\Util,
+6 -1
View File
@@ -35,6 +35,7 @@ use Espo\ORM\{
};
use Espo\Core\{
Exceptions\NotFound,
Exceptions\Error,
Exceptions\Forbidden,
Select\SearchParams,
@@ -107,16 +108,20 @@ class RecordTree extends Record
return null;
}
$searchParams = SearchParams::fromRaw($params);
$selectBuilder = $this->selectBuilderFactory
->create()
->from($this->entityType)
->withStrictAccessControl()
->withSearchParams(SearchParams::fromRaw($params))
->withSearchParams($searchParams)
->buildQueryBuilder()
->where([
'parentId' => $parentId,
]);
$selectBuilder->order([]);
if ($this->hasOrder()) {
$selectBuilder->order('order', 'ASC');
}
-4
View File
@@ -196,10 +196,6 @@ class Settings
foreach ($this->access->getAdminParamList() as $item) {
$ignoreItemList[] = $item;
}
foreach ($this->access->getSuperAdminParamList() as $item) {
$ignoreItemList[] = $item;
}
}
if ($this->config->get('restrictedMode') && !$user->isSuperAdmin()) {
+38 -16
View File
@@ -104,11 +104,24 @@ class Stream
private $selectBuilderFactory;
private $userAclManagerProvider;
private $noteAccessControl;
private $recordServiceContainer;
private const NOTE_ACL_PERIOD = '1 hour';
/**
* When a record is re-assigned, ACL will be recalculated for related notes
* created within the period.
*/
private const NOTE_ACL_PERIOD = '3 days';
private const NOTE_ACL_LIMIT = 50;
/**
* Not used currently.
*/
private const NOTE_NOTIFICATION_PERIOD = '1 hour';
public function __construct(
EntityManager $entityManager,
@@ -2110,6 +2123,8 @@ class Stream
return;
}
$limit = $this->config->get('noteAclLimit', self::NOTE_ACL_LIMIT);
$noteList = $this->entityManager
->getRDBRepository('Note')
->where([
@@ -2137,6 +2152,8 @@ class Stream
'relatedId',
'createdAt',
])
->order('number', 'DESC')
->limit(0, $limit)
->find();
$noteOptions = [];
@@ -2145,9 +2162,11 @@ class Stream
$noteOptions['forceProcessNotifications'] = true;
}
$period = '-' . $this->config->get('noteNotificationPeriod', self::NOTE_ACL_PERIOD);
$notificationPeriod = '-' . $this->config->get('noteNotificationPeriod', self::NOTE_NOTIFICATION_PERIOD);
$aclPeriod = '-' . $this->config->get('noteAclPeriod', self::NOTE_ACL_PERIOD);
$threshold = (new DateTime())->modify($period);
$notificationThreshold = (new DateTime())->modify($notificationPeriod);
$aclThreshold = (new DateTime())->modify($aclPeriod);
foreach ($noteList as $note) {
$this->processNoteAclItem($entity, $note, [
@@ -2156,7 +2175,8 @@ class Stream
'forceProcessNoteNotifications' => $forceProcessNoteNotifications,
'teamIdList' => $teamIdList,
'userIdList' => $userIdList,
'threshold' => $threshold,
'notificationThreshold' => $notificationThreshold,
'aclThreshold' => $aclThreshold,
]);
}
}
@@ -2170,21 +2190,21 @@ class Stream
$teamIdList = $params['teamIdList'];
$userIdList = $params['userIdList'];
$threshold = $params['threshold'];
$notificationThreshold = $params['notificationThreshold'];
$aclThreshold = $params['aclThreshold'];
$noteOptions = [
'forceProcessNotifications' => $forceProcessNoteNotifications,
];
$createdAt = $note->getCreatedAt();
if (!$entity->isNew() && $note->get('createdAt')) {
try {
$createdAtDt = new DateTime($note->get('createdAt'));
}
catch (Exception $e) {
return;
if (!$createdAt) {
return;
}
if (!$entity->isNew()) {
if ($createdAt->getTimestamp() < $notificationThreshold->getTimestamp()) {
$forceProcessNoteNotifications = false;
}
if ($createdAtDt->getTimestamp() < $threshold->getTimestamp()) {
if ($createdAt->getTimestamp() < $aclThreshold->getTimestamp()) {
return;
}
}
@@ -2197,7 +2217,9 @@ class Stream
$note->set('usersIds', $userIdList);
}
$this->entityManager->saveEntity($note, $noteOptions);
$this->entityManager->saveEntity($note, [
'forceProcessNotifications' => $forceProcessNoteNotifications,
]);
}
public function applyAccessControlToNote(NoteEntity $note, ?User $user = null): void
+4
View File
@@ -455,6 +455,10 @@ class Export
continue;
}
if (!$entityDefs->hasField($field)) {
continue;
}
if ($entityDefs->getField($field)->getParam('exportDisabled')) {
unset($fieldList[$i]);
}
@@ -30,6 +30,7 @@
namespace Espo\Tools\FieldManager;
use Espo\Core\{
Exceptions\BadRequest,
Exceptions\Error,
Exceptions\Conflict,
Container,
+2
View File
@@ -94,6 +94,8 @@ class Import
private $jobSchedulerFactory;
private $recordServiceContainer;
private $log;
public function __construct(
@@ -48,6 +48,8 @@ class PrinterController
private $injectableFactory;
private $engine;
public function __construct(
Metadata $metadata,
InjectableFactory $injectableFactory,
+1
View File
@@ -1,2 +1,3 @@
*
!.gitignore
!dummy.txt
+1
View File
@@ -0,0 +1 @@
dummy
@@ -2,4 +2,10 @@
{{#unless isNew}}
<a href="javascript:" data-action="change">{{translate 'change'}}</a>
{{/unless}}
<input type="password" class="main-element form-control {{#unless isNew}}hidden{{/unless}}" data-name="{{name}}" value="{{value}}" autocomplete="espo-{{name}}">
<input
type="password"
class="main-element form-control {{#unless isNew}}hidden{{/unless}}"
data-name="{{name}}"
value="{{value}}"
autocomplete="new-password"
>
+1 -1
View File
@@ -129,7 +129,7 @@ define('layout-manager', [], function () {
},
set: function (scope, type, layout, callback, setId) {
Espo.Ajax
return Espo.Ajax
.putRequest(this.getUrl(scope, type, setId), layout)
.then(
() => {
+2 -1
View File
@@ -387,7 +387,8 @@ define('view-helper', ['lib!marked', 'lib!dompurify'], function (marked, DOMPuri
}
text = text.replace(
/<a href="mailto:(.*)"/gm, '<a href="javascript:" data-email-address="$1" data-action="mailTo"'
/<a href="mailto:(.*!\")"/gm,
'<a href="javascript:" data-email-address="$1" data-action="mailTo"'
);
return new Handlebars.SafeString(text);
+11 -8
View File
@@ -101,17 +101,20 @@ define('views/admin/layouts/base', 'view', function (Dep) {
return false;
}
this.getHelper().layoutManager.set(this.scope, this.type, layout, () => {
this.notify('Saved', 'success', 2000);
this.getHelper()
.layoutManager
.set(this.scope, this.type, layout, () => {
this.notify('Saved', 'success', 2000);
this.setIsNotChanged();
this.setIsNotChanged();
if (typeof callback === 'function') {
callback();
}
if (typeof callback === 'function') {
callback();
}
this.getHelper().broadcastChannel.postMessage('update:layout');
}, this.setId);
this.getHelper().broadcastChannel.postMessage('update:layout');
}, this.setId)
.catch(() => this.enableButtons());
},
resetToDefault: function () {
+3 -1
View File
@@ -38,7 +38,7 @@ define('views/email/list', 'views/list', function (Dep) {
folderScope: 'EmailFolder',
currentFolderId: null,
selectedFolderId: null,
defaultFolderId: 'inbox',
@@ -169,6 +169,8 @@ define('views/email/list', 'views/list', function (Dep) {
},
applyFolder: function () {
this.collection.selectedFolderId = this.selectedFolderId;
if (!this.selectedFolderId) {
this.collection.whereFunction = null;
+2 -2
View File
@@ -42,7 +42,7 @@ define('views/email/record/compose', ['views/record/edit', 'views/email/record/d
if (!this.model.get('isHtml') && this.getPreferences().get('emailReplyForceHtml')) {
var body = (this.model.get('body') || '').replace(/\n/g, '<br>');
this.model.set('body', body);
this.model.set('isHtml', true);
}
@@ -183,7 +183,7 @@ define('views/email/record/compose', ['views/record/edit', 'views/email/record/d
if (isHtml) {
var signature = this.getSignature();
return body + '<p><br></p>' + signature;
return body + '' + signature;
}
var signature = this.getPlainTextSignature();
+160 -133
View File
@@ -52,187 +52,202 @@ define('views/email/record/list', 'views/record/list', function (Dep) {
this.addMassAction('markAsRead', false, true);
this.addMassAction('moveToTrash', false, true);
this.listenTo(this.collection, 'moving-to-trash', function (id) {
this.listenTo(this.collection, 'moving-to-trash', (id) => {
var model = this.collection.get(id);
if (model) {
model.set('inTrash', true);
}
if (this.collection.data.folderId !== 'trash' && this.collection.data.folderId !== 'all') {
if (this.collection.selectedFolderId !== 'trash' && this.collection.selectedFolderId !== 'all') {
this.removeRecordFromList(id);
}
}, this);
});
this.listenTo(this.collection, 'retrieving-from-trash', function (id) {
this.listenTo(this.collection, 'retrieving-from-trash', (id) => {
var model = this.collection.get(id);
if (model) {
model.set('inTrash', false);
}
if (this.collection.data.folderId === 'trash') {
if (this.collection.selectedFolderId === 'trash') {
this.removeRecordFromList(id);
}
}, this);
});
},
massActionMarkAsRead: function () {
var ids = [];
for (var i in this.checkedList) {
ids.push(this.checkedList[i]);
}
$.ajax({
url: 'Email/action/markAsRead',
type: 'POST',
data: JSON.stringify({
ids: ids
})
});
ids.forEach(function (id) {
Espo.Ajax
.postRequest('Email/action/markAsRead', {
ids: ids,
});
ids.forEach(id => {
var model = this.collection.get(id);
if (model) {
model.set('isRead', true);
}
}, this);
});
},
massActionMarkAsNotRead: function () {
var ids = [];
for (var i in this.checkedList) {
ids.push(this.checkedList[i]);
}
$.ajax({
url: 'Email/action/markAsNotRead',
type: 'POST',
data: JSON.stringify({
ids: ids
})
});
ids.forEach(function (id) {
Espo.Ajax
.postRequest('Email/action/markAsNotRead', {
ids: ids,
});
ids.forEach(id => {
var model = this.collection.get(id);
if (model) {
model.set('isRead', false);
}
}, this);
});
},
massActionMarkAsImportant: function () {
var ids = [];
for (var i in this.checkedList) {
ids.push(this.checkedList[i]);
}
$.ajax({
url: 'Email/action/markAsImportant',
type: 'POST',
data: JSON.stringify({
ids: ids
})
});
ids.forEach(function (id) {
Espo.Ajax
.postRequest('Email/action/markAsImportant', {
ids: ids,
});
ids.forEach(id => {
var model = this.collection.get(id);
if (model) {
model.set('isImportant', true);
}
}, this);
});
},
massActionMarkAsNotImportant: function () {
var ids = [];
for (var i in this.checkedList) {
ids.push(this.checkedList[i]);
}
$.ajax({
url: 'Email/action/markAsNotImportant',
type: 'POST',
data: JSON.stringify({
ids: ids
})
});
ids.forEach(function (id) {
Espo.Ajax
.postRequest('Email/action/markAsNotImportant', {
ids: ids,
});
ids.forEach(id => {
var model = this.collection.get(id);
if (model) {
model.set('isImportant', false);
}
}, this);
});
},
massActionMoveToTrash: function () {
var ids = [];
for (var i in this.checkedList) {
ids.push(this.checkedList[i]);
}
this.ajaxPostRequest('Email/action/moveToTrash', {
ids: ids
}).then(function () {
Espo.Ui.success(this.translate('Done'));
}.bind(this));
Espo.Ajax
.postRequest('Email/action/moveToTrash', {
ids: ids
})
.then(() => {
Espo.Ui.success(this.translate('Done'));
});
if (this.collection.data.folderId === 'trash') {
if (this.collection.selectedFolderId === 'trash') {
return;
}
ids.forEach(function (id) {
ids.forEach(id => {
this.collection.trigger('moving-to-trash', id);
}, this);
});
},
massActionRetrieveFromTrash: function () {
var ids = [];
for (var i in this.checkedList) {
ids.push(this.checkedList[i]);
}
this.ajaxPostRequest('Email/action/retrieveFromTrash', {
ids: ids
}).then(function () {
Espo.Ui.success(this.translate('Done'));
}.bind(this));
Espo.Ajax
.postRequest('Email/action/retrieveFromTrash', {
ids: ids
})
.then(() => {
Espo.Ui.success(this.translate('Done'));
});
if (this.collection.data.folderId !== 'trash') {
if (this.collection.selectedFolderId !== 'trash') {
return;
}
ids.forEach(function (id) {
ids.forEach(id => {
this.collection.trigger('retrieving-from-trash', id);
}, this);
});
},
massActionMoveToFolder: function () {
var ids = [];
for (var i in this.checkedList) {
ids.push(this.checkedList[i]);
}
this.createView('dialog', 'views/email-folder/modals/select-folder', {}, function (view) {
this.createView('dialog', 'views/email-folder/modals/select-folder', {}, view => {
view.render();
this.listenToOnce(view, 'select', function (folderId) {
this.listenToOnce(view, 'select', folderId => {
this.clearView('dialog');
this.ajaxPostRequest('Email/action/moveToFolder', {
ids: ids,
folderId: folderId
}).then(function () {
this.collection.fetch().then(function () {
Espo.Ui.success(this.translate('Done'));
}.bind(this));
}.bind(this));
}, this);
}, this);
Espo.Ajax
.postRequest('Email/action/moveToFolder', {
ids: ids,
folderId: folderId,
})
.then(() => {
this.collection.fetch().then(() => {
Espo.Ui.success(this.translate('Done'));
});
});
});
});
},
actionMarkAsImportant: function (data) {
data = data || {};
var id = data.id;
$.ajax({
url: 'Email/action/markAsImportant',
type: 'POST',
data: JSON.stringify({
id: id
})
});
Espo.Ajax
.postRequest('Email/action/markAsImportant', {
id: id,
});
var model = this.collection.get(id);
if (model) {
model.set('isImportant', true);
}
@@ -240,72 +255,81 @@ define('views/email/record/list', 'views/record/list', function (Dep) {
actionMarkAsNotImportant: function (data) {
data = data || {};
var id = data.id;
$.ajax({
url: 'Email/action/markAsNotImportant',
type: 'POST',
data: JSON.stringify({
id: id
})
});
var id = data.id;
Espo.Ajax
.postRequest('Email/action/markAsNotImportant', {
id: id,
});
var model = this.collection.get(id);
if (model) {
model.set('isImportant', false);
}
},
actionMarkAllAsRead: function () {
$.ajax({
url: 'Email/action/markAllAsRead',
type: 'POST'
});
Espo.Ajax
.postRequest('Email/action/markAllAsRead');
this.collection.forEach(function (model) {
this.collection.forEach(model => {
model.set('isRead', true);
}, this);
});
this.collection.trigger('all-marked-read');
},
actionMoveToTrash: function (data) {
var id = data.id;
this.ajaxPostRequest('Email/action/moveToTrash', {
id: id
}).then(function () {
Espo.Ui.warning(this.translate('Moved to Trash', 'labels', 'Email'));
this.collection.trigger('moving-to-trash', id);
}.bind(this));
Espo.Ajax
.postRequest('Email/action/moveToTrash', {
id: id
})
.then(() => {
Espo.Ui.warning(this.translate('Moved to Trash', 'labels', 'Email'));
this.collection.trigger('moving-to-trash', id);
});
},
actionRetrieveFromTrash: function (data) {
var id = data.id;
this.ajaxPostRequest('Email/action/retrieveFromTrash', {
id: id
}).then(function () {
Espo.Ui.warning(this.translate('Retrieved from Trash', 'labels', 'Email'));
this.collection.trigger('retrieving-from-trash', id);
}.bind(this));
Espo.Ajax
.postRequest('Email/action/retrieveFromTrash', {
id: id
})
.then(() => {
Espo.Ui.warning(this.translate('Retrieved from Trash', 'labels', 'Email'));
this.collection.trigger('retrieving-from-trash', id);
});
},
actionMoveToFolder: function (data) {
var id = data.id;
this.createView('dialog', 'views/email-folder/modals/select-folder', {}, function (view) {
this.createView('dialog', 'views/email-folder/modals/select-folder', {}, view => {
view.render();
this.listenToOnce(view, 'select', function (folderId) {
this.listenToOnce(view, 'select', folderId => {
this.clearView('dialog');
this.ajaxPostRequest('Email/action/moveToFolder', {
id: id,
folderId: folderId
}).then(function () {
this.collection.fetch().then(function () {
Espo.Ui.success(this.translate('Done'));
}.bind(this));
}.bind(this));
}, this);
}, this);
Espo.Ajax
.postRequest('Email/action/moveToFolder', {
id: id,
folderId: folderId
})
.then(() => {
this.collection.fetch().then(() => {
Espo.Ui.success(this.translate('Done'));
});
});
});
});
},
actionSend: function (data) {
@@ -314,27 +338,30 @@ define('views/email/record/list', 'views/record/list', function (Dep) {
this.confirm({
message: this.translate('sendConfirm', 'messages', 'Email'),
confirmText: this.translate('Send', 'labels', 'Email'),
}).then(
function () {
var model = this.collection.get(id);
if (!model) return;
}).then(() => {
var model = this.collection.get(id);
Espo.Ui.notify(this.translate('Sending...', 'labels', 'Email'));
if (!model) {
return;
}
model.save({
Espo.Ui.notify(this.translate('Sending...', 'labels', 'Email'));
model
.save({
status: 'Sending',
}).then(
function () {
Espo.Ui.success(this.translate('emailSent', 'messages', 'Email'));
if (this.collection.data.folderId === 'drafts') {
this.removeRecordFromList(id);
this.uncheckRecord(id, null, true);
this.collection.trigger('draft-sent');
}
}.bind(this)
);
}.bind(this)
);
})
.then(() => {
Espo.Ui.success(this.translate('emailSent', 'messages', 'Email'));
if (this.collection.selectedFolderId === 'drafts') {
this.removeRecordFromList(id);
this.uncheckRecord(id, null, true);
this.collection.trigger('draft-sent');
}
}
);
});
},
});
+43 -16
View File
@@ -55,18 +55,22 @@ define('views/fields/currency', 'views/fields/float', function (Dep) {
maxDecimalPlaces: 3,
data: function () {
var currencyValue = this.model.get(this.currencyFieldName) || this.getPreferences().get('defaultCurrency') || this.getConfig().get('defaultCurrency');
var currencyValue = this.model.get(this.currencyFieldName) ||
this.getPreferences().get('defaultCurrency') ||
this.getConfig().get('defaultCurrency');
return _.extend({
currencyFieldName: this.currencyFieldName,
currencyValue: currencyValue,
currencyOptions: this.currencyOptions,
currencyList: this.currencyList,
currencySymbol: this.getMetadata().get(['app', 'currency', 'symbolMap', currencyValue]) || ''
currencySymbol: this.getMetadata().get(['app', 'currency', 'symbolMap', currencyValue]) || '',
}, Dep.prototype.data.call(this));
},
setup: function () {
Dep.prototype.setup.call(this);
this.currencyFieldName = this.name + 'Currency';
this.defaultCurrency = this.getConfig().get('defaultCurrency');
this.currencyList = this.getConfig().get('currencyList') || [this.defaultCurrency];
@@ -90,13 +94,16 @@ define('views/fields/currency', 'views/fields/float', function (Dep) {
},
_getTemplateName: function () {
if (this.mode == 'detail' || this.mode == 'list') {
var prop
if (this.mode == 'list') {
if (this.mode === 'detail' || this.mode === 'list') {
var prop;
if (this.mode === 'list') {
var prop = 'listTemplate' + this.getCurrencyFormat().toString();
} else {
}
else {
var prop = 'detailTemplate' + this.getCurrencyFormat().toString();
}
if (this.options.hideCurrency) {
prop = 'detailTemplateNoCurrency';
}
@@ -105,6 +112,7 @@ define('views/fields/currency', 'views/fields/float', function (Dep) {
return this[prop];
}
}
return Dep.prototype._getTemplateName.call(this);
},
@@ -112,6 +120,7 @@ define('views/fields/currency', 'views/fields/float', function (Dep) {
if (this.mode === 'list' || this.mode === 'detail') {
return this.formatNumberDetail(value);
}
return this.formatNumberEdit(value);
},
@@ -120,6 +129,7 @@ define('views/fields/currency', 'views/fields/float', function (Dep) {
if (value !== null) {
var parts = value.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, this.thousandSeparator);
if (parts.length > 1) {
@@ -133,6 +143,7 @@ define('views/fields/currency', 'views/fields/float', function (Dep) {
return parts.join(this.decimalMark);
}
return '';
},
@@ -142,19 +153,28 @@ define('views/fields/currency', 'views/fields/float', function (Dep) {
if (currencyDecimalPlaces === 0) {
value = Math.round(value);
} else if (currencyDecimalPlaces) {
value = Math.round(value * Math.pow(10, currencyDecimalPlaces)) / (Math.pow(10, currencyDecimalPlaces));
} else {
value = Math.round(value * Math.pow(10, this.maxDecimalPlaces)) / (Math.pow(10, this.maxDecimalPlaces));
}
else if (currencyDecimalPlaces) {
value = Math.round(
value * Math.pow(10, currencyDecimalPlaces)) / (Math.pow(10, currencyDecimalPlaces)
);
}
else {
value = Math.round(
value * Math.pow(10, this.maxDecimalPlaces)) / (Math.pow(10, this.maxDecimalPlaces)
);
}
var parts = value.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, this.thousandSeparator);
if (currencyDecimalPlaces === 0) {
return parts[0];
} else if (currencyDecimalPlaces) {
}
else if (currencyDecimalPlaces) {
var decimalPartLength = 0;
if (parts.length > 1) {
decimalPartLength = parts[1].length;
} else {
@@ -163,6 +183,7 @@ define('views/fields/currency', 'views/fields/float', function (Dep) {
if (currencyDecimalPlaces && decimalPartLength < currencyDecimalPlaces) {
var limit = currencyDecimalPlaces - decimalPartLength;
for (var i = 0; i < limit; i++) {
parts[1] += '0';
}
@@ -171,34 +192,40 @@ define('views/fields/currency', 'views/fields/float', function (Dep) {
return parts.join(this.decimalMark);
}
return '';
},
afterRender: function () {
Dep.prototype.afterRender.call(this);
if (this.mode == 'edit') {
if (this.mode === 'edit') {
this.$currency = this.$el.find('[data-name="' + this.currencyFieldName + '"]');
this.$currency.on('change', function () {
this.$currency.on('change', () => {
this.model.set(this.currencyFieldName, this.$currency.val(), {ui: true});
}.bind(this));
});
}
},
fetch: function () {
var value = this.$element.val();
value = this.parse(value);
var data = {};
var currencyValue = this.$currency.val();
if (value === null) {
currencyValue = null;
}
data[this.name] = value;
data[this.currencyFieldName] = currencyValue
data[this.currencyFieldName] = currencyValue;
return data;
}
},
});
});
+7 -3
View File
@@ -123,12 +123,16 @@ define('views/fields/duration', 'views/fields/enum', function (Dep) {
return this.stringValue;
},
stringifyDuration: function (seconds) {
if (!seconds) {
stringifyDuration: function (secondsTotal) {
if (!secondsTotal) {
return '0';
}
var d = seconds;
if (secondsTotal < 60) {
return '0';
}
var d = secondsTotal;
var days = Math.floor(d / (86400));
+16 -2
View File
@@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/fields/float', 'views/fields/int', function (Dep) {
define('views/fields/float', 'views/fields/int', function (Dep) {
return Dep.extend({
@@ -43,7 +43,8 @@ Espo.define('views/fields/float', 'views/fields/int', function (Dep) {
if (this.getPreferences().has('decimalMark')) {
this.decimalMark = this.getPreferences().get('decimalMark');
} else {
}
else {
if (this.getConfig().has('decimalMark')) {
this.decimalMark = this.getConfig().get('decimalMark');
}
@@ -52,6 +53,7 @@ Espo.define('views/fields/float', 'views/fields/int', function (Dep) {
getValueForDisplay: function () {
var value = isNaN(this.model.get(this.name)) ? null : this.model.get(this.name);
return this.formatNumber(value);
},
@@ -59,11 +61,15 @@ Espo.define('views/fields/float', 'views/fields/int', function (Dep) {
if (this.disableFormatting) {
return value;
}
if (value !== null) {
var parts = value.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, this.thousandSeparator);
return parts.join(this.decimalMark);
}
return '';
},
@@ -72,29 +78,37 @@ Espo.define('views/fields/float', 'views/fields/int', function (Dep) {
validateFloat: function () {
var value = this.model.get(this.name);
if (isNaN(value)) {
var msg = this.translate('fieldShouldBeFloat', 'messages').replace('{field}', this.getLabelText());
this.showValidationMessage(msg);
return true;
}
},
parse: function (value) {
value = (value !== '') ? value : null;
if (value !== null) {
value = value.split(this.thousandSeparator).join('');
value = value.split(this.decimalMark).join('.');
value = parseFloat(value);
}
return value;
},
fetch: function () {
var value = this.$element.val();
value = this.parse(value);
var data = {};
data[this.name] = value;
return data;
},
});
+76 -26
View File
@@ -44,15 +44,27 @@ define('views/fields/int', 'views/fields/base', function (Dep) {
thousandSeparator: ',',
searchTypeList: ['isNotEmpty', 'isEmpty', 'equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterThanOrEquals', 'lessThanOrEquals', 'between'],
searchTypeList: [
'isNotEmpty',
'isEmpty',
'equals',
'notEquals',
'greaterThan',
'lessThan',
'greaterThanOrEquals',
'lessThanOrEquals',
'between',
],
setup: function () {
Dep.prototype.setup.call(this);
this.setupMaxLength();
if (this.getPreferences().has('thousandSeparator')) {
this.thousandSeparator = this.getPreferences().get('thousandSeparator');
} else {
}
else {
if (this.getConfig().has('thousandSeparator')) {
this.thousandSeparator = this.getConfig().get('thousandSeparator');
}
@@ -66,21 +78,22 @@ define('views/fields/int', 'views/fields/base', function (Dep) {
afterRender: function () {
Dep.prototype.afterRender.call(this);
if (this.mode == 'search') {
if (this.mode === 'search') {
var $searchType = this.$el.find('select.search-type');
this.handleSearchType($searchType.val());
this.$el.find('select.search-type').on('change', function () {
this.$el.find('select.search-type').on('change', () => {
this.trigger('change');
}.bind(this));
});
this.$element.on('input', function () {
this.$element.on('input', () => {
this.trigger('change');
}.bind(this));
});
this.$el.find('input.additional').on('input', function () {
this.$el.find('input.additional').on('input', () => {
this.trigger('change');
}.bind(this));
});
}
},
@@ -90,10 +103,12 @@ define('views/fields/int', 'views/fields/base', function (Dep) {
if (this.model.get(this.name) !== null && typeof this.model.get(this.name) !== 'undefined') {
data.isNotEmpty = true;
}
data.valueIsSet = this.model.has(this.name);
if (this.isSearchMode()) {
data.value = this.searchParams.value;
if (this.getSearchType() === 'between') {
data.value = this.getSearchParamsData().value1 || this.searchParams.value1;
data.value2 = this.getSearchParamsData().value2 || this.searchParams.value2;
@@ -105,6 +120,7 @@ define('views/fields/int', 'views/fields/base', function (Dep) {
getValueForDisplay: function () {
var value = isNaN(this.model.get(this.name)) ? null : this.model.get(this.name);
return this.formatNumber(value);
},
@@ -112,17 +128,21 @@ define('views/fields/int', 'views/fields/base', function (Dep) {
if (this.disableFormatting) {
return value;
}
if (value !== null) {
var stringValue = value.toString();
stringValue = stringValue.replace(/\B(?=(\d{3})+(?!\d))/g, this.thousandSeparator);
return stringValue;
}
return '';
},
setupSearch: function () {
this.events = _.extend({
'change select.search-type': function (e) {
'change select.search-type': (e) => {
this.handleSearchType($(e.currentTarget).val());
},
}, this.events || {});
@@ -130,15 +150,18 @@ define('views/fields/int', 'views/fields/base', function (Dep) {
handleSearchType: function (type) {
var $additionalInput = this.$el.find('input.additional');
var $input = this.$el.find('input[data-name="'+this.name+'"]');
if (type === 'between') {
$additionalInput.removeClass('hidden');
$input.removeClass('hidden');
} else if (~['isEmpty', 'isNotEmpty'].indexOf(type)) {
}
else if (~['isEmpty', 'isNotEmpty'].indexOf(type)) {
$additionalInput.addClass('hidden');
$input.addClass('hidden');
} else {
}
else {
$additionalInput.addClass('hidden');
$input.removeClass('hidden');
}
@@ -177,15 +200,19 @@ define('views/fields/int', 'views/fields/base', function (Dep) {
if (typeof max !== 'undefined' && max !== null) {
maxValue = this.formatNumber(maxValue);
this.params.maxLength = maxValue.toString().length;
}
},
validateInt: function () {
var value = this.model.get(this.name);
if (isNaN(value)) {
var msg = this.translate('fieldShouldBeInt', 'messages').replace('{field}', this.getLabelText());
this.showValidationMessage(msg);
return true;
}
},
@@ -202,25 +229,34 @@ define('views/fields/int', 'views/fields/base', function (Dep) {
if (minValue !== null && maxValue !== null) {
if (value < minValue || value > maxValue ) {
var msg = this.translate('fieldShouldBeBetween', 'messages').replace('{field}', this.getLabelText())
.replace('{min}', minValue)
.replace('{max}', maxValue);
var msg = this.translate('fieldShouldBeBetween', 'messages')
.replace('{field}', this.getLabelText())
.replace('{min}', minValue)
.replace('{max}', maxValue);
this.showValidationMessage(msg);
return true;
}
} else {
}
else {
if (minValue !== null) {
if (value < minValue) {
var msg = this.translate('fieldShouldBeGreater', 'messages').replace('{field}', this.getLabelText())
.replace('{value}', minValue);
var msg = this.translate('fieldShouldBeGreater', 'messages')
.replace('{field}', this.getLabelText())
.replace('{value}', minValue);
this.showValidationMessage(msg);
return true;
}
} else if (maxValue !== null) {
}
else if (maxValue !== null) {
if (value > maxValue) {
var msg = this.translate('fieldShouldBeLess', 'messages').replace('{field}', this.getLabelText())
.replace('{value}', maxValue);
var msg = this.translate('fieldShouldBeLess', 'messages')
.replace('{field}', this.getLabelText())
.replace('{value}', maxValue);
this.showValidationMessage(msg);
return true;
}
}
@@ -230,9 +266,12 @@ define('views/fields/int', 'views/fields/base', function (Dep) {
validateRequired: function () {
if (this.isRequired()) {
var value = this.model.get(this.name);
if (value === null || value === false) {
var msg = this.translate('fieldIsRequired', 'messages').replace('{field}', this.getLabelText());
this.showValidationMessage(msg);
return true;
}
}
@@ -240,22 +279,29 @@ define('views/fields/int', 'views/fields/base', function (Dep) {
parse: function (value) {
value = (value !== '') ? value : null;
if (value !== null) {
value = value.split(this.thousandSeparator).join('');
if (value.indexOf('.') !== -1 || value.indexOf(',') !== -1) {
value = NaN;
} else {
}
else {
value = parseInt(value);
}
}
return value;
},
fetch: function () {
var value = this.$element.val();
value = this.parse(value);
var data = {};
data[this.name] = value;
return data;
},
@@ -283,17 +329,20 @@ define('views/fields/int', 'views/fields/base', function (Dep) {
value2: valueTo
}
};
} else if (type == 'isEmpty') {
}
else if (type === 'isEmpty') {
data = {
type: 'isNull',
typeFront: 'isEmpty'
};
} else if (type == 'isNotEmpty') {
}
else if (type === 'isNotEmpty') {
data = {
type: 'isNotNull',
typeFront: 'isNotEmpty'
};
} else {
}
else {
data = {
type: type,
value: value,
@@ -302,12 +351,13 @@ define('views/fields/int', 'views/fields/base', function (Dep) {
}
};
}
return data;
},
getSearchType: function () {
return this.searchParams.typeFront || this.searchParams.type;
}
},
});
});
+13 -10
View File
@@ -480,10 +480,19 @@ define('views/fields/link-multiple', 'views/fields/base', function (Dep) {
fetchSearch: function () {
var type = this.$el.find('select.search-type').val();
var idList = this.ids || [];
if (~['anyOf', 'allOf', 'noneOf'].indexOf(type) && !idList.length) {
return {
type: 'isNotNull',
attribute: 'id',
data: {
type: type,
},
};
}
if (type === 'anyOf') {
var idList = this.ids || [];
var data = {
type: 'linkedWith',
value: idList,
@@ -493,16 +502,10 @@ define('views/fields/link-multiple', 'views/fields/base', function (Dep) {
},
};
if (!idList.length) {
data.value = null;
}
return data;
}
if (type === 'allOf') {
var idList = this.ids || [];
if (type === 'allOf') {
var data = {
type: 'linkedWithAll',
value: idList,
@@ -522,7 +525,7 @@ define('views/fields/link-multiple', 'views/fields/base', function (Dep) {
if (type === 'noneOf') {
var data = {
type: 'notLinkedWith',
value: this.ids || [],
value: idList,
data: {
type: type,
nameHash: this.nameHash,
+39 -24
View File
@@ -523,6 +523,16 @@ define('views/fields/link', 'views/fields/base', function (Dep) {
var type = this.$el.find('select.search-type').val();
var value = this.$el.find('[data-name="' + this.idName + '"]').val();
if (~['isOneOf', 'isNotOneOf'].indexOf(type) && !this.searchData.oneOfIdList.length) {
return {
type: 'isNotNull',
attribute: 'id',
data: {
type: type,
},
};
}
if (type === 'isEmpty') {
var data = {
type: 'isNull',
@@ -534,7 +544,8 @@ define('views/fields/link', 'views/fields/base', function (Dep) {
return data;
}
else if (type === 'isNotEmpty') {
if (type === 'isNotEmpty') {
var data = {
type: 'isNotNull',
attribute: this.idName,
@@ -545,7 +556,8 @@ define('views/fields/link', 'views/fields/base', function (Dep) {
return data;
}
else if (type === 'isOneOf') {
if (type === 'isOneOf') {
var data = {
type: 'in',
attribute: this.idName,
@@ -559,7 +571,8 @@ define('views/fields/link', 'views/fields/base', function (Dep) {
return data;
}
else if (type === 'isNotOneOf') {
if (type === 'isNotOneOf') {
var data = {
type: 'or',
value: [
@@ -582,7 +595,8 @@ define('views/fields/link', 'views/fields/base', function (Dep) {
return data;
}
else if (type === 'isNotOneOfAndIsNotEmpty') {
if (type === 'isNotOneOfAndIsNotEmpty') {
var data = {
type: 'notIn',
attribute: this.idName,
@@ -596,7 +610,8 @@ define('views/fields/link', 'views/fields/base', function (Dep) {
return data;
}
else if (type === 'isNot') {
if (type === 'isNot') {
if (!value) {
return false;
}
@@ -624,7 +639,8 @@ define('views/fields/link', 'views/fields/base', function (Dep) {
};
return data;
}
else if (type === 'isNotAndIsNotEmpty') {
if (type === 'isNotAndIsNotEmpty') {
if (!value) {
return false;
}
@@ -644,26 +660,25 @@ define('views/fields/link', 'views/fields/base', function (Dep) {
return data;
}
else {
if (!value) {
return false;
}
var nameValue = this.$el.find('[data-name="' + this.nameName + '"]').val();
var data = {
type: 'equals',
attribute: this.idName,
value: value,
data: {
type: type,
idValue: value,
nameValue: nameValue
}
};
return data;
if (!value) {
return false;
}
var nameValue = this.$el.find('[data-name="' + this.nameName + '"]').val();
var data = {
type: 'equals',
attribute: this.idName,
value: value,
data: {
type: type,
idValue: value,
nameValue: nameValue,
}
};
return data;
},
getSearchType: function () {
+99 -33
View File
@@ -43,8 +43,11 @@ define('views/fields/varchar', 'views/fields/base', function (Dep) {
'notEquals', 'notLike', 'isEmpty', 'isNotEmpty',
],
useAutocompleteUrl: null,
setup: function () {
this.setupOptions();
if (this.options.customOptionList) {
this.setOptionList(this.options.customOptionList);
}
@@ -57,9 +60,10 @@ define('views/fields/varchar', 'views/fields/base', function (Dep) {
if (!this.originalOptionList) {
this.originalOptionList = this.params.options || [];
}
this.params.options = Espo.Utils.clone(optionList);
if (this.mode == 'edit') {
if (this.mode === 'edit') {
if (this.isRendered()) {
this.reRender();
}
@@ -71,13 +75,35 @@ define('views/fields/varchar', 'views/fields/base', function (Dep) {
this.params.options = Espo.Utils.clone(this.originalOptionList);
}
if (this.mode == 'edit') {
if (this.mode === 'edit') {
if (this.isRendered()) {
this.reRender();
}
}
},
getAutocompleteUrl: function (q) {},
transformAutocompleteResult: function (response) {
let responseParsed = JSON.parse(response);
let list = [];
responseParsed.list.forEach(item => {
list.push({
id: item.id,
name: item.name || item.id,
data: item.id,
value: item.name || item.id,
attributes: item,
});
});
return {
suggestions: list,
};
},
setupSearch: function () {
this.events = _.extend({
'change select.search-type': function (e) {
@@ -98,6 +124,7 @@ define('views/fields/varchar', 'views/fields/base', function (Dep) {
) {
data.isNotEmpty = true;
}
data.valueIsSet = this.model.has(this.name);
if (this.mode === 'search') {
@@ -105,82 +132,114 @@ define('views/fields/varchar', 'views/fields/base', function (Dep) {
this.searchData.value = this.searchParams.value;
}
}
return data;
},
handleSearchType: function (type) {
if (~['isEmpty', 'isNotEmpty'].indexOf(type)) {
this.$el.find('input.main-element').addClass('hidden');
} else {
}
else {
this.$el.find('input.main-element').removeClass('hidden');
}
},
afterRender: function () {
Dep.prototype.afterRender.call(this);
if (this.mode == 'search') {
if (this.mode === 'search') {
var type = this.$el.find('select.search-type').val();
this.handleSearchType(type);
}
if ((this.mode == 'edit' || this.mode == 'search') && this.params.options && this.params.options.length) {
this.$element.autocomplete({
if (
(this.isEditMode() || this.isSearchMode()) &&
(
this.params.options && this.params.options.length ||
this.useAutocompleteUrl
)
) {
let autocompleteOptions = {
minChars: 0,
lookup: this.params.options,
maxHeight: 200,
beforeRender: function ($c) {
beforeRender: ($c) => {
if (this.$element.hasClass('input-sm')) {
$c.addClass('small');
}
}.bind(this),
formatResult: function (suggestion) {
},
formatResult: (suggestion) => {
return this.getHelper().escapeString(suggestion.value);
}.bind(this),
lookupFilter: function (suggestion, query, queryLowerCase) {
},
lookupFilter: (suggestion, query, queryLowerCase) => {
if (suggestion.value.toLowerCase().indexOf(queryLowerCase) === 0) {
if (suggestion.value.length === queryLowerCase.length) return false;
if (suggestion.value.length === queryLowerCase.length) {
return false;
}
return true;
}
return false;
},
onSelect: function () {
onSelect: () => {
this.trigger('change');
}.bind(this)
});
},
};
if (this.useAutocompleteUrl) {
autocompleteOptions.serviceUrl = q => this.getAutocompleteUrl(q);
autocompleteOptions.transformResult = response => this.transformAutocompleteResult(response);
autocompleteOptions.noCache = true;
autocompleteOptions.lookup = null;
}
this.$element.autocomplete(autocompleteOptions);
this.$element.attr('autocomplete', 'espo-' + this.name);
this.$element.on('focus', function () {
if (this.$element.val()) return;
this.$element.on('focus', () => {
if (this.$element.val()) {
return;
}
this.$element.autocomplete('onValueChange');
}.bind(this));
this.once('render', function () {
});
this.once('render', () => {
this.$element.autocomplete('dispose');
}, this);
this.once('remove', function () {
});
this.once('remove', () => {
this.$element.autocomplete('dispose');
}, this);
});
}
if (this.mode === 'search') {
this.$el.find('select.search-type').on('change', function () {
this.$el.find('select.search-type').on('change', () => {
this.trigger('change');
}.bind(this));
});
this.$element.on('input', function () {
this.$element.on('input', () => {
this.trigger('change');
}.bind(this));
});
}
},
fetch: function () {
var data = {};
var value = this.$element.val();
if (this.params.trim || this.forceTrim) {
if (typeof value.trim === 'function') {
value = value.trim();
}
}
data[this.name] = value || null;
return data;
},
@@ -190,7 +249,7 @@ define('views/fields/varchar', 'views/fields/base', function (Dep) {
var data;
if (~['isEmpty', 'isNotEmpty'].indexOf(type)) {
if (type == 'isEmpty') {
if (type === 'isEmpty') {
data = {
type: 'or',
value: [
@@ -207,8 +266,9 @@ define('views/fields/varchar', 'views/fields/base', function (Dep) {
data: {
type: type
}
}
} else {
};
}
else {
data = {
type: 'and',
value: [
@@ -228,27 +288,33 @@ define('views/fields/varchar', 'views/fields/base', function (Dep) {
}
}
}
return data;
} else {
}
else {
var value = this.$element.val().toString().trim();
value = value.trim();
if (value) {
data = {
value: value,
type: type,
data: {
type: type
}
}
},
};
return data;
}
}
return false;
},
getSearchType: function () {
return this.getSearchParamsData().type || this.searchParams.typeFront || this.searchParams.type;
}
},
});
});
+8 -4
View File
@@ -363,17 +363,19 @@ define('views/list-with-categories', 'views/list', function (Dep) {
},
getTreeCollection: function (callback) {
this.getCollectionFactory().create(this.categoryScope, function (collection) {
this.getCollectionFactory().create(this.categoryScope, collection => {
collection.url = collection.name + '/action/listTree';
collection.setOrder(null, null);
this.collection.treeCollection = collection;
this.listenToOnce(collection, 'sync', function () {
this.listenToOnce(collection, 'sync', () => {
callback.call(this, collection);
}, this);
});
collection.fetch();
}, this);
});
},
applyCategoryToNestedCategoriesCollection: function () {
@@ -397,6 +399,8 @@ define('views/list-with-categories', 'views/list', function (Dep) {
this.getCollectionFactory().create(this.categoryScope, collection => {
this.nestedCategoriesCollection = collection;
collection.setOrder(null, null);
collection.url = collection.name + '/action/listTree';
collection.maxDepth = 1;
+8
View File
@@ -106,6 +106,8 @@ define('views/record/list', 'view', function (Dep) {
setupHandlerType: 'record/list',
checkboxesDisabled: false,
events: {
'click a.link': function (e) {
e.stopPropagation();
@@ -492,6 +494,8 @@ define('views/record/list', 'view', function (Dep) {
this.checkboxes = _.isUndefined(this.options.checkboxes) ? this.checkboxes : this.options.checkboxes;
this.selectable = _.isUndefined(this.options.selectable) ? this.selectable : this.options.selectable;
this.checkboxesDisabled = this.options.checkboxes === false;
this.rowActionsView = _.isUndefined(this.options.rowActionsView) ?
this.rowActionsView :
this.options.rowActionsView;
@@ -1182,6 +1186,10 @@ define('views/record/list', 'view', function (Dep) {
if (allResult) {
this.checkAllResultMassActionList[method](item);
}
if (!this.checkboxesDisabled) {
this.checkboxes = true;
}
},
removeAllResultMassAction: function (item) {
+54 -52
View File
@@ -37,7 +37,7 @@ define('views/record/merge', 'view', function (Dep) {
data: function () {
var rows = [];
this.fields.forEach(function (field) {
this.fields.forEach((field) => {
var o = {
name: field,
scope: this.scope,
@@ -45,16 +45,16 @@ define('views/record/merge', 'view', function (Dep) {
o.columns = [];
this.models.forEach(function (m) {
this.models.forEach((m) => {
o.columns.push({
id: m.id,
fieldVariable: m.id + '-' + field,
isReadOnly: this.readOnlyFields[field] || false,
});
}, this);
});
rows.push(o);
}, this);
});
return {
rows: rows,
@@ -69,63 +69,65 @@ define('views/record/merge', 'view', function (Dep) {
events: {
'change input[type="radio"][name="check-all"]': function (e) {
e.stopPropagation();
var id = e.currentTarget.value;
$('input[data-id="'+id+'"]').prop('checked', true);
},
'click button[data-action="cancel"]': function () {
this.getRouter().navigate('#' + this.scope, {trigger: true});
},
'click button[data-action="merge"]': function () {
var id = $('input[type="radio"][name="check-all"]:checked').val();
var model;
this.models.forEach(function (m) {
this.models.forEach(m => {
if (m.id === id) {
model = m;
}
}.bind(this));
var self = this;
});
var attributes = {};
$('input.field-radio:checked').each(function (i, el) {
$('input.field-radio:checked').each((i, el) => {
var field = el.name;
var id = $(el).data('id');
if (model.id !== id) {
var fieldType = model.getFieldParam(field, 'type');
var fields = self.getFieldManager().getActualAttributeList(fieldType, field);
var modelFrom;
self.models.forEach(function (m) {
if (m.id === id) {
modelFrom = m;
return;
}
});
fields.forEach(function (field) {
attributes[field] = modelFrom.get(field);
});
var id = $(el).attr('data-id');
if (model.id === id) {
return;
}
var fieldType = model.getFieldParam(field, 'type');
var fields = this.getFieldManager().getActualAttributeList(fieldType, field);
var modelFrom;
this.models.forEach(m => {
if (m.id === id) {
modelFrom = m;
return;
}
});
fields.forEach(field => {
attributes[field] = modelFrom.get(field);
});
});
self.notify('Merging...');
this.notify('Merging...');
var sourceIdList =
this.models
.filter(function (m) {
.filter(m => {
if (m.id !== model.id) {
return true;
}
})
.map(function (m) {
.map(m => {
return m.id;
});
@@ -139,7 +141,7 @@ define('views/record/merge', 'view', function (Dep) {
attributes: attributes,
},
})
.then(function () {
.then(() => {
this.notify('Merged', 'success');
this.getRouter().navigate(
@@ -150,7 +152,7 @@ define('views/record/merge', 'view', function (Dep) {
if (this.collection) {
this.collection.fetch();
}
}.bind(this));
});
}
},
@@ -175,6 +177,7 @@ define('views/record/merge', 'view', function (Dep) {
if (type === 'linkMultiple') {
continue;
}
if (fieldsDefs[field].disabled) {
continue;
}
@@ -186,28 +189,27 @@ define('views/record/merge', 'view', function (Dep) {
if (field === 'createdAt' || field === 'modifiedAt') {
continue;
}
;
if (fieldManager.isMergeable(type)) {
var actualAttributeList = fieldManager.getActualAttributeList(type, field);
var differs = false;
actualAttributeList.forEach(function (field) {
actualAttributeList.forEach((field) => {
var values = [];
this.models.forEach(function (model) {
this.models.forEach((model) => {
values.push(model.get(field));
});
var firstValue = values[0];
values.forEach(function (value) {
values.forEach((value) => {
if (!_.isEqual(firstValue, value)) {
differs = true;
}
});
}.bind(this));
});
if (differs) {
differentFieldList.push(field);
@@ -219,26 +221,27 @@ define('views/record/merge', 'view', function (Dep) {
}
}
differentFieldList.sort(function (v1, v2) {
differentFieldList.sort((v1, v2) => {
return this.translate(v1, 'fields', this.scope)
.localeCompare(this.translate(v2, 'fields', this.scope));
}.bind(this));
});
differentFieldList = differentFieldList.sort(function (v1, v2) {
differentFieldList = differentFieldList.sort((v1, v2) => {
if (!this.readOnlyFields[v1] && this.readOnlyFields[v2]) {
return -1;
}
return 1;
}.bind(this));
});
this.fields = differentFieldList;
this.fields.forEach(function (field) {
this.fields.forEach((field) => {
var type = this.models[0].getFieldParam(field, 'type');
this.models.forEach(function (model) {
var viewName = model.getFieldParam(field, 'view') || this.getFieldManager().getViewName(type);
this.models.forEach((model) => {
var viewName = model.getFieldParam(field, 'view') ||
this.getFieldManager().getViewName(type);
this.createView(model.id + '-' + field, viewName, {
model: model,
@@ -249,14 +252,13 @@ define('views/record/merge', 'view', function (Dep) {
mode: 'detail',
readOnly: true,
});
}.bind(this));
}.bind(this));
});
});
this.hasCreatedAt = this.getMetadata().get(['entityDefs', this.scope, 'fields', 'createdAt']);
if (this.hasCreatedAt) {
this.models.forEach(function (model) {
this.models.forEach((model) => {
this.createView(model.id + '-' + 'createdAt', 'views/fields/datetime', {
model: model,
el: '.merge [data-id="'+model.id+'"] .field[data-name="createdAt"]',
@@ -266,14 +268,14 @@ define('views/record/merge', 'view', function (Dep) {
mode: 'detail',
readOnly: true,
});
}, this);
});
}
},
getDataList: function () {
var dataList = [];
this.models.forEach(function (model, i) {
this.models.forEach((model, i) => {
var o = {};
o.id = model.id;
@@ -281,7 +283,7 @@ define('views/record/merge', 'view', function (Dep) {
o.createdAtViewName = model.id + '-' + 'createdAt';
dataList.push(o);
}, this);
});
return dataList;
},
@@ -0,0 +1,54 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 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.
************************************************************************/
define('views/settings/fields/outbound-email-from-address', 'views/fields/varchar', function (Dep) {
return Dep.extend({
useAutocompleteUrl: true,
getAutocompleteUrl: function (q) {
return 'InboundEmail?searchParams=' + JSON.stringify({
select: ['emailAddress'],
maxSize: 7,
where: [
{
type: 'startsWith',
attribute: 'emailAddress',
value: q,
},
{
type: 'isTrue',
attribute: 'useSmtp',
},
],
});
},
});
});

Some files were not shown because too many files have changed in this diff Show More