mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-06 14:37:02 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cc4c8f974 | ||
|
|
a3aa74013a | ||
|
|
509b3affd1 | ||
|
|
ad9b2b54dd | ||
|
|
247eb00963 | ||
|
|
c0b59a49bf | ||
|
|
b525afe154 | ||
|
|
b449263854 | ||
|
|
12550c3d0a | ||
|
|
803a4ffb7f | ||
|
|
57bfea4d70 | ||
|
|
86f460866a | ||
|
|
8515157916 | ||
|
|
5bd18dee49 | ||
|
|
88ea5e7d6c | ||
|
|
3839335508 | ||
|
|
b8e94b52aa | ||
|
|
eeda450405 | ||
|
|
d051bd7df2 | ||
|
|
690a265450 | ||
|
|
8bec0a3caf | ||
|
|
a89463367e | ||
|
|
05dbcbd917 | ||
|
|
774bde3e20 | ||
|
|
90589e0d26 | ||
|
|
be199235f1 | ||
|
|
1b2d67b027 | ||
|
|
7bc56fc864 | ||
|
|
c706ddc809 | ||
|
|
1b6b2ea140 | ||
|
|
c1db037fc2 | ||
|
|
838e9ea773 | ||
|
|
10efe3513c | ||
|
|
6d301092b2 | ||
|
|
5acb5da8fa | ||
|
|
3ad343b274 | ||
|
|
c5fd749b21 | ||
|
|
1f8e0f16c7 | ||
|
|
92babe7fbc | ||
|
|
fb684837f8 | ||
|
|
669413b184 | ||
|
|
a945a01767 | ||
|
|
b4664eafa5 |
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -122,4 +122,9 @@ abstract class Base implements Injectable
|
||||
|
||||
return $eArgs;
|
||||
}
|
||||
}
|
||||
|
||||
protected function fetchRawArguments(\StdClass $item)
|
||||
{
|
||||
return $item->value ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
48
application/Espo/Core/Formula/Functions/WhileType.php
Normal file
48
application/Espo/Core/Formula/Functions/WhileType.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
301
application/Espo/Core/Password/Recovery.php
Normal file
301
application/Espo/Core/Password/Recovery.php
Normal 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();
|
||||
}
|
||||
}
|
||||
194
application/Espo/Core/TemplateHelpers/GoogleMaps.php
Normal file
194
application/Espo/Core/TemplateHelpers/GoogleMaps.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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' => [
|
||||
|
||||
@@ -140,21 +140,21 @@ class Email extends \Espo\Core\ORM\Entity
|
||||
|
||||
$body = $this->get('body');
|
||||
|
||||
$breaks = array("<br />","<br>","<br/>","<br />","<br />","<br/>","<br>");
|
||||
$breaks = ["<br />","<br>","<br/>","<br />","<br />","<br/>","<br>"];
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"log": "Log"
|
||||
},
|
||||
"labels": {
|
||||
"As often as possible": "As often as possible",
|
||||
"Create ScheduledJob": "Create Scheduled Job"
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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).",
|
||||
|
||||
@@ -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"}]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"googleMapsImage": "\\Espo\\Core\\TemplateHelpers\\GoogleMaps::image"
|
||||
}
|
||||
@@ -4,6 +4,9 @@
|
||||
"log": {
|
||||
"readOnly": true,
|
||||
"view": "views/scheduled-job/record/panels/log",
|
||||
"createDisabled": true,
|
||||
"selectDisabled": true,
|
||||
"viewDisabled": true,
|
||||
"unlinkDisabled": true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -175,6 +175,14 @@
|
||||
"passwordRecoveryForAdminDisabled": {
|
||||
"type": "bool"
|
||||
},
|
||||
"passwordRecoveryForInternalUsersDisabled": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
},
|
||||
"passwordRecoveryNoExposure": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
},
|
||||
"passwordGenerateLength": {
|
||||
"type": "int",
|
||||
"min": 6,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
) {
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
1520
client/lib/bootstrap-datepicker.js
vendored
1520
client/lib/bootstrap-datepicker.js
vendored
File diff suppressed because it is too large
Load Diff
1
client/lib/cronstrue-i18n.min.js
vendored
Normal file
1
client/lib/cronstrue-i18n.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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">
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<header data-name="{{name}}">
|
||||
<label data-is-custom="{{#if isCustomLabel}}true{{/if}}">{{label}}</label>
|
||||
<label data-is-custom="{{#if isCustomLabel}}true{{/if}}" data-label="{{label}}">{{labelTranslated}}</label>
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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));
|
||||
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
178
frontend/less/espo/elements/modal.less
Normal file
178
frontend/less/espo/elements/modal.less
Normal 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;
|
||||
}
|
||||
191
frontend/less/espo/elements/navbar.less
Normal file
191
frontend/less/espo/elements/navbar.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
279
frontend/less/espo/elements/panel.less
Normal file
279
frontend/less/espo/elements/panel.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
19
frontend/less/espo/elements/site.less
Normal file
19
frontend/less/espo/elements/site.less
Normal 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;
|
||||
}
|
||||
145
frontend/less/espo/elements/type.less
Normal file
145
frontend/less/espo/elements/type.less
Normal 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;
|
||||
}
|
||||
@@ -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
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "5.9.1",
|
||||
"version": "5.9.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "5.9.1",
|
||||
"version": "5.9.2",
|
||||
"description": "",
|
||||
"main": "index.php",
|
||||
"repository": {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('
|
||||
|
||||
@@ -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 /> &');
|
||||
$bodyPlain = $this->email->getBodyPlain();
|
||||
$this->assertEquals($bodyPlain, "\r\n &");
|
||||
$this->assertEquals("\r\n &", $bodyPlain);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user