Compare commits

...

43 Commits
5.9.1 ... 5.9.2

Author SHA1 Message Date
Yuri Kuznetsov
2cc4c8f974 fix mail sender 2020-06-04 14:49:50 +03:00
Yuri Kuznetsov
a3aa74013a oauth storing new refresh token 2020-06-03 19:26:57 +03:00
Yuri Kuznetsov
509b3affd1 email account: imap handler 2020-06-03 10:10:34 +03:00
Yuri Kuznetsov
ad9b2b54dd scheduled job log panel fix 2020-06-02 23:01:50 +03:00
Yuri Kuznetsov
247eb00963 fix field-manager.js 2020-06-02 20:07:49 +03:00
Yuri Kuznetsov
c0b59a49bf fix horizontal navbar 2020-06-02 20:06:18 +03:00
Yuri Kuznetsov
b525afe154 less re-grouping 2 2020-06-02 19:59:57 +03:00
Yuri Kuznetsov
b449263854 less re-grouping 2020-06-02 15:43:03 +03:00
Yuri Kuznetsov
12550c3d0a password recovery improvements 2020-05-31 18:50:54 +03:00
Yuri Kuznetsov
803a4ffb7f scheduling text fix 2020-05-28 13:41:11 +03:00
Yuri Kuznetsov
57bfea4d70 fix email body plain 2020-05-27 16:38:33 +03:00
Yuri Kuznetsov
86f460866a fix tests 2020-05-27 16:16:55 +03:00
Yuri Kuznetsov
8515157916 relationship panel: create from select modal 2020-05-27 12:01:14 +03:00
Yuri Kuznetsov
5bd18dee49 cs fix 2020-05-27 10:46:33 +03:00
Yuri Kuznetsov
88ea5e7d6c mail parser body plain change 2020-05-27 10:45:40 +03:00
Yuri Kuznetsov
3839335508 fix email body plain 2020-05-27 10:45:19 +03:00
Yuri Kuznetsov
b8e94b52aa detail layout panel label changes 2020-05-26 20:08:27 +03:00
Yuri Kuznetsov
eeda450405 pdf: ifMultipleOf 2020-05-26 13:20:22 +03:00
Yuri Kuznetsov
d051bd7df2 googleMapsImage helper name 2020-05-26 12:57:53 +03:00
Yuri Kuznetsov
690a265450 date-picker change 2020-05-26 10:36:12 +03:00
Yuri Kuznetsov
8bec0a3caf change 1 2020-05-25 16:04:53 +03:00
Yuri Kuznetsov
a89463367e v 2020-05-25 12:20:55 +03:00
Yuri Kuznetsov
05dbcbd917 field view fixes 2020-05-25 12:17:21 +03:00
Yuri Kuznetsov
774bde3e20 websocket update fixes 2020-05-25 11:46:51 +03:00
Yuri Kuznetsov
90589e0d26 scheduling color 2020-05-25 11:25:03 +03:00
Yuri Kuznetsov
be199235f1 Merge branch 'hotfix/5.9.2' of https://github.com/espocrm/espocrm into hotfix/5.9.2 2020-05-25 09:47:55 +03:00
Yuri Kuznetsov
1b2d67b027 massEmailSiteUrl admin only 2020-05-25 09:41:09 +03:00
Eymen Elkum
7bc56fc864 add missing import (#1720) 2020-05-24 11:10:23 +03:00
Yuri Kuznetsov
c706ddc809 person name field: fix typo 2020-05-22 21:55:56 +03:00
Yuri Kuznetsov
1b6b2ea140 pdf: google maps 2020-05-22 15:45:06 +03:00
Yuri Kuznetsov
c1db037fc2 htmlzier additions 2020-05-22 14:31:29 +03:00
Yuri Kuznetsov
838e9ea773 post preview: links in new tab 2020-05-22 13:41:46 +03:00
Yuri Kuznetsov
10efe3513c scheduled job: expression validation 2020-05-22 11:27:46 +03:00
Yuri Kuznetsov
6d301092b2 Merge branch 'hotfix/5.9.2' of https://github.com/espocrm/espocrm into hotfix/5.9.2 2020-05-22 11:13:14 +03:00
Yuri Kuznetsov
5acb5da8fa scheduled job: cron description 2020-05-22 11:12:30 +03:00
Eymen Elkum
3ad343b274 add missing use in settings.php service (#1711) 2020-05-22 09:45:34 +03:00
Yuri Kuznetsov
c5fd749b21 formula while 2020-05-21 14:35:06 +03:00
Yuri Kuznetsov
1f8e0f16c7 formula randomInt function 2020-05-21 13:37:17 +03:00
Yuri Kuznetsov
92babe7fbc fix gruntfile 2020-05-21 13:34:43 +03:00
Yuri Kuznetsov
fb684837f8 role table ui improvement 2020-05-21 11:40:59 +03:00
Yuri Kuznetsov
669413b184 mass email site url param 2020-05-20 16:44:10 +03:00
Yuri Kuznetsov
a945a01767 password request send support group email account smtp 2020-05-20 13:04:41 +03:00
Yuri Kuznetsov
b4664eafa5 fix password recovery disabled if smtpServer is empty 2020-05-20 12:53:07 +03:00
66 changed files with 3242 additions and 2157 deletions

View File

@@ -313,11 +313,11 @@ module.exports = function (grunt) {
});
grunt.registerTask("unit-tests-run", function() {
cp.execSync("vendor/bin/phpunit --bootstrap=vendor/autoload.php tests/unit", {stdio: 'inherit'});
cp.execSync("vendor/bin/phpunit --bootstrap=./vendor/autoload.php tests/unit", {stdio: 'inherit'});
});
grunt.registerTask("integration-tests-run", function() {
cp.execSync("vendor/bin/phpunit --bootstrap=vendor/autoload.php tests/integration", {stdio: 'inherit'});
cp.execSync("vendor/bin/phpunit --bootstrap=./vendor/autoload.php tests/integration", {stdio: 'inherit'});
});
grunt.registerTask("zip", function() {

View File

@@ -31,6 +31,7 @@ namespace Espo\Controllers;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
class EmailAccount extends \Espo\Core\Controllers\Record
{

View File

@@ -69,29 +69,7 @@ class User extends \Espo\Core\Controllers\Record
throw new BadRequest();
}
if ($this->getConfig()->get('passwordRecoveryDisabled')) {
throw new Forbidden("Password recovery disabled");
}
$request = $this->getEntityManager()->getRepository('PasswordChangeRequest')->where([
'requestId' => $data->requestId
])->findOne();
if (!$request) {
throw new Forbidden();
}
$userId = $request->get('userId');
if (!$userId) {
throw new Error();
}
if ($this->getService('User')->changePassword($userId, $data->password)) {
$this->getEntityManager()->removeEntity($request);
return [
'url' => $request->get('url')
];
}
return $this->getService('User')->changePasswordByRequest($data->requestId, $data->password);
}
public function postActionPasswordChangeRequest($params, $data, $request)

View File

@@ -71,11 +71,18 @@ class ClientManager
$externalAccountEntity->set('tokenType', $data['tokenType']);
$externalAccountEntity->set('expiresAt', $data['expiresAt'] ?? null);
if ($data['refreshToken'] ?? null) {
$externalAccountEntity->set('refreshToken', $data['refreshToken']);
}
$copy = $this->getEntityManager()->getEntity('ExternalAccount', $externalAccountEntity->id);
if ($copy) {
$copy->set('accessToken', $data['accessToken']);
$copy->set('tokenType', $data['tokenType']);
$copy->set('expiresAt', $data['expiresAt'] ?? null);
if ($data['refreshToken'] ?? null) {
$copy->set('refreshToken', $data['refreshToken'] ?? null);
}
$this->getEntityManager()->saveEntity($copy, ['isTokenRenewal' => true]);
}
}

View File

@@ -115,6 +115,10 @@ abstract class OAuth2Abstract implements IClient
$data['expiresAt'] = null;
if (isset($result['refresh_token']) && $result['refresh_token'] !== $this->refreshToken) {
$data['refreshToken'] = $result['refresh_token'];
}
if (isset($result['expires_in']) && is_numeric($result['expires_in'])) {
$data['expiresAt'] = (new \DateTime())
->modify('+' . $result['expires_in'] . ' seconds')

View File

@@ -122,4 +122,9 @@ abstract class Base implements Injectable
return $eArgs;
}
}
protected function fetchRawArguments(\StdClass $item)
{
return $item->value ?? [];
}
}

View File

@@ -0,0 +1,47 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula\Functions\NumberGroup;
use Espo\Core\Exceptions\Error;
class RandomIntType extends \Espo\Core\Formula\Functions\Base
{
public function process(\StdClass $item)
{
$args = $this->fetchArguments($item);
$min = $args[0] ?? 0;
$max = $args[1] ?? PHP_INT_MAX;
if (!is_int($min) || !is_int($max) ) throw new Error("Non-integer arguments passed to function number\\randomInt.");
return random_int($min, $max);
}
}

View File

@@ -0,0 +1,48 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula\Functions;
use Espo\Core\Exceptions\Error;
class WhileType extends Base
{
public function process(\StdClass $item)
{
$args = $this->fetchRawArguments($item);
if (count($args) < 2) {
throw new Error("Function \'while\' should receieve 2 arguments.");
}
while ($this->evaluate($args[0])) {
$this->evaluate($args[1]);
}
}
}

View File

@@ -451,6 +451,16 @@ class Htmlizer
return $context['inverse'] ? $context['inverse']() : '';
}
},
'ifMultipleOf' => function () {
$args = func_get_args();
$context = $args[count($args) - 1];
if ($args[0] % $args[1] === 0) {
return $context['fn']();
} else {
return $context['inverse'] ? $context['inverse']() : '';
}
},
'tableTag' => function () {
$args = func_get_args();
$context = $args[count($args) - 1];
@@ -567,11 +577,13 @@ class Htmlizer
$data[$k] = $value;
}
$data['__config'] = $this->config;
$data['__dateTime'] = $this->dateTime;
$data['__metadata'] = $this->metadata;
$data['__entityManager'] = $this->entityManager;
$data['__language'] = $this->language;
$data['__serviceFactory'] = $this->serviceFactory;
$data['__entityType'] = $entity->getEntityType();
$html = $renderer($data);

View File

