mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-09 14:47:00 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abd2016444 | ||
|
|
c247a24db4 | ||
|
|
17c9379c15 | ||
|
|
d6d83a209f | ||
|
|
0ffe39cec8 | ||
|
|
3347b7fba8 | ||
|
|
64aebdde6b | ||
|
|
f3ee5c654b | ||
|
|
1a413cb54e | ||
|
|
41bcaf50c4 | ||
|
|
bc7d9443b1 | ||
|
|
8188dc065b | ||
|
|
a2025d0a89 | ||
|
|
1d31637c2e | ||
|
|
2ba808c371 | ||
|
|
6bce395daf | ||
|
|
5b0787474e | ||
|
|
8c87f20374 | ||
|
|
ba35115a48 | ||
|
|
069010d0fe | ||
|
|
fe5878fd99 | ||
|
|
839ceea142 | ||
|
|
9cce9d7347 | ||
|
|
69d0dbbf1c | ||
|
|
68ef9ce4ac |
101
application/Espo/Classes/RecordHooks/Email/CheckFromAddress.php
Normal file
101
application/Espo/Classes/RecordHooks/Email/CheckFromAddress.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\RecordHooks\Email;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Mail\Account\SendingAccountProvider;
|
||||
use Espo\Core\Record\Hook\SaveHook;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @implements SaveHook<Email>
|
||||
*/
|
||||
class CheckFromAddress implements SaveHook
|
||||
{
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private SendingAccountProvider $sendingAccountProvider,
|
||||
private Config $config,
|
||||
private Acl $acl,
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity): void
|
||||
{
|
||||
if ($this->user->isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fromAddress = $entity->getFromAddress();
|
||||
|
||||
// Should be after 'getFromAddress'.
|
||||
if (!$entity->isAttributeChanged('from')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$fromAddress) {
|
||||
throw new BadRequest("No 'from' address");
|
||||
}
|
||||
|
||||
if ($this->acl->checkScope('Import')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fromAddress = strtolower($fromAddress);
|
||||
|
||||
foreach ($this->user->getEmailAddressGroup()->getAddressList() as $address) {
|
||||
if ($fromAddress === strtolower($address)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->sendingAccountProvider->getShared($this->user, $fromAddress)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$system = $this->sendingAccountProvider->getSystem();
|
||||
|
||||
if (
|
||||
$system &&
|
||||
$this->config->get('outboundEmailIsShared') &&
|
||||
$system->getEmailAddress()
|
||||
) {
|
||||
if ($fromAddress === strtolower($system->getEmailAddress())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Forbidden("Not allowed 'from' address.");
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,14 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Utils\Json;
|
||||
|
||||
class ApiIndex
|
||||
{
|
||||
public function getActionIndex(): string
|
||||
public function getActionIndex(Request $request, Response $response): void
|
||||
{
|
||||
return "EspoCRM REST API";
|
||||
$response->writeBody(Json::encode("EspoCRM REST API"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,8 +195,12 @@ class Auth
|
||||
throw new BadRequest("Auth: Bad authorization string provided.");
|
||||
}
|
||||
|
||||
/** @var array{string, string} */
|
||||
return explode(':', $stringDecoded, 2);
|
||||
[$username, $password] = explode(':', $stringDecoded, 2);
|
||||
|
||||
$username = trim($username);
|
||||
$password = trim($password);
|
||||
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
private function handleSecondStepRequired(Response $response, Result $result): void
|
||||
@@ -316,6 +320,14 @@ class Auth
|
||||
$username = $request->getServerParam('PHP_AUTH_USER');
|
||||
$password = $request->getServerParam('PHP_AUTH_PW');
|
||||
|
||||
if (is_string($username)) {
|
||||
$username = trim($username);
|
||||
}
|
||||
|
||||
if (is_string($password)) {
|
||||
$password = trim($password);
|
||||
}
|
||||
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ class Authentication
|
||||
$result = $this->processTwoFactor($result, $request);
|
||||
|
||||
if ($result->isFail()) {
|
||||
return $this->processFail($result, $data, $request);
|
||||
return $this->processTwoFactorFail($result, $data, $request, $authLogRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -818,4 +818,22 @@ class Authentication
|
||||
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
private function processTwoFactorFail(
|
||||
Result $result,
|
||||
AuthenticationData $data,
|
||||
Request $request,
|
||||
?AuthLogRecord $authLogRecord
|
||||
): Result {
|
||||
|
||||
if ($authLogRecord) {
|
||||
$authLogRecord
|
||||
->setIsDenied()
|
||||
->setDenialReason(AuthLogRecord::DENIAL_REASON_WRONG_CODE);
|
||||
|
||||
$this->entityManager->saveEntity($authLogRecord);
|
||||
}
|
||||
|
||||
return $this->processFail($result, $data, $request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,12 +80,12 @@ class FailedAttemptsLimit implements BeforeLogin
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
$apAddress = $this->util->obtainIpFromRequest($request);
|
||||
$ipAddress = $this->util->obtainIpFromRequest($request);
|
||||
|
||||
$where = [
|
||||
'requestTime>' => $requestTimeFrom->format('U'),
|
||||
'ipAddress' => $apAddress,
|
||||
'isDenied' => true,
|
||||
'ipAddress' => $ipAddress,
|
||||
];
|
||||
|
||||
$wasFailed = (bool) $this->entityManager
|
||||
@@ -107,6 +107,6 @@ class FailedAttemptsLimit implements BeforeLogin
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Forbidden("Max failed login attempts exceeded for IP address $apAddress.");
|
||||
throw new Forbidden("Max failed login attempts exceeded for IP address $ipAddress.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication\Hook\Hooks;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Authentication\AuthenticationData;
|
||||
use Espo\Core\Authentication\ConfigDataProvider;
|
||||
use Espo\Core\Authentication\Hook\BeforeLogin;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Entities\AuthLogRecord;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class FailedCodeAttemptsLimit implements BeforeLogin
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
private EntityManager $entityManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function process(AuthenticationData $data, Request $request): void
|
||||
{
|
||||
if (
|
||||
$request->getHeader('Espo-Authorization-Code') === null ||
|
||||
$this->configDataProvider->isAuthLogDisabled()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$isByTokenOnly = !$data->getMethod() && $request->getHeader('Espo-Authorization-By-Token') === 'true';
|
||||
|
||||
if ($isByTokenOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
$failedAttemptsPeriod = $this->configDataProvider->getFailedAttemptsPeriod();
|
||||
$maxFailedAttempts = $this->configDataProvider->getMaxFailedAttemptNumber();
|
||||
|
||||
$requestTime = intval($request->getServerParam('REQUEST_TIME_FLOAT'));
|
||||
|
||||
try {
|
||||
$requestTimeFrom = (new DateTime('@' . $requestTime))->modify('-' . $failedAttemptsPeriod);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
$where = [
|
||||
'requestTime>' => $requestTimeFrom->format('U'),
|
||||
'isDenied' => true,
|
||||
'username' => $data->getUsername(),
|
||||
'denialReason' => AuthLogRecord::DENIAL_REASON_WRONG_CODE,
|
||||
];
|
||||
|
||||
$wasFailed = (bool) $this->entityManager
|
||||
->getRDBRepository(AuthLogRecord::ENTITY_TYPE)
|
||||
->select(['id'])
|
||||
->where($where)
|
||||
->findOne();
|
||||
|
||||
if (!$wasFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$failAttemptCount = $this->entityManager
|
||||
->getRDBRepository(AuthLogRecord::ENTITY_TYPE)
|
||||
->where($where)
|
||||
->count();
|
||||
|
||||
if ($failAttemptCount <= $maxFailedAttempts) {
|
||||
return;
|
||||
}
|
||||
|
||||
$username = $data->getUsername() ?? '';
|
||||
|
||||
throw new Forbidden("Max failed 2FA login attempts exceeded for username '$username'.");
|
||||
}
|
||||
}
|
||||
@@ -77,8 +77,8 @@ abstract class Base
|
||||
];
|
||||
|
||||
private ZipArchive $zipUtil;
|
||||
private ?DatabaseHelper $databaseHelper;
|
||||
private ?Helper $helper;
|
||||
private ?DatabaseHelper $databaseHelper = null;
|
||||
private ?Helper $helper = null;
|
||||
|
||||
public function __construct(
|
||||
private Container $container,
|
||||
|
||||
@@ -51,6 +51,13 @@ class AfterUpgradeRunner
|
||||
throw new RuntimeException("No after-upgrade script $step.");
|
||||
}
|
||||
|
||||
try {
|
||||
$this->dataManager->rebuild();
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new RuntimeException("Error while rebuild: " . $e->getMessage());
|
||||
}
|
||||
|
||||
/** @var Script $script */
|
||||
$script = $this->injectableFactory->createWith($className, ['isUpgrade' => false]);
|
||||
$script->run();
|
||||
|
||||
@@ -36,6 +36,7 @@ class AuthLogRecord extends Entity
|
||||
public const ENTITY_TYPE = 'AuthLogRecord';
|
||||
|
||||
public const DENIAL_REASON_CREDENTIALS = 'CREDENTIALS';
|
||||
public const DENIAL_REASON_WRONG_CODE = 'WRONG_CODE';
|
||||
public const DENIAL_REASON_INACTIVE_USER = 'INACTIVE_USER';
|
||||
public const DENIAL_REASON_IS_PORTAL_USER = 'IS_PORTAL_USER';
|
||||
public const DENIAL_REASON_IS_NOT_PORTAL_USER = 'IS_NOT_PORTAL_USER';
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Modules\Crm\EntryPoints;
|
||||
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Crm\Entities\Campaign;
|
||||
use Espo\Modules\Crm\Entities\EmailQueueItem;
|
||||
use Espo\Modules\Crm\Entities\MassEmail;
|
||||
@@ -47,7 +48,8 @@ class CampaignTrackOpened implements EntryPoint
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private LogService $service
|
||||
private LogService $service,
|
||||
private User $user,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -105,7 +107,9 @@ class CampaignTrackOpened implements EntryPoint
|
||||
return;
|
||||
}
|
||||
|
||||
$this->service->logOpened($campaignId, $queueItem);
|
||||
if ($this->user->isSystem()) {
|
||||
$this->service->logOpened($campaignId, $queueItem);
|
||||
}
|
||||
|
||||
header('Content-Type: image/png');
|
||||
|
||||
|
||||
@@ -109,8 +109,11 @@ class Email extends Database implements
|
||||
|
||||
public function loadFromField(EmailEntity $entity): void
|
||||
{
|
||||
if ($entity->get('fromEmailAddressName')) {
|
||||
$entity->set('from', $entity->get('fromEmailAddressName'));
|
||||
$fromEmailAddressName = $entity->get('fromEmailAddressName');
|
||||
|
||||
if ($fromEmailAddressName && !$entity->isAttributeChanged('fromEmailAddressName')) {
|
||||
$entity->set('from', $fromEmailAddressName);
|
||||
$entity->setFetched('from', $fromEmailAddressName);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -122,6 +125,7 @@ class Email extends Database implements
|
||||
|
||||
if ($ea) {
|
||||
$entity->set('from', $ea->get('name'));
|
||||
$entity->setFetched('from', $ea->get('name'));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -132,6 +136,7 @@ class Email extends Database implements
|
||||
}
|
||||
|
||||
$entity->set('from', null);
|
||||
$entity->setFetched('from', null);
|
||||
}
|
||||
|
||||
public function loadToField(EmailEntity $entity): void
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"options": {
|
||||
"denialReason": {
|
||||
"CREDENTIALS": "Invalid credentials",
|
||||
"WRONG_CODE": "Wrong code",
|
||||
"INACTIVE_USER": "Inactive user",
|
||||
"IS_PORTAL_USER": "Portal user",
|
||||
"IS_NOT_PORTAL_USER": "Not a portal user",
|
||||
|
||||
@@ -238,6 +238,8 @@
|
||||
"Change": "Change",
|
||||
"Primary": "Primary",
|
||||
"Save Filter": "Save Filter",
|
||||
"Remove Filter": "Remove Filter",
|
||||
"Ready": "Ready",
|
||||
"Administration": "Administration",
|
||||
"Run Import": "Run Import",
|
||||
"Duplicate": "Duplicate",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"beforeLoginHookClassNameList": [
|
||||
"Espo\\Core\\Authentication\\Hook\\Hooks\\FailedAttemptsLimit"
|
||||
"Espo\\Core\\Authentication\\Hook\\Hooks\\FailedAttemptsLimit",
|
||||
"Espo\\Core\\Authentication\\Hook\\Hooks\\FailedCodeAttemptsLimit"
|
||||
],
|
||||
"onLoginHookClassNameList": [
|
||||
"Espo\\Core\\Authentication\\Hook\\Hooks\\IpAddressWhitelist"
|
||||
|
||||
@@ -81,8 +81,10 @@
|
||||
"view": "views/email/fields/from-address-varchar",
|
||||
"textFilterDisabled": true,
|
||||
"customizationDisabled": true,
|
||||
"layoutDefaultSidePanelDisabled": true,
|
||||
"layoutMassUpdateDisabled": true
|
||||
"layoutAvailabilityList": [
|
||||
"detail",
|
||||
"filters"
|
||||
]
|
||||
},
|
||||
"to": {
|
||||
"type": "varchar",
|
||||
@@ -97,8 +99,10 @@
|
||||
],
|
||||
"textFilterDisabled": true,
|
||||
"customizationDisabled": true,
|
||||
"layoutDefaultSidePanelDisabled": true,
|
||||
"layoutMassUpdateDisabled": true
|
||||
"layoutAvailabilityList": [
|
||||
"detail",
|
||||
"filters"
|
||||
]
|
||||
},
|
||||
"cc": {
|
||||
"type": "varchar",
|
||||
@@ -111,8 +115,9 @@
|
||||
],
|
||||
"customizationDisabled": true,
|
||||
"textFilterDisabled": true,
|
||||
"layoutDefaultSidePanelDisabled": true,
|
||||
"layoutMassUpdateDisabled": true
|
||||
"layoutAvailabilityList": [
|
||||
"detail"
|
||||
]
|
||||
},
|
||||
"bcc": {
|
||||
"type": "varchar",
|
||||
@@ -125,8 +130,9 @@
|
||||
],
|
||||
"customizationDisabled": true,
|
||||
"textFilterDisabled": true,
|
||||
"layoutDefaultSidePanelDisabled": true,
|
||||
"layoutMassUpdateDisabled": true
|
||||
"layoutAvailabilityList": [
|
||||
"detail"
|
||||
]
|
||||
},
|
||||
"replyTo": {
|
||||
"type": "varchar",
|
||||
@@ -135,8 +141,9 @@
|
||||
"view": "views/email/fields/email-address-varchar",
|
||||
"textFilterDisabled": true,
|
||||
"customizationDisabled": true,
|
||||
"layoutDefaultSidePanelDisabled": true,
|
||||
"layoutMassUpdateDisabled": true
|
||||
"layoutAvailabilityList": [
|
||||
"detail"
|
||||
]
|
||||
},
|
||||
"personStringData": {
|
||||
"type": "varchar",
|
||||
|
||||
@@ -40,9 +40,11 @@
|
||||
"Espo\\Classes\\RecordHooks\\Email\\MarkAsRead"
|
||||
],
|
||||
"beforeCreateHookClassNameList": [
|
||||
"Espo\\Classes\\RecordHooks\\Email\\CheckFromAddress",
|
||||
"Espo\\Classes\\RecordHooks\\Email\\BeforeCreate"
|
||||
],
|
||||
"beforeUpdateHookClassNameList": [
|
||||
"Espo\\Classes\\RecordHooks\\Email\\CheckFromAddress",
|
||||
"Espo\\Classes\\RecordHooks\\Email\\MarkAsReadBeforeUpdate",
|
||||
"Espo\\Classes\\RecordHooks\\Email\\BeforeUpdate"
|
||||
],
|
||||
|
||||
@@ -138,6 +138,7 @@ class SendService
|
||||
$userAddressList = [];
|
||||
|
||||
if ($user) {
|
||||
// @todo Use getEmailAddressGroup.
|
||||
/** @var Collection<EmailAddress> $emailAddressCollection */
|
||||
$emailAddressCollection = $this->entityManager
|
||||
->getRDBRepositoryByClass(User::class)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="panel-body">
|
||||
|
||||
<div class="cell form-group" data-name="name">
|
||||
<label class="control-label" data-name="name">{{translate 'Name' category='fields'}}</label>
|
||||
<label class="control-label" data-name="name">{{translate 'name' category='fields'}}</label>
|
||||
<div class="field" data-name="name">
|
||||
{{{name}}}
|
||||
</div>
|
||||
|
||||
@@ -263,20 +263,22 @@ class DateTime {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a date to a moment.
|
||||
* Convert a system-formatted date to a moment.
|
||||
*
|
||||
* @param {string} string A date value in a system representation.
|
||||
* @returns {moment.Moment}
|
||||
* @internal
|
||||
*/
|
||||
toMomentDate(string) {
|
||||
return moment.utc(string, this.internalDateFormat);
|
||||
return moment.tz(string, this.internalDateFormat, this.timeZone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a date-time to a moment.
|
||||
* Convert a system-formatted date-time to a moment.
|
||||
*
|
||||
* @param {string} string A date-time value in a system representation.
|
||||
* @returns {moment.Moment}
|
||||
* @internal
|
||||
*/
|
||||
toMoment(string) {
|
||||
let m = moment.utc(string, this.internalDateTimeFullFormat);
|
||||
|
||||
@@ -69,6 +69,10 @@ function init(langSets) {
|
||||
const options = context.options;
|
||||
const view = /** @type {import('view').default} */options.espoView;
|
||||
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.memo('button.cellParams', () => {
|
||||
return ui.button({
|
||||
className: '',
|
||||
@@ -144,6 +148,12 @@ function init(langSets) {
|
||||
const options = context.options;
|
||||
const view = /** @type {import('view').default} */options.espoView;
|
||||
|
||||
if (!view) {
|
||||
// Prevents an issue with a collapsed modal.
|
||||
// @todo Revise.
|
||||
return;
|
||||
}
|
||||
|
||||
context.memo('button.tableParams', () => {
|
||||
return ui.button({
|
||||
className: '',
|
||||
|
||||
@@ -505,7 +505,7 @@ class Dialog {
|
||||
$main.append($button);
|
||||
});
|
||||
|
||||
const allDdItemsHidden = this.dropdownItemList.filter(o => !o.hidden).length === 0;
|
||||
const allDdItemsHidden = this.dropdownItemList.filter(o => o && !o.hidden).length === 0;
|
||||
|
||||
const $dropdown = $('<div>')
|
||||
.addClass('btn-group')
|
||||
|
||||
@@ -58,7 +58,7 @@ define('views/admin/layouts/modals/panel-attributes', ['views/modal', 'model'],
|
||||
},
|
||||
{
|
||||
name: 'cancel',
|
||||
text: 'Cancel',
|
||||
label: 'Cancel',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -762,11 +762,12 @@ class AttachmentMultipleFieldView extends BaseFieldView {
|
||||
if (this.isDetailMode() || this.isListMode()) {
|
||||
const nameHash = this.nameHash;
|
||||
const typeHash = this.model.get(this.typeHashName) || {};
|
||||
const ids = /** @type {string[]} */this.model.get(this.idsName) || [];
|
||||
|
||||
const previews = [];
|
||||
const names = [];
|
||||
|
||||
for (const id in nameHash) {
|
||||
for (const id of ids) {
|
||||
const type = typeHash[id] || false;
|
||||
const name = nameHash[id];
|
||||
|
||||
|
||||
@@ -1253,6 +1253,7 @@ class BaseFieldView extends View {
|
||||
if (isInvalid) {
|
||||
Espo.Ui.error(this.translate('Not valid'));
|
||||
|
||||
// @todo Revise.
|
||||
model.set(prev, {silent: true});
|
||||
|
||||
return;
|
||||
@@ -1273,6 +1274,7 @@ class BaseFieldView extends View {
|
||||
.catch(() => {
|
||||
Espo.Ui.error(this.translate('Error occurred'));
|
||||
|
||||
// @todo Revise.
|
||||
model.set(prev, {silent: true});
|
||||
|
||||
this.reRender();
|
||||
|
||||
@@ -68,6 +68,7 @@ class SaveFiltersModalView extends ModalView {
|
||||
},
|
||||
mode: 'edit',
|
||||
model: model,
|
||||
labelText: this.translate('name', 'fields'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2085,7 +2085,10 @@ class DetailRecordView extends BaseRecordView {
|
||||
return;
|
||||
}
|
||||
|
||||
view.inlineEdit();
|
||||
const initialAttributes = {...view.initialAttributes};
|
||||
|
||||
view.inlineEdit()
|
||||
.then(() => view.initialAttributes = initialAttributes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ class RelationshipUnlinkOnlyActionsView extends RelationshipActionsView {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ class RemoveOnlyRowActionsView extends DefaultRowActionsView {
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -346,6 +346,13 @@ body[data-navbar="side"] {
|
||||
color: var(--navbar-inverse-link-icon-color);
|
||||
}
|
||||
}
|
||||
|
||||
.more-dropdown-menu {
|
||||
.short-label,
|
||||
.more-icon {
|
||||
color: var(--navbar-inverse-link-icon-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "8.3.1",
|
||||
"version": "8.3.6",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "espocrm",
|
||||
"version": "8.3.1",
|
||||
"version": "8.3.6",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "8.3.1",
|
||||
"version": "8.3.6",
|
||||
"description": "Open-source CRM.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user