diff --git a/application/Espo/Core/ORM/Entity.php b/application/Espo/Core/ORM/Entity.php index 262c8f5856..b0fdd88c92 100644 --- a/application/Espo/Core/ORM/Entity.php +++ b/application/Espo/Core/ORM/Entity.php @@ -137,7 +137,7 @@ class Entity extends \Espo\ORM\Entity } $this->set($idsAttribute, $ids); - if (!$this->hasFetched($idsAttribute)) { + if (!$this->isNew() && !$this->hasFetched($idsAttribute)) { $this->setFetched($idsAttribute, $ids); } @@ -170,7 +170,13 @@ class Entity extends \Espo\ORM\Entity $entityName = $entity->get('name'); } - $this->set($field . 'Id', $entityId); + $idAttribute = $field . 'Id'; + + if (!$this->isNew() && !$this->hasFetched($idAttribute)) { + $this->setFetched($idAttribute, $entityId); + } + + $this->set($idAttribute, $entityId); $this->set($field . 'Name', $entityName); } diff --git a/application/Espo/Core/ORM/Repositories/RDB.php b/application/Espo/Core/ORM/Repositories/RDB.php index 8c741c7d94..ae6b912a5d 100644 --- a/application/Espo/Core/ORM/Repositories/RDB.php +++ b/application/Espo/Core/ORM/Repositories/RDB.php @@ -465,17 +465,21 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable $data->$columnName = $foreignEntity->get($columnField); } $existingColumnsData->$foreignId = $data; - $entity->setFetched($columnsFieldsName, $existingColumnsData); + if (!$entity->isNew()) { + $entity->setFetched($columnsFieldsName, $existingColumnsData); + } } } } - if ($entity->has($fieldName)) { - $entity->setFetched($fieldName, $existingIds); - } - if ($entity->has($columnsFieldsName) && !empty($columns)) { - $entity->setFetched($columnsFieldsName, $existingColumnsData); + if (!$entity->isNew()) { + if ($entity->has($fieldName)) { + $entity->setFetched($fieldName, $existingIds); + } + if ($entity->has($columnsFieldsName) && !empty($columns)) { + $entity->setFetched($columnsFieldsName, $existingColumnsData); + } } foreach ($existingIds as $id) { @@ -540,13 +544,17 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable $where[$foreignKey] = $entity->id; $previousForeignEntity = $this->getEntityManager()->getRepository($foreignEntityType)->where($where)->findOne(); if ($previousForeignEntity) { - $entity->setFetched($idFieldName, $previousForeignEntity->id); + if (!$entity->isNew()) { + $entity->setFetched($idFieldName, $previousForeignEntity->id); + } if ($previousForeignEntity->id !== $entity->get($idFieldName)) { $previousForeignEntity->set($foreignKey, null); $this->getEntityManager()->saveEntity($previousForeignEntity); } } else { - $entity->setFetched($idFieldName, null); + if (!$entity->isNew()) { + $entity->setFetched($idFieldName, null); + } } if ($entity->get($idFieldName)) { diff --git a/application/Espo/Core/SelectManagers/Base.php b/application/Espo/Core/SelectManagers/Base.php index 18d709e0d1..a800584c97 100644 --- a/application/Espo/Core/SelectManagers/Base.php +++ b/application/Espo/Core/SelectManagers/Base.php @@ -1255,7 +1255,7 @@ class Base $value = $item['value']; - if (is_null($value)) break; + if (is_null($value) || !$value && !is_array($value)) break; $relationType = $seed->getRelationType($link); @@ -1565,6 +1565,12 @@ class Base $useFullTextSearch = false; } + if ($isAuxiliaryUse) { + if (mb_strpos($textFilter, '@') !== false) { + $useFullTextSearch = false; + } + } + if ($useFullTextSearch) { $textFilter = str_replace(['(', ')'], '', $textFilter); @@ -1582,10 +1588,11 @@ class Base $function = 'MATCH_NATURAL_LANGUAGE'; } else { $function = 'MATCH_BOOLEAN'; - - $textFilter = str_replace('@', '*', $textFilter); } + $textFilter = str_replace('"*', '"', $textFilter); + $textFilter = str_replace('*"', '"', $textFilter); + while (strpos($textFilter, '**')) { $textFilter = str_replace('**', '*', $textFilter); $textFilter = trim($textFilter); diff --git a/application/Espo/Core/Utils/Database/Orm/Fields/LinkMultiple.php b/application/Espo/Core/Utils/Database/Orm/Fields/LinkMultiple.php index 9b4cbcad94..ec0843890e 100644 --- a/application/Espo/Core/Utils/Database/Orm/Fields/LinkMultiple.php +++ b/application/Espo/Core/Utils/Database/Orm/Fields/LinkMultiple.php @@ -33,28 +33,29 @@ class LinkMultiple extends Base { protected function load($fieldName, $entityName) { - $data = array( - $entityName => array ( - 'fields' => array( - $fieldName.'Ids' => array( + $data = [ + $entityName => [ + 'fields' => [ + $fieldName.'Ids' => [ 'type' => 'jsonArray', 'notStorable' => true, 'isLinkMultipleIdList' => true, - 'relation' => $fieldName - ), - $fieldName.'Names' => array( + 'relation' => $fieldName, + 'isUnordered' => true + ], + $fieldName.'Names' => [ 'type' => 'jsonObject', 'notStorable' => true, 'isLinkMultipleNameMap' => true - ) - ) - ), - 'unset' => array( - $entityName => array( - 'fields.'.$fieldName - ) - ) - ); + ] + ] + ], + 'unset' => [ + $entityName => [ + 'fields.' . $fieldName + ] + ] + ]; $fieldParams = $this->getFieldParams(); @@ -67,10 +68,10 @@ class LinkMultiple extends Base $columns = $this->getMetadata()->get("entityDefs.{$entityName}.fields.{$fieldName}.columns"); if (!empty($columns)) { - $data[$entityName]['fields'][$fieldName . 'Columns'] = array( + $data[$entityName]['fields'][$fieldName . 'Columns'] = [ 'type' => 'jsonObject', 'notStorable' => true, - ); + ]; } return $data; diff --git a/application/Espo/Core/Utils/EntityManager.php b/application/Espo/Core/Utils/EntityManager.php index 8f6540b5a9..65898b43e7 100644 --- a/application/Espo/Core/Utils/EntityManager.php +++ b/application/Espo/Core/Utils/EntityManager.php @@ -662,7 +662,6 @@ class EntityManager $link => array( "type" => "linkMultiple", "layoutDetailDisabled" => !$linkMultipleField, - "layoutListDisabled" => true, "layoutMassUpdateDisabled" => !$linkMultipleField, "noLoad" => !$linkMultipleField, "importDisabled" => !$linkMultipleField, @@ -727,7 +726,6 @@ class EntityManager $linkForeign => array( "type" => "linkMultiple", "layoutDetailDisabled" => !$linkMultipleFieldForeign, - "layoutListDisabled" => true, "layoutMassUpdateDisabled" => !$linkMultipleFieldForeign, "noLoad" => !$linkMultipleFieldForeign, "importDisabled" => !$linkMultipleFieldForeign, @@ -751,7 +749,6 @@ class EntityManager $link => array( "type" => "linkMultiple", "layoutDetailDisabled" => !$linkMultipleField, - "layoutListDisabled" => true, "layoutMassUpdateDisabled" => !$linkMultipleField, "importDisabled" => !$linkMultipleField, "noLoad" => !$linkMultipleField, @@ -774,7 +771,6 @@ class EntityManager $linkForeign => array( "type" => "linkMultiple", "layoutDetailDisabled" => !$linkMultipleFieldForeign, - "layoutListDisabled" => true, "layoutMassUpdateDisabled" => !$linkMultipleFieldForeign, "importDisabled" => !$linkMultipleFieldForeign, "noLoad" => !$linkMultipleFieldForeign, @@ -848,7 +844,6 @@ class EntityManager $link => array( "type" => "linkMultiple", "layoutDetailDisabled" => !$linkMultipleField, - "layoutListDisabled" => true, "layoutMassUpdateDisabled" => !$linkMultipleField, "noLoad" => !$linkMultipleField, "importDisabled" => !$linkMultipleField, @@ -873,7 +868,6 @@ class EntityManager $linkForeign => array( "type" => "linkMultiple", "layoutDetailDisabled" => !$linkMultipleFieldForeign, - "layoutListDisabled" => true, "layoutMassUpdateDisabled" => !$linkMultipleFieldForeign, "noLoad" => !$linkMultipleFieldForeign, "importDisabled" => !$linkMultipleFieldForeign, diff --git a/application/Espo/Modules/Crm/Resources/routes.json b/application/Espo/Modules/Crm/Resources/routes.json index 35897869f4..935f83736c 100644 --- a/application/Espo/Modules/Crm/Resources/routes.json +++ b/application/Espo/Modules/Crm/Resources/routes.json @@ -1,22 +1,21 @@ [ - { - "route":"/Activities/:scope/:id/:name", - "method":"get", - "params":{ - "controller":"Activities", - "action":"list", - "scope":":scope", - "id":":id", - "name":":name" + { + "route": "/Activities/:scope/:id/:name", + "method": "get", + "params": { + "controller": "Activities", + "action": "list", + "scope": ":scope", + "id": ":id", + "name": ":name" + } + }, + { + "route": "/Activities", + "method": "get", + "params": { + "controller": "Activities", + "action": "listCalendarEvents" + } } - }, - - { - "route":"/Activities", - "method":"get", - "params":{ - "controller":"Activities", - "action":"listCalendarEvents" - } - } ] diff --git a/application/Espo/Modules/Crm/SelectManagers/CampaignLogRecord.php b/application/Espo/Modules/Crm/SelectManagers/CampaignLogRecord.php index 1cbf3d7e0f..01de4d388c 100644 --- a/application/Espo/Modules/Crm/SelectManagers/CampaignLogRecord.php +++ b/application/Espo/Modules/Crm/SelectManagers/CampaignLogRecord.php @@ -33,19 +33,23 @@ class CampaignLogRecord extends \Espo\Core\SelectManagers\Base { protected function accessOnlyOwn(&$result) { + $this->addLeftJoin(['campaign', 'campaignAccess'], $result); + $result['whereClause'][] = array( - 'campaign.assignedUserId' => $this->getUser()->id + 'campaignAccess.assignedUserId' => $this->getUser()->id ); } protected function accessOnlyTeam(&$result) { + $this->addLeftJoin(['campaign', 'campaignAccess'], $result); + $teamIdList = $this->user->get('teamsIds'); if (empty($result['customWhere'])) { $result['customWhere'] = ''; } if (empty($teamIdList)) { - $result['customWhere'] .= " AND campaign.assigned_user_id = ".$this->getEntityManager()->getPDO()->quote($this->getUser()->id); + $result['customWhere'] .= " AND campaignAccess.assigned_user_id = ".$this->getEntityManager()->getPDO()->quote($this->getUser()->id); return; } $arr = []; @@ -55,12 +59,12 @@ class CampaignLogRecord extends \Espo\Core\SelectManagers\Base } } - $result['customJoin'] .= " LEFT JOIN entity_team AS teamsMiddle ON teamsMiddle.entity_type = 'Campaign' AND teamsMiddle.entity_id = campaign.id AND teamsMiddle.deleted = 0"; + $result['customJoin'] .= " LEFT JOIN entity_team AS teamsMiddle ON teamsMiddle.entity_type = 'Campaign' AND teamsMiddle.entity_id = campaignAccess.id AND teamsMiddle.deleted = 0"; $result['customWhere'] .= " AND ( teamsMiddle.team_id IN (" . implode(', ', $arr) . ") OR - campaign.assigned_user_id = ".$this->getEntityManager()->getPDO()->quote($this->getUser()->id)." + campaignAccess.assigned_user_id = ".$this->getEntityManager()->getPDO()->quote($this->getUser()->id)." ) "; $result['whereClause'][] = array( diff --git a/application/Espo/Modules/Crm/SelectManagers/CampaignTrackingUrl.php b/application/Espo/Modules/Crm/SelectManagers/CampaignTrackingUrl.php index 294153903b..128c8a9931 100644 --- a/application/Espo/Modules/Crm/SelectManagers/CampaignTrackingUrl.php +++ b/application/Espo/Modules/Crm/SelectManagers/CampaignTrackingUrl.php @@ -33,19 +33,23 @@ class CampaignTrackingUrl extends \Espo\Core\SelectManagers\Base { protected function accessOnlyOwn(&$result) { + $this->addLeftJoin(['campaign', 'campaignAccess'], $result); + $result['whereClause'][] = array( - 'campaign.assignedUserId' => $this->getUser()->id + 'campaignAccess.assignedUserId' => $this->getUser()->id ); } protected function accessOnlyTeam(&$result) { + $this->addLeftJoin(['campaign', 'campaignAccess'], $result); + $teamIdList = $this->user->get('teamsIds'); if (empty($result['customWhere'])) { $result['customWhere'] = ''; } if (empty($teamIdList)) { - $result['customWhere'] .= " AND campaign.assigned_user_id = ".$this->getEntityManager()->getPDO()->quote($this->getUser()->id); + $result['customWhere'] .= " AND campaignAccess.assigned_user_id = ".$this->getEntityManager()->getPDO()->quote($this->getUser()->id); return; } $arr = []; @@ -55,12 +59,12 @@ class CampaignTrackingUrl extends \Espo\Core\SelectManagers\Base } } - $result['customJoin'] .= " LEFT JOIN entity_team AS teamsMiddle ON teamsMiddle.entity_type = 'Campaign' AND teamsMiddle.entity_id = campaign.id AND teamsMiddle.deleted = 0"; + $result['customJoin'] .= " LEFT JOIN entity_team AS teamsMiddle ON teamsMiddle.entity_type = 'Campaign' AND teamsMiddle.entity_id = campaignAccess.id AND teamsMiddle.deleted = 0"; $result['customWhere'] .= " AND ( teamsMiddle.team_id IN (" . implode(', ', $arr) . ") OR - campaign.assigned_user_id = ".$this->getEntityManager()->getPDO()->quote($this->getUser()->id)." + campaignAccess.assigned_user_id = ".$this->getEntityManager()->getPDO()->quote($this->getUser()->id)." ) "; $result['whereClause'][] = array( diff --git a/application/Espo/Modules/Crm/SelectManagers/MassEmail.php b/application/Espo/Modules/Crm/SelectManagers/MassEmail.php index fb6f8b1a09..e53cdd7960 100644 --- a/application/Espo/Modules/Crm/SelectManagers/MassEmail.php +++ b/application/Espo/Modules/Crm/SelectManagers/MassEmail.php @@ -45,21 +45,25 @@ class MassEmail extends \Espo\Core\SelectManagers\Base ); } - protected function acessOnlyOwn(&$result) + protected function accessOnlyOwn(&$result) { + $this->addLeftJoin(['campaign', 'campaignAccess'], $result); + $result['whereClause'][] = array( - 'campaign.assignedUserId' => $this->getUser()->id + 'campaignAccess.assignedUserId' => $this->getUser()->id ); } protected function accessOnlyTeam(&$result) { - $teamIdList = $this->user->get('teamsIds'); + $this->addLeftJoin(['campaign', 'campaignAccess'], $result); + + $teamIdList = $this->user->getLinkMultipleIdList('teams'); if (empty($result['customWhere'])) { $result['customWhere'] = ''; } if (empty($teamIdList)) { - $result['customWhere'] .= " AND campaign.assigned_user_id = ".$this->getEntityManager()->getPDO()->quote($this->getUser()->id); + $result['customWhere'] .= " AND campaignAccess.assigned_user_id = ".$this->getEntityManager()->getPDO()->quote($this->getUser()->id); return; } $arr = []; @@ -69,12 +73,12 @@ class MassEmail extends \Espo\Core\SelectManagers\Base } } - $result['customJoin'] .= " LEFT JOIN entity_team AS teamsMiddle ON teamsMiddle.entity_type = 'Campaign' AND teamsMiddle.entity_id = campaign.id AND teamsMiddle.deleted = 0"; + $result['customJoin'] .= " LEFT JOIN entity_team AS teamsMiddle ON teamsMiddle.entity_type = 'Campaign' AND teamsMiddle.entity_id = campaignAccess.id AND teamsMiddle.deleted = 0"; $result['customWhere'] .= " AND ( teamsMiddle.team_id IN (" . implode(', ', $arr) . ") OR - campaign.assigned_user_id = ".$this->getEntityManager()->getPDO()->quote($this->getUser()->id)." + campaignAccess.assigned_user_id = ".$this->getEntityManager()->getPDO()->quote($this->getUser()->id)." ) "; $result['whereClause'][] = array( diff --git a/application/Espo/ORM/DB/Mapper.php b/application/Espo/ORM/DB/Mapper.php index 16f1aece6e..7a4490cd5c 100644 --- a/application/Espo/ORM/DB/Mapper.php +++ b/application/Espo/ORM/DB/Mapper.php @@ -718,7 +718,7 @@ abstract class Mapper implements IMapper public function insert(IEntity $entity) { - $dataArr = $this->toArray($entity); + $dataArr = $this->toValueMap($entity); $fieldArr = array(); $valArr = array(); @@ -745,33 +745,35 @@ abstract class Mapper implements IMapper public function update(IEntity $entity) { - $dataArr = $this->toArray($entity); + $valueMap = $this->toValueMap($entity); - $setArr = array(); - foreach ($dataArr as $field => $value) { - if ($field == 'id') { + $setArr = []; + + foreach ($valueMap as $attribute => $value) { + if ($attribute == 'id') { continue; } - $type = $entity->fields[$field]['type']; + $type = $entity->getAttributeType($attribute); if ($type == IEntity::FOREIGN) { continue; } - if ($entity->getFetched($field) === $value && $type != IEntity::JSON_ARRAY && $type != IEntity::JSON_OBJECT) { + if (!$entity->isAttributeChanged($attribute) && $type !== IEntity::JSON_OBJECT) { continue; } $value = $this->prepareValueForInsert($type, $value); - $setArr[] = "`" . $this->toDb($field) . "` = " . $this->quote($value); + $setArr[] = "`" . $this->toDb($attribute) . "` = " . $this->quote($value); } + if (count($setArr) == 0) { return $entity->id; } $setPart = implode(', ', $setArr); - $wherePart = $this->query->getWhere($entity, array('id' => $entity->id, 'deleted' => 0)); + $wherePart = $this->query->getWhere($entity, ['id' => $entity->id, 'deleted' => 0]); $sql = $this->composeUpdateQuery($this->toDb($entity->getEntityType()), $setPart, $wherePart); @@ -815,21 +817,25 @@ abstract class Mapper implements IMapper return $this->update($entity); } - protected function toArray(IEntity $entity, $onlyStorable = true) + protected function toValueMap(IEntity $entity, $onlyStorable = true) { - $arr = array(); - foreach ($entity->fields as $field => $fieldDefs) { - if ($entity->has($field)) { + $data = []; + foreach ($entity->getAttributes() as $attribute => $defs) { + if ($entity->has($attribute)) { if ($onlyStorable) { - if (!empty($fieldDefs['notStorable']) || !empty($fieldDefs['autoincrement']) || isset($fieldDefs['source']) && $fieldDefs['source'] != 'db') - continue; - if ($fieldDefs['type'] == IEntity::FOREIGN) - continue; + if ( + !empty($defs['notStorable']) + || + !empty($defs['autoincrement']) + || + isset($defs['source']) && $defs['source'] != 'db' + ) continue; + if ($defs['type'] == IEntity::FOREIGN) continue; } - $arr[$field] = $entity->get($field); + $data[$attribute] = $entity->get($attribute); } } - return $arr; + return $data; } protected function fromRow(IEntity $entity, $data) diff --git a/application/Espo/ORM/Entity.php b/application/Espo/ORM/Entity.php index 74fbc3bd07..afd2757d3d 100644 --- a/application/Espo/ORM/Entity.php +++ b/application/Espo/ORM/Entity.php @@ -369,25 +369,92 @@ abstract class Entity implements IEntity return $this->isFetched; } - public function isFieldChanged($fieldName) + public function isFieldChanged($name) { - return $this->has($fieldName) && ($this->get($fieldName) != $this->getFetched($fieldName)); + return $this->has($name) && ($this->get($name) != $this->getFetched($name)); } - public function isAttributeChanged($fieldName) + public function isAttributeChanged($name) { - return $this->has($fieldName) && ($this->get($fieldName) != $this->getFetched($fieldName)); + if (!$this->has($name)) return false; + + if (!$this->hasFetched($name)) { + return true; + } + return !self::areValuesEqual( + $this->getAttributeType($name), + $this->get($name), + $this->getFetched($name), + $this->getAttributeParam($name, 'isUnordered') + ); + + return $this->get($name) != $this->getFetched($name); } - public function setFetched($fieldName, $value) + public static function areValuesEqual($type, $v1, $v2, $isUnordered = false) { - $this->fetchedValuesContainer[$fieldName] = $value; + if ($type === self::JSON_ARRAY) { + if (is_array($v1) && is_array($v2)) { + if ($isUnordered) { + sort($v1); + sort($v2); + } + if ($v1 != $v2) { + return false; + } + foreach ($v1 as $i => $itemValue) { + if (is_object($v1[$i]) && is_object($v2[$i])) { + if (!self::areValuesEqual(self::JSON_OBJECT, $v1[$i], $v2[$i])) { + return false; + } + continue; + } + if ($v1[$i] !== $v2[$i]) { + return false; + } + } + return true; + } + } else if ($type === self::JSON_OBJECT) { + if (is_object($v1) && is_object($v2)) { + if ($v1 != $v2) { + return false; + } + $a1 = get_object_vars($v1); + $a2 = get_object_vars($v2); + foreach ($v1 as $key => $itemValue) { + if (is_object($a1[$key]) && is_object($a2[$key])) { + if (!self::areValuesEqual(self::JSON_OBJECT, $a1[$key], $a2[$key])) { + return false; + } + continue; + } + if (is_array($a1[$key]) && is_array($a2[$key])) { + if (!self::areValuesEqual(self::JSON_ARRAY, $a1[$key], $a2[$key])) { + return false; + } + continue; + } + if ($a1[$key] !== $a2[$key]) { + return false; + } + } + return true; + } + } + + return $v1 === $v2; } - public function getFetched($fieldName) + public function setFetched($name, $value) { - if (isset($this->fetchedValuesContainer[$fieldName])) { - return $this->fetchedValuesContainer[$fieldName]; + $this->fetchedValuesContainer[$name] = $value; + } + + public function getFetched($name) + { + if (isset($this->fetchedValuesContainer[$name])) { + return $this->fetchedValuesContainer[$name]; } return null; } diff --git a/application/Espo/Resources/routes.json b/application/Espo/Resources/routes.json index 045684706f..62d8a70383 100644 --- a/application/Espo/Resources/routes.json +++ b/application/Espo/Resources/routes.json @@ -1,319 +1,326 @@ [ { - "route":"/", - "method":"get", - "params":"

EspoCRM REST API<\/h1>" + "route": "/", + "method": "get", + "params": "

EspoCRM REST API<\/h1>" }, { - "route":"/App/user", - "method":"get", - "params":{ - "controller":"App", - "action":"user" + "route": "/App/user", + "method": "get", + "params": { + "controller": "App", + "action": "user" } }, { - "route":"/Metadata", - "method":"get", - "params":{ - "controller":"Metadata" + "route": "/Metadata", + "method": "get", + "params": { + "controller": "Metadata" } }, { - "route":"I18n", - "method":"get", - "params":{ - "controller":"I18n" + "route": "I18n", + "method": "get", + "params": { + "controller": "I18n" }, - "conditions":{ - "auth":false + "conditions": { + "auth": false } }, { - "route":"/Settings", - "method":"get", - "params":{ - "controller":"Settings" + "route": "/Settings", + "method": "get", + "params": { + "controller": "Settings" }, - "conditions":{ - "auth":false + "conditions": { + "auth": false } }, { - "route":"/Settings", - "method":"patch", - "params":{ - "controller":"Settings" + "route": "/Settings", + "method": "patch", + "params": { + "controller": "Settings" } }, { - "route":"User/passwordChangeRequest", - "method":"post", - "params":{ - "controller":"User", - "action":"passwordChangeRequest" + "route": "/Settings", + "method": "put", + "params": { + "controller": "Settings" + } + }, + { + "route": "User/passwordChangeRequest", + "method": "post", + "params": { + "controller": "User", + "action": "passwordChangeRequest" }, - "conditions":{ - "auth":false + "conditions": { + "auth": false } }, { - "route":"User/changePasswordByRequest", - "method":"post", - "params":{ - "controller":"User", - "action":"changePasswordByRequest" + "route": "User/changePasswordByRequest", + "method": "post", + "params": { + "controller": "User", + "action": "changePasswordByRequest" }, - "conditions":{ - "auth":false + "conditions": { + "auth": false } }, { - "route":"/Stream", - "method":"get", - "params":{ - "controller":"Stream", - "action":"list", - "scope":"User" + "route": "/Stream", + "method": "get", + "params": { + "controller": "Stream", + "action": "list", + "scope": "User" } }, { - "route":"/GlobalSearch", - "method":"get", - "params":{ - "controller":"GlobalSearch", - "action":"search" + "route": "/GlobalSearch", + "method": "get", + "params": { + "controller": "GlobalSearch", + "action": "search" } }, { - "route":"/:controller/action/:action", - "method":"post", - "params":{ - "controller":":controller", - "action":":action" + "route": "/:controller/action/:action", + "method": "post", + "params": { + "controller": ":controller", + "action": ":action" } }, { - "route":"/:controller/action/:action", - "method":"put", - "params":{ - "controller":":controller", - "action":":action" + "route": "/:controller/action/:action", + "method": "put", + "params": { + "controller": ":controller", + "action": ":action" } }, { - "route":"/:controller/action/:action", - "method":"get", - "params":{ - "controller":":controller", - "action":":action" + "route": "/:controller/action/:action", + "method": "get", + "params": { + "controller": ":controller", + "action": ":action" } }, { - "route":"/:controller/layout/:name", - "method":"get", - "params":{ - "controller":"Layout", - "scope":":controller" + "route": "/:controller/layout/:name", + "method": "get", + "params": { + "controller": "Layout", + "scope": ":controller" } }, { - "route":"/:controller/layout/:name", - "method":"put", - "params":{ - "controller":"Layout", - "scope":":controller" + "route": "/:controller/layout/:name", + "method": "put", + "params": { + "controller": "Layout", + "scope": ":controller" } }, { - "route":"/:controller/layout/:name", - "method":"patch", - "params":{ - "controller":"Layout", - "scope":":controller" + "route": "/:controller/layout/:name", + "method": "patch", + "params": { + "controller": "Layout", + "scope": ":controller" } }, { - "route":"/Admin/rebuild", - "method":"post", - "params":{ - "controller":"Admin", - "action":"rebuild" + "route": "/Admin/rebuild", + "method": "post", + "params": { + "controller": "Admin", + "action": "rebuild" } }, { - "route":"/Admin/clearCache", - "method":"post", - "params":{ - "controller":"Admin", - "action":"clearCache" + "route": "/Admin/clearCache", + "method": "post", + "params": { + "controller": "Admin", + "action": "clearCache" } }, { - "route":"/Admin/jobs", - "method":"get", - "params":{ - "controller":"Admin", - "action":"jobs" + "route": "/Admin/jobs", + "method": "get", + "params": { + "controller": "Admin", + "action": "jobs" } }, { - "route":"/Admin/fieldManager/:scope/:name", - "method":"get", - "params":{ - "controller":"FieldManager", - "action":"read", - "scope":":scope", - "name":":name" + "route": "/Admin/fieldManager/:scope/:name", + "method": "get", + "params": { + "controller": "FieldManager", + "action": "read", + "scope": ":scope", + "name": ":name" } }, { - "route":"/Admin/fieldManager/:scope", - "method":"post", - "params":{ - "controller":"FieldManager", - "action":"create", - "scope":":scope" + "route": "/Admin/fieldManager/:scope", + "method": "post", + "params": { + "controller": "FieldManager", + "action": "create", + "scope": ":scope" } }, { - "route":"/Admin/fieldManager/:scope/:name", - "method":"put", - "params":{ - "controller":"FieldManager", - "action":"update", - "scope":":scope", - "name":":name" + "route": "/Admin/fieldManager/:scope/:name", + "method": "put", + "params": { + "controller": "FieldManager", + "action": "update", + "scope": ":scope", + "name": ":name" } }, { - "route":"/Admin/fieldManager/:scope/:name", - "method":"patch", - "params":{ - "controller":"FieldManager", - "action":"update", - "scope":":scope", - "name":":name" + "route": "/Admin/fieldManager/:scope/:name", + "method": "patch", + "params": { + "controller": "FieldManager", + "action": "update", + "scope": ":scope", + "name": ":name" } }, { - "route":"/Admin/fieldManager/:scope/:name", - "method":"delete", - "params":{ - "controller":"FieldManager", - "action":"delete", - "scope":":scope", - "name":":name" + "route": "/Admin/fieldManager/:scope/:name", + "method": "delete", + "params": { + "controller": "FieldManager", + "action": "delete", + "scope": ":scope", + "name": ":name" } }, { - "route":"/:controller/:id", - "method":"get", - "params":{ - "controller":":controller", - "action":"read", - "id":":id" + "route": "/:controller/:id", + "method": "get", + "params": { + "controller": ":controller", + "action": "read", + "id": ":id" } }, { - "route":"/:controller", - "method":"get", - "params":{ - "controller":":controller", - "action":"index" + "route": "/:controller", + "method": "get", + "params": { + "controller": ":controller", + "action": "index" } }, { - "route":"/:controller", - "method":"post", - "params":{ - "controller":":controller", - "action":"create" + "route": "/:controller", + "method": "post", + "params": { + "controller": ":controller", + "action": "create" } }, { - "route":"/:controller/:id", - "method":"put", - "params":{ - "controller":":controller", - "action":"update", - "id":":id" + "route": "/:controller/:id", + "method": "put", + "params": { + "controller": ":controller", + "action": "update", + "id": ":id" } }, { - "route":"/:controller/:id", - "method":"patch", - "params":{ - "controller":":controller", - "action":"patch", - "id":":id" + "route": "/:controller/:id", + "method": "patch", + "params": { + "controller": ":controller", + "action": "patch", + "id": ":id" } }, { - "route":"/:controller/:id", - "method":"delete", - "params":{ - "controller":":controller", - "action":"delete", - "id":":id" + "route": "/:controller/:id", + "method": "delete", + "params": { + "controller": ":controller", + "action": "delete", + "id": ":id" } }, { - "route":"/:controller/:id/stream", - "method":"get", - "params":{ - "controller":"Stream", - "action":"list", - "id":":id", - "scope":":controller" + "route": "/:controller/:id/stream", + "method": "get", + "params": { + "controller": "Stream", + "action": "list", + "id": ":id", + "scope": ":controller" } }, { - "route":"/:controller/:id/subscription", - "method":"put", - "params":{ - "controller":":controller", - "id":":id", - "action":"follow" + "route": "/:controller/:id/subscription", + "method": "put", + "params": { + "controller": ":controller", + "id": ":id", + "action": "follow" } }, { - "route":"/:controller/:id/subscription", - "method":"delete", - "params":{ - "controller":":controller", - "id":":id", - "action":"unfollow" + "route": "/:controller/:id/subscription", + "method": "delete", + "params": { + "controller": ":controller", + "id": ":id", + "action": "unfollow" } }, { - "route":"/:controller/:id/:link", - "method":"get", - "params":{ - "controller":":controller", - "action":"listLinked", - "id":":id", - "link":":link" + "route": "/:controller/:id/:link", + "method": "get", + "params": { + "controller": ":controller", + "action": "listLinked", + "id": ":id", + "link": ":link" } }, { - "route":"/:controller/:id/:link", - "method":"post", - "params":{ - "controller":":controller", - "action":"createLink", - "id":":id", - "link":":link" + "route": "/:controller/:id/:link", + "method": "post", + "params": { + "controller": ":controller", + "action": "createLink", + "id": ":id", + "link": ":link" } }, { - "route":"/:controller/:id/:link", - "method":"delete", - "params":{ - "controller":":controller", - "action":"removeLink", - "id":":id", - "link":":link" + "route": "/:controller/:id/:link", + "method": "delete", + "params": { + "controller": ":controller", + "action": "removeLink", + "id": ":id", + "link": ":link" } } ] diff --git a/application/Espo/Services/Email.php b/application/Espo/Services/Email.php index 829d1d0d3f..835be0589c 100644 --- a/application/Espo/Services/Email.php +++ b/application/Espo/Services/Email.php @@ -173,14 +173,26 @@ class Email extends Record if (!$smtpParams && $fromAddress === strtolower($this->getConfig()->get('outboundEmailFromAddress'))) { if (!$this->getConfig()->get('outboundEmailIsShared')) { - throw new Error('Can not use system smtp. System SMTP is not shared.'); + throw new Error('Can not use system SMTP. System account is not shared.'); } - $emailSender->setParams(array( + $emailSender->setParams([ 'fromName' => $this->getConfig()->get('outboundEmailFromName') - )); + ]); } - $params = array(); + if (!$smtpParams && !$this->getConfig()->get('outboundEmailIsShared')) { + throw new Error('No SMTP params found for '.$fromAddress.'.'); + } + + if (!$smtpParams) { + if (in_array($fromAddress, $userAddressList)) { + $emailSender->setParams([ + 'fromName' => $this->getUser()->get('name') + ]); + } + } + + $params = []; $parent = null; if ($entity->get('parentType') && $entity->get('parentId')) { diff --git a/application/Espo/Services/EmailAccount.php b/application/Espo/Services/EmailAccount.php index db31900fc5..5acf5a9cd6 100644 --- a/application/Espo/Services/EmailAccount.php +++ b/application/Espo/Services/EmailAccount.php @@ -209,12 +209,15 @@ class EmailAccount extends Record if (empty($fetchData)) { $fetchData = new \StdClass(); } + $fetchData = clone $fetchData; if (!property_exists($fetchData, 'lastUID')) { - $fetchData->lastUID = new \StdClass();; + $fetchData->lastUID = new \StdClass(); } if (!property_exists($fetchData, 'lastDate')) { - $fetchData->lastDate = new \StdClass();; + $fetchData->lastDate = new \StdClass(); } + $fetchData->lastUID = clone $fetchData->lastUID; + $fetchData->lastDate = clone $fetchData->lastDate; $storage = $this->getStorage($emailAccount); @@ -385,7 +388,7 @@ class EmailAccount extends Record $emailAccount = $this->getEntityManager()->getRepository('EmailAccount')->where([ 'emailAddress' => $address, 'assignedUserId' => $user->id, - 'active' => true + 'status' => 'Active' ])->findOne(); return $emailAccount; diff --git a/application/Espo/Services/InboundEmail.php b/application/Espo/Services/InboundEmail.php index 59f9688716..a3d0c4b632 100644 --- a/application/Espo/Services/InboundEmail.php +++ b/application/Espo/Services/InboundEmail.php @@ -210,12 +210,15 @@ class InboundEmail extends \Espo\Services\Record if (empty($fetchData)) { $fetchData = new \StdClass(); } + $fetchData = clone $fetchData; if (!property_exists($fetchData, 'lastUID')) { $fetchData->lastUID = new \StdClass();; } if (!property_exists($fetchData, 'lastDate')) { - $fetchData->lastDate = new \StdClass();; + $fetchData->lastDate = new \StdClass(); } + $fetchData->lastUID = clone $fetchData->lastUID; + $fetchData->lastDate = clone $fetchData->lastDate; $imapParams = array( 'host' => $emailAccount->get('host'), diff --git a/application/Espo/Services/Record.php b/application/Espo/Services/Record.php index 26ffa9c79b..f347c1eae8 100644 --- a/application/Espo/Services/Record.php +++ b/application/Espo/Services/Record.php @@ -2191,7 +2191,7 @@ class Record extends \Espo\Core\Services\Base } // TODO remove in 5.5.0 - if (in_array($this->getEntityType(), ['Report', 'Workflow'])) { + if (in_array($this->getEntityType(), ['Report', 'Workflow', 'ReportPanel'])) { return null; } diff --git a/application/Espo/Services/Stream.php b/application/Espo/Services/Stream.php index 4cc58766c8..f4327a28eb 100644 --- a/application/Espo/Services/Stream.php +++ b/application/Espo/Services/Stream.php @@ -972,7 +972,7 @@ class Stream extends \Espo\Core\Services\Base foreach ($auditedFields as $field => $item) { $updated = false; foreach ($item['actualList'] as $attribute) { - if ($entity->get($attribute) !== $entity->getFetched($attribute)) { + if ($entity->hasFetched($attribute) && $entity->isAttributeChanged($attribute)) { $updated = true; } } diff --git a/client/res/templates/fields/array/detail.tpl b/client/res/templates/fields/array/detail.tpl index 2c88ecc017..485f018e12 100644 --- a/client/res/templates/fields/array/detail.tpl +++ b/client/res/templates/fields/array/detail.tpl @@ -1 +1,3 @@ -{{#unless isEmpty}}{{{value}}}{{else}}{{translate 'None'}}{{/unless}} +{{#unless isEmpty}}{{{value}}}{{else}} + {{#if valueIsSet}}{{{translate 'None'}}}{{else}}...{{/if}} +{{/unless}} diff --git a/client/res/templates/fields/array/list.tpl b/client/res/templates/fields/array/list.tpl new file mode 100644 index 0000000000..4bc46255bf --- /dev/null +++ b/client/res/templates/fields/array/list.tpl @@ -0,0 +1 @@ +{{#unless isEmpty}}{{{value}}}{{/unless}} diff --git a/client/res/templates/fields/attachments-multiple/list.tpl b/client/res/templates/fields/attachments-multiple/list.tpl new file mode 100644 index 0000000000..310a5e0f15 --- /dev/null +++ b/client/res/templates/fields/attachments-multiple/list.tpl @@ -0,0 +1,3 @@ +{{#if value}} + {{{value}}} +{{/if}} \ No newline at end of file diff --git a/client/res/templates/fields/currency/detail-1.tpl b/client/res/templates/fields/currency/detail-1.tpl index 3d1d34e565..6828455061 100644 --- a/client/res/templates/fields/currency/detail-1.tpl +++ b/client/res/templates/fields/currency/detail-1.tpl @@ -1,5 +1,5 @@ -{{#if value}} +{{#if isNotEmpty}} {{value}} {{currencyValue}} {{else}} - {{translate 'None'}} + {{#if valueIsSet}}{{{translate 'None'}}}{{else}}...{{/if}} {{/if}} diff --git a/client/res/templates/fields/currency/detail-2.tpl b/client/res/templates/fields/currency/detail-2.tpl index 1fbd904fa4..678e6a1f83 100644 --- a/client/res/templates/fields/currency/detail-2.tpl +++ b/client/res/templates/fields/currency/detail-2.tpl @@ -1,5 +1,5 @@ -{{#if value}} +{{#if isNotEmpty}} {{currencySymbol}}{{value}} {{else}} - {{translate 'None'}} + {{#if valueIsSet}}{{{translate 'None'}}}{{else}}...{{/if}} {{/if}} diff --git a/client/res/templates/fields/currency/detail-no-currency.tpl b/client/res/templates/fields/currency/detail-no-currency.tpl index 3fd57a1a62..c78bc0890d 100644 --- a/client/res/templates/fields/currency/detail-no-currency.tpl +++ b/client/res/templates/fields/currency/detail-no-currency.tpl @@ -1,5 +1,5 @@ -{{#if value}} +{{#if isNotEmpty}} {{value}} {{else}} - {{translate 'None'}} + {{#if valueIsSet}}{{{translate 'None'}}}{{else}}...{{/if}} {{/if}} diff --git a/client/res/templates/fields/currency/detail.tpl b/client/res/templates/fields/currency/detail.tpl index 3d1d34e565..6828455061 100644 --- a/client/res/templates/fields/currency/detail.tpl +++ b/client/res/templates/fields/currency/detail.tpl @@ -1,5 +1,5 @@ -{{#if value}} +{{#if isNotEmpty}} {{value}} {{currencyValue}} {{else}} - {{translate 'None'}} + {{#if valueIsSet}}{{{translate 'None'}}}{{else}}...{{/if}} {{/if}} diff --git a/client/res/templates/fields/currency/list-1.tpl b/client/res/templates/fields/currency/list-1.tpl index e1b2948b5c..9669b122be 100644 --- a/client/res/templates/fields/currency/list-1.tpl +++ b/client/res/templates/fields/currency/list-1.tpl @@ -1,5 +1,3 @@ -{{#if value}} +{{#if isNotEmpty}} {{value}} {{currencyValue}} -{{else}} - {{translate 'None'}} {{/if}} diff --git a/client/res/templates/fields/currency/list-2.tpl b/client/res/templates/fields/currency/list-2.tpl index 826723bf51..67304d708e 100644 --- a/client/res/templates/fields/currency/list-2.tpl +++ b/client/res/templates/fields/currency/list-2.tpl @@ -1,5 +1,3 @@ -{{#if value}} - {{currencySymbol}}{{value}} -{{else}} - {{translate 'None'}} +{{#if isNotEmpty}} + {{currencySymbol}}{{value}} {{/if}} diff --git a/client/res/templates/fields/currency/list.tpl b/client/res/templates/fields/currency/list.tpl index e1b2948b5c..9669b122be 100644 --- a/client/res/templates/fields/currency/list.tpl +++ b/client/res/templates/fields/currency/list.tpl @@ -1,5 +1,3 @@ -{{#if value}} +{{#if isNotEmpty}} {{value}} {{currencyValue}} -{{else}} - {{translate 'None'}} {{/if}} diff --git a/client/res/templates/fields/int/detail.tpl b/client/res/templates/fields/int/detail.tpl index f6b34e1b68..c8454e9dbd 100644 --- a/client/res/templates/fields/int/detail.tpl +++ b/client/res/templates/fields/int/detail.tpl @@ -1,3 +1,3 @@ {{#if isNotEmpty}}{{value}}{{else}} - {{translate 'None'}} + {{#if valueIsSet}}{{{translate 'None'}}}{{else}}...{{/if}} {{/if}} \ No newline at end of file diff --git a/client/res/templates/fields/link-multiple/list.tpl b/client/res/templates/fields/link-multiple/list.tpl new file mode 100644 index 0000000000..660e229523 --- /dev/null +++ b/client/res/templates/fields/link-multiple/list.tpl @@ -0,0 +1,4 @@ +{{#if value}} + {{{value}}} +{{/if}} + diff --git a/client/res/templates/fields/person-name/detail.tpl b/client/res/templates/fields/person-name/detail.tpl index 153358eaee..702ccf1880 100644 --- a/client/res/templates/fields/person-name/detail.tpl +++ b/client/res/templates/fields/person-name/detail.tpl @@ -1,3 +1,3 @@ {{#if isNotEmpty}}{{translateOption salutationValue field='salutationName' scope=scope}} {{firstValue}} {{lastValue}}{{else}} -{{translate 'None'}} +{{#if valueIsSet}}{{{translate 'None'}}}{{else}}...{{/if}} {{/if}} \ No newline at end of file diff --git a/client/res/templates/fields/url/detail.tpl b/client/res/templates/fields/url/detail.tpl index 3787369c2b..17178c449a 100644 --- a/client/res/templates/fields/url/detail.tpl +++ b/client/res/templates/fields/url/detail.tpl @@ -1,5 +1,5 @@ {{#if value}} {{value}} {{else}} - {{translate 'None'}} + {{#if valueIsSet}}{{{translate 'None'}}}{{else}}...{{/if}} {{/if}} diff --git a/client/res/templates/fields/varchar/detail.tpl b/client/res/templates/fields/varchar/detail.tpl index dbfa5612db..e8f9641487 100644 --- a/client/res/templates/fields/varchar/detail.tpl +++ b/client/res/templates/fields/varchar/detail.tpl @@ -1 +1,3 @@ -{{#if isNotEmpty}}{{value}}{{else}}{{translate 'None'}}{{/if}} \ No newline at end of file +{{#if isNotEmpty}}{{value}}{{else}} +{{#if valueIsSet}}{{{translate 'None'}}}{{else}}...{{/if}} +{{/if}} \ No newline at end of file diff --git a/client/src/model.js b/client/src/model.js index 99c1594d04..0031f057a8 100644 --- a/client/src/model.js +++ b/client/src/model.js @@ -50,6 +50,11 @@ Espo.define('model', [], function () { Dep.prototype.initialize.call(this); }, + sync: function (method, model, options) { + if (method === 'patch') options.type = 'PUT'; + return Dep.prototype.sync.call(this, method, model, options); + }, + set: function (key, val, options) { if (typeof key === 'object') { var o = key; diff --git a/client/src/views/fields/address.js b/client/src/views/fields/address.js index 43f2238058..d0b85a15c7 100644 --- a/client/src/views/fields/address.js +++ b/client/src/views/fields/address.js @@ -52,45 +52,41 @@ Espo.define('views/fields/address', 'views/fields/base', function (Dep) { var data = Dep.prototype.data.call(this); data.ucName = Espo.Utils.upperCaseFirst(this.name); - data.postalCodeValue = this.model.get(this.postalCodeField); - data.streetValue = this.model.get(this.streetField); - data.cityValue = this.model.get(this.cityField); - data.stateValue = this.model.get(this.stateField); - data.countryValue = this.model.get(this.countryField); + this.addressPartList.forEach(function (item) { + var value = this.model.get(this[item + 'Field']); + data[item + 'Value'] = value; + }, this); if (this.mode == 'detail' || this.mode == 'list') { data.formattedAddress = this.getFormattedAddress(); } - data.isEmpty = !(data.postalCodeValue || - data.streetValue || - data.cityValue || - data.stateValue || - data.countryValue); + var isNotEmpty = false; return data; }, setupSearch: function () { - this.searchData.value = this.getSearchParamsData().value || this.searchParams.additionalValue; + this.searchData.value = this.getSearchParamsData().value || this.searchParams.additionalValue; }, getFormattedAddress: function () { - var postalCodeValue = this.model.get(this.postalCodeField); - var streetValue = this.model.get(this.streetField); - var cityValue = this.model.get(this.cityField); - var stateValue = this.model.get(this.stateField); - var countryValue = this.model.get(this.countryField); + var isNotEmpty = false; + var isSet = false; + this.addressAttributeList.forEach(function (attribute) { + isNotEmpty = isNotEmpty || this.model.get(attribute); + isSet = isSet || this.model.has(attribute); + }, this); - var isEmpty = !( - postalCodeValue || - streetValue || - cityValue || - stateValue || - countryValue - ); + var isEmpty = !isNotEmpty; if (isEmpty) { + if (this.mode === 'list') { + return ''; + } + if (!isSet) { + return this.translate('...'); + } return this.translate('None'); } @@ -320,13 +316,18 @@ Espo.define('views/fields/address', 'views/fields/base', function (Dep) { } }, - init: function () { - this.postalCodeField = this.options.defs.name + 'PostalCode'; - this.streetField = this.options.defs.name + 'Street'; - this.stateField = this.options.defs.name + 'State'; - this.cityField = this.options.defs.name + 'City'; - this.countryField = this.options.defs.name + 'Country'; - Dep.prototype.init.call(this); + setup: function () { + Dep.prototype.setup.call(this); + + var actualAttributePartList = this.getMetadata().get(['fields', this.type, 'actualFields']) || []; + this.addressAttributeList = []; + this.addressPartList = []; + actualAttributePartList.forEach(function (item) { + var attribute = this.name + Espo.Utils.upperCaseFirst(item); + this.addressAttributeList.push(attribute); + this.addressPartList.push(item); + this[item + 'Field'] = attribute; + }, this); }, validateRequired: function () { @@ -397,7 +398,7 @@ Espo.define('views/fields/address', 'views/fields/base', function (Dep) { type: 'like', field: this.countryField, value: value + '%' - }, + } ], data: { value: value diff --git a/client/src/views/fields/array.js b/client/src/views/fields/array.js index 882092b7c6..1cddea531d 100644 --- a/client/src/views/fields/array.js +++ b/client/src/views/fields/array.js @@ -32,7 +32,7 @@ Espo.define('views/fields/array', ['views/fields/base', 'lib!Selectize'], functi type: 'array', - listTemplate: 'fields/array/detail', + listTemplate: 'fields/array/list', detailTemplate: 'fields/array/detail', @@ -51,7 +51,8 @@ Espo.define('views/fields/array', ['views/fields/base', 'lib!Selectize'], functi translatedOptions: this.translatedOptions, hasOptions: this.params.options ? true : false, itemHtmlList: itemHtmlList, - isEmpty: (this.selected || []).length === 0 + isEmpty: (this.selected || []).length === 0, + valueIsSet: this.model.has(this.name) }, Dep.prototype.data.call(this)); }, @@ -109,7 +110,7 @@ Espo.define('views/fields/array', ['views/fields/base', 'lib!Selectize'], functi } if (this.options.customOptionList) { - this.setOptionList(this.options.customOptionList); + this.setOptionList(this.options.customOptionList, true); } }, @@ -146,13 +147,21 @@ Espo.define('views/fields/array', ['views/fields/base', 'lib!Selectize'], functi }, - setOptionList: function (optionList) { + setOptionList: function (optionList, silent) { if (!this.originalOptionList) { this.originalOptionList = this.params.options; } this.params.options = Espo.Utils.clone(optionList); - if (this.mode == 'edit') { + if (this.mode == 'edit' && !silent) { + var selectedOptionList = []; + this.selected.forEach(function (option) { + if (~optionList.indexOf(option)) { + selectedOptionList.push(option); + } + }, this); + this.selected = selectedOptionList; + if (this.isRendered()) { this.reRender(); this.trigger('change'); diff --git a/client/src/views/fields/attachment-multiple.js b/client/src/views/fields/attachment-multiple.js index 9826ee856e..44cc9365e8 100644 --- a/client/src/views/fields/attachment-multiple.js +++ b/client/src/views/fields/attachment-multiple.js @@ -32,7 +32,7 @@ Espo.define('views/fields/attachment-multiple', 'views/fields/base', function (D type: 'attachmentMultiple', - listTemplate: 'fields/attachments-multiple/detail', + listTemplate: 'fields/attachments-multiple/list', detailTemplate: 'fields/attachments-multiple/detail', @@ -495,7 +495,7 @@ Espo.define('views/fields/attachment-multiple', 'views/fields/base', function (D for (var id in nameHash) { var type = typeHash[id] || false; var name = nameHash[id]; - if (this.showPreviews && ~this.previewTypeList.indexOf(type)) { + if (this.showPreviews && ~this.previewTypeList.indexOf(type) && this.mode !== 'list') { previews.push('
' + this.getDetailPreview(name, type, id) + '
'); continue; } diff --git a/client/src/views/fields/int.js b/client/src/views/fields/int.js index 2772a7e91a..117fdd5fac 100644 --- a/client/src/views/fields/int.js +++ b/client/src/views/fields/int.js @@ -78,6 +78,7 @@ Espo.define('views/fields/int', 'views/fields/base', function (Dep) { if (this.model.get(this.name) !== null && typeof this.model.get(this.name) !== 'undefined') { data.isNotEmpty = true; } + data.valueIsSet = this.model.has(this.name); return data; }, diff --git a/client/src/views/fields/link-multiple.js b/client/src/views/fields/link-multiple.js index c952524a30..d097211838 100644 --- a/client/src/views/fields/link-multiple.js +++ b/client/src/views/fields/link-multiple.js @@ -32,7 +32,7 @@ Espo.define('views/fields/link-multiple', 'views/fields/base', function (Dep) { type: 'linkMultiple', - listTemplate: 'fields/link-multiple/detail', + listTemplate: 'fields/link-multiple/list', detailTemplate: 'fields/link-multiple/detail', @@ -359,16 +359,19 @@ Espo.define('views/fields/link-multiple', 'views/fields/base', function (Dep) { var type = this.$el.find('select.search-type').val(); if (type === 'anyOf') { - var values = this.ids || []; + var idList = this.ids || []; var data = { type: 'linkedWith', - value: this.ids || [], + value: idList, nameHash: this.nameHash, data: { type: type } }; + if (!idList.length) { + data.value = null; + } return data; } else if (type === 'noneOf') { var values = this.ids || []; diff --git a/client/src/views/fields/multi-enum.js b/client/src/views/fields/multi-enum.js index 19a6614afa..d0426d8ad8 100644 --- a/client/src/views/fields/multi-enum.js +++ b/client/src/views/fields/multi-enum.js @@ -32,7 +32,7 @@ Espo.define('views/fields/multi-enum', ['views/fields/array', 'lib!Selectize'], type: 'multiEnum', - listTemplate: 'fields//array/detail', + listTemplate: 'fields//array/list', detailTemplate: 'fields/array/detail', diff --git a/client/src/views/fields/person-name.js b/client/src/views/fields/person-name.js index 548bc35cf8..6a128077cc 100644 --- a/client/src/views/fields/person-name.js +++ b/client/src/views/fields/person-name.js @@ -46,6 +46,8 @@ Espo.define('views/fields/person-name', 'views/fields/varchar', function (Dep) { data.firstMaxLength = this.model.getFieldParam(this.firstField, 'maxLength'); data.lastMaxLength = this.model.getFieldParam(this.lastField, 'maxLength'); + data.valueIsSet = this.model.has(this.firstField) || this.model.has(this.lastField); + if (this.mode === 'detail') { data.isNotEmpty = !!data.firstValue || !!data.lastValue || !!data.salutationValue; } else if (this.mode === 'list' || this.mode === 'listLink') { diff --git a/client/src/views/fields/url.js b/client/src/views/fields/url.js index 8642565e72..2179726384 100644 --- a/client/src/views/fields/url.js +++ b/client/src/views/fields/url.js @@ -26,15 +26,15 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('Views.Fields.Url', 'Views.Fields.Varchar', function (Dep) { +Espo.define('views/fields/url', 'views/fields/varchar', function (Dep) { return Dep.extend({ type: 'url', - listTemplate: 'fields.url.list', + listTemplate: 'fields/url/list', - detailTemplate: 'fields.url.detail', + detailTemplate: 'fields/url/detail', setup: function () { Dep.prototype.setup.call(this); diff --git a/client/src/views/fields/varchar.js b/client/src/views/fields/varchar.js index ae5068ce64..57cf864d50 100644 --- a/client/src/views/fields/varchar.js +++ b/client/src/views/fields/varchar.js @@ -58,6 +58,8 @@ Espo.define('views/fields/varchar', 'views/fields/base', function (Dep) { ) { data.isNotEmpty = true; } + data.valueIsSet = this.model.has(this.name); + if (this.mode === 'search') { if (typeof this.searchParams.value === 'string') { this.searchData.value = this.searchParams.value; diff --git a/client/src/views/user/detail.js b/client/src/views/user/detail.js index 45021c6660..f133fd573b 100644 --- a/client/src/views/user/detail.js +++ b/client/src/views/user/detail.js @@ -38,7 +38,8 @@ Espo.define('views/user/detail', 'views/detail', function (Dep) { name: 'preferences', label: 'Preferences', style: 'default', - action: "preferences" + action: "preferences", + link: '#Preferences/edit/' + this.getUser().id }); if (!this.model.get('isPortalUser')) { @@ -47,7 +48,8 @@ Espo.define('views/user/detail', 'views/detail', function (Dep) { name: 'emailAccounts', label: "Email Accounts", style: 'default', - action: "emailAccounts" + action: "emailAccounts", + link: '#EmailAccount/list/userId=' + this.model.id + '&userName=' + encodeURIComponent(this.model.get('name')) }); } @@ -56,7 +58,8 @@ Espo.define('views/user/detail', 'views/detail', function (Dep) { name: 'externalAccounts', label: 'External Accounts', style: 'default', - action: "externalAccounts" + action: "externalAccounts", + link: '#ExternalAccount' }); } } diff --git a/package.json b/package.json index f898345426..b75bad4785 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "espocrm", - "version": "5.3.4", + "version": "5.3.5", "description": "", "main": "index.php", "repository": { diff --git a/tests/unit/Espo/ORM/DB/MapperTest.php b/tests/unit/Espo/ORM/DB/MapperTest.php index f960067e85..c257ff7fd5 100644 --- a/tests/unit/Espo/ORM/DB/MapperTest.php +++ b/tests/unit/Espo/ORM/DB/MapperTest.php @@ -35,6 +35,7 @@ use Espo\Entities\Post; use Espo\Entities\Comment; use Espo\Entities\Tag; use Espo\Entities\Note; +use Espo\Entities\Job; require_once 'tests/unit/testData/DB/Entities.php'; require_once 'tests/unit/testData/DB/MockPDO.php'; @@ -348,6 +349,34 @@ class DBMapperTest extends \PHPUnit\Framework\TestCase $this->db->update($this->post); } + public function testUpdateArray1() + { + $query = "UPDATE `job` SET `array` = '[\"2\",\"1\"]' WHERE job.id = '1' AND job.deleted = '0'"; + + $this->mockQuery($query, true); + + $job = new \Espo\Entities\Job(); + $job->id = '1'; + $job->setFetched('array', ['1', '2']); + $job->set('array', ['2', '1']); + + $this->db->update($job); + } + + public function testUpdateArray2() + { + $query = "UPDATE `job` SET `array` = NULL WHERE job.id = '1' AND job.deleted = '0'"; + + $this->mockQuery($query, true); + + $job = new \Espo\Entities\Job(); + $job->id = '1'; + $job->setFetched('array', ['1', '2']); + $job->set('array', null); + + $this->db->update($job); + } + public function testRemoveRelationHasMany() { $query = "UPDATE `comment` SET post_id = NULL WHERE comment.deleted = '0' AND comment.id = '100'"; @@ -448,6 +477,5 @@ class DBMapperTest extends \PHPUnit\Framework\TestCase ) )); } + } - - diff --git a/tests/unit/Espo/ORM/EntityTest.php b/tests/unit/Espo/ORM/EntityTest.php new file mode 100644 index 0000000000..825902f81f --- /dev/null +++ b/tests/unit/Espo/ORM/EntityTest.php @@ -0,0 +1,201 @@ +setFetched('string', 'test'); + $this->assertFalse($job->isAttributeChanged('string')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('string', 'test'); + $job->set('string', 'hello'); + $this->assertTrue($job->isAttributeChanged('string')); + + $job = new \Espo\Entities\Job(); + $job->set('string', 'hello'); + $this->assertTrue($job->isAttributeChanged('string')); + + $job = new \Espo\Entities\Job(); + $job->set('string', null); + $this->assertTrue($job->isAttributeChanged('string')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', ['1', '2']); + $job->set('array', ['2', '1']); + $this->assertTrue($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', ['1', '2']); + $job->set('array', ['1', '2']); + $this->assertFalse($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', ['1', '2']); + $job->set('array', ['1', 2]); + $this->assertTrue($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', [ + (object) ['1' => 'v1'] + ]); + $job->set('array', [ + (object) ['1' => 'v1'] + ]); + $this->assertFalse($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', [ + (object) ['k1' => 'v1'] + ]); + $job->set('array', [ + (object) ['k1' => 'v2'] + ]); + $this->assertTrue($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', [ + (object) ['k1' => 'v1'] + ]); + $job->set('array', [ + (object) ['k1' => 'v1', 'k2' => 'v2'], + ]); + $this->assertTrue($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', ['1', '2']); + $job->set('array', ['1', '2', '3']); + $this->assertTrue($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->set('array', ['1', '2', '3']); + $this->assertTrue($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->set('array', null); + $this->assertTrue($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', null); + $this->assertFalse($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('arrayUnordered', ['1', '2']); + $job->set('arrayUnordered', ['2', '1']); + $this->assertFalse($job->isAttributeChanged('arrayUnordered')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('arrayUnordered', ['1', '2']); + $job->set('arrayUnordered', ['1', '2']); + $this->assertFalse($job->isAttributeChanged('arrayUnordered')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('arrayUnordered', ['1', '2']); + $job->set('arrayUnordered', ['1', '2', '3']); + $this->assertTrue($job->isAttributeChanged('arrayUnordered')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('arrayUnordered', ['1', '2']); + $job->set('arrayUnordered', null); + $this->assertTrue($job->isAttributeChanged('arrayUnordered')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('object', (object) ['1' => 'value-1']); + $job->set('object', (object) ['1' => 'value-1']); + $this->assertFalse($job->isAttributeChanged('object')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('object', (object) ['1' => 'value-1']); + $job->set('object', ['1' => 'value-1']); + $this->assertTrue($job->isAttributeChanged('object')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('object', (object) ['1' => '1']); + $job->set('object', (object) ['1' => 1]); + $this->assertTrue($job->isAttributeChanged('object')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('object', (object) [ + 'k1' => (object) [ + 'k11' => 'v1' + ] + ]); + $job->set('object', (object) [ + 'k1' => (object) [ + 'k11' => 'v2' + ] + ]); + $this->assertTrue($job->isAttributeChanged('object')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('object', (object) [ + 'k1' => [ + 'k11' => 'v1' + ] + ]); + $job->set('object', (object) [ + 'k1' => (object) [ + 'k11' => 'v1' + ] + ]); + $this->assertTrue($job->isAttributeChanged('object')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('object', [ + 'k1' => [ + 'k11' => 'v1' + ] + ]); + $job->set('object', (object) [ + 'k1' => (object) [ + 'k11' => 'v1' + ] + ]); + $this->assertTrue($job->isAttributeChanged('object')); + } + +} diff --git a/tests/unit/testData/DB/Entities.php b/tests/unit/testData/DB/Entities.php index 93117edf1c..e59cbaaef4 100644 --- a/tests/unit/testData/DB/Entities.php +++ b/tests/unit/testData/DB/Entities.php @@ -242,3 +242,32 @@ class Article extends TEntity ) ); } + + +class Job extends TEntity +{ + public $fields = array( + 'id' => array( + 'type' => Entity::ID + ), + 'string' => array( + 'type' => Entity::VARCHAR, + 'len' => 50 + ), + 'array' => array( + 'type' => Entity::JSON_ARRAY + ), + 'arrayUnordered' => array( + 'type' => Entity::JSON_ARRAY, + 'isUnordered' => true + ), + 'object' => array( + 'type' => Entity::JSON_OBJECT + ), + 'deleted' => array( + 'type' => Entity::BOOL, + 'default' => 0 + ) + ); +} +