@@ -181,7 +181,9 @@ class MailMimeParser
if ($bodyHtml) {
$email->set('isHtml', true);
$email->set('body', $bodyHtml);
$email->set('bodyPlain', $bodyPlain);
if ($bodyPlain) {
$email->set('bodyPlain', $bodyPlain);
}
} else {
$email->set('isHtml', false);
$email->set('body', $bodyPlain);

View File

@@ -135,7 +135,7 @@ class Sender
if ($params['auth'] ?? false) {
$authMechanism = $params['authMechanism'] ?? $params['smtpAuthMechanism'] ?? null;
if ($authMechanism) {
$authMechanism = preg_replace("([\.]{2,})", '', $params['authMechanism']);
$authMechanism = preg_replace("([\.]{2,})", '', $authMechanism);
if (in_array($authMechanism, ['login', 'crammd5', 'plain'])) {
$options['connectionClass'] = $authMechanism;
} else {
@@ -199,6 +199,14 @@ class Sender
]);
}
public function hasSystemSmtp()
{
if ($this->config->get('smtpServer')) return true;
if ($this->getSystemInboundEmail()) return true;
return false;
}
protected function getSystemInboundEmail()
{
$address = $this->config->get('outboundEmailFromAddress');

View File

@@ -0,0 +1,301 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Password;
use Espo\Core\Utils\Util;
use Espo\Entities\User;
use Espo\Entities\PasswordChangeRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\Error;
class Recovery implements \Espo\Core\Interfaces\Injectable
{
use \Espo\Core\Traits\Injectable;
const REQUEST_DELAY = 3000; //ms
const REQUEST_LIFETIME = '3 hours';
public function __construct()
{
$this->addDependencyList([
'entityManager',
'config',
'mailSender',
'htmlizerFactory',
'templateFileManager',
]);
}
public function getRequest(string $id) : PasswordChangeRequest
{
$config = $this->getInjection('config');
$em = $this->getInjection('entityManager');
if ($config->get('passwordRecoveryDisabled')) {
throw new Forbidden("Password recovery: Disabled.");
}
$request = $em->getRepository('PasswordChangeRequest')->where([
'requestId' => $id,
])->findOne();
if (!$request) {
throw new NotFound("Password recovery: Request not found by id.");
}
$userId = $request->get('userId');
if (!$userId) {
throw new Error();
}
return $request;
}
public function removeRequest(string $id)
{
$em = $this->getInjection('entityManager');
$request = $em->getRepository('PasswordChangeRequest')->where([
'requestId' => $id,
])->findOne();
if ($request) {
$em->removeEntity($request);
}
}
public function request(string $emailAddress, ?string $userName = null, ?string $url) : bool
{
$config = $this->getInjection('config');
$em = $this->getInjection('entityManager');
$noExposure = $config->get('passwordRecoveryNoExposure') ?? false;
if ($config->get('passwordRecoveryDisabled')) {
throw new Forbidden("Password recovery: Disabled.");
}
$user = $em->getRepository('User')->where([
'userName' => $userName,
'emailAddress' => $emailAddress,
])->findOne();
if (!$user) {
$this->fail("Password recovery: User {$emailAddress} not found.", 404);
return false;
}
if (!$user->isActive()) {
$this->fail("Password recovery: User {$user->id} is not active.");
return false;
}
if ($user->isApi() || $user->isSystem() || $user->isSuperAdmin()) {
$this->fail("Password recovery: User {$user->id} is not allowed.");
return false;
}
if ($config->get('passwordRecoveryForInternalUsersDisabled')) {
if ($user->isRegular() || $user->isAdmin()) {
$this->fail("Password recovery: User {$user->id} is not allowed, disabled for internal users.");
return false;
}
}
if ($config->get('passwordRecoveryForAdminDisabled')) {
if ($user->isAdmin()) {
$this->fail("Password recovery: User {$user->id} is not allowed, disabled for admin users.");
return false;
}
}
if (!$user->isAdmin() && $config->get('authenticationMethod', 'Espo') !== 'Espo') {
$this->fail("Password recovery: User {$user->id} is not allowed, authentication method is not 'Espo'.");
return false;
}
$passwordChangeRequest = $em->getRepository('PasswordChangeRequest')->where([
'userId' => $user->id,
])->findOne();
if ($passwordChangeRequest) {
if (!$noExposure) {
throw new Forbidden(json_encode(['reason' => 'Already-Sent']));
}
$this->fail("Password recovery: Denied for {$user->id}, already sent.");
return false;
}
$requestId = Util::generateCryptId();
$passwordChangeRequest = $em->getEntity('PasswordChangeRequest');
$passwordChangeRequest->set([
'userId' => $user->id,
'requestId' => $requestId,
'url' => $url,
]);
$microtime = microtime(true);
$this->send($requestId, $emailAddress, $user);
$em->saveEntity($passwordChangeRequest);
if (!$passwordChangeRequest->id) throw new Error();
$lifetime = $config->get('passwordRecoveryRequestLifetime') ?? self::REQUEST_LIFETIME;
$dt = new \DateTime();
$dt->modify('+' . $lifetime);
$em->createEntity('Job', [
'serviceName' => 'User',
'methodName' => 'removeChangePasswordRequestJob',
'data' => ['id' => $passwordChangeRequest->id],
'executeTime' => $dt->format('Y-m-d H:i:s'),
'queue' => 'q1',
]);
$timeDiff = $this->getDelay() - floor((microtime(true) - $microtime) / 1000);
if ($noExposure && $timeDiff > 0) {
$this->delay($timeDiff);
}
return true;
}
private function getDelay()
{
return $this->getInjection('config')->get('passwordRecoveryRequestDelay') ?? self::REQUEST_DELAY;
}
protected function delay(?int $delay = null)
{
$delay = $delay ?? $this->getDelay();
usleep($delay * 1000);
}
protected function send(string $requestId, string $emailAddress, User $user)
{
$config = $this->getInjection('config');
$em = $this->getInjection('entityManager');
$mailSender = $this->getInjection('mailSender');
$htmlizerFactory = $this->getInjection('htmlizerFactory');
$templateFileManager = $this->getInjection('templateFileManager');
if (!$emailAddress) return;
$email = $em->getEntity('Email');
if (!$mailSender->hasSystemSmtp() && !$config->get('internalSmtpServer')) {
throw new Error("Password recovery: SMTP credentials are not defined.");
}
$subjectTpl = $templateFileManager->getTemplate('passwordChangeLink', 'subject', 'User');
$bodyTpl = $templateFileManager->getTemplate('passwordChangeLink', 'body', 'User');
$siteUrl = $config->getSiteUrl();
if ($user->isPortal()) {
$portal = $em->getRepository('Portal')->distinct()->join('users')->where([
'isActive' => true,
'users.id' => $user->id,
])->findOne();
if ($portal) {
if ($portal->get('customUrl')) {
$siteUrl = $portal->get('customUrl');
}
}
}
$data = [];
$link = $siteUrl . '?entryPoint=changePassword&id=' . $requestId;
$data['link'] = $link;
$htmlizer = $htmlizerFactory->create(true);
$subject = $htmlizer->render($user, $subjectTpl, null, $data, true);
$body = $htmlizer->render($user, $bodyTpl, null, $data, true);
$email->set([
'subject' => $subject,
'body' => $body,
'to' => $emailAddress,
'isSystem' => true,
]);
if ($mailSender->hasSystemSmtp()) {
$mailSender->useGlobal();
} else {
$mailSender->useSmtp([
'server' => $config->get('internalSmtpServer'),
'port' => $config->get('internalSmtpPort'),
'auth' => $config->get('internalSmtpAuth'),
'username' => $config->get('internalSmtpUsername'),
'password' => $config->get('internalSmtpPassword'),
'security' => $config->get('internalSmtpSecurity'),
'fromAddress' => $config->get('internalOutboundEmailFromAddress', $config->get('outboundEmailFromAddress')),
]);
}
$mailSender->send($email);
}
private function fail(?string $msg = null, int $errorCode = 403)
{
$config = $this->getInjection('config');
$noExposure = $config->get('passwordRecoveryNoExposure') ?? false;
if ($msg) {
$GLOBALS['log']->warning($msg);
}
if (!$noExposure) {
if ($errorCode === 403) {
throw new Forbidden();
} else if ($errorCode === 404) {
throw new NotFound();
} else {
throw new Error();
}
}
$this->delay();
}
}

View File

@@ -0,0 +1,194 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2020 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\TemplateHelpers;
class GoogleMaps
{
public static function image()
{
$args = func_get_args();
$context = $args[count($args) - 1];
$hash = $context['hash'];
$data = $context['data']['root'];
$em = $data['__entityManager'];
$metadata = $data['__metadata'];
$config = $data['__config'];
$entityType = $data['__entityType'];
$field = $hash['field'] ?? null;
$size = $hash['size'] ?? '400x400';
$zoom = $hash['zoom'] ?? null;
$language = $hash['language'] ?? $config->get('language');
if (strpos($size, 'x') === false) {
$size = $size .'x' . $size;
}
if ($field && $metadata->get(['entityDefs', $entityType, 'fields', $field, 'type']) !== 'address') {
$GLOBALS['log']->warning("Template helper _googleMapsImage: Specified field is not of address type.");
return null;
}
if (
!$field &&
!array_key_exists('street', $hash) &&
!array_key_exists('city', $hash) &&
!array_key_exists('country', $hash) &&
!array_key_exists('state', $hash) &&
!array_key_exists('postalCode', $hash)
) {
$field = ($entityType === 'Account') ? 'billingAddress' : 'address';
}
if ($field) {
$street = $data[$field . 'Street'] ?? null;
$city = $data[$field . 'City'] ?? null;
$country = $data[$field . 'Country'] ?? null;
$state = $data[$field . 'State'] ?? null;
$postalCode = $data[$field . 'postalCode'] ?? null;
} else {
$street = $hash['street'] ?? null;
$city = $hash['city'] ?? null;
$country = $hash['country'] ?? null;
$state = $hash['state'] ?? null;
$postalCode = $hash['postalCode'] ?? null;
}
$address = '';
if ($street) {
$address .= $street;
}
if ($city) {
if ($address != '') {
$address .= ', ';
}
$address .= $city;
}
if ($state) {
if ($address != '') {
$address .= ', ';
}
$address .= $state;
}
if ($postalCode) {
if ($state || $city) {
$address .= ' ';
} else {
if ($address) {
$address .= ', ';
}
}
$address .= $postalCode;
}
if ($country) {
if ($address != '') {
$address .= ', ';
}
$address .= $country;
}
$address = urlencode($address);
$apiKey = $config->get('googleMapsApiKey');
if (!$apiKey) {
$GLOBALS['log']->error("Template helper _googleMapsImage: No Google Maps API key.");
return null;
}
if (!$address) {
$GLOBALS['log']->debug("Template helper _googleMapsImage: No address to display.");
return null;
}
$format = 'jpg;';
$url = "https://maps.googleapis.com/maps/api/staticmap?" .
'center=' . $address .
'format=' . $format .
'&size=' . $size .
'&key=' . $apiKey;
if ($zoom) {
$url .= '&zoom=' . $zoom;
}
if ($language) {
$url .= '&language=' . $language;
}
$GLOBALS['log']->debug("Template helper _googleMapsImage: URL: {$url}.");
$image = \Espo\Core\TemplateHelpers\GoogleMaps::getImage($url);
if (!$image) {
return null;
}
$filePath = tempnam(sys_get_temp_dir(), 'google_maps_image');
file_put_contents($filePath, $image);
list($width, $height) = explode('x', $size);
$tag = "<img src=\"{$filePath}\" width=\"{$width}\" height=\"{$height}\">";
return new LightnCandy\SafeString($tag);
}
public static function getImage(string $url)
{
$headers = [];
$headers[] = 'Accept: image/jpeg, image/pjpeg';
$headers[] = 'Connection: Keep-Alive';
$agent = 'Mozilla/5.0';
$c = curl_init();
curl_setopt($c, \CURLOPT_URL, $url);
curl_setopt($c, \CURLOPT_HTTPHEADER, $headers);
curl_setopt($c, \CURLOPT_HEADER, 0);
curl_setopt($c, \CURLOPT_USERAGENT, $agent);
curl_setopt($c, \CURLOPT_TIMEOUT, 10);
curl_setopt($c, \CURLOPT_RETURNTRANSFER, 1);
curl_setopt($c, \CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($c, \CURLOPT_BINARYTRANSFER, 1);
$raw = curl_exec($c);
curl_close($c);
return $raw;
}
}

View File

@@ -156,6 +156,7 @@ return [
'ldapUserObjectClass',
'maxEmailAccountCount',
'massEmailMaxPerHourCount',
'massEmailSiteUrl',
'personalEmailMaxPortionSize',
'inboundEmailMaxPortionSize',
'authTokenLifetime',
@@ -181,7 +182,10 @@ return [
'authTokenPreventConcurrent',
'emailParser',
'passwordRecoveryDisabled',
'passwordRecoveryNoExposure',
'passwordRecoveryForAdminDisabled',
'passwordRecoveryForInternalUsersDisabled',
'passwordRecoveryRequestDelay',
'latestVersion',
],
'superAdminItems' => [

View File

@@ -140,21 +140,21 @@ class Email extends \Espo\Core\ORM\Entity
$body = $this->get('body');
$breaks = array("<br />","<br>","<br/>","<br />","&lt;br /&gt;","&lt;br/&gt;","&lt;br&gt;");
$breaks = ["<br />","<br>","<br/>","<br />","&lt;br /&gt;","&lt;br/&gt;","&lt;br&gt;"];
$body = str_ireplace($breaks, "\r\n", $body);
$body = strip_tags($body);
$reList = [
'/&(quot|#34);/i',
'/&(amp|#38);/i',
'/&(lt|#60);/i',
'/&(gt|#62);/i',
'/&(nbsp|#160);/i',
'/&(iexcl|#161);/i',
'/&(cent|#162);/i',
'/&(pound|#163);/i',
'/&(copy|#169);/i',
'/&(reg|#174);/i'
'&(quot|#34);',
'&(amp|#38);',
'&(lt|#60);',
'&(gt|#62);',
'&(nbsp|#160);',
'&(iexcl|#161);',
'&(cent|#162);',
'&(pound|#163);',
'&(copy|#169);',
'&(reg|#174);',
];
$replaceList = [
'',
@@ -166,10 +166,12 @@ class Email extends \Espo\Core\ORM\Entity
chr(162),
chr(163),
chr(169),
chr(174)
chr(174),
];
$body = preg_replace($reList, $replaceList, $body);
foreach ($reList as $i => $re) {
$body = mb_ereg_replace($re, $replaceList[$i], $body, 'i');
}
return $body;
}

View File

@@ -325,14 +325,14 @@ class MassEmail extends \Espo\Services\Record
$body = $emailData['body'];
$optOutUrl = $this->getConfig()->get('siteUrl') . '?entryPoint=unsubscribe&id=' . $queueItem->id;
$optOutUrl = $this->getSiteUrl() . '?entryPoint=unsubscribe&id=' . $queueItem->id;
$optOutLink = '<a href="'.$optOutUrl.'">'.$this->getLanguage()->translate('Unsubscribe', 'labels', 'Campaign').'</a>';
$body = str_replace('{optOutUrl}', $optOutUrl, $body);
$body = str_replace('{optOutLink}', $optOutLink, $body);
foreach ($trackingUrlList as $trackingUrl) {
$url = $this->getConfig()->get('siteUrl') . '?entryPoint=campaignUrl&id=' . $trackingUrl->id . '&queueItemId=' . $queueItem->id;
$url = $this->getSiteUrl() . '?entryPoint=campaignUrl&id=' . $trackingUrl->id . '&queueItemId=' . $queueItem->id;
$body = str_replace($trackingUrl->get('urlToUse'), $url, $body);
}
@@ -346,7 +346,7 @@ class MassEmail extends \Espo\Services\Record
$trackImageAlt = $this->getLanguage()->translate('Campaign', 'scopeNames');
$trackOpenedUrl = $this->getConfig()->get('siteUrl') . '?entryPoint=campaignTrackOpened&id=' . $queueItem->id;
$trackOpenedUrl = $this->getSiteUrl() . '?entryPoint=campaignTrackOpened&id=' . $queueItem->id;
$trackOpenedHtml = '<img alt="'.$trackImageAlt.'" width="1" height="1" border="0" src="'.$trackOpenedUrl.'">';
if ($massEmail->get('campaignId') && $this->getConfig()->get('massEmailOpenTracking')) {
@@ -387,7 +387,7 @@ class MassEmail extends \Espo\Services\Record
$message->getHeaders()->addHeaderLine('Precedence', 'bulk');
if (!$this->getConfig()->get('massEmailDisableMandatoryOptOutLink')) {
$optOutUrl = $this->getConfig()->getSiteUrl() . '?entryPoint=unsubscribe&id=' . $queueItem->id;
$optOutUrl = $this->getSiteUrl() . '?entryPoint=unsubscribe&id=' . $queueItem->id;
$message->getHeaders()->addHeaderLine('List-Unsubscribe', '<' . $optOutUrl . '>');
}
@@ -589,4 +589,8 @@ class MassEmail extends \Espo\Services\Record
return $dataList;
}
protected function getSiteUrl()
{
return $this->getConfig()->get('massEmailSiteUrl') ?? $this->getConfig()->get('siteUrl');
}
}

View File

@@ -9,6 +9,7 @@
"log": "Log"
},
"labels": {
"As often as possible": "As often as possible",
"Create ScheduledJob": "Create Scheduled Job"
},
"options": {

View File

@@ -126,6 +126,8 @@
"useWebSocket": "Use WebSocket",
"passwordRecoveryDisabled": "Disable password recovery",
"passwordRecoveryForAdminDisabled": "Disable password recovery for admin users",
"passwordRecoveryForInternalUsersDisabled": "Disable password recovery for internal users",
"passwordRecoveryNoExposure": "Prevent email address exposure on password recovery form",
"passwordGenerateLength": "Length of generated passwords",
"passwordStrengthLength": "Minimum password length",
"passwordStrengthLetterCount": "Number of letters required in password",
@@ -160,6 +162,8 @@
}
},
"tooltips": {
"passwordRecoveryForInternalUsersDisabled": "Only portal users will be able to recover password.",
"passwordRecoveryNoExposure": "It won't be possible to determine whether a specific email address is registered in the system.",
"emailAddressLookupEntityTypeList": "For email address autocomplete.",
"emailNotificationsDelay": "A message can be edited within the specified timeframe before the notification is sent.",
"outboundEmailFromAddress": "The system email address.",

View File

@@ -91,6 +91,7 @@
"portals": "Portals which this user has access to."
},
"messages": {
"passwordRecoverySentIfMatched": "Assuming the entered data matched any user account.",
"passwordStrengthLength": "Must be at least {length} characters long.",
"passwordStrengthLetterCount": "Must contain at least {count} letter(s).",
"passwordStrengthNumberCount": "Must contain at least {count} digit(s).",

View File

@@ -19,7 +19,8 @@
[{"name": "passwordGenerateLength"}, false],
[{"name": "passwordStrengthLength"}, {"name": "passwordStrengthLetterCount"}],
[{"name": "passwordStrengthBothCases"}, {"name": "passwordStrengthNumberCount"}],
[{"name": "passwordRecoveryDisabled"}, {"name": "passwordRecoveryForAdminDisabled"}]
[{"name": "passwordRecoveryDisabled"}, {"name": "passwordRecoveryForAdminDisabled"}],
[{"name": "passwordRecoveryNoExposure"}, {"name": "passwordRecoveryForInternalUsersDisabled"}]
]
}
]

