internation phone numbers

This commit is contained in:
Yuri Kuznetsov
2023-11-10 15:15:39 +02:00
parent e1c101b786
commit 66cbf57f5e
24 changed files with 1926 additions and 26 deletions

View File

@@ -29,25 +29,27 @@
namespace Espo\Classes\FieldValidators;
use Brick\PhoneNumber\PhoneNumber;
use Brick\PhoneNumber\PhoneNumberParseException;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Defs;
use Espo\ORM\Entity;
use stdClass;
/**
* @noinspection PhpUnused
*/
class PhoneType
{
private Metadata $metadata;
private Defs $defs;
private const DEFAULT_MAX_LENGTH = 36;
public function __construct(Metadata $metadata, Defs $defs)
{
$this->metadata = $metadata;
$this->defs = $defs;
}
public function __construct(
private Metadata $metadata,
private Defs $defs,
private Config $config
) {}
public function checkRequired(Entity $entity, string $field): bool
{
@@ -193,7 +195,22 @@ class PhoneType
$preparedPattern = '/^' . $pattern . '$/';
return (bool) preg_match($preparedPattern, $number);
if (!preg_match($preparedPattern, $number)) {
return false;
}
if (!$this->config->get('phoneNumberInternational')) {
return true;
}
try {
$numberObj = PhoneNumber::parse($number);
}
catch (PhoneNumberParseException) {
return false;
}
return $numberObj->isPossibleNumber();
}
protected function isNotEmpty(Entity $entity, string $field): bool

View File

@@ -279,6 +279,8 @@ return [
'oidcScopes' => ['profile', 'email', 'phone'],
'listViewSettingsDisabled' => false,
'cleanupDeletedRecords' => true,
'phoneNumberNumericSearch' => false,
'phoneNumberNumericSearch' => true,
'phoneNumberInternational' => true,
'phoneNumberPreferredCountryList' => ['us', 'de'],
'isInstalled' => false,
];

View File

@@ -305,6 +305,10 @@
"fieldNotMatchingPattern$uriOptionalProtocol": "{field} must be a valid URL",
"fieldInvalid": "{field} is invalid",
"fieldIsRequired": "{field} is required",
"fieldPhoneInvalid": "{field} is invalid",
"fieldPhoneInvalidCode": "Invalid country code",
"fieldPhoneTooShort": "{field} is too short",
"fieldPhoneTooLong": "{field} is too long",
"fieldPhoneInvalidCharacters": "Only digits, latin letters and characters `-+_@:#().` are allowed",
"fieldShouldBeEmail": "{field} should be a valid email",
"fieldShouldBeFloat": "{field} should be a valid float",

View File

@@ -71,6 +71,9 @@
"confirmRemoveImportLog" : "This will remove the import log. All imported records will be kept. You won't be able to revert import results. Are you sure?",
"removeImportLog": "This will remove the import log. All imported records will be kept. Use it if you are sure that import is fine."
},
"params": {
"phoneNumberCountry": "Telephone country code"
},
"fields": {
"file": "File",
"entityType": "Entity Type",

View File

@@ -110,6 +110,8 @@
"adminNotificationsNewExtensionVersion": "Show notification when new versions of extensions are available",
"textFilterUseContainsForVarchar": "Use 'contains' operator when filtering varchar fields",
"phoneNumberNumericSearch": "Numeric phone number search",
"phoneNumberInternational": "International phone numbers",
"phoneNumberPreferredCountryList": "Preferred telephone country codes",
"authTokenPreventConcurrent": "Only one auth token per user",
"scopeColorsDisabled": "Disable scope colors",
"tabColorsDisabled": "Disable tab colors",
@@ -282,6 +284,7 @@
"Misc": "Misc",
"SMTP": "SMTP",
"General": "General",
"Phone Numbers": "Phone Numbers",
"Navbar": "Navbar",
"Dashboard": "Dashboard",
"Configuration": "Configuration",

View File

@@ -10,8 +10,7 @@
{
"label": "Search",
"rows": [
[{"name": "textFilterUseContainsForVarchar"}, {"name": "globalSearchEntityList"}],
[{"name": "phoneNumberNumericSearch"}, false]
[{"name": "textFilterUseContainsForVarchar"}, {"name": "globalSearchEntityList"}]
]
},
{
@@ -36,6 +35,13 @@
[{"name": "addressStateList"}, false]
]
},
{
"label": "Phone Numbers",
"rows": [
[{"name": "phoneNumberInternational"}, {"name": "phoneNumberPreferredCountryList"}],
[{"name": "phoneNumberNumericSearch"}, false]
]
},
{
"label": "Activities",
"rows": [

View File

@@ -78,6 +78,18 @@
"exportsAs": "Selectize"
},
"autonumeric": {},
"intl-tel-input": {
"exportsTo": "window",
"exportsAs": "intlTelInput"
},
"intl-tel-input-utils": {
"exportsTo": "window",
"exportsAs": "intlTelInputUtils"
},
"intl-tel-input-globals": {
"exportsTo": "window",
"exportsAs": "intlTelInputGlobals"
},
"cronstrue": {
"path": "client/lib/cronstrue-i18n.js",
"devPath": "client/lib/original/cronstrue-i18n.js",

View File

@@ -691,6 +691,13 @@
"phoneNumberNumericSearch": {
"type": "bool"
},
"phoneNumberInternational": {
"type": "bool"
},
"phoneNumberPreferredCountryList": {
"type": "multiEnum",
"view": "views/settings/fields/phone-number-preferred-country-list"
},
"scopeColorsDisabled": {
"type": "bool"
},

View File

@@ -29,6 +29,8 @@
namespace Espo\Tools\Import;
use Brick\PhoneNumber\PhoneNumber;
use Brick\PhoneNumber\PhoneNumberParseException;
use Espo\Core\FieldValidation\Exceptions\ValidationError;
use Espo\Core\Job\JobSchedulerFactory;
use Espo\Entities\Attachment;
@@ -770,7 +772,7 @@ class Import
}
$o = (object) [
'phoneNumber' => $value,
'phoneNumber' => $this->formatPhoneNumber($value, $params),
'primary' => true,
];
@@ -867,7 +869,7 @@ class Import
}
$o = (object) [
'phoneNumber' => $value,
'phoneNumber' => $this->formatPhoneNumber($value, $params),
'type' => $type,
'primary' => $isPrimary,
];
@@ -917,7 +919,6 @@ class Import
{
$params = $this->params;
/** @noinspection PhpRedundantVariableDocTypeInspection */
/** @var non-empty-string $decimalMark */
$decimalMark = $params->getDecimalMark() ?? self::DEFAULT_DECIMAL_MARK;
@@ -1255,4 +1256,26 @@ class Import
$errorIndex++;
}
private function formatPhoneNumber(string $value, Params $params): string
{
if (str_starts_with($value, '+')) {
return $value;
}
if (!$params->getPhoneNumberCountry()) {
return $value;
}
$code = strtoupper($params->getPhoneNumberCountry());
try {
$number = PhoneNumber::parse($value, $code);
return (string) $number;
}
catch (PhoneNumberParseException) {
return $value;
}
}
}

