Compare commits

...

30 Commits
8.2.2 ... 8.2.4

Author SHA1 Message Date
Yuri Kuznetsov
94881de082 8.2.4 2024-05-08 11:32:01 +03:00
Yuri Kuznetsov
cbec1cbbe5 fix parser 2024-05-04 23:00:46 +03:00
Yuri Kuznetsov
71dd872618 calendar duplicate event fix 2024-04-30 14:15:39 +03:00
Yuri Kuznetsov
bfed154feb language load off 2024-04-26 11:01:54 +03:00
Yuri Kuznetsov
5a19b90b13 fix role empty stream level on ui 2024-04-22 12:38:33 +03:00
Yuri Kuznetsov
33b0ca8824 fix add dashlet 2024-04-20 11:32:41 +03:00
Yuri Kuznetsov
540c58a564 fix add dashlet quick seach 2024-04-20 08:35:16 +03:00
Yuri Kuznetsov
67c0be5699 8.2.3 2024-04-19 10:21:05 +03:00
Yuri Kuznetsov
695dc2eb42 record service create: keep new 2024-04-19 09:30:49 +03:00
Yuri Kuznetsov
d1dd39bf5a email parent allow autocomplete on empty 2024-04-16 12:38:03 +03:00
Yuri Kuznetsov
8a542adb7e calendar update class names 2024-04-11 17:31:14 +03:00
Yuri Kuznetsov
c6a172a6d0 style fix 2024-04-10 13:23:27 +03:00
Yuri Kuznetsov
89f75bfb37 oidc prevent errors when user is not allowed 2024-04-09 09:24:23 +03:00
Yuri Kuznetsov
b83a66cf49 bg lang 2024-04-08 12:51:16 +03:00
Yuri Kuznetsov
3cd6deb732 pagination previous step and modal detail pagination fix 2024-04-08 12:07:14 +03:00
Yuri Kuznetsov
59fff4cc80 fix detail pagination 2024-04-08 11:40:57 +03:00
Yuri Kuznetsov
dca1d34685 reminder usersColumns check 2024-04-07 18:30:55 +03:00
Yuri Kuznetsov
8133682288 ref 2024-04-06 09:58:48 +03:00
Yuri Kuznetsov
0b035b89c0 user fetch issue 2024-04-06 09:55:56 +03:00
Yuri Kuznetsov
d5930542d9 ref 2024-04-06 09:51:56 +03:00
Yuri Kuznetsov
831dfc5bfd fix sndAccessInfo fetch 2024-04-06 09:51:48 +03:00
Yuri Kuznetsov
078808604c ref 2024-04-06 09:38:39 +03:00
Yuri Kuznetsov
35f8c5652a avater flicker fix 2024-04-06 09:35:25 +03:00
Yuri Kuznetsov
14d8f88985 ref 2024-04-06 09:32:17 +03:00
Yuri Kuznetsov
2ae0a118b6 disable note in link manager 2024-04-05 23:11:44 +03:00
Yuri Kuznetsov
1a51759b0f fix user acl 2024-04-05 20:18:21 +03:00
Yuri Kuznetsov
48b0063c69 middle table prefix server side check 2024-04-05 09:51:22 +03:00
Yuri Kuznetsov
9395fa886e grid layout css fix 2024-04-05 09:39:22 +03:00
Yuri Kuznetsov
a6c95ff0d5 layouts title 2024-04-05 09:37:10 +03:00
Eymen Elkum
1c10ddd7b8 add missing & to google static url 2024-04-04 13:53:06 +03:00
43 changed files with 685 additions and 430 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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 */

View File

@@ -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());

View File

@@ -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']);

View File

