Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a965bfba4 | |||
| 7349f37786 | |||
| 3d03dfaef7 | |||
| 5bf88eaff1 | |||
| f3c4b2ba39 | |||
| 92f6fb4588 | |||
| 2d6d7fd618 | |||
| 3a1d997d5a | |||
| 5bbb259c95 | |||
| b8e44767b3 | |||
| 6b486beb5c | |||
| aab1265e4e | |||
| 676181a4be | |||
| 839e5b7c74 | |||
| aff7003ceb | |||
| 66aa60f303 | |||
| b196811cf9 | |||
| 3fdddadd53 |
@@ -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']);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,43 @@ class Manager
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array or StdClass data from PHP file.
|
||||
* For Windows: 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;
|
||||
}
|
||||
|
||||
if (stripos(\PHP_OS, 'WIN') !== 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
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 +311,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 +322,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 +360,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 +449,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 +484,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 +504,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 +559,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 +582,6 @@ class Manager
|
||||
$permissionDeniedList = [];
|
||||
|
||||
foreach ($fileList as $file) {
|
||||
|
||||
if ($copyOnlyFiles) {
|
||||
$file = pathinfo($file, PATHINFO_BASENAME);
|
||||
}
|
||||
@@ -667,7 +738,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 +769,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 +844,8 @@ class Manager
|
||||
/**
|
||||
* Check if $dirname is directory.
|
||||
*
|
||||
* @param string $dirname
|
||||
* @param string $basePath
|
||||
* @param string $dirname
|
||||
* @param string $basePath
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
@@ -893,10 +963,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 +977,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 +1030,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 +1067,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 +1077,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,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.",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}}"> </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}}"> </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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "6.0.0-beta2",
|
||||
"version": "6.0.0-beta3",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "6.0.0-beta2",
|
||||
"version": "6.0.0-beta3",
|
||||
"description": "",
|
||||
"main": "index.php",
|
||||
"repository": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -75,6 +75,25 @@ 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.5.0', '<')) {
|
||||
$message =
|
||||
"EspoCRM 6.0.0 is not compatible with Real Estate extension of a version lower than 1.5.0. " .
|
||||
"Please upgrade the extension or uninstall it. Then run the upgrade command again.";
|
||||
|
||||
throw new Error($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function processMyIsamCheck()
|
||||
|
||||
Reference in New Issue
Block a user