Compare commits

...

24 Commits

Author SHA1 Message Date
Yuri Kuznetsov 5b0787474e 8.3.2 2024-06-26 07:53:49 +03:00
Yuri Kuznetsov 8c87f20374 comment 2024-06-26 07:50:19 +03:00
Yuri Kuznetsov ba35115a48 inline edit: revert initial attributes on error 2024-06-26 07:44:44 +03:00
Yuri Kuznetsov 069010d0fe log opened only for system user 2024-06-26 07:22:09 +03:00
Yuri Kuznetsov fe5878fd99 2fa auth log records on fail 2024-06-25 20:39:44 +03:00
Yuri Kuznetsov 839ceea142 root api endpoint fix type 2024-06-25 08:35:56 +03:00
Yuri Kuznetsov 9cce9d7347 comment 2024-06-24 11:16:45 +03:00
Yuri Kuznetsov 69d0dbbf1c email collapse fix 2024-06-24 11:09:29 +03:00
Yuri Kuznetsov 68ef9ce4ac disable email field layouts 2024-06-24 10:54:48 +03:00
Yuri Kuznetsov 163cf047e5 8.3.1 2024-06-24 10:25:00 +03:00
Yuri Kuznetsov cc574afd3d labels 2024-06-23 17:52:57 +03:00
Yuri Kuznetsov 77d76fe0ee bg lang 2024-06-23 11:19:19 +03:00
Yuri Kuznetsov e7ab75ec5a link-one fix 2024-06-23 10:47:07 +03:00
Yuri Kuznetsov ff6983c9f3 fix wysiwyg sticky toolbar position 2024-06-22 09:26:40 +03:00
Yuri Kuznetsov e2673473c5 wysiwyg codeview not limited height 2024-06-22 09:09:42 +03:00
Yuri Kuznetsov e8bf70ab8e wysiwyg fullscreen codeview switch fix 2024-06-22 08:59:04 +03:00
Yuri Kuznetsov f459d5811d wysiwyg height not being changed fix 2024-06-22 08:45:10 +03:00
Yuri Kuznetsov 53bf9b024a address field: support list-link template 2024-06-21 22:12:04 +03:00
Yuri Kuznetsov 25aed1a1c4 style fix 2024-06-21 16:17:45 +03:00
Yuri Kuznetsov 2589801993 css fix 2024-06-21 13:14:40 +03:00
Yuri Kuznetsov 5f954c22da fix header dropdown on small screen 2024-06-21 12:41:36 +03:00
Yuri Kuznetsov 423e2ca544 fix favicon metadata path 2024-06-21 12:04:11 +03:00
Yuri Kuznetsov 6176b8770f fulltext hyphen use natural language mode 2024-06-21 09:40:20 +03:00
Yuri Kuznetsov 8ab7452859 fix kanban fetch 2024-06-20 15:42:03 +03:00
44 changed files with 418 additions and 84 deletions
+6 -2
View File
@@ -29,10 +29,14 @@
namespace Espo\Controllers;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Utils\Json;
class ApiIndex
{
public function getActionIndex(): string
public function getActionIndex(Request $request, Response $response): void
{
return "EspoCRM REST API";
$response->writeBody(Json::encode("EspoCRM REST API"));
}
}
+14 -2
View File
@@ -195,8 +195,12 @@ class Auth
throw new BadRequest("Auth: Bad authorization string provided.");
}
/** @var array{string, string} */
return explode(':', $stringDecoded, 2);
[$username, $password] = explode(':', $stringDecoded, 2);
$username = trim($username);
$password = trim($password);
return [$username, $password];
}
private function handleSecondStepRequired(Response $response, Result $result): void
@@ -316,6 +320,14 @@ class Auth
$username = $request->getServerParam('PHP_AUTH_USER');
$password = $request->getServerParam('PHP_AUTH_PW');
if (is_string($username)) {
$username = trim($username);
}
if (is_string($password)) {
$password = trim($password);
}
return [$username, $password];
}
@@ -241,7 +241,7 @@ class Authentication
$result = $this->processTwoFactor($result, $request);
if ($result->isFail()) {
return $this->processFail($result, $data, $request);
return $this->processTwoFactorFail($result, $data, $request, $authLogRecord);
}
}
@@ -818,4 +818,22 @@ class Authentication
throw new Forbidden();
}
private function processTwoFactorFail(
Result $result,
AuthenticationData $data,
Request $request,
?AuthLogRecord $authLogRecord
): Result {
if ($authLogRecord) {
$authLogRecord
->setIsDenied()
->setDenialReason(AuthLogRecord::DENIAL_REASON_WRONG_CODE);
$this->entityManager->saveEntity($authLogRecord);
}
return $this->processFail($result, $data, $request);
}
}
@@ -68,6 +68,8 @@ class FailedAttemptsLimit implements BeforeLogin
return;
}
$isSecondStep = $request->getHeader('Espo-Authorization-Code') !== null;
$failedAttemptsPeriod = $this->configDataProvider->getFailedAttemptsPeriod();
$maxFailedAttempts = $this->configDataProvider->getMaxFailedAttemptNumber();
@@ -80,14 +82,21 @@ class FailedAttemptsLimit implements BeforeLogin
throw new RuntimeException($e->getMessage());
}
$apAddress = $this->util->obtainIpFromRequest($request);
$ipAddress = $this->util->obtainIpFromRequest($request);
$where = [
'requestTime>' => $requestTimeFrom->format('U'),
'ipAddress' => $apAddress,
'isDenied' => true,
];
if ($isSecondStep) {
$where['username'] = $data->getUsername();
}
if (!$isSecondStep) {
$where['ipAddress'] = $ipAddress;
}
$wasFailed = (bool) $this->entityManager
->getRDBRepository(AuthLogRecord::ENTITY_TYPE)
->select(['id'])
@@ -107,6 +116,12 @@ class FailedAttemptsLimit implements BeforeLogin
return;
}
throw new Forbidden("Max failed login attempts exceeded for IP address $apAddress.");
if ($isSecondStep) {
$username = $data->getUsername() ?? '';
throw new Forbidden("Max failed 2FA login attempts exceeded for username '$username'.");
}
throw new Forbidden("Max failed login attempts exceeded for IP address $ipAddress.");
}
}
+8
View File
@@ -603,6 +603,14 @@ class Service implements Crud,
continue;
}
if (
// link-one
$attributeDefs->getType() === AttributeType::FOREIGN &&
$attributeDefs->getParam('attributeRole') === 'id'
) {
continue;
}
$attribute = $attributeDefs->getName();
unset($data->$attribute);
@@ -90,7 +90,8 @@ class DefaultDataComposer implements DataComposer
if (
mb_strpos($preparedFilter, ' ') === false &&
mb_strpos($preparedFilter, '+') === false &&
mb_strpos($preparedFilter, '-') === false &&
mb_strpos($preparedFilter, ' -') === false &&
mb_strpos($preparedFilter, '-') !== 0 &&
mb_strpos($preparedFilter, '*') === false
) {
$mode = Mode::NATURAL_LANGUAGE;
@@ -170,7 +170,7 @@ class ClientManager
$cssFileList = $this->metadata->get(['app', 'client', 'cssList'], []);
$linkList = $this->metadata->get(['app', 'client', 'linkList'], []);
$faviconAlternate = $this->metadata->get('app.client.favicon') ?? $this->faviconAlternate;
$faviconAlternate = $this->metadata->get('app.client.faviconAlternate') ?? $this->faviconAlternate;
[$favicon, $faviconType] = $this->getFaviconData();
$scriptsHtml = implode('',
@@ -397,7 +397,7 @@ class ClientManager
*/
private function getFaviconData(): array
{
$faviconSvgPath = $this->metadata->get('app.client.faviconSvg') ?? $this->favicon;
$faviconSvgPath = $this->metadata->get('app.client.favicon') ?? $this->favicon;
$faviconType = str_ends_with($faviconSvgPath, '.svg') ? 'image/svg+xml' : 'image/png';
return [$faviconSvgPath, $faviconType];
@@ -36,6 +36,7 @@ class AuthLogRecord extends Entity
public const ENTITY_TYPE = 'AuthLogRecord';
public const DENIAL_REASON_CREDENTIALS = 'CREDENTIALS';
public const DENIAL_REASON_WRONG_CODE = 'WRONG_CODE';
public const DENIAL_REASON_INACTIVE_USER = 'INACTIVE_USER';
public const DENIAL_REASON_IS_PORTAL_USER = 'IS_PORTAL_USER';
public const DENIAL_REASON_IS_NOT_PORTAL_USER = 'IS_NOT_PORTAL_USER';
@@ -29,6 +29,7 @@
namespace Espo\Modules\Crm\EntryPoints;
use Espo\Entities\User;
use Espo\Modules\Crm\Entities\Campaign;
use Espo\Modules\Crm\Entities\EmailQueueItem;
use Espo\Modules\Crm\Entities\MassEmail;
@@ -47,7 +48,8 @@ class CampaignTrackOpened implements EntryPoint
public function __construct(
private EntityManager $entityManager,
private LogService $service
private LogService $service,
private User $user,
) {}
/**
@@ -105,7 +107,9 @@ class CampaignTrackOpened implements EntryPoint
return;
}
$this->service->logOpened($campaignId, $queueItem);
if ($this->user->isSystem()) {
$this->service->logOpened($campaignId, $queueItem);
}
header('Content-Type: image/png');
@@ -0,0 +1,20 @@
{
"labels": {
"Create AddressCountry": "Създаване на Държава",
"Populate": "Попълни"
},
"fields": {
"code": "Код",
"isPreferred": "Е предпочитана"
},
"tooltips": {
"code": "ISO 3166-1 alpha-2 код.",
"isPreferred": "Предпочитаните държави се появяват първи в списъка за избор."
},
"messages": {
"confirmPopulateDefaults": "Всички съществуващи държави ще бъдат изтрити, ще бъде създаден списъкът с държави по подразбиране. Няма да е възможно да върнете операцията."
},
"strings": {
"populateDefaults": "Попълване със списък на държави по подразбиране"
}
}
@@ -83,7 +83,9 @@
"Working Time Calendars": "Календари на работното време",
"Group Email Folders": "Групови имейл папки",
"Authentication Providers": "Доставчици на удостоверяване",
"Setup": "Инсталация"
"Setup": "Инсталация",
"App Log": "Логове на приложението",
"Address Countries": "Списък с държави"
},
"layouts": {
"list": "Лист",
@@ -204,7 +206,9 @@
"autocompleteOnEmpty": "Автоматично довършване при натискане",
"relateOnImport": "Релации при импорт",
"aclScope": "ACL обхват",
"onlyAdmin": "Само за администратори"
"onlyAdmin": "Само за администратори",
"activeOptions": "Активни опции",
"labelType": "Тип заглавие"
},
"messages": {
"selectEntityType": "Изберете вида обект от лявото меню.",
@@ -226,7 +230,9 @@
"upgradeRecommendation": "Този начин на обновяване не се препоръчва. По-добре е да обновите от CLI.",
"newVersionIsAvailable": "Налична е нова версия на EspoCRM {latestVersion}. Моля, следвайте [инструкциите](https://www.espocrm.com/documentation/administration/upgrading/), за да обновите вашата инстанция.",
"formulaFunctions": "Още функции можете да намерите в [документацията]({documentationUrl}).",
"rebuildRequired": "Трябва да регенерирате кеша на системата от CLI."
"rebuildRequired": "Трябва да регенерирате кеша на системата от CLI.",
"cacheIsDisabled": "Кешът е деактивиран, приложението ще работи бавно. Активирайте кеширането в [настройки](#Admin/settings).",
"cronIsDisabled": "Cron е деактивиран, приложението не работи напълно. Активирайте cron в [настройки](#Admin/settings)."
},
"descriptions": {
"settings": "Системни настройки на платформата.",
@@ -276,7 +282,9 @@
"formulaSandbox": "Може да пишете и тествате скриптове за формули, преди да ги приложите в системата.",
"workingTimeCalendars": "Управление на работно време.",
"groupEmailFolders": "Споделени имейл акаунти с другите потребители.",
"authenticationProviders": "Допълнителни доставчици на удостоверяване за порталите."
"authenticationProviders": "Допълнителни доставчици на удостоверяване за порталите.",
"appLog": "Логове на приложението.",
"addressCountries": "Държави, налични в адресни полета."
},
"options": {
"previewSize": {
@@ -285,6 +293,10 @@
"medium": "Среден",
"large": "Голям",
"": "По подразбиране"
},
"labelType": {
"state": "Област",
"regular": "Основен"
}
},
"logicalOperators": {
@@ -0,0 +1,15 @@
{
"fields": {
"message": "Съобщение",
"code": "Код",
"level": "Ниво",
"exceptionClass": "Тип грешка",
"file": "Файл",
"line": "Ред",
"requestMethod": "HTTP метод",
"requestResourcePath": "Път до ресурс"
},
"presetFilters": {
"errors": "Грешки"
}
}
@@ -30,7 +30,9 @@
"INACTIVE_USER": "Деактивиран потребител",
"IS_PORTAL_USER": "Потребителят е към външен портал",
"IS_NOT_PORTAL_USER": "Потребителят не е към външен портал",
"USER_IS_NOT_IN_PORTAL": "Потребителят не е свързан с този портал"
"USER_IS_NOT_IN_PORTAL": "Потребителят не е свързан с този портал",
"IS_SYSTEM_USER": "Е системен потребител",
"FORBIDDEN": "Забранено"
}
}
}
@@ -68,7 +68,8 @@
"ccEmailAddresses": "CC имейл адреси",
"bccEmailAddresses": "BCC имейл адреси",
"replyToEmailAddresses": "Reply-To имейл адреси",
"groupFolder": "Групова папка"
"groupFolder": "Групова папка",
"createdEvent": "Създадено събитие"
},
"options": {
"status": {
@@ -112,7 +113,8 @@
"Insert Field": "Добавяне на поле",
"Event": "Събитие",
"Moving to folder": "Преместване в папка",
"Group Folders": "Групови папки"
"Group Folders": "Групови папки",
"View Attachments": "Преглед на прикачените файлове"
},
"messages": {
"testEmailSent": "Тестовия имейл е изпратен",
@@ -22,7 +22,8 @@
"smtpPassword": "SMTP парола",
"useImap": "Прихващане на имейли",
"smtpAuthMechanism": "Механизъм за SMTP удостоверяване",
"security": "Сигурност"
"security": "Сигурност",
"connectedAt": "Свързано с"
},
"links": {
"filters": "Филтри",
@@ -42,12 +43,16 @@
},
"messages": {
"couldNotConnectToImap": "Не може да се свърже с IMAP сървъра",
"connectionIsOk": "Връзката е ОК"
"connectionIsOk": "Връзката е ОК",
"imapNotConnected": "Не можа да се свърже с [IMAP акаунт](#EmailAccount/view/{id})."
},
"tooltips": {
"monitoredFolders": "Няколко папки трябва да бъдат разделени със запетая.\n\nМожете да добавите папка „Sent“, за да синхронизирате имейли, изпратени от външен имейл клиент.",
"storeSentEmails": "Изпратените имейли ще се съхраняват на IMAP сървъра. Полето за имейл адрес трябва да съвпада с адреса, от който ще бъдат изпращани имейлите.",
"useSmtp": "Възможността да се изпращат имейли.",
"emailAddress": "Потребителският запис (назначения потребител) трябва да има същия имейл адрес, за да можете да използвате този имейл акаунт за изпращане."
},
"presetFilters": {
"active": "Активен"
}
}
@@ -46,7 +46,8 @@
"module": "Модул",
"version": "Версия",
"selectFilter": "Избери филтър",
"primaryFilters": "Основни филтри"
"primaryFilters": "Основни филтри",
"stars": "Любими"
},
"options": {
"type": {
@@ -92,6 +93,7 @@
"optimisticConcurrencyControl": "Предотвратява възможност от презаписване на един и същи запис от двама или повече потребители едновременно.",
"duplicateCheckFieldList": "Кои полета да се проверяват, когато се извършва проверка за дублирани записи.",
"updateDuplicateCheck": "Извършване на проверка за дублирани записи при актуализиране на запис.",
"linkSelectFilter": "Основен филтър за прилагане по подразбиране при изберане на запис."
"linkSelectFilter": "Основен филтър за прилагане по подразбиране при изберане на запис.",
"stars": "Възможност за добавяне на записи в \"Любими\". Звездите могат да се използват от потребителите за отбелязване на записи."
}
}
@@ -56,11 +56,13 @@
"Note": "Коментар",
"ImportError": "Грешка при импортиране",
"WorkingTimeCalendar": "Календар на работно време",
"WorkingTimeRange": "Диапазон на работното време",
"GroupEmailFolder": "Групова имейл папка",
"AuthenticationProvider": "Доставчик на удостоверяване",
"GlobalStream": "Глобална активност",
"WebhookQueueItem": "Обект в опашка на Webhook"
"WebhookQueueItem": "Обект в опашка на Webhook",
"AppLogRecord": "Логове на приложението",
"WorkingTimeRange": "Изключение от работното време",
"AddressCountry": "Адрес Държава"
},
"scopeNamesPlural": {
"Email": "Имейли",
@@ -106,11 +108,13 @@
"Note": "Коментари",
"ImportError": "Грешки при импортиране",
"WorkingTimeCalendar": "Календари на работното време",
"WorkingTimeRange": "Диапазони на работно време",
"GroupEmailFolder": "Групови имейл папки",
"AuthenticationProvider": "Доставчици на удостоверяване",
"GlobalStream": "Глобална активност",
"WebhookQueueItem": "Обекти в опашка на Webhook"
"WebhookQueueItem": "Обекти в опашка на Webhook",
"AppLogRecord": "Логове на приложението",
"WorkingTimeRange": "Изключения от работното време",
"AddressCountry": "Адрес Държави"
},
"labels": {
"Misc": "Разни",
@@ -290,7 +294,10 @@
"Next Page": "Следваща страница",
"First Page": "Първа страница",
"Last Page": "Последна страница",
"Page": "Страница"
"Page": "Страница",
"Star": "Добави в любими",
"Unstar": "Махни от любими",
"Starred": "Добавено в любими"
},
"messages": {
"pleaseWait": "Моля изчакайте...",
@@ -393,7 +400,14 @@
"noLinkAccess": "Не може да свържете със запис {foreignEntityType} чрез връзката „{link}“. Нямате достъп.",
"attemptIntervalFailure": "Операцията не е разрешена през определен интервал от време. Изчакайте известно време преди следващия опит.",
"confirmRestoreFromAudit": "Предишните стойности ще бъдат зададени във формуляр. След това можете да запишете записа, за да възстановите предишните стойности.",
"pageNumberIsOutOfBound": "Номер на страницата е невалиден"
"pageNumberIsOutOfBound": "Номер на страницата е невалиден",
"fieldPhoneExtensionTooLong": "Разширението не трябва да е по-дълго от {maxLength}",
"cannotLinkAlreadyLinked": "Не може да се свърже вече свързан запис.",
"starsLimitExceeded": "Броят на 'любими' надхвърли лимита.",
"select2OrMoreRecords": "Изберете 2 или повече записа",
"selectNotMoreThanNumberRecords": "Изберете не повече от {number} записа",
"selectAtLeastOneRecord": "Изберете поне един запис",
"fieldNotMatchingPattern$phoneNumberLoose": "{field} съдържа знаци, които не са разрешени в телефонен номер"
},
"boolFilters": {
"onlyMy": "Само мои",
@@ -402,7 +416,8 @@
},
"presetFilters": {
"followed": "Следвани",
"all": "Всички"
"all": "Всички",
"starred": "Добавено в любими"
},
"massActions": {
"remove": "Премахване",
@@ -715,7 +730,7 @@
"providers": "(YouTube, Vimeo, лоза, Instagram, или DailyMotion)"
},
"table": {
"table": "маса"
"table": "Таблица"
},
"hr": {
"insert": "Поставете на хоризонтална линия"
@@ -788,10 +803,10 @@
"phoneNumber": "Валиден телефонен номер",
"array": "Масив",
"arrayOfString": "Масив от текстове",
"valid": "Валиден",
"noEmptyString": "Без празен текст",
"max": "Макс. стойност",
"min": "Мин. стойност"
"min": "Мин. стойност",
"valid": "Валидност"
},
"fieldValidationExplanations": {
"url_valid": "Невалидна URL стойност.",
@@ -806,7 +821,10 @@
"enum_valid": "Невалидна стойност на падащо меню. Стойността трябва да бъде една от дефинираните опции за изброяване. Празна стойност е разрешена само ако полето има празна опция.",
"multiEnum_valid": "Невалидна стойност за множество изброявания. Стойностите трябва да са една от дефинираните опции на полето.",
"int_valid": "Невалидна стойност на цяло число.",
"float_valid": "Невалидна стойност на число."
"float_valid": "Невалидна стойност на число.",
"valid": "Невалидна стойност",
"maxLength": "Дължината на стойността надвишава максималния лимит.",
"phone_valid": "Телефонният номер не е валиден. Може да се дължи на грешен или празен код на държавата."
},
"navbarTabs": {
"Business": "Бизнес",
@@ -816,5 +834,28 @@
},
"themes": {
"Light": "Светла"
},
"wysiwygLabels": {
"cell": "Клетка",
"align": "Подравняване",
"width": "Ширина",
"height": "Височина",
"borderWidth": "Ширина на контура",
"borderColor": "Цвят на контура",
"cellPadding": "Отстояние на клетката",
"backgroundColor": "Цвят на фона",
"verticalAlign": "Вертикално подравняване"
},
"wysiwygOptions": {
"align": {
"left": "Ляво",
"center": "Център",
"right": "Дясно"
},
"verticalAlign": {
"top": "Горе",
"middle": "В средата",
"bottom": "Долу"
}
}
}
@@ -38,7 +38,9 @@
"keepFetchedEmailsUnread": "Извлечените имейли остават непрочетени на сървъра",
"smtpAuthMechanism": "Механизъм за SMTP удостоверяване",
"security": "Сигурност",
"groupEmailFolder": "Групова имейл папка"
"groupEmailFolder": "Групова имейл папка",
"connectedAt": "Свързано с",
"excludeFromReply": "Изключване от отговор"
},
"tooltips": {
"reply": "Уведомете изпращачите на имейли, че техните имейли са получени.\n\nСамо един имейл ще бъде изпратен до конкретен получател през определен период от време, за да се предотврати зацикляне и спам.",
@@ -55,7 +57,8 @@
"smtpIsForMassEmail": "Ако е отметнато, SMTP ще бъде наличен за масов имейл.",
"storeSentEmails": "Изпратените имейли ще се съхраняват на IMAP сървъра.",
"useSmtp": "Възможността за изпращане на имейли.",
"groupEmailFolder": "Поставяне на входящите имейли в групова папка."
"groupEmailFolder": "Поставяне на входящите имейли в групова папка.",
"excludeFromReply": "Когато отговаряте на имейли, изпратени до имейл адреса на този акаунт, неговият имейл адрес няма да бъде добавен към CC.\n\nИмайте предвид, че като активирате този параметър, имейл адресът на този акаунт ще бъде изложен на потребителите, които имат достъп за изпращане на имейли."
},
"links": {
"filters": "Филтри",
@@ -81,6 +84,7 @@
"Main": "Основен"
},
"messages": {
"couldNotConnectToImap": "Систмата не може да се свърже с IMAP сървъра"
"couldNotConnectToImap": "Систмата не може да се свърже с IMAP сървъра",
"imapNotConnected": "Не може да се свърже с групата [IMAP акаунт](#InboundEmail/view/{id})."
}
}
@@ -15,7 +15,8 @@
"tabBreak": "Разделител на табове",
"width": "Ширина",
"noteText": "Текст за съобщението",
"noteStyle": "Стил на съобщението"
"noteStyle": "Стил на съобщението",
"isMuted": "Приглушен цвят"
},
"options": {
"align": {
@@ -12,7 +12,8 @@
"related": "Свързани",
"createdByGender": "Създаден по пол",
"data": "Данни",
"number": "Номер"
"number": "Номер",
"isPinned": "Закачено е"
},
"filters": {
"all": "Всички",
@@ -21,7 +22,8 @@
"activity": "Активност"
},
"messages": {
"writeMessage": "Напишете съобщението си тук"
"writeMessage": "Напишете съобщението си тук",
"pinnedMaxCountExceeded": "Не могат да се закачат повече бележки. Максималният разрешен брой е {count}."
},
"options": {
"targetType": {
@@ -52,6 +54,9 @@
},
"labels": {
"View Posts": "Преглед на коментарите",
"View Activity": "Преглед на активността"
"View Activity": "Преглед на активността",
"Pin": "Закачане",
"Unpin": "Откачане",
"Pinned": "Закачено"
}
}
@@ -16,7 +16,9 @@
"customUrl": "персонализиран URL адрес",
"customId": "Персонализиран идентификатор",
"layoutSet": "Оформление",
"authenticationProvider": "Доставчик на удостоверяване"
"authenticationProvider": "Доставчик на удостоверяване",
"authTokenLifetime": "Живот на токена за удостоверяване (часове)",
"authTokenMaxIdleTime": "Максимално време на неактивност на токена за удостоверяване (часове)"
},
"links": {
"users": "Потребители",
@@ -32,7 +32,10 @@
"assignmentEmailNotificationsIgnoreEntityTypeList": "Известия за назначения на записи (по имейл)",
"dashboardLocked": "Заключи дашборда",
"textSearchStoringDisabled": "Деактивиране на съхраняването на текстови филтри",
"calendarSlotDuration": "Продължителност на календарния слот"
"calendarSlotDuration": "Продължителност на календарния слот",
"calendarScrollHour": "Календар - Скролване до час",
"defaultRemindersTask": "Напомняния по подразбиране за задачи",
"addCustomTabs": "Добавете персонализирани табове"
},
"options": {
"weekStart": {
@@ -51,7 +54,8 @@
"autoFollowEntityTypeList": "Автоматично следвайте ВСИЧКИ нови записи (създадени от всеки потребител) на избраните типове обекти. За да можете да виждате информация в потока и да получавате известия за всички записи в системата.",
"doNotFillAssignedUserIfNotRequired": "При създаване на запис назначеният потребител няма да бъде попълнен със собствен потребител, освен ако полето не е задължително.",
"followCreatedEntities": "Когато създавате нови записи, те ще бъдат следвани автоматично, дори ако са назначени на друг потребител.",
"followCreatedEntityTypeList": "Когато създавате нови записи за избрани типове обекти, те ще бъдат следвани автоматично, дори ако са назначени на друг потребител."
"followCreatedEntityTypeList": "Когато създавате нови записи за избрани типове обекти, те ще бъдат следвани автоматично, дори ако са назначени на друг потребител.",
"addCustomTabs": "Ако е отбелязано, персонализираните раздели ще бъдат добавени към разделите по подразбиране. В противен случай ще се използват персонализирани раздели вместо раздели по подразбиране."
},
"tabFields": {
"label": "Текст",
@@ -13,7 +13,8 @@
"data": "Данни",
"fieldData": "Данни на полето",
"messagePermission": "Права за писане на съобщения",
"auditPermission": "Права за одитна история"
"auditPermission": "Права за одитна история",
"mentionPermission": "Права за споменаване"
},
"links": {
"users": "Потребители",
@@ -62,6 +63,7 @@
"groupEmailAccountPermission": "Достъп до групови имейл акаунти, възможност за изпращане на имейли от групов SMTP акаунт.",
"exportPermission": "Позволява експортването на записи.",
"massUpdatePermission": "Възможност за извършване на масово актуализиране на записи.",
"auditPermission": "Позволява потребителя да достъпва одитната история."
"auditPermission": "Позволява потребителя да достъпва одитната история.",
"mentionPermission": "Позволява споменаването на други потребители в потока.\n\n* всички – може да спомена всички\n* екип – може да споменава само хора от същия екип\n* не – не може да споменава"
}
}
@@ -87,7 +87,6 @@
"ldapPortalUserLdapAuth": "Използване на LDAP удостоверяване за потребители на портал",
"ldapPortalUserPortals": "Портали по подразбиране за потребител на портал",
"ldapPortalUserRoles": "Роли по подразбиране за потребител на портала",
"addressCountryList": "Списък за автоматично дописване на държави в адресни полета",
"fiscalYearShift": "Начало на фискалната година",
"jobRunInParallel": "Паралелно изпълнение на background задачите",
"daemonInterval": "Daemon интервал",
@@ -130,7 +129,12 @@
"phoneNumberInternational": "Международни телефонни номера",
"phoneNumberPreferredCountryList": "Предпочитани телефонни кодове на държави",
"jobForceUtc": "Принудително избиране на UTC часовата зона",
"emailAddressSelectEntityTypeList": "Обхвати за избор на имейл адрес"
"emailAddressSelectEntityTypeList": "Обхвати за избор на имейл адрес",
"phoneNumberExtensions": "Кодове за телефонни номера",
"quickSearchFullTextAppendWildcard": "Добавяне на * при бързо търсене",
"authIpAddressCheck": "Ограничаване на достъпа по IP адрес",
"authIpAddressWhitelist": "Списък с позволени IP адреси",
"authIpAddressCheckExcludedUsers": "Потребители с изключение от проверката"
},
"tooltips": {
"recordsPerPage": "Брой записи, които да се показват в лист UI",
@@ -154,7 +158,6 @@
"textFilterUseContainsForVarchar": "Ако не е отметнато, тогава се използва операторът 'Започва с'. Можете да използвате заместващия знак „%“.",
"streamEmailNotificationsEntityList": "Уведомления по имейл за актуализации на активности на следвани записи. Потребителите ще получават известия по имейл само за определени типове обекти.",
"authTokenPreventConcurrent": "Потребителите няма да могат да влизат на няколко устройства едновременно.",
"emailAddressIsOptedOutByDefault": "При създаване на нов запис имейл адресът ще бъде маркиран като отписан.",
"cleanupDeletedRecords": "Изтритите записи ще бъдат премахнати от базата данни след известно време.",
"ldapPortalUserLdapAuth": "Позволете на потребителите на портала да използват LDAP удостоверяване вместо нормално удостоверяване.",
"ldapPortalUserPortals": "Портали по подразбиране за създаден потребител на портал",
@@ -200,7 +203,12 @@
"oidcLogoutUrl": "URL адрес, към който браузърът ще пренасочи след излизане от Espo. Предназначен за изчистване на информацията за сесията в браузъра и извършване на излизане от страна на доставчика. Обикновено URL адресът съдържа URL параметър за пренасочване, за връщане обратно към Espo.\n\nНалични полета:\n* `{siteUrl}`\n* `{clientId}`",
"recordsPerPageKanban": "Брой записи, първоначално показани в канбан колоните.",
"jobForceUtc": "Използвайте часовата зона UTC за планирани задачи. В противен случай ще се използва часовата зона, зададена в настройките.",
"emailAddressSelectEntityTypeList": "Типове обекти, налични при търсене на имейл адрес от модален прозорец."
"emailAddressSelectEntityTypeList": "Типове обекти, налични при търсене на имейл адрес от модален прозорец.",
"authIpAddressCheckExcludedUsers": "Потребители, които ще могат да влизат независимо дали техният IP адрес е в позволения списък.",
"authIpAddressWhitelist": "Списък с IP адреси или диапазони в CIDR нотация.\n\nПорталите не са засегнати от ограничението.",
"emailAddressIsOptedOutByDefault": "При създаване на нов запис имейл адресът ще бъде маркиран като изключен от кореспонденция.",
"oidcGroupClaim": "Заявление за използване за картографиране на екипи.",
"quickSearchFullTextAppendWildcard": "Добавяне на * знак към заявка за търсене с автоматично довършване, когато е активирано пълнотекстово търсене. Намалява ефективността на търсенето."
},
"labels": {
"System": "Системни настройки",
@@ -226,7 +234,10 @@
"General": "Основни",
"Navbar": "Меню",
"Dashboard": "Дашборд",
"Phone Numbers": "Телефонни номера"
"Phone Numbers": "Телефонни номера",
"Access": "Достъп",
"Strength": "Сила",
"Recovery": "Възстановяване"
},
"messages": {
"ldapTestConnection": "Връзката беше успешна."
@@ -4,7 +4,8 @@
"roles": "Роли и права",
"positionList": "Списък с позиции",
"layoutSet": "Оформление",
"workingTimeCalendar": "Календар на работно време"
"workingTimeCalendar": "Календар на работно време",
"userRole": "Потребителска роля"
},
"links": {
"users": "Потребители",
@@ -40,7 +40,8 @@
"auth2FAMethod": "2FA Метод",
"auth2FATotpSecret": "2FA TOTP секретен ключ",
"workingTimeCalendar": "Календар на работно време",
"layoutSet": "Сет от оформления"
"layoutSet": "Сет от оформления",
"avatarColor": "Цвят на аватара"
},
"links": {
"teams": "Отдели",
@@ -56,8 +57,8 @@
"dashboardTemplate": "Шаблон за работен плот",
"userData": "Потребителски данни",
"workingTimeCalendar": "Календар на работно време",
"workingTimeRanges": "Диапазони на работното време",
"layoutSet": "Сет от оформления"
"layoutSet": "Сет от оформления",
"workingTimeRanges": "Изключения от работното време"
},
"labels": {
"Create User": "Създаване на потребител",
@@ -132,7 +133,8 @@
"loginAs": "Отворете връзката за влизане в инкогнито прозорец, за да запазите текущата си сесия. Използвайте администраторските си идентификационни данни, за да влезете.",
"failedToLogIn": "Неуспешно влизане",
"2faMethodNotConfigured": "Методът за двуфакторно оторизиране не е напълно конфигуриран в системата.",
"loginError": "Възникна неочаквана грешка"
"loginError": "Възникна неочаквана грешка",
"defaultTeamIsNotUsers": "Екипът по подразбиране трябва да бъде един от избраните екипи на потребителя"
},
"boolFilters": {
"onlyMyTeam": "Само моя отдел"
@@ -1,7 +1,6 @@
{
"labels": {
"Create WorkingTimeCalendar": "Създаване на календар",
"Ranges": "Диапазони"
"Create WorkingTimeCalendar": "Създаване на календар"
},
"fields": {
"timeZone": "Времева зона",
@@ -22,6 +21,6 @@
"weekday6TimeRanges": "График за Събота"
},
"links": {
"ranges": "Диапазони"
"ranges": "Изключения"
}
}
@@ -1,7 +1,7 @@
{
"labels": {
"Create WorkingTimeRange": "Създаване на диапазон",
"Calendars": "Календари"
"Calendars": "Календари",
"Create WorkingTimeRange": "Създаване на изключение"
},
"fields": {
"timeRanges": "График",
@@ -27,6 +27,7 @@
"options": {
"denialReason": {
"CREDENTIALS": "Invalid credentials",
"WRONG_CODE": "Wrong code",
"INACTIVE_USER": "Inactive user",
"IS_PORTAL_USER": "Portal user",
"IS_NOT_PORTAL_USER": "Not a portal user",
@@ -117,7 +117,9 @@
"No Subject": "No Subject",
"View Users": "View Users",
"Event": "Event",
"View Attachments": "View Attachments"
"View Attachments": "View Attachments",
"Moved to Trash": "Moved to Trash",
"Retrieved from Trash": "Retrieved from Trash"
},
"strings": {
"sendingFailed": "Email sending failed"
@@ -81,8 +81,10 @@
"view": "views/email/fields/from-address-varchar",
"textFilterDisabled": true,
"customizationDisabled": true,
"layoutDefaultSidePanelDisabled": true,
"layoutMassUpdateDisabled": true
"layoutAvailabilityList": [
"detail",
"filters"
]
},
"to": {
"type": "varchar",
@@ -97,8 +99,10 @@
],
"textFilterDisabled": true,
"customizationDisabled": true,
"layoutDefaultSidePanelDisabled": true,
"layoutMassUpdateDisabled": true
"layoutAvailabilityList": [
"detail",
"filters"
]
},
"cc": {
"type": "varchar",
@@ -111,8 +115,9 @@
],
"customizationDisabled": true,
"textFilterDisabled": true,
"layoutDefaultSidePanelDisabled": true,
"layoutMassUpdateDisabled": true
"layoutAvailabilityList": [
"detail"
]
},
"bcc": {
"type": "varchar",
@@ -125,8 +130,9 @@
],
"customizationDisabled": true,
"textFilterDisabled": true,
"layoutDefaultSidePanelDisabled": true,
"layoutMassUpdateDisabled": true
"layoutAvailabilityList": [
"detail"
]
},
"replyTo": {
"type": "varchar",
@@ -135,8 +141,9 @@
"view": "views/email/fields/email-address-varchar",
"textFilterDisabled": true,
"customizationDisabled": true,
"layoutDefaultSidePanelDisabled": true,
"layoutMassUpdateDisabled": true
"layoutAvailabilityList": [
"detail"
]
},
"personStringData": {
"type": "varchar",
@@ -0,0 +1,7 @@
{{#if formattedAddress}}
<a
href="#{{scope}}/view/{{model.id}}"
class="link"
data-id="{{model.id}}"
>{{breaklines formattedAddress}}</a>
{{/if}}
+17 -2
View File
@@ -69,6 +69,10 @@ function init(langSets) {
const options = context.options;
const view = /** @type {import('view').default} */options.espoView;
if (!view) {
return;
}
context.memo('button.cellParams', () => {
return ui.button({
className: '',
@@ -144,6 +148,12 @@ function init(langSets) {
const options = context.options;
const view = /** @type {import('view').default} */options.espoView;
if (!view) {
// Prevents an issue with a collapsed modal.
// @todo Revise.
return;
}
context.memo('button.tableParams', () => {
return ui.button({
className: '',
@@ -431,6 +441,10 @@ function init(langSets) {
$codable.removeClass('hidden');
if ($editor.hasClass('fullscreen')) {
$codable.css('height', $editable.css('height'));
}
requireAce().then(() => {
const html = prepareHtml($editable.html());
@@ -439,7 +453,7 @@ function init(langSets) {
aceEditor.setValue(html);
aceEditor.setOptions({
maxLines: !$editor.hasClass('fullscreen') ? 34: null,
maxLines: !$editor.hasClass('fullscreen') ? 100000: null,
enableLiveAutocompletion: true,
tabSize: 2,
useSoftTabs: true,
@@ -925,6 +939,7 @@ function init(langSets) {
}
this.$editable.css('maxHeight', this.$editable.css('orgMaxHeight'));
this.$editable.css('height', '');
if (this.isInModal) {
this.$modal.css('overflow-y', '');
@@ -936,7 +951,7 @@ function init(langSets) {
this._isFullscreen = false;
if (aceEditor) {
aceEditor.setOptions({maxLines: maxLines});
aceEditor.setOptions({maxLines: 100000});
aceEditor.resize();
}
}
+2 -1
View File
@@ -51,6 +51,7 @@ class AddressFieldView extends BaseFieldView {
// noinspection JSUnusedGlobalSymbols
editTemplate4 = 'fields/address/edit-4'
searchTemplate = 'fields/address/search'
listLinkTemplate = 'fields/address/list-link'
postalCodeField
streetField
@@ -88,7 +89,7 @@ class AddressFieldView extends BaseFieldView {
data[item + 'Value'] = this.model.get(this[item + 'Field']);
});
if (this.mode === this.MODE_DETAIL || this.mode === this.MODE_LIST) {
if (this.isReadMode()) {
data.formattedAddress = this.getFormattedAddress();
data.isNone = data.formattedAddress === null;
+2
View File
@@ -1253,6 +1253,7 @@ class BaseFieldView extends View {
if (isInvalid) {
Espo.Ui.error(this.translate('Not valid'));
// @todo Revise.
model.set(prev, {silent: true});
return;
@@ -1273,6 +1274,7 @@ class BaseFieldView extends View {
.catch(() => {
Espo.Ui.error(this.translate('Error occurred'));
// @todo Revise.
model.set(prev, {silent: true});
this.reRender();
+1 -1
View File
@@ -829,7 +829,7 @@ class WysiwygFieldView extends TextFieldView {
const offset = $buttonContainer.offset();
if (offset) {
edgeTop = offset.top + $buttonContainer.height();
edgeTop = offset.top + $buttonContainer.outerHeight();
edgeTopAbsolute = edgeTop - $(window).scrollTop();
}
}
+4 -1
View File
@@ -2085,7 +2085,10 @@ class DetailRecordView extends BaseRecordView {
return;
}
view.inlineEdit();
const initialAttributes = {...view.initialAttributes};
view.inlineEdit()
.then(() => view.initialAttributes = initialAttributes);
}
});
}
+2 -2
View File
@@ -1088,13 +1088,13 @@ class KanbanRecordView extends ListRecordView {
this.listenToOnce(view, 'after:save', () => {
if (this.orderDisabled) {
this.collection.fetch();
this.collection.fetch({maxSize: this.collection.maxSize});
return;
}
this.storeGroupOrder(group, view.model.id)
.then(() => this.collection.fetch());
.then(() => this.collection.fetch({maxSize: this.collection.maxSize}));
});
});
}
+25 -1
View File
@@ -155,10 +155,34 @@ img.image-preview {
}
@media screen and (max-width: (@screen-sm-min - 1px)) {
.header-title,
.header-title,
> h3 {
font-size: 21px;
}
.header-buttons {
> .btn {
max-width: 280px;
overflow: hidden;
text-overflow: ellipsis;
}
&:has(:nth-child(3)) {
> .btn {
max-width: 240px;
}
}
&:has(:nth-child(4)) {
> .btn {
max-width: 170px;
}
}
.dropdown-group:last-child {
float: right;
}
}
}
.page-header-row {
+4
View File
@@ -270,6 +270,10 @@ body[data-navbar="side"] {
> a {
width: 34px;
&:focus {
background-color: transparent;
}
}
}
+14
View File
@@ -194,6 +194,20 @@ body:not([data-navbar="side"]) {
.global-search-button {
margin-top: -1px;
}
.navbar-right {
a.global-search-button,
> li > a {
color: var(--navbar-inverse-link-icon-color);
&:hover {
color: var(--navbar-inverse-link-icon-hover-color);
}
}
> li.dropdown .open > a {
color: var(--navbar-inverse-link-icon-hover-color);
}
}
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "espocrm",
"version": "8.3.0",
"version": "8.3.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "espocrm",
"version": "8.3.0",
"version": "8.3.2",
"hasInstallScript": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "espocrm",
"version": "8.3.0",
"version": "8.3.2",
"description": "Open-source CRM.",
"repository": {
"type": "git",
@@ -31,6 +31,7 @@ namespace tests\integration\Espo\Record;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Field\Date;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Record\CreateParams;
use Espo\Core\Record\ServiceContainer;
use Espo\Core\Record\UpdateParams;
@@ -43,6 +44,7 @@ use Espo\Modules\Crm\Entities\Lead;
use Espo\Modules\Crm\Entities\Opportunity;
use Espo\Modules\Crm\Entities\Task;
use Espo\ORM\EntityManager;
use Espo\ORM\Type\RelationType;
use tests\integration\Core\BaseTestCase;
class LinkTest extends BaseTestCase
@@ -439,4 +441,63 @@ class LinkTest extends BaseTestCase
$case->get('contactsNames')
);
}
public function testOneToOne(): void
{
$metadata = $this->getContainer()->getByClass(Metadata::class);
$metadata->set('entityDefs', CaseObj::ENTITY_TYPE, [
'links' => [
'task' => [
'foreign' => 'case',
'type' => RelationType::BELONGS_TO,
'entity' => Task::ENTITY_TYPE,
],
]
]);
$metadata->set('entityDefs', Task::ENTITY_TYPE, [
'fields' => [
'case' => [
'type' => FieldType::LINK_ONE,
],
],
'links' => [
'case' => [
'foreign' => 'task',
'type' => RelationType::HAS_ONE,
'entity' => CaseObj::ENTITY_TYPE,
],
]
]);
$metadata->save();
/** @noinspection PhpUnhandledExceptionInspection */
$this->getDataManager()->rebuild();
$this->reCreateApplication();
$em = $this->getEntityManager();
$task = $em->getRDBRepositoryByClass(Task::class)->getNew();
$task->setMultiple(['name' => 'Task']);
$em->saveEntity($task);
$case = $em->getRDBRepositoryByClass(CaseObj::class)->getNew();
$case->setMultiple(['name' => 'Case']);
$em->saveEntity($case);
$service = $this->getContainer()->getByClass(ServiceContainer::class)->getByClass(Task::class);
/** @noinspection PhpUnhandledExceptionInspection */
$service->update($task->getId(), (object) [
'caseId' => $case->getId(),
], UpdateParams::create());
$this->assertTrue(
$em->getRDBRepositoryByClass(Task::class)
->getRelation($task, 'case')
->isRelatedById($case->getId())
);
}
}