View File

@@ -62,6 +62,7 @@ class Params
private ?string $currency = null;
private ?string $timezone = null;
private ?string $decimalMark = null;
private ?string $phoneNumberCountry = null;
private function __construct()
{
@@ -88,6 +89,11 @@ class Params
return $this->personNameFormat;
}
public function getPhoneNumberCountry(): ?string
{
return $this->phoneNumberCountry;
}
public function isIdleMode(): bool
{
return $this->idleMode;
@@ -193,6 +199,14 @@ class Params
return $obj;
}
public function withPhoneNumberCountry(?string $phoneNumberCountry): self
{
$obj = clone $this;
$obj->phoneNumberCountry = $phoneNumberCountry;
return $obj;
}
public function withIdleMode(bool $idleMode = true): self
{
$obj = clone $this;
@@ -339,6 +353,7 @@ class Params
->withIdleMode($raw->idleMode ?? false)
->withManualMode($raw->manualMode ?? false)
->withPersonNameFormat($raw->personNameFormat ?? null)
->withPhoneNumberCountry($raw->phoneNumberCountry ?? null)
->withSilentMode($raw->silentMode ?? false)
->withSkipDuplicateChecking($raw->skipDuplicateChecking ?? false)
->withStartFromLastIndex($raw->startFromLastIndex ?? false)
@@ -368,6 +383,7 @@ class Params
'skipDuplicateChecking' => $this->skipDuplicateChecking,
'startFromLastIndex' => $this->startFromLastIndex,
'textQualifier' => $this->textQualifier,
'phoneNumberCountry' => $this->phoneNumberCountry,
'timeFormat' => $this->timeFormat,
'timezone' => $this->timezone,
'updateBy' => $this->updateBy,

View File

@@ -109,6 +109,10 @@
</div>
</div>
<div class="col-sm-4 form-group cell">
<label class="control-label">{{translate 'phoneNumberCountry' category='params' scope='Import'}}</label>
<div data-name="phoneNumberCountry" class="field">
{{{phoneNumberCountryField}}}
</div>
</div>
</div>
<div class="row">

View File

@@ -28,6 +28,11 @@
import VarcharFieldView from 'views/fields/varchar';
import Select from 'ui/select';
import intlTelInput from 'intl-tel-input';
// noinspection NpmUsedModulesInstalled
import intlTelInputUtils from 'intl-tel-input-utils';
// noinspection NpmUsedModulesInstalled
import intlTelInputGlobals from 'intl-tel-input-globals';
class PhoneFieldView extends VarcharFieldView {
@@ -176,20 +181,47 @@ class PhoneFieldView extends VarcharFieldView {
.replace('{field}', this.getLabelText());
const number = row.phoneNumber;
const n = (i + 1).toString();
const selector = `div.phone-number-block:nth-child(${n}) input.phone-number`;
if (!regExp.test(number)) {
notValid = true;
const msg = this.translate('fieldPhoneInvalidCharacters', 'messages')
.replace('{field}', this.getLabelText());
this.showValidationMessage(msg, 'div.phone-number-block:nth-child(' + (i + 1)
.toString() + ') input.phone-number');
this.showValidationMessage(msg, selector);
}
if (this.useInternational) {
const element = this.$el.find(selector).get(0);
if (element) {
const obj = this.intlTelInputMap.get(element);
if (obj && !obj.isPossibleNumber()) {
notValid = true;
const code = obj.getValidationError();
const key = [
'fieldPhoneInvalid',
'fieldPhoneInvalidCode',
'fieldPhoneTooShort',
'fieldPhoneTooLong',
][code || 0] || 'fieldPhoneInvalid';
const msg = this.translate(key, 'messages')
.replace('{field}', this.getLabelText());
this.showValidationMessage(msg, selector);
}
}
}
const numberClean = String(number).replace(/[\s+]/g, '');
if (~numberList.indexOf(numberClean)) {
this.showValidationMessage(msg, 'div.phone-number-block:nth-child(' + (i + 1)
.toString() + ') input.phone-number');
@@ -239,13 +271,17 @@ class PhoneFieldView extends VarcharFieldView {
if (phoneNumberData) {
phoneNumberData = Espo.Utils.cloneDeep(phoneNumberData);
phoneNumberData.forEach((item) => {
phoneNumberData.forEach(item => {
const number = item.phoneNumber || '';
item.erased = number.indexOf(this.erasedPlaceholder) === 0;
if (!item.erased) {
item.valueForLink = number.replace(/ /g, '');
if (this.isReadMode()) {
item.phoneNumber = this.formatNumber(item.phoneNumber);
}
}
item.lineThrough = item.optOut || item.invalid || this.model.get('doNotCall');
@@ -253,13 +289,16 @@ class PhoneFieldView extends VarcharFieldView {
}
if ((!phoneNumberData || phoneNumberData.length === 0) && this.model.get(this.name)) {
const o = {
phoneNumber: number,
phoneNumber: this.formatNumber(number),
primary: true,
valueForLink: number.replace(/ /g, ''),
};
if (this.isReadMode()) {
o.phoneNumber = this.formatNumber(o.phoneNumber);
}
if (this.mode === 'edit' && this.model.isNew()) {
o.type = this.defaultType;
}
@@ -267,6 +306,8 @@ class PhoneFieldView extends VarcharFieldView {
phoneNumberData = [o];
}
const data = {
...super.data(),
phoneNumberData: phoneNumberData,
@@ -287,6 +328,7 @@ class PhoneFieldView extends VarcharFieldView {
}
data.valueIsSet = this.model.has(this.name);
data.value = this.formatNumber(data.value);
}
data.itemMaxLength = this.itemMaxLength;
@@ -314,6 +356,19 @@ class PhoneFieldView extends VarcharFieldView {
this.trigger('change');
}
formatNumber(value) {
if (!value || value === '' || !this.useInternational) {
return value;
}
// noinspection JSUnresolvedReference
return intlTelInputUtils.formatNumber(
value,
null,
intlTelInputUtils.numberFormat.INTERNATIONAL
);
}
addPhoneNumber() {
const data = Espo.Utils.cloneDeep(this.fetchPhoneNumberData());
@@ -346,6 +401,25 @@ class PhoneFieldView extends VarcharFieldView {
}
}
afterRenderEdit() {
super.afterRenderEdit();
if (this.useInternational) {
const inputElements = this.element.querySelectorAll('input.phone-number');
inputElements.forEach(inputElement => {
const obj = intlTelInput(inputElement, {
separateDialCode: true,
showFlags: false,
preferredCountries: this.preferredCountryList,
localizedCountries: this._codeNames,
});
this.intlTelInputMap.set(inputElement, obj);
});
}
}
afterRenderSearch() {
super.afterRenderSearch();
@@ -424,6 +498,17 @@ class PhoneFieldView extends VarcharFieldView {
this.isInvalidFieldName = this.name + 'IsInvalid';
this.phoneNumberOptedOutByDefault = this.getConfig().get('phoneNumberIsOptedOutByDefault');
this.useInternational = this.getConfig().get('phoneNumberInternational') || false;
this.preferredCountryList = this.getConfig().get('phoneNumberPreferredCountryList') || [];
if (this.useInternational && !this.isListMode() && !this.isSearchMode()) {
this._codeNames = intlTelInputGlobals.getCountryData()
.reduce((map, item) => {
map[item.iso2] = item.iso2.toUpperCase();
return map;
}, {});
}
if (this.model.has('doNotCall')) {
this.listenTo(this.model, 'change:doNotCall', (model, value, o) => {
@@ -443,6 +528,16 @@ class PhoneFieldView extends VarcharFieldView {
this.itemMaxLength = this.getMetadata()
.get(['entityDefs', 'PhoneNumber', 'fields', 'name', 'maxLength']);
this.intlTelInputMap = new Map();
this.once('remove', () => {
for (const obj of this.intlTelInputMap.values()) {
obj.destroy();
}
this.intlTelInputMap.clear();
})
}
fetchPhoneNumberData() {
@@ -455,7 +550,18 @@ class PhoneFieldView extends VarcharFieldView {
const row = {};
const $d = $(d);
row.phoneNumber = $d.find('input.phone-number').val().trim();
/** @type {HTMLInputElement} */
const inputElement = $d.find('input.phone-number').get(0);
if (!inputElement) {
return;
}
row.phoneNumber = inputElement.value.trim();
if (this.intlTelInputMap.has(inputElement)) {
row.phoneNumber = this.intlTelInputMap.get(inputElement).getNumber();
}
if (row.phoneNumber === '') {
return;

View File

@@ -28,6 +28,8 @@
import View from 'view';
import Model from 'model';
// noinspection NpmUsedModulesInstalled
import intlTelInputGlobals from 'intl-tel-input-globals';
class Step1ImportView extends View {
@@ -100,6 +102,7 @@ class Step1ImportView extends View {
'idleMode',
'skipDuplicateChecking',
'manualMode',
'phoneNumberCountry',
];
this.paramList.forEach(item => {
@@ -355,6 +358,22 @@ class Step1ImportView extends View {
tooltipText: this.translate('manualMode', 'tooltips', 'Import'),
});
this.createView('phoneNumberCountryField', 'views/fields/enum', {
selector: '.field[data-name="phoneNumberCountry"]',
model: this.model,
name: 'phoneNumberCountry',
mode: 'edit',
params: {
options: ['', ...intlTelInputGlobals.getCountryData().map(item => item.iso2)],
},
translatedOptions: intlTelInputGlobals.getCountryData()
.reduce((map, item) => {
map[item.iso2] = `${item.iso2.toUpperCase()} +${item.dialCode}`;
return map;
}, {})
});
this.listenTo(this.model, 'change', (m, o) => {
if (!o.ui) {
return;

View File

@@ -0,0 +1,55 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
import MultiEnumFieldView from 'views/fields/multi-enum';
// noinspection NpmUsedModulesInstalled
import intlTelInputGlobals from 'intl-tel-input-globals';
class SettingsPhoneNumberPreferredCountryListFieldView extends MultiEnumFieldView {
setupOptions() {
try {
const dataList = intlTelInputGlobals.getCountryData();
this.params.options = dataList
.map(item => item.iso2);
this.translatedOptions = dataList.reduce((map, item) => {
map[item.iso2] = `${item.iso2.toUpperCase()} +${item.dialCode}`;
return map;
}, {});
}
catch (e) {
console.error(e);
}
}
}
// noinspection JSUnusedGlobalSymbols
export default SettingsPhoneNumberPreferredCountryListFieldView;

View File

@@ -47,7 +47,8 @@
"johngrogg/ics-parser": "^3.0",
"phpseclib/phpseclib": "^3.0",
"openspout/openspout": "^4.9",
"dompdf/dompdf": "^2.0"
"dompdf/dompdf": "^2.0",
"brick/phonenumber": "^0.5.0"
},
"require-dev": {
"phpunit/phpunit": "^9",

181
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "2b80267e733b9b4153f66fc0cfce5315",
"content-hash": "d00cfc7a66d5f0fc733141d48f9eca16",
"packages": [
{
"name": "async-aws/core",
@@ -133,6 +133,59 @@
],
"time": "2021-02-01T21:10:42+00:00"
},
{
"name": "brick/phonenumber",
"version": "0.5.0",
"source": {
"type": "git",
"url": "https://github.com/brick/phonenumber.git",
"reference": "6286a218375a46a6f4d0fd361323f22313882814"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/brick/phonenumber/zipball/6286a218375a46a6f4d0fd361323f22313882814",
"reference": "6286a218375a46a6f4d0fd361323f22313882814",
"shasum": ""
},
"require": {
"ext-json": "*",
"giggsey/libphonenumber-for-php": "^7.0 || ^8.0",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.0",
"phpunit/phpunit": "^9.0",
"vimeo/psalm": "5.7.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Brick\\PhoneNumber\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Phone number library",
"keywords": [
"brick",
"phone",
"phone number",
"phonenumber"
],
"support": {
"issues": "https://github.com/brick/phonenumber/issues",
"source": "https://github.com/brick/phonenumber/tree/0.5.0"
},
"funding": [
{
"url": "https://github.com/BenMorel",
"type": "github"
}
],
"time": "2023-02-23T21:41:31+00:00"
},
{
"name": "cboden/ratchet",
"version": "v0.4.4",
@@ -901,6 +954,132 @@
},
"time": "2020-11-24T22:02:12+00:00"
},
{
"name": "giggsey/libphonenumber-for-php",
"version": "8.13.24",
"source": {
"type": "git",
"url": "https://github.com/giggsey/libphonenumber-for-php.git",
"reference": "746ca6a565b9d4167c94c80824f43fa6fb463fd1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/giggsey/libphonenumber-for-php/zipball/746ca6a565b9d4167c94c80824f43fa6fb463fd1",
"reference": "746ca6a565b9d4167c94c80824f43fa6fb463fd1",
"shasum": ""
},
"require": {
"giggsey/locale": "^1.7|^2.0",
"php": ">=5.3.2",
"symfony/polyfill-mbstring": "^1.17"
},
"require-dev": {
"pear/pear-core-minimal": "^1.9",
"pear/pear_exception": "^1.0",
"pear/versioncontrol_git": "^0.5",
"phing/phing": "^2.7",
"php-coveralls/php-coveralls": "^1.0|^2.0",
"symfony/console": "^2.8|^3.0|^v4.4|^v5.2",
"symfony/phpunit-bridge": "^4.2 || ^5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
}
},
"autoload": {
"psr-4": {
"libphonenumber\\": "src/"
},
"exclude-from-classmap": [
"/src/data/",
"/src/carrier/data/",
"/src/geocoding/data/",
"/src/timezone/data/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Joshua Gigg",
"email": "giggsey@gmail.com",
"homepage": "https://giggsey.com/"
}
],
"description": "PHP Port of Google's libphonenumber",
"homepage": "https://github.com/giggsey/libphonenumber-for-php",
"keywords": [
"geocoding",
"geolocation",
"libphonenumber",
"mobile",
"phonenumber",
"validation"
],
"support": {
"issues": "https://github.com/giggsey/libphonenumber-for-php/issues",
"source": "https://github.com/giggsey/libphonenumber-for-php"
},
"time": "2023-10-31T08:12:54+00:00"
},
{
"name": "giggsey/locale",
"version": "2.5",
"source": {
"type": "git",
"url": "https://github.com/giggsey/Locale.git",
"reference": "e6d4540109a01dd2bc7334cdc842d6a6a67cf239"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/giggsey/Locale/zipball/e6d4540109a01dd2bc7334cdc842d6a6a67cf239",
"reference": "e6d4540109a01dd2bc7334cdc842d6a6a67cf239",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"require-dev": {
"ext-json": "*",
"pear/pear-core-minimal": "^1.9",
"pear/pear_exception": "^1.0",
"pear/versioncontrol_git": "^0.5",
"phing/phing": "^2.7",
"php-coveralls/php-coveralls": "^2.0",
"phpunit/phpunit": "^8.5|^9.5",
"symfony/console": "^5.0|^6.0",
"symfony/filesystem": "^5.0|^6.0",
"symfony/finder": "^5.0|^6.0",
"symfony/process": "^5.0|^6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Giggsey\\Locale\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Joshua Gigg",
"email": "giggsey@gmail.com",
"homepage": "https://giggsey.com/"
}
],
"description": "Locale functions required by libphonenumber-for-php",
"support": {
"issues": "https://github.com/giggsey/Locale/issues",
"source": "https://github.com/giggsey/Locale/tree/2.5"
},
"time": "2023-11-01T17:19:48+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "1.9.1",

View File

@@ -43,7 +43,8 @@ button.btn.active:focus-visible,
input[type=radio]:focus-visible,
input[type=checkbox]:focus-visible,
input[type=file]:focus-visible,
label.attach-file-label:focus-visible {
label.attach-file-label:focus-visible,
.iti__selected-flag:focus-visible {
outline: 1px solid var(--input-border-focus-rgba);
outline-offset: -1px;
}
@@ -3536,6 +3537,88 @@ body > .autocomplete-suggestions.text-search-suggestions {
}
}
.input-group-item > .iti {
top: 3px;
margin-left: -1px;
width: ~"calc(100% + 1px)";
.iti__flag-container {
user-select: none;
&,
&:hover {
.iti__selected-flag {
background-color: transparent;
&[aria-expanded="true"] {
background-color: var(--dropdown-link-hover-bg);
}
}
}
.iti__country-list {
z-index: 5;
max-height: 300px;
background-color: var(--dropdown-bg);
border: var(--dropdown-border) solid var(--dropdown-border-width);
border-radius: var(--dropdown-border-radius);
box-shadow: var(--dropdown-box-shadow);
.iti__dial-code {
color: var(--text-muted-color);
}
.iti__country {
color: var(--dropdown-link-color);
padding: 4px 10px;
&.iti__highlight {
background-color: var(--dropdown-link-hover-bg);
}
}
.iti__divider {
border-bottom-color: var(--dropdown-divider-bg);
}
padding: 5px 0;
}
}
.iti__selected-flag {
z-index: 4;
padding: 0 8px 0 8px;
color: var(--gray-soft);
> .iti__arrow {
visibility: hidden;
display: none;
}
&:after {
content: " ";
color: var(--text-muted-color);
//display: inline-block;
display: none;
width: 0;
height: 0;
top: 15px;
right: 8px;
margin-left: 2px;
vertical-align: middle;
border-top: 4px dashed;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
position: absolute;
}
}
> input {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
@import "misc/kanban.less";
@import "elements/placement.less";

View File

@@ -10,5 +10,6 @@
@import "misc/colorpicker/colorpicker.less";
@import "misc/fontawesome/fontawesome.less";
@import "misc/gridstack/gridstack.less";
@import "misc/intl-tel-input/intlTelInput.less";
@import "fonts.less";

File diff suppressed because it is too large Load Diff

View File

@@ -114,6 +114,17 @@
"bundle": true,
"amdId": "autonumeric"
},
{
"src": "node_modules/intl-tel-input/build/js/intlTelInput.js",
"bundle": true,
"amdId": "intl-tel-input",
"suppressAmd": true
},
{
"src": "node_modules/intl-tel-input/build/js/utils.js",
"bundle": true,
"amdId": "intl-tel-input-utils"
},
{
"src": "node_modules/summernote/dist/summernote.js",
"amdId": "summernote",

11
package-lock.json generated
View File

@@ -31,6 +31,7 @@
"getobject": ">=1.0.0",
"gridstack": "github:yurikuzn/gridstack.js#5.1.1.e3",
"handlebars": "^4.7.7",
"intl-tel-input": "^18.2.1",
"jquery": "^3.7.0",
"jquery-textcomplete": "^1.8.5",
"jquery-ui-espo": "github:yurikuzn/jquery-ui-espo#0.2.2",
@@ -4725,6 +4726,11 @@
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
"integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ="
},
"node_modules/intl-tel-input": {
"version": "18.2.1",
"resolved": "https://registry.npmjs.org/intl-tel-input/-/intl-tel-input-18.2.1.tgz",
"integrity": "sha512-wOm0/61kTtpYjOOW1bhHzC4G8Om+atTxHmg31FS0KD0LQ8k8BpgO925npyi4jlT/EK4+joABABZzz0/XeSgupQ=="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -10688,6 +10694,11 @@
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
"integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ="
},
"intl-tel-input": {
"version": "18.2.1",
"resolved": "https://registry.npmjs.org/intl-tel-input/-/intl-tel-input-18.2.1.tgz",
"integrity": "sha512-wOm0/61kTtpYjOOW1bhHzC4G8Om+atTxHmg31FS0KD0LQ8k8BpgO925npyi4jlT/EK4+joABABZzz0/XeSgupQ=="
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",

View File

@@ -58,6 +58,7 @@
"getobject": ">=1.0.0",
"gridstack": "github:yurikuzn/gridstack.js#5.1.1.e3",
"handlebars": "^4.7.7",
"intl-tel-input": "^18.2.1",
"jquery": "^3.7.0",
"jquery-textcomplete": "^1.8.5",
"jquery-ui-espo": "github:yurikuzn/jquery-ui-espo#0.2.2",

10
upgrades/8.1/data.json Normal file
View File

@@ -0,0 +1,10 @@
{
"manifest": {
"delete": [
"client/lib/original/espo.js",
"client/lib/espo.min.js",
"client/lib/espo.min.js.map",
"client/cfg/pre-load.json"
]
}
}

View File

@@ -0,0 +1,48 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
use Espo\Core\Container;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Config\ConfigWriter;
class AfterUpgrade
{
public function run(Container $container): void
{
$configWriter = $container->getByClass(InjectableFactory::class)
->create(ConfigWriter::class);
$configWriter->setMultiple([
'phoneNumberNumericSearch' => false,
'phoneNumberInternational' => false,
]);
$configWriter->save();
}
}