Compare commits

..

82 Commits

Author SHA1 Message Date
Yuri Kuznetsov d1997089e4 v 2020-10-26 13:06:11 +02:00
Yuri Kuznetsov 57dd0177f9 fix export 2020-10-25 18:56:57 +02:00
Yuri Kuznetsov ba76e5ee3d fix language manager 2020-10-25 18:54:34 +02:00
Yuri Kuznetsov 76c93b842c case close reject actions only if available in status list 2020-10-23 10:25:26 +03:00
Yuri Kuznetsov e73bff5ddf v 2020-10-22 12:14:40 +03:00
Yuri Kuznetsov 7d5c0d754e fix upgrade script 2020-10-22 11:47:59 +03:00
Taras Machyshyn e08a74b129 Bug fixes 2020-10-22 11:30:00 +03:00
Taras Machyshyn be07aafaf4 Fix 2020-10-21 15:33:42 +03:00
Taras Machyshyn e2bf27e524 Fix 2020-10-21 12:00:47 +03:00
Yuri Kuznetsov b0157adbe2 cs fix 2020-10-21 09:40:34 +03:00
Yuri Kuznetsov 239593afb5 fix mass actions 2020-10-21 09:31:42 +03:00
Yuri Kuznetsov b97d764588 fix error msg 2020-10-19 15:30:36 +03:00
Yuri Kuznetsov bf8e7af984 export fixes 2020-10-19 15:19:24 +03:00
Yuri Kuznetsov bf68c475e5 email queue items filters email 2020-10-19 13:37:06 +03:00
Yuri Kuznetsov 29624239ba search queue items 2020-10-19 13:35:43 +03:00
Yuri Kuznetsov 4cb3c4918b campaign log search by email address 2020-10-19 12:59:36 +03:00
Yuri Kuznetsov 26fa0d65e1 fix campaign log search 2020-10-19 12:57:17 +03:00
Yuri Kuznetsov 958baed3df fix email invalid on bounced 2020-10-19 12:35:48 +03:00
Yuri Kuznetsov 40d6fb7565 fix export 2020-10-15 19:40:37 +03:00
Yuri Kuznetsov a7551b7dbf fix request getContentType 2020-10-15 17:35:58 +03:00
Yuri Kuznetsov 9ed33c4dc4 remove self assign in portal 2020-10-14 16:23:02 +03:00
Yuri Kuznetsov 6b18fbe8f7 select manager fix 2020-10-14 15:48:52 +03:00
Yuri Kuznetsov 53b9880ea8 entry point changes 2020-10-14 15:15:09 +03:00
Yuri Kuznetsov 62538664df Merge branch 'master' of github.com:espocrm/espocrm 2020-10-14 13:17:56 +03:00
Yuri Kuznetsov f678fb16cc ouauth callback changes 2020-10-14 13:17:46 +03:00
Yuri Kuznetsov fe09aa1cd9 cs fix 2020-10-13 11:01:52 +03:00
Yuri Kuznetsov 710decc610 fix htmlizer and email template no id 2020-10-13 10:47:37 +03:00
Yuri Kuznetsov 5d481d3528 email auto reply error log 2020-10-13 10:47:27 +03:00
Yuri Kuznetsov 8bcd3b72c5 hook fix 2020-10-13 09:56:13 +03:00
Yuri Kuznetsov 0a0da810e9 application state changes 2020-10-13 09:56:04 +03:00
Yuri Kuznetsov e630c046fe Merge branch 'hotfix/5.9.5' 2020-10-12 14:01:49 +03:00
Yuri Kuznetsov 2857b5c53d fix import button 2020-10-12 14:00:25 +03:00
Yuri Kuznetsov 0965c61f55 fix reminder submition 2020-10-12 10:14:17 +03:00
Yuri Kuznetsov 9dd318b176 fix label 2020-10-10 16:08:58 +03:00
Yuri Kuznetsov f8beca720c label fixes 2020-10-10 16:06:57 +03:00
Yuri Kuznetsov d8f58dcc1d upgrade cleanup 2020-10-09 14:58:34 +03:00
Yuri Kuznetsov 32aaaa350e v 2020-10-09 14:37:50 +03:00
Yuri Kuznetsov 0f6d1c6817 fix diff: ignore upgrades 2020-10-09 13:56:41 +03:00
Yuri Kuznetsov 8c5a342a5a fix diff 2020-10-09 13:53:07 +03:00
Yuri Kuznetsov 257401898b upgrade cleanup files and fix msg 2020-10-09 12:24:24 +03:00
Yuri Kuznetsov e7ff3f903a fix email modal 2020-10-08 14:05:28 +03:00
Taras Machyshyn 6f82638b5d Upgrade script 2020-10-07 17:46:08 +03:00
Taras Machyshyn 0625ce51c4 Upgrade script 2020-10-07 17:26:11 +03:00
Taras Machyshyn f5670436d0 Merge branch 'master' of https://github.com/espocrm/espocrm 2020-10-07 17:19:05 +03:00
Taras Machyshyn 5d20a56ac8 Upgrade script 2020-10-07 17:18:34 +03:00
Yuri Kuznetsov 020cf9713a Merge branch 'master' of https://github.com/espocrm/espocrm 2020-10-07 16:49:33 +03:00
Yuri Kuznetsov fe1361fc9c portal action list restriction 2020-10-07 16:49:01 +03:00
Taras Machyshyn 36621be496 Merge branch 'master' of https://github.com/espocrm/espocrm 2020-10-07 15:51:31 +03:00
Taras Machyshyn 25690030d1 Installation fixes 2020-10-07 15:50:57 +03:00
Yuri Kuznetsov fcd6be3a0c fix password recovery 2020-10-07 15:39:03 +03:00
Yuri Kuznetsov ad8dfa487c fix convert lead 2020-10-07 15:13:39 +03:00
Yuri Kuznetsov 7f6f54628d cs fix 2020-10-07 15:04:42 +03:00
Taras Machyshyn 8ea0b072c6 Installation fixes 2020-10-07 14:22:21 +03:00
Yuri Kuznetsov 0df6248226 getPhpSafeContents for unix 2020-10-07 13:12:55 +03:00
Yuri Kuznetsov 26a132658e date after before validation autocomplete 2020-10-07 12:42:51 +03:00
Yuri Kuznetsov d02529ee86 Merge branch 'hotfix/5.9.5' 2020-10-07 11:02:01 +03:00
Yuri Kuznetsov 0cec0fef51 fix image orientation 2020-10-07 11:01:32 +03:00
Yuri Kuznetsov e56f6ab75d fix image orientation 2020-10-07 10:55:39 +03:00
Yuri Kuznetsov a4980aa8f2 cs fix 2020-10-07 10:53:21 +03:00
Yuri Kuznetsov fd219b49be calendar modal fix 2020-10-06 13:44:35 +03:00
Yuri Kuznetsov b90579aadc cs fix 2020-10-06 13:44:18 +03:00
Yuri Kuznetsov 0b39d7f6b6 cs fix 2020-10-06 12:13:17 +03:00
Yuri Kuznetsov a44ce06162 Merge branch 'master' of https://github.com/espocrm/espocrm 2020-10-06 12:02:35 +03:00
Yuri Kuznetsov f1eb2ab5d5 cs fix 2020-10-06 12:00:48 +03:00
Yuri Kuznetsov 9a965bfba4 v 2020-10-05 15:27:48 +03:00
Yuri Kuznetsov 7349f37786 fix admin panel 2020-10-05 12:21:11 +03:00
Yuri Kuznetsov 3d03dfaef7 css fix 2020-10-03 19:01:19 +03:00
Yuri Kuznetsov 5bf88eaff1 categories tree improvements 2020-10-03 18:59:56 +03:00
Yuri Kuznetsov f3c4b2ba39 image preview improvements 2020-10-03 17:24:05 +03:00
Yuri Kuznetsov 92f6fb4588 settings tooltips 2020-10-03 12:27:54 +03:00
Yuri Kuznetsov 2d6d7fd618 list tree css fix 2020-10-02 14:57:26 +03:00
Yuri Kuznetsov 3a1d997d5a orm cleanup 2020-10-01 20:18:35 +03:00
Yuri Kuznetsov 5bbb259c95 orm fix order list 2020-10-01 20:16:32 +03:00
Yuri Kuznetsov b8e44767b3 upgrade real estate compatibility check 2020-10-01 16:27:46 +03:00
Yuri Kuznetsov 6b486beb5c fix autoload 2020-10-01 14:25:12 +03:00
Yuri Kuznetsov aab1265e4e data cache get safe contents 2020-10-01 14:08:03 +03:00
Yuri Kuznetsov 676181a4be fix ldap 2020-10-01 13:32:06 +03:00
Yuri Kuznetsov 839e5b7c74 data cache support onlt array and stdclass 2020-10-01 12:51:25 +03:00
Yuri Kuznetsov aff7003ceb cs fix 2020-10-01 12:01:51 +03:00
Yuri Kuznetsov 66aa60f303 file rename in loop for windows 2020-10-01 11:44:01 +03:00
Yuri Kuznetsov b196811cf9 fix notice 2020-10-01 10:24:36 +03:00
Yuri Kuznetsov 3fdddadd53 cs fix 2020-09-30 17:42:07 +03:00
82 changed files with 1686 additions and 545 deletions
+8 -2
View File
@@ -138,7 +138,13 @@ class RequestWrapper implements ApiRequest
public function getContentType() : ?string
{
return $this->getHeader('Content-Type');
if (!$this->hasHeader('Content-Type')) {
return null;
}
$contentType = $this->request->getHeader('Content-Type')[0];
return strtolower($contentType);
}
public function getBodyContents() : ?string
@@ -163,7 +169,7 @@ class RequestWrapper implements ApiRequest
{
$contents = $this->getBodyContents();
if (strtolower($this->getContentType()) === 'application/json' && $contents) {
if ($this->getContentType() === 'application/json' && $contents) {
$this->parsedBody = json_decode($contents);
if (is_array($this->parsedBody)) {
@@ -29,9 +29,8 @@
namespace Espo\Core\ApplicationRunners;
use Espo\Core\Exceptions\Error;
use Espo\Core\{
Exceptions\Error,
InjectableFactory,
EntryPointManager,
ApplicationUser,
@@ -39,7 +38,6 @@ use Espo\Core\{
Portal\Application as PortalApplication,
Utils\Route,
Utils\ClientManager,
Authentication\Authentication,
Api\Auth as ApiAuth,
Api\ErrorOutput as ApiErrorOutput,
@@ -48,18 +46,12 @@ use Espo\Core\{
};
use Slim\{
App as SlimApp,
Factory\AppFactory as SlimAppFactory,
};
use Psr\Http\{
Message\ResponseInterface as Psr7Response,
Message\ServerRequestInterface as Psr7Request,
Server\RequestHandlerInterface as Psr7RequestHandler,
ResponseEmitter,
Factory\ServerRequestCreatorFactory,
Psr7\Response,
};
use StdClass;
use Exception;
/**
@@ -113,30 +105,19 @@ class EntryPoint implements ApplicationRunner
}
}
$slim = SlimAppFactory::create();
$request = (ServerRequestCreatorFactory::create())->createServerRequestFromGlobals();
$slim->setBasePath(Route::detectBasePath());
if ($request->getMethod() !== 'GET') {
throw new Error("Only GET request allowed for entry points.");
}
$slim->add(
function (Psr7Request $request, Psr7RequestHandler $handler) use (
$entryPoint, $data, $authRequired, $authNotStrict, $slim
) : Psr7Response {
$requestWrapped = new RequestWrapper($request, $slim->getBasePath());
$responseWrapped = new ResponseWrapper($handler->handle($request));
$requestWrapped = new RequestWrapper($request, Route::detectBasePath());
$this->processRequest($entryPoint, $requestWrapped, $responseWrapped, $data, $authRequired, $authNotStrict);
$responseWrapped = new ResponseWrapper(new Response());
return $responseWrapped->getResponse();
}
);
$this->processRequest($entryPoint, $requestWrapped, $responseWrapped, $data, $authRequired, $authNotStrict);
$route = Route::detectEntryPointRoute();
$slim->get($route, function (Psr7Request $request, Psr7Response $response) : Psr7Response {
return $response;
});
$slim->run();
(new ResponseEmitter())->emit($responseWrapped->getResponse());
}
protected function processRequest(
+36 -9
View File
@@ -55,11 +55,13 @@ class ApplicationState
}
/**
* Get portal ID (if an applicaition is portal).
* Get a portal ID (if an applicaition is portal).
*/
public function getPortalId() : string
{
if (!$this->isPortal()) throw new Error("Can't get portal ID for non-portal application.");
if (!$this->isPortal()) {
throw new Error("Can't get portal ID for non-portal application.");
}
return $this->getPortal()->id;
}
@@ -69,7 +71,9 @@ class ApplicationState
*/
public function getPortal() : PortalEntity
{
if (!$this->isPortal()) throw new Error("Can't get portal for non-portal application.");
if (!$this->isPortal()) {
throw new Error("Can't get portal for non-portal application.");
}
return $this->container->get('portal');
}
@@ -83,21 +87,38 @@ class ApplicationState
}
/**
* Get current logged user. If no auth is applied, then system user will be returned.
* Get a current logged user. If no auth is applied, then the system user will be returned.
*/
public function getUser() : UserEntity
{
if (!$this->hasUser()) throw new Error("User is not yet available.");
if (!$this->hasUser()) {
throw new Error("User is not yet available.");
}
return $this->container->get('user');
}
/**
* Get an ID of a current logged user. If no auth is applied, then the system user will be returned.
*/
public function getUserId() : string
{
return $this->getUser()->id;
}
/**
* Whether a user is logged.
*/
public function isLogged() : bool
{
if (!$this->container->has('user')) return false;
if ($this->getUser()->isSystem()) return false;
if (!$this->container->has('user')) {
return false;
}
if ($this->getUser()->isSystem()) {
return false;
}
return true;
}
@@ -106,7 +127,10 @@ class ApplicationState
*/
public function isAdmin() : bool
{
if (!$this->isLogged()) return false;
if (!$this->isLogged()) {
return false;
}
return $this->getUser()->isAdmin();
}
@@ -116,7 +140,10 @@ class ApplicationState
*/
public function isApi() : bool
{
if (!$this->isLogged()) return false;
if (!$this->isLogged()) {
return false;
}
return $this->getUser()->isApi();
}
}
@@ -68,14 +68,14 @@ class LDAP extends Espo
Config $config,
EntityManager $entityManager,
PasswordHash $passwordHash,
Language $language,
Language $defaultLanguage,
ApplicationUser $applicationUser,
bool $isPortal = false
) {
$this->config = $config;
$this->entityManager = $entityManager;
$this->passwordHash = $passwordHash;
$this->language = $language;
$this->language = $defaultLanguage;
$this->applicationUser = $applicationUser;
$this->isPortal = $isPortal;
@@ -51,6 +51,8 @@ class ContainerConfiguration
public function getLoaderClassName(string $name) : ?string
{
$className = null;
try {
$className = $this->metadata->get(['app', 'containerServices', $name, 'loaderClassName']);
+1 -1
View File
@@ -120,7 +120,7 @@ class Htmlizer
$relationList = $entity->getRelationList();
if (!$skipLinks && $level === 0 && $this->entityManager) {
if (!$skipLinks && $level === 0 && $this->entityManager && $entity->id) {
foreach ($relationList as $relation) {
$collection = null;
+1 -1
View File
@@ -274,7 +274,7 @@ class Recovery
'isSystem' => true,
]);
if (!$this->emailAddress->hasSystemSmtp()) {
if (!$this->emailSender->hasSystemSmtp()) {
$sender->withSmtpParams([
'server' => $config->get('internalSmtpServer'),
'port' => $config->get('internalSmtpPort'),
+25 -5
View File
@@ -2368,7 +2368,9 @@ class SelectManager
protected function textFilter($textFilter, array &$result, $noFullText = false)
{
$fieldDefs = $this->getSeed()->getAttributes();
$fieldList = $this->getTextFilterFieldList();
$group = [];
$textFilterContainsMinLength = $this->getConfig()->get('textFilterContainsMinLength', self::MIN_LENGTH_FOR_CONTENT_SEARCH);
@@ -2401,6 +2403,7 @@ class SelectManager
$textFilterForFullTextSearch = str_replace('%', '*', $textFilterForFullTextSearch);
$skipFullTextSearch = false;
if (!$forceFullTextSearch) {
if (mb_strpos($textFilterForFullTextSearch, '*') === 0) {
$skipFullTextSearch = true;
@@ -2409,9 +2412,12 @@ class SelectManager
}
}
if ($noFullText) $skipFullTextSearch = true;
if ($noFullText) {
$skipFullTextSearch = true;
}
$fullTextSearchData = null;
if (!$skipFullTextSearch) {
$fullTextSearchData = $this->getFullTextSearchDataForTextFilter($textFilterForFullTextSearch, !$useFullTextSearch);
}
@@ -2419,6 +2425,7 @@ class SelectManager
$fullTextGroup = [];
$fullTextSearchFieldList = [];
if ($fullTextSearchData) {
if ($this->fullTextRelevanceThreshold) {
$fullTextGroup[] = [$fullTextSearchData['where'] . '>=' => $this->fullTextRelevanceThreshold];
@@ -2434,12 +2441,15 @@ class SelectManager
$orderTypeMap = [
'combined' => self::FT_ORDER_COMBINTED,
'relavance' => self::FT_ORDER_RELEVANCE,
'relevance' => self::FT_ORDER_RELEVANCE,
'original' => self::FT_ORDER_ORIGINAL,
];
$mOrderType = $this->getMetadata()->get(['entityDefs', $this->entityType, 'collection', 'fullTextSearchOrderType']);
if ($mOrderType) $fullTextOrderType = $orderTypeMap[$mOrderType];
if ($mOrderType) {
$fullTextOrderType = $orderTypeMap[$mOrderType];
}
if (!isset($result['orderBy']) || $fullTextOrderType === self::FT_ORDER_RELEVANCE) {
$result['orderBy'] = [[$relevanceExpression, 'desc']];
@@ -2468,9 +2478,14 @@ class SelectManager
foreach ($fieldList as $field) {
if ($useFullTextSearch) {
if (in_array($field, $fullTextSearchFieldList)) continue;
if (in_array($field, $fullTextSearchFieldList)) {
continue;
}
}
if ($forceFullTextSearch) {
continue;
}
if ($forceFullTextSearch) continue;
$seed = $this->getSeed();
@@ -2478,12 +2493,16 @@ class SelectManager
if (strpos($field, '.') !== false) {
list($link, $foreignField) = explode('.', $field);
$foreignEntityType = $seed->getRelationParam($link, 'entity');
$seed = $this->getEntityManager()->getEntity($foreignEntityType);
$this->addLeftJoin($link, $result);
if ($seed->getRelationParam($link, 'type') === $seed::HAS_MANY) {
$this->setDistinct(true, $result);
}
$attributeType = $seed->getAttributeType($foreignField);
} else {
$attributeType = $seed->getAttributeType($field);
@@ -2493,6 +2512,7 @@ class SelectManager
if (is_numeric($textFilter)) {
$group[$field] = intval($textFilter);
}
continue;
}
@@ -169,6 +169,7 @@ class AdminNotificationManager
$extension = $this->getEntityManager()->getRepository('Extension')
->select(['version'])
->where([
'name' => $extensionName,
'isInstalled' => true,
])
->order('createdAt', true)
+6 -3
View File
@@ -33,6 +33,7 @@ use Espo\Core\{
Exceptions\Error,
Utils\Autoload\Loader,
Utils\DataCache,
Utils\File\Manager as FileManager,
};
use Exception;
@@ -52,14 +53,16 @@ class Autoload
protected $config;
protected $metadata;
protected $dataCache;
protected $fileManager;
protected $loader;
public function __construct(Config $config, Metadata $metadata, DataCache $dataCache, Loader $loader)
{
public function __construct(
Config $config, Metadata $metadata, DataCache $dataCache, FileManager $fileManager, Loader $loader
) {
$this->config = $config;
$this->metadata = $metadata;
$this->dataCache = $dataCache;
$this->fileManager = $fileManager;
$this->loader = $loader;
}
+19 -12
View File
@@ -63,35 +63,33 @@ class DataCache
*
* @throws Error if is not cached.
*
* @return ?array|StdClass|int|float|string
* @return ?array|StdClass
*/
public function get(string $key)
{
$cacheFile = $this->getCacheFile($key);
$result = $this->fileManager->getPhpContents($cacheFile);
$data = $this->fileManager->getPhpSafeContents($cacheFile);
if ($result === false) {
if ($data === false) {
throw new Error("Could not get '{$key}'.");
}
return $result;
if (! $this->checkDataIsValid($data)) {
throw new Error("Bad data fetched from cache by key '{$key}'.");
}
return $data;
}
/**
* Store in cache.
*
* @param ?array|StdClass|int|float|string $data
* @param ?array|StdClass $data
*/
public function store(string $key, $data)
{
if (
! is_array($data) &&
! $data instanceof StdClass &&
! is_int($data) &&
! is_float($data) &&
! is_int($data)
) {
if (! $this->checkDataIsValid($data)) {
throw new InvalidArgumentException("Bad cache data type.");
}
@@ -104,6 +102,15 @@ class DataCache
}
}
protected function checkDataIsValid($data)
{
$isInvalid =
! is_array($data) &&
! $data instanceof StdClass;
return ! $isInvalid;
}
protected function getCacheFile(string $key) : string
{
if (
+95 -42
View File
@@ -47,6 +47,14 @@ class Manager
protected $tmpDir = 'data/tmp';
const RENAME_RETRY_NUMBER = 10;
const RENAME_RETRY_INTERVAL = 0.1;
const GET_SAFE_CONTENTS_RETRY_NUMBER = 10;
const GET_SAFE_CONTENTS_RETRY_INTERVAL = 0.1;
public function __construct(?Config $config = null)
{
$params = null;
@@ -175,10 +183,10 @@ class Manager
}
/**
* Get PHP array from PHP file.
* Get data from PHP file.
*
* @param string|array $path
* @return array|object|bool
* @return mixed|bool
*/
public function getPhpContents($path)
{
@@ -193,6 +201,39 @@ class Manager
return false;
}
/**
* Get array or StdClass data from PHP file.
* If a file is not yet written, it will wait until it's ready.
*
* @return array|StdClass
*/
public function getPhpSafeContents(string $path)
{
if (!file_exists($path)) {
throw new Error("Can't get contents from non-existing file '{$path}'.");
}
if (!strtolower(substr($path, -4)) == '.php') {
throw new Error("Only PHP file are allowed for getting contents.");
}
$counter = 0;
while ($counter < self::GET_SAFE_CONTENTS_RETRY_NUMBER) {
$data = include($path);
if (is_array($data) || $data instanceof StdClass) {
return $data;
}
usleep(self::GET_SAFE_CONTENTS_RETRY_INTERVAL * 1000000);
$counter ++;
}
throw new Error("Bad data stored in file '{$path}'.");
}
/**
* Write data to a file.
*
@@ -266,6 +307,10 @@ class Manager
$result = rename($tmpPath, $path);
if (!$result && stripos(\PHP_OS, 'WIN') === 0) {
$result = $this->renameInLoop($tmpPath, $path);
}
if ($this->isFile($tmpPath)) {
$this->removeFile($tmpPath);
}
@@ -273,6 +318,29 @@ class Manager
return $result;
}
protected function renameInLoop(string $source, string $destination) : bool
{
$counter = 0;
while ($counter < self::RENAME_RETRY_NUMBER) {
if (!$this->isWritable($destination)) {
break;
}
$result = rename($source, $destination);
if ($result !== false) {
return true;
}
usleep(self::RENAME_RETRY_INTERVAL * 1000000);
$counter ++;
}
return false;
}
/**
* Save PHP content to file.
*
@@ -288,8 +356,8 @@ class Manager
*
* @param string | array $path
* @param string $data
* @param integer $flags
* @param resource $context
* @param integer $flags
* @param resource $context
*
* @return bool
*/
@@ -377,8 +445,8 @@ class Manager
/**
* Unset some element of content data.
*
* @param string | array $path
* @param array | string $unsets
* @param string | array $path
* @param array | string $unsets
* @return bool
*/
public function unsetContents($path, $unsets, $isJSON = true)
@@ -412,8 +480,8 @@ class Manager
/**
* Concat paths
* @param string | array $paths Ex. array('pathPart1', 'pathPart2', 'pathPart3')
* Concat paths.
* @param string | array $paths Ex. array('pathPart1', 'pathPart2', 'pathPart3')
* @return string
*/
protected function concatPaths($paths)
@@ -432,11 +500,11 @@ class Manager
}
/**
* Create a new dir
* Create a new dir.
*
* @param string | array $path
* @param int $permission - ex. 0755
* @param bool $recursive
* @param string | array $path
* @param int $permission - ex. 0755
* @param bool $recursive
*
* @return bool
*/
@@ -487,11 +555,11 @@ class Manager
* Ex. $sourcePath = 'data/uploads/extensions/file.json',
* $destPath = 'data/uploads/backup', result will be data/uploads/backup/data/uploads/backup/file.json.
*
* @param string $sourcePath
* @param string $destPath
* @param boolean $recursively
* @param array $fileList - list of files that should be copied
* @param boolean $copyOnlyFiles - copy only files, instead of full path with directories,
* @param string $sourcePath
* @param string $destPath
* @param boolean $recursively
* @param array $fileList - list of files that should be copied
* @param boolean $copyOnlyFiles - copy only files, instead of full path with directories,
* Ex. $sourcePath = 'data/uploads/extensions/file.json',
* $destPath = 'data/uploads/backup', result will be 'data/uploads/backup/file.json'
* @return boolen
@@ -510,7 +578,6 @@ class Manager
$permissionDeniedList = [];
foreach ($fileList as $file) {
if ($copyOnlyFiles) {
$file = pathinfo($file, PATHINFO_BASENAME);
}
@@ -667,7 +734,6 @@ class Manager
*
* @param string $dirPath - directory path.
* @param bool $removeWithDir - if remove with directory.
*
* @return bool
*/
public function removeInDir($dirPath, $removeWithDir = false)
@@ -699,8 +765,8 @@ class Manager
/**
* Remove items (files or directories).
*
* @param string | array $items
* @param string $dirPath
* @param string | array $items
* @param string $dirPath
* @return boolean
*/
public function remove($items, $dirPath = null, $removeEmptyDirs = false)
@@ -774,8 +840,8 @@ class Manager
/**
* Check if $dirname is directory.
*
* @param string $dirname
* @param string $basePath
* @param string $dirname
* @param string $basePath
*
* @return boolean
*/
@@ -893,10 +959,10 @@ class Manager
}
/**
* Get parent dir name/path
* Get parent dir name/path.
*
* @param string $path
* @param boolean $isFullPath
* @param string $path
* @param boolean $isFullPath
* @return string
*/
public function getParentDirName($path, $isFullPath = true)
@@ -907,7 +973,6 @@ class Manager
/**
* Return content of PHP file.
*
* @param string $varName - name of variable which contains the content.
* @param array $content
*
* @return string | false
@@ -961,12 +1026,8 @@ class Manager
/**
* Check if $paths are writable. Permission denied list are defined in getLastPermissionDeniedList().
*
* @param array $paths
*
* @return boolean
*/
public function isWritableList(array $paths)
public function isWritableList(array $paths) : bool
{
$permissionDeniedList = [];
@@ -1002,12 +1063,8 @@ class Manager
/**
* Check if $path is writable.
*
* @param string |array $path
*
* @return boolean
*/
public function isWritable($path)
public function isWritable(string $path) : bool
{
$existFile = $this->getExistsPath($path);
@@ -1016,12 +1073,8 @@ class Manager
/**
* Check if $path is writable.
*
* @param string | array $path
*
* @return boolean
*/
public function isReadable($path)
public function isReadable(string $path) : bool
{
$existFile = $this->getExistsPath($path);
@@ -34,13 +34,15 @@ use Espo\Core\EntryPoints\{
NoAuth,
};
use Espo\Core\Api\Request;
class OauthCallback implements EntryPoint
{
use NoAuth;
public function run()
public function run(Request $request)
{
echo "EspoCRM rocks! If this window is not closed automatically, it's probable that URL you use to access ".
echo "If this window is not closed automatically, it's probable that URL you use to access ".
"EspoCRM doesn't match URL specified at Administration > Settings > Site URL.";
}
}
@@ -34,17 +34,20 @@ use Espo\ORM\Entity;
use Espo\Core\{
Utils\Config,
ORM\EntityManager,
ApplicationState,
};
class AssignmentEmailNotification
{
protected $config;
protected $entityManager;
protected $applicationState;
public function __construct(Config $config, EntityManager $entityManager)
public function __construct(Config $config, EntityManager $entityManager, ApplicationState $applicationState)
{
$this->config = $config;
$this->entityManager = $entityManager;
$this->applicationState = $applicationState;
}
public function afterSave(Entity $entity, array $options = [])
@@ -67,17 +70,25 @@ class AssignmentEmailNotification
if ($entity->has('assignedUsersIds')) {
$userIdList = $entity->getLinkMultipleIdList('assignedUsers');
$fetchedAssignedUserIdList = $entity->getFetched('assignedUsersIds');
if (!is_array($fetchedAssignedUserIdList)) {
$fetchedAssignedUserIdList = [];
}
foreach ($userIdList as $userId) {
if (in_array($userId, $fetchedAssignedUserIdList)) continue;
if (!$this->isNotSelfAssignment($entity, $userId)) continue;
if (in_array($userId, $fetchedAssignedUserIdList)) {
continue;
}
if (!$this->isNotSelfAssignment($entity, $userId)) {
continue;
}
$this->createJob($entity, $userId);
}
} else {
$userId = $entity->get('assignedUserId');
if (!empty($userId) &&
$entity->isAttributeChanged('assignedUserId') && $this->isNotSelfAssignment($entity, $userId)
) {
@@ -96,26 +107,29 @@ class AssignmentEmailNotification
$isNotSelfAssignment = $assignedUserId !== $entity->get('modifiedById');
}
} else {
$isNotSelfAssignment = $assignedUserId !== $this->getUser()->id;
$isNotSelfAssignment = $assignedUserId !== $this->applicationState->getUserId();
}
return $isNotSelfAssignment;
}
protected function createJob(Entity $entity, $userId)
{
$job = $this->entityManager->getEntity('Job');
$job->set([
'serviceName' => 'EmailNotification',
'methodName' => 'notifyAboutAssignmentJob',
'data' => [
'userId' => $userId,
'assignerUserId' => $this->getUser()->id,
'assignerUserId' => $this->applicationState->getUserId(),
'entityId' => $entity->id,
'entityType' => $entity->getEntityType(),
],
'executeTime' => date('Y-m-d H:i:s'),
'queue' => 'e0',
]);
$this->entityManager->saveEntity($job);
}
}
@@ -36,6 +36,9 @@ use Espo\Core\{
Jobs\Job,
};
use Throwable;
use DateTime;
class SubmitPopupReminders implements Job
{
const REMINDER_PAST_HOURS = 24;
@@ -53,21 +56,27 @@ class SubmitPopupReminders implements Job
public function run()
{
if (!$this->config->get('useWebSocket')) return;
if (!$this->config->get('useWebSocket')) {
return;
}
$dt = new \DateTime();
$dt = new DateTime();
$now = $dt->format('Y-m-d H:i:s');
$pastHours = $this->config->get('reminderPastHours', self::REMINDER_PAST_HOURS);
$nowShifted = $dt->modify('-' . $pastHours . ' hours')->format('Y-m-d H:i:s');
$reminderList = $this->entityManager->getRepository('Reminder')->where([
'type' => 'Popup',
'remindAt<=' => $now,
'startAt>' => $nowShifted,
'isSubmitted' => false,
])->find();
$reminderList = $this->entityManager
->getRepository('Reminder')
->where([
'type' => 'Popup',
'remindAt<=' => $now,
'startAt>' => $nowShifted,
'isSubmitted' => false,
])
->find();
$submitData = [];
@@ -78,6 +87,7 @@ class SubmitPopupReminders implements Job
if (!$userId || !$entityType || !$entityId) {
$this->deleteReminder($reminder);
continue;
}
@@ -85,19 +95,24 @@ class SubmitPopupReminders implements Job
if (!$entity) {
$this->deleteReminder($reminder);
continue;
}
if ($entity->hasLinkMultipleField('users')) {
$entity->loadLinkMultipleField('users', ['status' => 'acceptanceStatus']);
$status = $entity->getLinkMultipleColumn('users', 'status', $userId);
if ($status === 'Declined') {
$this->deleteReminder($reminder);
continue;
}
}
$dateAttribute = 'dateStart';
if ($entityType === 'Task') {
$dateAttribute = 'dateEnd';
}
@@ -125,10 +140,10 @@ class SubmitPopupReminders implements Job
foreach ($submitData as $userId => $list) {
try {
$this->getContainer()->get('webSocketSubmission')->submit('popupNotifications.event', $userId, (object) [
$this->webSocketSubmission->submit('popupNotifications.event', $userId, (object) [
'list' => $list
]);
} catch (\Throwable $e) {
} catch (Throwable $e) {
$GLOBALS['log']->error('Job SubmitPopupReminders: [' . $e->getCode() . '] ' .$e->getMessage());
}
}
@@ -38,7 +38,8 @@
},
"labels": {
"Create Task": "Create Task",
"Complete": "Complete"
"Complete": "Complete",
"overdue": "overdue"
},
"presetFilters": {
"actual": "Actual",
@@ -2,5 +2,6 @@
"massEmail",
"status",
"target",
"sentAt"
"sentAt",
"emailAddress"
]
@@ -78,6 +78,7 @@
}
},
"collection": {
"textFilterFields": ["queueItem.id", "queueItem.emailAddress"],
"orderBy": "createdAt",
"order": "desc"
},
@@ -45,6 +45,7 @@
}
},
"collection": {
"textFilterFields": ["id", "emailAddress"],
"orderBy": "createdAt",
"order": "desc"
}
@@ -4,6 +4,7 @@
"tab": false,
"acl": "recordAllTeamNo",
"aclPortal": "recordAllNo",
"aclPortalActionList": ["read"],
"module": "Crm",
"customizable": true,
"importable": false,
@@ -4,6 +4,7 @@
"tab": true,
"acl": "recordAllTeamNo",
"aclPortal": "recordAllNo",
"aclPortalActionList": ["read"],
"module": "Crm",
"customizable": true,
"importable": true,
@@ -4,6 +4,7 @@
"tab": false,
"acl": "recordAllTeamNo",
"aclPortal": "recordAllNo",
"aclPortalActionList": ["read"],
"module": "Crm",
"customizable": true,
"importable": false,
+157 -81
View File
@@ -31,14 +31,17 @@ namespace Espo\Modules\Crm\Services;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\ConflictSilent;
use Espo\ORM\Entity;
use Espo\Modules\Crm\Entities\Lead as LeadEntity;
use Espo\Core\Templates\Services\Person as PersonService;
use Espo\Core\Di;
class Lead extends \Espo\Core\Templates\Services\Person implements
class Lead extends PersonService implements
Di\FieldUtilAware
{
@@ -52,31 +55,36 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
{
if (!empty($data->emailId)) {
$email = $this->getEntityManager()->getEntity('Email', $data->emailId);
if ($email && !$email->get('parentId') && $this->getAcl()->check($email)) {
$email->set(array(
$email->set([
'parentType' => 'Lead',
'parentId' => $entity->id
));
]);
$this->getEntityManager()->saveEntity($email);
}
}
if ($entity->get('campaignId')) {
$campaign = $this->getEntityManager()->getEntity('Campaign', $entity->get('campaignId'));
if ($campaign) {
$log = $this->getEntityManager()->getEntity('CampaignLogRecord');
$log->set(array(
'action' => 'Lead Created',
'actionDate' => date('Y-m-d H:i:s'),
'parentType' => 'Lead',
'parentId' => $entity->id,
'campaignId' => $campaign->id
));
$this->getEntityManager()->saveEntity($log);
}
$campaign = $this->getEntityManager()->getEntity('Campaign', $entity->get('campaignId'));
if ($campaign) {
$log = $this->getEntityManager()->getEntity('CampaignLogRecord');
$log->set([
'action' => 'Lead Created',
'actionDate' => date('Y-m-d H:i:s'),
'parentType' => 'Lead',
'parentId' => $entity->id,
'campaignId' => $campaign->id,
]);
$this->getEntityManager()->saveEntity($log);
}
}
}
public function getConvertAttributes($id)
public function getConvertAttributes(string $id)
{
$lead = $this->getEntity($id);
@@ -84,7 +92,7 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
throw new Forbidden();
}
$data = array();
$data = [];
$entityList = $this->getMetadata()->get('entityDefs.Lead.convertEntityList', []);
@@ -93,22 +101,30 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
$convertFieldsDefs = $this->getMetadata()->get('entityDefs.Lead.convertFields', array());
foreach ($entityList as $entityType) {
if (!$this->getAcl()->checkScope($entityType, 'edit')) continue;
if (!$this->getAcl()->checkScope($entityType, 'edit')) {
continue;
}
$attributes = array();
$attributes = [];
$target = $this->getEntityManager()->getEntity($entityType);
$fieldMap = array();
$fieldMap = [];
$fieldList = array_keys($this->getMetadata()->get('entityDefs.Lead.fields', []));
$fieldList = array_keys($this->getMetadata()->get('entityDefs.Lead.fields', array()));
foreach ($fieldList as $field) {
if (!$this->getMetadata()->get('entityDefs.'.$entityType.'.fields.' . $field)) continue;
if (!$this->getMetadata()->get('entityDefs.'.$entityType.'.fields.' . $field)) {
continue;
}
if (
$this->getMetadata()->get(['entityDefs', $entityType, 'fields', $field, 'type'])
!==
$this->getMetadata()->get(['entityDefs', 'Lead', 'fields', $field, 'type'])
) continue;
) {
continue;
}
$fieldMap[$field] = $field;
}
@@ -121,55 +137,69 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
foreach ($fieldMap as $field => $leadField) {
$type = $this->getMetadata()->get(['entityDefs', 'Lead', 'fields', $field, 'type']);
if (in_array($type, ['file', 'image'])) {
$attachment = $lead->get($field);
if ($attachment) {
$attachment = $this->getEntityManager()->getRepository('Attachment')->getCopiedAttachment($attachment);
$idAttribute = $field . 'Id';
$nameAttribute = $field . 'Name';
if ($attachment) {
$attributes[$idAttribute] = $attachment->id;
$attributes[$nameAttribute] = $attachment->get('name');
}
}
continue;
} else if (in_array($type, ['attachmentMultiple'])) {
}
else if (in_array($type, ['attachmentMultiple'])) {
$attachmentList = $lead->get($field);
if (count($attachmentList)) {
$idList = [];
$nameHash = (object) [];
$typeHash = (object) [];
foreach ($attachmentList as $attachment) {
$attachment = $this->getEntityManager()->getRepository('Attachment')->getCopiedAttachment($attachment);
if ($attachment) {
$idList[] = $attachment->id;
$nameHash->{$attachment->id} = $attachment->get('name');
$typeHash->{$attachment->id} = $attachment->get('type');
}
}
$attributes[$field . 'Ids'] = $idList;
$attributes[$field . 'Names'] = $nameHash;
$attributes[$field . 'Types'] = $typeHash;
}
continue;
}
$leadAttributeList = $this->fieldUtil->getAttributeList('Lead', $leadField);
$attributeList = $this->fieldUtil->getAttributeList($entityType, $field);
foreach ($attributeList as $i => $attribute) {
if (in_array($attribute, $ignoreAttributeList)) continue;
if (in_array($attribute, $ignoreAttributeList)) {
continue;
}
$leadAttribute = $leadAttributeList[$i];
if (!$lead->has($leadAttribute)) continue;
if (!$lead->has($leadAttribute)) {
continue;
}
$attributes[$attribute] = $lead->get($leadAttribute);
}
}
$data[$entityType] = $attributes;
}
return $data;
@@ -197,7 +227,10 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
$account->set(get_object_vars($recordsData->Account));
if ($duplicateCheck) {
$rDuplicateList = $this->getServiceFactory()->create('Account')->findDuplicates($account, $recordsData->Account);
$rDuplicateList = $this->getServiceFactory()
->create('Account')
->findDuplicates($account, $recordsData->Account);
if ($rDuplicateList) {
foreach ($rDuplicateList as $e) {
$item = $e->getValueMap();
@@ -210,6 +243,7 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
if (!$skipSave) {
$entityManager->saveEntity($account);
$lead->set('createdAccountId', $account->id);
}
}
@@ -217,12 +251,16 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
if (!empty($recordsData->Contact)) {
$contact = $entityManager->getEntity('Contact');
$contact->set(get_object_vars($recordsData->Contact));
if (isset($account)) {
$contact->set('accountId', $account->id);
}
if ($duplicateCheck) {
$rDuplicateList = $this->getServiceFactory()->create('Contact')->findDuplicates($contact, $recordsData->Contact);
$rDuplicateList = $this->getServiceFactory()
->create('Contact')
->findDuplicates($contact, $recordsData->Contact);
if ($rDuplicateList) {
foreach ($rDuplicateList as $e) {
$item = $e->getValueMap();
@@ -235,6 +273,7 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
if (!$skipSave) {
$entityManager->saveEntity($contact);
$lead->set('createdContactId', $contact->id);
}
}
@@ -242,15 +281,18 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
if (!empty($recordsData->Opportunity)) {
$opportunity = $entityManager->getEntity('Opportunity');
$opportunity->set(get_object_vars($recordsData->Opportunity));
if (isset($account)) {
$opportunity->set('accountId', $account->id);
}
if (isset($contact)) {
$opportunity->set('contactId', $contact->id);
}
if ($duplicateCheck) {
$rDuplicateList = $this->getServiceFactory()->create('Opportunity')
$rDuplicateList = $this->getServiceFactory()
->create('Opportunity')
->findDuplicates($opportunity, $recordsData->Opportunity);
if ($rDuplicateList) {
@@ -265,9 +307,11 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
if (!$skipSave) {
$entityManager->saveEntity($opportunity);
if (isset($contact)) {
$entityManager->getRepository('Contact')->relate($contact, 'opportunities', $opportunity);
}
$lead->set('createdOpportunityId', $opportunity->id);
}
}
@@ -277,78 +321,110 @@ class Lead extends \Espo\Core\Templates\Services\Person implements
'reason' => 'duplicate',
'duplicates' => $duplicateList,
];
throw new \Espo\Core\Exceptions\ConflictSilent(json_encode($reason));
throw new ConflictSilent(json_encode($reason));
}
$lead->set('status', 'Converted');
$entityManager->saveEntity($lead);
if ($meetings = $lead->get('meetings')) {
foreach ($meetings as $meeting) {
if (!empty($contact)) {
$entityManager->getRepository('Meeting')->relate($meeting, 'contacts', $contact);
}
$leadRepisotory = $entityManager->getRepository('Lead');
if (!empty($opportunity)) {
$meeting->set('parentId', $opportunity->id);
$meeting->set('parentType', 'Opportunity');
$entityManager->saveEntity($meeting);
} else if (!empty($account)) {
$meeting->set('parentId', $account->id);
$meeting->set('parentType', 'Account');
$entityManager->saveEntity($meeting);
}
$meetings = $leadRepisotory
->getRelation($lead, 'meetings')
->select(['id', 'parentId', 'parentType'])
->find();
foreach ($meetings as $meeting) {
if (!empty($contact)) {
$entityManager->getRepository('Meeting')->relate($meeting, 'contacts', $contact);
}
}
if ($calls = $lead->get('calls')) {
foreach ($calls as $call) {
if (!empty($contact)) {
$entityManager->getRepository('Call')->relate($call, 'contacts', $contact);
}
if (!empty($opportunity)) {
$call->set('parentId', $opportunity->id);
$call->set('parentType', 'Opportunity');
$entityManager->saveEntity($call);
} else if (!empty($account)) {
$call->set('parentId', $account->id);
$call->set('parentType', 'Account');
$entityManager->saveEntity($call);
}
if (!empty($opportunity)) {
$meeting->set('parentId', $opportunity->id);
$meeting->set('parentType', 'Opportunity');
$entityManager->saveEntity($meeting);
}
}
if ($emails = $lead->get('emails')) {
foreach ($emails as $email) {
if (!empty($opportunity)) {
$email->set('parentId', $opportunity->id);
$email->set('parentType', 'Opportunity');
$entityManager->saveEntity($email);
} else if (!empty($account)) {
$email->set('parentId', $account->id);
$email->set('parentType', 'Account');
$entityManager->saveEntity($email);
}
else if (!empty($account)) {
$meeting->set('parentId', $account->id);
$meeting->set('parentType', 'Account');
$entityManager->saveEntity($meeting);
}
}
if ($documents = $lead->get('documents')) {
foreach ($documents as $document) {
if (!empty($account)) {
$entityManager->getRepository('Document')->relate($document, 'accounts', $account);
}
if (!empty($opportunity)) {
$entityManager->getRepository('Document')->relate($document, 'opportunities', $opportunity);
}
$calls = $leadRepisotory
->getRelation($lead, 'calls')
->select(['id', 'parentId', 'parentType'])
->find();
foreach ($calls as $call) {
if (!empty($contact)) {
$entityManager->getRepository('Call')->relate($call, 'contacts', $contact);
}
if (!empty($opportunity)) {
$call->set('parentId', $opportunity->id);
$call->set('parentType', 'Opportunity');
$entityManager->saveEntity($call);
}
else if (!empty($account)) {
$call->set('parentId', $account->id);
$call->set('parentType', 'Account');
$entityManager->saveEntity($call);
}
}
$emails = $leadRepisotory
->getRelation($lead, 'emails')
->select(['id', 'parentId', 'parentType'])
->find();
foreach ($emails as $email) {
if (!empty($opportunity)) {
$email->set('parentId', $opportunity->id);
$email->set('parentType', 'Opportunity');
$entityManager->saveEntity($email);
}
else if (!empty($account)) {
$email->set('parentId', $account->id);
$email->set('parentType', 'Account');
$entityManager->saveEntity($email);
}
}
$documents = $leadRepisotory
->getRelation($lead, 'documents')
->select(['id'])
->find();
foreach ($documents as $document) {
if (!empty($account)) {
$entityManager->getRepository('Document')->relate($document, 'accounts', $account);
}
if (!empty($opportunity)) {
$entityManager->getRepository('Document')->relate($document, 'opportunities', $opportunity);
}
}
$streamService = $this->getStreamService();
if ($streamService->checkIsFollowed($lead, $this->getUser()->id)) {
if (!empty($opportunity)) {
$streamService->followEntity($opportunity, $this->getUser()->id);
}
if (!empty($account)) {
$streamService->followEntity($account, $this->getUser()->id);
}
if (!empty($contact)) {
$streamService->followEntity($contact, $this->getUser()->id);
}
+1 -1
View File
@@ -175,7 +175,7 @@ class BaseEntity implements Entity
// @todo Remove this.
if ($this->hasRelation($name) && $this->id && $this->entityManager) {
trigger_error(
"Accessing related records with Entity::get is deprecated. Use \$entityManager->getRelation(...)->find()",
"Accessing related records with Entity::get is deprecated. Use \$repository->getRelation(...)->find()",
E_USER_DEPRECATED
);
+34 -9
View File
@@ -34,6 +34,8 @@ use Countable;
use ArrayAccess;
use SeekableIterator;
use RuntimeException;
use OutOfBoundsException;
use InvalidArgumentException;
/**
* A standard collection of entities. It allocates a memory for all entities.
@@ -80,7 +82,9 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
{
do {
$this->position ++;
$next = false;
if (!$this->valid() && $this->position <= $this->getLastValidKey()) {
$next = true;
}
@@ -90,13 +94,17 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
private function getLastValidKey()
{
$keys = array_keys($this->dataList);
$i = end($keys);
while ($i > 0) {
if (isset($this->dataList[$i])) {
break;
}
$i--;
}
return $i;
}
@@ -115,20 +123,23 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
if (!isset($this->dataList[$offset])) {
return null;
}
return $this->getEntityByOffset($offset);
}
public function offsetSet($offset, $value)
{
if (!($value instanceof Entity)) {
throw new \InvalidArgumentException('Only Entity is allowed to be added to EntityCollection.');
throw new InvalidArgumentException('Only Entity is allowed to be added to EntityCollection.');
}
if (is_null($offset)) {
$this->dataList[] = $value;
} else {
$this->dataList[$offset] = $value;
return;
}
$this->dataList[$offset] = $value;
}
public function offsetUnset($offset)
@@ -144,8 +155,9 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
public function seek($offset)
{
$this->position = $offset;
if (!$this->valid()) {
throw new \OutOfBoundsException("Invalid seek offset ($offset).");
throw new OutOfBoundsException("Invalid seek offset ($offset).");
}
}
@@ -160,13 +172,15 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
if ($value instanceof Entity) {
return $value;
} else if (is_array($value)) {
$this->dataList[$offset] = $this->buildEntityFromArray($value);
} else {
return null;
}
return $this->dataList[$offset];
if (is_array($value)) {
$this->dataList[$offset] = $this->buildEntityFromArray($value);
return $this->dataList[$offset];
}
return null;
}
protected function buildEntityFromArray(array $dataArray)
@@ -179,9 +193,11 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
if ($entity) {
$entity->set($dataArray);
if ($this->isFetched) {
$entity->setAsFetched();
}
return $entity;
}
}
@@ -213,6 +229,7 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
public function merge(EntityCollection $collection)
{
$newData = $this->dataList;
$incomingDataList = $collection->getDataList();
foreach ($incomingDataList as $v) {
@@ -230,12 +247,14 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
if ($this->indexOf($value) !== false) {
return true;
}
return false;
}
public function indexOf($value)
{
$index = 0;
if (is_array($value)) {
foreach ($this->dataList as $v) {
if (is_array($v)) {
@@ -247,6 +266,7 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
return $index;
}
}
$index ++;
}
} else if ($value instanceof Entity) {
@@ -260,9 +280,11 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
return $index;
}
}
$index ++;
}
}
return false;
}
@@ -272,14 +294,17 @@ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess,
public function toArray(bool $itemsAsObjects = false) : array
{
$arr = [];
foreach ($this as $entity) {
if ($itemsAsObjects) {
$item = $entity->getValueMap();
} else {
$item = $entity->toArray();
}
$arr[] = $item;
}
return $arr;
}
@@ -1296,7 +1296,7 @@ abstract class BaseQueryComposer implements QueryComposer
$modifiedOrder[] = $newItem;
}
$part = $this->getOrderExpressionPart($entity, $modifiedOrder, null, false, $params, true);
$part = $this->getOrderExpressionPart($entity, $modifiedOrder, null, $params, true);
return $part;
}
@@ -1446,21 +1446,32 @@ abstract class BaseQueryComposer implements QueryComposer
}
if (is_string($value)) {
return self::getAllAttributesFromComplexExpression($value);
$value = [[$value]];
}
if (!is_array($value)) {
return [];
}
$list = [];
if (is_array($value)) {
foreach ($value as $item) {
if (!is_array($item) || !isset($item[0])) continue;
$list = array_merge(
$list,
self::getAllAttributesFromComplexExpression($item[0])
);
foreach ($value as $item) {
if (!is_array($item) || !isset($item[0])) {
continue;
}
$expression = $item[0];
if (strpos($expression, 'LIST:') === 0 && substr_count($expression, ':') === 2) {
$expression = explode(':', $expression)[1];
}
$attributeList = self::getAllAttributesFromComplexExpression($expression);
$list = array_merge(
$list,
$attributeList
);
}
return $list;
@@ -1828,7 +1839,6 @@ abstract class BaseQueryComposer implements QueryComposer
Entity $entity,
$orderBy = null,
$order = null,
bool $useColumnAlias = false,
?array &$params = null,
bool $noCustom = false
) : ?string {
@@ -1849,7 +1859,7 @@ abstract class BaseQueryComposer implements QueryComposer
}
$arr[] = $this->getOrderExpressionPart(
$entity, $orderByInternal, $orderInternal, $useColumnAlias, $params, $noCustom
$entity, $orderByInternal, $orderInternal, $params, $noCustom
);
}
}
@@ -1860,13 +1870,8 @@ abstract class BaseQueryComposer implements QueryComposer
if (strpos($orderBy, 'LIST:') === 0) {
list($l, $field, $list) = explode(':', $orderBy);
if ($useColumnAlias) {
$fieldPath = $this->quoteIdentifier(
$this->sanitizeSelectAlias($field)
);
} else {
$fieldPath = $this->getAttributePathForOrderBy($entity, $field, $params);
}
$fieldPath = $this->getAttributePathForOrderBy($entity, $field, $params);
$listQuoted = [];
$list = array_reverse(explode(',', $list));
@@ -1905,13 +1910,7 @@ abstract class BaseQueryComposer implements QueryComposer
return $this->getAttributeOrderSql($entity, $orderBy, $params, $order);
}
if ($useColumnAlias) {
$fieldPath = $this->quoteIdentifier(
$this->sanitizeSelectAlias($orderBy)
);
} else {
$fieldPath = $this->getAttributePathForOrderBy($entity, $orderBy, $params);
}
$fieldPath = $this->getAttributePathForOrderBy($entity, $orderBy, $params);
if (!$fieldPath) {
throw new LogicException("Could not handle 'order' for '".$entity->getEntityType()."'.");
@@ -1922,7 +1921,7 @@ abstract class BaseQueryComposer implements QueryComposer
protected function getOrderPart(Entity $entity, $orderBy = null, $order = null, &$params = null) : ?string
{
return $this->getOrderExpressionPart($entity, $orderBy, $order, false, $params);
return $this->getOrderExpressionPart($entity, $orderBy, $order, $params);
}
protected function getAttributePathForOrderBy(Entity $entity, string $orderBy, array $params) : ?string
@@ -162,6 +162,7 @@
"max": "Max",
"translation": "Translation",
"previewSize": "Preview Size",
"listPreviewSize": "Preview Size in List View",
"noEmptyString": "Empty string value is not allowed",
"defaultType": "Default Type",
"seeMoreDisabled": "Disable Text Cut",
@@ -289,6 +290,7 @@
},
"options": {
"previewSize": {
"": "Default",
"x-small": "X-Small",
"small": "Small",
"medium": "Medium",
@@ -162,6 +162,19 @@
}
},
"tooltips": {
"displayListViewRecordCount": "A total number of records will be shown on the list view.",
"currencyList": "What currencies will be available in the system.",
"activitiesEntityList": "What records will be available in the Activities panel.",
"historyEntityList": "What records will be available in the History panel.",
"calendarEntityList": "What records will be available in the Calendar.",
"addressStateList": "State suggestions for address fields.",
"addressCityList": "City suggestions for address fields.",
"addressCountryList": "Country suggestions for address fields.",
"exportDisabled": "Users won't be able to export records. Only admin will be allowed.",
"globalSearchEntityList": "What records can be searched with Global Search.",
"siteUrl": "A URL of this EspoCRM instance. You need to change it if you move to another domain.",
"useCache": "Not recommended to disable, unless for development purpose.",
"useWebSocket": "WebSocket enables two-way interactive communication between a server and a browser. Requires setting up the WebSocket daemon on your server. Check the documentation for more info.",
"passwordRecoveryForInternalUsersDisabled": "Only portal users will be able to recover password.",
"passwordRecoveryNoExposure": "It won't be possible to determine whether a specific email address is registered in the system.",
"emailAddressLookupEntityTypeList": "For email address autocomplete.",
@@ -169,7 +169,7 @@
"Cancel": "Отменить",
"Apply": "Применить",
"Unlink": "Убрать ссылку",
"Mass Update": "Обновить все",
"Mass Update": "Массовое обновление",
"Export": "Экспортировать",
"No Data": "Нет данных",
"No Access": "Нет доступа",
@@ -2,7 +2,8 @@
"fields": {
"useCache": {
"type": "bool",
"default": true
"default": true,
"tooltip": true
},
"recordsPerPage": {
"type": "int",
@@ -61,7 +62,8 @@
"type": "multiEnum",
"default": ["USD", "EUR"],
"required": true,
"view": "views/settings/fields/currency-list"
"view": "views/settings/fields/currency-list",
"tooltip": true
},
"defaultCurrency": {
"type": "enum",
@@ -143,7 +145,8 @@
"globalSearchEntityList": {
"type": "multiEnum",
"translation": "Global.scopeNames",
"view": "views/settings/fields/global-search-entity-list"
"view": "views/settings/fields/global-search-entity-list",
"tooltip": true
},
"exportDelimiter": {
"type": "varchar",
@@ -326,7 +329,8 @@
},
"exportDisabled": {
"type": "bool",
"default": false
"default": false,
"tooltip": true
},
"emailNotificationsDelay": {
"type": "int",
@@ -399,7 +403,8 @@
"type": "varchar"
},
"displayListViewRecordCount": {
"type": "bool"
"type": "bool",
"tooltip": true
},
"userThemesDisabled": {
"type": "bool",
@@ -461,7 +466,8 @@
"disabled": true
},
"siteUrl": {
"type": "varchar"
"type": "varchar",
"tooltip": true
},
"applicationName": {
"type": "varchar"
@@ -503,15 +509,18 @@
},
"calendarEntityList": {
"type": "multiEnum",
"view": "views/settings/fields/calendar-entity-list"
"view": "views/settings/fields/calendar-entity-list",
"tooltip": true
},
"activitiesEntityList": {
"type": "multiEnum",
"view": "views/settings/fields/activities-entity-list"
"view": "views/settings/fields/activities-entity-list",
"tooltip": true
},
"historyEntityList": {
"type": "multiEnum",
"view": "views/settings/fields/history-entity-list"
"view": "views/settings/fields/history-entity-list",
"tooltip": true
},
"busyRangesEntityList": {
"type": "multiEnum",
@@ -578,13 +587,16 @@
"tooltip": true
},
"addressCountryList": {
"type": "multiEnum"
"type": "multiEnum",
"tooltip": true
},
"addressCityList": {
"type": "multiEnum"
"type": "multiEnum",
"tooltip": true
},
"addressStateList": {
"type": "multiEnum"
"type": "multiEnum",
"tooltip": true
},
"jobRunInParallel": {
"type": "bool",
@@ -621,7 +633,8 @@
"tooltip": true
},
"useWebSocket": {
"type": "bool"
"type": "bool",
"tooltip": true
}
}
}
@@ -42,11 +42,13 @@
},
{
"name":"after",
"type":"varchar"
"type":"varchar",
"view": "views/admin/field-manager/fields/date/after-before"
},
{
"name":"before",
"type":"varchar"
"type":"varchar",
"view": "views/admin/field-manager/fields/date/after-before"
},
{
"type": "bool",
@@ -39,11 +39,13 @@
},
{
"name":"after",
"type":"varchar"
"type":"varchar",
"view": "views/admin/field-manager/fields/date/after-before"
},
{
"name":"before",
"type":"varchar"
"type":"varchar",
"view": "views/admin/field-manager/fields/date/after-before"
},
{
"type": "bool",
@@ -39,11 +39,13 @@
},
{
"name":"after",
"type":"varchar"
"type":"varchar",
"view": "views/admin/field-manager/fields/date/after-before"
},
{
"name":"before",
"type":"varchar"
"type":"varchar",
"view": "views/admin/field-manager/fields/date/after-before"
},
{
"type": "bool",
@@ -11,6 +11,14 @@
"default":"small",
"options": ["x-small", "small", "medium", "large"]
},
{
"name": "listPreviewSize",
"type": "enum",
"default": "small",
"options": ["", "small", "medium"],
"translation": "Admin.options.previewSize",
"default": ""
},
{
"name": "maxFileSize",
"type": "float",
@@ -5,6 +5,7 @@
"acl": "recordAllTeamNo",
"aclPortal": true,
"aclPortalLevelList": ["all", "no"],
"aclPortalActionList": ["read"],
"customizable": true,
"importable": true,
"disabled": false
+31 -6
View File
@@ -133,6 +133,7 @@ class EmailTemplate extends Record implements
if (!empty($params['relatedId']) && !empty($params['relatedType'])) {
$related = $this->getEntityManager()->getEntity($params['relatedType'], $params['relatedId']);
if ($related) {
$entityHash[$related->getEntityType()] = $related;
}
@@ -155,6 +156,7 @@ class EmailTemplate extends Record implements
if ($handlebarsInSubject) {
$subject = $htmlizer->render($parent, $subject);
}
if ($handlebarsInBody) {
$body = $htmlizer->render($parent, $body);
}
@@ -244,7 +246,9 @@ class EmailTemplate extends Record implements
}
foreach ($attributeList as $attribute) {
if (in_array($attribute, $forbiddenAttributeList)) continue;
if (in_array($attribute, $forbiddenAttributeList)) {
continue;
}
$value = $entity->get($attribute);
@@ -252,13 +256,18 @@ class EmailTemplate extends Record implements
continue;
}
if (!$entity->getAttributeType($attribute)) continue;
if (!$entity->getAttributeType($attribute)) {
continue;
}
$value = $this->formatAttributeValue($entity, $attribute);
if (is_null($value)) continue;
if (is_null($value)) {
continue;
}
$variableName = $attribute;
if (!is_null($prefixLink)) {
$variableName = $prefixLink . '.' . $attribute;
}
@@ -266,7 +275,7 @@ class EmailTemplate extends Record implements
$text = str_replace('{' . $type . '.' . $variableName . '}', $value, $text);
}
if (!$skipLinks) {
if (!$skipLinks && $entity->id) {
$relationDefs = $entity->getRelations();
foreach ($entity->getRelationList() as $relation) {
@@ -301,10 +310,12 @@ class EmailTemplate extends Record implements
}
$replaceData = [];
$replaceData['today'] = $this->getDateTime()->getTodayString();
$replaceData['now'] = $this->getDateTime()->getNowString();
$timeZone = $this->getConfig()->get('timeZone');
$now = new DateTime('now', new DateTimezone($timeZone));
$replaceData['currentYear'] = $now->format('Y');
@@ -328,11 +339,13 @@ class EmailTemplate extends Record implements
$value = $this->getLanguage()->translateOption($value, $attribute, $entity->getEntityType());
} else if ($fieldType === 'array' || $fieldType === 'multiEnum' || $fieldType === 'checklist') {
$valueList = [];
if (is_array($value)) {
foreach ($value as $v) {
$valueList[] = $this->getLanguage()->translateOption($v, $attribute, $entity->getEntityType());
}
}
$value = implode(', ', $valueList);
$value = $this->getLanguage()->translateOption($value, $attribute, $entity->getEntityType());
} else {
@@ -348,13 +361,16 @@ class EmailTemplate extends Record implements
if (!is_string($value)) {
$value = '';
}
$value = nl2br($value);
} else if ($attributeType == 'float') {
if (is_float($value)) {
$decimalPlaces = 2;
if ($fieldType === 'currency') {
$decimalPlaces = $this->getConfig()->get('currencyDecimalPlaces');
}
$value = $this->getNumber()->format($value, $decimalPlaces);
}
} else if ($attributeType == 'int') {
@@ -370,7 +386,9 @@ class EmailTemplate extends Record implements
$value = '';
}
if (!is_string($value)) return null;
if (!is_string($value)) {
return null;
}
return $value;
}
@@ -389,6 +407,7 @@ class EmailTemplate extends Record implements
if ($parentId && $parentType) {
$e = $this->getEntityManager()->getEntity($parentType, $parentId);
if ($e && $this->getAcl()->check($e)) {
$dataList[] = [
'type' => 'parent',
@@ -426,6 +445,7 @@ class EmailTemplate extends Record implements
foreach ($fm->getEntityTypeFieldList($entityType) as $field) {
$fieldType = $fm->getEntityTypeFieldParam($entityType, $field, 'type');
$fieldAttributeList = $fm->getAttributeList($entityType, $field);
if (
$fm->getEntityTypeFieldParam($entityType, $field, 'disabled') ||
$fm->getEntityTypeFieldParam($entityType, $field, 'directAccessDisabled') ||
@@ -441,9 +461,14 @@ class EmailTemplate extends Record implements
$attributeList = $fm->getEntityTypeAttributeList($entityType);
$values = (object) [];
foreach ($attributeList as $a) {
if (!$e->has($a)) continue;
if (!$e->has($a)) {
continue;
}
$value = $emailTemplateService->formatAttributeValue($e, $a);
if ($value != '') {
$values->$a = $value;
}
+188 -71
View File
@@ -43,10 +43,16 @@ use Espo\Core\{
Mail\Parsers\MailMimeParser,
};
use Espo\Services\{
Email as EmailService,
Record as RecordService,
};
use Espo\Entities\{
Team,
InboundEmail as InboundEmailEntity,
User,
Email as EmailEntity,
};
use Espo\Core\Di;
@@ -57,7 +63,7 @@ use Throwable;
use DateTime;
use DateTimeZone;
class InboundEmail extends \Espo\Services\Record implements
class InboundEmail extends RecordService implements
Di\CryptAware,
Di\EmailSenderAware,
@@ -83,9 +89,11 @@ class InboundEmail extends \Espo\Services\Record implements
protected function handleInput($data)
{
parent::handleInput($data);
if (property_exists($data, 'password')) {
$data->password = $this->getCrypt()->encrypt($data->password);
}
if (property_exists($data, 'smtpPassword')) {
$data->smtpPassword = $this->getCrypt()->encrypt($data->smtpPassword);
}
@@ -106,6 +114,7 @@ class InboundEmail extends \Espo\Services\Record implements
{
if (!empty($params['id'])) {
$account = $this->getEntityManager()->getEntity('InboundEmail', $params['id']);
if ($account) {
$params['password'] = $this->getCrypt()->decrypt($account->get('password'));
$params['imapHandler'] = $account->get('imapHandler');
@@ -117,9 +126,11 @@ class InboundEmail extends \Espo\Services\Record implements
$storage = $this->createStorage($params);
$folders = new RecursiveIteratorIterator($storage->getFolders(), RecursiveIteratorIterator::SELF_FIRST);
foreach ($folders as $name => $folder) {
$foldersArr[] = mb_convert_encoding($folder->getGlobalName(), 'UTF-8', 'UTF7-IMAP');
}
return $foldersArr;
}
@@ -127,6 +138,7 @@ class InboundEmail extends \Espo\Services\Record implements
{
if (!empty($params['id'])) {
$account = $this->getEntityManager()->getEntity('InboundEmail', $params['id']);
if ($account) {
$params['imapHandler'] = $account->get('imapHandler');
}
@@ -137,6 +149,7 @@ class InboundEmail extends \Espo\Services\Record implements
if ($storage->getFolders()) {
return true;
}
throw new Error();
}
@@ -153,25 +166,30 @@ class InboundEmail extends \Espo\Services\Record implements
$maxSize = $this->getConfig()->get('emailMessageMaxSize');
$teamId = $emailAccount->get('teamId');
$userId = null;
if ($emailAccount->get('assignToUserId')) {
$userId = $emailAccount->get('assignToUserId');
}
$userIdList = [];
$teamIdList = $emailAccount->getLinkMultipleIdList('teams');
if (!empty($teamIdList)) {
if ($emailAccount->get('addAllTeamUsers')) {
$userList = $this->getEntityManager()->getRepository('User')->find([
'select' => ['id'],
'whereClause' => [
$userList = $this->getEntityManager()
->getRepository('User')
->select(['id'])
->distinct()
->join('teams')
->where([
'isActive' => true,
'teamsMiddle.teamId' => $teamIdList
],
'distinct' => true,
'joins' => ['teams'],
]);
'teamsMiddle.teamId' => $teamIdList,
])
->find();
foreach ($userList as $user) {
$userIdList[] = $user->id;
}
@@ -182,18 +200,21 @@ class InboundEmail extends \Espo\Services\Record implements
$teamIdList[] = $teamId;
}
$filterCollection = $this->getEntityManager()->getRepository('EmailFilter')->where([
'action' => 'Skip',
'OR' => [
[
'parentType' => $emailAccount->getEntityType(),
'parentId' => $emailAccount->id
$filterCollection = $this->getEntityManager()
->getRepository('EmailFilter')
->where([
'action' => 'Skip',
'OR' => [
[
'parentType' => $emailAccount->getEntityType(),
'parentId' => $emailAccount->id,
],
[
'parentId' => null
],
],
[
'parentId' => null
]
]
])->find();
])
->find();
$fetchData = $emailAccount->get('fetchData');
@@ -239,6 +260,7 @@ class InboundEmail extends \Espo\Services\Record implements
$GLOBALS['log']->error(
'InboundEmail '.$emailAccount->id.' (Select Folder) [' . $e->getCode() . '] ' .$e->getMessage()
);
continue;
}
@@ -266,6 +288,7 @@ class InboundEmail extends \Espo\Services\Record implements
$idList = $storage->getIdsFromUID($lastUID);
} else {
$fetchSince = $emailAccount->get('fetchSince');
if ($lastDate) {
$fetchSince = $lastDate;
}
@@ -296,8 +319,10 @@ class InboundEmail extends \Espo\Services\Record implements
if ($forceByDate && $previousLastUID) {
$uid = $storage->getUniqueId($id);
if ($uid <= $previousLastUID) {
$k++;
continue;
}
}
@@ -379,6 +404,7 @@ class InboundEmail extends \Espo\Services\Record implements
} else {
if ($emailAccount->get('reply')) {
$user = $this->getEntityManager()->getEntity('User', $userId);
$this->autoReply($emailAccount, $email, $user);
}
}
@@ -394,15 +420,18 @@ class InboundEmail extends \Espo\Services\Record implements
if ($email && $email->get('dateSent')) {
$dt = null;
try {
$dt = new DateTime($email->get('dateSent'));
} catch (Exception $e) {}
if ($dt) {
$nowDt = new DateTime();
if ($dt->getTimestamp() >= $nowDt->getTimestamp()) {
$dt = $nowDt;
}
$dateSent = $dt->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s');
$lastDate = $dateSent;
}
@@ -416,6 +445,7 @@ class InboundEmail extends \Espo\Services\Record implements
if ($forceByDate) {
$nowDt = new DateTime();
$lastDate = $nowDt->format('Y-m-d H:i:s');
}
@@ -425,8 +455,10 @@ class InboundEmail extends \Espo\Services\Record implements
if ($forceByDate) {
if ($previousLastUID) {
$idList = $storage->getIdsFromUID($previousLastUID);
if (count($idList)) {
$uid1 = $storage->getUniqueId($idList[0]);
if ($uid1 && $uid1 > $previousLastUID) {
unset($fetchData->byDate->$folder);
}
@@ -453,6 +485,7 @@ class InboundEmail extends \Espo\Services\Record implements
$filterCollection, $fetchOnlyHeader, $folderData = null
) {
$email = null;
try {
$email = $importer->importMessage(
$message, $userId, $teamIdList, $userIdList, $filterCollection, $fetchOnlyHeader, $folderData
@@ -475,8 +508,10 @@ class InboundEmail extends \Espo\Services\Record implements
{
if ($email->get('parentType') && $email->get('parentId')) {
$parent = $this->getEntityManager()->getEntity($email->get('parentType'), $email->get('parentId'));
if ($parent) {
$this->getServiceFactory()->create('Stream')->noteEmailReceived($parent, $email);
return;
}
}
@@ -490,6 +525,7 @@ class InboundEmail extends \Espo\Services\Record implements
$userIdList = $case->getLinkMultipleIdList('assignedUsers');
} else {
$assignedUserId = $case->get('assignedUserId');
if ($assignedUserId) {
$userIdList[] = $assignedUserId;
}
@@ -515,48 +551,58 @@ class InboundEmail extends \Espo\Services\Record implements
{
if ($email->get('parentType') == 'Case' && $email->get('parentId')) {
$case = $this->getEntityManager()->getEntity('Case', $email->get('parentId'));
if ($case) {
$this->processCaseToEmailFields($case, $email);
if (!$email->isFetched()) {
$this->getServiceFactory()->create('Stream')->noteEmailReceived($case, $email);
}
}
return;
}
if (preg_match('/\[#([0-9]+)[^0-9]*\]/', $email->get('name'), $m)) {
$caseNumber = $m[1];
$case = $this->getEntityManager()->getRepository('Case')->where([
'number' => $caseNumber
])->findOne();
$case = $this->getEntityManager()
->getRepository('Case')
->where([
'number' => $caseNumber,
])
->findOne();
if ($case) {
$email->set('parentType', 'Case');
$email->set('parentId', $case->id);
$this->processCaseToEmailFields($case, $email);
if (!$email->isFetched()) {
$this->getServiceFactory()->create('Stream')->noteEmailReceived($case, $email);
}
}
} else {
$params = [
'caseDistribution' => $inboundEmail->get('caseDistribution'),
'teamId' => $inboundEmail->get('teamId'),
'userId' => $inboundEmail->get('assignToUserId'),
'targetUserPosition' => $inboundEmail->get('targetUserPosition'),
'inboundEmailId' => $inboundEmail->id
];
$case = $this->emailToCase($email, $params);
return;
}
$user = $this->getEntityManager()->getEntity('User', $case->get('assignedUserId'));
$params = [
'caseDistribution' => $inboundEmail->get('caseDistribution'),
'teamId' => $inboundEmail->get('teamId'),
'userId' => $inboundEmail->get('assignToUserId'),
'targetUserPosition' => $inboundEmail->get('targetUserPosition'),
'inboundEmailId' => $inboundEmail->id,
];
$this->getServiceFactory()->create('Stream')->noteEmailReceived($case, $email, true);
$case = $this->emailToCase($email, $params);
if ($inboundEmail->get('reply')) {
$this->autoReply($inboundEmail, $email, $case, $user);
}
$user = $this->getEntityManager()->getEntity('User', $case->get('assignedUserId'));
$this->getServiceFactory()->create('Stream')->noteEmailReceived($case, $email, true);
if ($inboundEmail->get('reply')) {
$this->autoReply($inboundEmail, $email, $case, $user);
}
}
@@ -581,6 +627,7 @@ class InboundEmail extends \Espo\Services\Record implements
protected function assignLeastBusy(Entity $case, Team $team, $targetUserPosition)
{
$className = 'Espo\\Custom\\Business\\Distribution\\CaseObj\\LeastBusy';
if (!class_exists($className)) {
$className = 'Espo\\Modules\\Crm\\Business\\Distribution\\CaseObj\\LeastBusy';
}
@@ -595,10 +642,12 @@ class InboundEmail extends \Espo\Services\Record implements
}
}
protected function emailToCase(\Espo\Entities\Email $email, array $params = [])
protected function emailToCase(EmailEntity $email, array $params = [])
{
$case = $this->getEntityManager()->getEntity('Case');
$case->populateDefaults();
$case->set('name', $email->get('name'));
$bodyPlain = $email->getBodyPlain();
@@ -616,8 +665,13 @@ class InboundEmail extends \Espo\Services\Record implements
foreach ($attachmentIdList as $attachmentId) {
$attachment = $this->getEntityManager()->getRepository('Attachment')->get($attachmentId);
if (!$attachment) continue;
if (!$attachment) {
continue;
}
$copiedAttachment = $this->getEntityManager()->getRepository('Attachment')->getCopiedAttachment($attachment);
$copiedAttachmentIdList[] = $copiedAttachment->id;
}
@@ -626,6 +680,7 @@ class InboundEmail extends \Espo\Services\Record implements
}
$userId = null;
if (!empty($params['userId'])) {
$userId = $params['userId'];
}
@@ -635,19 +690,23 @@ class InboundEmail extends \Espo\Services\Record implements
}
$teamId = false;
if (!empty($params['teamId'])) {
$teamId = $params['teamId'];
}
if ($teamId) {
$case->set('teamsIds', [$teamId]);
}
$caseDistribution = '';
if (!empty($params['caseDistribution'])) {
$caseDistribution = $params['caseDistribution'];
}
$targetUserPosition = null;
if (!empty($params['targetUserPosition'])) {
$targetUserPosition = $params['targetUserPosition'];
}
@@ -660,21 +719,26 @@ class InboundEmail extends \Espo\Services\Record implements
$case->set('status', 'Assigned');
}
break;
case 'Round-Robin':
if ($teamId) {
$team = $this->getEntityManager()->getEntity('Team', $teamId);
if ($team) {
$this->assignRoundRobin($case, $team, $targetUserPosition);
}
}
break;
case 'Least-Busy':
if ($teamId) {
$team = $this->getEntityManager()->getEntity('Team', $teamId);
if ($team) {
$this->assignLeastBusy($case, $team, $targetUserPosition);
}
}
break;
}
@@ -686,18 +750,26 @@ class InboundEmail extends \Espo\Services\Record implements
$case->set('accountId', $email->get('accountId'));
}
$contact = $this->getEntityManager()->getRepository('Contact')->join('emailAddresses', 'emailAddressesMultiple')->where([
'emailAddressesMultiple.id' => $email->get('fromEmailAddressId')
])->findOne();
$contact = $this->getEntityManager()
->getRepository('Contact')
->join('emailAddresses', 'emailAddressesMultiple')
->where([
'emailAddressesMultiple.id' => $email->get('fromEmailAddressId')
])
->findOne();
if ($contact) {
$case->set('contactId', $contact->id);
} else {
if (!$case->get('accountId')) {
$lead = $this->getEntityManager()->getRepository('Lead')
->join('emailAddresses', 'emailAddressesMultiple')->where([
'emailAddressesMultiple.id' => $email->get('fromEmailAddressId')
])->findOne();
$lead = $this->getEntityManager()
->getRepository('Lead')
->join('emailAddresses', 'emailAddressesMultiple')
->where([
'emailAddressesMultiple.id' => $email->get('fromEmailAddressId')
])
->findOne();
if ($lead) {
$case->set('leadId', $lead->id);
}
@@ -726,16 +798,22 @@ class InboundEmail extends \Espo\Services\Record implements
}
$d = new DateTime();
$d->modify('-3 hours');
$threshold = $d->format('Y-m-d H:i:s');
$emailAddress = $this->getEntityManager()->getRepository('EmailAddress')->getByAddress($email->get('from'));
$sent = $this->getEntityManager()->getRepository('Email')->where([
'toEmailAddresses.id' => $emailAddress->id,
'dateSent>' => $threshold,
'status' => 'Sent'
])->join('toEmailAddresses')->findOne();
$sent = $this->getEntityManager()
->getRepository('Email')
->where([
'toEmailAddresses.id' => $emailAddress->id,
'dateSent>' => $threshold,
'status' => 'Sent',
])
->join('toEmailAddresses')
->findOne();
if ($sent) {
return false;
@@ -743,17 +821,23 @@ class InboundEmail extends \Espo\Services\Record implements
try {
$replyEmailTemplateId = $inboundEmail->get('replyEmailTemplateId');
if ($replyEmailTemplateId) {
$entityHash = [];
if ($case) {
$entityHash['Case'] = $case;
if ($case->get('contactId')) {
$contact = $this->getEntityManager()->getEntity('Contact', $case->get('contactId'));
}
}
if (empty($contact)) {
$contact = $this->getEntityManager()->getEntity('Contact');
$fromName = \Espo\Services\Email::parseFromName($email->get('fromString'));
$fromName = EmailService::parseFromName($email->get('fromString'));
if (!empty($fromName)) {
$contact->set('name', $fromName);
}
@@ -771,11 +855,13 @@ class InboundEmail extends \Espo\Services\Record implements
$replyData = $emailTemplateService->parse($replyEmailTemplateId, ['entityHash' => $entityHash], true);
$subject = $replyData['subject'];
if ($case) {
$subject = '[#' . $case->get('number'). '] ' . $subject;
}
$reply = $this->getEntityManager()->getEntity('Email');
$reply->set('to', $email->get('from'));
$reply->set('subject', $subject);
$reply->set('body', $replyData['body']);
@@ -829,8 +915,9 @@ class InboundEmail extends \Espo\Services\Record implements
return true;
}
} catch (Exception $e) {}
} catch (Exception $e) {
$GLOBALS['log']->error("Inbound Email: Auto-reply error: " . $e->getMessage());
}
}
protected function getSmtpParamsFromInboundEmail(InboundEmailEntity $emailAccount)
@@ -844,9 +931,11 @@ class InboundEmail extends \Espo\Services\Record implements
$smtpParams['security'] = $emailAccount->get('smtpSecurity');
$smtpParams['username'] = $emailAccount->get('smtpUsername');
$smtpParams['password'] = $emailAccount->get('smtpPassword');
if (array_key_exists('password', $smtpParams)) {
$smtpParams['password'] = $this->getCrypt()->decrypt($smtpParams['password']);
}
return $smtpParams;
}
@@ -858,6 +947,7 @@ class InboundEmail extends \Espo\Services\Record implements
$content = $message->getRawContent();
$isHard = false;
if (preg_match('/permanent[ ]*[error|failure]/', $content)) {
$isHard = true;
}
@@ -876,15 +966,21 @@ class InboundEmail extends \Espo\Services\Record implements
}
}
if (!$queueItemId) return false;
if (!$queueItemId) {
return false;
}
$queueItem = $this->getEntityManager()->getEntity('EmailQueueItem', $queueItemId);
if (!$queueItem) return false;
if (!$queueItem) {
return false;
}
$massEmailId = $queueItem->get('massEmailId');
$massEmail = $this->getEntityManager()->getEntity('MassEmail', $massEmailId);
$campaignId = null;
if ($massEmail) {
$campaignId = $massEmail->get('campaignId');
}
@@ -897,13 +993,19 @@ class InboundEmail extends \Espo\Services\Record implements
if ($isHard && $emailAddress) {
$emailAddressEntity = $this->getEntityManager()->getRepository('EmailAddress')->getByAddress($emailAddress);
$emailAddressEntity->set('invalid', true);
$this->getEntityManager()->saveEntity($emailAddressEntity);
if ($emailAddressEntity) {
$emailAddressEntity->set('invalid', true);
$this->getEntityManager()->saveEntity($emailAddressEntity);
}
}
if ($campaignId && $target && $target->id) {
$this->getCampaignService()
->logBounced($campaignId, $queueItemId, $target, $emailAddress, $isHard, null, $queueItem->get('isTest'));
->logBounced(
$campaignId, $queueItemId, $target, $emailAddress, $isHard, null, $queueItem->get('isTest')
);
}
return true;
@@ -918,14 +1020,17 @@ class InboundEmail extends \Espo\Services\Record implements
return $this->campaignService;
}
public function findAccountForSending(string $emailAddress) : ?\Espo\Entities\InboundEmail
public function findAccountForSending(string $emailAddress) : ?InboundEmailEntity
{
$inboundEmail = $this->getEntityManager()->getRepository('InboundEmail')->where([
'status' => 'Active',
'useSmtp' => true,
'smtpHost!=' => null,
'emailAddress' => $emailAddress,
])->findOne();
$inboundEmail = $this->getEntityManager()
->getRepository('InboundEmail')
->where([
'status' => 'Active',
'useSmtp' => true,
'smtpHost!=' => null,
'emailAddress' => $emailAddress,
])
->findOne();
return $inboundEmail;
}
@@ -933,6 +1038,7 @@ class InboundEmail extends \Espo\Services\Record implements
public function findSharedAccountForUser(User $user, $emailAddress)
{
$groupEmailAccountPermission = $this->getAclManager()->get($user, 'groupEmailAccountPermission');
$teamIdList = $user->getLinkMultipleIdList('teams');
$inboundEmail = null;
@@ -941,17 +1047,20 @@ class InboundEmail extends \Espo\Services\Record implements
if ($groupEmailAccountPermission && $groupEmailAccountPermission !== 'no') {
if ($groupEmailAccountPermission === 'team') {
if (!count($teamIdList)) return;
if (!count($teamIdList)) {
return;
}
$selectParams = [
'whereClause' => [
'status' => 'Active',
'useSmtp' => true,
'smtpIsShared' => true,
'teamsMiddle.teamId' => $teamIdList,
'emailAddress' => $emailAddress
'emailAddress' => $emailAddress,
],
'joins' => ['teams'],
'distinct' => true
'distinct' => true,
];
} else if ($groupEmailAccountPermission === 'all') {
$selectParams = [
@@ -959,7 +1068,7 @@ class InboundEmail extends \Espo\Services\Record implements
'status' => 'Active',
'useSmtp' => true,
'smtpIsShared' => true,
'emailAddress' => $emailAddress
'emailAddress' => $emailAddress,
]
];
}
@@ -967,6 +1076,7 @@ class InboundEmail extends \Espo\Services\Record implements
$inboundEmail = $this->getEntityManager()->getRepository('InboundEmail')->findOne($selectParams);
}
return $inboundEmail;
}
@@ -1034,9 +1144,11 @@ class InboundEmail extends \Espo\Services\Record implements
$storage = $this->getStorage($emailAccount);
$folder = $emailAccount->get('sentFolder');
if (empty($folder)) {
throw new Error("No sent folder for Email Account: " . $emailAccount->id . ".");
}
$storage->appendMessage($message->toString(), $folder);
}
@@ -1044,6 +1156,7 @@ class InboundEmail extends \Espo\Services\Record implements
{
$smtpParams = [];
$smtpParams['server'] = $emailAccount->get('smtpHost');
if ($smtpParams['server']) {
$smtpParams['port'] = $emailAccount->get('smtpPort');
$smtpParams['auth'] = $emailAccount->get('smtpAuth');
@@ -1058,9 +1171,11 @@ class InboundEmail extends \Espo\Services\Record implements
if ($emailAccount->get('fromName')) {
$smtpParams['fromName'] = $emailAccount->get('fromName');
}
if ($emailAccount->get('emailAddress')) {
$smtpParams['fromAddress'] = $emailAccount->get('emailAddress');
}
if (array_key_exists('password', $smtpParams) && is_string($smtpParams['password'])) {
$smtpParams['password'] = $this->getCrypt()->decrypt($smtpParams['password']);
}
@@ -1077,7 +1192,9 @@ class InboundEmail extends \Espo\Services\Record implements
{
$handlerClassName = $emailAccount->get('smtpHandler');
if (!$handlerClassName) return;
if (!$handlerClassName) {
return;
}
try {
$handler = $this->injectableFactory->create($handlerClassName);
+177 -42
View File
@@ -59,6 +59,7 @@ use Espo\Core\{
use Espo\Tools\Export\Export as ExportTool;
use StdClass;
use Exception;
use Espo\Core\Di;
@@ -1245,13 +1246,19 @@ class Record implements Crud,
public function delete(string $id)
{
if (empty($id)) throw new BadRequest("ID is empty.");
if (empty($id)) {
throw new BadRequest("ID is empty.");
}
$entity = $this->getRepository()->get($id);
if (!$entity) throw new NotFound("Record {$id} not found.");
if (!$entity) {
throw new NotFound("Record {$id} not found.");
}
if (!$this->getAcl()->check($entity, 'delete')) throw new ForbiddenSilent("No delete access.");
if (!$this->getAcl()->check($entity, 'delete')) {
throw new ForbiddenSilent("No delete access.");
}
$this->beforeDeleteEntity($entity);
@@ -1292,6 +1299,7 @@ class Record implements Crud,
}
$maxSize = 0;
if ($disableCount) {
if (!empty($params['maxSize'])) {
$maxSize = $params['maxSize'];
@@ -1376,10 +1384,16 @@ class Record implements Crud,
];
foreach ($statusList as $status) {
if (in_array($status, $statusIgnoreList)) continue;
if (!$status) continue;
if (in_array($status, $statusIgnoreList)) {
continue;
}
if (!$status) {
continue;
}
$selectParamsSub = $selectParams;
$selectParamsSub['whereClause'][] = [
$statusField => $status
];
@@ -1454,12 +1468,19 @@ class Record implements Crud,
public function restoreDeleted(string $id)
{
if (!$this->getUser()->isAdmin()) throw new Forbidden();
if (!$this->getUser()->isAdmin()) {
throw new Forbidden();
}
$entity = $this->getEntityEvenDeleted($id);
if (!$entity) throw new NotFound();
if (!$entity->get('deleted')) throw new Forbidden();
if (!$entity) {
throw new NotFound();
}
if (!$entity->get('deleted')) {
throw new Forbidden();
}
$this->getRepository()->restoreDeleted($entity->id);
@@ -1475,6 +1496,7 @@ class Record implements Crud,
return $this->getConfig()->get('maxSelectTextAttributeLengthForList', self::MAX_SELECT_TEXT_ATTRIBUTE_LENGTH);
}
}
return null;
}
@@ -1487,12 +1509,15 @@ class Record implements Crud,
public function findLinked(string $id, string $link, array $params) : RecordCollection
{
$entity = $this->getRepository()->get($id);
if (!$entity) {
throw new NotFound();
}
if (!$this->getAcl()->check($entity, 'read')) {
throw new Forbidden();
}
if (empty($link)) {
throw new Error();
}
@@ -1515,6 +1540,7 @@ class Record implements Crud,
}
$methodName = 'findLinkedEntities' . ucfirst($link);
if (method_exists($this, $methodName)) {
return $this->$methodName($id, $params);
}
@@ -1638,35 +1664,45 @@ class Record implements Crud,
}
$entity = $this->getRepository()->get($id);
if (!$entity) {
throw new NotFound();
}
if ($this->noEditAccessRequiredForLink) {
if (!$this->getAcl()->check($entity, 'read')) throw new Forbidden();
if (!$this->getAcl()->check($entity, 'read')) {
throw new Forbidden();
}
} else {
if (!$this->getAcl()->check($entity, 'edit')) throw new Forbidden();
if (!$this->getAcl()->check($entity, 'edit')) {
throw new Forbidden();
}
}
$methodName = 'link' . ucfirst($link);
if ($link !== 'entity' && $link !== 'entityMass' && method_exists($this, $methodName)) {
return $this->$methodName($id, $foreignId);
}
$foreignEntityType = $entity->getRelationParam($link, 'entity');
if (!$foreignEntityType) {
throw new Error("Entity '{$this->entityType}' has not relation '{$link}'.");
}
$foreignEntity = $this->getEntityManager()->getEntity($foreignEntityType, $foreignId);
if (!$foreignEntity) {
throw new NotFound();
}
$accessActionRequired = 'edit';
if (in_array($link, $this->noEditAccessRequiredLinkList)) {
$accessActionRequired = 'read';
}
if (!$this->getAcl()->check($foreignEntity, $accessActionRequired)) {
throw new Forbidden();
}
@@ -1712,30 +1748,39 @@ class Record implements Crud,
}
if ($this->noEditAccessRequiredForLink) {
if (!$this->getAcl()->check($entity, 'read')) throw new Forbidden();
if (!$this->getAcl()->check($entity, 'read')) {
throw new Forbidden();
}
} else {
if (!$this->getAcl()->check($entity, 'edit')) throw new Forbidden();
if (!$this->getAcl()->check($entity, 'edit')) {
throw new Forbidden();
}
}
$methodName = 'unlink' . ucfirst($link);
if ($link !== 'entity' && method_exists($this, $methodName)) {
return $this->$methodName($id, $foreignId);
}
$foreignEntityType = $entity->getRelationParam($link, 'entity');
if (!$foreignEntityType) {
throw new Error("Entity '{$this->entityType}' has not relation '{$link}'.");
}
$foreignEntity = $this->getEntityManager()->getEntity($foreignEntityType, $foreignId);
if (!$foreignEntity) {
throw new NotFound();
}
$accessActionRequired = 'edit';
if (in_array($link, $this->noEditAccessRequiredLinkList)) {
$accessActionRequired = 'read';
}
if (!$this->getAcl()->check($foreignEntity, $accessActionRequired)) {
throw new Forbidden();
}
@@ -1756,6 +1801,7 @@ class Record implements Crud,
if (!$this->getMetadata()->get(['scopes', $this->entityType, 'stream'])) throw new NotFound();
$entity = $this->getRepository()->get($id);
if (!$entity) throw new NotFound();
if (!$this->getAcl()->check($entity, 'edit')) throw new Forbidden();
if (!$this->getAcl()->check($entity, 'stream')) throw new Forbidden();
@@ -1776,6 +1822,7 @@ class Record implements Crud,
if (!$this->getMetadata()->get(['scopes', $this->entityType, 'stream'])) throw new NotFound();
$entity = $this->getRepository()->get($id);
if (!$entity) throw new NotFound();
if (!$this->getAcl()->check($entity, 'edit')) throw new Forbidden();
if (!$this->getAcl()->check($entity, 'stream')) throw new Forbidden();
@@ -1810,14 +1857,17 @@ class Record implements Crud,
}
$entity = $this->getRepository()->get($id);
if (!$entity) {
throw new NotFound();
}
if (!$this->getAcl()->check($entity, 'edit')) {
throw new Forbidden();
}
$methodName = 'massLink' . ucfirst($link);
if (method_exists($this, $methodName)) {
return $this->$methodName($id, $where, $selectData);
}
@@ -1874,7 +1924,9 @@ class Record implements Crud,
public function massUpdate(array $params, StdClass $data)
{
if ($this->getAcl()->get('massUpdatePermission') !== 'yes') throw new Forbidden();
if ($this->getAcl()->get('massUpdatePermission') !== 'yes') {
throw new Forbidden();
}
$resultIdList = [];
$repository = $this->getRepository();
@@ -1891,11 +1943,13 @@ class Record implements Crud,
foreach ($collection as $entity) {
if ($this->getAcl()->check($entity, 'edit') && $this->checkEntityForMassUpdate($entity, $data)) {
$entity->set($data);
try {
$this->processValidation($entity, $data);
} catch (\Exception $e) {
} catch (Exception $e) {
continue;
}
if ($this->checkAssignment($entity)) {
$repository->save($entity, ['massUpdate' => true, 'skipStreamNotesAcl' => true]);
$resultIdList[] = $entity->id;
@@ -1944,8 +1998,11 @@ class Record implements Crud,
foreach ($collection as $entity) {
if ($this->getAcl()->check($entity, 'delete') && $this->checkEntityForMassRemove($entity)) {
$repository->remove($entity);
$resultIdList[] = $entity->id;
$count++;
$this->processActionHistoryRecord('delete', $entity);
}
}
@@ -1960,7 +2017,9 @@ class Record implements Crud,
public function massRecalculateFormula(array $params)
{
if (!$this->getUser()->isAdmin()) throw new Forbidden();
if (!$this->getUser()->isAdmin()) {
throw new Forbidden();
}
$count = 0;
@@ -1981,7 +2040,10 @@ class Record implements Crud,
public function follow(string $id, ?string $userId = null)
{
$entity = $this->getRepository()->get($id);
if (!$entity) throw new NotFoundSilent();
if (!$entity) {
throw new NotFoundSilent();
}
if (!$this->getAcl()->check($entity, 'stream')) {
throw new Forbidden();
@@ -1997,7 +2059,10 @@ class Record implements Crud,
public function unfollow(string $id, ?string $userId = null)
{
$entity = $this->getRepository()->get($id);
if (!$entity) throw new NotFoundSilent();
if (!$entity) {
throw new NotFoundSilent();
}
if (empty($userId)) {
$userId = $this->getUser()->id;
@@ -2021,7 +2086,10 @@ class Record implements Crud,
$collection = $this->getRepository()->sth()->find($selectParams);
foreach ($collection as $entity) {
if (!$this->getAcl()->check($entity, 'stream') || !$this->getAcl()->check($entity, 'read')) continue;
if (!$this->getAcl()->check($entity, 'stream') || !$this->getAcl()->check($entity, 'read')) {
continue;
}
if ($streamService->followEntity($entity, $userId)) {
$resultIdList[] = $entity->id;
}
@@ -2031,7 +2099,9 @@ class Record implements Crud,
'count' => count($resultIdList),
];
if (isset($params['ids'])) $result['ids'] = $resultIdList;
if (isset($params['ids'])) {
$result['ids'] = $resultIdList;
}
return $result;
}
@@ -2042,7 +2112,9 @@ class Record implements Crud,
$streamService = $this->getStreamService();
if (empty($userId)) $userId = $this->getUser()->id;
if (empty($userId)) {
$userId = $this->getUser()->id;
}
$selectParams = $this->convertMassActionSelectParams($params);
@@ -2066,24 +2138,36 @@ class Record implements Crud,
protected function convertMassActionSelectParams($params)
{
if (array_key_exists('ids', $params)) {
if (!is_array($params['ids'])) throw new BadRequest();
$selectParams = $this->getSelectParams([]);
$selectParams['whereClause'][] = [
'id' => $params['ids']
];
} else if (array_key_exists('where', $params)) {
$p = ['where' => $params['where']];
if (!empty($params['selectData']) && is_array($params['selectData'])) {
foreach ($params['selectData'] as $k => $v) {
$p[$k] = $v;
}
if (!is_array($params['ids'])) {
throw new BadRequest();
}
$selectParams = $this->getSelectParams($p);
} else {
throw new BadRequest();
$selectParams = $this->getSelectParams([]);
$selectParams['whereClause'][] = [
'id' => $params['ids'],
];
return $selectParams;
}
return $selectParams;
if (array_key_exists('where', $params)) {
$searchParams = [
'where' => $params['where'],
];
if (!empty($params['selectData']) && is_array($params['selectData'])) {
foreach ($params['selectData'] as $k => $v) {
$searchParams[$k] = $v;
}
}
unset($searchParams['select']);
return $this->getSelectParams($searchParams);
}
throw new BadRequest();
}
protected function getDuplicateWhereClause(Entity $entity, $data)
@@ -2099,7 +2183,12 @@ class Record implements Crud,
if ($entity->id) {
$where['id!='] = $entity->id;
}
$duplicate = $this->getRepository()->select(['id'])->where($where)->findOne();
$duplicate = $this->getRepository()
->select(['id'])
->where($where)
->findOne();
if ($duplicate) {
return true;
}
@@ -2133,7 +2222,9 @@ class Record implements Crud,
$limit = self::FIND_DUPLICATES_LIMIT;
$duplicateList = $this->getRepository()->limit(0, $limit)->find($selectParams);
$duplicateList = $this->getRepository()
->limit(0, $limit)
->find($selectParams);
if (count($duplicateList)) {
return $duplicateList;
@@ -2168,14 +2259,17 @@ class Record implements Crud,
foreach ($this->internalAttributeList as $attribute) {
$entity->clear($attribute);
}
foreach ($this->forbiddenAttributeList as $attribute) {
$entity->clear($attribute);
}
if (!$this->getUser()->isAdmin()) {
foreach ($this->onlyAdminAttributeList as $attribute) {
$entity->clear($attribute);
}
}
foreach ($this->getAcl()->getScopeForbiddenAttributeList($entity->getEntityType(), 'read') as $attribute) {
$entity->clear($attribute);
}
@@ -2216,14 +2310,16 @@ class Record implements Crud,
$this->beforeMerge($entity, $sourceList, $attributes);
$fieldDefs = $this->getMetadata()->get('entityDefs.' . $entity->getEntityType() . '.fields', array());
$fieldDefs = $this->getMetadata()->get('entityDefs.' . $entity->getEntityType() . '.fields', []);
$hasPhoneNumber = false;
if (!empty($fieldDefs['phoneNumber']) && $fieldDefs['phoneNumber']['type'] == 'phone') {
$hasPhoneNumber = true;
}
$hasEmailAddress = false;
if (!empty($fieldDefs['emailAddress']) && $fieldDefs['emailAddress']['type'] == 'email') {
$hasEmailAddress = true;
}
@@ -2239,6 +2335,7 @@ class Record implements Crud,
if ($hasEmailAddress) {
$emailAddressToRelateList = [];
$emailAddressList = $repository->findRelated($entity, 'emailAddresses');
foreach ($emailAddressList as $emailAddress) {
$emailAddressToRelateList[] = $emailAddress;
}
@@ -2266,12 +2363,14 @@ class Record implements Crud,
if ($hasPhoneNumber) {
$phoneNumberList = $repository->findRelated($source, 'phoneNumbers');
foreach ($phoneNumberList as $phoneNumber) {
$phoneNumberToRelateList[] = $phoneNumber;
}
}
if ($hasEmailAddress) {
$emailAddressList = $repository->findRelated($source, 'emailAddresses');
foreach ($emailAddressList as $emailAddress) {
$emailAddressToRelateList[] = $emailAddress;
}
@@ -2280,10 +2379,12 @@ class Record implements Crud,
$mergeLinkList = [];
$linksDefs = $this->getMetadata()->get(['entityDefs', $this->getEntityType(), 'links']);
foreach ($linksDefs as $link => $d) {
if (!empty($d['notMergeable'])) {
continue;
}
if (!empty($d['type']) && in_array($d['type'], ['hasMany', 'hasChildren'])) {
$mergeLinkList[] = $link;
}
@@ -2292,6 +2393,7 @@ class Record implements Crud,
foreach ($sourceList as $source) {
foreach ($mergeLinkList as $link) {
$linkedList = $repository->findRelated($source, $link);
foreach ($linkedList as $linked) {
$repository->relate($entity, $link, $linked);
}
@@ -2306,10 +2408,13 @@ class Record implements Crud,
if ($hasEmailAddress) {
$emailAddressData = [];
foreach ($emailAddressToRelateList as $i => $emailAddress) {
$o = (object) [];
$o->emailAddress = $emailAddress->get('name');
$o->primary = false;
if (empty($attributes->emailAddress)) {
if ($i === 0) {
$o->primary = true;
@@ -2317,8 +2422,10 @@ class Record implements Crud,
} else {
$o->primary = $o->emailAddress === $attributes->emailAddress;
}
$o->optOut = $emailAddress->get('optOut');
$o->invalid = $emailAddress->get('invalid');
$emailAddressData[] = $o;
}
$attributes->emailAddressData = $emailAddressData;
@@ -2326,10 +2433,12 @@ class Record implements Crud,
if ($hasPhoneNumber) {
$phoneNumberData = [];
foreach ($phoneNumberToRelateList as $i => $phoneNumber) {
$o = (object) [];
$o->phoneNumber = $phoneNumber->get('name');
$o->primary = false;
if (empty($attributes->phoneNumber)) {
if ($i === 0) {
$o->primary = true;
@@ -2337,7 +2446,9 @@ class Record implements Crud,
} else {
$o->primary = $o->phoneNumber === $attributes->phoneNumber;
}
$o->type = $phoneNumber->get('type');
$phoneNumberData[] = $o;
}
$attributes->phoneNumberData = $phoneNumberData;
@@ -2364,9 +2475,11 @@ class Record implements Crud,
protected function findLinkedFollowers($id, $params)
{
$entity = $this->getRepository()->get($id);
if (!$entity) {
throw new NotFound();
}
if (!$this->getAcl()->check($entity, 'read')) {
throw new Forbidden();
}
@@ -2396,32 +2509,47 @@ class Record implements Crud,
foreach ($fields as $field => $item) {
if (!empty($item['duplicateIgnore']) || in_array($field, $this->duplicateIgnoreFieldList)) {
$attributeToIgnoreList = $fieldManager->getAttributeList($this->entityType, $field);
foreach ($attributeToIgnoreList as $attribute) {
unset($attributes->$attribute);
}
continue;
}
if (empty($item['type'])) {
continue;
}
if (empty($item['type'])) continue;
$type = $item['type'];
if (in_array($type, ['file', 'image'])) {
$attachment = $entity->get($field);
if ($attachment) {
$attachment = $this->getEntityManager()->getRepository('Attachment')->getCopiedAttachment($attachment);
$attachment = $this->getEntityManager()
->getRepository('Attachment')
->getCopiedAttachment($attachment);
$idAttribute = $field . 'Id';
if ($attachment) {
$attributes->$idAttribute = $attachment->id;
}
}
} else if (in_array($type, ['attachmentMultiple'])) {
$attachmentList = $entity->get($field);
if (count($attachmentList)) {
$idList = [];
$nameHash = (object) [];
$typeHash = (object) [];
foreach ($attachmentList as $attachment) {
$attachment = $this->getEntityManager()->getRepository('Attachment')->getCopiedAttachment($attachment);
$attachment = $this->getEntityManager()
->getRepository('Attachment')
->getCopiedAttachment($attachment);
if ($attachment) {
$idList[] = $attachment->id;
$nameHash->{$attachment->id} = $attachment->get('name');
@@ -2435,8 +2563,12 @@ class Record implements Crud,
} else if ($type === 'linkMultiple') {
$foreignLink = $entity->getRelationParam($field, 'foreign');
$foreignEntityType = $entity->getRelationParam($field, 'entity');
if ($foreignEntityType && $foreignLink) {
$foreignRelationType = $this->getMetadata()->get(['entityDefs', $foreignEntityType, 'links', $foreignLink, 'type']);
$foreignRelationType = $this->getMetadata()->get(
['entityDefs', $foreignEntityType, 'links', $foreignLink, 'type']
);
if ($foreignRelationType !== 'hasMany') {
unset($attributes->{$field . 'Ids'});
unset($attributes->{$field . 'Names'});
@@ -2461,7 +2593,9 @@ class Record implements Crud,
$duplicatingEntityId = $data->_duplicatingEntityId;
if (!$duplicatingEntityId) return;
$duplicatingEntity = $this->getEntityManager()->getEntity($entity->getEntityType(), $duplicatingEntityId);
if (!$duplicatingEntity) return;
if (!$this->getAcl()->check($duplicatingEntity, 'read')) return;
@@ -2474,6 +2608,7 @@ class Record implements Crud,
foreach ($this->duplicatingLinkList as $link) {
$linkedList = $repository->findRelated($duplicatingEntity, $link);
foreach ($linkedList as $linked) {
$repository->relate($entity, $link, $linked);
}
+19 -8
View File
@@ -138,26 +138,35 @@ class Export
if (array_key_exists('ids', $params)) {
$ids = $params['ids'];
$where = [
[
'type' => 'in',
'field' => 'id',
'value' => $ids
'value' => $ids,
]
];
$selectParams = $selectManager->getSelectParams(['where' => $where], true, true);
$selectParams = $selectManager->getSelectParams(['where' => $where], true, true, true);
}
else if (array_key_exists('where', $params)) {
$where = $params['where'];
$p = [];
$p['where'] = $where;
$searchParams = [];
$searchParams['where'] = $where;
if (!empty($params['selectData']) && is_array($params['selectData'])) {
foreach ($params['selectData'] as $k => $v) {
$p[$k] = $v;
$searchParams[$k] = $v;
}
}
$selectParams = $this->getSelectParams($p);
if (isset($searchParams['select']) && is_string($searchParams['select'])) {
$searchParams['select'] = explode(',', $searchParams['select']);
}
$selectParams = $selectManager->getSelectParams($searchParams, true, true, true);
}
else {
throw new BadRequest();
@@ -172,14 +181,15 @@ class Export
$select = Select::fromRaw($selectParams);
$collection = $this->entityManager->getRepository($this->entityType)
$collection = $this->entityManager
->getRepository($this->entityType)
->clone($select)
->sth()
->find();
}
$attributeListToSkip = [
'deleted'
'deleted',
];
foreach ($this->skipAttributeList as $attribute) {
@@ -225,6 +235,7 @@ class Export
unset($fieldList[$i]);
}
}
$fieldList = array_values($fieldList);
if (method_exists($exportObj, 'filterFieldList')) {
@@ -56,7 +56,11 @@ class Csv
public function loadAdditionalFields(Entity $entity, $fieldList)
{
foreach ($fieldList as $field) {
if ($this->metadata->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']) === 'linkMultiple') {
$fieldType = $this->metadata->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']);
if (
$fieldType === 'linkMultiple' || $fieldType === 'attachmentMultiple'
) {
if (!$entity->has($field . 'Ids')) {
$entity->loadLinkMultipleField($field);
}
@@ -113,7 +113,9 @@ class Xlsx
}
}
foreach ($fieldList as $field) {
if ($this->getMetadata()->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']) === 'linkMultiple') {
$fieldType = $this->getMetadata()->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']);
if ($fieldType === 'linkMultiple' || $fieldType === 'attachmentMultiple') {
if (!$entity->has($field . 'Ids')) {
$entity->loadLinkMultipleField($field);
}
@@ -442,7 +444,7 @@ class Xlsx
}
$sheet->setCellValue("$col$rowNumber", $value);
}
} else if ($type == 'linkMultiple') {
} else if ($type == 'linkMultiple' || $type == 'attachmentMultiple') {
if (array_key_exists($name . 'Ids', $row) && array_key_exists($name . 'Names', $row)) {
$nameList = [];
foreach ($row[$name . 'Ids'] as $relatedId) {
@@ -37,11 +37,12 @@ use Espo\Core\{
Utils\Language,
};
class LabelManager implements Di\DefaultLanguageAware, Di\MetadataAware, Di\FileManagerAware
class LabelManager implements Di\DefaultLanguageAware, Di\MetadataAware, Di\FileManagerAware, Di\DataCacheAware
{
use Di\DefaultLanguageSetter;
use Di\MetadataSetter;
use Di\FileManagerSetter;
use Di\DataCacheSetter;
protected $ignoreList = [
'Global.sets',
@@ -71,7 +72,7 @@ class LabelManager implements Di\DefaultLanguageAware, Di\MetadataAware, Di\File
public function getScopeData($language, $scope)
{
$languageObj = new Language($language, $this->fileManager, $this->metadata);
$languageObj = new Language($language, $this->fileManager, $this->metadata, $this->dataCache);
$data = $languageObj->get($scope);
@@ -179,8 +180,8 @@ class LabelManager implements Di\DefaultLanguageAware, Di\MetadataAware, Di\File
public function saveLabels($language, $scope, $labels)
{
$languageObj = new Language($language, $this->fileManager, $this->metadata);
$languageOriginalObj = new Language($language, $this->fileManager, $this->metadata, false, true);
$languageObj = new Language($language, $this->fileManager, $this->metadata, $this->dataCache);
$languageOriginalObj = new Language($language, $this->fileManager, $this->metadata, $this->dataCache, false, true);
$returnDataHash = [];
@@ -9,4 +9,4 @@
</div>
{{/if}}
<div class="edit-container">{{{edit}}}</div>
<div class="edit-container record">{{{edit}}}</div>
@@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep) {
define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep) {
return Dep.extend({
@@ -42,25 +42,32 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
return {
scopeList: this.scopeList,
scope: this.scope,
isNew: !(this.id)
isNew: !(this.id),
};
},
events: {
'change .scope-switcher input[name="scope"]': function () {
this.notify('Loading...');
var scope = $('.scope-switcher input[name="scope"]:checked').val();
this.scope = scope;
this.getModelFactory().create(this.scope, function (model) {
model.populateDefaults();
var attributes = this.getView('edit').fetch();
attributes = _.extend(attributes, this.getView('edit').model.toJSON());
model.set(attributes);
this.model = model;
this.createRecordView(model, function (view) {
view.render();
view.notify(false);
});
this.handleAccess(model);
}.bind(this));
},
@@ -75,11 +82,13 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
if (this.options.allDay) {
var allDayScopeList = this.getMetadata().get('clientDefs.Calendar.allDayScopeList') || [];
if (~allDayScopeList.indexOf(this.scope)) {
this.model.set('dateStart', null);
this.model.set('dateEnd', null);
this.model.set('dateStartDate', null);
this.model.set('dateEndDate', this.options.dateEndDate);
if (this.options.dateEndDate !== this.options.dateStartDate) {
this.model.set('dateStartDate', this.options.dateStartDate)
}
@@ -102,7 +111,9 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
this.dateIsChanged = true;
}
}, this);
this.listenTo(this.model, 'change:dateEnd', function (m, value, o) {
this.listenTo(
this.model, 'change:dateEnd', function (m, value, o) {
if (o.ui || o.updatedByDuration) {
this.dateIsChanged = true;
}
@@ -115,6 +126,7 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
if (this.id && !this.getAcl().checkModel(model, 'edit') || !this.id && !this.getAcl().checkModel(model, 'create')) {
this.hideButton('save');
this.hideButton('fullForm');
this.$el.find('button[data-name="save"]').addClass('hidden');
this.$el.find('button[data-name="fullForm"]').addClass('hidden');
} else {
@@ -131,8 +143,10 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
afterRender: function () {
Dep.prototype.afterRender.call(this);
if (this.hasView('edit')) {
var model = this.getView('edit').model;
if (model) {
this.handleAccess(model);
}
@@ -145,6 +159,7 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
if (!this.options.id && !this.options.scope) {
var scopeList = [];
this.scopeList.forEach(function (scope) {
if (this.getAcl().check(scope, 'create')) {
if (~this.enabledScopeList.indexOf(scope)) {
@@ -152,6 +167,7 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
}
}
}, this);
this.scopeList = scopeList;
var calendarDefaultEntity = scopeList[0];
@@ -187,6 +203,7 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
this.confirm(this.translate('removeRecordConfirmation', 'messages'), function () {
var $buttons = this.dialog.$el.find('.modal-footer button');
$buttons.addClass('disabled');
model.destroy({
success: function () {
this.trigger('after:destroy', model);
@@ -194,10 +211,9 @@ Espo.define('crm:views/calendar/modals/edit', 'views/modals/edit', function (Dep
}.bind(this),
error: function () {
$buttons.removeClass('disabled');
}
},
});
}, this);
}
},
});
});
@@ -34,19 +34,30 @@ define('crm:views/case/record/detail', 'views/record/detail', function (Dep) {
setupActionItems: function () {
Dep.prototype.setupActionItems.call(this);
if (
this.getAcl().checkModel(this.model, 'edit') &&
!~['Closed', 'Rejected', 'Duplicate'].indexOf(this.model.get('status')) &&
this.getAcl().checkField(this.entityType, 'status', 'edit')
) {
this.dropdownItemList.push({
'label': 'Close',
'name': 'close',
});
this.dropdownItemList.push({
'label': 'Reject',
'name': 'reject',
});
var statusList = this.getMetadata().get(
['entityDefs', 'Case', 'fields', 'status', 'options']
) || [];
if (~statusList.indexOf('Closed')) {
this.dropdownItemList.push({
'label': 'Close',
'name': 'close',
});
}
if (~statusList.indexOf('Rejected')) {
this.dropdownItemList.push({
'label': 'Reject',
'name': 'reject',
});
}
}
},
@@ -32,7 +32,8 @@ Espo.define('crm:views/task/fields/is-overdue', 'views/fields/base', function (D
readOnly: true,
_template: '{{#if isOverdue}}<span class="label label-danger">{{translate "overdue"}}</span>{{/if}}',
_template: '{{#if isOverdue}}<span class="label label-danger">' +
'{{translate "overdue" scope="Task"}}</span>{{/if}}',
data: function () {
var isOverdue = false;
+1 -1
View File
@@ -4,7 +4,7 @@
<div class="button-container">
<button class="btn btn-default pull-right hidden" data-action="reset">{{translate 'Reset'}}</button>
<div class="btn-group">
<button class="btn btn-default dropdown-toggle select-field" data-toggle="dropdown" tabindex="-1">{{translate 'Select Field'}} <span class="caret"></span></button>
<button class="btn btn-default dropdown-toggle select-field" data-toggle="dropdown" tabindex="-1">{{translate 'Add Field'}} <span class="caret"></span></button>
<ul class="dropdown-menu pull-left filter-list">
{{#each ../fieldList}}
<li data-name="{{./this}}"><a href="javascript:" data-name="{{./this}}" data-action="add-field">{{translate this scope=../../entityType category='fields'}}</a></li>
+26 -7
View File
@@ -1,9 +1,28 @@
<div class="cell">
<a href="javascript:"
class="action{{#unless showFold}} hidden{{/unless}} small"
data-action="fold"
data-id="{{model.id}}"><span class="fas fa-chevron-down"></span></a>
<div class="cell">
<a href="javascript:" class="action{{#unless showFold}} hidden{{/unless}} small" data-action="fold" data-id="{{model.id}}"><span class="fas fa-chevron-down"></span></a>
<a href="javascript:" class="action{{#unless showUnfold}} hidden{{/unless}} small" data-action="unfold" data-id="{{model.id}}"><span class="fas fa-chevron-right"></span></a>
<span data-name="white-space" data-id="{{model.id}}" class="empty-icon{{#unless isEnd}} hidden{{/unless}}">&nbsp;</span>
<a href="javascript:"
class="action{{#unless showUnfold}} hidden{{/unless}} small"
data-action="unfold"
data-id="{{model.id}}"><span class="fas fa-chevron-right"></span></a>
<a href="#{{model.name}}/view/{{model.id}}" class="link{{#if isSelected}} text-bold{{/if}}" data-id="{{model.id}}">{{name}}</a>
</div>
<div class="children{{#unless isUnfolded}} hidden{{/unless}}">{{{children}}}</div>
<span data-name="white-space" data-id="{{model.id}}" class="empty-icon{{#unless isEnd}} hidden{{/unless}}">&nbsp;</span>
<a href="#{{model.name}}/view/{{model.id}}"
class="link{{#if isSelected}} text-bold{{/if}}" data-id="{{model.id}}"
>{{name}}</a>
{{#unless readOnly}}
<a href="javascript:"
class="action small remove-link hidden"
data-action="remove" data-id="{{model.id}}"
title="{{translate 'Remove'}}"
>
<span class="fas fa-times"></span>
</a>
{{/unless}}
</div>
<div class="children{{#unless isUnfolded}} hidden{{/unless}}">{{{children}}}</div>
+1 -1
View File
@@ -14,7 +14,7 @@
{{/unless}}
{{/if}}
{{/unless}}
<div class="list list-expanded">
<div class="list list-expanded list-tree">
{{#if showRootMenu}}
<div class="btn-group pull-right">
<a href="javascript:" class="small dropdown-toggle btn-link" data-toggle="dropdown">
+17 -1
View File
@@ -142,7 +142,10 @@ define('ui', [], function () {
if (this.removeOnClose) {
this.$el.on('hidden.bs.modal', function (e) {
if (this.$el.get(0) == e.target) {
if (this.skipRemove) return;
if (this.skipRemove) {
return;
}
this.remove();
}
}.bind(this));
@@ -324,11 +327,22 @@ define('ui', [], function () {
$('body > .popover').addClass('hidden');
};
Dialog.prototype.hide = function () {
this.$el.find('.modal-content').addClass('hidden');
};
Dialog.prototype.hideWithBackdrop = function () {
var $modalBackdrop = $('.modal-backdrop');
$modalBackdrop.last().addClass('hidden');
$($modalBackdrop.get($modalBackdrop.length - 2)).removeClass('hidden');
var $modalConainer = $('.modal-container');
$($modalConainer.get($modalConainer.length - 2)).removeClass('overlaid');
this.skipRemove = true;
setTimeout(function () {
this.skipRemove = false;
}.bind(this), 50);
@@ -337,6 +351,7 @@ define('ui', [], function () {
this.$el.find('.modal-content').addClass('hidden');
};
Dialog.prototype.close = function () {
var $modalBackdrop = $('.modal-backdrop');
$modalBackdrop.last().removeClass('hidden');
@@ -347,6 +362,7 @@ define('ui', [], function () {
this.$el.modal('hide');
$(this).trigger('dialog:close');
};
Dialog.prototype.remove = function () {
this.onRemove();
this.$el.remove();
@@ -88,6 +88,7 @@ define('views/admin/field-manager/edit', ['view', 'model'], function (Dep, Model
this.model.set('tooltipText', this.getLanguage().translate(this.field, 'tooltips', this.scope));
}
} else {
this.model.scope = this.scope;
this.model.set('type', this.type);
}
@@ -0,0 +1,57 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy 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/admin/field-manager/fields/date/after-before', 'views/fields/varchar', function (Dep) {
return Dep.extend({
setupOptions: function () {
Dep.prototype.setupOptions.call(this);
if (!this.model.scope) {
return;
}
var list = this.getFieldManager().getEntityTypeFieldList(
this.model.scope,
{
typeList: ['date', 'datetime', 'datetimeOptional'],
}
);
if (this.model.get('name')) {
list = list.filter(function (item) {
return item !== this.model.get('name');
}, this);
}
this.params.options = list;
},
});
});
+1
View File
@@ -84,6 +84,7 @@ define('views/admin/index', 'view', function (Dep) {
panelItem.items.forEach(function (item) {
item.label = this.translate(item.label, 'labels', 'Admin');
panelItem.itemList.push(item);
item.keywords = [];
}, this);
}
+15 -5
View File
@@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/admin/integrations/index', 'view', function (Dep) {
define('views/admin/integrations/index', 'view', function (Dep) {
return Dep.extend({
@@ -51,12 +51,13 @@ Espo.define('views/admin/integrations/index', 'view', function (Dep) {
},
setup: function () {
this.integrationList = Object.keys(this.getMetadata().get('integrations') || {});;
this.integrationList = Object.keys(this.getMetadata().get('integrations') || {});
this.integration = this.options.integration || null;
this.on('after:render', function () {
this.renderHeader();
if (!this.integration) {
this.renderDefaultPage();
} else {
@@ -70,34 +71,45 @@ Espo.define('views/admin/integrations/index', 'view', function (Dep) {
this.getRouter().navigate('#Admin/integrations/name=' + integration, {trigger: false});
var viewName = this.getMetadata().get('integrations.' + integration + '.view') || 'views/admin/integrations/' + Espo.Utils.camelCaseToHyphen(this.getMetadata().get('integrations.' + integration + '.authMethod'));
var viewName = this.getMetadata().get('integrations.' + integration + '.view') ||
'views/admin/integrations/' +
Espo.Utils.camelCaseToHyphen(this.getMetadata().get('integrations.' + integration + '.authMethod'));
this.notify('Loading...');
this.createView('content', viewName, {
el: '#integration-content',
integration: integration,
}, function (view) {
this.renderHeader();
view.render();
this.notify(false);
$(window).scrollTop(0);
}.bind(this));
},
renderDefaultPage: function () {
$('#integration-header').html('').hide();
if (this.integrationList.length) {
var msg = this.translate('selectIntegration', 'messages', 'Integration');
} else {
var msg = '<p class="lead">' + this.translate('noIntegrations', 'messages', 'Integration') + '</p>';
}
$('#integration-content').html(msg);
},
renderHeader: function () {
if (!this.integration) {
$('#integration-header').html('');
return;
}
$('#integration-header').show().html(this.translate(this.integration, 'titles', 'Integration'));
},
@@ -106,5 +118,3 @@ Espo.define('views/admin/integrations/index', 'view', function (Dep) {
},
});
});
+17 -8
View File
@@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/export/modals/export', ['views/modal', 'model'], function (Dep, Model) {
define('views/export/modals/export', ['views/modal', 'model'], function (Dep, Model) {
return Dep.extend({
@@ -44,11 +44,11 @@ Espo.define('views/export/modals/export', ['views/modal', 'model'], function (De
{
name: 'export',
label: 'Export',
style: 'danger'
style: 'danger',
},
{
name: 'cancel',
label: 'Cancel'
label: 'Cancel',
}
];
@@ -73,44 +73,53 @@ Espo.define('views/export/modals/export', ['views/modal', 'model'], function (De
this.createView('record', 'views/export/record/record', {
scope: this.scope,
model: this.model,
el: this.getSelector() + ' .record'
el: this.getSelector() + ' .record',
});
},
actionExport: function () {
var data = this.getView('record').fetch();
this.model.set(data);
if (this.getView('record').validate()) return;
if (this.getView('record').validate()) {
return;
}
var returnData = {
exportAllFields: data.exportAllFields,
format: data.format
format: data.format,
};
if (!data.exportAllFields) {
var attributeList = [];
data.fieldList.forEach(function (item) {
if (item === 'id') {
attributeList.push('id');
return;
}
var type = this.getMetadata().get(['entityDefs', this.scope, 'fields', item, 'type']);
if (type) {;
if (type) {
this.getFieldManager().getAttributeList(type, item).forEach(function (attribute) {
attributeList.push(attribute);
}, this);
}
if (~item.indexOf('_')) {
attributeList.push(item);
}
}, this);
returnData.attributeList = attributeList;
returnData.fieldList = data.fieldList;
}
this.trigger('proceed', returnData);
this.close();
}
},
});
});
+22 -10
View File
@@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/export/record/record', 'views/record/base', function (Dep) {
define('views/export/record/record', 'views/record/base', function (Dep) {
return Dep.extend({
@@ -48,10 +48,10 @@ Espo.define('views/export/record/record', 'views/record/base', function (Dep) {
fieldList = fieldList.filter(function (item) {
var defs = this.getMetadata().get(['entityDefs', this.scope, 'fields', item]) || {};
if (defs.disabled) return;
if (defs.exportDisabled) return;
if (defs.type === 'map') return;
if (defs.type === 'attachmentMultiple') return;
return true;
}, this);
@@ -61,32 +61,43 @@ Espo.define('views/export/record/record', 'views/record/base', function (Dep) {
fieldList.unshift('id');
var translatedOptions = {};
fieldList.forEach(function (item) {
translatedOptions[item] = this.getLanguage().translate(item, 'fields', this.scope);
}, this);
this.createField('exportAllFields', 'views/fields/bool', {
});
this.createField('exportAllFields', 'views/fields/bool', {});
var setFieldList = this.model.get('fieldList') || [];
setFieldList.forEach(function (item) {
if (~fieldList.indexOf(item)) return;
if (!~item.indexOf('_')) return;
if (~fieldList.indexOf(item)) {
return;
}
if (!~item.indexOf('_')) {
return;
}
var arr = item.split('_');
fieldList.push(item);
var foreignScope = this.getMetadata().get(['entityDefs', this.scope, 'links', arr[0], 'entity']);
if (!foreignScope) return;
translatedOptions[item] = this.getLanguage().translate(arr[0], 'links', this.scope) + '.' + this.getLanguage().translate(arr[1], 'fields', foreignScope);
if (!foreignScope) {
return;
}
translatedOptions[item] = this.getLanguage().translate(arr[0], 'links', this.scope) + '.' +
this.getLanguage().translate(arr[1], 'fields', foreignScope);
}, this);
this.createField('fieldList', 'views/fields/multi-enum', {
required: true,
translatedOptions: translatedOptions,
options: fieldList
options: fieldList,
});
var formatList =
@@ -98,6 +109,7 @@ Espo.define('views/export/record/record', 'views/record/base', function (Dep) {
});
this.controlAllFields();
this.listenTo(this.model, 'change:exportAllFields', function () {
this.controlAllFields();
}, this);
@@ -109,7 +121,7 @@ Espo.define('views/export/record/record', 'views/record/base', function (Dep) {
} else {
this.hideField('fieldList');
}
}
},
});
});
+80 -21
View File
@@ -34,6 +34,8 @@ define('views/fields/file', 'views/fields/link', function (Dep) {
listTemplate: 'fields/file/list',
listLinkTemplate: 'fields/file/list',
detailTemplate: 'fields/file/detail',
editTemplate: 'fields/file/edit',
@@ -134,7 +136,9 @@ define('views/fields/file', 'views/fields/link', function (Dep) {
if (this.isUploading) {
var $target = this.$el.find('.gray-box');
var msg = this.translate('fieldIsUploading', 'messages').replace('{field}', this.getLabelText());
this.showValidationMessage(msg, $target);
return true;
}
},
@@ -150,12 +154,18 @@ define('views/fields/file', 'views/fields/link', function (Dep) {
var sourceDefs = this.getMetadata().get(['clientDefs', 'Attachment', 'sourceDefs']) || {};
this.sourceList = Espo.Utils.clone(this.params.sourceList || []).filter(function (item) {
if (!(item in sourceDefs)) return true;
if (!(item in sourceDefs)) {
return true;
}
var defs = sourceDefs[item];
if (defs.configCheck) {
var configCheck = defs.configCheck;
if (configCheck) {
var arr = configCheck.split('.');
if (this.getConfig().getByPath(arr)) {
return true;
}
@@ -193,6 +203,7 @@ define('views/fields/file', 'views/fields/link', function (Dep) {
var name = this.model.get(this.nameName);
var type = this.model.get(this.typeName) || this.defaultType;
var id = this.model.get(this.idName);
if (id) {
this.addAttachmentBox(name, type, id);
}
@@ -227,6 +238,7 @@ define('views/fields/file', 'views/fields/link', function (Dep) {
if (this.previewSize === 'large') {
this.handleResize();
this.resizeIsBeingListened = true;
$(window).on('resize.' + this.cid, function () {
this.handleResize();
}.bind(this));
@@ -241,11 +253,36 @@ define('views/fields/file', 'views/fields/link', function (Dep) {
getDetailPreview: function (name, type, id) {
name = Handlebars.Utils.escapeExpression(name);
var preview = name;
if (~this.previewTypeList.indexOf(type)) {
preview = '<a data-action="showImagePreview" data-id="' + id + '" href="' + this.getImageUrl(id) + '"><img src="'+this.getBasePath()+'?entryPoint=image&size='+this.previewSize+'&id=' + id + '" class="image-preview"></a>';
if (!~this.previewTypeList.indexOf(type)) {
return name;
}
var previewSize = this.previewSize;
if (this.isListMode()) {
previewSize = 'small';
if (this.params.listPreviewSize) {
previewSize = this.params.listPreviewSize;
}
}
var src = this.getBasePath() + '?entryPoint=image&size=' + previewSize + '&id=' + id;
var img = '<img src="' + src + '" class="image-preview">';
if (this.mode === 'listLink') {
var link = '#' + this.model.entityType + '/view/' + this.model.id;
return '<a href="'+link+'">' + img + '</a>';
}
var preview = '' +
'<a data-action="showImagePreview" data-id="' + id + '" href="' + this.getImageUrl(id) + '">' +
img +
'</a>';
return preview;
},
@@ -261,50 +298,68 @@ define('views/fields/file', 'views/fields/link', function (Dep) {
},
getValueForDisplay: function () {
if (this.mode == 'detail' || this.mode == 'list') {
var name = this.model.get(this.nameName);
var type = this.model.get(this.typeName) || this.defaultType;
var id = this.model.get(this.idName);
if (!id) {
return false;
}
var string = '';
if (this.showPreview && ~this.previewTypeList.indexOf(type)) {
string = '<div class="attachment-preview">' + this.getDetailPreview(name, type, id) + '</div>';
} else {
string = '<span class="fas fa-paperclip text-soft small"></span> <a href="'+ this.getDownloadUrl(id) +'" target="_BLANK">' + Handlebars.Utils.escapeExpression(name) + '</a>';
}
return string;
if (! (this.isDetailMode() || this.isListMode())) {
return '';
}
var name = this.model.get(this.nameName);
var type = this.model.get(this.typeName) || this.defaultType;
var id = this.model.get(this.idName);
if (!id) {
return false;
}
if (this.showPreview && ~this.previewTypeList.indexOf(type)) {
var classNamePart = '';
if (this.isListMode() && this.params.listPreviewSize) {
classNamePart += ' no-shrink';
}
return'<div class="attachment-preview'+classNamePart+'">' +
this.getDetailPreview(name, type, id) +
'</div>';
}
return '<span class="fas fa-paperclip text-soft small"></span> ' +
'<a href="'+ this.getDownloadUrl(id) +'" target="_BLANK">' +
Handlebars.Utils.escapeExpression(name) +
'</a>';
},
getImageUrl: function (id, size) {
var url = this.getBasePath() + '?entryPoint=image&id=' + id;
if (size) {
url += '&size=' + size;
}
if (this.getUser().get('portalId')) {
url += '&portalId=' + this.getUser().get('portalId');
}
return url;
},
getDownloadUrl: function (id) {
var url = this.getBasePath() + '?entryPoint=download&id=' + id;
if (this.getUser().get('portalId')) {
url += '&portalId=' + this.getUser().get('portalId');
}
return url;
},
deleteAttachment: function () {
var id = this.model.get(this.idName);
var o = {};
o[this.idName] = null;
o[this.nameName] = null;
this.model.set(o);
this.$attachment.empty();
@@ -322,8 +377,10 @@ define('views/fields/file', 'views/fields/link', function (Dep) {
setAttachment: function (attachment) {
var arr = _.clone(this.model.get(this.idsName));
var o = {};
o[this.idName] = attachment.id;
o[this.nameName] = attachment.get('name');
this.model.set(o);
},
@@ -343,11 +400,13 @@ define('views/fields/file', 'views/fields/link', function (Dep) {
exceedsMaxFileSize = true;
}
}
if (exceedsMaxFileSize) {
var msg = this.translate('fieldMaxFileSizeError', 'messages')
.replace('{field}', this.getLabelText())
.replace('{max}', maxFileSize);
this.showValidationMessage(msg, '.attachment-button label');
return;
}
+2 -2
View File
@@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/fields/image', 'views/fields/file', function (Dep) {
define('views/fields/image', 'views/fields/file', function (Dep) {
return Dep.extend({
@@ -38,7 +38,7 @@ Espo.define('views/fields/image', 'views/fields/file', function (Dep) {
defaultType: 'image/jpeg',
previewSize: 'small'
previewSize: 'small',
});
});
+20 -1
View File
@@ -624,6 +624,10 @@ define('views/fields/wysiwyg', ['views/fields/text', 'lib!Summernote'], function
var self = options.espoView;
var lang = options.langInfo;
if (!self) {
return;
}
context.memo('button.espoImage', function () {
var button = ui.button({
contents: options.espoImage.icon,
@@ -673,6 +677,10 @@ define('views/fields/wysiwyg', ['views/fields/text', 'lib!Summernote'], function
var self = options.espoView;
var lang = options.langInfo;
if (!self) {
return;
}
this.show = function () {
var linkInfo = context.invoke('editor.getLinkInfo');
@@ -705,6 +713,10 @@ define('views/fields/wysiwyg', ['views/fields/text', 'lib!Summernote'], function
var self = options.espoView;
var lang = options.langInfo;
if (!self) {
return;
}
var isMacLike = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
context.memo('button.espoLink', function () {
@@ -721,7 +733,10 @@ define('views/fields/wysiwyg', ['views/fields/text', 'lib!Summernote'], function
this.initialize = function () {};
this.destroy = function () {
if (!self) return;
if (!self) {
return;
}
self.clearView('dialogInsertLink');
}
@@ -757,6 +772,10 @@ define('views/fields/wysiwyg', ['views/fields/text', 'lib!Summernote'], function
var self = options.espoView;
var lang = options.langInfo;
if (!self) {
return;
}
this.$window = $(window);
this.$scrollbar = $('html, body');
+4 -4
View File
@@ -388,13 +388,13 @@ define('views/import/step2', 'view', function (Dep) {
},
disableButtons: function () {
this.$el.find('button[data-action="next"]').addClass('disabled');
this.$el.find('button[data-action="back"]').addClass('disabled');
this.$el.find('button[data-action="next"]').addClass('disabled').attr('disabled', 'disabled');
this.$el.find('button[data-action="back"]').addClass('disabled').attr('disabled', 'disabled');
},
enableButtons: function () {
this.$el.find('button[data-action="next"]').removeClass('disabled');
this.$el.find('button[data-action="back"]').removeClass('disabled');
this.$el.find('button[data-action="next"]').removeClass('disabled').removeAttr('disabled');
this.$el.find('button[data-action="back"]').removeClass('disabled').removeAttr('disabled');
},
fetch: function (skipValidation) {
+1 -2
View File
@@ -38,8 +38,7 @@ define('views/list-tree', 'views/list', function (Dep) {
getRecordViewName: function () {
return this.getMetadata().get('clientDefs.' + this.scope + '.recordViews.listTree') || 'views/record/list-tree';
}
},
});
});
+2 -2
View File
@@ -366,14 +366,14 @@ define('views/list-with-categories', 'views/list', function (Dep) {
collection: collection,
el: this.options.el + ' .categories-container',
selectable: true,
createDisabled: true,
showRoot: true,
rootName: this.translate(this.scope, 'scopeNamesPlural'),
buttonsDisabled: true,
checkboxes: false,
showEditLink: this.getAcl().check(this.categoryScope, 'edit'),
isExpanded: this.isExpanded,
hasExpandedToggler: this.hasExpandedToggler
hasExpandedToggler: this.hasExpandedToggler,
readOnly: true,
}, function (view) {
if (this.currentCategoryId) {
view.setSelected(this.currentCategoryId);
+41 -2
View File
@@ -70,8 +70,13 @@ define('views/modals/compose-email', 'views/modals/edit', function (Dep) {
this.once('after:render', function () {
this.actionClose();
}, this);
return;
}
this.once('remove', function () {
this.dialogIsHidden = false;
}, this);
},
createRecordView: function (model, callback) {
@@ -99,18 +104,34 @@ define('views/modals/compose-email', 'views/modals/edit', function (Dep) {
var model = editView.model;
var afterSend = function () {
this.dialogIsHidden = false;
this.trigger('after:save', model);
this.trigger('after:send', model);
dialog.close();
this.stopListening(editView, 'before:save', beforeSave);
this.stopListening(editView, 'error:save', errorSave);
this.remove();
}.bind(this);
var beforeSave = function () {
dialog.hide();
this.dialogIsHidden = true;
dialog.hideWithBackdrop();
editView.setConfirmLeaveOut(false);
};
if (!this.forceRemoveIsInitiated) {
this.initiateForceRemove();
}
}.bind(this);
var errorSave = function () {
this.dialogIsHidden = false;
if (this.isRendered()) {
dialog.show();
}
@@ -169,5 +190,23 @@ define('views/modals/compose-email', 'views/modals/edit', function (Dep) {
editView.saveDraft();
},
initiateForceRemove: function () {
this.forceRemoveIsInitiated = true;
var parentView = this.getParentView();
if (!parentView) {
return true;
}
parentView.once('remove', function () {
if (!this.dialogIsHidden) {
return;
}
this.remove();
}, this);
},
});
});
+17 -3
View File
@@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/modals/image-preview', ['views/modal', 'lib!exif'], function (Dep) {
define('views/modals/image-preview', ['views/modal', 'lib!exif'], function (Dep) {
return Dep.extend({
@@ -72,25 +72,29 @@ Espo.define('views/modals/image-preview', ['views/modal', 'lib!exif'], function
getImageUrl: function () {
var url = this.getBasePath() + '?entryPoint=image&id=' + this.options.id;
if (this.size) {
url += '&size=' + this.size;
}
if (this.getUser().get('portalId')) {
url += '&portalId=' + this.getUser().get('portalId');
}
return url;
},
getOriginalImageUrl: function () {
var url = this.getBasePath() + '?entryPoint=image&id=' + this.options.id;
if (this.getUser().get('portalId')) {
url += '&portalId=' + this.getUser().get('portalId');
}
return url;
},
onImageLoad: function () {
console.log(1);
},
afterRender: function () {
@@ -102,7 +106,12 @@ Espo.define('views/modals/image-preview', ['views/modal', 'lib!exif'], function
var imgEl = $img.get(0);
EXIF.getData(imgEl, function () {
if ($img.css('image-orientation') === 'from-image') {
return;
}
var orientation = EXIF.getTag(this, 'Orientation');
switch (orientation) {
case 2:
$img.addClass('transform-flip');
@@ -138,6 +147,7 @@ Espo.define('views/modals/image-preview', ['views/modal', 'lib!exif'], function
if (this.navigationEnabled) {
$img.css('cursor', 'pointer');
$img.click(function () {
this.switchToNext();
}.bind(this));
@@ -145,10 +155,12 @@ Espo.define('views/modals/image-preview', ['views/modal', 'lib!exif'], function
var manageSize = function () {
var width = $container.width();
$img.css('maxWidth', width);
}.bind(this);
$(window).off('resize.image-review');
$(window).on('resize.image-review', function () {
manageSize();
});
@@ -159,12 +171,12 @@ Espo.define('views/modals/image-preview', ['views/modal', 'lib!exif'], function
},
switchToNext: function () {
this.transformClassList.forEach(function (item) {
this.$img.removeClass(item);
}, this);
var index = -1;
this.imageList.forEach(function (d, i) {
if (d.id === this.options.id) {
index = i;
@@ -172,12 +184,14 @@ Espo.define('views/modals/image-preview', ['views/modal', 'lib!exif'], function
}, this);
index++;
if (index > this.imageList.length - 1) {
index = 0;
}
this.options.id = this.imageList[index].id
this.options.name = this.imageList[index].name;
this.reRender();
},
@@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/modals/select-category-tree-records', 'views/modals/select-records', function (Dep) {
define('views/modals/select-category-tree-records', 'views/modals/select-records', function (Dep) {
return Dep.extend({
@@ -113,13 +113,13 @@ Espo.define('views/modals/select-category-tree-records', 'views/modals/select-re
this.createView('list', viewName, {
collection: collection,
el: this.containerSelector + ' .list-container',
createDisabled: true,
readOnly: true,
selectable: true,
checkboxes: this.multiple,
massActionsDisabled: true,
searchManager: searchManager,
checkAllResultDisabled: true,
buttonsDisabled: true
buttonsDisabled: true,
}, function (list) {
list.once('select', function (model) {
this.trigger('select', model);
@@ -136,4 +136,3 @@ Espo.define('views/modals/select-category-tree-records', 'views/modals/select-re
});
});
@@ -55,8 +55,6 @@ define('views/modals/select-records-with-categories', ['views/modals/select-reco
!this.getAcl().checkScope(this.categoryScope);
Dep.prototype.setup.call(this);
},
loadList: function () {
@@ -77,12 +75,12 @@ define('views/modals/select-records-with-categories', ['views/modals/select-reco
collection: collection,
el: this.options.el + ' .categories-container',
selectable: true,
createDisabled: true,
readOnly: true,
showRoot: true,
rootName: this.translate(this.scope, 'scopeNamesPlural'),
buttonsDisabled: true,
checkboxes: false,
isExpanded: this.isExpanded
isExpanded: this.isExpanded,
}, function (view) {
if (this.isRendered()) {
view.render();
+2
View File
@@ -246,6 +246,8 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
this.getAcl().check(this.entityType, 'edit')
&&
!~this.getAcl().getScopeForbiddenFieldList(this.entityType).indexOf('assignedUser')
&&
!this.getUser().isPortal()
) {
if (this.model.has('assignedUserId')) {
this.dropdownItemList.push({
+105 -4
View File
@@ -45,7 +45,8 @@ define('views/record/list-tree-item', 'view', function (Dep) {
showFold: this.isUnfolded && !this.isEnd,
showUnfold: !this.isUnfolded && !this.isEnd,
isEnd: this.isEnd,
isSelected: this.isSelected
isSelected: this.isSelected,
readOnly: this.readOnly,
};
},
@@ -57,6 +58,10 @@ define('views/record/list-tree-item', 'view', function (Dep) {
'click [data-action="fold"]': function (e) {
this.fold();
e.stopPropagation();
},
'click [data-action="remove"]': function (e) {
this.actionRemove();
e.stopPropagation();
}
},
@@ -67,10 +72,13 @@ define('views/record/list-tree-item', 'view', function (Dep) {
var path = this.selectedData.path;
var names = this.selectedData.names;
path.length = 0;
var view = this;
while (1) {
path.unshift(view.model.id);
names[view.model.id] = view.model.get('name');
if (view.getParentView().level) {
view = view.getParentView().getParentView();
} else {
@@ -83,16 +91,27 @@ define('views/record/list-tree-item', 'view', function (Dep) {
if ('level' in this.options) {
this.level = this.options.level;
}
if ('isSelected' in this.options) {
this.isSelected = this.options.isSelected;
}
if ('selectedData' in this.options) {
this.selectedData = this.options.selectedData;
}
this.readOnly = this.options.readOnly;
if ('createDisabled' in this.options) {
this.createDisabled = this.options.createDisabled;
}
if (this.readOnly) {
this.createDisabled = true;
}
this.rootView = this.options.rootView;
this.scope = this.model.name;
this.isUnfolded = false;
@@ -123,23 +142,29 @@ define('views/record/list-tree-item', 'view', function (Dep) {
createChildren: function () {
var childCollection = this.model.get('childCollection');
var callback = null;
if (this.isRendered()) {
callback = function (view) {
this.listenToOnce(view, 'after:render', function () {
this.trigger('children-created');
}, this);
view.render();
}.bind(this);
}
this.createView('children', this.listViewName, {
collection: childCollection,
el: this.options.el + ' > .children',
createDisabled: this.options.createDisabled,
readOnly: this.options.readOnly,
level: this.level + 1,
selectedData: this.selectedData,
model: this.model,
selectable: this.options.selectable
selectable: this.options.selectable,
rootView: this.rootView,
}, callback);
},
@@ -148,14 +173,22 @@ define('views/record/list-tree-item', 'view', function (Dep) {
parentId: this.model.id
}).then(function (idList) {
var childrenView = this.getView('children');
idList.forEach(function (id) {
var model = this.model.get('childCollection').get(id);
if (model) {
model.isEnd = true;
}
var itemView = childrenView.getView(id);
if (!itemView) return;
if (!itemView) {
return;
}
itemView.isEnd = true;
itemView.afterIsEnd();
}, this);
@@ -167,6 +200,7 @@ define('views/record/list-tree-item', 'view', function (Dep) {
if (this.createDisabled) {
this.once('children-created', function () {
var childrenView = this.getView('children');
if (!this.model.lastAreChecked) {
this.checkLastChildren();
}
@@ -174,10 +208,15 @@ define('views/record/list-tree-item', 'view', function (Dep) {
}
var childCollection = this.model.get('childCollection');
if (childCollection !== null) {
this.createChildren();
this.isUnfolded = true;
this.afterUnfold();
this.trigger('after:unfold');
} else {
this.getCollectionFactory().create(this.scope, function (collection) {
collection.url = this.collection.url;
@@ -185,19 +224,27 @@ define('views/record/list-tree-item', 'view', function (Dep) {
collection.maxDepth = 1;
Espo.Ui.notify(this.translate('loading', 'messages'));
this.listenToOnce(collection, 'sync', function () {
this.notify(false);
this.model.set('childCollection', collection);
this.createChildren();
this.isUnfolded = true;
if (collection.length || !this.createDisabled) {
this.afterUnfold();
this.trigger('after:unfold');
} else {
this.isEnd = true;
this.afterIsEnd();
}
}, this);
collection.fetch();
}, this);
}
@@ -205,7 +252,9 @@ define('views/record/list-tree-item', 'view', function (Dep) {
fold: function () {
this.clearView('children');
this.isUnfolded = false;
this.afterFold();
},
@@ -215,9 +264,22 @@ define('views/record/list-tree-item', 'view', function (Dep) {
} else {
this.afterFold();
}
if (this.isEnd) {
this.afterIsEnd();
}
if (!this.readOnly) {
var $remove = this.$el.find('> .cell [data-action="remove"]');
this.$el.find('> .cell').on('mouseenter', function ($el) {
$remove.removeClass('hidden');
});
this.$el.find('> .cell').on('mouseleave', function ($el) {
$remove.addClass('hidden');
});
}
},
afterFold: function () {
@@ -237,7 +299,46 @@ define('views/record/list-tree-item', 'view', function (Dep) {
this.$el.find('a[data-action="fold"][data-id="'+this.model.id+'"]').addClass('hidden');
this.$el.find('span[data-name="white-space"][data-id="'+this.model.id+'"]').removeClass('hidden');
this.$el.find(' > .children').addClass('hidden');
}
},
getCurrentPath: function () {
var pointer = this;
var path = [];
while (true) {
path.unshift(pointer.model.id);
if (pointer.getParentView() === this.rootView) {
break;
}
pointer = pointer.getParentView().getParentView();
}
return path;
},
actionRemove: function () {
this.confirm({
message: this.translate('removeRecordConfirmation', 'messages', this.scope),
confirmText: this.translate('Remove'),
}, function () {
this.model
.destroy(
{
wait: true,
}
)
.then(
function () {
this.remove();
}
.bind(this)
);
}.bind(this));
},
});
});
+48 -7
View File
@@ -64,8 +64,11 @@ define('views/record/list-tree', 'views/record/list', function (Dep) {
data: function () {
var data = Dep.prototype.data.call(this);
data.createDisabled = this.createDisabled;
data.showRoot = this.showRoot;
if (data.showRoot) {
data.rootName = this.rootName || this.translate('Root');
}
@@ -96,12 +99,15 @@ define('views/record/list-tree', 'views/record/list', function (Dep) {
this.selectable = this.options.selectable;
}
this.createDisabled = this.options.createDisabled || this.createDisabled;
this.readOnly = this.options.readOnly;
this.createDisabled = this.readOnly || this.options.createDisabled || this.createDisabled;
this.isExpanded = this.options.isExpanded;
if ('showRoot' in this.options) {
this.showRoot = this.options.showRoot;
if ('rootName' in this.options) {
this.rootName = this.options.rootName;
}
@@ -114,16 +120,21 @@ define('views/record/list-tree', 'views/record/list', function (Dep) {
if ('level' in this.options) {
this.level = this.options.level;
}
this.rootView = this.options.rootView || this;
if (this.level == 0) {
this.selectedData = {
id: null,
path: [],
names: {}
names: {},
};
}
if ('selectedData' in this.options) {
this.selectedData = this.options.selectedData;
}
Dep.prototype.setup.call(this);
if (this.selectable) {
@@ -139,6 +150,7 @@ define('views/record/list-tree', 'views/record/list', function (Dep) {
o.selectedData = this.selectedData;
}
}
if (this.level > 0) {
this.getParentView().trigger('select', o);
}
@@ -152,6 +164,7 @@ define('views/record/list-tree', 'views/record/list', function (Dep) {
} else {
this.selectedData.id = id;
}
this.rowList.forEach(function (key) {
var view = this.getView(key);
@@ -160,6 +173,7 @@ define('views/record/list-tree', 'views/record/list', function (Dep) {
} else {
view.isSelected = false;
}
if (view.hasView('children')) {
view.getView('children').setSelected(id);
}
@@ -176,25 +190,32 @@ define('views/record/list-tree', 'views/record/list', function (Dep) {
var modelList = this.collection.models;
var count = modelList.length;
var built = 0;
modelList.forEach(function (model, i) {
var key = model.id;
this.rowList.push(key);
this.createView(key, this.itemViewName, {
model: model,
collection: this.collection,
el: this.options.el + ' ' + this.getRowSelector(model.id),
createDisabled: this.createDisabled,
readOnly: this.readOnly,
level: this.level,
isSelected: model.id == this.selectedData.id,
selectedData: this.selectedData,
selectable: this.selectable,
setViewBeforeCallback: this.options.skipBuildRows && !this.isRendered()
setViewBeforeCallback: this.options.skipBuildRows && !this.isRendered(),
rootView: this.rootView,
}, function () {
built++;
if (built == count) {
if (typeof callback == 'function') {
callback();
}
this.wait(false);
};
}.bind(this));
@@ -224,7 +245,16 @@ define('views/record/list-tree', 'views/record/list', function (Dep) {
var attributes = this.getCreateAttributes();
attributes.order = this.collection.length + 1;
var maxOrder = 0;
this.collection.models.forEach(function (m) {
if (m.get('order') > maxOrder) {
maxOrder = m.get('order');
}
}, this);
attributes.order = maxOrder + 1;
attributes.parentId = null;
attributes.parentName = null;
@@ -235,17 +265,22 @@ define('views/record/list-tree', 'views/record/list', function (Dep) {
var scope = this.collection.name;
var viewName = this.getMetadata().get('clientDefs.' + scope + '.modalViews.edit') || 'Modals.Edit';
var viewName = this.getMetadata().get('clientDefs.' + scope + '.modalViews.edit') || 'views/modals/edit';
this.createView('quickCreate', viewName, {
scope: scope,
attributes: attributes
attributes: attributes,
}, function (view) {
view.render();
this.listenToOnce(view, 'after:save', function (model) {
view.close();
model.set('childCollection', this.collection.createSeed());
if (model.get('parentId') !== attributes.parentId) {
var v = this;
while (1) {
if (v.level) {
v = v.getParentView().getParentView();
@@ -253,10 +288,14 @@ define('views/record/list-tree', 'views/record/list', function (Dep) {
break;
}
}
v.collection.fetch();
return;
}
this.collection.push(model);
this.buildRows(function () {
this.render();
}.bind(this));
@@ -266,12 +305,14 @@ define('views/record/list-tree', 'views/record/list', function (Dep) {
actionSelectRoot: function () {
this.trigger('select', {id: null});
if (this.selectable) {
this.$el.find('a.link').removeClass('text-bold');
this.$el.find('a.link[data-action="selectRoot"]').addClass('text-bold');
this.setSelected(null);
}
}
},
});
});
+7 -3
View File
@@ -31,6 +31,11 @@ if (substr(php_sapi_name(), 0, 3) != 'cli') exit;
include "bootstrap.php";
use Espo\Core\{
Application,
ApplicationRunners\Rebuild,
};
$arg = isset($_SERVER['argv'][1]) ? trim($_SERVER['argv'][1]) : '';
if (empty($arg)) {
@@ -46,7 +51,7 @@ if (!isset($pathInfo['extension']) || $pathInfo['extension'] !== 'zip' || !is_fi
die("Unsupported package.\n");
}
$app = new \Espo\Core\Application();
$app = new Application();
$app->setupSystemUser();
$config = $app->getContainer()->get('config');
@@ -67,8 +72,7 @@ try {
}
try {
$app = new \Espo\Core\Application();
$app->runRebuild();
(new Application())->run(Rebuild::class);
} catch (\Exception $e) {}
echo "Extension installation is complete.\n";
+15
View File
@@ -1053,6 +1053,13 @@ section {
margin-right: -@container-padding;
}
#main > .list-container > .list-tree > ul,
.modal-body > .list-container > .list-tree > ul {
padding-top: @table-cell-padding;
padding-bottom: @table-cell-padding;
}
@media screen and (min-width: @screen-sm-min) {
#main > .list-container > .list {
@@ -1540,6 +1547,9 @@ label.attach-file-label {
.list-row > .cell > .attachment-preview {
margin-bottom: -@table-cell-padding;
margin-top: -@table-cell-padding;
}
.list-row > .cell > .attachment-preview:not(.no-shrink) {
max-height: @line-height-computed + @table-cell-padding + @table-cell-padding;
a {
@@ -2121,6 +2131,11 @@ table.table td.cell .complex-text {
.list-group-tree > li .cell {
padding: 3px 0;
.remove-link {
margin-left: 4px;
margin-right: 4px;
}
}
.categories-container .root-item {
+23 -13
View File
@@ -410,32 +410,42 @@ class Installer
{
$this->auth();
$user = array(
$result = $this->createRecord('User', [
'id' => '1',
'userName' => $userName,
'password' => $this->getPasswordHash()->hash($password),
'lastName' => 'Admin',
'type' => 'admin',
);
]);
$result = $this->createRecord('User', $user);
return $result;
}
protected function saveAdminPreferences($preferences)
{
$data = [
$this->saveAdminPreferences([
'dateFormat' => '',
'timeFormat' => '',
'timeZone' => '',
'weekStart' => -1,
'defaultCurrency' => '',
'language' => '',
'thousandSeparator' => $preferences['thousandSeparator'] ?? ',',
'decimalMark' => $preferences['decimalMark'] ?? '.',
];
'thousandSeparator' => $this->getConfig()->get('thousandSeparator', ','),
'decimalMark' => $this->getConfig()->get('decimalMark', '.'),
]);
return $result;
}
protected function saveAdminPreferences($preferences)
{
$permittedSettingList = array(
'dateFormat',
'timeFormat',
'timeZone',
'weekStart',
'defaultCurrency',
'thousandSeparator',
'decimalMark',
'language',
);
$data = array_intersect_key($preferences, array_flip($permittedSettingList));
if (empty($data)) {
return true;
}
+3 -3
View File
@@ -100,7 +100,7 @@
{$langs['fields']['Thousand Separator']}
</label>
<div class="field field-thousandSeparator">
<input type="text" class="main-element form-control" name="thousandSeparator" value="{$fields['thousandSeparator'].value}">
<input type="text" class="main-element form-control" name="thousandSeparator" value="{$fields['thousandSeparator'].value}" maxlength="1">
</div>
</div>
@@ -108,8 +108,8 @@
<label class="field-label-decimalMark control-label">
{$langs['fields']['Decimal Mark']} *</label>
<div class="field field-decimalMark">
<input type="text" class="main-element form-control" name="decimalMark" value="{$fields['decimalMark'].value}">
</div>
<input type="text" class="main-element form-control" name="decimalMark" value="{$fields['decimalMark'].value}" maxlength="1">
</div>
</div>
</div>
+16 -4
View File
@@ -45,15 +45,21 @@ class Diff
}
getTagList () {
var dirInitial = process.cwd();
process.chdir(this.espoPath);
var tagsString = cp.execSync('git tag -l --sort=-v:refname').toString();
var tagList = tagsString.trim().split("\n");
process.chdir(dirInitial);
return tagList;
}
getPreviousVersionList () {
var dirInitial = process.cwd();
var version = (require(this.espoPath + '/package.json') || {}).version;
process.chdir(this.espoPath);
@@ -92,6 +98,8 @@ class Diff
}
}
process.chdir(dirInitial);
return versionFromList;
}
@@ -225,6 +233,9 @@ class Diff
}
}
var deletedFileList = this.getDeletedFileList(versionFrom);
var tagList = this.getTagList();
process.chdir(buildPath);
var fileList = [];
@@ -251,8 +262,6 @@ class Diff
fs.writeFileSync(diffBeforeUpgradeFolderPath, beforeUpgradeFileList.join('\n'));
}
var deletedFileList = this.getDeletedFileList(versionFrom);
if (beforeUpgradeFileList.length) {
cp.execSync(
'xargs -a ' + diffBeforeUpgradeFolderPath + ' cp -p --parents -t ' + upgradePath + '/beforeUpgradeFiles'
@@ -265,8 +274,6 @@ class Diff
}
}
var tagList = this.getTagList();
execute('xargs -a ' + diffFilePath + ' cp -p --parents -t ' + upgradePath + '/files' , function (stdout) {
var d = new Date();
@@ -442,6 +449,8 @@ class Diff
}
getDeletedFileList (versionFrom) {
var dirInitial = process.cwd();
process.chdir(this.espoPath);
var deletedFileList = this.getRepositoryDeletedFileList(versionFrom);
@@ -460,6 +469,7 @@ class Diff
deletedFileList = deletedFileList.filter(function (item) {
if (
item.indexOf('tests/') === 0 ||
item.indexOf('upgrades/') === 0 ||
item.indexOf('frontend/less') === 0
) {
return false;
@@ -468,6 +478,8 @@ class Diff
return true;
});
process.chdir(dirInitial);
return deletedFileList;
}
+13 -2
View File
@@ -29,5 +29,16 @@
include "bootstrap.php";
$app = new \Espo\Core\Application();
$app->runEntryPoint('OauthCallback');
use Espo\Core\{
Application,
ApplicationRunners\EntryPoint,
};
$app = new Application();
$app->run(
EntryPoint::class,
(object) [
'entryPoint' => 'oauthCallback',
]
);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "espocrm",
"version": "6.0.0-beta2",
"version": "6.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "espocrm",
"version": "6.0.0-beta2",
"version": "6.0.1",
"description": "",
"main": "index.php",
"repository": {
+5 -5
View File
@@ -72,17 +72,17 @@ class DataCacheTest extends \PHPUnit\Framework\TestCase
$this->assertFalse($result);
}
public function testGetData()
public function testGetDataInt()
{
$this->expectException(Error::class);
$this->fileManager
->expects($this->once())
->method('getPhpContents')
->method('getPhpSafeContents')
->with('data/cache/application/autoload.php')
->willReturn(1);
$result = $this->dataCache->get('autoload');
$this->assertEquals(1, $result);
}
public function testGetError()
@@ -91,7 +91,7 @@ class DataCacheTest extends \PHPUnit\Framework\TestCase
$this->fileManager
->expects($this->once())
->method('getPhpContents')
->method('getPhpSafeContents')
->with('data/cache/application/autoload.php')
->willReturn(false);
+23 -3
View File
@@ -937,9 +937,9 @@ class MysqlQueryComposerTest extends \PHPUnit\Framework\TestCase
$sql = $this->query->compose(Select::fromRaw([
'from' => 'Comment',
'select' => array('COUNT:id', 'post.name'),
'leftJoins' => array('post'),
'groupBy' => array('post.name'),
'select' => ['COUNT:id', 'post.name'],
'leftJoins' => ['post'],
'groupBy' => ['post.name'],
'orderBy' => 'LIST:post.name:Test,Hello',
]));
@@ -972,6 +972,26 @@ class MysqlQueryComposerTest extends \PHPUnit\Framework\TestCase
$this->assertEquals($expectedSql, $sql);
}
public function testOrderByList()
{
$sql = $this->query->compose(Select::fromRaw([
'from' => 'Comment',
'select' => ['id'],
'leftJoins' => ['post'],
'groupBy' => ['post.name'],
'orderBy' => 'LIST:post.name:Test,Hello',
'distinct' => true,
]));
$expectedSql =
"SELECT comment.id AS `id`, post.name AS `post.name` FROM `comment` " .
"LEFT JOIN `post` AS `post` ON comment.post_id = post.id " .
"WHERE comment.deleted = 0 " .
"GROUP BY post.name ".
"ORDER BY FIELD(post.name, 'Hello', 'Test') DESC";
$this->assertEquals($expectedSql, $sql);
}
public function testOrderByExpression1()
{
$select = $this->queryBuilder
+7 -3
View File
@@ -31,6 +31,11 @@ if (substr(php_sapi_name(), 0, 3) != 'cli') exit;
include "bootstrap.php";
use Espo\Core\{
Application,
ApplicationRunners\Rebuild,
};
$arg = isset($_SERVER['argv'][1]) ? trim($_SERVER['argv'][1]) : '';
if ($arg == 'version' || $arg == '-v') {
@@ -51,7 +56,7 @@ if (!isset($pathInfo['extension']) || $pathInfo['extension'] !== 'zip' || !is_fi
die("Unsupported package.\n");
}
$app = new \Espo\Core\Application();
$app = new Application();
$app->setupSystemUser();
$config = $app->getContainer()->get('config');
@@ -73,8 +78,7 @@ try {
}
try {
$app = new \Espo\Core\Application();
$app->runRebuild();
(new Application())->run(Rebuild::class);
} catch (\Exception $e) {}
echo "Upgrade is complete. Current version is " . $config->get('version') . ". \n";
+27
View File
@@ -34,6 +34,8 @@ class AfterUpgrade
$this->container = $container;
$this->processEmailAccountsUpdate();
$this->cleanupFiles();
}
protected function processEmailAccountsUpdate()
@@ -46,4 +48,29 @@ class AfterUpgrade
$pdo->exec($q1);
$pdo->exec($q2);
}
protected function cleanupFiles()
{
$fileManager = $this->container->get('fileManager');
$fileList = [
'application/Espo/Core/Loaders/ClientManager.php',
'client/res/templates/record/panels/default-side.tpl',
'application/Espo/Resources/templates/noteEmailRecieved',
];
foreach ($fileList as $file) {
if (is_dir($file)) {
$fileManager->removeInDir($file, true);
continue;
}
if (!file_exists($file)) {
continue;
}
unlink($file);
}
}
}
+40 -2
View File
@@ -75,6 +75,44 @@ class BeforeUpgrade
throw new Error($message);
}
}
$extension = $em->getRepository('Extension')
->where([
'name' => 'Real Estate',
'isInstalled' => true,
])
->findOne();
if ($extension) {
$version = $extension->get('version');
if (version_compare($version, '1.4.0', '<')) {
$message =
"EspoCRM 6.0.0 is not compatible with Real Estate extension of a version lower than 1.4.0. " .
"Please upgrade the extension or uninstall it. Then run the upgrade command again.";
throw new Error($message);
}
}
$extension = $em->getRepository('Extension')
->where([
'name' => 'VoIP Integration',
'isInstalled' => true,
])
->findOne();
if ($extension) {
$version = $extension->get('version');
if (version_compare($version, '1.15.0', '<')) {
$message =
"EspoCRM 6.0.0 is not compatible with VoIP Integration extension of a version lower than 1.15.0. " .
"Please upgrade the extension or uninstall it. Then run the upgrade command again.";
throw new Error($message);
}
}
}
protected function processMyIsamCheck()
@@ -97,8 +135,8 @@ class BeforeUpgrade
$message =
"In v6.0 we have dropped a support of MyISAM engine for DB tables. " .
"You have the following tables that use MyISAM: {$tableListString}.{$lineBreak}" .
"Please change the engine to InnoDB for these tables then run upgrade again.{$lineBreak}" .
"You have the following tables that use MyISAM: {$tableListString}.{$lineBreak}{$lineBreak}" .
"Please change the engine to InnoDB for these tables then run upgrade again.{$lineBreak}{$lineBreak}" .
"See: {$linkString}.";
throw new Error($message);