diff --git a/application/Espo/Core/HookManager.php b/application/Espo/Core/HookManager.php index 6f50cc91f7..72149ae8b8 100644 --- a/application/Espo/Core/HookManager.php +++ b/application/Espo/Core/HookManager.php @@ -46,10 +46,10 @@ class HookManager * * @var array */ - protected $ignoredMethods = array( + protected $ignoredMethodList = array( '__construct', 'getDependencyList', - 'inject', + 'inject' ); protected $paths = array( @@ -168,7 +168,7 @@ class HookManager $className = Util::getClassName($hookFilePath); $classMethods = get_class_methods($className); - $hookMethods = array_diff($classMethods, $this->ignoredMethods); + $hookMethods = array_diff($classMethods, $this->ignoredMethodList); foreach($hookMethods as $hookName) { $entityHookData = isset($hookData[$scopeName][$hookName]) ? $hookData[$scopeName][$hookName] : array(); diff --git a/application/Espo/Core/ORM/Repositories/RDB.php b/application/Espo/Core/ORM/Repositories/RDB.php index c99bcf3375..d95b62aa13 100644 --- a/application/Espo/Core/ORM/Repositories/RDB.php +++ b/application/Espo/Core/ORM/Repositories/RDB.php @@ -189,6 +189,13 @@ class RDB extends \Espo\ORM\Repositories\RDB implements Injectable $this->getEntityManager()->getHookManager()->process($this->entityType, 'afterRemove', $entity, $options); } + protected function afterMassRelate(Entity $entity, $relationName, array $params = array(), array $options = array()) + { + $options['params'] = $params; + + $this->getEntityManager()->getHookManager()->process($this->entityType, 'afterMassRelate', $entity, $options); + } + public function remove(Entity $entity, array $options = array()) { $result = parent::remove($entity, $options); diff --git a/application/Espo/Core/Utils/Api/Auth.php b/application/Espo/Core/Utils/Api/Auth.php index 2d735e6616..ec19e8a70f 100644 --- a/application/Espo/Core/Utils/Api/Auth.php +++ b/application/Espo/Core/Utils/Api/Auth.php @@ -61,6 +61,13 @@ class Auth extends \Slim\Middleware list($authUsername, $authPassword) = explode(':', base64_decode($espoAuth)); } + if (!isset($authUsername)) { + if (!empty($_COOKIE['auth-username']) && !empty($_COOKIE['auth-token'])) { + $authUsername = $_COOKIE['auth-username']; + $authPassword = $_COOKIE['auth-token']; + } + } + $espoCgiAuth = $req->headers('HTTP_ESPO_CGI_AUTH'); if (empty($espoCgiAuth)) { $espoCgiAuth = $req->headers('REDIRECT_HTTP_ESPO_CGI_AUTH'); diff --git a/application/Espo/Core/defaults/config.php b/application/Espo/Core/defaults/config.php index e999f55c68..2bbf79ad5e 100644 --- a/application/Espo/Core/defaults/config.php +++ b/application/Espo/Core/defaults/config.php @@ -157,8 +157,9 @@ return array ( 'activitiesEntityList' => ['Meeting', 'Call'], 'historyEntityList' => ['Meeting', 'Call', 'Email'], 'lastViewedCount' => 20, - 'cleanupJobPeriod' => '-1 month', - 'cleanupActionHistoryPeriod' => '-15 days', + 'cleanupJobPeriod' => '1 month', + 'cleanupActionHistoryPeriod' => '15 days', + 'cleanupAuthTokenPeriod' => '1 month', 'isInstalled' => false ); diff --git a/application/Espo/Jobs/Cleanup.php b/application/Espo/Jobs/Cleanup.php index 8b6ec1a63d..8e9f67e05d 100644 --- a/application/Espo/Jobs/Cleanup.php +++ b/application/Espo/Jobs/Cleanup.php @@ -33,9 +33,11 @@ use \Espo\Core\Exceptions; class Cleanup extends \Espo\Core\Jobs\Base { - protected $cleanupJobPeriod = '-1 month'; + protected $cleanupJobPeriod = '1 month'; - protected $cleanupActionHistoryPeriod = '-15 days'; + protected $cleanupActionHistoryPeriod = '15 days'; + + protected $cleanupAuthTokenPeriod = '1 month'; public function run() { @@ -46,6 +48,7 @@ class Cleanup extends \Espo\Core\Jobs\Base $this->cleanupNotes(); $this->cleanupNotifications(); $this->cleanupActionHistory(); + $this->cleanupAuthToken(); } protected function cleanupJobs() @@ -83,7 +86,7 @@ class Cleanup extends \Espo\Core\Jobs\Base protected function cleanupActionHistory() { - $period = $this->getConfig()->get('cleanupActionHistoryPeriod', $this->cleanupActionHistoryPeriod); + $period = '-' . $this->getConfig()->get('cleanupActionHistoryPeriod', $this->cleanupActionHistoryPeriod); $datetime = new \DateTime(); $datetime->modify($period); @@ -94,9 +97,22 @@ class Cleanup extends \Espo\Core\Jobs\Base $sth->execute(); } + protected function cleanupAuthToken() + { + $period = '-' . $this->getConfig()->get('cleanupAuthTokenPeriod', $this->cleanupAuthTokenPeriod); + $datetime = new \DateTime(); + $datetime->modify($period); + + $query = "DELETE FROM `auth_token` WHERE DATE(modified_at) < '" . $datetime->format('Y-m-d') . "' AND is_active = 0"; + + $pdo = $this->getEntityManager()->getPDO(); + $sth = $pdo->prepare($query); + $sth->execute(); + } + protected function getCleanupJobFromDate() { - $period = $this->getConfig()->get('cleanupJobPeriod', $this->cleanupJobPeriod); + $period = '-' . $this->getConfig()->get('cleanupJobPeriod', $this->cleanupJobPeriod); $datetime = new \DateTime(); $datetime->modify($period); return $datetime->format('Y-m-d'); diff --git a/application/Espo/Modules/Crm/Resources/i18n/da_DK/KnowledgeBaseArticle.json b/application/Espo/Modules/Crm/Resources/i18n/da_DK/KnowledgeBaseArticle.json index 9dcf12eab4..1b69660756 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/da_DK/KnowledgeBaseArticle.json +++ b/application/Espo/Modules/Crm/Resources/i18n/da_DK/KnowledgeBaseArticle.json @@ -39,7 +39,6 @@ } }, "tooltips": { - "portals": "Vil vises i alle portaler hvis den forbliver tom. Vil kun vises i valgte portaler hvis udfyldt." }, "presetFilters": { "published": "Publiseret" diff --git a/application/Espo/Modules/Crm/Resources/i18n/de_DE/KnowledgeBaseArticle.json b/application/Espo/Modules/Crm/Resources/i18n/de_DE/KnowledgeBaseArticle.json index e80a13fa40..e68c66d126 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/de_DE/KnowledgeBaseArticle.json +++ b/application/Espo/Modules/Crm/Resources/i18n/de_DE/KnowledgeBaseArticle.json @@ -39,7 +39,6 @@ } }, "tooltips": { - "portals": "Wenn in diesem Feld etwas eingetragen ist, wird dieser Artikel nur in den gewählten Portalen angezeigt. Ist das Feld leer ist der Artikel in allen Portalen zu sehen." }, "presetFilters": { "published": "Publiziert" diff --git a/application/Espo/Modules/Crm/Resources/i18n/en_US/KnowledgeBaseArticle.json b/application/Espo/Modules/Crm/Resources/i18n/en_US/KnowledgeBaseArticle.json index c3cf30b3b8..51d807ed65 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/en_US/KnowledgeBaseArticle.json +++ b/application/Espo/Modules/Crm/Resources/i18n/en_US/KnowledgeBaseArticle.json @@ -39,7 +39,7 @@ } }, "tooltips": { - "portals": "If not empty then this article will be available only in specified portals. If empty then it will available in all portals." + "portals": "Article will be available only in specified portals." }, "presetFilters": { "published": "Published" diff --git a/application/Espo/Modules/Crm/Resources/i18n/es_ES/KnowledgeBaseArticle.json b/application/Espo/Modules/Crm/Resources/i18n/es_ES/KnowledgeBaseArticle.json index 7e0d2b03ff..0403694f84 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/es_ES/KnowledgeBaseArticle.json +++ b/application/Espo/Modules/Crm/Resources/i18n/es_ES/KnowledgeBaseArticle.json @@ -39,7 +39,6 @@ } }, "tooltips": { - "portals": "Si no está vacía, entonces este artículo estará disponible solo en portales específicos. Si está vacío, entonces estará disponible en todos los portales.\t" }, "presetFilters": { "published": "Publicado" diff --git a/application/Espo/Modules/Crm/Resources/i18n/fr_FR/KnowledgeBaseArticle.json b/application/Espo/Modules/Crm/Resources/i18n/fr_FR/KnowledgeBaseArticle.json index 20c3af32f7..8d3b7c776d 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/fr_FR/KnowledgeBaseArticle.json +++ b/application/Espo/Modules/Crm/Resources/i18n/fr_FR/KnowledgeBaseArticle.json @@ -35,7 +35,7 @@ } }, "tooltips": { - "portals": "If not empty then this article will be available only in specified portals. If empty then it will available in all portals." + }, "presetFilters": { "published": "Publié" diff --git a/application/Espo/Modules/Crm/Resources/i18n/id_ID/KnowledgeBaseArticle.json b/application/Espo/Modules/Crm/Resources/i18n/id_ID/KnowledgeBaseArticle.json index e398c2cb5b..73171b2a90 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/id_ID/KnowledgeBaseArticle.json +++ b/application/Espo/Modules/Crm/Resources/i18n/id_ID/KnowledgeBaseArticle.json @@ -35,7 +35,6 @@ } }, "tooltips": { - "portals": "Jika tidak kosong maka artikel ini akan tersedia hanya dalam portal yang ditentukan. Jika kosong maka akan tersedia di semua portal." }, "presetFilters": { "published": "Diterbitkan" diff --git a/application/Espo/Modules/Crm/Resources/i18n/it_IT/KnowledgeBaseArticle.json b/application/Espo/Modules/Crm/Resources/i18n/it_IT/KnowledgeBaseArticle.json index 85199a4370..72dcd230ab 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/it_IT/KnowledgeBaseArticle.json +++ b/application/Espo/Modules/Crm/Resources/i18n/it_IT/KnowledgeBaseArticle.json @@ -37,7 +37,6 @@ } }, "tooltips": { - "portals": "Se non è vuoto allora questo articolo sarà disponibile solo nei portali specifici. Se vuoto, allora sarà disponibile in tutti i portali ." }, "presetFilters": { "published": "Pubblicato" diff --git a/application/Espo/Modules/Crm/Resources/i18n/nb_NO/KnowledgeBaseArticle.json b/application/Espo/Modules/Crm/Resources/i18n/nb_NO/KnowledgeBaseArticle.json index 1a8ad92d20..2147e52710 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/nb_NO/KnowledgeBaseArticle.json +++ b/application/Espo/Modules/Crm/Resources/i18n/nb_NO/KnowledgeBaseArticle.json @@ -39,7 +39,6 @@ } }, "tooltips": { - "portals": "Vil vises i alle portaler hvis den forblir tom. Vil kun vises i valgte portaler hvis utfylt." }, "presetFilters": { "published": "Publisert" diff --git a/application/Espo/Modules/Crm/Resources/i18n/pl_PL/KnowledgeBaseArticle.json b/application/Espo/Modules/Crm/Resources/i18n/pl_PL/KnowledgeBaseArticle.json index 8912423571..3571b8a91e 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/pl_PL/KnowledgeBaseArticle.json +++ b/application/Espo/Modules/Crm/Resources/i18n/pl_PL/KnowledgeBaseArticle.json @@ -34,7 +34,6 @@ } }, "tooltips": { - "portals": "If not empty then this article will be available only in specified portals. If empty then it will available in all portals." }, "presetFilters": { "published": "Published" diff --git a/application/Espo/Modules/Crm/Resources/i18n/ru_RU/KnowledgeBaseArticle.json b/application/Espo/Modules/Crm/Resources/i18n/ru_RU/KnowledgeBaseArticle.json index 6fd0f0ba4d..4a623513b8 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/ru_RU/KnowledgeBaseArticle.json +++ b/application/Espo/Modules/Crm/Resources/i18n/ru_RU/KnowledgeBaseArticle.json @@ -34,7 +34,7 @@ } }, "tooltips": { - "portals": "Если не пусто, то эта статья будет доступна только в указанных порталах. Если пусто, то будет доступна во всех порталах." + "portals": "Статья будет доступна только в указанных порталах." }, "presetFilters": { "published": "Опубликована" diff --git a/application/Espo/Modules/Crm/Resources/i18n/zh_CN/KnowledgeBaseArticle.json b/application/Espo/Modules/Crm/Resources/i18n/zh_CN/KnowledgeBaseArticle.json index ffc0fb41f5..f1c84cfc93 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/zh_CN/KnowledgeBaseArticle.json +++ b/application/Espo/Modules/Crm/Resources/i18n/zh_CN/KnowledgeBaseArticle.json @@ -37,7 +37,6 @@ } }, "tooltips": { - "portals": "如果不是空的那么这篇文章将只能在指定门户可用。如果是空的那么在所有门户她都可用" }, "presetFilters": { "published": "公布的" diff --git a/application/Espo/Modules/Crm/Resources/metadata/scopes/Document.json b/application/Espo/Modules/Crm/Resources/metadata/scopes/Document.json index 4037e0055b..1fe12fb732 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/scopes/Document.json +++ b/application/Espo/Modules/Crm/Resources/metadata/scopes/Document.json @@ -3,7 +3,8 @@ "layouts": true, "tab": true, "acl": true, - "aclPortal": "recordAllAccountOwnNo", + "aclPortal": true, + "aclPortalLevelList": ["all", "account", "contact", "own", "no"], "module": "Crm", "customizable": true, "importable": false, diff --git a/application/Espo/Modules/Crm/SelectManagers/KnowledgeBaseArticle.php b/application/Espo/Modules/Crm/SelectManagers/KnowledgeBaseArticle.php index 4a83c9d07b..9929f737d6 100644 --- a/application/Espo/Modules/Crm/SelectManagers/KnowledgeBaseArticle.php +++ b/application/Espo/Modules/Crm/SelectManagers/KnowledgeBaseArticle.php @@ -50,9 +50,6 @@ class KnowledgeBaseArticle extends \Espo\Core\SelectManagers\Base $this->addOrWhere(array( array( 'portals.id' => $this->getUser()->get('portalId') - ), - array( - 'portals.id' => null ) ), $result); } diff --git a/application/Espo/Modules/Crm/Services/KnowledgeBaseArticle.php b/application/Espo/Modules/Crm/Services/KnowledgeBaseArticle.php index 7765647232..5ce78fffd7 100644 --- a/application/Espo/Modules/Crm/Services/KnowledgeBaseArticle.php +++ b/application/Espo/Modules/Crm/Services/KnowledgeBaseArticle.php @@ -103,45 +103,6 @@ class KnowledgeBaseArticle extends \Espo\Services\Record ); } - public function moveToTop($id, $where = null) - { - $entity = $this->getEntityManager()->getEntity('KnowledgeBaseArticle', $id); - if (!$entity) throw new NotFound(); - if (!$this->getAcl()->check($entity, 'edit')) throw new Forbidden(); - - $currentIndex = $entity->get('order'); - - if (!is_int($currentIndex)) throw new Error(); - - if (!$where) { - $where = array(); - } - - $params = array( - 'where' => $where - ); - - $selectManager = $this->getSelectManager(); - $selectParams = $selectManager->buildSelectParams($params, true, true); - - $selectParams['whereClause'][] = array( - 'order<' => $currentIndex - ); - - $selectManager->applyOrder('order', false, $selectParams); - - $previousEntity = $this->getRepository()->findOne($selectParams); - - if (!$previousEntity) return; - - $entity->set('order', $previousEntity->get('order')); - $previousEntity->set('order', $currentIndex); - - $this->getEntityManager()->saveEntity($entity); - $this->getEntityManager()->saveEntity($previousEntity); - } - - public function moveUp($id, $where = null) { $entity = $this->getEntityManager()->getEntity('KnowledgeBaseArticle', $id); @@ -217,6 +178,43 @@ class KnowledgeBaseArticle extends \Espo\Services\Record $this->getEntityManager()->saveEntity($entity); $this->getEntityManager()->saveEntity($nextEntity); } + + public function moveToTop($id, $where = null) + { + $entity = $this->getEntityManager()->getEntity('KnowledgeBaseArticle', $id); + if (!$entity) throw new NotFound(); + if (!$this->getAcl()->check($entity, 'edit')) throw new Forbidden(); + + $currentIndex = $entity->get('order'); + + if (!is_int($currentIndex)) throw new Error(); + + if (!$where) { + $where = array(); + } + + $params = array( + 'where' => $where + ); + + $selectManager = $this->getSelectManager(); + $selectParams = $selectManager->buildSelectParams($params, true, true); + + $selectParams['whereClause'][] = array( + 'order<' => $currentIndex + ); + + $selectManager->applyOrder('order', false, $selectParams); + + $previousEntity = $this->getRepository()->findOne($selectParams); + + if (!$previousEntity) return; + + $entity->set('order', $previousEntity->get('order') - 1); + + $this->getEntityManager()->saveEntity($entity); + } + public function moveToBottom($id, $where = null) { $entity = $this->getEntityManager()->getEntity('KnowledgeBaseArticle', $id); @@ -248,10 +246,8 @@ class KnowledgeBaseArticle extends \Espo\Services\Record if (!$nextEntity) return; - $entity->set('order', $nextEntity->get('order')); - $nextEntity->set('order', $currentIndex); + $entity->set('order', $nextEntity->get('order') + 1); $this->getEntityManager()->saveEntity($entity); - $this->getEntityManager()->saveEntity($nextEntity); } } diff --git a/application/Espo/Modules/Crm/Services/MassEmail.php b/application/Espo/Modules/Crm/Services/MassEmail.php index 9801444792..5dd2d2d63c 100644 --- a/application/Espo/Modules/Crm/Services/MassEmail.php +++ b/application/Espo/Modules/Crm/Services/MassEmail.php @@ -337,8 +337,6 @@ class MassEmail extends \Espo\Services\Record $body = $emailData['body']; - - $optOutUrl = $this->getConfig()->get('siteUrl') . '?entryPoint=unsubscribe&id=' . $queueItem->id; $optOutLink = ''.$this->getLanguage()->translate('Unsubscribe', 'labels', 'Campaign').''; @@ -453,6 +451,13 @@ class MassEmail extends \Espo\Services\Record $header->setId($queueItem->id); $message->getHeaders()->addHeader($header); + $message->getHeaders()->addHeaderLine('Precedence', 'bulk'); + + if (!$this->getConfig()->get('massEmailDisableMandatoryOptOutLink')) { + $optOutUrl = $this->getConfig()->getSiteUrl() . '?entryPoint=unsubscribe&id=' . $queueItem->id; + $message->getHeaders()->addHeaderLine('List-Unsubscribe', '<' . $optOutUrl . '>'); + } + $this->getMailSender()->useGlobal()->send($email, $params, $message, $attachmentList); $isSent = true; diff --git a/application/Espo/Modules/Crm/Services/TargetList.php b/application/Espo/Modules/Crm/Services/TargetList.php index f75c5970fd..b75ad55569 100644 --- a/application/Espo/Modules/Crm/Services/TargetList.php +++ b/application/Espo/Modules/Crm/Services/TargetList.php @@ -48,6 +48,14 @@ class TargetList extends \Espo\Services\Record 'User' => 'users' ); + protected function init() + { + parent::init(); + $this->addDependencyList([ + 'hookManager' + ]); + } + public function loadAdditionalFields(Entity $entity) { parent::loadAdditionalFields($entity); @@ -178,6 +186,7 @@ class TargetList extends \Espo\Services\Record } if ($sql) { if ($pdo->query($sql)) { + $this->getInjection('hookManager')->process('TargetList', 'afterUnlinkAll', $entity, array('link' => $link)); return true; } } diff --git a/application/Espo/ORM/Repositories/RDB.php b/application/Espo/ORM/Repositories/RDB.php index acd956da5b..f832af56cb 100644 --- a/application/Espo/ORM/Repositories/RDB.php +++ b/application/Espo/ORM/Repositories/RDB.php @@ -354,22 +354,26 @@ class RDB extends \Espo\ORM\Repository protected function beforeRelate(Entity $entity, $relationName, $foreign, $data = null, array $options = array()) { - } protected function afterRelate(Entity $entity, $relationName, $foreign, $data = null, array $options = array()) { - } protected function beforeUnrelate(Entity $entity, $relationName, $foreign, array $options = array()) { - } protected function afterUnrelate(Entity $entity, $relationName, $foreign, array $options = array()) { + } + protected function beforeMassRelate(Entity $entity, $relationName, array $params = array(), array $options = array()) + { + } + + protected function afterMassRelate(Entity $entity, $relationName, array $params = array(), array $options = array()) + { } public function updateRelation(Entity $entity, $relationName, $foreign, $data) @@ -391,12 +395,18 @@ class RDB extends \Espo\ORM\Repository return null; } - public function massRelate(Entity $entity, $relationName, array $params = array()) + public function massRelate(Entity $entity, $relationName, array $params = array(), array $options = array()) { if (!$entity->id) { return; } - return $this->getMapper()->massRelate($entity, $relationName, $params); + $this->beforeMassRelate($entity, $relationName, $params, $options); + + $result = $this->getMapper()->massRelate($entity, $relationName, $params); + if ($result) { + $this->afterMassRelate($entity, $relationName, $params, $options); + } + return $result; } public function getAll() diff --git a/application/Espo/Resources/i18n/en_US/DashletOptions.json b/application/Espo/Resources/i18n/en_US/DashletOptions.json index f11e7f176a..530bf47a1e 100644 --- a/application/Espo/Resources/i18n/en_US/DashletOptions.json +++ b/application/Espo/Resources/i18n/en_US/DashletOptions.json @@ -8,7 +8,12 @@ "isDoubleHeight": "Height 2x", "mode": "Mode", "enabledScopeList": "What to display", - "users": "Users" + "users": "Users", + "entityType": "Entity Type", + "primaryFilter": "Primary Filter", + "boolFilterList": "Additional Filters", + "sortBy": "Order (field)", + "sortDirection": "Order (direction)" }, "options": { "mode": { @@ -19,5 +24,8 @@ "agendaDay": "Day (agenda)", "timeline": "Timeline" } + }, + "messages": { + "selectEntityType": "Select Entity Type in dashlet options." } } diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index 8a24989a1d..2c62a535f2 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -313,7 +313,8 @@ }, "dashlets": { "Stream": "Stream", - "Emails": "My Inbox" + "Emails": "My Inbox", + "Records": "Record List" }, "notificationMessages": { "assign": "{entityType} {entity} has been assigned to you", diff --git a/application/Espo/Resources/metadata/dashlets/Records.json b/application/Espo/Resources/metadata/dashlets/Records.json new file mode 100644 index 0000000000..aabec551d9 --- /dev/null +++ b/application/Espo/Resources/metadata/dashlets/Records.json @@ -0,0 +1,80 @@ +{ + "options": { + "fields": { + "title": { + "type": "varchar", + "required": true + }, + "autorefreshInterval": { + "type": "enumFloat", + "options": [0, 0.5, 1, 2, 5, 10] + }, + "displayRecords": { + "type": "enumInt", + "options": [3, 4, 5, 10, 15, 20, 30, 50] + }, + "entityType": { + "type": "enum", + "view": "views/dashlets/fields/records/entity-type", + "translation": "Global.scopeNames" + }, + "primaryFilter": { + "type": "enum", + "view": "views/dashlets/fields/records/primary-filter" + }, + "boolFilterList": { + "type": "multiEnum", + "view": "views/dashlets/fields/records/bool-filter-list" + }, + "sortBy": { + "type": "enum", + "view": "views/dashlets/fields/records/sort-by" + }, + "sortDirection": { + "type": "enum", + "view": "views/dashlets/fields/records/sort-direction", + "options": ["asc", "desc"], + "translation": "EntityManager.options.sortDirection" + } + }, + "defaults": { + "displayRecords": 10, + "autorefreshInterval": 0.5, + "expandedLayout": { + "rows": [ + [ + { + "name": "name", + "link": true + } + ] + ] + } + }, + "layout": [ + { + "rows": [ + [ + {"name": "title"} + ], + [ + {"name": "entityType"}, + false + ], + [ + {"name": "primaryFilter"}, + {"name": "sortBy"} + ], + [ + {"name": "boolFilterList"}, + {"name": "sortDirection"} + ], + [ + {"name": "displayRecords"}, + {"name": "autorefreshInterval"} + ] + ] + } + ] + } +} diff --git a/client/src/app.js b/client/src/app.js index 312fc55de2..028f761aa3 100644 --- a/client/src/app.js +++ b/client/src/app.js @@ -391,7 +391,7 @@ Espo.define( this.auth = Base64.encode(data.auth.userName + ':' + data.auth.token); this.storage.set('user', 'auth', this.auth); - this.setCookieAuthToken(data.auth.token); + this.setCookieAuth(data.auth.userName, data.auth.token); this.initUserData(data, function () { this.trigger('auth'); @@ -426,21 +426,24 @@ Espo.define( this.doAction({action: 'login'}); this.language.clearCache(); - this.unsetCookieAuthToken(); + this.unsetCookieAuth(); xhr = new XMLHttpRequest; - xhr.open('GET', this.url + '/', true, 'logout', 'logout'); + xhr.open('GET', this.url + '/'); + xhr.setRequestHeader('Authorization', 'Basic ' + Base64.encode('**logout:logout')); xhr.send(''); xhr.abort(); }, - setCookieAuthToken: function (token) { + setCookieAuth: function (username, token) { var date = new Date(); date.setTime(date.getTime() + (1000 * 24*60*60*1000)); + document.cookie = 'auth-username='+username+'; expires='+date.toGMTString()+'; path=/'; document.cookie = 'auth-token='+token+'; expires='+date.toGMTString()+'; path=/'; }, - unsetCookieAuthToken: function () { + unsetCookieAuth: function () { + document.cookie = 'auth-username' + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; document.cookie = 'auth-token' + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/'; }, @@ -485,14 +488,19 @@ Espo.define( return; } - var arr = Base64.decode(this.auth).split(':'); var xhr = new XMLHttpRequest(); - xhr.open('GET', this.basePath + this.url + '/', true, arr[0], arr[1]); + + xhr.open('GET', this.basePath + this.url + '/'); + xhr.setRequestHeader('Authorization', 'Basic ' + this.auth); + + xhr.onreadystatechange = function () { + if (callback) { + callback(); + } + } + xhr.send(''); - if (callback) { - callback(); - } }.bind(this)); }, diff --git a/client/src/view-helper.js b/client/src/view-helper.js index 6c5a8d6546..dcb92f7c84 100644 --- a/client/src/view-helper.js +++ b/client/src/view-helper.js @@ -43,7 +43,7 @@ Espo.define('view-helper', [], function () { this.mdReplace = [ '$1', function (s, string) { - return '' + string.replace(/\*/g, '*').replace(/\~/g, '~') + ''; + return '
' + string.replace(/\*/g, '*').replace(/\~/g, '~') + '
'; }, function (s, string) { return '' + string.replace(/\*/g, '*').replace(/\~/g, '~') + ''; diff --git a/client/src/views/dashlets/abstract/record-list.js b/client/src/views/dashlets/abstract/record-list.js index 6e410986d6..b8a83eddb7 100644 --- a/client/src/views/dashlets/abstract/record-list.js +++ b/client/src/views/dashlets/abstract/record-list.js @@ -60,9 +60,20 @@ Espo.define('views/dashlets/abstract/record-list', ['views/dashlets/abstract/bas return this.getAcl().check(this.scope, 'read'); }, + getSearchData: function () { + return this.getOption('searchData'); + }, + afterRender: function () { this.getCollectionFactory().create(this.scope, function (collection) { - var searchManager = this.searchManager = new SearchManager(collection, 'list', null, this.getDateTime(), this.getOption('searchData')); + var searchData = this.getSearchData(); + + var searchManager = this.searchManager = new SearchManager(collection, 'list', null, this.getDateTime(), searchData); + + if (!this.scope) { + this.$el.find('.list-container').html(this.translate('selectEntityType', 'messages', 'DashletOptions')); + return; + } if (!this.checkAccess()) { this.$el.find('.list-container').html(this.translate('No Access')); @@ -76,6 +87,13 @@ Espo.define('views/dashlets/abstract/record-list', ['views/dashlets/abstract/bas this.collection = collection; collection.sortBy = this.getOption('sortBy') || this.collection.sortBy; collection.asc = this.getOption('asc') || this.collection.asc; + + if (this.getOption('sortDirection') === 'asc') { + collection.asc = true; + } else if (this.getOption('sortDirection') === 'desc') { + collection.asc = false; + } + collection.maxSize = this.getOption('displayRecords'); collection.where = searchManager.getWhere(); @@ -102,7 +120,7 @@ Espo.define('views/dashlets/abstract/record-list', ['views/dashlets/abstract/bas }, setupActionList: function () { - if (this.getAcl().checkScope(this.scope, 'create')) { + if (this.scope && this.getAcl().checkScope(this.scope, 'create')) { this.actionList.unshift({ name: 'create', html: this.translate('Create ' + this.scope, 'labels', this.scope), @@ -113,6 +131,8 @@ Espo.define('views/dashlets/abstract/record-list', ['views/dashlets/abstract/bas }, actionRefresh: function () { + if (!this.collection) return; + this.collection.where = this.searchManager.getWhere(); this.collection.fetch(); }, diff --git a/client/src/views/dashlets/fields/records/bool-filter-list.js b/client/src/views/dashlets/fields/records/bool-filter-list.js new file mode 100644 index 0000000000..6895cedde2 --- /dev/null +++ b/client/src/views/dashlets/fields/records/bool-filter-list.js @@ -0,0 +1,61 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2017 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU General Public License version 3. + * + * In accordance with Section 7(b) of the GNU General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ +Espo.define('views/dashlets/fields/records/bool-filter-list', 'views/fields/multi-enum', function (Dep) { + + return Dep.extend({ + + setup: function () { + Dep.prototype.setup.call(this); + + this.listenTo(this.model, 'change:entityType', function () { + this.setupOptions(); + this.reRender(); + }, this); + }, + + setupOptions: function () { + var entityType = this.model.get('entityType'); + if (!entityType) { + this.params.options = []; + return; + } + this.params.options = this.getMetadata().get(['clientDefs', entityType, 'boolFilterList']) || []; + + if (this.getMetadata().get(['scopes', entityType, 'stream']) && this.getAcl().checkScope(entityType, 'stream')) { + this.params.options.push('followed'); + } + + this.translatedOptions = {}; + this.params.options.forEach(function (item) { + this.translatedOptions[item] = this.translate(item, 'boolFilters', entityType); + }, this); + } + + }); + +}); diff --git a/client/src/views/dashlets/fields/records/entity-type.js b/client/src/views/dashlets/fields/records/entity-type.js new file mode 100644 index 0000000000..49fb80eb95 --- /dev/null +++ b/client/src/views/dashlets/fields/records/entity-type.js @@ -0,0 +1,76 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2017 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU General Public License version 3. + * + * In accordance with Section 7(b) of the GNU General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ +Espo.define('views/dashlets/fields/records/entity-type', 'views/fields/enum', function (Dep) { + + return Dep.extend({ + + setup: function () { + Dep.prototype.setup.call(this); + + this.on('change', function () { + var o = { + primaryFilter: null, + boolFilterList: [], + title: this.translate('Records', 'dashlets'), + sortBy: null, + sortDirection: 'asc' + }; + var entityType = this.model.get('entityType'); + if (entityType) { + o.title = this.translate(entityType, 'scopeNamesPlural'); + o.sortBy = this.getMetadata().get(['entityDefs', entityType, 'collection', 'sortBy']); + var asc = this.getMetadata().get(['entityDefs', entityType, 'collection', 'asc']); + if (asc) { + o.sortDirection = 'asc'; + } else { + o.sortDirection = 'desc'; + } + } + + this.model.set(o); + }, this); + }, + + setupOptions: function () { + this.params.options = Object.keys(this.getMetadata().get('scopes')).filter(function (scope) { + if (this.getMetadata().get('scopes.' + scope + '.disabled')) return; + if (!this.getAcl().checkScope(scope, 'read')) return; + if (!this.getMetadata().get(['scopes', scope, 'entity'])) return; + if (!this.getMetadata().get(['scopes', scope, 'object'])) return; + + return true; + }, this).sort(function (v1, v2) { + return this.translate(v1, 'scopeNames').localeCompare(this.translate(v2, 'scopeNames')); + }.bind(this)); + + this.params.options.unshift(''); + } + + }); + +}); diff --git a/client/src/views/dashlets/fields/records/primary-filter.js b/client/src/views/dashlets/fields/records/primary-filter.js new file mode 100644 index 0000000000..96642794d6 --- /dev/null +++ b/client/src/views/dashlets/fields/records/primary-filter.js @@ -0,0 +1,68 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2017 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU General Public License version 3. + * + * In accordance with Section 7(b) of the GNU General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ +Espo.define('views/dashlets/fields/records/primary-filter', 'views/fields/enum', function (Dep) { + + return Dep.extend({ + + setup: function () { + Dep.prototype.setup.call(this); + + this.listenTo(this.model, 'change:entityType', function () { + this.setupOptions(); + this.reRender(); + }, this); + }, + + setupOptions: function () { + var entityType = this.model.get('entityType'); + if (!entityType) { + this.params.options = []; + return; + } + var filterList = this.getMetadata().get(['clientDefs', entityType, 'filterList']) || []; + this.params.options = []; + + filterList.forEach(function (item) { + if (typeof item === 'object' && item.name) { + this.params.options.push(item.name); + return; + } + this.params.options.push(item); + }, this); + + this.params.options.unshift('all'); + + this.translatedOptions = {}; + this.params.options.forEach(function (item) { + this.translatedOptions[item] = this.translate(item, 'presetFilters', entityType); + }, this); + } + + }); + +}); diff --git a/client/src/views/dashlets/fields/records/sort-by.js b/client/src/views/dashlets/fields/records/sort-by.js new file mode 100644 index 0000000000..b7c520f65b --- /dev/null +++ b/client/src/views/dashlets/fields/records/sort-by.js @@ -0,0 +1,71 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2017 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU General Public License version 3. + * + * In accordance with Section 7(b) of the GNU General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ +Espo.define('views/dashlets/fields/records/sort-by', 'views/fields/enum', function (Dep) { + + return Dep.extend({ + + setup: function () { + Dep.prototype.setup.call(this); + + this.listenTo(this.model, 'change:entityType', function () { + this.setupOptions(); + this.reRender(); + }, this); + }, + + setupOptions: function () { + var entityType = this.model.get('entityType'); + var scope = entityType; + + if (!entityType) { + this.params.options = []; + return; + } + var fieldDefs = this.getMetadata().get('entityDefs.' + scope + '.fields') || {}; + + var orderableFieldList = Object.keys(fieldDefs).filter(function (item) { + if (fieldDefs[item].notStorable) { + return false; + } + return true; + }, this).sort(function (v1, v2) { + return this.translate(v1, 'fields', scope).localeCompare(this.translate(v2, 'fields', scope)); + }.bind(this)); + + var translatedOptions = {}; + orderableFieldList.forEach(function (item) { + translatedOptions[item] = this.translate(item, 'fields', scope); + }, this); + + this.params.options = orderableFieldList; + this.translatedOptions = translatedOptions; + } + + }); + +}); diff --git a/client/src/views/dashlets/fields/records/sort-direction.js b/client/src/views/dashlets/fields/records/sort-direction.js new file mode 100644 index 0000000000..eb7fc7f24f --- /dev/null +++ b/client/src/views/dashlets/fields/records/sort-direction.js @@ -0,0 +1,43 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2017 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU General Public License version 3. + * + * In accordance with Section 7(b) of the GNU General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ +Espo.define('views/dashlets/fields/records/sort-direction', 'views/fields/enum', function (Dep) { + + return Dep.extend({ + + setup: function () { + Dep.prototype.setup.call(this); + + this.listenTo(this.model, 'change:entityType', function () { + this.setupOptions(); + this.reRender(); + }, this); + } + + }); + +}); diff --git a/client/src/views/dashlets/records.js b/client/src/views/dashlets/records.js new file mode 100644 index 0000000000..2cecc4a5e5 --- /dev/null +++ b/client/src/views/dashlets/records.js @@ -0,0 +1,75 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2017 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU General Public License version 3. + * + * In accordance with Section 7(b) of the GNU General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +Espo.define('views/dashlets/records', 'views/dashlets/abstract/record-list', function (Dep) { + + return Dep.extend({ + + name: 'Records', + + scope: null, + + rowActionsView: 'views/record/row-actions/view-and-edit', + + listView: 'views/email/record/list-expanded', + + init: function () { + Dep.prototype.init.call(this); + this.scope = this.getOption('entityType'); + }, + + getSearchData: function () { + var data = { + primary: this.getOption('primaryFilter') + }; + + var bool = {}; + (this.getOption('boolFilterList') || []).forEach(function (item) { + bool[item] = true; + }, this); + + data.bool = bool; + + return data; + }, + + setupActionList: function () { + var scope = this.getOption('entityType'); + if (scope && this.getAcl().checkScope(scope, 'create')) { + this.actionList.unshift({ + name: 'create', + html: this.translate('Create ' + scope, 'labels', scope), + iconHtml: '', + url: '#' + scope + '/create' + }); + } + }, + + }); +}); + diff --git a/client/src/views/email/record/detail.js b/client/src/views/email/record/detail.js index 7e92719dd7..e166310469 100644 --- a/client/src/views/email/record/detail.js +++ b/client/src/views/email/record/detail.js @@ -294,6 +294,28 @@ Espo.define('views/email/record/detail', 'views/record/detail', function (Dep) { this.save(); }, + exitAfterDelete: function () { + var folderId = ((this.collection || {}).data || {}).folderId || null; + + if (folderId === 'inbox') { + folderId = null; + } + + var options = { + isReturn: true, + isReturnThroughLink: false, + folder: folderId + }; + var url = '#' + this.scope; + var action = null; + if (folderId) { + action = 'list'; + url += '/list/folder=' + folderId; + } + this.getRouter().dispatch(this.scope, action, options); + this.getRouter().navigate(url, {trigger: false}); + } + }); }); diff --git a/client/src/views/record/detail.js b/client/src/views/record/detail.js index 4d60152ed8..3a1ae19650 100644 --- a/client/src/views/record/detail.js +++ b/client/src/views/record/detail.js @@ -612,7 +612,7 @@ Espo.define('views/record/detail', ['views/record/base', 'view-record-helper'], this.recordHelper = new ViewRecordHelper(this.defaultFieldStates, this.defaultFieldStates); - var collection = this.model.collection; + var collection = this.collection = this.model.collection; if (collection) { this.listenTo(this.model, 'destroy', function () { collection.remove(this.model.id); @@ -1174,6 +1174,13 @@ Espo.define('views/record/detail', ['views/record/base', 'view-record-helper'], if (this.returnUrl) { url = this.returnUrl; } else { + if (after) { + var methodName = 'exitAfter' + Espo.Utils.upperCaseFirst(after); + if (methodName in this) { + this[methodName](); + return; + } + } if (after == 'delete') { this.getRouter().dispatch(this.scope, null, { isReturn: true diff --git a/client/src/views/record/list.js b/client/src/views/record/list.js index 16d8b928ec..ace534d34c 100644 --- a/client/src/views/record/list.js +++ b/client/src/views/record/list.js @@ -271,7 +271,7 @@ Espo.define('views/record/list', 'view', function (Dep) { this.massActionsDisabled = this.options.massActionsDisabled || this.massActionsDisabled; - if (this.massActionsDisabled) { + if (this.massActionsDisabled && !this.selectable) { this.checkboxes = false; } diff --git a/frontend/less/espo/custom.less b/frontend/less/espo/custom.less index 9d9d128e85..7615ad60b8 100644 --- a/frontend/less/espo/custom.less +++ b/frontend/less/espo/custom.less @@ -747,6 +747,10 @@ td.cell[data-name="buttons"] > .btn-group { height: ~"calc(100% - 29px)"; } } + .grid-stack-item-content { + overflow-x: visible; + overflow-y: visible; + } } .ui-resizable-handle:before { @@ -1334,6 +1338,10 @@ h5 { position: relative; } +code { + font-style: italic; +} + @media screen and (min-width: @screen-sm-min) { #global-search-panel { margin-top: 5px; diff --git a/frontend/less/espo/variables.less b/frontend/less/espo/variables.less index 5496a6aab0..d0a4e18ca3 100644 --- a/frontend/less/espo/variables.less +++ b/frontend/less/espo/variables.less @@ -6,7 +6,6 @@ @font-family-base: @font-family-sans-serif; @headings-font-family: @font-family-base; - @border-radius-base: 0; @border-radius-large: 0; @border-radius-small: 0; @@ -57,6 +56,12 @@ @btn-default-bg: @main-gray; @btn-default-border: @main-gray; +@code-bg: @main-gray; +@code-color: @text-color; + +@pre-border-color: @main-gray; +@pre-bg: @main-gray; + @btn-success-border: @btn-success-bg; @btn-primary-border: @btn-primary-bg; @btn-danger-border: @btn-danger-bg;