Compare commits

...

91 Commits

Author SHA1 Message Date
Yurii 917dbdd3fe 9.2.6 2026-01-15 20:35:58 +02:00
Yurii f645af4347 fix url rendering 2026-01-15 19:47:54 +02:00
Yurii be228fa48a url validator util 2026-01-15 19:41:06 +02:00
Yurii 5d443dda10 fix mariadb 12 2026-01-09 12:50:28 +02:00
Yuri Kuznetsov 74db03090b fix linked where filters 2025-12-19 08:50:17 +02:00
Yuri Kuznetsov 03ac0a57c8 fix double shortcut issue 2025-12-17 20:02:39 +02:00
Yuri Kuznetsov 1f251eecb3 fix notification read 2025-12-06 10:43:26 +02:00
Usame Beşir 83439e5384 fix missing value of entityType variable (#3523) 2025-11-17 12:06:19 +02:00
Yuri Kuznetsov 1df75fcece currencyAttribute prop 2025-11-15 13:18:31 +02:00
Yuri Kuznetsov 5ce2e0b6dc 9.2.5 2025-11-14 15:16:05 +02:00
Anton Slyzhko 445ea1f240 Fix incorrect alias in subscriptionExclude join (#3520) 2025-11-14 14:51:47 +02:00
Yuri Kuznetsov 94d2f6aabb array field: override allowCustomOptions false 2025-11-13 18:12:26 +02:00
Yuri Kuznetsov c426a637fe email address add impr 2025-11-13 18:11:02 +02:00
Yuri Kuznetsov 9e78d4cad3 no-select 2025-11-13 18:03:00 +02:00
Yuri Kuznetsov 08fecd2dc0 search menu check icon fix 2025-11-13 17:56:06 +02:00
Yuri Kuznetsov 6dfbb08ce6 Merge branch 'fix' of https://github.com/espocrm/espocrm into fix 2025-11-13 17:25:45 +02:00
Yuri Kuznetsov 610c1004aa jsdoc fix 2025-11-13 17:25:10 +02:00
Shaikh Naasir 26ee35cb7d Fix typo in accessCheckCommand description (#3519)
This pull request fixes a minor typo in the WebSocket schema description:

Changed 'patameter' to 'parameter'.
No functional changes were made. Purely a documentation correction.
2025-11-13 16:07:22 +02:00
Yuri Kuznetsov e311ac6dbb fix width in layouts 2025-11-11 22:00:27 +02:00
Yuri Kuznetsov 9901070fb1 fix layout 2025-11-11 08:14:35 +02:00
Yuri Kuznetsov 379a2d22f3 autocomplete suggestion min width 2025-11-08 20:53:02 +02:00
Yuri Kuznetsov fc7c949cf3 link field: foreign name attribute 2025-11-08 20:31:46 +02:00
Yuri Kuznetsov f1f0f3ea9a process dynamic logic if model default preparator is used 2025-11-07 16:00:16 +02:00
Yuri Kuznetsov c1c4a2bedc controller name check 2025-11-01 16:04:19 +02:00
Yuri Kuznetsov 876b74aefc loader error callback fix 2025-11-01 15:53:04 +02:00
Yuri Kuznetsov 6e2416dc56 fix kanban menu with star 2025-10-30 17:00:57 +02:00
Yuri Kuznetsov 33451a1118 lead capture fields ui fix 2025-10-30 14:45:28 +02:00
bfzgaier 90d7b99936 fix Cannot access protected property id (#3506) 2025-10-30 13:03:44 +02:00
Anton Slyzhko 86215d0094 int getMinValue fix (#3507)
Fix getMinValue logic that overwrote valid minValue due to wrong null comparison
2025-10-30 11:29:47 +02:00
Yuri Kuznetsov 36f03f0a76 fix schema diff for decimal 2025-10-26 12:58:27 +02:00
Yuri Kuznetsov a141a664c6 default decimal to string 2025-10-26 12:38:06 +02:00
Yuri Kuznetsov 0a1b7a9817 create builder method 2025-10-26 11:22:27 +02:00
Yuri Kuznetsov 47b9d35153 default order in relations 2025-10-26 11:13:50 +02:00
Yuri Kuznetsov 169e6b53be default template value 2025-10-26 11:09:06 +02:00
Yuri Kuznetsov 9935a6cd11 default template value 2025-10-26 10:29:05 +02:00
Yuri Kuznetsov 02f1e94c86 fix link category parent field 2025-10-25 17:33:41 +03:00
Yuri Kuznetsov 0d4d4dc9ed category folders fetch on refresh 2025-10-25 17:25:13 +03:00
Yuri Kuznetsov d69cfd8b31 typo 2025-10-25 12:51:26 +03:00
Yuri Kuznetsov 599646c397 metadata aclDependency anyScopeList 2025-10-25 12:47:21 +03:00
Yuri Kuznetsov 4e0d5a2267 schema 2025-10-25 10:22:56 +03:00
Yuri Kuznetsov be6661454b 9.2.4 2025-10-23 10:46:34 +03:00
Yuri Kuznetsov 889a19b9bd fix link name validations 2025-10-22 16:26:31 +03:00
Yuri Kuznetsov ec242152e7 fix convert lead duplicate 2025-10-22 08:49:51 +03:00
Yuri Kuznetsov e0ae3beef9 add LEAD key word for mariadb, patch 2025-10-21 21:04:01 +03:00
Usame Beşir 4c6dd88a2d fix oauth refresh token check (#3498) 2025-10-21 20:00:52 +03:00
Yuri Kuznetsov d13a1ffbc7 9.2.3 2025-10-21 11:30:08 +03:00
Yuri Kuznetsov 986be9f189 scroll gutter for record modals 2025-10-21 10:17:13 +03:00
Yuri Kuznetsov 40f13f7a3d change password template fix 2025-10-21 10:03:51 +03:00
Yuri Kuznetsov 2de3c97f8d edit dashboard templte fix 2025-10-21 10:01:12 +03:00
Yuri Kuznetsov 3b6fd0d363 modal record wide css fix 2025-10-21 10:01:01 +03:00
Yuri Kuznetsov a7586e92a1 audit data 2025-10-20 18:19:22 +03:00
Yuri Kuznetsov 076644db85 auditView param 2025-10-20 13:06:57 +03:00
Yuri Kuznetsov 7eb139f35d small color correction 2025-10-18 19:43:06 +03:00
Yuri Kuznetsov 6f22686319 hazy logo remove 2025-10-18 19:32:18 +03:00
Yuri Kuznetsov 298307f613 color fix 2025-10-18 19:31:43 +03:00
Yuri Kuznetsov 664faa81ca narrower record full width in modal 2025-10-18 11:18:40 +03:00
Yuri Kuznetsov be2dd74c43 do not use full width 2025-10-18 10:33:28 +03:00
Yuri Kuznetsov d2ae211e53 small layout changes 2025-10-18 10:26:57 +03:00
Yuri Kuznetsov e522d8d709 modal size changes 2025-10-18 10:26:51 +03:00
Yuri Kuznetsov c5c191faa6 audit log email template 2025-10-17 18:00:32 +03:00
Yuri Kuznetsov 3ce4cd3bfc btm sm width 32px 2025-10-17 17:38:44 +03:00
Yuri Kuznetsov e620a1195f dropdown-link-active-bg variable 2025-10-17 12:25:41 +03:00
Yuri Kuznetsov 880478689f foreign array fix 2025-10-16 15:47:31 +03:00
Yuri Kuznetsov 0d5d1710bf schema 2025-10-16 14:49:58 +03:00
Yuri Kuznetsov 0c7d795081 compact-form class 2025-10-16 13:06:28 +03:00
Yuri Kuznetsov 624bdb7192 less ref 2025-10-15 18:49:53 +03:00
Yuri Kuznetsov db769d9c3e css ref 2025-10-15 18:33:36 +03:00
Yuri Kuznetsov 67045b12ac cleanup 2025-10-15 16:30:05 +03:00
Yuri Kuznetsov e67ab0fd4d whitespace removal 2025-10-15 12:56:15 +03:00
Yuri Kuznetsov 99c64770dd theme name in body 2025-10-15 10:51:48 +03:00
Yuri Kuznetsov ed60efcad4 hazyblue color tweak 2025-10-15 10:46:49 +03:00
Yuri Kuznetsov 443af201ed fix generate password special characters null 2025-10-14 20:34:55 +03:00
Yuri Kuznetsov 7a61dd096e fix ldap dynamic logic 2025-10-14 20:07:43 +03:00
Yuri Kuznetsov 9404c1d165 email update filter fix 2025-10-13 14:31:22 +03:00
Yuri Kuznetsov 2a76db6899 fix corrupted defs 2025-10-11 09:28:17 +03:00
Yuri Kuznetsov 915283d288 use all primary filter 2025-10-10 11:46:54 +03:00
Yuri Kuznetsov 483e6e9d5b apply all filter 2025-10-10 11:35:55 +03:00
Yuri Kuznetsov 046d94ceb8 update note ui impr 2025-10-09 10:42:51 +03:00
Yuri Kuznetsov 062008374d comment 2025-10-09 10:12:25 +03:00
Yuri Kuznetsov df2f857197 prevent action handler fired twice 2025-10-09 09:24:09 +03:00
Yuri Kuznetsov 25c88259f8 secret function change 2025-10-04 13:23:43 +03:00
Usame Beşir 2ddfa39966 Add getSecret formula function (#3481)
* add getSecret formula function

* simplify getSecret method by removing unnecessary null check

* update process method to allow nullable return type

* rename getSecret function to ext\appSecret\get
2025-10-04 13:23:20 +03:00
Yuri Kuznetsov ebec204d3a external account client manager using factory 2025-10-02 15:55:47 +03:00
Yuri Kuznetsov b2a4f80408 9.2.2 2025-10-02 11:37:07 +03:00
Yuri Kuznetsov 9153fbf280 limit fields for status 2025-10-02 10:52:04 +03:00
Yuri Kuznetsov cb16bc9254 ref 2025-10-02 10:43:35 +03:00
Yuri Kuznetsov dc8f674248 notification index 2025-10-02 10:28:43 +03:00
Yuri Kuznetsov 803b07866a cast to object 2025-10-01 19:15:08 +03:00
Yuri Kuznetsov 50f422ded8 ignore state.php 2025-10-01 10:24:22 +03:00
Yuri Kuznetsov 25357d75d3 9.2.1 2025-09-30 18:40:40 +03:00
Yuri Kuznetsov 2cea39b5a9 do not use suppress amd in runtime 2025-09-30 17:31:29 +03:00
114 changed files with 1593 additions and 873 deletions
+1
View File
@@ -7,6 +7,7 @@
/data/config-internal.php
/data/config-override.php
/data/config-internal-override.php
/data/state.php
/data/tmp/*
/build
/node_modules
@@ -73,12 +73,18 @@ class UserColumnsLoader implements Loader
return;
}
$entity->set([
$values = [
Email::USERS_COLUMN_IS_READ => $emailUser->get(Email::USERS_COLUMN_IS_READ),
Email::USERS_COLUMN_IS_IMPORTANT => $emailUser->get(Email::USERS_COLUMN_IS_IMPORTANT),
Email::USERS_COLUMN_IN_TRASH => $emailUser->get(Email::USERS_COLUMN_IN_TRASH),
Email::USERS_COLUMN_IN_ARCHIVE => $emailUser->get(Email::USERS_COLUMN_IN_ARCHIVE),
'isUsersSent' => $entity->getSentBy()?->getId() === $this->user->getId(),
]);
];
$entity->setMultiple($values);
foreach ($values as $key => $value) {
$entity->setFetched($key, $value);
}
}
}
@@ -31,8 +31,10 @@ namespace Espo\Classes\RecordHooks\Email;
use Espo\Core\Mail\EmailSender;
use Espo\Core\Name\Field;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Record\Hook\SaveHook;
use Espo\Core\Utils\FieldUtil;
use Espo\Core\Utils\Metadata;
use Espo\Core\Utils\SystemUser;
use Espo\Entities\Email;
use Espo\Entities\User;
@@ -54,7 +56,8 @@ class BeforeUpdate implements SaveHook
public function __construct(
private User $user,
private EntityManager $entityManager,
private FieldUtil $fieldUtil
private FieldUtil $fieldUtil,
private Metadata $metadata,
) {}
public function process(Entity $entity): void
@@ -125,18 +128,24 @@ class BeforeUpdate implements SaveHook
private function clearEntityForUpdate(Email $email): void
{
$fieldDefsList = $this->entityManager
$entityDefs = $this->entityManager
->getDefs()
->getEntity(Email::ENTITY_TYPE)
->getFieldList();
->getEntity(Email::ENTITY_TYPE);
foreach ($fieldDefsList as $fieldDefs) {
foreach ($entityDefs->getFieldList() as $fieldDefs) {
$field = $fieldDefs->getName();
if ($fieldDefs->getParam('isCustom')) {
continue;
}
if (
$fieldDefs->getType() === FieldType::LINK_MULTIPLE &&
$this->metadata->get("entityDefs.Email.links.$field.isCustom")
) {
continue;
}
if (in_array($field, $this->allowedForUpdateFieldList)) {
continue;
}
@@ -144,7 +153,9 @@ class BeforeUpdate implements SaveHook
$attributeList = $this->fieldUtil->getAttributeList(Email::ENTITY_TYPE, $field);
foreach ($attributeList as $attribute) {
$email->clear($attribute);
if ($email->isAttributeChanged($attribute) && $email->isAttributeWritten($attribute)) {
$email->set($attribute, $email->getFetched($attribute));
}
}
}
}
@@ -248,10 +248,8 @@ class ClientManager
$params[$k] = $v;
}
$client = new $className($oauth2Client, $params, $this);
if ($this->injectableFactory) {
$this->injectableFactory->createWith($className, [
$client = $this->injectableFactory->createWith($className, [
'client' => $oauth2Client,
'params' => $params,
'manager' => $this,
@@ -0,0 +1,67 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* 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\Formula\Functions\ExtGroup\SecretGroup;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Tools\AppSecret\SecretProvider;
/**
* @noinspection PhpUnused
*/
class GetType implements Func
{
public function __construct(private SecretProvider $secretProvider)
{}
public function process(EvaluatedArgumentList $arguments): string
{
if (count($arguments) < 1) {
throw TooFewArguments::create(1);
}
$string = $arguments[0];
if (!is_string($string)) {
throw BadArgumentType::create(1, 'string');
}
$secret = $this->secretProvider->get($string);
if ($secret === null) {
throw new Error("No secret '$string'.");
}
return $secret;
}
}
@@ -36,6 +36,7 @@ use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Formula\Functions\RecordGroup\Util\FindQueryUtil;
use Espo\Core\Di;
use Espo\Core\Select\Primary\Filters\All;
use Espo\Core\Select\SelectBuilderFactory;
/**
@@ -68,6 +69,7 @@ class CountType extends BaseFunction implements
$builder = $this->injectableFactory->create(SelectBuilderFactory::class)
->create()
->forUser($this->user)
->withPrimaryFilter(All::NAME)
->from($entityType);
(new FindQueryUtil())->applyFilter($builder, $filter, 2);
@@ -36,6 +36,7 @@ use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Di;
use Espo\Core\Formula\Functions\RecordGroup\Util\FindQueryUtil;
use Espo\Core\Select\Primary\Filters\All;
use Espo\Core\Select\SelectBuilderFactory;
/**
@@ -68,6 +69,7 @@ class ExistsType extends BaseFunction implements
$builder = $this->injectableFactory->create(SelectBuilderFactory::class)
->create()
->forUser($this->user)
->withPrimaryFilter(All::NAME)
->from($entityType);
(new FindQueryUtil())->applyFilter($builder, $filter, 2);
@@ -38,6 +38,7 @@ use Espo\Core\Formula\Exceptions\Error as FormulaError;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\Formula\Functions\RecordGroup\Util\FindQueryUtil;
use Espo\Core\Select\Primary\Filters\All;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
@@ -92,6 +93,7 @@ class FindManyType implements Func
$builder = $this->selectBuilderFactory
->create()
->withPrimaryFilter(All::NAME)
->from($entityType);
$whereClause = [];
@@ -36,6 +36,7 @@ use Espo\Core\Formula\Exceptions\Error as FormulaError;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Di;
use Espo\Core\Formula\Functions\RecordGroup\Util\FindQueryUtil;
use Espo\Core\Select\Primary\Filters\All;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Query\Part\Order;
@@ -65,6 +66,7 @@ class FindOneType extends BaseFunction implements
$builder = $this->injectableFactory->create(SelectBuilderFactory::class)
->create()
->forUser($this->user)
->withPrimaryFilter(All::NAME)
->from($entityType);
$whereClause = [];
@@ -41,6 +41,7 @@ use Espo\Core\Formula\ArgumentList;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Di;
use Espo\Core\Select\Helpers\RandomStringGenerator;
use Espo\Core\Select\Primary\Filters\All;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Name\Attribute;
@@ -165,6 +166,7 @@ class FindRelatedManyType extends BaseFunction implements
$builder = $this->injectableFactory->create(SelectBuilderFactory::class)
->create()
->forUser($this->user)
->withPrimaryFilter(All::NAME)
->from($foreignEntityType);
$whereClause = [];
@@ -32,6 +32,7 @@ namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\Select\Primary\Filters\All;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Name\Attribute;
@@ -151,6 +152,7 @@ class FindRelatedOneType extends BaseFunction implements
$builder = $this->injectableFactory->create(SelectBuilderFactory::class)
->create()
->forUser($this->user)
->withPrimaryFilter(All::NAME)
->from($foreignEntityType);
$whereClause = [];
+1 -1
View File
@@ -95,7 +95,7 @@ class Entity extends BaseEntity
throw new LogicException("No entity-manager.");
}
$toSetFetched = !$this->isNew() && !$this->hasFetched($idAttribute);
$toSetFetched = !$this->isNew() && !$this->isAttributeChanged($idAttribute);
if (!$parentId || !$parentType) {
/** @noinspection PhpRedundantOptionalArgumentInspection */
@@ -1358,16 +1358,22 @@ class ItemGeneralConverter implements ItemConverter
$nearKey = $defs->getMidKey();
$middleEntityType = ucfirst($defs->getRelationshipName());
$conditions = [
"$alias.$nearKey:" => Attribute::ID,
"$alias.deleted" => false,
];
foreach ($defs->getConditions() as $k => $v) {
$conditions["$alias.$k"] = $v;
}
// The foreign table is not joined as it would perform much slower.
// Trade off is that if a foreign record is deleted but the middle table
// is not yet deleted, it will give a non-actual result.
$subQuery = QueryBuilder::create()
->select(Attribute::ID)
->from($this->entityType)
->leftJoin($middleEntityType, $alias, [
"$alias.$nearKey:" => Attribute::ID,
"$alias.deleted" => false,
])
->leftJoin($middleEntityType, $alias, $conditions)
->where(["$alias.$key" => null])
->build();
@@ -1410,16 +1416,22 @@ class ItemGeneralConverter implements ItemConverter
$nearKey = $defs->getMidKey();
$middleEntityType = ucfirst($defs->getRelationshipName());
$conditions = [
"$alias.$nearKey:" => Attribute::ID,
"$alias.deleted" => false,
];
foreach ($defs->getConditions() as $k => $v) {
$conditions["$alias.$k"] = $v;
}
// The foreign table is not joined as it would perform much slower.
// Trade off is that if a foreign record is deleted but the middle table
// is not yet deleted, it will give a non-actual result.
$subQuery = QueryBuilder::create()
->select(Attribute::ID)
->from($this->entityType)
->leftJoin($middleEntityType, $alias, [
"$alias.$nearKey:" => Attribute::ID,
"$alias.deleted" => false,
])
->leftJoin($middleEntityType, $alias, $conditions)
->where(["$alias.$key!=" => null])
->build();
@@ -1476,7 +1488,6 @@ class ItemGeneralConverter implements ItemConverter
if ($relationType == Entity::MANY_MANY) {
$key = $defs->getForeignMidKey();
$nearKey = $defs->getMidKey();
// IN-sub-query performs faster than EXISTS on MariaDB when multiple IDs.
// Left-join performs faster than inner-join.
@@ -1489,12 +1500,6 @@ class ItemGeneralConverter implements ItemConverter
->from($this->entityType)
->leftJoin(
Join::create($link, $alias)
->withConditions(
Cond::equal(
Cond::column("$alias.$nearKey"),
Cond::column(Attribute::ID)
)
)
->withOnlyMiddle()
)
->where(["$alias.$key" => $value])
@@ -0,0 +1,52 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* 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\Utils\Database\Dbal\Platforms\Keywords;
use Doctrine\DBAL\Platforms\Keywords\MariaDBKeywords;
/**
* 'LEAD' happened to be a reserved words on some environments.
*/
class MariaDb102Keywords extends MariaDBKeywords
{
/** @deprecated */
public function getName(): string
{
return 'MariaDb102';
}
protected function getKeywords(): array
{
return [
...parent::getKeywords(),
'LEAD',
];
}
}
@@ -82,6 +82,14 @@ class Currency implements FieldConverter
->withDbType($dbType)
->withParam(AttributeParam::PRECISION, $precision)
->withParam(AttributeParam::SCALE, $scale);
$defaultValue = $fieldDefs->getParam(AttributeParam::DEFAULT);
if (is_int($defaultValue) || is_float($defaultValue)) {
$defaultValue = number_format($defaultValue, $scale, '.', '');
$amountDefs = $amountDefs->withParam(AttributeParam::DEFAULT, $defaultValue);
}
}
if ($fieldDefs->isNotStorable()) {
@@ -61,6 +61,12 @@ class RelationConverter
RelationParam::INDEXES,
];
/** @var string[] */
private $manyMergeParams = [
RelationParam::ORDER_BY,
RelationParam::ORDER,
];
public function __construct(
private Metadata $metadata,
private InjectableFactory $injectableFactory,
@@ -112,7 +118,7 @@ class RelationConverter
$raw = $convertedEntityDefs->toAssoc();
if (isset($raw[EntityParam::RELATIONS][$name])) {
$this->mergeParams($raw[EntityParam::RELATIONS][$name], $params, $foreignParams ?? []);
$this->mergeParams($raw[EntityParam::RELATIONS][$name], $params, $foreignParams ?? [], $linkType);
$this->correct($raw[EntityParam::RELATIONS][$name]);
}
@@ -172,9 +178,18 @@ class RelationConverter
* @param array<string, mixed> $params
* @param array<string, mixed> $foreignParams
*/
private function mergeParams(array &$relationDefs, array $params, array $foreignParams): void
private function mergeParams(array &$relationDefs, array $params, array $foreignParams, string $linkType): void
{
foreach ($this->mergeParams as $name) {
$mergeParams = $this->mergeParams;
if (
$linkType === RelationType::HAS_MANY ||
$linkType === RelationType::HAS_CHILDREN
) {
$mergeParams = array_merge($mergeParams, $this->manyMergeParams);
}
foreach ($mergeParams as $name) {
$additionalParam = $this->getMergedParam($name, $params, $foreignParams);
if ($additionalParam === null) {
@@ -227,6 +242,13 @@ class RelationConverter
*/
private function correct(array &$relationDefs): void
{
if (
isset($relationDefs[RelationParam::ORDER]) &&
is_string($relationDefs[RelationParam::ORDER])
) {
$relationDefs[RelationParam::ORDER] = strtoupper($relationDefs[RelationParam::ORDER]);
}
if (!isset($relationDefs[RelationParam::ADDITIONAL_COLUMNS])) {
return;
}
@@ -129,6 +129,12 @@ class LogicDefsBc implements AdditionalBuilder
$item = $subDefs->$subKey;
$logicDefs->$key ??= (object) [];
// Fix if corrupted.
if (is_array($logicDefs->$key)) {
$logicDefs->$key = (object) [];
}
$logicDefs->$key->$name ??= (object) [];
$logicDefs->$key->$name->$subKey = $item !== null ?
+18
View File
@@ -126,6 +126,24 @@ class Email extends Entity
return parent::has($attribute);
}
public function getFetched(string $attribute): mixed
{
if ($attribute === 'subject') {
return $this->getFetched(Field::NAME);
}
return parent::getFetched($attribute);
}
public function isAttributeChanged(string $name): bool
{
if ($name === 'subject') {
$name = Field::NAME;
}
return parent::isAttributeChanged($name);
}
/** @noinspection PhpUnused */
protected function _setSubject(?string $value): void
{
@@ -1,15 +1,14 @@
[
{
"label":"",
"rows":[
[{"name":"name", "fullWidth": true}],
[{"name":"name"}],
[{"name":"status"}, {"name":"direction"}],
[{"name":"dateStart", "fullWidth": true}],
[{"name":"duration", "fullWidth": true}],
[{"name":"dateEnd", "fullWidth": true}],
[{"name":"parent", "fullWidth": true}],
[{"name":"reminders", "fullWidth": true}],
[{"name":"description", "fullWidth": true}]
[{"name":"dateStart"}],
[{"name":"duration"}, false],
[{"name":"dateEnd"}],
[{"name":"parent"}],
[{"name":"reminders"}],
[{"name":"description"}]
]
}
]
]
@@ -1,14 +1,12 @@
[
{
"label":"",
"rows":[
[{"name":"name", "fullWidth": true}],
[{"name":"status"}, {"name":"priority"}],
[{"name":"type"}, {"name":"number"}],
[{"name":"account", "fullWidth": true}],
[{"name":"contacts", "fullWidth": true}],
[{"name":"description", "fullWidth": true}],
[{"name":"attachments", "fullWidth": true}]
[{"name": "name"}],
[{"name": "status"}, {"name": "priority"}],
[{"name": "type"}, {"name": "number"}],
[{"name": "account"}, {"name": "contacts"}],
[{"name": "description"}],
[{"name": "attachments"}]
]
}
]
]
@@ -1,5 +1,5 @@
[
{"name":"name","link":true, "width": "40"},
{"name":"name", "link":true, "width": 40},
{"name":"account"},
{"name":"emailAddress", "notSortable": true}
]
@@ -1,15 +1,14 @@
[
{
"label":"",
"rows":[
[{"name":"name", "fullWidth": true}],
[{"name":"status", "fullWidth": true}],
[{"name":"dateStart", "fullWidth": true}],
[{"name":"duration", "fullWidth": true}],
[{"name":"dateEnd", "fullWidth": true}],
[{"name":"parent", "fullWidth": true}],
[{"name":"reminders", "fullWidth": true}],
[{"name":"description", "fullWidth": true}]
[{"name":"name"}],
[{"name":"status"}, false],
[{"name":"dateStart"}],
[{"name":"duration"}, false],
[{"name":"dateEnd"}],
[{"name":"parent"}],
[{"name":"reminders"}],
[{"name":"description"}]
]
}
]
]
@@ -227,6 +227,7 @@
"type": "enum",
"notStorable": true,
"orderDisabled": true,
"directUpdateDisabled": true,
"layoutMassUpdateDisabled": true,
"layoutDetailDisabled": true,
"layoutAvailabilityList": ["listForContact"],
@@ -84,25 +84,18 @@ class ConvertService
{
$lead = $this->getLead($id);
$duplicateList = [];
$skipSave = false;
$duplicateCheck = !$params->skipDuplicateCheck();
if (!$params->skipDuplicateCheck()) {
$this->processDuplicateCheck($records);
}
$account = $this->processAccount(
lead: $lead,
records: $records,
duplicateCheck: $duplicateCheck,
duplicateList: $duplicateList,
skipSave: $skipSave,
);
$contact = $this->processContact(
lead: $lead,
records: $records,
duplicateCheck: $duplicateCheck,
duplicateList: $duplicateList,
skipSave: $skipSave,
account: $account,
);
@@ -111,17 +104,10 @@ class ConvertService
$opportunity = $this->processOpportunity(
lead: $lead,
records: $records,
duplicateCheck: $duplicateCheck,
duplicateList: $duplicateList,
skipSave: $skipSave,
account: $account,
contact: $contact,
);
if ($duplicateCheck && count($duplicateList)) {
throw ConflictSilent::createWithBody('duplicate', Json::encode($duplicateList));
}
$lead->setStatus(Lead::STATUS_CONVERTED);
$this->entityManager->saveEntity($lead);
@@ -280,19 +266,12 @@ class ConvertService
}
/**
* @param object[] $duplicateList
* @throws Forbidden
* @throws BadRequest
* @throws Conflict
*/
private function processAccount(
Lead $lead,
Values $records,
bool $duplicateCheck,
array &$duplicateList,
bool &$skipSave,
): ?Account {
private function processAccount(Lead $lead, Values $records): ?Account
{
if (!$records->has(Account::ENTITY_TYPE)) {
return null;
}
@@ -305,35 +284,14 @@ class ConvertService
$service = $this->recordServiceContainer->getByClass(Account::class);
$account = $this->entityManager->getRDBRepositoryByClass(Account::class)->getNew();
$account->set($values);
if ($duplicateCheck) {
foreach ($service->findDuplicates($account) ?? [] as $e) {
$duplicateList[] = (object) [
'id' => $e->getId(),
'name' => $e->getName(),
'_entityType' => $e->getEntityType(),
];
$skipSave = true;
}
}
if ($skipSave) {
return null;
}
$account = $service->create($values, CreateParams::create()->withSkipDuplicateCheck());
$lead->set('createdAccountId', $account->getId());
$lead->setCreatedAccount($account);
return $account;
}
/**
* @param object[] $duplicateList
* @throws Forbidden
* @throws BadRequest
* @throws Conflict
@@ -341,9 +299,6 @@ class ConvertService
private function processContact(
Lead $lead,
Values $records,
bool $duplicateCheck,
array &$duplicateList,
bool &$skipSave,
?Account $account,
): ?Contact {
@@ -363,25 +318,6 @@ class ConvertService
$service = $this->recordServiceContainer->getByClass(Contact::class);
$contact = $this->entityManager->getRDBRepositoryByClass(Contact::class)->getNew();
$contact->set($values);
if ($duplicateCheck) {
foreach ($service->findDuplicates($contact) ?? [] as $e) {
$duplicateList[] = (object) [
'id' => $e->getId(),
'name' => $e->getName(),
'_entityType' => $e->getEntityType(),
];
$skipSave = true;
}
}
if ($skipSave) {
return null;
}
$contact = $service->create($values, CreateParams::create()->withSkipDuplicateCheck());
$lead->set('createdContactId', $contact->getId());
@@ -391,14 +327,13 @@ class ConvertService
$contact->getAccount() &&
!$contact->getAccount()->get('originalLeadId')
) {
$lead->set('createdAccountId', $contact->getAccount()->getId());
$lead->setCreatedAccount($contact->getAccount());
}
return $contact;
}
/**
* @param object[] $duplicateList
* @throws Forbidden
* @throws BadRequest
* @throws Conflict
@@ -406,9 +341,6 @@ class ConvertService
private function processOpportunity(
Lead $lead,
Values $records,
bool $duplicateCheck,
array &$duplicateList,
bool &$skipSave,
?Account $account,
?Contact $contact,
): ?Opportunity {
@@ -433,30 +365,10 @@ class ConvertService
$service = $this->recordServiceContainer->getByClass(Opportunity::class);
$opportunity = $this->entityManager->getRDBRepositoryByClass(Opportunity::class)->getNew();
$opportunity->set($values);
if ($duplicateCheck) {
foreach ($service->findDuplicates($opportunity) ?? [] as $e) {
$duplicateList[] = (object) [
'id' => $e->getId(),
'name' => $e->getName(),
'_entityType' => $e->getEntityType(),
];
$skipSave = true;
}
}
if ($skipSave) {
return null;
}
$opportunity = $service->create($values, CreateParams::create()->withSkipDuplicateCheck());
if ($contact) {
$this->entityManager
->getRDBRepository(Contact::ENTITY_TYPE)
->getRelation($contact, 'opportunities')
->relate($opportunity);
}
@@ -532,7 +444,6 @@ class ConvertService
foreach ($calls as $call) {
if ($contact && $contact->hasId()) {
$this->entityManager
->getRDBRepository(Call::ENTITY_TYPE)
->getRelation($call, 'contacts')
->relate($contact);
}
@@ -579,14 +490,12 @@ class ConvertService
foreach ($documents as $document) {
if ($account && $account->hasId()) {
$this->entityManager
->getRDBRepository(Document::ENTITY_TYPE)
->getRelation($document, 'accounts')
->relate($account);
}
if ($opportunity && $opportunity->hasId()) {
$this->entityManager
->getRDBRepository(Document::ENTITY_TYPE)
->getRelation($document, 'opportunities')
->relate($opportunity);
}
@@ -637,4 +546,63 @@ class ConvertService
return $account;
}
/**
* @throws Conflict
*/
private function processDuplicateCheck(Values $records): void
{
$duplicateList = [];
if ($records->has(Account::ENTITY_TYPE)) {
$accountService = $this->recordServiceContainer->getByClass(Account::class);
$account = $this->entityManager->getRDBRepositoryByClass(Account::class)->getNew();
$account->set($records->get(Account::ENTITY_TYPE));
foreach ($accountService->findDuplicates($account) ?? [] as $e) {
$duplicateList[] = (object) [
'id' => $e->getId(),
'name' => $e->getName(),
'_entityType' => $e->getEntityType(),
];
}
}
if ($records->has(Contact::ENTITY_TYPE)) {
$contactService = $this->recordServiceContainer->getByClass(Contact::class);
$contact = $this->entityManager->getRDBRepositoryByClass(Contact::class)->getNew();
$contact->set($records->get(Contact::ENTITY_TYPE));
foreach ($contactService->findDuplicates($contact) ?? [] as $e) {
$duplicateList[] = (object) [
'id' => $e->getId(),
'name' => $e->getName(),
'_entityType' => $e->getEntityType(),
];
}
}
if ($records->has(Opportunity::ENTITY_TYPE)) {
$opportunityService = $this->recordServiceContainer->getByClass(Opportunity::class);
$opportunity = $this->entityManager->getRDBRepositoryByClass(Opportunity::class)->getNew();
$opportunity->set($records->get(Opportunity::ENTITY_TYPE));
foreach ($opportunityService->findDuplicates($opportunity) ?? [] as $e) {
$duplicateList[] = (object) [
'id' => $e->getId(),
'name' => $e->getName(),
'_entityType' => $e->getEntityType(),
];
}
}
if (!count($duplicateList)) {
return;
}
throw ConflictSilent::createWithBody('duplicate', Json::encode($duplicateList));
}
}
@@ -93,4 +93,18 @@ class RelationParam
* Deferred load.
*/
public const DEFERRED_LOAD = 'deferredLoad';
/**
* Default order by. Applied on the entity level.
*
* @since 9.2.5
*/
public const ORDER_BY = 'orderBy';
/**
* Default order. Applied on the entity level.
*
* @since 9.2.5
*/
public const ORDER = 'order';
}
+23 -15
View File
@@ -32,12 +32,14 @@ namespace Espo\ORM\Relation;
use Espo\ORM\BaseEntity;
use Espo\ORM\Defs\Defs;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Defs\RelationDefs;
use Espo\ORM\Entity;
use Espo\ORM\EntityCollection;
use Espo\ORM\EntityManager;
use Espo\ORM\Mapper\RDBMapper;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Query\Part\Order;
use Espo\ORM\Repository\RDBRelationSelectBuilder;
use Espo\ORM\Type\RelationType;
use LogicException;
use RuntimeException;
@@ -293,22 +295,11 @@ class RDBRelations implements Relations
->getEntity($this->getEntity()->getEntityType())
->getRelation($relation);
$orderBy = null;
$order = null;
$builder = $this->entityManager
->getRelation($this->getEntity(), $relation)
->createBuilder();
if ($relationDefs->getParam('orderBy')) {
$orderBy = $relationDefs->getParam('orderBy');
if ($relationDefs->getParam('order')) {
$order = strtoupper($relationDefs->getParam('order')) === Order::DESC ? Order::DESC : Order::ASC;
}
}
$builder = $this->entityManager->getRelation($this->getEntity(), $relation);
if ($orderBy) {
$builder->order($orderBy, $order);
}
$this->applyOrder($relationDefs, $builder);
$collection = $builder->find();
@@ -398,4 +389,21 @@ class RDBRelations implements Relations
return $foreignEntity;
}
private function applyOrder(RelationDefs $relationDefs, RDBRelationSelectBuilder $builder): void
{
$orderBy = $relationDefs->getParam(RelationParam::ORDER_BY);
if (!$orderBy) {
return;
}
$order = $relationDefs->getParam(RelationParam::ORDER);
if ($order !== null) {
$order = strtoupper($order) === Order::DESC ? Order::DESC : Order::ASC;
}
$builder->order($orderBy, $order);
}
}
@@ -51,7 +51,7 @@ use RuntimeException;
/**
* An access point for a specific relation of a record.
*
* @template TEntity of Entity
* @template TEntity of Entity = Entity
*/
class RDBRelation
{
@@ -95,6 +95,18 @@ class RDBRelation
}
}
/**
* Create a select builder.
*
* @return Builder<TEntity>
*
* @since 9.2.5
*/
public function createBuilder(): Builder
{
return $this->createSelectBuilder();
}
/**
* Create a select builder.
*
@@ -53,7 +53,7 @@ use InvalidArgumentException;
/**
* Builds select parameters for related records for RDB repository.
*
* @template TEntity of Entity
* @template TEntity of Entity = Entity
*/
class RDBRelationSelectBuilder
{
@@ -234,7 +234,7 @@ class RDBRepository implements Repository
*/
public function find(): EntityCollection|SthCollection
{
return $this->createSelectBuilder()->find();
return $this->createBuilder()->find();
}
/**
@@ -273,7 +273,7 @@ class RDBRepository implements Repository
*/
public function count(): int
{
return $this->createSelectBuilder()->count();
return $this->createBuilder()->count();
}
/**
@@ -283,7 +283,7 @@ class RDBRepository implements Repository
*/
public function max(string $attribute)
{
return $this->createSelectBuilder()->max($attribute);
return $this->createBuilder()->max($attribute);
}
/**
@@ -293,7 +293,7 @@ class RDBRepository implements Repository
*/
public function min(string $attribute)
{
return $this->createSelectBuilder()->min($attribute);
return $this->createBuilder()->min($attribute);
}
/**
@@ -303,7 +303,7 @@ class RDBRepository implements Repository
*/
public function sum(string $attribute)
{
return $this->createSelectBuilder()->sum($attribute);
return $this->createBuilder()->sum($attribute);
}
/**
@@ -334,7 +334,7 @@ class RDBRepository implements Repository
*/
public function join($target, ?string $alias = null, $conditions = null): RDBSelectBuilder
{
return $this->createSelectBuilder()->join($target, $alias, $conditions);
return $this->createBuilder()->join($target, $alias, $conditions);
}
/**
@@ -348,7 +348,7 @@ class RDBRepository implements Repository
*/
public function leftJoin($target, ?string $alias = null, $conditions = null): RDBSelectBuilder
{
return $this->createSelectBuilder()->leftJoin($target, $alias, $conditions);
return $this->createBuilder()->leftJoin($target, $alias, $conditions);
}
/**
@@ -358,7 +358,7 @@ class RDBRepository implements Repository
*/
public function distinct(): RDBSelectBuilder
{
return $this->createSelectBuilder()->distinct();
return $this->createBuilder()->distinct();
}
/**
@@ -368,7 +368,7 @@ class RDBRepository implements Repository
*/
public function forUpdate(): RDBSelectBuilder
{
return $this->createSelectBuilder()->forUpdate();
return $this->createBuilder()->forUpdate();
}
/**
@@ -378,7 +378,7 @@ class RDBRepository implements Repository
*/
public function sth(): RDBSelectBuilder
{
return $this->createSelectBuilder()->sth();
return $this->createBuilder()->sth();
}
/**
@@ -395,7 +395,7 @@ class RDBRepository implements Repository
*/
public function where($clause = [], $value = null): RDBSelectBuilder
{
return $this->createSelectBuilder()->where($clause, $value);
return $this->createBuilder()->where($clause, $value);
}
/**
@@ -412,7 +412,7 @@ class RDBRepository implements Repository
*/
public function having($clause = [], $value = null): RDBSelectBuilder
{
return $this->createSelectBuilder()->having($clause, $value);
return $this->createBuilder()->having($clause, $value);
}
/**
@@ -432,7 +432,7 @@ class RDBRepository implements Repository
*/
public function order($orderBy = Attribute::ID, $direction = null): RDBSelectBuilder
{
return $this->createSelectBuilder()->order($orderBy, $direction);
return $this->createBuilder()->order($orderBy, $direction);
}
/**
@@ -442,7 +442,7 @@ class RDBRepository implements Repository
*/
public function limit(?int $offset = null, ?int $limit = null): RDBSelectBuilder
{
return $this->createSelectBuilder()->limit($offset, $limit);
return $this->createBuilder()->limit($offset, $limit);
}
/**
@@ -462,7 +462,7 @@ class RDBRepository implements Repository
*/
public function select($select = [], ?string $alias = null): RDBSelectBuilder
{
return $this->createSelectBuilder()->select($select, $alias);
return $this->createBuilder()->select($select, $alias);
}
/**
@@ -479,15 +479,17 @@ class RDBRepository implements Repository
*/
public function group($groupBy): RDBSelectBuilder
{
return $this->createSelectBuilder()->group($groupBy);
return $this->createBuilder()->group($groupBy);
}
/**
* Create a select builder.
*
* @return RDBSelectBuilder<TEntity>
*
* @since 9.2.5
*/
protected function createSelectBuilder(): RDBSelectBuilder
public function createBuilder(): RDBSelectBuilder
{
/** @var RDBSelectBuilder<TEntity> $builder */
$builder = new RDBSelectBuilder($this->entityManager, $this->entityType);
@@ -495,6 +497,19 @@ class RDBRepository implements Repository
return $builder;
}
/**
* Create a select builder.
*
* @return RDBSelectBuilder<TEntity>
*
* @deprecated Since v9.2.5. Use `createBuilder` instead.
* @todo Remove in v10.0.
*/
protected function createSelectBuilder(): RDBSelectBuilder
{
return $this->createBuilder();
}
/**
* Use hooks instead.
*
+13 -1
View File
@@ -186,13 +186,21 @@ class Email extends Database implements
return;
}
$setFetched = !$entity->isAttributeChanged($type . 'EmailAddressesNames');
$addresses = [];
foreach (get_object_vars($names) as $address) {
$addresses[] = $address;
}
$entity->set($type, implode(';', $addresses));
$value = implode(';', $addresses);
$entity->set($type, $value);
if ($setFetched) {
$entity->setFetched($type, $value);
}
}
/**
@@ -289,6 +297,10 @@ class Email extends Database implements
$entity->set('nameHash', $nameHash);
$entity->set('typeHash', $typeHash);
$entity->set('idHash', $idHash);
$entity->setFetched('nameHash', $nameHash);
$entity->setFetched('typeHash', $typeHash);
$entity->setFetched('idHash', $idHash);
}
/**
@@ -27,16 +27,14 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
// To add aliases for backward compatibility.
// Aliases for backward compatibility or patches.
$map = [
'Espo\\Core\\Hook\\Deprecations\\BaseHook' => 'Espo\\Core\\Hooks\\Base',
'Espo\\Core\\Hooks\\Base' => 'Espo\\Core\\Hook\\Deprecations\\BaseHook',
'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDb102Keywords' => 'Espo\\Core\\Utils\\Database\\Dbal\\Platforms\\Keywords\\MariaDb102Keywords',
];
/** @phpstan-ignore-next-line */
foreach ($map as $item) {
$className = $item[0];
$alias = $item[1];
foreach ($map as $alias => $className) {
if (!class_exists($className)) {
continue;
}
@@ -1,6 +1,6 @@
[
{"name":"action", "width": "18", "notSortable": true},
{"name":"targetType", "width": "20", "notSortable": true},
{"name":"action", "width": 18, "notSortable": true},
{"name":"targetType", "width": 20, "notSortable": true},
{"name":"target", "notSortable": true},
{"name":"createdAt", "width": "18", "align": "right", "notSortable": true}
]
{"name":"createdAt", "width": 18, "align": "right", "notSortable": true}
]
@@ -1,7 +1,7 @@
[
{"name":"user"},
{"name":"isActive", "widthPx": "100"},
{"name":"ipAddress", "width": "17"},
{"name":"createdAt", "width": "19"},
{"name":"lastAccess", "width": "19"}
{"name":"isActive", "widthPx": 100},
{"name":"ipAddress", "width": 17},
{"name":"createdAt", "width": 19},
{"name":"lastAccess", "width": 19}
]
@@ -1,7 +1,7 @@
[
{"name":"user"},
{"name":"isActive", "widthPx": "100"},
{"name":"ipAddress", "width": "17"},
{"name":"createdAt", "width": "19"},
{"name":"lastAccess", "width": "19"}
{"name":"isActive", "widthPx": 100},
{"name":"ipAddress", "width": 17},
{"name":"createdAt", "width": 19},
{"name":"lastAccess", "width": 19}
]
@@ -1,5 +1,5 @@
[
{"name":"target", "notSortable": true},
{"name":"isCreated", "width": "25", "notSortable": true},
{"name":"createdAt", "width": "30", "notSortable": true}
{"name":"isCreated", "width": 25, "notSortable": true},
{"name":"createdAt", "width": 30, "notSortable": true}
]
@@ -631,6 +631,11 @@
"insertText": "ext\\oauth\\getAccessToken(ID)",
"returnType": "string",
"unsafe": true
},
{
"name": "ext\\appSecret\\get",
"insertText": "ext\\appSecret\\get(STRING)",
"returnType": "string"
}
],
"functionClassNameMap": {
@@ -642,6 +647,7 @@
"ext\\acl\\checkScope": "Espo\\Core\\Formula\\Functions\\ExtGroup\\AclGroup\\CheckScopeType",
"ext\\acl\\getLevel": "Espo\\Core\\Formula\\Functions\\ExtGroup\\AclGroup\\GetLevelType",
"ext\\acl\\getPermissionLevel": "Espo\\Core\\Formula\\Functions\\ExtGroup\\AclGroup\\GetPermissionLevelType",
"ext\\appSecret\\get": "Espo\\Core\\Formula\\Functions\\ExtGroup\\SecretGroup\\GetType",
"util\\base64Encode": "Espo\\Core\\Formula\\Functions\\UtilGroup\\Base64EncodeType",
"util\\base64Decode": "Espo\\Core\\Formula\\Functions\\UtilGroup\\Base64DecodeType",
"ext\\oauth\\getAccessToken": "Espo\\Core\\Formula\\Functions\\ExtGroup\\OauthGroup\\GetAccessTokenType"
@@ -89,6 +89,10 @@
{
"type": "isTrue",
"attribute": "ldapAuth"
}, {
"type": "equals",
"attribute": "authenticationMethod",
"value": "LDAP"
}
]
}
@@ -2,21 +2,25 @@
"fields": {
"name": {
"type": "varchar",
"required": true
"required": true,
"audited": true
},
"subject": {
"type": "varchar"
"type": "varchar",
"audited": true
},
"body": {
"type": "wysiwyg",
"view": "views/email-template/fields/body",
"useIframe": true,
"attachmentField": "attachments"
"attachmentField": "attachments",
"audited": true
},
"isHtml": {
"type": "bool",
"default": true,
"inlineEditDisabled": true
"inlineEditDisabled": true,
"audited": true
},
"status": {
"type": "enum",
@@ -28,7 +32,8 @@
"style": {
"Inactive": "info"
},
"maxLength": 8
"maxLength": 8,
"audited": true
},
"oneOff": {
"type": "bool",
@@ -36,7 +41,8 @@
"tooltip": true
},
"attachments": {
"type": "attachmentMultiple"
"type": "attachmentMultiple",
"audited": true
},
"category": {
"type": "link",
@@ -47,7 +53,8 @@
"view": "views/fields/assigned-user"
},
"teams": {
"type": "linkMultiple"
"type": "linkMultiple",
"audited": true
},
"createdAt": {
"type": "datetime",
@@ -86,7 +86,11 @@
},
"user": {
"type": "index",
"columns": ["userId", "createdAt"]
"columns": ["userId", "number"]
},
"userIdReadRelatedParentType": {
"type": "index",
"columns": ["userId", "deleted", "read", "relatedParentType"]
}
}
}
@@ -1,7 +1,7 @@
{
"stylesheet": "client/css/espo/hazyblue.css",
"stylesheetIframe": "client/css/espo/hazyblue-iframe.css",
"logo": "client/img/logo-hazy.svg",
"logo": "client/img/logo-light.svg",
"textColor": "#333",
"chartGridColor": "#ddd",
"chartTickColor": "#e8eced",
@@ -31,10 +31,14 @@ namespace Espo\Tools\App\Metadata;
class AclDependencyItem
{
/**
* @param ?string[] $anyScopeList
*/
public function __construct(
private string $target,
private string $scope,
private ?string $field
private ?string $scope,
private ?string $field,
private ?array $anyScopeList = null,
) {}
/**
@@ -45,7 +49,7 @@ class AclDependencyItem
return $this->target;
}
public function getScope(): string
public function getScope(): ?string
{
return $this->scope;
}
@@ -54,4 +58,13 @@ class AclDependencyItem
{
return $this->field;
}
/**
* @return ?string[]
* @since 9.2.5
*/
public function getAnyScopeList(): ?array
{
return $this->anyScopeList;
}
}
@@ -95,15 +95,13 @@ class AclDependencyProvider
$data = [];
foreach (($this->metadata->get(['app', 'metadata', 'aclDependencies']) ?? []) as $target => $item) {
$anyScopeList = $item['anyScopeList'] ?? null;
$scope = $item['scope'] ?? null;
$field = $item['field'] ?? null;
if (!$scope) {
continue;
}
$data[] = [
'target' => $target,
'anyScopeList' => $anyScopeList,
'scope' => $scope,
'field' => $field,
];
@@ -196,8 +194,14 @@ class AclDependencyProvider
$target = $rawItem['target'] ?? null;
$scope = $rawItem['scope'] ?? null;
$field = $rawItem['field'] ?? null;
$anyScopeList = $rawItem['anyScopeList'] ?? null;
$list[] = new AclDependencyItem($target, $scope, $field);
$list[] = new AclDependencyItem(
target: $target,
scope: $scope,
field: $field,
anyScopeList: $anyScopeList,
);
}
return $list;
+22 -10
View File
@@ -178,20 +178,32 @@ class MetadataService
foreach ($this->aclDependencyProvider->get() as $dependencyItem) {
$aclScope = $dependencyItem->getScope();
$aclField = $dependencyItem->getField();
$anyScopeList = $dependencyItem->getAnyScopeList();
if (!$aclScope) {
continue;
if ($anyScopeList) {
$skip = true;
foreach ($anyScopeList as $itemScope) {
if ($this->acl->tryCheck($itemScope)) {
$skip = false;
break;
}
}
if ($skip) {
continue;
}
}
if (!$this->acl->tryCheck($aclScope)) {
continue;
}
if ($aclScope) {
if (!$this->acl->tryCheck($aclScope)) {
continue;
}
if (
$aclField &&
in_array($aclField, $this->acl->getScopeForbiddenFieldList($aclScope))
) {
continue;
if ($aclField && in_array($aclField, $this->acl->getScopeForbiddenFieldList($aclScope))) {
continue;
}
}
$targetArr = explode('.', $dependencyItem->getTarget());
@@ -114,7 +114,7 @@ class Erasor implements
$this->emailAddressAccessChecker
->checkEdit($this->user, $emailAddress, $entity)
) {
$emailAddress->set(Field::NAME, 'ERASED:' . $emailAddress->id);
$emailAddress->set(Field::NAME, 'ERASED:' . $emailAddress->getId());
$emailAddress->set('optOut', true);
$this->entityManager->saveEntity($emailAddress);
}
@@ -132,7 +132,7 @@ class Erasor implements
$this->phoneNumberAccessChecker
->checkEdit($this->user, $phoneNumber, $entity)
) {
$phoneNumber->set(Field::NAME, 'ERASED:' . $phoneNumber->id);
$phoneNumber->set(Field::NAME, 'ERASED:' . $phoneNumber->getId());
$this->entityManager->saveEntity($phoneNumber);
}
@@ -29,6 +29,7 @@
namespace Espo\Tools\EntityManager;
use Espo\Core\Exceptions\Error;
use Espo\Core\Name\Field;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Metadata;
@@ -379,4 +380,20 @@ class NameUtil
{
return (bool) $this->metadata->get("entityDefs.$entityType.links.$name");
}
/**
* @since 9.2.4
*/
public function linkNameIsBad(string $name): bool
{
if ($name === '') {
return true;
}
if (preg_match('/[^a-z]/', $name[0])) {
return true;
}
return $this->nameIsBad(ucfirst($name));
}
}
@@ -216,7 +216,7 @@ class LabelManager implements
}
}
return json_decode(Json::encode($finalData));
return json_decode(Json::encode((object) $finalData));
}
/**
@@ -139,8 +139,11 @@ class LinkManager
throw new Error("Relation name is too long.");
}
if (preg_match('/[^a-z]/', $relationName[0])) {
throw new Error("Relation name should start with a lower case letter.");
if ($this->nameUtil->linkNameIsBad($relationName)) {
$message = "Relation name should contain only letters and numbers, " .
"start with a lower case letter.";
throw new Error($message);
}
if ($this->metadata->get(['scopes', ucfirst($relationName)])) {
@@ -180,6 +183,13 @@ class LinkManager
throw new Error("Link name is too long.");
}
if ($this->nameUtil->linkNameIsBad($link) || $this->nameUtil->linkNameIsBad($linkForeign)) {
$message = "Link name should contain only letters and numbers, " .
"start with a lower case letter.";
throw new Error($message);
}
if (preg_match('/[^a-z]/', $link[0])) {
throw new Error("Link name should start with a lower case letter.");
}
@@ -121,7 +121,11 @@ class RecordService
$ids = [];
$actionIds = [];
foreach ($collection as $entity) {
foreach ($collection as $i => $entity) {
if ($i === $limit) {
break;
}
$ids[] = $entity->getId();
$groupedCount = null;
@@ -67,8 +67,8 @@ class TokensProvider
if (
$account->getRefreshToken() &&
$account->getExpiresAt() &&
$account->getExpiresAt()->isGreaterThan(
DateTime::createNow()->addSeconds(- self::EXPIRATION_LEAD_TIME)
$account->getExpiresAt()->isLessThan(
DateTime::createNow()->addSeconds(self::EXPIRATION_LEAD_TIME)
)
) {
$this->refresh($account);
@@ -389,7 +389,7 @@ class UserRecordService
[
'entityType:' => 'parentType',
'entityId:' => 'parentId',
'subscription.userId' => $user->getId(),
'subscriptionExclude.userId' => $user->getId(),
]
)
->where([
@@ -48,7 +48,7 @@ class UrlValidator
{
$siteUrl = rtrim($this->config->get('siteUrl') ?? '', '/');
if (str_starts_with($url, $siteUrl)) {
if (UrlValidatorUtil::validate($url, $siteUrl)) {
return;
}
@@ -60,7 +60,7 @@ class UrlValidator
foreach ($portals as $portal) {
$siteUrl = rtrim($portal->getUrl() ?? '', '/');
if (str_starts_with($url, $siteUrl)) {
if (UrlValidatorUtil::validate($url, $siteUrl)) {
return;
}
}
@@ -0,0 +1,59 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* 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\Tools\UserSecurity\Password\Recovery;
use const FILTER_VALIDATE_URL;
use const PHP_URL_HOST;
/**
* @internal
*/
class UrlValidatorUtil
{
public static function validate(string $url, string $siteUrl): bool
{
$host = parse_url($url, PHP_URL_HOST);
$siteHost = parse_url($siteUrl, PHP_URL_HOST);
if ($host !== $siteHost) {
return false;
}
if (!filter_var($url, FILTER_VALIDATE_URL)) {
return false;
}
if (!str_starts_with($url, $siteUrl)) {
return false;
}
return true;
}
}
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="318" height="78" enable-background="new 0 0 307.813 75" overflow="visible" version="1.1" viewBox="0 0 318 78" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<switch transform="matrix(1.089 0 0 1.089 -.89733 -.52658)">
<foreignObject width="1" height="1" requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/">
</foreignObject>
<g transform="matrix(.96767 0 0 .96767 3.9659 -1.2011)">
<path d="m169.53 21.864c-7.453 2.972-9.569 11.987-9.005 19.212 1.587 2.982 3.845 5.562 5.783 8.312l4.262-1.083c-1.796-4.447-1.689-9.424-0.806-14.066 0.585-3.001 2.309-6.476 5.634-7.032 5.307-0.847 10.733-0.271 16.088-0.369 0.091-2.196 0.115-4.392 0.107-6.585-7.333 0.387-15.043-1.038-22.063 1.611zm52.714-1.294c-8.12-0.952-16.332-0.149-24.492-0.387-0.021 6.43-3e-3 12.854 0.078 19.274 2.625-0.849 5.251-1.739 7.909-2.532 0.042-3.272 0.028-6.527-0.071-9.789 4.869-0.029 9.874-0.757 14.639 0.451 1.838 0.298 2.051 2.25 2.687 3.641 2.541-0.891 5.111-1.717 7.672-2.574-0.703-4.246-4.129-7.633-8.422-8.084zm23.522-0.593c-3.954 0.072-7.912 0.064-11.864 0.047 0.051 2.544 0.063 5.074 0.072 7.617 4.263-1.482 8.553-2.889 12.848-4.268-0.35-1.128-0.706-2.268-1.056-3.396z" fill="#75797c"/>
<path d="m161.96 69.125c7.886-3.717 15.757-7.463 23.72-11.018 5.563 0.359 11.146 0.021 16.722 0.193 1.14-0.036 2.292-0.061 3.432-0.088-0.011-3.195-0.025-6.38-0.082-9.564 3.428-1.502 10.227-4.623 10.227-4.623l15.215 13.941 11.096 0.106-0.715-26.236 0.803-0.211 9.005 26.344 8.834-0.066 8.99-28.394-0.308 28.434 8.074-0.021-0.231-37.932-9.279 0.071 30.625-14.141s-37.593 14.279-56.404 21.385c-2.996 1.022-5.878 2.315-8.853 3.394-2.278 0.867-4.558 1.713-6.834 2.58-20.071 7.526-39.945 15.604-60.126 22.803-6.777-10.522-15.314-19.854-21.768-30.585zm72.116-17.961c-0.108 0.154-0.324 0.458-0.429 0.611-3.448-3.018-6.765-6.189-10.21-9.205 1.745-1.096 3.47-2.242 5.026-3.597 1.625-1.386 3.479-2.469 5.345-3.499 0.293 5.227 0.258 10.452 0.268 15.69zm23.942-9.67c-0.857 2.578-1.825 5.137-2.793 7.682-1.644-6.217-3.94-12.238-5.856-18.383-0.119-0.52-0.366-1.574-0.487-2.093 3.428-1.709 10.585-4.854 15.229-6.815-1.647 5.969-4.306 14.029-6.093 19.609z" fill="#ddaf28"/>
<g fill="#75797c">
<path d="m45.672 58.148h-18.526c-2.861 0-5.614-0.651-8.257-1.953-2.861-1.409-5.043-3.651-6.547-6.725-1.503-3.074-2.254-6.455-2.254-10.145 0-3.652 0.724-6.961 2.173-9.926 1.594-3.219 3.803-5.569 6.628-7.052 1.557-0.795 3.052-1.355 4.482-1.682 1.43-0.325 3.07-0.488 4.917-0.488h17.168v6.789h-15.886c-1.415 0-2.602 0.187-3.563 0.558-0.961 0.372-1.912 1.037-2.855 1.994s-1.597 1.887-1.959 2.791c-0.363 0.902-0.543 2.027-0.543 3.375h25.023v6.789h-25.025c0 1.24 0.164 2.325 0.491 3.256 0.327 0.93 0.919 1.887 1.776 2.871 0.856 0.985 1.749 1.732 2.677 2.242 0.929 0.512 2.03 0.767 3.306 0.767h16.774z"/>
<path d="m76.499 49.519c0 2.397-0.771 4.449-2.312 6.154-1.541 1.706-3.49 2.56-5.846 2.56h-18.653v-5.113h15.326c1.087 0 2.001-0.272 2.744-0.817s1.115-1.327 1.115-2.345c0-2.362-1.595-3.543-4.783-3.543h-7.825c-1.666 0-3.278-0.79-4.836-2.369-1.559-1.58-2.336-3.287-2.336-5.119 0-2.585 0.579-4.667 1.738-6.248 1.34-1.794 3.313-2.692 5.922-2.692h17.928v5.364h-15.938c-0.614 0-1.147 0.289-1.599 0.868s-0.677 1.235-0.677 1.972c0 0.807 0.298 1.498 0.896 2.076 0.597 0.579 1.311 0.867 2.144 0.867h8.415c2.643 0 4.733 0.79 6.271 2.369 1.536 1.579 2.306 3.584 2.306 6.016z"/>
<path d="m109.29 43.414c0 4.495-1.166 8.074-3.497 10.738s-5.395 3.996-9.188 3.996h-8.186v10.309h-7.627v-38.472h15.09c4.27 0 7.6 1.269 9.989 3.806 2.279 2.428 3.419 5.637 3.419 9.623zm-7.627 0.405c0-2.356-0.754-4.286-2.262-5.793-1.509-1.505-3.388-2.258-5.641-2.258h-5.341v16.429h5.886c2.179 0 3.951-0.771 5.313-2.313 1.363-1.54 2.045-3.562 2.045-6.065z"/>
<path d="m145.1 43.967c0 4.896-1.557 8.65-4.669 11.261-2.86 2.394-6.751 3.591-11.673 3.591-4.923 0-8.742-1.087-11.456-3.264-3.15-2.502-4.724-6.401-4.724-11.696 0-4.424 1.701-7.906 5.104-10.446 3.04-2.283 6.786-3.427 11.238-3.427 4.887 0 8.805 1.225 11.754 3.673s4.426 5.884 4.426 10.308zm-8.382-0.065c0-2.285-0.716-4.197-2.146-5.738-1.432-1.54-3.379-2.312-5.841-2.312-2.246 0-4.103 0.79-5.57 2.366-1.467 1.577-2.2 3.563-2.2 5.955 0 2.756 0.743 4.949 2.228 6.581s3.405 2.448 5.76 2.448c2.679 0 4.673-0.852 5.977-2.557 1.193-1.557 1.792-3.805 1.792-6.743z"/>
</g>
</g>
</switch>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

@@ -26,12 +26,8 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
define('crm:views/knowledge-base-article/record/detail-quick', ['views/record/detail-small'], function (Dep) {
import DetailRecordView from 'views/record/detail';
return Dep.extend({
isWide: true,
sideView: false,
});
});
export default class extends DetailRecordView {
}
@@ -26,11 +26,8 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
define('crm:views/knowledge-base-article/record/edit-quick', ['views/record/edit-small'], function (Dep) {
import EditRecordView from 'views/record/edit';
return Dep.extend({
export default class extends EditRecordView {
isWide: true,
sideView: false,
});
});
}
+37 -27
View File
@@ -1,30 +1,40 @@
<div class="panel panel-default no-side-margin">
<div class="panel-body">
<div class="row">
<div class="cell form-group col-md-6" data-name="currentPassword">
<label
class="control-label"
data-name="currentPassword"
>{{translate 'currentPassword' scope='User' category='fields'}}</label>
<div class="field" data-name="currentPassword">{{{currentPassword}}}</div>
</div>
</div>
<div class="row">
<div class="cell form-group col-md-6" data-name="password">
<label
class="control-label"
data-name="password"
>{{translate 'newPassword' scope='User' category='fields'}}</label>
<div class="field" data-name="password">{{{password}}}</div>
</div>
</div>
<div class="row">
<div class="cell form-group col-md-6" data-name="passwordConfirm">
<label
class="control-label"
data-name="passwordConfirm"
>{{translate 'passwordConfirm' scope='User' category='fields'}}</label>
<div class="field" data-name="passwordConfirm">{{{passwordConfirm}}}</div>
<div class="no-side-margin record">
<div>
<div class="record-grid-wide">
<div class="left">
<div class="middle">
<div class="panel panel-default first last">
<div class="panel-body panel-body-form">
<div class="row">
<div class="cell form-group col-md-6" data-name="currentPassword">
<label
class="control-label"
data-name="currentPassword"
>{{translate 'currentPassword' scope='User' category='fields'}}</label>
<div class="field" data-name="currentPassword">{{{currentPassword}}}</div>
</div>
</div>
<div class="row">
<div class="cell form-group col-md-6" data-name="password">
<label
class="control-label"
data-name="password"
>{{translate 'newPassword' scope='User' category='fields'}}</label>
<div class="field" data-name="password">{{{password}}}</div>
</div>
</div>
<div class="row">
<div class="cell form-group col-md-6" data-name="passwordConfirm">
<label
class="control-label"
data-name="passwordConfirm"
>{{translate 'passwordConfirm' scope='User' category='fields'}}</label>
<div class="field" data-name="passwordConfirm">{{{passwordConfirm}}}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
+32 -21
View File
@@ -1,26 +1,37 @@
<div class="panel panel-default no-side-margin">
<div class="panel-body panel-body-form">
<div class="row">
<div class="cell form-group col-md-6" data-name="dashboardTabList">
<label
class="control-label"
data-name="dashboardTabList"
>{{translate 'dashboardTabList' category='fields' scope="Preferences"}}</label>
<div class="field" data-name="dashboardTabList">
{{{dashboardTabList}}}
</div>
</div>
{{#if hasLocked}}
<div class="cell form-group col-md-6" data-name="dashboardLocked">
<label
class="control-label"
data-name="dashboardLocked"
>{{translate 'dashboardLocked' category='fields' scope="Preferences"}}</label>
<div class="field" data-name="dashboardLocked">
{{{dashboardLocked}}}
<div class="no-side-margin record">
<div>
<div class="record-grid-wide">
<div class="left">
<div class="middle">
<div class="panel panel-default first last">
<div class="panel-body panel-body-form">
<div class="row">
<div class="cell form-group col-md-6" data-name="dashboardTabList">
<label
class="control-label"
data-name="dashboardTabList"
>{{translate 'dashboardTabList' category='fields' scope="Preferences"}}</label>
<div class="field" data-name="dashboardTabList">
{{{dashboardTabList}}}
</div>
</div>
{{#if hasLocked}}
<div class="cell form-group col-md-6" data-name="dashboardLocked">
<label
class="control-label"
data-name="dashboardLocked"
>{{translate 'dashboardLocked' category='fields' scope="Preferences"}}</label>
<div class="field" data-name="dashboardLocked">
{{{dashboardLocked}}}
</div>
</div>
{{/if}}
</div>
</div>
</div>
</div>
{{/if}}
</div>
</div>
</div>
</div>
+1 -1
View File
@@ -4,7 +4,7 @@
<div>
{{#if isFirst}}
{{#unless rowActionsDisabled}}
<div class="pull-right item-menu-container fix-overflow">{{{../itemMenu}}}</div>
<div class="pull-right item-menu-container fix-position">{{{../itemMenu}}}</div>
{{/unless}}
{{/if}}
<div class="form-group">
+1 -1
View File
@@ -34,7 +34,7 @@
data-action="selectPreset"
>
<div class="{{#if style}}text-{{style}}{{/if}}">
{{#if label}}{{label}}{{else}}{{translate name category='presetFilters' scope=../entityType}}{{/if}}
{{~#if label}}{{label}}{{else}}{{translate name category='presetFilters' scope=../entityType}}{{/if~}}
</div>
</a>
</li>
+5 -3
View File
@@ -25,9 +25,11 @@
role="button"
tabindex="0"
data-action="expandDetails"
class="text-soft"
><span class="fas fa-chevron-down"></span></a><span style="user-select: none"> </span>
<span class="fields text-muted small">{{fieldsString}}</span>
class="text-muted no-underline"
><span class="fas fa-chevron-down text-soft" data-role="icon"></span>
<span style="user-select: none"> </span>
<span class="fields small">{{fieldsString}}</span>
</a>
</div>
{{/if}}
+2 -2
View File
@@ -3,7 +3,7 @@
<div class="buttons-panel margin hide floated-row clearfix">
<div>
<button class="btn btn-primary btn-xs-wide post">{{translate 'Post'}}</button>
{{#if allowInternalNotes}}
{{~#if allowInternalNotes~}}
<span
style="cursor: pointer;"
class="internal-mode-switcher{{#if isInternalNoteMode}} enabled{{/if}} action"
@@ -12,7 +12,7 @@
>
<span class="fas fa-lock"></span>
</span>
{{/if}}
{{~/if~}}
</div>
<div class="attachments-container">
{{{attachments}}}
+8
View File
@@ -691,6 +691,14 @@ class App {
if (!className) {
const module = this.metadata.get(`scopes.${name}.module`);
if (!/^[A-Za-z0-9]+$/.test(name)) {
console.error(`Bad controller name ${name}.`);
this.baseController.error404();
return;
}
className = Utils.composeClassName(module, name, 'controllers');
}
+1 -1
View File
@@ -174,7 +174,7 @@ class Collection {
parentModel
/**
* @param {Model[]|null} [models] Models.
* @param {Model[]|Record<string, *>[]|null} [models] Models.
* @param {{
* entityType?: string,
* model?: Model.prototype,
+15
View File
@@ -30,6 +30,12 @@
export default class {
/**
* @private
* @type {string}
*/
entityType
/**
* @param {module:views/fields/base} view A field view.
*/
@@ -48,6 +54,8 @@ export default class {
const entityType = metadata.get(['entityDefs', model.entityType, 'links', link, 'entity']) ||
model.entityType;
this.entityType = entityType;
const fieldDefs = metadata.get(['entityDefs', entityType, 'fields', field]) || {};
const type = fieldDefs.type;
@@ -78,4 +86,11 @@ export default class {
getForeignParams() {
return Espo.Utils.cloneDeep(this.foreignParams);
}
/**
* @return {string}
*/
getEntityType() {
return this.entityType;
}
}
+7 -21
View File
@@ -69,7 +69,6 @@
* @property {string|null} exportsAs
* @property {string} [url]
* @property {boolean} [useCache]
* @property {boolean} [suppressAmd]
*/
/**
@@ -570,7 +569,6 @@
let dataType, type, path, exportsTo, exportsAs;
let realName = id;
let suppressAmd = false;
if (id.indexOf('lib!') === 0) {
dataType = 'script';
@@ -601,14 +599,6 @@
type = 'amd';
}
if (!isDefinedLib && id.slice(-3) === '.js') {
suppressAmd = true;
}
if (exportsAs) {
suppressAmd = true;
}
if (path.indexOf(':') !== -1) {
console.error(`Not allowed path '${path}'.`);
@@ -698,7 +688,6 @@
errorCallback: errorCallback,
exportsAs: exportsAs,
exportsTo: exportsTo,
suppressAmd: suppressAmd,
};
if (path in this._pathsBeingLoaded) {
@@ -736,19 +725,11 @@
const fullUrl = urlObj.toString();
if (suppressAmd) {
define.amd = false;
}
if (type === 'amd') {
this._urlIdMap[fullUrl] = id;
}
this._addScript(fullUrl, () => {
if (suppressAmd) {
define.amd = true;
}
let value;
if (type === 'amd') {
@@ -782,7 +763,7 @@
}
console.warn(`Could not obtain ${id}.`);
});
}, errorCallback);
return;
}
@@ -811,9 +792,10 @@
* @private
* @param {string} url
* @param {function} callback
* @param {function|null} [errorCallback]
* @return {Promise}
*/
_addScript(url, callback) {
_addScript(url, callback, errorCallback = null) {
const script = document.createElement('script');
script.src = url;
@@ -821,6 +803,10 @@
script.addEventListener('error', e => {
console.error(`Could not load script '${url}'.`, e);
if (errorCallback) {
errorCallback();
}
});
document.head.appendChild(script);
+11 -5
View File
@@ -112,8 +112,14 @@ function uiAppInit() {
const $dashletBody = $(target).closest('.dashlet-body');
if ($dashletBody.length) {
const $body = $dashletBody;
const fixPosition = e.target.parentElement.classList.contains('fix-position');
if ($dashletBody.length || fixPosition) {
let $body = $dashletBody;
if (fixPosition) {
$body = $(window);
}
$(target).removeClass('dropup');
@@ -152,10 +158,10 @@ function uiAppInit() {
right: 'auto',
});
return;
//return;
}
if (e.target.parentElement.classList.contains('fix-overflow')) {
/*if (e.target.parentElement.classList.contains('fix-overflow')) {
$(target).removeClass('dropup');
const isRight = e.target.classList.contains('pull-right');
@@ -187,7 +193,7 @@ function uiAppInit() {
left: left,
right: 'auto',
});
}
}*/
});
}
+5
View File
@@ -121,6 +121,11 @@ Espo.Utils = {
});
}
else if (typeof view[method] === 'function') {
if (view?.events[`click [data-action="${action}"]`]) {
// Prevents from firing if a handler is already assigned. Important.
return false;
}
view[method].call(view, data, event);
event.preventDefault();
+1
View File
@@ -108,6 +108,7 @@ class View extends BullView {
* @param {module:view~actionHandlerCallback} handler A handler.
*/
addActionHandler(action, handler) {
// The key should be in sync with one in Utils.handleAction.
const fullAction = `click [data-action="${action}"]`;
this.events[fullAction] = e => {
+16 -3
View File
@@ -44,6 +44,12 @@ class EntityManagerEditView extends View {
additionalParams
defaultParamLocation = 'scopes'
/**
* @private
* @type {string[]}
*/
enumFieldList
data() {
return {
isNew: this.isNew,
@@ -131,7 +137,8 @@ class EntityManagerEditView extends View {
}
if (scope) {
const fieldDefs = this.getMetadata().get('entityDefs.' + scope + '.fields') || {};
/** @type {Record.<string, Record>} */
const fieldDefs = this.getMetadata().get(`entityDefs.${scope}.fields`) || {};
this.orderableFieldList = Object.keys(fieldDefs)
.filter(item => {
@@ -180,13 +187,19 @@ class EntityManagerEditView extends View {
this.enumFieldList = Object.keys(fieldDefs)
.filter(item => {
if (fieldDefs[item].disabled) {
return;
if (
fieldDefs[item].disabled ||
fieldDefs[item].utility ||
fieldDefs[item].directUpdateDisabled
) {
return false;
}
if (fieldDefs[item].type === 'enum') {
return true;
}
return false;
})
.sort((v1, v2) => {
return this.translate(v1, 'fields', scope)
@@ -31,9 +31,6 @@ import Detail from 'views/email-template/record/detail';
export default class extends EditRecordView {
isWide = true
sideView = false
setup() {
super.setup();
Detail.prototype.listenToInsertField.call(this);
@@ -420,7 +420,38 @@ class EmailAddressVarcharFieldView extends BaseFieldView {
return true;
}
/**
*
* @param {string} address
* @param {string} name
* @param {string} [type]
* @param {string} [id]
*/
addAddress(address, name, type, id) {
if (name === '') {
const nameHash = this.model.attributes.nameHash ?? {};
if (address in nameHash) {
name = nameHash[address];
}
}
if (type === undefined) {
const typeHash = this.model.attributes.typeHash ?? {};
if (address in typeHash) {
type = typeHash[address];
}
}
if (id === undefined) {
const idHash = this.model.attributes.idHash ?? {};
if (address in idHash) {
id = idHash[address];
}
}
if (this.justAddedAddress) {
this.deleteAddress(this.justAddedAddress);
}
@@ -482,9 +513,9 @@ class EmailAddressVarcharFieldView extends BaseFieldView {
$text.append(
$('<span>').text(name),
' ',
'<span class="no-select"> </span>',
$('<span>').addClass('text-muted middle-dot'),
' '
'<span class="no-select"> </span>'
);
}
+4
View File
@@ -256,6 +256,10 @@ class ArrayFieldView extends BaseFieldView {
this.allowCustomOptions = true;
}
if (this.params.allowCustomOptions === false) {
this.allowCustomOptions = false;
}
if (this.type === 'array') {
this.validations.push('noInputValue')
}
+8 -1
View File
@@ -100,6 +100,13 @@ class CurrencyFieldView extends FloatFieldView {
'range',
]
/**
* @protected
* @type {string}
* @since 9.2.6
*/
currencyAttribute
/** @inheritDoc */
data() {
const currencyValue = this.model.get(this.currencyFieldName) ||
@@ -123,7 +130,7 @@ class CurrencyFieldView extends FloatFieldView {
setup() {
super.setup();
this.currencyFieldName = this.name + 'Currency';
this.currencyFieldName = this.currencyAttribute ?? this.name + 'Currency';
this.defaultCurrency = this.getConfig().get('defaultCurrency');
this.currencyList = this.getConfig().get('currencyList') || [this.defaultCurrency];
this.decimalPlaces = this.getConfig().get('currencyDecimalPlaces');
+20
View File
@@ -28,11 +28,31 @@
import ArrayFieldView from 'views/fields/array';
import ForeignEnumFieldView from 'views/fields/foreign-enum';
import Helper from 'helpers/misc/foreign-field';
class ForeignArrayFieldView extends ArrayFieldView {
type = 'foreign'
/**
* @private
* @type {string}
*/
foreignEntityType
setup() {
const helper = new Helper(this);
const foreignParams = helper.getForeignParams();
for (const param in foreignParams) {
this.params[param] = foreignParams[param];
}
this.foreignEntityType = helper.getEntityType();
super.setup();
}
setupOptions() {
ForeignEnumFieldView.prototype.setupOptions.call(this);
}
+22 -25
View File
@@ -27,37 +27,34 @@
************************************************************************/
import ChecklistFieldView from 'views/fields/checklist';
import Helper from 'helpers/misc/foreign-field';
import ForeignArrayFieldView from 'views/fields/foreign-array';
class ForeignChecklistFieldView extends ChecklistFieldView {
type = 'foreign'
/**
* @private
* @type {string}
*/
foreignEntityType
setup() {
const helper = new Helper(this);
const foreignParams = helper.getForeignParams();
for (const param in foreignParams) {
this.params[param] = foreignParams[param];
}
this.foreignEntityType = helper.getEntityType();
super.setup();
}
setupOptions() {
this.params.options = [];
if (!this.params.field || !this.params.link) {
return;
}
const scope = this.getMetadata()
.get(['entityDefs', this.model.entityType, 'links', this.params.link, 'entity']);
if (!scope) {
return;
}
this.params.isSorted = this.getMetadata()
.get(['entityDefs', scope, 'fields', this.params.field, 'isSorted']) || false;
this.params.options = this.getMetadata()
.get(['entityDefs', scope, 'fields', this.params.field, 'options']) || [];
this.translatedOptions = {};
this.params.options.forEach(item => {
this.translatedOptions[item] = this.getLanguage()
.translateOption(item, this.params.field, scope);
});
ForeignArrayFieldView.prototype.setupOptions.call(this);
}
}
+25 -43
View File
@@ -27,14 +27,32 @@
************************************************************************/
import EnumFieldView from 'views/fields/enum';
import Helper from 'helpers/misc/foreign-field';
class ForeignEnumFieldView extends EnumFieldView {
type = 'foreign'
setupOptions() {
this.params.options = [];
/**
* @private
* @type {string}
*/
foreignEntityType
setup() {
const helper = new Helper(this);
const foreignParams = helper.getForeignParams();
for (const param in foreignParams) {
this.params[param] = foreignParams[param];
}
this.foreignEntityType = helper.getEntityType();
super.setup();
}
setupOptions() {
const field = this.params.field;
const link = this.params.link;
@@ -42,47 +60,15 @@ class ForeignEnumFieldView extends EnumFieldView {
return;
}
const entityType = this.getMetadata().get(`entityDefs.${this.model.entityType}.links.${link}.entity`);
if (!entityType) {
return;
}
/**
* @type {{
* optionsPath?: string|null,
* optionsReference?: string|null,
* translation?: string|null,
* options?: string[],
* isSorted?: boolean,
* displayAsLabel?: boolean,
* style?: Record,
* labelType?: string,
* }}
*/
const fieldDefs = this.getMetadata().get(`entityDefs.${entityType}.fields.${field}`);
if (!fieldDefs) {
return;
}
let {
optionsPath,
optionsReference,
translation,
options,
isSorted,
displayAsLabel,
style,
labelType,
} = fieldDefs;
let optionsPath = this.params.optionsPath;
const optionsReference = this.params.optionsReference;
let options = this.params.options;
const style = this.params.style;
if (!optionsPath && optionsReference) {
const [refEntityType, refField] = optionsReference.split('.');
optionsPath = `entityDefs.${refEntityType}.fields.${refField}.options`;
style = this.getMetadata().get(`entityDefs.${refEntityType}.fields.${refField}.style`) ?? {};
}
if (optionsPath) {
@@ -90,14 +76,10 @@ class ForeignEnumFieldView extends EnumFieldView {
}
this.params.options = Espo.Utils.clone(options) ?? [];
this.params.translation = translation;
this.params.isSorted = isSorted ?? false;
this.params.displayAsLabel = displayAsLabel ?? false;
this.params.labelType = labelType;
this.styleMap = style ?? {};
const pairs = this.params.options
.map(item => [item, this.getLanguage().translateOption(item, field, entityType)])
.map(item => [item, this.getLanguage().translateOption(item, field, this.foreignEntityType)])
this.translatedOptions = Object.fromEntries(pairs);
}
@@ -28,11 +28,31 @@
import MultiEnumFieldView from 'views/fields/multi-enum';
import ForeignArrayFieldView from 'views/fields/foreign-array';
import Helper from 'helpers/misc/foreign-field';
class ForeignMultiEnumFieldView extends MultiEnumFieldView {
type = 'foreign'
/**
* @private
* @type {string}
*/
foreignEntityType
setup() {
const helper = new Helper(this);
const foreignParams = helper.getForeignParams();
for (const param in foreignParams) {
this.params[param] = foreignParams[param];
}
this.foreignEntityType = helper.getEntityType();
super.setup();
}
setupOptions() {
ForeignArrayFieldView.prototype.setupOptions.call(this);
}
+3 -9
View File
@@ -298,17 +298,11 @@ class IntFieldView extends BaseFieldView {
* @return {number|null}
*/
getMinValue() {
let minValue = this.model.getFieldParam(this.name, 'min') ?? null;
if (minValue != null) {
minValue = null;
}
if ('min' in this.params) {
minValue = this.params.min;
return this.params.min;
}
return minValue;
return this.model.getFieldParam(this.name, 'min') ?? null;
}
// noinspection JSUnusedGlobalSymbols
@@ -34,6 +34,11 @@ class LinkCategoryTreeFieldView extends LinkFieldView {
autocompleteDisabled = false
getUrl() {
if (this.getMetadata().get(`scopes.${this.entityType}.type`) === 'CategoryTree') {
// Can be used for the 'parent' field of the category entity type.
return super.getUrl();
}
const id = this.model.get(this.idName);
if (!id) {
+28 -9
View File
@@ -217,6 +217,13 @@ class LinkFieldView extends BaseFieldView {
*/
linkClass
/**
* @protected
* @type {string}
* @since 9.2.5
*/
foreignNameAttribute
/** @inheritDoc */
events = {
/** @this LinkFieldView */
@@ -364,6 +371,10 @@ class LinkFieldView extends BaseFieldView {
this.model.getFieldParam(this.name, 'entity') ||
this.model.getLinkParam(this.name, 'entity');
this.foreignNameAttribute = this.model.getLinkParam(this.name, 'foreignName') ??
this.getMetadata().get(`clientDefs.${this.foreignScope}.nameAttribute`) ??
'name';
if ('createDisabled' in this.options) {
this.createDisabled = this.options.createDisabled;
}
@@ -407,12 +418,12 @@ class LinkFieldView extends BaseFieldView {
* @return {Promise|void}
*/
select(model) {
this.$elementName.val(model.get('name') || model.id);
this.$elementId.val(model.get('id'));
this.$elementName.val(model.get(this.foreignNameAttribute) || model.id);
this.$elementId.val(model.id);
if (this.mode === this.MODE_SEARCH) {
this.searchData.idValue = model.get('id');
this.searchData.nameValue = model.get('name') || model.id;
this.searchData.idValue = model.id;
this.searchData.nameValue = model.get(this.foreignNameAttribute) || model.id;
}
this.trigger('change');
@@ -611,7 +622,7 @@ class LinkFieldView extends BaseFieldView {
if (!this.forceSelectAllAttributes) {
const mandatorySelectAttributeList = this.getMandatorySelectAttributeList();
let select = ['id', 'name'];
let select = ['id', this.foreignNameAttribute];
if (mandatorySelectAttributeList) {
select = select.concat(mandatorySelectAttributeList);
@@ -824,12 +835,20 @@ class LinkFieldView extends BaseFieldView {
const list = [];
response.list.forEach(item => {
const name = item[this.foreignNameAttribute] || item.name || item.id;
const attributes = item;
if (this.foreignNameAttribute !== 'name') {
attributes[this.foreignNameAttribute] = name;
}
list.push({
id: item.id,
name: item.name || item.id,
name: name,
data: item.id,
value: item.name || item.id,
attributes: item,
value: name,
attributes: attributes,
});
});
@@ -1365,7 +1384,7 @@ class LinkFieldView extends BaseFieldView {
*/
selectOneOf(models) {
models.forEach(model => {
this.addLinkOneOf(model.id, model.get('name'));
this.addLinkOneOf(model.id, model.get(this.foreignNameAttribute));
});
}
}
@@ -140,7 +140,7 @@ export default class extends ArrayFieldView {
const item = div.querySelector('.list-group-item');
const group = document.createElement('div');
group.classList.add('btn-group', 'pull-right');
group.classList.add('btn-group', 'pull-right', 'item-button');
const button = document.createElement('button');
button.classList.add('btn', 'btn-link', 'btn-sm', 'dropdown-toggle');
+10
View File
@@ -902,6 +902,16 @@ class ListWithCategories extends ListView {
Espo.Ui.notify();
}
/**
* @protected
*/
async actionFullRefresh() {
await Promise.all([
super.actionFullRefresh(),
this.nestedCategoriesCollection?.fetch(),
]);
}
}
export default ListWithCategories;
+1 -1
View File
@@ -42,7 +42,7 @@ class ComposeEmailModalView extends EditModalView {
shortcutKeys = {
/** @this ComposeEmailModalView */
'Control+Enter': function (e) {
if (this.buttonList.findIndex(item => item.name === 'send' && !item.hidden) === -1) {
if (this.buttonList.findIndex(item => item.name === 'send' && !item.hidden && !item.disabled) === -1) {
return;
}
+2 -2
View File
@@ -73,7 +73,7 @@ class EditModalView extends ModalView {
return;
}
if (this.buttonList.findIndex(item => item.name === 'save' && !item.hidden) === -1) {
if (this.buttonList.findIndex(item => item.name === 'save' && !item.hidden && !item.disabled) === -1) {
return;
}
@@ -93,7 +93,7 @@ class EditModalView extends ModalView {
return;
}
if (this.buttonList.findIndex(item => item.name === 'save' && !item.hidden) === -1) {
if (this.buttonList.findIndex(item => item.name === 'save' && !item.hidden && !item.disabled) === -1) {
return;
}
+1 -1
View File
@@ -253,7 +253,7 @@ class RelatedListModalView extends ModalView {
this.$header.append(
title ||
$('<span>').text(
this.getLanguage().translate(this.link, 'links', this.entityType)
this.getLanguage().translate(this.link, 'links', this.scope)
)
);
+5 -1
View File
@@ -148,7 +148,11 @@ class EditRecordView extends DetailRecordView {
this.wait(promise);
// @todo Revise. Possible race condition issues.
promise.then(() => super.setupBeforeFinal());
promise.then(() => {
super.setupBeforeFinal();
this.processDynamicLogic();
});
}
if (this.model.isNew()) {
+1 -1
View File
@@ -938,7 +938,7 @@ class SearchView extends View {
this.$el
.find('ul.filter-menu a.preset[data-name="'+presetName+'"]')
.prepend('<span class="fas fa-check pull-right"></span>');
.prepend('<span class="fas fa-check check-icon pull-right"></span>');
}
manageBoolFilters() {
+1
View File
@@ -119,6 +119,7 @@ class MasterSiteView extends View {
}
body.dataset.isDark = this.getThemeManager().getParam('isDark') ?? false;
body.dataset.themeName = this.getThemeManager().getName();
const footerView = this.getView('footer');
+25 -22
View File
@@ -92,7 +92,7 @@ class UpdateNoteStreamView extends NoteStreamView {
const statusValue = data.value;
this.statusStyle = this.getMetadata()
.get(`entityDefs.${parentType}.fields.${statusField}.style.${statusValue}`) ||
.get(`entityDefs.${parentType}.fields.${statusField}.style.${statusValue}`) ||
'default';
this.statusText = this.getLanguage()
@@ -101,7 +101,7 @@ class UpdateNoteStreamView extends NoteStreamView {
this.wait(true);
this.getModelFactory().create(parentType, model => {
this.getModelFactory().create(parentType).then(model => {
const modelWas = model;
const modelBecame = model.clone();
@@ -116,7 +116,9 @@ class UpdateNoteStreamView extends NoteStreamView {
fields.forEach(field => {
const type = model.getFieldType(field) || 'base';
const viewName = this.getMetadata().get(['entityDefs', model.entityType, 'fields', field, 'view']) ||
const viewName = model.getFieldParam(field, 'auditView') ??
model.getFieldParam(field, 'view') ??
this.getFieldManager().getViewName(type);
const attributeList = this.getFieldManager().getEntityTypeFieldAttributeList(model.entityType, field);
@@ -143,24 +145,26 @@ class UpdateNoteStreamView extends NoteStreamView {
this.createView(field + 'Was', viewName, {
model: modelWas,
name: field,
readOnly: true,
defs: {
name: field
},
mode: 'detail',
inlineEditDisabled: true,
selector: `.row[data-name="${field}"] .cell-was`,
auditData: {
type: 'was',
},
});
this.createView(field + 'Became', viewName, {
model: modelBecame,
name: field,
readOnly: true,
defs: {
name: field,
},
mode: 'detail',
inlineEditDisabled: true,
selector: `.row[data-name="${field}"] .cell-became`,
auditData: {
type: 'became',
},
});
this.fieldDataList.push({
@@ -175,16 +179,17 @@ class UpdateNoteStreamView extends NoteStreamView {
});
}
toggleDetails() {
const target = this.element.querySelector('[data-action="expandDetails"]');
const $details = this.$el.find('> .details');
const $fields = this.$el.find('> .stream-details-container > .fields');
const detailsElement = this.element.querySelector(':scope > .details');
const fieldElement = this.element.querySelector(':scope > .stream-details-container > .fields');
const iconElement = target.querySelector('[data-role="icon"]');
if (!this.isExpanded) {
$details.removeClass('hidden');
$fields.addClass('hidden');
detailsElement.classList.remove('hidden');
fieldElement?.classList.add('hidden');
this.fieldList.forEach(field => {
const wasField = this.getView(field + 'Was');
@@ -196,21 +201,19 @@ class UpdateNoteStreamView extends NoteStreamView {
}
});
$(target).find('span')
.removeClass('fa-chevron-down')
.addClass('fa-chevron-up');
iconElement.classList.remove('fa-chevron-down');
iconElement.classList.add('fa-chevron-up');
this.isExpanded = true;
return;
}
$details.addClass('hidden');
$fields.removeClass('hidden');
detailsElement.classList.add('hidden');
fieldElement?.classList.remove('hidden');
$(target).find('span')
.addClass('fa-chevron-down')
.removeClass('fa-chevron-up');
iconElement.classList.remove('fa-chevron-up');
iconElement.classList.add('fa-chevron-down');
this.isExpanded = false;
}
@@ -63,26 +63,26 @@ class UserGeneratePasswordFieldView extends BaseFieldView {
this.strengthParams = this.options.strengthParams || {};
this.passwordStrengthLength = this.strengthParams.passwordStrengthLength ||
this.getConfig().get('passwordStrengthLength');
this.passwordStrengthLength = this.strengthParams.passwordStrengthLength ??
this.getConfig().get('passwordStrengthLength') ?? null;
this.passwordStrengthLetterCount = this.strengthParams.passwordStrengthLetterCount ||
this.getConfig().get('passwordStrengthLetterCount');
this.passwordStrengthLetterCount = this.strengthParams.passwordStrengthLetterCount ??
this.getConfig().get('passwordStrengthLetterCount') ?? null;
this.passwordStrengthNumberCount = this.strengthParams.passwordStrengthNumberCount ||
this.getConfig().get('passwordStrengthNumberCount');
this.passwordStrengthNumberCount = this.strengthParams.passwordStrengthNumberCount ??
this.getConfig().get('passwordStrengthNumberCount') ?? null;
this.passwordStrengthSpecialCharacterCount = this.strengthParams.passwordStrengthSpecialCharacterCount ||
this.getConfig().get('passwordStrengthSpecialCharacterCount');
this.passwordStrengthSpecialCharacterCount = this.strengthParams.passwordStrengthSpecialCharacterCount ??
this.getConfig().get('passwordStrengthSpecialCharacterCount') ?? null;
this.passwordGenerateLength = this.strengthParams.passwordGenerateLength ||
this.getConfig().get('passwordGenerateLength');
this.passwordGenerateLength = this.strengthParams.passwordGenerateLength ??
this.getConfig().get('passwordGenerateLength') ?? null;
this.passwordGenerateLetterCount = this.strengthParams.passwordGenerateLetterCount ||
this.getConfig().get('passwordGenerateLetterCount');
this.passwordGenerateLetterCount = this.strengthParams.passwordGenerateLetterCount ??
this.getConfig().get('passwordGenerateLetterCount') ?? null;
this.passwordGenerateNumberCount = this.strengthParams.passwordGenerateNumberCount ||
this.getConfig().get('passwordGenerateNumberCount');
this.passwordGenerateNumberCount = this.strengthParams.passwordGenerateNumberCount ??
this.getConfig().get('passwordGenerateNumberCount') ?? null;
}
fetch() {
@@ -139,12 +139,17 @@ export default class extends View {
const url = data.url || this.baseUrl;
const msg = this.translate('passwordChangedByRequest', 'messages', 'User') +
' <a href="' + url + '">' + this.translate('Login', 'labels', 'User') + '</a>.';
const a = document.createElement('a');
a.href = url;
a.innerText = this.translate('Login', 'labels', 'User');
const message = this.translate('passwordChangedByRequest', 'messages', 'User');
const html = this.getHelper().escapeString(message) + ' ' + a.outerHTML;
this.$el.find('.msg-box')
.removeClass('hidden')
.html('<span class="text-success">' + msg + '</span>');
.html('<span class="text-success">' + html + '</span>');
})
.catch(() => {
return $submit.removeClass('disabled');
+1
View File
@@ -106,6 +106,7 @@
@dropdown-link-hover-bg-value: @navbar-inverse-link-hover-bg-value;
@dropdown-divider-bg-value: @default-border-color-value;
@dropdown-border-value: @panel-default-border-value;
@dropdown-link-active-bg-value: lighten(@navbar-inverse-link-hover-bg-value, 3%);
@select-item-bg-value: @table-bg-accent-value;
@select-item-border-value: @btn-default-border-value;
+16 -290
View File
@@ -112,7 +112,7 @@ img.image-preview {
.preset {
text-overflow: ellipsis;
overflow: hidden !important;
//overflow: hidden !important;
}
.preset > div{
@@ -448,10 +448,6 @@ input.numeric-text {
font-variant-numeric: tabular-nums;
}
.filter .selectize-input {
min-height: var(--input-height-small);
}
.btn.active {
box-shadow: none;
}
@@ -642,60 +638,6 @@ input.global-search-input {
margin-right: var(--panel-padding);
}
.cell > .field {
overflow-wrap: break-word;
word-wrap: break-word;
}
.field .row {
margin-left: calc(var(--3px) * -1) !important;
margin-right: calc(var(--3px) * -1) !important;
> div {
padding-left: var(--3px) !important;
padding-right: var(--3px) !important;
}
}
.field .form-control,
.field .btn {
margin-bottom: var(--3px);
}
.field .input-group .form-control,
.field .input-group .btn {
margin-bottom: 0;
}
.field .input-group {
margin-bottom: var(--3px);
}
.field .link-container {
margin-bottom: 0;
&:not(.no-input) {
> .list-group-item:last-child {
border-bottom-width: 0;
}
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
> .list-group-item:last-child {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom-width: 0;
}
}
> .list-group-item {
.text:empty:before {
content: "\200b";
}
}
}
.list-group {
border-radius: var(--border-radius);
@@ -798,20 +740,6 @@ input.global-search-input {
}
}
.field {
.list-group .list-group-item {
background-color: var(--panel-bg);
}
.list-group {
background-color: var(--default-border-color);
}
}
.panel-body .field > .link-container > .list-group-item {
background-color: @panel-bg;
}
.panel-body .list-group-item {
background-color: @panel-bg;
@@ -820,214 +748,6 @@ input.global-search-input {
}
}
.field {
.link-container {
.list-group-item {
> div {
margin: calc(var(--6px) * -1) 0 calc(var(--6px) * -1);
}
> div > div {
margin: var(--6px) 0;
}
.form-control,
.btn {
margin-top: var(--2px);
margin-bottom: 0;
}
> span.text {
width: calc(100% - var(--18px));
display: inline-block;
}
> span.drag-handle {
display: inline-block;
width: var(--18px);
color: var(--text-muted-color);
cursor: grab;
vertical-align: top;
&:active {
cursor: grabbing;
}
}
> span.item-button {
display: inline-block;
width: var(--18px);
vertical-align: top;
}
&:has(> .item-button) {
> span.text {
width: calc(100% - var(--36px));
}
}
&:has(> .drag-handle) {
> span.text {
width: calc(100% - var(--36px));
}
}
&:has(> .item-button):has(> .drag-handle) {
> span.text {
width: calc(100% - var(--36px) - var(--18px));
}
}
> a[role="button"] {
margin-top: var(--1px);
margin-left: var(--2px);
}
}
.link-group-item-with-columns {
> div > .btn-group {
margin-top: 0;
margin-bottom: 0;
.caret {
border-top-color: var(--gray-light);
}
}
}
}
}
.field, .cell {
.checklist-label {
color: @text-color;
margin-bottom: 0;
padding-left: var(--7px);
}
.checklist-item-container {
margin-bottom: var(--2px);
&:last-child {
margin-bottom: 0;
}
> input[type=checkbox] {
float: left;
}
> input[type=checkbox]:not(:disabled) + label {
cursor: pointer;
}
user-select: none;
}
.multi-enum-item-label-container {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding-bottom: var(--4px);
&:last-child {
padding-bottom: var(--1px);
}
.label-md {
line-height: 1.5;
}
}
}
.field {
> .label {
white-space: normal;
}
}
.filter > .form-group .field {
.link-container {
font-size: var(--font-size-small);
.fa-times {
font-size: var(--12px);
}
> .list-group-item {
padding-top: var(--4px);
padding-bottom: var(--4px);
line-height: var(--20px);
}
}
.input-group {
input {
font-size: var(--font-size-small);
height: var(--input-height-small);
min-height: var(--input-height-small);
}
button.btn {
font-size: var(--font-size-small);
height: var(--input-height-small);
}
}
}
.link-container {
> .list-group-item {
.link-item-column {
float: right;
display: inline-block;
width: 40%;
> .selectize-control {
width: 100%;
}
}
> a.pull-right {
margin-left: var(--4px);
}
&,
> .text {
> img.avatar {
margin-right: var(--7px);
top: var(--2px);
}
}
.link-item-name {
> img.avatar {
margin-right: var(--7px);
top: var(--2px);
}
}
}
}
.filter .field .link-container .list-group-item a[role="button"] {
margin-top: 0;
margin-left: var(--2px);
}
.field .link-container > .list-group-item.link-with-role > a[role="button"],
.field .link-container > .list-group-item.link-with-role > div > a[role="button"] {
margin-top: var(--7px);
}
.field .link-container > .list-group-item.link-group-item-with-columns,
.field .link-container > .list-group-item.link-group-item-with-primary {
> div:nth-child(1) {
display: inline-block;
width: calc(~"100% - 23px");
float: left;
input.form-control {
width: 100%;
}
}
> div:nth-child(2) {
display: inline-block;
width: var(--23px);
float: right;
vertical-align: top;
}
}
#main > .list-container {
> .no-data {
@@ -1249,14 +969,6 @@ input.global-search-input {
}
}
.filter a.remove-filter {
display: none;
}
.filter:hover a.remove-filter {
display: block;
}
optgroup {
font-weight: 600;
}
@@ -3415,7 +3127,7 @@ table.table td.cell .html-container {
.post-container {
.internal-mode-switcher {
color: var(--text-muted-color);
margin-left: var(--10px);
margin-left: var(--14px);
display: inline-block;
position: relative;
top: var(--2px);
@@ -4557,3 +4269,17 @@ body > .autocomplete-suggestions.text-search-suggestions {
@import "elements/popup-notification.less";
@import "elements/grid.less";
@import "elements/animation.less";
.compact-form {
--input-height-base: var(--input-height-small);
--line-height-computed: var(--line-height-small);
--line-height-base: var(--line-height-small);
--font-size-base: var(--font-size-small);
--padding-base-vertical: var(--padding-small-vertical);
--padding-base-horizontal: var(--padding-small-horizontal);
--icon-size-base: var(--icon-size-small);
--icon-line-height-base: var(--icon-line-height-small);
--btn-icon-width: var(--btn-icon-width-small);
}
+2 -6
View File
@@ -56,10 +56,6 @@ a.btn {
}
}
.panel-heading .btn-sm {
height: auto;
}
.input-group {
.input-group-btn > {
.btn {
@@ -102,7 +98,7 @@ a.btn {
}
.btn-icon {
width: var(--36px);
width: var(--btn-icon-width);
padding-left: 0;
padding-right: 0;
@@ -121,7 +117,7 @@ a.btn {
}
.btn-icon.btn-sm {
width: var(--34px);
width: var(--btn-icon-width-small);
.fa, .fas {
font-size: var(--12px);
+5 -3
View File
@@ -8,6 +8,8 @@
padding: var(--5px) 0;
user-select: none;
min-width: calc(var(--50px) + var(--10px));
.autocomplete-suggestion.autocomplete-selected {
background-color: var(--dropdown-link-hover-bg) !important;
color: var(--dropdown-link-hover-color) !important;
@@ -100,7 +102,7 @@ ul.dropdown-menu {
line-height: var(--line-height-computed);
&.active {
background-color: @gray-lighter;
background-color: var(--dropdown-link-active-bg);
text-decoration: none;
outline: 0;
}
@@ -204,11 +206,11 @@ ul.dropdown-menu {
margin-right: var(--2px);
}
.fix-overflow {
/*.fix-position {
.btn-group {
position: unset;
}
}
}*/
.dropdown-menu {
.quick-search-list-item {
+302 -39
View File
@@ -124,21 +124,6 @@ input.form-control {
}
}
.link-container:not(:empty) + div,
.link-container:not(:empty) + div > div.input-group {
> .form-control,
> .btn,
> .input-group-btn > .btn {
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
}
.link-container:not(:empty) + .form-control {
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
.input-group > .input-group-btn:first-child > select.form-control:first-child {
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
@@ -510,6 +495,8 @@ input[type="radio"].form-radio {
.control-label {
user-select: none;
font-size: var(--font-size-base);
}
.form-group.hidden-cell {
@@ -663,46 +650,322 @@ select.form-control.native-select {
}
}
/*
.link-container {
a[data-action="clearLink"] {
color: var(--gray-soft);
&:hover {
color: var(--btn-text-color);
.field {
.link-container {
.list-group-item {
> div {
margin: calc(var(--6px) * -1) 0 calc(var(--6px) * -1);
}
> div > div {
margin: var(--6px) 0;
}
.form-control,
.btn {
margin-top: var(--2px);
margin-bottom: 0;
}
> span.text {
width: calc(100% - var(--18px));
display: inline-block;
}
> span.drag-handle {
display: inline-block;
width: var(--18px);
color: var(--text-muted-color);
cursor: grab;
vertical-align: top;
&:active {
cursor: grabbing;
}
}
> span.item-button {
display: inline-block;
width: var(--18px);
vertical-align: top;
}
&:has(> .item-button) {
> span.text {
width: calc(100% - var(--36px));
}
}
&:has(> .drag-handle) {
> span.text {
width: calc(100% - var(--36px));
}
}
&:has(> .item-button):has(> .drag-handle) {
> span.text {
width: calc(100% - var(--36px) - var(--18px));
}
}
&:has(> .item-button.btn-group):has(> .drag-handle) {
> span.text {
width: calc(100% - var(--36px) - var(--26px));
}
}
> a[role="button"] {
margin-top: var(--1px);
margin-left: var(--2px);
}
}
.link-group-item-with-columns {
> div > .btn-group {
margin-top: 0;
margin-bottom: 0;
.caret {
border-top-color: var(--gray-light);
}
}
}
> .list-group-item.link-with-role {
> a[role="button"],
> div > a[role="button"] {
margin-top: var(--7px);
}
}
> .list-group-item.link-group-item-with-columns,
> .list-group-item.link-group-item-with-primary {
> div:nth-child(1) {
display: inline-block;
width: calc(~"100% - 23px");
float: left;
input.form-control {
width: 100%;
}
}
> div:nth-child(2) {
display: inline-block;
width: var(--23px);
float: right;
vertical-align: top;
}
}
}
> .label {
white-space: normal;
}
.list-group .list-group-item {
background-color: var(--panel-bg);
}
.list-group {
background-color: var(--default-border-color);
}
}
.field,
.cell {
.checklist-label {
color: var(--text-color);
margin-bottom: 0;
padding-left: var(--7px);
}
.checklist-item-container {
margin-bottom: var(--2px);
&:last-child {
margin-bottom: 0;
}
> input[type=checkbox] {
float: left;
}
> input[type=checkbox]:not(:disabled) + label {
cursor: pointer;
}
user-select: none;
}
.multi-enum-item-label-container {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding-bottom: var(--4px);
&:last-child {
padding-bottom: var(--1px);
}
.label-md {
line-height: 1.5;
}
}
}
.panel-body {
.field {
> .link-container {
> .list-group-item {
background-color: var(--panel-bg);
}
}
}
}
.filter {
> .form-group .field {
.link-container {
font-size: var(--font-size-small);
.fa-times {
font-size: var(--12px);
}
> .list-group-item {
padding-top: var(--4px);
padding-bottom: var(--4px);
line-height: var(--20px);
}
}
.input-group {
input {
font-size: var(--font-size-small);
height: var(--input-height-small);
min-height: var(--input-height-small);
}
button.btn {
font-size: var(--font-size-small);
height: var(--input-height-small);
}
}
}
.link-container {
.list-group-item a[role="button"] {
margin-top: 0;
margin-left: var(--2px);
}
}
.selectize-input {
min-height: var(--input-height-small);
}
}
.link-container {
> .list-group-item {
a[role="button"] {
color: var(--gray-soft);
&:hover {
color: var(--btn-text-color);
.link-item-column {
float: right;
display: inline-block;
width: 40%;
> .selectize-control {
width: 100%;
}
}
> a.pull-right {
margin-left: var(--4px);
}
&,
> .text {
> img.avatar {
margin-right: var(--7px);
top: var(--2px);
}
}
.link-item-name {
> img.avatar {
margin-right: var(--7px);
top: var(--2px);
}
}
}
&:not(:empty) {
+ div,
+ div > div.input-group {
> .form-control,
> .btn,
> .input-group-btn > .btn {
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
}
+ .form-control {
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
}
}
.cell > .field {
overflow-wrap: break-word;
word-wrap: break-word;
}
.field {
.add-item-container {
a.add-item {
color: var(--gray-soft);
&:hover {
color: var(--btn-text-color);
}
.row {
margin-left: calc(var(--3px) * -1) !important;
margin-right: calc(var(--3px) * -1) !important;
> div {
padding-left: var(--3px) !important;
padding-right: var(--3px) !important;
}
}
.item-list {
.item {
a.remove-item {
color: var(--gray-soft);
&:hover {
color: var(--btn-text-color);
}
.form-control,
.btn {
margin-bottom: var(--3px);
}
.input-group .form-control,
.input-group .btn {
margin-bottom: 0;
}
.input-group {
margin-bottom: var(--3px);
}
.link-container {
margin-bottom: 0;
&:not(.no-input) {
> .list-group-item:last-child {
border-bottom-width: 0;
}
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
> .list-group-item:last-child {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom-width: 0;
}
}
> .list-group-item {
.text:empty:before {
content: "\200b";
}
}
}
}
*/
+5 -11
View File
@@ -1,8 +1,8 @@
.fas,
.far,
.fa {
font-size: var(--16px);
line-height: var(--21px);
font-size: var(--icon-size-base);
line-height: var(--icon-line-height-base);
}
.btn {
@@ -11,7 +11,7 @@
line-height: var(--line-height-computed);
&.fa-sm {
line-height: var(--18px);
line-height: var(--icon-line-height-small);
}
}
}
@@ -24,14 +24,8 @@
.small .fas,
.fas.small,
.far.small {
font-size: var(--12px);
line-height: var(--18px);
}
.btn:not(.btn-sm) {
> .fas.fa-ellipsis-h {
//line-height: var(--23px);
}
font-size: var(--icon-size-small);
line-height: var(--icon-line-height-small);
}
.btn:not(.btn-sm),
+8 -4
View File
@@ -271,7 +271,7 @@
@media screen and (min-width: @screen-md-min) {
.modal-dialog {
width: ~"min(var(--900px), 90vw)";
width: ~"min(calc(var(--900px) + var(--30px)), 90vw)";
}
.dialog-confirm > .modal-dialog,
@@ -348,8 +348,12 @@
}
.modal.dialog-record {
.modal-body {
scrollbar-gutter: stable;
.modal {
&.dialog-record,
&:has(> .modal-dialog > .modal-content > .modal-body > .record.no-side-margin),
&:has(> .modal-dialog > .modal-content > .modal-body > .record-container.no-side-margin) {
.modal-body {
scrollbar-gutter: stable;
}
}
}
+4
View File
@@ -618,6 +618,10 @@ body {
}
}
.panel-heading .btn-sm {
height: auto;
}
.panel.highlighted {
outline: var(--2px) solid var(--brand-info);
outline-offset: var(--minus-1px);
+33 -1
View File
@@ -13,7 +13,7 @@
}
&.record-grid-small {
grid-template-columns: minmax(auto, 60%) minmax(auto, 40%);
grid-template-columns: minmax(auto, 61%) minmax(auto, 39%);
}
max-width: var(--record-grid-max-width);
@@ -35,6 +35,38 @@
.record .record-grid {
grid-column-gap: var(--padding-base-horizontal);
}
@media screen and (min-width: @screen-sm-min) {
.record,
.record-container,
.edit-container {
&.no-side-margin {
> div {
> .record-grid-wide {
margin-right: var(--panel-padding);
> .left {
> .middle {
> .panel {
&:not(.middle):not(.last),
&.first {
border-top-right-radius: var(--panel-border-radius);
}
&:not(.middle):not(.first),
&.last {
border-bottom-right-radius: var(--panel-border-radius);
}
}
}
}
}
}
}
}
}
}
@media screen and (max-width: @screen-sm-max) {

Some files were not shown because too many files have changed in this diff Show More