@@ -11,7 +11,8 @@
"description": "Описание",
"lead": "Потенциална продажба",
"attachments": "Прикачени файлове",
"inboundEmail": "Групов имейл акаунт"
"inboundEmail": "Групов имейл акаунт",
"originalEmail": "Оригинален имейл"
},
"links": {
"account": "Клиент",

View File

@@ -21,7 +21,8 @@
"acceptanceStatusMeetings": "Статус на приемане (Срещи)",
"acceptanceStatusCalls": "Статус на приемане (Разговори)",
"title": "Роля при клиент",
"hasPortalUser": "Притежава акаунт в портала"
"hasPortalUser": "Притежава акаунт в портала",
"originalEmail": "Оригинален имейл"
},
"links": {
"opportunities": "Сделки",

View File

@@ -30,7 +30,8 @@
"opportunityAmountCurrency": "Валута на сумата в сделката",
"acceptanceStatusMeetings": "Статус на приемане (Срещи)",
"acceptanceStatusCalls": "Статус на приемане (Разговори)",
"convertedAt": "Конвертирана на"
"convertedAt": "Конвертирана на",
"originalEmail": "Оригинален имейл"
},
"links": {
"targetLists": "Целеви списъци",

View File

@@ -10,7 +10,8 @@
"excludingActionList": "С изключение на",
"optedOutCount": "Отписали се",
"targetStatus": "Целеви статус",
"isOptedOut": "Отписал се"
"isOptedOut": "Отписал се",
"sourceCampaign": "Източник на кампанията"
},
"links": {
"accounts": "Клиенти",

View File

@@ -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']);

View File

@@ -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": "Изберете вида обект от лявото меню.",

View File

@@ -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": "Основен филтър за прилагане по подразбиране при изберане на запис."
}
}

View File

@@ -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": "Назначен потребител",

View File

@@ -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": "Оформлението не може да бъде празно.",

View File

@@ -31,7 +31,8 @@
"assignmentNotificationsIgnoreEntityTypeList": "Известия за назначения на записи (в приложението)",
"assignmentEmailNotificationsIgnoreEntityTypeList": "Известия за назначения на записи (по имейл)",
"dashboardLocked": "Заключи дашборда",
"textSearchStoringDisabled": "Деактивиране на съхраняването на текстови филтри"
"textSearchStoringDisabled": "Деактивиране на съхраняването на текстови филтри",
"calendarSlotDuration": "Продължителност на календарния слот"
},
"options": {
"weekStart": {
@@ -51,5 +52,10 @@
"doNotFillAssignedUserIfNotRequired": "При създаване на запис назначеният потребител няма да бъде попълнен със собствен потребител, освен ако полето не е задължително.",
"followCreatedEntities": "Когато създавате нови записи, те ще бъдат следвани автоматично, дори ако са назначени на друг потребител.",
"followCreatedEntityTypeList": "Когато създавате нови записи за избрани типове обекти, те ще бъдат следвани автоматично, дори ако са назначени на друг потребител."
},
"tabFields": {
"label": "Текст",
"iconClass": "Иконка",
"color": "Цвят"
}
}

View File

@@ -12,7 +12,8 @@
"followerManagementPermission": "Права за управление на последователи",
"data": "Данни",
"fieldData": "Данни на полето",
"messagePermission": "Права за писане на съобщения"
"messagePermission": "Права за писане на съобщения",
"auditPermission": "Права за одитна история"
},
"links": {
"users": "Потребители",
@@ -60,6 +61,7 @@
"portalPermission": "Достъп до информация в портала, възможност за публикуване на съобщения до потребителите на портала.",
"groupEmailAccountPermission": "Достъп до групови имейл акаунти, възможност за изпращане на имейли от групов SMTP акаунт.",
"exportPermission": "Позволява експортването на записи.",
"massUpdatePermission": "Възможност за извършване на масово актуализиране на записи."
"massUpdatePermission": "Възможност за извършване на масово актуализиране на записи.",
"auditPermission": "Позволява потребителя да достъпва одитната история."
}
}

View File

@@ -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": "Системни настройки",

View File

@@ -1 +1,18 @@
{}
{
"fields": {
"event": "Събитие",
"target": "Цел",
"data": "Данни",
"status": "Статус",
"processedAt": "Обработено на",
"attempts": "Опити",
"processAt": "Обработка на"
},
"options": {
"status": {
"Pending": "Текущо",
"Success": "Успешно",
"Failed": "Неуспешно"
}
}
}

View File

@@ -331,6 +331,7 @@
"fieldManagerParamList": [
"required",
"entityList",
"autocompleteOnEmpty",
"audited",
"tooltipText"
]

View File

@@ -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.");

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -204,6 +204,8 @@ class Language {
* @returns {Promise}
*/
load(callback, disableCache, loadDefault) {
this.off('sync');
if (callback) {
this.once('sync', callback);
}

View File

@@ -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;

View File

@@ -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));
}

View 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(' ')) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
});
});
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 (

View File

@@ -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 {

View File

@@ -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
View File

@@ -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": {

View File

@@ -1,6 +1,6 @@
{
"name": "espocrm",
"version": "8.2.2",
"version": "8.2.4",
"description": "Open-source CRM.",
"repository": {
"type": "git",

View File

@@ -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);
}
}