View File

@@ -0,0 +1,3 @@
{
"googleMapsImage": "\\Espo\\Core\\TemplateHelpers\\GoogleMaps::image"
}

View File

@@ -4,6 +4,9 @@
"log": {
"readOnly": true,
"view": "views/scheduled-job/record/panels/log",
"createDisabled": true,
"selectDisabled": true,
"viewDisabled": true,
"unlinkDisabled": true
}
},

View File

@@ -175,6 +175,14 @@
"passwordRecoveryForAdminDisabled": {
"type": "bool"
},
"passwordRecoveryForInternalUsersDisabled": {
"type": "bool",
"tooltip": true
},
"passwordRecoveryNoExposure": {
"type": "bool",
"tooltip": true
},
"passwordGenerateLength": {
"type": "int",
"min": 6,

View File

@@ -92,6 +92,7 @@ class EmailAccount extends Record
$entity = $this->getEntityManager()->getEntity('EmailAccount', $params['id']);
if ($entity) {
$params['password'] = $this->getCrypt()->decrypt($entity->get('password'));
$params['imapHandler'] = $entity->get('imapHandler');
}
}
@@ -108,6 +109,13 @@ class EmailAccount extends Record
public function testConnection(array $params)
{
if (!empty($params['id'])) {
$account = $this->getEntityManager()->getEntity('EmailAccount', $params['id']);
if ($account) {
$params['imapHandler'] = $account->get('imapHandler');
}
}
$storage = $this->createStorage($params);
$userId = $params['userId'] ?? null;
@@ -132,7 +140,22 @@ class EmailAccount extends Record
$imapParams = null;
if ($emailAddress && $userId) {
$handlerClassName = $params['imapHandler'] ?? null;
if ($handlerClassName && !empty($params['id'])) {
try {
$handler = $this->getInjection('injectableFactory')->createByClassName($handlerClassName);
} catch (\Throwable $e) {
$GLOBALS['log']->error(
"EmailAccount: Could not create Imap Handler. Error: " . $e->getMessage()
);
}
if (method_exists($handler, 'prepareProtocol')) {
$imapParams = $handler->prepareProtocol($params['id'], $params);
}
}
if ($emailAddress && $userId && !$handlerClassName) {
$emailAddress = strtolower($emailAddress);
$userData = $this->getEntityManager()->getRepository('UserData')->getByUserId($userId);
if ($userData) {
@@ -218,6 +241,9 @@ class EmailAccount extends Record
$params['ssl'] = true;
}
$params['imapHandler'] = $emailAccount->get('imapHandler');
$params['id'] = $emailAccount->id;
$storage = $this->createStorage($params);
return $storage;

View File

@@ -29,7 +29,25 @@
namespace Espo\Services;
use Espo\ORM\Entity;
use Espo\Core\Exceptions\BadRequest;
class ScheduledJob extends Record
{
protected $findLinkedLogCountQueryDisabled = true;
public function processValidation(Entity $entity, $data)
{
parent::processValidation($entity, $data);
$scheduling = $entity->get('scheduling');
try {
$cronExpression = \Cron\CronExpression::factory($scheduling);
$nextDate = $cronExpression->getNextRunDate()->format('Y-m-d H:i:s');
} catch (\Exception $e) {
throw new BadRequest("Not valid scheduling expression.");
}
}
}

View File

@@ -30,7 +30,7 @@
namespace Espo\Services;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\BadRequest;
use Espo\ORM\Entity;
@@ -146,7 +146,7 @@ class Settings extends \Espo\Core\Services\Base
}
if (
($this->getConfig()->get('smtpServer') || $this->getConfig()->get('internalSmtpServer'))
($this->getConfig()->get('outboundEmailFromAddress') || $this->getConfig()->get('internalSmtpServer'))
&&
!$this->getConfig()->get('passwordRecoveryDisabled')
) {

View File

@@ -29,17 +29,15 @@
namespace Espo\Services;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\Error;
use \Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Utils\Util;
use \Espo\ORM\Entity;
use Espo\ORM\Entity;
class User extends Record
{
const PASSWORD_CHANGE_REQUEST_LIFETIME = 360; // minutes
protected function init()
{
parent::init();
@@ -209,83 +207,30 @@ class User extends Record
public function passwordChangeRequest($userName, $emailAddress, $url = null)
{
if ($this->getConfig()->get('passwordRecoveryDisabled')) {
throw new Forbidden("Password recovery disabled");
}
$user = $this->getEntityManager()->getRepository('User')->where([
'userName' => $userName,
'emailAddress' => $emailAddress
])->findOne();
if (empty($user)) {
throw new NotFound();
}
if (!$user->isActive()) {
throw new NotFound();
}
if ($user->isApi()) {
throw new NotFound();
}
if ($this->getConfig()->get('passwordRecoveryForAdminDisabled')) {
if ($user->isAdmin()) {
throw new NotFound();
}
}
$userId = $user->id;
$passwordChangeRequest = $this->getEntityManager()->getRepository('PasswordChangeRequest')->where([
'userId' => $userId
])->findOne();
if ($passwordChangeRequest) {
throw new Forbidden(json_encode(['reason' => 'Already-Sent']));
}
$requestId = Util::generateCryptId();
$passwordChangeRequest = $this->getEntityManager()->getEntity('PasswordChangeRequest');
$passwordChangeRequest->set([
'userId' => $userId,
'requestId' => $requestId,
'url' => $url
]);
if (!$user->isAdmin() && $this->getConfig()->get('authenticationMethod', 'Espo') !== 'Espo') {
throw new Forbidden();
}
$this->sendChangePasswordLink($requestId, $emailAddress, $user);
$this->getEntityManager()->saveEntity($passwordChangeRequest);
if (!$passwordChangeRequest->id) {
throw new Error();
}
$dt = new \DateTime();
$dt->add(new \DateInterval('PT'. self::PASSWORD_CHANGE_REQUEST_LIFETIME . 'M'));
$job = $this->getEntityManager()->getEntity('Job');
$job->set([
'serviceName' => 'User',
'methodName' => 'removeChangePasswordRequestJob',
'data' => [
'id' => $passwordChangeRequest->id
],
'executeTime' => $dt->format('Y-m-d H:i:s'),
'queue' => 'q1'
]);
$this->getEntityManager()->saveEntity($job);
$recovery = $this->getContainer()->get('injectableFactory')->createByClassName('\\Espo\\Core\\Password\\Recovery');
$recovery->request($emailAddress, $userName, $url);
return true;
}
public function changePasswordByRequest(string $requestId, string $password)
{
$recovery = $this->getContainer()->get('injectableFactory')->createByClassName('\\Espo\\Core\\Password\\Recovery');
$request = $recovery->getRequest($requestId);
$userId = $request->get('userId');
if (!$userId) throw new Error();
$this->changePassword($userId, $password);
$recovery->removeRequest($requestId);
return (object) [
'url' => $request->get('url'),
];
}
public function removeChangePasswordRequestJob($data)
{
if (empty($data->id)) {
@@ -439,7 +384,7 @@ class User extends Record
throw new Forbidden("Generate new password: Can't process because user desn't have email address.");
}
if (!$this->getConfig()->get('smtpServer') && !$this->getConfig()->get('internalSmtpServer')) {
if (!$this->getMailSender()->hasSystemSmtp() && !$this->getConfig()->get('internalSmtpServer')) {
throw new Forbidden("Generate new password: Can't process because SMTP is not configured.");
}
@@ -585,7 +530,7 @@ class User extends Record
$email = $this->getEntityManager()->getEntity('Email');
if (!$this->getConfig()->get('smtpServer') && !$this->getConfig()->get('internalSmtpServer')) {
if (!$this->getMailSender()->hasSystemSmtp() && !$this->getConfig()->get('internalSmtpServer')) {
return;
}
@@ -643,7 +588,7 @@ class User extends Record
'to' => $emailAddress
]);
if ($this->getConfig()->get('smtpServer')) {
if ($this->getMailSender()->hasSystemSmtp()) {
$this->getMailSender()->useGlobal();
} else {
$this->getMailSender()->useSmtp(array(
@@ -659,69 +604,7 @@ class User extends Record
$this->getMailSender()->send($email);
}
protected function sendChangePasswordLink($requestId, $emailAddress, Entity $user)
{
if (empty($emailAddress)) {
return;
}
$email = $this->getEntityManager()->getEntity('Email');
if (!$this->getConfig()->get('smtpServer') && !$this->getConfig()->get('internalSmtpServer')) {
throw new Error("SMTP credentials are not defined.");
}
$templateFileManager = $this->getContainer()->get('templateFileManager');
$subjectTpl = $templateFileManager->getTemplate('passwordChangeLink', 'subject', 'User');
$bodyTpl = $templateFileManager->getTemplate('passwordChangeLink', 'body', 'User');
$siteUrl = $this->getConfig()->getSiteUrl();
if ($user->isPortal()) {
$portal = $this->getEntityManager()->getRepository('Portal')->distinct()->join('users')->where([
'isActive' => true,
'users.id' => $user->id,
])->findOne();
if ($portal) {
if ($portal->get('customUrl')) {
$siteUrl = $portal->get('customUrl');
}
}
}
$data = [];
$link = $siteUrl . '?entryPoint=changePassword&id=' . $requestId;
$data['link'] = $link;
$htmlizer = new \Espo\Core\Htmlizer\Htmlizer($this->getFileManager(), $this->getDateTime(), $this->getNumber(), null);
$subject = $htmlizer->render($user, $subjectTpl, null, $data, true);
$body = $htmlizer->render($user, $bodyTpl, null, $data, true);
$email->set([
'subject' => $subject,
'body' => $body,
'to' => $emailAddress,
'isSystem' => true
]);
if ($this->getConfig()->get('smtpServer')) {
$this->getMailSender()->useGlobal();
} else {
$this->getMailSender()->useSmtp([
'server' => $this->getConfig()->get('internalSmtpServer'),
'port' => $this->getConfig()->get('internalSmtpPort'),
'auth' => $this->getConfig()->get('internalSmtpAuth'),
'username' => $this->getConfig()->get('internalSmtpUsername'),
'password' => $this->getConfig()->get('internalSmtpPassword'),
'security' => $this->getConfig()->get('internalSmtpSecurity'),
'fromAddress' => $this->getConfig()->get('internalOutboundEmailFromAddress', $this->getConfig()->get('outboundEmailFromAddress'))
]);
}
$this->getMailSender()->send($email);
}
public function delete($id)
{

File diff suppressed because it is too large Load Diff

1
client/lib/cronstrue-i18n.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -3,20 +3,8 @@
<% _.each(layout, function (panel, columnNumber) { %>
<% hasHiddenPanel = panel.hidden || hasHiddenPanel; %>
<div class="panel panel-<%= panel.style %><% if (panel.name) { %>{{#if hiddenPanels.<%= panel.name %>}} hidden{{/if}}<% } %>"<% if (panel.name) print(' data-name="'+panel.name+'"') %>>
<%
var panelLabelString = null;
if ('customLabel' in panel) {
if (panel.customLabel) {
panelLabelString = panel.customLabel;
}
} else {
if (panel.label) {
panelLabelString = "{{translate \"" + panel.label + "\" scope=\""+model.name+"\"}}";
}
}
%>
<% if (panelLabelString) { %>
<div class="panel-heading"><h4 class="panel-title"><%= panelLabelString %></h4></div>
<% if (panel.label) { %>
<div class="panel-heading"><h4 class="panel-title"><%= panel.label %></h4></div>
<% } %>
<div class="panel-body panel-body-form">

View File

@@ -1,5 +1,5 @@
<header data-name="{{name}}">
<label data-is-custom="{{#if isCustomLabel}}true{{/if}}">{{label}}</label>&nbsp;
<label data-is-custom="{{#if isCustomLabel}}true{{/if}}" data-label="{{label}}">{{labelTranslated}}</label>&nbsp;
<a href="javascript:" data-action="edit-panel-label" class="edit-panel-label"><i class="fas fa-pencil-alt fa-sm"></i></a>
<a href="javascript:" style="float: right;" data-action="removePanel" class="remove-panel" data-number="{{number}}"><i class="fas fa-times"></i></a>
</header>

View File

@@ -5,7 +5,7 @@
</div>
<div class="panel-body">
<div class="no-margin">
<table class="table table-bordered no-margin">
<table class="table table-bordered no-margin scope-level">
<tr>
<th></th>
<th width="20%">{{translate 'Access' scope='Role'}}</th>
@@ -25,7 +25,12 @@
{{#each ../list}}
<td>
{{#if levelList}}
<select name="{{name}}" class="form-control{{#ifNotEqual ../../../access 'enabled'}} hidden{{/ifNotEqual}}" data-scope="{{../../name}}"{{#ifNotEqual ../../access 'enabled'}} disabled{{/ifNotEqual}} title="{{translate action scope='Role' category='actions'}}">
<select name="{{name}}"
class="form-control scope-action{{#ifNotEqual ../../../access 'enabled'}} hidden{{/ifNotEqual}}"
data-scope="{{../../name}}"
{{#ifNotEqual ../../access 'enabled'}} disabled{{/ifNotEqual}}
title="{{translate action scope='Role' category='actions'}}"
data-role-action="{{action}}">
{{options levelList level field='levelList' scope='Role'}}
</select>
{{/if}}
@@ -35,6 +40,18 @@
</tr>
{{/each}}
</table>
<div class="sticky-header-scope hidden sticky-head">
<table class="table borderless no-margin">
<tr>
<th></th>
<th width="20%">{{translate 'Access' scope='Role'}}</th>
{{#each actionList}}
<th width="11%">{{translate this scope='Role' category='actions'}}</th>
{{/each}}
</tr>
</table>
</div>
</div>
</div>
</div>
@@ -46,7 +63,7 @@
</div>
<div class="panel-body">
<div class="no-margin">
<table class="table table-bordered no-margin">
<table class="table table-bordered no-margin field-level">
<tr>
<th></th>
<th width="20%"></th>
@@ -67,7 +84,13 @@
<td><b>{{translate name category='fields' scope=../name}}</b></td>
{{#each list}}
<td>
<select name="field-{{../../name}}-{{../name}}" class="form-control" data-field="{{../name}}" data-scope="{{../../name}}" data-action="{{name}}" title="{{translate name scope='Role' category='actions'}}">{{options ../../../fieldLevelList value scope='Role' field='accessList'}}</select>
<select
name="field-{{../../name}}-{{../name}}"
class="form-control field-action"
data-field="{{../name}}"
data-scope="{{../../name}}"
data-action="{{name}}"
title="{{translate name scope='Role' category='actions'}}">{{options ../../../fieldLevelList value scope='Role' field='accessList'}}</select>
</td>
{{/each}}
<td colspan="2">
@@ -77,6 +100,19 @@
{{/each}}
{{/each}}
</table>
<div class="sticky-header-field hidden sticky-head">
<table class="table borderless no-margin">
<tr>
<th></th>
<th width="20%"></th>
{{#each fieldActionList}}
<th width="11%">{{translate this scope='Role' category='actions'}}</th>
{{/each}}
<th width="33%"></th>
</tr>
</table>
</div>
</div>
</div>
</div>

View File

@@ -25,7 +25,10 @@
{{#each ../list}}
<td>
{{#ifNotEqual ../../access 'not-set'}}
<span style="color: {{prop ../../../../colors level}};">{{translateOption level field='levelList' scope='Role'}}</span>
<span
style="color: {{prop ../../../../colors level}};"
title="{{translate action scope='Role' category='actions'}}"
>{{translateOption level field='levelList' scope='Role'}}</span>
{{/ifNotEqual}}
</td>
{{/each}}
@@ -68,7 +71,9 @@
<td><b>{{translate name category='fields' scope=../name}}</b></td>
{{#each list}}
<td>
<span style="color: {{prop ../../../colors value}};">{{translateOption value scope='Role' field='accessList'}}</span>
<span
title="{{translate name scope='Role' category='actions'}}"
style="color: {{prop ../../../colors value}};">{{translateOption value scope='Role' field='accessList'}}</span>
</td>
{{/each}}
<td colspan="3"></td>

View File

@@ -1,5 +1,5 @@
<div class="container content">
<div class="col-md-4 col-md-offset-4 col-sm-8 col-sm-offset-2">
<div class="col-md-4 col-md-offset-3 col-sm-8 col-sm-offset-2">
<div class="panel panel-default password-change">
<div class="panel-heading">
<h4 class="panel-title">{{translate 'Change Password' scope='User'}}</h4>

View File

@@ -184,7 +184,7 @@
},
getEntityTypeFieldParam: function (entityType, field, param) {
this.metadata.get(['entityDefs', entityType, 'fields', field, param]);
return this.metadata.get(['entityDefs', entityType, 'fields', field, param]);
},
getViewName: function (fieldType) {

View File

@@ -55,6 +55,11 @@ define('views/admin/authentication', 'views/settings/record/edit', function (Dep
this.listenTo(this.model, 'change:auth2FA', function () {
this.manage2FAFields();
}, this);
this.managePasswordRecoveryFields();
this.listenTo(this.model, 'change:passwordRecoveryDisabled', function () {
this.managePasswordRecoveryFields();
}, this);
},
setupBeforeFinal: function () {
@@ -126,5 +131,17 @@ define('views/admin/authentication', 'views/settings/record/edit', function (Dep
}
},
managePasswordRecoveryFields: function () {
if (!this.model.get('passwordRecoveryDisabled')) {
this.showField('passwordRecoveryForAdminDisabled');
this.showField('passwordRecoveryForInternalUsersDisabled');
this.showField('passwordRecoveryNoExposure');
} else {
this.hideField('passwordRecoveryForAdminDisabled');
this.hideField('passwordRecoveryForInternalUsersDisabled');
this.hideField('passwordRecoveryNoExposure');
}
},
});
});

View File

@@ -297,10 +297,10 @@ define('views/admin/layouts/grid', ['views/admin/layouts/base', 'res!client/css/
data.isCustomLabel = false;
if (data.customLabel) {
data.label = data.customLabel;
data.labelTranslated = data.customLabel;
data.isCustomLabel = true;
} else {
data.label = this.translate(data.label, 'labels', this.scope);
data.labelTranslated = this.translate(data.label, 'labels', this.scope);
}
data.style = data.style || null;
@@ -437,7 +437,7 @@ define('views/admin/layouts/grid', ['views/admin/layouts/base', 'res!client/css/
if ($label.attr('data-is-custom')) {
o.customLabel = $label.text();
} else {
o.label = $label.text();
o.label = $label.data('label');
}
$(el).find('ul.rows > li').each(function (i, li) {
var row = [];

View File

@@ -255,6 +255,9 @@ define('views/detail', 'views/main', function (Dep) {
view.render();
view.notify(false);
this.listenToOnce(view, 'after:save', function () {
if (data.fromSelectRelated) {
this.clearView('dialogSelectRelated');
}
this.updateRelationshipPanel(link);
this.model.trigger('after:relate');
}, this);
@@ -329,18 +332,27 @@ define('views/detail', 'views/main', function (Dep) {
var viewName = this.getMetadata().get('clientDefs.' + scope + '.modalViews.select') || 'views/modals/select-records';
this.notify('Loading...');
this.createView('dialog', viewName, {
this.createView('dialogSelectRelated', viewName, {
scope: scope,
multiple: true,
createButton: false,
createButton: data.createButton || false,
triggerCreateEvent: true,
filters: filters,
massRelateEnabled: massRelateEnabled,
primaryFilterName: primaryFilterName,
boolFilterList: boolFilterList
boolFilterList: boolFilterList,
}, function (dialog) {
dialog.render();
this.notify(false);
dialog.once('select', function (selectObj) {
Espo.Ui.notify(false);
this.listenTo(dialog, 'create', function () {
this.actionCreateRelated({
link: data.link,
fromSelectRelated: true,
});
}, this);
this.listenToOnce(dialog, 'select', function (selectObj) {
var data = {};
if (Object.prototype.toString.call(selectObj) === '[object Array]') {
var ids = [];
@@ -356,21 +368,21 @@ define('views/detail', 'views/main', function (Dep) {
data.id = selectObj.id;
}
}
$.ajax({
url: this.scope + '/' + this.model.id + '/' + link,
type: 'POST',
data: JSON.stringify(data),
success: function () {
Espo.Ajax.postRequest(this.scope + '/' + this.model.id + '/' + link, data)
.then(
function () {
this.notify('Linked', 'success');
this.updateRelationshipPanel(link);
this.model.trigger('after:relate');
}.bind(this),
error: function () {
}.bind(this)
)
.fail(
function () {
this.notify('Error occurred', 'error');
}.bind(this)
});
}.bind(this));
}.bind(this));
);
}, this);
});
},
actionDuplicate: function () {

View File

@@ -168,6 +168,9 @@ define('views/email/detail', ['views/detail', 'email-helper'], function (Dep, Em
}, function (view) {
view.render();
view.notify(false);
this.listenTo(view, 'before:save', function () {
this.getView('record').blockUpdateWebSocket(true);
}, this);
this.listenToOnce(view, 'after:save', function () {
this.model.fetch();
this.removeMenuItem('createContact');
@@ -227,6 +230,9 @@ define('views/email/detail', ['views/detail', 'email-helper'], function (Dep, Em
this.removeMenuItem('createCase');
view.close();
}, this);
this.listenTo(view, 'before:save', function () {
this.getView('record').blockUpdateWebSocket(true);
}, this);
});
}.bind(this));
},
@@ -306,6 +312,9 @@ define('views/email/detail', ['views/detail', 'email-helper'], function (Dep, Em
this.removeMenuItem('createLead');
view.close();
}.bind(this));
this.listenTo(view, 'before:save', function () {
this.getView('record').blockUpdateWebSocket(true);
}, this);
}.bind(this));
},

View File

@@ -359,20 +359,19 @@ Espo.define('views/fields/base', 'view', function (Dep) {
}
if (this.mode != 'search') {
this.attributeList = this.getAttributeList();
this.attributeList = this.getAttributeList(); // for backward compatibility, to be removed
this.listenTo(this.model, 'change', function (model, options) {
if (this.isRendered() || this.isBeingRendered()) {
if (options.ui) {
return;
}
var changed = false;
this.attributeList.forEach(function (attribute) {
this.getAttributeList().forEach(function (attribute) {
if (model.hasChanged(attribute)) {
changed = true;
}
});
}, this);
if (changed && !options.skipReRender) {
this.reRender();

View File

@@ -41,14 +41,17 @@ define('views/fields/complex-created', 'views/fields/base', function (Dep) {
return [this.fieldAt, this.fieldBy];
},
setup: function () {
Dep.prototype.setup.call(this);
init: function () {
this.baseName = this.options.baseName || this.baseName;
this.fieldAt = this.baseName + 'At';
this.fieldBy = this.baseName + 'By';
Dep.prototype.init.call(this);
},
setup: function () {
Dep.prototype.setup.call(this);
this.createField('at');
this.createField('by');
},

View File

@@ -161,7 +161,7 @@ define('views/fields/date', 'views/fields/base', function (Dep) {
weekStart: this.getDateTime().weekStart,
autoclose: true,
todayHighlight: true,
keyboardNavigation: false,
keyboardNavigation: true,
todayBtn: this.getConfig().get('datepickerTodayButton') || false,
};
@@ -181,15 +181,11 @@ define('views/fields/date', 'views/fields/base', function (Dep) {
options.language = language;
var $datePicker = this.$element.datepicker(options).on('show', function (e) {
$('body > .datepicker.datepicker-dropdown').css('z-index', 1200);
}.bind(this));
var $datePicker = this.$element.datepicker(options);
if (this.mode == 'search') {
var $elAdd = this.$el.find('input.additional');
$elAdd.datepicker(options).on('show', function (e) {
$('body > .datepicker.datepicker-dropdown').css('z-index', 1200);
}.bind(this));
$elAdd.datepicker(options);
$elAdd.parent().find('button.date-picker-btn').on('click', function (e) {
$elAdd.datepicker('show');
});

View File

@@ -51,7 +51,7 @@ define('views/fields/person-name', 'views/fields/varchar', function (Dep) {
data.middleValue = this.model.get(this.middleField);
data.salutationOptions = this.model.getFieldParam(this.salutationField, 'options');
if (this.model === 'edit') {
if (this.mode === 'edit') {
data.firstMaxLength = this.model.getFieldParam(this.firstField, 'maxLength');
data.lastMaxLength = this.model.getFieldParam(this.lastField, 'maxLength');
data.middleMaxLength = this.model.getFieldParam(this.middleField, 'maxLength');

View File

@@ -169,6 +169,10 @@ Espo.define('views/modals/edit', 'views/modal', function (Dep) {
this.dialog.close();
}, this);
editView.once('before:save', function () {
this.trigger('before:save', model);
}, this);
var $buttons = this.dialog.$el.find('.modal-footer button');
$buttons.addClass('disabled').attr('disabled', 'disabled');

View File

@@ -144,6 +144,8 @@ define('views/modals/password-change-request', 'views/modal', function (Dep) {
var msg = this.translate('uniqueLinkHasBeenSent', 'messages', 'User');
msg += ' ' + this.translate('passwordRecoverySentIfMatched', 'messages', 'User');
this.$el.find('.cell-userName').addClass('hidden');
this.$el.find('.cell-emailAddress').addClass('hidden');

View File

@@ -57,7 +57,7 @@ define('views/modals/select-records', ['views/modal', 'search-manager'], functio
events: {
'click button[data-action="create"]': function () {
this.create();
this.create();
},
'click .list a': function (e) {
e.preventDefault();
@@ -256,6 +256,11 @@ define('views/modals/select-records', ['views/modal', 'search-manager'], functio
},
create: function () {
if (this.options.triggerCreateEvent) {
this.trigger('create');
return;
}
var self = this;
this.notify('Loading...');
@@ -285,4 +290,3 @@ define('views/modals/select-records', ['views/modal', 'search-manager'], functio
},
});
});

View File

@@ -1487,10 +1487,19 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
for (var p in simplifiedLayout) {
var panel = {};
panel.label = simplifiedLayout[p].label || null;
if ('customLabel' in simplifiedLayout[p]) {
panel.customLabel = simplifiedLayout[p].customLabel;
panel.label = simplifiedLayout[p].customLabel;
if (panel.label) {
panel.label = this.getLanguage().translate(panel.label, 'panelCustomLabels', this.entityType);
}
} else {
panel.label = simplifiedLayout[p].label || null;
if (panel.label) {
panel.label = this.getLanguage().translate(panel.label, 'labels', this.entityType);
}
}
panel.name = simplifiedLayout[p].name || null;
panel.style = simplifiedLayout[p].style || 'default';
panel.rows = [];
@@ -1837,8 +1846,14 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
}
},
blockUpdateWebSocket: function () {
blockUpdateWebSocket: function (toUnblock) {
this.updateWebSocketIsBlocked = true;
if (toUnblock) {
setTimeout(function () {
this.unblockUpdateWebSocket();
}.bind(this), this.blockUpdateWebSocketPeriod || 500);
}
},
unblockUpdateWebSocket: function () {

View File

@@ -92,6 +92,8 @@ define('views/record/panels/relationship', ['views/record/panels/bottom', 'searc
this.defs.view = false;
}
var hasCreate = false;
if (this.defs.create) {
if (this.getAcl().check(this.scope, 'create') && !~this.noCreateScopeList.indexOf(this.scope)) {
this.buttonList.push({
@@ -104,6 +106,7 @@ define('views/record/panels/relationship', ['views/record/panels/bottom', 'searc
link: this.link,
}
});
hasCreate = true;
}
}
@@ -116,6 +119,7 @@ define('views/record/panels/relationship', ['views/record/panels/bottom', 'searc
data.boolFilterList = this.defs.selectBoolFilterList;
}
data.massSelect = this.defs.massSelect;
data.createButton = hasCreate;
this.actionList.unshift({
label: 'Select',

View File

@@ -58,6 +58,10 @@ define('views/role/record/table', 'view', function (Dep) {
booleanActionList: ['create'],
defaultLevels: {
delete: 'no',
},
colors: {
yes: '#6BC924',
all: '#6BC924',
@@ -121,15 +125,37 @@ define('views/role/record/table', 'view', function (Dep) {
if (this.lowestLevelByDefault) {
$select.find('option').last().prop('selected', true);
} else {
$select.find('option').first().prop('selected', true);
var setFirst = true;
var action = $select.data('role-action');
var defaultLevel = null;
if (action) defaultLevel = this.defaultLevels[action];
if (defaultLevel) {
var $option = $select.find('option[value="'+defaultLevel+'"]');
if ($option.length) {
$option.prop('selected', true);
setFirst = false;
}
}
if (setFirst) {
$select.find('option').first().prop('selected', true);
}
}
$select.trigger('change');
this.controlSelectColor($select);
}.bind(this));
} else {
$dropdowns.attr('disabled', 'disabled');
$dropdowns.addClass('hidden');
}
}
this.controlSelectColor($(e.currentTarget));
},
'change select.scope-action': function (e) {
this.controlSelectColor($(e.currentTarget));
},
'change select.field-action': function (e) {
this.controlSelectColor($(e.currentTarget));
},
},
getTableDataList: function () {
@@ -250,6 +276,13 @@ define('views/role/record/table', 'view', function (Dep) {
if (this.mode == 'edit') {
this.template = 'role/table-edit';
}
this.once('remove', function () {
$(window).off('scroll.scope-' + this.cid);
$(window).off('resize.scope-' + this.cid);
$(window).off('scroll.field-' + this.cid);
$(window).off('resize.field-' + this.cid);
}, this);
},
setupData: function () {
@@ -427,6 +460,11 @@ define('views/role/record/table', 'view', function (Dep) {
}, this);
}, this);
this.initStickyHeader('scope');
this.initStickyHeader('field');
this.setSelectColors();
}
},
@@ -447,6 +485,8 @@ define('views/role/record/table', 'view', function (Dep) {
$o.removeAttr('disabled');
}
}.bind(this));
this.controlSelectColor($edit);
},
controlEditSelect: function (scope, value, dontChange) {
@@ -466,6 +506,8 @@ define('views/role/record/table', 'view', function (Dep) {
$o.removeAttr('disabled');
}
}.bind(this));
this.controlSelectColor($edit);
},
controlStreamSelect: function (scope, value, dontChange) {
@@ -485,6 +527,8 @@ define('views/role/record/table', 'view', function (Dep) {
$o.removeAttr('disabled');
}
}.bind(this));
this.controlSelectColor($stream);
},
controlDeleteSelect: function (scope, value, dontChange) {
@@ -504,6 +548,8 @@ define('views/role/record/table', 'view', function (Dep) {
$o.removeAttr('disabled');
}
}.bind(this));
this.controlSelectColor($delete);
},
showAddFieldModal: function (scope) {
@@ -568,5 +614,97 @@ define('views/role/record/table', 'view', function (Dep) {
}, this);
},
initStickyHeader: function (type) {
var $sticky = this.$el.find('.sticky-header-' + type);
var $window = $(window);
var screenWidthXs = this.getThemeManager().getParam('screenWidthXs');
var $buttonContainer = $('.detail-button-container');
var $table = this.$el.find('table.'+type+'-level');
if (!$buttonContainer.length) return;
var handle = function (e) {
if ($(window.document).width() < screenWidthXs) {
$sticky.addClass('hidden');
return;
}
var stickTopPosition = $buttonContainer.get(0).getBoundingClientRect().top + $buttonContainer.outerHeight();
var topEdge = $table.position().top;
topEdge += $buttonContainer.height();
topEdge += $table.find('tr > th').height();
var bottomEdge = topEdge + $table.outerHeight(true);
var scrollTop = $window.scrollTop();
var width = $table.width();
if (scrollTop > topEdge && scrollTop < bottomEdge) {
$sticky.css({
position: 'fixed',
marginTop: stickTopPosition + 'px',
top: 0,
width: width + 'px',
marginLeft: '1px',
});
$sticky.removeClass('hidden');
} else {
$sticky.addClass('hidden');
}
}.bind(this);
$window.off('scroll.' + type + '-' + this.cid);
$window.on('scroll.' + type + '-' + this.cid, handle);
$window.off('resize.' + type + '-' + this.cid);
$window.on('resize.' + type + '-' + this.cid, handle);
},
setSelectColors: function () {
this.$el.find('select[data-type="access"]').each(function (i, el) {
var $select = $(el);
this.controlSelectColor($select);
}.bind(this));
this.$el.find('select.scope-action').each(function (i, el) {
var $select = $(el);
this.controlSelectColor($select);
}.bind(this));
this.$el.find('select.field-action').each(function (i, el) {
var $select = $(el);
this.controlSelectColor($select);
}.bind(this));
},
controlSelectColor: function ($select) {
var level = $select.val();
var color = this.colors[level] || '';
if (level === 'not-set') color = '';
$select.css('color', color);
$select.children().each(function (j, el) {
var $o = $(el);
var level = $o.val();
var color = this.colors[level] || '';
if (level === 'not-set') color = '';
if ($o.attr('disabled')) {
color = '';
}
$o.css('color', color);
}.bind(this));
},
});
});

View File

@@ -25,14 +25,71 @@
* 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/scheduled-job/fields/scheduling', 'views/fields/base', function (Dep) {
define('views/scheduled-job/fields/scheduling',
['views/fields/varchar', 'lib!client/lib/cronstrue-i18n.min.js'], function (Dep) {
return Dep.extend({
forceTrim: true,
setup: function () {
Dep.prototype.setup.call(this);
}
if (this.isEditMode() || this.isDetailMode()) {
this.listenTo(this.model, 'change:' + this.name, function () {
this.showText();
}, this);
}
},
afterRender: function () {
Dep.prototype.afterRender.call(this);
if (this.isEditMode() || this.isDetailMode()) {
var $text = this.$text = $('<div class="small text-success"/>');
this.$el.append($text);
this.showText();
}
},
showText: function () {
if (!this.$text || !this.$text.length) return;
var exp = this.model.get(this.name);
if (!exp) {
this.$text.text('');
return;
}
if (exp == '* * * * *') {
this.$text.text(this.translate('As often as possible', 'labels', 'ScheduledJob'));
return;
}
var locale = 'en';
var localeList = Object.keys(cronstrue.default.locales);
var language = this.getLanguage().name;
if (~localeList.indexOf(language)) {
locale = language;
} else if (~localeList.indexOf(language.split('_')[0])) {
locale = language.split('_')[0];
}
try {
var text = cronstrue.toString(exp, {
use24HourTimeFormat: !this.getDateTime().hasMeridian(),
locale: locale,
});
} catch (e) {
text = this.translate('Not valid');
}
this.$text.text(text);
},
});
});

View File

@@ -334,7 +334,7 @@ define('views/site/navbar', 'view', function (Dep) {
$navbar.css('overflow', 'visible');
}
var navbarBaseWidth = this.getThemeManager().getParam('navbarBaseWidth') || 556;
var navbarBaseWidth = this.getThemeManager().getParam('navbarBaseWidth') || 565;
var tabCount = this.tabList.length;

View File

@@ -610,7 +610,7 @@ define('views/stream/panel', ['views/record/panels/relationship', 'lib!Textcompl
preview: function () {
this.createView('dialog', 'views/modal', {
templateContent: '<div class="complex-text">{{complexText viewObject.options.text}}</div>',
templateContent: '<div class="complex-text">{{complexText viewObject.options.text linksInNewTab=true}}</div>',
text: this.$textarea.val(),
headerText: this.translate('Preview'),
backdrop: true,

File diff suppressed because it is too large Load Diff

View File

@@ -112,3 +112,56 @@
color: @gray-light;
}
.btn-text {
color: @gray-dark;
border-radius: 0;
.far,
.fas {
color: @gray-soft;
}
&,
&:active,
&.active,
&[disabled],
fieldset[disabled] & {
background-color: transparent;
.box-shadow(none);
}
&,
&:hover,
&:focus,
&:active {
border-color: transparent;
}
&:hover,
&:focus {
color: @gray-dark;
text-decoration: none;
background-color: transparent;
.far,
.fas {
color: @gray-dark;
}
}
&.disabled,
&[disabled],
fieldset[disabled] & {
&:hover,
&:focus {
color: @gray-light;
text-decoration: none;
}
color: @gray-light;
.far,
.fas {
color: @gray-light;
}
}
&.active {
border-bottom: 1px solid @gray-light;
}
font-weight: 400;
}

View File

@@ -0,0 +1,178 @@
.modal-header.fixed-height {
height: 40px;
max-height: 40px;
overflow: hidden;
.modal-title {
width: ~"calc(100% - 20px)";
> .modal-title-text {
max-width: 100%;
overflow: hidden;
white-space: nowrap;
display: inline-block;
}
&.overlapped {
> .modal-title-text {
&:before {
content: " ";
position: absolute;
right: 34px;
background: linear-gradient(to right, transparent, @modal-header-bg);
height: 30px;
width: 60px;
}
}
}
}
}
.modal-header a {
color: @text-color;
}
.modal-container.overlaid .modal-content {
.box-shadow(none);
}
.modal-header {
background-color: @modal-header-bg;
}
.modal-header {
padding: @modal-title-padding @panel-padding;
border-bottom: 0 solid @modal-header-border-color;
min-height: (@modal-title-padding + @modal-title-line-height);
}
.modal-header .close {
font-size: 31px;
font-weight: normal;
margin-top: -4px;
max-height: @font-size-h3;
> span {
position: relative;
max-height: @font-size-h3;
height: @font-size-h3;
display: inline-block;
}
opacity: 0.4;
}
.modal-footer {
margin-top: @modal-title-padding;
padding: @panel-padding @panel-padding @panel-padding;
}
.modal-footer > .main-btn-group {
float: right;
}
.modal-footer > .additional-btn-group {
float: left;
}
.modal-dialog > .modal-content {
background-clip: border-box;
border-color: @default-border-color;
border-width: 0;
}
.modal-footer {
margin-top: 0;
}
.modal-header {
padding-top: 8px;
padding-bottom: 8px;
}
.modal-footer > .main-btn-group {
float: left;
}
.modal-footer > .additional-btn-group {
float: right;
}
.dialog-confirm .modal-footer > .main-btn-group {
float: right;
}
.dialog-confirm .modal-footer > .additional-btn-group {
float: left;
}
.modal-footer {
padding-top: @modal-title-padding;
padding-bottom: @modal-title-padding;
}
@media (min-width: @screen-sm-min) {
.modal-dialog {
margin: 0 0 0 auto;
}
.modal-content .note-editor > .modal > .modal-dialog {
margin: 0 0 0 -@container-padding;
}
.dialog-confirm > .modal-dialog {
margin: 0 auto;
}
}
@media (max-width: @screen-sm-min) {
.modal-dialog {
margin: 0 0 0 0;
}
}
.modal-dialog .modal-body .list-buttons-container .sticked-bar {
top: 94px !important;
width: 100% !important;
right: 0 !important;
left: unset;
padding-top: 0;
padding-left: @container-padding - 1px;
}
@media screen and (max-width: (@screen-sm-min - 1px)) {
.modal-dialog {
margin: 0 0;
}
.modal-dialog .image-container {
margin-left: -@panel-padding;
margin-right: -@panel-padding;
}
}
.dialog-confirm > .modal-dialog > .modal-content {
text-align: center;
}
@media screen and (min-width: @screen-sm-min) {
.modal-dialog {
width: 740px;
}
}
@media screen and (min-width: @screen-md-min) {
.modal-dialog,
.dialog-centered {
width: 900px;
}
.dialog-confirm > .modal-dialog,
.dialog-centered > .modal-dialog {
width: 550px;
}
}
.modal-body > .no-side-margin {
margin-left: -@panel-padding;
margin-right: -@panel-padding;
}
.modal.in .modal.in {
overflow: visible !important;
overflow-y: visible !important;
}

View File

@@ -0,0 +1,191 @@
#navbar .navbar {
border-bottom-width: 0 !important;
img.logo {
max-height: 100%;
}
.navbar-brand {
padding: 0;
}
.navbar-right {
margin-right: -2px !important;
> li > a {
text-align: center;
}
}
.navbar-form.global-search-container {
padding-left: 0;
padding-right: 0;
}
.navbar-toggle {
float: left;
text-align: center;
width: 38px;
margin: 0;
color: @navbar-inverse-link-color;
}
.tab span.color-icon {
left: 0;
font-size: 75%;
top: -2px;
}
@media screen and (min-width: @screen-sm-min) {
.tabs > .more {
li.show-more a {
text-align: center;
}
.after-show-more {
display: none;
}
.more-expanded {
.show-more {
display: none;
}
.after-show-more {
display: list-item;
}
}
}
}
@media screen and (max-width: (@screen-sm-min - 1px)) {
li.show-more {
display: none;
}
}
}
@media screen and (min-width: @screen-sm-min) {
#navbar .navbar {
.navbar-body {
-webkit-overflow-scrolling: unset;
}
}
}
@media screen and (max-width: (@screen-sm-min - 1px)) {
#navbar .navbar {
min-height: @navbar-height-xsmall;
.navbar-form.global-search-container {
padding-left: 8px;
padding-right: 8px;
}
.navbar-form.global-search-container > .input-group {
margin-left: 14px;
}
.navbar-right {
> li {
display: inline-block;
}
> li.notifications-badge-container {
display: inline-block;
float: right;
}
}
.notifications-badge-container > .notifications-button > .number-badge {
left: unset;
right: 4px;
}
.navbar-body {
overflow-x: hidden !important;
}
.navbar-brand {
height: @navbar-height-xsmall;
> img.logo {
height: @navbar-logo-height-xsmall;
}
}
ul.tabs {
li > a {
padding-top: 9px;
padding-bottom: 9px;
}
> li.more {
&.open {
> ul {
li > a {
padding-top: 9px;
padding-bottom: 9px;
padding-left: 15px;
padding-right: 15px;
}
}
}
}
}
@supports (display: grid) {
ul.tabs {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
&::before {
content: none;
display: none;
}
&::after {
content: none;
display: none;
}
> li.more {
grid-column: ~"1 / -1";
&.open {
> ul {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
}
}
}
}
.navbar-toggle {
padding-top: 9px;
padding-bottom: 9px;
border: 0;
}
}
}
@media screen and (max-width: (@screen-sm-min - 1px)) {
#navbar .navbar {
.navbar-body:not(.in) {
border-top-width: 0;
box-shadow: none;
height: 0;
overflow: hidden;
}
.navbar-right-container {
li.notifications-badge-container {
position: absolute;
top: -1px;
right: 0;
}
}
}
}

View File

@@ -0,0 +1,279 @@
.panel-body {
padding: @panel-padding;
.clearfix();
}
.panel-group .panel + .panel {
margin-top: 4px;
}
.panel > .panel-body.panel-body-form {
padding-bottom: @panel-padding - @form-group-margin-bottom;
}
.panel-body > .no-margin {
margin: -@panel-padding;
}
.panel-body > .no-side-margin .revert-margin {
margin-left: @panel-padding;
margin-right: @panel-padding;
}
.panel-body > .no-margin .revert-margin {
margin: @panel-padding;
}
.panel-body.no-padding {
padding-left: 0 !important;
padding-top: 0 !important;
padding-right: 0 !important;
padding-bottom: 0 !important;
}
.panel-body .no-side-margin {
margin-left: -@panel-padding;
margin-right: -@panel-padding;
> table td:first-child,
> table th:first-child {
padding-left: @panel-padding;
}
> table td:last-child,
> table th:last-child {
padding-right: @panel-padding;
}
> .list-group-item {
border-left-width: 0;
border-right-width: 0;
}
> .table-bordered {
border-left: 0;
border-right: 0;
td:last-child,
th:last-child {
border-right: 0;
}
td:first-child,
th:first-child {
border-left: 0;
}
}
}
.panel-body .no-bottom-margin {
margin-bottom: -@panel-padding !important;
}
.panel {
> .panel-heading {
.btn {
background-color: @panel-default-heading-bg;
border-color: @panel-default-heading-bg;
color: @gray-soft;
}
.open .btn {
border-color: transparent;
}
.btn,
.btn-default {
&:active,
&.active {
.box-shadow(none);
}
}
.btn-group .dropdown-toggle,
.btn-group.open .dropdown-toggle
{
&,
&:active {
.box-shadow(none);
}
}
> .link-group {
> a {
padding-left: 20px;
}
float: right;
}
> .btn-group > .btn + .dropdown-toggle {
padding-left: @padding-small-horizontal;
padding-right: @padding-small-horizontal;
}
}
}
.panel-heading > .btn-group {
top: -7px;
right: -11px;
}
body {
.panel-default {
> .panel-heading {
.btn-group,
.btn-group.open {
.dropdown-toggle.btn {
&,
&:focus {
background-color: @panel-default-heading-bg;
border-color: @panel-default-heading-bg;
}
}
}
}
}
.panel-warning {
border-color: @panel-warning-bg;
> .panel-heading {
background-color: @panel-warning-bg;
border-color: @panel-warning-bg;
.btn,
.btn-warning {
color: @panel-warning-text;
background-color: @panel-warning-bg;
border-color: @panel-warning-bg;
}
.btn-group,
.btn-group.open {
.dropdown-toggle.btn {
&,
&:focus {
background-color: @panel-warning-bg;
border-color: @panel-warning-bg;
color: @panel-warning-text;
}
}
}
}
}
.panel-danger {
> .panel-heading {
background-color: @state-danger-border;
.btn,
.btn-danger {
color: @panel-danger-text;
background-color: @state-danger-border;
border-color: @state-danger-border;
}
.btn-group,
.btn-group.open {
.dropdown-toggle.btn {
&,
&:focus {
background-color: @state-danger-border;
border-color: @state-danger-border;
color: @panel-danger-text;
}
}
}
}
}
.panel-info {
border-color: @state-info-bg;
> .panel-heading {
color: @panel-info-text;
background-color: @state-info-bg;
border-color: @state-info-bg;
.btn,
.btn-info {
color: @panel-info-text;
background-color: @state-info-bg;
border-color: @state-info-bg;
}
.btn-group.open {
.dropdown-toggle.btn {
&,
&:focus {
background-color: @state-info-bg;
border-color: @state-info-bg;
color: @panel-info-text;
}
}
}
}
}
.panel-success {
> .panel-heading {
background-color: @state-success-border;
.btn,
.btn-success {
color: @panel-success-text;
background-color: @state-success-border;
border-color: @state-success-border;
}
.btn-group.open {
.dropdown-toggle.btn {
&,
&:focus {
background-color: @state-success-border;
border-color: @state-success-border;
color: @panel-success-text;
}
}
}
}
}
.panel-primary {
> .panel-heading {
background-color: @panel-primary-border;
.btn,
.btn-primary {
color: @panel-primary-text;
background-color: @panel-primary-border;
border-color: @panel-primary-border;
}
.btn-group.open {
.dropdown-toggle.btn {
&,
&:focus {
background-color: @panel-primary-border;
border-color: @panel-primary-border;
color: @panel-primary-text;
}
}
}
}
}
}
.panel {
border-width: @panel-border-width;
}
.panel-heading > .btn-group > .btn {
border-radius: 0 !important;
}
h4.panel-title span.fas.color-icon {
font-size: 50%;
left: 0px;
position: relative;
margin-right: 2px;
top: -2px;
}
.panel.sticked {
margin-top: -1 * @line-height-computed;
border-top-width: 0;
> .panel-heading {
.btn-group {
top: -6px;
.btn {
border-top-width: 0;
}
}
}
}

View File

@@ -1,48 +0,0 @@
.modal-footer {
margin-top: 0;
}
.modal-header {
padding-top: 8px;
padding-bottom: 8px;
}
.modal-footer > .main-btn-group {
float: left;
}
.modal-footer > .additional-btn-group {
float: right;
}
.dialog-confirm .modal-footer > .main-btn-group {
float: right;
}
.dialog-confirm .modal-footer > .additional-btn-group {
float: left;
}
.modal-footer {
padding-top: @modal-title-padding;
padding-bottom: @modal-title-padding;
}
@media (min-width: @screen-sm-min) {
.modal-dialog {
margin: 0 0 0 auto;
}
.modal-content .note-editor > .modal > .modal-dialog {
margin: 0 0 0 -@container-padding;
}
.dialog-confirm > .modal-dialog {
margin: 0 auto;
}
}
@media (max-width: @screen-sm-min) {
.modal-dialog {
margin: 0 0 0 0;
}
}

View File

@@ -0,0 +1,19 @@
body > footer {
background-color: @gray-lighter;
}
body > footer > p {
padding: 6px @panel-padding;
}
body > footer > p.credit {
margin: 0;
}
body > footer > p,
body > footer > p a,
body > footer > p a:hover,
body > footer > p a:active,
body > footer > p a:visited {
color: @gray;
}

View File

@@ -0,0 +1,145 @@
.margin {
margin: @table-cell-padding 0;
}
.margin-top {
margin-top: @table-cell-padding;
}
.margin-bottom {
margin-bottom: @table-cell-padding;
}
.margin-bottom-2x {
margin-bottom: @table-cell-padding * 2;
}
.margin-bottom.margin {
margin-top: 0;
}
h1 {
font-size: 32px;
}
h2 {
font-size: 26px;
}
h3 {
font-size: 22px;
}
h4 {
font-size: 18px;
}
h5 {
font-weight: 600;
margin-top: 10px;
margin-bottom: 10px;
font-size: 15px;
color: @gray-soft;
}
h6 {
font-weight: 600;
color: @gray-soft;
font-size: 14px;
}
code {
font-style: italic;
}
b, strong {
font-weight: 600;
}
.text-primary {
color: @state-primary-text;
}
.text-soft {
color: @gray-soft;
}
.text-smaller {
font-size: @font-size-smaller;
}
.text-bold {
font-weight: 600;
}
pre > code {
font-style: normal;
}
hr {
border-top: 1px solid @panel-default-border;
}
blockquote {
font-size: @font-size-base;
border-left-width: 3px;
}
.well > p:last-child {
margin-bottom: 0;
}
.well {
padding: @well-padding;
}
.badge {
font-weight: 600;
}
.badge-circle {
width: 8px;
height: 8px;
border-radius: 4px;
text-align: center;
display: table-cell;
font-size: 5px;
line-height: 8px;
text-align: center;
background-color: @main-gray;
}
.badge-circle.badge-circle-danger {
background-color: @brand-danger;
}
.badge-circle.badge-circle-warning {
background-color: @brand-warning;
}
.label {
padding: .09em .5em .2em;
position: relative;
top: -1px;
font-weight: 600;
}
.label-md {
font-weight: normal;
font-size: 100%;
padding: @label-padding-medium;
top: 0;
}
.label-sm {
font-weight: normal;
font-size: 75%;
padding: @label-padding-small;
}
.small .label-md,
.label-lg.small {
font-size: 75%;
padding: @label-padding-small;
top: -1px;
}

View File

@@ -1,444 +1,257 @@
/*!
* Datepicker for Bootstrap
*
* Copyright 2012 Stefan Petre
* Improvements by Andrew Rowls
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
*/
.datepicker {
padding: 4px;
border-radius: @border-radius-base;
&-inline {
width: 220px;
}
direction: ltr;
/*.dow {
border-top: 1px solid #ddd !important;
}*/
&-rtl {
direction: rtl;
&.dropdown-menu { left: auto; }
table tr td span {
float: right;
}
}
&-dropdown {
top: 0;
left: 0;
padding: 4px;
&:before {
content: '';
display: inline-block;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid @dropdown-border;
border-top: 0;
border-bottom-color: rgba(0,0,0,.2);
position: absolute;
}
&:after {
content: '';
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid @dropdown-bg;
border-top: 0;
position: absolute;
}
&.datepicker-orient-left:before { left: 6px; }
&.datepicker-orient-left:after { left: 7px; }
&.datepicker-orient-right:before { right: 6px; }
&.datepicker-orient-right:after { right: 7px; }
&.datepicker-orient-bottom:before { top: -7px; }
&.datepicker-orient-bottom:after { top: -6px; }
&.datepicker-orient-top:before {
bottom: -7px;
border-bottom: 0;
border-top: 7px solid @dropdown-border;
}
&.datepicker-orient-top:after {
bottom: -6px;
border-bottom: 0;
border-top: 6px solid @dropdown-bg;
}
}
table {
margin: 0;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
tr {
td, th {
text-align: center;
width: 30px;
height: 30px;
border-radius: 4px;
border: none;
}
}
}
// Inline display inside a table presents some problems with
// border and background colors.
.table-striped & table tr {
td, th {
background-color: transparent;
}
}
table tr td {
&.old,
&.new {
color: @btn-link-disabled-color;
}
&.day:hover,
&.focused {
background: @gray-lighter;
cursor: pointer;
}
&.disabled,
&.disabled:hover {
background: none;
color: @btn-link-disabled-color;
cursor: default;
}
&.highlighted {
@highlighted-bg: @state-info-bg;
.button-variant(#000, @highlighted-bg, darken(@highlighted-bg, 20%));
border-radius: 0;
&.focused {
background: darken(@highlighted-bg, 10%);
}
&.disabled,
&.disabled:active {
background: @highlighted-bg;
color: @btn-link-disabled-color;
}
}
&.today {
@today-bg: lighten(orange, 30%);
.button-variant(#000, @today-bg, darken(@today-bg, 20%));
&.focused {
background: darken(@today-bg, 10%);
}
&.disabled,
&.disabled:active {
background: @today-bg;
color: @btn-link-disabled-color;
}
}
&.range {
@range-bg: @gray-lighter;
.button-variant(#000, @range-bg, darken(@range-bg, 20%));
border-radius: 0;
&.focused {
background: darken(@range-bg, 10%);
}
&.disabled,
&.disabled:active {
background: @range-bg;
color: @btn-link-disabled-color;
}
}
&.range.highlighted {
@range-highlighted-bg: mix(@state-info-bg, @gray-lighter, 50%);
.button-variant(#000, @range-highlighted-bg, darken(@range-highlighted-bg, 20%));
&.focused {
background: darken(@range-highlighted-bg, 10%);
}
&.disabled,
&.disabled:active {
background: @range-highlighted-bg;
color: @btn-link-disabled-color;
}
}
&.range.today {
@range-today-bg: mix(orange, @gray-lighter, 50%);
.button-variant(#000, @range-today-bg, darken(@range-today-bg, 20%));
&.disabled,
&.disabled:active {
background: @range-today-bg;
color: @btn-link-disabled-color;
}
}
&.selected,
&.selected.highlighted {
.button-variant(#fff, @gray-light, @gray);
text-shadow: 0 -1px 0 rgba(0,0,0,.25);
}
&.active,
&.active.highlighted {
.button-variant(@btn-primary-color, @btn-primary-bg, @btn-primary-border);
text-shadow: 0 -1px 0 rgba(0,0,0,.25);
}
span {
display: block;
width: 23%;
height: 54px;
line-height: 54px;
float: left;
margin: 1%;
cursor: pointer;
border-radius: 4px;
&:hover,
&.focused {
background: @gray-lighter;
}
&.disabled,
&.disabled:hover {
background: none;
color: @btn-link-disabled-color;
cursor: default;
}
&.active,
&.active:hover,
&.active.disabled,
&.active.disabled:hover {
.button-variant(@btn-primary-color, @btn-primary-bg, @btn-primary-border);
text-shadow: 0 -1px 0 rgba(0,0,0,.25);
}
&.old,
&.new {
color: @btn-link-disabled-color;
}
}
}
.datepicker-switch {
width: 145px;
}
.datepicker-switch,
.prev,
.next,
tfoot tr th {
cursor: pointer;
&:hover {
background: @gray-lighter;
}
}
.prev, .next {
&.disabled {
visibility: hidden;
}
}
// Basic styling for calendar-week cells
.cw {
font-size: 10px;
width: 12px;
padding: 0 2px 0 5px;
vertical-align: middle;
}
}
.datepicker-inline {
width: 220px;
}
.datepicker.datepicker-rtl {
direction: rtl;
}
.datepicker.datepicker-rtl table tr td span {
float: right;
}
.datepicker-dropdown {
top: 0;
left: 0;
}
.datepicker-dropdown:before {
content: '';
display: inline-block;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid #ccc;
border-top: 0;
border-bottom-color: rgba(0, 0, 0, 0.2);
position: absolute;
}
.datepicker-dropdown:after {
content: '';
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #ffffff;
border-top: 0;
position: absolute;
}
.datepicker-dropdown.datepicker-orient-left:before {
left: 6px;
}
.datepicker-dropdown.datepicker-orient-left:after {
left: 7px;
}
.datepicker-dropdown.datepicker-orient-right:before {
right: 6px;
}
.datepicker-dropdown.datepicker-orient-right:after {
right: 7px;
}
.datepicker-dropdown.datepicker-orient-top:before {
top: -7px;
}
.datepicker-dropdown.datepicker-orient-top:after {
top: -6px;
}
.datepicker-dropdown.datepicker-orient-bottom:before {
bottom: -7px;
border-bottom: 0;
border-top: 7px solid #999;
}
.datepicker-dropdown.datepicker-orient-bottom:after {
bottom: -6px;
border-bottom: 0;
border-top: 6px solid #ffffff;
}
.datepicker > div {
display: none;
}
.datepicker.days div.datepicker-days {
display: block;
}
.datepicker.months div.datepicker-months {
display: block;
}
.datepicker.years div.datepicker-years {
display: block;
}
.datepicker table {
margin: 0;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.datepicker td,
.datepicker th {
text-align: center;
width: 20px;
height: 20px;
border: none;
}
.table-striped .datepicker table tr td,
.table-striped .datepicker table tr th {
background-color: transparent;
}
.datepicker table tr td.day:hover {
background: #eeeeee;
.input-group.date .input-group-addon {
cursor: pointer;
}
.datepicker table tr td.old,
.datepicker table tr td.new {
color: #999999;
}
.datepicker table tr td.disabled,
.datepicker table tr td.disabled:hover {
background: none;
color: #999999;
cursor: default;
}
.datepicker table tr td.today,
.datepicker table tr td.today:hover,
.datepicker table tr td.today.disabled,
.datepicker table tr td.today.disabled:hover {
background-color: #fde19a;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0);
border-color: #fdf59a #fdf59a #fbed50;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
color: #000;
}
.datepicker table tr td.today:hover,
.datepicker table tr td.today:hover:hover,
.datepicker table tr td.today.disabled:hover,
.datepicker table tr td.today.disabled:hover:hover,
.datepicker table tr td.today:active,
.datepicker table tr td.today:hover:active,
.datepicker table tr td.today.disabled:active,
.datepicker table tr td.today.disabled:hover:active,
.datepicker table tr td.today.active,
.datepicker table tr td.today:hover.active,
.datepicker table tr td.today.disabled.active,
.datepicker table tr td.today.disabled:hover.active,
.datepicker table tr td.today.disabled,
.datepicker table tr td.today:hover.disabled,
.datepicker table tr td.today.disabled.disabled,
.datepicker table tr td.today.disabled:hover.disabled,
.datepicker table tr td.today[disabled],
.datepicker table tr td.today:hover[disabled],
.datepicker table tr td.today.disabled[disabled],
.datepicker table tr td.today.disabled:hover[disabled] {
background-color: #fdf59a;
}
.datepicker table tr td.today:active,
.datepicker table tr td.today:hover:active,
.datepicker table tr td.today.disabled:active,
.datepicker table tr td.today.disabled:hover:active,
.datepicker table tr td.today.active,
.datepicker table tr td.today:hover.active,
.datepicker table tr td.today.disabled.active,
.datepicker table tr td.today.disabled:hover.active {
background-color: #fbf069 \9;
}
.datepicker table tr td.today:hover:hover {
color: #000;
}
.datepicker table tr td.today.active:hover {
color: #fff;
}
.datepicker table tr td.range,
.datepicker table tr td.range:hover,
.datepicker table tr td.range.disabled,
.datepicker table tr td.range.disabled:hover {
background: #eeeeee;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
}
.datepicker table tr td.range.today,
.datepicker table tr td.range.today:hover,
.datepicker table tr td.range.today.disabled,
.datepicker table tr td.range.today.disabled:hover {
background-color: #f3d17a;
background-image: -moz-linear-gradient(top, #f3c17a, #f3e97a);
background-image: -ms-linear-gradient(top, #f3c17a, #f3e97a);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f3c17a), to(#f3e97a));
background-image: -webkit-linear-gradient(top, #f3c17a, #f3e97a);
background-image: -o-linear-gradient(top, #f3c17a, #f3e97a);
background-image: linear-gradient(top, #f3c17a, #f3e97a);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0);
border-color: #f3e97a #f3e97a #edde34;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
}
.datepicker table tr td.range.today:hover,
.datepicker table tr td.range.today:hover:hover,
.datepicker table tr td.range.today.disabled:hover,
.datepicker table tr td.range.today.disabled:hover:hover,
.datepicker table tr td.range.today:active,
.datepicker table tr td.range.today:hover:active,
.datepicker table tr td.range.today.disabled:active,
.datepicker table tr td.range.today.disabled:hover:active,
.datepicker table tr td.range.today.active,
.datepicker table tr td.range.today:hover.active,
.datepicker table tr td.range.today.disabled.active,
.datepicker table tr td.range.today.disabled:hover.active,
.datepicker table tr td.range.today.disabled,
.datepicker table tr td.range.today:hover.disabled,
.datepicker table tr td.range.today.disabled.disabled,
.datepicker table tr td.range.today.disabled:hover.disabled,
.datepicker table tr td.range.today[disabled],
.datepicker table tr td.range.today:hover[disabled],
.datepicker table tr td.range.today.disabled[disabled],
.datepicker table tr td.range.today.disabled:hover[disabled] {
background-color: #f3e97a;
}
.datepicker table tr td.range.today:active,
.datepicker table tr td.range.today:hover:active,
.datepicker table tr td.range.today.disabled:active,
.datepicker table tr td.range.today.disabled:hover:active,
.datepicker table tr td.range.today.active,
.datepicker table tr td.range.today:hover.active,
.datepicker table tr td.range.today.disabled.active,
.datepicker table tr td.range.today.disabled:hover.active {
background-color: #efe24b \9;
}
.datepicker table tr td.selected,
.datepicker table tr td.selected:hover,
.datepicker table tr td.selected.disabled,
.datepicker table tr td.selected.disabled:hover {
background-color: #9e9e9e;
background-image: -moz-linear-gradient(top, #b3b3b3, #808080);
background-image: -ms-linear-gradient(top, #b3b3b3, #808080);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#b3b3b3), to(#808080));
background-image: -webkit-linear-gradient(top, #b3b3b3, #808080);
background-image: -o-linear-gradient(top, #b3b3b3, #808080);
background-image: linear-gradient(top, #b3b3b3, #808080);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0);
border-color: #808080 #808080 #595959;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.selected:hover,
.datepicker table tr td.selected:hover:hover,
.datepicker table tr td.selected.disabled:hover,
.datepicker table tr td.selected.disabled:hover:hover,
.datepicker table tr td.selected:active,
.datepicker table tr td.selected:hover:active,
.datepicker table tr td.selected.disabled:active,
.datepicker table tr td.selected.disabled:hover:active,
.datepicker table tr td.selected.active,
.datepicker table tr td.selected:hover.active,
.datepicker table tr td.selected.disabled.active,
.datepicker table tr td.selected.disabled:hover.active,
.datepicker table tr td.selected.disabled,
.datepicker table tr td.selected:hover.disabled,
.datepicker table tr td.selected.disabled.disabled,
.datepicker table tr td.selected.disabled:hover.disabled,
.datepicker table tr td.selected[disabled],
.datepicker table tr td.selected:hover[disabled],
.datepicker table tr td.selected.disabled[disabled],
.datepicker table tr td.selected.disabled:hover[disabled] {
background-color: #808080;
}
.datepicker table tr td.selected:active,
.datepicker table tr td.selected:hover:active,
.datepicker table tr td.selected.disabled:active,
.datepicker table tr td.selected.disabled:hover:active,
.datepicker table tr td.selected.active,
.datepicker table tr td.selected:hover.active,
.datepicker table tr td.selected.disabled.active,
.datepicker table tr td.selected.disabled:hover.active {
background-color: #666666 \9;
}
.datepicker table tr td.active,
.datepicker table tr td.active:hover,
.datepicker table tr td.active.disabled,
.datepicker table tr td.active.disabled:hover {
background-color: #006dcc;
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.active:hover,
.datepicker table tr td.active:hover:hover,
.datepicker table tr td.active.disabled:hover,
.datepicker table tr td.active.disabled:hover:hover,
.datepicker table tr td.active:active,
.datepicker table tr td.active:hover:active,
.datepicker table tr td.active.disabled:active,
.datepicker table tr td.active.disabled:hover:active,
.datepicker table tr td.active.active,
.datepicker table tr td.active:hover.active,
.datepicker table tr td.active.disabled.active,
.datepicker table tr td.active.disabled:hover.active,
.datepicker table tr td.active.disabled,
.datepicker table tr td.active:hover.disabled,
.datepicker table tr td.active.disabled.disabled,
.datepicker table tr td.active.disabled:hover.disabled,
.datepicker table tr td.active[disabled],
.datepicker table tr td.active:hover[disabled],
.datepicker table tr td.active.disabled[disabled],
.datepicker table tr td.active.disabled:hover[disabled] {
background-color: @brand-primary;
}
.datepicker table tr td.active:active,
.datepicker table tr td.active:hover:active,
.datepicker table tr td.active.disabled:active,
.datepicker table tr td.active.disabled:hover:active,
.datepicker table tr td.active.active,
.datepicker table tr td.active:hover.active,
.datepicker table tr td.active.disabled.active,
.datepicker table tr td.active.disabled:hover.active {
background-color: #003399 \9;
}
.datepicker table tr td span {
display: block;
width: 23%;
height: 54px;
line-height: 54px;
float: left;
margin: 1%;
cursor: pointer;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.datepicker table tr td span:hover {
background: #eeeeee;
}
.datepicker table tr td span.disabled,
.datepicker table tr td span.disabled:hover {
background: none;
color: #999999;
cursor: default;
}
.datepicker table tr td span.active,
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active.disabled,
.datepicker table tr td span.active.disabled:hover {
background-color: #006dcc;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
border-color: #0044cc #0044cc #002a80;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active:hover:hover,
.datepicker table tr td span.active.disabled:hover,
.datepicker table tr td span.active.disabled:hover:hover,
.datepicker table tr td span.active:active,
.datepicker table tr td span.active:hover:active,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.active,
.datepicker table tr td span.active:hover.active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active.disabled:hover.active,
.datepicker table tr td span.active.disabled,
.datepicker table tr td span.active:hover.disabled,
.datepicker table tr td span.active.disabled.disabled,
.datepicker table tr td span.active.disabled:hover.disabled,
.datepicker table tr td span.active[disabled],
.datepicker table tr td span.active:hover[disabled],
.datepicker table tr td span.active.disabled[disabled],
.datepicker table tr td span.active.disabled:hover[disabled] {
background-color: @brand-primary;
}
.datepicker table tr td span.active:active,
.datepicker table tr td span.active:hover:active,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.active,
.datepicker table tr td span.active:hover.active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active.disabled:hover.active {
background-color: #003399 \9;
}
.datepicker table tr td span.old,
.datepicker table tr td span.new {
color: #999999;
}
.datepicker th.datepicker-switch {
width: 145px;
}
.datepicker thead tr:first-child th,
.datepicker tfoot tr th {
cursor: pointer;
}
.datepicker thead tr:first-child th:hover,
.datepicker tfoot tr th:hover {
background: #eeeeee;
}
.datepicker .cw {
font-size: 10px;
width: 12px;
padding: 0 2px 0 5px;
vertical-align: middle;
}
.datepicker thead tr:first-child th.cw {
cursor: default;
background-color: transparent;
}
.input-append.date .add-on i,
.input-prepend.date .add-on i {
display: block;
cursor: pointer;
width: 16px;
height: 16px;
}
.input-daterange input {
text-align: center;
}
.input-daterange input:first-child {
}
.input-daterange input:last-child {
}
.input-daterange .add-on {
display: inline-block;
width: auto;
min-width: 16px;
height: 18px;
padding: 4px 5px;
font-weight: normal;
line-height: 18px;
text-align: center;
text-shadow: 0 1px 0 #ffffff;
vertical-align: middle;
background-color: #eeeeee;
border: 1px solid #ccc;
margin-left: -5px;
margin-right: -5px;
}
.input-daterange {
width: 100%;
input {
text-align: center;
}
input:first-child {
border-radius: 3px 0 0 3px;
}
input:last-child {
border-radius: 0 3px 3px 0;
}
.input-group-addon {
width: auto;
min-width: 16px;
padding: 4px 5px;
line-height: @line-height-base;
border-width: 1px 0;
margin-left: -5px;
margin-right: -5px;
}
}

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "espocrm",
"version": "5.9.1",
"version": "5.9.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "espocrm",
"version": "5.9.1",
"version": "5.9.2",
"description": "",
"main": "index.php",
"repository": {

View File

@@ -165,4 +165,27 @@ class EvaluatorTest extends \PHPUnit\Framework\TestCase
$actual = $this->evaluator->process($expression);
$this->assertEquals(4, $actual);
}
public function testWhile()
{
$expression = "
\$source = list(0, 1, 2);
\$target = list();
\$i = 0;
while(\$i < array\\length(\$source),
\$target = array\\push(
\$target,
array\\at(\$source, \$i)
);
\$i = \$i + 1;
);
";
$vars = (object) [];
$this->evaluator->process($expression, null, $vars);
$this->assertEquals([0, 1, 2], $vars->target);
}
}

View File

@@ -1412,6 +1412,28 @@ class FormulaTest extends \PHPUnit\Framework\TestCase
$this->assertEquals(3, $actual);
}
function testNumberRandomInt()
{
$item = json_decode('
{
"type": "number\\\\randomInt",
"value": [
{
"type": "value",
"value": 0
},
{
"type": "value",
"value": 10
}
]
}
');
$actual = $this->formula->process($item, $this->entity);
$this->assertIsInt($actual);
}
function testDatetime()
{
$item = json_decode('

View File

@@ -462,13 +462,13 @@ class EmailTest extends \PHPUnit\Framework\TestCase
->will($this->returnValue($attachment));
$body = $this->email->getBodyForSending();
$this->assertEquals($body, 'test <img src="cid:Id01">');
$this->assertEquals('test <img src="cid:Id01">', $body);
}
function testBodyPlain()
{
$this->email->set('body', '<br />&nbsp;&amp;');
$bodyPlain = $this->email->getBodyPlain();
$this->assertEquals($bodyPlain, "\r\n &");
$this->assertEquals("\r\n &", $bodyPlain);
}
}