mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-05 10:37:01 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94881de082 | ||
|
|
cbec1cbbe5 | ||
|
|
71dd872618 | ||
|
|
bfed154feb | ||
|
|
5a19b90b13 | ||
|
|
33b0ca8824 | ||
|
|
540c58a564 | ||
|
|
67c0be5699 | ||
|
|
695dc2eb42 | ||
|
|
d1dd39bf5a | ||
|
|
8a542adb7e | ||
|
|
c6a172a6d0 | ||
|
|
89f75bfb37 | ||
|
|
b83a66cf49 | ||
|
|
3cd6deb732 | ||
|
|
59fff4cc80 | ||
|
|
dca1d34685 | ||
|
|
8133682288 | ||
|
|
0b035b89c0 | ||
|
|
d5930542d9 | ||
|
|
831dfc5bfd | ||
|
|
078808604c | ||
|
|
35f8c5652a | ||
|
|
14d8f88985 | ||
|
|
2ae0a118b6 | ||
|
|
1a51759b0f | ||
|
|
48b0063c69 | ||
|
|
9395fa886e | ||
|
|
a6c95ff0d5 | ||
|
|
1c10ddd7b8 |
@@ -165,7 +165,7 @@ class GoogleMaps implements Helper
|
||||
|
||||
$url = "https://maps.googleapis.com/maps/api/staticmap?" .
|
||||
'center=' . $addressEncoded .
|
||||
'format=' . $format .
|
||||
'&format=' . $format .
|
||||
'&size=' . $size .
|
||||
'&key=' . $apiKey;
|
||||
|
||||
|
||||
@@ -51,6 +51,10 @@ class DefaultUserProvider implements UserProvider
|
||||
{
|
||||
$user = $this->findUser($payload);
|
||||
|
||||
if ($user === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
$this->syncUser($user, $payload);
|
||||
|
||||
@@ -60,7 +64,10 @@ class DefaultUserProvider implements UserProvider
|
||||
return $this->tryToCreateUser($payload);
|
||||
}
|
||||
|
||||
private function findUser(Payload $payload): ?User
|
||||
/**
|
||||
* @return User|false|null
|
||||
*/
|
||||
private function findUser(Payload $payload): User|bool|null
|
||||
{
|
||||
$usernameClaim = $this->configDataProvider->getUsernameClaim();
|
||||
|
||||
@@ -82,24 +89,26 @@ class DefaultUserProvider implements UserProvider
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$user->isActive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$userId = $user->getId();
|
||||
|
||||
if (!$user->isActive()) {
|
||||
$this->log->info("Oidc: User $userId found but it's not active.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$isPortal = $this->applicationState->isPortal();
|
||||
|
||||
if (!$isPortal && !$user->isRegular() && !$user->isAdmin()) {
|
||||
$this->log->info("Oidc: User $userId found but it's neither regular user not admin.");
|
||||
$this->log->info("Oidc: User $userId found but it's neither regular user nor admin.");
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($isPortal && !$user->isPortal()) {
|
||||
$this->log->info("Oidc: User $userId found but it's not portal user.");
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($isPortal) {
|
||||
@@ -108,20 +117,20 @@ class DefaultUserProvider implements UserProvider
|
||||
if (!$user->getPortals()->hasId($portalId)) {
|
||||
$this->log->info("Oidc: User $userId found but it's not related to current portal.");
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($user->isSuperAdmin()) {
|
||||
$this->log->info("Oidc: User $userId found but it's super-admin, not allowed.");
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($user->isAdmin() && !$this->configDataProvider->allowAdminUser()) {
|
||||
$this->log->info("Oidc: User $userId found but it's admin, not allowed.");
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user;
|
||||
|
||||
@@ -861,7 +861,7 @@ class Parser
|
||||
$offset = -1;
|
||||
|
||||
while (true) {
|
||||
$index = strrpos($expression, $operator, $offset);
|
||||
$index = strrpos($modifiedExpression, $operator, $offset);
|
||||
|
||||
if ($index === false) {
|
||||
break;
|
||||
@@ -882,7 +882,7 @@ class Parser
|
||||
|
||||
if (strlen($operator) === 1) {
|
||||
if ($index < strlen($expression) - 1) {
|
||||
$possibleRightOperator = trim($operator . $expression[$index + 1]);
|
||||
$possibleRightOperator = trim($operator . $modifiedExpression[$index + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -898,7 +898,7 @@ class Parser
|
||||
|
||||
if (strlen($operator) === 1) {
|
||||
if ($index > 0) {
|
||||
$possibleLeftOperator = trim($expression[$index - 1] . $operator);
|
||||
$possibleLeftOperator = trim($modifiedExpression[$index - 1] . $operator);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -830,9 +830,16 @@ class Service implements Crud,
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
$this->beforeCreateEntity($entity, $data);
|
||||
|
||||
$this->entityManager->saveEntity($entity, [SaveOption::API => true]);
|
||||
$this->entityManager->saveEntity($entity, [
|
||||
SaveOption::API => true,
|
||||
SaveOption::KEEP_NEW => true,
|
||||
]);
|
||||
|
||||
$this->getRecordHookManager()->processAfterCreate($entity, $params);
|
||||
|
||||
$entity->setAsNotNew();
|
||||
$entity->updateFetchedValues();
|
||||
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
$this->afterCreateEntity($entity, $data);
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
|
||||
@@ -106,7 +106,10 @@ class EmailReminder
|
||||
return;
|
||||
}
|
||||
|
||||
if ($entity->hasLinkMultipleField('users')) {
|
||||
if (
|
||||
$entity->hasLinkMultipleField('users') &&
|
||||
$entity->hasAttribute('usersColumns')
|
||||
) {
|
||||
$entity->loadLinkMultipleField('users', ['status' => 'acceptanceStatus']);
|
||||
$status = $entity->getLinkMultipleColumn('users', 'status', $user->getId());
|
||||
|
||||
|
||||
@@ -106,7 +106,8 @@ class SubmitPopupReminders implements JobDataLess
|
||||
|
||||
if (
|
||||
$entity instanceof CoreEntity &&
|
||||
$entity->hasLinkMultipleField('users')
|
||||
$entity->hasLinkMultipleField('users') &&
|
||||
$entity->hasAttribute('usersColumns')
|
||||
) {
|
||||
$entity->loadLinkMultipleField('users', ['status' => 'acceptanceStatus']);
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"description": "Описание",
|
||||
"lead": "Потенциална продажба",
|
||||
"attachments": "Прикачени файлове",
|
||||
"inboundEmail": "Групов имейл акаунт"
|
||||
"inboundEmail": "Групов имейл акаунт",
|
||||
"originalEmail": "Оригинален имейл"
|
||||
},
|
||||
"links": {
|
||||
"account": "Клиент",
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"acceptanceStatusMeetings": "Статус на приемане (Срещи)",
|
||||
"acceptanceStatusCalls": "Статус на приемане (Разговори)",
|
||||
"title": "Роля при клиент",
|
||||
"hasPortalUser": "Притежава акаунт в портала"
|
||||
"hasPortalUser": "Притежава акаунт в портала",
|
||||
"originalEmail": "Оригинален имейл"
|
||||
},
|
||||
"links": {
|
||||
"opportunities": "Сделки",
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"opportunityAmountCurrency": "Валута на сумата в сделката",
|
||||
"acceptanceStatusMeetings": "Статус на приемане (Срещи)",
|
||||
"acceptanceStatusCalls": "Статус на приемане (Разговори)",
|
||||
"convertedAt": "Конвертирана на"
|
||||
"convertedAt": "Конвертирана на",
|
||||
"originalEmail": "Оригинален имейл"
|
||||
},
|
||||
"links": {
|
||||
"targetLists": "Целеви списъци",
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"excludingActionList": "С изключение на",
|
||||
"optedOutCount": "Отписали се",
|
||||
"targetStatus": "Целеви статус",
|
||||
"isOptedOut": "Отписал се"
|
||||
"isOptedOut": "Отписал се",
|
||||
"sourceCampaign": "Източник на кампанията"
|
||||
},
|
||||
"links": {
|
||||
"accounts": "Клиенти",
|
||||
|
||||
@@ -113,7 +113,8 @@ class PopupNotificationsProvider implements Provider
|
||||
|
||||
if (
|
||||
$entity instanceof CoreEntity &&
|
||||
$entity->hasLinkMultipleField('users')
|
||||
$entity->hasLinkMultipleField('users') &&
|
||||
$entity->hasAttribute('usersColumns')
|
||||
) {
|
||||
$entity->loadLinkMultipleField('users', ['status' => 'acceptanceStatus']);
|
||||
|
||||
|
||||
@@ -82,7 +82,8 @@
|
||||
"Formula Sandbox": "Тестър за формули",
|
||||
"Working Time Calendars": "Календари на работното време",
|
||||
"Group Email Folders": "Групови имейл папки",
|
||||
"Authentication Providers": "Доставчици на удостоверяване"
|
||||
"Authentication Providers": "Доставчици на удостоверяване",
|
||||
"Setup": "Инсталация"
|
||||
},
|
||||
"layouts": {
|
||||
"list": "Лист",
|
||||
@@ -201,7 +202,9 @@
|
||||
"readOnlyAfterCreate": "Read-only след създаване",
|
||||
"createButton": "Бутон за създаване",
|
||||
"autocompleteOnEmpty": "Автоматично довършване при натискане",
|
||||
"relateOnImport": "Релации при импорт"
|
||||
"relateOnImport": "Релации при импорт",
|
||||
"aclScope": "ACL обхват",
|
||||
"onlyAdmin": "Само за администратори"
|
||||
},
|
||||
"messages": {
|
||||
"selectEntityType": "Изберете вида обект от лявото меню.",
|
||||
|
||||
@@ -44,7 +44,9 @@
|
||||
"layout": "Оформление",
|
||||
"author": "Автор",
|
||||
"module": "Модул",
|
||||
"version": "Версия"
|
||||
"version": "Версия",
|
||||
"selectFilter": "Избери филтър",
|
||||
"primaryFilters": "Основни филтри"
|
||||
},
|
||||
"options": {
|
||||
"type": {
|
||||
@@ -59,6 +61,9 @@
|
||||
"linkType": {
|
||||
"oneToOneRight": "Едно към едно с десния",
|
||||
"oneToOneLeft": "Едно към едно Left"
|
||||
},
|
||||
"module": {
|
||||
"Custom": "Персонализиран"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
@@ -71,7 +76,8 @@
|
||||
"nameIsAlreadyUsed": "Името '{name}' вече е използвано.",
|
||||
"nameIsNotAllowed": "Името '{name}' не е позволено.",
|
||||
"nameIsTooLong": "Името е прекалено дълго.",
|
||||
"confirmRemoveLink": "Сигурни ли сте, че искате да премахнете релацията *{link}*?"
|
||||
"confirmRemoveLink": "Сигурни ли сте, че искате да премахнете релацията *{link}*?",
|
||||
"urlHashCopiedToClipboard": "URL за филтъра *{name}* се копира в клипборда. Можете да го добавите към навигационната лента."
|
||||
},
|
||||
"tooltips": {
|
||||
"statusField": "Актуализациите на това поле се регистрират в активността.",
|
||||
@@ -85,6 +91,7 @@
|
||||
"countDisabled": "Общият брой няма да се показва в списъчния изглед. Може да намали времето за зареждане, когато DB таблицата е голяма.",
|
||||
"optimisticConcurrencyControl": "Предотвратява възможност от презаписване на един и същи запис от двама или повече потребители едновременно.",
|
||||
"duplicateCheckFieldList": "Кои полета да се проверяват, когато се извършва проверка за дублирани записи.",
|
||||
"updateDuplicateCheck": "Извършване на проверка за дублирани записи при актуализиране на запис."
|
||||
"updateDuplicateCheck": "Извършване на проверка за дублирани записи при актуализиране на запис.",
|
||||
"linkSelectFilter": "Основен филтър за прилагане по подразбиране при изберане на запис."
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,8 @@
|
||||
"WorkingTimeRange": "Диапазон на работното време",
|
||||
"GroupEmailFolder": "Групова имейл папка",
|
||||
"AuthenticationProvider": "Доставчик на удостоверяване",
|
||||
"GlobalStream": "Глобална активност"
|
||||
"GlobalStream": "Глобална активност",
|
||||
"WebhookQueueItem": "Обект в опашка на Webhook"
|
||||
},
|
||||
"scopeNamesPlural": {
|
||||
"Email": "Имейли",
|
||||
@@ -108,7 +109,8 @@
|
||||
"WorkingTimeRange": "Диапазони на работно време",
|
||||
"GroupEmailFolder": "Групови имейл папки",
|
||||
"AuthenticationProvider": "Доставчици на удостоверяване",
|
||||
"GlobalStream": "Глобална активност"
|
||||
"GlobalStream": "Глобална активност",
|
||||
"WebhookQueueItem": "Обекти в опашка на Webhook"
|
||||
},
|
||||
"labels": {
|
||||
"Misc": "Разни",
|
||||
@@ -281,7 +283,14 @@
|
||||
"Hide Navigation Panel": "Скриване на страничния панел",
|
||||
"Print": "Принтиране",
|
||||
"Copy to Clipboard": "Бутон за копиране",
|
||||
"Copied to clipboard": "Копирано в клипборда"
|
||||
"Copied to clipboard": "Копирано в клипборда",
|
||||
"Audit Log": "Одитна история",
|
||||
"View Audit Log": "Преглед на одитна история",
|
||||
"Previous Page": "Предишна страница",
|
||||
"Next Page": "Следваща страница",
|
||||
"First Page": "Първа страница",
|
||||
"Last Page": "Последна страница",
|
||||
"Page": "Страница"
|
||||
},
|
||||
"messages": {
|
||||
"pleaseWait": "Моля изчакайте...",
|
||||
@@ -382,7 +391,9 @@
|
||||
"fieldPhoneTooLong": "{field} е твърде дълго",
|
||||
"barcodeInvalid": "{field} не е валидно {type}",
|
||||
"noLinkAccess": "Не може да свържете със запис {foreignEntityType} чрез връзката „{link}“. Нямате достъп.",
|
||||
"attemptIntervalFailure": "Операцията не е разрешена през определен интервал от време. Изчакайте известно време преди следващия опит."
|
||||
"attemptIntervalFailure": "Операцията не е разрешена през определен интервал от време. Изчакайте известно време преди следващия опит.",
|
||||
"confirmRestoreFromAudit": "Предишните стойности ще бъдат зададени във формуляр. След това можете да запишете записа, за да възстановите предишните стойности.",
|
||||
"pageNumberIsOutOfBound": "Номер на страницата е невалиден"
|
||||
},
|
||||
"boolFilters": {
|
||||
"onlyMy": "Само мои",
|
||||
@@ -443,7 +454,9 @@
|
||||
"types": "Типове",
|
||||
"middleName": "Презиме",
|
||||
"emailAddressIsInvalid": "Имейл адреса е невалиден",
|
||||
"phoneNumberIsInvalid": "Телефонния номер е невалиден"
|
||||
"phoneNumberIsInvalid": "Телефонния номер е невалиден",
|
||||
"users": "Потребители",
|
||||
"childList": "Дъщерен списък"
|
||||
},
|
||||
"links": {
|
||||
"assignedUser": "Назначен потребител",
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
"noLabel": "Няма заглавие",
|
||||
"tabLabel": "Заглавие на таба",
|
||||
"tabBreak": "Разделител на табове",
|
||||
"width": "Ширина"
|
||||
"width": "Ширина",
|
||||
"noteText": "Текст за съобщението",
|
||||
"noteStyle": "Стил на съобщението"
|
||||
},
|
||||
"options": {
|
||||
"align": {
|
||||
@@ -43,7 +45,8 @@
|
||||
"tabBreak": "Отделен раздел за панела и всички следващи панели до следващия разделител на таба.",
|
||||
"noLabel": "Не показвай заглавието на колоната в хедъра.",
|
||||
"notSortable": "Деактивира възможността за сортиране по колона.",
|
||||
"width": "Ширина на колона. Препоръчително е да имате една колона без указана ширина, обикновено това трябва да е полето *Име*."
|
||||
"width": "Ширина на колона. Препоръчително е да имате една колона без указана ширина, обикновено това трябва да е полето *Име*.",
|
||||
"noteText": "Текст, който да се показва в панела. Поддържа се Markdown."
|
||||
},
|
||||
"messages": {
|
||||
"cantBeEmpty": "Оформлението не може да бъде празно.",
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
"assignmentNotificationsIgnoreEntityTypeList": "Известия за назначения на записи (в приложението)",
|
||||
"assignmentEmailNotificationsIgnoreEntityTypeList": "Известия за назначения на записи (по имейл)",
|
||||
"dashboardLocked": "Заключи дашборда",
|
||||
"textSearchStoringDisabled": "Деактивиране на съхраняването на текстови филтри"
|
||||
"textSearchStoringDisabled": "Деактивиране на съхраняването на текстови филтри",
|
||||
"calendarSlotDuration": "Продължителност на календарния слот"
|
||||
},
|
||||
"options": {
|
||||
"weekStart": {
|
||||
@@ -51,5 +52,10 @@
|
||||
"doNotFillAssignedUserIfNotRequired": "При създаване на запис назначеният потребител няма да бъде попълнен със собствен потребител, освен ако полето не е задължително.",
|
||||
"followCreatedEntities": "Когато създавате нови записи, те ще бъдат следвани автоматично, дори ако са назначени на друг потребител.",
|
||||
"followCreatedEntityTypeList": "Когато създавате нови записи за избрани типове обекти, те ще бъдат следвани автоматично, дори ако са назначени на друг потребител."
|
||||
},
|
||||
"tabFields": {
|
||||
"label": "Текст",
|
||||
"iconClass": "Иконка",
|
||||
"color": "Цвят"
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,8 @@
|
||||
"followerManagementPermission": "Права за управление на последователи",
|
||||
"data": "Данни",
|
||||
"fieldData": "Данни на полето",
|
||||
"messagePermission": "Права за писане на съобщения"
|
||||
"messagePermission": "Права за писане на съобщения",
|
||||
"auditPermission": "Права за одитна история"
|
||||
},
|
||||
"links": {
|
||||
"users": "Потребители",
|
||||
@@ -60,6 +61,7 @@
|
||||
"portalPermission": "Достъп до информация в портала, възможност за публикуване на съобщения до потребителите на портала.",
|
||||
"groupEmailAccountPermission": "Достъп до групови имейл акаунти, възможност за изпращане на имейли от групов SMTP акаунт.",
|
||||
"exportPermission": "Позволява експортването на записи.",
|
||||
"massUpdatePermission": "Възможност за извършване на масово актуализиране на записи."
|
||||
"massUpdatePermission": "Възможност за извършване на масово актуализиране на записи.",
|
||||
"auditPermission": "Позволява потребителя да достъпва одитната история."
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,8 @@
|
||||
"phoneNumberNumericSearch": "Числово търсене на телефонен номер",
|
||||
"phoneNumberInternational": "Международни телефонни номера",
|
||||
"phoneNumberPreferredCountryList": "Предпочитани телефонни кодове на държави",
|
||||
"jobForceUtc": "Принудително избиране на UTC часовата зона"
|
||||
"jobForceUtc": "Принудително избиране на UTC часовата зона",
|
||||
"emailAddressSelectEntityTypeList": "Обхвати за избор на имейл адрес"
|
||||
},
|
||||
"tooltips": {
|
||||
"recordsPerPage": "Брой записи, които да се показват в лист UI",
|
||||
@@ -198,7 +199,8 @@
|
||||
"oidcTeams": "Екипите на Espo са съпоставени с групи/екипи/роли на доставчика на идентичност (LDAP/OIDC). Екипи с празна стойност за съпоставяне винаги ще бъдат присвоени на потребител (при създаване или синхронизиране).",
|
||||
"oidcLogoutUrl": "URL адрес, към който браузърът ще пренасочи след излизане от Espo. Предназначен за изчистване на информацията за сесията в браузъра и извършване на излизане от страна на доставчика. Обикновено URL адресът съдържа URL параметър за пренасочване, за връщане обратно към Espo.\n\nНалични полета:\n* `{siteUrl}`\n* `{clientId}`",
|
||||
"recordsPerPageKanban": "Брой записи, първоначално показани в канбан колоните.",
|
||||
"jobForceUtc": "Използвайте часовата зона UTC за планирани задачи. В противен случай ще се използва часовата зона, зададена в настройките."
|
||||
"jobForceUtc": "Използвайте часовата зона UTC за планирани задачи. В противен случай ще се използва часовата зона, зададена в настройките.",
|
||||
"emailAddressSelectEntityTypeList": "Типове обекти, налични при търсене на имейл адрес от модален прозорец."
|
||||
},
|
||||
"labels": {
|
||||
"System": "Системни настройки",
|
||||
|
||||
@@ -1 +1,18 @@
|
||||
{}
|
||||
{
|
||||
"fields": {
|
||||
"event": "Събитие",
|
||||
"target": "Цел",
|
||||
"data": "Данни",
|
||||
"status": "Статус",
|
||||
"processedAt": "Обработено на",
|
||||
"attempts": "Опити",
|
||||
"processAt": "Обработка на"
|
||||
},
|
||||
"options": {
|
||||
"status": {
|
||||
"Pending": "Текущо",
|
||||
"Success": "Успешно",
|
||||
"Failed": "Неуспешно"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,6 +331,7 @@
|
||||
"fieldManagerParamList": [
|
||||
"required",
|
||||
"entityList",
|
||||
"autocompleteOnEmpty",
|
||||
"audited",
|
||||
"tooltipText"
|
||||
]
|
||||
|
||||
@@ -126,7 +126,9 @@ class LinkManager
|
||||
$params['relationName'] :
|
||||
lcfirst($entity) . $entityForeign;
|
||||
|
||||
$relationName = $this->nameUtil->addCustomPrefix($relationName);
|
||||
if ($relationName[0] !== 'c' || !preg_match('/[A-Z]/', $relationName[1])) {
|
||||
$relationName = $this->nameUtil->addCustomPrefix($relationName);
|
||||
}
|
||||
|
||||
if ($this->isNameTooLong($relationName)) {
|
||||
throw new Error("Relation name is too long.");
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
#layout ul.panels > li {
|
||||
padding: 5px 10px;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 14px;
|
||||
min-height: 80px;
|
||||
border: 1px solid var(--select-item-border);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
@@ -1197,7 +1197,8 @@ class CalendarView extends View {
|
||||
|
||||
const event = this.convertToFcEvent(attributes);
|
||||
|
||||
this.calendar.addEvent(event);
|
||||
// true passed to prevent duplicates after re-fetch.
|
||||
this.calendar.addEvent(event, true);
|
||||
}
|
||||
|
||||
updateModel(model) {
|
||||
@@ -1258,6 +1259,12 @@ class CalendarView extends View {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === 'className') {
|
||||
event.setProp('classNames', value);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.extendedProps.includes(key)) {
|
||||
event.setExtendedProp(key, value);
|
||||
|
||||
|
||||
@@ -45,7 +45,9 @@
|
||||
{{#if hasCustomLabel}}
|
||||
data-custom-label="{{customLabel}}"
|
||||
{{/if}}
|
||||
data-no-label="{{noLabel}}" >
|
||||
data-no-label="{{noLabel}}"
|
||||
title="{{label}}"
|
||||
>
|
||||
<div class="left" style="width: calc(100% - 14px);">{{label}}</div>
|
||||
<div class="right" style="width: 14px;">
|
||||
<a
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<header>{{translate 'Available Fields' scope='Admin'}}</header>
|
||||
<ul class="disabled cells clearfix">
|
||||
{{#each disabledFields}}
|
||||
<li class="cell" data-name="{{./this}}">
|
||||
<li class="cell" data-name="{{./this}}" title="{{translate this scope=../scope category='fields'}}">
|
||||
<div class="left" style="width: calc(100% - 14px);">
|
||||
{{translate this scope=../scope category='fields'}}
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
<header>{{translate 'Enabled' scope='Admin'}}</header>
|
||||
<ul class="enabled connected">
|
||||
{{#each layout}}
|
||||
<li class="cell" draggable="true" {{#each ../dataAttributeList}}data-{{toDom this}}="{{prop ../this this}}" {{/each}}>
|
||||
<li
|
||||
class="cell"
|
||||
draggable="true"
|
||||
{{#each ../dataAttributeList}}data-{{toDom this}}="{{prop ../this this}}" {{/each}}
|
||||
title="{{labelText}}"
|
||||
>
|
||||
<div class="left" style="width: calc(100% - 17px);">
|
||||
<span>{{labelText}}</span>
|
||||
</div>
|
||||
@@ -40,6 +45,7 @@
|
||||
class="cell"
|
||||
draggable="true"
|
||||
{{#each ../dataAttributeList}}data-{{toDom this}}="{{prop ../this this}}" {{/each}}
|
||||
title="{{labelText}}"
|
||||
>
|
||||
<div class="left" style="width: calc(100% - 17px);">
|
||||
<span>{{labelText}}</span>
|
||||
|
||||
@@ -32,11 +32,12 @@ class UserAcl extends Acl {
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
checkModelRead(model, data, precise) {
|
||||
if (model.isPortal()) {
|
||||
if (this.get('portalPermission') === 'yes') {
|
||||
// @todo Support getPermissionLevel.
|
||||
/*if (model.isPortal()) {
|
||||
if (this.getPermissionLevel('portalPermission') === 'yes') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
return this.checkModel(model, data, 'read', precise);
|
||||
}
|
||||
|
||||
@@ -204,6 +204,8 @@ class Language {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
load(callback, disableCache, loadDefault) {
|
||||
this.off('sync');
|
||||
|
||||
if (callback) {
|
||||
this.once('sync', callback);
|
||||
}
|
||||
|
||||
@@ -97,7 +97,19 @@ class LinkManagerEditModalView extends ModalView {
|
||||
|
||||
const allEntityList = this.getMetadata().getScopeEntityList()
|
||||
.filter(item => {
|
||||
return this.getMetadata().get(['scopes', item, 'customizable']);
|
||||
const defs = /** @type {Record} */this.getMetadata().get(['scopes', item]) || {};
|
||||
|
||||
if (!defs.customizable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const emDefs = /** @type {Record} */defs.entityManager || {};
|
||||
|
||||
if (emDefs.relationships === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.sort((v1, v2) => {
|
||||
const t1 = this.translate(v1, 'scopeNames');
|
||||
@@ -196,9 +208,19 @@ class LinkManagerEditModalView extends ModalView {
|
||||
|
||||
const entityList = (Object.keys(scopes) || [])
|
||||
.filter(item => {
|
||||
const d = scopes[item];
|
||||
const defs = /** @type {Record} */scopes[item] || {};
|
||||
|
||||
return d.customizable && d.entity;
|
||||
if (!defs.entity || !defs.customizable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const emDefs = /** @type {Record} */defs.entityManager || {};
|
||||
|
||||
if (emDefs.relationships === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.sort((v1, v2) => {
|
||||
const t1 = this.translate(v1, 'scopeNames');
|
||||
@@ -665,10 +687,6 @@ class LinkManagerEditModalView extends ModalView {
|
||||
Espo.Utils.lowerCaseFirst(this.scope) + entityForeign :
|
||||
Espo.Utils.lowerCaseFirst(entityForeign) + this.scope;
|
||||
|
||||
if (relationName[0] !== 'c' || !/[A-Z]/.test(relationName[1])) {
|
||||
relationName = 'c' + Espo.Utils.upperCaseFirst(relationName);
|
||||
}
|
||||
|
||||
this.model.set('relationName', relationName);
|
||||
|
||||
break;
|
||||
|
||||
@@ -110,6 +110,7 @@ class FileFieldView extends LinkFieldView {
|
||||
},
|
||||
}
|
||||
|
||||
// noinspection JSCheckFunctionSignatures
|
||||
data() {
|
||||
const data = {
|
||||
...super.data(),
|
||||
@@ -123,6 +124,7 @@ class FileFieldView extends LinkFieldView {
|
||||
|
||||
data.valueIsSet = this.model.has(this.idName);
|
||||
|
||||
// noinspection JSValidateTypes
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -523,6 +525,9 @@ class FileFieldView extends LinkFieldView {
|
||||
return maxFileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {File} file
|
||||
*/
|
||||
uploadFile(file) {
|
||||
let isCanceled = false;
|
||||
|
||||
@@ -627,6 +632,11 @@ class FileFieldView extends LinkFieldView {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @param {File} file
|
||||
* @return {Promise<unknown>}
|
||||
*/
|
||||
handleUploadingFile(file) {
|
||||
return new Promise(resolve => resolve(file));
|
||||
}
|
||||
|
||||
@@ -97,6 +97,10 @@ class AddDashletModalView extends ModalView {
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
this.dashletList.forEach(item => {
|
||||
this.translations[item] = this.translate(item, 'dashlets');
|
||||
});
|
||||
}
|
||||
|
||||
afterRender() {
|
||||
@@ -125,6 +129,10 @@ class AddDashletModalView extends ModalView {
|
||||
const lowerCaseText = text.toLowerCase();
|
||||
|
||||
this.dashletList.forEach(item => {
|
||||
if (!(item in this.translations)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const label = this.translations[item].toLowerCase();
|
||||
|
||||
for (const word of label.split(' ')) {
|
||||
|
||||
@@ -429,26 +429,25 @@ class DetailModalView extends ModalView {
|
||||
return;
|
||||
}
|
||||
|
||||
const collection = this.model.collection;
|
||||
|
||||
const indexOfRecord = this.indexOfRecord;
|
||||
|
||||
let previousButtonEnabled = false;
|
||||
let nextButtonEnabled = false;
|
||||
|
||||
if (indexOfRecord > 0) {
|
||||
if (indexOfRecord > 0 || collection.offset > 0) {
|
||||
previousButtonEnabled = true;
|
||||
}
|
||||
|
||||
if (indexOfRecord < this.model.collection.total - 1) {
|
||||
if (indexOfRecord < collection.total - 1 - collection.offset) {
|
||||
nextButtonEnabled = true;
|
||||
}
|
||||
else {
|
||||
if (this.model.collection.total === -1) {
|
||||
nextButtonEnabled = true;
|
||||
} else if (this.model.collection.total === -2) {
|
||||
if (indexOfRecord < this.model.collection.length - 1) {
|
||||
nextButtonEnabled = true;
|
||||
}
|
||||
}
|
||||
else if (collection.total === -1) {
|
||||
nextButtonEnabled = true;
|
||||
}
|
||||
else if (collection.total === -2 && indexOfRecord < collection.length - 1 - collection.offset) {
|
||||
nextButtonEnabled = true;
|
||||
}
|
||||
|
||||
if (previousButtonEnabled) {
|
||||
@@ -514,7 +513,30 @@ class DetailModalView extends ModalView {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(this.indexOfRecord > 0)) {
|
||||
const collection = this.model.collection;
|
||||
|
||||
if (this.indexOfRecord <= 0 && !collection.offset) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.indexOfRecord === 0 &&
|
||||
collection.offset > 0 &&
|
||||
collection.maxSize
|
||||
) {
|
||||
collection.offset = Math.max(0, collection.offset - collection.maxSize);
|
||||
|
||||
collection.fetch()
|
||||
.then(() => {
|
||||
const indexOfRecord = collection.length - 1;
|
||||
|
||||
if (indexOfRecord < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.switchToModelByIndex(indexOfRecord);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -528,19 +550,25 @@ class DetailModalView extends ModalView {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(this.indexOfRecord < this.model.collection.total - 1) && this.model.collection.total >= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.model.collection.total === -2 && this.indexOfRecord >= this.model.collection.length - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const collection = this.model.collection;
|
||||
|
||||
if (
|
||||
!(this.indexOfRecord < collection.total - 1 - collection.offset) &&
|
||||
this.model.collection.total >= 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
collection.total === -2 &&
|
||||
this.indexOfRecord >= collection.length - 1 - collection.offset
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexOfRecord = this.indexOfRecord + 1;
|
||||
|
||||
if (indexOfRecord <= collection.length - 1) {
|
||||
if (indexOfRecord <= collection.length - 1 - collection.offset) {
|
||||
this.switchToModelByIndex(indexOfRecord);
|
||||
|
||||
return;
|
||||
|
||||
@@ -1669,14 +1669,14 @@ class DetailRecordView extends BaseRecordView {
|
||||
let nextButtonEnabled = false;
|
||||
|
||||
if (navigateButtonsEnabled) {
|
||||
if (this.indexOfRecord > 0) {
|
||||
if (this.indexOfRecord > 0 || this.model.collection.offset) {
|
||||
previousButtonEnabled = true;
|
||||
}
|
||||
|
||||
const total = this.model.collection.total !== undefined ?
|
||||
this.model.collection.total : this.model.collection.length;
|
||||
|
||||
if (this.indexOfRecord < total - 1) {
|
||||
if (this.indexOfRecord < total - 1 - this.model.collection.offset) {
|
||||
nextButtonEnabled = true;
|
||||
}
|
||||
else {
|
||||
@@ -1684,7 +1684,7 @@ class DetailRecordView extends BaseRecordView {
|
||||
nextButtonEnabled = true;
|
||||
}
|
||||
else if (total === -2) {
|
||||
if (this.indexOfRecord < this.model.collection.length - 1) {
|
||||
if (this.indexOfRecord < this.model.collection.length - 1 - this.model.collection.offset) {
|
||||
nextButtonEnabled = true;
|
||||
}
|
||||
}
|
||||
@@ -2235,7 +2235,9 @@ class DetailRecordView extends BaseRecordView {
|
||||
const model = collection.at(indexOfRecord);
|
||||
|
||||
if (!model) {
|
||||
throw new Error("Model is not found in collection by index.");
|
||||
console.error("Model is not found in collection by index.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const id = model.id;
|
||||
@@ -2254,23 +2256,34 @@ class DetailRecordView extends BaseRecordView {
|
||||
actionPrevious() {
|
||||
this.model.abortLastFetch();
|
||||
|
||||
let collection;
|
||||
|
||||
if (!this.model.collection) {
|
||||
collection = this.collection;
|
||||
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.indexOfRecord--;
|
||||
|
||||
if (this.indexOfRecord < 0) {
|
||||
this.indexOfRecord = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(this.indexOfRecord > 0)) {
|
||||
const collection = this.model.collection;
|
||||
|
||||
if (this.indexOfRecord <= 0 && !collection.offset) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.indexOfRecord === 0 &&
|
||||
collection.offset > 0 &&
|
||||
collection.maxSize
|
||||
) {
|
||||
collection.offset = Math.max(0, collection.offset - collection.maxSize);
|
||||
|
||||
collection.fetch()
|
||||
.then(() => {
|
||||
const indexOfRecord = collection.length - 1;
|
||||
|
||||
if (indexOfRecord < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.switchToModelByIndex(indexOfRecord);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2282,36 +2295,23 @@ class DetailRecordView extends BaseRecordView {
|
||||
actionNext() {
|
||||
this.model.abortLastFetch();
|
||||
|
||||
let collection;
|
||||
|
||||
if (!this.model.collection) {
|
||||
collection = this.collection;
|
||||
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.indexOfRecord--;
|
||||
|
||||
if (this.indexOfRecord < 0) {
|
||||
this.indexOfRecord = 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
collection = this.model.collection;
|
||||
}
|
||||
|
||||
if (!(this.indexOfRecord < collection.total - 1) && collection.total >= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (collection.total === -2 && this.indexOfRecord >= collection.length - 1) {
|
||||
const collection = this.model.collection;
|
||||
|
||||
if (!(this.indexOfRecord < collection.total - 1 - collection.offset) && collection.total >= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (collection.total === -2 && this.indexOfRecord >= collection.length - 1 - collection.offset) {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexOfRecord = this.indexOfRecord + 1;
|
||||
|
||||
if (indexOfRecord <= collection.length - 1) {
|
||||
if (indexOfRecord <= collection.length - 1 - collection.offset) {
|
||||
this.switchToModelByIndex(indexOfRecord);
|
||||
|
||||
return;
|
||||
|
||||
@@ -193,12 +193,18 @@ class RoleRecordTableView extends View {
|
||||
|
||||
let level = null;
|
||||
|
||||
const levelList = this.getLevelList(scope, action);
|
||||
|
||||
if (scope in aclData) {
|
||||
if (access === 'enabled') {
|
||||
if (aclData[scope] !== true) {
|
||||
if (action in aclData[scope]) {
|
||||
level = aclData[scope][action];
|
||||
}
|
||||
|
||||
if (level === null) {
|
||||
level = levelList[levelList.length - 1];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
level = 'no';
|
||||
@@ -209,7 +215,7 @@ class RoleRecordTableView extends View {
|
||||
level: level,
|
||||
name: scope + '-' + action,
|
||||
action: action,
|
||||
levelList: this.getLevelList(scope, action),
|
||||
levelList: levelList,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,113 +26,128 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
define('views/user/fields/avatar', ['views/fields/image'], function (Dep) {
|
||||
import ImageFieldView from 'views/fields/image';
|
||||
|
||||
return Dep.extend({
|
||||
class UserAvatarFieldView extends ImageFieldView {
|
||||
|
||||
setup: function () {
|
||||
Dep.prototype.setup.call(this);
|
||||
getAttributeList() {
|
||||
if (this.isEditMode()) {
|
||||
return super.getAttributeList();
|
||||
}
|
||||
|
||||
this.on('after:inline-save', () => {
|
||||
this.suspendCache = true;
|
||||
return [];
|
||||
}
|
||||
|
||||
this.reRender();
|
||||
});
|
||||
},
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
handleUploadingFile: function (file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let fileReader = new FileReader();
|
||||
this.on('after:inline-save', () => {
|
||||
this.suspendCache = true;
|
||||
|
||||
fileReader.onload = (e) => {
|
||||
this.createView('crop', 'views/modals/image-crop', {contents: e.target.result})
|
||||
.then(view => {
|
||||
view.render();
|
||||
this.reRender();
|
||||
});
|
||||
}
|
||||
|
||||
let cropped = false;
|
||||
/**
|
||||
* @protected
|
||||
* @param {File} file
|
||||
* @return {Promise<unknown>}
|
||||
*/
|
||||
handleUploadingFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
|
||||
this.listenToOnce(view, 'crop', (dataUrl) => {
|
||||
cropped = true;
|
||||
fileReader.onload = (e) => {
|
||||
this.createView('crop', 'views/modals/image-crop', {contents: e.target.result})
|
||||
.then(view => {
|
||||
view.render();
|
||||
|
||||
setTimeout(() => {
|
||||
fetch(dataUrl)
|
||||
.then(result => result.blob())
|
||||
.then(blob => {
|
||||
resolve(
|
||||
new File([blob], 'avatar.jpg', {type: 'image/jpeg'})
|
||||
);
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
let cropped = false;
|
||||
|
||||
this.listenToOnce(view, 'remove', () => {
|
||||
if (!cropped) {
|
||||
setTimeout(() => this.render(), 10);
|
||||
this.listenToOnce(view, 'crop', dataUrl => {
|
||||
cropped = true;
|
||||
|
||||
reject();
|
||||
}
|
||||
|
||||
this.clearView('crop');
|
||||
});
|
||||
setTimeout(() => {
|
||||
fetch(dataUrl)
|
||||
.then(result => result.blob())
|
||||
.then(blob => {
|
||||
resolve(
|
||||
new File([blob], 'avatar.jpg', {type: 'image/jpeg'})
|
||||
);
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
};
|
||||
|
||||
fileReader.readAsDataURL(file);
|
||||
this.listenToOnce(view, 'remove', () => {
|
||||
if (!cropped) {
|
||||
setTimeout(() => this.render(), 10);
|
||||
|
||||
reject();
|
||||
}
|
||||
|
||||
this.clearView('crop');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
fileReader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
getValueForDisplay() {
|
||||
if (!this.isReadMode()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const id = this.model.get(this.idName);
|
||||
const userId = this.model.id;
|
||||
|
||||
let t = this.cacheTimestamp = this.cacheTimestamp || Date.now();
|
||||
|
||||
if (this.suspendCache) {
|
||||
t = Date.now();
|
||||
}
|
||||
|
||||
const src = this.getBasePath() +
|
||||
'?entryPoint=avatar&size=' + this.previewSize + '&id=' + userId +
|
||||
'&t=' + t + '&attachmentId=' + (id || 'false');
|
||||
|
||||
// noinspection HtmlRequiredAltAttribute,RequiredAttributes
|
||||
const $img = $('<img>')
|
||||
.attr('src', src)
|
||||
.attr('alt', this.labelText)
|
||||
.css({
|
||||
maxWidth: (this.imageSizes[this.previewSize] || {})[0],
|
||||
maxHeight: (this.imageSizes[this.previewSize] || {})[1],
|
||||
});
|
||||
},
|
||||
|
||||
getValueForDisplay: function () {
|
||||
if (!this.isReadMode()) {
|
||||
return '';
|
||||
if (!this.isDetailMode()) {
|
||||
if (this.getCache()) {
|
||||
t = this.getCache().get('app', 'timestamp');
|
||||
}
|
||||
|
||||
let id = this.model.get(this.idName);
|
||||
let userId = this.model.id;
|
||||
const src = `${this.getBasePath()}?entryPoint=avatar&size=${this.previewSize}&id=${userId}&t=${t}`;
|
||||
|
||||
let t = this.cacheTimestamp = this.cacheTimestamp || Date.now();
|
||||
|
||||
if (this.suspendCache) {
|
||||
t = Date.now();
|
||||
}
|
||||
|
||||
let src = this.getBasePath() +
|
||||
'?entryPoint=avatar&size=' + this.previewSize + '&id=' + userId +
|
||||
'&t=' + t + '&attachmentId=' + (id || 'false');
|
||||
|
||||
let $img = $('<img>')
|
||||
$img
|
||||
.attr('width', '16')
|
||||
.attr('src', src)
|
||||
.css({
|
||||
maxWidth: (this.imageSizes[this.previewSize] || {})[0],
|
||||
maxHeight: (this.imageSizes[this.previewSize] || {})[1],
|
||||
});
|
||||
.css('maxWidth', '16px');
|
||||
}
|
||||
|
||||
if (!this.isDetailMode()) {
|
||||
if (this.getCache()) {
|
||||
t = this.getCache().get('app', 'timestamp');
|
||||
}
|
||||
|
||||
let src = this.getBasePath() + '?entryPoint=avatar&size=' +
|
||||
this.previewSize + '&id=' + userId + '&t=' + t;
|
||||
|
||||
$img
|
||||
.attr('width', '16')
|
||||
.attr('src', src)
|
||||
.css('maxWidth', '16px');
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
return $img
|
||||
.get(0)
|
||||
.outerHTML;
|
||||
}
|
||||
|
||||
return $('<a>')
|
||||
.attr('data-id', id)
|
||||
.attr('data-action', 'showImagePreview')
|
||||
.attr('href', this.getBasePath() + '?entryPoint=image&id=' + id)
|
||||
.append($img)
|
||||
if (!id) {
|
||||
return $img
|
||||
.get(0)
|
||||
.outerHTML;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return $('<a>')
|
||||
.attr('data-id', id)
|
||||
.attr('data-action', 'showImagePreview')
|
||||
.attr('href', this.getBasePath() + '?entryPoint=image&id=' + id)
|
||||
.append($img)
|
||||
.get(0)
|
||||
.outerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
export default UserAvatarFieldView;
|
||||
|
||||
@@ -26,140 +26,153 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
define('views/user/fields/generate-password', ['views/fields/base'], function (Dep) {
|
||||
import BaseFieldView from 'views/fields/base';
|
||||
|
||||
return Dep.extend({
|
||||
class UserGeneratePasswordFieldView extends BaseFieldView {
|
||||
|
||||
templateContent: '<button type="button" class="btn btn-default" data-action="generatePassword">' +
|
||||
'{{translate \'Generate\' scope=\'User\'}}</button>',
|
||||
templateContent = `
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-action="generatePassword"
|
||||
>{{translate 'Generate' scope='User'}}</button>`
|
||||
|
||||
events: {
|
||||
'click [data-action="generatePassword"]': function () {
|
||||
this.actionGeneratePassword();
|
||||
},
|
||||
events = {
|
||||
/** @this {UserGeneratePasswordFieldView} */
|
||||
'click [data-action="generatePassword"]': function () {
|
||||
this.actionGeneratePassword();
|
||||
},
|
||||
}
|
||||
|
||||
setup: function () {
|
||||
Dep.prototype.setup.call(this);
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
this.listenTo(this.model, 'change:password', (model, value, o) => {
|
||||
if (o.isGenerated) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.set({
|
||||
passwordPreview: '',
|
||||
});
|
||||
});
|
||||
|
||||
this.strengthParams = this.options.strengthParams || {};
|
||||
|
||||
this.passwordStrengthLength = this.strengthParams.passwordStrengthLength ||
|
||||
this.getConfig().get('passwordStrengthLength');
|
||||
|
||||
this.passwordStrengthLetterCount = this.strengthParams.passwordStrengthLetterCount ||
|
||||
this.getConfig().get('passwordStrengthLetterCount');
|
||||
|
||||
this.passwordStrengthNumberCount = this.strengthParams.passwordStrengthNumberCount ||
|
||||
this.getConfig().get('passwordStrengthNumberCount');
|
||||
|
||||
this.passwordGenerateLength = this.strengthParams.passwordGenerateLength ||
|
||||
this.getConfig().get('passwordGenerateLength');
|
||||
|
||||
this.passwordGenerateLetterCount = this.strengthParams.passwordGenerateLetterCount ||
|
||||
this.getConfig().get('passwordGenerateLetterCount');
|
||||
|
||||
this.passwordGenerateNumberCount = this.strengthParams.passwordGenerateNumberCount ||
|
||||
this.getConfig().get('passwordGenerateNumberCount');
|
||||
},
|
||||
|
||||
fetch: function () {
|
||||
return {};
|
||||
},
|
||||
|
||||
actionGeneratePassword: function () {
|
||||
var length = this.passwordStrengthLength;
|
||||
var letterCount = this.passwordStrengthLetterCount;
|
||||
var numberCount = this.passwordStrengthNumberCount;
|
||||
|
||||
var generateLength = this.passwordGenerateLength || 10;
|
||||
var generateLetterCount = this.passwordGenerateLetterCount || 4;
|
||||
var generateNumberCount = this.passwordGenerateNumberCount || 2;
|
||||
|
||||
length = (typeof length === 'undefined') ? generateLength : length;
|
||||
letterCount = (typeof letterCount === 'undefined') ? generateLetterCount : letterCount;
|
||||
numberCount = (typeof numberCount === 'undefined') ? generateNumberCount : numberCount;
|
||||
|
||||
if (length < generateLength) length = generateLength;
|
||||
if (letterCount < generateLetterCount) letterCount = generateLetterCount;
|
||||
if (numberCount < generateNumberCount) numberCount = generateNumberCount;
|
||||
|
||||
var password = this.generatePassword(length, letterCount, numberCount, true);
|
||||
|
||||
this.model.set({
|
||||
password: password,
|
||||
passwordConfirm: password,
|
||||
passwordPreview: password,
|
||||
}, {isGenerated: true});
|
||||
},
|
||||
|
||||
generatePassword: function (length, letters, numbers, bothCases) {
|
||||
var chars = [
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
||||
'0123456789',
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
'abcdefghijklmnopqrstuvwxyz',
|
||||
];
|
||||
|
||||
var upperCase = 0;
|
||||
var lowerCase = 0;
|
||||
|
||||
if (bothCases) {
|
||||
upperCase = 1;
|
||||
lowerCase = 1;
|
||||
|
||||
if (letters >= 2) {
|
||||
letters = letters - 2;
|
||||
} else {
|
||||
letters = 0;
|
||||
}
|
||||
this.listenTo(this.model, 'change:password', (model, value, o) => {
|
||||
if (o.isGenerated) {
|
||||
return;
|
||||
}
|
||||
|
||||
var either = length - (letters + numbers + upperCase + lowerCase);
|
||||
if (value !== undefined) {
|
||||
this.model.set('passwordPreview', null);
|
||||
|
||||
if (either < 0) {
|
||||
either = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var setList = [letters, numbers, either, upperCase, lowerCase];
|
||||
this.model.unset('passwordPreview');
|
||||
});
|
||||
|
||||
var shuffle = function (array) {
|
||||
var currentIndex = array.length, temporaryValue, randomIndex;
|
||||
this.strengthParams = this.options.strengthParams || {};
|
||||
|
||||
while (0 !== currentIndex) {
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex -= 1;
|
||||
temporaryValue = array[currentIndex];
|
||||
array[currentIndex] = array[randomIndex];
|
||||
array[randomIndex] = temporaryValue;
|
||||
}
|
||||
this.passwordStrengthLength = this.strengthParams.passwordStrengthLength ||
|
||||
this.getConfig().get('passwordStrengthLength');
|
||||
|
||||
return array;
|
||||
};
|
||||
this.passwordStrengthLetterCount = this.strengthParams.passwordStrengthLetterCount ||
|
||||
this.getConfig().get('passwordStrengthLetterCount');
|
||||
|
||||
var array = setList.map(
|
||||
function (len, i) {
|
||||
return Array(len).fill(chars[i]).map(
|
||||
function (x) {
|
||||
return x[Math.floor(Math.random() * x.length)];
|
||||
}
|
||||
).join('');
|
||||
}
|
||||
).concat();
|
||||
this.passwordStrengthNumberCount = this.strengthParams.passwordStrengthNumberCount ||
|
||||
this.getConfig().get('passwordStrengthNumberCount');
|
||||
|
||||
return shuffle(array).join('');
|
||||
},
|
||||
this.passwordGenerateLength = this.strengthParams.passwordGenerateLength ||
|
||||
this.getConfig().get('passwordGenerateLength');
|
||||
|
||||
});
|
||||
});
|
||||
this.passwordGenerateLetterCount = this.strengthParams.passwordGenerateLetterCount ||
|
||||
this.getConfig().get('passwordGenerateLetterCount');
|
||||
|
||||
this.passwordGenerateNumberCount = this.strengthParams.passwordGenerateNumberCount ||
|
||||
this.getConfig().get('passwordGenerateNumberCount');
|
||||
}
|
||||
|
||||
fetch() {
|
||||
return {};
|
||||
}
|
||||
|
||||
actionGeneratePassword() {
|
||||
let length = this.passwordStrengthLength;
|
||||
let letterCount = this.passwordStrengthLetterCount;
|
||||
let numberCount = this.passwordStrengthNumberCount;
|
||||
|
||||
const generateLength = this.passwordGenerateLength || 10;
|
||||
const generateLetterCount = this.passwordGenerateLetterCount || 4;
|
||||
const generateNumberCount = this.passwordGenerateNumberCount || 2;
|
||||
|
||||
length = (typeof length === 'undefined') ? generateLength : length;
|
||||
letterCount = (typeof letterCount === 'undefined') ? generateLetterCount : letterCount;
|
||||
numberCount = (typeof numberCount === 'undefined') ? generateNumberCount : numberCount;
|
||||
|
||||
if (length < generateLength) {
|
||||
length = generateLength;
|
||||
}
|
||||
|
||||
if (letterCount < generateLetterCount) {
|
||||
letterCount = generateLetterCount;
|
||||
}
|
||||
|
||||
if (numberCount < generateNumberCount) {
|
||||
numberCount = generateNumberCount;
|
||||
}
|
||||
|
||||
const password = this.generatePassword(length, letterCount, numberCount, true);
|
||||
|
||||
this.model.set({
|
||||
password: password,
|
||||
passwordConfirm: password,
|
||||
passwordPreview: password,
|
||||
}, {isGenerated: true});
|
||||
}
|
||||
|
||||
generatePassword(length, letters, numbers, bothCases) {
|
||||
const chars = [
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
||||
'0123456789',
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
'abcdefghijklmnopqrstuvwxyz',
|
||||
];
|
||||
|
||||
let upperCase = 0;
|
||||
let lowerCase = 0;
|
||||
|
||||
if (bothCases) {
|
||||
upperCase = 1;
|
||||
lowerCase = 1;
|
||||
|
||||
if (letters >= 2) {
|
||||
letters = letters - 2;
|
||||
} else {
|
||||
letters = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let either = length - (letters + numbers + upperCase + lowerCase);
|
||||
|
||||
if (either < 0) {
|
||||
either = 0;
|
||||
}
|
||||
|
||||
const setList = [letters, numbers, either, upperCase, lowerCase];
|
||||
|
||||
const shuffle = function (array) {
|
||||
let currentIndex = array.length, temporaryValue, randomIndex;
|
||||
|
||||
while (0 !== currentIndex) {
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex -= 1;
|
||||
temporaryValue = array[currentIndex];
|
||||
array[currentIndex] = array[randomIndex];
|
||||
array[randomIndex] = temporaryValue;
|
||||
}
|
||||
|
||||
return array;
|
||||
};
|
||||
|
||||
const array = setList.map(
|
||||
(len, i) => Array(len).fill(chars[i]).map(
|
||||
x => x[Math.floor(Math.random() * x.length)]
|
||||
).join('')
|
||||
).concat();
|
||||
|
||||
return shuffle(array).join('');
|
||||
}
|
||||
}
|
||||
|
||||
export default UserGeneratePasswordFieldView;
|
||||
|
||||
@@ -26,122 +26,159 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
define('views/user/fields/password', ['views/fields/password'], function (Dep) {
|
||||
import PasswordFieldView from 'views/fields/password';
|
||||
|
||||
return Dep.extend({
|
||||
class UserPasswordFieldView extends PasswordFieldView {
|
||||
|
||||
validations: ['required', 'strength', 'confirm'],
|
||||
validations = [
|
||||
'required',
|
||||
'strength',
|
||||
'confirm',
|
||||
]
|
||||
|
||||
setup: function () {
|
||||
Dep.prototype.setup.call(this);
|
||||
},
|
||||
init() {
|
||||
const tooltipItemList = [];
|
||||
|
||||
init: function () {
|
||||
var tooltipItemList = [];
|
||||
this.strengthParams = this.options.strengthParams || {
|
||||
passwordStrengthLength: this.getConfig().get('passwordStrengthLength'),
|
||||
passwordStrengthLetterCount: this.getConfig().get('passwordStrengthLetterCount'),
|
||||
passwordStrengthNumberCount: this.getConfig().get('passwordStrengthNumberCount'),
|
||||
passwordStrengthBothCases: this.getConfig().get('passwordStrengthBothCases'),
|
||||
};
|
||||
|
||||
this.strengthParams = this.options.strengthParams || {
|
||||
passwordStrengthLength: this.getConfig().get('passwordStrengthLength'),
|
||||
passwordStrengthLetterCount: this.getConfig().get('passwordStrengthLetterCount'),
|
||||
passwordStrengthNumberCount: this.getConfig().get('passwordStrengthNumberCount'),
|
||||
passwordStrengthBothCases: this.getConfig().get('passwordStrengthBothCases'),
|
||||
};
|
||||
const minLength = this.strengthParams.passwordStrengthLength;
|
||||
|
||||
var minLength = this.strengthParams.passwordStrengthLength;
|
||||
if (minLength) {
|
||||
tooltipItemList.push(
|
||||
'* ' + this.translate('passwordStrengthLength', 'messages', 'User').replace('{length}', minLength.toString())
|
||||
);
|
||||
if (minLength) {
|
||||
tooltipItemList.push(
|
||||
'* ' + this.translate('passwordStrengthLength', 'messages', 'User')
|
||||
.replace('{length}', minLength.toString())
|
||||
);
|
||||
}
|
||||
|
||||
const requiredLetterCount = this.strengthParams.passwordStrengthLetterCount;
|
||||
|
||||
if (requiredLetterCount) {
|
||||
tooltipItemList.push(
|
||||
'* ' + this.translate('passwordStrengthLetterCount', 'messages', 'User')
|
||||
.replace('{count}', requiredLetterCount.toString())
|
||||
);
|
||||
}
|
||||
|
||||
const requiredNumberCount = this.strengthParams.passwordStrengthNumberCount;
|
||||
|
||||
if (requiredNumberCount) {
|
||||
tooltipItemList.push(
|
||||
'* ' + this.translate('passwordStrengthNumberCount', 'messages', 'User')
|
||||
.replace('{count}', requiredNumberCount.toString())
|
||||
);
|
||||
}
|
||||
|
||||
const bothCases = this.strengthParams.passwordStrengthBothCases;
|
||||
|
||||
if (bothCases) {
|
||||
tooltipItemList.push(
|
||||
'* ' + this.translate('passwordStrengthBothCases', 'messages', 'User')
|
||||
);
|
||||
}
|
||||
|
||||
if (tooltipItemList.length) {
|
||||
this.tooltip = true;
|
||||
this.tooltipText = this.translate('Requirements', 'labels', 'User') + ':\n' + tooltipItemList.join('\n');
|
||||
}
|
||||
|
||||
super.init();
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
validateStrength() {
|
||||
if (!this.model.get(this.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = this.model.get(this.name);
|
||||
const minLength = this.strengthParams.passwordStrengthLength;
|
||||
|
||||
if (minLength) {
|
||||
if (password.length < minLength) {
|
||||
const msg = this.translate('passwordStrengthLength', 'messages', 'User')
|
||||
.replace('{length}', minLength.toString());
|
||||
|
||||
this.showValidationMessage(msg);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
var requiredLetterCount = this.strengthParams.passwordStrengthLetterCount;
|
||||
if (requiredLetterCount) {
|
||||
tooltipItemList.push(
|
||||
'* ' + this.translate('passwordStrengthLetterCount', 'messages', 'User').replace('{count}', requiredLetterCount.toString())
|
||||
);
|
||||
}
|
||||
const requiredLetterCount = this.strengthParams.passwordStrengthLetterCount;
|
||||
|
||||
var requiredNumberCount = this.strengthParams.passwordStrengthNumberCount;
|
||||
if (requiredNumberCount) {
|
||||
tooltipItemList.push(
|
||||
'* ' + this.translate('passwordStrengthNumberCount', 'messages', 'User').replace('{count}', requiredNumberCount.toString())
|
||||
);
|
||||
}
|
||||
if (requiredLetterCount) {
|
||||
let letterCount = 0;
|
||||
|
||||
var bothCases = this.strengthParams.passwordStrengthBothCases;
|
||||
if (bothCases) {
|
||||
tooltipItemList.push(
|
||||
'* ' + this.translate('passwordStrengthBothCases', 'messages', 'User')
|
||||
);
|
||||
}
|
||||
|
||||
if (tooltipItemList.length) {
|
||||
this.tooltip = true;
|
||||
this.tooltipText = this.translate('Requirements', 'labels', 'User') + ':\n' + tooltipItemList.join('\n');
|
||||
}
|
||||
|
||||
Dep.prototype.init.call(this);
|
||||
},
|
||||
|
||||
validateStrength: function () {
|
||||
if (!this.model.get(this.name)) return;
|
||||
|
||||
var password = this.model.get(this.name);
|
||||
|
||||
var minLength = this.strengthParams.passwordStrengthLength;
|
||||
if (minLength) {
|
||||
if (password.length < minLength) {
|
||||
var msg = this.translate('passwordStrengthLength', 'messages', 'User').replace('{length}', minLength.toString());
|
||||
this.showValidationMessage(msg);
|
||||
return true;;
|
||||
password.split('').forEach(c => {
|
||||
if (c.toLowerCase() !== c.toUpperCase()) {
|
||||
letterCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (letterCount < requiredLetterCount) {
|
||||
const msg = this.translate('passwordStrengthLetterCount', 'messages', 'User')
|
||||
.replace('{count}', requiredLetterCount.toString());
|
||||
|
||||
this.showValidationMessage(msg);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
var requiredLetterCount = this.strengthParams.passwordStrengthLetterCount;
|
||||
if (requiredLetterCount) {
|
||||
var letterCount = 0;
|
||||
password.split('').forEach(function (c) {
|
||||
if (c.toLowerCase() !== c.toUpperCase()) letterCount++;
|
||||
}, this);
|
||||
const requiredNumberCount = this.strengthParams.passwordStrengthNumberCount;
|
||||
|
||||
if (letterCount < requiredLetterCount) {
|
||||
var msg = this.translate('passwordStrengthLetterCount', 'messages', 'User').replace('{count}', requiredLetterCount.toString());
|
||||
this.showValidationMessage(msg);
|
||||
return true;;
|
||||
if (requiredNumberCount) {
|
||||
let numberCount = 0;
|
||||
|
||||
password.split('').forEach((c) => {
|
||||
if (c >= '0' && c <= '9') {
|
||||
numberCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (numberCount < requiredNumberCount) {
|
||||
const msg = this.translate('passwordStrengthNumberCount', 'messages', 'User')
|
||||
.replace('{count}', requiredNumberCount.toString());
|
||||
|
||||
this.showValidationMessage(msg);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
var requiredNumberCount = this.strengthParams.passwordStrengthNumberCount;
|
||||
if (requiredNumberCount) {
|
||||
var numberCount = 0;
|
||||
password.split('').forEach(function (c) {
|
||||
if (c >= '0' && c <= '9') numberCount++;
|
||||
}, this);
|
||||
const bothCases = this.strengthParams.passwordStrengthBothCases;
|
||||
|
||||
if (numberCount < requiredNumberCount) {
|
||||
var msg = this.translate('passwordStrengthNumberCount', 'messages', 'User').replace('{count}', requiredNumberCount.toString());
|
||||
this.showValidationMessage(msg);
|
||||
return true;;
|
||||
if (bothCases) {
|
||||
let ucCount = 0;
|
||||
|
||||
password.split('').forEach((c) => {
|
||||
if (c.toLowerCase() !== c.toUpperCase() && c === c.toUpperCase()) {
|
||||
ucCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var bothCases = this.strengthParams.passwordStrengthBothCases;
|
||||
if (bothCases) {
|
||||
var ucCount = 0;
|
||||
password.split('').forEach(function (c) {
|
||||
if (c.toLowerCase() !== c.toUpperCase() && c === c.toUpperCase()) ucCount++;
|
||||
}, this);
|
||||
var lcCount = 0;
|
||||
password.split('').forEach(function (c) {
|
||||
if (c.toLowerCase() !== c.toUpperCase() && c === c.toLowerCase()) lcCount++;
|
||||
}, this);
|
||||
let lcCount = 0;
|
||||
|
||||
if (!ucCount || !lcCount) {
|
||||
var msg = this.translate('passwordStrengthBothCases', 'messages', 'User');
|
||||
this.showValidationMessage(msg);
|
||||
return true;
|
||||
password.split('').forEach(c => {
|
||||
if (c.toLowerCase() !== c.toUpperCase() && c === c.toLowerCase()) {
|
||||
lcCount++;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
if (!ucCount || !lcCount) {
|
||||
const msg = this.translate('passwordStrengthBothCases', 'messages', 'User');
|
||||
|
||||
this.showValidationMessage(msg);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UserPasswordFieldView;
|
||||
|
||||
@@ -124,11 +124,15 @@ class UserEditRecordView extends EditRecordView {
|
||||
|
||||
this.hideField('sendAccessInfo');
|
||||
|
||||
if (!this.model.has('sendAccessInfo')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.set('sendAccessInfo', false);
|
||||
}
|
||||
|
||||
controlSendAccessInfoFieldForNew() {
|
||||
let skipSettingTrue = this.recordHelper.getFieldStateParam('sendAccessInfo', 'hidden') === false;
|
||||
const skipSettingTrue = this.recordHelper.getFieldStateParam('sendAccessInfo', 'hidden') === false;
|
||||
|
||||
if (this.isPasswordSendable()) {
|
||||
this.showField('sendAccessInfo');
|
||||
@@ -173,7 +177,7 @@ class UserEditRecordView extends EditRecordView {
|
||||
getGridLayout(callback) {
|
||||
this.getHelper().layoutManager
|
||||
.get(this.model.entityType, this.options.layoutName || this.layoutName, simpleLayout => {
|
||||
let layout = Espo.Utils.cloneDeep(simpleLayout);
|
||||
const layout = Espo.Utils.cloneDeep(simpleLayout);
|
||||
|
||||
layout.push({
|
||||
"label": "Teams and Access Control",
|
||||
@@ -279,7 +283,7 @@ class UserEditRecordView extends EditRecordView {
|
||||
});
|
||||
}
|
||||
|
||||
let gridLayout = {
|
||||
const gridLayout = {
|
||||
type: 'record',
|
||||
layout: this.convertDetailLayout(layout),
|
||||
};
|
||||
@@ -302,7 +306,7 @@ class UserEditRecordView extends EditRecordView {
|
||||
}
|
||||
|
||||
fetch() {
|
||||
let data = super.fetch();
|
||||
const data = super.fetch();
|
||||
|
||||
if (!this.isNew) {
|
||||
if (
|
||||
|
||||
@@ -494,7 +494,8 @@ body {
|
||||
border-bottom-left-radius: var(--panel-border-radius);
|
||||
border-bottom-right-radius: var(--panel-border-radius);
|
||||
|
||||
> table,
|
||||
> table:not(:has(+ .show-more)),
|
||||
> table:not(:has(+ .show-more)) tr:last-child td,
|
||||
> .list-group,
|
||||
> .list-group:last-child > .list-group-item:last-child,
|
||||
> .show-more > a {
|
||||
@@ -516,6 +517,7 @@ body {
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
> table,
|
||||
> table tr:last-child td,
|
||||
> .list-group,
|
||||
> .list-group:last-child > .list-group-item:last-child,
|
||||
> .show-more > a {
|
||||
|
||||
@@ -45,7 +45,10 @@
|
||||
"extension is missing": "разширението липсва",
|
||||
"headerTitle": "EspoCRM инсталация",
|
||||
"Crontab setup instructions": "Без стартиране на планирани задачи входящите имейли, известията и напомнянията няма да работят. Тук можете да прочетете повече {SETUP_INSTRUCTIONS}.",
|
||||
"Setup instructions": "инструкции за настройка"
|
||||
"Setup instructions": "инструкции за настройка",
|
||||
"requiredMysqlVersion": "MySQL версия",
|
||||
"requiredMariadbVersion": "MariaDB версия",
|
||||
"requiredPostgresqlVersion": "PostgreSQL версия"
|
||||
},
|
||||
"fields": {
|
||||
"Choose your language": "Изберете вашия език",
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "8.2.2",
|
||||
"version": "8.2.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "espocrm",
|
||||
"version": "8.2.2",
|
||||
"version": "8.2.4",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "8.2.2",
|
||||
"version": "8.2.4",
|
||||
"description": "Open-source CRM.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1380,4 +1380,16 @@ class EvaluatorTest extends \PHPUnit\Framework\TestCase
|
||||
|
||||
$this->assertEquals(2, $vars->j);;
|
||||
}
|
||||
|
||||
public function testStringsWithOperator(): void
|
||||
{
|
||||
$expression = "\$a = '='";
|
||||
|
||||
$vars = (object) [];
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$this->evaluator->process($expression, null, $vars);
|
||||
|
||||
$this->assertEquals("=", $vars->a);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user